mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +00:00
* fix(pty): deliver multiline agent-launch prompts via bracketed paste Multiline agent-launch prompts (claude/codex/opencode argv injection) were mangled when Orca typed the startup command into the interactive shell. The command is single-quoted, but its literal embedded newlines survive quoting; bash readline / zsh zle read every raw LF as accept-line (Enter), so the first newline submits an unterminated single-quoted command, drops the shell into PS2 (>) continuation, and the rest executes piecemeal — backticks/$ evaluate, quotes go unbalanced, and the agent never receives the intact prompt. Short single-line prompts worked because they have no embedded newline. Fix: when a startup command contains a newline, wrap the payload in bracketed-paste markers (ESC[200~ … ESC[201~) before the trailing submit CR/LF so the line editor inserts the multiline text literally and only the trailing byte submits it. The single-line fast path is unchanged. Gated on the target line editor having bracketed-paste mode active (Orca-wrapped bash/zsh) so shells without it never echo the markers as garbage; Orca's bash rc wrappers now force `enable-bracketed-paste on` (zsh has it on by default). Applied consistently across every startup-command delivery path: - src/main/providers/local-pty-shell-ready.ts (in-process / degraded local) - src/main/daemon/terminal-host.ts (daemon host — primary local) - src/relay/pty-handler.ts (SSH relay, remote host) - src/renderer/src/lib/ssh-background-startup-delivery.ts (hidden SSH tab) All share src/shared/startup-command-submission.ts. Windows cmd.exe and other shells keep the current CR submit path (no regression); PSReadLine/POSIX bash/zsh get the fix. * Fix multiline detection for CRLF-terminated startup commands Strip the entire CRLF terminator (or lone CR/LF) from startup commands before checking if the body contains newline characters. Previously, slicing off only the last character of a CRLF-terminated command left a trailing CR in the body. This caused a single-line command to be incorrectly categorized as multiline and wrapped in bracketed paste.
45 lines
2.2 KiB
TypeScript
45 lines
2.2 KiB
TypeScript
/**
|
|
* Builds the exact bytes Orca writes into an interactive shell to deliver and
|
|
* submit a startup command (agent launch, setup script, etc.).
|
|
*
|
|
* Why bracketed paste: agent launch prompts are single-quoted, but their
|
|
* literal embedded newlines survive quoting. bash readline / zsh zle read every
|
|
* raw LF as accept-line (Enter), so the first newline inside a multiline prompt
|
|
* submits an unterminated single-quoted command and drops the shell into PS2
|
|
* continuation — the prompt is executed piecemeal and mangled. Wrapping the
|
|
* payload in bracketed-paste markers (ESC[200~ … ESC[201~) tells the line
|
|
* editor to insert the whole multiline text literally; only the trailing CR/LF
|
|
* written after the end marker submits it. Single-line commands keep the proven
|
|
* raw-write path unchanged so the fast path never regresses.
|
|
*/
|
|
|
|
// DEC 2004 bracketed-paste bracket sequences.
|
|
const BRACKETED_PASTE_START = '\x1b[200~'
|
|
const BRACKETED_PASTE_END = '\x1b[201~'
|
|
|
|
export type StartupCommandSubmissionOptions = {
|
|
/** Byte that submits the line: CR on Windows (PSReadLine/cmd.exe), LF on
|
|
* POSIX; SSH relays remote shells with CR. A caller-supplied trailing submit
|
|
* byte on `command` is preserved as-is. */
|
|
submit: string
|
|
/** Whether the target line editor has bracketed-paste mode active (Orca's
|
|
* wrapped bash/zsh). Only wrap multiline payloads when true — a shell without
|
|
* bracketed paste would echo the ESC[200~ markers as literal garbage. */
|
|
bracketedPasteSafe: boolean
|
|
}
|
|
|
|
export function buildStartupCommandSubmission(
|
|
command: string,
|
|
{ submit, bracketedPasteSafe }: StartupCommandSubmissionOptions
|
|
): string {
|
|
// Strip a full CRLF (or lone CR/LF) terminator so a single-line command ending
|
|
// in \r\n isn't misread as multiline by the \r/\n body check below.
|
|
const trailingTerminator = /\r\n$|\r$|\n$/.exec(command)?.[0] ?? ''
|
|
const endsWithSubmit = trailingTerminator.length > 0
|
|
const body = endsWithSubmit ? command.slice(0, -trailingTerminator.length) : command
|
|
if (bracketedPasteSafe && (body.includes('\n') || body.includes('\r'))) {
|
|
return `${BRACKETED_PASTE_START}${body}${BRACKETED_PASTE_END}${submit}`
|
|
}
|
|
return endsWithSubmit ? command : `${command}${submit}`
|
|
}
|