coalesce cursor thinking deltas in run log streaming
This commit is contained in:
@@ -136,7 +136,7 @@ export interface ServerAdapterModule {
|
|||||||
|
|
||||||
export type TranscriptEntry =
|
export type TranscriptEntry =
|
||||||
| { kind: "assistant"; ts: string; text: string }
|
| { 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: "user"; ts: string; text: string }
|
||||||
| { kind: "tool_call"; ts: string; name: string; input: unknown }
|
| { kind: "tool_call"; ts: string; name: string; input: unknown }
|
||||||
| { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean }
|
| { 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") {
|
if (type === "thinking") {
|
||||||
const text = asString(parsed.text).trim() || asString(asRecord(parsed.delta)?.text).trim();
|
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 [];
|
if (!text) return [];
|
||||||
return [{ kind: "thinking", ts, text }];
|
return [{ kind: "thinking", ts, text, ...(isDelta ? { delta: true } : {}) }];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "tool_call") {
|
if (type === "tool_call") {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ describe("cursor ui stdout parser", () => {
|
|||||||
}),
|
}),
|
||||||
ts,
|
ts,
|
||||||
),
|
),
|
||||||
).toEqual([{ kind: "thinking", ts, text: "planning next command" }]);
|
).toEqual([{ kind: "thinking", ts, text: "planning next command", delta: true }]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
parseCursorStdoutLine(
|
parseCursorStdoutLine(
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ import type { TranscriptEntry, StdoutLineParser } from "./types";
|
|||||||
|
|
||||||
type RunLogChunk = { ts: string; stream: "stdout" | "stderr" | "system"; chunk: string };
|
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[] {
|
export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser): TranscriptEntry[] {
|
||||||
const entries: TranscriptEntry[] = [];
|
const entries: TranscriptEntry[] = [];
|
||||||
let stdoutBuffer = "";
|
let stdoutBuffer = "";
|
||||||
@@ -22,14 +34,18 @@ export function buildTranscript(chunks: RunLogChunk[], parser: StdoutLineParser)
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
entries.push(...parser(trimmed, chunk.ts));
|
for (const entry of parser(trimmed, chunk.ts)) {
|
||||||
|
appendTranscriptEntry(entries, entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const trailing = stdoutBuffer.trim();
|
const trailing = stdoutBuffer.trim();
|
||||||
if (trailing) {
|
if (trailing) {
|
||||||
const ts = chunks.length > 0 ? chunks[chunks.length - 1]!.ts : new Date().toISOString();
|
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;
|
return entries;
|
||||||
|
|||||||
@@ -97,6 +97,25 @@ function parseStdoutChunk(
|
|||||||
pendingByRun.set(pendingKey, split.pop() ?? "");
|
pendingByRun.set(pendingKey, split.pop() ?? "");
|
||||||
const adapter = getUIAdapter(run.adapterType);
|
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[] = [];
|
const items: FeedItem[] = [];
|
||||||
for (const line of split.slice(-8)) {
|
for (const line of split.slice(-8)) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
@@ -108,13 +127,15 @@ function parseStdoutChunk(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const entry of parsed) {
|
for (const entry of parsed) {
|
||||||
const summary = summarizeEntry(entry);
|
appendSummary(entry);
|
||||||
if (!summary) continue;
|
|
||||||
const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++);
|
|
||||||
if (item) items.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const summary of summarized) {
|
||||||
|
const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++);
|
||||||
|
if (item) items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,25 @@ function parseStdoutChunk(
|
|||||||
pendingByRun.set(pendingKey, split.pop() ?? "");
|
pendingByRun.set(pendingKey, split.pop() ?? "");
|
||||||
const adapter = getUIAdapter(run.adapterType);
|
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[] = [];
|
const items: FeedItem[] = [];
|
||||||
for (const line of split.slice(-8)) {
|
for (const line of split.slice(-8)) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
@@ -119,13 +138,15 @@ function parseStdoutChunk(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const entry of parsed) {
|
for (const entry of parsed) {
|
||||||
const summary = summarizeEntry(entry);
|
appendSummary(entry);
|
||||||
if (!summary) continue;
|
|
||||||
const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++);
|
|
||||||
if (item) items.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const summary of summarized) {
|
||||||
|
const item = createFeedItem(run, ts, summary.text, summary.tone, nextIdRef.current++);
|
||||||
|
if (item) items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user