diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index ffcd90f0e69..4ece7442bb1 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -4651,6 +4651,7 @@ export default function SessionScreen() { ) : activeStructuredTab ? ( { act(() => { renderer = create( createElement(MobileStructuredAgentSessionView, { + agent: 'codex', items: [], status: 'ready', hasOlder: false, @@ -108,4 +109,29 @@ describe('MobileStructuredAgentSessionView command seam', () => { renderer!.root.findByProps({ accessibilityRole: 'alert' }).findByType('Text').children ).toEqual(['/review is not available in chat sessions.']) }) + + it('uses Claude branding and command suggestions for Claude sessions', () => { + act(() => { + renderer!.update( + createElement(MobileStructuredAgentSessionView, { + ...renderer!.root.findByType(MobileStructuredAgentSessionView).props, + agent: 'claude' + }) + ) + }) + + const composer = renderer!.root.findByType('MobileNativeChatComposer') + expect(composer.props).toMatchObject({ agent: 'claude', placeholder: 'Message Claude' }) + expect(composer.props.slashCommands.map((command: { name: string }) => command.name)).toEqual([ + 'model', + 'effort', + 'clear', + 'compact', + 'init', + 'review', + 'help' + ]) + const empty = renderer!.root.findByType('FlatList').props.ListEmptyComponent + expect(empty.props.children[0].props.children).toBe('New Claude chat') + }) }) diff --git a/mobile/src/session/MobileStructuredAgentSessionView.tsx b/mobile/src/session/MobileStructuredAgentSessionView.tsx index a8733f55306..8165e7b0d2b 100644 --- a/mobile/src/session/MobileStructuredAgentSessionView.tsx +++ b/mobile/src/session/MobileStructuredAgentSessionView.tsx @@ -40,10 +40,12 @@ import { useMobileStructuredComposerState } from './use-mobile-structured-compos import { dispatchMobileStructuredComposerCommand, isMobileStructuredComposerCommand, - MOBILE_STRUCTURED_SLASH_COMMANDS + mobileStructuredSlashCommands } from './mobile-structured-composer-command' +import type { MobileStructuredAgent } from './mobile-structured-session-create' type Props = { + agent: MobileStructuredAgent items: AgentJournalRenderItem[] status: 'idle' | 'loading' | 'ready' | 'error' error?: string @@ -75,6 +77,8 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen const [commandError, setCommandError] = useState(null) const turnId = activeMobileStructuredTurnId(props.items) const allAttachments = [...restored, ...props.attachments] + const agentName = props.agent === 'claude' ? 'Claude' : 'Codex' + const slashCommands = useMemo(() => mobileStructuredSlashCommands(props.agent), [props.agent]) const rows = useMemo( () => buildMobileStructuredTimeline(props.items, props.outbox), [props.items, props.outbox] @@ -179,7 +183,7 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen ListEmptyComponent={ - {props.status === 'error' ? 'Conversation unavailable' : 'New Codex chat'} + {props.status === 'error' ? 'Conversation unavailable' : `New ${agentName} chat`} {props.error ?? 'Send a message to get started.'} @@ -206,11 +210,15 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen value={composerText} onChangeText={setComposerText} onSend={async (text) => { - if (allAttachments.length > 0 && isMobileStructuredComposerCommand(text)) { + if (allAttachments.length > 0 && isMobileStructuredComposerCommand(text, props.agent)) { setCommandError('Remove attachments before using a chat-session command.') return false } - const command = await dispatchMobileStructuredComposerCommand(text, props.sessionOptions) + const command = await dispatchMobileStructuredComposerCommand( + text, + props.sessionOptions, + props.agent + ) if (command.handled) { setCommandError(command.error) if (command.accepted) { @@ -226,8 +234,8 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen } return accepted }} - agent="codex" - slashCommands={MOBILE_STRUCTURED_SLASH_COMMANDS} + agent={props.agent} + slashCommands={slashCommands} sessionOptions={{ controller: props.sessionOptions, isWorking: false }} onAttachImage={props.onAttachImage} attachments={allAttachments} @@ -240,7 +248,7 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen }} isAttaching={props.isAttaching} disabled={props.status !== 'ready'} - placeholder={props.status === 'ready' ? 'Message Codex' : 'Reconnecting…'} + placeholder={props.status === 'ready' ? `Message ${agentName}` : 'Reconnecting…'} /> ) diff --git a/mobile/src/session/mobile-structured-composer-command.test.ts b/mobile/src/session/mobile-structured-composer-command.test.ts index f5c48900564..08657d63621 100644 --- a/mobile/src/session/mobile-structured-composer-command.test.ts +++ b/mobile/src/session/mobile-structured-composer-command.test.ts @@ -46,12 +46,12 @@ describe('structured composer slash dispatch', () => { it('opens live model and effort pickers instead of sending command text', async () => { const options = controller() - expect(await dispatchMobileStructuredComposerCommand('/model', options)).toEqual({ + expect(await dispatchMobileStructuredComposerCommand('/model', options, 'codex')).toEqual({ handled: true, accepted: true, error: null }) - expect(await dispatchMobileStructuredComposerCommand('/effort', options)).toEqual({ + expect(await dispatchMobileStructuredComposerCommand('/effort', options, 'codex')).toEqual({ handled: true, accepted: true, error: null @@ -63,15 +63,15 @@ describe('structured composer slash dispatch', () => { it('applies command arguments through structured setOption', async () => { const options = controller() - await dispatchMobileStructuredComposerCommand('/model GPT Next', options) - await dispatchMobileStructuredComposerCommand('/effort high', options) + await dispatchMobileStructuredComposerCommand('/model GPT Next', options, 'codex') + await dispatchMobileStructuredComposerCommand('/effort high', options, 'claude') expect(options.setOption).toHaveBeenNthCalledWith(1, 'model', 'gpt-next') expect(options.setOption).toHaveBeenNthCalledWith(2, 'effort', 'high') }) it('visibly refuses every other advertised Codex command', async () => { - const result = await dispatchMobileStructuredComposerCommand('/review', controller()) + const result = await dispatchMobileStructuredComposerCommand('/review', controller(), 'codex') expect(result).toEqual({ handled: true, accepted: true, @@ -81,7 +81,7 @@ describe('structured composer slash dispatch', () => { it('leaves unadvertised slash text available as an ordinary prompt', async () => { await expect( - dispatchMobileStructuredComposerCommand('/not-a-catalog-command', controller()) + dispatchMobileStructuredComposerCommand('/not-a-catalog-command', controller(), 'claude') ).resolves.toEqual({ handled: false, accepted: false, error: null }) }) }) diff --git a/mobile/src/session/mobile-structured-composer-command.ts b/mobile/src/session/mobile-structured-composer-command.ts index 6c36fb4ce0a..0c6f4488cb6 100644 --- a/mobile/src/session/mobile-structured-composer-command.ts +++ b/mobile/src/session/mobile-structured-composer-command.ts @@ -1,17 +1,28 @@ import { getVerifiedNativeChatCommands } from '../../../src/shared/native-chat-agent-profiles' import type { SlashCommandSuggestion } from '../../../src/shared/native-chat-slash-commands' import type { MobileNativeChatSessionOptionsController } from './use-mobile-native-chat-session-options' +import type { MobileStructuredAgent } from './mobile-structured-session-create' +const MODEL_COMMAND: SlashCommandSuggestion = { + name: 'model', + description: 'Choose the model' +} const EFFORT_COMMAND: SlashCommandSuggestion = { name: 'effort', description: 'Choose reasoning effort' } -export const MOBILE_STRUCTURED_SLASH_COMMANDS: readonly SlashCommandSuggestion[] = [ - ...getVerifiedNativeChatCommands('codex').slice(0, 1), - EFFORT_COMMAND, - ...getVerifiedNativeChatCommands('codex').slice(1) -] +export function mobileStructuredSlashCommands( + agent: MobileStructuredAgent +): readonly SlashCommandSuggestion[] { + return [ + MODEL_COMMAND, + EFFORT_COMMAND, + ...getVerifiedNativeChatCommands(agent).filter( + (command) => command.name !== 'model' && command.name !== 'effort' + ) + ] +} export type MobileStructuredCommandOutcome = { handled: boolean @@ -27,10 +38,13 @@ function commandParts(text: string): { name: string; argument: string } | null { return match ? { name: match[1]!.toLowerCase(), argument: match[2]?.trim() ?? '' } : null } -export function isMobileStructuredComposerCommand(text: string): boolean { +export function isMobileStructuredComposerCommand( + text: string, + agent: MobileStructuredAgent +): boolean { const command = commandParts(text) return Boolean( - command && MOBILE_STRUCTURED_SLASH_COMMANDS.some((entry) => entry.name === command.name) + command && mobileStructuredSlashCommands(agent).some((entry) => entry.name === command.name) ) } @@ -44,10 +58,11 @@ function unavailable(name: string): MobileStructuredCommandOutcome { export async function dispatchMobileStructuredComposerCommand( text: string, - controller: MobileNativeChatSessionOptionsController + controller: MobileNativeChatSessionOptionsController, + agent: MobileStructuredAgent ): Promise { const command = commandParts(text) - if (!command || !isMobileStructuredComposerCommand(text)) { + if (!command || !isMobileStructuredComposerCommand(text, agent)) { return { handled: false, accepted: false, error: null } } if (command.name !== 'model' && command.name !== 'effort') {