Files
orca/src/main/shell-templates.ts
T
a0818b9a79 Fix agent tabs opening as a bare shell with oh-my-zsh vi-mode (#5315)
* fix(terminal): emit zsh shell-ready marker even when a user zle-line-init hook fails

Orca's zsh wrapper registered its OSC-777 shell-ready marker via
`add-zle-hook-widget line-init`. zsh's azhw dispatcher aborts the whole
hook chain when an earlier hook returns non-zero, and oh-my-zsh's vi-mode
installs a raw `zle-line-init` that exits 1 when VI_MODE_SET_CURSOR is
unset. The marker never fired, so every queued startup command sat on the
daemon's 15s pre-ready timeout — agent quick-launches looked like they
just opened a bare terminal.

Own `zle-line-init` directly: emit the marker first, then chain to any
previously-installed widget (called as a plain function so $WIDGET stays
`zle-line-init` for downstream azhw dispatchers). The registration is
idempotent — a re-source keeps the originally captured widget instead of
clobbering the chain. Applied to both the daemon and local-PTY wrappers
via a shared template helper.

* test(shell-ready): skip global zsh rcs so CI compinit can't block the marker tests

The real-zsh node-pty tests spawned 'zsh -l'/'zsh -i', which on CI runners
sources /etc/zsh/* whose global compinit hits insecure (group-writable) fpath
dirs and blocks on an interactive 'insecure directories [y/n]?' prompt before
zle-line-init fires. The marker never appeared and the tests timed out. Spawn
with -o noglobalrcs so only our ZDOTDIR wrapper/fixtures load; the marker
contract is unaffected.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-06-13 14:16:05 -07:00

151 lines
5.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}
_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
export ZDOTDIR=${quotePosixSingle(zshDir)}
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir
`
}
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
`
}
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
`
}