Files
orca/src/main/shell-templates.ts
T
Brennan Benson 6858e072cf fix(terminal): agent pane auto-launch lost under fish + Starship (STA-3417) (#12840)
* fix(terminal): extend the shell-ready startup barrier to fish (STA-3417)

Fish never emitted the OSC 777 shell-ready marker, so agent launch
commands were written into the PTY while fish/Starship were still
initializing: the daemon path wrote them synchronously at session
create and the local path blind-wrote ~30ms after the first output
byte. The command was echoed by the kernel but never executed.

- shell-templates: shared fish --init-command that emits the marker
  once on the first fish_prompt event (the earliest point fish's own
  reader owns the PTY, mirroring zsh's zle-line-init marker)
- daemon shell-ready: fish joins the startup barrier so the launch
  command queues until the marker (timeout fallback unchanged)
- local-pty-shell-ready: fish launch config gains the marker wrapper
- codex-startup-delivery/tui-agent-startup: omp/pi/opencode plans now
  request shell-ready delivery (codex parity) so the SSH renderer path
  also waits for the prompt; plain payload-free codex stays on the
  markerless fast path

* fix(terminal): answer DA1 past the shell-ready barrier

The barrier queues all inbound input until the ready marker, including the
renderer's DA1 reply. A shell that withholds its first prompt until DA1 is
answered — fish waits 10s — therefore never emits the marker that would
release the reply it is waiting for. Measured: 10.37s to launch an agent,
versus 0.35s once the reply lands.

Answer DA1 from the daemon while the barrier holds, writing straight to the
subprocess so the reply bypasses the queue, and consume the query so the
renderer's xterm cannot also reply. Released on ready, timeout, or dispose,
handing DA1 back to the renderer for steady state.

Consolidates the identical DA1 handler the ConPTY override already used.

* fix(terminal): prevent duplicate startup DA1 replies
2026-08-09 14:29:14 -07:00

188 lines
7.7 KiB
TypeScript

// Why: local PTYs and the daemon/SSH path must use identical ZDOTDIR discovery;
// small drift here breaks different terminal transports in different ways.
function quotePosixSingle(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
export function getZshEnvTemplate(zshDir: string, headerPrefix = ''): string {
const header = headerPrefix
? `Orca ${headerPrefix} zsh shell-ready wrapper`
: 'Orca zsh shell-ready wrapper'
return `# ${header}
# Why: capture the runtime wrapper dir before it is unset below. On WSL this
# file is generated with a Windows path but sourced via /mnt/c, so the baked
# literal is unusable there and ZDOTDIR must be restored from this value.
# Derive it from the file being sourced (%x, zsh's internal script name) rather
# than the env-imported $ZDOTDIR: zsh corrupts environment values whose UTF-8
# bytes fall in its 0x84-0x9D token range (e.g. a non-ASCII Windows username
# such as a Korean login), which would make the self-check below fail and fall
# back to the unusable baked literal, so the user's .zshrc never loads (#8003).
# %x is not subject to that corruption; keep $ZDOTDIR as a fallback for the
# rare shell where %x prompt expansion yields nothing.
_orca_wrapper_zdotdir_self="\${\${(%):-%x}:h}"
if [[ -z "\${_orca_wrapper_zdotdir_self:-}" ]]; then
_orca_wrapper_zdotdir_self="\${ZDOTDIR:-}"
fi
while [[ "\${_orca_wrapper_zdotdir_self:-}" == */ ]]; do
_orca_wrapper_zdotdir_self="\${_orca_wrapper_zdotdir_self%/}"
done
_orca_spawn_orig_zdotdir="\${ORCA_ORIG_ZDOTDIR:-}"
_orca_user_zdotdir="\${_orca_spawn_orig_zdotdir:-$HOME}"
_orca_zshenv_source_dir="\${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
_orca_zshenv_path=""
unset ORCA_ZSHENV_SOURCE_DIR
# Normalize fallback and source roots before reading user .zshenv so nested
# Orca PTYs never source another Orca wrapper recursively.
while [[ "\${_orca_user_zdotdir}" == */ ]]; do
_orca_user_zdotdir="\${_orca_user_zdotdir%/}"
done
case "\${_orca_user_zdotdir}" in
""|*/shell-ready/zsh) _orca_user_zdotdir="$HOME" ;;
esac
while [[ "\${_orca_zshenv_source_dir}" == */ ]]; do
_orca_zshenv_source_dir="\${_orca_zshenv_source_dir%/}"
done
case "\${_orca_zshenv_source_dir}" in
""|*/shell-ready/zsh) _orca_zshenv_source_dir="$HOME" ;;
esac
# Why: source at wrapper top level, not in a function/subshell, so .zshenv
# exports, functions, path/fpath typesets, and zsh options keep normal scope.
unset ZDOTDIR
if [[ -n "\${_orca_zshenv_source_dir:-}" && -f "\${_orca_zshenv_source_dir}/.zshenv" ]]; then
_orca_zshenv_path="\${_orca_zshenv_source_dir}/.zshenv"
fi
if [[ -n "\${_orca_zshenv_path:-}" ]]; then
source "\${_orca_zshenv_path}"
fi
_orca_discovered_zdotdir="\${ZDOTDIR:-}"
while [[ "\${_orca_discovered_zdotdir}" == */ ]]; do
_orca_discovered_zdotdir="\${_orca_discovered_zdotdir%/}"
done
case "\${_orca_discovered_zdotdir}" in
*[![:space:]]*) ;;
*) _orca_discovered_zdotdir="" ;;
esac
if [[ -n "\${_orca_discovered_zdotdir}" && ! -d "\${_orca_discovered_zdotdir}" ]]; then
[[ "\${ORCA_DEBUG:-0}" == "1" ]] && echo "[orca-shell-ready] Discovered ZDOTDIR '\${_orca_discovered_zdotdir}' does not exist, falling back" >&2
_orca_discovered_zdotdir=""
fi
export ORCA_ORIG_ZDOTDIR="\${_orca_discovered_zdotdir:-\${_orca_user_zdotdir:-$HOME}}"
while [[ "\${ORCA_ORIG_ZDOTDIR}" == */ ]]; do
ORCA_ORIG_ZDOTDIR="\${ORCA_ORIG_ZDOTDIR%/}"
done
case "\${ORCA_ORIG_ZDOTDIR}" in
""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;
esac
# Why: use :- after user .zshenv — a pathological unset under set -u must not
# abort the wrapper; empty falls through to the baked-literal branch.
if [[ -n "\${_orca_wrapper_zdotdir_self:-}" && -f "\${_orca_wrapper_zdotdir_self:-}/.zshenv" ]]; then
export ZDOTDIR="\${_orca_wrapper_zdotdir_self:-}"
else
export ZDOTDIR=${quotePosixSingle(zshDir)}
fi
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
`
}
export function getZshStartupFileSourceBlock(options: {
fileName: '.zprofile' | '.zshrc' | '.zlogin'
homeExpression?: string
interactiveOnly?: boolean
skipWhenHomeIsCurrentZdotdir?: boolean
}): string {
const homeExpression = options.homeExpression ?? '"${ORCA_ORIG_ZDOTDIR:-$HOME}"'
const checks = [
options.skipWhenHomeIsCurrentZdotdir ? '"$_orca_home" != "$ZDOTDIR"' : null,
options.interactiveOnly ? '-o interactive' : null,
`-f "$_orca_home/${options.fileName}"`
].filter(Boolean)
return `_orca_home=${homeExpression}
case "\${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
if [[ ${checks.join(' && ')} ]]; then
_orca_wrapper_zdotdir="$ZDOTDIR"
# Why: user startup files resolve plugin/config paths from their own ZDOTDIR;
# Orca restores its wrapper dir afterward so zsh still loads wrapper files.
export ZDOTDIR="$_orca_home"
source "$_orca_home/${options.fileName}"
export ZDOTDIR="$_orca_wrapper_zdotdir"
unset _orca_wrapper_zdotdir
fi
`
}
// Why: zsh precmd fires before zle switches the PTY into line-editing mode,
// so the marker must be emitted from zle-line-init. Registering it through
// add-zle-hook-widget is unsafe: the azhw dispatcher aborts its hook chain
// when an earlier hook exits non-zero, and a pre-existing raw user widget
// (e.g. oh-my-zsh vi-mode without VI_MODE_SET_CURSOR) is preserved as the
// first hook and fails — silently suppressing the marker and stalling every
// startup command on the pre-ready timeout. Instead, own zle-line-init: emit
// the marker first, then chain to whatever widget was installed before.
export function getZshShellReadyMarkerRegistrationBlock(escapedMarker: string): string {
return `if [[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
# Why: capture the prior zle-line-init so the marker chains to it. On a
# re-source we are already the bound widget, so keep the function captured
# the first time instead of clobbering it to empty (which would silently
# drop the user's widget on every prompt after the second source). Only
# user-defined widgets are chainable as plain functions; builtin/completion
# forms (rare for zle-line-init) are left unchained.
if [[ "\${widgets[zle-line-init]:-}" == "user:__orca_prompt_mark" ]]; then
:
elif (( \${+widgets[zle-line-init]} )) && [[ "\${widgets[zle-line-init]}" == user:* ]]; then
__orca_prev_line_init_fn="\${widgets[zle-line-init]#user:}"
else
__orca_prev_line_init_fn=""
fi
__orca_prompt_mark() {
printf "${escapedMarker}"
# Why: call the prior hook as a plain function, not an aliased widget, so
# $WIDGET stays zle-line-init for add-zle-hook-widget dispatchers.
if [[ -n "\${__orca_prev_line_init_fn:-}" ]]; then
"\${__orca_prev_line_init_fn}" "$@"
fi
}
zle -N zle-line-init __orca_prompt_mark
fi
`
}
// Why: fish has no ZDOTDIR-style wrapper dir, so the marker rides `--init-command`
// and fires on fish_prompt — the earliest event fish exposes (STA-3417). Unlike zsh's
// zle-line-init this lands just *before* fish arms `?2004h`, which PostReadyFlushGate
// absorbs. `builtin printf` so a user-defined printf can't silently swallow the marker
// and send every launch to the ready timeout.
export function getFishShellReadyInitCommand(escapedMarker: string): string {
return `if test "$ORCA_SHELL_READY_MARKER" = 1
function __orca_shell_ready_marker --on-event fish_prompt
builtin printf "${escapedMarker}"
functions -e __orca_shell_ready_marker
end
end`
}
export function getZshFinalZdotdirRestoreBlock(homeExpression = '"${ORCA_ORIG_ZDOTDIR:-$HOME}"') {
return `_orca_home=${homeExpression}
case "\${_orca_home%/}" in
*/shell-ready/zsh) _orca_home="$HOME" ;;
esac
# Why: after Orca's last wrapper file has loaded, the interactive shell should
# expose the same ZDOTDIR a normal zsh startup would expose.
export ZDOTDIR="$_orca_home"
unset _orca_home
`
}