| 1 | # Turn outline and cross-page navigation |
| 2 | |
| 3 | [简体中文](TRANSCRIPT_OUTLINE_NAVIGATION.zh-CN.md) |
| 4 | |
| 5 | > **Historical acceptance record — superseded by PR #10385 (Follow v2).** |
| 6 | > This document records PR #10276's complete-outline implementation and the |
| 7 | > validation performed at that time. Its before/after comparisons and results |
| 8 | > are not acceptance evidence for current production wiring. |
| 9 | |
| 10 | Current behavior uses a bounded bidirectional history window and a loaded-turn |
| 11 | rail; complete persisted history remains accessible through canonical |
| 12 | search/locate. See [Transcript v2](TRANSCRIPT_V2.md), the |
| 13 | [scroll and history contract](TRANSCRIPT_SCROLL_CONTRACT.md), and |
| 14 | [Natural-flow chat](TRANSCRIPT_ARCHITECTURE.md). The historical design and |
| 15 | validation below are preserved for traceability. |
| 16 | |
| 17 | ## Reported problem |
| 18 | |
| 19 | In a long, tool-heavy conversation the first body page holds the newest 120 |
| 20 | records. When that window contains fewer than two user questions, the rail hid |
| 21 | entirely: the reader had to press "load earlier messages" repeatedly before any |
| 22 | navigation appeared. Switching to another conversation and back rebuilt the |
| 23 | body window, so the rail could disappear again, and markers were renumbered |
| 24 | from 1 because the ordinal was derived from the loaded window. A turn outside |
| 25 | the window could not be reached at all. |
| 26 | |
| 27 | | | Before | After | |
| 28 | | --- | --- | --- | |
| 29 | | Rail content | Loaded user turns only | Every turn of the conversation | |
| 30 | | First paint, long session | Hidden until history was loaded manually | Complete, without loading any body | |
| 31 | | A → B → A | Rail reset with the rebuilt window | Rail restored from the snapshot index | |
| 32 | | Ordinal | Renumbered from 1 per loaded window | Absolute turn number of the conversation | |
| 33 | | Unloaded turn | Not listed | Listed, previewable, clickable | |
| 34 | | Clicking it | Not possible | Pages history in, then scrolls to it | |
| 35 | |
| 36 | ## Design |
| 37 | |
| 38 | - `internal/transcript` publishes a bounded turn outline beside the body pages, |
| 39 | bound to the same immutable snapshot, revision, and event coverage. Entries |
| 40 | carry the user record's stable `RecordID`, optional `MessageID`, absolute turn |
| 41 | ordinal, the record's `order` inside that snapshot, and two previews built |
| 42 | from display bodies only: 50 grapheme clusters of the prompt and 120 of the |
| 43 | last non-empty assistant body in that turn's group. Reasoning, tool output, |
| 44 | submitted text, and injected context never enter an entry. |
| 45 | - The index is built once per frozen cut, so repeated reads reuse one pass, body |
| 46 | paging never shrinks it, and its previews count against the existing snapshot |
| 47 | cache budget and lifetime. |
| 48 | - `GET /transcript/outline` serves it, advertised to remotes as |
| 49 | `transcript-outline-v1`. `TranscriptOutlineAPI` is an optional capability |
| 50 | beside `TranscriptProjectionAPI`, so an existing controller keeps compiling |
| 51 | and an unsupported route answers 404/405/501 rather than an empty page. |
| 52 | - The frontend keeps one shared outline store for local and remote sessions, |
| 53 | fenced by tab generation and snapshot identity. A recycled cut is reported as |
| 54 | stale; the body is only replaced when the reader explicitly retries. |
| 55 | - Selecting an unloaded turn starts a jump transaction that reuses the ordinary |
| 56 | older-history paging one page at a time, waits for the progressive mount to |
| 57 | advance, and writes the viewport only once the target node is really mounted. |
| 58 | Reader intent, an explicit cancel, a newer target, or a session/snapshot |
| 59 | replacement ends the pending transaction without taking scroll control back. |
| 60 | |
| 61 | ## Verification |
| 62 | |
| 63 | Code and automated checks are complete for the local and remote paths. Platform |
| 64 | verification is listed separately below and is not claimed beyond what ran. |
| 65 | |
| 66 | ### Go (ran) |
| 67 | |
| 68 | ```sh |
| 69 | go test ./internal/transcript/ ./internal/control/ ./internal/serve/ |
| 70 | go test -race ./internal/transcript/ |
| 71 | cd desktop && go test ./... |
| 72 | ``` |
| 73 | |
| 74 | Covers: outline completeness for a tool-heavy tail whose body page holds no |
| 75 | user turn; identity shared with body records; absolute numbering and survival of |
| 76 | older paging; staleness for a recycled cut; preview bounds, whitespace |
| 77 | collapsing, Unicode safety, and exclusion of reasoning/tool output; empty |
| 78 | prompts keeping turn identity; byte budget and cursor advance; the response |
| 79 | limit; past-end offsets; an empty session encoding `entries` as `[]`; the HTTP |
| 80 | endpoint's paging, session binding, capability advertisement, and 501 for a |
| 81 | controller without the capability; and outline reads racing streaming commits. |
| 82 | |
| 83 | ### Frontend (ran) |
| 84 | |
| 85 | ```sh |
| 86 | cd desktop/frontend |
| 87 | pnpm test:transcript # includes transcript-outline-store, chat-turn-jump, |
| 88 | # chat-turn-outline-jump |
| 89 | pnpm test:remote |
| 90 | pnpm build # typecheck, scroll-writer gate, CSS/theme, bundle budget |
| 91 | ``` |
| 92 | |
| 93 | `transcript-outline-store` covers multi-page assembly, duplicate identity, |
| 94 | stale cuts, a stalled cursor, absent capability versus a real failure, and |
| 95 | release fencing of an in-flight read. `chat-turn-jump` covers mount-confirmed |
| 96 | paging, an unproductive page, exhausted history, reader preemption, |
| 97 | supersession by a newer target, session replacement, explicit cancel, and the |
| 98 | bounded mount wait. `chat-turn-outline-jump` drives the real `Transcript` in |
| 99 | jsdom: the complete rail renders while only the newest turns are loaded, older |
| 100 | turns are marked unloaded, clicking one pages history until its node mounts, |
| 101 | numbering is unchanged by that paging, and the busy state clears. |
| 102 | |
| 103 | ### Browser (ran) |
| 104 | |
| 105 | ```sh |
| 106 | cd desktop/frontend |
| 107 | CHAT_BROWSER=chromium node bench/chat-transcript.mjs |
| 108 | ``` |
| 109 | |
| 110 | Confirms the production transcript fixture still renders, streams, and scrolls |
| 111 | with the navigation change in place. Both scenarios in that run reported zero |
| 112 | errors. Recorded on 2026-09-14, arm64 darwin, Chromium 153.0.8010.12: |
| 113 | |
| 114 | | Metric | 240 turns | 1000 turns | Gate | |
| 115 | | --- | --- | --- | --- | |
| 116 | | Input P95 | 46.5 ms | 137.6 ms | ≤ 200 ms | |
| 117 | | Switch P95 | — | 38.2 ms | ≤ 300 ms | |
| 118 | | Longest task | 53 ms | 218 ms | ≤ 500 ms | |
| 119 | | Heap growth | — | 0.33 MiB | ≤ 20 MiB | |
| 120 | | Anchor / prepend drift | 0 / 0.09 px | — | no drift | |
| 121 | | Mounted DOM nodes | 12089 | 47833 | bounded rail | |
| 122 | |
| 123 | It is a regression check of the real page, not a substitute for the native |
| 124 | platform runs below. The 1000-turn figure also shows the rail does not create a |
| 125 | mark per turn: marks are rendered only for the visible range. |
| 126 | |
| 127 | ### Windowed application (not run here) |
| 128 | |
| 129 | The Electron bench (`node bench/transcript-layout.mjs --electron`) and a packaged |
| 130 | Desktop build require a signed application bundle and were not exercised in this |
| 131 | environment. |
| 132 | |
| 133 | ### Windows (not verified here) |
| 134 | |
| 135 | The reported path was not replayed on Windows in this environment, so no Windows |
| 136 | build SHA can be recorded. Replaying the original scenario on a real Windows |
| 137 | desktop build remains an open external verification item and is not claimed by |
| 138 | this record. |
| 139 | |
| 140 | ## Compatibility |
| 141 | |
| 142 | The change is additive. A client without the capability keeps the loaded-turn |
| 143 | rail and does not claim complete navigation. No persisted session format, |
| 144 | provider message, tool schema, permissions, or prompt-cache byte changes, so |
| 145 | there is no prompt-cache impact. Rollback restores the previous frontend and |
| 146 | ignores the extra read-only endpoint. |
| 147 |