Skip to content

[upstream #12991] fix(mobile): warm GitHub project repositories before filtering - #150

Open
innocarpe wants to merge 1112 commits into
mainfrom
fix/mobile-project-mode-repo-load
Open

[upstream #12991] fix(mobile): warm GitHub project repositories before filtering#150
innocarpe wants to merge 1112 commits into
mainfrom
fix/mobile-project-mode-repo-load

Conversation

@innocarpe

Copy link
Copy Markdown
Owner

Portfolio mirror of my contribution to upstream stablyai/orca.
Exhibition only — the real review/merge target is upstream.

Upstream

Summary

Description Warm the GitHub repository list before filtering Project rows in Orca Mobile. ## Focused fix - In scope: load the repository cache before the project-mode row filter runs. - Out of scope: changing the project-row filtering contract or desktop behavior. ## Prese

Note

  • Do not merge this into innocarpe/orca main until the upstream PR is merged.
  • After upstream merges: sync fork from upstream, then close this mirror PR.
  • This open PR exists so visitors see in-flight work on this fork's Pull requests tab.

AmethystLiang and others added 30 commits August 3, 2026 15:57
…azy status loading (stablyai#12225)

* Optimize worktree parent picker: conditional mount, virtualization, lazy

- Mount WorktreeParentPickerPopover only when open to avoid hundreds of
  unmounted instances subscribing to lineage and worktree store updates.
- Virtualize the candidate list and resolve activity statuses only for
  visible rows, eliminating redundant status subscriptions.
- Extract filtering, placement calculations, and row rendering into
  separate modules for testability and clarity.

* Memoize worktree parent picker search handler

Wrap search state update in useCallback to stabilize the handler
across re-renders. Reduces unnecessary effect runs and enables better
memoization of child components.

* Optimize worktree parent picker: defer unmount, memoize IDs

- Defer unmount until exit animation completes (200ms) to prevent premature teardown
- Memoize visibleWorktreeIds to prevent status hook from rebuilding its selector on every render
* Post fixing replies after launching PR comment resolution agents

Orca now automatically replies to selected comments after launching an
agent to resolve them. Review-thread comments get nested replies; review
summaries and issue comments get top-level @-replies. Payloads are
snapshotted at queue time and posted only after prompt delivery
succeeds. Bounded concurrency (4) prevents SSH slowdown.

* Batch conversation replies instead of posting per-comment

Combine N unresolvable comments into a single PR conversation reply to avoid timeline spam. Review threads still get one nested reply each. Refactor helper functions to pr-comment-fixing-reply-body.ts and change replyAsConversation(comment, body) → replyAsConversation(body).

* Handle bot account mentions in PR conversation replies

Use buildPRCommentConversationReplyBody to properly handle GitHub App
bot accounts, which don't resolve as @-mentions. Enforce delivery semantics
with type-safe PendingPRCommentAiAck payloads and improve error handling
around agent launch to prevent stranded state.

* Fix React Doctor ref-mutated-during-render errors in ChecksPanel

Move comment-resolution payload ref clears and latest-callback mirrors
into effects so render stays pure for the static analysis gate.
* fix: preserve Codex launch drafts through setup

* test: enforce single startup draft delivery

* test: name startup draft release behavior
)

* fix(ai-vault): support session scanning in SSH worktrees

Add relay-native aiVault.listSessions scanning that discovers agent
sessions on SSH hosts. Includes fallback to filesystem crawl for
legacy relays, full cancellation support, result validation, and
scan coalescing to reduce redundant work.

* fix(ai-vault): scan sessions in SSH worktrees with coordinated cancellat

- Extract batching logic to `mapRemoteScanBatches` for reuse and proper cancellation checkpoints
- Move `AiVaultScanCoordinator` from relay to main to handle concurrent same-key requests with individual cancellation signals
- Report scope path truncation consistently across relay and SSH fallback paths
- Gracefully degrade relay handler on unsupported platforms instead of aborting startup
- Refactor issue display to separate blocking errors, scope notices, and skipped transcript counts

* fix(ai-vault): stabilize SSH session scan CI

Swallow async WSL relay stdin EPIPE so the live hook-relay shard no longer
fails after all tests pass. Merge main, resolve scan/relay conflicts, and
align cancellation/host-issue reporting with IPC expectations.

* fix(ai-vault): harden session scan cancellation, relay timeouts, and preemption

Thread the abort signal through every scan and parse path so superseded or
cancelled scans stop promptly instead of parsing every remaining transcript
for a caller that already left.  Replace the fragile message-text relay
timeout check with a typed error code so unrelated errors carrying the
phrase "timed out after" no longer suppress the filesystem fallback.  Fix
scan coordinator preemption so a forced Refresh in one window no longer
re-enters as a spurious cancellation in another.  Add a host-leg cache for
the all-hosts view and cap filesystem concurrency so a single slow remote
home cannot stall the whole merge.

Co-authored-by: Orca <help@stably.ai>

* fix(ai-vault): use stable React keys for scan issue banners

Drop array-index keys so react-doctor/no-array-index-as-key passes.
Uniqueness comes from host, kind, agent, path, and message.

* fix(ai-vault): SSH session scanning with configurable depth limits

Implement depth-aware caching and proper scan boundaries to make SSH session
scanning reliable in worktrees. Users can now select between faster (250
sessions) and comprehensive (unlimited) history scans. The scanner:
- Deduplicates scans across relay, host leg, runtime, and renderer layers
- Reuses larger scans to serve smaller depth requests
- Properly bounds in-scope discovery per-limit
- Fixes timeout enforcement when SSH providers ignore abort signals

* Move sessionLimit ref update to useLayoutEffect

Keep render pure for React Doctor by deferring ref updates to
a layout effect, which still executes before render-dependent
effects that consume the ref.

* fix(adhoc): stamp version prefix from main, not the feature branch

Adhoc builds check out arbitrary refs whose package.json often lags
version bumps (e.g. 1.4.165-rc.0 while main is 1.4.168-rc.1). Hourly
always builds main so it already tracks the product line; adhoc now
resolves the base version from origin/main (or ORCA_ADHOC_BASE_VERSION)
so branch builds share that prefix.

* Revert "fix(adhoc): stamp version prefix from main, not the feature branch"

This reverts commit a26a18e.

* fix(ai-vault): fix scoped backfill and coordinator race conditions

Resolve race where the last waiter leaving could abort an already-settled scan (add `settled` flag). Redesign scoped session backfill to keep searching through newer files until the scope reaches its requested session quota instead of stopping at the candidate limit; out-of-scope files no longer consume the scope budget. Centralize scan limit normalization and fix error classification for cancelled scans using the proper helper instead of checking Error.name. Disambiguate cache keys using JSON and add cancellation check after scope discovery phase.

---------

Co-authored-by: Orca <help@stably.ai>
* chore(daemon): disambiguate audit observations

* fix(daemon): reject future audit protocol roles

* fix(telemetry): protect daemon audit observations
* fix(terminal): reconcile status after escape interrupt

* fix(terminal): preserve absent interrupt baseline

* fix(terminal): ignore stale input acknowledgments

* fix(terminal): order interrupt acknowledgments
…minals (STA-905) (stablyai#12355)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…8) (stablyai#12359)

* fix(renderer): drive agent working spinner on the compositor (STA-3328)

The shared 12Hz JS clock wrote el.style.transform on every mounted spinner
(41 live = ~490 main-thread style writes/s), keeping style perpetually dirty
and delaying keystroke dispatch (measured typing inputDelay p90 363ms; 19ms
with the writes stopped). Replace it with a steps(12) CSS transform animation
so rotation runs on the compositor; one mount-time animation-delay write
preserves the shared-epoch phase sync.

* fix(renderer): lock agent spinner phases to document time

* fix(renderer): tolerate missing spinner animation API
…ed (stablyai#12346)

* fix(agent-status): restore hydrated nonterminal statuses as unconfirmed

A hook transition that fires while Electron is down has no receiver and is
discarded, so last-status.json can restore a stale 'working' as confirmed
truth for up to the 7-day hydrate TTL. Stamp hydrated nonterminal rows with
restoredUnconfirmed, carry it through both IPC paths, and treat such rows as
never-fresh in the shared and renderer freshness gates so the sidebar,
worktree.ps, and the raw snapshot all present the same degraded semantics.
Terminal states restore as-is; any accepted live event clears the flag; the
flag itself is never persisted. Interrupt/question inference refuses to
fabricate transitions onto unconfirmed rows.

* fix(agent-status): shed unconfirmed marker when the liveness sweep verifies done

The restored-subagent reaper's reconciled entry spread carried
restoredUnconfirmed onto a process-probe-verified 'done', making freshness
gates suppress a legitimate completion. Keep the marker only while the
reconciled state stays nonterminal.

* fix(agent-status): let live evidence replace hydrated rows

* fix(agent-status): keep restored rows degraded

Sort accepted live evidence after hydrated rows even across wall-clock rollback. Let unconfirmed rows own their preserved pane titles without asserting live state, while retaining independently live sibling evidence.

* fix(agent-status): suppress unmapped restored titles

Treat a single runtime title as covered by the single restored hook row while layout identity is unavailable. Preserve ordinary age-stale fallback and mapped sibling-pane evidence.
* feat(ssh): add SSH config host picker for add-host form

Users can now click 'Fill from ~/.ssh/config…' to browse available SSH
config hosts in a picker, select one, and have the form automatically
prefill with resolved connection details (hostname, port, username, auth).

Previously, an 'import' button provided bulk sync on this form—confusing
and unhelpful when everything was already synced. That action is now
available as a secondary 'Add all' option in the picker.

* fix(ssh): import filter preservation and label fallback

- Reuse search loader on import completion to preserve active filter inside generation guard
- Fall back to hostname when manual host has no label, not empty string
- Make alias duplicate detection case-insensitive to match config picker behavior
- Validate host availability when restoring project group selection
- Add aria-selected attribute to picker options for accessibility

* fix(ssh): harden config picker import, alias folding, and host targeting

Review findings on the ~/.ssh/config picker + bulk add:

- Guard config-host resolution with a generation counter so a late resolve
  cannot overwrite a later pick or a form the user backed out of; freeze the
  other rows while a pick resolves.
- Stop "Add all N" from re-adopting deleted hosts — it now imports without
  reAdopt, matching the new-host count it advertises. Settings → Import keeps
  the explicit re-adopt path.
- Fold SSH aliases through a shared normalizeSshConfigAlias for import
  ownership, delete tombstones, reclaim, picker search, and the save-time
  duplicate check, which now occupies configHost *and* label like the picker.
- Persist GSSAPIAuthentication only when a parsed Host entry asks for it, not
  when `ssh -G` merely echoes the /etc/ssh system default.
- Fail closed with unavailable/setup-not-found when an explicit
  projectHostSetupId names a non-actionable host instead of silently creating
  the workspace on a sibling host.
- Cache the parsed config for the picker session (refresh on open/retry) so
  filter keystrokes no longer reparse and Include-expand the file, keep the
  filter usable during loads, add a Retry on load errors, explain an empty
  Identity file after a config fill, and drop the always-false aria-selected.

* refactor(ssh): centralize host result limit and extract folder group val

Move SSH_CONFIG_HOST_RESULT_LIMIT to shared types so the renderer's limit message
cannot drift from the host's query limit. Extract findActionableFolderProjectGroup
to avoid repeating the folder-host-availability check across the composer hook.

* fix(ssh): pass -F to ssh -G when HOME differs from passwd home

In E2E tests and sandboxes, isolated HOME can differ from the system
passwd home. OpenSSH resolves the default config via getpwuid (passwd),
while Node's loadUserSshConfig uses os.homedir() (HOME-aware). Pass -F
to explicitly specify the config path when they diverge, so ssh -G and
the picker resolve the same file.

* fix(ssh): verify config host exists before resolving with ssh -G

When a user edits ~/.ssh/config and removes a host, the import picker
should not fall back to ssh -G's echoed response (which treats any alias
as valid). Check the reloaded config file before resolving.

- Force reload config on each resolve to catch user edits post-open
- Reject aliases not in the current config before calling ssh -G
- Add test for deleted alias edge case
- Fix workspace-target fallback to honor explicit host selection

* fix(ssh): let tombstoned aliases be re-picked in the config picker

Allow users to reclaim a deleted SSH host by re-picking it from ~/.ssh/config. Tombstoned aliases now appear in the picker with a "Removed from Orca" badge and remain pickable, but don't count toward "Add all" operations — ensuring passive import never resurrects a deleted alias while still giving the user a recovery path.
…TA-3337) (stablyai#12362)

* fix(mobile): bound terminal viewport resubscribe loop with backoff (STA-3337)

An empty scrollback frame with absent host dims was coerced to 80x24, which
never equals a phone viewport, arming a zero-delay unsubscribe/resubscribe
loop (~25/s) that broke long-press gestures and drained battery.

- Absent host dims now hold the stream instead of resubscribing.
- Fit resubscribes are budgeted per handle (3 attempts, escalating backoff)
  with an absence-gated refill mirroring the chat-side rearm bound; on
  exhaustion the view degrades visibly via toast instead of hot-looping.
- A fresh post-measure match counts as convergence instead of resubscribing.
- setTerminalModes keeps the Map identity when the mode is unchanged, so
  same-mode frames no longer re-render the session route.
- Host emits the subscriber viewport as scrollback dims when the snapshot
  and PTY size are both unavailable, so current hosts converge immediately.

* fix(mobile): cancel stale viewport retries after convergence
…yai#12375)

gh repo create leaves the repo empty, and publishing a release creates a
git tag that needs a commit to point at. The adhoc build got all the way
through notarization and upload before failing on "Repository is empty".

Co-authored-by: Orca <help@stably.ai>
…son (stablyai#12376)

main's version only moves on `release:` commits, and stable patches are cut
from release branches that never merge back. On 2026-08-03 main read
1.4.165-rc.0 for twenty hours while 1.4.165, 1.4.166 and 1.4.167 all shipped,
so every hourly built in that window was stamped 1.4.165-hourly.* while
carrying code newer than 1.4.167 — and sorted below the stable its user was
already running.

Resolve the base from the main repo's published tags instead, taking the patch
above the highest shipped stable. package.json stays a floor for the case where
main leads the tags.

Co-authored-by: Orca <help@stably.ai>
…load (stablyai#12245)

* perf(runtime): withhold unchanged mobile snapshots from the graph payload

Every graph sync structured-cloned all 222 worktree snapshots to main even when
none had changed: 374 KB and ~5 ms per clone, paid twice because Electron clones
on serialize and again on deserialize. That transport cost — not the renderer
rebuild — is the bulk of a publication.

The renderer now sends only the snapshots main has not acknowledged and names
the rest in unchangedMobileSessionWorktrees. Detection is object identity, not a
deep compare: an unchanged worktree already returns its cached snapshot object.
Main seeds nextWorktrees from that list so its prune keeps withheld worktrees
live instead of removing them.

The call itself is unconditional. syncWindowGraph is not a one-way publish — its
return value is the only channel carrying agentOrchestrationByPaneKey to the
renderer, and the handler adopts pre-allocated handles, merges detached leaves,
refreshes writable flags, and drains graph-sync callbacks on every sync. Skipping
it would starve all of that.

Two failure modes are closed explicitly. The memo advances only after main
acknowledges, so a publication that throws is resent in full rather than
silently withheld forever. And a worktree main dropped on its own — worktree
metadata removal — comes back in mobileSessionResyncWorktrees, which also clears
the accepted-revision record so the republish is not rejected as a no-op.

Unchanged republish at 222 worktrees / 787 tabs: 374 KB to 3.4 KB, 5.08 ms to
0.02 ms per clone. One changed worktree: 5.3 KB.

* fix(runtime): resync stale withheld mobile snapshots

* fix(runtime): align accepted mobile snapshot membership

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…ts (stablyai#12161)

The adhoc workflow checked out any requested ref and ran its scripts and
electron-builder config with MAC_CERTS, the notary password, and the adhoc
publisher token in reach — including refs/pull/* fork code a maintainer
could dispatch in one innocuous-looking click.

Vet the ref before checkout: PR refs are refused, branches/tags resolve in
a bare tree:0 scratch fetch, raw SHAs must be reachable from a repo branch
or tag (a partial clone lazily serves PR-only commits by SHA, so name
resolution alone is not a trust test), and checkout pins the vetted SHA so
a race push cannot swap the commit. Also reference an adhoc-mac-build
environment so the secrets can later be fenced off from stale workflow
copies via repo settings.
…nload veto on interrupt (stablyai#12232)

Two robustness follow-ups from the stablyai#12194 safety-review loop (pass 3),
staged by the reviewer before its session ended; both fail toward extra
retention only:
- recency bookkeeping now runs while the retention kill switch is off, so
  re-enabling evicts in true LRU order instead of worktree-list order
- interrupted downloads release the eviction veto instead of pinning the
  worktree as downloading forever
…(STA-3328) (stablyai#12377)

* perf(agent-status): coalesce live status bursts into one render pass (STA-3328)

Each live agent-status event arrives as its own IPC task, so a multi-agent
burst paid one full React render pass per event (200-488ms main-thread tasks
under heavy orchestration). Same-task store commits batch to a single render
pass, so buffer a burst for one 33ms window and apply it in one task. The
leading event still applies synchronously (zero added latency for single
events), only an applied event arms the window (dropped/pending events stay
immediate), and both clear paths purge queued sets so a coalesced event
cannot resurrect a removed status.

* fix(agent-status): preserve queued completion on teardown
…g host (stablyai#12388)

Destructive worktree removal swept PTYs by worktree id alone. Worktree ids are
`repoId::path` and the store keeps one per host, so deleting an SSH worktree
could stop a same-id local (or other-connection) workspace's terminals — or fail
outright with `selector_ambiguous` when two hosts owned the id.

Every destructive teardown now names its owner (resolvedWorktreeId plus the
connection/runtime environment), matching the already-hardened forget-local path:

- IPC `worktrees:remove` (git + folder workspaces)
- runtime `removeManagedWorktree` (CLI/mobile `worktree.rm`, git + folder)
- missing-worktree terminal reconciliation, including its no-provider fallback

The stablyai#11960 allowUnverifiedStop force-delete gate is untouched.
…efore reporting success (stablyai#12387)

The sync before-unload checkpoint staged renderer state and then queued
store.flushPendingAsync() fire-and-forget, so reload/restart/update paths
navigated while the staged session, scrollback and UI state were still
only in memory. Quit is covered by the will-quit flush barrier; those
paths were not.

Keep staging synchronous (no sync durable writes), but record the flush
outcome and expose it on app:await-before-unload-checkpoint. Restart,
updater install and lazy-chunk recovery reload now join that write before
navigating and abort the attempt when it fails or outlives a 20s deadline.
stablyai#12383)

The per-worktree rebuild memo from stablyai#12207 refused to skip any worktree
with a registered TerminalPane, because builders read live PaneManager/DOM
state the memo could not witness. Hidden worktrees stay mounted (8 parked
+ 12 retained by default, unbounded with terminalHiddenViewParking off),
so that set rebuilt on every publication — on the always-on hosts the memo
targeted, ~20 worktrees paid the full content build plus the new inputs
build and deep compare every tick.

Capture the live reads instead: snapshot pane leaf ids, the live active
leaf, the serialized pane tree, and per-pane numeric/pty bindings once per
publication into MountedTerminalSurfaceCapture, compare captures by value
in the memo, and have the builders read only the capture. Live state
outside the capture is now unrepresentable in a builder, so the skip stays
provably complete — and a mounted worktree whose panes did not change
reuses its snapshot like any other.
)

* fix(ui): always show Onboarding in the Help menu

Users could not find "Show onboarding again" because it required holding Alt. Surface it on the default Help menu so replay is discoverable without reinstalling.

* fix(ui): always show Restart Orca in the Help menu

Surface Restart next to Check for Updates by default and drop the Alt-only admin gate, which no longer hid anything else.
…ever (stablyai#12390)

GitLab's project-ref cache stored `null` forever and returned any cached
value straight from the map, so a repo probed before `origin` was configured
— or before `glab auth login` ran for its self-hosted host — kept
hosted-review provider detection stale until app restart. The negative-TTL
work that shipped for Azure DevOps / Bitbucket / Gitea skipped it.

Mirror `createRemoteRefProbeCache`'s semantics: negatives expire on the
shared interval, positives stay, the SSH provider generation joins the cache
signature so a reconnect re-asks, and a probe abandoned as stale can no
longer publish over its successor. Transient git/SSH failures stay uncached.

Expiring negatives would otherwise turn `glab auth status --hostname` into
one spawn per repo per interval on the hosted-review poll, since a non-GitLab
remote reaches it too, so remember the unauthenticated answer per host — not
per repo — on the same clock.
* fix(terminal): preserve restored snapshot fidelity

* test(terminal): align legacy history handoff snapshot expectation

* fix(terminal): keep legacy snapshot panes mounted

* fix(terminal): refresh snapshot capability after startup

* fix(terminal): refresh snapshot capability in degraded startup

* fix(terminal): await snapshot provider authority
* fix(mobile): render Mermaid diagrams in MobileMarkdown (stablyai#11141)

Co-Authored-By: Grok Companion <noreply@x.ai>

* fix(mobile): keep streaming mermaid fences as raw code until the fence closes

* perf(mobile): memoize MermaidDiagram and add a CDN load watchdog

* fix(mobile): escape mermaid source before embedding in WebView script

JSON.stringify leaves </script>, &, and U+2028/U+2029 raw, so a diagram
source containing </script> broke out of the inline script and ran
arbitrary WebView JS. Diagram source is untrusted (agent output, PR/chat
content), and this component now renders from chat and markdown preview,
not just the PR sidebar. Escape those chars to \uXXXX; the literal still
parses back to the exact source. Adds an adversarial buildHtml test.

* fix(mobile): embed the mermaid engine instead of fetching it from a CDN

The diagram WebView loaded mermaid from jsdelivr at runtime: offline and
constrained-network renders always fell back, the stalled-load watchdog
existed only to paper over that, and an unpinned floating-major CDN script
with no integrity check ran inside the WebView. Embed the lockfile-pinned
package's prebuilt bundle via a postinstall generator (same mechanism as
the terminal WebView engine) so the document loads nothing external; the
watchdog is removed as obsolete and a no-external-URL gate pins it.

* chore(deps): align mermaid at 11.16.0 across desktop and mobile

Desktop floated ^11.15.0 while the mobile embedded engine resolved 11.16.0.
Raise the desktop floor so both lockfiles resolve the same version, and pin
mobile exact: the generated WebView engine embeds the package bytes, so an
implicit range bump would silently change what ships.

* fix(mobile): block Mermaid diagram network requests

Mermaid image-node URLs can initiate subresource requests even with the engine embedded. Keep the WebView offline by restricting resource types through its document CSP.

* style(mobile): format Mermaid routing test

* fix(mobile): use stable keys for Mermaid diagrams

* fix(mobile): keep duplicate Mermaid keys distinct

Combine each diagram source with its sibling occurrence so identical diagrams remain unique while source edits still remount the WebView and later streaming prose does not.

* fix(mobile): keep Mermaid transitive within release-age policy

---------

Co-authored-by: Grok Companion <noreply@x.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
…lyai#9178)

* fix(mobile): keep main-buffer TUI footer above the iOS keyboard

The iOS keyboard-avoidance lift anchored on the terminal cursor row. Pi's
TUI renders in the main screen buffer (not the alternate screen) with its
footer/status rows below the input caret, so the altScreen full-lift branch
was skipped and those rows stayed under the raised dock / keyboard.

Anchor the lift on the bottom-most non-blank viewport row instead of just
the cursor: the WebView now emits contentBottomRow, and the lift uses
max(cursorY, contentBottomRow). This generalizes the alt-screen case,
keeps short output at the top put, and matches prior behavior for a
scrolled shell prompt.

Extracted the lift into a pure, unit-tested function
(terminal-keyboard-avoidance-lift.ts) and moved metrics parsing into a
tested helper on the contract.

* fix(mobile): preserve keyboard metrics through notification dispatch

* fix(mobile): harden terminal keyboard metrics

* fix(mobile): ignore unstyled terminal whitespace

* fix(mobile): preserve decorated terminal whitespace

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Attribute destructive runtime and daemon close diagnostics to the requesting client and exact target, record outcomes only after completion, and add reliability-gated attribution regressions.
…both PATH and Path (stablyai#12218)

* fix(windows): stop the Orca CLI dying on a duplicated PATH/Path environment

The packaged Windows `orca.exe` launcher read
`ProcessStartInfo.EnvironmentVariables`, whose lazy getter copies the
case-sensitive process block into a case-insensitive dictionary via `.Add`.
An inherited block carrying both `PATH` and `Path` threw
`ArgumentException: Item has already been added. Key in dictionary: 'PATH'`,
so every `orca` invocation exited 1 before Electron started
(native/windows-cli-launcher/OrcaCliLauncher.cs:46, printed at :67).

The launcher now mutates its own environment with
`Environment.SetEnvironmentVariable` and never touches either
`ProcessStartInfo` env property, so `CreateProcess` passes a NULL environment
block and the child inherits the live one verbatim.

Orca was also minting the duplicate itself. `applyTerminalAttributionEnv`
read `baseEnv.PATH` and unconditionally wrote `baseEnv.PATH`, so a Windows
PTY that inherited `Path` got a second spelling; which one the child resolved
was non-deterministic. `createLaunchEnv` did the same and, because its read
always missed on Windows, shipped Agent Teams terminals a `PATH` containing
only the tmux shim dir.

`resolvePathEnvKey` (extracted from the existing precedent in
windows-environment-path.ts) now drives every PATH read and write in the PTY
env pipeline, and attribution collapses Windows onto the single OS-resolved
spelling. Off Windows the resolver always returns `PATH`, so POSIX behavior
is unchanged and a case-sensitive POSIX `Path` variable is never touched.

Closes stablyai#12046

* test(windows): track the launcher's own-environment marker

The stablyai#12046 fix moved ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER and ORCA_CLI_COMMAND
off ProcessStartInfo.EnvironmentVariables, but this asset test still pinned the
old dictionary writes and failed.

Co-authored-by: Orca <help@stably.ai>

* fix(windows): follow the host block's PATH spelling on sparse daemon env patches

Resolving a path-less Windows env to `Path` handed the daemon's own
`{...process.env, ...opts.env}` merge both spellings when the host block spelt
`PATH`. Fall back to the host block's own key, and collapse again inside the
daemon since that merge happens after attribution.

Co-authored-by: Orca <help@stably.ai>

* fix(windows): resolve the live PATH spelling by block order, not casing

Win32 resolves a duplicated variable by taking the first case-insensitive
match in the block, so `resolvePathEnvKey`'s hardcoded `Path`-first
preference targeted the shadowed spelling on the reporter's own
`["PATH","Path"]` block. Drop the attribution-side collapse with it: it
deleted the other spelling's value, and deleting the live key promotes
the shadowed one, so an env that stripped down to empty lost both.

* chore: drop unrelated merge formatting

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…blyai#12409)

* test(e2e): start the board marquee off the board's padding cliff

`selects the full lane across a single large marquee scroll jump` failed the
changed-e2e-specs job with `Received: 0` — no card ever previewed. The CI
trace shows the drag started at (284, 195) and the failure screenshot shows a
native text selection with no marquee overlay, so the board never accepted
the pointerdown.

An element scan across that row shows why: the marquee may only start on
empty board space, and the usable strip is only x 280-298 — the board's own
left edge on one side, the first lane's cards on the other. `selectionBox.x +
4` aims 4px inside that strip, so a layout that rounds a few pixels
differently lands outside the board entirely, where the pointerdown never
reaches the handler and the browser text-selects instead.

Aim at the middle of the measured strip, assert the start point really is
empty board space before pressing, and assert the selection rect appears so a
rejected gesture fails immediately instead of surfacing 15s later as "0 cards
previewed".

Also carried over from the first attempt at this deflake: the lane scroll now
jumps until the virtualizer stops moving the bottom, because a measured card
is much taller than the row estimate and a fixed pass budget commits the
marquee short of the last cards; and the final assertion checks the badge's
text so a short selection reports its count.

* test(e2e): pick the marquee start point from live geometry, not a precomputed one

Round 2 fired the new guard: `marquee start point (290, 195) must be empty
board space` with `onSurface: false` AND `onIgnoredTarget: false` — so
elementFromPoint returned something outside the board entirely, even though
(290, 195) sits inside the measured 280..299 strip. Aiming at the middle of
the strip is still aiming at a point computed before the probe runs, and the
sheet, sidebar and lane fill keep resizing that strip afterwards.

Stop precomputing the point. Read the geometry and scan for an empty point in
the same DOM turn, walking a grid across the strip between the board's left
edge and the first card, over the lane's top rows only — the marquee anchors
its range in content space, so a start below the first card would drop it from
the 102. Take the first point the board itself reports as empty, and require
two consecutive probes to agree so a frame mid-relayout cannot win.

Keep the precondition guard, now reporting the live geometry and the elements
that blocked each rejected point. If no empty point exists at all, skip with
that reason instead of failing on a layout the test cannot drive.

* test(e2e): start marquee from an explicit empty lane

* test(e2e): find visible board space for marquee start

* test(e2e): quarantine unstable marquee hit test
…2101); stop OSC color-reply echo leak in POSIX agent panes (stablyai#12112) (stablyai#12202)

* fix(terminal): reset stale mouse tracking on cold restore (stablyai#12101); stop OSC color-reply echo leak in POSIX agent panes (stablyai#12112)

stablyai#12101: a force-killed TUI never emits its DECRST reset, so its armed mouse
mode is latched into the on-disk checkpoint and re-derived into the
replacement process's emulator via the cold-restore history seed -- through
both rehydrateSequences and SerializeAddon's own mode trailer. The revived
bare shell then echoed SGR motion reports at the prompt. Seed a
RESET_MOUSE_REPORTING segment after the snapshot (before the torn escape
tail), only when there is real recovered content so the empty-array
"nothing to recover" sentinel survives.

stablyai#12112: agent panes arm a main-side PtyStartupIngress that answered opencode's
startup OSC 10/11 queries synchronously inside node-pty's onData, while the
POSIX tty still had ECHO on. The line discipline echoed Orca's own reply back
out as visible text. Echo suppression existed but was gated on windows-conpty.
Add PtyStartupReplyDelivery: POSIX defers the write off the query's turn and
recognizes its own echo anywhere in a span (bounded, non-destructive); ConPTY
keeps its synchronous write; windows-wsl is byte-identical to before.

Fixes stablyai#12101

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): read the slave's ECHO bit before answering a color query

The startup color reply was written into a PTY still in cooked mode, so the
line discipline echoed it back as visible junk (stablyai#12112). Whether that will
happen is readable state on the slave rather than something to infer from
returning bytes, so the reply now waits until the ECHO bit is observably
clear instead of guessing at echo shapes.

Two echo sources exist and only one is readable. A `quiet` verdict proves
the kernel will not echo, so it retires the caret projection; readline
echoes a master write in software with the tty already raw, so that
projection stays armed on every path. Scoping `quiet` narrowly is the whole
correctness argument here: reading it as "no suppression needed"
reintroduces the bug at a plain shell prompt.

Polling is bounded by a wall-clock budget rather than an attempt count,
because each probe is a subprocess and a multi-pane restore serializes them
on fork. Withholding measures flat at ~210ms from 1 to 100 panes.

Also resets a cold-restored pane's mouse reporting (stablyai#12101). The armed mode
is re-derived from the dead process's own persisted bytes through two
channels, so the daemon seeds a reset into recovered history and the
renderer stops trusting a persisted "live agent" signal after a cold
restore. The reset literals move to one shared profile module.

Fixes stablyai#12101
Fixes stablyai#12112

Co-authored-by: Orca <help@stably.ai>

* test(terminal): pin the cold-restore reset on the spawn-adopted reattach path

A spawn can be answered with an adopted session, which reaches the reattach
handler by a door that skips the restored-session path. Pin that the cold-restore
signal survives it, so stablyai#12101's junk cannot come back through it.

Co-authored-by: Orca <help@stably.ai>

* test(terminal): note why the adopted-reattach snapshot leaves the cursor visible

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): harden startup reply delivery

---------

Co-authored-by: Orca <help@stably.ai>
erishforG and others added 28 commits August 6, 2026 02:26
…o load (stablyai#12867)

* fix(repo-icon): fall back to a lucide icon when an image icon fails to load

Private-mode GitHub Enterprise avatars need a logged-in web session, so the
stored avatar URL fails to load and the image branch rendered blank space.
The lucide and missing-icon paths already fall back to Folder; the image
branch had no equivalent.

Track the failed src in state so a repo switched to a different icon still
renders that icon instead of staying on the fallback.

Fixes stablyai#11211

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(repo-icon): assert the specific fallback icon instead of any svg

The fallback and unchanged-icon tests only checked that an svg rendered, so
they passed even if the wrong icon came back. Assert the lucide class name,
and cover an unknown lucide name falling back to Folder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: prioritize filenames in new-tab file results

* fix: preserve root separator in filename-first paths

* refactor: use native file path tooltips

* fix: position file path tooltips

* fix: use the native OS tooltip for new-tab file paths

Co-authored-by: Orca <help@stably.ai>

* fix: show new-tab file paths in a system-style tooltip

Co-authored-by: Orca <help@stably.ai>

* fix: anchor new-tab path tooltip to the cursor

Co-authored-by: Orca <help@stably.ai>

* fix: tighten cursor tooltip to file rows and design tokens

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
* fix: show full paths in quick open results

* refactor: use native file path tooltips

* fix: position file path tooltips

* refactor: share the cursor path tooltip with quick open

Co-authored-by: Orca <help@stably.ai>

* fix: let path tooltips run wider before wrapping

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
…blyai#12828)

* perf: bound repeated watcher, vault, and terminal work

* fix(terminal): preserve redraw recovery while bounding fit retries

* fix(watcher): retain structural fallback after crash fuse

* fix(ai-vault): preserve forced scan budget
* fix(mobile): choose host for new workspace

* fix(mobile): close stale workspace host picker

* fix(mobile): disambiguate workspace host choices

* fix(mobile): keep host endpoint paths private

* fix(mobile): redact invalid host endpoints

* fix(mobile): handle opaque host endpoints

* fix(mobile): announce host picker options

* fix(mobile): harden workspace host picker

* fix(mobile): preserve host through workspace creation
…tablyai#12816)

Reverts the classification change and keeps behavior at base.

cursor-agent's native OSC title is the bare literal "Cursor Agent" and never
carries a status word, so it names the agent without proving one is present.
The title tracker drops it live, so main records it only when the stale-working
timer strips the spinner off the synthesized "⠋ Cursor Agent" — and that fires
both when Cursor parks idle and when cursor-agent exited and the shell reclaimed
the pane. The two states are observationally identical: same title, same null
foreground read.

Classifying it as an agent therefore removes a refusal rather than adding
evidence. Guarded sends auto-submit Enter, so the false positive types into the
user's shell. A null foreground is also not "unreadable" on the default local
provider, which returns null when the pty is gone.

hasPty, probePtyLiveness, hasChildProcesses and inspectProcess were each checked
as corroborating signals; none separates alive-with-agent from alive-with-shell
when the foreground read is unavailable.

Tests pin every no-evidence branch fail-closed and document the mechanism, so
both attempted fixes fail loudly if reintroduced.

Real gap tracked in stablyai#12946.
* fix(terminal): use provider-native session titles

* refactor(terminal): source session names from AI Vault

* fix(tabs): harden AI Vault title sync
…evel Git base for on-prem Server (STA-3494) (stablyai#12832)

* fix(azure-devops): retry with -preview api-version and keep project-level Git base for on-prem Server (STA-3494)

Azure DevOps Server rejects api-version=7.1 with 400
VssInvalidPreviewVersionException unless the -preview suffix is supplied,
so auth and every Git endpoint failed. Retry once with -preview on that
rejection and remember the requirement per origin. Also stop letting a
same-origin ORCA_AZURE_DEVOPS_API_BASE_URL (collection-level, needed only
for the connectionData auth probe) override the project-level base derived
from the remote for Git endpoints; cross-origin (proxy) overrides keep
working.

* fix(azure-devops): constrain preview retry and base override
…3505) (stablyai#12833)

* fix(permissions): surface macOS silent Local Network denial with diagnostic and workaround (STA-3505)

On macOS 27 beta, NECP silently denies Orca's whole process tree Local
Network access: no prompt fires, the app never appears in System
Settings, and terminal child processes fail with EHOSTUNREACH. The
Settings trigger swallowed the probe's socket error and reported
'unknown' + a 'Permission request sent' toast, indistinguishable from
success.

Classify the mDNS probe outcome (EHOSTUNREACH/EHOSTDOWN -> denied,
clean send -> granted, else unknown), remember the verdict for the
status chip, and render an inline diagnostic with the documented
NECP re-evaluation workaround when denial is detected.

* fix(permissions): avoid false Local Network grants

* fix(permissions): use standard Local Network request flow

* feat(permissions): add local network connection test

* fix(permissions): nest local network connection test

* fix(permissions): collapse connection test by default

* fix(permissions): emphasize connection test action

* fix(permissions): restore outlined connection action
…ever lands (stablyai#12950)

* fix(renderer): contain corrupt lazy chunks when the recovery reload never lands

9 react-error-boundary crash reports across v1.4.171-1.4.175 (macOS, Linux,
Windows) all end the same way: a corrupt lazy chunk fails to import, recovery
requests a reload, the reload never lands, and loadLazyWithRetry re-throws the
raw SyntaxError/TypeError. RecoverableRenderErrorBoundary only suppresses
LazyChunkLoadError, so the raw error files a user-facing crash report.

LazyChunkLoadError was unreachable in production. Its precondition is a guard
written by a *different* document ('reload-landed'), but the finally block
clears that guard before the throw, so the only path that could construct it
never ran. Confirmed by the shipped bundles: 16/16 lazy_chunk_reload_vetoed
breadcrumbs carry outcome=never-landed, zero carry any other outcome, and no
bundle contains a boundary-degraded breadcrumb.

Route every exhausted-recovery path through exhaustedRecoveryFailure() so an
attempted-and-failed recovery yields a LazyChunkLoadError the boundary can
contain, and record a lazy_chunk_recovery_exhausted breadcrumb carrying the
call site, the real chunk error, and the outcome.

Deliberately unchanged: when recovery is never *attempted* (no window/SSR,
blocked sessionStorage, guard write failure) the raw error is still thrown so
normal crash reporting is unaffected. Only isKnownDynamicImportFailure matches
are contained, so module logic bugs keep reporting.

* perf(renderer): trim redundant work on the lazy-chunk failure path

Hoist the dynamic-import message patterns to module scope so classification
stops allocating seven RegExp objects per call, thread the already-computed
classification into exhaustedRecoveryFailure so the guard-not-landed path does
not re-run it, and bound recordedExhaustionKeys the way the breadcrumb and
renderer-error key stores are bounded, since error.name is library-controlled.

Failure path only; the success path is unchanged.

* refactor(renderer): remove a transposition trap on the lazy-chunk failure path

exhaustedRecoveryFailure ended in two adjacent booleans with opposite
consequences: transposing them would have returned the raw SyntaxError and
silently restored the crash this branch fixes, with no test able to catch it
(the only call site passed true for both). The isChunkFailure parameter saved
one regex scan on a path that only runs after a 10s reload wait, so drop it.

Also evict recordedExhaustionKeys oldest-first instead of clearing wholesale,
matching the breadcrumb and renderer-error key stores the comment cites, so an
overflow cannot re-open the entire set to a repeat burst.

* test(renderer): cover the exhaustion dedupe bound

The bound had no coverage, unlike the crash-breadcrumb store it mirrors, so a
refactor could drop it or invert the comparison with every test still green.
Drive 200 distinct error names through the contained path and assert the set
stays capped. Also move MAX_RECORDED_EXHAUSTION_KEYS above the comment that
describes the set, not between them.

* test(renderer): pin the exhaustion eviction policy, not just the cap

The bound test asserted only the size cap, so it stayed green under the old
wholesale clear(): after 200 distinct keys a clear-on-overflow leaves 72, which
still satisfies the cap. Replay a key that oldest-first eviction retains and
assert it emits no second breadcrumb — that fails under clear(), which would
otherwise silently re-open the whole set to a repeat burst and flush the
30-entry ring the dedupe exists to protect.

* refactor(renderer): cut the breadcrumb machinery down to the actual fix

The lazy_chunk_recovery_exhausted breadcrumb was an optional addition that paid
for itself in complexity and nothing else: it needed a dedupe set to avoid
flushing the 30-entry ring, the set needed a bound because error.name is
library-controlled, the bound needed an oldest-first eviction policy, and that
needed two more tests plus a boolean parameter that review flagged as a
transposition trap. On the dominant never-landed path it did not even fire,
because lazy_chunk_reload_vetoed already records the same reloadKey, message and
outcome.

Drop it. Observability on every path returns to the main baseline, and the fix
is what it always was: name an exhausted recovery so the boundary can contain
it. Also revert the unrelated regex hoist -- its only caller is the failure
path, so the saved allocations are noise.

* Verify ordinary errors bypass lazy chunk containment

Add test ensuring module evaluation bugs still surface despite
never-landed reload attempts. Clarify containment scope: recovery
only applies to known dynamic-import failures, not ordinary errors.
…tablyai#12945)

* fix(terminal): per-pane WebGL attach latch and fit-anchored reattach

The attach-failure latch was module-global: one pane's failed WebGL context
creation stranded every other pane on the DOM renderer — whose cell metrics
and rasterization differ visibly (bolder, ~5% wider text) — until the next
recovery boundary. The latch is now per-pane.

A successful fit additionally offers an event-anchored reattach: a pane that
is WebGL-eligible but addon-less (late mount that missed the coalesced reveal
repaint, stale fallback) regains WebGL the moment it proves measurable, so a
user resize now heals a DOM-stuck pane instead of leaving it. Failed attaches
still retry only at recovery boundaries. A webgl-fit-attach diagnostic records
each late attach so the stuck state is finally visible in telemetry.

Client-size fit helpers move to pane-fit-client-size.ts to stay under the
pane-fit.ts line cap.

* fix(terminal): refit onto WebGL cell metrics after a fit-anchored attach

The fit that triggers the reattach measures DOM cell metrics; WebGL floors
the device cell width, so healing a DOM-stuck pane left it on the DOM-derived
column count — an unpainted right gutter and a PTY narrower than the pane.
Refit on the next frame, mirroring the dispose-side refreshDimensions.

Also cover the real wiring: the existing fit-anchored tests drive the signal
module directly, so they stay green even if safeFit stops calling it. The new
suite goes through safeFit, which is also what proves the import-time hook
registration works.

* test(terminal): gate the fit-anchored refit frame on a deferred rAF

The existing suites stub requestAnimationFrame synchronously, so the window
in which the refit handle is live never exists there — nothing covered the
two properties that window has to hold. With a deferred stub:

- disposing the pane cancels the refit, so it cannot fit (and forward a PTY
  resize for) an already-disposed terminal;
- the deferred fit re-enters the hook exactly once and settles, so there is
  no fit -> attach -> fit cycle.

Both fail against mutated production code (handle kept out of the
cancellable slot; addon-less guard dropped).
…lyai#12584)

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
stablyai#12954)

* Fix cmd+j search ranking to require coverage of meaningful query words

Extract tokenization logic into a shared module to ensure consistent
ranking across settings and project search bands. Enhance ranking with:
- Coverage requirement: candidates must match most meaningful words, not
  just one (fixes "linear triage" matching all projects on "linear" alone)
- Filler words: ignore navigation words like "open", "go", "the" when
  measuring coverage
- Unicode support: split on Unicode word boundaries, not ASCII only

* Fix cmd+j search to require query coverage and handle Unicode

The search ranking now requires all query words to match candidate values before applying shortcut rules, preventing false positives where middle words could be ignored. Query normalization now iterates over Unicode characters instead of code units to properly lowercase supplementary-plane characters.
…ng (stablyai#12822)

* fix(github-project): index fork upstream slugs for project row matching

Project cards often reference the public upstream repo while the open
clone's origin is a personal fork. Map the parent slug to the same Repo
so selected-repo filters no longer hide every board row.

Preserves origin-based getRepoSlug identity for non-project callers.

Fixes stablyai#12647

* fix(github-project): match project rows against fork upstream slugs

Resolve the referenced call to a nonexistent `resolveRepoUpstreamSlug` and
match the persisted `repo.upstream` parent instead of issuing an extra
`github.repoUpstream` RPC per repo on every index build — that lookup shells
out to `gh repo view` for non-forks, so it would have gated the Projects tab
on N network calls. `repo.upstream` is already resolved at repo-add time and
backfilled at startup, so the fix costs no IPC.

Origin matches take precedence over upstream ones so an open clone of the
upstream repo itself is never made ambiguous by someone's fork of it.

Also covers the two surfaces the origin-only match broke alongside the desktop
table: mobile's project row matcher and the store-slice row-mutation routing.

* fix(github-project): scope fork upstream matching by host and selection

Round-1 review fixes on top of the upstream-slug index:

- Apply origin-over-upstream precedence among *selected* repos instead of
  globally. An open-but-unselected clone of the upstream repo was shadowing the
  selected fork, so stablyai#12647 still reproduced for anyone holding both — and repo
  selection collapses to one repo per project key, which is exactly that case.
- Scope a fork's upstream identity key to the fork's own origin host.
  Persistence strips upstream.host, so GHES forks never matched their own rows
  and a GHES fork's parent could bind a same-named github.com row.

* fix(github-project): skip the fork alias when its own origin is unresolved

Round-2 review fix. `githubHostFromIdentityKey` cannot tell "origin resolved to
github.com" from "origin did not resolve" — both yield no host. A GHES fork
whose slug resolution had failed (auth lapse, unreachable runtime) therefore
landed in the github.com namespace, so an unrelated public Project row matched
it and Start work opened the wrong clone on the wrong server.

Require a resolved origin before indexing the upstream alias: it is the only
host evidence there is, and a repo with an unresolved origin was already absent
from the origin index, so nothing is lost that origin matching had.

* fix(repos): persist the fork upstream host instead of dropping it

`sanitizeRepoUpstream` kept only `{owner, repo}`, so a fork's parent lost the
server it lives on every time the record round-tripped through disk.

That forced the Project row matcher to re-infer the host from `origin`. The
inference is right for an API-resolved fork parent — `getRepoUpstream` stamps
`origin.host` there precisely because "a fork parent lives on the same server as
the fork". It is wrong for the other branch: a local `upstream` remote carries
its own host, so a github.com clone with a GHES `upstream` remote was indexed
into the github.com namespace, where an unrelated same-owner/name public repo
could claim it and Start work would open the wrong clone.

Keeping the host removes the guess. Absent stays absent, so records written
before this hydrate unchanged and the origin-derived fallback still covers them.
Also fixes the avatar for rehydrated GHES forks, which resolved against
github.com for the same reason.

* docs(github-project): correct upstream host fallback comment

Persistence now keeps non-empty upstream.host; originIdentityKey remains
the host fallback for older records without one (CodeRabbit nit).

* fix(github-project): own slug-index retry timer cleanup

Move the failure-retry setTimeout into its own effect so cleanup always
clears it. Scheduling from the async buildIndex then-handler failed the
react-doctor effect-needs-cleanup gate in static analysis.

* test(github-project): guard the slug-index retry timer, fix the mobile twin comment

Two follow-ups on 52298d8 and 2f89c20:

- Cover the retry timer both ways: a failed resolution still re-resolves after
  the TTL and recovers the match, and the pending timer is gone after unmount.
  The second fails if the timer moves back into the async then-handler, so the
  property is guarded by more than the lint rule.
- The mobile matcher's comment made the same stale "persistence strips
  upstream.host" claim that 2f89c20 fixed on the renderer side.

* test(github-project): unmount slug-index hooks so React cannot flush after teardown

CI shard `tests node 24 6/16` failed with 10 unhandled
`ReferenceError: window is not defined` traced to this file. The tests mounted
hooks without unmounting, so React scheduler work flushed after the DOM
environment was disposed. All assertions passed; the shard failed on the
unhandled errors alone.

`cleanup()` after each test unmounts the trees. Does not reproduce locally in
isolation — it needs CI's worker pooling and file ordering.

---------

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…#12965)

* feat(diff): HTML preview + always-visible open actions in View all

Expose Open Preview to the Side for HTML sections in combined diffs
when the working-tree file still exists, and keep the open-file
external-link icon visible without hover. Split DiffSectionItem
props/lifecycle helpers to stay under the max-lines limit.

* Fix HTML preview: always-visible buttons and multi-pane group selection

- Make preview buttons always visible (not hover-reveal) for touch support
- Fix event propagation so clicking preview doesn't toggle sections
- Use combined-diff tab's group for sourceGroupId in multi-pane layouts
- Add accessibility label to open-section button
- Support untracked, renamed, and uppercase HTML file extensions

* fix(diff): avoid render-time ref mutation in section model lifecycle

React Doctor fails static analysis when refs are written during render.
Move the disposer ref sync into an effect so the stable callback-ref
still disposes with the latest model paths.
…e-host count (stablyai#12478)

* fix(i18n): localize the status bar Resource Manager tooltip and remote-host count

The Resource Manager tooltip/aria label and the SSH segment's host count were
assembled from bare English literals inside helper functions, so they stayed
English under every non-English UI language while the labels around them
translated. Route them through the catalog with _one/_other plural keys and
whole-line messages (locales reorder and repunctuate the summary), and add
en/es/ja/ko/zh entries.

Root cause of the miss: audit-localization-coverage bailed on any ancestor
binary expression whose operator was not `+`, which hid every string under a
`cond && <JSX/>` guard or a `?? 'fallback'` — including this segment's
'Connecting…'. Only comparison operands are code, so keep `??`, `||` and
`&&` walking, and localize the four real strings that surfaced.

Co-authored-by: Orca <help@stably.ai>

* fix(status-bar): flag the space-scan tooltip row instead of matching its English text

The tooltip tinted a row with `line === 'Space scan ready'`, so routing that
copy through the catalog silently dropped the tint in every translated build.
Return `{ text, emphasized }` and let the segment read the flag.

Adopted from stablyai#12439 by @smwbev.

Co-authored-by: Evgenii <smwbev@users.noreply.github.com>

Co-authored-by: Orca <help@stably.ai>

* fix(status-bar): key Resource Manager tooltip rows by role instead of array index

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
stablyai#12962)

* fix(mobile): never auto-advertise virtual bridge addresses for pairing

Container/VM bridges stay manually pickable, but automatic defaults skip them so
QR codes do not race an unreachable direct path. Relay pairs without a local
address; LAN-only and runtime pairing fail closed on bridge-only hosts.

* fix(mobile): never auto-advertise virtual bridge addresses for pairing

- Set endpoint to null when no direct address is advertised, so the QR
  doesn't show an unreachable address to the scanning phone
- Distinguish "No address selected" (bridge exists but not advertised)
  from "No interfaces found" (genuinely nothing to pick)
- Add tests for NetworkInterfacePicker placeholder behavior
…tablyai#12462)

* fix(terminal): gate Ctrl+Enter CSI-u on a negotiated kitty pane

Ctrl+Enter emitted \x1b[13;5u unconditionally, so a pane that never
negotiated the kitty keyboard protocol (local Windows ConPTY, plain
shell) printed the escape verbatim into the prompt. Mirror the
Shift+Enter guard and fall back to the legacy CR every emulator sends
for this chord. Keeps the intercept, so IME commit ordering and the
single-send dedupe still apply.

Fixes stablyai#12329

Co-authored-by: Orca <help@stably.ai>

* test(e2e): negotiate kitty via PTY output in the Ctrl+Enter spec

The Ctrl+Enter gate reads the PTY-output kitty tracker, which
enableKittyKeyboardReporting never feeds (it writes straight into
xterm's parser), so the spec pressed the chord on a pane the policy
still saw as un-negotiated and got the CR fallback. Negotiate from the
application side like the neighbouring Shift+Enter spec, and reset the
flags afterwards for the serial suite.

Co-authored-by: Orca <help@stably.ai>

* fix(terminal): preserve trusted Ctrl+Enter routing

* fix(terminal): scope IME redispatch ownership

* fix(terminal): reject conflicting Ctrl+Enter evidence

---------

Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
…ai#12983)

Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
@innocarpe innocarpe added the bug Something isn't working label Aug 7, 2026
Pass the current-load isCurrent guard into loadRepos and only commit repo
list state when the load is still current, so a finished request for an old
host cannot overwrite the active host cache.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.