mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 00:02:34 +00:00
* 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>
83 lines
3.2 KiB
TypeScript
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))
|
|
}
|