feat(chat): name tool calls by what they did - #117
Merged
Conversation
Timeline rows read "Tool call: Read: {"file_path":...}" or "File change:
TaskCreate: {...}" because titles come from a 7-value item type table per
adapter, and the payload projection dropped the tool's own name for every
non-MCP call. Three adapters also disagreed: "Command run" (Claude) vs "Ran
command" (Codex/ACP) for the same action.
Adds a shared render-time vocabulary keyed on tool name, then item type, then
the provider's own label, used by the timeline, the agents panel, the approval
card and mobile. The projection now ships data.toolName plus a length-capped
argument allowlist, so existing history renames too.
Titles, labels and details are never rewritten: they are the identity tool rows
collapse on, and the server drops superseded rows using the same triple.
incognitojam
enabled auto-merge (squash)
August 13, 2026 12:48
yngatech-nightly Bot
pushed a commit
that referenced
this pull request
Aug 13, 2026
> [!NOTE] > Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers. ## The problem Every tool row's title came from a 7-value item-type table, one per adapter, so a `Read`, a `Skill`, a `ToolSearch` and a `SendMessage` all rendered as **Tool call** followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread. `TaskCreate` classified as a file write, so it rendered as **File change** with an edit pencil. The clients could not do better on their own: `projectActivityPayload` rebuilt `payload.data` from an allowlist that kept `toolName` only for `mcp_tool_call`, so for every other call the row genuinely did not know which tool ran. None of this is a regression — `titleForTool` and `summarizeToolRequest` have been unchanged since the Claude adapter landed. ## Before / after | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/before.png" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/after.png" width="460"> | ## How it works `packages/shared/src/toolRowPresentation.ts` resolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect. - Well-typed actions share one verb across providers: **Ran command**, **Edited file** / **Edited 3 files**, **Viewed image**. - The unknown-tool bucket names itself: **Read**, **Skill**, **ToolSearch**, **SendMessage**, **Monitor**. New tools stay correct with no table entry. - Three overrides where the tool name would mislead: **Created task** / **Updated task** (they arrive typed as file changes) and **Saved memory** / **Updated memory**. - A title the provider chose for a specific call (an ACP tool's own name, Codex's `server · tool`) outranks anything derived here. The projection now ships `data.toolName` plus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively. ## Notes for review - **Titles, labels and details are never rewritten.** They are the identity tool rows collapse on, and the server's `toolLifecycleIdentity` drops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only. - **Wire size.** The argument allowlist is keyed *and* value-capped at 200 characters, with `content`, `new_string`, `old_string`, `prompt` and message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shipped `input` uncapped. - **Icon ordering fix.** `image_view` is now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil. - **Deliberately untouched:** `classifyToolItemType`. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed #115 for the approval-routing half of that bug, which is not cosmetic, and #116 for the uncapped MCP input, which is upstream's. ## Verification - `vp test run` across the touched packages: 1,035 passing, plus the full `apps/server` suite at 2,563. - `vp run -r typecheck` clean; targeted lint and `vp fmt --check` clean. - Replayed 4,000 real persisted rows through the new naming to confirm every category renames and Codex's already-good titles do not move. - Screenshots above are a live client against seeded fixtures. --- Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot
pushed a commit
that referenced
this pull request
Aug 13, 2026
> [!NOTE] > Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers. ## The problem Every tool row's title came from a 7-value item-type table, one per adapter, so a `Read`, a `Skill`, a `ToolSearch` and a `SendMessage` all rendered as **Tool call** followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread. `TaskCreate` classified as a file write, so it rendered as **File change** with an edit pencil. The clients could not do better on their own: `projectActivityPayload` rebuilt `payload.data` from an allowlist that kept `toolName` only for `mcp_tool_call`, so for every other call the row genuinely did not know which tool ran. None of this is a regression — `titleForTool` and `summarizeToolRequest` have been unchanged since the Claude adapter landed. ## Before / after | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/before.png" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/after.png" width="460"> | ## How it works `packages/shared/src/toolRowPresentation.ts` resolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect. - Well-typed actions share one verb across providers: **Ran command**, **Edited file** / **Edited 3 files**, **Viewed image**. - The unknown-tool bucket names itself: **Read**, **Skill**, **ToolSearch**, **SendMessage**, **Monitor**. New tools stay correct with no table entry. - Three overrides where the tool name would mislead: **Created task** / **Updated task** (they arrive typed as file changes) and **Saved memory** / **Updated memory**. - A title the provider chose for a specific call (an ACP tool's own name, Codex's `server · tool`) outranks anything derived here. The projection now ships `data.toolName` plus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively. ## Notes for review - **Titles, labels and details are never rewritten.** They are the identity tool rows collapse on, and the server's `toolLifecycleIdentity` drops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only. - **Wire size.** The argument allowlist is keyed *and* value-capped at 200 characters, with `content`, `new_string`, `old_string`, `prompt` and message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shipped `input` uncapped. - **Icon ordering fix.** `image_view` is now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil. - **Deliberately untouched:** `classifyToolItemType`. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed #115 for the approval-routing half of that bug, which is not cosmetic, and #116 for the uncapped MCP input, which is upstream's. ## Verification - `vp test run` across the touched packages: 1,035 passing, plus the full `apps/server` suite at 2,563. - `vp run -r typecheck` clean; targeted lint and `vp fmt --check` clean. - Replayed 4,000 real persisted rows through the new naming to confirm every category renames and Codex's already-good titles do not move. - Screenshots above are a live client against seeded fixtures. --- Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot
pushed a commit
that referenced
this pull request
Aug 14, 2026
> [!NOTE] > Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers. ## The problem Every tool row's title came from a 7-value item-type table, one per adapter, so a `Read`, a `Skill`, a `ToolSearch` and a `SendMessage` all rendered as **Tool call** followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread. `TaskCreate` classified as a file write, so it rendered as **File change** with an edit pencil. The clients could not do better on their own: `projectActivityPayload` rebuilt `payload.data` from an allowlist that kept `toolName` only for `mcp_tool_call`, so for every other call the row genuinely did not know which tool ran. None of this is a regression — `titleForTool` and `summarizeToolRequest` have been unchanged since the Claude adapter landed. ## Before / after | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/before.png" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/after.png" width="460"> | ## How it works `packages/shared/src/toolRowPresentation.ts` resolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect. - Well-typed actions share one verb across providers: **Ran command**, **Edited file** / **Edited 3 files**, **Viewed image**. - The unknown-tool bucket names itself: **Read**, **Skill**, **ToolSearch**, **SendMessage**, **Monitor**. New tools stay correct with no table entry. - Three overrides where the tool name would mislead: **Created task** / **Updated task** (they arrive typed as file changes) and **Saved memory** / **Updated memory**. - A title the provider chose for a specific call (an ACP tool's own name, Codex's `server · tool`) outranks anything derived here. The projection now ships `data.toolName` plus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively. ## Notes for review - **Titles, labels and details are never rewritten.** They are the identity tool rows collapse on, and the server's `toolLifecycleIdentity` drops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only. - **Wire size.** The argument allowlist is keyed *and* value-capped at 200 characters, with `content`, `new_string`, `old_string`, `prompt` and message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shipped `input` uncapped. - **Icon ordering fix.** `image_view` is now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil. - **Deliberately untouched:** `classifyToolItemType`. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed #115 for the approval-routing half of that bug, which is not cosmetic, and #116 for the uncapped MCP input, which is upstream's. ## Verification - `vp test run` across the touched packages: 1,035 passing, plus the full `apps/server` suite at 2,563. - `vp run -r typecheck` clean; targeted lint and `vp fmt --check` clean. - Replayed 4,000 real persisted rows through the new naming to confirm every category renames and Codex's already-good titles do not move. - Screenshots above are a live client against seeded fixtures. --- Written by an agent (Claude Code, claude-opus-5).
yngatech-nightly Bot
pushed a commit
that referenced
this pull request
Aug 14, 2026
> [!NOTE] > Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers. ## The problem Every tool row's title came from a 7-value item-type table, one per adapter, so a `Read`, a `Skill`, a `ToolSearch` and a `SendMessage` all rendered as **Tool call** followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread. `TaskCreate` classified as a file write, so it rendered as **File change** with an edit pencil. The clients could not do better on their own: `projectActivityPayload` rebuilt `payload.data` from an allowlist that kept `toolName` only for `mcp_tool_call`, so for every other call the row genuinely did not know which tool ran. None of this is a regression — `titleForTool` and `summarizeToolRequest` have been unchanged since the Claude adapter landed. ## Before / after | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/before.png" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/after.png" width="460"> | ## How it works `packages/shared/src/toolRowPresentation.ts` resolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect. - Well-typed actions share one verb across providers: **Ran command**, **Edited file** / **Edited 3 files**, **Viewed image**. - The unknown-tool bucket names itself: **Read**, **Skill**, **ToolSearch**, **SendMessage**, **Monitor**. New tools stay correct with no table entry. - Three overrides where the tool name would mislead: **Created task** / **Updated task** (they arrive typed as file changes) and **Saved memory** / **Updated memory**. - A title the provider chose for a specific call (an ACP tool's own name, Codex's `server · tool`) outranks anything derived here. The projection now ships `data.toolName` plus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively. ## Notes for review - **Titles, labels and details are never rewritten.** They are the identity tool rows collapse on, and the server's `toolLifecycleIdentity` drops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only. - **Wire size.** The argument allowlist is keyed *and* value-capped at 200 characters, with `content`, `new_string`, `old_string`, `prompt` and message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shipped `input` uncapped. - **Icon ordering fix.** `image_view` is now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil. - **Deliberately untouched:** `classifyToolItemType`. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed #115 for the approval-routing half of that bug, which is not cosmetic, and #116 for the uncapped MCP input, which is upstream's. ## Verification - `vp test run` across the touched packages: 1,035 passing, plus the full `apps/server` suite at 2,563. - `vp run -r typecheck` clean; targeted lint and `vp fmt --check` clean. - Replayed 4,000 real persisted rows through the new naming to confirm every category renames and Codex's already-good titles do not move. - Screenshots above are a live client against seeded fixtures. --- Written by an agent (Claude Code, claude-opus-5).
incognitojam
added a commit
that referenced
this pull request
Aug 15, 2026
> [!NOTE] > Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers. ## The problem Every tool row's title came from a 7-value item-type table, one per adapter, so a `Read`, a `Skill`, a `ToolSearch` and a `SendMessage` all rendered as **Tool call** followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread. `TaskCreate` classified as a file write, so it rendered as **File change** with an edit pencil. The clients could not do better on their own: `projectActivityPayload` rebuilt `payload.data` from an allowlist that kept `toolName` only for `mcp_tool_call`, so for every other call the row genuinely did not know which tool ran. None of this is a regression — `titleForTool` and `summarizeToolRequest` have been unchanged since the Claude adapter landed. ## Before / after | Before | After | | --- | --- | | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/before.png" width="460"> | <img src="https://raw.githubusercontent.com/yngatech/t3code/assets/pr-tool-call-display/after.png" width="460"> | ## How it works `packages/shared/src/toolRowPresentation.ts` resolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect. - Well-typed actions share one verb across providers: **Ran command**, **Edited file** / **Edited 3 files**, **Viewed image**. - The unknown-tool bucket names itself: **Read**, **Skill**, **ToolSearch**, **SendMessage**, **Monitor**. New tools stay correct with no table entry. - Three overrides where the tool name would mislead: **Created task** / **Updated task** (they arrive typed as file changes) and **Saved memory** / **Updated memory**. - A title the provider chose for a specific call (an ACP tool's own name, Codex's `server · tool`) outranks anything derived here. The projection now ships `data.toolName` plus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively. ## Notes for review - **Titles, labels and details are never rewritten.** They are the identity tool rows collapse on, and the server's `toolLifecycleIdentity` drops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only. - **Wire size.** The argument allowlist is keyed *and* value-capped at 200 characters, with `content`, `new_string`, `old_string`, `prompt` and message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shipped `input` uncapped. - **Icon ordering fix.** `image_view` is now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil. - **Deliberately untouched:** `classifyToolItemType`. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed #115 for the approval-routing half of that bug, which is not cosmetic, and #116 for the uncapped MCP input, which is upstream's. ## Verification - `vp test run` across the touched packages: 1,035 passing, plus the full `apps/server` suite at 2,563. - `vp run -r typecheck` clean; targeted lint and `vp fmt --check` clean. - Replayed 4,000 real persisted rows through the new naming to confirm every category renames and Codex's already-good titles do not move. - Screenshots above are a live client against seeded fixtures. --- Written by an agent (Claude Code, claude-opus-5).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Tool rows in the timeline said "Tool call" and printed raw JSON. They now say what the tool did, with a readable argument, consistently across Claude, Codex and ACP providers.
The problem
Every tool row's title came from a 7-value item-type table, one per adapter, so a
Read, aSkill, aToolSearchand aSendMessageall rendered as Tool call followed by their serialized input. The three adapters also disagreed with each other: the same action read "Command run" from Claude and "Ran command" from Codex in the same thread.TaskCreateclassified as a file write, so it rendered as File change with an edit pencil.The clients could not do better on their own:
projectActivityPayloadrebuiltpayload.datafrom an allowlist that kepttoolNameonly formcp_tool_call, so for every other call the row genuinely did not know which tool ran.None of this is a regression —
titleForToolandsummarizeToolRequesthave been unchanged since the Claude adapter landed.Before / after
How it works
packages/shared/src/toolRowPresentation.tsresolves a row in layers — tool name, then item type, then the provider's own label — and returns a heading plus a typed argument that the caller formats. The timeline, the agents panel, the approval card and mobile all read from it, so the vocabulary converges instead of gaining a fourth dialect.server · tool) outranks anything derived here.The projection now ships
data.toolNameplus a short, length-capped argument allowlist. Because the projection runs at read time over persisted payloads, existing threads rename retroactively.Notes for review
toolLifecycleIdentitydrops superseded rows using the same triple; changing one side without the other would silently stop update rows folding into their completions. Naming happens at render time only.content,new_string,old_string,promptand message bodies deliberately excluded; a test asserts a 50 KB edit cannot survive projection. MCP keeps its arbitrary argument names (an allowlist would blank every server's expanded row) and caps values instead — that path previously shippedinputuncapped.image_viewis now tested before the file-change branch; a viewed image discovers a path, which was stamping a read-only row with an edit pencil.classifyToolItemType. Its misclassification is corrected at display time so the adapter stays byte-identical to upstream. Filed Approval routing misclassifies TaskCreate as a file change #115 for the approval-routing half of that bug, which is not cosmetic, and Upstreamable: MCP tool arguments ship uncapped in thread payloads #116 for the uncapped MCP input, which is upstream's.Verification
vp test runacross the touched packages: 1,035 passing, plus the fullapps/serversuite at 2,563.vp run -r typecheckclean; targeted lint andvp fmt --checkclean.Written by an agent (Claude Code, claude-opus-5).