Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d387cf3
fix(terminal): refuse to submit a CLI prompt over an unsent composer …
luisgonzaleznf Aug 24, 2026
0c33381
fix(terminal): harden the pending-draft guard against dialogs and lon…
luisgonzaleznf Aug 24, 2026
100a179
Merge origin/main into fix/terminal-send-pending-draft
luisgonzaleznf Aug 25, 2026
ee4965c
fix(terminal): use evidence, not text prefixes, for composer boundaries
luisgonzaleznf Aug 25, 2026
dc9bbe9
chore: drop pre-commit re-indent of an unrelated test
luisgonzaleznf Aug 25, 2026
4ed4064
chore: drop pre-commit reformatting of files untouched by this change
luisgonzaleznf Aug 25, 2026
1c5cba7
Merge origin/main into fix/terminal-send-pending-draft
luisgonzaleznf Aug 26, 2026
8621757
fix(terminal): tell a dialog from a draft by cursor visibility, not r…
luisgonzaleznf Aug 26, 2026
08003a1
Merge origin/main into fix/terminal-send-pending-draft
luisgonzaleznf Aug 27, 2026
4d6692f
Merge origin/main into fix/terminal-send-pending-draft
luisgonzaleznf Aug 28, 2026
d80e6cc
fix(terminal): accept a labelled composer frame line when detecting d…
luisgonzaleznf Aug 28, 2026
893909e
Merge remote-tracking branch 'origin/main' into fix/terminal-send-pen…
luisgonzaleznf Sep 21, 2026
324bc51
fix(terminal): corroborate a labelled composer frame and recheck the …
luisgonzaleznf Sep 21, 2026
61e09c2
Merge remote-tracking branch 'origin/main' into fix/terminal-send-pen…
luisgonzaleznf Oct 8, 2026
aa27734
test(terminal): pass the input kind main now requires on agent-prompt…
luisgonzaleznf Oct 8, 2026
3522f39
Merge remote-tracking branch 'origin/main' into fix/terminal-send-pen…
luisgonzaleznf Oct 8, 2026
edd0aa8
Merge remote-tracking branch 'origin/main' into fix/terminal-send-pen…
luisgonzaleznf Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions skill-guides/orca-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ Terminal rules:
- Use workspace Sleep, not close, when the terminals and agent sessions should resume later. `terminal stop` is legacy compatibility plumbing and should not be used in new agent workflows.
- `terminal list --json` omits `visualLayouts` to keep the common agent payload bounded. Add `--include-visual-layouts` only when tab and pane topology is required.
- Use `terminal read` before `terminal send` unless the next input is obvious.
- `terminal send --text ... --enter` into a Claude Code or Codex composer is refused while the composer holds unsent input (`accepted: false`, `refusedReason: pending-input`, `pendingInput: <draft>`), so a user's draft is never submitted with your text. Wait for the user to send or clear it; pass `--force` only when appending to that draft is intended.
- Use `terminal send` only for direct terminal input or one-off prompts where no task state, inbox, or reply tracking is needed.
- `accepted: true` proves input acceptance, not a started turn. Use the receipt's `turn_started` stage when submission proof is needed; never resend on silence.
- A text-plus-Enter agent prompt returns a durable request ID and additive stages: `input_accepted`, then `turn_started` once the agent's turn is proven. Raw text-only, bare Enter, interrupt, and terminal query replies keep their existing direct-input behavior.
Expand Down
4 changes: 2 additions & 2 deletions src/cli/bundled-skill-guides.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/cli/handlers/terminal-send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const terminalSendHandler: CommandHandler = async ({ flags, client, cwd,
...(waitSubmitMs ? { waitSubmitMs } : {})
}
: {}),
...(flags.get('force') === true ? { allowPendingInput: true as const } : {}),
client: { id: 'orca-cli', type: 'desktop' }
}
const options = promptDeliverySupported
Expand Down Expand Up @@ -108,6 +109,7 @@ export const terminalSendHandler: CommandHandler = async ({ flags, client, cwd,
formatTerminalSend
)
if (!result.result.send.accepted) {
// Why: callers chain `terminal send && ...`; a refusal must not read as delivered.
process.exitCode = 1
}
}
82 changes: 82 additions & 0 deletions src/cli/handlers/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,88 @@ describe('terminal send CLI', () => {
expect(process.exitCode).toBe(1)
})

