* 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>
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.
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.
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>
* 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>
* 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>
`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.
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.
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.
* 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.
* 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>
* 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.
`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.