dsh-better-display 0.1.1

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 (300) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +41 -0
  5. package/README.md +41 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/cordis.patch.yml +5 -0
  8. package/cordis.yml +4 -0
  9. package/dshx.yml +5 -0
  10. package/lib/client.js +49923 -0
  11. package/lib/client.js.map +1 -0
  12. package/lib/dsh-better-display.js +256 -0
  13. package/lib/types/client/Blocks.d.ts +45 -0
  14. package/lib/types/client/Blocks.d.ts.map +1 -0
  15. package/lib/types/client/Blocks.js +136 -0
  16. package/lib/types/client/Blocks.js.map +1 -0
  17. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  18. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.js +170 -0
  20. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  21. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  22. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.js +15 -0
  24. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  25. package/lib/types/client/LiveFold.d.ts +10 -0
  26. package/lib/types/client/LiveFold.d.ts.map +1 -0
  27. package/lib/types/client/LiveFold.js +39 -0
  28. package/lib/types/client/LiveFold.js.map +1 -0
  29. package/lib/types/client/McpAppFrame.d.ts +32 -0
  30. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  31. package/lib/types/client/McpAppFrame.js +193 -0
  32. package/lib/types/client/McpAppFrame.js.map +1 -0
  33. package/lib/types/client/Reader.d.ts +3 -0
  34. package/lib/types/client/Reader.d.ts.map +1 -0
  35. package/lib/types/client/Reader.js +537 -0
  36. package/lib/types/client/Reader.js.map +1 -0
  37. package/lib/types/client/ReasoningCard.d.ts +11 -0
  38. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  39. package/lib/types/client/ReasoningCard.js +301 -0
  40. package/lib/types/client/ReasoningCard.js.map +1 -0
  41. package/lib/types/client/StickyLane.d.ts +8 -0
  42. package/lib/types/client/StickyLane.d.ts.map +1 -0
  43. package/lib/types/client/StickyLane.js +28 -0
  44. package/lib/types/client/StickyLane.js.map +1 -0
  45. package/lib/types/client/TimelineRail.d.ts +10 -0
  46. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  47. package/lib/types/client/TimelineRail.js +168 -0
  48. package/lib/types/client/TimelineRail.js.map +1 -0
  49. package/lib/types/client/ToolActivity.d.ts +17 -0
  50. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  51. package/lib/types/client/ToolActivity.js +192 -0
  52. package/lib/types/client/ToolActivity.js.map +1 -0
  53. package/lib/types/client/TurnMetrics.d.ts +11 -0
  54. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  55. package/lib/types/client/TurnMetrics.js +44 -0
  56. package/lib/types/client/TurnMetrics.js.map +1 -0
  57. package/lib/types/client/WaitingStatus.d.ts +6 -0
  58. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  59. package/lib/types/client/WaitingStatus.js +19 -0
  60. package/lib/types/client/WaitingStatus.js.map +1 -0
  61. package/lib/types/client/conversation-scroll.d.ts +19 -0
  62. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  63. package/lib/types/client/conversation-scroll.js +25 -0
  64. package/lib/types/client/conversation-scroll.js.map +1 -0
  65. package/lib/types/client/deliverables.d.ts +24 -0
  66. package/lib/types/client/deliverables.d.ts.map +1 -0
  67. package/lib/types/client/deliverables.js +104 -0
  68. package/lib/types/client/deliverables.js.map +1 -0
  69. package/lib/types/client/entry-policy.d.ts +10 -0
  70. package/lib/types/client/entry-policy.d.ts.map +1 -0
  71. package/lib/types/client/entry-policy.js +24 -0
  72. package/lib/types/client/entry-policy.js.map +1 -0
  73. package/lib/types/client/entry.d.ts +4 -0
  74. package/lib/types/client/entry.d.ts.map +1 -0
  75. package/lib/types/client/entry.js +34 -0
  76. package/lib/types/client/entry.js.map +1 -0
  77. package/lib/types/client/fold-choreography.d.ts +28 -0
  78. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  79. package/lib/types/client/fold-choreography.js +31 -0
  80. package/lib/types/client/fold-choreography.js.map +1 -0
  81. package/lib/types/client/index.d.ts +7 -0
  82. package/lib/types/client/index.d.ts.map +1 -0
  83. package/lib/types/client/index.js +153 -0
  84. package/lib/types/client/index.js.map +1 -0
  85. package/lib/types/client/live-turn.d.ts +58 -0
  86. package/lib/types/client/live-turn.d.ts.map +1 -0
  87. package/lib/types/client/live-turn.js +144 -0
  88. package/lib/types/client/live-turn.js.map +1 -0
  89. package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
  90. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  91. package/lib/types/client/markdown/MarkdownText.js +171 -0
  92. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  93. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  94. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  95. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  96. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  97. package/lib/types/client/markdown/incremental.d.ts +69 -0
  98. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  99. package/lib/types/client/markdown/incremental.js +108 -0
  100. package/lib/types/client/markdown/incremental.js.map +1 -0
  101. package/lib/types/client/markdown/katex.d.ts +27 -0
  102. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  103. package/lib/types/client/markdown/katex.js +88 -0
  104. package/lib/types/client/markdown/katex.js.map +1 -0
  105. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  106. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  107. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  108. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  109. package/lib/types/client/markdown/parse.d.ts +25 -0
  110. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  111. package/lib/types/client/markdown/parse.js +42 -0
  112. package/lib/types/client/markdown/parse.js.map +1 -0
  113. package/lib/types/client/markdown/render.d.ts +119 -0
  114. package/lib/types/client/markdown/render.d.ts.map +1 -0
  115. package/lib/types/client/markdown/render.js +435 -0
  116. package/lib/types/client/markdown/render.js.map +1 -0
  117. package/lib/types/client/mcp-app.d.ts +50 -0
  118. package/lib/types/client/mcp-app.d.ts.map +1 -0
  119. package/lib/types/client/mcp-app.js +328 -0
  120. package/lib/types/client/mcp-app.js.map +1 -0
  121. package/lib/types/client/message-chrome.d.ts +19 -0
  122. package/lib/types/client/message-chrome.d.ts.map +1 -0
  123. package/lib/types/client/message-chrome.js +42 -0
  124. package/lib/types/client/message-chrome.js.map +1 -0
  125. package/lib/types/client/motion.d.ts +38 -0
  126. package/lib/types/client/motion.d.ts.map +1 -0
  127. package/lib/types/client/motion.js +367 -0
  128. package/lib/types/client/motion.js.map +1 -0
  129. package/lib/types/client/native/ContextBody.d.ts +130 -0
  130. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  131. package/lib/types/client/native/ContextBody.js +398 -0
  132. package/lib/types/client/native/ContextBody.js.map +1 -0
  133. package/lib/types/client/native/ContextInjectionRow.d.ts +26 -0
  134. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  135. package/lib/types/client/native/ContextInjectionRow.js +27 -0
  136. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  137. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  138. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  139. package/lib/types/client/native/ReferenceIcon.js +16 -0
  140. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  141. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  142. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  143. package/lib/types/client/native/tool-call-model.js +219 -0
  144. package/lib/types/client/native/tool-call-model.js.map +1 -0
  145. package/lib/types/client/open-file.d.ts +24 -0
  146. package/lib/types/client/primitive-labels.d.ts +10 -0
  147. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  148. package/lib/types/client/primitive-labels.js +37 -0
  149. package/lib/types/client/primitive-labels.js.map +1 -0
  150. package/lib/types/client/projection.d.ts +41 -0
  151. package/lib/types/client/projection.d.ts.map +1 -0
  152. package/lib/types/client/projection.js +112 -0
  153. package/lib/types/client/projection.js.map +1 -0
  154. package/lib/types/client/reasoning-follow.d.ts +6 -0
  155. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  156. package/lib/types/client/reasoning-follow.js +12 -0
  157. package/lib/types/client/reasoning-follow.js.map +1 -0
  158. package/lib/types/client/settings-copy.d.ts +6 -0
  159. package/lib/types/client/settings-slots.d.ts +16 -0
  160. package/lib/types/client/store.d.ts +17 -0
  161. package/lib/types/client/store.d.ts.map +1 -0
  162. package/lib/types/client/store.js +13 -0
  163. package/lib/types/client/store.js.map +1 -0
  164. package/lib/types/client/stream-buffer.d.ts +29 -0
  165. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  166. package/lib/types/client/stream-buffer.js +113 -0
  167. package/lib/types/client/stream-buffer.js.map +1 -0
  168. package/lib/types/client/streaming.d.ts +18 -0
  169. package/lib/types/client/streaming.d.ts.map +1 -0
  170. package/lib/types/client/streaming.js +77 -0
  171. package/lib/types/client/streaming.js.map +1 -0
  172. package/lib/types/client/timeline.d.ts +26 -0
  173. package/lib/types/client/timeline.d.ts.map +1 -0
  174. package/lib/types/client/timeline.js +48 -0
  175. package/lib/types/client/timeline.js.map +1 -0
  176. package/lib/types/client/tool-activity.d.ts +52 -0
  177. package/lib/types/client/tool-activity.d.ts.map +1 -0
  178. package/lib/types/client/tool-activity.js +178 -0
  179. package/lib/types/client/tool-activity.js.map +1 -0
  180. package/lib/types/client/types.d.ts +66 -0
  181. package/lib/types/client/types.d.ts.map +1 -0
  182. package/lib/types/client/types.js +2 -0
  183. package/lib/types/client/types.js.map +1 -0
  184. package/lib/types/client/waiting-clock.d.ts +17 -0
  185. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  186. package/lib/types/client/waiting-clock.js +24 -0
  187. package/lib/types/client/waiting-clock.js.map +1 -0
  188. package/lib/types/client/word-motion.d.ts +16 -0
  189. package/lib/types/client/word-motion.d.ts.map +1 -0
  190. package/lib/types/client/word-motion.js +89 -0
  191. package/lib/types/client/word-motion.js.map +1 -0
  192. package/lib/types/client/word-timeline.d.ts +28 -0
  193. package/lib/types/client/word-timeline.d.ts.map +1 -0
  194. package/lib/types/client/word-timeline.js +100 -0
  195. package/lib/types/client/word-timeline.js.map +1 -0
  196. package/lib/types/dsh-better-display.d.ts +17 -0
  197. package/lib/types/dsh-better-display.d.ts.map +1 -0
  198. package/lib/types/dsh-better-display.js +53 -0
  199. package/lib/types/dsh-better-display.js.map +1 -0
  200. package/package.json +121 -0
  201. package/scripts/build-motion-fixture.mjs +26 -0
  202. package/scripts/link-harness-dependencies.mjs +51 -0
  203. package/skills/generative-mcpapps/SKILL.md +47 -0
  204. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  205. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  206. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  207. package/src/client/Blocks.tsx +205 -0
  208. package/src/client/ChoreographedFlow.tsx +182 -0
  209. package/src/client/ClosedProcessSummary.tsx +19 -0
  210. package/src/client/LiveFold.tsx +43 -0
  211. package/src/client/McpAppFrame.module.css +210 -0
  212. package/src/client/McpAppFrame.tsx +317 -0
  213. package/src/client/Reader.module.css +329 -0
  214. package/src/client/Reader.tsx +814 -0
  215. package/src/client/ReasoningCard.tsx +285 -0
  216. package/src/client/SettingsSection.module.css +147 -0
  217. package/src/client/SettingsSection.tsx +117 -0
  218. package/src/client/StickyLane.tsx +24 -0
  219. package/src/client/TimelineRail.module.css +170 -0
  220. package/src/client/TimelineRail.tsx +262 -0
  221. package/src/client/ToolActivity.tsx +214 -0
  222. package/src/client/TurnMetrics.module.css +139 -0
  223. package/src/client/TurnMetrics.tsx +160 -0
  224. package/src/client/WaitingStatus.tsx +22 -0
  225. package/src/client/conversation-scroll.ts +28 -0
  226. package/src/client/deliverables.ts +122 -0
  227. package/src/client/entry-policy.ts +20 -0
  228. package/src/client/entry.tsx +40 -0
  229. package/src/client/fold-choreography.ts +38 -0
  230. package/src/client/index.tsx +198 -0
  231. package/src/client/live-turn.ts +155 -0
  232. package/src/client/markdown/MarkdownText.module.css +304 -0
  233. package/src/client/markdown/MarkdownText.tsx +187 -0
  234. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  235. package/src/client/markdown/incremental.ts +131 -0
  236. package/src/client/markdown/katex.tsx +91 -0
  237. package/src/client/markdown/mathCompatibility.ts +350 -0
  238. package/src/client/markdown/parse.ts +45 -0
  239. package/src/client/markdown/render.tsx +630 -0
  240. package/src/client/markdown/upstream.json +14 -0
  241. package/src/client/mcp-app.ts +323 -0
  242. package/src/client/message-chrome.ts +47 -0
  243. package/src/client/motion.tsx +338 -0
  244. package/src/client/native/ContextBody.module.css +161 -0
  245. package/src/client/native/ContextBody.tsx +591 -0
  246. package/src/client/native/ContextInjectionRow.module.css +62 -0
  247. package/src/client/native/ContextInjectionRow.tsx +72 -0
  248. package/src/client/native/ReferenceIcon.tsx +35 -0
  249. package/src/client/native/tool-call-model.ts +248 -0
  250. package/src/client/native/upstream.json +41 -0
  251. package/src/client/open-file.ts +94 -0
  252. package/src/client/pending-submission.ts +49 -0
  253. package/src/client/primitive-labels.ts +53 -0
  254. package/src/client/projection.ts +118 -0
  255. package/src/client/reasoning-follow.ts +12 -0
  256. package/src/client/settings-copy.ts +53 -0
  257. package/src/client/settings-slots.ts +20 -0
  258. package/src/client/settings.tsx +73 -0
  259. package/src/client/skill-status.ts +78 -0
  260. package/src/client/store.ts +30 -0
  261. package/src/client/stream-buffer.ts +100 -0
  262. package/src/client/streaming.tsx +62 -0
  263. package/src/client/style.d.ts +4 -0
  264. package/src/client/timeline.ts +77 -0
  265. package/src/client/tool-activity.ts +152 -0
  266. package/src/client/types.ts +66 -0
  267. package/src/client/waiting-clock.ts +25 -0
  268. package/src/client/word-motion.tsx +91 -0
  269. package/src/client/word-timeline.ts +96 -0
  270. package/src/dsh-better-display.ts +108 -0
  271. package/src/skill-roots.ts +138 -0
  272. package/src/skill-status.ts +81 -0
  273. package/tests/browser-closed-gap.tsx +20 -0
  274. package/tests/browser-motion.tsx +65 -0
  275. package/tests/browser-sticky-lanes.tsx +22 -0
  276. package/tests/browser-waiting-clock.tsx +14 -0
  277. package/tests/chat-flow-hook.test.ts +66 -0
  278. package/tests/cloud-gate.test.ts +34 -0
  279. package/tests/conversation-scroll.test.ts +47 -0
  280. package/tests/deliverables.test.ts +128 -0
  281. package/tests/entry.test.ts +42 -0
  282. package/tests/fixtures/maid-composer-hide.html +31 -0
  283. package/tests/fold-choreography.test.ts +38 -0
  284. package/tests/history-word-regression.test.ts +16 -0
  285. package/tests/live-turn.test.ts +218 -0
  286. package/tests/mcp-apps.test.ts +157 -0
  287. package/tests/message-chrome.test.ts +29 -0
  288. package/tests/open-file.test.ts +103 -0
  289. package/tests/pending-submission.test.ts +45 -0
  290. package/tests/projection.test.ts +184 -0
  291. package/tests/reasoning-follow.test.ts +34 -0
  292. package/tests/settings-copy.test.ts +63 -0
  293. package/tests/skill-status.test.ts +137 -0
  294. package/tests/stock-install.test.ts +50 -0
  295. package/tests/stream-buffer.test.ts +97 -0
  296. package/tests/timeline.test.ts +59 -0
  297. package/tests/waiting-clock.test.ts +24 -0
  298. package/tests/word-timeline.test.ts +136 -0
  299. package/tsconfig.json +11 -0
  300. package/tsdown.config.ts +49 -0
