* fix(shell-ready): honor ZDOTDIR without breaking zsh scoping
Fixes#1866
This reimplements PR #1737 (reverted in #1864) with a safer approach
that preserves normal zsh startup semantics.
**Core fix**: Discover ZDOTDIR by sourcing user ~/.zshenv in a subshell
instead of inside a wrapper function. This preserves top-level zsh
scoping for common patterns like `typeset -U path` that broke in the
original implementation.
**Shell safety improvements**:
- Use `printf '%s\n'` instead of `echo` for capturing ZDOTDIR (handles
special characters in paths safely)
- Subshell isolates early returns and side effects from wrapper
**Code quality**:
- Extract duplicated zsh wrapper template to `src/main/shell-templates.ts`
- Both local-pty and daemon paths now share identical wrapper logic
**Test coverage**:
- Add live zsh subprocess tests that spawn real zsh to verify:
- XDG ZDOTDIR discovery works
- `typeset -U path` in .zshrc preserves top-level scoping
- Early returns in .zshenv don't crash the wrapper
- Vanilla (non-XDG) configs fall back to HOME correctly
- Template structure tests validate subshell discovery logic
Before (broken):
```zsh
__orca_source_user_zshenv() {
source "$HOME/.zshenv" # typeset becomes function-scoped
}
```
After (fixed):
```zsh
_orca_discovered_zdotdir=$(
unset ZDOTDIR
[[ -f "$HOME/.zshenv" ]] && source "$HOME/.zshenv" 2>/dev/null
printf '%s\n' "${ZDOTDIR}"
)
export ORCA_ORIG_ZDOTDIR="${_orca_discovered_zdotdir:-${_orca_spawn_orig_zdotdir:-$HOME}}"
```
The subshell sources .zshenv at top-level (preserving normal scoping),
captures only the ZDOTDIR value, then exits. User rcfiles (.zshrc, etc.)
are still sourced at the wrapper's top level, so all scoping works normally.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add shell-script-literal test framework and improvements
Adds a new declarative test framework for shell-ready tests that uses
literal shell scripts (copy-pastable into terminals) with inline snapshots.
Framework features:
- Shell scripts as string literals with # Run: marker to split setup/test
- Direct script execution (no brittle parsing) via temp files
- Path normalization for reproducible snapshots (<HOME>, <WRAPPER_DIR>)
- Auto-detects shell from command, supports bash/zsh/sh
- Inline snapshot testing with vitest toMatchInlineSnapshot()
Code quality improvements:
- Extract escapeRegex to shared string-utils.ts (deduplicates 2 copies)
- Refactor shell-templates.ts for readability (condense comments, add structure)
- Pre-compile regex patterns to avoid hot-path allocation
- Fix path normalization to sort by length (prevent nested path corruption)
- Fix actualUserHome handling to skip empty values
All tests passing (64/64 shell-ready tests, 55/55 affected tests).
Files added:
- src/main/providers/__tests__/shell-ready-framework/shell-script-test.ts
- src/main/providers/__tests__/shell-ready-framework/README.md
- src/main/providers/__tests__/shell-ready-framework-example.test.ts
- src/shared/string-utils.ts
Files modified:
- src/main/shell-templates.ts (readability cleanup)
- src/main/codex/config-toml-trust.ts (use shared escapeRegex)
- src/main/daemon/shell-ready.test.ts (updated for new framework)
- src/main/providers/local-pty-shell-ready.test.ts (updated for new framework)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(shell-ready): preserve zshenv semantics
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Add Command Code as a supported CLI agent
Register Command Code (https://commandcode.ai) across the agent
catalog, launch config, telemetry kind, agent-status icon record, and
settings search. Detection uses the full `command-code` binary rather
than the shorter `cmd` alias that `npm i -g command-code` also
installs — agent-process-recognition normalizes process names by
stripping `.exe`/`.cmd` extensions, so using `cmd` would collide with
Windows' built-in `cmd.exe` shell.
Closes#2083
* Complete Command Code agent registration
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Adds OMP as a first-class TUI agent and keeps Pi/OMP overlay state isolated across local and relay PTY paths.\n\nValidated locally with focused Vitest coverage, typecheck, lint, git diff --check, and Electron manual verification using installed omp (omp/15.3.2).
Reverts the hidden terminal pressure changes from #2661 to restore the prior terminal rendering path while the fullscreen TUI regression is handled separately.
* Fix Orca Mobile layout and sidebar controls
* Use brand icons for mobile platform badges
* Add readable card for mobile pairing steps
* Align mobile flow primary actions
* Fix mobile sidebar toggle defaults
Fix JIS bracket/backslash shortcut intent matching across tab and terminal-pane shortcuts.\n\nReview follow-up:\n- centralize semantic punctuation matching for bracket/backslash keys\n- preserve physical-code fallback when key text is inconclusive\n- avoid stealing AltGr bracket input on Windows/Linux\n- add regression coverage for JIS, AltGr, and fallback behavior
* Surface dev-server URLs in workspace ports panel
Why: the OS scanner only sees `127.0.0.1:3001`, so clicking a Vite
port in the panel opens the wrong host/protocol when the dev server
binds with a custom hostname or HTTPS (e.g. `https://local.getmontecarlo.com:3001/`).
We now watch PTY output for advertised origins and prefer them at
display, copy, and open time. Falls back to the OS-derived address.
The watcher taps the central `runtime.onPtyData` path so it sees local,
daemon-backed, and SSH PTYs uniformly. URL candidates are validated with
`new URL()`, sanitized to origin only (no path/query/fragment/userinfo),
and gated by listener-PID validation so stale entries get evicted when
a different process reuses the port.
SSH `DetectedPort` enrichment is out of scope here — that panel uses
separate data shapes and port-forwarding may rewrite the local port,
making naive URL reuse incorrect. Tracked as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Extend advertised-URL enrichment to SSH ports
Why: the first cut only enriched the local workspace ports panel. SSH
worktrees use a separate panel with `DetectedPort` and `PortForwardEntry`
shapes scanned by the remote relay, so the same dev-server banners that
flow through Orca's SSH PTYs were captured by the watcher but never
surfaced.
The renderer-side SSH scanner doesn't know which worktreeId the watcher
keyed an entry under, so a small helper walks the store to find every
worktree attached to a given connection and asks the watcher for the
best match per port (custom DNS host > loopback > LAN IP, https and
recency as tie-breakers). Enrichment runs at every broadcast and list
site in src/main/ipc/ssh.ts.
For a forwarded SSH port, the open action now uses
`advertisedProtocol://customHost:localPort`. We deliberately reuse the
local forward port — the remote port number is not reachable from the
local machine — and we only reuse the host when it's a custom DNS name
(IP literals and the remote's `localhost` get ignored, falling back to
127.0.0.1 like before). Loopback advertisements from the remote box are
a no-op because they don't translate to the local browser.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Codex review of advertised-URL enrichment
Four findings from the post-implementation review:
- Watcher rejects wildcard advertised hosts (`0.0.0.0`, `::`, `*`). The
OS scanner already normalizes wildcards to localhost for the connect
host; if a dev server prints `http://0.0.0.0:3000/` verbatim we keep
that sensible default instead of letting the watcher overwrite it
with a host the browser can't open.
- Pre-bind pending buffer is now capped at 32 distinct PTY IDs with
LRU eviction. Spawn-failure paths could otherwise leave never-bound
entries forever — per-PTY 16 KiB bound was the only guard.
- `clearProviderPtyState` now calls `advertisedUrlWatcher.unbindPty`.
SSH reattach failures, `clearPtyOwnershipForConnection`, and daemon
spawn-failure cleanup bypass `runtime.onPtyExit`; routing through the
central provider teardown plugs those holes (and is idempotent with
the existing `onPtyExit` hook for natural exits).
- `lookupBest` accepts an optional current listener PID and runs the
same pin-then-evict logic as `lookup`. `enrichSshDetectedPorts`
threads `DetectedPort.pid` through so a stale advertised URL gets
evicted on remote port reuse. Forward enrichment stays best-effort
(forwards are persisted config, not observed listeners — detected
enrichment is the eviction path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: refresh SSH ports when advertised URLs change
Co-authored-by: Orca <help@stably.ai>
* fix: show advertised URLs in port rows
Co-authored-by: Orca <help@stably.ai>
* fix: address advertised URL review issues
Co-authored-by: Orca <help@stably.ai>
* fix: tighten advertised URL cache handling
Co-authored-by: Orca <help@stably.ai>
* fix: align advertised URL edge cases
Co-authored-by: Orca <help@stably.ai>
* fix: handle advertised URL cache invalidation
Co-authored-by: Orca <help@stably.ai>
* fix: show status port action tooltips
Co-authored-by: Orca <help@stably.ai>
* fix: keep port action tooltips above popover
Co-authored-by: Orca <help@stably.ai>
* fix: clear port action focus after click
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>