diff --git a/mobile/src/session/__mocks__/mobile-prompt-controller.ts b/mobile/src/session/__mocks__/mobile-prompt-controller.ts index 20fb46add65..87f21239cac 100644 --- a/mobile/src/session/__mocks__/mobile-prompt-controller.ts +++ b/mobile/src/session/__mocks__/mobile-prompt-controller.ts @@ -83,7 +83,7 @@ vi.mock('../../components/ActionSheetModal', () => ({ ActionSheetModal: 'ActionS vi.mock('../../components/MobileMarkdown', () => ({ MobileMarkdown: 'Markdown' })) export const visible = { value: true } const session = { messages: [], folded: [], status: 'ready', transcriptLoading: false } -const structured = { +const structuredSession = { session, isWorking: false, turnId: null, @@ -102,6 +102,7 @@ const structured = { setStructuredOption: vi.fn(), invokeStructuredOption: vi.fn() } +const sessionCommands = { commands: undefined, refresh: vi.fn() } vi.mock('../use-mobile-session-view-mode', () => ({ useMobileSessionViewMode: () => ({ isTabChatView: () => visible.value, @@ -111,7 +112,7 @@ vi.mock('../use-mobile-session-view-mode', () => ({ }) })) vi.mock('../use-mobile-native-chat-session-lane', () => ({ - useMobileNativeChatSessionLane: () => ({ session, structuredSession: structured }) + useMobileNativeChatSessionLane: () => ({ session, structuredSession, sessionCommands }) })) vi.mock('../native-chat/composer/use-mobile-native-chat-drafts', () => ({ useMobileNativeChatDrafts: () => ({ diff --git a/mobile/src/session/mobile-native-chat-controller-contract.ts b/mobile/src/session/mobile-native-chat-controller-contract.ts index 4bbcc47e646..ad4d117397d 100644 --- a/mobile/src/session/mobile-native-chat-controller-contract.ts +++ b/mobile/src/session/mobile-native-chat-controller-contract.ts @@ -22,6 +22,7 @@ import type { MobileNativeChatPendingMessage } from './native-chat/mobile-native import type { useMobileNativeChatSession } from './use-mobile-native-chat-session' import type { MobileNativeChatSessionOptionPickersProps } from './MobileNativeChatSessionOptionPickers' import type { MobileNativeChatVisualSource } from './mobile-native-chat-visual-read' +import type { useMobileStructuredSessionCommands } from './use-mobile-structured-session-commands' export type MobileNativeChatController = { sessionViewReady: boolean @@ -93,5 +94,6 @@ export type MobileNativeChatController = { // Read parked TUI launch text to clear one line per Ctrl+U before pasting images. readSeededLaunchDraft: () => string | null nativeChatSessionOptions: MobileNativeChatSessionOptionPickersProps | null + nativeChatSessionCommands: ReturnType nativeChatAcceptsImages: boolean } diff --git a/mobile/src/session/mobile-structured-agent-session-contract.ts b/mobile/src/session/mobile-structured-agent-session-contract.ts index 005723bcc44..236e48420bc 100644 --- a/mobile/src/session/mobile-structured-agent-session-contract.ts +++ b/mobile/src/session/mobile-structured-agent-session-contract.ts @@ -1,3 +1,4 @@ +import type { AgentSessionSlashCommand } from '../../../src/shared/agent-session-wire' import type { RpcClient } from '../transport/rpc-client' import type { StructuredAgentSessionHostSupport } from './mobile-structured-agent-session-host-support' import type { MobileNativeChatSendOutcome } from './native-chat/mobile-native-chat-send' @@ -56,6 +57,8 @@ export type MobileStructuredAgentSessionArgs = { /** Capability facts from the shared runtime status probe; null follows the legacy wire. */ hostSupport: StructuredAgentSessionHostSupport | null agent: string | null + /** What the session reported for its `/` menu; undefined until read or on an older host. */ + sessionCommands?: readonly AgentSessionSlashCommand[] appendComposerText?: (text: string) => boolean onSendError: MobileNativeChatSendErrorReporter // Accepted queued actions retire the route’s failure banner. diff --git a/mobile/src/session/mobile-structured-composer-command.test.ts b/mobile/src/session/mobile-structured-composer-command.test.ts index bf41efc55c6..76fffa44af6 100644 --- a/mobile/src/session/mobile-structured-composer-command.test.ts +++ b/mobile/src/session/mobile-structured-composer-command.test.ts @@ -108,6 +108,19 @@ describe('mobile structured conversation commands', () => { expect(input.onError).toHaveBeenCalled() } ) + it('leaves a session-reported command such as OMP /fast to an ordinary send', async () => { + const { input, sendRequest } = setup() + input.text = '/fast' + input.controller.agent = 'omp' + await dispatchMobileStructuredCommand(input) + expect(input.onError).toHaveBeenCalledWith( + expect.stringContaining('/fast is not available'), + undefined + ) + input.controller.sessionCommands = [{ name: 'fast', kind: 'command' }] + expect(await dispatchMobileStructuredCommand(input)).toBeNull() + expect(sendRequest).not.toHaveBeenCalled() + }) it('/clear is a plain command on every host: no withdrawal fields, nothing persisted', async () => { // The host carries cards to replacement sessions; client restoration would duplicate them. const { input, sendRequest } = setup() diff --git a/mobile/src/session/mobile-structured-composer-command.ts b/mobile/src/session/mobile-structured-composer-command.ts index ead086c5045..c90d091dcb9 100644 --- a/mobile/src/session/mobile-structured-composer-command.ts +++ b/mobile/src/session/mobile-structured-composer-command.ts @@ -44,7 +44,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/native-chat/composer/use-mobile-native-chat-structured-composer.ts b/mobile/src/session/native-chat/composer/use-mobile-native-chat-structured-composer.ts index 89b71d40c80..a538f8106ac 100644 --- a/mobile/src/session/native-chat/composer/use-mobile-native-chat-structured-composer.ts +++ b/mobile/src/session/native-chat/composer/use-mobile-native-chat-structured-composer.ts @@ -1,31 +1,22 @@ import { useMemo } from 'react' import type { AgentSessionConversationCommand } from '../../../../../src/shared/agent-session/agent-session-conversation-command' -import type { RpcClient } from '../../../transport/rpc-client' import type { MobileNativeChatStructuredComposer } from './MobileNativeChatComposer' import type { MobileNativeChatFileAttachments } from './use-mobile-native-chat-file-attachments' -import { useMobileStructuredSessionCommands } from '../../use-mobile-structured-session-commands' +import type { useMobileStructuredSessionCommands } from '../../use-mobile-structured-session-commands' const NO_COMMANDS: readonly AgentSessionConversationCommand[] = [] export function useMobileNativeChatStructuredComposer(args: { - client: RpcClient | null - hostId: string | null - sessionId: string | null structured: boolean - connected: boolean + sessionCommands: ReturnType /** The host keeps files a phone uploads; an older one refuses the upload. */ acceptsFiles: boolean conversationCommands: readonly AgentSessionConversationCommand[] | undefined files: MobileNativeChatFileAttachments }): MobileNativeChatStructuredComposer | null { - const { client, hostId, sessionId, structured, connected, acceptsFiles, files } = args + const { structured, acceptsFiles, files } = args const conversationCommands = args.conversationCommands ?? NO_COMMANDS - const { commands, refresh } = useMobileStructuredSessionCommands({ - client, - hostId, - sessionId, - enabled: structured && connected - }) + const { commands, refresh } = args.sessionCommands const { files: pendingFiles, attachFiles, removeFiles } = files // Memoize commands separately so attachment changes cannot rebuild the menu catalog. const catalog = useMemo( diff --git a/mobile/src/session/use-mobile-native-chat-controller.test.ts b/mobile/src/session/use-mobile-native-chat-controller.test.ts index 5473ebc672f..2d5ea171644 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.test.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.test.ts @@ -81,6 +81,9 @@ vi.mock('./use-mobile-session-view-mode', () => ({ toggleTabChatView: vi.fn() }) })) +vi.mock('./use-mobile-structured-session-commands', () => ({ + useMobileStructuredSessionCommands: () => ({ commands: undefined, refresh: vi.fn() }) +})) vi.mock('./use-mobile-native-chat-session', () => ({ useMobileNativeChatSession: () => sessionState })) diff --git a/mobile/src/session/use-mobile-native-chat-controller.ts b/mobile/src/session/use-mobile-native-chat-controller.ts index 158ec1a0e6b..8a1af933a95 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.ts @@ -54,7 +54,6 @@ export function useMobileNativeChatController(args: { onSendResolved } = args const { - activeChatAgent, activeChatAgentConfirmed, activeChatAgentRef, activeChatResolution, @@ -75,22 +74,20 @@ export function useMobileNativeChatController(args: { // Fixed hook order mounts the lane before drafts; defer Edit’s append through a ref and delete nothing until copying succeeds. const appendComposerTextRef = useRef<(text: string) => boolean>(() => false) - const { structuredSession: structuredNativeChat, session: nativeChatSession } = - useMobileNativeChatSessionLane({ - client, - structured: activeChatStructured, - agent: activeChatAgent, - resolvedAgent: activeChatResolution?.agent ?? null, - transcriptPath: activeChatResolution?.transcriptPath ?? null, - sessionId: activeChatSessionId, - sourceIdentity, - enabled: showNativeChat, - connState, - hostSupport: agentSessionHostSupport, - appendComposerTextRef, - onSendError, - onActionResolved: onSendResolved - }) + const nativeChatLane = useMobileNativeChatSessionLane({ + client, + hostId, + structured: activeChatStructured, + resolution: activeChatResolution, + sourceIdentity, + enabled: showNativeChat, + connState, + hostSupport: agentSessionHostSupport, + appendComposerTextRef, + onSendError, + onActionResolved: onSendResolved + }) + const { structuredSession: structuredNativeChat, session: nativeChatSession } = nativeChatLane const { composerText: chatComposerText, setComposerText: setChatComposerText, @@ -207,6 +204,7 @@ export function useMobileNativeChatController(args: { const structuredNativeChatSend = useMobileStructuredNativeChatSendBridge({ agent: activeChatResolution?.agent, + sessionCommands: nativeChatLane.sessionCommands.commands, sendStructured: structuredNativeChat.sendWithOutcome, captureSendOrigin, clearDraftForSend, @@ -317,6 +315,7 @@ export function useMobileNativeChatController(args: { : handleNativeChatSendWithOutcome, readSeededLaunchDraft, nativeChatSessionOptions, + nativeChatSessionCommands: nativeChatLane.sessionCommands, nativeChatAcceptsImages: !activeChatStructured || structuredNativeChat.acceptsImages } } diff --git a/mobile/src/session/use-mobile-native-chat-session-lane.ts b/mobile/src/session/use-mobile-native-chat-session-lane.ts index f3b320d4dcf..b1fb1138f2f 100644 --- a/mobile/src/session/use-mobile-native-chat-session-lane.ts +++ b/mobile/src/session/use-mobile-native-chat-session-lane.ts @@ -4,16 +4,16 @@ import type { ConnectionState } from '../transport/types' import type { StructuredAgentSessionHostSupport } from './mobile-structured-agent-session-host-support' import { useMobileNativeChatSession } from './use-mobile-native-chat-session' import { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session' +import { useMobileStructuredSessionCommands } from './use-mobile-structured-session-commands' +import type { MobileNativeChatResolution } from './native-chat/mobile-native-chat-eligibility' import type { MobileNativeChatSendErrorReporter } from './native-chat/use-mobile-native-chat-send-error' // Run both hooks in fixed order and starve the inactive lane’s identity so flips preserve cached transcripts. export function useMobileNativeChatSessionLane({ client, + hostId, structured, - agent, - resolvedAgent, - transcriptPath, - sessionId, + resolution, sourceIdentity, hostSupport, appendComposerTextRef, @@ -23,11 +23,9 @@ export function useMobileNativeChatSessionLane({ onActionResolved }: { client: RpcClient | null + hostId: string structured: boolean - agent: string | null - resolvedAgent: string | null - transcriptPath: string | null - sessionId: string | null + resolution: MobileNativeChatResolution | null sourceIdentity: Parameters[0]['sourceIdentity'] hostSupport: StructuredAgentSessionHostSupport | null // Defer Edit through the composer ref because drafts mount after this lane. @@ -39,17 +37,27 @@ export function useMobileNativeChatSessionLane({ }): { structuredSession: ReturnType session: ReturnType + sessionCommands: ReturnType } { const appendComposerText = useCallback( (text: string) => appendComposerTextRef.current(text), [appendComposerTextRef] ) + const agent = resolution?.agent ?? null + const sessionId = resolution?.sessionId ?? null + // One read serves both send-side ownership and the composer's `/` menu. + const sessionCommands = useMobileStructuredSessionCommands({ + client, + hostId, + sessionId: structured ? sessionId : null, + enabled: connState === 'connected' + }) const bridgeSession = useMobileNativeChatSession({ client, sourceIdentity, - agent: structured ? null : resolvedAgent, + agent: structured ? null : agent, sessionId: structured ? null : sessionId, - transcriptPath: structured ? null : transcriptPath + transcriptPath: structured ? null : (resolution?.transcriptPath ?? null) }) const structuredSession = useMobileStructuredAgentSession({ client, @@ -62,11 +70,13 @@ export function useMobileNativeChatSessionLane({ // reacquire the provider without clearing the cached transcript. connected: connState === 'connected', agent: structured ? agent : null, + sessionCommands: sessionCommands.commands, onSendError, ...(onActionResolved ? { onActionResolved } : {}) }) return { structuredSession, - session: structured ? structuredSession.session : bridgeSession + session: structured ? structuredSession.session : bridgeSession, + sessionCommands } } diff --git a/mobile/src/session/use-mobile-session-attachments.ts b/mobile/src/session/use-mobile-session-attachments.ts index 2e5bb595243..4f9814cdc84 100644 --- a/mobile/src/session/use-mobile-session-attachments.ts +++ b/mobile/src/session/use-mobile-session-attachments.ts @@ -109,11 +109,8 @@ export function useMobileSessionAttachments(scope: MobileSessionAccessorySelecti onError: triggerError }) const nativeChatStructuredComposer = useMobileNativeChatStructuredComposer({ - client, - hostId, - sessionId: nativeChatSessionId, structured: structuredNativeChat, - connected: connState === 'connected', + sessionCommands: nativeChatController.nativeChatSessionCommands, acceptsFiles: agentSessionHostSupport?.phoneAttachments === true, conversationCommands: nativeChatController.nativeChatSessionOptions?.controller.conversationCommands, diff --git a/mobile/src/session/use-mobile-structured-agent-session.ts b/mobile/src/session/use-mobile-structured-agent-session.ts index adc55580d06..791915909bb 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.ts +++ b/mobile/src/session/use-mobile-structured-agent-session.ts @@ -191,6 +191,7 @@ export function useMobileStructuredAgentSession( hold: sends.hold, commandPending: commandPendingRef, controller: sendController, + sessionCommands: args.sessionCommands, onSendError, sendOptions: hostSupport?.sendOptions === true ? options.sendSelection : undefined }) 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 6609fdfa9af..834f997c2a3 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 './native-chat/mobile-native-chat-send' import type { MobileNativeChatSendOrigin } from './native-chat/mobile-native-chat-pending-echo' @@ -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[], @@ -32,7 +34,8 @@ export function useMobileStructuredNativeChatSendBridge(args: { clearDraftForSend, onSendError, restoreRejectedDraft, - sendStructured + sendStructured, + sessionCommands } = args const sendWithOutcome = useCallback( async ( @@ -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 @@ -62,7 +65,15 @@ export function useMobileStructuredNativeChatSendBridge(args: { } return outcome }, - [agent, captureSendOrigin, clearDraftForSend, onSendError, restoreRejectedDraft, sendStructured] + [ + agent, + captureSendOrigin, + clearDraftForSend, + onSendError, + restoreRejectedDraft, + sendStructured, + sessionCommands + ] ) 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 01f3df3663e..d6fe941dbeb 100644 --- a/mobile/src/session/use-mobile-structured-send-with-outcome.ts +++ b/mobile/src/session/use-mobile-structured-send-with-outcome.ts @@ -8,6 +8,7 @@ import { type StructuredAgentSessionAttachment } from '../../../src/shared/structured-agent-session-send-mutation' import type { StructuredAgentSessionComposerOptions } from '../../../src/shared/structured-agent-session-composer' +import type { AgentSessionSlashCommand } from '../../../src/shared/agent-session-wire' import type { StructuredAgentSessionState } from '../../../src/shared/structured-agent-session-reducer' import type { RpcClient } from '../transport/rpc-client' import type { MobileNativeChatSendOutcome } from './native-chat/mobile-native-chat-send' @@ -53,6 +54,7 @@ export function useMobileStructuredSendWithOutcome(args: { StructuredAgentSessionComposerOptions, 'snapshot' | 'setOption' | 'invokeAction' | 'conversationCommands' > + sessionCommands?: readonly AgentSessionSlashCommand[] onSendError: MobileNativeChatSendErrorReporter // Older hosts accept neither per-chat model nor effort. sendOptions?: () => Readonly> | undefined @@ -74,6 +76,7 @@ export function useMobileStructuredSendWithOutcome(args: { onSendError, queueCapable, sendOptions, + sessionCommands, sessionId, stateRef, stopping @@ -107,7 +110,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) @@ -180,6 +183,7 @@ export function useMobileStructuredSendWithOutcome(args: { onSendError, queueCapable, sendOptions, + sessionCommands, sessionId, 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 8e7ad36f2f3..a3b315023df 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 @@ -47,7 +47,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 7d31bf3aee6..aad904a609e 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 @@ -86,6 +86,7 @@ export function useNativeChatStructuredComposerTransport(args: { return true }, setOption: controller.setStructuredOption, + sessionCommands: controller.sessionCommands, conversationCommands: controller.conversationCommands, runConversationCommand: controller.runConversationCommand, ...(setThreadGoalObjective ? { setThreadGoalObjective } : {}) diff --git a/src/shared/structured-agent-session-composer.test.ts b/src/shared/structured-agent-session-composer.test.ts index d809a59bc1d..d3585d6bbf4 100644 --- a/src/shared/structured-agent-session-composer.test.ts +++ b/src/shared/structured-agent-session-composer.test.ts @@ -264,6 +264,25 @@ describe('agent-implemented commands pass through to the agent', () => { expect(setThreadGoalObjective).not.toHaveBeenCalled() }) + // OMP's ACP catalog advertises only commands it handles itself, e.g. /fast. + it('passes a session-reported command on to the session that advertised it', async () => { + const sessionCommands = [ + { name: 'fast', kind: 'command' }, + { name: 'compact', kind: 'command' } + ] as const + expect(isStructuredAgentSessionComposerCommand('/fast', 'omp')).toBe(true) + expect(isStructuredAgentSessionComposerCommand('/fast', 'omp', sessionCommands)).toBe(false) + expect( + await dispatchStructuredAgentSessionComposerCommand('/fast status', { + ...controller, + agent: 'omp', + sessionCommands + }) + ).toEqual(PASSED_THROUGH) + // Orca still runs its own commands even when the session reports them. + expect(isStructuredAgentSessionComposerCommand('/compact', 'omp', sessionCommands)).toBe(true) + }) + it('keeps refusing a Codex command the model cannot carry out', async () => { expect(isStructuredAgentSessionComposerCommand('/permissions', 'codex')).toBe(true) expect( diff --git a/src/shared/structured-agent-session-composer.ts b/src/shared/structured-agent-session-composer.ts index b0d4b6eb841..98bfa427cf4 100644 --- a/src/shared/structured-agent-session-composer.ts +++ b/src/shared/structured-agent-session-composer.ts @@ -37,6 +37,8 @@ export type StructuredAgentSessionComposerOptions = { snapshot: readonly SessionOptionDescriptor[] invokeAction: (id: string) => Promise setOption: (id: string, value: SessionOptionValue) => Promise + /** What the running session reported for its `/` menu; undefined until read or on an older host. */ + sessionCommands?: readonly AgentSessionSlashCommand[] conversationCommands?: readonly AgentSessionConversationCommand[] runConversationCommand?: ( command: AgentSessionConversationCommand @@ -106,12 +108,19 @@ export function structuredSessionSlashCommands( ] } -/** Claim hidden TUI-only commands too, preventing literal prose delivery; leave agent-handled commands unclaimed for passthrough. */ -function structuredRecognizedCommands(agent: AgentType): readonly SlashCommandSuggestion[] { +/** Claim hidden TUI-only commands too, preventing literal prose delivery; leave agent-handled + * and session-reported commands unclaimed for passthrough. */ +function structuredRecognizedCommands( + agent: AgentType, + reported: readonly AgentSessionSlashCommand[] | undefined +): readonly SlashCommandSuggestion[] { + const sessionHandled = new Set( + reported?.filter((entry) => entry.kind === 'command').map((entry) => entry.name) + ) return [ ...STRUCTURED_AGENT_SESSION_SLASH_COMMANDS, ...CONVERSATION_COMMANDS, - ...getHostClaimedNativeChatCommands(agent) + ...getHostClaimedNativeChatCommands(agent).filter((entry) => !sessionHandled.has(entry.name)) ] } @@ -120,11 +129,13 @@ 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', + reported?: readonly AgentSessionSlashCommand[] ): boolean { const command = commandParts(text) return Boolean( - command && structuredRecognizedCommands(agent).some((entry) => entry.name === command.name) + command && + structuredRecognizedCommands(agent, reported).some((entry) => entry.name === command.name) ) } @@ -189,7 +200,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') {