## What changes for the user
The timeline now matches the transcript block for block, and a click
lands exactly where it should.
- **One marker per visible block, in transcript order:** each reply,
each thinking run, each tool (or collapsed tool group), each compaction
and each system context. Markers follow Message content: a block that is
hidden in the transcript is hidden in the timeline too.
- **The clicked marker stays active** until you scroll, and its own
block, not just the start of its message, is placed at the reading
position. This also works for history that is not loaded yet.
- **While scrolling,** the active marker is the first block below the
reading line.
## Reproduced in numbers before fixing
We replayed 45 of the user's real sessions (65,241 messages, exported
read-only) into the real `SessionView`, with the outline, windows and
previews served by the real server readers. Each session was measured at
1280×800 with two presets, using 16 real pointer clicks and 8 wheel
scrolls per run. Baseline and fix used the same seeds.
| Metric | Before, Complet | Before, Balanced | After, both presets |
|---|---|---|---|
| Sessions out of sync | 43/45 | 42/45 | 0/90 runs |
| Markers / expected | 68,949 / 130,818 | 68,949 / 98,759 | 130,818 /
130,818 and 98,759 / 98,759 |
| Click: clicked marker active | 53.6% (46.0% the one above) | 57.2% |
1,440 / 1,440 |
| Click: block at reading position | 41.5% (median off by 35 px) | 82.2%
| 1,440 / 1,440 (median 0.27 px) |
| Scroll: reading-line block active | 93.1% | 92.2% | 505 / 505 moving
wheels |
What was wrong with the markers in the Complet runs:
- 29,782 tool markers were collapsed into one marker per step.
- 31,777 thinking blocks and 282 system blocks had no marker.
- There were 10,232 order inversions.
## Causes
1. Once an outline existed, the outline projection replaced the markers
of loaded messages too (`message-section.tsx`). It collapsed a step's
tools into one marker placed before its reply, and dropped system
blocks.
2. Resident segments merged thinking into replies, dropped thinking-only
steps and skipped system parts.
3. After a click or a scroll, the active marker was the message's first
marker. The message itself came from a scroll offset that rounds a row
starting just below the top to the previous message.
4. Clicks navigated to the message, not to the block.
5. Jumps into unloaded history had no row to hold while the target was
not yet rendered.
## How
- **Server (outline):** each assistant step can now carry an optional
`layout` field. It lists the step's visible parts in order: thinking
runs, reply runs and each tool, at most 64 entries. It is computed in
the same bounded SQL. Older plugin copies don't send it, and the client
then falls back to the counts: thinking, then text, then tools.
- **Markers:** `buildTimelineSegments` emits one marker per block, keyed
`messageId:type:ordinal`. Loaded messages keep their exact markers.
Unloaded steps are placed at their native position from `layout`.
- **Click and scroll:** the list's `scrollToKey(key, selector)` holds
the selected block on every frame, survives row remounts, and follows
Virtua's estimate until the row renders. `timeline-transcript-blocks.ts`
maps markers to blocks and finds the block at the reading line.
- **Saved indexes:** outline indexes saved without `layout` are
revalidated once.
- **Docs and help:** updated in `dev-docs/SESSION_HISTORY_NAVIGATION.md`
and in the help pages for all 10 locales.
## Behaviour to be aware of
- **The timeline has more markers:** about +90% with Complet and +43%
with Balanced on this sample, because every tool, thinking and system
block now has its own marker.
- **Tool icons for unloaded steps:** a step not loaded yet shows its
first tool's icon on all of its tool markers, until its window loads.
- **Plugin requirement:** the pruning plugin must ship with the new
`layout` field.
## Validation
- **New regression test:** `tests/browser/timeline-sync.test.ts` passes
4/4 and fails 4/4 on unmodified dev. Fixtures are synthetic.
- **Measurement harness:** the opt-in qualification `npm run
qualify:browser:timeline-sync`.
- **Browser suites:**
| Suite | Result |
|---|---|
| history-navigation | 65 |
| message-scroll | 128 |
| session-rendering | 47 |
| render-cost | 17 |
| full-source-copy | 12 |
| search-highlight | 11 |
| system-messages | 10 |
| timeline-tool-filters | pass |
| streaming-outline (no full reprojection over 64 token deltas) | pass |
| session-search | pass |
| transcript-presets | pass |
- **Other checks:** UI unit tests 75/75, server session-pruning 70/70,
help 9/9 and the UI typecheck pass.
- **After rebasing on dev:** the typecheck passes, along with
timeline-sync 4/4, timeline-tool-filters, full-source-copy 12/12,
usage-metrics 5/5 and streaming-outline.
Oversized files touched: `message-section.tsx` (~1,637 lines),
`virtual-follow-list.tsx` (~1,368), `message-timeline.tsx` (~508),
`message-timeline.css` (204).
fix(timeline): stop adding an empty agent marker after every tool step
In OpenCode V2 each agent step is its own assistant message, and most steps only call tools. The timeline's structural index for history knew an assistant message's tool and reasoning counts but not whether it had text, so the projection drew a tool marker followed by an agent marker for every step. For tool-only and reasoning-only steps that agent marker was empty: no hover preview, and clicking it reached a message showing nothing but its tools, or nothing when tools are hidden in Message content. Resident (loaded) messages already dropped such markers.
The outline index now also counts an assistant message's non-blank text parts that are neither synthetic nor ignored, the same text the hover preview shows. The projection draws an assistant marker only when that count (or the resident segments) shows visible text, so a tool-only step keeps its tool marker alone and a reasoning-only step has none. The count is optional in the contract: an older plugin copy still loaded by the daemon reports no count, and its markers stay as before. Persisted indexes saved without the count display immediately and then revalidate every checkpoint once, like the representative tool names. The unused message-timeline-group-* classes, which no stylesheet or test referenced, are removed; the group spacing that still uses tool membership is kept.
Validation: navigation.test covers text counts for text, tool-only and blank/synthetic/ignored/reasoning-only steps; session-outline-projection covers tool-only, reasoning-only, text, unknown-count, shell and resident cases; session-outline-persistence covers validation and the one-time refresh of indexes without counts; the native navigation script now asserts the counts on real native storage (CI). history-navigation, timeline-tool-filters, system-messages and render-cost browser tests, the session-outline tests and the UI typecheck pass.
Restore provisional timeline geometry through optional content-addressed index chunks, bounded independently from drafts and attachments. Validate durable session/location/project/revert identity, preserve other restored state when an index chunk is missing, and reuse immutable normalized values and encoded leaves during scroll or draft captures. Fence older renderers with a graph extension and retain monolithic-host compatibility.
Revalidate native structural indexes using stable checkpoint ranges and return only changed structure. Grow the live tail before adding ranges, reserve a complete block within each RPC page budget, and schedule a trailing scan when messages arrive during a fixed-horizon read. Route bulk-cleanup completion and ambiguous failure through the shared coalesced refresh path so the final invalidation cannot strand an obsolete native transcript window.
Cover large partition round trips, corruption isolation, restoration clear/disable, offline edits/pruning/deletions/appends, manifest growth, deletion-hole pagination, active-window refresh races and Solid trailing scans. All 21 Chromium navigation cases, package typechecks, packaged-resource smoke, Electron state tests and isolated OpenCode 2.0.5 integration pass. The second independent whole-PR gatekeeper review reports zero actionable findings; installed-app restart verification remains pending and is documented explicitly.
Build rail geometry from a structural native index of IDs, sequence, types and technical-part counts. Project that metadata in SQLite without transporting or parsing full message bodies in JS. Bounded 16384-entry pages replace 256-entry excerpt scans, and indexing runs independently of transcript hydration. Remove the excerpt-loading countdown.
Retain up to sixteen structural snapshots with a 200000-entry LRU budget, allowing one larger active index. Unchanged session returns reuse the index without a read; message changes refresh only the last 32 indexed rows and arrivals. Preserve destructive-mutation, undo, connection-generation and route ownership fences.
Restore bounded Markdown hover and focus previews. Fetch visible-nearby excerpts in batches of twelve, prioritize the hovered marker and cancel obsolete demand. Keep a separate 512-excerpt cache with on-demand age revalidation. Unavailable excerpts show no empty popup, and arriving content cannot alter marker geometry or load a transcript window.
All nineteen browser navigation cases pass, including blocked transcript/excerpt hydration, exact scrollbar extent, native thumb gestures, rich historical/resident previews, six intervening session indexes and tail-only live refresh. Six SQL tests, three route race tests, UI/server typechecks, packaged-resource smoke and the full isolated OpenCode 2.0.5 native/UI fixture pass. Record installed desktop cold-index responses of 2.05s for 11210 messages and 0.58s for 4857 messages; the warm-switch run was interrupted by user selection and is not claimed as qualified.
Large-session desktop switching reproduced repeated restoration requests for the same absent anchor. Distinguish anchor_missing in the bounded native window RPC from ownership and revert conflicts. Automatic restoration recovers once through a fresh visible latest page and replaces the stale saved cursor only after success; explicit navigation and authority failures retain their error semantics.
Cancel view-owned initial reads when hidden, release their request authority before an immediate return, and prevent obsolete hydration callbacks from reviving after away-and-back navigation. Pause outline scans while transcript hydration is loading, preserving their accepted cursor and completed display. Add the user-approved lightweight timeline scrollbar separator, hidden when its measured content does not overflow.
Record the real Windows desktop baseline and remaining priorities in SESSION_HISTORY_STRESS_REVIEW.md: 62.62 MiB of transcript responses, repeated outline scans, transient heap growth and main-thread stalls. Separate observations from unproven leak or CPU attribution, and document the live-event merge, incremental metadata and byte-budget work still needed.
Validate 18 browser navigation cases (17 in the complete run and the corrected localized conflict assertion in a targeted rerun), four active-load lifecycle tests, seven navigation store/routes tests and both typechecks. The complete isolated native Windows OpenCode 2.0.5 UI fixture passes, including missing-anchor RPC semantics and native outline/window parity.
The installed Windows application returned only four metadata entries from a 10,763-message session because the outline's wall-time budget included cooperative event-loop waits. Remove scheduler time from pagination and bound each page by 256 rows and 24 MiB of parsed source payloads instead. Yield every 16 rows in both outline and message-window reads, preserving cancellation without paying one scheduler round trip per message.
Regression coverage advances the clock across each observation, checks full metadata pages, verifies cancellation releases the read transaction, and proves byte-budget pagination retains the first unread message. Strengthen the isolated native fixture to require full small-record pages. Seven navigation/store/route checks and the server typecheck pass.
Extend the existing history-query plugin with bounded outline and anchor-window reads so timeline and current-session search can reach distant passages without loading intervening transcript pages. Share daemon-storage identity binding, preserve location/project/revert authorization, enforce payload budgets and retain overlapping neighbors for reading-position restoration.
Route timeline, search and missing restoration anchors through the existing message-load authority fences. Keep the visible transcript during navigation, let the latest requested destination win, reject late results after cancellation or leaving the session, and retain historical reading positions during live generation.
Restore a discreet timeline scrollbar with stable reserved geometry. Only marker rectangles narrow; icon dimensions and vertical spacing remain fixed across hover, focus, RTL and fractional zoom. Include localized loading/retry UI and document the navigation contract.
Validated with server/plugin/route regressions, 75 UI store tests, 56 distinct Chromium cases, server/UI typechecks, UI/plugin builds and an isolated Windows OpenCode 2.0.5 fixture comparing 1501-message navigation against native export and message reads. Installed desktop and other-platform validation remain separate.