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
117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
import type {
|
|
TerminalQuickCommand,
|
|
TerminalQuickCommandAction,
|
|
TerminalQuickCommandScope,
|
|
TuiAgent
|
|
} from '../../../src/shared/types'
|
|
import {
|
|
isAgentQuickCommand,
|
|
MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH,
|
|
MAX_QUICK_COMMAND_LABEL_LENGTH,
|
|
MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH,
|
|
supportsTerminalAgentQuickCommand
|
|
} from '../terminal/quick-commands'
|
|
|
|
// A single mutable draft covering both quick-command actions. Both the command
|
|
// text and the agent/prompt are kept while editing so toggling Action back and
|
|
// forth doesn't discard what the user already typed (mirrors desktop's draft).
|
|
export type QuickCommandDraft = {
|
|
id: string | null
|
|
label: string
|
|
action: TerminalQuickCommandAction
|
|
command: string
|
|
appendEnter: boolean
|
|
agent: TuiAgent | null
|
|
prompt: string
|
|
scope: TerminalQuickCommandScope
|
|
}
|
|
|
|
export function createEmptyQuickCommandDraft(scope: TerminalQuickCommandScope): QuickCommandDraft {
|
|
return {
|
|
id: null,
|
|
label: '',
|
|
action: 'terminal-command',
|
|
command: '',
|
|
appendEnter: true,
|
|
agent: null,
|
|
prompt: '',
|
|
scope
|
|
}
|
|
}
|
|
|
|
export function quickCommandToDraft(command: TerminalQuickCommand): QuickCommandDraft {
|
|
const scope: TerminalQuickCommandScope =
|
|
command.scope?.type === 'repo' && command.scope.repoId
|
|
? { type: 'repo', repoId: command.scope.repoId }
|
|
: { type: 'global' }
|
|
if (isAgentQuickCommand(command)) {
|
|
return {
|
|
id: command.id,
|
|
label: command.label,
|
|
action: 'agent-prompt',
|
|
command: '',
|
|
appendEnter: true,
|
|
agent: command.agent,
|
|
prompt: command.prompt,
|
|
scope
|
|
}
|
|
}
|
|
return {
|
|
id: command.id,
|
|
label: command.label,
|
|
action: 'terminal-command',
|
|
command: command.command,
|
|
appendEnter: command.appendEnter !== false,
|
|
agent: null,
|
|
prompt: '',
|
|
scope
|
|
}
|
|
}
|
|
|
|
export function isQuickCommandDraftValid(draft: QuickCommandDraft): boolean {
|
|
if (!draft.label.trim()) {
|
|
return false
|
|
}
|
|
if (draft.action === 'agent-prompt') {
|
|
return Boolean(
|
|
draft.agent &&
|
|
supportsTerminalAgentQuickCommand(draft.agent) &&
|
|
draft.prompt.trim().length > 0
|
|
)
|
|
}
|
|
return draft.command.trim().length > 0
|
|
}
|
|
|
|
// Build the persisted command from a draft. Returns null when incomplete so the
|
|
// caller can keep the editor open. The server re-normalizes, but we trim/cap
|
|
// here so optimistic local state matches what will be saved.
|
|
export function draftToQuickCommand(draft: QuickCommandDraft): TerminalQuickCommand | null {
|
|
if (!isQuickCommandDraftValid(draft)) {
|
|
return null
|
|
}
|
|
// Why: timestamps alone collide when desktop and mobile add commands in the
|
|
// same millisecond, which would turn an atomic upsert into an overwrite.
|
|
const id =
|
|
draft.id ??
|
|
`quick-command-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
const label = draft.label.trim().slice(0, MAX_QUICK_COMMAND_LABEL_LENGTH)
|
|
if (draft.action === 'agent-prompt' && draft.agent) {
|
|
return {
|
|
id,
|
|
label,
|
|
action: 'agent-prompt',
|
|
agent: draft.agent,
|
|
prompt: draft.prompt.trimEnd().slice(0, MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH),
|
|
scope: draft.scope
|
|
}
|
|
}
|
|
return {
|
|
id,
|
|
label,
|
|
action: 'terminal-command',
|
|
command: draft.command.trimEnd().slice(0, MAX_QUICK_COMMAND_TERMINAL_TEXT_LENGTH),
|
|
appendEnter: draft.appendEnter,
|
|
scope: draft.scope
|
|
}
|
|
}
|