>
- toggleAppendEnter: () => void
+ toggleImmediateSubmission: () => void
}
export function TerminalQuickCommandContentSection({
draft,
isAgentAction,
selectedAgent,
+ agentDraftsSupported,
draftMemoryRef,
setDraft,
- toggleAppendEnter
+ toggleImmediateSubmission
}: TerminalQuickCommandContentSectionProps): React.JSX.Element {
const commandText = isTerminalAgentQuickCommand(draft) ? draft.prompt : draft.command
// Why: the frame header is a plain span, so the textarea carries the accessible name itself.
@@ -189,11 +191,15 @@ export function TerminalQuickCommandContentSection({
/>
- {!isTerminalAgentQuickCommand(draft) ? (
-
) : (
diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx
index 7a155d11bfb..334b5104060 100644
--- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx
+++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx
@@ -3,7 +3,11 @@
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { ExecutionHostId } from '../../../../shared/execution-host'
+import { TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import type { TerminalQuickCommand } from '../../../../shared/terminal-quick-command-types'
+import { useAppStore } from '@/store'
+import type { AppState } from '@/store/types'
import { TerminalQuickCommandDialog } from './TerminalQuickCommandDialog'
const mountedRoots: Root[] = []
@@ -12,6 +16,7 @@ async function renderDialog(
command: TerminalQuickCommand,
props: {
defaultAdvancedOpen?: boolean
+ hostId?: ExecutionHostId
onOpenChange?: ReturnType void>>
onSave?: ReturnType void>>
} = {}
@@ -32,6 +37,7 @@ async function renderDialog(
open={true}
mode="add"
command={command}
+ hostId={props.hostId ?? 'local'}
repos={[]}
defaultAdvancedOpen={props.defaultAdvancedOpen}
onOpenChange={onOpenChange}
@@ -67,6 +73,7 @@ function findAnimatedRowContaining(text: string): HTMLElement {
describe('TerminalQuickCommandDialog animation structure', () => {
beforeEach(() => {
vi.stubGlobal('navigator', { userAgent: 'Macintosh' })
+ useAppStore.setState({ runtimeStatusByEnvironmentId: new Map() })
})
afterEach(async () => {
@@ -186,18 +193,71 @@ describe('TerminalQuickCommandDialog animation structure', () => {
expect(onOpenChange).toHaveBeenCalledWith(false)
})
- it('hides append enter and shows agent toolbar hint in agent mode', async () => {
- await renderDialog({
+ it('saves an agent prompt as an editable draft when immediate submission is toggled off', async () => {
+ useAppStore.setState({
+ runtimeStatusByEnvironmentId: new Map([
+ [
+ 'env-new',
+ {
+ checkedAt: 1,
+ status: {
+ capabilities: [TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY]
+ }
+ }
+ ]
+ ]) as AppState['runtimeStatusByEnvironmentId']
+ })
+ const { onOpenChange, onSave } = await renderDialog(
+ {
+ id: 'qc-3',
+ label: 'Investigate',
+ action: 'agent-prompt',
+ agent: 'claude',
+ prompt: 'Look into the build',
+ scope: { type: 'global' }
+ },
+ { hostId: 'runtime:env-new' }
+ )
+ const submitPromptSwitch = document.body.querySelector(
+ '[aria-label="Toggle immediate prompt submission"]'
+ )
+ const saveButton = Array.from(document.body.querySelectorAll('button')).find(
+ (button) => button.textContent?.includes('Save')
+ )
+
+ expect(document.body.textContent).toContain('Supports /goal, skills, paths')
+ expect(document.body.textContent).toContain('Submit prompt — run immediately')
+ expect(submitPromptSwitch?.getAttribute('aria-checked')).toBe('true')
+
+ await click(submitPromptSwitch!)
+ await click(saveButton!)
+
+ expect(onSave).toHaveBeenCalledWith({
id: 'qc-3',
label: 'Investigate',
action: 'agent-prompt',
agent: 'claude',
prompt: 'Look into the build',
+ submitPrompt: false,
scope: { type: 'global' }
})
+ expect(onOpenChange).toHaveBeenCalledWith(false)
+ })
- expect(document.body.textContent).toContain('Supports /goal, skills, paths')
- expect(document.body.textContent).not.toContain('Append Enter — run immediately')
+ it('hides agent draft submission controls for older remote hosts', async () => {
+ await renderDialog(
+ {
+ id: 'qc-legacy-agent',
+ label: 'Investigate',
+ action: 'agent-prompt',
+ agent: 'claude',
+ prompt: 'Look into the build'
+ },
+ { hostId: 'runtime:env-old' }
+ )
+
+ expect(document.body.textContent).not.toContain('Submit prompt — run immediately')
+ expect(document.body.textContent).toContain('Multi-line prompts are fine — keep them focused.')
})
it('shows scope summary on the collapsed advanced toggle', async () => {
diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx
index c0d5a26c2e1..7f959c08dd4 100644
--- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx
+++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.tsx
@@ -5,6 +5,8 @@ import type {
} from '../../../../shared/terminal-quick-command-types'
import type { Repo } from '../../../../shared/repo-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
+import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
+import { TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import {
getTerminalQuickCommandAction,
getTerminalQuickCommandScope,
@@ -33,6 +35,7 @@ import {
switchTerminalQuickCommandDialogAction
} from './terminal-quick-command-dialog-draft'
import { translate } from '@/i18n/i18n'
+import { useAppStore } from '@/store'
type TerminalQuickCommandDialogMode = 'add' | 'edit'
@@ -40,6 +43,7 @@ type TerminalQuickCommandDialogProps = {
open: boolean
mode: TerminalQuickCommandDialogMode
command: TerminalQuickCommand
+ hostId: ExecutionHostId
repos?: readonly Pick[]
/** Settings has no ambient workspace to imply scope from, so it opens the
* Advanced section up front. In-workspace entry points leave it collapsed. */
@@ -66,11 +70,21 @@ export function TerminalQuickCommandDialog({
open,
mode,
command,
+ hostId,
repos = EMPTY_REPOS,
defaultAdvancedOpen = false,
onOpenChange,
onSave
}: TerminalQuickCommandDialogProps): React.JSX.Element {
+ const host = parseExecutionHostId(hostId)
+ const agentDraftsSupported = useAppStore(
+ (state) =>
+ host?.kind !== 'runtime' ||
+ state.runtimeStatusByEnvironmentId
+ .get(host.environmentId)
+ ?.status?.capabilities?.includes(TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY) ===
+ true
+ )
const fallbackAgent: TuiAgent =
getAgentCatalog().find((entry) => supportsTerminalAgentQuickCommand(entry.id))?.id ?? 'claude'
const [draft, setDraft] = useState(command)
@@ -115,19 +129,25 @@ export function TerminalQuickCommandDialog({
})
}
- const toggleAppendEnter = (): void => {
- setDraft((current) =>
- isTerminalAgentQuickCommand(current)
- ? current
- : (() => {
- const appendEnter = !current.appendEnter
- draftMemoryRef.current = {
- ...draftMemoryRef.current,
- terminalAppendEnter: appendEnter
- }
- return { ...current, appendEnter }
- })()
- )
+ const toggleImmediateSubmission = (): void => {
+ if (isTerminalAgentQuickCommand(draft)) {
+ const submitPrompt = draft.submitPrompt === false
+ draftMemoryRef.current = { ...draftMemoryRef.current, agentSubmitPrompt: submitPrompt }
+ if (!submitPrompt) {
+ setDraft({ ...draft, submitPrompt: false })
+ return
+ }
+ const { submitPrompt: _submitPrompt, ...submitted } = draft
+ void _submitPrompt
+ setDraft(submitted)
+ return
+ }
+ const appendEnter = !draft.appendEnter
+ draftMemoryRef.current = {
+ ...draftMemoryRef.current,
+ terminalAppendEnter: appendEnter
+ }
+ setDraft({ ...draft, appendEnter })
}
const saveDraft = (): void => {
@@ -138,6 +158,7 @@ export function TerminalQuickCommandDialog({
action: 'agent-prompt',
agent: draft.agent,
prompt: draft.prompt.trimEnd(),
+ ...(draft.submitPrompt === false ? { submitPrompt: false as const } : {}),
scope: selectedScope
}
: {
@@ -233,9 +254,10 @@ export function TerminalQuickCommandDialog({
draft={draft}
isAgentAction={isAgentAction}
selectedAgent={selectedAgent}
+ agentDraftsSupported={agentDraftsSupported}
draftMemoryRef={draftMemoryRef}
setDraft={setDraft}
- toggleAppendEnter={toggleAppendEnter}
+ toggleImmediateSubmission={toggleImmediateSubmission}
/>
void
+}
+
+export function TerminalQuickCommandSubmissionSwitch({
+ action,
+ checked,
+ onToggle
+}: TerminalQuickCommandSubmissionSwitchProps): React.JSX.Element {
+ const isAgentPrompt = action === 'agent-prompt'
+ const ariaLabel = isAgentPrompt
+ ? translate(
+ 'auto.components.terminal.quick.commands.TerminalQuickCommandSubmissionSwitch.toggle_prompt',
+ 'Toggle immediate prompt submission'
+ )
+ : translate(
+ 'auto.components.terminal.quick.commands.TerminalQuickCommandAppendEnterSwitch.e4e5fed3b3',
+ 'Toggle append Enter'
+ )
+
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts
index becc121fb15..e6188d60ccd 100644
--- a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts
+++ b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.test.ts
@@ -59,7 +59,7 @@ describe('terminal quick command dialog draft transitions', () => {
})
})
- it('preserves independent text, agent, and append-enter drafts across toggles', () => {
+ it('preserves independent text and submission drafts across toggles', () => {
const initial: TerminalQuickCommand = {
id: 'qc-1',
label: 'Work',
@@ -74,7 +74,8 @@ describe('terminal quick command dialog draft transitions', () => {
...toAgent.draft,
action: 'agent-prompt',
agent: 'codex',
- prompt: 'Investigate failures'
+ prompt: 'Investigate failures',
+ submitPrompt: false
}
const backToTerminal = switchTerminalQuickCommandDialogAction(
editedAgent,
@@ -101,7 +102,8 @@ describe('terminal quick command dialog draft transitions', () => {
expect(backToAgent.draft).toMatchObject({
action: 'agent-prompt',
agent: 'codex',
- prompt: 'Investigate failures'
+ prompt: 'Investigate failures',
+ submitPrompt: false
})
const finalTerminal = switchTerminalQuickCommandDialogAction(
diff --git a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts
index 01dc86367c7..49191c7ef8c 100644
--- a/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts
+++ b/src/renderer/src/components/terminal-quick-commands/terminal-quick-command-dialog-draft.ts
@@ -12,6 +12,7 @@ export type TerminalQuickCommandDialogDraftMemory = {
terminalAppendEnter: boolean
agent: TuiAgent
agentPrompt: string
+ agentSubmitPrompt: boolean
}
export function createTerminalQuickCommandDialogDraftMemory(
@@ -23,14 +24,16 @@ export function createTerminalQuickCommandDialogDraftMemory(
terminalCommand: '',
terminalAppendEnter: true,
agent: command.agent,
- agentPrompt: command.prompt
+ agentPrompt: command.prompt,
+ agentSubmitPrompt: command.submitPrompt !== false
}
}
return {
terminalCommand: command.command,
terminalAppendEnter: command.appendEnter,
agent: fallbackAgent,
- agentPrompt: ''
+ agentPrompt: '',
+ agentSubmitPrompt: true
}
}
@@ -42,7 +45,8 @@ export function rememberTerminalQuickCommandDialogDraft(
return {
...memory,
agent: draft.agent,
- agentPrompt: draft.prompt
+ agentPrompt: draft.prompt,
+ agentSubmitPrompt: draft.submitPrompt !== false
}
}
return {
@@ -76,7 +80,8 @@ export function switchTerminalQuickCommandDialogAction(
...base,
action: 'agent-prompt',
agent: nextMemory.agent,
- prompt: nextMemory.agentPrompt
+ prompt: nextMemory.agentPrompt,
+ ...(nextMemory.agentSubmitPrompt ? {} : { submitPrompt: false as const })
}
}
}
diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts
index 6237537af39..13df5c0401d 100644
--- a/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts
+++ b/src/renderer/src/lib/run-quick-command-in-new-tab.test.ts
@@ -170,6 +170,37 @@ describe('runQuickCommandInNewTab', () => {
expect(mockState.setRecentQuickCommandForGroup).toHaveBeenCalledWith('group-1', 'agent-review')
})
+ it('launches insertion-only agent prompts as editable drafts', () => {
+ mocks.launchAgentInNewTab.mockReturnValue({ tabId: 'tab-agent' })
+ mockState.unifiedTabsByWorktree['repo::worktree'] = [
+ { entityId: 'tab-agent', contentType: 'terminal', groupId: 'group-1' }
+ ]
+
+ const result = runQuickCommandInNewTab({
+ command: {
+ id: 'agent-review-draft',
+ label: 'Review draft',
+ action: 'agent-prompt',
+ agent: 'codex',
+ prompt: 'Review this diff',
+ submitPrompt: false
+ },
+ worktreeId: 'repo::worktree',
+ groupId: 'group-1'
+ })
+
+ expect(result).toEqual({ tabId: 'tab-agent' })
+ expect(mocks.launchAgentInNewTab).toHaveBeenCalledWith({
+ agent: 'codex',
+ prompt: 'Review this diff',
+ worktreeId: 'repo::worktree',
+ groupId: 'group-1',
+ promptDelivery: 'draft',
+ launchSource: 'quick_command',
+ quickCommandLabel: 'Review draft'
+ })
+ })
+
it('falls back to the active group when context-menu group resolution is missing', () => {
mockState.activeGroupIdByWorktree['repo::worktree'] = 'active-group'
mocks.launchAgentInNewTab.mockReturnValue({ tabId: 'tab-agent' })
diff --git a/src/renderer/src/lib/run-quick-command-in-new-tab.ts b/src/renderer/src/lib/run-quick-command-in-new-tab.ts
index fd88a3f5bbf..64be2d5040a 100644
--- a/src/renderer/src/lib/run-quick-command-in-new-tab.ts
+++ b/src/renderer/src/lib/run-quick-command-in-new-tab.ts
@@ -60,6 +60,7 @@ export function runQuickCommandInNewTab({
prompt: command.prompt,
worktreeId,
groupId: targetGroupId,
+ ...(command.submitPrompt === false ? { promptDelivery: 'draft' as const } : {}),
launchSource: 'quick_command',
quickCommandLabel: command.label
})
diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts
index 9a262cafd7e..fb15700df65 100644
--- a/src/shared/protocol-version.ts
+++ b/src/shared/protocol-version.ts
@@ -96,6 +96,9 @@ export const TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY = 'terminal.paired-parki
// Why: older hosts lack the targeted settings RPCs and strip agentPrompt from
// terminal creation, so mobile must hide Quick Commands unless both are present.
export const TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY = 'terminal.quick-commands.v1' as const
+// Why: older clients reject unknown Quick Command keys, so hosts must omit Agent draft state unless negotiated.
+export const TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY =
+ 'terminal.quick-commands.agent-drafts.v1' as const
// Why: older hosts strip worktree.create's clientMutationId, so mobile must only
// replay ambiguous cutovers when the host advertises idempotent create support;
// status.worktreeCreateIdempotency carries the optional host retention policy.
@@ -155,6 +158,7 @@ export const NATIVE_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
// host still requires the separate authenticated browser-client lease.
export const ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
...NATIVE_REMOTE_RUNTIME_CLIENT_CAPABILITIES,
+ TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY,
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY
] as const
@@ -192,6 +196,7 @@ export const RUNTIME_CAPABILITIES = [
TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY,
TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY,
TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY,
+ TERMINAL_QUICK_COMMAND_AGENT_DRAFTS_RUNTIME_CAPABILITY,
WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY,
TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
diff --git a/src/shared/terminal-quick-command-types.ts b/src/shared/terminal-quick-command-types.ts
index a887f47b84d..4e38c7b3f7d 100644
--- a/src/shared/terminal-quick-command-types.ts
+++ b/src/shared/terminal-quick-command-types.ts
@@ -27,6 +27,8 @@ export type TerminalAgentQuickCommand = TerminalQuickCommandBase & {
action: 'agent-prompt'
agent: TuiAgent
prompt: string
+ /** Omitted means submit immediately, preserving commands saved by older Orca versions. */
+ submitPrompt?: false
}
export type TerminalQuickCommand = TerminalCommandQuickCommand | TerminalAgentQuickCommand
diff --git a/src/shared/terminal-quick-commands.test.ts b/src/shared/terminal-quick-commands.test.ts
index 3b07e72aef6..df4abe6a777 100644
--- a/src/shared/terminal-quick-commands.test.ts
+++ b/src/shared/terminal-quick-commands.test.ts
@@ -132,6 +132,7 @@ describe('terminal quick commands', () => {
action: 'agent-prompt',
agent: 'codex',
prompt: ' Review this diff\n',
+ submitPrompt: false,
command: "codex 'old workaround'"
},
{
@@ -156,6 +157,7 @@ describe('terminal quick commands', () => {
action: 'agent-prompt',
agent: 'codex',
prompt: ' Review this diff',
+ submitPrompt: false,
scope: { type: 'global' }
}
])
@@ -218,10 +220,30 @@ describe('terminal quick commands', () => {
it('accepts only complete canonical command lists at protocol boundaries', () => {
const canonical = normalizeTerminalQuickCommands([
- { id: 'status', label: 'Status', command: 'git status', appendEnter: true }
+ { id: 'status', label: 'Status', command: 'git status', appendEnter: true },
+ {
+ id: 'review',
+ label: 'Review',
+ action: 'agent-prompt',
+ agent: 'codex',
+ prompt: 'Review this diff',
+ submitPrompt: false
+ }
])
expect(parseNormalizedTerminalQuickCommands(canonical)).toEqual(canonical)
+ expect(
+ parseNormalizedTerminalQuickCommands([
+ {
+ id: 'legacy-review',
+ label: 'Legacy review',
+ action: 'agent-prompt',
+ agent: 'codex',
+ prompt: 'Review this diff',
+ scope: { type: 'global' }
+ }
+ ])
+ ).not.toBeNull()
expect(parseNormalizedTerminalQuickCommands([{ ...canonical[0], command: 42 }])).toBeNull()
expect(
parseNormalizedTerminalQuickCommands([...canonical, ...canonical.slice(0, 1)])
diff --git a/src/shared/terminal-quick-commands.ts b/src/shared/terminal-quick-commands.ts
index 28d51bc0323..3472f63e8c2 100644
--- a/src/shared/terminal-quick-commands.ts
+++ b/src/shared/terminal-quick-commands.ts
@@ -142,7 +142,8 @@ export function normalizeTerminalQuickCommands(input: unknown): TerminalQuickCom
prompt: (hasPrompt ? String(record.prompt).trimEnd() : '').slice(
0,
MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH
- )
+ ),
+ ...(record.submitPrompt === false ? { submitPrompt: false as const } : {})
})
} else {
const command = hasCommand ? String(record.command).trimEnd() : ''
@@ -198,11 +199,16 @@ function isNormalizedTerminalQuickCommand(value: unknown, expected: TerminalQuic
return false
}
if (isTerminalAgentQuickCommand(expected)) {
+ const expectedKeys =
+ expected.submitPrompt === false
+ ? ['id', 'label', 'action', 'agent', 'prompt', 'submitPrompt', 'scope']
+ : ['id', 'label', 'action', 'agent', 'prompt', 'scope']
return (
- hasExactKeys(command, ['id', 'label', 'action', 'agent', 'prompt', 'scope']) &&
+ hasExactKeys(command, expectedKeys) &&
command.action === 'agent-prompt' &&
command.agent === expected.agent &&
- command.prompt === expected.prompt
+ command.prompt === expected.prompt &&
+ command.submitPrompt === expected.submitPrompt
)
}
return (