diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index d7e9d553..95143b51 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -136,7 +136,7 @@ export interface ServerAdapterModule { export type TranscriptEntry = | { kind: "assistant"; ts: string; text: string } - | { kind: "thinking"; ts: string; text: string } + | { kind: "thinking"; ts: string; text: string; delta?: boolean } | { kind: "user"; ts: string; text: string } | { kind: "tool_call"; ts: string; name: string; input: unknown } | { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean } diff --git a/packages/adapters/cursor-local/src/ui/parse-stdout.ts b/packages/adapters/cursor-local/src/ui/parse-stdout.ts index 6202ec10..9690b507 100644 --- a/packages/adapters/cursor-local/src/ui/parse-stdout.ts +++ b/packages/adapters/cursor-local/src/ui/parse-stdout.ts @@ -219,8 +219,10 @@ export function parseCursorStdoutLine(line: string, ts: string): TranscriptEntry if (type === "thinking") { const text = asString(parsed.text).trim() || asString(asRecord(parsed.delta)?.text).trim(); + const subtype = asString(parsed.subtype).trim().toLowerCase(); + const isDelta = subtype === "delta" || asRecord(parsed.delta) !== null; if (!text) return []; - return [{ kind: "thinking", ts, text }]; + return [{ kind: "thinking", ts, text, ...(isDelta ? { delta: true } : {}) }]; } if (type === "tool_call") { diff --git a/server/src/__tests__/cursor-local-adapter.test.ts b/server/src/__tests__/cursor-local-adapter.test.ts index 70547200..aad83bc7 100644 --- a/server/src/__tests__/cursor-local-adapter.test.ts +++ b/server/src/__tests__/cursor-local-adapter.test.ts @@ -162,7 +162,7 @@ describe("cursor ui stdout parser", () => { }), ts, ), - ).toEqual([{ kind: "thinking", ts, text: "planning next command" }]); + ).toEqual([{ kind: "thinking", ts, text: "planning next command", delta: true }]); expect( parseCursorStdoutLine( diff --git a/ui/src/adapters/transcript.ts b/ui/src/adapters/transcript.ts index 33add4d1..58cff1b5 100644 --- a/ui/src/adapters/transcript.ts +++ b/ui/src/adapters/transcript.ts @@ -2,6 +2,18 @@ import type { TranscriptEntry, StdoutLineParser } from "./types"; type RunLogChunk = { ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }; +function appendTranscriptEntry(entries: TranscriptEntry[], entry: TranscriptEntry) { + if (entry.kind === "thinking" && entry.delta) { + const last = entries[entries.length - 1]; + if (last && last.kind === "thinking" && last.delta) { + last.text += entry.text; + last.ts = entry.ts; + return; + } + } + entries.push(entry); +} + export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser): TranscriptEntry[] { const entries: TranscriptEntry[] = []; let stdoutBuffer = ""; @@ -22,14 +34,18 @@ export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser) for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - entries.push(...parser(trimmed, chunk.ts)); + for (const entry of parser(trimmed, chunk.ts)) { + appendTranscriptEntry(entries, entry); + } } } const trailing = stdoutBuffer.trim(); if (trailing) { const ts = chunks.length > 0 ? chunks[chunks.length - 1]!.ts : new Date().toISOString(); - entries.push(...parser(trailing, ts)); + for (const entry of parser(trailing, ts)) { + appendTranscriptEntry(entries, entry); + } } return entries; diff --git a/ui/src/components/ActiveAgentsPanel.tsx b/ui/src/components/ActiveAgentsPanel.tsx index cf8c8baf..53a1af9f 100644 --- a/ui/src/components/ActiveAgentsPanel.tsx +++ b/ui/src/components/ActiveAgentsPanel.tsx @@ -97,6 +97,25 @@ function parseStdoutChunk( pendingByRun.set(pendingKey, split.pop() ?? ""); const adapter = getUIAdapter(run.adapterType); + const summarized: Array<{ text: string; tone: FeedTone; thinkingDelta?: boolean }> = []; + const appendSummary = (entry: TranscriptEntry) => { + if (entry.kind === "thinking" && entry.delta) { + const text = entry.text.trim(); + if (!text) return; + const last = summarized[summarized.length - 1]; + if (last && last.thinkingDelta) { + last.text += text; + } else { + summarized.push({ text: `[thinking] ${text}`, tone: "info", thinkingDelta: true }); + } + return; + } + + const summary = summarizeEntry(entry); + if (!summary) return; + summarized.push({ text: summary.text, tone: summary.tone }); + }; + const items: FeedItem[] = []; for (const line of split.slice(-8)) { const trimmed = line.trim(); @@ -108,13 +127,15 @@ function parseStdoutChunk( continue; } for (const entry of parsed) { - const summary = summarizeEntry(entry); - if (!summary) continue; - const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++); - if (item) items.push(item); + appendSummary(entry); } } + for (const summary of summarized) { + const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++); + if (item) items.push(item); + } + return items; } diff --git a/ui/src/components/LiveRunWidget.tsx b/ui/src/components/LiveRunWidget.tsx index 7af7164b..0febefd4 100644 --- a/ui/src/components/LiveRunWidget.tsx +++ b/ui/src/components/LiveRunWidget.tsx @@ -108,6 +108,25 @@ function parseStdoutChunk( pendingByRun.set(pendingKey, split.pop() ?? ""); const adapter = getUIAdapter(run.adapterType); + const summarized: Array<{ text: string; tone: FeedTone; thinkingDelta?: boolean }> = []; + const appendSummary = (entry: TranscriptEntry) => { + if (entry.kind === "thinking" && entry.delta) { + const text = entry.text.trim(); + if (!text) return; + const last = summarized[summarized.length - 1]; + if (last && last.thinkingDelta) { + last.text += text; + } else { + summarized.push({ text: `[thinking] ${text}`, tone: "info", thinkingDelta: true }); + } + return; + } + + const summary = summarizeEntry(entry); + if (!summary) return; + summarized.push({ text: summary.text, tone: summary.tone }); + }; + const items: FeedItem[] = []; for (const line of split.slice(-8)) { const trimmed = line.trim(); @@ -119,13 +138,15 @@ function parseStdoutChunk( continue; } for (const entry of parsed) { - const summary = summarizeEntry(entry); - if (!summary) continue; - const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++); - if (item) items.push(item); + appendSummary(entry); } } + for (const summary of summarized) { + const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++); + if (item) items.push(item); + } + return items; }