mirror of
https://github.com/block/buzz.git
synced 2026-10-30 14:01:37 +00:00
- Remove the compact layout prop from AgentSessionTranscriptList, ManagedAgentSessionPanel, and AgentSessionThreadPanel; always render the tighter transcript density - Revert consecutive tool-call grouping (tool_group segments, ToolCallGroup, segment spacing helpers) and render each tool as a normal transcript row - Add symmetric per-type row spacing: my-2.5 for messages, my-1 for tools, my-2 for other items - Simplify ToolItem wrapper padding now that grouped/compact props are gone
212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
import type { TranscriptItem } from "./agentSessionTypes";
|
|
|
|
export type TranscriptTurnSegment =
|
|
| { kind: "item"; item: TranscriptItem }
|
|
| { kind: "setup"; items: Extract<TranscriptItem, { type: "lifecycle" }>[] }
|
|
| {
|
|
kind: "prompt";
|
|
user: Extract<TranscriptItem, { type: "message" }>;
|
|
context: Extract<TranscriptItem, { type: "metadata" }> | null;
|
|
setup: Extract<TranscriptItem, { type: "lifecycle" }>[];
|
|
};
|
|
|
|
export type TranscriptDisplayBlock =
|
|
| { kind: "single"; item: TranscriptItem }
|
|
| { kind: "turn"; turnId: string; segments: TranscriptTurnSegment[] };
|
|
|
|
function isUserPrompt(
|
|
item: TranscriptItem,
|
|
): item is Extract<TranscriptItem, { type: "message" }> {
|
|
return (
|
|
item.type === "message" &&
|
|
item.role === "user" &&
|
|
item.acpSource === "session/prompt:user"
|
|
);
|
|
}
|
|
|
|
function isPromptContext(
|
|
item: TranscriptItem,
|
|
): item is Extract<TranscriptItem, { type: "metadata" }> {
|
|
return (
|
|
item.type === "metadata" && item.acpSource === "session/prompt:context"
|
|
);
|
|
}
|
|
|
|
function isSetupLifecycle(
|
|
item: TranscriptItem,
|
|
): item is Extract<TranscriptItem, { type: "lifecycle" }> {
|
|
return (
|
|
item.type === "lifecycle" &&
|
|
(item.acpSource === "turn_started" || item.acpSource === "session_resolved")
|
|
);
|
|
}
|
|
|
|
function isErrorLifecycle(
|
|
item: TranscriptItem,
|
|
): item is Extract<TranscriptItem, { type: "lifecycle" }> {
|
|
return (
|
|
item.type === "lifecycle" && item.title.toLowerCase().includes("error")
|
|
);
|
|
}
|
|
|
|
type TurnBucket = {
|
|
turnId: string;
|
|
items: TranscriptItem[];
|
|
};
|
|
|
|
function classifyTurnItems(items: TranscriptItem[]): TranscriptTurnSegment[] {
|
|
const userPrompt = items.find(isUserPrompt) ?? null;
|
|
const setupLifecycle = items.filter(isSetupLifecycle);
|
|
const promptContext = items.find(isPromptContext) ?? null;
|
|
const consumed = new Set<TranscriptItem>();
|
|
|
|
if (userPrompt) consumed.add(userPrompt);
|
|
for (const item of setupLifecycle) consumed.add(item);
|
|
if (promptContext) consumed.add(promptContext);
|
|
|
|
const activity = items.filter((item) => !consumed.has(item));
|
|
|
|
if (!userPrompt) {
|
|
return activity.map((item) => ({ kind: "item", item }));
|
|
}
|
|
|
|
const segments: TranscriptTurnSegment[] = [
|
|
{
|
|
kind: "prompt",
|
|
user: userPrompt,
|
|
context: promptContext,
|
|
setup: setupLifecycle,
|
|
},
|
|
];
|
|
|
|
for (const item of activity) {
|
|
if (isErrorLifecycle(item)) {
|
|
segments.push({ kind: "item", item });
|
|
continue;
|
|
}
|
|
if (isSetupLifecycle(item)) {
|
|
continue;
|
|
}
|
|
segments.push({ kind: "item", item });
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
/**
|
|
* Build presentation-only display blocks from normalized transcript items.
|
|
* Raw observer order is preserved in the source items; this only reorders
|
|
* within a turn for user-facing narrative flow.
|
|
*/
|
|
export function buildTranscriptDisplayBlocks(
|
|
items: TranscriptItem[],
|
|
): TranscriptDisplayBlock[] {
|
|
const blocks: TranscriptDisplayBlock[] = [];
|
|
const turnBuckets = new Map<string, TurnBucket>();
|
|
const displayOrder: Array<
|
|
{ kind: "single"; item: TranscriptItem } | { kind: "turn"; turnId: string }
|
|
> = [];
|
|
|
|
for (const item of items) {
|
|
const turnId = item.turnId;
|
|
if (!turnId) {
|
|
displayOrder.push({ kind: "single", item });
|
|
continue;
|
|
}
|
|
|
|
let bucket = turnBuckets.get(turnId);
|
|
if (!bucket) {
|
|
bucket = { turnId, items: [] };
|
|
turnBuckets.set(turnId, bucket);
|
|
displayOrder.push({ kind: "turn", turnId });
|
|
}
|
|
bucket.items.push(item);
|
|
}
|
|
|
|
for (const entry of displayOrder) {
|
|
if (entry.kind === "single") {
|
|
blocks.push({ kind: "single", item: entry.item });
|
|
continue;
|
|
}
|
|
|
|
const bucket = turnBuckets.get(entry.turnId);
|
|
if (!bucket || bucket.items.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const segments = classifyTurnItems(bucket.items);
|
|
if (segments.length > 0) {
|
|
blocks.push({
|
|
kind: "turn",
|
|
turnId: entry.turnId,
|
|
segments,
|
|
});
|
|
}
|
|
}
|
|
|
|
return blocks;
|
|
}
|
|
|
|
/** Flatten display blocks back to items for testing display order. */
|
|
export function flattenDisplayBlocks(
|
|
blocks: TranscriptDisplayBlock[],
|
|
): TranscriptItem[] {
|
|
const result: TranscriptItem[] = [];
|
|
|
|
for (const block of blocks) {
|
|
if (block.kind === "single") {
|
|
result.push(block.item);
|
|
continue;
|
|
}
|
|
|
|
for (const segment of block.segments) {
|
|
if (segment.kind === "item") {
|
|
result.push(segment.item);
|
|
} else if (segment.kind === "prompt") {
|
|
result.push(segment.user);
|
|
result.push(...segment.setup);
|
|
if (segment.context) {
|
|
result.push(segment.context);
|
|
}
|
|
} else {
|
|
result.push(...segment.items);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/** Human-readable labels for a collapsed turn setup row. */
|
|
export function formatTurnSetupLabel(
|
|
items: Extract<TranscriptItem, { type: "lifecycle" }>[],
|
|
): string {
|
|
const labels = items.map((item) => item.title);
|
|
return labels.join(" · ");
|
|
}
|
|
|
|
/** Earliest timestamp among setup lifecycle items. */
|
|
export function turnSetupTimestamp(
|
|
items: Extract<TranscriptItem, { type: "lifecycle" }>[],
|
|
): string | null {
|
|
if (items.length === 0) return null;
|
|
return items.reduce(
|
|
(earliest, item) =>
|
|
Date.parse(item.timestamp) < Date.parse(earliest)
|
|
? item.timestamp
|
|
: earliest,
|
|
items[0].timestamp,
|
|
);
|
|
}
|
|
|
|
/** Optional detail text from setup lifecycle items (e.g. trigger count). */
|
|
export function turnSetupDetail(
|
|
items: Extract<TranscriptItem, { type: "lifecycle" }>[],
|
|
): string | null {
|
|
const details = items
|
|
.map((item) => item.text.trim())
|
|
.filter((text) => text.length > 0);
|
|
if (details.length === 0) return null;
|
|
return details.join(" ");
|
|
}
|