返回 DeepSeek-Reasonix
TRANSCRIPT_SCROLL_CONTRACT.md
根目录 / docs / TRANSCRIPT_SCROLL_CONTRACT.md
1 # Transcript scroll and history contract
2
3 [中文](TRANSCRIPT_SCROLL_CONTRACT.zh-CN.md)
4
5 ## Scope
6
7 The transcript (`../desktop/frontend/src/components/Transcript.tsx`) renders
8 through `ChatSource` and `ChatScrollController` in natural document flow. There
9 is one implementation for local and remote sessions. The retired window
10 adapter, measurement ledger, geometry-revision loop and logical selection are
11 not coming back: do not reintroduce a second rendering stack, a nested virtual
12 vertical scroller, or a platform-specific scroll compensation.
13
14 Keep these contracts when touching anything that can move the transcript
15 viewport or change which history is resident.
16
17 ## Identity and rendering
18
19 - **Stable node keys**: node and block keys derive from message, turn and tool
20 identities, never array positions. Prepend, settlement and content patches
21 must not rename a mounted node.
22 - **Unchanged nodes keep their object**: streaming and settlement update the
23 same assistant host; an unchanged node or order snapshot retains its
24 reference so React does not remount it.
25 - **Markdown block identity** comes from the parse: each block carries a key
26 (top-level index within one parse) and a content fingerprint stamped by the
27 parse that produced it. The render path keeps the previous AST object when
28 both match, which is what preserves native selection and code disclosure
29 across stream publications. Do not compare serialized trees on the render
30 path — that cost is what the fingerprint replaced.
31 - **Natural flow**: Markdown, tables and loaded history use document flow.
32 Parsing may be lazy and content may be fetched on demand, but the transcript
33 must not create a nested virtual vertical scroller. Collapsed process/tool
34 bodies are mounted on demand.
35 - **Business state lives in its owner**: the controller and the history stores
36 own state; `ChatSource` is a reconstructable view projection. Structural
37 changes batch in microtasks.
38 - **Turn order survives settlement**: a delayed user record precedes its own
39 output, while later answers and tools retain their surviving or newly formal
40 predecessors within that turn. Turn identity must not pull every live row
41 directly behind the user and reverse the order of sampling rounds.
42
43 ## Single writer
44
45 - Only `../desktop/frontend/src/lib/transcriptViewportWriter.ts` may mutate the
46 transcript's native scroll position. `ChatScrollController` owns programmatic
47 follow, reader anchoring and navigation; everything else submits to it.
48 `../desktop/frontend/scripts/check-single-scroll-writer.mjs` must reject any
49 bypass.
50 - Native input is never synthesized or prevented to keep the tail pinned. A
51 small upward reader movement releases follow, including inside the bottom
52 threshold.
53 - Prepend, resize and page replacement preserve a stable node plus a viewport
54 offset.
55
56 ## Bounded reading window
57
58 History is a bounded window, not an ever-growing list.
59
60 - The resident store keeps a small number of adjacent pages per session
61 (`windowMaxPages`, default 3, over 32-message pages) **including the active
62 session**. Paging past that budget reclaims a page from the end the reader is
63 moving away from and reports the item ids the caller must drop; a caller that
64 ignores them renders rows the store has already released.
65 - **Pins protect a session's identity and its live edge, not an unbounded
66 record set.** A running or visible session still cannot be evicted, but its
67 history is subject to the same page budget as any other.
68 - Reclaiming is not deletion. The persisted session stays authoritative and the
69 reclaimed direction stays reachable through its cursor, so every message is
70 still findable, searchable and exportable. Do not treat "all history is
71 mounted" as a correctness property; assert reachability and bounded residency
72 instead.
73 - Live events and batched stream deltas update the offscreen tail while the
74 reader is on an older page. They preserve the visible rows and the newer-page
75 flag until history navigation actually reaches that tail.
76 - Paging is bidirectional (`loadOlder` / `loadNewer`). A binding that reports no
77 newer cursor keeps its forward paging rather than being asked to simulate one
78 through full downloads.
79 - Window cursors pin a fixed snapshot. Appends keep a cursor valid; a storage
80 replacement or projection rebuild answers the typed `stale_cursor`, and a
81 cursor the server cannot read is that same typed answer rather than a
82 transport error. A client re-anchors at most once and keeps its current page
83 with a retry affordance after a second failure.
84 - The turn rail describes the complete durable conversation through paged
85 metadata (`history-outline-v1`), independently of the resident body window.
86 Summary eviction never removes navigation positions. Mounted live turns enrich
87 the metadata by message identity; pending submissions remain visible.
88 - Jumps to unloaded history (for example, a canonical search hit) resolve through
89 the history index and request the page around the target. They never walk
90 pages from the newest position.
91
92 ## Routing
93
94 - History reads route by the tab's **binding identity**, resolved before the
95 request from the tab metadata the controller already loads. A failed local
96 call must never be answered by a remote service holding a different session.
97 - Chat requires negotiated `transcript-v2` on Desktop and Serve. An older
98 service receives an upgrade error, without legacy chat fallback. Permission,
99 corruption and network errors do not trigger a protocol downgrade.
100
101 ## Generation fence
102
103 - Session or surface replacement increments the generation. Every delayed
104 measurement, timer, animation-frame callback and write request carries that
105 generation; stale work performs zero writes.
106 - Async paging owns a source-session request identity; navigation owns the
107 generation plus its interaction revision from request through the positioned
108 terminal state. Native takeover cancels navigation, not a valid source data
109 load. An old completion or `finally` may release only its identical request.
110 - A response from a replaced session must not advance coverage or mutate
111 another tab's state.
112
113 ## Budgets
114
115 - Per-renderer history body cache 32 MiB and parsed-markdown cache 16 MiB are
116 admission budgets for rebuildable data. They are not a bound on the whole
117 Electron process or on model-execution memory.
118 - String sizes are counted as resident representation; media is counted by
119 decoded size. Network bytes are not heap bytes.
120 - Reclaiming a page withdraws the body requests, parse tasks, DOM and object
121 URLs that belong to it.
122 - Text length and element counts that exceed a preview budget degrade to a
123 bounded preview with an explicit detail path. Do not silently truncate a
124 copy or export: an explicit full-content action or a streamed file export
125 carries the whole value.
126
127 ## Deterministic behaviour
128
129 - Scroll logic goes through the same injectable clock the controller uses
130 (`requestAnimationFrame`, `Date.now`, timer functions). No real sleeps and no
131 hidden retry clocks.
132 - A transaction whose requested offset has already landed may commit as a
133 no-op, but must not assign `scrollTop` again.
134 - **Race tests are mandatory**: any scroll or paging behaviour change ships
135 with a deterministic event sequence in
136 `../desktop/frontend/src/__tests__/`, and `pnpm test:transcript` runs before
137 committing transcript changes.
138
138 lines MARKDOWN