Skip to content

feat(chat): name tool calls by what they did - #117

Merged
incognitojam merged 1 commit into
mainfrom
t3code/improve-tool-call-display
Aug 13, 2026
Merged

feat(chat): name tool calls by what they did#117
incognitojam merged 1 commit into
mainfrom
t3code/improve-tool-call-display

Conversation

@incognitojam

Copy link
Copy Markdown
Collaborator

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

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 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 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).

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.
@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 13, 2026
@incognitojam
incognitojam enabled auto-merge (squash) August 13, 2026 12:48
@incognitojam
incognitojam merged commit ed76312 into main Aug 13, 2026
11 checks passed
@incognitojam
incognitojam deleted the t3code/improve-tool-call-display branch August 13, 2026 12:54
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant