mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(native-chat): pass session-reported slash commands through to the session (#34048)
OMP's ACP session advertises commands it handles itself (e.g. /fast), but native chat claimed them as host-only TUI commands and refused them as unavailable. Commands the running session reports in its slash menu are now left unclaimed so they send through, on desktop and mobile. Mobile reads the session command list once in the native-chat lane and shares it between the send path and the composer menu.
This commit is contained in:
@@ -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: () => ({
|
||||
|
||||
@@ -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<typeof useMobileStructuredSessionCommands>
|
||||
nativeChatAcceptsImages: boolean
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+4
-13
@@ -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<typeof useMobileStructuredSessionCommands>
|
||||
/** 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(
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof useMobileNativeChatSession>[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<typeof useMobileStructuredAgentSession>
|
||||
session: ReturnType<typeof useMobileNativeChatSession>
|
||||
sessionCommands: ReturnType<typeof useMobileStructuredSessionCommands>
|
||||
} {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<Record<string, string>> | 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
|
||||
|
||||
@@ -47,7 +47,7 @@ function isNativeChatStructuredHostCommand(
|
||||
transport: NativeChatStructuredComposerTransport
|
||||
): boolean {
|
||||
return (
|
||||
isStructuredAgentSessionComposerCommand(text, agent) ||
|
||||
isStructuredAgentSessionComposerCommand(text, agent, transport.sessionCommands) ||
|
||||
(transport.threadGoal !== undefined && isStructuredAgentSessionGoalCommand(text))
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -86,6 +86,7 @@ export function useNativeChatStructuredComposerTransport(args: {
|
||||
return true
|
||||
},
|
||||
setOption: controller.setStructuredOption,
|
||||
sessionCommands: controller.sessionCommands,
|
||||
conversationCommands: controller.conversationCommands,
|
||||
runConversationCommand: controller.runConversationCommand,
|
||||
...(setThreadGoalObjective ? { setThreadGoalObjective } : {})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -37,6 +37,8 @@ export type StructuredAgentSessionComposerOptions = {
|
||||
snapshot: readonly SessionOptionDescriptor[]
|
||||
invokeAction: (id: string) => Promise<boolean>
|
||||
setOption: (id: string, value: SessionOptionValue) => Promise<boolean>
|
||||
/** 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') {
|
||||
|
||||
Reference in New Issue
Block a user