Pull upstream remote-runtime terminal fixes for CHE-1586 (conflicts unresolved) - #7
Open
smk508 wants to merge 1432 commits into
Open
Pull upstream remote-runtime terminal fixes for CHE-1586 (conflicts unresolved)#7smk508 wants to merge 1432 commits into
smk508 wants to merge 1432 commits into
Conversation
…eate (stablyai#9234) * fix(mobile): survive connection-migration cutovers during worktree create A worktree.create in flight when the mobile transport migrates (relay/direct hand-off on shoddy cellular, relay lease rotation, relay recovery) rejects with "RPC interrupted by connection migration" even though the host completed it — leaving the Create Workspace modal stuck while the worktree exists on desktop. A naive retry hits a name collision and spawns a duplicate. Mirror the existing mobile terminal-create idempotency: worktree.create now accepts an optional clientMutationId that the host dedupes (in-flight + brief post-success TTL), and mobile mints one key per candidate name and re-issues the create on a cutover so the retry reconciles instead of duplicating. * fix(mobile): gate worktree cutover replay by capability * fix(mobile): await worktree replay capability
stablyai#9121) * Replace assistant-prose heuristic with explicit turn lifecycle markers Extract provider-authored turn boundaries (completion, interruption) directly from Claude/Codex transcripts so the chat view knows when work ends without guessing from message presence. Reconciles live hook state with transcript lifecycle: when a terminal boundary lands, it settles a dropped Stop hook instead of letting prose mislead the UI into showing 'working' after done. * fix(review): cover Claude terminal stop_reasons and RPC lifecycle frames Treat max_tokens/stop_sequence/refusal as completed markers so capable hosts do not stay working after a dropped Stop, and assert lifecycle payloads on runtime subscribe/read frames plus mid-turn non-terminal stop_reason cases. * test(native-chat): clarify that lifecycle field is optional Add type assertion and comment documenting that lifecycle field is optional and can be omitted in truncation-gating test fixtures. * fix(native-chat): settle status on interruption despite working subagent When Claude's turn is explicitly interrupted, the session should show ready immediately — even if background subagents are still running. Add an interruption check before consulting the hook's working-subagents flag so interruptions take precedence. Also normalize omitted lifecycle timestamps to null instead of leaving them undefined, and add test coverage for both cases. * fix(native-chat): settle loading spinner on explicit turn boundaries Explicit transcript turn-lifecycle markers now fully replace the prose-fallback settlement path. Remove the now-unused `turnLifecycleCapable` flag and wire lifecycle to suppress spinner even when hook status lingers. Refine Claude lifecycle detection to distinguish terminal stops from mid-turn tool_use rows, exclude harness noise from new-generation detection, and apply clock-skew slack over SSH/relay. Serialize PTY sends per line to prevent rapid prompts from gluing before Enter, clearing unsubmitted input on cancel. Update working suppression to detect epoch rollovers so interrupt+next-turn without a ready gap resets the spinner correctly.
…ction (stablyai#9218) * feat(settings): unify skill card status and add Linear agent-skill section Show the freshness-aware status pill ("Up to date" / "Update available") on the Orchestration skill card, matching the Computer Use card — it was the only installed-skill card still limited to a presence-only "Installed" pill because it omitted the freshnessSkillName prop. Add a Linear capability section under Settings → Agents that only appears once the Linear provider is connected. It mirrors the other skill panes: an orca-linear skill card (Update / Re-check + freshness pill) and a "How to use it" grid of copyable /orca-linear prompts. The Orchestration and Linear "How to use it" surfaces now share extracted SkillUsageExamplesSection / SkillUsageExampleDialog components (the old orchestration-only dialog is renamed/generalized with a slashCommand prop). The nav registry and Settings page gate the Linear section on one shared useLinearProviderConnected() selector so the sidebar entry and the section appear and disappear together. * fix(settings): tighten Linear skill section behavior * fix(settings): preserve Linear freshness and dialog translations * fix(linear): migrate legacy skill installs * Revert "fix(linear): migrate legacy skill installs" This reverts commit 3ff45f9. * fix(settings): show Linear freshness in sidebar * fix(settings): type Linear navigation target
…blyai#8899) * feat(terminal): add flag-gated render-desync sentinel for WebGL panes Detects the buffer-clean/render-stale glyph garble class in the field: per visible WebGL pane, compare the cells the xterm buffer says hold glyphs against the ink actually present on the canvas, sampled in the same task as a forced synchronous redraw so a divergence proves the render model/atlas is wrong rather than a missed present. A trip requires the same screen cells to stay divergent across three samples (real desync is pinned; scroll lag moves), then records a webgl-render-desync breadcrumb, stashes evidence (canvas PNG + buffer text) for bug reports, and runs the same shared-atlas recovery a tab reveal performs, so a stuck-garbled pane self-heals within seconds. Off by default; arm on any build via localStorage.setItem('orca:render-desync-sentinel', '1') and reload. * fix(terminal): invalidate glyph cache on atlas replacement Reproduce the WebGL atlas identity mismatch with two live terminals and force cached geometry to rebuild whenever a different shared atlas is attached. Persist flag-gated render-desync evidence and retain the investigation tooling used to validate the field signature. * fix(terminal): harden render desync diagnostics * docs(reliability): clarify Linux WebGL evidence gap
…markdown (stablyai#9209) Restore readable block flow, typography, nested spacing, and heading semantics for structured review Markdown in the PR sidebar. Preserve the compact renderer's existing remote-image trust boundary.
…r new users (stablyai#9240) * feat(keybindings): swap tab-switch chords to the common convention for new users New installs now get the widespread mapping — Mod+Shift+[ / ] cycles across all tabs, Mod+Alt+[ / ] cycles within the active tab type. Pre-existing installs keep today's mapping: a one-time cohort seed (frozen on first launch via the fileExistedOnLoad signal, mirroring the telemetry migration) pins the legacy chords into keybindings.json, skipping any action the user already customized. - shared registry: swap the four tab.*SameType / tab.*AllTypes defaults; export LEGACY_TAB_SWITCH_BINDINGS for the seed - persistence: migrateTabSwitchKeybindings freezes the existing-vs-fresh cohort (tabSwitchKeybindingSeed = pending | done) - keybinding-file: seedLegacyTabSwitchBindings writes the legacy pins into the active-platform section so Settings reset still works - refresh stale default-chord comments - tests for the swapped defaults, the seed (fresh/existing/customized/idempotent), and the cohort migration * test(keybindings): prove existing-user parity + make the seed strictly per-action seedLegacyTabSwitchBindings now pins each un-customized action individually instead of skipping all four when any one is customized. A partially-customized existing user keeps their rebound action AND the pre-swap default on the rest; no existing user's behavior changes. The skip check keys on this platform's effective overrides so a foreign-platform-only override can't leave the active platform on a new default. Adds keybinding-service.test.ts: constructs a real KeybindingService and asserts effective bindings + real keystroke matching for both cohorts across darwin/ linux/win32, plus partial-customization, idempotency, and seed-failure retry. * fix(keybindings): preserve legacy files during tab shortcut seed * fix(keybindings): preserve valid pre-swap overrides
…lyai#8954) * fix(terminal): paint paired remote terminals on first subscribe * test(terminal): cover desktop legacy subscriber PTY wait The widened terminal.subscribe gate (isMobile -> params.client) had no regression test: reverting it to the mobile-only gate left the suite green. Add a desktop legacy-subscriber case asserting the mount request and late-PTY wait fire before the scrollback-only fallback. * fix(terminal): cancel abandoned multiplex PTY waits Desktop subscribers can close while the new late-PTY wait is pending. Register provisional slot cancellation and abort all pending waits on unsubscribe, connection abort, or multiplex teardown so a late PTY cannot resurrect a ghost output stream. --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…ablyai#9247) * fix(mobile): make the :6768 WebSocket accept loop leak-proof and self-healing The desktop runtime's mobile WebSocket server on :6768 could wedge with the socket in LISTEN but the TCP accept loop stalled — new connections piling up in SYN_RCVD with zero ESTABLISHED, even from localhost, with no self-heal. Since Orca mobile has no APNs (the persistent WS doubles as the push channel), a wedged accept loop silently drops every notification while the app still shows cached UI. Root cause: connections were only capped at the WebSocket-upgrade layer (MAX_WS_CONNECTIONS), after the socket is already accepted. TCP-level sockets were unbounded, so leaked/half-open sockets (backgrounded phones, flaky relay, reconnect storms) could grow until the process ran out of file descriptors and accept() started failing with EMFILE. - Bound TCP sockets via httpServer.maxConnections (2x the WS cap). At the cap Node accepts-then-closes, so the accept loop always keeps draining and the EMFILE/SYN_RCVD wedge is structurally impossible on this listener. - Handle accept-level errors: ws forwards httpServer 'error' onto the WebSocketServer; with no listener Node rethrows it as an uncaught exception. Swallow + log and keep listening. - Force-terminate over-capacity sockets: a bare ws.close(1013) left half-open phones lingering at >cap forever, rejecting everyone after them. Add an error handler + a 1s terminate fallback so the descriptor is always freed. - Log heartbeat reaping / near-cap live counts so the leak is observable. * fix(mobile): avoid masking fatal accept failures
* refactor(native-chat): extract option appliers and improve toggle UI - Extract setOption apply logic to native-chat-session-option-apply.ts - Add invokeAction method for toggle-only options without tracked baseline - Replace checkbox UI with On/Off radio groups for boolean options - Display option values only in pills (remove redundant label prefixes) - Serialize concurrent applies so later dispatches win in order * Remove focus-visible border from composer container The container uses a steady hairline border (no focus/click flash), with focus-visible styling delegated to the inner textarea. The container is a layout wrapper, not a focus target. * Prevent option commits on model switch during dispatch When a user changes an option and the model switches before dispatch completes, committing the stale option would overwrite state under the new model. Guard by capturing the baseline option state before dispatch and validating it hasn't changed post-dispatch. * Use invokeAction for model option in NativeChatComposer test Codex model is an agent picker mid-session, so setOption rejects. Update test to use invokeAction to match the actual UI behavior.
) * test(e2e): harden Windows terminal restart regressions * test(e2e): cover renderer replacement rejection * test(e2e): tolerate ESRCH when force-killing the daemon on POSIX * fix(ci): preserve Windows restart test selection --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Translate 105 ko.json entries that were still identical to their English source after the machine bootstrap — dialog bodies, settings descriptions, shortcut recorder labels, Windows firewall notices, Jira connection copy, crash-report notices, and source-control panel text. Intentionally left in English: brand/product names, command and config examples, CSS/selector strings used by animated visuals, and lowercase concatenation fragments whose word order cannot be reordered safely.
Co-authored-by: weixin <weixin@thunisoft.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agents): pi session resume support
* fix(pi): require persisted session files for resume
* test(sleeping-agent): use non-resumable sentinel in malformed-record fixture
The 'drops malformed sleeping agent resume records' test used agent:'pi' as
its example of an unknown/non-resumable agent, expecting the record to be
dropped. This PR added 'pi' to RESUMABLE_TUI_AGENTS, making that fixture
valid and retained, so the toBeUndefined assertion broke. Switch the
malformed-case fixture to a genuinely non-resumable sentinel
('definitely-not-an-agent') so the drop-malformed path is still exercised;
no other assertions changed.
* Add durable resume identity for Pi sessions without fabricating turn sta
Pi's `session_start` hook now carries the session file needed to resume
a sleeping pane, but until now Orca either discarded it or treated it
as a fake status transition. Thread a `providerSessionOnly` envelope
through the hook listener, relay, main-process server, and renderer
store so resume identity (and its session-file-scoped equality/claim
key) can be persisted and replayed without emitting prompt telemetry
or a visible working/done row.
* Add durable resume identity for completed Pi sessions
Pi's agent_end hook marks a turn done, but the underlying TUI session
stays alive and resumable. Previously a `done` status wiped sleeping
records and launch config as if the session ended, so hibernation,
manual worktree sleep, and quit-capture all lost Pi's resume identity.
- Track a "live recovery" record for done-but-still-resumable Pi
sessions, exempting it from the usual done-state cleanup paths in
agent-status.ts and agent-hibernation-planner.ts
- Gate providerSessionOnly rows and sleeping-agent schema records on
actual resumability (getAgentResumeArgv) instead of trusting the
presence of a provider session
- Wait for Pi to persist its session file before advertising resume
metadata, and treat `/reload` as a non-terminal event so it doesn't
clobber visible status
- Extend SSH relay envelopes to carry providerSessionOnly so remote
hosts get the same behavior
* Add explicit periodic/quit mode to sleeping-agent session capture
Split captureAllSleepingAgentSessions into 'periodic' and 'quit' modes
so a background checkpoint can no longer downgrade a confirmed-quit
record or promote a completed Pi session without an authoritative
transcript path. Updates all call sites and tests accordingly.
* Use normalizeAgentStatusPayload for default pi status
Remove unnecessary JSON.stringify wrapper and call the appropriate normalization function directly.
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
…ow details (stablyai#9248) * fix(updater): humanize update-error card and hide raw error behind Show details Windows auto-update failures surfaced the raw electron-updater message as the card headline — most visibly the PowerShell "Command failed: … Get-Authenticode Signature …" dump when antivirus/EDR blocks the post-download signature check. That reads as a crash, not an actionable state. Classify each failure and lead with one plain-language sentence + the right action, keeping the raw error one click away: - New shared classifier (updater-windows-signature-check): distinguishes an AV/EDR-blocked signature check (environment) from a genuine wrong-publisher mismatch (security). The two are mutually exclusive so a real integrity failure is never softened into "try again". - UpdateCard: raw error moves behind a collapsed "Show details" toggle; adds a security-stop variant (wrong publisher → no retry, "Open official releases") and the AV-blocked variant ("Update Verification Blocked"). HTTP/2 and generic paths keep their existing actions, now with the same details disclosure. - Main process records a windows_signature_check_blocked lifecycle event so we can size the affected Windows cohort in the field. Verified each error scenario in a running Electron build (signature-check blocked, wrong-publisher security stop, HTTP/2, generic, and the expanded details view). * fix(updater): make Show details a caret disclosure above the action row Move the raw-error toggle directly above the Retry/Download buttons and give it a rotating chevron; the Last error block now expands in place beneath the caret instead of appearing above the summary, with the action buttons pinned below. * fix(updater): prevent signature-check bypass * fix(updater): surface retry start immediately
Co-authored-by: Orca <help@stably.ai>
…tablyai#9244) * fix(terminal): don't report agent exit on a degraded foreground scan On Windows/ConPTY the foreground scan (a Get-CimInstance Win32_Process PowerShell fork) can exceed its 3s budget under load, and there is no wmic fallback on Win11 24H2+. A timed-out scan returned the shell name, which the completion coordinator reads as "agent exited" and fires a false "agent done" notification while the agent is still working. Prefer the last positively-recognized agent across a degraded (available:false) scan so a transient failure no longer looks like an exit. An authoritative (available:true) scan still refreshes/clears the memory, so a genuine exit or crash is still detected. No-op on macOS, where the scan is always available. * fix(terminal): drop stale foreground scan results
Co-authored-by: Orca <help@stably.ai>
…ree picker (stablyai#9251) Co-authored-by: Orca <help@stably.ai>
stablyai#9252) Co-authored-by: Orca <help@stably.ai>
The account-switched restart card had two near-synonymous dismissive buttons (Dismiss / Collapse) whose distinct behaviors weren't legible, and body copy that attributed "keep working with the current account" to Collapse — which only minimizes the notice. Remap to intent-named actions: - Dismiss -> "Keep old account" (clears the notice; pane stays on the old account) - Collapse -> "Not now" (minimize to the pill; decision deferred) - Body no longer claims the minimize action commits you to an account. Restart, the title, and the heading are unchanged. New English strings get fresh auto keys so stale es/ja/ko/zh translations don't persist; catalog re-synced.
…ai#9253) Co-authored-by: Orca <help@stably.ai>
…lyai#9165) (stablyai#9255) * fix(ssh): probe npm via prepended PATH, not colocated with node (stablyai#9165) The remote Node/npm toolchain gate invoked npm by its absolute path <nodeBinDir>/npm (POSIX) / npm.cmd (Windows, behind a Test-Path colocation check). But deploy (commandWithNodePath) runs bare `npm` with nodeBinDir merely prepended to PATH, so npm can resolve from anywhere on PATH. A host whose only resolvable node has npm elsewhere on PATH (e.g. node symlinked into a dir without npm) deployed fine on v1.4.144, but after upgrade the candidate is rejected with no fallback → SSH/relay connection fails to establish. Make the probe resolve npm exactly the way deploy does — bare `npm --version` under the same prepended PATH — so it still confirms npm is runnable (the stablyai#8450 concern) without requiring colocation. Windows now prepends the backslash-form dir (matching deploy) so bare-command PATH lookup resolves reliably. * test(ssh): cover split Node npm PATH resolution
* fix(keybindings): use AltGr-safe default for Add Review Note The editor.addReviewNote default was Mod+Alt+N, which resolves to Ctrl+Alt+N (AltGr) on Windows/Linux. On diacritic layouts AltGr+N types a real character (e.g. Polish n-acute), so the editor-scope chord hijacked normal typing. Switch the default to Mod+Shift+A, which is AltGr-safe and keeps a mnemonic (A for annotate). * test(keybindings): cover Add Review Note chord end to end
…ai#9260) The connectPanePty suite intermittently fails in CI on `clearAgentLaunchConfig` call-count assertions (e.g. expected exactly once, received 5 with pane keys belonging to other tests). Root cause is cross-test async leakage: pane foreground-agent trackers kick off `confirmForegroundProcess` reads whose continuations settle on the microtask queue, not on fake timers. A test that ends with one still in flight leaks it into the next test, where it resolves against that test's fresh store mock and calls clearAgentLaunchConfig with the prior test's pane key. Drain pending microtasks in afterEach (before tearing down fake timers, while the test still owns its store mock) so each test absorbs its own async fallout. No behavior change; full suite stays green (446/446). Note: this is a CI-timing flake not reproducible locally in isolation (ordered runs pass repeatedly, incl. under GC pressure); the fix targets the confirmed leak mechanism and removes the timing dependence.
…yai#9259) * fix(terminal): filter focus reports after Codex history resume * test(terminal): preserve focus reports outside Codex resume --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
…ablyai#9816) The default DialogContent close button is absolutely positioned at top-4/right-4 for standard p-6 dialogs; the agent terminal dialog uses p-0 with a compact py-2 header, so the X floated below the title line. Render the close control inside the header row instead so it centers with the title and matches the header's horizontal padding.
…/disconnect churn (stablyai#9460) * fix(mobile-relay): back off relay reconnects to stop cellular connect/disconnect churn On cellular, the relay path re-dialed instantly on every network flap: a NAT rebind / Wi-Fi<->cellular handoff silently kills the socket, the revival trigger treats it as 'link came back' and calls recoverRelay(), and the relay cell answers the overlapping resume with PEER_DROPPED (4408) or LIMIT_EXCEEDED (4429). The session collapsed every close to a plain 'disconnected' and re-dialed with no delay, so the phone ping-ponged connect/disconnect. The documented recovery contract (mobileRelayRecoveryFor, which prescribes fullJitter backoff) had no callers. - Add RelayReconnectBackoff: full-jitter exponential backoff (250ms floor, 30s ceiling) that debounces re-dials via a cooldown window and wires up mobileRelayRecoveryFor. Reset on a successful migrate and on a genuine background->foreground transition (not on repeat foreground nudges). - Extract the lease-rotation timer into RelayLeaseRotationTimer so the supervisor stays under max-lines (the direct-probe path can't be split out — it shares the operationInFlight mutex with recoverRelay). - Add a deterministic test: repeated network-flap nudges re-dial instantly before the fix and are suppressed by the backoff window after. * fix(mobile-relay): recover drops during direct probes * fix(mobile-relay): recover half-open relay sessions * fix(mobile-relay): preserve direct handshakes * fix(mobile-relay): keep recovery retries bounded * fix(mobile-relay): avoid redundant recovery dials * fix(mobile-relay): keep all retries inside cooldown * fix(mobile-relay): close recovery lifecycle races * fix(mobile-relay): preserve in-progress direct auth * fix(mobile-relay): preserve fatal recovery gates * fix(mobile-relay): preserve backoff across unstable resumes * fix(mobile-relay): close remaining recovery lifecycle gaps * fix(mobile-relay): reset backoff only after stable relay
…rds (stablyai#9789) * fix(sidebar): keep branch-discovered PR status visible on worktree cards The sidebar status lane and the right checks panel read the same PR caches but disagreed for unlinked worktrees: every successful GitHub PR fetch mirrors the result into hostedReviewCache stamped with a linked-style hint key ('github:<n>'), and getWorktreeCardPrDisplay suppresses unlinked reviews with a non-empty hint. The checks panel renders straight from prCache, so it showed "stablyai#9387 OPEN" while the same worktree's card fell back to the plain branch icon. Thread the branch-keyed PR cache number into getWorktreeCardPrDisplay as corroboration: when the branch cache names the same PR the hosted entry holds, the review provably belongs to this branch and stays visible. WorktreeCard passes its existing cachedBranchFallbackGitHubPRNumber, which is already guarded for linked metadata and the merged-head rule. The hint stamping itself is unchanged on purpose: the 'github:' marker also flags the entry as GitHub-scoped so neutral lookups still re-run GitLab MR discovery. * fix(sidebar): preserve PR lookup provenance * fix(sidebar): preserve merged PR head guard * test(github): assert exact refresh cache write
…ent" (stablyai#9792) * fix(mobile): stop reporting delivered chat messages as "Message not sent" A relay drop or response timeout while terminal.send is in flight rejects the RPC even though the request usually already reached the desktop — only the ack was lost. The chat composer treated every failure as definite, showing "Message not sent" and keeping the draft for a message that is visibly in the transcript after resync (and baiting a duplicate send). Mark transport failures that happen after the request frame hit the wire as delivery-unknown, and hold those sends instead of erroring: when the transcript echo lands the draft clears silently; only if no echo arrives within 20s is the failure surfaced. Failures before the frame was written (and host rejections) still error immediately. * fix(mobile): close delivery ambiguity races * fix(mobile): harden ambiguous send reconciliation * test(mobile): assert ambiguity deadline boundary
…ablyai#9506) * fix(editor): don't flag editor-initiated moves as changed-on-disk An in-app move/rename (explorer drag-drop, inline rename, tab rename) re-homes the open tab to the new path and carries its unsaved draft forward. The move also physically relocates the file, which the worktree watcher reports as delete(old)+create(new) a few ms later. Because the tab already lives at the new path by then, that create echo was treated as an external write landing on a dirty tab and raised a spurious "changed on disk" banner. Add a short-lived self-move registry (the move analog of the existing self-write registry) stamped at the single remap choke point, and have the external-watch handler recognize the move's own watcher echo: suppress the changed-on-disk mark on the re-homed dirty tab and the tombstone on the source path. Genuine external edits are unaffected. Covered by unit tests for the registry, the remap recorder, and the watch-hook suppression (plus a no-over-suppression guard). Co-authored-by: Orca <help@stably.ai> * refactor(editor): harden move-echo suppression per adversarial review Addresses review findings on the self-move suppression: - Stamp the self-move at the call sites BEFORE the on-disk rename (recordSelfMoveForOpenTabs), not after the tab re-home, so the watcher echo can't win the race. This makes the source-side delete guard actually effective and removes a possible one-frame flash. - Suppress only the move's own create echo, not update events, so a genuine external write to the moved path within the TTL still raises the changed-on-disk banner (closes an over-suppression gap). - Track source/target roles independently per path so an immediate undo can't clobber the original move's still-in-flight stamp. - Raise the registry cap above realistic bulk-move sizes so a large directory move never self-evicts its own not-yet-echoed stamps. Updated + added tests: registry undo/role + cap, the new call-site helper (incl. directory move), and a real-update-still-marks case. Co-authored-by: Orca <help@stably.ai> * fix(editor): make move-echo suppression watcher- and TTL-robust Round-3 hardening after adversarial review: - Suppress the move's own watcher echo regardless of event kind. The main-process watcher coalesces a create+attr-change burst into a lone update, so a create-only gate let the echo through on some hosts and re-exposed the false banner. Suppression is now bounded by the self-move TTL; a genuine write to the exact path within that short window is the documented trade-off (draft is preserved regardless). - Bracket the on-disk rename with the self-move stamp via a single renameOpenTabsPathOnDisk wrapper: stamp before (to beat the watcher), re-stamp on success (a slow SSH/runtime rename can outlive the TTL, so the fresh window must start when the file actually moved), and clear on failure (a rename that never happened must not suppress real events). All move entry points (explorer move, inline/tab rename incl. undo/redo, untitled rename) route through it. - Treat a tab as remote for TTL purposes when it has a runtime owner OR an SSH worktree connection (an SSH tab can carry a null runtime owner). - Registry tracks source/target roles independently per path so an immediate undo can't clobber the original move's in-flight stamp; cap raised above realistic bulk-move sizes. Tests: registry role/clear/cap, the rename wrapper (success re-stamp + failure clear), the call-site helper (dir move + clear), and watch-hook coalescing-robust suppression + post-TTL surfacing. Co-authored-by: Orca <help@stably.ai> * fix(editor): refcount self-move roles so a failed move can't clear a live one Two concurrent moves onto the same destination both stamp that path as a target; if the second rename fails and clears, it must not erase the first (successful) move's still-live target stamp. Reference count each role's registrations and clear only the failed move's own contribution. Adds a regression test for the shared-destination case. Co-authored-by: Orca <help@stably.ai> * fix(editor): give self-move stamps per-registration expiries + retract tokens The refcount model used a single shared expiry scalar per role that only grew via max() and reset at refs=0, so releasing the max-contributing registration left survivors inheriting an over-extended window (and an expired registration could be resurrected by a later stamp on a key the opposite role kept resident). Both over-suppress genuine changes. Model each stamp as an independent registration carrying its own expiry (a list per role). recordSelfMove returns a ticket; clearSelfMove retracts exactly that registration. A role is live while any of its registrations is unexpired, so concurrent stamps, failed-move clears, and expiry are all precise. Wrapper/helper thread the tickets through. Adds regressions for the over-extension and resurrection cases. Co-authored-by: Orca <help@stably.ai> * test(editor): cover source-side self-move guard in the pre-remap ordering Adds the case where the watcher's delete(old) arrives while the tab is still at the old path (before remap re-homes it): with a live self-move source stamp the tombstone must be suppressed. Pairs with the existing naked-delete control (no stamp → deleted) to pin the guard's behavior. Co-authored-by: Orca <help@stably.ai> * docs(editor): document the failed-move suppression window as a bounded trade-off A self-move stamp is placed before the rename and retracted if it fails, so an event consumed during the pre-failure window is swallowed. Note that this only matters for the rare unrelated-dirty-tab-at-destination case and the recoverable missed-source-tombstone case, and that a move has no bytes to echo-verify the way self-writes do. Co-authored-by: Orca <help@stably.ai> * feat(editor): decide move echo vs external write by content identity Replaces the time-bounded self-move suppression heuristic with a correct-by-construction identity check, so a genuine external write to a just-moved path is never swallowed and the move's own echo is never a false conflict — regardless of watcher event-kind coalescing or timing. - Remap now carries the edit-session identity (lastKnownDiskSignature, externalMutation, pendingDiskBaselineVerification) onto the re-homed tab. Previously the close+reopen dropped it, so a moved tab lost its disk baseline (and any pre-existing changed-on-disk conflict silently vanished on move). - On a live self-move-target dirty event the watch hook now reads the destination and compares getDiskBaselineSignature(disk) to the tab's carried baseline: equal => move echo (suppress), differ/binary => genuine write (banner). Autosave is suspended synchronously before the read so a write landing mid-read can't be overwritten; a generation token makes overlapping reads safe. Fails CLOSED (marks changed) on a missing baseline or read error — never blind-suppresses. - The self-move registry now only scopes WHEN to verify. The source-side delete still can't be content-verified (nothing to read), so it stays a bounded, documented suppression. - Trim the verbose Why-comments across these files to 1-2 lines. Adds content-identity verification tests (echo/differ/no-baseline/read- error/binary/autosave-gate) and a remap test for the carried identity; splits the watch-hook suite to stay under the max-lines limit. Co-authored-by: Orca <help@stably.ai> * fix(editor): give live move-verification its own autosave gate The live self-move echo verification reused pendingDiskBaselineVerification as its autosave gate, but that field is also the always-mounted restored-tab conflict scanner's work queue: the scanner scans any pending dirty tab, launches its own read, and clears the flag without checking the live generation — so it could lift the gate mid-read and let autosave overwrite a genuine external write. Give live verification a dedicated pendingLiveDiskVerification field (both suspend autosave; each cleared only by its owner). Transient, not persisted, not carried across a re-home. Co-authored-by: Orca <help@stably.ai> * feat(editor): atomic rekeyOpenFilesForPathChange store action (move restructure stage 1) Foundation for treating an Orca-owned move as an in-place retarget of the open edit session (not close+reopen), per the locked design. One commit-only store update migrates every path-derived id + all id-keyed state (openFiles full-spread, the 6 file-id maps, activeFileId(+byWorktree), tabBarOrder, unified tabs/groups via the now editor-family-widened migrateHydratedEditorTabsAndGroups, pendingEditorReveal, untitled consume). Preflight fails closed on collision (never merges two live sessions) or stale with zero mutation. Not yet wired to a coordinator (stage 4). Stage 1 of 5; behind the shipped content-identity fix. Co-authored-by: Orca <help@stably.ai> * feat(editor): op-scoped in-flight move registry + source integration (stage 2) editor-path-move-inflight.ts tracks Orca-owned moves for the exact duration of the rename+rekey (no TTL): source paths suppress the delete tombstone, target paths latch a destination event seen before the rekey (never suppress). Wired into the watcher's delete filter alongside the old TTL registry (OR fallback) so suppression keeps working until the stage-4 coordinator drives every move through beginEditorPathMove. Stage 2 of 5. Co-authored-by: Orca <help@stably.ai> * feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store) Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the rekey action install it + pendingLiveDiskVerification on dirty autosave-capable destinations (moveOperationId arg), atomically in the same commit that re-homes the tab — so the verify gate survives the rekey and its op-id token supersedes a stale in-flight verification. Replaces the module-scoped generation map (which broke under rekey). Verification-reader wiring + coordinator follow. Co-authored-by: Orca <help@stably.ai> * refactor(editor): remap moves via atomic in-place rekey, not close+reopen (stage 4a) remapOpenEditorTabsForPathChange now builds an owner-aware rekey plan (plain-path id to the first owner, owner-qualified to the rest; previews resolve their source to the moved edit's new id) and applies it via rekeyOpenFilesForPathChange in one commit — preserving the full OpenFile + cursor/view/group/MRU state and closing the close/reopen watcher-race window. Passes moveOperationId through so dirty destinations get the content-verify gate. 4545 tests green. Co-authored-by: Orca <help@stably.ai> * feat(editor): move coordinator drives rename/drag/undo/redo/untitled (stage 4b) executeOpenEditorPathMove is the single transaction for every in-app move: quiesce affected saves -> op-scoped begin (per runtime owner) -> on-disk rename -> atomic in-place rekey (installs the content-verify gate/provenance) -> settle -> re-verify any destination echo latched before the rekey. On failure the store is untouched. Verification now triggers off the on-OpenFile provenance (consumed on resolve) and the watcher latches pre-rekey destination events. Wired into all five call sites; old renameOpenTabsPathOnDisk + separate remap removed from them. 2751 tests green. (TTL self-move registry now dead; removed next.) Co-authored-by: Orca <help@stably.ai> * refactor(editor): remove the dead TTL self-move registry (stage 4c) The coordinator + op-scoped in-flight suppression + on-OpenFile provenance fully replace the time-bounded self-move registry, so delete it and its two helper modules (record-self-move-for-open-tabs, rename-open-editor-tabs-path). The watcher source filter now uses only isActiveMoveSourcePath and the verification trigger only the tab's pendingSelfMoveEcho. Rewrote the self-move test suite onto the new primitives. 7225 tests green. Co-authored-by: Orca <help@stably.ai> * test(editor): end-to-end coordinator move test (stage 4 done) executeOpenEditorPathMove renames on disk, retargets the session in place with draft/dirty/baseline preserved + gate/provenance installed, settles the in-flight transaction; on rename failure the store is byte-identical and the transaction is released. Co-authored-by: Orca <help@stably.ai> * feat(editor): mirror-safe move — detach moved mirrored tab + close-notify host (stage 5) The atomic rekey changes a tab's id, so a moved mirrored tab would be culled by the host snapshot (losing its draft) or resurrect the old path. Ship the safe minimum: the rekey detaches a moved tab from the host mirror (mirroredFromRuntimeSession cleared) and the coordinator close-notifies the host's old-path tab (close intent suppresses re-mirroring). Prevents the data-loss/resurrection; the moved tab becomes companion-local. The full host-rekey path-change protocol (preserving mirror ownership) is a documented follow-up. Co-authored-by: Orca <help@stably.ai> * fix(editor): address review round 1 (4 majors) - Coordinator now propagates the rekey result: a collision/stale AFTER a successful on-disk rename triggers an inverse rename + throws, instead of reporting success with the source tab stranded at a vanished path (#1). - Cross-worktree: affected set spans all worktrees at the source path, sub-ops scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar migration by each file's own worktree (was applied under one scope) (#2). - Diff tabs: single-file unstaged diff tabs are now retargeted on a directory move (rebuild the diff id + relative path) instead of stranding (#3). - Mirror close-notify moved to AFTER a successful rename so a failed rename can't desync the host by closing its authoritative tab (#4). Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green. Co-authored-by: Orca <help@stably.ai> * fix(editor): review round 2 (mirror ordering, rollback error, diff sources) - Close the host mirror tab only AFTER the rekey commits (capture pre-rekey resolution first): a rekey collision after a successful rename no longer desyncs the host by closing its authoritative tab (high). - Surface a failed inverse rename instead of swallowing it: the thrown error now states the on-disk move may remain at the new path (high). - Restrict diff-tab retargeting to staged/unstaged (purely path-derived ids); branch/commit diffs carry compare metadata and combined 'Changes' is worktree-rooted, so rebuilding them from path would produce a wrong id (med). Co-authored-by: Orca <help@stably.ai> * fix(editor): resolve the move verify gate proactively (review round 3) The rekey gates every dirty moved tab pending a destination content check, but verification only ran when a watcher event arrived for that path. If the watcher was down, throttled, or the event coalesced away, the gate never cleared and autosave stayed suspended for the tab. The coordinator now drives verification for every tab it gated once the rename has committed, so the gate resolves on its own. That makes the destination-side event latch redundant, so the in-flight tracker is source-only again. Co-authored-by: Orca <help@stably.ai> * fix(editor): review round 4 (gate strand, cross-worktree verify path, leak) - Don't install the move-echo verify gate on a tab already showing the changed-on-disk banner: it's autosave-suspended via externalMutation and verification skips a 'changed' tab, so the gate would strand forever. - Content-verify reads each moved tab's own filePath. A cross-worktree/ floating tab's relativePath is relative to its own root ('../…') and must not be joined onto the initiating worktree path (would read the wrong file and raise a false conflict banner on unsaved work). - Coordinator settles the in-flight source suppression in a finally so a throw between rename and commit can't leak it, and only after the rollback rename so a late forward-rename delete stays suppressed. - Migrate pendingEditorReveal.fileId across the rekey (matcher prefers it). Co-authored-by: Orca <help@stably.ai> * fix(editor): don't consume move-echo provenance in the safety-net verify (round 5) The round-3 proactive post-commit verify ran resolveLiveMoveVerification, which consumed pendingSelfMoveEcho. On FSEvents/SSH the real destination watcher event reliably lands AFTER the fast local read, so it then found no provenance, took the immediate changed-on-disk mark (no baseline check when there is no recent self-write), and raised a false conflict banner on the just-moved dirty tab — the exact data-loss this change removes. The proactive verify is a safety net: it now releases the autosave gate but leaves the provenance, so a later destination event is still recognized as the move's own echo and content-verified. Only a real watcher event consumes the provenance. Keeping it is safe — every watcher consumer verifies by content, which is strictly safer than the immediate mark. Co-authored-by: Orca <help@stably.ai> * fix(editor): scope move rekey to the initiating execution host (round 6) Co-authored-by: Orca <help@stably.ai> * fix(editor): prefix-suppress the move root so late tabs under a dir move aren't flagged (round 8) Co-authored-by: Orca <help@stably.ai> * chore(editor): trim move-fix comments to the why; drop redundant rename quiesce Co-authored-by: Orca <help@stably.ai> * fix(editor): record mirrored-close intent synchronously to close the ghost-tab window Co-authored-by: Orca <help@stably.ai> * fix(editor): use flavor-aware path containment for move selection (Windows/UNC case) Co-authored-by: Orca <help@stably.ai> * fix(editor): reconstruct moved path by segment count (WSL alias / duplicate-separator safe) Co-authored-by: Orca <help@stably.ai> * fix(editor): infer moved-path flavor by syntax, preserving legal POSIX backslashes Co-authored-by: Orca <help@stably.ai> * test(editor): lock POSIX ancestor-backslash destination flavor Co-authored-by: Orca <help@stably.ai> * fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes Co-authored-by: Orca <help@stably.ai> * fix(editor): flavor-aware separator folding in relative-path recompute (POSIX backslash) Co-authored-by: Orca <help@stably.ai> * perf(editor): keep the fs-watcher delete path O(deletes) when no move is in flight Co-authored-by: Orca <help@stably.ai> * docs(editor): tighten move-fix comments to one-line why-only Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
…e load (stablyai#9822) * fix(browser): stop failing goto when a redirect or download aborts the load Electron's loadURL rejects with ERR_ABORTED (-3) when the initial navigation is superseded — a client-side/meta redirect (common in SSO flows) or a download-triggered load. Since stablyai#9633 drives goto through wc.loadURL directly, that rejection surfaced as a spurious 'Failed to navigate' browser_error even though the page landed fine. Treat ERR_ABORTED like offscreen-browser-backend already does: resolve with the page's actual URL/title. Every other loadURL failure still fails closed. * fix(browser): settle replacement navigation after abort * fix(browser): clean up aborted navigation destruction race
) * fix(naming): remove identifier-first name post-processing Workspace display names and tab titles were being rewritten after generation by prompt-scanning heuristics from stablyai#8238 — a stray "#1" in prose became a workspace named "#1 - Fix", and the rewrite ran downstream of generation so user naming instructions couldn't override it. Per the same principle as stablyai#9088, naming defaults stay minimal and user overrides own the style. - Delete work-item-reference.ts and display-name-from-work.ts (+tests). - Auto-rename display names return to the humanized branch slug; tab titles return to the cleaned first prompt clause. - Explicit create-from-work-item naming returns to action-first ("Review PR 1234"). - Keep stablyai#8238's URL-before-markdown strip-order bugfix in tab titles, with regression tests adjusted to the natural expectations. * test(naming): pin incidental marker regression
…onment (stablyai#9836) stablyai#9804 added a leading `environmentId` parameter to recordWebSessionCloseIntent (and isWebSessionCloseIntentPending) and updated the web-runtime-session.ts callers, but missed the second caller in close-mirrored-editor-tab.ts, which still passed 3 args. This broke the web typecheck on main and, had it compiled, would have recorded the close intent under the wrong scope key (closeIntentScopeKey(environmentId, worktreeId)) — so the host snapshot could flash the just-closed mirrored tab back. Pass the already-validated runtimeEnvironmentId (the same value handed to closeWebRuntimeSessionTab immediately below) so the intent is scoped correctly. Update the test's isWebSessionCloseIntentPending assertion to the 4-arg form. Co-authored-by: Orca <help@stably.ai>
…ne per 15s (stablyai#9829) * perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s The managed Claude statusline script posted on every statusLine tick carrying rate_limits — ~3 curl spawns/sec per streaming pane, multiplied across concurrent panes. The service drops same-value posts inside its 30s dedupe window anyway, so most spawns bought nothing. Gate the post on a per-pane stamp file: POSIX compares date +%s against the stamp; Windows uses an all-builtin seconds-of-day parse of %TIME% (octal-safe, no extra process). Both fail open — unparseable time, garbage stamp, or midnight wrap posts rather than darkening the live usage feed — and the stamp only advances when a post actually fires, so skipped ticks never defer the next one. Measured: 30 rapid rate-limit ticks spawn 1 curl (was 30). * perf(rate-limits): keep throttled statusline ticks process-free Use Claude's monotonic session duration for the POSIX throttle so skipped ticks do not replace curl churn with date churn, while retaining a fail-open date fallback. Key temp files by the stable leaf UUID so path-like or long host tab IDs cannot disable the throttle. * fix(rate-limits): preserve throttle isolation across upgrades * fix(rate-limits): reject leading-zero stamp values before arithmetic All-digits validation still admitted values like 008, which are invalid octal inside $(( )) — and dash treats that expansion error as fatal, so the script died before rewriting the stamp and the pane's live usage feed stayed dark until the file was deleted. Allow-list canonical decimals (same pattern as the duration parse) on both the stamp and the computed clock so malformed values fail open to posting. Verified under dash: the old digits-only check aborts at the arithmetic (Illegal number: 008); the allow-list survives and posts. * perf(rate-limits): eliminate POSIX statusline cat churn * test(rate-limits): exercise overlapping statusline ticks The stamp check/write is deliberately lock-free (a lock could wedge the feed closed; fail-open is the contract), so a truly concurrent burst may post more than once, bounded by overlap width — an exact at-most-one assertion would be flaky by design. Assert the invariants that do hold: every overlapping run exits 0, the raced stamp lands valid, and it throttles the following ticks.
…eze (stablyai#8539) (stablyai#9847) On wake, remote/SSH runtimes reconnect in a staggered burst; the sidebar refetched all worktrees once per host, piling up K synchronous full-sidebar remounts and freezing the UI. Wrap the reconnect refresh in a single-flight coalescer so at most one refresh runs at a time plus one queued rerun, for any K. fetchAllWorktrees and its lineage follow-up are unchanged; only how often they fire changes. Co-authored-by: Orca <help@stably.ai>
…ai#9866) Mirror the noqa deploy workflow pattern so Cut Release runs surface kind/ref/dry_run/version_suffix as a table under Workflow Input Parameters.
Co-authored-by: Orca <help@stably.ai> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…teardown (stablyai#9030) (stablyai#9878) After a relay daemon restarts on an SSH host, main drops its status rows but renderer agentStatusByPaneKey entries whose connectionId stamp never matched (unstamped / SSH-mis-normalized) survived, stayed 'fresh' for 30 minutes, and made their sidebar rows permanently un-clickable (tab gone, handleActivateAgentTab silently returns). Broaden the transient clear to also drop worktree-attributed rows whose owning repo is on the torn-down connection, proving the host via the worktree->repo mapping instead of relying solely on the entry's connectionId. Co-authored-by: Orca <help@stably.ai>
…ch-editor preprocessor (stablyai#9887) Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Orca <help@stably.ai>
…p-free (stablyai#9881) Co-authored-by: Orca <help@stably.ai>
…idebar rows (stablyai#9850) * fix(agent-status): keep Claude in-process teammates visible as idle sidebar rows Claude Code 2.1.21x runs named Agent-tool agents as turn-based in-process teammates: SubagentStop and TeammateIdle fire at every TURN end while the teammate stays alive awaiting mail (verified live on 2.1.217). Treating those events as finish signals deleted the child row seconds after each burst, so the sidebar showed no subagents for most of a teammate's life. Root-cause fix: the roster now tracks a working/idle state per child. - One-shot children (hyphen-free ids) keep remove-on-stop: their SubagentStop is a true finish. - Teammate-shaped rows park as idle on SubagentStop/TeammateIdle and revive to working via the next SubagentStart (same lifecycle id, first-observed startedAt preserved). - Idle rows never gate the pane 'working' (stablyai#8825's done-gate rule). - Only TeammateIdle-confirmed idle rows survive a complete lead-Stop fold; a stopped workflow lane wearing a teammate-shaped id is reaped there (or immediately, once a fold tagged it listedAsSubagentTask), so the pre-stablyai#8825 idle pile cannot rebuild. - At the wire cap, the oldest idle row is evicted to admit a working spawn; working children are never displaced. - Hydrate keeps pruning idle snapshots: idle-teammate liveness cannot be proven across a restart, and a live teammate re-earns its row. * fix(agent-status): restore inventory-confirmed workflow lanes
* feat(skills): land remaining hybrid stubs * fix(build): exclude skill stub sources from packages
…gration (stablyai#9798) Opening the Quick Commands sheet right after connecting over relay races the relay->direct cutover, which rejects the in-flight one-shot settings.getTerminalQuickCommands with LogicalClientCutoverError while connState stays 'connected'. The sheet then strands on "RPC interrupted by connection migration" with an empty list until closed and reopened. The read is side-effect-free, so replay it on cutover (capped at 5, cancelled if the sheet closes or the client is replaced). Same failure class and pattern as stablyai#9794 (capability probe) and stablyai#9796 (terminal create).
…rch terms (stablyai#9967) The Language setting's native word (语言 / 언어 / 言語 / Idioma) only reached settings search via the localized title in that word's own UI locale — so a Chinese speaker on the default English UI could not find it by typing 语言. Always-index the native word for 'language' in every supported language (plus the previously-omitted Spanish native name Español), so speakers can locate and switch to their language from any starting locale. Native words are locale-invariant constants, so they are plain keyword literals with reviewed localization-coverage allowlist entries. Co-authored-by: Orca <help@stably.ai>
stablyai#9861) * fix(agent-status): map codex request_user_input questions to waiting Codex 0.145 asks user questions via the auto-allowed request_user_input tool (experimental default_mode_request_user_input): PreToolUse fires while blocked on the answer with no Stop, so Orca showed the pane as working/idle instead of Needs You. Map that PreToolUse to waiting (mirrors grok's ask_user_question), exempt question waits from the codex yolo auto-approval suppressor, and deliver native-chat answers to the digit-commit selector by option number (typed labels are ignored and Enter commits the highlighted first option). Older codex versions emit no such event and are unchanged. * fix(native-chat): preserve codex question answer semantics
…l fixes Merges stablyai/orca main (through stablyai#9874) into our fork's main. This brings in the three upstream fixes for the permanently-stale remote-runtime terminal pane bug (stablyai#8180) that CHE-1586 tracks: - stablyai#9774 fix(remote-runtime): recover terminals after network loss (pause-aware liveness detection + terminal multiplexer resubscription) - stablyai#9804 fix(terminal): prevent reconnect from killing live daemon sessions - stablyai#9874 fix(terminal): make remote workspace sleep converge UNRESOLVED CONFLICTS left in place (raw conflict markers, not compilable as-is) — all in the fork's own light/dark mobile theme feature, colliding with unrelated upstream changes to the same screens. Needs manual resolution before this can land on main: - mobile/app/_layout.tsx - mobile/app/index.tsx - mobile/app/settings.tsx (theme picker vs. upstream's new settings additions — API key management, KeyRound/MessageSquare icons) - mobile/src/storage/preferences.test.ts Ref CHE-1586.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges
stablyai/orcamain (through stablyai#9874) into our fork. Brings in thethree upstream fixes for the permanently-stale remote-runtime terminal
pane bug (upstream stablyai#8180):
pause-aware liveness detection + terminal multiplexer resubscription
(the exact root cause our internal ticket names)
Not mergeable as-is. 4 files carry unresolved conflicts (raw conflict
markers, will not compile) — all in our own light/dark mobile theme
feature colliding with unrelated upstream settings-screen changes:
mobile/app/_layout.tsxmobile/app/index.tsxmobile/app/settings.tsx(our theme picker vs. upstream's new API-keymanagement additions)
mobile/src/storage/preferences.test.tsThese need manual resolution (keep both feature sets) before this can
land. The original 3-PR upstream stack this was chasing (stablyai#8253→stablyai#8254→stablyai#8255)
is stale — stablyai#8253 closed unmerged, absorbed into the fixes above; stablyai#8254/stablyai#8255
remain open and are intentionally left out of this pull.
Tracked by our internal ticket CHE-1586 (parent: CHE-1585, the Orca
keep/fork/replace evidence tracker). An observation is filed to confirm,
through real usage after this is resolved and deployed, whether the
stale-pane symptom actually stops recurring.