dsh-better-display 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/DESIGN.md +43 -3
  3. package/README.en.md +9 -1
  4. package/README.md +9 -1
  5. package/docs/dependency-update-policy.md +9 -0
  6. package/docs/extension-points.md +172 -0
  7. package/docs/official-rendering-bridge.md +59 -18
  8. package/docs/openapi.yaml +147 -0
  9. package/docs/runbooks/blank-reader.md +39 -0
  10. package/lib/client.js +1629 -586
  11. package/lib/client.js.map +1 -1
  12. package/lib/dsh-better-display.js +535 -57
  13. package/lib/types/client/AssistantNode.d.ts +20 -0
  14. package/lib/types/client/AssistantNode.d.ts.map +1 -0
  15. package/lib/types/client/AssistantNode.js +46 -0
  16. package/lib/types/client/AssistantNode.js.map +1 -0
  17. package/lib/types/client/Blocks.d.ts.map +1 -1
  18. package/lib/types/client/Blocks.js +33 -10
  19. package/lib/types/client/Blocks.js.map +1 -1
  20. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -1
  21. package/lib/types/client/ChoreographedFlow.js +2 -0
  22. package/lib/types/client/ChoreographedFlow.js.map +1 -1
  23. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -1
  24. package/lib/types/client/ClosedProcessSummary.js +3 -1
  25. package/lib/types/client/ClosedProcessSummary.js.map +1 -1
  26. package/lib/types/client/McpAppFrame.d.ts.map +1 -1
  27. package/lib/types/client/McpAppFrame.js +5 -11
  28. package/lib/types/client/McpAppFrame.js.map +1 -1
  29. package/lib/types/client/OfficialContent.d.ts +17 -3
  30. package/lib/types/client/OfficialContent.d.ts.map +1 -1
  31. package/lib/types/client/OfficialContent.js +33 -7
  32. package/lib/types/client/OfficialContent.js.map +1 -1
  33. package/lib/types/client/Reader.d.ts.map +1 -1
  34. package/lib/types/client/Reader.js +76 -73
  35. package/lib/types/client/Reader.js.map +1 -1
  36. package/lib/types/client/SettingsSection.d.ts.map +1 -1
  37. package/lib/types/client/SettingsSection.js +0 -5
  38. package/lib/types/client/SettingsSection.js.map +1 -1
  39. package/lib/types/client/StickyLane.d.ts +12 -0
  40. package/lib/types/client/StickyLane.d.ts.map +1 -1
  41. package/lib/types/client/StickyLane.js +78 -2
  42. package/lib/types/client/StickyLane.js.map +1 -1
  43. package/lib/types/client/ToolActivity.d.ts.map +1 -1
  44. package/lib/types/client/ToolActivity.js +10 -5
  45. package/lib/types/client/ToolActivity.js.map +1 -1
  46. package/lib/types/client/deliverables.d.ts.map +1 -1
  47. package/lib/types/client/deliverables.js +2 -0
  48. package/lib/types/client/deliverables.js.map +1 -1
  49. package/lib/types/client/index.d.ts +3 -1
  50. package/lib/types/client/index.d.ts.map +1 -1
  51. package/lib/types/client/index.js +15 -1
  52. package/lib/types/client/index.js.map +1 -1
  53. package/lib/types/client/live-turn.d.ts +17 -5
  54. package/lib/types/client/live-turn.d.ts.map +1 -1
  55. package/lib/types/client/live-turn.js +106 -76
  56. package/lib/types/client/live-turn.js.map +1 -1
  57. package/lib/types/client/motion.d.ts +26 -2
  58. package/lib/types/client/motion.d.ts.map +1 -1
  59. package/lib/types/client/motion.js +144 -10
  60. package/lib/types/client/motion.js.map +1 -1
  61. package/lib/types/client/official-slots.d.ts +9 -0
  62. package/lib/types/client/official-slots.d.ts.map +1 -1
  63. package/lib/types/client/official-slots.js +22 -4
  64. package/lib/types/client/official-slots.js.map +1 -1
  65. package/lib/types/client/open-file.d.ts.map +1 -1
  66. package/lib/types/client/open-file.js +1 -6
  67. package/lib/types/client/open-file.js.map +1 -1
  68. package/lib/types/client/projection.d.ts +15 -2
  69. package/lib/types/client/projection.d.ts.map +1 -1
  70. package/lib/types/client/projection.js +16 -4
  71. package/lib/types/client/projection.js.map +1 -1
  72. package/lib/types/client/rewind-cut.d.ts +131 -0
  73. package/lib/types/client/rewind-cut.d.ts.map +1 -0
  74. package/lib/types/client/rewind-cut.js +321 -0
  75. package/lib/types/client/rewind-cut.js.map +1 -0
  76. package/lib/types/client/styles.d.ts +3 -0
  77. package/lib/types/client/styles.d.ts.map +1 -0
  78. package/lib/types/client/styles.js +25 -0
  79. package/lib/types/client/styles.js.map +1 -0
  80. package/lib/types/client/text-transform.d.ts +93 -0
  81. package/lib/types/client/text-transform.d.ts.map +1 -0
  82. package/lib/types/client/text-transform.js +183 -0
  83. package/lib/types/client/text-transform.js.map +1 -0
  84. package/lib/types/client/timeline.d.ts.map +1 -1
  85. package/lib/types/client/timeline.js.map +1 -1
  86. package/lib/types/client/tool-activity.d.ts +11 -4
  87. package/lib/types/client/tool-activity.d.ts.map +1 -1
  88. package/lib/types/client/tool-activity.js +54 -9
  89. package/lib/types/client/tool-activity.js.map +1 -1
  90. package/lib/types/client/turn-slots.d.ts +29 -0
  91. package/lib/types/client/turn-slots.d.ts.map +1 -0
  92. package/lib/types/client/turn-slots.js +62 -0
  93. package/lib/types/client/turn-slots.js.map +1 -0
  94. package/lib/types/client/types.d.ts +18 -0
  95. package/lib/types/client/types.d.ts.map +1 -1
  96. package/lib/types/dsh-better-display.d.ts +4 -1
  97. package/lib/types/dsh-better-display.d.ts.map +1 -1
  98. package/lib/types/dsh-better-display.js +55 -43
  99. package/lib/types/dsh-better-display.js.map +1 -1
  100. package/lib/types/flags.d.ts +7 -0
  101. package/lib/types/flags.d.ts.map +1 -0
  102. package/lib/types/flags.js +37 -0
  103. package/lib/types/flags.js.map +1 -0
  104. package/lib/types/log.d.ts +17 -0
  105. package/lib/types/log.d.ts.map +1 -0
  106. package/lib/types/log.js +122 -0
  107. package/lib/types/log.js.map +1 -0
  108. package/lib/types/metrics.d.ts +18 -0
  109. package/lib/types/metrics.d.ts.map +1 -0
  110. package/lib/types/metrics.js +35 -0
  111. package/lib/types/metrics.js.map +1 -0
  112. package/lib/types/reveal.d.ts +12 -0
  113. package/lib/types/reveal.d.ts.map +1 -0
  114. package/lib/types/reveal.js +147 -0
  115. package/lib/types/reveal.js.map +1 -0
  116. package/lib/types/workspace-path.d.ts +3 -0
  117. package/lib/types/workspace-path.d.ts.map +1 -0
  118. package/lib/types/workspace-path.js +5 -0
  119. package/lib/types/workspace-path.js.map +1 -0
  120. package/package.json +36 -5
  121. package/scripts/changelog.mjs +20 -0
  122. package/scripts/check-agents-md.mjs +67 -0
  123. package/scripts/check-bundle-size.mjs +38 -0
  124. package/scripts/check-dead-flags.mjs +55 -0
  125. package/scripts/check-todo-debt.mjs +69 -0
  126. package/scripts/security-audit-report.mjs +92 -0
  127. package/scripts/verify-commentary-visible.mjs +627 -0
  128. package/src/client/AssistantNode.tsx +72 -0
  129. package/src/client/Blocks.tsx +49 -10
  130. package/src/client/ChoreographedFlow.tsx +3 -1
  131. package/src/client/ClosedProcessSummary.tsx +3 -1
  132. package/src/client/McpAppFrame.tsx +5 -13
  133. package/src/client/OfficialContent.tsx +47 -9
  134. package/src/client/Reader.module.css +103 -21
  135. package/src/client/Reader.tsx +83 -88
  136. package/src/client/SettingsSection.tsx +1 -7
  137. package/src/client/StickyLane.tsx +81 -2
  138. package/src/client/ToolActivity.tsx +10 -6
  139. package/src/client/deliverables.ts +1 -0
  140. package/src/client/index.tsx +18 -2
  141. package/src/client/live-turn.ts +100 -69
  142. package/src/client/motion.tsx +142 -7
  143. package/src/client/official-slots.tsx +23 -4
  144. package/src/client/open-file.ts +2 -8
  145. package/src/client/projection.ts +16 -4
  146. package/src/client/rewind-cut.ts +340 -0
  147. package/src/client/styles.ts +22 -0
  148. package/src/client/text-transform.ts +265 -0
  149. package/src/client/timeline.ts +0 -2
  150. package/src/client/tool-activity.ts +67 -9
  151. package/src/client/turn-slots.ts +80 -0
  152. package/src/client/types.ts +19 -0
  153. package/src/dsh-better-display.ts +57 -42
  154. package/src/flags.ts +40 -0
  155. package/src/log.ts +137 -0
  156. package/src/metrics.ts +50 -0
  157. package/src/reveal.ts +90 -0
  158. package/src/workspace-path.ts +4 -0
  159. package/tests/browser-commentary.tsx +191 -0
  160. package/tests/chat-flow-hook.test.ts +1 -1
  161. package/tests/cloud-gate.test.ts +1 -1
  162. package/tests/context-tool-update.test.ts +1 -1
  163. package/tests/conversation-scroll.test.ts +1 -1
  164. package/tests/deliverables.test.ts +1 -1
  165. package/tests/entry.test.ts +1 -1
  166. package/tests/fixtures/reader-lane-owners.tsx +46 -0
  167. package/tests/fixtures/reader-official-users.tsx +234 -0
  168. package/tests/fixtures/reader-theme-overlap.html +80 -0
  169. package/tests/fixtures/reader-user-bubble.tsx +222 -0
  170. package/tests/flags.test.ts +66 -0
  171. package/tests/fold-choreography.test.ts +32 -8
  172. package/tests/fold-intensity.test.ts +11 -6
  173. package/tests/footer-precedence.test.ts +9 -1
  174. package/tests/history-word-regression.test.ts +1 -1
  175. package/tests/live-turn.test.ts +125 -54
  176. package/tests/log.test.ts +99 -0
  177. package/tests/mcp-apps.test.ts +1 -1
  178. package/tests/message-chrome.test.ts +1 -1
  179. package/tests/metrics.test.ts +52 -0
  180. package/tests/official-slots.test.ts +41 -2
  181. package/tests/open-file.test.ts +1 -1
  182. package/tests/pending-submission.test.ts +12 -1
  183. package/tests/pinned-selection.test.ts +16 -0
  184. package/tests/platform-media.test.ts +1 -1
  185. package/tests/projection.test.ts +66 -3
  186. package/tests/reader-glass-css.test.ts +28 -0
  187. package/tests/reader-official-users.test.ts +91 -0
  188. package/tests/reader-sticky-surface.test.ts +377 -0
  189. package/tests/reader-theme-lanes.test.ts +54 -0
  190. package/tests/reader-theme-overlap.test.ts +151 -0
  191. package/tests/reader-user-bubble.test.ts +137 -0
  192. package/tests/reasoning-follow.test.ts +1 -1
  193. package/tests/retiring-content.test.ts +229 -0
  194. package/tests/reveal.test.ts +128 -0
  195. package/tests/rewind-cut.test.ts +1090 -0
  196. package/tests/settings-copy.test.ts +1 -1
  197. package/tests/skill-status.test.ts +1 -1
  198. package/tests/stock-install.test.ts +9 -1
  199. package/tests/stream-buffer.test.ts +1 -1
  200. package/tests/styles.test.ts +75 -0
  201. package/tests/terminal-input-preview.test.ts +113 -0
  202. package/tests/text-transform.test.ts +272 -0
  203. package/tests/timeline.test.ts +1 -1
  204. package/tests/tool-clock.test.ts +1 -1
  205. package/tests/tool-diff.test.ts +1 -1
  206. package/tests/waiting-clock.test.ts +1 -1
  207. package/tests/word-timeline.test.ts +1 -1
  208. package/tsdown.config.ts +49 -1
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Which chat rows a `/rewind` withdrew, expressed as a pure function of the
3
+ * chat snapshot.
4
+ *
5
+ * WHY THIS EXISTS. A rewind never deletes history: the host appends one
6
+ * replacement `user/message` marker whose `surfaceOp` shadows everything after
7
+ * the target, and the transcript must then read as it did *before* the target
8
+ * was sent. The official ChatView is a per-node renderer, so the rewind plugin
9
+ * (`dsh-rewind-plugin`) hides withdrawn rows by DOM: it computes the shadowed
10
+ * anchor seqs from the command node and sets `display:none` on every element
11
+ * carrying the official `data-chat-anchor-key` seat attribute.
12
+ *
13
+ * The Reader replaces ChatView's message list and renders its own row shapes
14
+ * (turn sections, answer articles, tool rows), so it does not republish that
15
+ * seat attribute on every row. DOM hiding therefore reaches only the user and
16
+ * steering bubbles, and every other withdrawn row — the answer, the tool rows,
17
+ * and the `/rewind` command row itself — stayed on screen. That is the reported
18
+ * defect: the command is recorded as a rewind row instead of the Reader rolling
19
+ * back to the state before the message was sent.
20
+ *
21
+ * The Reader decides visibility from React state, so it must apply the same
22
+ * contract itself instead of waiting for a third-party MutationObserver. The
23
+ * marker node is not admitted as a chat node (only append-origin surface events
24
+ * become nodes), so the command node's `outcome.sourceEventSeq` — the marker's
25
+ * log seq — plus the `@<seq>` target in its `args` are the only client-side
26
+ * signal for the shadowed range. This mirrors the plugin's own reader of that
27
+ * contract, and is pinned by `tests/rewind-cut.test.ts` so a plugin or host
28
+ * change cannot silently regress it.
29
+ *
30
+ * @module dsh-better-display/rewind-cut
31
+ */
32
+ function asRecord(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
34
+ ? value
35
+ : undefined;
36
+ }
37
+ function safeSeq(value) {
38
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
39
+ }
40
+ /** The command payload of a `command` chat node, when it is structurally readable. */
41
+ function commandOf(node) {
42
+ if (node.kind !== 'command')
43
+ return undefined;
44
+ return asRecord(node.data);
45
+ }
46
+ /**
47
+ * The engine names an executed rewind command node can carry. The plugin
48
+ * registers its command as `rewind-plugin` (`ENGINE_COMMANDS = new
49
+ * Set(['rewind-plugin', 'rewind'])` in dsh-rewind-plugin's
50
+ * `src/client/hidden.ts`), and that is the name the client projection actually
51
+ * publishes — the session log's `command/run.name` is not what the Reader sees.
52
+ * `rewind` stays accepted because older logs and the bili-side proxy record the
53
+ * shorter spelling.
54
+ */
55
+ const REWIND_COMMANDS = new Set(['rewind-plugin', 'rewind']);
56
+ /** True when a command payload is the rewind engine command, in either spelling. */
57
+ function isRewindCommand(command) {
58
+ return typeof command.name === 'string' && REWIND_COMMANDS.has(command.name);
59
+ }
60
+ /**
61
+ * The render POSITION a row is anchored at: the view node's `anchorSeq`, else
62
+ * its payload seq.
63
+ *
64
+ * This is deliberately NOT integer-only. The host anchors synthetic rows at
65
+ * fractional offsets off a real seq (`CHAT_SYNTHETIC_SEQ_OFFSETS` in the
66
+ * official ChatView: `turn-process` at `controlAnchorSeq - 0.1`, `turn-tail` at
67
+ * `seq + 0.1`, an interrupted assistant step at `seq - 0.9`). An integer-only
68
+ * read made those rows invisible to the cut, so the last `turn-process` of a
69
+ * withdrawn turn survived on its own. Log seqs (marker, target) stay integer:
70
+ * see `safeSeq`.
71
+ */
72
+ export function anchorSeqOf(node) {
73
+ const raw = node.anchorSeq ?? asRecord(node.data)?.seq;
74
+ return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : undefined;
75
+ }
76
+ /**
77
+ * Extract the rewind target seq from a `/rewind` command's structured `args`
78
+ * (e.g. `@5 chat`, `preview @5 both`). Locale-independent: it never parses the
79
+ * host's human outcome copy.
80
+ */
81
+ export function rewindTargetOfArgs(args) {
82
+ if (typeof args !== 'string')
83
+ return undefined;
84
+ const match = args.match(/@(\d+)/);
85
+ return match === null ? undefined : Number(match[1]);
86
+ }
87
+ /** True for the internal candidate-list probe (`/rewind __candidates`). */
88
+ function isCandidateCommand(command) {
89
+ return typeof command.args === 'string' && command.args.includes('__candidates');
90
+ }
91
+ /** True for an impact preview (`/rewind preview @5 both`). */
92
+ function isPreviewCommand(command) {
93
+ return typeof command.args === 'string' && command.args.includes('preview');
94
+ }
95
+ /**
96
+ * Merge overlapping or touching shadow ranges.
97
+ *
98
+ * Spans are kept SEPARATE unless they genuinely overlap: collapsing every cut
99
+ * to `[min target, max marker]` would hide a still-on-surface gap of new traffic
100
+ * between an earlier marker and a later target.
101
+ */
102
+ export function coalesceCutSpans(spans) {
103
+ if (spans.length < 2)
104
+ return [...spans];
105
+ const sorted = [...spans].sort((left, right) => left.start - right.start || left.end - right.end);
106
+ const merged = [];
107
+ for (const span of sorted) {
108
+ const last = merged.at(-1);
109
+ if (last !== undefined && span.start <= last.end) {
110
+ if (span.end > last.end)
111
+ last.end = span.end;
112
+ continue;
113
+ }
114
+ merged.push({ start: span.start, end: span.end });
115
+ }
116
+ return merged;
117
+ }
118
+ /**
119
+ * True when a row anchored at `seq` falls inside one of the cut spans.
120
+ *
121
+ * A cut runs from the target seq through the marker's own log seq, inclusive
122
+ * (`target <= anchorSeq && anchorSeq <= marker`), the same comparison
123
+ * `dsh-rewind-plugin`'s `RewindVisibilityBuilder` uses. The upper bound stops
124
+ * at the marker. Extending it to the next integer hides the following turn's
125
+ * process row, which the host anchors at `startSeq - 0.1` and therefore in
126
+ * `[marker, marker + 1)`. Fractional anchors inside the closed range still
127
+ * match: `anchorSeqOf` keeps them. A process or tail row parked outside the
128
+ * range is handled by `summaryTurns`, not by widening the cut. Spans stay
129
+ * disjoint, so new traffic between two rewinds survives.
130
+ */
131
+ export function cutContains(cuts, seq) {
132
+ let low = 0;
133
+ let high = cuts.length - 1;
134
+ let found = -1;
135
+ while (low <= high) {
136
+ const middle = (low + high) >> 1;
137
+ const cut = cuts[middle];
138
+ if (cut === undefined)
139
+ return false;
140
+ if (seq < cut.start)
141
+ high = middle - 1;
142
+ else {
143
+ found = middle;
144
+ low = middle + 1;
145
+ }
146
+ }
147
+ const span = found < 0 ? undefined : cuts[found];
148
+ return span !== undefined && seq <= span.end;
149
+ }
150
+ /** The turn id a node belongs to (absent outside a turn). */
151
+ function turnIdOf(node) {
152
+ // The location's own `kind` varies (`turn` for a turn's synthetic rows,
153
+ // `step` for one of its messages); only the turn it carries matters here.
154
+ const turn = asRecord(asRecord(node.location)?.turn);
155
+ if (turn === undefined || !Object.hasOwn(turn, 'turn'))
156
+ return undefined;
157
+ return turn.turn;
158
+ }
159
+ /**
160
+ * Node kinds that SUMMARIZE a turn instead of carrying its content, so the host
161
+ * anchors them at the turn's edges rather than at an event inside it.
162
+ */
163
+ const TURN_SUMMARY_KINDS = new Set(['turn-process', 'turn-tail']);
164
+ /**
165
+ * True for a rewind command row the transcript should not keep.
166
+ *
167
+ * Probes (`preview`, `__candidates`) exist only to feed the popover. An
168
+ * executed rewind — success plus a marker seq — is the command that performed
169
+ * the cut, and the plugin hides that row even when its anchor sits outside
170
+ * the numeric range. A failed or still-running command stays visible.
171
+ */
172
+ function commandHidden(node) {
173
+ const command = commandOf(node);
174
+ if (command === undefined || !isRewindCommand(command))
175
+ return false;
176
+ if (isPreviewCommand(command) || isCandidateCommand(command))
177
+ return true;
178
+ const outcome = asRecord(command.outcome);
179
+ return outcome?.kind === 'success' && safeSeq(outcome.sourceEventSeq) !== undefined;
180
+ }
181
+ /** True when the row's own anchor falls in a cut, or the row is a rewind command to hide. */
182
+ function anchorWithdrawn(node, cuts) {
183
+ if (commandHidden(node))
184
+ return true;
185
+ const seq = anchorSeqOf(node);
186
+ return seq !== undefined && cutContains(cuts, seq);
187
+ }
188
+ /**
189
+ * Turns whose process and tail rows must leave.
190
+ *
191
+ * The host opens a turn before its user message is logged and anchors
192
+ * `turn-process` at `controlAnchorSeq - 0.1`, below the first withdrawn
193
+ * event, and `turn-tail` at `turn.end.seq + 0.1`, which can sit past the
194
+ * marker. Containment therefore misses those two rows, and the transcript
195
+ * kept announcing `执行过程 / 本轮已停止,已生成的内容仍保留。`
196
+ *
197
+ * A summary leaves only when every content row of its turn is already
198
+ * withdrawn — the same rule as the plugin's `deriveTurnProcess`. One content
199
+ * row before the target (a mid-turn steering rewind) keeps the summary and
200
+ * that earlier message. A turn with no content rows is left alone.
201
+ */
202
+ function summaryTurns(chat, cuts) {
203
+ const stays = new Map();
204
+ for (const key of chat.order) {
205
+ const node = chat.nodes.get(key);
206
+ if (node === undefined || TURN_SUMMARY_KINDS.has(node.kind ?? ''))
207
+ continue;
208
+ const id = turnIdOf(node);
209
+ if (id === undefined)
210
+ continue;
211
+ const visible = node.visibility !== 'hidden' && !anchorWithdrawn(node, cuts);
212
+ stays.set(id, (stays.get(id) ?? false) || visible);
213
+ }
214
+ const withdrawn = new Set();
215
+ for (const [id, visible] of stays)
216
+ if (!visible)
217
+ withdrawn.add(id);
218
+ return withdrawn;
219
+ }
220
+ /** True when this row is a summary of a turn whose content is fully withdrawn. */
221
+ function summaryWithdrawn(node, summaries) {
222
+ if (!TURN_SUMMARY_KINDS.has(node.kind ?? ''))
223
+ return false;
224
+ const id = turnIdOf(node);
225
+ return id !== undefined && summaries.has(id);
226
+ }
227
+ /**
228
+ * The shadow ranges a session's executed rewinds produced, newest state last.
229
+ * An executed rewind cuts `[target, marker]` — the target itself is included,
230
+ * because a rewind restores the state *before* that message was sent (its text
231
+ * goes back to the composer).
232
+ */
233
+ export function rewindCutSpans(chat) {
234
+ if (chat === undefined)
235
+ return [];
236
+ const spans = [];
237
+ for (const key of chat.order) {
238
+ const node = chat.nodes.get(key);
239
+ if (node === undefined)
240
+ continue;
241
+ const command = commandOf(node);
242
+ if (command === undefined || !isRewindCommand(command))
243
+ continue;
244
+ // Probes never open a span, even when a later host stamps them with a
245
+ // success marker. Matching them would roll the transcript back on preview.
246
+ if (isPreviewCommand(command) || isCandidateCommand(command))
247
+ continue;
248
+ const outcome = asRecord(command.outcome);
249
+ if (outcome?.kind !== 'success')
250
+ continue;
251
+ const marker = safeSeq(outcome.sourceEventSeq);
252
+ if (marker === undefined)
253
+ continue;
254
+ const target = rewindTargetOfArgs(command.args);
255
+ if (target === undefined || target > marker)
256
+ continue;
257
+ spans.push({ start: target, end: marker });
258
+ }
259
+ return coalesceCutSpans(spans);
260
+ }
261
+ /**
262
+ * The anchor seqs the Reader must not render: every withdrawn message (the
263
+ * target, everything after it, and the unrendered marker), plus every `/rewind`
264
+ * row that exists only to feed the popover — the candidate-list probe and each
265
+ * impact preview. A SUCCESSFUL executed command row is covered by its own cut;
266
+ * a failed or unsettled one stays visible as an ordinary command record.
267
+ */
268
+ export function hiddenRewindSeqs(chat) {
269
+ const hidden = new Set();
270
+ if (chat === undefined)
271
+ return hidden;
272
+ const cuts = rewindCutSpans(chat);
273
+ const summaries = summaryTurns(chat, cuts);
274
+ for (const key of chat.order) {
275
+ const node = chat.nodes.get(key);
276
+ if (node === undefined)
277
+ continue;
278
+ if (!anchorWithdrawn(node, cuts) && !summaryWithdrawn(node, summaries))
279
+ continue;
280
+ const seq = anchorSeqOf(node);
281
+ if (seq !== undefined)
282
+ hidden.add(seq);
283
+ }
284
+ return hidden;
285
+ }
286
+ /**
287
+ * The chat keys the Reader should render, in snapshot order: withdrawn rows and
288
+ * rows the host itself marked hidden are dropped, everything else keeps its
289
+ * position. Returns the snapshot's own `order` array when nothing is filtered,
290
+ * so React selectors see a stable reference.
291
+ */
292
+ export function visibleChatKeys(chat) {
293
+ const cuts = rewindCutSpans(chat);
294
+ const summaries = summaryTurns(chat, cuts);
295
+ const visible = [];
296
+ for (const key of chat.order) {
297
+ const node = chat.nodes.get(key);
298
+ if (node === undefined)
299
+ continue;
300
+ if (node.visibility === 'hidden')
301
+ continue;
302
+ if (anchorWithdrawn(node, cuts) || summaryWithdrawn(node, summaries))
303
+ continue;
304
+ visible.push(key);
305
+ }
306
+ return visible.length === chat.order.length ? chat.order : visible;
307
+ }
308
+ /**
309
+ * A comparable signal for the visible row set, built for
310
+ * `useSyncExternalStore`-style subscription: the host re-runs a chat selector
311
+ * on every publish and compares the RESULT by `Object.is`, and a rewind
312
+ * publishes an in-place upsert of an existing command row. The snapshot's
313
+ * `order` array and node store are therefore both reference-stable across a
314
+ * rewind, so a selector on them never re-renders; a string does change exactly
315
+ * when the visible set does. Never return the array itself from a selector —
316
+ * a fresh array fails the `Object.is` check on every publish.
317
+ */
318
+ export function visibleChatSignature(chat) {
319
+ return visibleChatKeys(chat).join('\u0000');
320
+ }
321
+ //# sourceMappingURL=rewind-cut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rewind-cut.js","sourceRoot":"","sources":["../../../src/client/rewind-cut.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** Retain injected CSS across host cleanup and restore it before Reader paints. */
2
+ export declare function ensureStyles(): void;
3
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/client/styles.ts"],"names":[],"mappings":"AAGA,mFAAmF;AACnF,wBAAgB,YAAY,IAAI,IAAI,CAiBnC"}
@@ -0,0 +1,25 @@
1
+ const plugin = 'dsh-better-display';
2
+ const styles = new Map();
3
+ /** Retain injected CSS across host cleanup and restore it before Reader paints. */
4
+ export function ensureStyles() {
5
+ if (typeof document === 'undefined')
6
+ return;
7
+ const live = new Set();
8
+ for (const tag of document.querySelectorAll(`style[data-plugin="${plugin}"][data-plugin-css]`)) {
9
+ const id = tag.dataset.pluginCss;
10
+ if (id === undefined)
11
+ continue;
12
+ styles.set(id, tag.textContent ?? '');
13
+ live.add(id);
14
+ }
15
+ for (const [id, text] of styles) {
16
+ if (live.has(id))
17
+ continue;
18
+ const tag = document.createElement('style');
19
+ tag.dataset.plugin = plugin;
20
+ tag.dataset.pluginCss = id;
21
+ tag.textContent = text;
22
+ document.head.appendChild(tag);
23
+ }
24
+ }
25
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../../src/client/styles.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,GAAG,oBAAoB,CAAC;AACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;AAEzC,mFAAmF;AACnF,MAAM,UAAU,YAAY;IAC1B,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,gBAAgB,CAAmB,sBAAsB,MAAM,qBAAqB,CAAC,EAAE,CAAC;QACjH,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC;QACjC,IAAI,EAAE,KAAK,SAAS;YAAE,SAAS;QAC/B,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACf,CAAC;IACD,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,SAAS;QAC3B,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC5C,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;QAC5B,GAAG,CAAC,OAAO,CAAC,SAAS,GAAG,EAAE,CAAC;QAC3B,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;AACH,CAAC"}
@@ -0,0 +1,93 @@
1
+ import type { ReaderBlockOwner, ReaderBlockPresentation } from './types.js';
2
+ /**
3
+ * Cordis service name. Another client plugin lists this in `inject` and
4
+ * registers a transformer. The reading view paints the result. The session
5
+ * block, transcript, and fold inputs stay on the original text.
6
+ */
7
+ export declare const TEXT_TRANSFORM_SERVICE = "dsh-better-display.text-transform";
8
+ export type DisplayTextKind = 'reasoning' | 'body';
9
+ export interface DisplayTextRequest {
10
+ /** Original block text. Registrants must not write it back onto the block. */
11
+ text: string;
12
+ streaming: boolean;
13
+ /** Opaque id of this mounted block. Stable until the block unmounts. */
14
+ blockKey: string;
15
+ kind: DisplayTextKind;
16
+ /** Assistant body and reasoning are the usual targets. Tool prose uses `tool`. */
17
+ source?: ReaderBlockOwner['source'];
18
+ }
19
+ export interface DisplayText {
20
+ text: string;
21
+ /**
22
+ * True when this string is the settled display for the current source.
23
+ * False keeps the live reveal open, including after the model has stopped
24
+ * but a translation batch is still outstanding.
25
+ */
26
+ final: boolean;
27
+ }
28
+ /**
29
+ * Sync `read` on every paint, plus `subscribe` when a later batch changes
30
+ * what `read` returns. `subscribe` may be called once per kind and must
31
+ * accept more than one listener.
32
+ */
33
+ export interface DisplayTextTransformer {
34
+ read(request: DisplayTextRequest): DisplayText | string | undefined;
35
+ subscribe?(listener: () => void): () => void;
36
+ }
37
+ export interface DisplayCursor extends DisplayText {
38
+ /**
39
+ * Increments when the displayed string or its `final` bit changes.
40
+ * This is not the reveal clock. Passing it to the word timeline as the
41
+ * motion revision replays or snaps the paragraph.
42
+ */
43
+ transformRevision: number;
44
+ /**
45
+ * Reveal identity. Stays put when the new string extends the previous
46
+ * display string, so an appended translation batch continues the fade.
47
+ * Increments only when the string is replaced, which settles the new
48
+ * text instead of typing it again from the first grapheme.
49
+ */
50
+ motionRevision: number;
51
+ }
52
+ export interface TextTransformRegistry {
53
+ register(kind: DisplayTextKind | readonly DisplayTextKind[], transformer: DisplayTextTransformer): () => void;
54
+ has(kind: DisplayTextKind): boolean;
55
+ read(request: DisplayTextRequest): DisplayCursor;
56
+ subscribe(kind: DisplayTextKind, listener: () => void): () => void;
57
+ release(kind: DisplayTextKind, blockKey: string): void;
58
+ }
59
+ /**
60
+ * Appends keep the motion revision. A replacement gets a new one so the
61
+ * timeline treats the swapped string as already settled.
62
+ */
63
+ export declare function nextDisplayCursor(previous: DisplayCursor | undefined, display: DisplayText): DisplayCursor;
64
+ /**
65
+ * Streaming flag fed to the reveal. With no transformer this is the model's
66
+ * flag. A transformer that reports `final: !streaming` produces the same flag.
67
+ */
68
+ export declare function displayStreaming(sourceStreaming: boolean, shown: Pick<DisplayCursor, 'final'>, active: boolean): boolean;
69
+ /** No registry, or none for this kind: the source string, and no cursor write. */
70
+ export declare function readDisplayText(registry: TextTransformRegistry | undefined, request: DisplayTextRequest): DisplayCursor;
71
+ export declare function createTextTransformRegistry(): TextTransformRegistry;
72
+ /** Points the reading view at the registry this client plugin provides. */
73
+ export declare function installTextTransformRegistry(registry: TextTransformRegistry | undefined): () => void;
74
+ export declare function activeTextTransform(): TextTransformRegistry | undefined;
75
+ /**
76
+ * Subscribe while mounted. The reveal components call this only after
77
+ * `has(kind)`, so an empty registry adds no hook and no listener.
78
+ */
79
+ export declare function useDisplayText(registry: TextTransformRegistry, request: DisplayTextRequest): DisplayCursor;
80
+ /** Chain-slot owner. Extra fields are optional so older callers still type-check. */
81
+ export declare function readerBlockOwner(block: ReaderBlockOwner['block'], streaming: boolean, source: ReaderBlockOwner['source'], presentation: ReaderBlockPresentation, holdFormatting: boolean): ReaderBlockOwner;
82
+ /** Copy actions read this. Display text never replaces it. */
83
+ export declare function originalAnswerText(blocks: readonly {
84
+ readonly kind: string;
85
+ readonly text?: string;
86
+ }[]): string;
87
+ declare module '@deepseek-ai/cordis' {
88
+ interface Context {
89
+ /** Display-only reader text transforms. Present after this client plugin applies. */
90
+ 'dsh-better-display.text-transform'?: TextTransformRegistry;
91
+ }
92
+ }
93
+ //# sourceMappingURL=text-transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-transform.d.ts","sourceRoot":"","sources":["../../../src/client/text-transform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ /**
3
+ * Cordis service name. Another client plugin lists this in `inject` and
4
+ * registers a transformer. The reading view paints the result. The session
5
+ * block, transcript, and fold inputs stay on the original text.
6
+ */
7
+ export const TEXT_TRANSFORM_SERVICE = 'dsh-better-display.text-transform';
8
+ const KINDS = new Set(['reasoning', 'body']);
9
+ function isKind(value) {
10
+ return KINDS.has(value);
11
+ }
12
+ function reportTransformError(error) {
13
+ console.warn('[dsh-better-display] text transform failed; showing original text', error);
14
+ }
15
+ function passthrough(request) {
16
+ return { text: request.text, final: !request.streaming, transformRevision: 0, motionRevision: 0 };
17
+ }
18
+ function normalize(value, request) {
19
+ if (typeof value === 'string')
20
+ return { text: value, final: !request.streaming };
21
+ if (!value || typeof value.text !== 'string')
22
+ return { text: request.text, final: !request.streaming };
23
+ return { text: value.text, final: typeof value.final === 'boolean' ? value.final : !request.streaming };
24
+ }
25
+ /**
26
+ * Appends keep the motion revision. A replacement gets a new one so the
27
+ * timeline treats the swapped string as already settled.
28
+ */
29
+ export function nextDisplayCursor(previous, display) {
30
+ if (!previous)
31
+ return { ...display, transformRevision: 0, motionRevision: 0 };
32
+ if (previous.text === display.text && previous.final === display.final)
33
+ return previous;
34
+ const appended = display.text.startsWith(previous.text);
35
+ return {
36
+ text: display.text,
37
+ final: display.final,
38
+ transformRevision: previous.transformRevision + 1,
39
+ motionRevision: appended ? previous.motionRevision : previous.motionRevision + 1,
40
+ };
41
+ }
42
+ /**
43
+ * Streaming flag fed to the reveal. With no transformer this is the model's
44
+ * flag. A transformer that reports `final: !streaming` produces the same flag.
45
+ */
46
+ export function displayStreaming(sourceStreaming, shown, active) {
47
+ return active ? !shown.final : sourceStreaming;
48
+ }
49
+ /** No registry, or none for this kind: the source string, and no cursor write. */
50
+ export function readDisplayText(registry, request) {
51
+ if (!registry?.has(request.kind))
52
+ return passthrough(request);
53
+ return registry.read(request);
54
+ }
55
+ class TextTransformRegistryImpl {
56
+ stacks = new Map();
57
+ listeners = new Map();
58
+ cursors = new Map();
59
+ register(kind, transformer) {
60
+ const requested = [...new Set(Array.isArray(kind) ? kind : [kind])];
61
+ for (const item of requested) {
62
+ if (!isKind(item))
63
+ throw new Error(`Unknown display text kind: ${String(item)}`);
64
+ }
65
+ const bound = [];
66
+ for (const item of requested) {
67
+ const entry = { transformer };
68
+ try {
69
+ entry.unsubscribe = transformer.subscribe?.(() => { this.emit(item); });
70
+ }
71
+ catch (error) {
72
+ reportTransformError(error);
73
+ }
74
+ const stack = this.stacks.get(item) ?? [];
75
+ stack.push(entry);
76
+ this.stacks.set(item, stack);
77
+ bound.push({ kind: item, entry });
78
+ this.emit(item);
79
+ }
80
+ return () => {
81
+ for (const { kind: item, entry } of bound) {
82
+ try {
83
+ entry.unsubscribe?.();
84
+ }
85
+ catch (error) {
86
+ reportTransformError(error);
87
+ }
88
+ const stack = this.stacks.get(item);
89
+ if (!stack)
90
+ continue;
91
+ const next = stack.filter(candidate => candidate !== entry);
92
+ if (next.length > 0)
93
+ this.stacks.set(item, next);
94
+ else
95
+ this.stacks.delete(item);
96
+ this.emit(item);
97
+ }
98
+ };
99
+ }
100
+ has(kind) {
101
+ return (this.stacks.get(kind)?.length ?? 0) > 0;
102
+ }
103
+ read(request) {
104
+ const transformer = this.stacks.get(request.kind)?.at(-1)?.transformer;
105
+ if (!transformer)
106
+ return passthrough(request);
107
+ let display;
108
+ try {
109
+ display = normalize(transformer.read(request), request);
110
+ }
111
+ catch (error) {
112
+ // A broken transformer must never hide the original text. Show it and
113
+ // ask again on the next text change or notify, so a recovered
114
+ // transformer takes over without a remount.
115
+ reportTransformError(error);
116
+ display = { text: request.text, final: !request.streaming };
117
+ }
118
+ const key = `${request.kind}\u0000${request.blockKey}`;
119
+ const next = nextDisplayCursor(this.cursors.get(key), display);
120
+ this.cursors.set(key, next);
121
+ return next;
122
+ }
123
+ subscribe(kind, listener) {
124
+ const set = this.listeners.get(kind) ?? new Set();
125
+ set.add(listener);
126
+ this.listeners.set(kind, set);
127
+ return () => { set.delete(listener); };
128
+ }
129
+ release(kind, blockKey) {
130
+ this.cursors.delete(`${kind}\u0000${blockKey}`);
131
+ }
132
+ emit(kind) {
133
+ const set = this.listeners.get(kind);
134
+ if (!set)
135
+ return;
136
+ for (const listener of [...set])
137
+ listener();
138
+ }
139
+ }
140
+ export function createTextTransformRegistry() {
141
+ return new TextTransformRegistryImpl();
142
+ }
143
+ let active;
144
+ /** Points the reading view at the registry this client plugin provides. */
145
+ export function installTextTransformRegistry(registry) {
146
+ const previous = active;
147
+ active = registry;
148
+ return () => { active = previous; };
149
+ }
150
+ export function activeTextTransform() {
151
+ return active;
152
+ }
153
+ /**
154
+ * Subscribe while mounted. The reveal components call this only after
155
+ * `has(kind)`, so an empty registry adds no hook and no listener.
156
+ */
157
+ export function useDisplayText(registry, request) {
158
+ const [epoch, setEpoch] = useState(0);
159
+ const { text, streaming, blockKey, kind, source } = request;
160
+ useEffect(() => registry.subscribe(kind, () => { setEpoch(value => value + 1); }), [registry, kind]);
161
+ useEffect(() => () => { registry.release(kind, blockKey); }, [registry, kind, blockKey]);
162
+ return useMemo(() => registry.read({ text, streaming, blockKey, kind, source }), [registry, text, streaming, blockKey, kind, source, epoch]);
163
+ }
164
+ /** Chain-slot owner. Extra fields are optional so older callers still type-check. */
165
+ export function readerBlockOwner(block, streaming, source, presentation, holdFormatting) {
166
+ return {
167
+ block,
168
+ streaming,
169
+ source,
170
+ presentation: {
171
+ startedAt: presentation.startedAt,
172
+ interrupted: presentation.interrupted,
173
+ liveText: presentation.liveText,
174
+ },
175
+ holdFormatting,
176
+ liveText: presentation.liveText,
177
+ };
178
+ }
179
+ /** Copy actions read this. Display text never replaces it. */
180
+ export function originalAnswerText(blocks) {
181
+ return blocks.flatMap(block => block.kind === 'text' && typeof block.text === 'string' ? [block.text] : []).join('\n\n');
182
+ }
183
+ //# sourceMappingURL=text-transform.js.map