Files
orca/src/shared/native-chat-agent-profiles.ts
Brennan BensonandMerge Sim ecd7b19ad4 fix(native-chat): pass agent-implemented slash commands through to the agent (#19929)
* fix(native-chat): pass agent-implemented slash commands through to the agent

Claim what the host implements; pass through what the agent implements.
Claude's harness expands a slash command out of the message text, so the
host claimed catalog commands it had no way to run and answered "/init is
not available in chat sessions" for commands Claude does run. Codex's
app-server has no slash parser at all, so its catalog stays claimed —
except /goal, which the model carries out through its own goal tools.

* fix(native-chat): offer the agent-run commands in the structured picker

Codex reports no command catalog, so its structured `/` menu is the host
fallback -- which listed only the host's own commands and hid `/goal`, the
one command the model itself acts on. The picker now appends the profile's
text-driven commands, described from the curated catalog, so a command that
passes through is discoverable and not merely typable.

The menu invariant holds either way: a pick is answered by the host or run
by the agent, never refused with "not available in chat sessions".

* fix mobile structured command reconciliation

* fix(mobile): keep native chat controller within lint budget

* fix mobile controller lint budget

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-10 20:50:40 -07:00

83 lines
3.2 KiB
TypeScript

import type { AgentType } from './agent-status-types'
import { getAgentSlashCommands, type SlashCommandSuggestion } from './native-chat-slash-commands'
export type NativeChatAgentProfile = {
skillPrefix: '$' | '/'
/** OpenClaude reads Claude-owned roots, so this can differ from the agent. */
skillSourceOwner: AgentType
/** The agent's own harness expands a slash command out of the message text, so
* the chat host claims only the commands it implements itself. */
expandsSlashCommandsFromText?: true
/** Catalog commands the model acts on when they arrive as prose, even though
* the runtime has no slash parser of its own. */
textDrivenCommands?: readonly string[]
}
const NATIVE_CHAT_AGENT_PROFILES: Partial<Record<AgentType, NativeChatAgentProfile>> = {
codex: {
skillPrefix: '$',
skillSourceOwner: 'codex',
// The app-server has no slash parser, but the model owns goal tools and
// calls create_goal itself when `/goal <objective>` reaches it as prose.
textDrivenCommands: ['goal']
},
claude: {
skillPrefix: '/',
skillSourceOwner: 'claude',
expandsSlashCommandsFromText: true
},
openclaude: {
skillPrefix: '/',
skillSourceOwner: 'claude',
expandsSlashCommandsFromText: true
},
grok: {
skillPrefix: '/',
skillSourceOwner: 'grok'
}
}
export function getNativeChatAgentProfile(
agent: AgentType | null | undefined
): NativeChatAgentProfile | null {
return agent ? (NATIVE_CHAT_AGENT_PROFILES[agent] ?? null) : null
}
/** The catalog that send classification, collision detection, and transcript
* envelope surfacing key off. Grok has no verified catalog yet, so its slash
* surface stays skills-only — this is the single place that policy lives. */
export function getVerifiedNativeChatCommands(agent: AgentType): readonly SlashCommandSuggestion[] {
return agent === 'grok' ? [] : getAgentSlashCommands(agent)
}
/** The mirror of the claimed set: catalog commands this agent acts on when they
* arrive as message text. The picker offers these too, so a command the agent
* implements is discoverable and not merely typable. */
export function getTextDrivenNativeChatCommands(
agent: AgentType | null | undefined
): readonly SlashCommandSuggestion[] {
if (!agent) {
return []
}
const names = new Set(getNativeChatAgentProfile(agent)?.textDrivenCommands ?? [])
return names.size === 0
? []
: getVerifiedNativeChatCommands(agent).filter((command) => names.has(command.name))
}
/** Catalog commands the chat host answers itself. Whatever is left over reaches
* the agent as ordinary text, which is only correct where the agent implements
* the command — so an agent unclaims a command only via the profile above.
* Claiming stays the default: it is what stops a hand-typed `/clear` from being
* sent to the model as literal prompt text. */
export function getHostClaimedNativeChatCommands(
agent: AgentType
): readonly SlashCommandSuggestion[] {
const profile = getNativeChatAgentProfile(agent)
if (profile?.expandsSlashCommandsFromText) {
return []
}
const passedThrough = new Set(profile?.textDrivenCommands ?? [])
return getVerifiedNativeChatCommands(agent).filter((command) => !passedThrough.has(command.name))
}