返回 DeepSeek-Reasonix
TRANSCRIPT_OUTLINE_NAVIGATION.md
根目录 / docs / TRANSCRIPT_OUTLINE_NAVIGATION.md
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
147 lines MARKDOWN