expand cursor stream-json event parsing coverage

This commit is contained in:
Dotta
2026-03-05 08:25:41 -06:00
parent 426c1044b6
commit eabfd9d9f6
3 changed files with 305 additions and 1 deletions

View File

@@ -24,6 +24,30 @@ function stringifyUnknown(value: unknown): string {
}
}
function printUserMessage(messageRaw: unknown): void {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
if (text) console.log(pc.gray(`user: ${text}`));
return;
}
const message = asRecord(messageRaw);
if (!message) return;
const directText = asString(message.text).trim();
if (directText) console.log(pc.gray(`user: ${directText}`));
const content = Array.isArray(message.content) ? message.content : [];
for (const partRaw of content) {
const part = asRecord(partRaw);
if (!part) continue;
const type = asString(part.type).trim();
if (type !== "output_text" && type !== "text") continue;
const text = asString(part.text).trim();
if (text) console.log(pc.gray(`user: ${text}`));
}
}
function printAssistantMessage(messageRaw: unknown): void {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
@@ -82,6 +106,56 @@ function printAssistantMessage(messageRaw: unknown): void {
}
}
function printToolCallEventTopLevel(parsed: Record<string, unknown>): void {
const subtype = asString(parsed.subtype).trim().toLowerCase();
const callId = asString(parsed.call_id, asString(parsed.callId, asString(parsed.id, "")));
const toolCall = asRecord(parsed.tool_call ?? parsed.toolCall);
if (!toolCall) {
console.log(pc.yellow(`tool_call${subtype ? `: ${subtype}` : ""}`));
return;
}
const [toolName] = Object.keys(toolCall);
if (!toolName) {
console.log(pc.yellow(`tool_call${subtype ? `: ${subtype}` : ""}`));
return;
}
const payload = asRecord(toolCall[toolName]) ?? {};
const args = payload.args ?? asRecord(payload.function)?.arguments;
const result =
payload.result ??
payload.output ??
payload.error ??
asRecord(payload.function)?.result ??
asRecord(payload.function)?.output;
const isError =
parsed.is_error === true ||
payload.is_error === true ||
subtype === "failed" ||
subtype === "error" ||
subtype === "cancelled" ||
payload.error !== undefined;
if (subtype === "started" || subtype === "start") {
console.log(pc.yellow(`tool_call: ${toolName}${callId ? ` (${callId})` : ""}`));
if (args !== undefined) {
console.log(pc.gray(stringifyUnknown(args)));
}
return;
}
if (subtype === "completed" || subtype === "complete" || subtype === "finished") {
const header = `tool_result${isError ? " (error)" : ""}${callId ? ` (${callId})` : ""}`;
console.log((isError ? pc.red : pc.cyan)(header));
if (result !== undefined) {
console.log((isError ? pc.red : pc.gray)(stringifyUnknown(result)));
}
return;
}
console.log(pc.yellow(`tool_call: ${toolName}${subtype ? ` (${subtype})` : ""}`));
}
function printLegacyToolEvent(part: Record<string, unknown>): void {
const tool = asString(part.tool, "tool");
const callId = asString(part.callID, asString(part.id, ""));
@@ -158,6 +232,22 @@ export function printCursorStreamEvent(raw: string, _debug: boolean): void {
return;
}
if (type === "user") {
printUserMessage(parsed.message);
return;
}
if (type === "thinking") {
const text = asString(parsed.text).trim() || asString(asRecord(parsed.delta)?.text).trim();
if (text) console.log(pc.gray(`thinking: ${text}`));
return;
}
if (type === "tool_call") {
printToolCallEventTopLevel(parsed);
return;
}
if (type === "result") {
const usage = asRecord(parsed.usage);
const input = asNumber(usage?.input_tokens, asNumber(usage?.inputTokens));

View File

@@ -32,6 +32,32 @@ function stringifyUnknown(value: unknown): string {
}
}
function parseUserMessage(messageRaw: unknown, ts: string): TranscriptEntry[] {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
return text ? [{ kind: "user", ts, text }] : [];
}
const message = asRecord(messageRaw);
if (!message) return [];
const entries: TranscriptEntry[] = [];
const directText = asString(message.text).trim();
if (directText) entries.push({ kind: "user", ts, text: directText });
const content = Array.isArray(message.content) ? message.content : [];
for (const partRaw of content) {
const part = asRecord(partRaw);
if (!part) continue;
const type = asString(part.type).trim();
if (type !== "output_text" && type !== "text") continue;
const text = asString(part.text).trim();
if (text) entries.push({ kind: "user", ts, text });
}
return entries;
}
function parseAssistantMessage(messageRaw: unknown, ts: string): TranscriptEntry[] {
if (typeof messageRaw === "string") {
const text = messageRaw.trim();
@@ -101,6 +127,64 @@ function parseAssistantMessage(messageRaw: unknown, ts: string): TranscriptEntry
return entries;
}
function parseCursorToolCallEvent(event: Record<string, unknown>, ts: string): TranscriptEntry[] {
const subtype = asString(event.subtype).trim().toLowerCase();
const callId =
asString(event.call_id) ||
asString(event.callId) ||
asString(event.id) ||
"tool_call";
const toolCall = asRecord(event.tool_call ?? event.toolCall);
if (!toolCall) {
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }];
}
const [toolName] = Object.keys(toolCall);
if (!toolName) {
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }];
}
const payload = asRecord(toolCall[toolName]) ?? {};
const input = payload.args ?? asRecord(payload.function)?.arguments ?? {};
if (subtype === "started" || subtype === "start") {
return [{
kind: "tool_call",
ts,
name: toolName,
input,
}];
}
if (subtype === "completed" || subtype === "complete" || subtype === "finished") {
const result =
payload.result ??
payload.output ??
payload.error ??
asRecord(payload.function)?.result ??
asRecord(payload.function)?.output;
const isError =
event.is_error === true ||
payload.is_error === true ||
asString(payload.status).toLowerCase() === "error" ||
asString(payload.status).toLowerCase() === "failed" ||
asString(payload.status).toLowerCase() === "cancelled" ||
payload.error !== undefined;
return [{
kind: "tool_result",
ts,
toolUseId: callId,
content: result !== undefined ? stringifyUnknown(result) : `${toolName} completed`,
isError,
}];
}
return [{
kind: "system",
ts,
text: `tool_call${subtype ? ` (${subtype})` : ""}: ${toolName}`,
}];
}
export function parseCursorStdoutLine(line: string, ts: string): TranscriptEntry[] {
const normalized = normalizeCursorStreamLine(line);
if (!normalized.line) return [];
@@ -129,6 +213,20 @@ export function parseCursorStdoutLine(line: string, ts: string): TranscriptEntry
return entries.length > 0 ? entries : [{ kind: "assistant", ts, text: asString(parsed.result) }];
}
if (type === "user") {
return parseUserMessage(parsed.message, ts);
}
if (type === "thinking") {
const text = asString(parsed.text).trim() || asString(asRecord(parsed.delta)?.text).trim();
if (!text) return [];
return [{ kind: "thinking", ts, text }];
}
if (type === "tool_call") {
return parseCursorToolCallEvent(parsed, ts);
}
if (type === "result") {
const usage = asRecord(parsed.usage);
const inputTokens = asNumber(usage?.input_tokens, asNumber(usage?.inputTokens));

View File

@@ -136,6 +136,74 @@ describe("cursor ui stdout parser", () => {
),
).toEqual([{ kind: "thinking", ts, text: "streamed" }]);
});
it("parses user, top-level thinking, and top-level tool_call events", () => {
const ts = "2026-03-05T00:00:00.000Z";
expect(
parseCursorStdoutLine(
JSON.stringify({
type: "user",
message: {
role: "user",
content: [{ type: "text", text: "Please inspect README.md" }],
},
}),
ts,
),
).toEqual([{ kind: "user", ts, text: "Please inspect README.md" }]);
expect(
parseCursorStdoutLine(
JSON.stringify({
type: "thinking",
subtype: "delta",
text: "planning next command",
}),
ts,
),
).toEqual([{ kind: "thinking", ts, text: "planning next command" }]);
expect(
parseCursorStdoutLine(
JSON.stringify({
type: "tool_call",
subtype: "started",
call_id: "call_1",
tool_call: {
readToolCall: {
args: { path: "README.md" },
},
},
}),
ts,
),
).toEqual([{ kind: "tool_call", ts, name: "readToolCall", input: { path: "README.md" } }]);
expect(
parseCursorStdoutLine(
JSON.stringify({
type: "tool_call",
subtype: "completed",
call_id: "call_1",
tool_call: {
readToolCall: {
result: { success: { content: "README contents" } },
},
},
}),
ts,
),
).toEqual([
{
kind: "tool_result",
ts,
toolUseId: "call_1",
content: '{\n "success": {\n "content": "README contents"\n }\n}',
isError: false,
},
]);
});
});
function stripAnsi(value: string): string {
@@ -143,7 +211,7 @@ function stripAnsi(value: string): string {
}
describe("cursor cli formatter", () => {
it("prints init, assistant, tool, and result events", () => {
it("prints init, user, assistant, tool, and result events", () => {
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
try {
@@ -151,6 +219,15 @@ describe("cursor cli formatter", () => {
JSON.stringify({ type: "system", subtype: "init", session_id: "chat_abc", model: "gpt-5" }),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "user",
message: {
content: [{ type: "text", text: "run tests" }],
},
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "assistant",
@@ -160,6 +237,14 @@ describe("cursor cli formatter", () => {
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "thinking",
subtype: "delta",
text: "looking at package.json",
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "assistant",
@@ -178,6 +263,32 @@ describe("cursor cli formatter", () => {
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "tool_call",
subtype: "started",
call_id: "call_1",
tool_call: {
readToolCall: {
args: { path: "README.md" },
},
},
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "tool_call",
subtype: "completed",
call_id: "call_1",
tool_call: {
readToolCall: {
result: { success: { content: "README contents" } },
},
},
}),
false,
);
printCursorStreamEvent(
JSON.stringify({
type: "result",
@@ -196,8 +307,13 @@ describe("cursor cli formatter", () => {
expect(lines).toEqual(
expect.arrayContaining([
"Cursor init (session: chat_abc, model: gpt-5)",
"user: run tests",
"assistant: hello",
"thinking: looking at package.json",
"tool_call: bash",
"tool_call: readToolCall (call_1)",
"tool_result (call_1)",
'{\n "success": {\n "content": "README contents"\n }\n}',
"tool_result",
"AGENTS.md",
"result: subtype=success",