* fix(mobile): normalize Grok rotating OSC titles at the main observation boundary
Desktop already collapses Grok Build's rotating working frames via the
renderer's normalizeTerminalTitle, but the main process stored raw OSC
titles, so mobile session tabs (fed from pty.lastOscTitle) still saw a
distinct title every spinner frame and re-touched snapshots each time.
Apply normalizeTerminalTitle once where main records an observed OSC
title, before the prevTitle comparison that gates session-tab and
mobile-snapshot touches, and normalize hydration-seeded titles the same
way so the first live frame after a seed compares equal. Agent status
stays detected from the raw title, mirroring the renderer tracker.
Covers remoted/SSH PTYs too since they surface through the same main
observation path.
Co-authored-by: Orca <help@stably.ai>
* Fix Grok and Pi terminal title normalization and status detection
- Require a strict "spinner - phrase - grok" pattern for Grok working
titles to prevent false positives on other agent tasks ending in
"- grok" (such as Claude or Codex).
- Treat collapsed "Pi" and "OMP" synthetic titles as idle by default
to prevent them from reverting to neutral status after normalization.
- Prevent duplicate mobile session tab updates during Grok status
frame rotations by ensuring they normalize to a stable title.
---------
Co-authored-by: Orca <help@stably.ai>
Introduce an external link action in the header of the Mobile PR View
Panel. This provides a persistent and easily accessible shortcut to open the
current pull request's canonical URL in the system browser.
Cursor Agent parks the real cursor on a blank row and hides it (?25l),
drawing its own caret. Orca's post-replay resets forced ?25h, painting a
stray cursor block below the agent's prompt after reattach and after
hidden-to-visible snapshot restores.
Root cause fix: stop re-showing a cursor the replayed bytes intentionally
hid, instead of compensating afterwards.
- Live-agent reattach reset now preserves the payload's final DECTCEM
state (last ?25l/?25h wins); the post-parse viewport veto re-shows the
cursor when the screen disproves a live parked agent, so a shell can
never inherit a permanently hidden cursor from a dead TUI's leftovers.
- Hidden-output restore now uses a live-agent variant of the snapshot
reset when status/title corroborates a live agent: forcing ?25h
re-showed the parked cursor, and ?1004l permanently silenced the
focus-in the agent needs to unpark (agents only enable focus reporting
at startup, so nothing ever re-armed it).
Co-authored-by: Orca <help@stably.ai>
* feat(source-control): add Fix push failure with AI for pre-push hooks
Detect pre-push hook failures separately from auth/transport errors so
push toasts and inline messages no longer suggest checking repo access.
Mirror the commit-failure recovery flow with a fixPushFailure action,
summary panel, details dialog, and agent launch recipe in Settings.
Fixes#6497
* feat(source-control): add push failure AI recovery
Co-authored-by: dalveytech-vincent <vincent@dalveytech.com>
---------
Co-authored-by: dalveytech-vincent <vincent@dalveytech.com>
Grok Build's working OSC titles interpolate a rotating status or tool
phrase between the spinner and its name, causing tab and sidebar
titles to fluctuate rapidly. Collapse these working frames to a stable
"⠋ Grok" label, while leaving idle and session titles untouched.
* Show empty sessions by default in AI Vault Panel
Change the default state of `hideEmptySessions` from true to false, and update the active filter count calculation to treat false as the new default.
* Centralize AI Vault view defaults and fix reset state alignment
Consolidate the default values for sorting, grouping, and hiding empty
sessions into a single place. This prevents inconsistencies between the
initial state, the badge count adjustment checks, and the "Reset" view
action, where the reset previously toggled hideEmptySessions to true
contrary to the initial false value.
* Count agent adjustments by membership instead of list length
Ensure that swapping one agent for another (which keeps the total
array length the same) is correctly identified as a deviation
from the default of having all agents enabled.
Claude's AskUserQuestion tool is auto-allowed, so it emits a PreToolUse (not
PermissionRequest) while blocked on a human answer. normalizeClaudeEvent mapped
that to `working`, so the sidebar showed a spinner that decayed to grey while
the question sat. Map the auto-allowed AskUserQuestion PreToolUse to `waiting`
instead, mirroring the Kimi/OpenCode handling, so the row and worktree dots read
amber "Waiting for input".
Separately, a frozen braille-spinner title left by an exited agent (e.g.
"⠐ Review branch for regressions" over a shell prompt) kept classifyTitleActivity
returning `working`, spinning the worktree dot forever with 0 agents — the row
builder rejects the same unattributable title, so no agent row is shown. Gate
tabHasStatus's title-derived working/permission on the same agent attribution the
row builder uses (resolveAgentTypeFromTerminalTitle), so a title only spins the
dot when it would also show a row. Hook-driven status (hasLiveWorking/
hasPermission) is untouched.
* fix(terminal): shape Arabic/RTL runs on the cell grid via xterm character joiner (#5262)
Register a character joiner that groups contiguous RTL runs (tunneling
through neutral spaces/digits/punctuation between RTL words) so both the
WebGL and DOM renderers draw each run as one string, applying native
contextual shaping and BiDi ordering inside the run's grid-aligned cell
box. Buffer and PTY stream are untouched; xterm un-joins ranges holding
the cursor or a partial selection, keeping cursor visibility and
selection cell-accurate. Non-RTL lines early-return via a single
charCodeAt sweep.
Co-authored-by: Orca <help@stably.ai>
* Deregister Arabic shaping joiner when disposing pane
Since xterm.js does not automatically remove registered character joiners
upon terminal disposal, they must be explicitly deregistered to avoid
memory and registration leaks.
- Update `registerArabicShapingJoiner` to return a cleanup function.
- Store and call the cleanup handler when disposing a managed pane.
- Update tests to assert proper registration and deregistration.
* fix(terminal): gate RTL joining on live WebGL, tunnel ZWNJ/ZWJ/RLM, lock joiner lifecycle in tests
Review findings on the Arabic shaping branch:
- The DOM renderer sizes a joined span with one letter-spacing value applied
per character, so joined RTL runs blew out row grid alignment whenever the
WebGL renderer was unavailable (GPU off, auto policy, context loss). The
joiner now only returns ranges while the pane's WebGL addon is live.
- ZWNJ/ZWJ/RLM (mandatory in Persian orthography) hard-broke RTL runs,
rendering word halves in swapped visual order. They are now transparent
inside a run without extending or counting toward it.
- openTerminal's existing unicode-ordering test failed at HEAD because the
fake terminal lacked registerCharacterJoiner; the harness now implements
the joiner API and a new test locks register-on-open/deregister-on-dispose.
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): keep zero-width RTL marks from opening joined runs, treat ALM as transparent
Round-2 review findings:
- A combining mark (tashkeel/niqqud) orphaned after an LTR base could open
an RTL run mid-cell; xterm rounds that string range to an empty joined
cell range and the WebGL renderer draws an empty glyph over the next
character. Marks now extend and count only in runs opened by a spacing
RTL letter.
- ALM (U+061C) sat inside the strong-RTL block scan, so it opened and
counted toward runs unlike RLM. It now joins the transparent set.
- New test locks openTerminal wiring the joiner predicate to
pane.webglAddon (previously () => true would pass the suite).
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): treat zero-width Cf controls in RTL blocks as run-transparent
Round-3 review finding: zero-width format controls that live inside the
strong-RTL scan ranges (Arabic number signs U+0600-0605, end of ayah U+06DD,
Syriac abbreviation mark U+070F, disputed end of ayah U+08E2) and the BOM/
ZWNBSP U+FEFF are width-0 in xterm. Like combining marks they could open a
run mid-cell, producing an empty joined cell range that blanks the following
glyph in the WebGL renderer. They are Cf not Mn, so canOpenRtlRun could not
catch them. Fold them into the run-transparent set (never open, count, or
break a run) and document the upstream standalone-width-0-cell join skew.
Co-authored-by: Orca <help@stably.ai>
* Document WebGL decoration limitation for Arabic shaping joiner
Add comments explaining a known upstream limitation in xterm.js WebGL
renderer where search-match highlights inside a joined run render
all-or-nothing, similar to ligatures.
---------
Co-authored-by: Orca <help@stably.ai>
* feat(mobile): add explicit keyboard dismiss control to terminal command dock
Add a fixed Hide control at the left of the terminal command dock accessory
bar whenever the software keyboard is open (keyboardHeight > 0). Tapping it
clears any pending live-input focus timer, blurs the live and buffered command
inputs, and dismisses the keyboard without sending bytes, switching input mode,
or clearing typed text.
The dismiss behavior lives in a dedicated, unit-tested terminal-keyboard-dismiss
module rather than the customizable accessory-key path, so the escape hatch
cannot be hidden by user shortcut customization. Available on every platform
where the IME covers the app (iOS and Android).
* review: harden keyboard dismiss control per adversarial review
- document the load-bearing clear-before-blur order in dismissTerminalKeyboard
- cover the both-handles-missing case in unit tests (5/5)
- move the #5106 first-tap comment onto the accessory ScrollView and add a
why-comment for the fixed Hide control
- add accessibilityRole=button and hitSlop to the Hide control for a larger,
semantically-correct touch target
* fix(mobile): harden hide button visibility and scroll layout
* refactor(mobile): use stacked keyboard+chevron glyph for dismiss control
Replace the icon+'Hide' text with the iOS-native dismiss glyph (keyboard
with a chevron-down beneath it). Narrower in the accessory row, removes the
icon/word redundancy, and reads as distinct from the >> input-mode toggle.
Accessibility label/hint/role unchanged.
* fix(mobile): align keyboard dismiss accessory height
* test(mobile): align vitest transform with Vite 8
---------
Co-authored-by: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
After #7750 removed the containment guard, the residual #7239 failure mode
is a persisted/inherited startupCwd whose directory no longer exists: every
spawn dies with the provider's missing-directory error. Fresh local renderer
spawns now opt in (cwdFallback: 'worktree') to recover at the workspace root
with a generic in-terminal notice; reattach, SSH, remote-runtime, runtime/API
and mobile callers keep exact cwd semantics, and existing directories —
including outside the worktree (#7685) — spawn as requested.
Co-authored-by: Orca <help@stably.ai>
The relay CLI shim on SSH remotes rejected every orchestration/mutation
command with 'Unsupported SSH Orca CLI command' because the host handled
relay CLI requests with a hand-rolled allowlist of five read-only-ish
commands. The host now runs the real bundled orca CLI entry (same entry
as the local shell command, via ELECTRON_RUN_AS_NODE) as a captured
subprocess, so remote invocations get the full command surface by
construction. Remote cwd is carried via ORCA_CLI_CWD so cwd-based
selectors (--worktree active) resolve against the caller's remote
directory; only Orca terminal-context env vars cross the bridge.
Host-interactive commands (serve, claude-teams, agent-teams-tmux) get a
targeted error, and the legacy in-process switch remains as a fallback
when the host CLI entry cannot be launched. Relay-side request timeouts
are raised to fit mutation and long-poll (--wait/--timeout-ms) commands,
and stdin forwarding now covers *-stdin payload flags.
Fixes#7716
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): guard mux dead-link detector against sleep/App Nap timer pauses
After system sleep or App Nap timer throttling, the first post-wake
timeout-check tick saw pre-pause keepalives as >20s stale and killed a
healthy link (false 'Connection timed out (no ack received)' ->
dispose('connection_lost') -> reconnect overlay churn). Track the last
tick time; when a tick gap far exceeds the interval, reset staleness
tracking, probe with a fresh keepalive, and let the next full window
make an honest liveness determination. A genuinely dead link is still
detected within ~25s after wake.
Also adds probeLiveness(timeoutMs): a keepalive round-trip primitive
that resolves true on the first frame of any kind, used by the resume
path to distinguish surviving links from dead ones.
Refs #7773
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): probe relay liveness on system resume instead of unconditional reconnect
powerMonitor 'resume' previously called connectionManager.reconnect()
for every active target, guaranteeing a teardown + reconnect overlay on
every wake even when the connection survived sleep. Now each session's
relay link is probed (keepalive round-trip, 5s timeout, one retry for
slow post-wake network); only targets whose probe fails are reconnected.
Dead-after-sleep connections still reconnect promptly. The 'suspend'
grace-time handling is unchanged.
Refs #7773
Co-authored-by: Orca <help@stably.ai>
* feat(relay): prefix relay.log diagnostic lines with ISO timestamps
The remote relay.log had no timestamps, which blocked correlating
reconnect flaps with user activity and sleep/wake windows while
diagnosing #7773. Daemon-mode diagnostic lines now carry an ISO
timestamp prefix ('<ISO> [relay] ...', grep-stable). Connect-mode and
orca-cli passthrough stderr is untouched since it goes back to the
app/user terminal and is parsed (handshake-mismatch detection).
The relay bundle is content-hashed at build time, so the versioned
install picks up the new relay automatically on next deploy.
Refs #7773
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): re-check session identity before post-probe resume reconnect
The resume probe can take ~10s; if the user disconnected the target or the
session/connection was replaced during that window, reconnecting would
resurrect an intentionally torn-down connection (CodeRabbit).
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* Sweep all persisted carriers of a removed SSH target id on re-adoption
reassignSshTargetId re-pointed repos and worktree metas but left the old
target id embedded in persisted session pty ids (ssh:<id>@@pty-N in tabs,
layouts, remoteSessionIdsByTabId), the startup reconnect list
(activeConnectionIdsAtShutdown, replayed via ssh.connect at boot — the
exact 'SSH target not found' in STA-1468), sleeping-agent resume records,
provisioned project host setups, sidebar host-scope arrays, and relay pty
leases. Any survivor resurfaces later as a failing connect or reattach.
New ssh-target-id-migration module re-points every carrier in one pass,
wired into reassignSshTargetId with per-carrier unit and store-level
round-trip tests.
Co-authored-by: Orca <help@stably.ai>
* Bridge SSH connection state to paired remote clients
The SSH surface was desktop-only: ssh:state-changed went to the host's
own BrowserWindow and the web client's ssh API was a no-op stub, so a
paired client's reconnect overlay never learned the host connected and
its target labels stayed empty (STA-1468 — overlay stuck on 'please
connect' over a live terminal).
- New sshStateChanged runtime client event, emitted from broadcastSshState
through OrcaRuntimeService onto the existing clientEvents stream.
- New ssh.listTargets / ssh.listRemovedTargetLabels RPC methods next to
the previously unused ssh.getState / ssh.connect.
- Web preload now routes listTargets / listRemovedTargetLabels / getState
/ connect to the paired host's runtime RPC instead of stubbing them.
- useIpcEvents applies sshStateChanged on paired web clients through the
same guarded path as desktop ssh.onStateChanged; desktop clients ignore
the event since a foreign runtime's targets would pollute their local
SSH store.
Co-authored-by: Orca <help@stably.ai>
* Harden the SSH reconnect overlay against stale or unknown target state
- Only present the destructive 'SSH host removed' state on positive
evidence (a removal tombstone label, or a hydrated non-empty target
list lacking the id). A client whose SSH state never hydrated has an
empty labels map for every id and must not offer workspace removal.
- After a failed Connect, resync target metadata so a stale overlay
converges to the ghost/re-adopted state instead of offering the same
failing Connect forever (the repeated 'SSH target not found' toast
loop in STA-1468).
Co-authored-by: Orca <help@stably.ai>
* Address CodeRabbit review on #7767
- Re-key workspaceSessionsByHostId partitions stored under a removed SSH
host id during re-adoption (no writer keys partitions by ssh host today,
but the schema tolerates it — re-key instead of stranding; live partition
wins when both keys exist).
- Track SSH target-list hydration explicitly (sshTargetsHydrated) instead
of inferring it from a non-empty label map, so a legitimately empty
target list still counts as removal evidence and a never-hydrated client
still never offers destructive removal.
- Apply the refreshed target list before the best-effort removed-labels
fetch in the overlay resync, so a labels failure can't discard it.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(main): detect CJK input source via cfprefsd on macOS 15
macOS 15's `plutil -extract <key> json` aborts with "invalid object in
plist for destination format" on the AppleSelectedInputSources array even
though it is all strings, so the selected-input-source probe threw and fell
back to the keyboard layout id (com.apple.keylayout.US). That disabled
forwardAsciiPunctuation, so third-party IMEs (Sogou, Doubao) sent half-width
,.? to the PTY instead of full-width ,。? in terminal panes and agent chat.
Apple's built-in IME happened not to trip the plutil bug.
Read the live prefs via `defaults export` (cfprefsd) and extract as xml1
before converting the clean subtree to JSON, dodging both the plutil json
bug and the stale on-disk plist. The parser and CJK term list are unchanged.
* fix(main): reap CJK input-source probe process group on timeout
Run the macOS input-source probe via detached spawn and SIGKILL the whole
process group on timeout so a wedged cfprefsd can't orphan the defaults/plutil
pipeline stages (the probe re-runs on every window focus-in). Pin absolute
/usr/bin paths, guard the stdout stream, and cover the non-zero-exit, spawn-
failure, and timeout fallbacks in tests.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: su'qiang <nslogname@MacBook-Pro.local>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): keep same-path imports host-qualified (#7018)
* review: harden runtime repo host match against SSH-repo hijack
An unstamped repo with a connectionId is an SSH repo (resolves to
ssh:<id>), so a same-path runtime import must not adopt it into a
runtime/local host. Match/adopt an unstamped repo only when it has no
connectionId, mirroring the existing local-IPC dedup guard
(src/main/ipc/repos.ts). Adds a regression test that fails without the
guard (SSH repo hijacked into runtime host).
Co-authored-by: Orca <help@stably.ai>
* review: only runtime hosts backfill an unstamped repo
A legacy unstamped repo is indistinguishable from a genuine local repo
(both have null executionHostId and connectionId). Restrict the adoption
branch to runtime incoming hosts so a local/ssh import at a colliding
path can never re-attribute a real local project to the wrong host.
Runtime is the only host that lost its identity to the pre-#7018
path-only import and needs the backfill. Adds a regression test.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>