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:
Taylor Ho
2026-08-22 15:51:52 -07:00
committed by Rizz
parent 32405545e5
commit 96bb026c64
11 changed files with 133 additions and 19 deletions

View File

@@ -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",

View File

@@ -932,6 +932,7 @@ export function AppShell() {
hasCommunityRail={hasCommunityRail}
isHuddleRoom={isHuddleRoom}
isHuddleRoomStarting={isHuddleRoomStarting}
unframed={isCompanionWindow}
mainInsetRef={mainInsetRef}
terminal={
<TerminalBootstrap {...effectiveTerminalContext} />

View File

@@ -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>

View File

@@ -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 (

View 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",
);
});
});

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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}
/>
}

View File

@@ -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" />
);
}

View File

@@ -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;

View File

@@ -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(