mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(mobile): keep structured chat provider-aware
This commit is contained in:
@@ -4651,6 +4651,7 @@ export default function SessionScreen() {
|
||||
) : activeStructuredTab ? (
|
||||
<MobileStructuredAgentSessionView
|
||||
key={activeStructuredTab.sessionId}
|
||||
agent={activeStructuredTab.agent}
|
||||
items={structuredSessionEntry.session.items}
|
||||
status={structuredSessionEntry.session.status}
|
||||
error={structuredSessionEntry.session.error}
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('MobileStructuredAgentSessionView command seam', () => {
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string | null>(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={
|
||||
<View style={styles.center}>
|
||||
<Text style={styles.title}>
|
||||
{props.status === 'error' ? 'Conversation unavailable' : 'New Codex chat'}
|
||||
{props.status === 'error' ? 'Conversation unavailable' : `New ${agentName} chat`}
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>{props.error ?? 'Send a message to get started.'}</Text>
|
||||
</View>
|
||||
@@ -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…'}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<MobileStructuredCommandOutcome> {
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user