coalesce cursor thinking deltas in run log streaming

This commit is contained in:
Dotta
2026-03-05 08:35:00 -06:00
parent 8f70e79240
commit 1c9b7ef918
6 changed files with 73 additions and 13 deletions

View File

@@ -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 }

View File

@@ -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") {

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}