Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pi-extensions/pi-agents-tmux/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | 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.toLowerCase()}`;
const row = push(stamp, label, "running");
const queue = openTools.get(id) ?? [];
queue.push({ label, row, startedAtMs: atMs });
Expand All @@ -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<string, unknown> | undefined;
Comment thread
bmethod marked this conversation as resolved.
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.toLowerCase()}`;
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 || 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;
const detail = [status, duration, resultSize ? formatByteSize(resultSize) : undefined].filter(Boolean).join(" · ");
if (open) {
Expand Down
2 changes: 1 addition & 1 deletion pi-extensions/pi-agents-tmux/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
30 changes: 30 additions & 0 deletions pi-extensions/pi-agents-tmux/tests/transcript-timeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,36 @@ 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("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" }),
Expand Down
Loading