From 96bb026c64ec1a07e720042b1e6515aab4d17e01 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sat, 22 Aug 2026 15:51:52 -0700 Subject: [PATCH] Polish agent activity companion window Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho --- desktop/src-tauri/capabilities/default.json | 1 + desktop/src/app/AppShell.tsx | 1 + desktop/src/app/AppShellChannelSurface.tsx | 12 ++++---- desktop/src/app/BuzzThemeSurfaces.tsx | 2 +- .../agents/lib/agentActivityWindow.test.mjs | 20 +++++++++++++ .../agents/lib/agentActivityWindow.ts | 23 ++++++++++++++ .../channels/ui/AgentSessionThreadPanel.tsx | 20 ++++++++++++- .../features/channels/ui/ChannelScreen.tsx | 11 +++++-- .../ui/ChannelScreenLoadingFallback.tsx | 4 ++- .../shared/layout/AuxiliaryPanelHeader.tsx | 30 ++++++++++++++----- .../layout/auxiliaryPanelContext.test.mjs | 28 +++++++++++++++++ 11 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 desktop/src/features/agents/lib/agentActivityWindow.test.mjs diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index 9f082cc4ef..97ee30cbdd 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -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", diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index db1d24879a..58f8748582 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -932,6 +932,7 @@ export function AppShell() { hasCommunityRail={hasCommunityRail} isHuddleRoom={isHuddleRoom} isHuddleRoomStarting={isHuddleRoomStarting} + unframed={isCompanionWindow} mainInsetRef={mainInsetRef} terminal={ diff --git a/desktop/src/app/AppShellChannelSurface.tsx b/desktop/src/app/AppShellChannelSurface.tsx index da6907a902..7fdd7f7254 100644 --- a/desktop/src/app/AppShellChannelSurface.tsx +++ b/desktop/src/app/AppShellChannelSurface.tsx @@ -11,6 +11,7 @@ type AppShellChannelSurfaceProps = { hasCommunityRail: boolean; isHuddleRoom: boolean; isHuddleRoomStarting: boolean; + unframed?: boolean; mainInsetRef: React.RefObject; 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 ? : null} - + {isHuddleRoomStarting ? : children} diff --git a/desktop/src/app/BuzzThemeSurfaces.tsx b/desktop/src/app/BuzzThemeSurfaces.tsx index b7912c98bc..45101d5e65 100644 --- a/desktop/src/app/BuzzThemeSurfaces.tsx +++ b/desktop/src/app/BuzzThemeSurfaces.tsx @@ -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 ( diff --git a/desktop/src/features/agents/lib/agentActivityWindow.test.mjs b/desktop/src/features/agents/lib/agentActivityWindow.test.mjs new file mode 100644 index 0000000000..a40fd4d578 --- /dev/null +++ b/desktop/src/features/agents/lib/agentActivityWindow.test.mjs @@ -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", + ); + }); +}); diff --git a/desktop/src/features/agents/lib/agentActivityWindow.ts b/desktop/src/features/agents/lib/agentActivityWindow.ts index a4520828de..ffcf2ac324 100644 --- a/desktop/src/features/agents/lib/agentActivityWindow.ts +++ b/desktop/src/features/agents/lib/agentActivityWindow.ts @@ -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 { + if (!isAgentActivityWindow() || !channelName.trim()) return false; + + await getCurrentWindow().setTitle( + agentActivityWindowTitle(agentName, channelName), + ); + return true; +} diff --git a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx index c1933f14bb..a0d5c9402d 100644 --- a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx +++ b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx @@ -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(null); const contentRef = React.useRef(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} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 2e0501d3b0..8c38e237ad 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -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({ } diff --git a/desktop/src/features/channels/ui/ChannelScreenLoadingFallback.tsx b/desktop/src/features/channels/ui/ChannelScreenLoadingFallback.tsx index a566b71d90..461fb2d19a 100644 --- a/desktop/src/features/channels/ui/ChannelScreenLoadingFallback.tsx +++ b/desktop/src/features/channels/ui/ChannelScreenLoadingFallback.tsx @@ -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 ? ( ) : ( - + ); } diff --git a/desktop/src/shared/layout/AuxiliaryPanelHeader.tsx b/desktop/src/shared/layout/AuxiliaryPanelHeader.tsx index 5140c4d8d5..547e7f81e0 100644 --- a/desktop/src/shared/layout/AuxiliaryPanelHeader.tsx +++ b/desktop/src/shared/layout/AuxiliaryPanelHeader.tsx @@ -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)} ); @@ -181,15 +183,21 @@ export function AuxiliaryPanelHeader({ data-tauri-drag-region >
- {renderAuxiliaryPanelHeaderContent(children)} + {renderAuxiliaryPanelHeaderContent(children, showCloseAction)}
); } -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} - + ); } -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; diff --git a/desktop/src/shared/layout/auxiliaryPanelContext.test.mjs b/desktop/src/shared/layout/auxiliaryPanelContext.test.mjs index 69cea29974..7a06e9d661 100644 --- a/desktop/src/shared/layout/auxiliaryPanelContext.test.mjs +++ b/desktop/src/shared/layout/auxiliaryPanelContext.test.mjs @@ -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 { const html = render( React.createElement(