it('passes --force through as allowPendingInput', async () => {
const call = vi.fn().mockResolvedValue({
result: {
send: {
handle: 'term-1',
accepted: true,
bytesWritten: 7,
prompt: {
requestId: '22222222-2222-4222-8222-222222222222',
stages: ['input_accepted', 'turn_started'],
provider: 'claude',
observation: 'supported',
processIncarnation: 'inc-1',
generation: 1,
baselineWorkingSequence: 0
}
}
}
})
vi.spyOn(console, 'log').mockImplementation(() => {})

await TERMINAL_HANDLERS['terminal send']({
flags: new Map<string, string | true>([
['terminal', 'term-1'],
['text', 'review'],
['enter', true],
['force', true]
]),
client: promptClient(call, true),
cwd: '/tmp/worktree',
json: true
})

expect(call).toHaveBeenCalledWith(
'terminal.send',
{
terminal: 'term-1',
text: 'review',
enter: true,
interrupt: false,
agentPrompt: true,
allowPendingInput: true,
client: { id: 'orca-cli', type: 'desktop' }
},
{ terminalPromptPreflight: { runtimeId: 'runtime-current' } }
)
})

it('exits non-zero and reports the draft when the send is refused for pending input', async () => {
const call = vi.fn().mockResolvedValue({
result: {
send: {
handle: 'term-1',
accepted: false,
bytesWritten: 0,
refusedReason: 'pending-input',
pendingInput: 'Refactor the login page so that it'
}
}
})
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const previousExitCode = process.exitCode
try {
await TERMINAL_HANDLERS['terminal send']({
flags: new Map<string, string | true>([
['terminal', 'term-1'],
['text', 'status update'],
['enter', true]
]),
client: promptClient(call, true),
cwd: '/tmp/worktree',
json: false
})

expect(process.exitCode).toBe(1)
expect(String(log.mock.calls[0]?.[0])).toContain('"Refactor the login page so that it"')
expect(String(log.mock.calls[0]?.[0])).toContain('--force')
} finally {
process.exitCode = previousExitCode
}
})

