Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";

import {
clampHeadline,
getActivityHeadline,
isMeaningfulItem,
isSpineItem,
Expand Down Expand Up @@ -49,6 +50,59 @@ test("getActivityHeadline formats tool titles and assistant text", () => {
assert.equal(getActivityHeadline(makeMessage({ text: " " })), "Responding");
});

test("getActivityHeadline clamps long shell and generic tool previews", () => {
const longCommand = `printf '${"x".repeat(200)}' > /tmp/out.txt`;
const shellHeadline = getActivityHeadline(
makeTool({
title: "Shell",
toolName: "dev__shell",
buzzToolName: null,
args: { command: longCommand },
descriptor: {
renderClass: "shell",
label: "Ran command",
preview: longCommand,
source: "harness",
groupKey: "shell:command",
},
}),
);
assert.ok(shellHeadline);
assert.ok(shellHeadline.length <= 72);
assert.ok(shellHeadline.endsWith("…"));
assert.ok(shellHeadline.startsWith("Ran command ·"));

const longContent = "A".repeat(120);
const genericHeadline = getActivityHeadline(
makeTool({
title: "Tool",
toolName: "dev__generic",
buzzToolName: null,
args: { content: longContent },
descriptor: {
renderClass: "generic",
label: "Ran tool",
preview: longContent,
source: "harness",
groupKey: "generic",
},
}),
);
assert.ok(genericHeadline);
assert.ok(genericHeadline.length <= 72);
assert.ok(genericHeadline.endsWith("…"));
});

test("clampHeadline collapses whitespace and ellipsizes", () => {
assert.equal(clampHeadline("short"), "short");
assert.equal(clampHeadline("line one\nline two with more"), "line one");
assert.equal(clampHeadline(" lots of\tspace "), "lots of space");
const long = "y".repeat(100);
const clamped = clampHeadline(long);
assert.ok(clamped.length <= 72);
assert.equal(clamped, `${"y".repeat(69)}…`);
});

test("isMeaningfulItem ignores lifecycle noise and raw JSON-RPC metadata", () => {
assert.equal(
isMeaningfulItem({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,58 @@ const LIFECYCLE_NOISE = new Set([
"wire parse error",
]);

/** Max length for composer / View-all activity headlines. */
export const ACTIVITY_HEADLINE_MAX = 72;

/**
* Collapse whitespace on the first line and ellipsize to `max` chars.
* Prevents shell/tool payload dumps from exploding overview UIs.
*/
export function clampHeadline(
text: string,
max = ACTIVITY_HEADLINE_MAX,
): string {
const firstLine = text.split("\n")[0]?.replace(/\s+/g, " ").trim() ?? "";
if (firstLine.length === 0) {
return "";
}
if (firstLine.length <= max) {
return firstLine;
}
return `${firstLine.slice(0, Math.max(0, max - 3))}…`;
}

/** Human-readable headline for a single transcript item. */
export function getActivityHeadline(item: TranscriptItem): string | null {
if (item.type === "tool") {
const summary = buildCompactToolSummary(item);
return [summary.label, summary.preview].filter(Boolean).join(" · ");
const joined = [summary.label, summary.preview].filter(Boolean).join(" · ");
return joined ? clampHeadline(joined) : null;
}

if (item.type === "message") {
if (item.role === "assistant") {
const trimmed = item.text.trim();
if (trimmed.length > 0) {
const firstLine = trimmed.split("\n")[0]?.trim() ?? "";
if (firstLine.length > 0) {
return firstLine.length > 72
? `${firstLine.slice(0, 69)}…`
: firstLine;
const clamped = clampHeadline(trimmed);
if (clamped.length > 0) {
return clamped;
}
}
return "Responding";
}
return item.title || "User prompt";
return clampHeadline(item.title || "User prompt");
}

if (item.type === "thought") {
return item.title === "Plan" ? "Planning" : item.title;
return clampHeadline(item.title === "Plan" ? "Planning" : item.title);
}

if (item.type === "metadata") {
return item.title;
return clampHeadline(item.title);
}

return item.title;
return clampHeadline(item.title);
}

function isLifecycleNoise(
Expand Down
175 changes: 175 additions & 0 deletions desktop/src/features/channels/ui/AllAgentsActivityPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import * as React from "react";
import { Loader2 } from "lucide-react";

import { ManagedAgentSessionPanel } from "@/features/agents/ui/ManagedAgentSessionPanel";
import type { BotActivityAgent } from "@/features/channels/ui/BotActivityBar";
import { agentsForAllActivityPanel } from "@/features/channels/ui/botActivityViewAll";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { useEscapeKey } from "@/shared/hooks/useEscapeKey";
import { useIsThreadPanelOverlay } from "@/shared/hooks/use-mobile";
import {
AuxiliaryPanel,
AuxiliaryPanelBody,
AuxiliaryPanelHeader,
AuxiliaryPanelHeaderGroup,
AuxiliaryPanelHeaderTitleBlock,
} from "@/shared/layout/AuxiliaryPanel";
import { cn } from "@/shared/lib/cn";
import { UserAvatar } from "@/shared/ui/UserAvatar";

type AllAgentsActivityPanelProps = {
agents: BotActivityAgent[];
channelId?: string | null;
isSinglePanelView?: boolean;
layout?: "standalone" | "split";
onClose: () => void;
onOpenAgentSession: (pubkey: string, channelId?: string | null) => void;
profiles?: UserProfileLookup;
transparentChrome?: boolean;
widthPx: number;
workingBotPubkeys: string[];
};

type WorkingAgentActivityCardProps = {
agent: BotActivityAgent;
avatarUrl: string | null;
channelId?: string | null;
onOpenAgentSession: (pubkey: string, channelId?: string | null) => void;
profiles?: UserProfileLookup;
};

function WorkingAgentActivityCard({
agent,
avatarUrl,
channelId = null,
onOpenAgentSession,
profiles,
}: WorkingAgentActivityCardProps) {
return (
<article
className="overflow-hidden rounded-lg border border-border/70 bg-background/80 shadow-xs"
data-testid={`all-agents-activity-card-${agent.pubkey}`}
>
<div className="flex items-center gap-3 border-b border-border/50 px-3 py-2.5">
<UserAvatar
avatarUrl={avatarUrl}
className="shrink-0 ring-1 ring-primary/20"
displayName={agent.name}
size="sm"
/>
<div className="flex min-w-0 flex-1 items-center gap-2">
<h3 className="truncate text-sm font-semibold text-foreground">
{agent.name}
</h3>
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-primary/70" />
</div>
<button
className="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors hover:bg-primary/10"
data-testid={`all-agents-activity-open-${agent.pubkey}`}
onClick={() => onOpenAgentSession(agent.pubkey, channelId)}
type="button"
>
View
</button>
</div>

<div className="relative flex h-44 flex-col overflow-hidden">
<ManagedAgentSessionPanel
agent={{
pubkey: agent.pubkey,
name: agent.name,
status: agent.status ?? "running",
avatarUrl,
}}
autoTail={true}
channelId={channelId}
className="min-h-0 flex-1 border-0 bg-transparent px-3 text-xs shadow-none **:data-message-id:pointer-events-none"
emptyDescription="Waiting for activity…"
emptyState="loading"
panelPadding={false}
profiles={profiles}
rawLayout="responsive"
showHeader={false}
showRaw={false}
transcriptContentClassName="py-2"
transcriptVariant="compactPreview"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-6 bg-linear-to-b from-background/80 to-transparent"
/>
</div>
</article>
);
}

export function AllAgentsActivityPanel({
agents,
channelId = null,
isSinglePanelView = false,
layout = "standalone",
onClose,
onOpenAgentSession,
profiles,
transparentChrome = false,
widthPx,
workingBotPubkeys,
}: AllAgentsActivityPanelProps) {
const isOverlay = useIsThreadPanelOverlay();
useEscapeKey(onClose, isOverlay || isSinglePanelView);

const panelAgents = React.useMemo(
() => agentsForAllActivityPanel({ agents, workingBotPubkeys }),
[agents, workingBotPubkeys],
);
const agentAvatarUrl = (agent: BotActivityAgent) =>
profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null;

return (
<AuxiliaryPanel
isSinglePanelView={isSinglePanelView}
layout={layout}
onClose={onClose}
testId="all-agents-activity-panel"
transparentChrome={transparentChrome}
widthPx={widthPx}
header={
<AuxiliaryPanelHeader
backdrop={layout !== "split" && !isOverlay}
backdropSurface="soft"
inset={layout !== "split" ? "wide" : "default"}
>
<AuxiliaryPanelHeaderGroup align="start">
<AuxiliaryPanelHeaderTitleBlock
subtitle={`${panelAgents.length} agent${
panelAgents.length === 1 ? "" : "s"
} working now`}
title="All agent activity"
/>
</AuxiliaryPanelHeaderGroup>
</AuxiliaryPanelHeader>
}
>
<AuxiliaryPanelBody
className={cn(
// Use px/pb only — a full `p-*` would override the single-panel
// `pt-13` inset that clears the overlapping header chrome.
"flex flex-col gap-2 overflow-y-auto px-4 pb-4",
layout === "split" && "bg-transparent",
)}
panelPadding
>
{panelAgents.map((agent) => (
<WorkingAgentActivityCard
agent={agent}
avatarUrl={agentAvatarUrl(agent)}
channelId={channelId}
key={agent.pubkey}
onOpenAgentSession={onOpenAgentSession}
profiles={profiles}
/>
))}
</AuxiliaryPanelBody>
</AuxiliaryPanel>
);
}
Loading