diff --git a/docs/site/content/docs/agents/native-chat.mdx b/docs/site/content/docs/agents/native-chat.mdx index 1a812697e0ad..a1a45e76ee2e 100644 --- a/docs/site/content/docs/agents/native-chat.mdx +++ b/docs/site/content/docs/agents/native-chat.mdx @@ -18,7 +18,7 @@ Toggle Chat UI ↔ terminal from the agent pane once enabled. ## Composer - Type a message and send (or stop a working turn). Attach files/images when the host supports it. Unsent draft text and attachments stay with the pane across disconnect/reconnect when the host can restore them. A settled transcript also stays on screen while the same session reconnects, instead of blanking until the fresh load finishes (desktop and mobile share that retention). -- Type `/` for slash commands and discovered **skills** (filtered per agent when possible). The slash catalog is agent-aware (Claude vs Codex command sets) and shared with mobile Chat UI. +- Type `/` for slash commands and discovered **skills** (filtered per agent when possible). The slash catalog is agent-aware (Claude vs Codex command sets) and shared with mobile Chat UI. In OMP ACP chats, `/fast` is sent to OMP when the current session advertises it; use OMP's `/fast status`, `/fast on`, or `/fast off` commands rather than a separate speed control. - Use the **model** and options pills (thought level, mode, agent-specific session options) when the agent exposes them. **Claude** model choices come from the **installed Claude CLI on that host** (not a hardcoded list), so new models show up after you update the CLI. **Codex** supports direct model selection from the pill (sends `/model `), with bare `/model` still opening the agent picker. **Grok** exposes **model** (`-m` at launch, `/model` mid-session) and **reasoning effort** (`--reasoning-effort` at launch, `/effort` mid-session); the model list is probed from the signed-in Grok CLI (`grok models`) so retired ids drop out of the picker. Effort choices follow the selected model: Grok 4.6 includes **Extra high**, while Grok 4.5 stops at **High**. **Before the agent process starts**, the composer can show draft launch options that apply on first start; after the session is live, pills that only affect a running turn stay disabled or labeled until the session is ready. Live values go to the CLI in that tab. ## Questions from the agent diff --git a/mobile/src/session/mobile-structured-agent-session-contract.ts b/mobile/src/session/mobile-structured-agent-session-contract.ts index 79fc4574d78c..f79b392de08f 100644 --- a/mobile/src/session/mobile-structured-agent-session-contract.ts +++ b/mobile/src/session/mobile-structured-agent-session-contract.ts @@ -10,6 +10,7 @@ import type { NativeChatResolvedPrompt } from '../../../src/shared/native-chat-r import type { useMobileStructuredAgentOptions } from './use-mobile-structured-agent-options' import type { useMobileStructuredAgentTurnTiming } from './use-mobile-structured-agent-turn-timing' import type { StructuredMobileSendAttachment } from './use-mobile-structured-send-with-outcome' +import type { AgentSessionSlashCommand } from '../../../src/shared/agent-session-wire' import type { MobileNativeChatCommandRefusalCauses, MobileNativeChatSendErrorReporter @@ -20,6 +21,7 @@ import type { MobileStructuredBackgroundTasks } from './use-mobile-structured-ba export type StructuredMobileSession = ReturnType & ReturnType & { session: MobileNativeChatSession + sessionCommands?: readonly AgentSessionSlashCommand[] isWorking: boolean turnId: string | null /** What labels the live turn's one indicator row. */ diff --git a/mobile/src/session/mobile-structured-composer-command.ts b/mobile/src/session/mobile-structured-composer-command.ts index 048fb8cc1407..9531ae4e05ef 100644 --- a/mobile/src/session/mobile-structured-composer-command.ts +++ b/mobile/src/session/mobile-structured-composer-command.ts @@ -46,7 +46,13 @@ export async function dispatchMobileStructuredCommand(input: { if (input.pending.current) { return 'rejected' } - if (!isStructuredAgentSessionComposerCommand(input.text, input.controller.agent)) { + if ( + !isStructuredAgentSessionComposerCommand( + input.text, + input.controller.agent, + input.controller.sessionCommands + ) + ) { return null } if (input.hasAttachments) { diff --git a/mobile/src/session/use-mobile-native-chat-controller.ts b/mobile/src/session/use-mobile-native-chat-controller.ts index 04c711fd477b..be48b1b1223d 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.ts @@ -219,6 +219,7 @@ export function useMobileNativeChatController(args: { const structuredNativeChatSend = useMobileStructuredNativeChatSendBridge({ agent: activeChatResolution?.agent, + sessionCommands: structuredNativeChat.sessionCommands, sendStructured: structuredNativeChat.sendWithOutcome, captureSendOrigin, clearDraftForSend, diff --git a/mobile/src/session/use-mobile-structured-agent-session.ts b/mobile/src/session/use-mobile-structured-agent-session.ts index 66fd76fda000..4ee8d35ae20f 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.ts +++ b/mobile/src/session/use-mobile-structured-agent-session.ts @@ -184,6 +184,7 @@ export function useMobileStructuredAgentSession( stopPressed: stopPress.pressed }) === 'stopping' const stopRequestInFlight = isWorking && stopPress.pressed + const sessionCommands = enabled && connected ? (state.commands ?? undefined) : undefined const sendWithOutcome = useMobileStructuredSendWithOutcome({ agent, client, @@ -197,6 +198,7 @@ export function useMobileStructuredAgentSession( hold: sends.hold, commandPending: commandPendingRef, controller: sendController, + sessionCommands, onSendError, carriesOptions: hostSupport?.sendOptions === true }) @@ -283,6 +285,7 @@ export function useMobileStructuredAgentSession( return { ...options, + sessionCommands, visualSource, imagePreviewsByMessageId: sends.imagePreviewsByMessageId, session: { diff --git a/mobile/src/session/use-mobile-structured-native-chat-send-bridge.ts b/mobile/src/session/use-mobile-structured-native-chat-send-bridge.ts index c9e19224aa97..c52f350766b4 100644 --- a/mobile/src/session/use-mobile-structured-native-chat-send-bridge.ts +++ b/mobile/src/session/use-mobile-structured-native-chat-send-bridge.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' import type { StructuredAgentId } from '../../../src/shared/agent-session-provider-handle' +import type { AgentSessionSlashCommand } from '../../../src/shared/agent-session-wire' import { isStructuredAgentSessionComposerCommand } from '../../../src/shared/structured-agent-session-composer' import type { MobileNativeChatSendOutcome } from './mobile-native-chat-send' import type { MobileNativeChatSendOrigin } from './use-mobile-native-chat-drafts' @@ -7,6 +8,7 @@ import type { StructuredMobileSendAttachment } from './use-mobile-structured-sen export function useMobileStructuredNativeChatSendBridge(args: { agent: StructuredAgentId | undefined + sessionCommands?: readonly AgentSessionSlashCommand[] sendStructured: ( text: string, images?: string[], @@ -28,6 +30,7 @@ export function useMobileStructuredNativeChatSendBridge(args: { } { const { agent, + sessionCommands, captureSendOrigin, clearDraftForSend, onSendError, @@ -46,7 +49,7 @@ export function useMobileStructuredNativeChatSendBridge(args: { onSendError('Message not sent (disconnected)') return 'rejected' } - const isHostCommand = isStructuredAgentSessionComposerCommand(text, agent) + const isHostCommand = isStructuredAgentSessionComposerCommand(text, agent, sessionCommands) clearDraftForSend(origin, text) const outcome = attachments !== undefined @@ -63,7 +66,15 @@ export function useMobileStructuredNativeChatSendBridge(args: { } return outcome }, - [agent, captureSendOrigin, clearDraftForSend, onSendError, restoreRejectedDraft, sendStructured] + [ + agent, + sessionCommands, + captureSendOrigin, + clearDraftForSend, + onSendError, + restoreRejectedDraft, + sendStructured + ] ) const send = useCallback( async ( diff --git a/mobile/src/session/use-mobile-structured-send-with-outcome.ts b/mobile/src/session/use-mobile-structured-send-with-outcome.ts index 724acfb5c2e6..59086ba5fdbd 100644 --- a/mobile/src/session/use-mobile-structured-send-with-outcome.ts +++ b/mobile/src/session/use-mobile-structured-send-with-outcome.ts @@ -63,6 +63,7 @@ export function useMobileStructuredSendWithOutcome(args: { StructuredAgentSessionComposerOptions, 'snapshot' | 'setOption' | 'invokeAction' | 'conversationCommands' > + sessionCommands?: StructuredAgentSessionComposerOptions['sessionCommands'] onSendError: MobileNativeChatSendErrorReporter /** Each send carries the model and effort the picker shows; an older host takes neither. */ carriesOptions?: boolean @@ -85,6 +86,7 @@ export function useMobileStructuredSendWithOutcome(args: { queueCapable, carriesOptions, sessionId, + sessionCommands, stateRef, stopping } = args @@ -117,7 +119,7 @@ export function useMobileStructuredSendWithOutcome(args: { sessionId, fence: currentFence, pending: commandPending, - controller: { agent: agent ?? undefined, ...controller }, + controller: { agent: agent ?? undefined, ...controller, sessionCommands }, busy: () => actionableStructuredApproval(stateRef.current) || actionableStructuredQuestion(stateRef.current) @@ -196,6 +198,7 @@ export function useMobileStructuredSendWithOutcome(args: { queueCapable, carriesOptions, sessionId, + sessionCommands, stateRef, stopping ] diff --git a/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.ts b/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.ts index 9d065e6a923c..b9182788c2e7 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.ts @@ -38,7 +38,7 @@ function isNativeChatStructuredHostCommand( transport: NativeChatStructuredComposerTransport ): boolean { return ( - isStructuredAgentSessionComposerCommand(text, agent) || + isStructuredAgentSessionComposerCommand(text, agent, transport.sessionCommands) || (transport.threadGoal !== undefined && isStructuredAgentSessionGoalCommand(text)) ) } diff --git a/src/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.ts b/src/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.ts index 05f783fb7030..959ba0eaac42 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-structured-composer-transport.ts @@ -60,6 +60,7 @@ export function useNativeChatStructuredComposerTransport(args: { dispatchCommand: (text: string) => dispatchStructuredAgentSessionComposerCommand(text, { agent: props.agent, + sessionCommands: controller.sessionCommands, snapshot: controller.optionSnapshot, invokeAction: async (id) => { setOptionPickerRequest({ id, consume: () => setOptionPickerRequest(null) }) diff --git a/src/shared/structured-agent-session-composer.test.ts b/src/shared/structured-agent-session-composer.test.ts index aaaad68e3054..20c171b4f0af 100644 --- a/src/shared/structured-agent-session-composer.test.ts +++ b/src/shared/structured-agent-session-composer.test.ts @@ -105,13 +105,6 @@ describe('dispatchStructuredAgentSessionComposerCommand', () => { setOption: async () => true } - it('names what does work when a TUI-only command is typed', async () => { - const outcome = await dispatchStructuredAgentSessionComposerCommand('/vim', controller) - expect(outcome.handled).toBe(true) - expect(outcome.error).toBe( - '/vim is not available in chat sessions. Use the slash menu to see available commands.' - ) - }) it.each(['claude', 'codex'] as const)( 'handles %s conversation commands without message fallthrough', async (agent) => { @@ -152,6 +145,52 @@ describe('agent-implemented commands pass through to the agent', () => { } const PASSED_THROUGH = { handled: false, accepted: false, error: null } + it('passes an advertised OMP fast command to the native session', async () => { + expect( + await dispatchStructuredAgentSessionComposerCommand('/fast status', { + ...controller, + agent: 'omp', + sessionCommands: [{ name: 'fast', kind: 'command' }] + }) + ).toEqual(PASSED_THROUGH) + }) + + it('does not infer native fast support from the OMP terminal catalog', async () => { + expect( + ( + await dispatchStructuredAgentSessionComposerCommand('/fast', { + ...controller, + agent: 'omp', + sessionCommands: [] + }) + ).handled + ).toBe(true) + }) + + it('does not treat a skill named fast as native command support', async () => { + expect( + ( + await dispatchStructuredAgentSessionComposerCommand('/fast', { + ...controller, + agent: 'omp', + sessionCommands: [{ name: 'fast', kind: 'skill' }] + }) + ).handled + ).toBe(true) + }) + + it('keeps OMP terminal-only commands claimed when fast is advertised', async () => { + expect( + ( + await dispatchStructuredAgentSessionComposerCommand('/git', { + ...controller, + agent: 'omp', + sessionCommands: [{ name: 'fast', kind: 'command' }] + }) + ).handled + ).toBe(true) + }) + // Claude's harness runs a slash command it finds in the message text, so // claiming these answered "not available" for commands that do work. it.each(['init', 'review', 'help'] as const)( diff --git a/src/shared/structured-agent-session-composer.ts b/src/shared/structured-agent-session-composer.ts index 8ef8bff69a3a..0262db05280f 100644 --- a/src/shared/structured-agent-session-composer.ts +++ b/src/shared/structured-agent-session-composer.ts @@ -6,6 +6,7 @@ import type { AgentType } from './agent-status-types' import type { SessionOptionDescriptor, SessionOptionValue } from './native-chat-session-options' import type { SlashCommandSuggestion } from './native-chat-slash-commands' import type { AgentSessionConversationCommand } from './agent-session-conversation-command' +import type { AgentSessionSlashCommand } from './agent-session-wire' const MODEL_COMMAND: SlashCommandSuggestion = { name: 'model', @@ -30,6 +31,7 @@ export const STRUCTURED_AGENT_SESSION_SLASH_COMMANDS: readonly SlashCommandSugge export type StructuredAgentSessionComposerOptions = { agent?: AgentType + sessionCommands?: readonly AgentSessionSlashCommand[] snapshot: readonly SessionOptionDescriptor[] invokeAction: (id: string) => Promise setOption: (id: string, value: SessionOptionValue) => Promise @@ -108,9 +110,18 @@ function structuredRecognizedCommands(agent: AgentType): readonly SlashCommandSu * would be silently dropped, whereas a pass-through command is a real send. */ export function isStructuredAgentSessionComposerCommand( text: string, - agent: AgentType = 'codex' + agent: AgentType = 'codex', + sessionCommands?: readonly AgentSessionSlashCommand[] ): boolean { const command = commandParts(text) + // OMP advertises this native ACP command; it is not a host option mutation. + if ( + agent === 'omp' && + command?.name === 'fast' && + sessionCommands?.some((entry) => entry.name === 'fast' && entry.kind === 'command') + ) { + return false + } return Boolean( command && structuredRecognizedCommands(agent).some((entry) => entry.name === command.name) ) @@ -178,7 +189,10 @@ export async function dispatchStructuredAgentSessionComposerCommand( error: null } } - if (!command || !isStructuredAgentSessionComposerCommand(text, controller.agent)) { + if ( + !command || + !isStructuredAgentSessionComposerCommand(text, controller.agent, controller.sessionCommands) + ) { return { handled: false, accepted: false, error: null } } if (command.name === 'clear' || command.name === 'compact') {