it('keeps text-only and bare Enter sends as direct terminal input', async () => {
const call = vi.fn().mockResolvedValue({
result: { send: { handle: 'term-1', accepted: true, bytesWritten: 1 } }
Expand Down
5 changes: 5 additions & 0 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ function formatCommandFlagHelp(flag: string, commandPath: string[]): string {
if (command.startsWith('account ') && flag === 'agent') {
return '--agent <id> Account provider: opencode or devin'
}
// Why: the shared --force help describes worktree removal; here it overrides the
// pending-input refusal that protects a user's unsent composer draft.
if (command === 'terminal send' && flag === 'force') {
return '--force Append to unsent Claude Code/Codex composer input instead of refusing (pending-input)'
}
if (flag === 'key' && command === 'computer hotkey') {
return '--key <key-combo> Modifier chord with one key, e.g. CmdOrCtrl+A'
}
Expand Down
3 changes: 2 additions & 1 deletion src/cli/root-help-text-secondary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const ROOT_HELP_TEXT_SECONDARY = [
' orca terminal list [--worktree <selector>] [--limit <n>] [--include-visual-layouts] [--json]',
' orca terminal show [--terminal <handle>] [--json]',
' orca terminal read [--terminal <handle>] [--cursor <n>] [--limit <n>] [--json]',
' orca terminal send [--terminal <handle>] [--text <text>] [--enter] [--interrupt] [--wait-submit <seconds>] [--retry-request <id>] [--json]',
' orca terminal send [--terminal <handle>] [--text <text>] [--enter] [--interrupt] [--wait-submit <seconds>] [--retry-request <id>] [--force] [--json]',
' orca terminal wait [--terminal <handle>] --for exit|tui-idle [--timeout-ms <ms>] [--json]',
' orca terminal create [--worktree <selector>] [--title <name>] [--command <text>] [--shell <shell>] [--focus] [--json]',
' orca terminal split [--terminal <handle>] [--direction horizontal|vertical] [--json]',
Expand Down Expand Up @@ -95,6 +95,7 @@ export const ROOT_HELP_TEXT_SECONDARY = [
' --interrupt Send as an interrupt-style input when supported',
' --wait-submit <seconds> Observe this accepted prompt without resending it',
' --retry-request <id> Resume the same durable prompt request after an ambiguous transport failure',
' --force Append to unsent Claude Code/Codex composer input instead of refusing (pending-input)',
'',
'Terminal List Options:',
' --include-visual-layouts Include tab and pane topology in JSON output',
Expand Down
5 changes: 3 additions & 2 deletions src/cli/specs/terminal-send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ export const TERMINAL_SEND_COMMAND_SPEC: CommandSpec = {
path: ['terminal', 'send'],
summary: 'Send input to a live terminal',
usage:
'orca terminal send [--terminal <handle>] [--text <text>] [--enter] [--interrupt] [--wait-submit <seconds>] [--retry-request <id>] [--json]',
'orca terminal send [--terminal <handle>] [--text <text>] [--enter] [--interrupt] [--wait-submit <seconds>] [--retry-request <id>] [--force] [--json]',
allowedFlags: [
...GLOBAL_FLAGS,
'terminal',
'text',
'enter',
'interrupt',
'wait-submit',
'retry-request'
'retry-request',
'force'
],
notes: [
'For a text-plus-Enter agent prompt, the result separates input acceptance from observed submission and turn start.',
Expand Down
26 changes: 26 additions & 0 deletions src/cli/terminal-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,32 @@ describe('formatTerminalClose', () => {
})

describe('formatTerminalSend', () => {
it('reports delivered bytes only for an accepted send', () => {
expect(
formatTerminalSend({ send: { handle: 'term_1', accepted: true, bytesWritten: 9 } })
).toBe('Sent 9 bytes to term_1.')
expect(
formatTerminalSend({ send: { handle: 'term_1', accepted: false, bytesWritten: 0 } })
).toBe('Input refused by term_1.')
})

it('shows the unsent draft and the --force escape hatch for a pending-input refusal', () => {
expect(
formatTerminalSend({
send: {
handle: 'term_1',
accepted: false,
bytesWritten: 0,
refusedReason: 'pending-input',
pendingInput: 'fix the\nflaky test'
}
})
).toBe(
'Refused: terminal term_1 has unsent input in its composer: "fix the\\nflaky test"\n' +
'Wait for the user to send or clear it, or re-run with --force to append to it.'
)
})

it('exposes the provider and healthy delivery observation', () => {
expect(
formatTerminalSend({
Expand Down
6 changes: 6 additions & 0 deletions src/cli/terminal-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ function formatTerminalReadLimitedWarning(terminal: RuntimeTerminalRead): string
}

export function formatTerminalSend(result: { send: RuntimeTerminalSend }): string {
if (result.send.refusedReason === 'pending-input') {
return [
`Refused: terminal ${result.send.handle} has unsent input in its composer: ${JSON.stringify(result.send.pendingInput ?? '')}`,
'Wait for the user to send or clear it, or re-run with --force to append to it.'
].join('\n')
}
if (!result.send.accepted) {
const reason = result.send.refusedReason ? `: ${result.send.refusedReason}` : ''
return `Input refused by ${result.send.handle}${reason}.`
Expand Down
1 change: 1 addition & 0 deletions src/main/daemon/__fixtures__/claude-composer-draft.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
78[?25h[?25l[?2004h[?1004h[?2031h[>0q[?1049h[?1000h[?1002h[?1003h[?1006h]0;✳ Claude Code ▐▛███▛█Claude Codev2.1.246▝▜██████▀Opus 5 (1M context) with xhigh effort · Claude Max ▝▝ ▝▝ /private/tmp/orca-fixture/repo✦ ultracode · xhigh effort + dynamic workflows for maximum thoroughness────────────────────────────────────────────────────────────────────────────────────────ultracode─❯ Try "fix typecheck errors"────────────────────────────────────────────────────────────────────────────────────────────────────⏵⏵ auto mode on (shift+tab to cycle) · ← for agents[?25h[?25l⚠1 MCP server needs authentication · run /mcp[?25h[?25l2s ned[?25h(B[?1000h[?1002h[?1003h[?1006h[?25lRefactor the login page sothatit[?25h[?25l[?25h
Expand Down
1 change: 1 addition & 0 deletions src/main/daemon/__fixtures__/claude-permission-dialog.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
78[?25h[?25l[?2004h[?1004h[?2031h[>0q[?1049h[?1000h[?1002h[?1003h[?1006h]0;✳ Claude Code ▐▛███▛█Claude Codev2.1.246▝▜██████▀Opus 5 (1M context) with xhigh effort · Claude Max ▝▝ ▝▝ /private/tmp/orca-fixture/repo✦ ultracode · xhigh effort + dynamic workflows for maximum thoroughness────────────────────────────────────────────────────────────────────────────────────────ultracode─❯ Try "refactor <filepath>"────────────────────────────────────────────────────────────────────────────────────────────────────⏸ manual mode on · ? for shortcuts · ← for agents[?25h[?25l⚠1 MCP server needs authentication · run /mcp[?25h[?25l2s ned[?25h(B[?1000h[?1002h[?1003h[?1006h[?25lCreate a file named hello.txtcontainingthewordhi.UsetheWritetool.[?25h]0;◐ Claude Code[?25l❯ Create a file named hello.txt containing the word hi. Use the Write tool. ✢Reticulating… ❯  · esc to interrupt · ← for agents[?25h[?25l·li[?25h[?25lut[?25h[?25l✢ca[?25h[?25l[?25h[?25l✳il[?25h[?25l✶[?25h[?25l✻tu[?25h]0;◐ hello.txt file creation]0;◑ hello.txt file creation[?25l✽ec[?25h[?25lHatching… [?25h[?25l✻[?25h[?25l✶[?25h[?25l✳[?25h[?25l✢[?25h]0;◐ hello.txt file creation[?25l(1s · thinking with xhigh effort)[?25h[?25l2thinking with xhigh effort[?25h[?25l·thinking with xhigh effort[?25h[?25lthinking with xhigh effort[?25h[?25lthinking with xhigh effort[?25h[?25l✢thinking with xhigh effort[?25h[?25lthinking with xhigh effort[?25h[?25l✳thinking with xhigh effort[?25h[?25l✶[?25h]0;◑ hello.txt file creation[?25l✻[?25h[?25l3thinking with xhigh effort[?25h[?25l✽thinking with xhigh effort[?25h[?25lthinking with xhigh effort[?25h[?25lthinking with xhigh effort[?25h]0;✳ hello.txt file creation[?25l⏺Write(]8;id=ieqqac;file:///private/tmp/orca-fixture/repo/hello.txthello.txt]8;;)────────────────────────────────────────────────────────────────────────────────────────────────────Create filehello.txt╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ 1 hi╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌Doyouwanttocreatehello.txt?❯1. Yes2. Yes,andswitchtoaccept edits (auto-approve file edits and common file commands)forthissession(shift+tab)3. NoEsc to cancel · Tab to amend
Expand Down
Loading