The splice hazard the tail seed guards against is live in remote-lifecycle's
scrapeReadyPort: the remote spawn log merges stdout and stderr, yet it was
matched with the `^`-anchored READY_RE. Export one READY_IN_MERGED_OUTPUT_RE
(lookbehind token boundary — the hand-rolled `[^0-9A-Z_]` class admitted
lowercase-glued tokens) from backend-ready.ts and use it for both merged
buffers. Comments trimmed to the WHY; 6 new tests collapsed to the two
invariants (splice recovers, prose does not match) plus one remote-log case.
Both callers build the wait in the same synchronous block as
`outputTail.attach(child)`, and stream 'data' is asynchronous, so
`bufferedOutput()` is always empty at the seed and this block recovers
nothing today. Say so at the code, so nobody reads the seed as the live
recovery path for a lost READY sentinel — and so the condition that makes it
load-bearing again (any await reintroduced between attach and this call, the
pre-#100442 ordering) is written down next to the code that depends on it.
The spawn-time output tail feeds stdout AND stderr into ONE buffer, so its
contents are not line-accurate. uvicorn logs to stderr in raw chunks, and a
chunk that ends without a newline is concatenated directly onto the stdout
sentinel that follows it:
INFO Started server process [4711]HERMES_BACKEND_READY port=65238
The late-attach seed scan added for #60323 matched that buffer with the
line-anchored `_READY_RE`, so `^` never lined up and the seed silently
recovered nothing. The live stdout listener cannot cover the gap either: the
sentinel was already consumed by the tail, and flowing-mode streams never
replay chunks to late listeners. The wait then runs out its full 90s deadline
and a healthy, listening backend is SIGTERMed — while desktop.log, which reads
both streams, plainly shows the READY line. That contradiction (READY logged,
boot timed out anyway) is the reported macOS regression.
Scan the tail with a boundary-guarded pattern instead of a line-anchored one.
`port=<digits>` keeps the token unambiguous, so prose naming the sentinel and
longer identifiers ending in it still do not match. The live per-line scanner
keeps `^`: individual stream chunks ARE line-accurate there, and loosening it
would let unrelated child output settle the boot on a bogus port.
Scope: the seed only. Watching stderr on the live path is #97086's change and
is deliberately untouched here — the two compose.
The core-tool rename shipped `todo_list` on the wire (legacy alias `todo`
kept for old transcripts), but the Desktop renderer still matched the tool
by the literal `todo` in seven places: the live tool.start/tool.complete
mirror into the composer status stack, the todo-stream router, args
carry-over, the transcript hoist, the silent-tool class, the count noun,
and stored-history hydration. Every live task update therefore went into
the transcript as an ordinary tool row while the task panel stayed empty,
and reopening a chat never restored a finished list.
One predicate (`isTodoToolName`) now owns the wire/legacy name pair and
every site reads it.
Salvage the focused renderer hunk from #104131. Preserve raw gateway tilde and relative paths at the authenticated file bridge; do not reinterpret remote paths on the client or widen external protocol handling.
Co-authored-by: Halldrix <12357213+Halldrix@users.noreply.github.com>
Slim salvage of #103992: preserve dismissal provenance as removed ids rather than a second metadata schema; discovery only overrides successful git removals. Explicit hide is durable even before a discovery scan finishes.
Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
Remount scope-owned credential state on Applies-to changes and bind onboarding requests to their initiating route. Cancel polling and invalidate late results when setup closes or reopens, without undoing writes already sent.
Co-authored-by: By JTT <29462570+jordan-thirkle@users.noreply.github.com>
`session-unread-tile.test.ts` intermittently fails CI with
`Test timed out in 15000ms` at the first case. It has hit at least three
independent branches, `main` included, so it is not tied to any one change.
The cost is module reconstruction. `beforeEach`/`afterEach` both call
`vi.resetModules()`, so each of the three cases re-imports six modules,
including `@/lib/chat-runtime` and the pane-tree store. Locally that is
~3.3s on median but the tail reaches 11.8s (3.6x the median) on a warm
machine; a loaded CI runner pushes that past the 15s budget. One observed
CI run passed at 14614ms, 386ms under the limit, which is the same test
sitting on the wrong side of the same boundary.
Drop `resetModules` and undo the per-test state explicitly instead:
- collect the `registry.register` disposers and run them in `afterEach`,
matching what `session-states.test.ts` already does;
- reset `$layoutTree` and `$activeTreeGroup` at the top of `setup()`.
`declareDefaultTree` only seeds the layout when it is empty, so without
this the second case would adopt the first case's tree.
The test keeps its teeth: reverting the `$focusedStoredSessionId` change
from a5b5043 still fails exactly the same two cases as before this patch.
Slowest of 30 consecutive local runs goes from 11.84s to 3.70s, with the
median roughly unchanged.
Keep command-palette literal filter semantics unchanged; opt model surfaces into the shared fold. Preserve original highlighter contracts and trim new regressions to two invariants. Native Electron catalog before/after and visibility dialog verified; campaign suites remain queued.
Review-response sweep found one straggler: model-picker.tsx's
visibleDownloads filter still used raw toLowerCase().includes(), so a
hyphen-style query wouldn't match a space-separated download target —
the same bug class this PR fixes, in the same picker. The catalog
menu's equivalent filter was already converted.
Model ids use hyphens/underscores, display names use spaces, versions use
dots. The pickers' filter haystacks contained both the raw id and the
display name, but the highlight only ever saw the display label — so
'qwen3.8-flash' revealed the row (via the id segment) while lighting up
nothing, and model-picker.tsx had the inverse polarity (spaces matched,
hyphens didn't highlight). This violated HighlightMatches' own documented
contract: the query must mirror the filter's semantics or the emphasis
lies.
One length-preserving searchFold ([-_.] -> space, 1 char in / 1 char out)
now runs on both sides of every model-search filter AND inside
HighlightMatches' range finder. Length preservation keeps mark ranges
valid against the original text, so <mark> rendering is untouched. The
fold is a per-character substitution applied to both sides, so any query
that matched before still matches — only coverage grows.
- lib/text.ts: searchFold + foldIncludes, the one matcher for all pickers
- highlight-matches.tsx: ranges computed on folded text, marks slice original
- model-catalog-menu.tsx: family, MoA, and download haystacks use foldIncludes
- model-visibility-dialog.tsx, model-picker.tsx: same (fixes inverse polarity)
- dropdown-menu.tsx: DropdownMenuSearch sets spellCheck={false} — squiggles
under model ids are noise; composer/settings inputs already disable it
Tests: fold primitives (equivalence, 1:1 length, superset), highlighter
fold behavior + index fidelity, end-to-end menu behavior (hyphen query
marks the spaced label; space query finds the hyphenated id without
over-matching), and an updated hidden-model-search test whose id-style
query now legitimately highlights.
Observe post-settle transcript resizes until reader input or a live run takes ownership. Keep parked upward-wheel cancellation and namespace capture intact. Extend the maintained Chromium restoration probe with long reload and streaming controls; retain the two existing component invariants.
Rework per hermes-sweeper review (keep_open, salvageability=medium):
- Capture moves out of the render body into the session-switch layout-effect
cleanup, so only committed switches persist state (no uncommitted render
can write localStorage).
- Live state is fed by both scroll events and a ResizeObserver on the
content element, so distance-from-bottom stays fresh under async
relayout that changes height without a scroll event (the staleness gap
#70478's own review threads flagged).
- State is distance-from-bottom (or sticky-bottom), not raw scrollTop:
the render-budget backfill prepends older turns and main anchors by
distance-from-bottom, so an absolute offset no longer identifies the
same reading location after the height change.
- Restore integrates with main's hasGroups/settle/anchor lifecycle: the
settle loop re-applies the remembered target, defers on a clamped offset
(content still arriving), hands back locked only for sticky-bottom, and
leaves mid-read sessions escaped at their offset.
- anchorBeforePrepend no longer records 0 mid-load, which would clobber a
restored offset once the backfill lands; the settle loop owns the target
every frame until settled.
- Storage is scoped per profile with the session.ts .profile.<encoded>
key pattern (no cross-profile bleed, #67709 pattern) and LRU-capped at
120 sessions per profile.
- Regression tests: state classification, target math, profile isolation,
LRU eviction, corrupt/invalid payload handling.
Related to #45562 (partial; no automatic closure)
(cherry picked from commit 1d79bef93f63d7b190b471bf652b5109b4288a8e)
Preserve producer descriptions without identity wrappers and size the existing themed tooltip to the viewport. Replace skipped and structural tests with two behavioral invariants. Native Electron before/after hover, click and keyboard verification passed; campaign suite validation remains queued.