mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(composer): terse tool-call headline for the activity pill
- Rewrite the tool branch of getActivityHeadline (agentSessionTranscriptPresentation.ts)
to use the descriptor's terse action tier — verb + compact object, e.g.
"Read foo.ts" — instead of "label · full preview" ("Read file ·
src/agents/ui/foo.ts"), so the composer pill's 200px cap shows the
informative part of the action instead of ellipsizing a long preview
- Basename path-like objects only (file-read / file-edit / skill-read);
shell commands stay whole since they legitimately contain "/", and file
edits reuse the already-basenamed fileEditSummary.filename
- Fall back to the previous "label · preview" format when a descriptor
carries no action (older descriptors)
- Export the existing private basename() from agentSessionFileEditDiff.ts
rather than adding a third copy
- Document the terse format on deriveActivityPillLabel (composerLiveActivity.ts);
no logic change — the pill picks it up via getActivityHeadline, whose only
production consumer is the pill
- Tests: update presentation expectations ("Send Message · abc" → "Sent abc"),
add cases for file-read/file-edit basenaming, shell commands kept whole,
and the no-action fallback; add a pill-level terse-headline case to
composerLiveActivity.test.mjs
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -197,7 +197,8 @@ function getDiffStats(
|
||||
return null;
|
||||
}
|
||||
|
||||
function basename(path: string) {
|
||||
/** Final path segment (cross-platform); returns the input when empty. */
|
||||
export function basename(path: string) {
|
||||
const parts = path.replace(/\\/g, "/").split("/");
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,9 @@ function makeMessage(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
test("getActivityHeadline formats tool titles and assistant text", () => {
|
||||
assert.equal(getActivityHeadline(makeTool()), "Send Message · abc");
|
||||
test("getActivityHeadline formats tool actions tersely and assistant text", () => {
|
||||
// Tool items use the terse action tier (verb + object), not label · preview.
|
||||
assert.equal(getActivityHeadline(makeTool()), "Sent abc");
|
||||
assert.equal(
|
||||
getActivityHeadline(makeMessage({ text: "First line\nSecond line" })),
|
||||
"First line",
|
||||
@@ -49,6 +50,72 @@ test("getActivityHeadline formats tool titles and assistant text", () => {
|
||||
assert.equal(getActivityHeadline(makeMessage({ text: " " })), "Responding");
|
||||
});
|
||||
|
||||
test("getActivityHeadline basenames path-like tool objects", () => {
|
||||
assert.equal(
|
||||
getActivityHeadline(
|
||||
makeTool({
|
||||
title: "Read file",
|
||||
toolName: "dev__read_file",
|
||||
buzzToolName: null,
|
||||
args: { path: "src/agents/ui/foo.ts" },
|
||||
descriptor: {
|
||||
renderClass: "file-read",
|
||||
label: "Read file",
|
||||
preview: "src/agents/ui/foo.ts",
|
||||
action: { verb: "Read", object: "src/agents/ui/foo.ts" },
|
||||
source: "harness",
|
||||
groupKey: "read_file",
|
||||
},
|
||||
}),
|
||||
),
|
||||
"Read foo.ts",
|
||||
);
|
||||
assert.equal(
|
||||
getActivityHeadline(
|
||||
makeTool({
|
||||
title: "Edit file",
|
||||
toolName: "dev__str_replace",
|
||||
buzzToolName: null,
|
||||
args: { path: "desktop/src/features/channels/ui/ChannelPane.tsx" },
|
||||
descriptor: {
|
||||
renderClass: "file-edit",
|
||||
label: "Edited file",
|
||||
preview: "desktop/src/features/channels/ui/ChannelPane.tsx",
|
||||
action: {
|
||||
verb: "Edited",
|
||||
object: "desktop/src/features/channels/ui/ChannelPane.tsx",
|
||||
},
|
||||
source: "harness",
|
||||
groupKey: "file-edit:str_replace",
|
||||
},
|
||||
}),
|
||||
),
|
||||
"Edited ChannelPane.tsx",
|
||||
);
|
||||
});
|
||||
|
||||
test("getActivityHeadline keeps shell commands whole (no basenaming)", () => {
|
||||
assert.equal(
|
||||
getActivityHeadline(
|
||||
makeTool({
|
||||
title: "Shell",
|
||||
toolName: "dev__shell",
|
||||
buzzToolName: null,
|
||||
args: { command: "cat src/agents/ui/foo.ts" },
|
||||
descriptor: {
|
||||
renderClass: "shell",
|
||||
label: "Ran command",
|
||||
preview: "cat src/agents/ui/foo.ts",
|
||||
action: { verb: "Ran", object: "cat src/agents/ui/foo.ts" },
|
||||
source: "harness",
|
||||
groupKey: "shell:command",
|
||||
},
|
||||
}),
|
||||
),
|
||||
"Ran cat src/agents/ui/foo.ts",
|
||||
);
|
||||
});
|
||||
|
||||
test("isMeaningfulItem ignores lifecycle noise and raw JSON-RPC metadata", () => {
|
||||
assert.equal(
|
||||
isMeaningfulItem({
|
||||
@@ -111,7 +178,7 @@ test("isMeaningfulItem ignores lifecycle noise and raw JSON-RPC metadata", () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("getActivityHeadline uses semantic tool descriptors", () => {
|
||||
test("getActivityHeadline falls back to label · preview when a descriptor has no action", () => {
|
||||
assert.equal(
|
||||
getActivityHeadline(
|
||||
makeTool({
|
||||
@@ -225,7 +292,7 @@ test("two-tier headline: metadata excluded when spine work is present", () => {
|
||||
"System prompt should not headline when spine work exists",
|
||||
);
|
||||
assert.ok(
|
||||
headlines.some((h) => h?.includes("Send Message")),
|
||||
headlines.some((h) => h?.includes("Sent")),
|
||||
"Tool headline should appear",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { TranscriptItem } from "./agentSessionTypes";
|
||||
import { buildCompactToolSummary } from "./agentSessionToolSummary";
|
||||
import { basename } from "./agentSessionFileEditDiff";
|
||||
import {
|
||||
buildCompactToolSummary,
|
||||
type CompactToolSummary,
|
||||
} from "./agentSessionToolSummary";
|
||||
|
||||
/**
|
||||
* Whether a polished activity row should render the opt-in timestamp footer.
|
||||
@@ -26,10 +30,49 @@ const LIFECYCLE_NOISE = new Set([
|
||||
"wire parse error",
|
||||
]);
|
||||
|
||||
/** Human-readable headline for a single transcript item. */
|
||||
/**
|
||||
* Render classes whose action object is a filesystem path — compact to the
|
||||
* basename for terse headlines. Shell commands are deliberately excluded:
|
||||
* they legitimately contain "/" (e.g. `cat src/foo.ts`) and must not be
|
||||
* reduced to their last path segment.
|
||||
*/
|
||||
const PATHLIKE_RENDER_CLASSES = new Set([
|
||||
"file-read",
|
||||
"file-edit",
|
||||
"skill-read",
|
||||
]);
|
||||
|
||||
function terseActionObject(summary: CompactToolSummary): string | null {
|
||||
if (summary.fileEditSummary) {
|
||||
return summary.fileEditSummary.filename;
|
||||
}
|
||||
const object = summary.action?.object ?? null;
|
||||
if (
|
||||
object &&
|
||||
PATHLIKE_RENDER_CLASSES.has(summary.kind) &&
|
||||
/[\\/]/.test(object)
|
||||
) {
|
||||
return basename(object);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable headline for a single transcript item.
|
||||
*
|
||||
* Tool items use the terse action tier — verb + compact object (file paths
|
||||
* reduced to their basename), e.g. "Read foo.ts" rather than
|
||||
* "Read file · src/agents/ui/foo.ts" — so the composer pill's 200px cap
|
||||
* shows the informative part instead of ellipsizing a long preview.
|
||||
*/
|
||||
export function getActivityHeadline(item: TranscriptItem): string | null {
|
||||
if (item.type === "tool") {
|
||||
const summary = buildCompactToolSummary(item);
|
||||
if (summary.action) {
|
||||
return [summary.action.verb, terseActionObject(summary)]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
return [summary.label, summary.preview].filter(Boolean).join(" · ");
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,32 @@ const lifecycleMeta = (acpSource, timestamp, channelId = CHANNEL) => ({
|
||||
channelId,
|
||||
});
|
||||
|
||||
/** Tool item with a classifier descriptor: headlines tersely (verb + object). */
|
||||
const fileReadTool = (path, timestamp, channelId = CHANNEL) => ({
|
||||
id: `tool-${path}-${timestamp}`,
|
||||
type: "tool",
|
||||
renderClass: "file-read",
|
||||
title: "Read file",
|
||||
toolName: "dev__read_file",
|
||||
buzzToolName: null,
|
||||
status: "completed",
|
||||
args: { path },
|
||||
result: "",
|
||||
isError: false,
|
||||
timestamp,
|
||||
startedAt: timestamp,
|
||||
completedAt: timestamp,
|
||||
descriptor: {
|
||||
renderClass: "file-read",
|
||||
label: "Read file",
|
||||
preview: path,
|
||||
action: { verb: "Read", object: path },
|
||||
source: "harness",
|
||||
groupKey: "read_file",
|
||||
},
|
||||
channelId,
|
||||
});
|
||||
|
||||
/** Streaming assistant message: headline is the (growing) first line. */
|
||||
const assistantMessage = (id, text, timestamp, channelId = CHANNEL) => ({
|
||||
id,
|
||||
@@ -71,6 +97,17 @@ test("deriveActivityPillLabel returns the newest headline, no rotation", () => {
|
||||
assert.deepEqual(headline, { id: editing.id, label: "Editing ChannelPane" });
|
||||
});
|
||||
|
||||
test("deriveActivityPillLabel headlines tool items tersely (verb + basename)", () => {
|
||||
const read = fileReadTool("src/agents/ui/foo.ts", secondsBeforeNow(2));
|
||||
const headline = deriveActivityPillLabel({
|
||||
channelId: CHANNEL,
|
||||
transcript: [read],
|
||||
});
|
||||
// Terse action tier, not "Read file · src/agents/ui/foo.ts" — the pill's
|
||||
// narrow cap must show the informative part of the action.
|
||||
assert.deepEqual(headline, { id: read.id, label: "Read foo.ts" });
|
||||
});
|
||||
|
||||
test("deriveActivityPillLabel keeps the last action headline regardless of age", () => {
|
||||
// A quiet stretch (long tool call, thinking gap) must NOT decay the label
|
||||
// to the generic placeholder — the last real action stays informative.
|
||||
|
||||
@@ -28,6 +28,10 @@ export type ActivityPillHeadline = {
|
||||
/**
|
||||
* Latest action headline for a working agent's composer pill.
|
||||
*
|
||||
* Tool items headline in the terse action format from getActivityHeadline —
|
||||
* verb + compact object ("Read foo.ts"), not "label · full preview" — so the
|
||||
* pill's narrow cap shows the informative part of the action.
|
||||
*
|
||||
* Channel-scoped, two-tier scan (spine items headline over metadata reads,
|
||||
* mirroring the session transcript's noise gate), newest wins. The headline
|
||||
* PERSISTS regardless of age — while a turn is in progress, the last real
|
||||
|
||||
Reference in New Issue
Block a user