Commit Graph
8835 Commits
Author SHA1 Message Date
Neil cd92c98b50 Merge remote-tracking branch 'origin/main' into nwparker/sta-4319-ssh-host-key-verification 2026-08-16 15:39:23 -07:00
Brennan Benson 0abbecb17e fix(terminal): keep unresolved snapshot-capability verdicts re-askable (#14676)
* fix(terminal): keep unresolved snapshot-capability verdicts re-askable

A daemon that could not answer the snapshot-capability probe within the
~91s startup ladder had its ptys settled 'no authoritative snapshot'
permanently: settled ids were excluded from every later synchronization,
and the only refresh callers run during renderer startup. Because the
eviction-exemption predicate treats absent/unknown capability as exempt,
one slow daemon start converted every local pty into a permanently
eviction-exempt tab — the hidden-worktree retention budget could free
nothing for the rest of the session (its own degenerate-case log:
'retention force-park freed no panes').

Three changes, none touching the safety direction (unknown still means
exempt, panes stay mounted):
- The retry ladder now decays to a slow 5-minute re-ask instead of a
  permanent verdict, so a recovered daemon is consulted again without any
  event wiring; the synchronization loop's existing timer carries it.
- The ongoing collector now gathers the same fields the startup refresh
  does (layouts, pending reconnect ids), keyed on the sorted id set —
  synchronization prunes cached verdicts outside its collected ids, so
  the narrower collector could evict valid split-leaf answers back into
  the exempt-by-default state.
- The degenerate-case log now carries per-route exemption counts
  (fail-open / foreign-worktree / capability-unknown / split-pane) and a
  matching crash breadcrumb, so field bundles can say which route
  dominates instead of a fourth investigation.

Guard: exemption flips are a pin-unreachable input to the rendered park
verdict, so capability verdicts must only change between commits — the
new react185 harness force-parks a worktree, lets pane mounts write
layouts/titles, flips capability repeatedly, and asserts every flip
settles far from React's nested-update limit.

Gates (previously red): a recovered resolver is re-consulted and the
exemption clears; retained mounted tabs stay bounded by the retention
limit as hidden worktrees accumulate after daemon recovery.

Scope honesty: this bounds an unbounded-by-design retention residual; it
is NOT the fix for the unexplained multi-GB field OOM cluster, whose
retainer remains unidentified (field sessions crash with <= 1 mounted
manager).

* test(terminal): pin the widened capability collector and breadcrumb route buckets

Review found two unpinned pieces of the re-settlement fix: reverting the
ongoing collector to the narrow field set (dropping split-leaf layout and
pending-reconnect ptys, which the sync would then prune back to exempt
unknown) survived every existing test, and the force-park breadcrumb's
route counters had no coverage at all. Both mutations now fail.

* perf(terminal): memoize the capability collect key on its four store maps

The widened collector ran collect+sort+join on every Terminal render and
the memo-dep completeness was unpinned. Memoize the key on the four map
identities (behavior-identical; the string-keyed second memo still damps
layout-only churn) and pin the layouts dep with a staleness test — a
missing dep silently drops new split-leaf ptys from the sync.

* fix(terminal): keep a superseded capability pass's re-ask chain alive

Review loop 2: a synchronization pass cancelled by a newer generation
returned null, which ends the caller's timer chain — but the winner (the
startup refresh) ignores its own return value, so unknowns it leaves
behind had no scheduler left; recovery then waited on an id-set change.
Return 0 instead: the superseded chain re-checks immediately and the
early-outs collapse the re-check when nothing is pending.

Also pin the collect-key memo's remaining deps at runtime (pending
reconnect, tabs, pty ids) — exhaustive-deps is only a warn here, and
loop 1's staleness test covered the layouts dep alone.

* test(terminal): pin the same-identity re-ask path and closed-pty retry prune

Review loop 3: mutation testing showed the chain's own re-ask path was
unpinned — collapsing the first early-out to identity-only (returning null
whenever the live-set reference is unchanged) passed every test, yet it
kills the timer chain after one backoff: the hook refires with the SAME
memoized array, so that mutation is the settled-forever bug in a worse
shape. Pin it at both levels: a module test drives two passes over one
array identity, and a hook test drives the real timer chain through a
backoff refire with no id churn.

Also pin the closed-pty retry prune (its removal survived the battery):
the empty-set pass must return null — a leaked entry keeps a phantom
5-minute timer for the session — and a reappeared id must restart the 1s
ladder rather than resume the decayed cadence.

* fix(terminal): apply recovered snapshot capabilities

* test(terminal): remove stale lint suppression

* test(terminal): clean up capability prefetch hook
2026-08-16 15:39:11 -07:00
Neil 905aa11ea1 fix(ssh): stop a reconnect deleting local state the host has not seen
Reported from a 60-second manual test: reconnect an SSH workspace and the app
drops to the home screen, a second tab running pnpm install is gone entirely, and
one launched agent is listed twice. Three symptoms, one cause.

The snapshot is applied as the whole truth for the reconnecting target. The tab
merge iterates only the host's worktrees, then the result is spread over a gap
where every local tab for that worktree has just been dropped — so a tab created
locally whose upload has not landed has no branch that keeps it. Not a race: it
cannot survive. Same for the pointers, where a snapshot that names no active
worktree nulls activeWorktreeId and activeWorkspaceKey, which is the home screen
while the user's terminals are still running.

So the host is now authoritative for what it knows and not for what it has never
been told. A local tab absent from the snapshot is kept, the worktree union is
used so a snapshot with no entry for it at all cannot erase it, and a null active
worktree only defers to local state when that workspace demonstrably still exists
in the merged result.

Two guards this change had to earn rather than assume. A null activeTabId is NOT
missing information — it is a deliberate deselect that arms the duplicate-tab
repair, and my first attempt defeated it and broke that test; it is honoured
verbatim now. And preserving by tab id alone reintroduces the duplicate agent,
because the host can carry the same session under a new tab id, so the preserve
also checks the remote session id — the identity that survives a tab-id change.

