Tool group summary
A folded run of tool calls in the chat transcript is headed by one line. This page defines what that line says: a short account of what the calls did, built on the client from the tool calls already in the transcript, with a +N −M badge when the run changed files. It also covers whether a turn-level summary is worth adding.
Where the row comes from
assistant-bubble.tsx splits a turn's ordered blocks into segments. Each text block stands alone. Each run of consecutive thinking and tool blocks becomes one ExecutionStrip from execution-strip.tsx, and summarizeToolGroup() writes its header. Older records without ordered blocks go through LegacyRuntimeTrace, which feeds its calls to the same function. The collapsed row is a single 28px line, as the chat-turn visual spec requires.
Data each call carries
| Field | Source | Use |
|---|---|---|
tool | block | Category through presentTool() in tool-presentation.ts; this is the same categorisation the timeline rows use. |
input | block, full JSON of the call arguments | Paths (file_path, path), commands, search patterns, URLs. edit carries old_string/new_string, write carries content, and apply_patch carries the whole patch. |
result, is_error, outcome | block | Success or failure. The edit result says (N replacements), and the apply_patch result says Added, Updated or Deleted. |
| running | msg.tools[].status | Which call is in progress. |
turnFiles | assistant message, written at finalize from the mutation journal | Authoritative per-file op (create/modify/delete) and added/removed for the whole turn. It is aggregated per path, has no tool-call id, and only exists once the turn settles. |
So per-group stats cannot be read straight from turnFiles when two groups in a turn touch the same file. They can be derived from the arguments, and turnFiles fills the gaps.
Summary grammar
Phrases
Calls in a group are bucketed by kind. Each bucket yields one phrase, placed where its first call occurred, so the line reads in the order things happened. The first phrase is capitalised. Phrases are joined with ", " (Chinese ","). Thinking gives "Thought" only when the group has nothing else; otherwise it is left out, because every turn thinks.
| Kind (tools) | One | Several | Running |
|---|---|---|---|
Command (bash, process, terminal_use, execute_code) | Ran a command | Ran 3 commands | Running a command… |
Created file (write to a new path, apply_patch Add File) | Created pill-shadow.html | Created 2 files | Creating pill-shadow.html… |
Edited file (edit, write over an existing file, apply_patch Update) | Edited app.tsx | Edited 4 files | Editing app.tsx… |
Deleted file (apply_patch Delete File) | Deleted old.css | Deleted 2 files | Deleting old.css… |
Read (read, pdf) | Read package.json | Read 3 files | Reading package.json… |
List (list) | Listed a folder | Listed 2 folders | Listing a folder… |
Code search (grep, glob, semble_*, lsp_*) | Searched for "tl-toggle" | Ran 3 searches | Searching for "tl-toggle"… |
Web search (web_search) | Searched the web | Searched the web 2 times | Searching the web… |
Fetch (web_fetch) | Fetched docs.anthropic.com | Fetched 3 pages | Fetching docs.anthropic.com… |
Browser (web_use, agent_browser, playwright_browser) | Operated the browser | Operating the browser… | |
Sub-agent (agent, task, send_message) | Ran sub-agent Reviewer | Ran 2 sub-agents | Running sub-agent Reviewer… |
Image (image_generate / image_analyze) | Generated an image | Generated 2 images | Generating an image… |
Todos (todo_*) | Updated todos | Updating todos… | |
Question (ask_user_question) | Asked a question | Asking a question… | |
Memory (memory_*) | Accessed memory | Accessing memory… | |
| Program / skill | Ran program deploy, Used skill pdf | Ran 2 programs | Running program deploy… |
| Anything else (MCP, custom) | Used linear_search (×2 adds " 2 times") | Used linear_search and jira_create; Used 3 tools | Using linear_search… |
Names: one distinct file shows its basename. A command is never named in the header, because commands are long and the expanded row already shows them. A search pattern is quoted and cut to 24 characters. A fetch shows the host only. A bucket names its single target only when it has exactly one distinct target, so "Read a.ts" turns into "Read 2 files" once a second file appears. Repeated calls on the same file count once.
Numbers: English uses "a"/"an" for one and digits for two or more. Chinese always uses digits with a measure word (1 条命令, 3 个文件, 2 次).
Failures: if any call in the group failed, a muted red "· 1 failed" follows the label. A failed call still counts in its bucket, and failed file writes add nothing to the badge.
Running state
While the group is the live part of a streaming turn, completed buckets keep the past tense and the bucket of the call in progress switches to the present tense with a trailing "…". It moves to the end of the line, where the eye expects the newest step, for example "Read 2 files, running a command…". The shimmer stays. When the call finishes, the phrase settles back to the past tense in its first-occurrence position. If the group only has thinking so far, the label is "Thinking…".
Truncation
On screen the label is one line: the label span has min-width:0 with a CSS ellipsis, and the failure note, badge and chevron are flex:none, so they never drop off. There is no character budget, so the same label fits a narrow side pane and a wide window. The row has no hover tip of any kind: it is transcript content, not a control whose purpose needs explaining, and expanding it shows every call.
Change badge
The badge shows when at least one file-writing call in the group succeeded and every successful one has known counts. It sums those counts, shows zeros (+16 −0), and reuses the turn file card's .turn-files-diff pill. Counts per call:
apply_patch: count the+and-lines of the patch body. This is exact.edit: a line diff ofold_stringagainstnew_string, multiplied by the replacement count from the result. Above 2,000 lines on either side it falls back to the plain line counts.write: whenturnFilesmarks the path ascreate, it is+lines(content) −0. When the path ismodifyand every write to it in the turn is inside this group, it takes theturnFilesnumbers. Otherwise the count is unknown, and the badge waits; during streaming that usually means it appears when the turn settles.
This also decides create versus edit for write. Until turnFiles exists, a write counts as "Created" only if the turn has not read or edited that path before. When turnFiles says modify, it becomes "Edited".
Strings
Every phrase goes through the existing text(en, zh) from useTranslation(). Each bucket has one entry: a past and a present form, each in a one-target and a count variant. Chinese uses 了 for the past and 正在 for the present: "运行了 1 条命令,创建了 pill-shadow.html" and "读取了 2 个文件,正在运行命令…".
Where the summary is built
All of it is built on the client from structured tool data, as a pure function next to tool-presentation.ts. No model call and no backend change is needed. One optional backend refinement would make write exact while streaming and give exact per-group counts when two groups touch one file: stamp the tool-call id on each mutation-journal entry and send per-call {path, op, added, removed} with the tool result.
Preview
Each pair contrasts a category-count header with this design's header for the same calls. Both are collapsed rows at the transcript's 15px size.
Turn-level summary
A single line at the top of a turn, such as "Read 9 files, edited 3 files, ran 6 commands · +40 −12", was considered.
- A deterministic version is free: the same function runs over every tool block in the turn. But the turn already ends with the file card's per-turn totals, each group already has its own header, and the reply text is the model's own account of the turn. A top line would repeat both.
- A model-written version ("Fixed the pill shadow and verified the build") would need an extra call per turn through the title-model path that session auto rename uses. It costs tokens and latency, can only appear after the turn ends, and sends the turn's content to a model a second time. It is also most likely to say the same thing as the reply.
The turn-level line is not built. Where a turn must collapse to one line elsewhere, such as the message minimap, a hover preview, or a sub-agent row in the parent turn, that surface calls the deterministic aggregate.
Implementation
- tool-group-summary.ts is a pure module.
summarizeToolGroup(blocks, {text, active, runningIds, spawnNames, turnFiles, turnBlocks})returns{label, failed, added, removed};addedandremovedare bothnullwhen there is no badge. It ownsSPAWNING_TOOL_NAMES, reusespresentTool()'s tones for tools it has no bucket for, and exportslineDiff()(LCS over lines after trimming the common head and tail, capped atLINE_DIFF_CAP= 2,000 lines) andparsePatch(). ExecutionStriptakeslabel: string | ToolGroupSummary. A plain string, as the workflow runtime card and the debugger panel pass, renders as before with no note or badge. The row renders the ellipsis label, the red failure note, the badge (the.turn-files-diff/.turn-files-statpill of the turn file card), the chevron, and the hover tip.AssistantBubblepasses each segment's blocks with the whole turn's blocks,msg.turnFiles, the running-call ids, and whether the segment is the live tail.- Legacy records go through the same function.
LegacyRuntimeTrace(agent calls stored without ordered blocks) turns each call into a tool block named after its function, so the header reads "Used extract_tables" rather than "Called 1 function", and it now shimmers while a call runs. Legacy sub-agent strips with no tool blocks pass only their spawn labels. - CSS in
app/styles/chat/execution-strip.css:.tl-labelellipsis,.tl-fail, andflex:noneon.tl-diffand.tl-chev. - Tests:
apps/web/tests/chat/tool-group-summary.test.mjscovers grammar per bucket, plurals in both languages, ordering, the running tense, failures, badge sources and unknown counts, apply_patch parsing, and the capped line diff.
Implementation status
Implemented as described. The optional backend refinement (tool-call ids on mutation-journal entries) is not built, so a write over an existing file shows its badge only once the turn settles.