Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/site/content/docs/agents/native-chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>`), 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +21,7 @@ import type { MobileStructuredBackgroundTasks } from './use-mobile-structured-ba
export type StructuredMobileSession = ReturnType<typeof useMobileStructuredAgentOptions> &
ReturnType<typeof useMobileStructuredAgentTurnTiming> & {
session: MobileNativeChatSession
sessionCommands?: readonly AgentSessionSlashCommand[]
isWorking: boolean
turnId: string | null
/** What labels the live turn's one indicator row. */
Expand Down
8 changes: 7 additions & 1 deletion mobile/src/session/mobile-structured-composer-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions mobile/src/session/use-mobile-native-chat-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export function useMobileNativeChatController(args: {

const structuredNativeChatSend = useMobileStructuredNativeChatSendBridge({
agent: activeChatResolution?.agent,
sessionCommands: structuredNativeChat.sessionCommands,
sendStructured: structuredNativeChat.sendWithOutcome,
captureSendOrigin,
clearDraftForSend,
Expand Down
3 changes: 3 additions & 0 deletions mobile/src/session/use-mobile-structured-agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -197,6 +198,7 @@ export function useMobileStructuredAgentSession(
hold: sends.hold,
commandPending: commandPendingRef,
controller: sendController,
sessionCommands,
onSendError,
carriesOptions: hostSupport?.sendOptions === true
})
Expand Down Expand Up @@ -283,6 +285,7 @@ export function useMobileStructuredAgentSession(

return {
...options,
sessionCommands,
visualSource,
imagePreviewsByMessageId: sends.imagePreviewsByMessageId,
session: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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'
import type { StructuredMobileSendAttachment } from './use-mobile-structured-send-with-outcome'

export function useMobileStructuredNativeChatSendBridge(args: {
agent: StructuredAgentId | undefined
sessionCommands?: readonly AgentSessionSlashCommand[]
sendStructured: (
text: string,
images?: string[],
Expand All @@ -28,6 +30,7 @@ export function useMobileStructuredNativeChatSendBridge(args: {
} {
const {
agent,
sessionCommands,
captureSendOrigin,
clearDraftForSend,
onSendError,
Expand All @@ -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
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -85,6 +86,7 @@ export function useMobileStructuredSendWithOutcome(args: {
queueCapable,
carriesOptions,
sessionId,
sessionCommands,
stateRef,
stopping
} = args
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -196,6 +198,7 @@ export function useMobileStructuredSendWithOutcome(args: {
queueCapable,
carriesOptions,
sessionId,
sessionCommands,
stateRef,
stopping
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function isNativeChatStructuredHostCommand(
transport: NativeChatStructuredComposerTransport
): boolean {
return (
isStructuredAgentSessionComposerCommand(text, agent) ||
isStructuredAgentSessionComposerCommand(text, agent, transport.sessionCommands) ||
(transport.threadGoal !== undefined && isStructuredAgentSessionGoalCommand(text))
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) })
Expand Down
53 changes: 46 additions & 7 deletions src/shared/structured-agent-session-composer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)(
Expand Down
18 changes: 16 additions & 2 deletions src/shared/structured-agent-session-composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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<boolean>
setOption: (id: string, value: SessionOptionValue) => Promise<boolean>
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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') {
Expand Down
Loading