feat(frontend): Show readable summaries for tool activity in the playground - #6007
feat(frontend): Show readable summaries for tool activity in the playground#6007ashrafchowdury wants to merge 12 commits into
Conversation
The playground labelled each tool call with its raw wire name. Under Codex that
name is not a name — the runner records shell calls under the command itself and
file reads under an English sentence — and our own tools leaked through as
"Mcp.agenta tools.test run", since only Claude's `mcp__` wrapper was unwrapped.
Rows now read as plain sentences ("Tested the agent", "Ran a command") with the
command or filename beside them; the exact wire name, arguments and output stay
in the expander. The summary slot no longer spills raw JSON either.
Wording is derived rather than tabulated: a verb table plus a small product
glossary turn `verb_noun` names into both tenses, so a newly shipped platform op
or Composio action reads correctly with no code change. App names come from the
tool catalog instead of a hardcoded brand list.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds canonical tool display resolution, shared tool-row state and output helpers, catalog-aware activity rendering, input-aware approval text, and updated elicitation messaging. Tests cover platform, MCP, shell, file, search, connection, builtin, output, and failure cases. ChangesTool activity display
Estimated code review effort: 4 (Complex) | ~60 minutes Merge Risk: 🔵 Low · up to The change is mergeable with owner follow-up: the approval UI may include one call's query or arguments in an “always allow” label even though the permission applies to every use of that tool, which could mislead users about its scope. Sequence Diagram(s)sequenceDiagram
participant ToolEvent
participant resolveToolDisplay
participant ToolCatalog
participant ToolActivity
ToolEvent->>resolveToolDisplay: pass tool input and output
resolveToolDisplay->>ToolCatalog: resolve source and integration metadata
ToolCatalog-->>resolveToolDisplay: return catalog metadata
resolveToolDisplay-->>ToolActivity: return activity, kind, source, and detail
ToolActivity->>ToolActivity: apply row state and output summary
ToolActivity-->>ToolEvent: render readable activity text
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The row read "Searched issues" with a separate "GitHub" chip. Issue #5976 asks for "Searched GitHub issues", which reads as one thought instead of two. The app now modifies the object, and the article moves in front of it ("Created a Linear issue"). Naming the app in the sentence retires the chip, so the provenance is stated once. An action whose verb we do not know still has no sentence to fold into, and keeps its chip. The sentence now depends on the catalog's app name, which arrives asynchronously, so `resolveToolDisplay` takes an optional `appName` and the row resolves twice: once for the integration slug, then again with the real name.
A Google Calendar action on "calendar settings" folded into "Got Google Calendar calendar settings". When the object already names the app, the sentence drops it and the chip carries it instead, so the row reads "Got calendar settings" beside a "Google Calendar" chip. Matched on whole words only. A near-miss like Gmail against "email" shares no word and still reads "Sent a Gmail email", which is redundant but not wrong; matching substrings would start guessing.
The stutter guard only matched whole words, so it fired on the catalog's "Google Calendar" but not on the title-cased slug "Googlecalendar" the row paints first. It now also matches an object word inside the squashed slug, with a length floor so "file" does not match "googledrive". Erring toward skipping is safe: the app moves to the chip, so nothing is lost. Also drops our own MCP server's chip in the package mirror, which OSS already did via canonicalToolName but the copy never reproduced.
8b8138a to
940e7a3
Compare
Three simplifications, no behaviour change. Drops the copy of this logic in the chat package's skin registry. Nothing calls that resolver: the store starts empty, nobody calls registerChatSkin, and mobile renders raw names from @agenta/chat/model instead. Mirroring 340 lines into a dead path was speculative work that would drift before anything consumed it. The re-plumb PR can copy from OSS when it happens. The package's toolSummary change stays, since mobile does use rowSummary. Removes the builder closure. The app name arrives late, so conjugate returned a function that resolveToolDisplay called later; threading the name through parseShape instead does the same job with one less layer, and retires the ActivityBuilder type, the fixed() wrapper, and a ParsedShape field that had gone dead. Tightens comment blocks that ran six to ten lines each, against this repo's one-line rule.
A row showed "Ran a c…" beside a full sandbox path. All three spans in the header carried min-w-0 truncate, so flex shrank whichever it liked and the one thing that must stay readable lost. The sentence no longer yields; the detail and the status beside it absorb the squeeze. The path was the real noise. Every path in a session sits under the sandbox root, so it is identical on every row and says nothing. Stripping it turns "find /tmp/agenta-sandbox-agent-wzkOSy/agents/skills" into "find agents/skills". Generated id segments go with it; a named directory like workspace/ stays, since it carries no digit. Quote stripping now only unwraps quotes the login-shell wrapper added. It was unanchored and ate the closing quote of a command ending in -name '*.md'.
…ched with
A tool search's arguments are model-written keyword salad ("list google calendar events date range"), so a row built from them read as keywords with a verb glued on. The result is the better source: discover_tools reports the integration and the ACTION token of the tool it matched, and that token is the same shape a gateway wire name carries — so the discovery row and the row that later runs that tool derive their wording from one path. Only read-only verbs are adopted: the call found the tool, it did not run it, so SEND_EMAIL must not read "Sent an email". With no matched tool the query still carries the row, trimmed to a short qualifier once the catalog can spell the app.
Also in the sentence builder: the verb may sit at the END of an action name (EVENTS_LIST), and those used to read with no tense at all in either direction. An article the name carries ("Create an issue") now comes off before the app goes in front of the noun, which had been landing it mid-phrase as "Created GitHub an issue". request_connection names the app it is waiting on, read from its own arguments, and says the run is blocked on the reader rather than that the agent is asking. rename_agent says "the agent": a session has exactly one.
…be tested partSentence, hasLanded, rowSummary and the output summariser lived inside ToolActivity.tsx, which has no test file — so the denied/deferred tense fix and the failure sentence shipped with no coverage. They move to assets/toolRow.ts with 27 tests. One bug surfaced on the way: the 80-character output cut counted UTF-16 units while every other cap in this area counts code points, so emoji-heavy output could split a surrogate pair and render a replacement character. It cuts on code points now. The catalog subscription keys on sourceKey alone rather than on kind, and MCP tools stop reporting one — an MCP server name is not a catalog integration, so that lookup had nothing to find and the row paid for it anyway.
The transcript learned the plain-English sentence; the approval dock, the always-allow switch and the turn inspector kept the old title-cased label, so the card gating a call and the row reporting it used different words for the same tool. All three also resolved without the call's arguments, so none stripped the sandbox root — a Codex shell approval showed 60 characters of raw command as its "name".
The dock's frame had to change with it: a gerund cannot slot into "wants to use ___", so it now reads "The agent needs your approval before running a command". The inspector splits by event, a call taking the present tense and a result the past.
ElicitationWidget deliberately keeps the label. It names the tool as an actor ("Asked by …"), where the sentence reads as broken English; that one needs a copy decision, not a mechanical swap.
A cleanup pass over this branch. No behaviour change except where noted below. Dead code, each verified rather than assumed: "check" was the one QUERY_VERBS entry with no VERB_FORMS counterpart, so both readers bailed before reaching it; the preposition regex listed five alternatives where exactly two of 37 verb forms end in one; parseShape's bare-identifier tail branched on a source that parseGatewayToolName can only return for names containing "__", which that branch has already excluded; and ToolDisplayOverride's label/source fields were set by no registry entry, which is what forced the source expression into a nested ternary. Wasted work: discover_tools parsed its own result up to eight times per render — two override hooks each walked to the same capability, and the resolver runs once bare then again with the catalog name, on a payload carrying a JSON Schema per capability. One `app()` hook now returns slug and action from one read, and it is not called at all on the bare resolve, where its result was provably discarded. clamp built a code-point array unconditionally, so a 2 KB command became a 2000-element array to keep 48 characters; short strings now skip it. Duplication: echoesApp and qualifierFor each spelled out the same "is this word part of the app's name?" rule 90 lines apart, now one appWordTest. ToolActivity repeated the resolve-then-re-resolve-with-catalog dance four times, now displayFor plus a memoized useCatalogDisplay — the conditional-subscription split that keeps virtualized rows off the IndexedDB-backed query is unchanged. PATH_KEYS had drifted to four keys in a different precedence from the entities helper it mirrors; realigned. Two fixes that are not cleanup. `__ag__request_input` is the wire name a real run streams, and canonicalToolName did not unwrap our reserved slug namespace — the generic parser read "ag" as the app and derived "Requested an Ag input" for every elicitation in the turn inspector. And request_input now reads "Waiting for your answers" / "Asked you some questions": the run is parked on the reader there, which "Asking you for details" left implicit. The chat package's copy gets the code-point cut it never received (it still split surrogate pairs at 80 UTF-16 units) plus a test. Its five "copied verbatim" banners named ToolActivity.tsx, which this branch moved the code out of, and its byte-parity claim is no longer achievable now that rowSummary deliberately differs — both corrected to say what is actually true.
A paused turn showed the same sentence in three places at once: the turn's status line ("Waiting for your input"), the composer dock ("The agent is waiting for your response — new messages will be queued"), and the elicitation card's subtext ("Waiting on your input").
The card's copy goes, since it is the one with the questions and buttons already in front of you. The status line drops "input" for plain "Waiting for you" — it is shared by all three parked states (approval, connect, elicitation), so it cannot name any one of them, and "Your turn" was not an option next to an "Inspect turn" button. The dock keeps its line: it is the only one that says new messages will be queued.
The card also stopped naming its own asker. "Asked by Request input" restates what the card in front of you already is; a third-party tool rendering through the same widget (anything with render.kind "elicitation") is still named, because there it is real information.
The acceptance spec asserted the removed text. It could never have matched in any case — it wanted a literal "request_input" where the label resolves to "Request input" — so it now asserts the required-field count the subtext actually carries.
web/CLAUDE.md sets a hard rule — at most one short line per comment, longer only for a genuinely surprising constraint — and this branch had drifted well past it: toolDisplay.ts was 27% comment across 15 blocks of four or more lines, several of them narrating in prose what the code already says. 31 blocks rewritten, 73 lines gone, no behaviour change. The four long blocks that remain in toolDisplay.ts predate this branch (the file header, canonicalToolName, and the two call-description helpers) and are left alone rather than grown into the diff. The two of mine still over one line are parseShape and resolveToolDisplay, both at four lines: Codex recording a shell call under its own command — because otel.ts uses the ACP title as the identifier — and the catalog's two-pass resolve are the cases the rule's exception is for, since neither is visible from the code.
|
Regression testing it / final code review / after that, I will add some preview demo |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx (1)
299-300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winKeep changed in-code comments to one short line. These three sites exceed the repository comment-length rule.
web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx#L299-L300: shorten the requester-attribution rationale.web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx#L357-L359: shorten the JSX comment.web/oss/tests/playwright/acceptance/agent-chat/index.ts#L83-L84: shorten the test rationale comment.Source: Coding guidelines
web/packages/agenta-chat/src/model/toolSummary.ts (1)
23-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftExtract the mirrored helpers instead of hand-porting them.
SETTLED,isDeferredError,isUnknownResultError,isNotHandledOutput,parseJsonish, andsummarizeOutputare byte-for-byte copies ofweb/oss/src/components/AgentChatSlice/assets/toolRow.ts. This PR fixes the UTF-16 truncation bug in this copy only because the copy had already drifted, which shows the manual port process fails silently. Move the pure helpers into one shared module and let bothtoolRow.tsand this file import them. Keep only the intentional divergence (rowSummaryerror wording) local.The same two-line "Mirrors … Port changes both ways." note also repeats four times in this range. One file-level note is enough. As per coding guidelines: "Keep in-code comments to at most one short line; use longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements."
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: d299e43d-6a25-411d-817e-3f09c722e7a5
📒 Files selected for processing (14)
web/oss/src/components/AgentChatSlice/assets/toolDisplay.test.tsweb/oss/src/components/AgentChatSlice/assets/toolDisplay.tsweb/oss/src/components/AgentChatSlice/assets/toolRow.test.tsweb/oss/src/components/AgentChatSlice/assets/toolRow.tsweb/oss/src/components/AgentChatSlice/components/ApprovalDock.tsxweb/oss/src/components/AgentChatSlice/components/Inspector/EventRow.tsxweb/oss/src/components/AgentChatSlice/components/ToolActivity.tsxweb/oss/src/components/AgentChatSlice/components/TurnActivity.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.settleOnce.test.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsxweb/oss/src/hooks/useAlwaysAllowTool.tsxweb/oss/tests/playwright/acceptance/agent-chat/index.tsweb/packages/agenta-chat/src/model/toolSummary.tsweb/packages/agenta-chat/tests/unit/model/toolSummary.test.ts
| {inSentence(friendly?.activity.running ?? "") || | ||
| current.toolName} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not describe a per-tool grant with this call's arguments.
friendly is resolved with current.input, so its sentence can embed call-specific detail. resolveToolDisplay folds a query argument into the sentence for gateway tools. A gate on tools__composio__github__SEARCH_ISSUES__… with input {query: "open bugs"} yields "Searching GitHub open bugs", so the switch reads "Always allow searching GitHub open bugs for this agent". The grant applies to every call of that tool with any arguments, so the label misstates the scope of the permission the user is about to write.
Resolve a second, input-free display for this label.
🐛 Proposed fix: resolve the grant label without the call input
Add beside friendly (near Line 252):
const friendly = current ? resolveToolDisplay(current.toolName, current.input) : null
+// The grant covers the tool, not this call, so its label must not carry this call's arguments.
+const grantLabel = current ? resolveToolDisplay(current.toolName).activity.running : ""Then use it here:
- {inSentence(friendly?.activity.running ?? "") ||
- current.toolName}
+ {inSentence(grantLabel) || current.toolName}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {inSentence(friendly?.activity.running ?? "") || | |
| current.toolName} | |
| const friendly = current ? resolveToolDisplay(current.toolName, current.input) : null | |
| const grantLabel = current ? resolveToolDisplay(current.toolName).activity.running : "" | |
| {inSentence(grantLabel) || current.toolName} |
| await expect(page.getByText(/Asked by .*request_input/)).toBeVisible() | ||
| // Our own elicitation tool goes unnamed — the card IS the agent asking — | ||
| // so the subtext is the required-field count alone. | ||
| await expect(page.getByText("1 required")).toBeVisible() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file="web/oss/tests/playwright/acceptance/agent-chat/index.ts"
printf '%s\n' '--- test context ---'
sed -n '70,95p' "$file"
printf '%s\n' '--- related assertions and rendered text ---'
rg -n -C 3 '1 required|Asked by|requester|required' web/oss/tests/playwright web/packages web/oss --glob '*.{ts,tsx}' | head -200Repository: Agenta-AI/agenta
Length of output: 20169
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- requester text sources ---'
rg -n -C 4 --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'Asked by|requester|elicitation.*required|required.*count|required-field count' .
printf '%s\n' '--- locator API usage ---'
rg -n -C 2 'getByText\([^)]*exact|exact:\s*true' web/oss/tests/playwright/acceptance --glob '*.{ts,tsx}' | head -120Repository: Agenta-AI/agenta
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- focused web matches ---'
rg -n -C 5 --glob '*.{ts,tsx,css}' --glob '!**/generated/**' --glob '!**/node_modules/**' \
'Asked by|asked by|requester|elicitation' web | head -300
printf '%s\n' '--- exact assertion candidates ---'
rg -n -C 2 'getByText\("1 required"|getByText\([^,]+,\s*\{exact:\s*true\}' \
web/oss/tests/playwright/acceptance/agent-chat web/packages --glob '*.{ts,tsx}' | head -120Repository: Agenta-AI/agenta
Length of output: 32868
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- focused literal matches ---'
rg -n -i -C 4 --glob '*.{ts,tsx}' \
'request input|asked by|subtext|required.*field|field.*required' \
web/packages/agenta-ui web/packages/agenta-entity-ui web/oss \
--glob '!**/tests/**' | head -300
printf '%s\n' '--- candidate files ---'
rg -l -i --glob '*.{ts,tsx}' \
'request input|asked by|elicitation|required.*field|field.*required' \
web/packages/agenta-ui web/packages/agenta-entity-ui web/oss \
--glob '!**/tests/**' | head -120Repository: Agenta-AI/agenta
Length of output: 37079
🏁 Script executed:
#!/bin/bash
set -e
for file in \
web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx \
web/oss/src/components/AgentChatSlice/components/clientTools/ClientToolPart.tsx \
web/oss/src/components/AgentChatSlice/components/clientTools/registry.tsx; do
printf '\n--- %s ---\n' "$file"
wc -l "$file"
ast-grep outline "$file" | head -120
doneRepository: Agenta-AI/agenta
Length of output: 1466
🏁 Script executed:
#!/bin/bash
set -e
file="web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx"
printf '%s\n' '--- ElicitationWidget implementation ---'
sed -n '1,230p' "$file"
printf '%s\n' '--- remaining rendering ---'
sed -n '230,444p' "$file"
printf '%s\n' '--- client tool wrapper ---'
cat -n web/oss/src/components/AgentChatSlice/components/clientTools/ClientToolPart.tsxRepository: Agenta-AI/agenta
Length of output: 21982
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
widget = Path("web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx").read_text()
fixture = Path("web/oss/tests/playwright/acceptance/agent-chat/assets/elicitationStream.ts").read_text()
assert 'CLIENT_TOOL_NAMES.has(canonicalToolName(meta.toolName))' in widget
assert 'const subtext = [asker, requiredCount > 0 ? `${requiredCount} required` : null]' in widget
assert 'export const REQUEST_INPUT_TOOL_NAME = "__ag__request_input"' in fixture
assert 'toolName?: string' in fixture
# Model the rendered subtext branch used by the widget.
def subtext(is_own_tool: bool, required_count: int) -> str:
asker = None if is_own_tool else "Asked by Request input"
return " · ".join(x for x in (asker, f"{required_count} required" if required_count > 0 else None) if x)
assert subtext(True, 1) == "1 required"
assert subtext(False, 1) == "Asked by Request input · 1 required"
assert "1 required" in subtext(False, 1)
assert subtext(False, 1) != "1 required"
print("own tool:", subtext(True, 1))
print("named tool:", subtext(False, 1))
print("substring matcher would accept named tool:", "1 required" in subtext(False, 1))
print("exact matcher rejects named tool:", subtext(False, 1) != "1 required")
PYRepository: Agenta-AI/agenta
Length of output: 310
Use an exact required-count assertion.
getByText("1 required") can match Asked by Request input · 1 required. Add {exact: true} to check the complete subtext.
Railway Preview Environment
|
Context
The playground labelled every tool call with its raw wire name, so the activity feed read like a terminal log. Under Codex that name is not a name at all. The runner stores the ACP display title as the identifier, so a shell call is recorded under the command itself and a file read under an English sentence containing an absolute sandbox path.
Our own tools were no better. The frontend unwrapped only Claude's
mcp__server__toolform, so Codex's dottedmcp.agenta-tools.test_runfell through to a title-caser that strips underscores but not dots, and printedMcp.agenta tools.test run.Closes #5976.
Changes
A row now shows a plain sentence. The technical string sits beside it, and the exact wire name, arguments, and output stay in the expander.
mcp.agenta-tools.test_runMcp.agenta tools.test runcreate_subscriptionCreate subscriptionquery_spansQuery spansread_configRead configrename_agentRename agent__ag__request_inputRequest input+ chipAgrequest_connectionRequest connectionBashBashrg -n 'todo' srcReadReadSKILL.mdGrepGrephandleSubmit\(sed -n '1,80p' /tmp/agenta-…/agent.pysed -n '1,80p' agent.pyRead file '/tmp/agenta-…/SKILL.md'SKILL.mdtools__composio__github__SEARCH_ISSUES__a4fSearch issues+ chipGithubtools__composio__slack__SEND_MESSAGE__ySend message+ chipSlacktools__composio__github__CREATE_AN_ISSUE__c1Create an issue+ chipGithubtools__composio__googlecalendar__EVENTS_LIST__c1Events list+ chipGooglecalendarmcp.linear.create_issueMcp.linear.create issuediscover_tools(foundYOUTUBE_LIST_CHANNEL_VIDEOS)Discover toolsEvery row above comes from running the resolver, not from writing out what it should say.
The wording is derived, not tabulated
A verb table turns a
verb nounname into both tenses, and a small glossary maps our internal nouns to the words the product uses. Asubscriptionis a trigger, aspanis a run,configis the agent's setup. A platform op we ship next month reads correctly with no code change, and so does a Composio action nobody has seen. Only four names carry hand-written wording, because derivation gets them wrong:test_run("Tested a run" misses that the run is the agent),request_inputandrequest_connection(both address the reader rather than describe the agent), andcommit_revision(wording derives; the entry exists only for its commit-message summary).Two guards keep it honest. An action whose verb is not in the table keeps its plain label, so an unfamiliar verb reads as it does today instead of as "Getted". And the glossary applies only to our own tools, because a gateway action like
stripe__CANCEL_SUBSCRIPTIONmeans a real subscription and must never be renamed to "trigger".The verb can also sit at the end of an action name.
EVENTS_LISTused to produce "Events list" in both tenses, so the row never visibly progressed. The tail word is checked when the head is not a verb.App names come from the catalog, never from a list in the source
There are thousands of Composio integrations, so hardcoding names does not scale. The wire name only supports title case, which gets
GithubandGooglecalendarwrong. The real spelling comes from the tool catalog, the same query the agent config panel already uses.The resolver is pure and synchronous while the catalog answers late, so a row resolves twice: once to learn the integration slug, then again with the real name once
useToolIntegrationDetailanswers. Only rows that have a slug subscribe. Rows live in a virtualized transcript and that query carries an IndexedDB persister, so a subscription on every row would charge every session for a lookup almost none of them need.The app is named inside the sentence rather than sitting in a chip beside it, so the row reads as one thought. Where naming it would stutter ("Checked Google Calendar calendar settings"), the app moves back to the chip.
discover_toolsgets its app from its own result. The arguments are keyword salad the model wrote for a search ("get youtube channel activities uploads recent"), but the result reports the integration and the ACTION token of the tool it matched. That token has the same shape a gateway wire name carries, so the discovery row and the row that later runs that tool derive their wording from one path. Only read-only verbs are adopted. The call found the tool, it did not run it, soSEND_EMAILmust never read "Sent an email".Tense follows what actually happened
A denied or deferred call never ran, so it keeps the present tense instead of claiming "Tested the agent" beside the word "denied". A genuine failure reads as one thought, "Testing the agent failed", rather than claiming the action completed and contradicting it four words later.
The summary slot stopped leaking payloads
Gateway tools return their payload as a JSON string, which took the plain-text path and got sliced at 80 characters. A serialised payload is now read as structure. A file's contents and a command's stdout report their size instead ("194 lines"), because their first 80 characters are never the point and they leak whatever the file happens to start with. The generic "2 fields" fallback is gone, since a field count tells the reader nothing the status icon has not already said.
Everywhere a tool is named now says the same thing
The approval dock, the always-allow switch, and the turn inspector kept the old title-cased label, so the card gating a call and the row reporting it used different words for the same tool. All three now use the sentence, and all three pass the call's arguments, so they strip the sandbox root too. The dock's frame changed with them, because a gerund cannot slot into "wants to use ___":
Before:
The agent wants to use Test run before it can keep going.Now:
The agent needs your approval before testing the agent.A paused turn also said the same sentence three times at once: the turn status line, the composer dock, and the elicitation card. The card's copy goes, since it is the one with the questions and buttons already in front of you. The status line drops the jargon and reads "Waiting for you", which is true for all three parked states (approval, connect, elicitation). The dock keeps its line because it is the only one that mentions queued messages.
Two bugs found along the way
__ag__request_inputis the wire name a real run streams, and the reserved slug namespace was never unwrapped. The generic parser readagas the app and derived "Requested an Ag input" for every elicitation in the turn inspector.An action name carrying its own article landed it mid-phrase.
CREATE_AN_ISSUEproduced "Created GitHub an issue". The article now comes off before the app goes in front of the noun.Tests
pnpm vitest run src/components/AgentChatSlice src/hooksinweb/oss: 485 passing across 37 files. 71 cover the resolver and 27 cover the row text, using tool names taken from recorded Codex sessions.pnpm vitest runinweb/packages/agenta-chat: 28 passing.tsc --noEmitclean on every changed file.The row's text derivation moved out of
ToolActivity.tsxintoassets/toolRow.tsso it could be tested at all. The component had no test file, so the denied and deferred tense fix and the failure sentence shipped with no coverage. That move also surfaced an output cut that counted UTF-16 units where every other cap in the area counts code points, which could split a surrogate pair and render a replacement character.Worth a reviewer's eye:
ElicitationWidgetdeliberately keeps the plain label rather than the sentence. It names the tool as an actor ("Asked by X"), where a gerund reads as broken English. For our own elicitation tools it now shows no attribution at all, because the card in front of you is the agent asking.What to QA
Mcp.agenta tools...../tmp/agenta/mounts/...path. The file row ends in a line count, not the first line of the file.discover_tools. Rows name the app it found with the real spelling ("Checked YouTube channel videos"), not the keywords it searched with.toolwith the exact wire name, followed by input and output as before.