mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 08:02:37 +00:00
* Render native chat task lists with incremental checklist updates * Keep native chat task-list review plan out of repository root * Render live Codex plan notifications through task checklists * fix(native-chat): keep checklist test fixture within shared boundary * Keep agent task progress in one composer panel * Restore inline task checklists and historical update diffs --------- Co-authored-by: Merge Sim <sim@local>
165 lines
6.4 KiB
TypeScript
165 lines
6.4 KiB
TypeScript
import { mcpToolIdentity, type NativeChatMcpIdentity } from './native-chat-tool-identity'
|
|
/**
|
|
* The category vocabulary for native-chat tool rows, and the one glyph each
|
|
* category keeps. A row is `icon + word + argument`: the icon is decorative and
|
|
* the word carries identity, so a renderer must never draw the glyph alone.
|
|
*
|
|
* The glyph is fixed per category across running/completed/failed — only tone
|
|
* changes, plus a trailing mark on failure. A row that swapped glyphs when it
|
|
* finished would read as changing identity.
|
|
*/
|
|
import { EDIT_TOOL_NAMES } from './native-chat-diff'
|
|
import { isCommandToolName } from './native-chat-tool-activity'
|
|
import { toolInputCommand } from './native-chat-tool-summary'
|
|
|
|
export type NativeChatToolCategory =
|
|
| 'read'
|
|
| 'search'
|
|
| 'listFiles'
|
|
/** A shell command that ran unclassified — Codex's own word for one. */
|
|
| 'unknown'
|
|
| 'fileChange'
|
|
| 'webSearch'
|
|
| 'mcpToolCall'
|
|
| 'subAgentActivity'
|
|
| 'todoList'
|
|
/** A tool this vocabulary doesn't model. Distinct from `unknown`: claiming a
|
|
* terminal for it would assert a shell ran when nothing says one did. */
|
|
| 'other'
|
|
|
|
/** lucide glyph ids. Spelled the same by `lucide-react` and `lucide-react-native`,
|
|
* so desktop and mobile can resolve one name to their own component. */
|
|
export type NativeChatToolIconName =
|
|
| 'eye'
|
|
| 'search'
|
|
| 'folder'
|
|
| 'square-terminal'
|
|
| 'pencil'
|
|
| 'globe'
|
|
| 'plug'
|
|
| 'bot'
|
|
| 'list-checks'
|
|
| 'wrench'
|
|
|
|
/** Category to glyph. */
|
|
export const NATIVE_CHAT_TOOL_ICON_NAMES: Record<NativeChatToolCategory, NativeChatToolIconName> = {
|
|
read: 'eye',
|
|
search: 'search',
|
|
listFiles: 'folder',
|
|
unknown: 'square-terminal',
|
|
fileChange: 'pencil',
|
|
webSearch: 'globe',
|
|
mcpToolCall: 'plug',
|
|
subAgentActivity: 'bot',
|
|
todoList: 'list-checks',
|
|
other: 'wrench'
|
|
}
|
|
|
|
/**
|
|
* Row word to category, keyed by the word a lane actually renders rather than by
|
|
* the protocol type, because that word is all a row model carries. The edit
|
|
* family and the command tools come from their own shared sets below, so this
|
|
* table holds only what neither of those already names.
|
|
* A `Map`, not an object: an object index answers `__proto__` with a truthy value.
|
|
*/
|
|
const CATEGORY_BY_ROW_WORD = new Map<string, NativeChatToolCategory>([
|
|
// Codex's classified shell rows.
|
|
['read', 'read'],
|
|
['search', 'search'],
|
|
['list', 'listFiles'],
|
|
// Codex's rollout-transcript names for a shell call, which the activity set
|
|
// below does not carry: `isCommandToolName` also picks the running row's copy,
|
|
// and this vocabulary only picks a glyph.
|
|
['exec', 'unknown'],
|
|
['local_shell', 'unknown'],
|
|
// Every Codex file change projects as a `Diff` call, and the edit set below
|
|
// names the tools that carry the edit in their input, not that projection.
|
|
['diff', 'fileChange'],
|
|
// Claude's tool names, which its lane renders verbatim.
|
|
['grep', 'search'],
|
|
['glob', 'search'],
|
|
['task', 'subAgentActivity'],
|
|
['webfetch', 'webSearch'],
|
|
['todowrite', 'todoList'],
|
|
['update_plan', 'todoList'],
|
|
['web search', 'webSearch'],
|
|
['websearch', 'webSearch'],
|
|
['web_search', 'webSearch']
|
|
])
|
|
|
|
/** The edit family, lowercased for row-word matching. Deliberately not
|
|
* `isEditToolName`: that predicate answers "could this input wrap a patch",
|
|
* which is true of command tools too, and a shell row is not an edit. */
|
|
const EDIT_ROW_WORDS = new Set([...EDIT_TOOL_NAMES].map((name) => name.toLowerCase()))
|
|
|
|
/** MCP tools arrive as `mcp__<server>__<tool>` and the row is named after the
|
|
* tool, so only the prefix identifies one. */
|
|
const MCP_TOOL_PREFIX = 'mcp__'
|
|
|
|
/** The category a row word names, or null when the lane emitted something this
|
|
* vocabulary doesn't model yet. */
|
|
export function nativeChatToolCategory(
|
|
rowWord: string,
|
|
mcpIdentity?: NativeChatMcpIdentity
|
|
): NativeChatToolCategory | null {
|
|
const word = rowWord.trim().toLowerCase()
|
|
if (word.startsWith(MCP_TOOL_PREFIX) || mcpToolIdentity(rowWord, mcpIdentity) !== null) {
|
|
return 'mcpToolCall'
|
|
}
|
|
// Before the edit family: a command tool runs whatever it is handed, so a
|
|
// patch in its input is not evidence the row is an edit.
|
|
if (isCommandToolName(word)) {
|
|
return 'unknown'
|
|
}
|
|
return CATEGORY_BY_ROW_WORD.get(word) ?? (EDIT_ROW_WORDS.has(word) ? 'fileChange' : null)
|
|
}
|
|
|
|
/** The glyph for a row word. Never empty, so rows stay left-aligned: a word
|
|
* outside the vocabulary takes the generic tool glyph, and only a row that
|
|
* really ran a command claims the terminal. */
|
|
export function nativeChatToolIconName(
|
|
rowWord: string,
|
|
mcpIdentity?: NativeChatMcpIdentity
|
|
): NativeChatToolIconName {
|
|
return NATIVE_CHAT_TOOL_ICON_NAMES[nativeChatToolCategory(rowWord, mcpIdentity) ?? 'other']
|
|
}
|
|
|
|
/** The one category every call in a run shares, or null when the run spans
|
|
* categories or holds no calls. A run header names the whole run, not any one
|
|
* call in it, so it may only claim a category true of all of them. */
|
|
export function nativeChatToolRunCategory(
|
|
calls: readonly { name: string; mcpIdentity?: NativeChatMcpIdentity }[]
|
|
): NativeChatToolCategory | null {
|
|
let shared: NativeChatToolCategory | null = null
|
|
for (const call of calls) {
|
|
const category = nativeChatToolCategory(call.name, call.mcpIdentity) ?? 'other'
|
|
if (shared !== null && shared !== category) {
|
|
return null
|
|
}
|
|
shared = category
|
|
}
|
|
return shared
|
|
}
|
|
|
|
/** The glyph for a run header: the shared category's glyph, the generic tool
|
|
* glyph for a run that spans categories, and null when the run has no tool
|
|
* call to describe and so heads with no glyph at all. */
|
|
export function nativeChatToolRunIconName(
|
|
calls: readonly { name: string; mcpIdentity?: NativeChatMcpIdentity }[]
|
|
): NativeChatToolIconName | null {
|
|
if (calls.length === 0) {
|
|
return null
|
|
}
|
|
return NATIVE_CHAT_TOOL_ICON_NAMES[nativeChatToolRunCategory(calls) ?? 'other']
|
|
}
|
|
|
|
/** Whether a call reads as terminal activity, for a lane with no per-category
|
|
* glyph (mobile) that only chooses between a terminal and a generic tool.
|
|
* The row word cannot decide it alone: Codex names a classified shell row
|
|
* `read` / `search` / `list`, which lowercase to Claude's own `Read` / `Grep` /
|
|
* `Glob`, and those ran no command. So the input breaks the tie — Codex keeps
|
|
* the command it ran, while Claude's `Read` carries only a file path. */
|
|
export function isShellActivityToolCall(call: { name: string; input?: unknown }): boolean {
|
|
return isCommandToolName(call.name) || toolInputCommand(call.input) !== null
|
|
}
|