Commit Graph
9772 Commits
Author SHA1 Message Date
Neil c475d23d9b chore(max-lines): prune preload baseline entry
(cherry picked from commit aad2e1ec54)
2026-09-01 00:57:54 -07:00
Neil 85b95aada2 fix(preload): merge split plugin imports
(cherry picked from commit 682ff4f5be)
2026-09-01 00:57:54 -07:00
Neil 7e8337b155 test(preload): census split GitHub bridge owners
(cherry picked from commit cd08e4e91c)
2026-09-01 00:57:54 -07:00
Neil d1abe28471 refactor(preload): split bridge API modules
(cherry picked from commit b77e31873b)
2026-09-01 00:57:54 -07:00
Neil 488f66c56a fix(renderer): preserve accepted visibility snapshots
(cherry picked from commit bc5f8a5ac5b8e8ad3fc59250795842e663bad0ee)
2026-09-01 00:28:37 -07:00
Neil ca12be1921 chore(max-lines): prune runtime baseline entries
(cherry picked from commit 08820a4386)
2026-09-01 00:28:37 -07:00
Neil 08b8f271e6 fix(renderer): merge split runtime imports
(cherry picked from commit 90bf505afe)
2026-09-01 00:28:37 -07:00
Neil 41015f9393 refactor(renderer): split runtime and store modules
(cherry picked from commit 207bef0198)
2026-09-01 00:28:37 -07:00
Neil 41ef1ddd80 fix(renderer): document pre-split render-time refs in workspace search
Both assignments are verbatim from the pre-split monolith; the split relocated
them onto changed lines, which the React Doctor gate scans.
2026-09-01 00:06:23 -07:00
Neil 6c48d7c0d7 fix(lint): drop unused exhaustive-deps suppression after split
The split's dependency list no longer trips the rule, so the directive is dead
and the changed-code quality gate rejects it.
2026-09-01 00:06:23 -07:00
Neil f05981dc29 refactor(automations): extract list toolbar and drop dead list projection 2026-09-01 00:06:23 -07:00
Neil 074f135ee1 style(renderer): format extracted palette modules 2026-09-01 00:06:23 -07:00
Neil 0c638b0fe5 fix(renderer): preserve palette create telemetry
(cherry picked from commit b13c2b1960)
2026-09-01 00:06:23 -07:00
Neil aad0c632a7 fix(renderer): preserve Codex sign-in behavior
(cherry picked from commit 81f4803e6f)
2026-09-01 00:06:23 -07:00
Neil 936595a8e5 fix(automations): preserve destination form save semantics
(cherry picked from commit a793b07c12)
2026-09-01 00:06:23 -07:00
Neil e897acc471 fix(renderer): preserve floating editor visibility
(cherry picked from commit bcf715b76e)
2026-09-01 00:06:23 -07:00
Neil 01d6725be3 fix(renderer): preserve status localization parity
(cherry picked from commit 0a802c70e1)
2026-09-01 00:06:23 -07:00
Neil e8e94ecaab chore(max-lines): prune stale lifecycle baseline
(cherry picked from commit 42bf3eb674)
2026-09-01 00:06:23 -07:00
Neil bae9ea8c79 fix(renderer): document intentional render-time refs
(cherry picked from commit 6fc78dd869)
2026-09-01 00:06:23 -07:00
Neil 6848a6958c refactor(renderer): finish worktree palette extraction
(cherry picked from commit ea8e6e595a)
2026-09-01 00:06:23 -07:00
Neil a8d9acb3f6 style(renderer): format palette project candidates
(cherry picked from commit 1e93d66854)
2026-09-01 00:06:23 -07:00
Neil 29c760726b fix(renderer): restore worktree palette behavior after split
(cherry picked from commit 0b78c1cbbc)
2026-09-01 00:06:23 -07:00
Neil 3e262c1fc6 fix(automations): preserve Escape drill-out behavior
(cherry picked from commit 790680dfc2)
2026-09-01 00:06:23 -07:00
Neil 3fe63b5104 fix(status-bar): preserve workspace-space review and agent freshness
(cherry picked from commit 9455e318fa)
2026-09-01 00:06:23 -07:00
Neil b94a65a4fc fix(lint): preserve TaskPage effect suppressions after split
(cherry picked from commit 4a3bc23670)
2026-09-01 00:06:23 -07:00
Neil cd986c7759 fix(status-bar): preserve Git refresh ordering after split
(cherry picked from commit e65d298afb)
2026-09-01 00:06:23 -07:00
Neil cc2451ca25 chore(max-lines): prune refactored file suppressions
(cherry picked from commit 787228ee46)
2026-09-01 00:06:23 -07:00
Neil 3349a460e2 fix(renderer): preserve extracted lifecycle and retention behavior
(cherry picked from commit 1731f2a2f3)
2026-09-01 00:06:23 -07:00
Neil 7123146ac2 fix(renderer): keep split imports lint-clean
(cherry picked from commit c778ac7a7a)
2026-09-01 00:06:23 -07:00
Neil 286c21005b refactor(renderer): split oversized UI surfaces
(cherry picked from commit da89be4345)
2026-09-01 00:06:23 -07:00
Brennan BensonandMerge Sim 0b2912b507 fix(mobile-native-chat): retire an image echo glued with the send beside it (#17783)
* fix(mobile-native-chat): retire an image echo glued with the send beside it

A message sent with images could render two or three times over, with the copy
carrying the photos sorting below the reply that answered it — and it never
cleared.

A send issued while the agent is mid-turn is glued onto the agent's input line
with any send adjacent to it, so the pair lands as one transcript row whose text
is the concatenation. Every retirement path then declined the pair:

- The image matcher wanted the whole row to equal the echo's text, so a glued
  row never bound. That also stranded the local preview: the phone's photo never
  reached the authoritative row.
- The exact-count path skips image echoes by design.
- The glue path excluded image echoes too, which made one a *barrier* — splitting
  the run so the text-only send beside it was left alone, and a lone match is
  rejected as an ordinary landing.

So neither echo could ever retire, and the unmatched image echo fell through to
the trailing bucket, which is what put it below the reply.

Match a glued row in the image matcher, and let an image echo take part in the
glue pass once its preview has been rebound. It stays a barrier while unbound,
so the existing guarantee is kept: an image echo never retires before its local
preview reaches the transcript row, or the photo would disappear.

* fix(mobile-chat): require image provenance for glued prefix matches

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-01 00:05:00 -07:00
Jinjing e7f15367a8 fix(agent-send-target): acknowledge delivery even when picker closes bef (#17835)
Delivery callbacks and telemetry belong to the completed send operation, not to the
picker instance that launched it. Remove early returns that skipped delivery
acknowledgment and success toast when the popover was already closed.
2026-08-31 23:51:32 -07:00
Neil f088872554 fix(mobile): keep split session hooks render-pure
Documents the pre-existing render-time refs the split relocated onto changed
lines, and drops a ref assignment the split added that the monolith never had.
2026-08-31 23:35:08 -07:00
Neil 451002ba1a fix(mobile): honor host-follow tab snapshots
(cherry picked from commit a4a9c4da19d6967eceb8025dd01480a56261039f)
2026-08-31 23:35:08 -07:00
Neil a8d92e5df9 chore(mobile): prune stale max-lines overrides
(cherry picked from commit be954c3663409116f34ba08658876262733b3f67)
2026-08-31 23:35:08 -07:00
Neil cb94265cf4 fix(mobile): restore session parity after extraction
(cherry picked from commit 47706a5388)
2026-08-31 23:35:08 -07:00
Neil 7caf7ab1e1 refactor(mobile): split session and terminal surfaces
(cherry picked from commit f129f2926a)
2026-08-31 23:35:08 -07:00
Brennan BensonandMerge Sim d4db524ba7 fix(native-chat): stop unjournaled provider frames from killing the session (#17813)
A frame the classifier declines (status-chrome, suppressed-benign, stream-into-item)
is deliberately not journaled. #17720 turned that null translation into
`{accepted: false, reason: 'untranslated'}`, which is not `backpressure`, so the
notification retry queue treated it as unreplayable and escalated through fail() ->
forceCloseUnexpected -> connection.close(). The app-server latched `closing` and the
create path's next model/list rejected with "codex app-server connection is closed
(model/list)".

The provider emits `remoteControl/status/changed` right after initialize, so every
structured Codex session died on its first chrome frame. Admit the null translation
instead, before any bookkeeping or publish.

Also restore the error-frame exemption from the generic row cap, dropped by the same
PR: the cap now runs after the classification check, so a noisy turn can no longer
reduce provider errors to a suppression count. The test that pinned the capped
behavior is inverted to assert the exemption.

Co-authored-by: Merge Sim <sim@local>
2026-08-31 23:31:27 -07:00
Brennan BensonandMerge Sim 51bc2ec343 fix(native-chat): keep the attachments on a Claude turn that pasted images (#17801)
* fix(native-chat): keep the attachments on a Claude turn that pasted images

A Claude turn carrying pasted images reached native chat with no images at all —
no thumbnails on mobile, and not even an attachment chip on desktop. Nothing
showed that the message had any.

Both carriers were being dropped:

- Claude records the paths in a companion turn marked `isMeta`, holding one
  `[Image: source: <path>]` text block per image. The decoder treats an `isMeta`
  user row as injected, filters it down to tool-result blocks, and returns null
  when none remain — so the whole row went away.
- The prompt row's own `image` blocks are `{source: {type: 'base64'}}`, which
  carry no url or path, so `imageRefBlock` drops them too.

With the companion gone, `isImageSourceUserTurn` could never fire and the fold in
`normalizeImageTranscriptMessages` was unreachable on the Claude path.

Surveying every transcript under `~/.claude/projects`: 238 of 241 image-source
rows are `isMeta`, across every versioned release (2.1.220 through 2.1.237); the
3 that are not carry no version field at all. 38 of those rows hold more than one
content block, which also defeated the single-block rule in
`isImageSourceUserTurn`.

Let image-source text survive the injected-turn filter, and recognize a turn
whose blocks are *all* markers rather than only a lone one. An ordinary injected
turn (a skill preamble, a compact summary) is still dropped, and a turn that
mixes prose with a marker is still not an image-source turn.

Carrying the paths keeps the payload small; decoding the base64 instead would put
hundreds of KB per image on the wire to mobile.

* fix(native-chat): preserve image companion ordering

* fix(native-chat): keep image companions turn-local

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-31 23:27:56 -07:00
Brennan BensonandMerge Sim a9e6fb7eff fix(native-chat): stop rendering tool output as the agent's streaming reply (#17782)
* fix(native-chat): stop rendering tool output as the agent's streaming reply

A tool result could appear in native chat as a raw, un-collapsed "assistant"
bubble that never went away for the rest of the turn — on mobile it showed up
as a wall of a source file's contents, prefixed by "Exit code 1".

Providers publish a tool's stdout/error as `lastAssistantMessage` so status
cards and dashboard rows can preview what the agent just did. Native chat reuses
that same field as its live streaming bubble, so the preview rendered as prose.
For Claude the preview is *only ever* tool output mid-turn: claude-tool-fields
writes real prose exclusively at Stop, so the bubble could never contain an
actual streaming reply.

It also could not be retired. The bubble hides once a transcript assistant block
leads with the streamed text, and tool output never lands in one — so the only
remaining exit was the turn ending, which is why a long tool-heavy turn pinned it
on screen.

Carry provenance instead of changing what the status surfaces show: mark the
writes that come from a tool result/error, keep the flag in lockstep with the
value it describes through the listener merge, and have both native-chat
streaming paths ignore a flagged preview. Status cards, dashboard rows and
automation capture are untouched.

The wire field is optional, so an older host that never sends it keeps today's
behavior rather than silently suppressing previews.

* fix(native-chat): preserve tool output provenance through renderer sync

* fix(native-chat): retain preview provenance in Claude roster state

* test(native-chat): cover restored tool preview provenance

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-31 23:05:31 -07:00
Neil 9bc564c2aa fix(cleanup): follow WSL-written gitdir pointers on a Windows host (#17806)
`readLocalWorktreeGitDir` resolved a linked worktree's `.git` gitfile
pointer by hand, translating a POSIX-rooted pointer only when the
worktree path was itself `\\wsl.localhost\...`. On a Windows host
`path.isAbsolute('/mnt/c/repo/.git/worktrees/wt')` is true, so a
drive-path worktree (`C:\Users\me\wt`) whose gitfile was written by git
running inside WSL kept the guest spelling and was joined to
`\mnt\c\repo\.git\worktrees\wt\HEAD`. All four probes (HEAD,
COMMIT_EDITMSG, ORIG_HEAD, tail of logs/HEAD) missed, so the row's
lastActivityAt fell back to the worktree directory mtime and a worktree
with recent commits could read as stale in the cleanup browser.

Delegate to `resolveGitMetadataPath` (src/shared/git-metadata-path.ts,
landed in 7f63db7d7a, already used by repo-git-marker-scan.ts). Five
lines out, one in; the shared resolver is not modified.

Delta, enumerated over 10 base x 18 pointer x 3 platform combinations
(540 pairs) against a reimplementation of the removed branch: 28 differ,
every one win32 + non-UNC base + `/mnt/<lowercase-letter>` pointer.

- WSL-on-Windows: a drive-path worktree with a WSL-written pointer now
  probes its real git metadata.
- WSL UNC worktrees, native Windows, macOS, Linux: no change (0 deltas
  on darwin/linux, 0 for any `\\wsl.localhost\...` base).
- SSH / relay / folder workspaces: no change; remote repos return the
  persisted timestamp before any probe, and a folder workspace has no
  gitfile pointer.

Not strictly monotone: the old probe target `\mnt\c\...` is a real
drive-relative location, so if it existed with a newer mtime than the
genuine gitdir this lowers lastActivityAt for that row. The persisted
timestamp stays a floor via Math.max, and in every realistic case the
change only raises the value.

No signature changes, no options threading, no new call sites.
2026-08-31 22:37:34 -07:00
Neil d7d3114716 perf(wsl): single-flight the async WSL distro list (#17805)
On Windows, seven production call sites reach listWslDistrosAsync and on a cold
cache each spawned its own `wsl.exe --list --quiet` (5s timeout each): the
wsl:listDistros IPC behind the renderer capability read, the host.wsl.listDistros
RPC, the skill-install IPC, CLI registration reconciliation, the hook relay deps,
the kimi runtime home, plus relay preflight in the relay process. Concurrent
callers in one process now share one spawn.

Joining happens ahead of the negative cache, which also fixes a stranding bug: a
synchronous listWslDistros() landing an empty result mid-probe arms the 15s retry
window, and later async callers read that [] even though the pending probe is
about to see a distro that just finished provisioning. The non-empty-cache
short-circuit sits ahead of the join so a list already found synchronously is
still returned without waiting; that is main's existing behaviour preserved, not
a new fast path.

The shared promise cannot reject -- `catch` sits ahead of the stored promise, so
joiners get the same fail-safe [] the old per-caller catch returned -- and the
slot is cleared on settle, by the owning probe only.

wsl-directory-probe-command.ts is a verbatim move of the guest directory-probe
marker protocol and its parser out of wsl.ts, for oxlint max-lines headroom:
inlining it back makes wsl.ts 306 effective lines against a cap of 300. It takes
WslUncPathInfo from ../shared/wsl-paths -- the actual type of every value passed
at both call sites -- so it does not import from wsl.ts. _resetWslCachesForTests
and _setWslCachesForTests now share one resetWslDistroListState() instead of
repeating the same six assignments.

Per-platform delta:
- WSL on Windows: fewer wsl.exe spawns under startup fan-out, and a distro
  provisioned while a probe is pending is no longer hidden for the retry window.
- Native Windows without WSL: no behavioural change. The empty/failure retry
  windows, their backoff and the cache sequence guard are unchanged; N concurrent
  callers now cost one failed spawn instead of N.
- macOS, Linux, folder workspaces: no change. Both new early returns are
  unreachable off win32.
- SSH remote: no change for macOS/Linux hosts; a remote Windows host gets the
  Windows behaviour in its own process. No wire change -- host.wsl.listDistros
  keeps its string[] shape and its [] failure value.
- Relay: same single-flight inside the relay process. It stays per-process; the
  relay and main process still probe independently, as before.

Costs: a never-settling execFileUtf8 now pins the shared slot for the process
lifetime rather than only its own callers -- transient-to-permanent, not identical
exposure. And a joiner inherits the first probe's failure instead of making an
independent attempt.
2026-08-31 22:37:23 -07:00
Neil a9babde9a3 refactor(git): accept a caller-named WSL distro on the metadata path resolver (#17804)
Two small changes to the Git metadata read path. Neither has a user-visible
effect on any platform except for a malformed `.git` gitfile, described below.

1. resolveGitMetadataPath's third parameter becomes an options object
   `{ platform?, wslDistro? }`. A caller that knows which distro wrote a pointer
   can now say so, where previously only a WSL UNC base path could. The distro
   encoded in the base path still outranks the caller's, and translation only
   happens when the reading host is win32, so a caller-named distro cannot make
   a POSIX host fabricate a Windows path. The UNC-base branch is exempt from
   that gate because that spelling only exists on Windows. Main's other
   contracts are verbatim: never null for a non-empty pointer, and a drvfs
   pointer keeps its drive spelling even when a distro is named. Both production
   call sites (repo-git-marker-scan.ts) pass no options, so they are unchanged.

2. The `.git` gitfile marker parse moves into one shared function,
   parseGitdirMarkerPayload: `gitdir:` at the start of the file, payload
   trimmed, empty payload rejected — git's own read_gitfile_gently rule.
   resolve-git-dir.ts and repo-git-marker-scan.ts both call it; the latter had a
   near-identical private copy and is behaviorally identical after the swap
   (verified across twelve marker spellings; the only divergence, a
   whitespace-only payload, already resolved to null one call further down).
   Main's `/^gitdir:\s*(.+)\s*$/m` in resolve-git-dir captured trailing padding
   into the path and honored a `gitdir:` line anywhere in the file.

Per-platform delta: none on macOS, Linux, native Windows, WSL, SSH, relay, or
folder workspaces. The wslDistro option is inert; this change adds no caller.
For a malformed `.git` gitfile, padding is now stripped (strict improvement), a
whitespace-only payload falls back to `<worktree>/.git`, and a `gitdir:` line
that is not the first line is no longer honored — a narrowing, since main could
return a working gitdir there. All four resolveGitDir consumers already degrade
through a catch, so that case reports no sparse state / conflict operation /
diff stamp rather than failing.

Six other hand-rolled `gitdir:` parsers remain, including the relay's SSH copy;
converging them is its own change.
2026-08-31 22:37:09 -07:00
Neil bf62abc3c9 fix(docs): align OSS docs presentation and SEO (#17809) 2026-08-31 22:34:53 -07:00
Neil 2e30187560 feat(dev): sweep the backlog of idle dev Electron bundles (#17803)
* fix(dev): make reclaim report real sizes on Windows and keep setuid intact

Two bugs found by running the reclaim script on real Linux and Windows hosts.

The size report shelled out to `du`, which does not exist on Windows, so every
worktree measured 0 bytes and the script reported nothing reclaimable on the
platform with the largest dist (374MB). Walk the tree in Node instead.

makeTreeReadOnly chmod'd files to a flat 0o555, which clears setuid. On Linux
that would silently strip the bit from chrome-sandbox if a developer had run
the usual `sudo chown root && chmod 4755` workaround -- and under hardlink
sharing it would strip it from every worktree and the cache at once. Clear the
write bits and nothing else.

Measured after the fix: 7.30 GiB across 23 worktrees on one Windows host and
18.31 GiB across 56 on another, both previously reported as 0.

* feat(dev): sweep the backlog of idle dev Electron bundles

out/electron-dev holds one ~275MB patched Electron.app per branch title x
Electron version. The dev runner already prunes them, but only inside the
worktree it is starting and only when that worktree holds more than one bundle
-- and a worktree almost always holds exactly one, so the sweep returns early
every time and nothing ever reclaims another worktree's bundle.

pnpm reclaim:dev-bundles sweeps across every worktree of the repo. Bundles are
pure build output that pnpm dev rebuilds on demand, and rebuilding is cheap now
that the Electron dist is shared.

Reuses the runner's own staleness rules, so a bundle a live process is running
from, or one whose build is still in flight, is never removed. Refuses to run
at all if the process table cannot be read, rather than guessing.

Measured: 120 bundles, 32.2 GiB, on one machine.

Also guards both reclaim scripts behind a direct-invocation check; importing
one for tests previously ran a full sweep at import time.
2026-08-31 22:18:50 -07:00
Jinwoo Hong e2f326cad7 ci(release): prevent signing on workflow reruns (#17802) 2026-09-01 01:05:00 -04:00
Maxon PhongandJinjing 05b31d6e92 fix(i18n): correct zh-CN translation for editor view toggle buttons (#13723)
* fix(i18n): correct zh-CN translation for editor view toggle buttons

- "Rich Editor" (aff15f94f5): 丰富的编辑器 → 富文本编辑器
- "Source" (4d6ccb7ba6): 来源 → 源码
- Settings description (f80603d293): 丰富的编辑器 → 富文本编辑器

"Source" in the Markdown editor context means source-code view, not
data source. "丰富的编辑器" is an awkward literal translation; the
standard term is "富文本编辑器", already used inconsistently in
nearby keys (5f02e6fb21, 8090:694613d47f).

* fix translation

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-08-31 21:49:58 -07:00
Neil 28373fcea7 fix(windows): repair the install-dir package ACL that blanks the window (#17740)
* fix(windows): repair the install-dir package ACL that blanks the window

An install tree carrying an orphan AppContainer ACE (S-1-15-2-<x>) with no
ALL RESTRICTED APPLICATION PACKAGES grant denies Chromium's LPAC children read
on the shipped modules; they die at init with 0x80000003 and the window stays
blank forever (electron/electron#51761).

- Tighten the probe verdict to require the S-1-15-2-2 grant specifically: an
  ALL APPLICATION PACKAGES (S-1-15-2-1) ACE, the Program Files default, does
  not appear in an LPAC token and cannot satisfy the orphan.
- Drop BUILTIN from the English-locale heuristic (fr-FR/es-ES print it
  verbatim) so a localized icacls is correctly reported as un-name-checkable.
- Add an additive, marker-guarded icacls self-repair: an inheritable root
  grant plus a flagless (RX) /T pass, never /grant:r.
- Route the crash-loop dialog through a testable prompt module that names the
  permission cause, offers Copy Commands without dismissing itself, and keeps
  the graphics-driver hint.

The repair only runs on win32, off serve mode, and only on the exact probe
verdict that reproduced the crash.

* docs(windows): correct the install-tree ACL walk cost model

* fix(windows): keep the install-ACL poison gate at the reproduced shape

An orphan package ACE alongside the Program Files ALL APPLICATION
PACKAGES default launches clean on win32 10.0.26200 / Electron 43.4.1,
so requiring S-1-15-2-2 specifically declared poison on healthy installs
- and this branch acts on that verdict with a tree-wide icacls write and
the crash-recovery dialog's primary cause. hasRestrictedPackageGrant
stays reported for triage; only the verdict reverts.
2026-08-31 21:15:19 -07:00
Neil abe1d30881 fix(dev): make reclaim report real sizes on Windows and keep setuid intact (#17800) 2026-08-31 21:12:43 -07:00
Neil e6257b6e32 perf(worktree): resolve the WSL workspace root off the main thread when preparing (#17792)
`computeWorkspaceRoot` resolves a WSL repo's mirror root through `getWslHome`,
which is a synchronous `execFileSync('wsl.exe', ...)` with a 5s timeout. Two
worktree preparation paths ran it on the Electron main thread:
`prepareLocalWorktreeRootForRepo` (repo registration, clone completion, repo
update, project host setup, folder->git upgrade) and `prepareWorktreeCreateForRepo`
(the speculative checkout started while the create composer is open). On a stopped
or cold distro that froze every window for up to 5s. Being fire-and-forget did not
help: only 3 of the 16 `prepareLocalWorktreeRootForRepo` call sites are `void`-ed,
the other 13 are awaited inside IPC handlers, and the sync probe blocks the main
thread either way. `prepareLocalWorktreeRootsForRepos` runs the same probe for
every repo from the settings-save handler.

Adopt the existing `computeWorkspaceRootAsync` (now exported) at those two call
sites, and give the two resolvers a shared mirror-distro decision and shared
root-from-home layout so they cannot drift apart.

Also thread the mirror distro into the prepare-side path settings.
`createLocalWorktree` passes `getWorktreeMirrorDistro(store, repo)` and
`prepareWorktreeCreateForRepo` did not, so a `C:\` repo on a WSL project runtime
prepared under `C:\workspaces` while the create click looked under the mirrored
WSL root: the keys never matched and every prepared checkout was discarded, after
paying for a full checkout that sat until the 5min TTL. Pre-existing on main;
included because it is the same line and the same resolver.

Scope of the win, stated precisely: only those two preparation paths stop
blocking. On a reachable distro `getWslHome` caches on success, so before this
change the first repo paid one blocking probe and the rest were cache hits -- the
change makes that one probe non-blocking, it does not remove N probes. Failed
probes are never cached, so on a stopped distro N repos did pay N sequential 5s
blocking probes and now share one in-flight async probe.

Costs: five sync `computeWorkspaceRoot` callers remain (allowed-roots resolution,
the create click in worktree-remote, CLI create, watch targets, worktree trash),
and `getWslHome` reads only `wslHomeCache` -- it cannot join an in-flight async
probe. The guaranteed synchronous cache warm-up therefore becomes a window in
which one of those callers can still block and can spawn a second concurrent
`wsl.exe`. Concretely: opening the create composer and clicking Create within a
few hundred ms on a cold distro now pays the freeze on the click instead of on the
background prep. Separately, the mirror-distro fix makes prepare spawn an async
`wsl.exe` home probe for `C:\` repos on a WSL runtime, where it previously spawned
none.

No race added: `prepareWorktreeCreateForRepo` computes the preparation key and
inserts the registry entry in one synchronous run after the await, so two
concurrent creates still dedupe to a single prepared checkout.

`worktree-create-preparation-wsl-root.test.ts` runs the real resolver through
prepare and then claims the entry with the production consume-side call shape
(including the mirror distro), so a divergence between the two resolvers fails a
test instead of silently discarding every prepared checkout.
2026-08-31 21:11:09 -07:00