@@ -0,0 +1,59 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mergeTimelineItems } from '../src/client/timeline.ts';
4
+
5
+ test('mergeTimelineItems handles empty inputs gracefully', () => {
6
+ assert.deepEqual(mergeTimelineItems([], undefined), []);
7
+ assert.deepEqual(mergeTimelineItems([], []), []);
8
+ assert.deepEqual(mergeTimelineItems(undefined, null), []);
9
+ });
10
+
11
+ test('mergeTimelineItems parses and normalizes wire turn outline entries', () => {
12
+ const outline = [
13
+ { turn: 1, seq: 10, prompt: 'Hello world', response: 'Hi there' },
14
+ { turn: 2, seq: 25, prompt: 'Fix the bug', response: 'Fixed' },
15
+ { turn: 'invalid', seq: 30 }, // should be ignored
16
+ null,
17
+ { turn: 0, seq: 1, prompt: '', response: '' },
18
+ ];
19
+
20
+ const items = mergeTimelineItems([], outline);
21
+ assert.equal(items.length, 3);
22
+ assert.equal(items[0].turn, 0);
23
+ assert.equal(items[1].turn, 1);
24
+ assert.equal(items[1].prompt, 'Hello world');
25
+ assert.equal(items[1].anchor.kind, 'unloaded');
26
+ if (items[1].anchor.kind === 'unloaded') {
27
+ assert.equal(items[1].anchor.seq, 10);
28
+ }
29
+ assert.equal(items[2].turn, 2);
30
+ });
31
+
32
+ test('mergeTimelineItems prioritizes loaded anchors and preserves deliverable flags', () => {
33
+ const outline = [
34
+ { turn: 1, seq: 10, prompt: 'Outline prompt', response: 'Outline reply' },
35
+ { turn: 2, seq: 20, prompt: 'Turn 2 prompt', response: '' },
36
+ ];
37
+
38
+ const loaded = [
39
+ { turn: 1, anchorKey: 'node-turn-1', prompt: 'Loaded prompt 1', response: 'Loaded reply 1' },
40
+ ];
41
+
42
+ const deliverables = new Set([1]);
43
+ const items = mergeTimelineItems(loaded, outline, deliverables);
44
+
45
+ assert.equal(items.length, 2);
46
+ // Turn 1 should be loaded anchor with freshest prompt & hasDeliverables = true
47
+ assert.equal(items[0].turn, 1);
48
+ assert.equal(items[0].anchor.kind, 'loaded');
49
+ if (items[0].anchor.kind === 'loaded') {
50
+ assert.equal(items[0].anchor.key, 'node-turn-1');
51
+ }
52
+ assert.equal(items[0].prompt, 'Loaded prompt 1');
53
+ assert.equal(items[0].hasDeliverables, true);
54
+
55
+ // Turn 2 is still unloaded
56
+ assert.equal(items[1].turn, 2);
57
+ assert.equal(items[1].anchor.kind, 'unloaded');
58
+ assert.equal(items[1].hasDeliverables, false);
59
+ });
@@ -0,0 +1,24 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { waitingAnchor } from '../src/client/waiting-clock.js';
4
+ const nodes = new Map([
5
+ ['u1', {kind:'user',data:{time:1000}}],
6
+ ['s1', {kind:'steering',data:{time:301000}}],
7
+ ['s2', {kind:'steering',data:{time:305000}}],
8
+ ['a1', {kind:'assistant-step',data:{time:302000}}],
9
+ ['ctx', {kind:'context',data:{time:399000}}],
10
+ ]);
11
+ const get=(key:string)=>nodes.get(key);
12
+ test('ordinary waiting starts at user input',()=>assert.deepEqual(waitingAnchor(['u1'],get),{key:'u1',time:1000}));
13
+ test('steering resets a five-minute-old turn to the new input',()=>assert.deepEqual(waitingAnchor(['u1','s1'],get),{key:'s1',time:301000}));
14
+ test('repeated steering resets even while the waiting indicator stays mounted',()=>assert.deepEqual(waitingAnchor(['u1','s1','s2'],get),{key:'s2',time:305000}));
15
+ test('assistant, context and tool rerenders do not reset waiting',()=>assert.deepEqual(waitingAnchor(['u1','s1','a1','ctx'],get),{key:'s1',time:301000}));
16
+ test('local echo resets immediately before steering admission',()=>assert.deepEqual(waitingAnchor(['u1'],get,[{requestId:'new',time:301000,placement:'next-step'}]),{key:'pending:new',time:301000}));
17
+ test('queued future turns and stale echoes cannot reset current waiting',()=>{
18
+ assert.deepEqual(waitingAnchor(['u1','s1'],get,[{requestId:'q',time:400000,placement:'queued'}]),{key:'s1',time:301000});
19
+ assert.deepEqual(waitingAnchor(['u1','s1'],get,[{requestId:'old',time:1000}]),{key:'s1',time:301000});
20
+ });
21
+ test('missing latest timestamp falls back to a new-input clock, not old history',()=>{
22
+ assert.deepEqual(waitingAnchor(['u1','bad'],key=>key==='bad'?{kind:'steering',data:{}}:get(key)),{key:'bad',time:null});
23
+ assert.deepEqual(waitingAnchor([],get),{key:'unresolved',time:null});
24
+ });
@@ -0,0 +1,136 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import type { Nodes } from 'mdast';
4
+ import { WORD_MOTION, WordTimeline } from '../src/client/word-timeline.js';
5
+ import { IncrementalMarkdownParser } from '../src/client/markdown/incremental.js';
6
+ import { parseGfm, parseGfmWithMath } from '../src/client/markdown/parse.js';
7
+
8
+ function textLeaves(node: Nodes): { value: string; offset: number }[] {
9
+ if (node.type === 'text') return [{ value: node.value, offset: node.position!.start.offset! }];
10
+ return 'children' in node ? node.children.flatMap(textLeaves) : [];
11
+ }
12
+
13
+ test('CJK resegmentation and unfinished Markdown cannot reveal later text before queued earlier text', () => {
14
+ for (const full of [
15
+ '前面的字还在排队,后面 **加粗重点** 怎么抢先出现。',
16
+ '前面 words waiting... [后面的链接](https://example.com) 接着文字。',
17
+ ]) {
18
+ const timeline = new WordTimeline();
19
+ timeline.begin('', true, 0, 0);
20
+ for (let end = 1; end <= full.length; end++) {
21
+ const now = 100 + end * 8;
22
+ const source = full.slice(0, end);
23
+ timeline.begin(source, true, 0, now);
24
+ const words = textLeaves(parseGfm(source)).flatMap(leaf => timeline.words(leaf.value, leaf.offset)).filter(word => word.text.trim());
25
+ for (let index = 1; index < words.length; index++) {
26
+ const previous = words[index - 1]!;
27
+ const current = words[index]!;
28
+ assert.ok((previous.born ?? -Infinity) <= (current.born ?? -Infinity), `${source}: ${JSON.stringify(previous)} overtaken by ${JSON.stringify(current)}`);
29
+ }
30
+ }
31
+ }
32
+ });
33
+
34
+ test('leaf render order cannot determine the order of word arrival', () => {
35
+ const timeline = new WordTimeline();
36
+ timeline.begin('', true, 0, 0);
37
+ timeline.begin('alpha beta', true, 0, 100);
38
+ const later = timeline.words('beta', 6)[0]!;
39
+ const earlier = timeline.words('alpha', 0)[0]!;
40
+ assert.ok(earlier.born! <= later.born!, 'A later-rendered earlier source leaf was scheduled last');
41
+ });
42
+
43
+ test('historical and already-received prefixes never replay', () => {
44
+ const history = new WordTimeline();
45
+ history.begin('已经完成的正文。', false, 0, 100);
46
+ assert.equal(history.hasLiveText, false);
47
+ assert.ok(history.words('已经完成的正文。', 0).every(word => word.born === null));
48
+ const running = new WordTimeline();
49
+ running.begin('已经收到的部分。', true, 0, 100);
50
+ assert.ok(running.words('已经收到的部分。', 0).every(word => word.born === null));
51
+ });
52
+
53
+ test('only new word identities receive the reference stagger, with a bounded queue', () => {
54
+ const timeline = new WordTimeline();
55
+ timeline.begin('', true, 0, 0);
56
+ timeline.begin('one two three four five six seven', true, 0, 100);
57
+ const words = timeline.words('one two three four five six seven', 0).filter(word => word.text.trim());
58
+ assert.deepEqual(words.slice(0, 3).map(word => word.born), [100, 160, 220]);
59
+ assert.ok(words.every(word => word.born! <= 100 + WORD_MOTION.maxDelay));
60
+ timeline.begin('one two three four five six seven eight', true, 0, 900);
61
+ assert.deepEqual(timeline.words('one two three', 0).filter(word => word.text.trim()).map(word => word.born), [100, 160, 220]);
62
+ });
63
+
64
+ test('stop, motion-off and authoritative replacement cancel old births', () => {
65
+ const timeline = new WordTimeline();
66
+ timeline.begin('', true, 0, 0);
67
+ timeline.begin('原始输出', true, 0, 100);
68
+ assert.ok(timeline.words('原始输出', 0).some(word => word.born !== null));
69
+ const previous = timeline.generation;
70
+ timeline.begin('修正的输出', true, 1, 200);
71
+ assert.ok(timeline.generation > previous);
72
+ assert.ok(timeline.words('修正的输出', 0).every(word => word.born === null));
73
+ timeline.begin('修正的输出', false, 1, 300);
74
+ timeline.begin('修正的输出', true, 1, 300);
75
+ assert.ok(timeline.words('修正的输出', 0).every(word => word.born === null));
76
+ });
77
+
78
+ test('word segmentation retains all whitespace, CJK, and combined emoji', () => {
79
+ const text = '中文 English\n👩🏽‍💻 e\u0301 🇨🇳';
80
+ const timeline = new WordTimeline();
81
+ timeline.begin('', true, 0, 0);
82
+ timeline.begin(text, true, 0, 100);
83
+ const words = timeline.words(text, 0);
84
+ assert.equal(words.map(word => word.text).join(''), text);
85
+ assert.ok(words.some(word => word.text === '👩🏽‍💻'));
86
+ assert.ok(words.some(word => word.text === 'e\u0301'));
87
+ });
88
+
89
+ test('animated word boxes preserve Chinese punctuation wrapping and source offsets', () => {
90
+ const text = '慢慢走着,看看「夜晚」与(远方)。 English, too!';
91
+ const timeline = new WordTimeline();
92
+ timeline.begin('', true, 0, 0);
93
+ timeline.begin(' '.repeat(20) + text, true, 0, 100);
94
+ const words = timeline.words(text, 20);
95
+ assert.equal(words.map(word => word.text).join(''), text);
96
+ for (const word of words) assert.equal(text.slice(word.key - 20, word.key - 20 + word.text.length), word.text);
97
+ assert.ok(words.some(word => word.text === '着,'));
98
+ assert.ok(words.some(word => word.text === '「夜晚」'));
99
+ assert.ok(words.some(word => word.text === '(远方)。'));
100
+ assert.ok(words.some(word => word.text === 'English,'));
101
+ });
102
+
103
+ test('a closing mark arriving later does not restart the preceding word', () => {
104
+ const timeline = new WordTimeline();
105
+ timeline.begin('', true, 0, 0);
106
+ timeline.begin('夜晚', true, 0, 100);
107
+ const first = timeline.words('夜晚', 0)[0];
108
+ timeline.begin('夜晚。', true, 0, 500);
109
+ const next = timeline.words('夜晚。', 0)[0];
110
+ assert.equal(next.key, first.key);
111
+ assert.equal(next.born, first.born);
112
+ assert.equal(next.text, '夜晚。');
113
+ });
114
+
115
+ test('incremental source offsets survive freezing and match the final parse', () => {
116
+ const text = '第一段。\n\n第二段 **加粗**。\n\n第三段。\n\n第四段。';
117
+ const parser = new IncrementalMarkdownParser(parseGfm);
118
+ for (let i = 1; i <= text.length; i++) parser.update(text.slice(0, i));
119
+ const parsed = parser.update(text);
120
+ const blocks = [...parsed.frozen, ...parsed.tail];
121
+ assert.deepEqual(blocks.map(block => block.key), parseGfm(text).children.map(node => node.position!.start.offset));
122
+ for (const block of blocks) {
123
+ const base = block.key - block.node.position!.start.offset!;
124
+ assert.equal(text.slice(base + block.node.position!.start.offset!, base + block.node.position!.end.offset!), text.slice(block.key, block.key + block.node.position!.end.offset! - block.node.position!.start.offset!));
125
+ }
126
+ });
127
+
128
+ test('the pinned DSH grammar retains CJK emphasis, GFM and math compatibility', () => {
129
+ const parsed = parseGfm('**加粗。**中文\n\n| 项目 | 数量 |\n| --- | --- |\n| A | 2 |\n\n- [x] 完成');
130
+ assert.ok(parsed.children.some(node => node.type === 'table'));
131
+ assert.ok(parsed.children.some(node => node.type === 'list'));
132
+ const first = parsed.children[0];
133
+ assert.ok(first?.type === 'paragraph' && first.children.some(node => node.type === 'strong'));
134
+ const math = parseGfmWithMath('公式 \\(x^2\\)');
135
+ assert.ok(math.children.some(node => node.type === 'paragraph' && node.children.some(child => child.type === 'inlineMath')));
136
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024", "module": "NodeNext", "moduleResolution": "NodeNext",
4
+ "strict": true, "skipLibCheck": true, "declaration": true,
5
+ "declarationMap": true, "sourceMap": true,
6
+ "rootDir": "src", "outDir": "lib/types", "jsx": "react-jsx",
7
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
8
+ "types": ["node", "react"]
9
+ },
10
+ "include": ["src"]
11
+ }
@@ -0,0 +1,49 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import type { TsdownPlugin, UserConfig } from 'tsdown';
6
+
7
+ function resolveHarness(): string {
8
+ const configured = process.env.DSHX_HARNESS?.trim();
9
+ if (configured) return resolve(configured);
10
+ const configPath = join(homedir(), '.config/dshx/harness');
11
+ const recorded = existsSync(configPath) ? readFileSync(configPath, 'utf8').trim() : undefined;
12
+ if (!recorded) {
13
+ throw new Error('client rebuild requires a Harness root from DSHX_HARNESS or ~/.config/dshx/harness');
14
+ }
15
+ return resolve(recorded);
16
+ }
17
+
18
+ const adapter = join(resolveHarness(), 'tools/dshx/src/client-build.js');
19
+ if (!existsSync(adapter)) throw new Error('externalClientBundle adapter is missing.');
20
+ const { externalClientBundle } = await import(pathToFileURL(adapter).href);
21
+
22
+ const bundle = externalClientBundle('dsh-better-display', ['src/dsh-better-display.ts'], {
23
+ clientEntry: 'src/client/index.tsx',
24
+ }) as UserConfig[];
25
+
26
+ const portableOutput: TsdownPlugin = {
27
+ name: 'dsh-better-display-portable-output',
28
+ generateBundle(_options, output) {
29
+ const client = output['client.js'];
30
+ if (client?.type !== 'chunk') this.error('client.js was not emitted');
31
+ client.code = client.code.replace(
32
+ /^([ \t]*\/\/#region \\0dshx-css-module:).*[\\/]([^/\\\r\n]+\.module\.css\.mjs)(\r?)$/gmu,
33
+ '$1$2$3',
34
+ );
35
+ if (/^.*\/\/#region \\0dshx-css-module:.*[\\/].*$/mu.test(client.code)) {
36
+ this.error('client.js contains a non-portable CSS module path');
37
+ }
38
+ },
39
+ };
40
+
41
+ export default bundle.map((config) => {
42
+ if (config.name !== 'dsh-better-display/client') return config;
43
+ const plugins = Array.isArray(config.plugins)
44
+ ? config.plugins
45
+ : config.plugins === undefined
46
+ ? []
47
+ : [config.plugins];
48
+ return { ...config, plugins: [...plugins, portableOutput] };
49
+ });