mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(terminal): submit startup commands with Enter on every platform LF is Ctrl+J to a line editor, so a shell that remaps it left the command at the prompt. Fixes #23250 Co-authored-by: Aashish Mahato <145881415+aashish254@users.noreply.github.com> * test(terminal): expect CR in pty ipc startup tests * fix(terminal): submit every line of a raw multiline startup command with CR The CR switch only replaced the trailing terminator. On the raw path — a shell with no bracketed paste, so each line break submits its own line — interior LFs stayed LF, which is the same remappable Ctrl+J the fix set out to remove. A multiline startup command in sh/dash/nushell/PowerShell therefore still stalled partway through for a user who rebinds Ctrl+J. Normalise interior breaks to CR on that path only; the bracketed-paste payload keeps its literal newlines. * fix(terminal): quote multi-line PowerShell arguments on one line A multi-line agent prompt was quoted as a single-quoted PowerShell string with raw line breaks. Typed into Windows PowerShell 5.1 without PSReadLine, each break submits a line and the shell waits at `>>` forever. Values with CR/LF now become one double-quoted line with backtick escapes, keeping every byte; single-line values keep the single-quoted form unchanged. * style(terminal): keep typographic quotes as \u escapes in PowerShell quoting Restores the escape form the single-quoted regex used on main and applies it to the new double-quoted regex and the native argv test, so the diff against main stays minimal and the characters are visible in review. --------- Co-authored-by: Aashish Mahato <145881415+aashish254@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
65 lines
3.0 KiB
TypeScript
65 lines
3.0 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 a raw
|
|
* LF as accept-line by default, 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
|
|
* 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~'
|
|
// Why CR on every platform: it is the byte the Enter key sends. A line editor reads
|
|
// the tty raw, so LF arrives as Ctrl+J — a key users rebind (e.g. vi-mode newline).
|
|
const STARTUP_COMMAND_SUBMIT = '\r'
|
|
|
|
export type StartupCommandSubmissionOptions = {
|
|
/** Whether the target line editor has bracketed-paste mode active (Orca's
|
|
* wrapped bash/zsh/fish). Only wrap multiline payloads when true — a shell
|
|
* without bracketed paste would echo the ESC[200~ markers as literal garbage. */
|
|
bracketedPasteSafe: boolean
|
|
}
|
|
|
|
/**
|
|
* Whether a spawned POSIX shell will read a bracketed-paste payload as one
|
|
* multiline command.
|
|
*
|
|
* Why fish needs the ready barrier: bash readline and zsh zle interpret the
|
|
* ESC[200~ wrapper out of their buffered input, but fish consumes bytes during
|
|
* its startup terminal-query handshake, so a payload written before its reader
|
|
* is up lands as literal `200~` text and the command never runs (verified
|
|
* against fish 4.7). Waiting for the shell-ready barrier is what makes fish
|
|
* paste-safe, and it is what the daemon and relay backends already require.
|
|
*/
|
|
export function isBracketedPasteSafeShell(args: {
|
|
shellName: string
|
|
waitsForShellReady: boolean
|
|
}): boolean {
|
|
const name = args.shellName.toLowerCase()
|
|
if (name === 'bash' || name === 'zsh') {
|
|
return true
|
|
}
|
|
return name === 'fish' && args.waitsForShellReady
|
|
}
|
|
|
|
export function buildStartupCommandSubmission(
|
|
command: string,
|
|
{ bracketedPasteSafe }: StartupCommandSubmissionOptions
|
|
): string {
|
|
// Why replace a caller's terminator: a trailing LF is Ctrl+J again, and CRLF submits twice.
|
|
const body = command.replace(/\r\n$|\r$|\n$/, '')
|
|
if (bracketedPasteSafe && (body.includes('\n') || body.includes('\r'))) {
|
|
return `${BRACKETED_PASTE_START}${body}${BRACKETED_PASTE_END}${STARTUP_COMMAND_SUBMIT}`
|
|
}
|
|
// Why normalise interior breaks too: without bracketed paste each line submits itself, and an
|
|
// interior LF is the same remappable Ctrl+J as a trailing one.
|
|
return `${body.replace(/\r?\n/g, STARTUP_COMMAND_SUBMIT)}${STARTUP_COMMAND_SUBMIT}`
|
|
}
|