mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
* feat(mobile): add Quick Commands (terminal + agent-prompt presets)
Brings the desktop Terminal Quick Commands feature to mobile: saved
agent-prompt or terminal-command presets that launch a new terminal tab.
Entry point sits in the session tab strip next to the "+" new-terminal
button (with a divider) — quick commands spawn a tab, so they live with
tab creation, mirroring desktop's tab-bar split button.
- Launcher button + Quick Commands bottom sheet (search, This project /
Global groups, run/edit/delete rows, add row).
- Add/Edit sheet mirroring desktop TerminalQuickCommandDialog: Label,
Action toggle (Terminal Command | Agent Prompt), Agent select, Prompt /
Command Text, Advanced (Append Enter, Scope Global/Project), validation
and save-failure feedback.
- Launch reuses handleCreateTerminal (extended with enter + toast copy):
agent prompts launch the agent then deliver the prompt; terminal
commands run the (Enter-appended) command text.
- Expose terminalQuickCommands over the remote/mobile RPC surface
(getClientSettings/updateClientSettings allowlists, RuntimeStore type,
and the strict SettingsUpdate zod schema).
- Mirror the agent-prompt support predicate mobile-side (stdin-after-start
agents are unsupported) with a parity test guarding drift from desktop.
- Mock server: sample quick commands + settings.update handler for QA.
* fix(mobile): harden quick command execution
* fix(mobile): harden quick command persistence and launch
* test(mobile): preserve unexpected quick command errors
* fix(mobile): harden quick command launch performance
* fix(runtime): reject malformed quick command updates
* refactor(mobile): reuse shared quick-command logic instead of mirroring
The mobile quick-commands mirror was built on a false premise — that
runtime-importing src/shared/terminal-quick-commands breaks the RN bundle
/ Vitest. It doesn't: tui-agent-config → orca-cli-command-name is a pure
leaf with no module-load Node APIs (verified via probe + bundle-graph).
- Mobile now reuses the canonical desktop helpers (action/agent/scope/
matchesRepo/support/flatten) directly from src/shared; only genuinely
mobile-specific pieces (agent-branded labels, native row truncation,
the launch plan) stay local.
- Multiline runnable terminal commands now flatten via the shared
flattenTerminalQuickCommand (";"-join) — unity with desktop, so a
command saved on one runs identically on the other.
- Drop the MOBILE_TUI_AGENT_PROMPT_COMMAND_UNSUPPORTED mirror + its parity
test; use the shared supportsTerminalAgentQuickCommand predicate.
- Export the shared MAX_QUICK_COMMAND_* length caps for reuse.
* fix(mobile): protect quick command data boundaries
* fix(mobile): enforce quick command limits
* fix(mobile): make quick command updates atomic
* fix(mobile): keep quick command filters recoverable
* fix(mobile): use filled play icon for quick commands
* Revert "fix(mobile): use filled play icon for quick commands"
This reverts commit 169bf053b0.
* fix(mobile): gate quick commands on host capability
91 lines
2.3 KiB
TypeScript
91 lines
2.3 KiB
TypeScript
import { createElement } from 'react'
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { QUICK_COMMAND_SEARCH_QUERY_MAX_LENGTH, QuickCommandsList } from './QuickCommandsList'
|
|
|
|
vi.mock('react-native', () => ({
|
|
ActivityIndicator: 'ActivityIndicator',
|
|
Pressable: 'Pressable',
|
|
StyleSheet: {
|
|
create: <T>(styles: T) => styles,
|
|
hairlineWidth: 1
|
|
},
|
|
Text: 'Text',
|
|
TextInput: 'TextInput',
|
|
View: 'View'
|
|
}))
|
|
|
|
vi.mock('lucide-react-native', () => ({
|
|
Check: 'Check',
|
|
Pencil: 'Pencil',
|
|
Play: 'Play',
|
|
Plus: 'Plus',
|
|
Search: 'Search',
|
|
Trash2: 'Trash2'
|
|
}))
|
|
|
|
vi.mock('../components/MobileAgentIcon', () => ({ MobileAgentIcon: 'MobileAgentIcon' }))
|
|
|
|
describe('QuickCommandsList search', () => {
|
|
let renderer: ReactTestRenderer | null = null
|
|
|
|
beforeEach(() => {
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
|
})
|
|
|
|
afterEach(() => {
|
|
act(() => renderer?.unmount())
|
|
renderer = null
|
|
})
|
|
|
|
it('keeps an active filter clearable when only one command remains', async () => {
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsList, {
|
|
repoCommands: [],
|
|
globalCommands: [],
|
|
totalCount: 1,
|
|
query: 'missing',
|
|
loading: false,
|
|
disabled: false,
|
|
canAdd: true,
|
|
error: null,
|
|
onQueryChange: vi.fn(),
|
|
onLaunch: vi.fn(),
|
|
onEdit: vi.fn(),
|
|
onDelete: vi.fn(),
|
|
onAdd: vi.fn()
|
|
})
|
|
)
|
|
})
|
|
|
|
const search = renderer!.root.findByType('TextInput')
|
|
expect(search.props.value).toBe('missing')
|
|
expect(search.props.maxLength).toBe(QUICK_COMMAND_SEARCH_QUERY_MAX_LENGTH)
|
|
})
|
|
|
|
it('omits idle search chrome for a single command', async () => {
|
|
await act(async () => {
|
|
renderer = create(
|
|
createElement(QuickCommandsList, {
|
|
repoCommands: [],
|
|
globalCommands: [],
|
|
totalCount: 1,
|
|
query: '',
|
|
loading: false,
|
|
disabled: false,
|
|
canAdd: true,
|
|
error: null,
|
|
onQueryChange: vi.fn(),
|
|
onLaunch: vi.fn(),
|
|
onEdit: vi.fn(),
|
|
onDelete: vi.fn(),
|
|
onAdd: vi.fn()
|
|
})
|
|
)
|
|
})
|
|
|
|
expect(renderer!.root.findAllByType('TextInput')).toHaveLength(0)
|
|
})
|
|
})
|