Commit Graph
5382 Commits
Author SHA1 Message Date
Brennan Benson b63d4cde28 feat(agent-status): question glyph for "needs you" state everywhere (#9996)
* feat(agent-status): show question glyph for needs-you state everywhere

Replace the amber attention dot with the dashboard's MessageCircleQuestion
chat glyph for the waiting/permission "needs you" state across all
surfaces: the agent dashboard cards, in-app dashboard rows, the sidebar's
Agent Dashboard quick-indicator counts, the worktree-level status dot, and
the shared agent-row/terminal-tab indicators.

On the dashboard card the header glyph is suppressed when a question
summary pill is present so "needs you" reads once, not twice.

* test(agent-status): assert amber question glyph, not amber dot

The needs-you unification replaced the amber dot with the amber
MessageCircleQuestion glyph, so update the remaining state assertions in
DashboardAgentRow, WorktreeCardStatusSlot, and TerminalTabLeadingIcon to
match (lucide-message-circle-question + text-amber-500).

* test(agent-status): cover needs-you glyph surfaces
2026-07-22 16:15:25 -07:00
a356b9d5c2 fix(worktrees): show CLI-created local worktrees in the sidebar while a remote runtime is active (#6628)
* fix(worktrees): refresh local worktrees in the sidebar while a remote runtime is active

When a remote runtime is active, a local `worktrees:changed` event for an
unbound repo was dropped by the renderer guard in useIpcEvents. Worktrees
created outside Orca for that repo (e.g. `orca worktree create` from a CLI or
automation flow) therefore stayed invisible in the sidebar until an app
restart, even though their sessions were already running.

The guard existed because an unbound repo's list fetch routes to the active
runtime (settingsForKnownRepoOwner's unbound fall-through), so refreshing with
local worktree ids could query — and purge against — the remote host.

Instead of dropping the event, pin the refresh to the local host
(forceLocalOwner): fetch the worktree list against the local owner and merge
additively. The merge is host-scoped and the deletion-purge is skipped on this
path, so it only ever adds local-host worktrees and never overwrites the active
runtime's worktree state. A genuinely-removed local worktree is reclaimed by
the next unguarded full refresh.

* test(e2e): regression — CLI-created worktree visible while a remote runtime is active

Drives the real `orca worktree create` path: the CLI RuntimeClient calls
`worktree.create` over the app's socket, registering a managed worktree and
firing the `worktrees:changed` IPC the renderer listens for. Stages a remote
runtime as active by injecting `activeRuntimeEnvironmentId` into the renderer
store, so no real remote host is needed. Fails on the prior behavior (the
worktree never appears while a runtime is active) and passes with this fix.

* fix(worktrees): pin local lineage refresh during runtime activity

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

* review: trim comments to house style, normalize queue coalescing to booleans

* review: sweep rename-grace expiry before early returns in worktrees:changed handler

* review: document accepted workspace-space gap, drop imprecise 'additive' wording

* fix(worktrees): route duplicate local repo events locally

* fix(worktrees): tag local worktree events at origin, gate purge skip on runtime overlap

* test: pin origin-based forceLocalOwner with a no-runtime local event assertion

---------

Co-authored-by: brennanb2025 <brennankbenson@gmail.com>
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-22 16:10:53 -07:00
Brennan Benson bc57cf8787 fix(sidebar): use automation icon for Hide automation-created filter (#10000)
The workspace-options filter row used the Workflow icon while the
Automations nav item and page use CalendarClock. Match them so the
filter clearly maps to automation-created workspaces.
2026-07-22 16:09:19 -07:00
OrcaWinandOrcaWin b606117eed Fix paired remote terminal parse backpressure (#9683)
* fix(terminal): defer remote output ACKs until parse

* test(terminal): document synchronous credit claims

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-22 16:03:42 -07:00
OrcaWin 0d82cc3dbb Fix legacy worktree lineage projection after stable updates (#9913) 2026-07-22 15:52:42 -07:00
Brennan Benson 8dc1ca4920 fix(daemon): retire macOS daemons whose login session died (#7936) (#9826)
* fix(daemon): retire macOS daemons whose login session died (#7936)

A daemon that survives a full macOS logout is unsalvageable: its PAM
context can no longer host login(1) spawns (every new PTY becomes a
'Login incorrect' prompt zombie) and its Mach bootstrap namespace has
lost the system DNS resolver, so terminals it hosts have no egress.
Today it also keeps the #9301 preflight's cached 'accepted' verdict, so
it keeps wrapping spawns in login(1) forever; only a manual daemon
restart recovers.

GUI-spawned daemons now watch for login-session death from the inside:
a fresh cache-bypassing PAM probe (triggered by PTY-exit bursts, fresh
client hellos, and a slow periodic timer) must conclusively reject
three consecutive times AND the in-process system resolver must be
degraded; then the daemon exits crash-style so session meta stays
unclean and the replacement daemon cold-restores scrollback. A
conclusive rejection also flips the spawn-wrapper cache off
immediately.

Headless serve/SSH daemons never get the watch (they must survive their
spawning session ending), and a session that never conclusively
accepted login(1) never arms it — a PAM anomaly alone can't kill a
healthy daemon (fast user switching keeps accepting, so switched-away
sessions are preserved).

* test(daemon): e2e seam to drive login-session death oracles from a verdict file

A dead macOS login session cannot be fabricated without root (PAM owns
audit-session teardown), so live lifecycle QA drives the death watch's
probe and resolver oracles from ORCA_E2E_LOGIN_SESSION_PROBE_FILE:
'alive' → accepted/healthy, 'dead' → rejected/unhealthy, anything else
inconclusive — with compressed watch timing. Mirrors the existing
ORCA_E2E_DAEMON_INIT_DELAY_MS seam; inert unless the env var is set.

* fix(daemon): close the hang-shaped gap in login-session death detection

The conclusive-PAM-verdict trigger had one blind failure shape: login(1)
hanging at the prompt past the probe bound (killed → inconclusive
forever → the watch never fires). Three changes close it:

- The death-watch probe gets its own 4s bound (the 500ms preflight bound
  exists for spawn-path latency, which doesn't apply off-path), so a
  slow-but-answering PAM stack isn't misread as a hang.
- An inconclusive pipe probe escalates to a PTY-hosted probe via
  script(1) — a dead session's PAM stack may only misbehave under a real
  tty (the pipe-vs-PTY fidelity limit the preflight documents).
- A streak of timeout-killed probes (which a live session never
  produces) is a second retirement trigger, at a higher threshold (5)
  and still gated on the degraded resolver, logged with a distinct
  cause so field logs discriminate the two paths.

Every dead-session behavior — fast reject, prompt-then-EOF, or hang —
now fires retirement; all inconclusive states still fail toward
preserving the daemon.

* fix(daemon): keep login-session retirement conclusive

* fix(daemon): stop login watch before clean shutdown

* fix(daemon): make login-session PTY probe reliable

* fix(daemon): close login-session watch races

* fix(daemon): ignore health probes for login watch activity
2026-07-22 14:45:03 -07:00
Brennan Benson 4062ed5a73 perf(worktrees): back off worktree scans for agent-scratch repos (#9985)
Production crash diagnostics measured ~128 `git worktree list` execs/min
(9,400 in one 80-minute session, ~16% of wall-clock in git subprocesses):
the resolved-worktree scan fans out over every registered repo on a 30s
cache TTL, and most registered repos on the affected installs were
agent-CLI scratch repos (~/.codex-tmp capsules, vendor imports, skill
checkouts) that need no freshness.

Classify agent-scratch repo roots with a curated shared matcher and stamp
their scan-cache entries with a 5-minute TTL instead of 30s. Orca-driven
mutations still bypass the TTL via the per-repo generation bump, so only
passive pickup of external changes slows for scratch repos. Expected
steady-state reduction on the measured install: ~82% fewer git spawns.
2026-07-22 14:42:58 -07:00
Jinjing b0e36169d8 fix: return native promise from awaitClick on Zone.js pages (#9995)
Zone.js patches the global Promise with a non-native thenable. When a bare
`new Promise(...)` crosses the Electron executeJavaScript boundary, it's
serialized as-is, losing { page, target } and exposing __zone_symbol__*
fields instead. Wrap in an async IIFE to return a native promise that
Electron always unwraps correctly.
2026-07-22 14:37:34 -07:00
NeilandOrca 8cfb8a2a2b perf(emulator): park device stream when the window is hidden (#9842)
The iOS MJPEG and Android scrcpy device streams are gated only on the pane
being the active tab (isActive, PR #7382). When the emulator tab is frontmost
but the whole Orca window is hidden/minimized/occluded/display-asleep, the
full-fps pipeline keeps running: main-process socket read + JPEG/H.264 decode
+ IPC + renderer decode. Renderer background-throttling (#9395) cannot stop it
because the pipeline is IPC-push driven from main.

Gate showStream additionally on window visibility via a new occlusion-safe
hook that honors the terminal stale-visibility latch (so a display-sleep
occlusion wedge can't freeze the emulator on a black frame) and delays the
visible->hidden park by 500ms so a quick Cmd+Tab round-trip doesn't renegotiate
the device stream.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:33:03 -07:00
NeilandOrca 17e471c51e perf(editor): memoize the markdown preview render pipeline (#9843)
react-markdown's <Markdown> has no internal memoization: it rebuilds the whole
unified remark->rehype->highlight->katex processor and re-parses the document on
every render. MarkdownPreview re-renders on internal state that does not affect
the rendered output — most visibly, every keystroke in Find (query/match-index
state) — so a large doc re-ran the full parse + syntax-highlight + KaTeX pass per
keypress, making Find laggy.

Hoist the two fully-static plugin arrays to module scope (a fresh array identity
per render would defeat the memo) and render the body through a React.memo'd
MarkdownBody keyed on content + components. The pipeline now re-runs only when the
rendered content or the components map actually changes; Find/review-pulse/copied-
note re-renders skip it. The components map was already memoized, so its identity
is stable across those re-renders.

Behavior unchanged: 106 existing MarkdownPreview tests pass.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 14:32:34 -07:00
Jinjing 034aeb15e4 feat(editor): seed Find from selected text (#9982) 2026-07-22 14:11:19 -07:00
Brennan Benson d6c9fcd537 feat(mobile): surface pairing-auth failures on desktop and mobile (#9782)
* feat(mobile-pairing): surface unpaired-device auth failures instead of silent 4001 loops

Desktop: when a phone repeatedly fails direct-transport E2EE auth with a
token missing from the device registry (pre-v1.4.106 pairing-path bug left
desktops that regenerated their registry rejecting paired phones forever),
throttle to one notification per session and show an actionable toast
pointing at Settings -> Mobile to re-pair.

Mobile: map a bare 4001 close onto the existing auth retry budget (the
encrypted e2ee_error is undecryptable when the desktop keypair changed, so
the close code is the only surviving signal) instead of looping the generic
reconnect forever, and make the auth-failed verdict say 'Pairing invalid -
re-pair with your desktop' instead of a bare 'Auth failed'.

* fix(mobile-pairing): handle stale keys and startup notification races

* fix(mobile-pairing): isolate auth notification failures

* fix(mobile-pairing): keep recovery alert actionable
2026-07-22 14:05:37 -07:00
OrcaWinandOrcaWin 8f40419800 fix(agent-status): cap the live agent-status map to stop Windows renderer OOM (#9872) (#9936)
The live `agentStatusByPaneKey` map had no size cap. `setAgentStatus` rewrites it
with a full spread copy on every status ping and keeps rows until a pane/tab
teardown event removes them; a missed teardown (agent killed without a Stop hook,
pane/tab closed while its status lingered) orphans a row forever. On long
multi-agent sessions orphaned heavy rows accumulate without bound, and because
each ping spread-copies the whole map, once it is ~1.9 GB one ping transiently
doubles it past the 3586 MB old-space limit -> renderer crash (exit -36861).

Cap the live map at MAX_LIVE_AGENT_STATUSES=500, shedding only rows whose pane is
provably gone (a mounted tab's rooted layout no longer lists the leaf) or long
idle, and never an open pane's row (any state, incl. needs-input waiting/blocked).
Rootless/empty-snapshot, not-yet-hydrated, and no-renderer-tab (orchestration
worker) rows are unprovable: kept while a fresh agent could own them, shed only
past the stale window or by a hard-cap fallback that guarantees the bound.
Eviction bumps the status/sort epochs so the retention sync snapshots disappeared
done rows. Cost is one Object.keys length check on the reducer's already-O(n)
spread under the cap; the layout walk + sweeps run only on the rare over-cap ping.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-22 14:04:17 -07:00
Neil 53e018a1a6 fix(terminal): make SSH reconnect UI non-blocking (#9928) 2026-07-22 13:46:05 -07:00
Brennan Benson 56a31a5af0 Show current branch in Source Control header (#9787)
* feat(source-control): show current branch in header

* fix(source-control): keep header focused on branch

* fix(source-control): compact detached head identity

* fix(source-control): make branch identity keyboard accessible

* fix(source-control): keep create review in checks
2026-07-22 13:29:45 -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
Brennan Benson 0121f571e4 fix(agent-status): map codex request_user_input questions to Needs You (#9861)
* fix(agent-status): map codex request_user_input questions to waiting

Codex 0.145 asks user questions via the auto-allowed request_user_input
tool (experimental default_mode_request_user_input): PreToolUse fires
while blocked on the answer with no Stop, so Orca showed the pane as
working/idle instead of Needs You. Map that PreToolUse to waiting
(mirrors grok's ask_user_question), exempt question waits from the codex
yolo auto-approval suppressor, and deliver native-chat answers to the
digit-commit selector by option number (typed labels are ignored and
Enter commits the highlighted first option). Older codex versions emit
no such event and are unchanged.

* fix(native-chat): preserve codex question answer semantics
2026-07-22 12:00:14 -07:00
JinjingandOrca 4c2bb508c3 feat(settings): make Language setting findable by native-language search terms (#9967)
The Language setting's native word (语言 / 언어 / 言語 / Idioma) only reached
settings search via the localized title in that word's own UI locale — so a
Chinese speaker on the default English UI could not find it by typing 语言.

Always-index the native word for 'language' in every supported language (plus
the previously-omitted Spanish native name Español), so speakers can locate and
switch to their language from any starting locale. Native words are
locale-invariant constants, so they are plain keyword literals with reviewed
localization-coverage allowlist entries.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 11:52:49 -07:00
Brennan Benson 1a9e819c40 feat(skills): land remaining hybrid stubs (#9846)
* feat(skills): land remaining hybrid stubs

* fix(build): exclude skill stub sources from packages
2026-07-22 11:43:01 -07:00
Brennan Benson c4d903ff21 fix(agent-status): keep Claude in-process teammates visible as idle sidebar rows (#9850)
* fix(agent-status): keep Claude in-process teammates visible as idle sidebar rows

Claude Code 2.1.21x runs named Agent-tool agents as turn-based in-process
teammates: SubagentStop and TeammateIdle fire at every TURN end while the
teammate stays alive awaiting mail (verified live on 2.1.217). Treating
those events as finish signals deleted the child row seconds after each
burst, so the sidebar showed no subagents for most of a teammate's life.

Root-cause fix: the roster now tracks a working/idle state per child.

- One-shot children (hyphen-free ids) keep remove-on-stop: their
  SubagentStop is a true finish.
- Teammate-shaped rows park as idle on SubagentStop/TeammateIdle and
  revive to working via the next SubagentStart (same lifecycle id,
  first-observed startedAt preserved).
- Idle rows never gate the pane 'working' (#8825's done-gate rule).
- Only TeammateIdle-confirmed idle rows survive a complete lead-Stop
  fold; a stopped workflow lane wearing a teammate-shaped id is reaped
  there (or immediately, once a fold tagged it listedAsSubagentTask), so
  the pre-#8825 idle pile cannot rebuild.
- At the wire cap, the oldest idle row is evicted to admit a working
  spawn; working children are never displaced.
- Hydrate keeps pruning idle snapshots: idle-teammate liveness cannot be
  proven across a restart, and a live teammate re-earns its row.

* fix(agent-status): restore inventory-confirmed workflow lanes
2026-07-22 11:42:34 -07:00
NeilandOrca 39b124c75b perf(dashboard-popout): park the kanban clock when hidden or timestamp-free (#9881)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 03:32:30 -07:00
NeilandOrca 95ae346b87 perf(ssh): send a single keepalive after wake, not two (#9880)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 03:31:44 -07:00
NeilandOrca dfbd9baec5 perf(editor): don't slice the markdown suffix per-character in the rich-editor preprocessor (#9887)
Co-authored-by: Orca <help@stably.ai>
2026-07-22 03:30:23 -07:00
NeilandOrca 7304776c20 fix(agent-status): clear worktree-attributed orphans on relay/daemon teardown (#9030) (#9878)
After a relay daemon restarts on an SSH host, main drops its status rows but
renderer agentStatusByPaneKey entries whose connectionId stamp never matched
(unstamped / SSH-mis-normalized) survived, stayed 'fresh' for 30 minutes, and
made their sidebar rows permanently un-clickable (tab gone, handleActivateAgentTab
silently returns). Broaden the transient clear to also drop worktree-attributed
rows whose owning repo is on the torn-down connection, proving the host via the
worktree->repo mapping instead of relying solely on the entry's connectionId.

Co-authored-by: Orca <help@stably.ai>
2026-07-22 03:01:09 -07:00
OrcaWin 300ee19950 fix(terminal): make remote workspace sleep converge (#9874) 2026-07-22 00:22:36 -07:00
e60060039a Show setup-needed hosts in workspace run picker (#7835)
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-21 23:08:34 -07:00
NeilandOrca 9d3ae3adc7 fix(sidebar): coalesce staggered reconnect refreshes to stop wake freeze (#8539) (#9847)
On wake, remote/SSH runtimes reconnect in a staggered burst; the sidebar refetched all worktrees once per host, piling up K synchronous full-sidebar remounts and freezing the UI. Wrap the reconnect refresh in a single-flight coalescer so at most one refresh runs at a time plus one queued rerun, for any K. fetchAllWorktrees and its lineage follow-up are unchanged; only how often they fire changes.

Co-authored-by: Orca <help@stably.ai>
2026-07-21 21:09:02 -07:00
Brennan Benson 586cf781ca perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s (#9829)
* perf(rate-limits): throttle statusline usage posts to one curl per pane per 15s

The managed Claude statusline script posted on every statusLine tick
carrying rate_limits — ~3 curl spawns/sec per streaming pane, multiplied
across concurrent panes. The service drops same-value posts inside its
30s dedupe window anyway, so most spawns bought nothing.

Gate the post on a per-pane stamp file: POSIX compares date +%s against
the stamp; Windows uses an all-builtin seconds-of-day parse of %TIME%
(octal-safe, no extra process). Both fail open — unparseable time,
garbage stamp, or midnight wrap posts rather than darkening the live
usage feed — and the stamp only advances when a post actually fires, so
skipped ticks never defer the next one.

Measured: 30 rapid rate-limit ticks spawn 1 curl (was 30).

* perf(rate-limits): keep throttled statusline ticks process-free

Use Claude's monotonic session duration for the POSIX throttle so skipped ticks do not replace curl churn with date churn, while retaining a fail-open date fallback. Key temp files by the stable leaf UUID so path-like or long host tab IDs cannot disable the throttle.

* fix(rate-limits): preserve throttle isolation across upgrades

* fix(rate-limits): reject leading-zero stamp values before arithmetic

All-digits validation still admitted values like 008, which are invalid
octal inside $(( )) — and dash treats that expansion error as fatal, so
the script died before rewriting the stamp and the pane's live usage
feed stayed dark until the file was deleted. Allow-list canonical
decimals (same pattern as the duration parse) on both the stamp and the
computed clock so malformed values fail open to posting.

Verified under dash: the old digits-only check aborts at the arithmetic
(Illegal number: 008); the allow-list survives and posts.

* perf(rate-limits): eliminate POSIX statusline cat churn

* test(rate-limits): exercise overlapping statusline ticks

The stamp check/write is deliberately lock-free (a lock could wedge the
feed closed; fail-open is the contract), so a truly concurrent burst may
post more than once, bounded by overlap width — an exact at-most-one
assertion would be flaky by design. Assert the invariants that do hold:
every overlapping run exits 0, the raced stamp lands valid, and it
throttles the following ticks.
2026-07-21 20:59:33 -07:00
OrcaWin b232df732b fix(terminal): make remote agent sessions host-authoritative (#9687) 2026-07-21 20:51:28 -07:00
NeilandOrca 2a32c5c9a1 fix(runtime): scope mirrored-editor close intent to its runtime environment (#9836)
#9804 added a leading `environmentId` parameter to recordWebSessionCloseIntent
(and isWebSessionCloseIntentPending) and updated the web-runtime-session.ts
callers, but missed the second caller in close-mirrored-editor-tab.ts, which
still passed 3 args. This broke the web typecheck on main and, had it compiled,
would have recorded the close intent under the wrong scope key
(closeIntentScopeKey(environmentId, worktreeId)) — so the host snapshot could
flash the just-closed mirrored tab back.

Pass the already-validated runtimeEnvironmentId (the same value handed to
closeWebRuntimeSessionTab immediately below) so the intent is scoped correctly.
Update the test's isWebSessionCloseIntentPending assertion to the 4-arg form.

Co-authored-by: Orca <help@stably.ai>
2026-07-21 20:23:09 -07:00
Brennan Benson 9097ea0409 fix(naming): remove identifier-first name post-processing (#9821)
* fix(naming): remove identifier-first name post-processing

Workspace display names and tab titles were being rewritten after
generation by prompt-scanning heuristics from #8238 — a stray "#1" in
prose became a workspace named "#1 - Fix", and the rewrite ran
downstream of generation so user naming instructions couldn't override
it. Per the same principle as #9088, naming defaults stay minimal and
user overrides own the style.

- Delete work-item-reference.ts and display-name-from-work.ts (+tests).
- Auto-rename display names return to the humanized branch slug; tab
  titles return to the cleaned first prompt clause.
- Explicit create-from-work-item naming returns to action-first
  ("Review PR 1234").
- Keep #8238's URL-before-markdown strip-order bugfix in tab titles,
  with regression tests adjusted to the natural expectations.

* test(naming): pin incidental marker regression
2026-07-21 20:06:31 -07:00
Brennan Benson b81e782ffc fix(browser): stop failing goto when a redirect or download aborts the load (#9822)
* fix(browser): stop failing goto when a redirect or download aborts the load

Electron's loadURL rejects with ERR_ABORTED (-3) when the initial
navigation is superseded — a client-side/meta redirect (common in SSO
flows) or a download-triggered load. Since #9633 drives goto through
wc.loadURL directly, that rejection surfaced as a spurious
'Failed to navigate' browser_error even though the page landed fine.

Treat ERR_ABORTED like offscreen-browser-backend already does: resolve
with the page's actual URL/title. Every other loadURL failure still
fails closed.

* fix(browser): settle replacement navigation after abort

* fix(browser): clean up aborted navigation destruction race
2026-07-21 19:56:07 -07:00
OrcaWin f9f3cd2fbe fix(terminal): prevent reconnect from killing live daemon sessions (#9804) 2026-07-21 19:20:16 -07:00
NeilandOrca 96df5f5715 fix(editor): don't flag editor-initiated moves as changed-on-disk (#9506)
* fix(editor): don't flag editor-initiated moves as changed-on-disk

An in-app move/rename (explorer drag-drop, inline rename, tab rename)
re-homes the open tab to the new path and carries its unsaved draft
forward. The move also physically relocates the file, which the worktree
watcher reports as delete(old)+create(new) a few ms later. Because the
tab already lives at the new path by then, that create echo was treated
as an external write landing on a dirty tab and raised a spurious
"changed on disk" banner.

Add a short-lived self-move registry (the move analog of the existing
self-write registry) stamped at the single remap choke point, and have
the external-watch handler recognize the move's own watcher echo:
suppress the changed-on-disk mark on the re-homed dirty tab and the
tombstone on the source path. Genuine external edits are unaffected.

Covered by unit tests for the registry, the remap recorder, and the
watch-hook suppression (plus a no-over-suppression guard).

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

* refactor(editor): harden move-echo suppression per adversarial review

Addresses review findings on the self-move suppression:

- Stamp the self-move at the call sites BEFORE the on-disk rename
  (recordSelfMoveForOpenTabs), not after the tab re-home, so the
  watcher echo can't win the race. This makes the source-side delete
  guard actually effective and removes a possible one-frame flash.
- Suppress only the move's own create echo, not update events, so a
  genuine external write to the moved path within the TTL still raises
  the changed-on-disk banner (closes an over-suppression gap).
- Track source/target roles independently per path so an immediate
  undo can't clobber the original move's still-in-flight stamp.
- Raise the registry cap above realistic bulk-move sizes so a large
  directory move never self-evicts its own not-yet-echoed stamps.

Updated + added tests: registry undo/role + cap, the new call-site
helper (incl. directory move), and a real-update-still-marks case.

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

* fix(editor): make move-echo suppression watcher- and TTL-robust

Round-3 hardening after adversarial review:

- Suppress the move's own watcher echo regardless of event kind. The
  main-process watcher coalesces a create+attr-change burst into a lone
  update, so a create-only gate let the echo through on some hosts and
  re-exposed the false banner. Suppression is now bounded by the
  self-move TTL; a genuine write to the exact path within that short
  window is the documented trade-off (draft is preserved regardless).

- Bracket the on-disk rename with the self-move stamp via a single
  renameOpenTabsPathOnDisk wrapper: stamp before (to beat the watcher),
  re-stamp on success (a slow SSH/runtime rename can outlive the TTL, so
  the fresh window must start when the file actually moved), and clear on
  failure (a rename that never happened must not suppress real events).
  All move entry points (explorer move, inline/tab rename incl.
  undo/redo, untitled rename) route through it.

- Treat a tab as remote for TTL purposes when it has a runtime owner OR
  an SSH worktree connection (an SSH tab can carry a null runtime owner).

- Registry tracks source/target roles independently per path so an
  immediate undo can't clobber the original move's in-flight stamp; cap
  raised above realistic bulk-move sizes.

Tests: registry role/clear/cap, the rename wrapper (success re-stamp +
failure clear), the call-site helper (dir move + clear), and watch-hook
coalescing-robust suppression + post-TTL surfacing.

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

* fix(editor): refcount self-move roles so a failed move can't clear a live one

Two concurrent moves onto the same destination both stamp that path as a
target; if the second rename fails and clears, it must not erase the
first (successful) move's still-live target stamp. Reference count each
role's registrations and clear only the failed move's own contribution.

Adds a regression test for the shared-destination case.

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

* fix(editor): give self-move stamps per-registration expiries + retract tokens

The refcount model used a single shared expiry scalar per role that only
grew via max() and reset at refs=0, so releasing the max-contributing
registration left survivors inheriting an over-extended window (and an
expired registration could be resurrected by a later stamp on a key the
opposite role kept resident). Both over-suppress genuine changes.

Model each stamp as an independent registration carrying its own expiry
(a list per role). recordSelfMove returns a ticket; clearSelfMove
retracts exactly that registration. A role is live while any of its
registrations is unexpired, so concurrent stamps, failed-move clears, and
expiry are all precise. Wrapper/helper thread the tickets through.

Adds regressions for the over-extension and resurrection cases.

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

* test(editor): cover source-side self-move guard in the pre-remap ordering

Adds the case where the watcher's delete(old) arrives while the tab is
still at the old path (before remap re-homes it): with a live self-move
source stamp the tombstone must be suppressed. Pairs with the existing
naked-delete control (no stamp → deleted) to pin the guard's behavior.

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

* docs(editor): document the failed-move suppression window as a bounded trade-off

A self-move stamp is placed before the rename and retracted if it fails,
so an event consumed during the pre-failure window is swallowed. Note
that this only matters for the rare unrelated-dirty-tab-at-destination
case and the recoverable missed-source-tombstone case, and that a move
has no bytes to echo-verify the way self-writes do.

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

* feat(editor): decide move echo vs external write by content identity

Replaces the time-bounded self-move suppression heuristic with a
correct-by-construction identity check, so a genuine external write to a
just-moved path is never swallowed and the move's own echo is never a
false conflict — regardless of watcher event-kind coalescing or timing.

- Remap now carries the edit-session identity (lastKnownDiskSignature,
  externalMutation, pendingDiskBaselineVerification) onto the re-homed
  tab. Previously the close+reopen dropped it, so a moved tab lost its
  disk baseline (and any pre-existing changed-on-disk conflict silently
  vanished on move).
- On a live self-move-target dirty event the watch hook now reads the
  destination and compares getDiskBaselineSignature(disk) to the tab's
  carried baseline: equal => move echo (suppress), differ/binary =>
  genuine write (banner). Autosave is suspended synchronously before the
  read so a write landing mid-read can't be overwritten; a generation
  token makes overlapping reads safe. Fails CLOSED (marks changed) on a
  missing baseline or read error — never blind-suppresses.
- The self-move registry now only scopes WHEN to verify. The source-side
  delete still can't be content-verified (nothing to read), so it stays a
  bounded, documented suppression.
- Trim the verbose Why-comments across these files to 1-2 lines.

Adds content-identity verification tests (echo/differ/no-baseline/read-
error/binary/autosave-gate) and a remap test for the carried identity;
splits the watch-hook suite to stay under the max-lines limit.

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

* fix(editor): give live move-verification its own autosave gate

The live self-move echo verification reused pendingDiskBaselineVerification
as its autosave gate, but that field is also the always-mounted restored-tab
conflict scanner's work queue: the scanner scans any pending dirty tab,
launches its own read, and clears the flag without checking the live
generation — so it could lift the gate mid-read and let autosave overwrite a
genuine external write. Give live verification a dedicated
pendingLiveDiskVerification field (both suspend autosave; each cleared only by
its owner). Transient, not persisted, not carried across a re-home.

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

* feat(editor): atomic rekeyOpenFilesForPathChange store action (move restructure stage 1)

Foundation for treating an Orca-owned move as an in-place retarget of the
open edit session (not close+reopen), per the locked design. One commit-only
store update migrates every path-derived id + all id-keyed state
(openFiles full-spread, the 6 file-id maps, activeFileId(+byWorktree),
tabBarOrder, unified tabs/groups via the now editor-family-widened
migrateHydratedEditorTabsAndGroups, pendingEditorReveal, untitled consume).
Preflight fails closed on collision (never merges two live sessions) or stale
with zero mutation. Not yet wired to a coordinator (stage 4).

Stage 1 of 5; behind the shipped content-identity fix.

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

* feat(editor): op-scoped in-flight move registry + source integration (stage 2)

editor-path-move-inflight.ts tracks Orca-owned moves for the exact duration of
the rename+rekey (no TTL): source paths suppress the delete tombstone, target
paths latch a destination event seen before the rekey (never suppress). Wired
into the watcher's delete filter alongside the old TTL registry (OR fallback)
so suppression keeps working until the stage-4 coordinator drives every move
through beginEditorPathMove. Stage 2 of 5.

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

* feat(editor): move-echo provenance + autosave gate on OpenFile (stage 3 store)

Adds pendingSelfMoveEcho {operationId,targetPath} to OpenFile and has the
rekey action install it + pendingLiveDiskVerification on dirty autosave-capable
destinations (moveOperationId arg), atomically in the same commit that re-homes
the tab — so the verify gate survives the rekey and its op-id token supersedes a
stale in-flight verification. Replaces the module-scoped generation map (which
broke under rekey). Verification-reader wiring + coordinator follow.

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

* refactor(editor): remap moves via atomic in-place rekey, not close+reopen (stage 4a)

remapOpenEditorTabsForPathChange now builds an owner-aware rekey plan (plain-path
id to the first owner, owner-qualified to the rest; previews resolve their source
to the moved edit's new id) and applies it via rekeyOpenFilesForPathChange in one
commit — preserving the full OpenFile + cursor/view/group/MRU state and closing
the close/reopen watcher-race window. Passes moveOperationId through so dirty
destinations get the content-verify gate. 4545 tests green.

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

* feat(editor): move coordinator drives rename/drag/undo/redo/untitled (stage 4b)

executeOpenEditorPathMove is the single transaction for every in-app move:
quiesce affected saves -> op-scoped begin (per runtime owner) -> on-disk rename
-> atomic in-place rekey (installs the content-verify gate/provenance) -> settle
-> re-verify any destination echo latched before the rekey. On failure the store
is untouched. Verification now triggers off the on-OpenFile provenance (consumed
on resolve) and the watcher latches pre-rekey destination events. Wired into all
five call sites; old renameOpenTabsPathOnDisk + separate remap removed from them.
2751 tests green. (TTL self-move registry now dead; removed next.)

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

* refactor(editor): remove the dead TTL self-move registry (stage 4c)

The coordinator + op-scoped in-flight suppression + on-OpenFile provenance fully
replace the time-bounded self-move registry, so delete it and its two helper
modules (record-self-move-for-open-tabs, rename-open-editor-tabs-path). The
watcher source filter now uses only isActiveMoveSourcePath and the verification
trigger only the tab's pendingSelfMoveEcho. Rewrote the self-move test suite onto
the new primitives. 7225 tests green.

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

* test(editor): end-to-end coordinator move test (stage 4 done)

executeOpenEditorPathMove renames on disk, retargets the session in place with
draft/dirty/baseline preserved + gate/provenance installed, settles the in-flight
transaction; on rename failure the store is byte-identical and the transaction is
released.

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

* feat(editor): mirror-safe move — detach moved mirrored tab + close-notify host (stage 5)

The atomic rekey changes a tab's id, so a moved mirrored tab would be culled by
the host snapshot (losing its draft) or resurrect the old path. Ship the safe
minimum: the rekey detaches a moved tab from the host mirror
(mirroredFromRuntimeSession cleared) and the coordinator close-notifies the
host's old-path tab (close intent suppresses re-mirroring). Prevents the
data-loss/resurrection; the moved tab becomes companion-local. The full
host-rekey path-change protocol (preserving mirror ownership) is a documented
follow-up.

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

* fix(editor): address review round 1 (4 majors)

- Coordinator now propagates the rekey result: a collision/stale AFTER a
  successful on-disk rename triggers an inverse rename + throws, instead of
  reporting success with the source tab stranded at a vanished path (#1).
- Cross-worktree: affected set spans all worktrees at the source path, sub-ops
  scoped per (worktree, owner), and the rekey partitions tab/group/tab-bar
  migration by each file's own worktree (was applied under one scope) (#2).
- Diff tabs: single-file unstaged diff tabs are now retargeted on a directory
  move (rebuild the diff id + relative path) instead of stranding (#3).
- Mirror close-notify moved to AFTER a successful rename so a failed rename
  can't desync the host by closing its authoritative tab (#4).

Adds tests: collision->inverse-rename, diff-tab retarget. 6698 tests green.

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

* fix(editor): review round 2 (mirror ordering, rollback error, diff sources)

- Close the host mirror tab only AFTER the rekey commits (capture pre-rekey
  resolution first): a rekey collision after a successful rename no longer
  desyncs the host by closing its authoritative tab (high).
- Surface a failed inverse rename instead of swallowing it: the thrown error
  now states the on-disk move may remain at the new path (high).
- Restrict diff-tab retargeting to staged/unstaged (purely path-derived ids);
  branch/commit diffs carry compare metadata and combined 'Changes' is
  worktree-rooted, so rebuilding them from path would produce a wrong id (med).

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

* fix(editor): resolve the move verify gate proactively (review round 3)

The rekey gates every dirty moved tab pending a destination content check,
but verification only ran when a watcher event arrived for that path. If the
watcher was down, throttled, or the event coalesced away, the gate never
cleared and autosave stayed suspended for the tab.

The coordinator now drives verification for every tab it gated once the
rename has committed, so the gate resolves on its own. That makes the
destination-side event latch redundant, so the in-flight tracker is
source-only again.

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

* fix(editor): review round 4 (gate strand, cross-worktree verify path, leak)

- Don't install the move-echo verify gate on a tab already showing the
  changed-on-disk banner: it's autosave-suspended via externalMutation and
  verification skips a 'changed' tab, so the gate would strand forever.
- Content-verify reads each moved tab's own filePath. A cross-worktree/
  floating tab's relativePath is relative to its own root ('../…') and must
  not be joined onto the initiating worktree path (would read the wrong file
  and raise a false conflict banner on unsaved work).
- Coordinator settles the in-flight source suppression in a finally so a
  throw between rename and commit can't leak it, and only after the rollback
  rename so a late forward-rename delete stays suppressed.
- Migrate pendingEditorReveal.fileId across the rekey (matcher prefers it).

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

* fix(editor): don't consume move-echo provenance in the safety-net verify (round 5)

The round-3 proactive post-commit verify ran resolveLiveMoveVerification,
which consumed pendingSelfMoveEcho. On FSEvents/SSH the real destination
watcher event reliably lands AFTER the fast local read, so it then found no
provenance, took the immediate changed-on-disk mark (no baseline check when
there is no recent self-write), and raised a false conflict banner on the
just-moved dirty tab — the exact data-loss this change removes.

The proactive verify is a safety net: it now releases the autosave gate but
leaves the provenance, so a later destination event is still recognized as
the move's own echo and content-verified. Only a real watcher event consumes
the provenance. Keeping it is safe — every watcher consumer verifies by
content, which is strictly safer than the immediate mark.

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

* fix(editor): scope move rekey to the initiating execution host (round 6)

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

* fix(editor): prefix-suppress the move root so late tabs under a dir move aren't flagged (round 8)

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

* chore(editor): trim move-fix comments to the why; drop redundant rename quiesce

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

* fix(editor): record mirrored-close intent synchronously to close the ghost-tab window

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

* fix(editor): use flavor-aware path containment for move selection (Windows/UNC case)

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

* fix(editor): reconstruct moved path by segment count (WSL alias / duplicate-separator safe)

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

* fix(editor): infer moved-path flavor by syntax, preserving legal POSIX backslashes

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

* test(editor): lock POSIX ancestor-backslash destination flavor

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

* fix(editor): flavor-aware trailing-separator strip; preserve POSIX literal backslashes

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

* fix(editor): flavor-aware separator folding in relative-path recompute (POSIX backslash)

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

* perf(editor): keep the fs-watcher delete path O(deletes) when no move is in flight

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

* docs(editor): tighten move-fix comments to one-line why-only

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

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-21 19:01:15 -07:00
Brennan Benson 4d0e3f51ce fix(sidebar): keep branch-discovered PR status visible on worktree cards (#9789)
* fix(sidebar): keep branch-discovered PR status visible on worktree cards

The sidebar status lane and the right checks panel read the same PR
caches but disagreed for unlinked worktrees: every successful GitHub PR
fetch mirrors the result into hostedReviewCache stamped with a
linked-style hint key ('github:<n>'), and getWorktreeCardPrDisplay
suppresses unlinked reviews with a non-empty hint. The checks panel
renders straight from prCache, so it showed "#9387 OPEN" while the same
worktree's card fell back to the plain branch icon.

Thread the branch-keyed PR cache number into getWorktreeCardPrDisplay as
corroboration: when the branch cache names the same PR the hosted entry
holds, the review provably belongs to this branch and stays visible.
WorktreeCard passes its existing cachedBranchFallbackGitHubPRNumber,
which is already guarded for linked metadata and the merged-head rule.

The hint stamping itself is unchanged on purpose: the 'github:' marker
also flags the entry as GitHub-scoped so neutral lookups still re-run
GitLab MR discovery.

* fix(sidebar): preserve PR lookup provenance

* fix(sidebar): preserve merged PR head guard

* test(github): assert exact refresh cache write
2026-07-21 18:31:39 -07:00
Brennan Benson 51873780c5 fix(dashboard): align terminal-dialog close X with compact header (#9816)
The default DialogContent close button is absolutely positioned at
top-4/right-4 for standard p-6 dialogs; the agent terminal dialog uses
p-0 with a compact py-2 header, so the X floated below the title line.
Render the close control inside the header row instead so it centers
with the title and matches the header's horizontal padding.
2026-07-21 18:24:38 -07:00
OrcaWin 34c160442f Fix headless Linux serve pairing readiness (#9785) 2026-07-21 18:23:20 -07:00
44f77a3b7d [codex] Continue agent work in a new session (#9170)
* feat: continue agent work in a new session

* fix: harden new-session continuation

* fix: source last prompt from provider-authenticated transcript records

Preview user entries can be tool results or harness-injected skill text,
so the continuation prompt's last-prompt hint now comes from the vault
scanner's provider-authenticated lastUserPrompt. Also softens the
continuation instructions for already-complete tasks and adds a cost
warning to the full-transcript option.

* fix: move Continue in New Session row action into the hover group

Edge-usage action; keep the resting row at three icons and reveal it with
the other session actions on hover, matching Resume's gating.

---------

Co-authored-by: jz.feng <jz.feng@aftership.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-21 18:01:31 -07:00
OrcaWin 1fef1e1ddd Relaunch macOS orca serve safely after updates (#9634) 2026-07-21 17:44:40 -07:00
Brennan Benson 71bbfa022d fix(worktrees): bound shared worktree list scans (#9786) 2026-07-21 16:10:01 -07:00
OrcaWinandBrennan Benson 54c1ec5e7a fix(remote-runtime): recover terminals after network loss (#9774)
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-21 15:18:45 -07:00
Brennan Benson e986a7ba1a feat(codex): surface nested subagents (#9637)
* feat(codex): surface nested subagents

* fix(codex): retire child rows on root stop

* fix(codex): preserve nested agent state on restart

* fix(codex): preserve nested agent identity

* fix(codex): preserve subagents across relay restarts

* chore(skills): refresh generated manifests

* fix(codex): keep inferred interrupts terminal

* chore(skills): record latest release snapshots
2026-07-21 15:12:14 -07:00
Brennan Benson e09ed30a0b fix(dashboard-popout): zoom the pop-out window, not the main window behind it (#9769)
* fix(dashboard-popout): zoom the pop-out window, not the main window behind it

The View menu's zoom handlers always sent terminal:zoom to the main
window, so zooming while the dashboard pop-out was focused zoomed the
window behind it. The pop-out also never applied the persisted
uiZoomLevel, so it always rendered at 100% in a zoomed app.

The pop-out now applies uiZoomLevel on dom-ready and follows app-zoom
changes while open (main-window zoom, settings control, mobile ui.set).
Menu zoom routes to the pop-out when it is the focused window, stepping
its own webContents zoom, and a narrow before-input-event handler
resolves the zoom.in/out/reset chords (honoring keybinding overrides)
since the pop-out has no renderer-side shortcut plumbing. The step/clamp
constants move to shared/ui-zoom-level.ts so main and renderer share one
definition.

* fix(dashboard-popout): isolate window zoom

* fix(dashboard-popout): deny unused permissions

* docs(dashboard-popout): clarify wheel zoom path
2026-07-21 14:55:14 -07:00
6458ebcf20 fix(dashboard-popout): wire terminal copy/paste in the popped-out window (#9765)
* fix(dashboard-popout): wire terminal copy/paste in the popped-out window

The Edit menu's Paste is a custom item that routes Cmd/Ctrl+V to the
focused window as ui:appMenuPaste, and only the main window's React root
listens for it — the popout dropped it, so paste silently did nothing.
The copy chord similarly resolves in the main window's before-input-event
handler, which the popout window never registers; the menu's role:'copy'
no-ops on xterm's empty hidden textarea.

AgentTerminalPreview now subscribes to onAppMenuPaste (guarded on focus
inside the preview) and pastes via terminal.paste(), which xterm flags as
user input so the existing preview->PTY routing and main-process input
limits apply. A custom key handler honors the terminal.copySelection and
terminal.paste keybindings, skipping plain Mod+V since the menu
accelerator owns that chord (handling it twice would paste double). The
popout bootstrap fetches keybinding overrides so custom bindings apply.

* fix(dashboard-popout): harden terminal clipboard routing

* fix(dashboard-popout): authorize terminal clipboard text

* test(dashboard-popout): harden terminal paste coverage

* fix(terminal): normalize streamed paste newlines

* fix(dashboard-popout): forward macOS IME native-text commits in the preview terminal (#9771)

* fix(dashboard-popout): forward macOS IME native-text commits in the preview terminal

The preview terminal enables xterm's kitty keyboard protocol (via
buildDefaultTerminalOptions), whose encoder can encode and cancel a
printable keydown before Chromium commits the real IME/native text —
silently dropping macOS input-source commits and synthetic Unicode
injection. Main-window panes guard this with the IME native-text
forwarder; the pop-out preview never installed it.

Install the composition tracker + forwarder (macOS-only, mirroring
TerminalPane) and claim native-text key events at the top of the
preview's custom key handler so the committed glyph reaches the PTY via
terminal.input() and the existing user-input routing.

* fix(dashboard-popout): prewarm IME input source

* fix(skills): preserve released history across new tags (#9778)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

* fix(skills): record latest Linear release history (#9777)

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>

---------

Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-21 14:51:34 -07:00
OrcaWinandOrcaWin 15362fde16 fix(web): preserve paired runtime ownership (#9776)
* fix(web): preserve paired runtime ownership

* fix(web): validate runtime port scan payloads

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-21 17:48:59 -04:00
OrcaWin d8378e8ecf fix(settings): stop mislabeling a WSL default shell as PowerShell (#9779)
* fix(settings): stop mislabeling a WSL default shell as PowerShell

The Terminal settings shell toggle coerced a stored `wsl.exe` default to
`powershell.exe` for display, so a WSL default (set in onboarding) showed
"PowerShell" selected and wrongly surfaced the PowerShell-version options.

Drop the coercion and surface WSL as a disabled, already-selected segment
when it's the active default, so the control reflects the real shell. WSL
stays non-selectable here because choosing it needs a companion distro that
only the onboarding step configures; wiring a full WSL picker into Settings
is left for a deliberate design pass against the per-project runtime model.

* test(settings): cover persisted WSL shell display
2026-07-21 14:43:20 -07:00
Brennan Benson f1c84d3858 refactor(cli): split oversized command modules (#9775) 2026-07-21 13:50:17 -07:00
Brennan Benson 7ca3e670c5 fix(codex): count per-account homes in usage and state removal blast radius (#9763)
* fix(codex): count per-account homes in usage and state removal blast radius

- usage scanner now includes codex-accounts/*/home/sessions so multi-account
  usage is no longer silently undercounted (audit F2)
- account-removal dialog copy now states that session history and MCP logins
  are permanently deleted with the managed home (audit F1 mitigation)

* fix(codex): harden account usage discovery
2026-07-21 13:28:34 -07:00
Brennan Benson a10a2ba53c feat(linear): add MCP-style save issue (#9670)
* feat(linear): add MCP-style save issue

* fix(linear): harden save issue parity

* fix(linear): close save issue contract gaps

* docs(linear): bundle project discovery with save issue
2026-07-21 13:25:22 -07:00
Brennan Benson 87af1c8673 feat(linear): add complete issue relations (#9674)
* feat(linear): add complete issue relations

* fix(linear): harden relation reads and writes

* fix(linear): classify ambiguous relation writes
2026-07-21 13:21:19 -07:00