mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
Polish agent activity companion window
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"core:window:allow-set-badge-label",
|
||||
"core:window:allow-request-user-attention",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-unminimize",
|
||||
|
||||
@@ -932,6 +932,7 @@ export function AppShell() {
|
||||
hasCommunityRail={hasCommunityRail}
|
||||
isHuddleRoom={isHuddleRoom}
|
||||
isHuddleRoomStarting={isHuddleRoomStarting}
|
||||
unframed={isCompanionWindow}
|
||||
mainInsetRef={mainInsetRef}
|
||||
terminal={
|
||||
<TerminalBootstrap {...effectiveTerminalContext} />
|
||||
|
||||
@@ -11,6 +11,7 @@ type AppShellChannelSurfaceProps = {
|
||||
hasCommunityRail: boolean;
|
||||
isHuddleRoom: boolean;
|
||||
isHuddleRoomStarting: boolean;
|
||||
unframed?: boolean;
|
||||
mainInsetRef: React.RefObject<HTMLElement | null>;
|
||||
terminal?: React.ReactNode;
|
||||
};
|
||||
@@ -20,12 +21,13 @@ export function AppShellChannelSurface({
|
||||
hasCommunityRail,
|
||||
isHuddleRoom,
|
||||
isHuddleRoomStarting,
|
||||
unframed = false,
|
||||
mainInsetRef,
|
||||
terminal,
|
||||
}: AppShellChannelSurfaceProps) {
|
||||
const { isMobile, openMobile, state: sidebarState } = useSidebar();
|
||||
const hasCollapsedSidebarGutter =
|
||||
!isHuddleRoom &&
|
||||
!unframed &&
|
||||
!hasCommunityRail &&
|
||||
(isMobile ? !openMobile : sidebarState === "collapsed");
|
||||
|
||||
@@ -35,11 +37,11 @@ export function AppShellChannelSurface({
|
||||
ref={mainInsetRef}
|
||||
className={cn(
|
||||
"isolate z-0 min-h-0 min-w-0 overflow-hidden",
|
||||
isHuddleRoom ? "bg-background" : "bg-sidebar",
|
||||
unframed ? "bg-background" : "bg-sidebar",
|
||||
hasCollapsedSidebarGutter && "pl-2",
|
||||
)}
|
||||
data-buzz-content-surface={isHuddleRoom ? true : undefined}
|
||||
data-buzz-content-unframed={isHuddleRoom ? true : undefined}
|
||||
data-buzz-content-surface={unframed ? true : undefined}
|
||||
data-buzz-content-unframed={unframed ? true : undefined}
|
||||
data-buzz-glass-inset
|
||||
data-buzz-shadow-viewport
|
||||
style={chromeCssVarDefaults as React.CSSProperties}
|
||||
@@ -51,7 +53,7 @@ export function AppShellChannelSurface({
|
||||
/>
|
||||
) : null}
|
||||
{isHuddleRoom && !isHuddleRoomStarting ? <HuddleRoomHeader /> : null}
|
||||
<BuzzTheme.ContentSurface terminal={terminal} unframed={isHuddleRoom}>
|
||||
<BuzzTheme.ContentSurface terminal={terminal} unframed={unframed}>
|
||||
{isHuddleRoomStarting ? <HuddleStartingView /> : children}
|
||||
</BuzzTheme.ContentSurface>
|
||||
</SidebarInset>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ContentSurface({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
terminal?: ReactNode;
|
||||
/** Used by dedicated huddle windows, which should not resemble app cards. */
|
||||
/** Used by dedicated companion windows, which should not resemble app cards. */
|
||||
unframed?: boolean;
|
||||
}) {
|
||||
return (
|
||||
|
||||
20
desktop/src/features/agents/lib/agentActivityWindow.test.mjs
Normal file
20
desktop/src/features/agents/lib/agentActivityWindow.test.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { agentActivityWindowTitle } from "./agentActivityWindow.ts";
|
||||
|
||||
describe("agentActivityWindowTitle", () => {
|
||||
it("names both the agent and channel", () => {
|
||||
assert.equal(
|
||||
agentActivityWindowTitle("Mongo", "buzz-inline-chip-wrap"),
|
||||
"Mongo · #buzz-inline-chip-wrap",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not duplicate a supplied channel hash", () => {
|
||||
assert.equal(
|
||||
agentActivityWindowTitle(" Mongo ", " #design "),
|
||||
"Mongo · #design",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,3 +15,26 @@ export function isAgentActivityWindow(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the concise native title for a channel-scoped activity window. */
|
||||
export function agentActivityWindowTitle(
|
||||
agentName: string,
|
||||
channelName: string,
|
||||
): string {
|
||||
const normalizedAgentName = agentName.trim() || "Agent";
|
||||
const normalizedChannelName = channelName.trim().replace(/^#+/, "");
|
||||
return `${normalizedAgentName} · #${normalizedChannelName}`;
|
||||
}
|
||||
|
||||
/** Keep a companion window's native title aligned with its resolved scope. */
|
||||
export async function setAgentActivityWindowTitle(
|
||||
agentName: string,
|
||||
channelName: string,
|
||||
): Promise<boolean> {
|
||||
if (!isAgentActivityWindow() || !channelName.trim()) return false;
|
||||
|
||||
await getCurrentWindow().setTitle(
|
||||
agentActivityWindowTitle(agentName, channelName),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useAgentWorking } from "@/features/agents/agentWorkingSignal";
|
||||
import {
|
||||
isAgentActivityWindow,
|
||||
setAgentActivityWindowTitle,
|
||||
} from "@/features/agents/lib/agentActivityWindow";
|
||||
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
|
||||
import {
|
||||
mergeObserverEventWindows,
|
||||
@@ -96,6 +100,7 @@ export function AgentSessionThreadPanel({
|
||||
widthPx,
|
||||
transparentChrome = false,
|
||||
}: AgentSessionThreadPanelProps) {
|
||||
const isDedicatedActivityWindow = isAgentActivityWindow();
|
||||
const isLive = isManagedAgentActive(agent);
|
||||
const isOverlay = useIsThreadPanelOverlay();
|
||||
const sessionChannelId = channelId ?? channel?.id ?? null;
|
||||
@@ -106,7 +111,10 @@ export function AgentSessionThreadPanel({
|
||||
sessionChannelId,
|
||||
);
|
||||
const canStopCurrentTurn = isWorking && canInterruptTurn;
|
||||
useEscapeKey(onClose, isOverlay || isSinglePanelView);
|
||||
useEscapeKey(
|
||||
onClose,
|
||||
(isOverlay || isSinglePanelView) && !isDedicatedActivityWindow,
|
||||
);
|
||||
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
@@ -236,6 +244,15 @@ export function AgentSessionThreadPanel({
|
||||
profiles,
|
||||
preferResolvedSelfLabel: true,
|
||||
});
|
||||
React.useEffect(() => {
|
||||
if (!scopeChannelName) return;
|
||||
|
||||
void setAgentActivityWindowTitle(agentLabel, scopeChannelName).catch(
|
||||
(error) => {
|
||||
console.error("Failed to update agent activity window title:", error);
|
||||
},
|
||||
);
|
||||
}, [agentLabel, scopeChannelName]);
|
||||
const viewLabel = showRawFeed ? "Raw ACP activity" : "Activity";
|
||||
const headerScopeLabel = `${viewLabel} · ${scopeLabel}`;
|
||||
const animateActivity = useTranscriptAnimationEnabled();
|
||||
@@ -469,6 +486,7 @@ export function AgentSessionThreadPanel({
|
||||
backdrop={layout !== "split" && !isOverlay}
|
||||
backdropSurface="soft"
|
||||
inset={layout !== "split" ? "wide" : "default"}
|
||||
showCloseAction={!isDedicatedActivityWindow}
|
||||
>
|
||||
{agentHeaderContent}
|
||||
</AuxiliaryPanelHeader>
|
||||
|
||||
@@ -98,6 +98,7 @@ export function ChannelScreen({
|
||||
targetMessageEvents,
|
||||
targetMessageId,
|
||||
}: ChannelScreenProps) {
|
||||
const isDedicatedActivityWindow = isAgentActivityWindow();
|
||||
const { goHome } = useAppNavigation();
|
||||
const { activeCommunity } = useCommunities();
|
||||
const {
|
||||
@@ -713,10 +714,13 @@ export function ChannelScreen({
|
||||
const isNarrowPanelViewport =
|
||||
channelContentWidthPx > 0 &&
|
||||
channelContentWidthPx < AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX;
|
||||
// A dedicated activity window is the panel itself, not a responsive channel
|
||||
// view. Keep the channel timeline out of that window at every width.
|
||||
const isSinglePanelView =
|
||||
isNarrowPanelViewport &&
|
||||
activeChannel?.channelType !== "forum" &&
|
||||
hasAuxiliaryPanel;
|
||||
isDedicatedActivityWindow ||
|
||||
(isNarrowPanelViewport &&
|
||||
activeChannel?.channelType !== "forum" &&
|
||||
hasAuxiliaryPanel);
|
||||
const shouldCompactHeaderActions =
|
||||
hasAuxiliaryPanel &&
|
||||
channelContentWidthPx > 0 &&
|
||||
@@ -853,6 +857,7 @@ export function ChannelScreen({
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<ChannelScreenLoadingFallback
|
||||
includeHeader={!isDedicatedActivityWindow}
|
||||
isHuddleTranscript={isHuddleTranscript}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import { HuddleStartingView } from "@/features/huddle/components/HuddleStartingV
|
||||
import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
|
||||
export function ChannelScreenLoadingFallback({
|
||||
includeHeader = true,
|
||||
isHuddleTranscript,
|
||||
}: {
|
||||
includeHeader?: boolean;
|
||||
isHuddleTranscript: boolean;
|
||||
}) {
|
||||
return isHuddleTranscript ? (
|
||||
<HuddleStartingView />
|
||||
) : (
|
||||
<ViewLoadingFallback includeHeader kind="channel" />
|
||||
<ViewLoadingFallback includeHeader={includeHeader} kind="channel" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type AuxiliaryPanelHeaderProps = Omit<
|
||||
inset?: "default" | "wide";
|
||||
mode?: AuxiliaryPanelMode;
|
||||
resizeBorder?: boolean;
|
||||
showCloseAction?: boolean;
|
||||
surface?: AuxiliaryPanelSurface;
|
||||
/** Render header content without its own backdrop for a shared parent chrome. */
|
||||
transparent?: boolean;
|
||||
@@ -113,6 +114,7 @@ export function AuxiliaryPanelHeader({
|
||||
inset = "default",
|
||||
mode,
|
||||
resizeBorder = false,
|
||||
showCloseAction = true,
|
||||
surface = "default",
|
||||
transparent,
|
||||
...props
|
||||
@@ -159,7 +161,7 @@ export function AuxiliaryPanelHeader({
|
||||
data-tauri-drag-region
|
||||
{...props}
|
||||
>
|
||||
{renderAuxiliaryPanelHeaderContent(children)}
|
||||
{renderAuxiliaryPanelHeaderContent(children, showCloseAction)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -181,15 +183,21 @@ export function AuxiliaryPanelHeader({
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className="flex h-9 min-w-0 items-center gap-2.5">
|
||||
{renderAuxiliaryPanelHeaderContent(children)}
|
||||
{renderAuxiliaryPanelHeaderContent(children, showCloseAction)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAuxiliaryPanelHeaderContent(children: React.ReactNode) {
|
||||
const { foundActions, content } = attachCloseActionToHeaderActions(children);
|
||||
function renderAuxiliaryPanelHeaderContent(
|
||||
children: React.ReactNode,
|
||||
showCloseAction: boolean,
|
||||
) {
|
||||
const { foundActions, content } = attachCloseActionToHeaderActions(
|
||||
children,
|
||||
showCloseAction,
|
||||
);
|
||||
|
||||
if (foundActions) {
|
||||
return content;
|
||||
@@ -198,12 +206,15 @@ function renderAuxiliaryPanelHeaderContent(children: React.ReactNode) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<AuxiliaryPanelHeaderActions includeCloseAction />
|
||||
<AuxiliaryPanelHeaderActions includeCloseAction={showCloseAction} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function attachCloseActionToHeaderActions(children: React.ReactNode): {
|
||||
function attachCloseActionToHeaderActions(
|
||||
children: React.ReactNode,
|
||||
includeCloseAction: boolean,
|
||||
): {
|
||||
content: React.ReactNode;
|
||||
foundActions: boolean;
|
||||
} {
|
||||
@@ -216,11 +227,14 @@ function attachCloseActionToHeaderActions(children: React.ReactNode): {
|
||||
|
||||
if (child.type === AuxiliaryPanelHeaderActions) {
|
||||
foundActions = true;
|
||||
return React.cloneElement(child, { includeCloseAction: true });
|
||||
return React.cloneElement(child, { includeCloseAction });
|
||||
}
|
||||
|
||||
if (child.type === React.Fragment) {
|
||||
const nested = attachCloseActionToHeaderActions(child.props.children);
|
||||
const nested = attachCloseActionToHeaderActions(
|
||||
child.props.children,
|
||||
includeCloseAction,
|
||||
);
|
||||
|
||||
if (!nested.foundActions) {
|
||||
return child;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AuxiliaryPanel } from "./AuxiliaryPanel/index.ts";
|
||||
import { AuxiliaryPanelBody } from "./AuxiliaryPanel/index.ts";
|
||||
import {
|
||||
AuxiliaryPanelHeader,
|
||||
AuxiliaryPanelHeaderActions,
|
||||
AuxiliaryPanelHeaderGroup,
|
||||
} from "./AuxiliaryPanel/index.ts";
|
||||
import {
|
||||
@@ -177,6 +178,33 @@ test("AuxiliaryPanelHeader renders a generic close action from context", () => {
|
||||
assert.match(html, /data-testid="auxiliary-panel-close"/);
|
||||
});
|
||||
|
||||
test("AuxiliaryPanelHeader can suppress its close action", () => {
|
||||
const html = render(
|
||||
React.createElement(
|
||||
AuxiliaryPanel,
|
||||
{
|
||||
header: React.createElement(
|
||||
AuxiliaryPanelHeader,
|
||||
{ showCloseAction: false },
|
||||
React.createElement(AuxiliaryPanelHeaderGroup, null, "Title"),
|
||||
React.createElement(
|
||||
AuxiliaryPanelHeaderActions,
|
||||
null,
|
||||
React.createElement("button", { type: "button" }, "Settings"),
|
||||
),
|
||||
),
|
||||
onClose: () => {},
|
||||
widthPx: 420,
|
||||
},
|
||||
"Panel",
|
||||
),
|
||||
);
|
||||
|
||||
assert.match(html, />Settings</);
|
||||
assert.doesNotMatch(html, /aria-label="Close panel"/);
|
||||
assert.doesNotMatch(html, /data-testid="auxiliary-panel-close"/);
|
||||
});
|
||||
|
||||
test("AuxiliaryPanelHeader keeps resize border in single-panel mode when requested", () => {
|
||||
const html = render(
|
||||
React.createElement(
|
||||
|
||||
Reference in New Issue
Block a user