feat(cursor): compact shell tool calls and format results in run log
Show only the command for shellToolCall/shell inputs instead of the full payload. Format shell results with exit code + truncated stdout/stderr sections for readability. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,46 @@ function stringifyUnknown(value: unknown): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Max chars of stdout/stderr to show in run log for shell tool results. */
|
||||||
|
const SHELL_OUTPUT_TRUNCATE = 2000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format shell tool result for run log: exit code + stdout/stderr (truncated).
|
||||||
|
* If the result is not a shell-shaped object, returns full stringify.
|
||||||
|
*/
|
||||||
|
function formatShellToolResultForLog(result: unknown): string {
|
||||||
|
const obj = asRecord(result);
|
||||||
|
if (!obj) return stringifyUnknown(result);
|
||||||
|
const success = asRecord(obj.success);
|
||||||
|
if (!success) return stringifyUnknown(result);
|
||||||
|
const exitCode = asNumber(success.exitCode, NaN);
|
||||||
|
const stdout = asString(success.stdout).trim();
|
||||||
|
const stderr = asString(success.stderr).trim();
|
||||||
|
const hasShellShape = Number.isFinite(exitCode) || stdout.length > 0 || stderr.length > 0;
|
||||||
|
if (!hasShellShape) return stringifyUnknown(result);
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (Number.isFinite(exitCode)) lines.push(`exit ${exitCode}`);
|
||||||
|
if (stdout) {
|
||||||
|
const out = stdout.length > SHELL_OUTPUT_TRUNCATE ? stdout.slice(0, SHELL_OUTPUT_TRUNCATE) + "\n... (truncated)" : stdout;
|
||||||
|
lines.push("<stdout>");
|
||||||
|
lines.push(out);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
const err = stderr.length > SHELL_OUTPUT_TRUNCATE ? stderr.slice(0, SHELL_OUTPUT_TRUNCATE) + "\n... (truncated)" : stderr;
|
||||||
|
lines.push("<stderr>");
|
||||||
|
lines.push(err);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return compact input for run log when tool is shell/shellToolCall (command only). */
|
||||||
|
function compactShellToolInput(rawInput: unknown, payload?: Record<string, unknown>): unknown {
|
||||||
|
const cmd = asString(payload?.command ?? asRecord(rawInput)?.command);
|
||||||
|
if (cmd) return { command: cmd };
|
||||||
|
return rawInput;
|
||||||
|
}
|
||||||
|
|
||||||
function parseUserMessage(messageRaw: unknown, ts: string): TranscriptEntry[] {
|
function parseUserMessage(messageRaw: unknown, ts: string): TranscriptEntry[] {
|
||||||
if (typeof messageRaw === "string") {
|
if (typeof messageRaw === "string") {
|
||||||
const text = messageRaw.trim();
|
const text = messageRaw.trim();
|
||||||
@@ -92,11 +132,17 @@ function parseAssistantMessage(messageRaw: unknown, ts: string): TranscriptEntry
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "tool_call") {
|
if (type === "tool_call") {
|
||||||
|
const name = asString(part.name, asString(part.tool, "tool"));
|
||||||
|
const rawInput = part.input ?? part.arguments ?? part.args ?? {};
|
||||||
|
const input =
|
||||||
|
name === "shellToolCall" || name === "shell"
|
||||||
|
? compactShellToolInput(rawInput, asRecord(rawInput) ?? undefined)
|
||||||
|
: rawInput;
|
||||||
entries.push({
|
entries.push({
|
||||||
kind: "tool_call",
|
kind: "tool_call",
|
||||||
ts,
|
ts,
|
||||||
name: asString(part.name, asString(part.tool, "tool")),
|
name,
|
||||||
input: part.input ?? part.arguments ?? part.args ?? {},
|
input,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -108,11 +154,11 @@ function parseAssistantMessage(messageRaw: unknown, ts: string): TranscriptEntry
|
|||||||
asString(part.call_id) ||
|
asString(part.call_id) ||
|
||||||
asString(part.id) ||
|
asString(part.id) ||
|
||||||
"tool_result";
|
"tool_result";
|
||||||
|
const rawOutput = part.output ?? part.result ?? part.text;
|
||||||
const contentText =
|
const contentText =
|
||||||
asString(part.output) ||
|
typeof rawOutput === "object" && rawOutput !== null
|
||||||
asString(part.text) ||
|
? formatShellToolResultForLog(rawOutput)
|
||||||
asString(part.result) ||
|
: asString(rawOutput) || stringifyUnknown(rawOutput);
|
||||||
stringifyUnknown(part.output ?? part.result ?? part.text ?? part);
|
|
||||||
const isError = part.is_error === true || asString(part.status).toLowerCase() === "error";
|
const isError = part.is_error === true || asString(part.status).toLowerCase() === "error";
|
||||||
entries.push({
|
entries.push({
|
||||||
kind: "tool_result",
|
kind: "tool_result",
|
||||||
@@ -144,7 +190,9 @@ function parseCursorToolCallEvent(event: Record<string, unknown>, ts: string): T
|
|||||||
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }];
|
return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }];
|
||||||
}
|
}
|
||||||
const payload = asRecord(toolCall[toolName]) ?? {};
|
const payload = asRecord(toolCall[toolName]) ?? {};
|
||||||
const input = payload.args ?? asRecord(payload.function)?.arguments ?? {};
|
const rawInput = payload.args ?? asRecord(payload.function)?.arguments ?? payload;
|
||||||
|
const isShellTool = toolName === "shellToolCall" || toolName === "shell";
|
||||||
|
const input = isShellTool ? compactShellToolInput(rawInput, payload) : rawInput;
|
||||||
|
|
||||||
if (subtype === "started" || subtype === "start") {
|
if (subtype === "started" || subtype === "start") {
|
||||||
return [{
|
return [{
|
||||||
@@ -169,11 +217,17 @@ function parseCursorToolCallEvent(event: Record<string, unknown>, ts: string): T
|
|||||||
asString(payload.status).toLowerCase() === "failed" ||
|
asString(payload.status).toLowerCase() === "failed" ||
|
||||||
asString(payload.status).toLowerCase() === "cancelled" ||
|
asString(payload.status).toLowerCase() === "cancelled" ||
|
||||||
payload.error !== undefined;
|
payload.error !== undefined;
|
||||||
|
const content =
|
||||||
|
result !== undefined
|
||||||
|
? isShellTool
|
||||||
|
? formatShellToolResultForLog(result)
|
||||||
|
: stringifyUnknown(result)
|
||||||
|
: `${toolName} completed`;
|
||||||
return [{
|
return [{
|
||||||
kind: "tool_result",
|
kind: "tool_result",
|
||||||
ts,
|
ts,
|
||||||
toolUseId: callId,
|
toolUseId: callId,
|
||||||
content: result !== undefined ? stringifyUnknown(result) : `${toolName} completed`,
|
content,
|
||||||
isError,
|
isError,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,71 @@ describe("cursor ui stdout parser", () => {
|
|||||||
).toEqual([{ kind: "thinking", ts, text: "streamed" }]);
|
).toEqual([{ kind: "thinking", ts, text: "streamed" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("compacts shellToolCall and shell tool result for run log", () => {
|
||||||
|
const ts = "2026-03-05T00:00:00.000Z";
|
||||||
|
const longCommand = "curl -s -X POST \"$PAPERCLIP_API_URL/api/issues/abc/checkout\" -H \"Authorization: Bearer $PAPERCLIP_API_KEY\"";
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseCursorStdoutLine(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "tool_call",
|
||||||
|
subtype: "started",
|
||||||
|
call_id: "call_shell_1",
|
||||||
|
tool_call: {
|
||||||
|
shellToolCall: {
|
||||||
|
command: longCommand,
|
||||||
|
workingDirectory: "/tmp",
|
||||||
|
timeout: 30000,
|
||||||
|
toolCallId: "tool_xyz",
|
||||||
|
simpleCommands: ["curl"],
|
||||||
|
parsingResult: { parsingFailed: false, executableCommands: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ts,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "tool_call",
|
||||||
|
ts,
|
||||||
|
name: "shellToolCall",
|
||||||
|
input: { command: longCommand },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
parseCursorStdoutLine(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "tool_call",
|
||||||
|
subtype: "completed",
|
||||||
|
call_id: "call_shell_1",
|
||||||
|
tool_call: {
|
||||||
|
shellToolCall: {
|
||||||
|
result: {
|
||||||
|
success: {
|
||||||
|
command: longCommand,
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: '{"id":"abc","status":"in_progress"}',
|
||||||
|
stderr: "",
|
||||||
|
executionTime: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ts,
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "tool_result",
|
||||||
|
ts,
|
||||||
|
toolUseId: "call_shell_1",
|
||||||
|
content: "exit 0\n<stdout>\n{\"id\":\"abc\",\"status\":\"in_progress\"}",
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("parses user, top-level thinking, and top-level tool_call events", () => {
|
it("parses user, top-level thinking, and top-level tool_call events", () => {
|
||||||
const ts = "2026-03-05T00:00:00.000Z";
|
const ts = "2026-03-05T00:00:00.000Z";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user