diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx index 08bc730a3..019b4d7d9 100644 --- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx +++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx @@ -26,6 +26,9 @@ import { buildTranscriptPresentation } from "./agentSessionTranscriptPresentatio import { formatTranscriptTime } from "./agentSessionUtils"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +/** Dev-only: surface the observer wire label that produced each transcript row. */ +const SHOW_TRANSCRIPT_ACP_SOURCE = import.meta.env.DEV; + export function AgentSessionTranscriptList({ agentName, compact = false, @@ -87,6 +90,9 @@ export function AgentSessionTranscriptList({ )} key={item.id} > + {SHOW_TRANSCRIPT_ACP_SOURCE && item.acpSource ? ( + + ) : null} + {source} + + ); +} + function TranscriptNowSummary({ agentName, compact, diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs new file mode 100644 index 000000000..dda17b174 --- /dev/null +++ b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { buildTranscript } from "./agentSessionTranscript.ts"; + +test("buildTranscript tags assistant chunks with agent_message_chunk", () => { + const items = buildTranscript([ + { + seq: 1, + timestamp: "2026-06-14T20:47:14.000Z", + kind: "acp_read", + agentIndex: 0, + channelId: "channel-1", + sessionId: "sess-1", + turnId: "turn-1", + payload: { + method: "session/update", + params: { + update: { + sessionUpdate: "agent_message_chunk", + messageId: "msg-1", + content: [{ type: "text", text: "Marge is summoned." }], + }, + }, + }, + }, + ]); + + assert.equal(items.length, 1); + assert.equal(items[0]?.type, "message"); + assert.equal(items[0]?.acpSource, "agent_message_chunk"); +}); + +test("buildTranscript tags thought chunks with agent_thought_chunk", () => { + const items = buildTranscript([ + { + seq: 2, + timestamp: "2026-06-14T20:47:15.000Z", + kind: "acp_read", + agentIndex: 0, + channelId: "channel-1", + sessionId: "sess-1", + turnId: "turn-1", + payload: { + method: "session/update", + params: { + update: { + sessionUpdate: "agent_thought_chunk", + messageId: "thought-1", + content: [{ type: "text", text: "Considering next step." }], + }, + }, + }, + }, + ]); + + assert.equal(items.length, 1); + assert.equal(items[0]?.type, "thought"); + assert.equal(items[0]?.acpSource, "agent_thought_chunk"); +}); diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.ts b/desktop/src/features/agents/ui/agentSessionTranscript.ts index 60cf7f62b..62d642f59 100644 --- a/desktop/src/features/agents/ui/agentSessionTranscript.ts +++ b/desktop/src/features/agents/ui/agentSessionTranscript.ts @@ -117,6 +117,7 @@ function upsertMessage( timestamp: string, channelId: string | null, authorPubkey: string | null = null, + acpSource?: string, ) { const currentKey = d.activeMessageKey.get(id); @@ -128,6 +129,7 @@ function upsertMessage( text: existing.text + text, channelId, authorPubkey: authorPubkey ?? existing.authorPubkey, + acpSource: acpSource ?? existing.acpSource, }); return; } @@ -144,6 +146,7 @@ function upsertMessage( timestamp, channelId, authorPubkey, + acpSource, }); d.activeMessageKey = new Map(d.activeMessageKey); d.activeMessageKey.set(id, newKey); @@ -157,14 +160,20 @@ function upsertTextItem( text: string, timestamp: string, channelId: string | null, + acpSource?: string, ) { const existing = d.itemsById.get(id); if (existing && existing.type === type) { - replaceItem(d, id, { ...existing, text: existing.text + text, channelId }); + replaceItem(d, id, { + ...existing, + text: existing.text + text, + channelId, + acpSource: acpSource ?? existing.acpSource, + }); return; } sealOpenMessages(d); - pushItem(d, { id, type, title, text, timestamp, channelId }); + pushItem(d, { id, type, title, text, timestamp, channelId, acpSource }); } function upsertMetadata( @@ -174,14 +183,28 @@ function upsertMetadata( sections: PromptSection[], timestamp: string, channelId: string | null, + acpSource?: string, ) { const existing = d.itemsById.get(id); if (existing?.type === "metadata") { - replaceItem(d, id, { ...existing, sections, channelId }); + replaceItem(d, id, { + ...existing, + sections, + channelId, + acpSource: acpSource ?? existing.acpSource, + }); return; } sealOpenMessages(d); - pushItem(d, { id, type: "metadata", title, sections, timestamp, channelId }); + pushItem(d, { + id, + type: "metadata", + title, + sections, + timestamp, + channelId, + acpSource, + }); } function upsertTool( @@ -196,6 +219,7 @@ function upsertTool( isError: boolean, timestamp: string, channelId: string | null, + acpSource?: string, ) { const existing = d.itemsById.get(id); const canonicalBuzzToolName = @@ -225,6 +249,7 @@ function upsertTool( ? timestamp : existing.completedAt, channelId, + acpSource: acpSource ?? existing.acpSource, }); return; } @@ -243,6 +268,7 @@ function upsertTool( startedAt: timestamp, completedAt: null, channelId, + acpSource, }); } @@ -268,6 +294,7 @@ export function processTranscriptEvent( describeTurnStarted(event.payload), event.timestamp, channelId, + event.kind, ); } else if (event.kind === "session_resolved") { upsertTextItem( @@ -278,6 +305,7 @@ export function processTranscriptEvent( describeSessionResolved(event.payload), event.timestamp, channelId, + event.kind, ); } else if (event.kind === "acp_parse_error") { upsertTextItem( @@ -288,6 +316,7 @@ export function processTranscriptEvent( extractBlockText(event.payload), event.timestamp, channelId, + event.kind, ); } else if (event.kind === "turn_error" || event.kind === "agent_panic") { const payload = asRecord(event.payload); @@ -303,6 +332,7 @@ export function processTranscriptEvent( `${outcome}: ${error}`, event.timestamp, channelId, + event.kind, ); } else if (event.kind === "acp_read" || event.kind === "acp_write") { const payload = asRecord(event.payload); @@ -322,6 +352,7 @@ export function processTranscriptEvent( event.timestamp, channelId, parsedPrompt.userPubkey, + "session/prompt:user", ); } if (parsedPrompt.sections.length > 0) { @@ -332,6 +363,7 @@ export function processTranscriptEvent( parsedPrompt.sections, event.timestamp, channelId, + "session/prompt:context", ); } } @@ -351,6 +383,8 @@ export function processTranscriptEvent( extractContentText(update.content), event.timestamp, channelId, + null, + updateType, ); } else if (updateType === "user_message_chunk") { upsertMessage( @@ -361,6 +395,8 @@ export function processTranscriptEvent( extractContentText(update.content), event.timestamp, channelId, + null, + updateType, ); } else if (updateType === "agent_thought_chunk") { upsertTextItem( @@ -371,6 +407,7 @@ export function processTranscriptEvent( extractContentText(update.content), event.timestamp, channelId, + updateType, ); } else if (updateType === "tool_call") { const toolId = asString(update.toolCallId) ?? `tool:${event.seq}`; @@ -387,6 +424,7 @@ export function processTranscriptEvent( false, event.timestamp, channelId, + updateType, ); } else if (updateType === "tool_call_update") { const toolId = asString(update.toolCallId) ?? `tool:${event.seq}`; @@ -406,6 +444,7 @@ export function processTranscriptEvent( status === "failed", event.timestamp, channelId, + updateType, ); } else if (updateType === "plan") { upsertTextItem( @@ -416,6 +455,7 @@ export function processTranscriptEvent( extractContentText(update.content) || JSON.stringify(update, null, 2), event.timestamp, channelId, + updateType, ); } } diff --git a/desktop/src/features/agents/ui/agentSessionTypes.ts b/desktop/src/features/agents/ui/agentSessionTypes.ts index 2ff4ea305..90fb2500e 100644 --- a/desktop/src/features/agents/ui/agentSessionTypes.ts +++ b/desktop/src/features/agents/ui/agentSessionTypes.ts @@ -20,6 +20,9 @@ export type ConnectionState = export type ToolStatus = "executing" | "completed" | "failed" | "pending"; +/** Observer/ACP wire label for dev-only transcript debugging. */ +export type TranscriptAcpSource = string; + export type TranscriptItem = | { id: string; @@ -28,6 +31,7 @@ export type TranscriptItem = title: string; text: string; timestamp: string; + acpSource?: TranscriptAcpSource; authorPubkey?: string | null; channelId?: string | null; } @@ -37,6 +41,7 @@ export type TranscriptItem = title: string; text: string; timestamp: string; + acpSource?: TranscriptAcpSource; channelId?: string | null; } | { @@ -45,6 +50,7 @@ export type TranscriptItem = title: string; text: string; timestamp: string; + acpSource?: TranscriptAcpSource; channelId?: string | null; } | { @@ -53,6 +59,7 @@ export type TranscriptItem = title: string; sections: PromptSection[]; timestamp: string; + acpSource?: TranscriptAcpSource; channelId?: string | null; } | { @@ -68,6 +75,7 @@ export type TranscriptItem = timestamp: string; startedAt: string; completedAt: string | null; + acpSource?: TranscriptAcpSource; channelId?: string | null; };