* refactor(mobile): demote address picker to optional disclosure on Relay
Relay provides remote access without requiring a specific local address,
so hide the picker behind a disclosure to keep the direct fast path
accessible without visual clutter. Reposition Sign in between the Relay
and LAN options to clarify it's Relay-specific. Keep custom addresses
always visible and force the disclosure open when settings search
targets the address picker.
* refactor(mobile): improve relay pairing guide and interface ranking
- Rank Docker/VirtualBox bridges below real LAN addresses so they're never auto-advertised as the default
- Clarify UI copy: 'Local network address (optional)' → 'Direct connection on this network'
- Better explain direct connection vs Relay roles and when each is used
- Fix Relay unavailability to be a build property, not dependent on current selection
* refactor(mobile): reframe local network address as optional in relay pai
Demote the address picker from primary action styling to an optional
disclosure with quieter visual treatment. Update messaging from "Direct
connection on this network" to "Also use a faster local path" to
clarify Relay is the default path and local addressing only applies
when nearby. Add explanatory hint text to set expectations that Relay
remains available when away.
* Retire SSH worktree metadata an authoritative scan proved gone
The metadata fallback's protection against resurrecting externally deleted
worktrees lived only in renderer module state, so it died on every reload
while the SSH WorktreeMeta it guarded against persists forever
(gcStaleWorktreeMeta exempts any repo with a connectionId, because a local
existsSync cannot probe a remote path). Repro: `git worktree remove` on the
SSH host, let the authoritative scan purge the row, restart — the startup
fetch runs before SSH connects and the fallback re-lists the deleted
worktree as a ghost row.
Chose option (a), deleting the stale persisted meta in main, over persisting
the removal memory: the metadata is the thing that outlives the worktree, and
Orca's own removals already delete it (removeWorktreeMetadataAndTransientState),
so external removals now converge on the same end state instead of accumulating
a second, parallel tombstone list that would itself need eviction. The
in-session memory stays for the window before the async delete lands.
New `worktrees:forgetRemovedForExecutionHost` only accepts SSH hosts, requires
an exact repo owner, skips metas owned by another host, and refuses folder
repos — a folder workspace's meta IS the workspace record (gcStaleWorktreeMeta
skips those keys for the same reason) and no remote scan can retire one. The
renderer only calls it from the authoritative-removal path, so a mere
disconnect never deletes anything.
Also:
- hoist resetAuthoritativelyRemovedWorktreeMemoryForTests into a top-level
beforeEach; removeWorktree writes that memory too, so suppression could leak
across describes and silently hide a row.
- cover the requireAuthoritative gate that skips the fallback, which had no test.
- replace the raw NUL byte committed inside the coalesce-key template literal
with a \0 escape; it made the file scan as binary to grep/ripgrep.
* test(worktrees): verify non-authoritative fallback skips removal
The non-authoritative fallback must not trigger worktree cleanup when it observes an absence — only an authoritative scan should. Tighten the expectation to ensure cleanup happens exactly once, when new data arrives after the connection state changes.
hasCursorAgentReattachPayloadScreenSignal built a char-by-char copy of the
entire reattach payload so it could read the last header plus 5000 chars. On a
2MB daemon snapshot that cost 17.5ms of synchronous renderer main-thread work —
~75% of what xterm then spends parsing the same bytes — and the miss case paid
it in full for a result that is always false.
Two changes, both matching existing in-tree precedent: bound the scan to a
256KB tail (as the kitty tracker already bounds its own scan), and strip via
the shared precompiled CSI_SEQUENCE_PATTERN instead of a hand-rolled loop,
which is also faster in V8 because it copies spans rather than building a rope
per character.
2MB snapshot, header hit 17.5ms -> 0.80ms (22x)
2MB snapshot, miss 8.7ms -> 0.52ms (17x)
200KB snapshot, header hit 1.5ms -> 0.62ms (2.4x)
config/scripts/terminal-reattach-payload-scan-benchmark.mjs reproduces this and
asserts every candidate agrees with the baseline before timing it. It also
records a negative result: porting the daemon mouse mirror's includes()
pre-filter to the kitty tracker makes reattach slower, because snapshots always
contain the introducer.
Adds guards for the two behaviours a future shortcut would silently break: a
CSI-split header must still match, and a header behind the tail bound must not.
Also byte-pins POST_REPLAY_REATTACH_RESET_KEEP_MOUSE, which shipped unpinned.
Co-authored-by: Orca <help@stably.ai>
- `truncate` has no effect on inline boxes, so long branch names would
overflow their flex item and run under the line-total chip
- Adding `block` display forces text truncation with ellipsis instead
- Increase gap from 1.5 to 2 so ellipsis doesn't visually merge with chip
* fix(daemon): detect severed macOS TCC attribution and surface daemon-restart remedy (STA-3491)
macOS pins the detached PTY daemon's TCC responsible process to the app
binary that forked it. Once that binary is deleted (packaged updates
replace the bundle), Accessibility/Automation grants on Orca silently
stop covering every daemon-hosted terminal: osascript/System Events
fails with -25211 no matter what the user grants.
- record spawnerExecPath in the daemon pid file at fork
- adoption checks it: severed + 0 live sessions -> replace the daemon
(reason severed_tcc_attribution); live sessions are preserved
- Settings (Developer Permissions + Manage Sessions) show a visible
banner pointing at Manage Sessions -> Restart while severed
* fix(daemon): harden TCC attribution recovery
* fix(terminal): clear stranded link hover tooltip
* fix(terminal): declare the tooltip reserve var where it resolves
--orca-terminal-link-tooltip-height was declared on .pane-manager-root, a
class no live element carries, so both .xterm-container height calc()s were
invalid at computed-value time and collapsed to height:auto — the element
FitAddon measures, making rows a fixed point.
Also isolate _clearCurrentLink() so a throwing provider leave() cannot skip
the cache invalidation, and bound the e2e gap assertion on both sides.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(menu): restore paste in macOS native dialogs
* test(menu): pin platform for paste routing coverage
CI runs unit shards on ubuntu-latest only, so the unpinned paste-routing
test asserted "no native paste" vacuously: a double-route regression
passed green on Linux. Cover darwin/linux/win32 explicitly so the
exactly-once contract holds on every platform, and assert the item exists
in the negative-only cases so a rename cannot pass them silently.
Also document why the native first-responder fallback exists.
---------
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* feat(dashboard): add experimental agent map view
* fix(dashboard): harden agent map behavior
* fix(dashboard): harden agent map recovery
* fix(dashboard): close map selection on view change
* fix(agent-map): center sparse layouts
* fix(agent-map): align completion and workspace actions
* Polish agent map interactions and repo labels
* feat(agent-map): add worktree lineage and project actions
* fix(agent-map): use marker for unread agents
* fix(agent-map): compact orchestrated families
* fix(dashboard): harden agent map actions and layout
* fix(agent-map): bound layout work and preserve interactions
* fix(agent-map): move unread marker to ring top-right
* fix(agent-map): seat unread marker on the ring's top-left edge
* feat(agent-map): restore the agent launcher and declutter map labels
Three gaps in the experimental Agent Map:
- The "start a new agent" picker was split onto a preserved branch during the
08-02 rebase (47829cb226) and never re-landed. Restores that commit and its
pop-out IPC, keyed on the raw worktree id rather than the map identity.
- Workspace labels draw at a fixed screen size with no collision handling, so a
zoomed-out map stacked dozens of names on each other. Adds a declutter pass
that seats project names first, then workspace names by attention, then
project counts in whatever room is left.
- The pop-out had no workspace right-click at all: its renderer has no store, so
the shared sidebar menu cannot mount there. Adds a snapshot-driven menu with
the launcher and Sleep, relayed to the main renderer.
* refactor(agent-map): fold the map's filter rail into the shared toolbar filter
The rail duplicated the toolbar's project filter and cost the canvas 14rem of
width on the surface that needs it most. Agent states move into the toolbar's
Filter dropdown (map view only — the board's columns already separate them) and
count toward its badge; project filtering falls back to the toolbar's own. Show
all is the dropdown's Clear all, and Fit already lives in the viewport controls.
* fix(agent-map): isolate map work from main renderer
* perf(agent-map): stream status updates to popout
* fix(i18n): add agent map catalog entries
* feat(agent-map): glow working entities
* fix(agent-map): prioritize attention ring status
* fix(agent-map): distinguish subagent connectors
* fix(skills): match official skill files despite local sidecars
Scope known-snapshot matching to manifest-listed files so agent-written
sidecars (e.g. agents/openai.yaml) no longer mark a package unrecognized
and block updates when official bytes still match.
Preserves fail-closed detection when a listed file's content drifts.
Fixes#12694
* fix(skills): scope lock trust and convergence to official files too
Sidecar tolerance stopped at the snapshot match, leaving three disk-vs-official
comparisons still judging the whole folder.
The lock-comparable hash covered every observed file, so a clean update beside
agents/openai.yaml reported as failed and read 'may be modified'. It is now
carried both whole and scoped to the current bundle's paths, and either may
satisfy the lock: the sidecar case only ever matches scoped, while an upstream
revision that ADDS a file only ever matches whole, so publishing one alone
would trade this bug for #11220.
Convergence re-derived the disk revision from that same whole-folder digest,
which no revision matches once a sidecar lands, retiring the stuck-lock gate
and arming an update the command provably cannot perform; it now honours the
revision observation already resolved.
Subset matching also let an older revision launder drift on a file the current
bundle lists, since that revision does not list it and so read it as a
neighbour. Identity now keys tolerance on what the current bundle owns.
---------
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
httpProxyUrl is the only network setting stored via safeStorage. Two
failure modes silently killed the configured proxy on macOS:
- A keychain reset/denial makes decryptString throw at load; the raw
ciphertext then masqueraded as the configured proxy URL, so
applyElectronProxySettings silently fell back to DIRECT and the
garbage re-persisted forever (no self-heal).
- safeStorage.isEncryptionAvailable() throwing (keychain/API errors,
pre-ready use) was uncaught in encrypt/decrypt, failing the entire
state save - the data file never gained the proxy keys at all.
Load now validates the decrypted value and clears undecryptable
ciphertext (plaintext URLs still pass, preserving the pre-encryption
upgrade path), the availability check is exception-safe, and startup
logs when persisted proxy settings are invalid instead of silently
using direct networking.
* fix(browser): recover browser tab when guest WebContents is destroyed (STA-3448)
A <webview> whose guest WebContents died without render-process-gone
(detach/reattach race, guest-side close) stayed attached and painted
black forever; reload and focus on the dead guest threw uncaught.
- listen for the webview 'destroyed' event at both layers, mirroring
render-process-gone: registry marks recovery pending (survives pane
unmount), BrowserPane triggers guest recovery immediately
- route reload-on-dead-guest into guest recovery instead of throwing
(toolbar, Cmd+R renderer+IPC paths, context menu)
- guard webview.focus() against Electron's null-internals throw
* fix(browser): close guest reload destruction race
* fix(tasks): hold dialog-confirmed issue state over stale list refetches (STA-3343)
Closing an issue from GitHubItemDialog patched workItemsCache directly
with no mutation-registry record, so a search-lagged Tasks refetch
(GitHub search index eventual consistency + gh's ~120s URL cache)
silently reverted the row to Open. Record the confirmed state as
registry authority (same mechanism the list-row mutations use) so list
fetch paths re-assert it until search catches up; quiet adopt still
releases it on match, so external reverts win once the index is fresh.
Covers issue close/reopen, PR close/reopen, and PR merge in the dialog.
* fix(tasks): preserve newer state authority on rollback
Mouse events posted with CGEventPostToPid reach the target app with no
window association, so AppKit never routes the press to a view: hover
states fire but the control is never activated, and the mouseUp is
dropped outright when posted back-to-back. Post click events to the HID
event tap instead (as keyboard synthesis already does), pace them, and
stamp mouseEventClickState so multi-clicks register.
Synthetic clicks now also report verification unverified/synthetic_input
from the helper itself, matching the other synthetic actions.
* fix(agent-hooks): give resumed Claude sessions a sidebar row at SessionStart (STA-3386)
Claude's hook set never registered SessionStart and normalizeClaudeEvent
dropped it at ingest, so a resumed session that idled produced zero hook
traffic and earned no sidebar agent row until the first prompt.
- Register SessionStart in CLAUDE_EVENTS (local + remote installs).
- Map lead SessionStart (startup/resume/clear) to an idle 'done' row,
resetting stale roster/task/cron/tool/prompt state like the Codex path;
compact restarts and child-attributed SessionStart stay dropped.
- Thread hookEventName through the agent-status IPC payload so the
completion coordinator can tell a session connect from a turn result;
a SessionStart 'done' no longer raises agent-task-complete.
* fix(agent-hooks): mark SessionStart rows as session boundaries, not completions (STA-3386)
Review follow-up: represent the idle connect as a first-class
sessionBoundary flag on the status payload instead of gating one
renderer consumer on hookEventName.
- sessionBoundary rides AgentStatusPayload/AgentStatusEntry (done-only,
clamped like interrupted); drops the hookEventName IPC threading.
- Completion-reactive consumers ignore session boundaries: the
completion coordinator (task-complete notifications), automation
dispatch observers (a connecting agent no longer completes the run
and closes its tab), activity unread counts, and the dashboard
finished timestamp; the status slice keeps boundaries out of
stateHistory and preserves the flag across done->done repaints.
- SessionStart sources are allowlisted (startup/resume/clear) so
compact restarts or unknown sources fail closed mid-turn.
- A live SessionStart now un-retires a reusable pane like a fresh
prompt, so resume-in-reused-pane earns its row too.
* fix(agent-hooks): keep session-boundary dones out of teardown and completion history (STA-3386)
Review round 2:
- A boundary done no longer deletes the pane's launch-config registry
entry, so a resumed idle TUI keeps its registered-launch-agent
identity evidence.
- A boundary landing on a REAL done pushes that completion into
stateHistory so the finished timestamp and unread badge survive a
resume//clear right after a finish.
- The done->done flag carry yields to turn evidence (assistant message
or changed prompt) so a genuine completion can never be suppressed.
- Star-nag value-moment observer and the server's OSC-equivalence
dedupe now discriminate the flag.
* fix(agent-hooks): keep a displaced completion unread in the sidebar badge (STA-3386)
Review round 3: sidebar-badge mode counts only the live entry, so a
session boundary landing on an unacknowledged completion silently
dropped the sidebar badge while the agent-events count kept it. Count
the displaced completion from history for boundary rows, and pin the
behavior with countActivityUnread tests.
* fix(agent-hooks): prevent SessionStart completion side effects (STA-3386)
* fix(agent-hooks): preserve SessionStart through renderer IPC (STA-3386)
* fix(native-chat): locate Claude's model row by frame structure
The scraper assumed the model descriptor sits within three rows of the
`Claude Code vX` line. It does not: Claude prints it near the bottom of the
startup frame with the welcome art and release-notes panel in between — eight
rows down at 100 columns. Narrow panes degrade the frame further, dropping the
version from the title row entirely below ~70 columns and wrapping the billing
tail onto its own row. Any one of those made the scrape return null, so the
model picker showed no current selection at all.
Search the frame from its bottom border upward for the row carrying model
metadata, read only the leftmost frame cell so release-notes prose can never
win, accept the frame corner as header proof when the version is gone, and
tolerate the effort suffix being elided to an ellipsis. Catalog families now
match as a leading word, which both survives the resolved-name suffix
("Opus 5 (1M context)") and keeps custom slugs like company/my-haiku-v2 from
being claimed as haiku; an unrecognized name is reported as a custom model.
Fixtures are real: captured from a live claude 2.1.220 by replaying the PTY
bytes through @xterm/headless and serializing exactly as TerminalPane does.
* fix(native-chat): resolve the scraped model against the host's real catalog
The scraper matched the static seed while the picker lists what #12369
discovers from the host CLI, so the two spoke different id spaces. On a current
CLI `list_models` returns `opus[1m]`, `sonnet`, `sonnet[1m]`, `fable` and
`haiku` — no plain `opus` — while the seed only knows families. Reporting
`opus` therefore selected a row the picker had to invent, dropping the host's
own effort and fast-mode descriptors with it. Locating the model row correctly
made this the normal case rather than a rarity, since the scrape now succeeds.
Resolve against the discovered list first, falling back to the seed for aliases
a host no longer lists and to the raw name for genuinely custom models. Matching
requires the family to lead as a whole word and the label's remaining tokens to
appear in order, so `Opus 5 (1M context)` picks `opus[1m]`, plain `Sonnet 5`
keeps `sonnet` instead of being captured by the 1M-context row, and
`opus-internal-v3` stays custom. Most specific label wins.
The hook keeps the screen that parsed so a discovery landing after the first
read re-resolves it, rather than stranding a family id once the frame has
scrolled out of the buffer.
* fix(native-chat): identify option-less models on narrow panes
Live capture at 60 columns: a Haiku session prints a bare `Haiku 4.5` row with
its billing wrapped to the next line. No middot, no effort suffix — nothing
marks it as the model, so it reported nothing. Claude always closes the frame
with the working directory and prints at most the descriptor plus a wrapped
billing line above it, so fall back to walking up from there when no row
carries descriptor metadata. The height bound is what keeps the walk from
climbing into the welcome art.
* test(native-chat): pin re-resolution when discovery lands after the read
Covers the wiring the parser tests cannot reach: the frame is visible at mount
and gone by the time the host's model list arrives, so only the cached screen
can drive the second resolve. Fails against a listener that merely replaces the
models.
* fix(native-chat): prevent stale Claude model reports
* fix(terminal): fence daemon endpoint ownership
* fix(terminal): clean failed daemon PID claims
* fix(terminal): close daemon ownership review gaps
* test(daemon): release startup IPC in boot smoke
* test(daemon): mirror production stdio in boot smoke
* fix(daemon): exit after rpc shutdown cleanup
* fix(terminal): make the socket name the daemon endpoint authority
The reported failure was a live daemon hosting PTYs that nothing could
reach: terminals acknowledged input and never ran it, listings diverged
from reality, and restarting the app never helped because the detached
helper survived. The ownership fence added for it could not fire in the
sequence that produces the split brain.
libuv unlinks the pathname a server bound to when that server closes,
with no ownership check. A daemon that lost its endpoint name therefore
deleted whichever socket then sat at that path — including a live
replacement's — stranding a daemon that still hosted every session.
Bind a private same-directory name and hard-link it into place instead:
libuv can only ever unlink our own bind name, the exclusive link is a
kernel-enforced endpoint claim, and the canonical name is removed only
under an inode ownership check. The bind name replaces the basename
rather than extending it, so it cannot overflow sun_path.
killStaleDaemon removed the PID record unconditionally immediately
before every fork, so the exclusive PID claim was always uncontested at
bind time. It also unlinked a live daemon's endpoint whenever a connect
probe merely timed out, and treated a `ps` timeout as proof of PID
recycling. Now only positive evidence of a dead endpoint authorizes
reclaiming it, SIGKILL is confirmed rather than assumed, and a daemon
that cannot be proven stopped keeps its record and endpoint while the
launcher refuses to fork beside it.
A daemon whose endpoint was taken over now retires itself, draining
rather than killing, so an unreachable orphan stops being permanent.
A repaired PID record re-derives entryPath, appVersion and the Linux
incarnation markers from the authenticated owner instead of dropping
them; without appVersion a healthy daemon read as a permanently stale
bundle and, on Windows, went unpinned against daemon-host pruning.
Repair failure now fails open — abandoning a healthy daemon over a pid
file write cost every persistent terminal on the machine.
Also: treat only ENOENT as an unclaimed record so a Windows file lock is
not reported as an ownership conflict; settle start() before close() so
an accepted connection cannot defer it forever; sweep abandoned claim
and bind names; and type the endpoint-identity seam so a rename cannot
silently disable the fence.
Adds a real-process handover smoke that reproduces the failure with two
daemons racing one endpoint, and wires it into the native-smoke job.
* fix(daemon): retire only on proven endpoint ownership loss
The ownership watchdog read a null identity for any stat failure, so a
transient EACCES or EIO on the runtime directory would retire a daemon
that was still serving every terminal on the machine. Distinguish "the
entry is gone" from "the probe failed" and act only on the former.
Also require the loss to persist across two polls: a replacement
publishes by unlink-then-link, and a single observation can land in that
gap.
* fix(daemon): source repaired ownership metadata from the authenticated hello
Adversarial review found three defects in the previous two commits.
Re-deriving entryPath from the owner's command line truncated it at the
first space. A command line is a single space-joined string, so
`C:\Program Files\Orca\...` and `/Applications/Orca 2.app/...` came back
as `"C:\Program` and `/Applications/Orca`. getDaemonLaunchIdentity treats
a present entryPath as authoritative, so a healthy daemon read as
`different_app_path` and was killed and re-forked — worse than the
missing-metadata case the derivation was added to fix. Carry entryPath
and appVersion as optional fields on the daemon hello identity instead:
the daemon already has both from its own argv, and per
docs/reference/remote-wire-compatibility.md a new optional field is safe
because every reader falls back when it is absent. This also removes a
synchronous `ps` spawn from the Electron main thread during startup.
`start()` rolled back the PID record even when it never published one.
Losing the endpoint link now runs that path, and the ownership-checked
unlink briefly renames the incumbent's record aside — enough to strand a
live daemon's ownership. Roll back only what we actually wrote.
publishDaemonSocketPath read its identity from the canonical name after
linking, so a concurrent unlink returned null: no ownership watchdog and
no endpoint cleanup on any shutdown path. Read it from the bound name
before linking, which shares the inode.
Refusing to fork beside an unconfirmed daemon left the user with no
daemon at all and no in-app recovery, since restart re-entered the same
fence. We have just proved something answers the endpoint, so adopt it
in degraded mode: live sessions keep working, fresh terminals run
locally. SIGTERM is also individually guarded now — an EPERM fell into
the blanket catch and reported "nothing alive", authorizing the very
duplicate this fence exists to prevent.
Also reset the ownership-loss streak on an inconclusive probe so the
confirmations are consecutive, and sweep scratch names before the launch
so a failed launch still reclaims them.
* fix(terminal): stop transient probe blips from erroring restored panes (STA-3536)
terminal_pane_owner_unverified fired for every restored pane whenever one
liveness probe answer went missing: a cold-start daemon draining an attach
stampede misses the 2s getSize deadline, and a wedged superseded daemon
(protocol upgrades leave them running) turns every unmapped fan-out probe
null forever.
- probePtyOwners now skips legacy daemons whose startup inventory listing
succeeded: fresh sessions never route to them, so they provably don't own
an unmapped id and one wedged zombie can't poison every pane's verdict.
- attachStablePaneOwner retries the probe over a short backoff ladder before
surfacing unverified, so a single missed deadline resolves to a verdict.
- The renderer replaces the raw error code with actionable copy.
* fix(terminal): stop retrying definitive owner probes
* fix(terminal): recover live panes after renderer restart
* refactor(terminal): share owner resolution abort guard
Post-merge review of #12790 demonstrated a real leak: the resume-from-pause
pardon cleared banked misses outright, so a host whose sweep stalls once every
three ticks reset the budget forever and a dead socket was never reaped. The
reviewer ran 300 sweeps against a permanently dead peer with a >1.5x gap every
third tick and observed zero terminate() calls.
Pre-#12790 that required a stall on *every* tick; the counter widened the
pathological window 3x, and the failure mode is permanent non-reaping — the
MAX_WS_CONNECTIONS leak the reaper exists to prevent.
A stalled tick now charges no miss, which is all the original rationale needed
(the client had no chance to answer that probe), but no longer forgives the
misses already banked. A live client still clears its own count by answering
the probe that is still sent on the stalled tick.
The tolerance test's pause case is rewritten to assert the new contract rather
than the old forgive-everything one, and a new test pins the leak directly: a
host stalling every third tick must still reap a dead socket.
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
Stacked on the #12793 revert. Widens the passive-identity set from
{inactive} back to {active, done, inactive}, so the PR/check glyph
returns to the left status lane for workspaces that are actively being
worked, not just idle ones.
Tradeoff, deliberate: #12658 was not purely a regression. It also fixed
#8813, where an active workspace with branch identity and no PR showed
the grey branch glyph instead of the emerald Active dot. This revert
reintroduces that, and removes its e2e guard.
The left lane holds one glyph, so activity, branch identity, and review
status cannot all be shown. This picks review status.
#12793 fixed PR status being hidden by workspace activity by relocating
it: prDisplay was dropped from WorktreeCardStatusSlot and re-rendered as
WorktreeCardReviewStatus in the title-row indicator group, at the right
edge of the card.
Reverting restores the review glyph to the left status lane. Because
#12658 still narrows the passive-identity set to {inactive}, this alone
brings PR status back only for inactive workspaces; reverting #12658
widens it to active and done.
No behavior change for branch identity, and #8813's guard stays intact.
The paired-runtime WS heartbeat terminated a client after a single unanswered
15s ping. One missed pong is UNKNOWN, not proof the peer is gone: a cellular or
Tailscale blackhole, or a stalled TCP retransmit, routinely swallows one pong
from a peer that is still there. Users on flaky paths saw constant drops, each
costing a full redial plus E2EE re-handshake and subscription replay.
Reap now needs MISSED_PROBE_LIMIT (3) consecutive unanswered probes, counted per
socket rather than timed. Any proof of life -- pong or any inbound frame -- clears
the count, as does a resume from a server-loop pause, since a gap the client was
never given a chance to answer must not top up its budget. Missed sweeps still
re-probe, so a recovered path proves itself on the next tick.
Three matches the liveness budgets already in the product: the web client gives
45s (25s idle + 20s probe grace) and the relay control gives 75s. The paired
transport's single miss was the outlier.
Also gives the web client's redial the one-sided jitter the shared-control path
already had, so a fleet dropped by one shared blip does not re-dial in lockstep;
the helper is extracted to src/shared/reconnect-jitter.ts and shared by both.
STA-3320, #12327
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* fix(mobile): keep the cached transcript visible while reconnecting
A manual retry closes the client and opens a fresh one, so the chat session
hook saw a new client under an unchanged identity, dropped its settled read,
and handed out an empty list — the transcript collapsed to a full-screen
spinner until the swapped client's snapshot landed.
Hold the last settled list per identity (captured post-commit) and keep
rendering it while the re-read is in flight. `transcriptLoading` still gates
consumers that decide from an empty transcript, so the launch-draft seed is
unaffected. The held list is keyed by a new `sourceIdentity` (host/workspace)
in addition to agent/session/transcript, so it can never serve another
source's messages.
Refs STA-3333.
* test(mobile): assert the whole reconnect window, not just its first frame
The re-subscribe lands a commit after the first render of the swap, so a
regression that cleared the held list there left frame 0 green and still
blanked the transcript. Verified: clearing the cache in the subscribe
cleanup now fails this test, where before only the view-toggle test caught it.
* fix(mobile): don't derive a tappable ask card from the held transcript
The cache this PR adds keeps the previous list rendered while a swapped
client re-reads. useMobileNativeChatPrompts was the one consumer reading
`messages` without honouring `transcriptLoading`, so an ask answered on
the terminal resurrected as a live, tappable card during that window.
Gating on `transcriptLoading` is exactly base behaviour: `setRead` only
ever stores 'ready'/'error', so status==='loading' implied an empty list
before this PR. The live `askFromStatus` path is untouched.
* chore: keep merge formatting scoped
`orca serve` publishes a ready graph under HEADLESS_RUNTIME_WINDOW_ID with no
BrowserWindow behind it. `shouldCreateInBackground` only degraded when the
create was renderer-backed, so any focus-requested create fell through to
getAuthoritativeWindow() and threw "No renderer window available" — leaving
`terminal create --focus` with no workaround on a remote server (#10333).
With a worktree selector and no renderer window, a background spawn is the only
usable path, so collapse the renderer-backed window check into a plain
"no window" check. That is the existing rendererBacked clause plus exactly the
missing focus case, and it drops the confusing `rendererWindow === null`
indirection (rendererWindow is already gated on rendererBacked).
Focus is not lost by the degrade: the spawned pane is still published to the
session-tab model and revealed with `activate: true`, which is how a paired
client learns about it. Mirrors the in-tree precedent in
runCreateMobileSessionTerminal.
Headed hosts are unaffected — the clause only fires when no window exists.
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* fix(file-explorer): sort numbered file names naturally
The File Explorer compared names with bare localeCompare, so numbered
files listed 100, 200 before 99. Hoist the numeric collator Source
Control file rows already use (#10850) into src/shared and apply it to
the local and runtime directory listings, the name-filtered view, and
Source Control directory nodes, which were inconsistent with the file
rows one line below (#11426).
* fix(file-explorer): natural sort on SSH funnels, relay, and pickers
Adversarial-review round 1 rework:
- Both readDir funnels short-circuited to the SSH filesystem provider
before the patched sort, so SSH workspaces kept lexicographic order;
re-sort locally after the provider returns (the remote relay may be an
older build), and fix the relay's own comparator for relay-native
consumers.
- sortDirEntries (shared, unit-tested) owns the directories-first +
natural-order listing contract used by every funnel.
- compareFileNames breaks numeric-collation ties ('2' vs '02') by code
units so sibling order stays total instead of readdir order, and pins
the collator locale to 'en' so every host produces one order.
- The SSH folder browser and runtime server dir picker now match the
Explorer they browse into.
- Ordering pinned by tests at the relay, source-control tree, and shared
helper.
* fix(mobile): natural sort in the mobile file explorer
Mobile re-sorted host readDir results with bare localeCompare, undoing
the host funnel's natural order (round-2 review). Reuse the shared
comparator and pin the order in the mobile suite.
* fix(file-explorer): natural sort at the renderer choke point and remaining ties
Round-3 review: the remote-runtime RPC and paired-web routes return the
host's order verbatim, so re-sort in readFileExplorerDirectory where
every desktop route converges; pin the SSH funnel with a handler-level
test; and route Source Control path compares through compareFileNames so
numeric-collation ties share one total order with the Explorer.
* docs(file-name-sort): state the real perf baseline in the hoist comment
* refactor(source-control): drop the dead collator export; pin the test oracle locale
* fix(file-listings): cover remaining natural-sort surfaces
Add a new census module that tracks pending and retained OSC sequences
across all active PTY output processors. Each processor registers a gauge
at creation and unregisters it on dispose, detach, or destroy — this
prevents retained gauges from inflating later heap high-water profiles
and allows the memory profiler to detect stalled processors as a sign of
leaks.
* Display SSH worktrees immediately using persisted metadata
Users can now see known worktrees for SSH hosts without waiting for the
provider connection to establish. Worktrees are fetched from local metadata
and displayed as non-authoritative, then merged without replacing richer
live data once the provider becomes available.
* Show SSH folder workspaces immediately via persisted metadata
Add safeguards for metadata fallback: track authoritatively removed
worktrees per host to prevent resurrection, position new rows within
the host block to avoid jumping on authoritative scan arrival, and
preserve co-owner detection status during merge. Coalesce concurrent
metadata fetches to dedupe overlapping queries.
* Reorder source control to show staged changes first by default
Stages are closest to the commit action and most relevant to the
commit workflow. Merges untracked files into Changes visually while
preserving their Git area. Removes the untracked-first preset and
includes migration logic for existing user settings.
* Drop source control group order user preference
Remove the sourceControlGroupOrder setting and related UI, migrations, and persistence logic. The source control view now always displays sections in the order: staged changes, unstaged changes, untracked files.
* Reorder source control to show changes before staged
Aligns with the edit-stage-commit workflow by showing unstaged
changes (active edits) before staged changes (queued for commit).
- Replace 'Send answer' with 'Submit' for clarity and consistency
- Update all locale translations (en, es, ja, ko, zh)
- Remove fixed button width and add whitespace-nowrap for flexible sizing
- Update component and test references
* Add branch line total chip to source control header
Display the total lines added and removed across a branch from its fork point, measured via `git diff <mergeBase>`. Only computed when the chip is visible (request gate on merge base OID), with 500ms soft deadline to protect status latency and 15s hard timeout. Deduplicated across concurrent pollers and cached alongside line stats. Omitted on failure — always shows exact or nothing, never a partial estimate. Updates throughout the stack: native git status, relay, renderer store/API, and UI components.
* Pin branch line total to app locale
Format line counts using the app's configured locale instead of the system
locale, ensuring consistent cross-platform display and test reliability.
* test: wait for coalescer joins instead of fixed sleep
Hold the diff until the second status pass actually takes the
branch-total coalescer lease instead of using a fixed 400ms sleep.
Fixes timing-dependent flakiness on slow machines.
* fix(windows): make managed grok-hook.cmd safe when GROK_HOME is unset
Fixes#9358 and #9941.
cmd.exe expands %VAR:~n,m% at parse time. When GROK_HOME is unset (default
outside Orca terminals), the generated length/trailing-backslash guards
became a syntax error and every Grok hook event failed with exit 255.
- Skip substring work when GROK_HOME is undefined (if defined + goto)
- Replace if "%x:~-1%"=="\" (itself a quote-parser bug) with findstr
- Extract Windows script builder; add template + spawn tests
* fix(windows): harden grok-hook GROK_HOME guards and tests
Address review on #11782:
- Inject grokHome via buildWindowsAgentHookPostCommand extra form lines
(no fragile string replace of the shared payload line)
- Spawn tests delete GROK_HOME and keep PORT/TOKEN/PANE_KEY set so the
GROK_HOME path actually runs before curl
* fix(windows): cover Grok hook home boundaries
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
PR 9501 shipped real-home routing for the host system default, and the
env override that could turn it back off was never a shipped control. The
managed-account half of the shared runtime mirror has been unreachable
since: every host account routes to its own self-contained CODEX_HOME
before that code runs.
Delete the flag module and its env plumbing plus the managed branch of
syncForCurrentSelection and the six helpers only it called. The three
lanes that still use the shared mirror -- Windows, a custom CODEX_HOME,
and a hook-lane gate that reports unusable -- are untouched, as are every
legacy migration and the WSL read-back helpers.
* feat(browser): add hard reload option and shortcut hints to reload button
Add a tooltip to the browser reload button showing the reload shortcut.
Add a right-click context menu with Reload and Hard Reload options.
Add localized labels for Hard Reload across EN, ZH, JA, KO, ES.
* fix(browser): add aria-labels to reload buttons
* feat(browser): make reload button contextual and extract action logic
- Button label now reflects actual action: Stop when loading, Retry on failure, Reload when idle
- Extract reload intent resolution into reusable browser-reload-action module with tests
- Add keyboard support (Enter/Space) for the reload button
- Simplify remote page reload to tooltip-only (no ignore-cache RPC for remote pages)
- Add "Stop" translations for all supported languages
* fix(browser): exhaust reload intent switch for type-aware lint
Replace the default branch with an explicit reload case so oxlint
switch-exhaustiveness-check accepts BrowserReloadIntent.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Refactor GitHub work-item mutations onto a shared optimistic coordinator
- Extract PR/issue status, assignee, reviewer, and merge/auto-merge mutations
out of TaskPage cell components into a registry-backed
begin/confirm/rollback pipeline (task-page-github-work-item-mutation-*),
so soft-hide, sticky filter-membership, and quiet revalidation behave
consistently across all mutation types instead of each cell re-implementing
optimistic update/rollback/toast logic.
- Add quiet revalidation (no filter skeleton, no page blanking) and soft-hide
handling so a row that exits the active filter (e.g. closing an issue under
`is:open`) stays hidden without a jarring list reflow.
- Restyle the GitHub task table: opaque sticky ID/Title cells, distinct header
fill, accent hover, and tighter row/toolbar chrome to fix background bleed
and muddy contrast in the scrolled table.
* Fix quiet-revalidate cancellation and sticky-hide scoping in TaskPage
- Replace per-render `cancelled` flag with a ref that only flips on
true unmount, so a nonce-triggered re-render no longer strands the
shared quietState's trailing/backoff bookkeeping mid-flight.
- Fix backoff index to use max lag attempts instead of lagging-key
count, matching processTaskPageQuietRevalidateSettle so several
single-lag items can't jump the delay tier.
- Scope sticky-hide retention in materializeTaskPageItemList to the
originating query key, preventing non-membership confirms (e.g.
auto-merge) from lingering as stale rows across refetches.
* Fix is:draft filter to soft-hide non-draft PRs
Previously state was forced to 'open' for is:draft queries, so a PR
that stopped being a draft still passed the state check and stayed
visible. Add an explicit draft check to soft-hide it.
* Improve GitHub work-item mutations with scoped quiet revalidation
Prevent race conditions and stale data by tracking quiet run ownership,
validating scope changes with generations, and blocking overlapping mutations
with pre-flight checks. Extract quiet state management into a dedicated module
with improved authority clearing and network retry logic.
Reorganize the host and project filters to share a unified single-row design
(label left, value right) with detailed selection moved to nested panels. Group
both filters under a "Show" section label to keep the parent menu flat. Extract
project-filter search logic into SidebarProjectFilterPanel with explicit focus
and keyboard-handling tests.