From 1bb30e11878e450f59f16e4d82c656a1b9aca68f Mon Sep 17 00:00:00 2001 From: bmethod Date: Mon, 17 Aug 2026 10:57:21 -0700 Subject: [PATCH 1/2] fix(pi-agents-tmux): the timeline reads the nested toolCall carrier for name, id, status, and failure Claude-Session: https://claude.ai/code/session_012epxJEzGqT7q3qcFhdZUt5 --- .../subagent/transcript-timeline.ts | 19 +++++++++++-------- .../tests/transcript-timeline.test.ts | 10 ++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts b/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts index 964afc9db..8f05649a4 100644 --- a/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts +++ b/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts @@ -222,10 +222,12 @@ export function formatTranscriptForDisplay(raw: string, options?: { droppedEvent break; } case "tool_execution_start": { - const name = stringValue(event.toolName ?? event.tool_name) ?? stringValue(event.name) ?? "tool"; - const target = primaryToolArgument(event.args ?? event.arguments ?? event.input ?? event.params); + // Pi also emits the nested carrier shape { toolCall: { name, id, … } }. + const call = (event.toolCall ?? event.tool_call) as Record | undefined; + const name = stringValue(event.toolName ?? event.tool_name) ?? stringValue(event.name) ?? stringValue(call?.name) ?? "tool"; + const target = primaryToolArgument(event.args ?? event.arguments ?? event.input ?? event.params ?? call?.arguments ?? call?.input); const label = target ? `tool ${name} (${oneLine(target, 60)})` : `tool ${name}`; - const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id) ?? `name:${name}`; + const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id ?? call?.id) ?? `name:${name}`; const row = push(stamp, label, "running"); const queue = openTools.get(id) ?? []; queue.push({ label, row, startedAtMs: atMs }); @@ -236,12 +238,13 @@ export function formatTranscriptForDisplay(raw: string, options?: { droppedEvent // Folded into the paired tool row; the full payload stays in the file. break; case "tool_execution_end": { - const name = stringValue(event.toolName ?? event.tool_name) ?? stringValue(event.name) ?? "tool"; - const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id) ?? `name:${name}`; + const call = (event.toolCall ?? event.tool_call) as Record | undefined; + const name = stringValue(event.toolName ?? event.tool_name) ?? stringValue(event.name) ?? stringValue(call?.name) ?? "tool"; + const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id ?? call?.id) ?? `name:${name}`; const open = openTools.get(id)?.shift(); - const failed = event.isError === true || event.is_error === true || stringValue(event.status) === "error"; - const status = stringValue(event.status) ?? (failed ? "error" : "ok"); - const resultSize = payloadByteSize(event.result ?? event.output ?? event.content); + const failed = event.isError === true || event.is_error === true || call?.isError === true || stringValue(event.status ?? call?.status) === "error"; + const status = stringValue(event.status ?? call?.status) ?? (failed ? "error" : "ok"); + const resultSize = payloadByteSize(event.result ?? event.output ?? event.content ?? call?.result); const duration = open?.startedAtMs !== undefined && atMs !== undefined ? formatToolDuration(atMs - open.startedAtMs) : undefined; const detail = [status, duration, resultSize ? formatByteSize(resultSize) : undefined].filter(Boolean).join(" · "); if (open) { diff --git a/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts b/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts index 2c7092872..f1992a097 100644 --- a/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts +++ b/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts @@ -89,6 +89,16 @@ test("toolUseId variants pair out-of-order same-named calls correctly", () => { assert.match(rows[1]!, /^ .*tool bash \(second\) · ok · 1\.0s/); }); +test("a nested toolCall end pairs with its start and carries the failure", () => { + const out = formatTranscriptForDisplay([ + stream(0, { args: { command: "edit it" }, toolCallId: "tcl_3", toolName: "edit", type: "tool_execution_start" }), + stream(2, { error: "boom", toolCall: { id: "tcl_3", isError: true, name: "Edit", status: "error" }, type: "tool_execution_end" }), + ].join("\n")); + const rows = out.split("\n"); + assert.equal(rows.length, 1); + assert.match(rows[0]!, /^✖.*tool edit \(edit it\) · error · 2\.0s/); +}); + test("id-less same-named tool calls pair first-started-first-ended", () => { const out = formatTranscriptForDisplay([ stream(0, { args: { command: "first" }, toolName: "bash", type: "tool_execution_start" }), From 62168ff4b196e1b21abbce5227322cc1f95cd97d Mon Sep 17 00:00:00 2001 From: bmethod Date: Mon, 17 Aug 2026 11:21:23 -0700 Subject: [PATCH 2/2] fix(pi-agents-tmux): nested is_error read, case-insensitive id-less pairing, 2.8.5 Claude-Session: https://claude.ai/code/session_012epxJEzGqT7q3qcFhdZUt5 --- pi-extensions/pi-agents-tmux/CHANGELOG.md | 4 ++++ .../subagent/transcript-timeline.ts | 6 +++--- pi-extensions/pi-agents-tmux/package.json | 2 +- .../tests/transcript-timeline.test.ts | 20 +++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pi-extensions/pi-agents-tmux/CHANGELOG.md b/pi-extensions/pi-agents-tmux/CHANGELOG.md index 3ebb8ef57..1c9e7a15b 100644 --- a/pi-extensions/pi-agents-tmux/CHANGELOG.md +++ b/pi-extensions/pi-agents-tmux/CHANGELOG.md @@ -2,6 +2,10 @@ ## Consumer-impacting changes +### 2.8.5 + +- The Transcript timeline reads the nested `toolCall`/`tool_call` end-event carrier — name, id, status, `isError`/`is_error`, arguments, result (the pi-session-bridge event-sanitizer shape). Previously a nested end fell back to `name:tool`, leaving its start marked `✖ no result recorded` beside a separate neutral `tool tool · ok` row, and a nested snake-case error flag rendered as success. Id-less fallback pairing is case-insensitive on the tool name. + ### 2.8.4 - The Agents popup Transcript tab renders an event timeline instead of raw JSONL (vstack VST-327). One row per event — elapsed stamp, kind, capped one-line detail — covering input, assistant text/thinking previews, turn boundaries, and lifecycle records; a tool call collapses into a single row pairing start with result (name, primary argument, status, duration, result size), a tool-call-only assistant message renders as its calls, and errors/aborts/non-zero exits are `✖`-marked rows. No event type falls through to a raw JSONL line: unrecognized types render as their type and size. The tail read grew 24 KB → 256 KB, is byte-based and streaming (only the final window is materialized; the dropped prefix is newline-counted through a fixed buffer), cuts on a line boundary only, and states how many earlier events were dropped. Decoded text is scrubbed of C0/C1 terminal controls before rendering — JSON.parse would otherwise revive escaped OSC/CSI sequences the raw view kept inert. Native pane-session `message` records render as conversation content, and only trouble-shaped lifecycle records (`abort_*`, `*_failed`, escalations) carry the failure tone. New `e` key in the trace viewer opens the item's file in `$VISUAL`/`$EDITOR` (own tmux window), listed in the footer hint. diff --git a/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts b/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts index 8f05649a4..1a4e9f5f0 100644 --- a/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts +++ b/pi-extensions/pi-agents-tmux/extensions/subagent/transcript-timeline.ts @@ -227,7 +227,7 @@ export function formatTranscriptForDisplay(raw: string, options?: { droppedEvent const name = stringValue(event.toolName ?? event.tool_name) ?? stringValue(event.name) ?? stringValue(call?.name) ?? "tool"; const target = primaryToolArgument(event.args ?? event.arguments ?? event.input ?? event.params ?? call?.arguments ?? call?.input); const label = target ? `tool ${name} (${oneLine(target, 60)})` : `tool ${name}`; - const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id ?? call?.id) ?? `name:${name}`; + const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id ?? call?.id) ?? `name:${name.toLowerCase()}`; const row = push(stamp, label, "running"); const queue = openTools.get(id) ?? []; queue.push({ label, row, startedAtMs: atMs }); @@ -240,9 +240,9 @@ export function formatTranscriptForDisplay(raw: string, options?: { droppedEvent case "tool_execution_end": { const call = (event.toolCall ?? event.tool_call) as Record | undefined; const name = stringValue(event.toolName ?? event.tool_name) ?? stringValue(event.name) ?? stringValue(call?.name) ?? "tool"; - const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id ?? call?.id) ?? `name:${name}`; + const id = stringValue(event.toolCallId ?? event.tool_call_id ?? event.toolUseId ?? event.tool_use_id ?? call?.id) ?? `name:${name.toLowerCase()}`; const open = openTools.get(id)?.shift(); - const failed = event.isError === true || event.is_error === true || call?.isError === true || stringValue(event.status ?? call?.status) === "error"; + const failed = event.isError === true || event.is_error === true || call?.isError === true || call?.is_error === true || stringValue(event.status ?? call?.status) === "error"; const status = stringValue(event.status ?? call?.status) ?? (failed ? "error" : "ok"); const resultSize = payloadByteSize(event.result ?? event.output ?? event.content ?? call?.result); const duration = open?.startedAtMs !== undefined && atMs !== undefined ? formatToolDuration(atMs - open.startedAtMs) : undefined; diff --git a/pi-extensions/pi-agents-tmux/package.json b/pi-extensions/pi-agents-tmux/package.json index c25b01ed6..7b0182d61 100644 --- a/pi-extensions/pi-agents-tmux/package.json +++ b/pi-extensions/pi-agents-tmux/package.json @@ -1,6 +1,6 @@ { "name": "@vanillagreen/pi-agents-tmux", - "version": "2.8.4", + "version": "2.8.5", "description": "Pi extension for delegating work to project or user agents, including persistent tmux agent panes.", "license": "MIT", "keywords": [ diff --git a/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts b/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts index f1992a097..ca3932925 100644 --- a/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts +++ b/pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts @@ -99,6 +99,26 @@ test("a nested toolCall end pairs with its start and carries the failure", () => assert.match(rows[0]!, /^✖.*tool edit \(edit it\) · error · 2\.0s/); }); +test("a snake_case nested carrier pairs by id and carries is_error", () => { + const out = formatTranscriptForDisplay([ + stream(0, { args: { command: "risky" }, toolCallId: "x", toolName: "bash", type: "tool_execution_start" }), + stream(2, { tool_call: { id: "x", is_error: true }, type: "tool_execution_end" }), + ].join("\n")); + const rows = out.split("\n"); + assert.equal(rows.length, 1); + assert.match(rows[0]!, /^✖.*tool bash \(risky\) · error · 2\.0s/); +}); + +test("id-less pairing is case-insensitive on the tool name", () => { + const out = formatTranscriptForDisplay([ + stream(0, { args: { command: "go" }, toolName: "edit", type: "tool_execution_start" }), + stream(3, { toolCall: { name: "Edit", status: "ok" }, type: "tool_execution_end" }), + ].join("\n")); + const rows = out.split("\n"); + assert.equal(rows.length, 1); + assert.match(rows[0]!, /^ .*tool edit \(go\) · ok · 3\.0s/); +}); + test("id-less same-named tool calls pair first-started-first-ended", () => { const out = formatTranscriptForDisplay([ stream(0, { args: { command: "first" }, toolName: "bash", type: "tool_execution_start" }),