fix(mobile): keep the terminal glyph on a classified Codex shell row

Mobile's run header picks between a terminal and a generic glyph by tool
name. Now that the host publishes `read`/`search`/`list` for the same
commands it used to publish as `shell`, that name check answers false and
a command that really ran heads its run with a wrench.

Ask the shared category vocabulary instead. Mobile keeps its two icons —
porting the full glyph set is a separate lane.
This commit is contained in:
Merge Sim
2026-09-05 02:01:03 -07:00
parent 2916f81b07
commit ab07acde13
3 changed files with 64 additions and 2 deletions
@@ -14,9 +14,9 @@ import {
describeActiveToolCall,
formatActiveToolLabel,
formatToolCallCount,
isCommandToolName,
selectActiveToolCall
} from '../../../src/shared/native-chat-tool-activity'
import { isShellActivityToolRow } from '../../../src/shared/native-chat-tool-icon'
import type { NativeChatBlock } from '../../../src/shared/native-chat-types'
import { colors } from '../theme/mobile-theme'
import { styles } from './mobile-native-chat-message-styles'
@@ -195,7 +195,11 @@ export function ToolRun({
}
callCount ||= pairs.length
const summary = summarizeToolRun(blocks)
const ActiveToolIcon = activeCall && isCommandToolName(activeCall.name) ? SquareTerminal : Wrench
// Read through the shared category vocabulary, not the tool name: Codex names
// a classified shell row `read`/`search`/`list`, and a wrench on one of those
// would call a command that ran a generic tool.
const ActiveToolIcon =
activeCall && isShellActivityToolRow(activeCall.name) ? SquareTerminal : Wrench
return (
<View style={styles.toolRun}>
<View style={styles.toolRunHeader}>
+39
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
isShellActivityToolRow,
NATIVE_CHAT_TOOL_ICON_NAMES,
nativeChatToolCategory,
nativeChatToolIconName,
@@ -108,6 +109,44 @@ describe('native chat tool icons', () => {
}
})
it('reads a classified shell row as terminal activity, not as a named tool', () => {
// A lane with only a terminal and a generic glyph (mobile) asks this instead
// of `isCommandToolName`, which answers false for the words Codex publishes
// for a command that really ran.
for (const word of ['read', 'search', 'list', 'shell', 'bash', 'run_terminal_cmd']) {
expect(isShellActivityToolRow(word)).toBe(true)
}
// Claude's own filesystem tools share those categories, so they read as
// terminal activity on that lane too — it has no closer glyph for them.
for (const word of ['Read', 'Grep', 'Glob']) {
expect(isShellActivityToolRow(word)).toBe(true)
}
for (const word of ['Edit', 'Diff', 'Task', 'WebFetch', 'TodoWrite', 'AskUserQuestion', '']) {
expect(isShellActivityToolRow(word)).toBe(false)
}
})
it('answers terminal activity for exactly the shell categories, and no others', () => {
// A `Record` of the whole vocabulary, so a category added later cannot join
// or leave the terminal set without this listing changing.
const rowWordByCategory: Record<NativeChatToolCategory, string> = {
read: 'read',
search: 'search',
listFiles: 'list',
unknown: 'shell',
fileChange: 'Edit',
webSearch: 'WebFetch',
mcpToolCall: 'mcp__linear__create_issue',
subAgentActivity: 'Task',
todoList: 'TodoWrite',
other: 'AskUserQuestion'
}
expect(
ALL_CATEGORIES.filter((category) => isShellActivityToolRow(rowWordByCategory[category]))
).toEqual(['read', 'search', 'listFiles', 'unknown'])
})
it('does not answer a prototype key with a glyph', () => {
expect(nativeChatToolCategory('__proto__')).toBeNull()
expect(nativeChatToolCategory('constructor')).toBeNull()
+19
View File
@@ -108,3 +108,22 @@ export function nativeChatToolCategory(rowWord: string): NativeChatToolCategory
export function nativeChatToolIconName(rowWord: string): NativeChatToolIconName {
return NATIVE_CHAT_TOOL_ICON_NAMES[nativeChatToolCategory(rowWord) ?? 'other']
}
/** The categories a shell command produces: Codex's three classified classes,
* plus an unclassified one. Kept here rather than in a lane so the classified
* words and `shell` stay one answer. */
const SHELL_ACTIVITY_CATEGORIES: ReadonlySet<NativeChatToolCategory> = new Set([
'read',
'search',
'listFiles',
'unknown'
])
/** Whether a row reads as terminal activity. For a lane with no per-category
* glyph (mobile), which only chooses between a terminal and a generic tool:
* keying that on the tool name alone would call Codex's classified `read` /
* `search` / `list` rows generic tools, though a shell command produced them. */
export function isShellActivityToolRow(rowWord: string): boolean {
const category = nativeChatToolCategory(rowWord)
return category !== null && SHELL_ACTIVITY_CATEGORIES.has(category)
}