fix(native-chat): say what the run header's glyph actually guarantees

The comment claimed the header names the same tool in both states, so its
glyph cannot change on settle. It can: the live header names the running
call while the settled one names the run's last tool call, and with
out-of-order completion those differ. The glyph is fixed for whichever
tool the header names — say that, and drop the never-taken running branch
from the settled header's call.

Also pin the other half of the file-target rule: `read` keeps `path`, so
its row stays tappable, where `list`/`search` lift a folder to
`directory` and offer no target at all.
This commit is contained in:
Merge Sim
2026-09-05 02:01:12 -07:00
parent ab07acde13
commit d5413b6710
2 changed files with 34 additions and 25 deletions
@@ -197,24 +197,24 @@ describe('codex item bodies', () => {
})
it('names a classified read command by its class and keeps the raw command', () => {
expect(
codexItemBody({
type: 'commandExecution',
id: 'item-read',
command: "sed -n '1,200p' notes.txt",
cwd: '/repo',
status: 'completed',
exitCode: 0,
commandActions: [
{
type: 'read',
command: "sed -n '1,200p' notes.txt",
name: 'notes.txt',
path: '/repo/notes.txt'
}
]
})
).toEqual({
const body = codexItemBody({
type: 'commandExecution',
id: 'item-read',
command: "sed -n '1,200p' notes.txt",
cwd: '/repo',
status: 'completed',
exitCode: 0,
commandActions: [
{
type: 'read',
command: "sed -n '1,200p' notes.txt",
name: 'notes.txt',
path: '/repo/notes.txt'
}
]
})
expect(body).toEqual({
kind: 'tool-call',
name: 'read',
// `name` is the target's basename, which `path` already carries and no
@@ -222,6 +222,11 @@ describe('codex item bodies', () => {
input: { command: "sed -n '1,200p' notes.txt", cwd: '/repo', path: '/repo/notes.txt' },
state: 'completed'
})
// `read` is the one class that keeps `path`, so its row stays a tappable
// file on mobile — the other half of the rule `list`/`search` obey below.
const display = createToolInputDisplay(body?.kind === 'tool-call' ? body.input : null)
expect(display.filePath).toBe('/repo/notes.txt')
expect(display.label).toBe('/repo/notes.txt')
})
it('carries a classified search query so the row labels by term, not scan root', () => {
@@ -226,11 +226,12 @@ export function NativeChatToolRun({
() => (open ? buildEditCards(blocks) : NO_EDIT_CARDS),
[open, blocks]
)
// The header is named by the run's latest tool in both states, so its glyph is
// chosen once and does not change when that tool settles. State rides on the
// trailing mark instead — a leading glyph that flipped to a check would read as
// the row changing identity.
const headerCall = latestActiveCall ?? blocks.findLast(isToolCallBlock) ?? null
// Only the settled header reads this; the live one names `latestActiveCall`.
// Those are the same call except under out-of-order completion, where the
// header re-names on settle to the run's last tool. The glyph is fixed for
// whichever tool the header names, so state rides on the trailing mark — a
// leading glyph that flipped to a check would read as a change of identity.
const settledHeaderCall = blocks.findLast(isToolCallBlock) ?? null
const fallbackLabel =
callCount === 1
? translate('components.native-chat.tool.countOne', NATIVE_CHAT_TOOL_ACTIVITY_COPY.countOne)
@@ -275,8 +276,11 @@ export function NativeChatToolRun({
className="group flex min-h-6 w-full items-center gap-1.5 py-0.5 text-left"
aria-expanded={open}
>
{structuredActivityUi && headerCall ? (
<NativeChatToolIcon rowWord={headerCall.name} className="text-muted-foreground" />
{structuredActivityUi && settledHeaderCall ? (
<NativeChatToolIcon
rowWord={settledHeaderCall.name}
className="text-muted-foreground"
/>
) : null}
<span className="shrink-0 font-mono text-[11px] font-bold text-muted-foreground transition-colors group-hover:text-foreground/80">
{callCount}×