The schema/config/bindings/registry/dispatcher module, useKeybind hooks,
recording dialog, reworked shortcuts settings page, help dialog, and the
localized action labels — re-based onto current main rather than merged
(the branch predates 440+ commits including the session-tabs shortcuts).
Review fixes applied on top of the original:
- close_session_tab (alt+w) joins the schema with labels in every locale;
it shipped on main after the PR's base and would otherwise silently die.
- switch_context_surface's special-case in conflict resolution is now a
declared prefixStyle config property instead of a magic id string.
- Duplicate handler registration warns in dev builds.
- The risky-browser-shortcut warning inspects every chord and covers
mod+q/d/h/j/o/u plus mod+shift+w/q.
- The dispatcher remembers which target armed a two-chord prefix so the
window-level completion handler can distinguish a deliberate sequence
from typing in an editable field (guard lands with the dispatch hook).
- Schema tests: unique normalized default bindings enforced, and the
flat-file-era override format proven to keep resolving.
The global session-status store patched its own setState to derive
active membership for callers replacing statusById — a boundary that
silently trusted any caller passing both fields to keep them consistent.
replaceGlobalSessionStatusById is now the one sanctioned way to swap the
map from outside the event reducers; the runtime-switch reset and the
tests that replaced the map directly go through it, and the patch is
gone. Follow-up to #3126 review.
The staggered re-send lived as three near-identical private methods in
the chat view, agent manager, and session editor providers. One helper
now owns the delays, the connected gate, and the replaced-target guard;
the providers keep thin wrappers over their own panel/view identity.
Also restores the package.json trailing newline the branch dropped.
The floating editor toolbar this branch was based on was removed on main
(the toolbar is always docked now); the file-view region resolves to
main's structure with this branch's virtualizer-bound ScrollableOverlay.
When the caller passes its own AbortSignal and AbortSignal.any is
unavailable, the timeout was silently dropped — disabling the fix on
exactly the bootstrap reads it targets, since those carry a cancellation
signal. Compose the two signals manually through an AbortController in
that case; listeners detach when the request settles.
Highlights trimmed to the four genuinely major entries and ordered by
user impact; the markdown-preview comment surface folded into the chat
comment bullet, Cmd+P path matching folded into the unified search
matcher bullet, the instant session-open and uncached fade-in clauses
folded into their owning entries; fixes grouped after features, visual
polish last.
- Markdown DOM cache: key includes a content-length fingerprint so an
edited or reverted part re-materializing under the same id cannot
restore stale DOM, memoization uses scalar identities instead of the
part object (store reducers recreate part objects on unrelated updates,
which re-ran the async render pipeline for identical content), and a
probe with a mismatched locale/directory no longer destroys the entry
it failed to claim.
- Sidebar bootstrap: the layout-level sync owner only knows known
directories, so expanded projects bootstrapped serialized at background
priority. The visible collection now publishes a second, expansion-
aware demand owner, restoring concurrent hydration for expanded
projects and worktree groups.
- Settings: local changes still sitting in the debounce buffer are not
yet tracked as mutations, so a settings GET racing the debounce window
briefly reverted them; reconciled results now reapply the pending
buffer.
Main replaced the chat timeline scroll engine while this branch was in
flight, which obsoletes two of its subareas and reshapes a third:
- Chat timeline: main's LegendList-based MessageList/ChatContainer win;
the activation-overscan staircase targeted the removed tanstack path
(LegendList provides adaptive rendering natively) and is dropped along
with its test.
- Scroll shadows: main's hook-based masks stay (the virtualized list owns
its scroll element — there is no wrapper to hand the styling to); the
viewport-wrapper ScrollShadow rewrite, its index.css replacement, its
test, and the call-site viewportClassName adaptations are reverted to
main. The chat OverlayScrollbar keeps this branch's disableHorizontal.
- OverlayScrollbar: the direct-DOM rewrite lands, but binding now follows
the live container node instead of binding once per ref object — the
chat scroller remounts on every session switch, and a bind-once
contract left the scrollbar attached to a dead element.
- Markdown renderer: the detached-DOM cache and warm-block fast path
merge with main's block-commit reveal (enter cascade), streaming code
highlighting, and gutter reservation; the per-block reconcile keeps
both the decoration-refresh path and the reveal cascade.
The composer rendered a second StatusRow instance carrying the
pending-changes bar and the todos dropdown, so every restyle of the
floating assistant-status chip (glass, placement, sizing) silently
restyled the composer bar and its dropdown too — for the fourth time.
StatusRow is now only the floating chip above the composer; the
composer's own bar is a new ComposerStatusBar with the pre-glass layout,
its own container name, and its own container-query classes (the
anti-overlap rules that hide the long active todo under 38rem and the
changed-files label under 30rem moved with it — losing them during the
split let the two dropdown triggers overlap on mobile). The two
components share no markup and no CSS hooks anymore.
The base rule declared --scroll-shadow-size as var(--scroll-shadow-size,
48px) — a self-referential cycle, which computes to invalid and silently
resolves every mask below to none. The old ScrollShadow component masked
the bug by setting the variable inline; the hook-based path (the chat
scroller) sets only the data attributes, so the chat lost its top and
bottom scroll fades. A concrete 48px default restores them; inline
overrides from the component still win.
The animated maintainScrollAtEnd also ran outside streaming, so opening
a historical session glided visibly through the whole conversation as
late row measurements corrected the end position, and an in-flight glide
could supersede explicit navigation. Corrections are instant unless the
session is actively working.
Returning to the bottom during a stream also lands on the end as of that
moment, and the list's own follow may not have re-armed after the user's
earlier gestures — the queued-send jump then fell behind the growing
reply. goToBottom now re-asserts the edge a few times (150/400/800ms)
until it holds; a new user gesture cancels the window.
During a long stream the follow scroll masks the entrance entirely, so
the blur was invisible where it streamed and only a cost risk on weak
GPUs. Keep the compositor-only fade and rise (and the same-tick cascade)
— those are effectively free and still show on short replies where no
scroll motion is involved.
The 140ms fade read as a pop next to the scroll glide. The reveal now
runs 320ms on an ease-out-quint curve with a 10px rise and a touch of
blur resolving — paint-only properties throughout, so row measurement is
untouched — and blocks committed in the same tick cascade with a 55ms
stagger instead of appearing together. Reduced motion still disables the
whole thing.
Retain detached Markdown DOM for eight recent sessions so medium-frequency session switching can restore more first-screen content without reparsing it.
Limit each session to four cached parts, reducing the worst-case retained fragment count from 36 to 32 while preventing long sessions from dominating the cache.
Keep sidebar rows independent from the active sync directory by moving export history loading behind an explicit-directory command.
Make active-project selection preserve project topology and remove activeProjectId from mounted group render props. Reuse a per-session-array ID index so subscription snapshots do not repeatedly scan session lists.
Batch global session, status, ordering, and activity timing mutations at the existing directory event boundary so large subagent bursts publish each owner once.
Maintain active session roots, children, and directory buckets in the global store, reuse them in Sidebar projections, and avoid rebuilding live aggregates and structural data for unrelated renders while preserving authoritative ordering reconciliation.
Skip mutation history when no settings operation is in flight and merge updates that share the same operation visibility window. This keeps stale-response reconciliation unchanged while preventing history from growing with repeated debounced updates.
Add a repeated-update regression covering an older pending load and latest-value reconciliation.
Tests: bun test src/lib/persistence.test.ts
Keep settings mutation revisions and in-flight operations behind a private tracker so stale GET and PUT responses cannot overwrite newer local settings.
Remove redundant runtime identity and numeric operation IDs. Use operation object identity while preserving runtime reset and mutation retention behavior.
Tests: bun test src/lib/persistence.test.ts
With block-level commit the open code fence grows by whole lines at the
throttle cadence, so the old reason to skip highlighting it (re-tokenizing
a growing block ~40x/sec) no longer applies: a partial fence now
highlights too, and streamed code arrives colored instead of colorizing
only when the fence closes. Fences beyond 300 lines fall back to plain
text until closed, keeping the repeated worker re-tokenization bounded.
A freshly committed block also enters with a short fade-and-rise. The
class goes on the block's children (the wrapper is display:contents and
cannot animate) and the transform is paint-only, so virtualized row
measurement is unaffected; reduced motion disables it.
Verified over CDP on a production build: streamed code inside a still-
open fence renders highlighted, and the code text's x-position is
identical during and after the stream.
Token-by-token streaming mutates the trailing paragraph in place on
every tick: words rewrap, the last line jitters, and the whole reply
reads as flicker. Streamed text now commits only up to the last complete
line — prose arrives a paragraph at a time (a markdown paragraph is one
logical line), code fences reveal line by line, tables row by row — and
a shown block never changes again. A paragraph that runs long without a
newline releases at the last sentence (then word) boundary so the stream
never stalls. Applies to assistant text and reasoning; tool output keeps
its raw tail.
With growth arriving in block steps, the end follow switches to the
list's animated mode so each step is a glide — reveal and scroll read as
one continuous motion. The gesture opt-out now measures at-end from the
live list state instead of the cached flag, which the animated glide
deliberately leaves stale while trailing the edge; without that, a drag
during a glide could leave the scroll-to-bottom pill unshown.
Measured: end-following holds at distance 0 for the whole stream, and
the mobile drag opt-out shows the pill in three of three runs. The
continuous glide costs ~15% more main-thread time per streamed character
than the instant follow — the price of the motion.
Three first-render polish issues:
Code blocks jumped at end-of-stream: streaming defers the per-line
line-number markup, so the finished decorate pass inserted the gutter
column and shifted every code line right. The gutter's horizontal
footprint is now reserved with CSS while the markup is deferred, so the
final pass only fills in numbers and colors.
File references were verified twice per render and re-verified with the
wrong directory: the annotation pass's own DOM writes re-triggered its
MutationObserver, and the pass also ran before the effective directory
resolved — issuing stat probes under an empty directory and a second
time under the real one. The observer now ignores the pass's own
mutations and annotation waits for a resolved directory.
Content replacing the hydration skeleton popped in: it now plays a
one-shot 180ms fade (reduced-motion aware); cached session switches
never carry the class and stay instant. Also removed the dead
disableStaging prop and its unused pendingRevealWork threading.
Verified on a production build over CDP: a streamed code block's text
keeps its exact x-position across end-of-stream, and stat probes for a
message with file mentions are unique per path with the directory header
always present. The hydration fade path could not be exercised in the
harness (the live event stream pre-populates parts in a single-project
environment) and needs an eyeball check on a cold multi-project open.
Sending from mid-history positioned the new turn with an animated
scrollToIndex; over a long distance the smooth scroll gets cancelled by
rows mounting and measuring along the way — a weak scroll attempt that
dies partway, leaving the reader where they were. Park instantly when
the viewport was not at the end at send time; the short smooth park
remains for sends from the live edge.
Sending while the agent is working queues the message without creating a
user row, so the anchor-arming send path had nothing to claim and the
viewport stayed parked mid-history. Queued sends now jump straight to
the live edge (instant, so end following re-latches immediately).
Rail selection also switches from smooth to instant scrolling: a long
smooth scroll through the virtualized timeline gets cancelled by row
remounts and lands mid-way or on the wrong message, while a teleport
always arrives.