coalesce cursor thinking deltas in run log streaming
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user