返回 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
39 ## Single writer
40
41 - Only `../desktop/frontend/src/lib/transcriptViewportWriter.ts` may mutate the
42 transcript's native scroll position. `ChatScrollController` owns programmatic
43 follow, reader anchoring and navigation; everything else submits to it.
44 `../desktop/frontend/scripts/check-single-scroll-writer.mjs` must reject any
45 bypass.
46 - Native input is never synthesized or prevented to keep the tail pinned. A
47 small upward reader movement releases follow, including inside the bottom
48 threshold.
49 - Prepend, resize and page replacement preserve a stable node plus a viewport
50 offset.
51
52 ## Bounded reading window
53
54 History is a bounded window, not an ever-growing list.
55
56 - The resident store keeps a small number of adjacent pages per session
57 (`windowMaxPages`, default 3, over 32-message pages) **including the active
58 session**. Paging past that budget reclaims a page from the end the reader is
59 moving away from and reports the item ids the caller must drop; a caller that
60 ignores them renders rows the store has already released.
61 - **Pins protect a session's identity and its live edge, not an unbounded
62 record set.** A running or visible session still cannot be evicted, but its
63 history is subject to the same page budget as any other.
64 - Reclaiming is not deletion. The persisted session stays authoritative and the
65 reclaimed direction stays reachable through its cursor, so every message is
66 still findable, searchable and exportable. Do not treat "all history is
67 mounted" as a correctness property; assert reachability and bounded residency
68 instead.
69 - Paging is bidirectional (`loadOlder` / `loadNewer`). A binding that reports no
70 newer cursor keeps its forward paging rather than being asked to simulate one
71 through full downloads.
72 - Window cursors pin a fixed snapshot. Appends keep a cursor valid; a storage
73 replacement or projection rebuild answers the typed `stale_cursor`, and a
74 cursor the server cannot read is that same typed answer rather than a
75 transport error. A client re-anchors at most once and keeps its current page
76 with a retry affordance after a second failure.
77 - The turn rail describes loaded turns only, as specified in
78 [Transcript v2](TRANSCRIPT_V2.md). Its marks track the resident window; it does
79 not enumerate unloaded history.
80 - Jumps to unloaded history (for example, a canonical search hit) resolve through
81 the history index and request the page around the target. They never walk
82 pages from the newest position.
83
84 ## Routing
85
86 - History reads route by the tab's **binding identity**, resolved before the
87 request from the tab metadata the controller already loads. A failed local
88 call must never be answered by a remote service holding a different session.
89 - Chat requires negotiated `transcript-v2` on Desktop and Serve. An older
90 service receives an upgrade error, without legacy chat fallback. Permission,
91 corruption and network errors do not trigger a protocol downgrade.
92
93 ## Generation fence
94
95 - Session or surface replacement increments the generation. Every delayed
96 measurement, timer, animation-frame callback and write request carries that
97 generation; stale work performs zero writes.
98 - Async paging owns a source-session request identity; navigation owns the
99 generation plus its interaction revision from request through the positioned
100 terminal state. Native takeover cancels navigation, not a valid source data
101 load. An old completion or `finally` may release only its identical request.
102 - A response from a replaced session must not advance coverage or mutate
103 another tab's state.
104
105 ## Budgets
106
107 - Per-renderer history body cache 32 MiB and parsed-markdown cache 16 MiB are
108 admission budgets for rebuildable data. They are not a bound on the whole
109 Electron process or on model-execution memory.
110 - String sizes are counted as resident representation; media is counted by
111 decoded size. Network bytes are not heap bytes.
112 - Reclaiming a page withdraws the body requests, parse tasks, DOM and object
113 URLs that belong to it.
114 - Text length and element counts that exceed a preview budget degrade to a
115 bounded preview with an explicit detail path. Do not silently truncate a
116 copy or export: an explicit full-content action or a streamed file export
117 carries the whole value.
118
119 ## Deterministic behaviour
120
121 - Scroll logic goes through the same injectable clock the controller uses
122 (`requestAnimationFrame`, `Date.now`, timer functions). No real sleeps and no
123 hidden retry clocks.
124 - A transaction whose requested offset has already landed may commit as a
125 no-op, but must not assign `scrollTop` again.
126 - **Race tests are mandatory**: any scroll or paging behaviour change ships
127 with a deterministic event sequence in
128 `../desktop/frontend/src/__tests__/`, and `pnpm test:transcript` runs before
129 committing transcript changes.
130
130 lines MARKDOWN