Commit Graph
189 Commits
Author SHA1 Message Date
Brennan Benson ed00ab0f34 fix(ssh): restore relay ownership after app restart (#11860) 2026-07-31 20:45:52 -07:00
Brennan Benson e698241aab fix(mobile): remember custom pairing addresses (#11741)
* fix(mobile): remember custom pairing address

* fix(mobile): stabilize custom pairing address sync

* fix(mobile): update pairing refresh refs after commit

* feat(mobile): manage saved custom pairing addresses

* fix(mobile): harden custom address selection
2026-07-31 13:03:11 -07:00
Eugenio Jesus Jose ValeirasandOrcaWin ef9e6ab9a8 fix(board): stop truncating workflows longer than 12 columns (#11605)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-30 23:58:42 -07:00
NeilandOrca 6442a9f649 fix(persistence): backfill the jira-issue workspace-card property for upgraded profiles (#11618)
Co-authored-by: Orca <help@stably.ai>
2026-07-30 17:56:58 -07:00
NeilandOrca 8e9dc4ecf2 [P2] fix(persistence): tolerate null worktreeMeta entries in linked-item normalization (#11614)
* fix(persistence): tolerate null worktreeMeta entries in linked-item normalization

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

* fix(persistence): drop null worktreeMeta entries instead of retaining them

gcStaleWorktreeMeta never reaps a timestamp-less key, so a skipped null meta
would persist forever and reach unguarded consumers (reassignSshTargetId,
push-target cleanup, disconnected-SSH listing, usage metadata).

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

* fix(persistence): drop every corrupt worktreeMeta shape with its lineage rows

Widen the guard past null so scalars/arrays can't survive load into a
Record<string, WorktreeMeta> that consumers trust, self-heal a whole-map
"worktreeMeta": null instead of leaning on gcStaleWorktreeMeta running next,
and clear the two lineage companions the way gcStaleWorktreeMeta and
removeWorktreeMeta already do.

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

* fix(persistence): persist whole-map metadata repairs

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-30 17:55:54 -07:00
Brennan Benson c67791e4c1 fix(setup-prompt): isolate state by execution host (#11447)
Prevent setup prompt inspection, caching, dismissal, saves, telemetry, and settings navigation from leaking across local, direct SSH, and runtime-relayed hosts.
2026-07-29 19:56:19 -07:00
Jinjing 74563b6498 feat(jira): link Jira issues from the workspace create dialog (#11296)
* Link Jira issues from workspace create dialog

Add Jira issue linking to workspace creation, matching existing GitHub and Linear workflows. Users can paste Jira issue URLs in the smart name field to auto-populate workspace names and link the issue to the created workspace/worktree.

Linked Jira issues appear on workspace cards via the new 'jira-issue' card property. Implements cancellable searches and summary reads to prevent stalled requests from blocking the shared Jira pool. Persists paired issue + source context metadata with validation of provider/site identity.

Fixes git-username rate-limit handling to reject malformed JSON responses so garbage never becomes branch prefixes.

* feat(jira): link issues during workspace creation

- Display linked Jira issues on worktree cards
- Fetch issue summaries and timestamps via Jira API
- Gate Jira linking behind runtime capability check
- Preserve user-typed names during async lookups

* Enforce git check-ref-format rules in login validation

Extend isBranchSafeHostedLogin to reject usernames that git rejects as
invalid branch components: trailing dots, consecutive dots, and .lock
suffix. Prevents invalid branch names from login usernames.

* Enforce filesystem filename cap for branch-safe logins

Loose refs store logins as single filenames, so the real constraint is the
255-byte filesystem cap, not git check-ref-format rules. This allows longer
provider-agnostic logins while staying platform-safe.
2026-07-29 19:50:18 -07:00
JinjingandOrcaWin 5f7807497e feat(ssh): bound relay PTY output end to end (#11005)
* docs: design SSH relay PTY backpressure

* fix(ssh): bound relay frame decoding

* fix(relay): bound PTY output publication

* fix(ssh): bound PTY model admission

* fix(ssh): settle closed model admissions

* feat(ssh): negotiate bounded PTY consumer sessions

* fix(ssh): fence exit on renderer settlement

* feat(ssh): track PTY source credit end to end

* fix(ssh): recover bounded PTY output across reconnect

* feat(ssh): complete relay PTY output backpressure

* fix(ssh): close final PTY source credit races

* docs(ssh): record final backpressure validation

* feat(ssh): complete relay PTY source-credit lifecycle

* test(ssh): complete provider notification fixture

* fix(ssh): preserve terminal source credit across rotation

* fix(ssh): fail closed on recovery cancellation

* fix(ssh): prioritize mux control writes after drain

* fix(ssh): retire canceled relay restore deliveries

* fix(ssh): order exit cancellation cleanup

* fix(ssh): gate provisional source activation

* test(ssh): register mux drain-priority coverage

* fix(ssh): type stale owner recovery mismatches

* fix(ssh): close projection replacement races

* fix(relay): contain streaming edge failures

* fix(ssh): secure relay endpoint credentials

* docs(ssh): reconcile final backpressure lifecycle

* fix(ssh): bound main IPC output lifecycle

* fix(ssh): close recovery ownership gaps

* docs(ssh): record exact artifact validation

* fix(ssh): reject reclaimed snapshot replacements

* fix(ssh): fence model admission across reconnect

* fix(ssh): contain migration failure per PTY

* docs(ssh): record final exact-head validation

* test(ssh): align deploy fixtures with credential publication

* feat(ssh): add per-target bounded output setting

* fix(ssh): close source recovery review gaps

* fix(ssh): latch source credit environment override

* feat(ssh): make PTY source credit the default

* docs(ssh): record always-on relay validation

* docs(ssh): bind validation to current main

* test(ssh): grant source credit in IPC fixture

* test(ssh): grant source credit in fake relay

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 17:03:15 -07:00
OrcaWinandOrcaWin 363e478909 fix(orchestration): preserve active workers across updates (#11271)
* fix(orchestration): preserve active workers across updates

* test(ssh): model absent legacy adoption

* test(orchestration): align compatibility contracts

* fix(windows): escape updater PowerShell booleans

* fix(windows): restore stock uninstall process check

* fix(orchestration): keep recovery off renderer startup barrier

* fix(orchestration): harden legacy recovery migration

* fix(orchestration): close recovery review gaps

* fix(orchestration): complete legacy worker cutover recovery

* fix(orchestration): preserve legacy workers across updates

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 11:31:35 -07:00
NeilandOrca 4326707285 fix(persistence): unbreak main by expecting the new 'cli' card property in fresh defaults (#10722)
Co-authored-by: Orca <help@stably.ai>
2026-07-26 01:39:49 -07:00
9042ef9792 fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default (#10588)
* fix(terminal): make Zellij/TUI OSC 52 clipboard copy work by default

Zellij and other multiplexers copy via OSC 52. Empty Pc is a valid XTerm
default for clipboard, but we rejected it, and the feature defaulted off so
copy silently failed inside Zellij. Accept empty Pc as clipboard, default the
setting on (query still blocked; size capped), and surface Zellij in settings.

Closes #10567

* fix(review): make the OSC 52 default actually reach existing installs

Review fixes for #10588:

- Persistence: profiles saved under the old off default persisted `false`,
  which is indistinguishable from a real opt-out, so the default flip never
  reached #10567's reporter. Added the repo's one-shot stamp
  (terminalAllowOsc52ClipboardDefaultedOnForAllUsers) so unmigrated profiles
  flip once and a later opt-out sticks.
- Replay: reattach/cold-restore re-writes recorded PTY bytes through the same
  parser, so a stale `\e]52;c;...` silently clobbered the clipboard on every
  restart. Gated behind isPaneReplaying via a new resolveOsc52ClipboardGate.
- Blocked toast latches once per renderer session and could be burned by a
  pre-hydration read; it now fires only for a real opt-out.
- An empty Pd decoded to '' and, with the gate default-on, silently blanked
  the clipboard. Now rejected as invalid.
- Localization: en.json is bundled and the catalog beats the code fallback,
  so all three copy changes were inert. Resynced across five locales.
- Corrected the empty-Pc rationale: tmux (not Zellij) emits `\e]52;;<b64>`.

* test(terminal): cover the OSC 52 gate wiring and settings copy

Extracts createOsc52OscHandler so the replay/hydration gate wiring is
covered, not just the pure gate — dropping the isReplaying getter now
fails a test instead of passing silently.

Adds catalog assertions for the two OSC 52 settings strings. Only the
toast key was pinned, so the same inert-copy regression (code fallback
edited, bundled en.json not) could still ship for the settings pane.

* docs(settings): note that the OSC 52 default only covers new profiles

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

* fix(terminal): migrate the web settings store to the OSC 52 default-on flip

The default-on flip only reached the Electron store. The web/remote client
keeps its own settings in localStorage, so a profile that persisted the old
`false` there stayed opted out — the same bug the Electron migration fixed,
in the second store.

Extract the migration into shared/osc52-clipboard-settings.ts and call it
from both stores. Also coalesce OSC 52 writes onto a microtask so a hostile
chunk of ~15-byte sequences cannot fan out into a million clipboard writes,
and latch the blocked-write toast after it renders rather than before.

* feat(terminal): tell users when the OSC 52 flip overrides their opt-out

The default-on migration cannot distinguish a deliberate opt-out from a
profile that simply never touched the setting — both persisted `false` under
the old default. Flipping everyone is the only way to fix #10567 for existing
installs, but doing it silently reverses a security choice the user made.

Arm a one-shot notice at load when the migration overrides a persisted
`false`, on both settings stores, and show it once the renderer hydrates.
Profiles that never opted out are never notified.

* fix(terminal): clear the OSC 52 notice after it renders, not before

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

* fix(terminal): keep the web OSC 52 notice armed against an unmigrated host

The host store always projects osc52ClipboardDefaultOnNoticePending, so the
plain spread in the web client's runtime UI merge overwrote an arm raised by
its own localStorage settings migration — flipping the opt-out in silence.

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

* fix(terminal): stop the OSC 52 notice overclaiming, and cover it

Round-3 review fixes:
- Rename the arming predicate to osc52ClipboardDefaultOnOverridesPersistedOff.
  Both stores rewrite the whole settings object on every save, so every profile
  saved under the old off default holds `false` — the deliberate-opt-out cohort
  is not distinguishable on disk. Name, docs and test names now say so.
- Read settings before the UI snapshot in readLocalWebUIState: getStoredSettings()
  arms the notice, so reading first snapshotted a pre-arm state that callers wrote
  back, erasing an arm the stamp can never raise again.
- Give the notice toast a stable id; StrictMode re-runs the effect against the
  same closure, so the early return cannot catch the second pass.
- Restore guardParserHandler parity in the coalescer microtask.
- Drop the unverified Zellij claim justifying all-selections routing; that routing
  predates this branch and PRIMARY routing stays an open question.
- Cover the notice hook (order, single-fire, deep-link), the armed flag reaching
  disk and surviving a clear, and pin the notice catalog to its code fallbacks.

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

* test(terminal): pin OSC 52 setting discovery by product name

The migration notice says to turn it off in Terminal settings, so searching
Zellij/Grok/tmux has to find it. Also note why the OSC 52 write-back clauses
stay despite an unrelated always-true clause in the same condition.

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

* fix(terminal): consume the OSC 52 notice on close, and cover the guards it relies on

The notice was cleared the moment the toast was enqueued, so a quit inside its
15s window spent the profile's only warning on a launch where nothing was ever
seen — and the settings stamp means it can never re-arm. Clear on
onAutoClose/onDismiss instead, plus explicitly in the action handler, because
sonner's action path deletes the toast without firing onDismiss.

Also closes three coverage gaps a review found:
- ui.set must accept osc52ClipboardDefaultOnNoticePending. The update schema is
  strict, so dropping the key rejects the whole call rather than stripping it,
  and the renderer only logs that failure — every paired client would re-toast
  forever with nothing red.
- the coalescer's try/catch and .catch had no test; the rejection case needs a
  plain function because vi.fn tracks settled results and hides the leak.
- pin that every selection kind (including bare `p`) lands in the system
  clipboard, so routing PRIMARY separately later is a deliberate break.

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

* test(web): pin that ui.get arms the OSC 52 notice when it runs the migration

readLocalWebUIState reads settings before the UI blob so the migration's arm is
in place before the snapshot every caller writes back. Seeding localStorage
after install is what makes ui.get the first settings read, and therefore what
makes swapping those two lines fail.

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

* test(store): cover the OSC 52 notice clear and its hydration

The clear sets local state before persisting so a rejected ui.set cannot leave
the toast re-firing for the rest of the session; losing the persist only re-arms
the notice next launch.

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

* docs(terminal): state the real residual risk of default-on OSC 52

Three comment corrections from review:
- the safety note claimed exfil was the risk; queries are blocked, so it isn't.
  The actual accepted risk is execute-on-paste: decoded text goes to the
  clipboard verbatim, newlines included. Filtering here would break multi-line
  TUI copies, which is the feature; bracketed paste is where that is handled,
  and kitty/Ghostty take the same posture.
- the coalescer bounds a flood per parse yield, not overall.
- the replay gate reads at parse time while queued live bytes are drained
  before the guard engages, so a copy racing a reattach is dropped silently.

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

* test(terminal): close the four OSC 52 gaps a full revert walked through

Mutation testing found four assertions that stayed green against the very
change they were written to pin.

The notice suite passed 8/9 against a complete revert to clear-at-enqueue:
`calls[0][1][callback]?.()` is a silent no-op when the option is absent, and
the call count was already satisfied by the enqueue-clear, so nothing
separated "cleared by this callback" from "cleared earlier". Assert the
option exists and the notice is unspent before invoking it.

The stable toast id was deletable with all 9 green despite the adjacent
comment calling it load-bearing for StrictMode. Pin it.

The blocked toast's latch-after-throw fix was unproven: both orderings pass
when `toast.info` succeeds. Only a throwing first call tells them apart.

Deleting the hook call in App.tsx silenced the desktop notice with every
suite green. Pin it alongside the static Toaster import, since sonner drops
a toast enqueued before any Toaster subscribes and never replays it.

Also retone the coalescer-latch comment, which claimed the reset ordering
was load-bearing on its own; the try/catch reaches the same end, so the
test binds the pair.

All four verified green->red by mutation, then restored.

* test(terminal): cover the OSC 52 notice and its guards

Add tests pinning the static Toaster mount required to prevent notice dropout (#10567), the stable toast ID deduping StrictMode double-invokes, that the notice stays unspent on toast throws, and that flush-latch guards prevent silent consumption across error boundaries.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-25 19:43:29 -07:00
Brennan Benson baf25785e0 fix(tasks): keep repos with a pending remote-identity probe in the picker (#10527)
* fix(tasks): keep repos with a pending remote-identity probe in the picker

Task-repo eligibility filtered on `hasProjectRemoteIdentity`, which is
populated by a background `git remote -v` probe. When the probe could not
reach git — an SSH-hosted repo whose connection is not up yet, a cold
launch — the repo silently vanished from the Tasks picker and stayed
hidden for the full 5-minute negative-cache TTL, even after the host came
back. GitHub repos were largely shielded because a persisted `upstream`
satisfies the identity projection through a different route; GitLab and
other providers depend on the probe.

Distinguish unknown from settled instead of hiding both:

- `probeGitRemoteIdentity` reports `resolved` / `no-remote` (git answered,
  no usable remote) / `unavailable` (never reached git).
- Enrichment persists `gitRemoteIdentity: null` only on `no-remote`,
  mirroring the existing `upstream: null` "not a fork" marker. An
  unreachable host leaves the identity undefined.
- Persistence keeps the explicit `null` instead of dropping it.
- `getTaskEligibleRepos` keeps a repo whose identity is still pending;
  folders and settled remote-less repos stay filtered out.

* test(tasks): cover the SSH probe exec paths for remote-identity status

Addresses CodeRabbit review: the unavailable-on-error case only exercised
the local git runner. Adds a connected-provider whose exec rejects, and an
SSH repo git answered for with no remotes.

* test(tasks): pin that a settled no-remote repo still resolves once it gains a remote

Three independent reviewers flagged that the candidate filter's `!repo.gitRemoteIdentity`
looks like an oversight next to the new null marker. Tightening it to `=== undefined`
would silently stop detecting a remote added after the marker landed. Document that the
re-probe is deliberate and pin the behavior with a test.
2026-07-25 04:04:19 -07:00
moseohandBrennan Benson 1aaf049a4d fix(rate-limits): keep Codex PTY reset text for weekly-only plans (#8643)
* fix(rate-limits): keep Codex PTY reset text for weekly-only plans

The PTY /status fallback parses '5h limit' and 'Weekly limit' lines by
label, but the extracted reset text was only ever attached to the
session window. Codex plans without a 5h session bucket (e.g. current
Pro) produce a weekly-only parse, so the reset time the CLI printed was
silently dropped. Fall back to the weekly window when no session window
exists.

* review: parse Codex PTY reset text per window into resetsAt

* review: make Codex PTY status fallback work on codex >=0.145

* review: harden PTY status parse against model-scoped rows and styled output

* fix(rate-limits): strip private PTY control sequences

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-24 16:26:55 -07:00
kazu-42andBrennan Benson 4a71a0ecb2 feat(mobile): add safe Codex rate-limit resets (#9394)
* feat(mobile): add safe Codex rate-limit resets

* fix(mobile): address reset credit review feedback

* review: purge removed-account reset attempts, shared capability constant, rebase test mocks

* review: preserve host compatibility and reset durability

* fix(mobile): recover reset capability after cutover

* fix(mobile): validate runtime capability payloads

* fix(mobile): enforce capability payload contract

* fix(mobile): route mock terminals to selected worktree

* test(mobile): pin malformed probe retry behavior

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-24 14:42:04 -07:00
253ccd29f5 fix: prune workspace session state when a project is removed (#9024) (#9025)
* fix: prune workspace session state when a project is removed

removeProject → pruneWorktreeStateForRepo cleared worktreeMeta, lineage,
and workspace lineage, but left the removed repo's worktrees behind in
workspaceSession (lastVisitedAtByWorktreeId, sleepingAgentSessionsByPaneKey,
tab state, …) and in every workspaceSessionsByHostId partition.

Those dangling references caused the runtime to treat the deleted repo's
worktrees as recently-active on the next launch and re-materialize their
worktreeMeta. With no owning project left, the UI rendered them as an
orphaned "unknown" workspace that returned after every restart.

Reuse the existing removeWorkspaceSessionOwner cleanup (already used by
removeFolderWorkspace and deleteProjectGroup) against the legacy session
blob and each per-host partition, collecting owner keys before the
worktreeMeta delete loop so host classification still works.

Fixes #9024

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: scope session cleanup to the removed host in removeProjectForHost

The workspace-session cleanup applied every collected owner key to the legacy
(local) session blob AND every workspaceSessionsByHostId partition. Owner keys
are `${repoId}::${path}` and carry no host, so a host-scoped removal
(removeProjectForHost) of one host wrongly wiped a surviving host's session for
a shared repo id/path — discarding its lastVisitedAt, tabs, sleeping-agent
state, and active-worktree pointer.

Scope the session prune to the removed host: prune the legacy blob only when the
target host is local (or on a full removeProject where hostId is null), and
prune only the matching workspaceSessionsByHostId partition for a non-local
host; a full removal still clears every partition. Collect session owner keys by
`${repoId}::` prefix (not belongsToHost, which is worktreeMeta-host-classified)
so a partition-only key is still prunable.

Adds regression tests for a shared repo id + path across local and an SSH host,
covering removeProjectForHost on the SSH host (local session survives) and on
the local host (SSH session survives).

Addresses review feedback on #9024.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cover third surviving host in removeProjectForHost session prune

Add a regression case where a shared repo id + path exists on local and two
SSH hosts. Removing one non-local host must prune only that host's session
partition, leaving both the legacy/local session and the other surviving SSH
host's partition intact. Locks in that the host-scoped cleanup never touches a
non-targeted third partition (per review feedback on #9024).

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

* perf: prune workspace session in a single clone+scan per partition

Project removal collected every owner key to prune, then called
removeWorkspaceSessionOwner once per owner AND per host partition. Each
call structuredClones and scans the entire session, so removal was
O(worktrees x hosts x session size) in full-session clones on the main
thread.

Add removeWorkspaceSessionOwners(session, ownerKeys) which clones once
and scans each collection once for the whole set, and route
pruneWorktreeStateForRepo through it. The per-owner O(1) field deletes
and the single-scan pane-key passes are factored into shared functions
so removeWorkspaceSessionOwner keeps identical behavior for its other
callers. No behavior change.

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

---------

Co-authored-by: PannenetsF <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-24 00:25:26 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
OrcaWin 41751dd90d fix(runtime): route HUB-owned SSH worktrees through owning runtime (#9994) 2026-07-22 18:25:05 -07:00
Brennan Benson 24b8fcc918 fix(mobile): restore the last-open tab when returning to a worktree on mobile (#9801)
* fix(mobile): persist per-device tab selection so worktree return restores the last open tab

A phone's tab selection lived only in the host's in-memory
ClientSessionTabSelectionStore. Any host restart wiped it, and the
per-device projection then fell back to deterministic topology, so
returning to a worktree on mobile always landed on the first tab
instead of the tab last opened on the phone.

Persist the per-device selections in the Store (keyed deviceId ->
worktreeId), hydrate them when the runtime constructs, and guard
projection so an early empty snapshot after restart cannot wipe a
hydrated selection before tabs arrive. Selections are pruned with the
worktree/repo and on device revoke, and malformed persisted payloads
degrade to empty instead of throwing.

* fix(mobile): harden persisted tab selection cleanup

* fix(mobile): preserve tab selection across worktree rename
2026-07-22 13:12:38 -07:00
OrcaWin b232df732b fix(terminal): make remote agent sessions host-authoritative (#9687) 2026-07-21 20:51:28 -07:00
OrcaWinandBrennan Benson 6444be3a01 fix(accounts): follow the runtime for WSL provider account detection (#9537) (#9611)
* fix(accounts): follow the runtime for WSL provider account detection (#9537)

On Windows + WSL, provider-account detection (usage recognition and the
status-bar account switcher) was pinned to the Windows host even when the
project runs in WSL, so WSL accounts were never recognized and the WSL
switcher group never appeared.

Root cause: `localAccountRuntime` hard-defaulted to 'host', which
short-circuited `getInitialClaude/CodexRateLimitTarget` before the existing
"follow the global Windows runtime default" branch could run. That branch was
therefore dead for every real user.

Fix: add an 'auto' value for `localAccountRuntime`, make it the default, and
migrate the untouched legacy 'host' default to 'auto' once (guarded by
`localAccountRuntimeDefaultedToAutoForAllUsers`; explicit 'wsl' is preserved).
'auto' resolves via a shared `resolveLocalAccountRuntimeTarget` helper: on a
windows-host default it stays host (no behavior change); on a WSL default it
follows WSL, so WSL accounts are recognized and the WSL group appears.

Wired the shared resolver into the managed-account default target, the
status-bar WSL-group gate, and the Accounts settings location toggle.

Note: detection follows the global Windows runtime default, not the live
active project's runtime (the fetch target is a single global value); the
latter is a larger follow-up.

* fix(accounts): align auto runtime consumers

* fix(accounts): keep runtime polling aligned with settings

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-20 18:35:14 -07:00
NeilandOrca 98b00d3a64 refactor(comments): slim verbose comments in main core (runtime/ipc/daemon/pty) (#9542)
Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.

Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.

Area: main — core runtime, ipc, daemon, pty, providers. 73 files changed, 3206 insertions(+), 10475 deletions(-).

Co-authored-by: Orca <help@stably.ai>
2026-07-20 03:18:24 -07:00
NeilandOrca 2783f758df perf(persistence): compact JSON for durable-state save payload (drop pretty-print) (#9290)
Co-authored-by: Orca <help@stably.ai>
2026-07-17 23:56:05 -07:00
Brennan Benson 5907816457 feat(keybindings): swap tab-switch chords to the common convention for new users (#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
2026-07-17 16:58:04 -07:00
Brennan Benson ce910e5d52 fix(naming): keep the built-in branch-name prompt general (#9088)
* fix(naming): keep the built-in branch-name prompt general

The shipped auto-rename prompt no longer hard-codes style rules (word
count, kebab-case, no prefixes). Users can already override naming via
Source Control AI instructions and the branch-name command template; a
prescriptive default fought those overrides. Git-safe sanitization still
runs after generation.

* fix(naming): preserve branch prompt overrides

* chore(skills): refresh rc.2 manifest version
2026-07-16 18:33:34 -07:00
Jinjing 5b75a87dd3 feat(native-chat): add per-model session option pickers with verified Claude switch (#9085)
* Add per-model session-option pickers (model/effort/fast-mode) to native

Introduces a shared agent session-option catalog (Claude/Codex/Gemini/Cursor)
with model-scoped options, launch-command composition, mid-session dispatch
via slash commands, and per-model persisted defaults. Wires the new
NativeChatSessionOptionPickers UI into the composer, threads sessionOptions
through every startup-plan builder (worktree creation, onboarding, source
control actions, folder workspace, direct work-item launches), and adds
localized strings and tests across the affected surfaces.

* Add per-model session-option pickers with a verified Claude model switch

Native chat's model/effort picker now dispatches option commands through a
body-then-verified-Enter write path and, for Claude, arms a PTY observer that
classifies the cached-history confirmation prompt as applied/rejected/needs
interaction before the picker returns — falling back to the terminal only
when Claude genuinely requires manual input, and clearing stale truth
otherwise. Also reorders the composer's model/effort pills, disambiguates
their tooltip labels, and switches disabled-reason strings to a closed enum
so producer and localized copy can't drift.

* Add live model/effort detection from Claude's TUI header

- Reads the mounted xterm's main-buffer snapshot (falling back to the
  rendered screen when the alternate screen owns the buffer) to parse
  Claude's header for the currently active model and effort, so the
  native chat picker reflects reality instead of only dispatched state
- Extracts command-apply recording and reported-value application into
  dedicated modules, and factors file-link click handling into a hook,
  to keep native-chat-pty-session-options.ts focused
2026-07-16 17:12:44 -07:00
Jinjing 1536171fdb Add a native macOS menu bar status item with activity indicator (#9042)
* Add native macOS menu bar status item with settings toggle

Extend the Windows-only system tray into a shared status-item module
so macOS gets a template menu bar icon (Open/Settings/Check for
Updates/Quit), a theme-aware attention dot, and a "Show Menu Bar
Icon" appearance setting. Also fixes a startup race where a tray
"Settings…" click could fire before the renderer's ui:openSettings
listener attached, by queuing a one-shot pending-open-settings intent
the renderer consumes on mount.

* Fix Retina blur, race conditions, and menu-label duplication in tray Set

- Rebuild the tray attention icon's @2x representation since toBitmap only
  read 1x pixels, blurring the glyph on Retina displays
- Fix premultiplied-alpha math so light-glyph tinting uses per-pixel alpha
  instead of a flat 0xff, keeping antialiased edges valid
- Always push ui:openSettings and leave a longer-lived pending flag, since
  there was no reliable signal that a renderer's listener was attached
- Preserve tray attention state across macOS menu-bar hide/show toggles
  instead of resetting it on tray destroy
- Route macOS tray creation through syncMacMenuBarIcon so startup and the
  live toggle share one visibility policy
- Reuse app-menu translation keys for tray Settings/Check for Updates and
  drop the now-duplicate tray-scoped locale strings

* Make menu bar icon default on and platform-independent in settings

Previously showMenuBarIcon was hardcoded to darwin-only in both the
default settings and the sanitize/load paths, so a profile written on
macOS lost its opt-out when touched from another OS. Store the raw
boolean everywhere and let darwin-specific consumers decide whether
to act on it, so the value round-trips unchanged across platforms.

* Fix Settings menu click being silently dropped after a slow cold rendere

Replace the 60s pendingOpenSettings TTL with an untimed intent, since a
cold renderer start can outrun any fixed timeout and cause the flag to
expire before the click is consumed. webContents-id scoping plus
consume-on-read still prevent the intent from leaking to an unrelated
renderer. Adds a test covering the queued-before-mount pull path.
2026-07-16 13:06:21 -07:00
Jinwoo HongandOrca 319ae4e9ea fix(terminal): make whole-tab close durable (#8958)
Co-authored-by: Orca <help@stably.ai>
2026-07-15 22:09:37 -07:00
Jinwoo HongandOrca a2d3451efd Fix manual project order across paired runtime hosts (#8894)
* fix(sidebar): persist manual project order across hosts

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

* fix(sidebar): scope desktop repo reorder by host

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

* fix(web): cover host-scoped repo reorder API

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-15 13:32:13 -07:00
Brennan Benson 36cd8a3347 fix(terminal): retire sessions when tabs close (#8628)
* fix(terminal): retire sessions when tabs close

* fix(terminal): close remaining session lifecycle gaps

* fix(terminal): close review-discovered lifecycle gaps

* fix(terminal): revalidate bulk session retirement

* fix(terminal): harden retirement review edges

* fix(agent): reverify restored pane authority

* test: make terminal retirement gate portable on Windows

* test: use POSIX join in Linux PATH assertion

* test: keep simulated Linux PATH host-consistent
2026-07-14 00:15:34 -07:00
7fe6742b8b fix(pr-comments): let users mark comment authors as bots for the Humans/Bots filter (#7598)
* fix(pr-comments): let users mark comment authors as bots for the Humans/Bots filter

Some review bots post from regular user accounts that defeat both provider
bot metadata and login heuristics, so their comments were misclassified as
human. Adds a persisted prBotAuthorOverrides setting with a "Mark author as
bot" comment action, applied consistently across desktop and mobile.

Fixes #7597

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(pr-comments): address review feedback on bot-author overrides

- Cap sanitized prBotAuthorOverrides at 500 entries so malformed payloads
  can't bloat GlobalSettings or slow comment classification
- Reuse the shared normalizePRCommentAuthorLogin in isBotPRComment on
  desktop and mobile instead of duplicating the normalization inline
- Pass botAuthorOverrides from CommentRow to CommentMoreMenu instead of
  re-subscribing per menu instance
- Re-fetch mobile bot-author overrides alongside each PR refetch so they
  don't stay a stale one-shot snapshot for the whole session

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(pr-comments): harden bot author override sync

* fix(pr-comments): bound and recover override updates

* fix(pr-comments): merge overrides from canonical settings

* fix(pr-comments): make bot override updates atomic

* fix(pr-comments): surface rejected bot overrides

* fix(i18n): translate bot override warning

* fix(i18n): translate bot author actions

---------

Co-authored-by: Dzmitry Bachko <dbachko@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
2026-07-12 14:09:15 -07:00
Neil 8a4ec4e856 feat(terminal): expose right-click paste on every platform (#8322)
* WIP: Changes before auto-review fixes

* fix: preserve terminal paste defaults across platforms
2026-07-11 15:02:46 -07:00
Jinjing 6be8687c34 feat(status-bar): notify upgraded users usage meters show % used (#8319)
* feat(status-bar): notify upgraded users usage meters show % used

Show a one-time status-bar callout when upgraded profiles still use the
new percent-used default. Brand-new profiles and users who already chose
remaining stay quiet; dismissing or changing the setting is permanent.

* Add settings deep-link to expand Appearance's Window accordion for one-s

- Replaces the searchQuery-based redirect (fragile, flashed filter UI) with a
  dedicated appearanceAccordionDeepLink store field that force-opens the
  correct accordion and scrolls to the target row
- Rebuilds the status-bar change notice as a plain elevated card instead of
  a Popover, since PopoverContent's glass/backdrop-filter defaults fought
  the opaque callout styling and needed heavy overrides
- Simplifies the light/dark card CSS tokens accordingly

* Reposition status-bar usage-change notice via fixed-position portal

Portal the one-shot callout to document.body with fixed positioning
anchored via getBoundingClientRect, since the status-bar's overflow-hidden
flex ancestors clipped or mispositioned the previous in-tree absolute
layout.

* Refine status-bar usage notice styling and test coverage

- Replace hand-tuned light/dark card colors with existing design
  tokens (--popover, --border) and the documented floating elevation,
  so the callout stays in sync with the design system instead of
  duplicating its own palette
- Add tests covering dismiss via X button, "Got it", and Escape
- Scope the foreground-process confirm assertion to the pane's ptyId
  so an unrelated pane's delayed confirm can't cause a false failure
2026-07-11 14:49:49 -07:00
Jinwoo HongandOrca 25ecf2eea2 fix: reconcile SSH repo rows after host re-add (#8201)
Co-authored-by: Orca <help@stably.ai>
2026-07-11 03:20:55 -07:00
2cfb028163 fix: Cap automationRuns retention to stop unbounded state file growth (#8119)
* Cap automationRuns retention to stop unbounded state file growth

`automationRuns` was the only unbounded durable collection in
`orca-data.json`, and the whole blob is re-serialized and rewritten on
every save. On a machine running four `* * * * *` automations it had
grown to 11,184 rows / 21 MB of a 28.5 MB file — all of them
`skipped_precheck` no-ops — so each synchronous `flush()` blocked the
Electron main thread for 190-210 ms and macOS filed 24 `disk writes`
diagnostic reports against Orca (12-30 MB/s sustained, 549 GB/session).

Prune to the newest 100 runs per automation, on load and on append. This
mirrors the retention that `pruneLocalTerminalScrollbackBuffers` and
`pruneWorkspaceSessionBrowserHistory` already apply to their
collections; `automationRuns` was simply missed.

The load-path prune marks state dirty, so an oversized file heals on
first load. Without that flag the shrink lives only in memory: the sole
load-time save trigger is `normalized.changed || loadNeedsSave ||
adaptedProjectGroups`, and `normalized` covers pane identity only. A
user who took the documented workaround (`automations edit --disabled`)
fires no runs, so nothing would ever rewrite the file.

Measured against the affected 28.5 MB file: flush() 190 ms -> 9 ms,
bytes written per save 28.5 MB -> 1.3 MB.

Pruning breaks the old `runNumber` derivation, which counted retained
runs, so every run after the cap would have been titled "run 101".
Carry the ordinal on the run itself and derive the next number from the
highest survivor. Legacy rows are numbered from the highest number their
automation already carries, not from their append position: a downgrade
to a pre-`runNumber` build appends unnumbered runs after pruned
survivors numbered 101+, and a position would reissue one of those,
giving two runs the same title.

Fixes #8118

* Never evict in-flight automation runs from retention

A dispatched run's completion can land hours later (renderer round-trip or
headless completion promise); pruning it makes updateAutomationRun throw
'Automation run not found.'. Only final-status runs are evictable now, with
the final-status predicate shared between retention and the service.

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

* Skip the usage write when retention evicted the run mid-collection

markDispatchResult finalizes a run, awaits usage collection, then writes
usage by id. The run is final during that await, so a concurrent
create-time prune can evict it and the write threw 'Automation run not
found.' — in the headless path that cascaded into an unhandled rejection.

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

* Pin backfill-before-prune ordering with true legacy fixture rows

The heal-on-load fixture rows carried runNumber, so swapping backfill and
prune passed every test while renumbering real legacy survivors 1..100 and
re-minting colliding titles. Seed unnumbered rows and assert healed ordinals.

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

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-10 20:02:45 -07:00
Neil e4c7aab4a2 Normalize invalid terminal line height before xterm (#8050) 2026-07-10 01:28:22 -07:00
Jinjing ad43dd19ed Disable experimental new worktree card style by default (#7977) 2026-07-09 14:52:03 -07:00
NeilandOrca 06afbc4a4b Add optional Orca account sign-in (#7515)
* auth v1

* fable review

* lint

* Account menu with org membership management

Default UX is a compact account menu (sign in, organization selection, sign
out) that renders only when cloud auth is configured; adds an organization
members dialog (invite, role, remove) gated on server-side role checks. The
multi-profile switcher UI is preserved behind ORCA_MULTI_PROFILE_UI=1.

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

* Gate the optional account sign-in UI to dev builds

The account switcher stays hidden in packaged builds while the feature is
in progress. Dev builds still show it when the client env vars are set, and
a dev-only Settings > Dev Tools > Orca Cloud section mirrors it.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-09 02:13:13 -07:00
Jinwoo HongandOrca 864594ffdd Fix SSH state for paired remote clients + complete target re-adoption sweep (STA-1468) (#7767)
* Sweep all persisted carriers of a removed SSH target id on re-adoption

reassignSshTargetId re-pointed repos and worktree metas but left the old
target id embedded in persisted session pty ids (ssh:<id>@@pty-N in tabs,
layouts, remoteSessionIdsByTabId), the startup reconnect list
(activeConnectionIdsAtShutdown, replayed via ssh.connect at boot — the
exact 'SSH target not found' in STA-1468), sleeping-agent resume records,
provisioned project host setups, sidebar host-scope arrays, and relay pty
leases. Any survivor resurfaces later as a failing connect or reattach.

New ssh-target-id-migration module re-points every carrier in one pass,
wired into reassignSshTargetId with per-carrier unit and store-level
round-trip tests.

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

* Bridge SSH connection state to paired remote clients

The SSH surface was desktop-only: ssh:state-changed went to the host's
own BrowserWindow and the web client's ssh API was a no-op stub, so a
paired client's reconnect overlay never learned the host connected and
its target labels stayed empty (STA-1468 — overlay stuck on 'please
connect' over a live terminal).

- New sshStateChanged runtime client event, emitted from broadcastSshState
  through OrcaRuntimeService onto the existing clientEvents stream.
- New ssh.listTargets / ssh.listRemovedTargetLabels RPC methods next to
  the previously unused ssh.getState / ssh.connect.
- Web preload now routes listTargets / listRemovedTargetLabels / getState
  / connect to the paired host's runtime RPC instead of stubbing them.
- useIpcEvents applies sshStateChanged on paired web clients through the
  same guarded path as desktop ssh.onStateChanged; desktop clients ignore
  the event since a foreign runtime's targets would pollute their local
  SSH store.

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

* Harden the SSH reconnect overlay against stale or unknown target state

- Only present the destructive 'SSH host removed' state on positive
  evidence (a removal tombstone label, or a hydrated non-empty target
  list lacking the id). A client whose SSH state never hydrated has an
  empty labels map for every id and must not offer workspace removal.
- After a failed Connect, resync target metadata so a stale overlay
  converges to the ghost/re-adopted state instead of offering the same
  failing Connect forever (the repeated 'SSH target not found' toast
  loop in STA-1468).

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

* Address CodeRabbit review on #7767

- Re-key workspaceSessionsByHostId partitions stored under a removed SSH
  host id during re-adoption (no writer keys partitions by ssh host today,
  but the schema tolerates it — re-key instead of stranding; live partition
  wins when both keys exist).
- Track SSH target-list hydration explicitly (sshTargetsHydrated) instead
  of inferring it from a non-empty label map, so a legitimately empty
  target list still counts as removal evidence and a never-hydrated client
  still never offers destructive removal.
- Apply the refreshed target list before the best-effort removed-labels
  fetch in the overlay resync, so a labels failure can't discard it.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-08 11:36:19 -07:00
2ce77d9098 fix(runtime): keep same-path imports host-qualified (#7395)
* fix(runtime): keep same-path imports host-qualified (#7018)

* review: harden runtime repo host match against SSH-repo hijack

An unstamped repo with a connectionId is an SSH repo (resolves to
ssh:<id>), so a same-path runtime import must not adopt it into a
runtime/local host. Match/adopt an unstamped repo only when it has no
connectionId, mirroring the existing local-IPC dedup guard
(src/main/ipc/repos.ts). Adds a regression test that fails without the
guard (SSH repo hijacked into runtime host).

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

* review: only runtime hosts backfill an unstamped repo

A legacy unstamped repo is indistinguishable from a genuine local repo
(both have null executionHostId and connectionId). Restrict the adoption
branch to runtime incoming hosts so a local/ssh import at a colliding
path can never re-attribute a real local project to the wrong host.
Runtime is the only host that lost its identity to the pre-#7018
path-only import and needs the backfill. Adds a regression test.

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

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-07 20:45:55 -07:00
Jinwoo HongandOrca 98bee1b419 Fix un-removable SSH ghost worktrees after host remove/re-add (#7753)
Co-authored-by: Orca <help@stably.ai>
2026-07-07 19:01:42 -07:00
Brennan BensonandOrca 3826169c11 Keep Claude live-PTY refresh gate closed across restarts; recover wiped runtime credentials (STA-1246) (#7483)
Co-authored-by: Orca <help@stably.ai>
2026-07-06 01:16:23 -07:00
Jinwoo Hong 2f3c7e8660 Fix Windows slow startup and multi-minute UI freezes (#7225) (#7266) 2026-07-03 19:40:45 -04:00
Brennan Benson 653f349b5f Fix terminal theme selection persistence (#7227)
Fixes #7166.
2026-07-03 02:55:27 -07:00
NeilandOrca ad13532ab1 Garbage-collect stale worktreeMeta for externally-deleted worktrees (#7107)
* Garbage-collect stale worktreeMeta for externally-deleted worktrees at load

worktreeMeta rows are minted for every worktree Orca ever discovers but
were only deleted by Orca-initiated removal flows. Worktrees deleted
outside Orca (git worktree remove, rm -rf, agent scripts) purge renderer
session state without ever removing the meta, so the map grew
monotonically - 318 of 503 entries (63%, ~110KB) on the reference
machine pointed at long-deleted paths. These entries never reach the
renderer; they only cost disk bytes, parse time, and memory.

GC at load is deliberately narrow: local-host entries only (SSH/runtime
repos, remote meta.hostId, and WSL UNC paths are skipped - a local
existsSync would falsely condemn remote paths), only when the checkout
path no longer exists, only after a 30-day idle grace (preserving
pushTarget cleanup for recently-vanished worktrees and quick
recreations), and never when the entry lacks timestamps to prove
idleness. Lineage rows cascade like removeWorktreeMeta.

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

* Fix GC false positives: folder-workspace instances, WSL paths on Windows

From adversarial review of the GC commit:
- Folder-project workspace instances are keyed repoId::path::workspace:
  <uuid>; splitting on the first :: made the existence probe test a
  path-with-suffix that never exists, deleting idle folder workspaces -
  whose meta IS the workspace record - and cascading into terminal-
  history deletion. Instance keys are now skipped entirely.
- On Windows, WSL linked worktrees carry Linux-style paths from git
  porcelain that a Windows existsSync cannot probe; skip non-Windows
  paths on win32.
- Idle-grace check now runs before existsSync (skips the stat fan-out
  for active entries and shrinks the slow-NFS startup tail), and a
  null worktreeMeta map in the file is normalized instead of throwing
  outside the parse guard.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-02 03:12:26 -07:00
NeilandOrca 3ba3630150 Move githubCache out of the durable state file into a quit-time sidecar (#7101)
* Move githubCache out of the durable state file into a quit-time sidecar

Every PR/issue poll restamps fetchedAt even when nothing changed, which
defeated the content-hash write guard by construction: ~100KB of
refetchable 5-min-TTL cache rewrote the whole 1.6MB orca-data.json once
per refresh cycle - the last remaining non-user-driven writer.

The cache is now memory-only during the session (setGitHubCache no
longer schedules a save; the durable payload and state hash both omit
it) and is snapshotted best-effort to orca-github-cache.json at flush
(quit/reload), so PR/issue badges still paint instantly on next launch.
A legacy in-file cache is used as the seed and stripped by the next
durable write; downgrades just recreate the key, which the next upgrade
strips again. Sidecar loss costs nothing - the data is refetchable.

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

* Reject null/array pr-issue shapes when reading the github cache sidecar

A corrupt-but-valid-JSON sidecar ({"pr":null}) would seed
state.githubCache.pr = null and crash mobile worktree.ps PR grouping
until the renderer's first cache save repaired it.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-02 02:51:09 -07:00
NeilandOrca b51dd80252 Skip no-op state writes and bound save postponement in persistence (#7092)
* Skip no-op state writes and bound save postponement in persistence

orca-data.json (1.6MB live) was fully rewritten (pretty-print + tmp +
rename) on every debounced save even when the state content had not
changed - measured 3 rewrites/min (~5MB/min) on an idle production
instance, and a sync-flush storm of identical multi-MB writes at every
warm start via persistPtyBinding re-binds.

- Content-hash guard in both writers: a save whose plaintext state hash
  matches the last successful write skips serialize+write+rename
  entirely. Hashing plaintext (not the payload) because encrypt() uses
  a random IV per call. Safe under flushOrThrow's durability contract:
  a matching hash means the file already holds exactly this state.
- Debounce 300ms -> 1s trailing with a 5s max-wait. The old timer reset
  on every mutation with no bound, so sustained sub-interval mutation
  bursts could postpone the write indefinitely; now staleness is capped
  at 5s while bursts coalesce.

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

* Guard the state-hash against sync-flush interleaving mid-rename

From adversarial review: an async writer that had already passed its
generation check and dispatched the rename could have a sync flush
interleave during the await, write fresher state, and record its hash -
the async continuation then clobbered lastWrittenStateHash with a value
describing content NOT on disk, making later saves (including the quit
flush) silently skip. Re-check writeGeneration before recording.

Twin edge: a hash-matching sync flush could skip its write while a
stale dispatched rename lands afterwards, leaving stale content
unchallenged. flushOrThrow now forces the sync write whenever an async
write chain was in flight at entry, restoring the sync-write-last disk
ordering.

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-02 01:56:25 -07:00
4dbc9f3817 feat(ssh): add ControlMaster multiplexing for system SSH transport (#6922)
* feat(ssh): add ControlMaster multiplexing for system SSH transport

System SSH transport spawns a new OpenSSH process per exec command
(platform detect, relay install check, node resolution, relay launch,
socket probe). Each process pays the full SSH handshake cost — ~9s on
Uber devpods — making a typical relay connect take 54s+ and reliably
exceeding the 15s startup reconnect budget.

Add SSH ControlMaster multiplexing via a per-target socket in
$TMPDIR/orca-ssh-ctl/<hash>.sock. The first command establishes the
master; subsequent commands reuse it at ~100ms per exec instead of ~9s.
ControlPersist=300 keeps the master alive after commands exit so rapid
reconnects (e.g. on tab focus) also benefit. Windows is excluded since
OpenSSH's ControlMaster support there is limited.

* fix(ssh): address ControlMaster key collision and directory permission risks

- Use target.id in the socket key so distinct SSH targets can never
  collide even when configHost/port/user happen to match
- Switch from SHA1 to SHA256 and extend hash slice from 12 to 16 chars
- Stat the control-socket directory after mkdirSync to reject pre-existing
  dirs that are symlinks, foreign-owned, or have group/other write bits
  (mkdirSync mode is ignored on pre-existing dirs)
- Update two tests that used exact spawn-arg arrays; replace with
  ordering assertions (forward flags before --) that stay correct
  regardless of which extra ControlMaster options are injected

* fix(ssh): bind ControlPath identity to route and reject symlinked ctl dir

Fold proxyCommand/jumpHost/identity fields into the ControlPath hash so a
target whose route is edited no longer reuses a still-alive master built on
the old route. Switch the control-socket dir check from statSync to lstatSync
so a planted symlink fails the directory validation outright.

* test(ssh): drop tautological argv re-assertion in spawn checks

The toHaveBeenCalledWith re-passed the args array extracted from the same
mock call, making that argument position always pass. argv content is
already verified by the index-ordering assertions above; use expect.any(Array)
so the spawn check only claims what it actually verifies (binary path, stdio).

* fix(ssh): harden system ssh connection reuse

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

* test(ssh): isolate control socket runtime dir

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

---------

Co-authored-by: Test <test@example.com>
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-01 21:29:48 -07:00
Jinwoo HongandOrca f8c1f0fdf8 Fix delayed TUI mouse wheel reports (#7060)
Co-authored-by: Orca <help@stably.ai>
2026-07-01 19:54:53 -07:00
Eddie JaoudeandJinjing e0a7f0eadd fix: reordered default columns in kanban board (#6934)
* fix: reordered default columns in kanban board

* Introduce dedicated flag to repair reversed default workspace statuses

- Add `_workspaceStatusesReorderedDefaultRepaired` to decouple the
  one-shot repair from the initial status order migration.
- Ensure the repair runs for users who saved the reversed default
  payload (with "Done" on the left) during a short-lived broken build.
- Support both "Completed" and "Done" labels when identifying default
  status shapes to migrate or repair.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-01 13:17:52 -07:00