* Don't enqueue local PR refresh for remote-host repos (fix renderer OOM)
The renderer enqueues GitHub PR refreshes for worktrees. Remote/SSH/runtime
worktrees are meant to refresh through the runtime route (getRuntimeRepoTarget),
but when that route is unavailable — host not the active environment, or
disconnected — the call falls through to the local `gh:enqueuePRRefresh` IPC.
The local handler only resolves repos registered in the local store, so it
rejects every such call with "Access denied: unknown repository path".
With a remote worktree active (e.g. a runtime "Project server" workspace), this
fires on a loop (worktree activation + SWR polling), flooding the renderer with
failed invokes and unhandled rejections. Observed hundreds of these per session;
the renderer JS heap climbs to the V8 ceiling (~3.5GB) and V8 aborts the process
(crash-reports.json: renderer crash, exitCode 5, usedHeapMB == heapLimitMB).
Add isLocalHostPRRefreshCandidate and gate all four enqueue sites on it, so the
local handler only ever receives local-host candidates. Remote candidates with
no available runtime route are skipped (their PR status refreshes once the host
is active/reconnected) instead of spamming a handler that can't serve them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: cover local executionHostId in PR-refresh host guard
Address CodeRabbit nitpick on #6094: add a positive case where a local repo
carries an explicit executionHostId === LOCAL_EXECUTION_HOST_ID, completing
predicate coverage for isLocalHostPRRefreshCandidate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: route PR refreshes by repo host
Co-authored-by: Orca <help@stably.ai>
* fix: skip disconnected SSH PR refreshes
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* defer committing workspace directory path changes until blur or enter
Introduce draft state for the workspace directory path input, only
committing the changes on blur, Enter, or Browse completion. This
prevents the backend from creating incomplete directory path prefixes on
disk with `mkdir` during intermediate typing states.
* Fix agent-hooks test mock and clean up unused imports
* Update the RuntimeClient mock in agent-hooks test to return a fully
typed instance with the correct schema, eliminating type assertions.
* Remove an unused React import in WorkspaceDirectorySetting test.
* Detect agents in the serve user's package-manager bins; don't pin empty remote-detect results
Two defects prevent the desktop client from detecting agents installed on a
remote `orca serve`:
D1 (src/relay/relay-command-env.ts): the relay probes agents with a login shell
(`/bin/sh -lc`), which sources ~/.profile but not the interactive ~/.bashrc that
real PTY sessions source. Per-user package-manager bins added to PATH only in
~/.bashrc are therefore invisible to detection. Resolve the serve user's
package-manager bins from $HOME (.local/bin, .npm-global/bin, .cargo/bin,
.bun/bin, go/bin, .deno/bin, .local/share/pnpm), honor npm_config_prefix, append
to the POSIX fallback set, dedupe against inherited PATH; win32 unchanged.
D2 (src/renderer/src/store/slices/detected-agents.ts): an empty detection result
([], truthy in JS) was pinned by the `if (existing)` short-circuit, so detection
never retried after the server env was fixed. Changed to `if (existing?.length)`;
in-flight dedup still prevents re-detect storms; failures stay uncached.
No new transport, no PowerShell/bash bridge. D1 and D2 are independent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Also patch the runtime (orca serve) detection path against the empty-pin bug
The D2 fix guarded only ensureRemoteDetectedAgents (the SSH path). The
desktop client's headline scenario — pairing to a remote `orca serve` —
routes through ensureRuntimeDetectedAgents (kind: 'runtime'), which had
the identical bug: an empty detection result ([], truthy in JS) was pinned
by the `if (existing)` short-circuit, so a later install / PATH fix was
never picked up without a reconnect.
Change the runtime guard to `if (existing?.length)`, mirroring the remote
path. In-flight dedup still collapses concurrent re-detects; failed
requests stay uncached. Add a runtime regression test proving detection
re-runs after an empty result (fails on the pre-fix guard).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Honor package-manager relocation env vars in relay PATH detection
Extend the existing npm_config_prefix precedence to cargo, bun, deno, go,
and pnpm so relocated installs (CARGO_HOME, BUN_INSTALL, DENO_INSTALL,
GOBIN/GOPATH, PNPM_HOME/XDG_DATA_HOME) are detected, falling back to the
$HOME defaults. Mirrors the remote-node probe's NVM_DIR handling (#6037).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Tighten remote agent detection retries
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: LesleyMurfin <260182349+LesleyMurfin@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Apply styling via data attributes and custom CSS in main.css instead
of basic utility classes. This provides a more distinct background color
for selected files in both light and dark modes, and adds a subtle border
to ensure legibility against panel backgrounds.
* Load all hosts' repos at startup so cold start doesn't hide local repos
On a cold start, the renderer restores the last-active workspace. If that
workspace lived on a remote runtime environment, settingsForExecutionHostOwner
re-activates that environment, and startup's fetchRepos() then loads only the
active host's repos. The result: every local repo disappears from the sidebar
(even under the "All hosts" scope, which can only filter already-loaded repos),
and adding a local folder fails with "Not a valid git repository" because the
path is validated against the remote host. This reproduces on any cold start
regardless of launcher; warm activations skip restore and appear unaffected.
Add fetchReposForAllHosts(), which loads the local host plus every configured
runtime environment and merges them into the unified repos array (each host
fails soft so an unreachable/disconnected host is skipped without blocking the
others). Call it at startup instead of fetchRepos() so the sidebar shows local
+ remote together no matter which environment is active.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Schedule safe-auto fork sync per host in fetchReposForAllHosts
Codex review caught that the new all-host startup fetch dropped the
scheduleSafeAutoForkSync side effect that both fetchRepos and
fetchRuntimeEnvironmentRepos run after merging a host. Without it, repos with
forkSyncMode 'safe-auto' would stop syncing on cold start. Schedule the sync
for each fetched host's repos after applying its result.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Load startup project data across all hosts
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Fix remote Node.js detection for nvm, mise, asdf, and volta
Remote Node resolution failed when node was installed via a version
manager (nvm with custom NVM_DIR, mise, asdf, volta) or when the user's
login shell was zsh/fish rather than bash.
Root cause: the SSH exec transport runs every command under /bin/sh,
which never sources shell init files. The only init-aware path was a
hardcoded `bash -lc` fallback that missed zsh/fish users and was never
reached for the newer version managers. nvm was handled by guessing
~/.nvm (breaking custom NVM_DIR), and mise/asdf/volta had no probes at
all. There was also no version gate, so nvm's highest-version glob
could return Node 8/10/12 and crash the relay on launch.
Fix: resolve via the user's own $SHELL as a login shell first (the only
path that runs nvm.sh / mise activate / asdf.sh init hooks), then fall
back to direct path probes for all major managers (nvm respecting
$NVM_DIR, fnm, mise, asdf, volta, n) plus system locations. Every
candidate is version-checked against the relay's Node 18+ requirement
before being accepted.
* Address CodeRabbit review: probes-first, no || short-circuit
- Reorder to path-probes first (deterministic, doesn't depend on shell
rc-file semantics where bash -lc skips .bashrc and zsh -lc skips
.zshrc — exactly where nvm/mise/asdf hooks live).
- Join probes with newlines instead of || so an empty
`ls | sort -V | tail -1` (exit 0) doesn't mask later probes.
- Deduplicate candidate paths before version-checking.
- Drop unreachable mock and fix misleading $SHELL-unset test name.
- Login shell is now a fallback for custom ~/.profile PATH setups.
* Fix remote Node path probing portability
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Fix blank/unclosable mobile emulator tab in floating workspace
FloatingTerminalPanel only handled terminal/browser/editor content types, so simulator tabs rendered no pane and routed close through closeFile (a no-op for simulator tabs). Treat simulator as its own content type: render EmulatorPane for the active simulator tab, wire activeSimulatorTabId into TabBar, and close via closeUnifiedTab.
* Add floating Mobile Emulator tab E2E smoke test
Adds a deterministic Electron/Playwright spec that seeds a simulator unified tab in the floating workspace, asserts the emulator pane renders, and closes it through the real tab-strip X. Adds stable data-emulator-pane selectors and a data-tab-close-button hook on the simulator tab chrome, plus a targeted package script. No live iOS Simulator or Orca Computer/AX dependency.
* Exclude simulator tabs from floating Close All Files
Close All Files filtered out only terminal/browser tabs, so after the simulator render/close fix it would also close the Mobile Emulator. Simulator tabs are not files; exclude contentType 'simulator' to match terminal/browser behavior, and add a regression test asserting Close All Files closes the editor tab but leaves the simulator open.
* Keep floating simulator tabs mounted
---------
Co-authored-by: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
* fix(codex): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)
Windows splits raw hook commands on whitespace, so a user profile path
like `C:\Users\Jane Doe` made Codex hooks exit with code 1. Add a
wrapWindowsHookCommand helper that invokes the .cmd through
`cmd.exe /d /c call "..."` and use it in getManagedCommand.
* fix(agent-hooks): wrap Windows hook command in cmd.exe for all agents with raw .cmd path (#6078)
Apply the wrapWindowsHookCommand helper to cursor, command-code, gemini,
grok, and droid, which shared the same raw-scriptPath-on-Windows pattern
as codex. A user profile path with a space (e.g. `C:\Users\Jane Doe`)
used to split at the space and fail with exit code 1.
Agents that already handle spaces correctly are left untouched:
- claude/openclaude (Git Bash + forward slashes)
- copilot (PowerShell with quoted path)
- kimi (Git Bash + forward slashes)
- antigravity (event-specific wrapper .cmd files)
- devin (already wraps via `cmd /d /s /c ""...""`)
Each fixed agent gets a Windows-only test asserting the cmd.exe wrapping
survives spaces in the profile path.
* fix(claude): wrap Windows hook command in cmd.exe to survive spaces in profile path (#6078)
Claude Code runs hooks through Git Bash on Windows. The previous
forward-slash trick only works when the path has no spaces — Git Bash
splits `C:/Users/Jane Doe/...` at the space and tries to execute
`C:/Users/Jane` as a command. Use wrapWindowsHookCommand so the .cmd is
invoked through `cmd.exe /d /c call "..."`, which Git Bash treats as one
argument. Applies to both Claude and OpenClaude (shared getManagedCommand).
* Harden Windows agent hook launcher
---------
Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
Fixes#5906
The divider line on the workspace tab strip used full opacity
border-border, while other divider lines in the UI use varied
opacities. Changed to border-border/70 to match the standard
divider opacity used throughout the application.
Signed-off-by: Noah Khomer <108771853+noahkhomer18@users.noreply.github.com>
* feat(mobile): surface "Link an existing PR" in PR sidebar empty state
The mobile link-PR building blocks (MobileLinkPrForm, linkMobilePr,
parseGitHubPrReference) existed and were tested, but had no entry point —
only unlink was wired up. Add a "Link an existing PR" action to the no-PR
empty state that opens MobileLinkPrForm and refetches the sidebar on
success, mirroring desktop's link flow (GitHub-scoped via worktree.set).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Polish mobile PR link empty state
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
* feat: copy a file from the explorer to the OS clipboard
Add a "Copy" action to the file explorer context menu that puts the
actual file on the system clipboard, so pasting in Finder/Explorer/a
file manager drops the file itself instead of its path as text.
- macOS: write a public.file-url buffer; Finder synthesizes the legacy
file types it needs for paste.
- Windows: Set-Clipboard -LiteralPath populates the CF_HDROP file drop
list that Explorer pastes as a file.
- Linux: best-effort, picked by desktop — text/uri-list on KDE,
x-special/gnome-copied-files on GNOME-family — via wl-copy or xclip.
- Local files only; the action is hidden for remote/SSH files and the
web client, where no OS clipboard reference is possible.
The platform logic never throws: failures resolve to a structured
result and the renderer surfaces an error toast.
* Review copy-file clipboard safety
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix: darken Builtin Tango Light ANSI colors for readability
Nine ANSI colors had contrast ratios below 3:1 against the white
background, making CLI output unreadable in light mode. Darkened
to readable Tango shades while keeping the warm palette identity.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: enable xterm minimumContrastRatio and add light theme tests
xterm.js minimumContrastRatio (4.5) auto-adjusts low-contrast ANSI
foreground colors at render time — a safety net for all themes,
including custom imports. Added test coverage for this option and
for the Tango Light ANSI color contrast assertions.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: trigger CI re-run
---------
Co-authored-by: zhangqinzhong <vhudsongit@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(terminal): attach dropped image files via bracketed paste
Dragging an image file into a terminal pane wrote the shell-escaped path
as raw PTY input. Terminal TUIs (Claude Code, Codex, etc.) only turn a
path into an image attachment when it arrives as a *bracketed paste*, so
dropped images showed up as a literal path instead of an `[Image]`
placeholder — unlike iTerm2/Warp, which wrap dropped paths in bracketed
paste, and unlike Orca's own clipboard screenshot flow (#2842).
Route dropped image files (by extension, mirroring IMAGE_MIME_TYPES)
through `wrapTerminalBracketedPasteText` with the raw, un-escaped path so
the file-existence check those tools run on the pasted path succeeds.
Non-image drops keep the original shell-escaped, space-separated
behaviour for use in shell commands.
* fix(terminal): separate image paste from following non-image path
A mixed drop where an image precedes a non-image path concatenated the
two: the image bracketed-paste payload has no trailing space, so the
next shell-escaped path was appended directly after it. Add a single
separating space after an image payload only when the next path is a
non-image — back-to-back image pastes are self-delimiting and a stray
space between them would land in the TUI input.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Review terminal image drop path handling
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* fix: close mirrored editor tabs on the host so they stop reopening
On web and mobile companions, editor file tabs are mirrored from the
host's runtime-session snapshot, which the host derives from its
authoritative `openFiles`. Closing a mirrored tab only removed it
locally, so the next snapshot re-mirrored the still-open host file and
the tab immediately reopened.
Route the close to the host from the single chokepoint every editor
close funnels through (`store.closeFile`): when the file is mirrored and
a web runtime session is active, send `session.tabs.close` for the host
tab id. The RPC's recorded close-intent suppresses re-mirroring until the
host snapshot catches up, so the local removal is not undone. No-op for
the host's own (non-mirrored) files.
Also fix the desktop `ui:closeSessionTab` handler so a companion-driven
editor close goes through `closeFile` (clears `openFiles`) instead of
`closeUnifiedTab` (tab strip only), which had the same re-mirror bug for
mobile-originated closes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix mirrored editor close import cycle
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Fix idle remote runtime session work
* fix: stamp remote/SSH worktrees with their repo execution host
Remote-runtime and SSH worktrees are fetched/created/reparented through the
owning host, which reports them from its own perspective (hostId defaults to
"local"). After the per-worktree hostId override in #2, that bogus "local"
overrode the repo's runtime owner, so terminals/sessions for a remote worktree
resolved to the local machine instead of the remote (clicking an omarchy-office
worktree opened a shell on the local omarchy-thinkpad).
Re-stamp every runtime worktree payload with the repo's execution host via a
shared withRepoHostId helper, applied at all worktreesByRepo ingress points:
toVisibleWorktrees (fetch), createWorktree, and applyWorktreeLineageUpdate.
Local-owned repos are left untouched, so an explicit local worktree still
overrides a runtime repo owner. Mirrors how repos already get repoWithFetchedOwner.
* fix: auto-discover remote runtime projects on connect
PR #2 gated the global session-tab sync to web clients only, removing the
desktop path that eagerly populated remote projects. With no on-connect repo
fetch left, remote projects only appeared after the user opened the
Add-Project dropdown (which calls fetchRuntimeEnvironmentRepos directly).
Seed an initial repo/worktree/lineage refresh for every runtime environment
that is already connected when useIpcEvents mounts, and for each one that
becomes connected afterward. Reuses the debounced/throttled refresh scheduler,
and works regardless of whether the remote server emits client events.
* fix: tighten idle runtime refresh behavior
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Don't show local Windows shells for serve-runtime worktrees
The Windows shell menu (PowerShell/CMD/Git Bash) was offered for a
serve/remote-runtime worktree whose host is not Windows (e.g. a Linux
orca serve), where those local shell choices are meaningless and the
plain New Terminal already opens the runtime's default shell. AND a new
runtimeHostIsNonWindows exclusion into the existing shouldShowWindowsShellMenu
gate, keyed on the probed runtime host platform so a LOCAL Windows-WSL
project runtime (hostPlatform === win32) keeps its shell menu. Adds
regression tests.
Rebased onto current main (was 75 commits stale); net delta unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Hide runtime Windows shells until host is known
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Fix floating workspace terminal selector_not_found on a remote runtime
resolveWorktreeSelector threw selector_not_found for the floating-terminal
sentinel id (global-floating-terminal) because it is a repo-less synthetic
session with no entry in the worktree catalog. A remote client paired to this
serve sends that sentinel, so the normal id: lookup failed and the floating
pane rendered black. Resolve the sentinel (bare and id:-prefixed) to a virtual
ResolvedWorktree rooted at the serve user's home so the PTY spawns in a real
existing dir; unknown id selectors still throw selector_not_found. Adds two
regression tests.
Rebased onto current main (was 75 commits stale); net delta unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Scope floating terminal selector to terminal launches
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Harden mobile session-tab snapshot reconciliation and terminal creation. Rejects stale mobile snapshots, tombstones locally closed tabs until the publisher catches up, rolls back half-created terminal tabs when their surface never appears, and scopes terminal-create idempotency by worktree.
* fix(mobile): keep iOS terminal inputs on the default keyboard
iOS treated keyboardType="ascii-capable" as an ASCII-only input surface,
which hides non-Latin keyboards (Zhuyin, Japanese, Korean) from the iOS
keyboard switcher, so terminal users could not switch away from English.
Use the system default keyboard for terminal inputs on every platform so
IMEs stay selectable, while keeping autoCorrect/spellCheck off so commands,
flags, and paths are not rewritten by the OS keyboard. The keyboard-type
helpers now return a single 'default' value; their dead per-platform
branching was removed.
Fixes#5525.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mobile): tighten terminal keyboard comment
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
* Resolve push targets for linked hosted reviews
Automatically resolve and persist push targets for linked GitHub PRs
and GitLab MRs to align source control actions with the review branch.
- Add `ensureHostedReviewPushTarget` to hydrate missing push targets
- Implement GitLab MR push target resolution alongside GitHub PRs
- Block actions from fallback to unrelated upstreams while resolving
- Prevent duplicate concurrent API lookups with in-flight tracking
* Clear stale review push targets when unlinking or replacing reviews
- Ensure unlinking a hosted review or replacing it with another provider
properly clears the previously associated push target, preventing pushes
from being steered to outdated review heads.
- Encode explicit push target clears as null over JSON-RPC to bypass the
automatic omission of undefined properties during transit, then map them
back to undefined in the main runtime database.
- Add stricter validation to ensure pull request and merge request IDs are
positive integers, ignoring invalid or dummy numbers like zero.
Long branch names and workspace titles were getting truncated, hiding
important identity context from developers.
* Add a dynamic `TruncatedSidebarLabel` component that measures text
overflow and renders a tooltip only when the text is actually truncated.
* Use the new label for branch names in `WorktreeCard`, disabling its
nested tooltip when parent hover details are active to prevent conflict.
* Update `WorktreeCardDetailsHover` to use `break-words` instead of
truncating, ensuring full branch and workspace identities wrap and remain
fully readable in the details panel.