Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 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
aba267d
Merge remote-tracking branch 'origin/main' into fix/terminal-send-pen…
luisgonzaleznf Oct 30, 2026
4f76634
fix(terminal): re-read the composer after upstream's beforeWrite wrapper
luisgonzaleznf Oct 30, 2026
d06d755
fix(terminal): assert writability after the composer read, not before it
luisgonzaleznf Oct 30, 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
10 changes: 5 additions & 5 deletions docs/reference/plugin-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ Leave `publisher` out. Orca names the publisher after where the plugin comes fro
repository, lowercased (`acme` for `github.com/Acme/gleam-syntax`), so the plugin installs as
`acme.gleam-syntax`. Nobody can choose a publisher name that is not theirs.

| Installed from | Publisher |
| ----------------------------- | ------------------------------------------------------------- |
| Installed from | Publisher |
| --------------------------------- | ------------------------------------------------------------ |
| The official or community catalog | the repository owner, set by the catalog in the entry's `id` |
| Another catalog | whatever that catalog's entry `id` says |
| A Git URL | the URL's owner (`.` and `_` become `-`) |
| A local or development folder | `local` |
| Another catalog | whatever that catalog's entry `id` says |
| A Git URL | the URL's owner (`.` and `_` become `-`) |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor · Data Integrity & Integration · Quick win

Supported by static analysis

Script executed:

set -eu
printf '%s\n' '--- diff ---'
git diff --unified=20 e030868874155f45a5ebd5cef875bb6102614499 4f766347080568168b3efab65bd841435d51282b -- docs/reference/plugin-authoring.md docs/reference/plugin-manifest.md
printf '%s\n' '--- normalization references ---'
rg -n -F --glob '!node_modules' --glob '!dist' -e 'publisher' -e 'replaceAll' -e 'owner' -e 'git' src packages docs/reference | head -n 240
printf '%s\n' '--- manifest reference ---'
nl -ba docs/reference/plugin-manifest.md | sed -n '1,180p'
printf '%s\n' '--- authoring reference ---'
nl -ba docs/reference/plugin-authoring.md | sed -n '1,100p'

Repository: stablyai/orca

Length of output: 41492


Script executed:

set -eu
printf '%s\n' '--- publisher references ---'
rg -n -F --glob '!node_modules' --glob '!dist' --glob '!build' --glob '*.ts' --glob '*.tsx' --glob '*.js' --glob '*.json' --glob '*.md' --glob '*.mjs' --glob '*.cjs' -- 'publisher' . | head -n 260
printf '%s\n' '--- manifest beginning ---'
nl -ba docs/reference/plugin-manifest.md | sed -n '1,48p'
printf '%s\n' '--- likely plugin files ---'
rg --files | rg 'plugin|marketplace|catalog|install' | head -n 180

Repository: stablyai/orca

Length of output: 41468


Script executed:

set -eu
printf '%s\n' '--- derivation helper ---'
nl -ba src/shared/plugins/installation/plugin-publisher-derivation.ts
printf '%s\n' '--- derivation tests ---'
nl -ba src/shared/plugins/installation/plugin-publisher-derivation.test.ts
printf '%s\n' '--- derivation call sites ---'
rg -n -F --glob '*.ts' -e 'gitPluginPublisher' -e 'gitUrl' -e 'derivePluginPublisher' -e 'withPluginPublisher' -e 'localPluginPublisher' src/shared src/main src/cli
printf '%s\n' '--- git fixture and install flow references ---'
nl -ba tests/e2e/helpers/plugin-git-fixture.ts | sed -n '1,180p'
rg -n -F --glob '*.ts' -e 'installPlugin' -e 'clonePlugin' -e 'Git URL' -e 'gitUrl' src/shared/plugins src/main | head -n 220

Repository: stablyai/orca

Length of output: 24200


Document Git publisher normalization in the manifest reference.

The runtime already converts Git owners such as john.doe__x to john-doe-x. Update the manifest-reference generator text in src/shared/plugins/manifest/plugin-manifest.ts to describe this rule and include an example, then regenerate docs/reference/plugin-manifest.md.

| A local or development folder | `local` |

`publisher` is deprecated: Orca ignores it for catalog and Git installs. Leaving it out needs
`engines.orca` of at least `>=1.4.246`, because older Orca requires the field and refuses a
Expand Down
1 change: 1 addition & 0 deletions skill-guides/orca-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ Terminal rules:
- Use `terminal read` before `terminal send` unless the next input is obvious.
- `terminal read` returns accumulated output with escape sequences stripped, not the rendered screen, so anything that repaints (shells, progress bars, TUIs) reads as stacked fragments. Use `--screen` whenever the answer depends on what the terminal actually shows.
- `--screen` and `--cursor` are mutually exclusive: a screen read is the current frame and has no history to page.
- `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 is refused with `agent_prompt_blocked` while the agent shows a permission or trust menu. To answer the menu, send `--text` and `--enter` as separate commands.
Expand Down
1 change: 1 addition & 0 deletions src/cli/bundled-skill-guides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ const ORCA_CLI_MARKDOWN = [
"- Use `terminal read` before `terminal send` unless the next input is obvious.",
"- `terminal read` returns accumulated output with escape sequences stripped, not the rendered screen, so anything that repaints (shells, progress bars, TUIs) reads as stacked fragments. Use `--screen` whenever the answer depends on what the terminal actually shows.",
"- `--screen` and `--cursor` are mutually exclusive: a screen read is the current frame and has no history to page.",
"- `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 is refused with `agent_prompt_blocked` while the agent shows a permission or trust menu. To answer the menu, send `--text` and `--enter` as separate commands.",
Expand Down
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 @@ -107,6 +108,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 @@ -446,6 +446,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 @@ -187,6 +187,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 @@ -63,7 +63,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>] [--screen] [--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 agent launch --agent <id> [--worktree <selector>] [--prompt <text>] [--model <id>] [--effort <level>] [--focus] [--json]',
' orca terminal create [--worktree <selector>] [--title <name>] [--command <text>] [--shell <shell>] [--focus] [--json]',
Expand Down Expand Up @@ -97,6 +97,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: [
"Without --enter, --text is written to the PTY verbatim, so control bytes pass through (Bash/Zsh: --text $'\\x1b' for Escape, $'\\x15' for Ctrl+U).",
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 @@ -89,6 +89,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 @@ -174,6 +174,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
Loading