mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
fix(agents): group activity tools and hide orphan prompt context
- Update desktop/src/features/agents/ui/agentSessionTranscriptGrouping.ts to group consecutive tool calls into transcript tool groups and suppress setup/context-only turn noise when no user prompt bubble exists - Update desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx and AgentSessionToolItem.tsx to render grouped tool rows with tighter activity-feed spacing - Expand desktop/src/features/agents/ui/agentSessionToolSummary.ts so Buzz and generic tools use the compact summary presentation with useful previews - Adjust desktop/src/shared/ui/toggle.tsx to support the ghost prompt-context toggle styling used by the transcript footer - Add focused unit coverage for grouped tools, Buzz tool summaries, promptless turns, and setup/context-only turns
This commit is contained in:
@@ -11,9 +11,11 @@ import { asRecord, formatCodeValue, formatDuration } from "./agentSessionUtils";
|
||||
|
||||
export function ToolItem({
|
||||
compact = false,
|
||||
grouped = false,
|
||||
item,
|
||||
}: {
|
||||
compact?: boolean;
|
||||
grouped?: boolean;
|
||||
item: Extract<TranscriptItem, { type: "tool" }>;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
@@ -32,7 +34,11 @@ export function ToolItem({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("not-prose w-full", compact ? "px-0" : "px-1")}
|
||||
className={cn(
|
||||
"not-prose w-full",
|
||||
compact ? "px-0" : "px-1",
|
||||
grouped ? "py-0" : compact ? "py-0.5" : "py-0.5",
|
||||
)}
|
||||
data-testid="transcript-tool-item"
|
||||
>
|
||||
<details
|
||||
@@ -42,7 +48,7 @@ export function ToolItem({
|
||||
>
|
||||
<summary
|
||||
className={cn(
|
||||
"inline-flex max-w-full cursor-pointer list-none items-center gap-5 py-px",
|
||||
"inline-flex max-w-full cursor-pointer list-none items-center gap-1.5 py-px",
|
||||
compactSummaryTone(),
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -155,6 +155,14 @@ function TranscriptDisplayBlockView({
|
||||
profiles?: UserProfileLookup;
|
||||
}) {
|
||||
if (block.kind === "single") {
|
||||
if (block.item.type === "tool") {
|
||||
return (
|
||||
<div className={cn("first:mt-0", compact ? "mt-3" : "mt-3.5")}>
|
||||
<ToolCallGroup compact={compact} items={[block.item]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TranscriptItemRow
|
||||
agentAvatarUrl={agentAvatarUrl}
|
||||
@@ -169,25 +177,51 @@ function TranscriptDisplayBlockView({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("first:mt-0", compact ? "mt-2.5" : "mt-4")}
|
||||
className={cn("first:mt-0", compact ? "mt-2.5" : "mt-3.5")}
|
||||
data-testid="transcript-turn-group"
|
||||
data-turn-id={block.turnId}
|
||||
>
|
||||
{block.segments.map((segment) => (
|
||||
<TranscriptTurnSegmentView
|
||||
agentAvatarUrl={agentAvatarUrl}
|
||||
agentName={agentName}
|
||||
agentPubkey={agentPubkey}
|
||||
compact={compact}
|
||||
{block.segments.map((segment, index) => (
|
||||
<div
|
||||
className={getTurnSegmentSpacing(
|
||||
block.segments[index - 1],
|
||||
segment,
|
||||
compact,
|
||||
)}
|
||||
key={getTurnSegmentKey(block.turnId, segment)}
|
||||
profiles={profiles}
|
||||
segment={segment}
|
||||
/>
|
||||
>
|
||||
<TranscriptTurnSegmentView
|
||||
agentAvatarUrl={agentAvatarUrl}
|
||||
agentName={agentName}
|
||||
agentPubkey={agentPubkey}
|
||||
compact={compact}
|
||||
profiles={profiles}
|
||||
segment={segment}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getTurnSegmentSpacing(
|
||||
previous: TranscriptTurnSegment | undefined,
|
||||
segment: TranscriptTurnSegment,
|
||||
compact: boolean,
|
||||
): string | undefined {
|
||||
if (!previous) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const involvesTool =
|
||||
previous.kind === "tool_group" || segment.kind === "tool_group";
|
||||
if (involvesTool) {
|
||||
return compact ? "mt-3" : "mt-3.5";
|
||||
}
|
||||
|
||||
return compact ? "mt-2" : "mt-2.5";
|
||||
}
|
||||
|
||||
function getTurnSegmentKey(turnId: string, segment: TranscriptTurnSegment) {
|
||||
if (segment.kind === "setup") {
|
||||
return `turn:${turnId}:setup`;
|
||||
@@ -195,6 +229,9 @@ function getTurnSegmentKey(turnId: string, segment: TranscriptTurnSegment) {
|
||||
if (segment.kind === "prompt") {
|
||||
return `turn:${turnId}:prompt`;
|
||||
}
|
||||
if (segment.kind === "tool_group") {
|
||||
return `turn:${turnId}:tools:${segment.items.map((item) => item.id).join("+")}`;
|
||||
}
|
||||
return segment.item.id;
|
||||
}
|
||||
|
||||
@@ -226,12 +263,17 @@ function TranscriptTurnSegmentView({
|
||||
return <TurnSetupStatus compact={compact} items={segment.items} />;
|
||||
}
|
||||
|
||||
if (segment.kind === "tool_group") {
|
||||
return <ToolCallGroup compact={compact} items={segment.items} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TranscriptItemRow
|
||||
agentAvatarUrl={agentAvatarUrl}
|
||||
agentName={agentName}
|
||||
agentPubkey={agentPubkey}
|
||||
compact={compact}
|
||||
embedded
|
||||
item={segment.item}
|
||||
profiles={profiles}
|
||||
/>
|
||||
@@ -252,10 +294,7 @@ function TurnPromptBlock({
|
||||
user: Extract<TranscriptItem, { type: "message" }>;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn("first:mt-0", compact ? "mt-2.5" : "mt-4")}
|
||||
data-testid="transcript-prompt-bundle"
|
||||
>
|
||||
<div data-testid="transcript-prompt-bundle">
|
||||
{SHOW_TRANSCRIPT_ACP_SOURCE ? (
|
||||
<div className="mb-1 flex flex-wrap gap-1">
|
||||
<TranscriptAcpSourceBadge source="session/prompt:user" />
|
||||
@@ -464,24 +503,42 @@ function TurnSetupFooter({
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCallGroup({
|
||||
compact,
|
||||
items,
|
||||
}: {
|
||||
compact: boolean;
|
||||
items: Extract<TranscriptItem, { type: "tool" }>[];
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-0" data-testid="transcript-tool-group">
|
||||
{items.map((item) => (
|
||||
<ToolItem compact={compact} grouped key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TranscriptItemRow({
|
||||
agentAvatarUrl,
|
||||
agentName,
|
||||
agentPubkey,
|
||||
compact,
|
||||
embedded = false,
|
||||
item,
|
||||
profiles,
|
||||
}: AgentTranscriptIdentityProps & {
|
||||
compact: boolean;
|
||||
embedded?: boolean;
|
||||
item: TranscriptItem;
|
||||
profiles?: UserProfileLookup;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"first:mt-0",
|
||||
compact ? "mt-2.5" : "mt-4",
|
||||
getItemSpacingClass(item),
|
||||
!embedded && "first:mt-0",
|
||||
!embedded && (compact ? "mt-2" : "mt-2.5"),
|
||||
!embedded && getItemSpacingClass(item),
|
||||
)}
|
||||
key={item.id}
|
||||
>
|
||||
@@ -513,7 +570,7 @@ function TurnSetupStatus({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-md px-2 py-1.5", compact ? "mt-2" : "mt-2.5")}>
|
||||
<div className={cn("rounded-md px-2 py-1.5", compact ? "mt-1.5" : "mt-2")}>
|
||||
<TurnSetupFooter items={items} timestamp={timestamp} />
|
||||
</div>
|
||||
);
|
||||
@@ -521,10 +578,10 @@ function TurnSetupStatus({
|
||||
|
||||
function getItemSpacingClass(item: TranscriptItem) {
|
||||
if (item.type === "lifecycle") {
|
||||
return "mt-2 first:mt-0";
|
||||
return "mt-1.5 first:mt-0";
|
||||
}
|
||||
if (item.type === "metadata" || item.type === "thought") {
|
||||
return "mt-2 first:mt-0";
|
||||
return "mt-1.5 first:mt-0";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -603,7 +660,13 @@ function MessageItem({
|
||||
className={cn(
|
||||
"flex animate-in fade-in duration-200 motion-reduce:animate-none",
|
||||
isAssistant ? "flex-row" : "flex-row items-start justify-end",
|
||||
compact ? "px-0 py-0.5" : "px-1 py-1",
|
||||
isAssistant
|
||||
? compact
|
||||
? "px-0 py-0"
|
||||
: "px-1 py-0"
|
||||
: compact
|
||||
? "px-0 py-0.5"
|
||||
: "px-1 py-1",
|
||||
)}
|
||||
data-role={isAssistant ? "assistant-message" : "user-message"}
|
||||
data-testid={
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildCompactToolSummary,
|
||||
isCompactDeveloperTool,
|
||||
} from "./agentSessionToolSummary.ts";
|
||||
import { buildCompactToolSummary } from "./agentSessionToolSummary.ts";
|
||||
|
||||
const baseTimestamp = "2026-06-14T19:00:00.000Z";
|
||||
|
||||
@@ -26,29 +23,19 @@ function makeTool(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
test("isCompactDeveloperTool returns false for Buzz relay tools", () => {
|
||||
assert.equal(
|
||||
isCompactDeveloperTool(
|
||||
makeTool({
|
||||
toolName: "send_message",
|
||||
buzzToolName: "send_message",
|
||||
title: "Send Message",
|
||||
}),
|
||||
),
|
||||
false,
|
||||
test("buildCompactToolSummary formats Buzz send_message preview", () => {
|
||||
const summary = buildCompactToolSummary(
|
||||
makeTool({
|
||||
toolName: "send_message",
|
||||
buzzToolName: "send_message",
|
||||
title: "Send Message",
|
||||
args: { content: "Hello team" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("isCompactDeveloperTool detects buzz-dev-mcp shell tools", () => {
|
||||
assert.equal(
|
||||
isCompactDeveloperTool(
|
||||
makeTool({
|
||||
toolName: "buzz-dev-mcp__shell",
|
||||
title: "buzz-dev-mcp__shell",
|
||||
}),
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(summary.kind, "buzz");
|
||||
assert.equal(summary.label, "Send Message");
|
||||
assert.equal(summary.preview, "Hello team");
|
||||
});
|
||||
|
||||
test("buildCompactToolSummary formats shell command preview", () => {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { ToolStatus, TranscriptItem } from "./agentSessionTypes";
|
||||
import {
|
||||
formatToolTitle,
|
||||
getBuzzToolInfo,
|
||||
isGenericToolTitle,
|
||||
normalizeToolNameText,
|
||||
} from "./agentSessionToolCatalog";
|
||||
import { asRecord, getToolString } from "./agentSessionUtils";
|
||||
import {
|
||||
asRecord,
|
||||
getToolString,
|
||||
getToolStringList,
|
||||
} from "./agentSessionUtils";
|
||||
|
||||
export type CompactToolKind =
|
||||
| "shell"
|
||||
@@ -13,7 +19,9 @@ export type CompactToolKind =
|
||||
| "todo"
|
||||
| "stop_hook"
|
||||
| "post_compact_hook"
|
||||
| "dev_mcp";
|
||||
| "dev_mcp"
|
||||
| "buzz"
|
||||
| "generic";
|
||||
|
||||
export type CompactToolSummary = {
|
||||
kind: CompactToolKind;
|
||||
@@ -35,26 +43,33 @@ const DEVELOPER_TOOL_BASES = new Set([
|
||||
|
||||
type ToolItem = Extract<TranscriptItem, { type: "tool" }>;
|
||||
|
||||
/** Whether this tool row should use the muted compact developer summary. */
|
||||
export function isCompactDeveloperTool(item: ToolItem): boolean {
|
||||
if (item.buzzToolName && getBuzzToolInfo(item.buzzToolName)) {
|
||||
return false;
|
||||
}
|
||||
return resolveDeveloperToolKind(item) !== null;
|
||||
}
|
||||
|
||||
/** Build the compact summary label and preview for developer MCP tool rows. */
|
||||
/** Build the muted compact summary label and preview for any tool row. */
|
||||
export function buildCompactToolSummary(item: ToolItem): CompactToolSummary {
|
||||
const kind = resolveDeveloperToolKind(item) ?? "dev_mcp";
|
||||
const kind = resolveCompactToolKind(item);
|
||||
const { preview, thumbnailSrc } = extractCompactToolPreview(item, kind);
|
||||
return {
|
||||
kind,
|
||||
label: compactToolLabel(kind, item.status, item.isError),
|
||||
label: compactToolLabel(kind, item, item.status, item.isError),
|
||||
preview,
|
||||
thumbnailSrc,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCompactToolKind(item: ToolItem): CompactToolKind {
|
||||
const developerKind = resolveDeveloperToolKind(item);
|
||||
if (developerKind) {
|
||||
return developerKind;
|
||||
}
|
||||
|
||||
for (const value of [item.buzzToolName, item.toolName, item.title]) {
|
||||
if (value && getBuzzToolInfo(value)) {
|
||||
return "buzz";
|
||||
}
|
||||
}
|
||||
|
||||
return "generic";
|
||||
}
|
||||
|
||||
function resolveDeveloperToolKind(item: ToolItem): CompactToolKind | null {
|
||||
for (const value of [item.toolName, item.title, item.buzzToolName]) {
|
||||
const kind = classifyDeveloperToolName(value);
|
||||
@@ -98,16 +113,51 @@ function stripMcpServerPrefix(normalized: string): string {
|
||||
|
||||
function compactToolLabel(
|
||||
kind: CompactToolKind,
|
||||
item: ToolItem,
|
||||
status: ToolStatus,
|
||||
isError: boolean,
|
||||
): string {
|
||||
const failed = isError || status === "failed";
|
||||
const running = status === "executing" || status === "pending";
|
||||
|
||||
if (kind === "buzz") {
|
||||
const title = formatToolTitle(
|
||||
item.buzzToolName ?? item.toolName,
|
||||
item.title,
|
||||
);
|
||||
if (failed) return `${title} failed`;
|
||||
if (running) return title;
|
||||
return title;
|
||||
}
|
||||
|
||||
const labels: Record<
|
||||
CompactToolKind,
|
||||
{ completed: string; running: string; failed: string }
|
||||
> = {
|
||||
generic: {
|
||||
completed: "Ran tool",
|
||||
running: "Running tool",
|
||||
failed: "Tool failed",
|
||||
},
|
||||
...developerToolLabels(),
|
||||
};
|
||||
|
||||
const labelsMap = labels as Record<
|
||||
CompactToolKind,
|
||||
{ completed: string; running: string; failed: string }
|
||||
>;
|
||||
|
||||
const set = labelsMap[kind];
|
||||
if (failed) return set.failed;
|
||||
if (running) return set.running;
|
||||
return set.completed;
|
||||
}
|
||||
|
||||
function developerToolLabels(): Record<
|
||||
Exclude<CompactToolKind, "buzz" | "generic">,
|
||||
{ completed: string; running: string; failed: string }
|
||||
> {
|
||||
return {
|
||||
shell: {
|
||||
completed: "Ran command",
|
||||
running: "Running command",
|
||||
@@ -149,11 +199,6 @@ function compactToolLabel(
|
||||
failed: "Tool failed",
|
||||
},
|
||||
};
|
||||
|
||||
const set = labels[kind];
|
||||
if (failed) return set.failed;
|
||||
if (running) return set.running;
|
||||
return set.completed;
|
||||
}
|
||||
|
||||
type CompactToolPreview = {
|
||||
@@ -181,13 +226,56 @@ function extractCompactToolPreview(
|
||||
case "post_compact_hook":
|
||||
return emptyPreview();
|
||||
case "dev_mcp":
|
||||
case "generic":
|
||||
return textPreview(
|
||||
getToolString(args, ["command", "path", "source", "query", "name"]) ??
|
||||
null,
|
||||
getToolString(args, [
|
||||
"command",
|
||||
"path",
|
||||
"source",
|
||||
"query",
|
||||
"name",
|
||||
"content",
|
||||
"message",
|
||||
]) ??
|
||||
(item.title && !isGenericToolTitle(item.title) ? item.title : null),
|
||||
);
|
||||
case "buzz":
|
||||
return textPreview(extractBuzzToolPreview(args));
|
||||
}
|
||||
}
|
||||
|
||||
function extractBuzzToolPreview(args: Record<string, unknown>): string | null {
|
||||
const content = getToolString(args, ["content", "message", "text", "body"]);
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const query = getToolString(args, ["query", "search"]);
|
||||
if (query) {
|
||||
return query;
|
||||
}
|
||||
|
||||
const channelId = getToolString(args, ["channel_id", "channelId"]);
|
||||
if (channelId) {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
const workflowId = getToolString(args, ["workflow_id", "workflowId"]);
|
||||
if (workflowId) {
|
||||
return workflowId;
|
||||
}
|
||||
|
||||
const pubkeys = getToolStringList(args, ["pubkeys", "pubkey"]);
|
||||
if (pubkeys.length === 1) {
|
||||
return pubkeys[0];
|
||||
}
|
||||
if (pubkeys.length > 1) {
|
||||
return `${pubkeys.length} users`;
|
||||
}
|
||||
|
||||
return getToolString(args, ["event_id", "eventId", "name"]);
|
||||
}
|
||||
|
||||
function textPreview(preview: string | null): CompactToolPreview {
|
||||
return { preview, thumbnailSrc: null };
|
||||
}
|
||||
|
||||
@@ -124,6 +124,32 @@ test("buildTranscriptDisplayBlocks bundles user prompt, setup, and context toget
|
||||
assert.equal(promptSegment.context?.id, "context");
|
||||
assert.equal(promptSegment.setup.length, 2);
|
||||
assert.equal(turnBlock.segments[1]?.kind, "item");
|
||||
assert.equal(turnBlock.segments[2]?.kind, "tool_group");
|
||||
});
|
||||
|
||||
test("buildTranscriptDisplayBlocks groups consecutive tool calls", () => {
|
||||
const rawItems = [
|
||||
userPrompt("prompt", "run these", "turn-1"),
|
||||
toolCall("tool-1", "turn-1"),
|
||||
toolCall("tool-2", "turn-1"),
|
||||
assistantMessage("assistant", "Done.", "turn-1"),
|
||||
toolCall("tool-3", "turn-1"),
|
||||
];
|
||||
|
||||
const blocks = buildTranscriptDisplayBlocks(rawItems);
|
||||
const turnBlock = blocks[0];
|
||||
assert.equal(turnBlock?.kind, "turn");
|
||||
assert.equal(turnBlock.segments[1]?.kind, "tool_group");
|
||||
assert.deepEqual(
|
||||
turnBlock.segments[1]?.items?.map((item) => item.id),
|
||||
["tool-1", "tool-2"],
|
||||
);
|
||||
assert.equal(turnBlock.segments[2]?.kind, "item");
|
||||
assert.equal(turnBlock.segments[3]?.kind, "tool_group");
|
||||
assert.deepEqual(
|
||||
turnBlock.segments[3]?.items?.map((item) => item.id),
|
||||
["tool-3"],
|
||||
);
|
||||
});
|
||||
|
||||
test("buildTranscriptDisplayBlocks collapses setup lifecycle inside prompt bundle", () => {
|
||||
@@ -146,23 +172,25 @@ test("buildTranscriptDisplayBlocks collapses setup lifecycle inside prompt bundl
|
||||
);
|
||||
});
|
||||
|
||||
test("buildTranscriptDisplayBlocks hides setup lifecycle when prompt is missing", () => {
|
||||
test("buildTranscriptDisplayBlocks hides setup and context when prompt is missing", () => {
|
||||
const rawItems = [
|
||||
lifecycle("turn", "Turn started", "turn_started", "turn-1"),
|
||||
lifecycle("session", "Session ready", "session_resolved", "turn-1"),
|
||||
promptContext("context", "turn-1"),
|
||||
toolCall("tool", "turn-1"),
|
||||
];
|
||||
|
||||
const blocks = buildTranscriptDisplayBlocks(rawItems);
|
||||
const displayOrder = flattenDisplayBlocks(blocks).map((item) => item.id);
|
||||
|
||||
assert.deepEqual(displayOrder, ["context"]);
|
||||
assert.deepEqual(displayOrder, ["tool"]);
|
||||
});
|
||||
|
||||
test("buildTranscriptDisplayBlocks drops setup-only turns", () => {
|
||||
test("buildTranscriptDisplayBlocks drops setup-and-context-only turns", () => {
|
||||
const rawItems = [
|
||||
lifecycle("turn", "Turn started", "turn_started", "turn-1"),
|
||||
lifecycle("session", "Session ready", "session_resolved", "turn-1"),
|
||||
promptContext("context", "turn-1"),
|
||||
];
|
||||
|
||||
const blocks = buildTranscriptDisplayBlocks(rawItems);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TranscriptItem } from "./agentSessionTypes";
|
||||
|
||||
export type TranscriptTurnSegment =
|
||||
| { kind: "item"; item: TranscriptItem }
|
||||
| { kind: "tool_group"; items: Extract<TranscriptItem, { type: "tool" }>[] }
|
||||
| { kind: "setup"; items: Extract<TranscriptItem, { type: "lifecycle" }>[] }
|
||||
| {
|
||||
kind: "prompt";
|
||||
@@ -62,12 +63,14 @@ function classifyTurnItems(items: TranscriptItem[]): TranscriptTurnSegment[] {
|
||||
|
||||
if (userPrompt) consumed.add(userPrompt);
|
||||
for (const item of setupLifecycle) consumed.add(item);
|
||||
if (userPrompt && promptContext) consumed.add(promptContext);
|
||||
if (promptContext) consumed.add(promptContext);
|
||||
|
||||
const activity = items.filter((item) => !consumed.has(item));
|
||||
|
||||
if (!userPrompt) {
|
||||
return activity.map((item) => ({ kind: "item", item }));
|
||||
return groupConsecutiveToolSegments(
|
||||
activity.map((item) => ({ kind: "item", item })),
|
||||
);
|
||||
}
|
||||
|
||||
const segments: TranscriptTurnSegment[] = [
|
||||
@@ -90,7 +93,34 @@ function classifyTurnItems(items: TranscriptItem[]): TranscriptTurnSegment[] {
|
||||
segments.push({ kind: "item", item });
|
||||
}
|
||||
|
||||
return segments;
|
||||
return groupConsecutiveToolSegments(segments);
|
||||
}
|
||||
|
||||
function groupConsecutiveToolSegments(
|
||||
segments: TranscriptTurnSegment[],
|
||||
): TranscriptTurnSegment[] {
|
||||
const grouped: TranscriptTurnSegment[] = [];
|
||||
let toolBuffer: Extract<TranscriptItem, { type: "tool" }>[] = [];
|
||||
|
||||
const flushTools = () => {
|
||||
if (toolBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
grouped.push({ kind: "tool_group", items: toolBuffer });
|
||||
toolBuffer = [];
|
||||
};
|
||||
|
||||
for (const segment of segments) {
|
||||
if (segment.kind === "item" && segment.item.type === "tool") {
|
||||
toolBuffer.push(segment.item);
|
||||
continue;
|
||||
}
|
||||
flushTools();
|
||||
grouped.push(segment);
|
||||
}
|
||||
|
||||
flushTools();
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +192,8 @@ export function flattenDisplayBlocks(
|
||||
for (const segment of block.segments) {
|
||||
if (segment.kind === "item") {
|
||||
result.push(segment.item);
|
||||
} else if (segment.kind === "tool_group") {
|
||||
result.push(...segment.items);
|
||||
} else if (segment.kind === "prompt") {
|
||||
result.push(segment.user);
|
||||
result.push(...segment.setup);
|
||||
|
||||
@@ -10,12 +10,14 @@ const toggleVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
ghost:
|
||||
"bg-transparent hover:bg-muted/70 hover:text-foreground data-[state=on]:bg-muted data-[state=on]:text-foreground",
|
||||
outline:
|
||||
"border border-input/40 bg-background hover:bg-muted/70 data-[state=on]:bg-muted data-[state=on]:text-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-3 min-w-9",
|
||||
xs: "h-5 min-h-0 min-w-0 gap-1 rounded-md px-1.5 text-xs font-medium",
|
||||
xs: "h-5 min-h-0 min-w-0 gap-1 rounded-md px-1.5 text-xs font-medium [&_svg]:size-3.5",
|
||||
sm: "h-8 px-2 min-w-8",
|
||||
lg: "h-10 px-3 min-w-10",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user