Testing, which is the part that failed here before. Eight tests fail on the
unfixed code and pass on this one, at two levels: the merge decision table, and
the real apply path driven through a store. The end-to-end version of the same
scenario is deliberately NOT the guard and now says so in its header — measured
against unfixed code it only reproduces about one run in three, because the
destruction needs the tab created inside the debounced upload's suppression
window and nothing external can force that. Its earlier green run is exactly why
this shipped.
2026-08-16 15:36:33 -07:00
Brennan Benson f4319ba4f6 fix(crash-reporting): preserve coalesced repeat accounting (#14666)
* fix(crash-reporting): attribute coalesced repeats exactly once

Two ways one burst's suppressed repeats were misattributed in forensics,
found across two adversarial review loops on the sibling-correlation work
but pre-dating it (the coalesce machinery shipped in #8800/#5818/#10729):

- Double-claim: a crash report filed mid-window snapshots the ring, folding
  the suppressed repeats into the emitted crumb — but the next emit still
  re-claimed those repeats in its suppressedSinceLast, reporting one burst
  twice across two crumbs.
- Mirror erasure: a fold resolving onto a re-emitted crumb overwrote the
  count that crumb was born carrying, deleting the previous window's repeats.

Track what each crumb has claimed (carried at emit, resolved by folds) so
every repeat is attributed exactly once. And never resolve into an evicted
crumb: when a storm pushes the burst crumb out of the 30-entry ring — or
past the retained-slot snapshot budget — mid-window, folding there would
mark the repeats claimed by evidence no snapshot can see and the burst
would vanish from the record entirely; drop the handle so the next emit
claims them instead.

Semantic note for trace-mirroring consumers: the returned
suppressedSinceLast is now net of already-folded repeats, so exactly-once
holds over the union of trace spans and report snapshots rather than
within the trace stream alone.

* fix(crash-reporting): preserve orphaned repeat debt on cleanup

* fix(crash-reporting): preserve data-less repeat debt

* fix(crash-reporting): make coalescing window monotonic
2026-08-16 15:34:16 -07:00
Neil b6ea3f17a9 refactor(shell): one portable Unix startup dialect instead of shell detection (#14863)
Orca had to guess which shell would parse a queued command line, then emit
syntax for it. Guessing is unreliable for a remote or WSL host, and every
dialect-dependent function is a place to get it wrong.

Replace the guess. Everything emitted for a Unix shell is now built to be
correct in sh, bash, zsh, dash, ksh and fish alike, so no detection is needed:

- quoteStartupArg emits backslashes as "\\" and apostrophes as "'" between
  single-quoted runs. Both families read that identically, unlike the sh '\''
  idiom, which fish silently halves and which makes a trailing backslash a
  hard syntax error.
- clearEnvCommand emits a self-contained fish/sh branch. It deliberately does
  NOT call a helper defined by Orca's shell wrappers: Orca wraps only zsh, bash
  and fish, so an `sh`/`dash`/`ksh` login shell launches unwrapped — and the
  same text is copied to the clipboard and pasted into shells Orca never
  spawned. In both, a helper would be `command not found`, which is the exact
  failure this exists to avoid. Two guarded statements rather than `A && B ||
  C`, because fish's `set -e` returns non-zero for an already-unset variable
  and would fall through to the sh branch; a trailing `true` pins the status,
  since this is the last statement of a launch line and the prompt renders it.
- One tokenizer for Unix. The input is a settings string the shell never
  parses, so parsing it per-shell only made the same setting mean different
  things in different workspaces.

AgentStartupShell loses its 'fish' and 'unix' members, and the three
login-shell resolvers, the fish tokenizer and the agentEnv.SHELL probe go with
them.

Per-worktree shell history now actually works:

- zsh on macOS was a no-op. /etc/zshrc assigns HISTFILE unconditionally before
  any wrapper Orca controls, so the injected value was already gone — and with
  ZDOTDIR still pointing at Orca's wrapper dir, history landed inside it. The
  intended path rides ORCA_HISTFILE and is restored after user config.
  Fixes #11044.
- fish keeps history in its own data dir keyed by session name, since it
  ignores HISTFILE and has no custom-directory knob. Files are deleted rather
  than truncated, a symlinked ~/.local/share no longer disables cleanup, and a
  GC sweep reclaims orphans whose meta.json is gone. The sweep refuses an empty
  live-worktree set (indistinguishable from a store that failed to hydrate) and
  skips files younger than GC_MIN_AGE_MS, mirroring the tree GC's guard against
  the live-set snapshot race.

Verified against real shells rather than asserted as strings:
startup-shell-portability.live-shell.test.ts runs 194 assertions across
sh/bash/zsh/dash/ksh/fish, and zsh-scoped-histfile.live-shell.test.ts drives a
real login zsh through /etc/zshrc. Both are vacuity-checked. The same quoting
corpus was replayed byte-exact on Linux, where /bin/sh is dash.
2026-08-16 15:28:50 -07:00
Jinwoo Hong 9e3e583a83 feat(relay): prefer the closest available region (#14366) 2026-08-16 13:51:04 -07:00
Jinwoo Hong fa9b20cb41 feat(skills): reland private bundle sharing safely (#14934) 2026-08-16 13:45:54 -07:00
Neil 9f3a912c1e fix(terminal): type Option-composed ASCII instead of reporting it as a chord (#14743)
* fix(terminal): preserve Option-composed ASCII input

* fix(terminal): preserve Option keyboard protocol semantics

* fix(terminal): complete Option keyboard event encoding

* fix(terminal): harden Option input encoding

* fix(terminal): close keyboard protocol fallback gaps

* test(terminal): prove Option-composed ASCII reaches the pty end to end

The Option-compose fix had unit coverage only. This drives a live Electron
pane whose kitty flags are armed by the application's own CSI > 1 u and
asserts the bytes at the pty boundary: composed `@` and Shift-layer `\`
arrive as text, configured Option-as-Alt still reports the layout-resolved
chord, and a non-ASCII glyph still reaches the app as its alt hotkey.
Restoring the pre-fix policy fails exactly the two composed-text scenarios.

Also records the ASCII rule's rationale where the rule lives, not only in a
test comment.

* refactor(terminal): drop the unread Option layers from the layout snapshot

The native helper computed an Option and Option+Shift character for every
key, shipped both over IPC, validated them in the parser and cached them in
the renderer — but no production caller ever asked for them. Only the base
and Shift layers are read, and Shift is the one the web layout map cannot
supply, which is why the helper exists at all.

Removing them halves the helper's UCKeyTranslate work per key and drops the
option parameter that six signatures were threading through for nobody.
2026-08-16 12:49:02 -07:00
Jinwoo HongandOliver Mee 5e9159af16 fix(shell-ready): preserve Bash PROMPT_COMMAND composition (#14619)
Co-authored-by: Oliver Mee <102673257+oliver-mee@users.noreply.github.com>
2026-08-16 12:48:53 -07:00
Jinwoo Hong 3811881410 fix(mobile): present pending Relay fallback accurately (#14922) 2026-08-16 12:38:26 -07:00
Jinjing 3c8410d927 Revert "fix(browser): route every cookie-import write through CDP identities …" (#14942)
This reverts commit bf6dc6fcba.
2026-08-16 12:33:45 -07:00
Jinjing ef1224c4f7 Revert "Preserve OpenCode session across command completion, control SessionS…" (#14943)
This reverts commit 1da1bdc01c.
2026-08-16 12:33:28 -07:00
Brennan Benson bf6dc6fcba fix(browser): route every cookie-import write through CDP identities (STA-4300) (#14729)
* test(browser): repro STA-4300 CHIPS partition downgrade on native cookie import success path

* fix(browser): route every cookie-import write through CDP identities (STA-4300)

* test(browser): cover partition fidelity on both import write paths (STA-4300)

* fix(browser): never stage unreadable cookie partitions (STA-4300)

* fix(browser): gate partition skips on client support (STA-4300)

* test(browser): anchor the client partition-skip capability assertion (STA-4300)

* fix(browser): skip Firefox CHIPS cookies whose Chromium identity cannot be rebuilt (STA-4300)

* fix(browser): tolerate a Firefox schema without originAttributes (STA-4300)

* docs(browser): correct a comment that still named the removed cookies.set write

* fix(browser): block lossy Firefox cookie import on old clients

* fix(browser): read Firefox CHIPS from schema flag
2026-08-16 11:46:45 -07:00
m4air 516a269315 docs: refresh WeChat group 7 QR
Replace the expired community QR. Group number and README copy stay on group 7.
2026-08-16 10:43:53 -07:00
Jinjing b60df2e3d6 Revert "feat(workspace): set project location from the create-worktree host p…" (#14912)
This reverts commit e4e54a17d0.
2026-08-16 10:41:39 -07:00
Jinjing 763b1febeb Revert "feat(skills): add private bundle sharing (#14401)" (#14913)
This reverts commit 757fae28d7.
2026-08-16 10:39:57 -07:00
Jinjing 1da1bdc01c Preserve OpenCode session across command completion, control SessionStart emission (#14866)
* Preserve OpenCode session across command completion

- Add session start events and launch token tracking to establish session boundaries
- Defer retiring launch authority until OpenCode process actually exits, not just when a command finishes
- Fence previous tokens after restarts to prevent status updates from stale sessions
- Maps SessionStart as a session boundary for proper turn/state management

* Emit SessionStart only from OpenCode, not mimo-code

Restrict SessionStart lifecycle events to OpenCode exclusively. Mimo-code no longer emits SessionStart, as it should rely on OpenCode for session boundary signals. This prevents duplicate lifecycle events that could interfere with pane authority tracking and session state management. Also tighten foreground process result validation to reject stale results after title observation changes, fixing a race where a delayed foreground read from a previous cycle would incorrectly retire authority.
2026-08-16 10:01:27 -07:00
Neil e4e54a17d0 feat(workspace): set project location from the create-worktree host picker (#14868)
* feat(workspace): set project location from the create-worktree host picker

Hosts that still say "Project location not set" now get an inline Set location action. It opens a nested dialog over Create worktree so the in-progress form stays put.

* fix(workspace): replace unset-location status copy with a button

Drop the redundant "Project location not set" caption and show a Set project location action with a hover tooltip instead.

* refactor(workspace): tighten the set-project-location dialog

- reuse CreateProjectParentBrowser instead of a second host-filesystem browse view
- drop ProjectLocationBrowseTarget; parseExecutionHostId already models it
- single setLocation path in RunTargetCombobox (row, button, Enter)
- memoize the default clone URL instead of scanning on every store update
- fix missing required props in the new composer-card test

* fix(workspace): close the correctness gaps in set-project-location

- Escape in the host browser now backs out to the form instead of dismissing
  the dialog and discarding the half-filled path/clone URL. Radix dismisses
  from a document-capture listener, so only preventDefault can stop it.
- Drop the stopPropagation guards: window-capture (the composer's Escape
  handler) already ran by then, so they never protected it — the nestedDialogOpen
  gate does. They did silently kill RemoteFileBrowser's own key handling.
- Hide Set project location for host-local repo:<id> projects (folder projects,
  git repos with no remote). Linking on another host matches by project identity,
  which those have none of, so the call could only ever toast an error.
- Drop a standalone placeholder setup once a repo projection covers the same
  project+host, restoring the (projectId, hostId) uniqueness invariant. Setting a
  location on a host with a pending setup was leaving a ghost that sorts first and
  reads back as 'not set up'.
- Existing-folder submit label matched a catalog string reading 'Importing...'

* test(composer): follow the renamed local in the host-retarget source assertion
2026-08-16 03:30:47 -07:00
Jinwoo HongandE2E Test 757fae28d7 feat(skills): add private bundle sharing (#14401)
Co-authored-by: E2E Test <e2e@test.local>
2026-08-16 02:36:18 -07:00
Jinjing 1b6d2403cb ci: run full e2e against the daily cut commit (#14870)
After a live daily publish, dispatch e2e.yml at the cut SHA. Detached on
purpose so a red suite cannot fail or delay the signed daily.
2026-08-16 01:57:01 -07:00
Jinjing 5c56bfb28b ci: run the daily macOS build 4 hours later (#14869)
The 14:15 UTC cut is too early (6:15am PST / 7:15am PDT). Move it to
18:15 UTC so dailies land late morning Pacific instead.
2026-08-16 01:52:58 -07:00
Neil fd1dba9db9 fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal (#14848)
* fix(daemon): validate spawn cwd asynchronously so one dead share cannot freeze every terminal

createOrAttach validated the working directory synchronously on the daemon's
only thread. Measured on Windows 11 + Ubuntu-24.04:

  existsSync on an unreachable UNC share   21,022 ms
  wsl.exe probe, cold distro                1,266 ms
  wsl.exe probe, warm distro                   59 ms
  existsSync/statSync on healthy \\wsl.localhost  4 ms / 1 ms

A single unreachable share therefore blocks the whole RPC loop past the
client's 30s request ceiling, so every other terminal stalls behind it and
reports `DaemonProtocolError: Request createOrAttach timed out after 30000ms`.
The main process already validates asynchronously and passes prevalidatedCwd
(ipc/pty.ts); the daemon never got the same treatment.

Add validateWorkingDirectoryAsync (one stat, not exists-then-stat, so an
unreachable share is not paid for twice) and await it from the daemon spawn
preflights. spawnSubprocess now returns SubprocessHandle | Promise<...>, which
existing sync stubs still satisfy.

Deliberately not bounding the stat with a timeout: the 30s ceiling comes from
blocking the shared loop, not from the duration. A timeout cannot tell "slow
share" from "gone share", so it would fail spawns that succeed today at 3-8s
on a cold VPN mount, and trade an accurate "working directory does not exist"
for a guess.

The new await opened a race: it sits between the "already exists?" check and
the sessions.set that publishes the session, so two concurrent creates for one
session id both spawned. Gate creation per session id; distinct ids still spawn
in parallel.

STA-4470

* fix(daemon): fence async spawn lifecycle
2026-08-16 01:16:11 -07:00
Jinjing 2f4e9aed70 fix(worktrees): address review feedback on the worktrees slice split (#14669)
* fix(worktrees): address review feedback on the worktrees slice split

Follow-ups to #14643. All of these predate the split (the constructs moved
verbatim from worktrees.ts), so they are behavior fixes, not refactor fallout.

- fetchAllWorktrees: route runtime-scope-forbidden errors through the toast in
  the hydration path, matching the fast path
- updateWorktreeMeta / ensureHostedReviewPushTarget: use
  trySettingsForWorktreeOwner so an ambiguous owner skips push-target resolution
  instead of throwing past the { ok, error } contract
- updateWorktreeMeta: compare linkedPR with ?? null so an unknown worktree does
  not trigger a spurious hosted-review refresh (matches the four sibling providers)
- persistWorktreeMeta: localize the two runtime-capability messages
- buildWorktreeRenameState: fix a doc comment that contradicted the code
- setWorktreesPinnedAndReveal: skip updateWorktreesMeta on an empty map
- pruneLastVisitedTimestamps: treat an empty worktreesByRepo list as unhydrated,
  and clear activeWorkspaceKey alongside a stale activeWorktreeId
- forceDeletePreservedBranch: fail closed when several retained cleanups match
  and no host was given, rather than routing to the active runtime
- purgeOrphanedRuntimeSshProjects: drop blank target ids so a repo with no
  connectionId cannot match
- markWorktreesDeleting: make the skip guard phase-aware so queued rows are
  promoted to deleting
- worktree purge: validate right-sidebar tabs with normalizeRightSidebarRoute so
  pr-checks and plugin panels are not silently dropped

* test(worktrees): add edge case tests from review feedback

Add defensive tests covering phase-aware deletion state transitions, tab
preservation across worktree purges, graceful handling of ambiguous owner
scenarios, and stale workspace cleanup. These catch edge cases that could
lead to silent failures or invalid state transitions.

* fix(worktrees): preserve workspace key pointing at live worktree

Replace overly broad scope check with direct equality check, so pruning
only drops the stale worktree's own workspace key while preserving keys
pointing to other live worktrees or folder workspaces.

* fix(worktrees): clear a legacy unprefixed active workspace key on prune

pruneLastVisitedTimestamps only dropped the `worktree:`-prefixed derived key,
so sessions predating the prefix kept a phantom workspace selected. Match the
purge path, which already treats a bare worktree id as a workspace key.

* chore(i18n): re-key the two runtime-capability strings to the canonical scheme

The keys added when persistWorktreeMeta was localized were hand-written. Derive
them the way localize-renderer-strings.mjs does — module path plus
sha1(path:text) — so a drift audit against the generator does not flag them.

* fix(worktrees): stop updateWorktreeLineage rejecting past its never-rejects contract

The sidebar's "Remove parent link" (WorktreeContextMenu.handleRemoveParentLink)
awaits updateWorktreeLineage in a bare `void Promise.all(...)` with no catch, so
the action must always resolve. Two paths broke that:

- settingsForWorktreeOwner ran outside the try and throws on an ambiguous owner.
  Skip with warnAmbiguousOwnerOnce instead, matching the ensure-push-target fix.
- the recovery refresh inside the catch has no internal try/catch, so a failing
  lineage RPC rejected out of the handler meant to absorb the failure.

assignWorktreeParent keeps rethrowing (both its callers catch and toast), but its
recovery refresh is now best-effort too so it can't mask the original cause.

Also: localize the new ambiguous-host preserved-branch throw (it reaches a toast
description; the throw above it stays verbatim because it mirrors a main-process
message), and drop blank SSH target ids once so both purge lookups agree.

* fix(worktrees): surface un-nest failures instead of silencing them

Supersedes the approach in 789edeaa7f. That commit fixed the unhandled rejection
in WorktreeContextMenu.handleRemoveParentLink by making updateWorktreeLineage
never reject, which had two costs:

- it made the .catch in use-lineage-drop-commit unreachable, so a drag-unnest
  failure went from an error toast to a silent no-op; and
- trySettingsForWorktreeOwner returns null for BOTH 'ambiguous' and 'missing'
  route resolutions (worktree-operation-route.ts collapses the three-way result),
  so the skip also swallowed unresolvable-route cases while logging "identity is
  ambiguous", which is the wrong cause for a legacy row on a multi-runtime setup.

Fix the caller instead: updateWorktreeLineage keeps rejecting, and both call
sites go through a shared unnestWorktrees() that catches and toasts. The
best-effort recovery refresh from 789edeaa7f is kept — that part was correct and
stops a failing refresh masking the original error on the assign path.

unnestWorktrees also de-duplicates the identical console.error/toast pair the two
callers had, and reuses the existing failedUnnestWorkspace key (already
translated in all five locales), so no new string ships.
2026-08-16 01:00:36 -07:00
Jinjing c066a557ea fix(persistence): address review feedback on extracted modules (#14862)
Follow-ups to the module extraction (#14252):

- Guard the lineage companion maps in worktree meta GC, and clear them
  alongside a corrupt worktreeMeta so stale rows can't re-attach.
- Repair a renamed worktree's stale lineage.worktreeId and flag the save.
- Key the git-username cache by execution host + path so the same checkout
  path on local/SSH/runtime hosts can't cross-hydrate usernames.
- Drop undefined keys before the automation update spread; a Partial with
  an explicit undefined blanked stored values.
- Normalize pane identities in every workspaceSessionsByHostId partition,
  not just the legacy blob, and use the merged leaf maps for lease and
  acknowledgement remapping.
- Reject duplicate preferred leaf ids so two panes can't collapse onto one
  pty/buffer/scrollback key.
- Let an explicit rightSidebarExplorerView outrank the legacy search-tab
  fallback; the legacy migration now runs on the raw payload at load.
- Record why the mobile pairing migration leaves its sources in place.
- Trim persisted folderPath, normalize synthesized worktree visibility
  preferences, validate notification settings field-by-field, strip
  undefined optional keys before the SSH lease merge, reuse `now` for an
  automation's updatedAt, and rename the pane alias registrar.
2026-08-16 00:36:21 -07:00
Jinjing 470ef65bd7 fix(editor): address review feedback on the split editor slice (#14850)
* fix(editor): address review feedback on the split editor slice

- Localize the conflict-placeholder guidance string (en/es/ja/ko/zh).
- Filter target-worktree tabs by migrated tab id so an owner transition
  cannot leave two tabs sharing one id.
- Resolve a pending editor reveal by fileId first; the oldFilePath scan
  could pick another worktree's rekey.
- Return before opening a workspace editor item when conflict metadata
  is missing, so no tab is created for a file that never entered openFiles.
- Skip a persisted open file whose resolved id is already used; the
  session schema allows repeated (path, worktree, runtime) tuples.

* fix(editor): remove migrated tab ids from sibling groups

When tabs migrate to a target group during editor owner transition, the
same tab IDs can be left in sibling groups, causing state corruption.
Strip these IDs from all sibling groups to ensure each tab ID exists
only once across the editor layout.
2026-08-16 00:27:01 -07:00
Neil 6cf6a7faff feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK (#14823)
* feat(crash-reporting): capture Crashpad minidumps and name the failing CHECK

40% of renderer deaths report exit 0x80000003 (STATUS_BREAKPOINT) — a Chromium
CHECK/DCHECK — and we captured only the exit code, so the cause was structurally
unknowable. Nothing in the tree wired crashReporter at all.

Start Crashpad pre-whenReady and lift the text signature out of the dump:
Chromium stores the fatal log line in the LOG_FATAL annotation, so the check
name, file and line are recoverable with no symbols and no minidump_stackwalk.

Upload stays off. The existing transport is a user-initiated 4 MiB text bundle;
raw dumps are multi-MB binary carrying process memory. Dumps stay on disk and
only the signature rides the existing crash-report flow.

- minidump-stream-reader: bounds-checked view; a truncated dump degrades
- minidump-crashpad-annotations: allowlisted keys (switch-N carries command lines)
- minidump-crash-signature: LOG_FATAL -> file/line, exception, faulting module
- crashpad-capture: polls for the dump, which races process-gone delivery

STA-4469

* refactor(crash-reporting): claim dumps once and match them to the dead process

A newer dump from a different process could be paired to the wrong report, and
two reports in one crash burst could both claim the same file. Match the dump's
own ptype against the process Electron said died, and claim each dump once.

Also let the fatal line use the stack-length budget: a CHECK message truncated
at 240 chars can lose the condition, which is the diagnosis.

Fix the child-type test to match the classifier: GPU exits are recoverable
churn and never become reports, so they must not burn a dump poll either.

STA-4469

* fix(crash-reporting): recover real Electron CHECK logs

Electron 43 Windows dumps carry the Chromium CHECK line in captured memory but omit the claimed LOG_FATAL annotation. Recover only bounded Chromium-formatted fatal/CHECK lines, and prune raw dumps after crash events so suppressed child crash storms stay within the 128 MiB budget.

* fix(crash-reporting): satisfy not-found lint
2026-08-15 23:45:32 -07:00
Neil eb3f6838af perf: coalesce git upstream status reads (#11697)
* perf: coalesce git upstream status reads

* fix(git): repair upstream lease key imports and guard its field list

The read owner imported the shared/types barrel deleted by #14447, and its
push-target key hand-enumerated fields, so a new GitPushTarget field would
silently share a lease between two different targets. The destructure now
fails to compile if a field is added. Lease tests moved into their own file
after #14728 split ssh-git-provider.test.ts.

* test(git): enforce native upstream coalescing in CI

The 10-caller benchmark only runs under ORCA_GIT_UPSTREAM_COALESCING_BENCH_JSON,
so nothing in CI failed when the native/WSL lease was bypassed. Route status.ts
through invalidateGitUpstreamStatusReads so the export has a production caller.
2026-08-15 23:24:05 -07:00
Neil 8dc29a5be6 ci: render LoC signs Huge bold (#14855) 2026-08-15 23:23:47 -07:00
Neil ac48d753a7 ci: color added/deleted LoC counts in PR summary (#14839)
* ci: color added/deleted LoC counts in PR summary

* ci: use GitHub color-swatch dots for added/deleted LoC counts

* ci: color LoC counts with LaTeX textsf

* ci: bold LoC counts; render zero in white

* ci: render LoC counts large bold sans-serif

* ci: use bold math font for LoC counts

* ci: color only the + and - signs on LoC counts
2026-08-15 23:18:20 -07:00
Jinjing 31e9f4af30 Refactor: split editor.ts into modular state actions (#14847)
* refactor: split editor.ts under 400 lines

Move editor slice types, file-id/tab helpers, and action factories under
src/renderer/src/store/slices/editor/. The source file is now a public
barrel. No intentional behavior change.

* refactor: split editor-chrome-slice into state modules

- Move EditorDraftState, ExplorerDirState, and RightSidebarState type definitions into their respective action files
- Simplify state creator return types from Pick<EditorSlice, ...> to specific state types
- Improve modularity by colocating types with implementations
2026-08-15 22:14:37 -07:00
Neil 1464cec0f9 test(ssh): separate the reconnect paint guard from the tab-destruction bug
The paint guard was failing about two runs in three, and after the merge every
run, for a reason that has nothing to do with painting. It staged its full-screen
check in a tab it had opened AFTER a reconnect — which is exactly the tab an
unrelated session-sync bug destroys on the NEXT reconnect. Two independent
failures were riding on one assertion, and the one that fired was not the one the
spec is for.

Running top in the ORIGINAL tab fixes it. That tab predates every reconnect, so it
is in the host snapshot and survives. No assertion changed, none were weakened,
and the new-tab case simply moves after the full-screen case rather than before
it — it still opens its tab after a reconnect, which is the regression it exists
to cover. Five consecutive runs pass at ~13s, against three in eleven before.

The bug itself is not swept up. ssh-reconnect-tab-destruction.spec.ts records it
as a fixme with the mechanism written down: session-merge spreads the host tab
list over the local one, so a local tab missing from the host snapshot has no
surviving branch, and the upload that would have put it there is dropped rather
than deferred inside the 1s window after a snapshot apply. It is worse than a
vanishing tab — the tab bar keeps rendering it, correctly titled, while the
terminal slice has dropped it and no pane manager exists, so the user clicks a
selected tab that never paints, with no error and no recovery, while the process
runs on untouched.

fixme rather than a workaround because waiting for the upload would hide it, and a
user opening a tab right after a reconnect has no such signal to wait on. It is
pre-existing: none of the four files in that path is touched by this branch, and
nothing in the merge range touches them either.

Also lifts openTerminalTab into a shared helper, since both specs need it and the
group argument it must pass is the kind of thing worth stating once.
2026-08-15 21:45:00 -07:00
Jinjing 8b04e060fa refactor(persistence): extract modules to half persistence.ts (#14252)
* refactor(persistence): extract modules to half persistence.ts

* refactor(persistence): tighten the extracted operations seam

Review follow-ups on the module extraction, all behavior-neutral.

The extracted operations read and mutate the Store's state object in place, but
every seam typed it as a bare PersistedState, so nothing at the boundary said a
caller must pass the live reference — a future caller handing over a clone would
have its writes silently dropped. Name that contract: StoreOwnedPersistedState
carries it to every operations interface and every mutating free function.
normalizePersistedPaneIdentityState and backfillFolderScopeConnectionIds stay on
PersistedState; they build a fresh state rather than mutating the Store's.

The six *PersistenceOperations wrappers were constructed per delegate call. They
are stateless today, so this was inert, but any future instance state would be
lost between calls. Memoize them, and mark state and gitUsernameCache readonly
so the compiler enforces the single-assignment invariant memoizing them relies
on.

Also: restore flushSshPtyConsumerRecovery, whose inlining left its rationale
duplicated at both call sites; document that migrateWorktreeIdentity's boolean
gates the caller's save, since the extracted function kept no docs of its own;
and merge a duplicate shared/types import that was failing lint under
--deny-warnings.

* delete plan doc

* refactor(persistence): add error recovery and improve field cleanup

- Rollback failed migrations to prevent corrupted state that blocks retry
- Gracefully skip malformed entries in normalization instead of aborting
- Strip retired fields to prevent orphaned state and sync issues

* refactor(persistence): drop the redundant persistence- filename prefix

The extracted modules already live in src/main/persistence/, so name
them after the domain they own. Point leftover shared/types imports
at the real type modules while touching those files.

* refactor(persistence): optimize lookups and fix unsanitized updates

- Use Maps instead of repeated array searches for O(1) lookups
- Apply sanitized updates instead of raw input in ui-state-update
- Compare fields directly rather than JSON strings to avoid false dirty states from persisted key ordering differences

* refactor(persistence): group modules into lifecycle folders

Move the 42 flat persistence modules into six folders named for what the
module does, and lift the Store class out of the barrel so persistence.ts
becomes an 8-line public surface.

Bodies are unchanged: every moved file diffs clean against HEAD once import
blocks are excluded. Only import specifiers were rewritten, by resolving each
one to an absolute path and mapping it through the move map.

Store keeps its existing max-lines suppression; its baseline entry is repathed
rather than re-added. Its 119-method public API sets a ~525-line floor, so it
cannot meet the 400-line cap without breaking the API for 153 importers.

* Sanitize worktree visibility sources and preferences on hydration

Ensure invalid or corrupted data from disk (untracked whitespace,
relative paths, bogus preference values) is cleaned during load
rather than corrupting the in-memory store.
2026-08-15 21:41:01 -07:00
Neil ae456c891c chore(e2e): add a typecheck entry point for tests/, unenforced for now
tests/ has never been typechecked. That is how a spec could read target.targetId
off a type with no such field, and call a function without its required argument,
while the suite reported green — the spec was skipping, so nothing ever
disagreed with it.

Pointing tsc at tests/ finds both immediately. It also finds ~198 errors across
~94 files, which is a cleanup project rather than a change to make here, so this
ships as pnpm typecheck:e2e and is deliberately NOT added to the typecheck chain
or to CI. An unenforced script is worth less than a gate, but it is worth more
than nothing: it is runnable, it is discoverable, and the header says plainly
what it is so nobody mistakes it for coverage we have.

runtime-types.ts is the one fix included, because it was actively misleading:
every PaneManagerLike method was optional, so every call site was a
possibly-undefined invocation that TypeScript could not help with. They are real
methods on a real instance. Also widens AppStore to the StoreApi that
window.__store actually is.
2026-08-15 21:39:18 -07:00
Neil c8994323d5 test(ssh): make the reconnect spec actually run — it now fails on a real bug
It had never executed once. The CI condition that enables Docker-SSH was gated on
an unrelated spec, so this skipped and reported green — and running it for the
first time found two bugs in the spec itself, both of which a typechecked tests/
would have caught instantly.

startDockerSshRelayTarget returns a DockerSshRelayTarget, which has no targetId;
the id comes from connectDockerSshRelayTarget's return value, which the spec
discarded. So every reconnect call passed undefined and the relay answered
'SSH target "undefined" not found'. And openNewTerminalTabInActiveWorkspace takes
the group to open into; called with no argument the new tab lands nowhere.

The third problem was the fixture rather than the spec. The image ships Debian's
/etc/bash.bashrc with the xterm title block commented out and an all-comments
/root/.bashrc, so its shell never emits OSC 0 — which is what Orca derives a tab
title from. The title assertion could not have passed for any shell, healthy or
not, so it was proving nothing. enableDockerSshRelayTargetShellTitle opts a spec
into the title-setting PS1 a real user's shell already has.

IT STILL FAILS, and that is the point: it fails on a PRODUCT bug it was written to
catch. An SSH reconnect destroys the terminal state behind a tab whose local
creation has not yet reached the host. remote-workspace-session-merge.ts:86-89
spreads the host's tab list over the local one for that worktree, so a local tab
missing from the host snapshot has no surviving branch; the upload that would have
put it there is DROPPED rather than deferred inside the 1s suppression window
after a snapshot apply. The tab bar still renders the tab, correctly titled, but
the terminal slice holds one tab and no pane manager exists for the second — so
the user clicks a tab that never paints, with no error and no recovery, while the
process keeps running on the host.

Pre-existing: none of remote-workspace-target-sync.ts,
remote-workspace-session-merge.ts, use-app-session-persistence.ts or
remote-workspace-snapshot-apply.ts is touched by this branch, and nothing in the
merge range touches them either.

Not worked around here. Waiting for the upload would hide it, and a user opening a
tab right after a reconnect has no such signal to wait on.

An earlier version of this message claimed the spec passes. It does not; I had
seen five green runs out of six and generalised from them. Sustained runs are
about three in eleven before the merge and zero in four after.
2026-08-15 21:39:09 -07:00
Neil 7215734088 ci(e2e): run the Docker-SSH specs when SSH SOURCE changes, not just specs
The earlier fix only helped when a spec file itself changed. Edit pty-connection,
pty-handler or ssh-relay-session and touch no test — which is what every one of
these regressions actually looked like — and the lane still did not run.

pr.yml now maps SSH source paths onto five Docker-backed specs. Five rather than
all fifteen because the rest are covered by unit tests that prove the same source
without paying for a container; that is a deliberate narrowing and this comment is
where it is admitted rather than left implicit. Test files are excluded from
triggering, since they prove themselves.

Simulated against the real paths this PR touches: pty-connection.ts,
pty-handler.ts, ssh-relay-session.ts and ssh-pty-session-reattach.ts all now pull
the SSH specs in, while pty-connection.test.ts, ssh-known-hosts.test.ts,
SshTargetCard.tsx and README.md correctly do not. The gate contract test covers
the mapping: 11 pass.

e2e.yml pays for it — 30 to 45 minutes, because the lane can now build a container
image and run SSH specs serially on top of whatever changed — and installs
openssh-client, which the fixture shells out to and which the lane did not need
back when it never received these specs.
2026-08-15 21:33:49 -07:00
Neil d20c11ce7d docs(ssh): the root cause — checkpointed recovery never runs on a reconnect
Chasing why the pane-retry path could not request source recovery turned up the
real answer: nothing can. Recovery is dead on every SSH reconnect, and the byte
tail is not a fallback but the only path that has ever run.

Five links, each read rather than inferred. setWrite reuses primaryClient
including its id, so a reconnected client presents the SAME clientId. activate()
tests exactly that at line 99 and returns 'existing' at 108, which makes the
rotateDelivery branch at 118-142 reachable only when the ids differ — never here.
So no sourceRecovery comes back, so finishSourceRecovery fails its
!pendingRecovery guard and abandons, cancelling the delivery and deleting the
checkpoint. The pane retry then opens fresh and takes the tail.

This also explains the blank panes exactly. The relay concluded that this client
already held the stream because, by its own identity rule, it does.

The fix that implies is smaller than anything proposed so far and avoids what
sank the three earlier attempts: bump a transport generation on the client record
in setWrite and compare it alongside clientId, so a reconnect rotates the delivery
instead of matching as 'existing'. Deliveries still outlive their clients and
nothing retires on onClientDetached — the rotation happens on re-attach, which is
what the recovery design already intends. RequestContext, setWrite and the
publication are all relay-internal, and client and relay ship in one build, so
there is no wire change and no compatibility exposure.

Left explicitly unverified: whether rotateDelivery's identity preconditions hold
at that moment, whether outputFlowControl is granted on the reconnected session,
and what a rotation gives the RENDERER — which still remounts an empty xterm and
needs a screen, not a post-checkpoint delta. Recovery keeps main's model whole; it
does not by itself repaint a fresh terminal, so the tail may still be wanted for
the pane even once the model stops going stale.
2026-08-15 21:23:14 -07:00
Neil a74c124712 Merge origin/main into the SSH host-key + reconnect branch
One conflict, and it was structural rather than textual: main split
ssh-connection.test.ts from 2904 lines into seventeen files with a shared harness
and fixtures (#14728), taking it off the max-lines suppression list, while this
branch had added host key verification tests to the old monolith.

Resolved by adopting main's split rather than reinstating the big file, since
restoring it would put back the suppression the refactor just removed. The seven
host key tests move to ssh-connection-host-key-verification.test.ts unchanged,
including their comments — they are the reasoning, not decoration.

The load-bearing piece was the ssh2 mock, which now lives in the shared harness
and is therefore used by all seventeen files. It has to keep the callback form:
ssh2 calls hostVerifier(key, verify) and only accepts synchronously when the
return is not undefined, so the old single-argument mock would pass against a
verifier that never decides — exactly the regression this work exists to prevent.
It also has to abort on denial, or a refused key looks like a successful connect.
Defaulting the presented key to a valid ed25519 blob keeps the other sixteen
files unaffected, which the full run confirms: 1549 passed, 13 skipped, zero
failures, typecheck clean.

The one remaining caller of the old signature — the obsolete-generation
fingerprint test — moved to real key blobs, because the verifier now identifies a
key before recording a fingerprint and a placeholder string would be refused
before reaching the generation check that test covers.
2026-08-15 21:17:46 -07:00
Neil f1be93c669 docs(ssh): record why the obvious reconnect follow-up does not work
I proposed making the pane-retry path request source recovery the way
reattachKnownPtys does, and argued it was probably client-side routing. Tracing it
says the wiring is indeed trivial and the checkpoint state does survive a drop —
and that the change would still be wrong three ways, one of them harmful.

The relay short-circuits to 'existing' on a same-clientId attach BEFORE it looks
at the recovery argument, and the reconnecting client has already rotated the
delivery onto its id. A failed reattachKnownPtys then deletes the checkpoint on
purpose, so a later pane retry presents checkpointUnavailable, which becomes
restoreRequired and then SSH_SESSION_EXPIRED_ERROR — trading a blank pane with a
tail for a killed session. And the payloads answer different questions anyway:
recovery replays the post-checkpoint delta to keep main's model whole, while the
tail is a screen snapshot for a fresh empty xterm. Even a successful recovery
would put almost nothing in a remounted pane.

Also corrects the argument I had been leaning on hardest. "Old relays ignore
requireReplay, so those users still get blank panes" is false for the SSH relay:
the client deploys its own relay into a version-scoped directory and rejects any
grant whose serverBuildId differs, because client and relay ship in one build.
Mixed versions cannot occur on this channel. The independent-update rule still
governs remote runtime hosts, just not this one — so there is no stranded
population, and the urgency that framing created was imaginary.

What replaces it is a sharper question. Source recovery is gated on
outputFlowControl and on the client presenting a NEW clientId. We have empirical
evidence it does not: the blank-pane bug existed because the relay concluded this
client already held the stream, and the shipped fix works by bypassing that exact
early return. If the id is reused, reattachKnownPtys' recovery hits the same
short-circuit — meaning checkpointed recovery may never have run for SSH
reconnects, and the tail is not a fallback but the only path. Whether that is so
turns on daemon versus stdio-primary relay mode, which I did not verify and which
decides whether the work is "extend recovery" or "recovery has never run here."
2026-08-15 20:58:22 -07:00
Neil 1f483c3e4e fix(tab-bar): exclude disabled agents from new-tab search (#14824) 2026-08-15 20:28:21 -07:00
Neil 306c5d545f refactor(source-control): split the dropdown action resolver under the max-lines budget (#14835)
`source-control-dropdown-items.ts` was 524 counted lines behind an
`eslint-disable max-lines`. It splits along the seams the resolver already had:

- `source-control-dropdown-item-types` — the row union, consumed by CommitArea,
  the composer and the action dispatcher without pulling in the state machine.
- `source-control-dropdown-labels` — count/label/title wording.
- `source-control-dropdown-action-context` — the branch, upstream and review
  facts every row reads, derived once so rows cannot disagree about them.
- `source-control-dropdown-commit-items` / `-remote-items` / `-review-items` —
  the three row groups, each keeping its own disabled-reason ladder intact.

`resolveDropdownItems` is now just the entry order plus the conflict-abort and
hosted-review-busy passes.

Verified output-identical to the pre-split resolver: a differential harness ran
both implementations over 16,380 generated input combinations (every upstream
shape × PR state × conflict operation × blocked reason × staged count ×
provider) and compared entries deeply. The harness was scaffolding and is not
committed.
2026-08-15 19:44:02 -07:00
Neil 73aa5d0ca7 refactor(daemon,runtime): split daemon, pty and rpc modules under the max-lines budget (#14834)
Splits the nine oversized modules in the daemon/provider/runtime domain into
focused per-concern files and drops their max-lines baseline entries.

- daemon: `Session` decomposes into an output plane (emulator, pending-output
  buffer, client fan-out), a producer-pause controller, a shell-ready barrier and
  a termination controller; `DaemonClient` into socket connect, hello handshake,
  ndjson readers, pending-request settlement, listener registry and notify
  settlement; `daemon-health` into pid-file parsing, process identity,
  stale-kill, TCC attribution and bundle staleness; `shell-ready` into the marker
  constant and the bash/zsh rcfile generators.
- providers: local-pty shell-ready wrapper generation, wrapper root, startup
  command and bash rcfile split out of local-pty-shell-ready.
- runtime: `Coordinator` sheds DAG convergence, decision gates, escalation
  triage, the runtime contract, the stale-base flag and task dispatch; the
  files/git/github rpc modules split into per-domain method groups.

Behavior-preserving: the extracted units keep their original construction order,
guards and timer lifetimes, and every RPC method name is still registered.
Test `vi.mock` surfaces were re-partitioned to follow the moved symbols.
2026-08-15 19:36:03 -07:00
Neil 6854cceb90 refactor(panes,tabs): split pane manager and tab-group modules under the max-lines budget (#14760)
The two tab-group hooks, the pane manager, worktree activation, and the terminal
pane context menu each carried a file-level `eslint-disable max-lines` and ran
461-745 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all five suppressions and prunes their entries
(341 -> 335).

Pure move, no behavior change. useTabDragSplit is cut into gesture lifecycle,
hover preview and drop commit; useTabGroupWorkspaceModel into item projections
plus the tab-close, close-scope, activation and creation command sets; the pane
manager into host, tree mutations, pane creation, drag wiring, reparent frame
tracking, layout sweeps and rendering diagnostics.

react-hooks exhaustive-deps stays at zero warnings, matching HEAD. Dependency
additions are only stable identifiers -- refs and callbacks that became
parameters -- and no `.current` dereference was added to any dependency array.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the three remaining failures are pre-existing load flakes in untouched files,
each green when re-run serially), no new runtime import cycles among 1020
modules, no barrel files, and no lint suppression added anywhere.
2026-08-15 19:10:36 -07:00
Neil cc19692f93 refactor(editor): split Monaco, autosave and notebook modules under the max-lines budget (#14748)
The four editor modules, the diff-comment decorator and the file-type icon table
each carried a file-level `eslint-disable max-lines` and ran 319-806 counted
lines against 300/400-line budgets. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all six suppressions and prunes their entries (341 -> 334).

Pure move, no behavior change. MonacoEditor is cut along its own seams -- mount,
input bindings, markdown annotations, decorations, content sync, view-state
persistence and reveal scheduling -- with the markdown overlay becoming its own
component. useEditorPanelContentState splits into file and diff content loaders
plus the active-tab load and reload triggers.

When the mount module came in at 370 counted lines, over the 300 ceiling, it was
split again into its parameter types and its input bindings rather than carrying
a suppression.

Hook usage is identical to HEAD across all three React split families: the same
counts of every hook type between each original and its extracted modules, so no
hook was added, dropped, or converted to a plain function. react-hooks
exhaustive-deps stays at zero warnings, matching HEAD.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the four remaining failures are pre-existing load flakes in untouched files,
each green when re-run serially), no new runtime import cycles among 884
modules, and no lint suppression added anywhere.
2026-08-15 19:02:11 -07:00
Neil 98450718f7 refactor(right-sidebar): split file explorer and sidebar under the max-lines budget (#14741)
The six right-sidebar modules and the remote file browser each carried a
file-level `eslint-disable max-lines` and ran 347-797 counted lines against
300/400-line budgets. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all seven
suppressions and prunes their entries (341 -> 333).

Pure move, no behavior change.

Two renderer-specific hazards were found and fixed rather than shipped.

First, effect and ref LIFETIME. FileExplorer's `if (!worktreePath) return` sits
above the files pane, so moving the worktree-reset effect into that pane made its
guard ref `lastResetWorktreePathRef` die on any render where worktreePath was
transiently null (workspace-list refresh, store rehydrate, remote worktree
reload). On remount the guard read null, so the reset fired even when returning
to the SAME worktree -- wiping dirCache, collapsing every expanded directory,
clearing the name filter and undo history, and forcing a full re-read over SSH.
The tree-load effects now live in a hook called from FileExplorer above the early
return, and the pane is purely presentational with zero hooks. That also restores
the original parent-effect ordering, which had shifted because React flushes
child effects before parent effects.

Second, extracting a hook silently degrades dependency analysis: `setX` setters
that the linter knew were stable when created locally become opaque parameters,
producing 8 new react-hooks/exhaustive-deps warnings where src/renderer had zero.
Those are fixed by listing the genuinely stable identifiers (useState setters and
ref OBJECTS). No `.current` dereference was added to any dependency array, since
that would change callback identity as the ref mutates.

Verified: oxlint clean with exhaustive-deps back to zero, ratchet passes,
typecheck clean, full unit suite green on the first pass, no new runtime import
cycles, no lint suppression added, and hook usage identical to HEAD across both
split families.
2026-08-15 18:48:23 -07:00
Neil 97b71c2285 refactor(usage): split AI-usage scanners and stores under the max-lines budget (#14668)
The three usage scanners and their stores, plus the renderer usage-overview
model, each carried a file-level `eslint-disable max-lines` and had grown to
338-769 counted lines against a 300-line budget. AGENTS.md calls for splitting
rather than suppressing, and config/max-lines-baseline.txt is a shrink-only
ratchet, so this removes all seven suppressions and prunes their entries
(341 -> 334).

Each file is cut along the seams it already had -- and that several of the
suppression comments named out loud: filesystem discovery / record parsing /
attribution / aggregation for the scanners, and pricing policy / scope filters /
rollups / session rows / automation attribution for the stores.

Pure move, no behavior change. Code is relocated verbatim; the only edits are
import plumbing and, where a private class method became a free function, the
mechanical `this.state` -> `state` parameter threading. Every converted call
site passes `this.state` at call time and the automation path takes a live
`getState: () => this.state` getter, so no state is snapshotted. No barrel
exports: each new module owns real logic and importers point at the owner.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no import cycles among the 64 affected modules, and a
statement-level diff of every split confirms the moves are verbatim.
2026-08-15 18:33:33 -07:00
Neil bc28107864 refactor(hooks,relay): split agent hook services and relay under the max-lines budget (#14725)
The four agent hook services, the main hooks module, and the two relay modules
each carried a file-level `eslint-disable max-lines` and ran 365-628 counted
lines against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this
removes all seven suppressions and prunes their entries (341 -> 334).

Pure move, no behavior change. Each hook service splits into its managed script
source, its config/bundle serialization, and its remote-install path, keeping the
per-agent integrations independent: copilot, amp, antigravity and hermes each
retain their own getManagedScript rather than sharing one, because each emits a
different script body for a different agent. Merging them by name would have
been a behavior change, not a refactor.

For antigravity the suppression's stated rationale -- that local install, Windows
wrapper generation, status cleanup, and SSH remote install must share one event
list and managed-command matcher so stale-hook cleanup cannot drift by platform
-- is now enforced structurally instead: both install paths call
buildInstalledConfig + createAntigravityManagedCommandMatcher over the single
ANTIGRAVITY_EVENTS catalog, with the graph a strict DAG.

Also registers the six new antigravity/ and copilot/ modules in
config/tsconfig.cli.json. That project uses a curated `include` list rather than
a glob, so an unlisted module fails `tsc -p config/tsconfig.tc.cli.json` with
TS6307 even though the entire unit suite passes.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, green when
re-run serially), no new runtime import cycles, and no lint suppression added.
2026-08-15 18:25:37 -07:00
Neil 83117f2860 refactor(integrations): split issue-tracker clients under the max-lines budget (#14704)
The GitLab, GitHub, Jira and Linear integration modules, their two IPC
registrars, and the shared GitHub project types each carried a file-level
`eslint-disable max-lines` and ran 351-614 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all
eight suppressions and prunes their entries (341 -> 333).

Pure move, no behavior change. Each client is cut along the seam it already
had: per-operation modules for the issue APIs (create / update / comment /
field options), and for Jira the request queue, site credential store,
authenticated request, and site identity. The two IPC registrars keep their own
handlers and delegate the rest to per-domain sub-registrars, so they remain
real entry points rather than re-export shims.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 52 registrations across 52
distinct channels on both sides.

Provider-neutrality is preserved -- GitLab and GitHub keep separate, parallel
module layouts rather than being merged behind a shared abstraction.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 744 modules,
and no lint suppression added anywhere.
2026-08-15 18:17:20 -07:00
Neil 15e1ba3f84 refactor(ipc): split main-process IPC modules under the max-lines budget (#14703)
The six oversized src/main/ipc modules each carried a file-level
`eslint-disable max-lines` and ran 427-671 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all six
suppressions and prunes their entries (341 -> 335).

Pure move, no behavior change. Each file is cut along the seams it already had:
pet splits into format allowlist / storage paths / symlink-safe copy / bundle
manifest + import; filesystem-auth into path-containment primitives, the
config-derived allow-list, and the git-registered root cache; notifications into
sound selection, native lifecycle, permission probe, and burst cooldown;
crash-reporting into renderer error reports, breadcrumbs, and sender.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 49 registrations across 49
distinct channels on both sides. filesystem-auth's security boundary keeps its
acyclic layering -- containment primitives, then allow-list, then root cache,
then path-resolution orchestration -- with no layer gaining a back-edge.

Also keeps clipboard-ipc-handlers.test.ts under the 800-line test budget. The
split had briefly added a redundant vi.mock for isENOENT (byte-identical to the
real implementation) that pushed it to 801; the mock is dropped in favor of the
real function, with realpath added to the existing node:fs/promises mock.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 617 modules,
and no lint suppression added anywhere.
2026-08-15 18:08:54 -07:00
Neil c8fe5fc8c1 refactor(browser): split browser and browser-IPC modules under the max-lines budget (#14697)
The five oversized src/main/browser modules and src/main/ipc/browser.ts each
carried a file-level `eslint-disable max-lines` and ran 377-654 counted lines
against a 300-line budget. AGENTS.md calls for splitting rather than
suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so
this removes all six suppressions and prunes their entries (341 -> 335).

Pure move, no behavior change. cdp-ws-proxy is decomposed into collaborating
objects rather than free functions because its state is genuinely
per-connection: every collaborator is a private readonly instance field built
in the constructor with live closures over `this`, so per-connection state
stays per-connection. Likewise the screencast pacer's isClosed/isStopping and
snapshot capture's getSeq are live thunks, not values captured at wiring time,
so guards inside already-armed timers still observe a later stop().

browser-guest-ui.ts is renamed to browser-guest-shortcut-forwarding.ts: after
the split it exports exactly one function, setupGuestShortcutForwarding, so the
old name no longer described its contents.

Also restores a single `webContents.debugger` read in the screencast path. The
extraction had left three reads where the original had one; the accessor is
stable today, so this is not a behavior fix but it removes a latent divergence.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(remaining failures are pre-existing load flakes in untouched files, each green
when re-run serially), no new runtime import cycles, and the IPC channel set
diffed identical before/after with all 23 handlers still trust-gated.
2026-08-15 17:59:29 -07:00
Jinjing 93ab6e142e refactor(source-control): extract modules to half SourceControl.tsx (#14396)
* docs(source-control): plan half-size extraction

* refactor(source-control): extract modules to half SourceControl.tsx

* move git decoration token comment to correct component

* delete plan doc

* test: add useSourceControlBranchCompare and git-history hook tests

Comprehensive unit tests covering the scheduling, stale response filtering,
and visibility logic of the extracted branch-compare and git-history hooks.

* refactor(source-control): internationalize UI strings

Add translate() support for all hardcoded strings throughout source control UI,
extract reusable SourceControlTreeDirectoryHeader component, improve error
handling in bulk operations with logging and user-facing toasts, and add proper
return type annotations to hooks.

* fix(source-control): satisfy react-doctor rules in extracted modules

Reset worktree-scoped hook state during render instead of in an effect,
and give dropdown separators stable ids so the changed-code quality gate
stops flagging the extracted SourceControl modules.

* docs: add JSDoc comments to source-control hooks and components

Clarify the purpose, behavior, and constraints of test-harness functions,
directory-row components, and the git-history hook to help maintainers
understand the extracted and refactored source-control module.

* refactor: organize source-control into lifecycle dest folders

* fix(source-control): clear remaining react-doctor findings

Reset worktree- and history-scoped state during render, keep Cmd/Ctrl
selection updates free of setter side effects, and key graph paths by
swimlane/parent id. Also add the PR LoC helper scripts the quality
workflow fetches from the branch head.

* fix(source-control): refetch git history when owner host changes

Track activeRuntimeEnvironmentId as a stable key in useSourceControlGitHistory so that when the owner host changes but the worktree and path remain the same, the git history panel correctly refetches from the new host instead of keeping stale commits from the previous one. Add ownerHostKey to the useEffect dependency array to trigger refetch on host changes. Include JSDoc documentation for related components and expand test coverage to verify the host-change scenario.

* fix(source-control): refetch git history when owner host changes

Track activeRuntimeEnvironmentId as a stable key in useSourceControlGitHistory so that when the owner host changes but the worktree and path remain the same, the git history panel correctly refetches from the new host instead of keeping stale commits from the previous one. Add ownerHostKey to the useEffect dependency array to trigger refetch on host changes. Include JSDoc documentation for related components and expand test coverage to verify the host-change scenario.

* refactor(source-control): consolidate bulk mutation error handling

Extracts repeated error reporting into a dedicated helper function and applies
it consistently across all bulk stage/unstage handlers, including two that were
previously missing error handling.
2026-08-15 16:47:03 -07:00