@drscrewdriver/dsh-better-display 0.3.3-fork.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 (390) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +55 -0
  5. package/README.md +55 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/compat/harness-rc2.json +99 -0
  8. package/cordis.patch.yml +5 -0
  9. package/cordis.yml +4 -0
  10. package/docs/official-rendering-bridge.md +65 -0
  11. package/dshx.yml +5 -0
  12. package/lib/client.js +49292 -0
  13. package/lib/client.js.map +1 -0
  14. package/lib/dsh-better-display.js +101 -0
  15. package/lib/types/client/Blocks.d.ts +49 -0
  16. package/lib/types/client/Blocks.d.ts.map +1 -0
  17. package/lib/types/client/Blocks.js +143 -0
  18. package/lib/types/client/Blocks.js.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  20. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  21. package/lib/types/client/ChoreographedFlow.js +218 -0
  22. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  24. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  25. package/lib/types/client/ClosedProcessSummary.js +16 -0
  26. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  27. package/lib/types/client/DiffPanel.d.ts +13 -0
  28. package/lib/types/client/DiffPanel.d.ts.map +1 -0
  29. package/lib/types/client/DiffPanel.js +115 -0
  30. package/lib/types/client/DiffPanel.js.map +1 -0
  31. package/lib/types/client/LiveFold.d.ts +10 -0
  32. package/lib/types/client/LiveFold.d.ts.map +1 -0
  33. package/lib/types/client/LiveFold.js +39 -0
  34. package/lib/types/client/LiveFold.js.map +1 -0
  35. package/lib/types/client/McpAppFrame.d.ts +32 -0
  36. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  37. package/lib/types/client/McpAppFrame.js +193 -0
  38. package/lib/types/client/McpAppFrame.js.map +1 -0
  39. package/lib/types/client/OfficialContent.d.ts +25 -0
  40. package/lib/types/client/OfficialContent.d.ts.map +1 -0
  41. package/lib/types/client/OfficialContent.js +53 -0
  42. package/lib/types/client/OfficialContent.js.map +1 -0
  43. package/lib/types/client/Reader.d.ts +3 -0
  44. package/lib/types/client/Reader.d.ts.map +1 -0
  45. package/lib/types/client/Reader.js +603 -0
  46. package/lib/types/client/Reader.js.map +1 -0
  47. package/lib/types/client/ReasoningCard.d.ts +11 -0
  48. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  49. package/lib/types/client/ReasoningCard.js +322 -0
  50. package/lib/types/client/ReasoningCard.js.map +1 -0
  51. package/lib/types/client/SettingsSection.d.ts +37 -0
  52. package/lib/types/client/SettingsSection.d.ts.map +1 -0
  53. package/lib/types/client/SettingsSection.js +63 -0
  54. package/lib/types/client/SettingsSection.js.map +1 -0
  55. package/lib/types/client/StickyLane.d.ts +8 -0
  56. package/lib/types/client/StickyLane.d.ts.map +1 -0
  57. package/lib/types/client/StickyLane.js +39 -0
  58. package/lib/types/client/StickyLane.js.map +1 -0
  59. package/lib/types/client/TimelineRail.d.ts +10 -0
  60. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  61. package/lib/types/client/TimelineRail.js +168 -0
  62. package/lib/types/client/TimelineRail.js.map +1 -0
  63. package/lib/types/client/ToolActivity.d.ts +17 -0
  64. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  65. package/lib/types/client/ToolActivity.js +244 -0
  66. package/lib/types/client/ToolActivity.js.map +1 -0
  67. package/lib/types/client/TurnMetrics.d.ts +11 -0
  68. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  69. package/lib/types/client/TurnMetrics.js +44 -0
  70. package/lib/types/client/TurnMetrics.js.map +1 -0
  71. package/lib/types/client/WaitClock.d.ts +16 -0
  72. package/lib/types/client/WaitClock.d.ts.map +1 -0
  73. package/lib/types/client/WaitClock.js +29 -0
  74. package/lib/types/client/WaitClock.js.map +1 -0
  75. package/lib/types/client/WaitingClock.d.ts +15 -0
  76. package/lib/types/client/WaitingClock.d.ts.map +1 -0
  77. package/lib/types/client/WaitingClock.js +30 -0
  78. package/lib/types/client/WaitingClock.js.map +1 -0
  79. package/lib/types/client/WaitingStatus.d.ts +6 -0
  80. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  81. package/lib/types/client/WaitingStatus.js +9 -0
  82. package/lib/types/client/WaitingStatus.js.map +1 -0
  83. package/lib/types/client/conversation-scroll.d.ts +19 -0
  84. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  85. package/lib/types/client/conversation-scroll.js +25 -0
  86. package/lib/types/client/conversation-scroll.js.map +1 -0
  87. package/lib/types/client/deliverables.d.ts +24 -0
  88. package/lib/types/client/deliverables.d.ts.map +1 -0
  89. package/lib/types/client/deliverables.js +104 -0
  90. package/lib/types/client/deliverables.js.map +1 -0
  91. package/lib/types/client/entry-policy.d.ts +10 -0
  92. package/lib/types/client/entry-policy.d.ts.map +1 -0
  93. package/lib/types/client/entry-policy.js +24 -0
  94. package/lib/types/client/entry-policy.js.map +1 -0
  95. package/lib/types/client/entry.d.ts +4 -0
  96. package/lib/types/client/entry.d.ts.map +1 -0
  97. package/lib/types/client/entry.js +34 -0
  98. package/lib/types/client/entry.js.map +1 -0
  99. package/lib/types/client/fold-choreography.d.ts +40 -0
  100. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  101. package/lib/types/client/fold-choreography.js +48 -0
  102. package/lib/types/client/fold-choreography.js.map +1 -0
  103. package/lib/types/client/fold-intensity.d.ts +11 -0
  104. package/lib/types/client/fold-intensity.d.ts.map +1 -0
  105. package/lib/types/client/fold-intensity.js +32 -0
  106. package/lib/types/client/fold-intensity.js.map +1 -0
  107. package/lib/types/client/index.d.ts +7 -0
  108. package/lib/types/client/index.d.ts.map +1 -0
  109. package/lib/types/client/index.js +195 -0
  110. package/lib/types/client/index.js.map +1 -0
  111. package/lib/types/client/live-turn.d.ts +58 -0
  112. package/lib/types/client/live-turn.d.ts.map +1 -0
  113. package/lib/types/client/live-turn.js +190 -0
  114. package/lib/types/client/live-turn.js.map +1 -0
  115. package/lib/types/client/markdown/MarkdownText.d.ts +41 -0
  116. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  117. package/lib/types/client/markdown/MarkdownText.js +178 -0
  118. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  119. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  120. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  121. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  122. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  123. package/lib/types/client/markdown/incremental.d.ts +69 -0
  124. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  125. package/lib/types/client/markdown/incremental.js +108 -0
  126. package/lib/types/client/markdown/incremental.js.map +1 -0
  127. package/lib/types/client/markdown/katex.d.ts +27 -0
  128. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  129. package/lib/types/client/markdown/katex.js +88 -0
  130. package/lib/types/client/markdown/katex.js.map +1 -0
  131. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  132. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  133. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  134. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  135. package/lib/types/client/markdown/parse.d.ts +25 -0
  136. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  137. package/lib/types/client/markdown/parse.js +42 -0
  138. package/lib/types/client/markdown/parse.js.map +1 -0
  139. package/lib/types/client/markdown/render.d.ts +121 -0
  140. package/lib/types/client/markdown/render.d.ts.map +1 -0
  141. package/lib/types/client/markdown/render.js +452 -0
  142. package/lib/types/client/markdown/render.js.map +1 -0
  143. package/lib/types/client/mcp-app.d.ts +50 -0
  144. package/lib/types/client/mcp-app.d.ts.map +1 -0
  145. package/lib/types/client/mcp-app.js +328 -0
  146. package/lib/types/client/mcp-app.js.map +1 -0
  147. package/lib/types/client/message-chrome.d.ts +19 -0
  148. package/lib/types/client/message-chrome.d.ts.map +1 -0
  149. package/lib/types/client/message-chrome.js +42 -0
  150. package/lib/types/client/message-chrome.js.map +1 -0
  151. package/lib/types/client/motion.d.ts +38 -0
  152. package/lib/types/client/motion.d.ts.map +1 -0
  153. package/lib/types/client/motion.js +453 -0
  154. package/lib/types/client/motion.js.map +1 -0
  155. package/lib/types/client/native/ContextBody.d.ts +130 -0
  156. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  157. package/lib/types/client/native/ContextBody.js +398 -0
  158. package/lib/types/client/native/ContextBody.js.map +1 -0
  159. package/lib/types/client/native/ContextInjectionRow.d.ts +35 -0
  160. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  161. package/lib/types/client/native/ContextInjectionRow.js +42 -0
  162. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  163. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  164. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  165. package/lib/types/client/native/ReferenceIcon.js +16 -0
  166. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  167. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  168. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  169. package/lib/types/client/native/tool-call-model.js +219 -0
  170. package/lib/types/client/native/tool-call-model.js.map +1 -0
  171. package/lib/types/client/native/tool-update.d.ts +23 -0
  172. package/lib/types/client/native/tool-update.d.ts.map +1 -0
  173. package/lib/types/client/native/tool-update.js +22 -0
  174. package/lib/types/client/native/tool-update.js.map +1 -0
  175. package/lib/types/client/official-slots.d.ts +71 -0
  176. package/lib/types/client/official-slots.d.ts.map +1 -0
  177. package/lib/types/client/official-slots.js +183 -0
  178. package/lib/types/client/official-slots.js.map +1 -0
  179. package/lib/types/client/open-file.d.ts +29 -0
  180. package/lib/types/client/open-file.d.ts.map +1 -0
  181. package/lib/types/client/open-file.js +71 -0
  182. package/lib/types/client/open-file.js.map +1 -0
  183. package/lib/types/client/pending-submission.d.ts +28 -0
  184. package/lib/types/client/pending-submission.d.ts.map +1 -0
  185. package/lib/types/client/pending-submission.js +28 -0
  186. package/lib/types/client/pending-submission.js.map +1 -0
  187. package/lib/types/client/platform-media.d.ts +6 -0
  188. package/lib/types/client/platform-media.d.ts.map +1 -0
  189. package/lib/types/client/platform-media.js +18 -0
  190. package/lib/types/client/platform-media.js.map +1 -0
  191. package/lib/types/client/primitive-labels.d.ts +10 -0
  192. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  193. package/lib/types/client/primitive-labels.js +38 -0
  194. package/lib/types/client/primitive-labels.js.map +1 -0
  195. package/lib/types/client/projection.d.ts +41 -0
  196. package/lib/types/client/projection.d.ts.map +1 -0
  197. package/lib/types/client/projection.js +112 -0
  198. package/lib/types/client/projection.js.map +1 -0
  199. package/lib/types/client/reasoning-follow.d.ts +10 -0
  200. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  201. package/lib/types/client/reasoning-follow.js +21 -0
  202. package/lib/types/client/reasoning-follow.js.map +1 -0
  203. package/lib/types/client/resource-address.d.ts +3 -0
  204. package/lib/types/client/resource-address.d.ts.map +1 -0
  205. package/lib/types/client/resource-address.js +33 -0
  206. package/lib/types/client/resource-address.js.map +1 -0
  207. package/lib/types/client/settings-copy.d.ts +7 -0
  208. package/lib/types/client/settings-copy.d.ts.map +1 -0
  209. package/lib/types/client/settings-copy.js +53 -0
  210. package/lib/types/client/settings-copy.js.map +1 -0
  211. package/lib/types/client/settings-slots.d.ts +23 -0
  212. package/lib/types/client/settings-slots.d.ts.map +1 -0
  213. package/lib/types/client/settings-slots.js +2 -0
  214. package/lib/types/client/settings-slots.js.map +1 -0
  215. package/lib/types/client/settings.d.ts +4 -0
  216. package/lib/types/client/settings.d.ts.map +1 -0
  217. package/lib/types/client/settings.js +60 -0
  218. package/lib/types/client/settings.js.map +1 -0
  219. package/lib/types/client/skill-status.d.ts +25 -0
  220. package/lib/types/client/skill-status.d.ts.map +1 -0
  221. package/lib/types/client/skill-status.js +44 -0
  222. package/lib/types/client/skill-status.js.map +1 -0
  223. package/lib/types/client/store.d.ts +35 -0
  224. package/lib/types/client/store.d.ts.map +1 -0
  225. package/lib/types/client/store.js +37 -0
  226. package/lib/types/client/store.js.map +1 -0
  227. package/lib/types/client/stream-buffer.d.ts +37 -0
  228. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  229. package/lib/types/client/stream-buffer.js +150 -0
  230. package/lib/types/client/stream-buffer.js.map +1 -0
  231. package/lib/types/client/streaming.d.ts +18 -0
  232. package/lib/types/client/streaming.d.ts.map +1 -0
  233. package/lib/types/client/streaming.js +84 -0
  234. package/lib/types/client/streaming.js.map +1 -0
  235. package/lib/types/client/timeline.d.ts +26 -0
  236. package/lib/types/client/timeline.d.ts.map +1 -0
  237. package/lib/types/client/timeline.js +48 -0
  238. package/lib/types/client/timeline.js.map +1 -0
  239. package/lib/types/client/tool-activity.d.ts +98 -0
  240. package/lib/types/client/tool-activity.d.ts.map +1 -0
  241. package/lib/types/client/tool-activity.js +301 -0
  242. package/lib/types/client/tool-activity.js.map +1 -0
  243. package/lib/types/client/types.d.ts +91 -0
  244. package/lib/types/client/types.d.ts.map +1 -0
  245. package/lib/types/client/types.js +2 -0
  246. package/lib/types/client/types.js.map +1 -0
  247. package/lib/types/client/waiting-clock.d.ts +49 -0
  248. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  249. package/lib/types/client/waiting-clock.js +80 -0
  250. package/lib/types/client/waiting-clock.js.map +1 -0
  251. package/lib/types/client/word-motion.d.ts +16 -0
  252. package/lib/types/client/word-motion.d.ts.map +1 -0
  253. package/lib/types/client/word-motion.js +90 -0
  254. package/lib/types/client/word-motion.js.map +1 -0
  255. package/lib/types/client/word-timeline.d.ts +32 -0
  256. package/lib/types/client/word-timeline.d.ts.map +1 -0
  257. package/lib/types/client/word-timeline.js +119 -0
  258. package/lib/types/client/word-timeline.js.map +1 -0
  259. package/lib/types/dsh-better-display.d.ts +17 -0
  260. package/lib/types/dsh-better-display.d.ts.map +1 -0
  261. package/lib/types/dsh-better-display.js +93 -0
  262. package/lib/types/dsh-better-display.js.map +1 -0
  263. package/lib/types/skill-roots.d.ts +22 -0
  264. package/lib/types/skill-roots.d.ts.map +1 -0
  265. package/lib/types/skill-roots.js +126 -0
  266. package/lib/types/skill-roots.js.map +1 -0
  267. package/lib/types/skill-status.d.ts +36 -0
  268. package/lib/types/skill-status.d.ts.map +1 -0
  269. package/lib/types/skill-status.js +51 -0
  270. package/lib/types/skill-status.js.map +1 -0
  271. package/package.json +133 -0
  272. package/scripts/build-motion-fixture.mjs +26 -0
  273. package/scripts/check-harness-compat.mjs +71 -0
  274. package/scripts/link-harness-dependencies.mjs +54 -0
  275. package/scripts/verify-auto-fold.mjs +114 -0
  276. package/scripts/verify-official-bridge.mjs +121 -0
  277. package/scripts/verify-tool-shimmer.mjs +62 -0
  278. package/skills/generative-mcpapps/SKILL.md +47 -0
  279. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  280. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  281. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  282. package/src/client/Blocks.tsx +213 -0
  283. package/src/client/ChoreographedFlow.tsx +232 -0
  284. package/src/client/ClosedProcessSummary.tsx +23 -0
  285. package/src/client/DiffPanel.tsx +124 -0
  286. package/src/client/LiveFold.tsx +43 -0
  287. package/src/client/McpAppFrame.module.css +210 -0
  288. package/src/client/McpAppFrame.tsx +317 -0
  289. package/src/client/OfficialContent.tsx +69 -0
  290. package/src/client/Reader.module.css +692 -0
  291. package/src/client/Reader.tsx +878 -0
  292. package/src/client/ReasoningCard.tsx +306 -0
  293. package/src/client/SettingsSection.module.css +184 -0
  294. package/src/client/SettingsSection.tsx +194 -0
  295. package/src/client/StickyLane.tsx +34 -0
  296. package/src/client/TimelineRail.module.css +170 -0
  297. package/src/client/TimelineRail.tsx +262 -0
  298. package/src/client/ToolActivity.tsx +274 -0
  299. package/src/client/TurnMetrics.module.css +139 -0
  300. package/src/client/TurnMetrics.tsx +160 -0
  301. package/src/client/WaitClock.tsx +33 -0
  302. package/src/client/WaitingStatus.tsx +13 -0
  303. package/src/client/conversation-scroll.ts +28 -0
  304. package/src/client/deliverables.ts +122 -0
  305. package/src/client/entry-policy.ts +20 -0
  306. package/src/client/entry.tsx +40 -0
  307. package/src/client/fold-choreography.ts +55 -0
  308. package/src/client/fold-intensity.ts +36 -0
  309. package/src/client/index.tsx +222 -0
  310. package/src/client/live-turn.ts +198 -0
  311. package/src/client/markdown/MarkdownText.module.css +304 -0
  312. package/src/client/markdown/MarkdownText.tsx +195 -0
  313. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  314. package/src/client/markdown/incremental.ts +131 -0
  315. package/src/client/markdown/katex.tsx +91 -0
  316. package/src/client/markdown/mathCompatibility.ts +350 -0
  317. package/src/client/markdown/parse.ts +45 -0
  318. package/src/client/markdown/render.tsx +647 -0
  319. package/src/client/markdown/upstream.json +14 -0
  320. package/src/client/mcp-app.ts +323 -0
  321. package/src/client/message-chrome.ts +47 -0
  322. package/src/client/motion.tsx +410 -0
  323. package/src/client/native/ContextBody.module.css +161 -0
  324. package/src/client/native/ContextBody.tsx +591 -0
  325. package/src/client/native/ContextInjectionRow.module.css +66 -0
  326. package/src/client/native/ContextInjectionRow.tsx +106 -0
  327. package/src/client/native/ReferenceIcon.tsx +35 -0
  328. package/src/client/native/tool-call-model.ts +248 -0
  329. package/src/client/native/tool-update.ts +32 -0
  330. package/src/client/native/upstream.json +41 -0
  331. package/src/client/official-slots.tsx +211 -0
  332. package/src/client/open-file.ts +94 -0
  333. package/src/client/pending-submission.ts +49 -0
  334. package/src/client/platform-media.ts +17 -0
  335. package/src/client/primitive-labels.ts +54 -0
  336. package/src/client/projection.ts +118 -0
  337. package/src/client/reasoning-follow.ts +28 -0
  338. package/src/client/settings-copy.ts +80 -0
  339. package/src/client/settings-slots.ts +20 -0
  340. package/src/client/settings.tsx +73 -0
  341. package/src/client/skill-status.ts +78 -0
  342. package/src/client/store.ts +75 -0
  343. package/src/client/stream-buffer.ts +136 -0
  344. package/src/client/streaming.tsx +70 -0
  345. package/src/client/style.d.ts +4 -0
  346. package/src/client/timeline.ts +77 -0
  347. package/src/client/tool-activity.ts +298 -0
  348. package/src/client/types.ts +89 -0
  349. package/src/client/waiting-clock.ts +83 -0
  350. package/src/client/word-motion.tsx +92 -0
  351. package/src/client/word-timeline.ts +113 -0
  352. package/src/dsh-better-display.ts +108 -0
  353. package/src/skill-roots.ts +138 -0
  354. package/src/skill-status.ts +81 -0
  355. package/tests/browser-auto-fold.tsx +64 -0
  356. package/tests/browser-closed-gap.tsx +20 -0
  357. package/tests/browser-motion.tsx +65 -0
  358. package/tests/browser-official-bridge.tsx +152 -0
  359. package/tests/browser-sticky-lanes.tsx +22 -0
  360. package/tests/browser-waiting-clock.tsx +14 -0
  361. package/tests/chat-flow-hook.test.ts +66 -0
  362. package/tests/cloud-gate.test.ts +34 -0
  363. package/tests/context-tool-update.test.ts +33 -0
  364. package/tests/conversation-scroll.test.ts +47 -0
  365. package/tests/deliverables.test.ts +128 -0
  366. package/tests/entry.test.ts +42 -0
  367. package/tests/fixtures/maid-composer-hide.html +31 -0
  368. package/tests/fold-choreography.test.ts +54 -0
  369. package/tests/fold-intensity.test.ts +94 -0
  370. package/tests/footer-precedence.test.ts +129 -0
  371. package/tests/history-word-regression.test.ts +16 -0
  372. package/tests/live-turn.test.ts +218 -0
  373. package/tests/mcp-apps.test.ts +157 -0
  374. package/tests/message-chrome.test.ts +29 -0
  375. package/tests/official-slots.test.ts +112 -0
  376. package/tests/open-file.test.ts +114 -0
  377. package/tests/pending-submission.test.ts +45 -0
  378. package/tests/platform-media.test.ts +23 -0
  379. package/tests/projection.test.ts +184 -0
  380. package/tests/reasoning-follow.test.ts +34 -0
  381. package/tests/settings-copy.test.ts +63 -0
  382. package/tests/skill-status.test.ts +137 -0
  383. package/tests/stock-install.test.ts +58 -0
  384. package/tests/stream-buffer.test.ts +102 -0
  385. package/tests/timeline.test.ts +59 -0
  386. package/tests/tool-diff.test.ts +57 -0
  387. package/tests/waiting-clock.test.ts +83 -0
  388. package/tests/word-timeline.test.ts +146 -0
  389. package/tsconfig.json +11 -0
  390. package/tsdown.config.ts +51 -0
@@ -0,0 +1,128 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import type { TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
4
+ import { basename, createProducedFileMentions, dirname, getTurnDeliverables, showDeliverablesRow } from '../src/client/deliverables.ts';
5
+ import type { ReaderFlowEntry } from '../src/client/tool-activity.ts';
6
+
7
+ test('basename and dirname handle POSIX and Windows style paths', () => {
8
+ assert.equal(basename('src/client/Reader.tsx'), 'Reader.tsx');
9
+ assert.equal(dirname('src/client/Reader.tsx'), 'src/client');
10
+ assert.equal(basename('C:\\project\\src\\index.ts'), 'index.ts');
11
+ assert.equal(dirname('C:\\project\\src\\index.ts'), 'C:\\project\\src');
12
+ assert.equal(basename('simple.txt'), 'simple.txt');
13
+ assert.equal(dirname('simple.txt'), '.');
14
+ assert.equal(basename('/root/file.md/'), 'file.md');
15
+ assert.equal(dirname('/root/file.md/'), '/root');
16
+ });
17
+
18
+ test('getTurnDeliverables extracts produced paths from turn deliverables data', () => {
19
+ const map = new Map<string, unknown>();
20
+ map.set('deliverables', {
21
+ produced: [
22
+ { seq: 1, path: 'src/client/Reader.tsx' },
23
+ { seq: 2, path: 'src/client/Reader.module.css' },
24
+ { seq: 3, path: 'src/client/Reader.tsx' }, // duplicate
25
+ ],
26
+ });
27
+ const turn = { data: map } as unknown as TurnLocation;
28
+ const paths = getTurnDeliverables(turn);
29
+ assert.deepEqual(paths, ['src/client/Reader.tsx', 'src/client/Reader.module.css']);
30
+ });
31
+
32
+ test('getTurnDeliverables falls back to tool flow when turn data is absent', () => {
33
+ const flow: ReaderFlowEntry[] = [
34
+ {
35
+ kind: 'tool',
36
+ key: 'tool:1',
37
+ callId: 'call:1',
38
+ step: 1,
39
+ order: 0,
40
+ block: {
41
+ kind: 'tool-call',
42
+ name: 'write',
43
+ argsRaw: JSON.stringify({ file_path: 'src/client/new-feature.ts', content: 'hello' }),
44
+ } as any,
45
+ },
46
+ {
47
+ kind: 'tool',
48
+ key: 'tool:2',
49
+ callId: 'call:2',
50
+ step: 2,
51
+ order: 1,
52
+ block: {
53
+ kind: 'tool-call',
54
+ name: 'edit',
55
+ argsRaw: JSON.stringify({ file_path: 'src/client/Reader.tsx', old_string: 'a', new_string: 'b' }),
56
+ } as any,
57
+ },
58
+ {
59
+ kind: 'tool',
60
+ key: 'tool:3',
61
+ callId: 'call:3',
62
+ step: 3,
63
+ order: 2,
64
+ block: {
65
+ kind: 'tool-result',
66
+ isError: true, // error result should be skipped
67
+ name: 'write',
68
+ argsRaw: JSON.stringify({ file_path: 'bad.txt', content: 'err' }),
69
+ } as any,
70
+ },
71
+ {
72
+ kind: 'tool',
73
+ key: 'tool:4',
74
+ callId: 'call:4',
75
+ step: 4,
76
+ order: 3,
77
+ block: {
78
+ kind: 'tool-call',
79
+ name: 'read', // read should not be considered a deliverable
80
+ argsRaw: JSON.stringify({ file_path: 'package.json' }),
81
+ } as any,
82
+ },
83
+ ];
84
+
85
+ const paths = getTurnDeliverables(undefined, flow);
86
+ assert.deepEqual(paths, ['src/client/new-feature.ts', 'src/client/Reader.tsx']);
87
+ });
88
+
89
+ test('createProducedFileMentions resolves exact paths and unique basenames, leaving ambiguous basenames inert', () => {
90
+ const opened: string[] = [];
91
+ const openFile = (p: string) => { opened.push(p); };
92
+ const paths = [
93
+ 'src/client/Reader.tsx',
94
+ 'src/server/Reader.tsx', // duplicate basename
95
+ 'src/client/Reader.module.css', // unique basename
96
+ ];
97
+ const mentions = createProducedFileMentions(paths, openFile);
98
+
99
+ // Exact path resolves
100
+ const exact = mentions.resolve('src/client/Reader.tsx');
101
+ assert.ok(exact);
102
+ assert.equal(exact.title, 'src/client/Reader.tsx');
103
+ assert.equal(exact.label, '打开 src/client/Reader.tsx');
104
+ exact.open();
105
+ assert.deepEqual(opened, ['src/client/Reader.tsx']);
106
+
107
+ // Unique basename resolves
108
+ const unique = mentions.resolve('Reader.module.css');
109
+ assert.ok(unique);
110
+ assert.equal(unique.title, 'src/client/Reader.module.css');
111
+ unique.open();
112
+ assert.deepEqual(opened, ['src/client/Reader.tsx', 'src/client/Reader.module.css']);
113
+
114
+ // Ambiguous basename leaves undefined (never guess or open the wrong file)
115
+ const ambiguous = mentions.resolve('Reader.tsx');
116
+ assert.equal(ambiguous, undefined);
117
+
118
+ // Unrelated file leaves undefined
119
+ const unrelated = mentions.resolve('unknown.js');
120
+ assert.equal(unrelated, undefined);
121
+ });
122
+
123
+ test('produced-files row waits for turn close even when paths already exist', () => {
124
+ const paths = ['src/client/Watcher.tsx'];
125
+ assert.equal(showDeliverablesRow('open', paths), false);
126
+ assert.equal(showDeliverablesRow('closed', paths), true);
127
+ assert.equal(showDeliverablesRow('closed', []), false);
128
+ });
@@ -0,0 +1,42 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { ReaderEntryPolicy, readerEntryRequested } from '../src/client/entry-policy.js';
4
+
5
+ test('fresh sessions default to reading, without requiring a special URL', () => {
6
+ const policy = new ReaderEntryPolicy(false);
7
+ assert.equal(policy.select(null), 'reader');
8
+ assert.equal(policy.select('reader'), null);
9
+ assert.equal(policy.select(undefined), 'reader');
10
+ });
11
+
12
+ test('an explicit native or third-party tab selection is not overwritten', () => {
13
+ const policy = new ReaderEntryPolicy(false);
14
+ for (const view of ['chat', 'trajectory', 'other-plugin']) assert.equal(policy.select(view), null);
15
+ assert.equal(policy.select(null), 'reader');
16
+ assert.equal(policy.select('chat'), null);
17
+ });
18
+
19
+ test('the trial URL enters reading once and releases later tab choices', () => {
20
+ let consumed = 0;
21
+ const policy = new ReaderEntryPolicy(true, () => { consumed++; });
22
+ assert.equal(policy.select('chat'), 'reader');
23
+ assert.equal(consumed, 1);
24
+ assert.equal(policy.select('reader'), null);
25
+ assert.equal(policy.select('chat'), null);
26
+ assert.equal(policy.select('trajectory'), null);
27
+ assert.equal(consumed, 1);
28
+ assert.equal(policy.select(null), 'reader');
29
+ });
30
+
31
+ test('already-selected reading still consumes the entry request', () => {
32
+ let consumed = false;
33
+ const policy = new ReaderEntryPolicy(true, () => { consumed = true; });
34
+ assert.equal(policy.select('reader'), null);
35
+ assert.equal(consumed, true);
36
+ assert.equal(policy.select('chat'), null);
37
+ });
38
+
39
+ test('current and previously delivered trial links have real entry semantics', () => {
40
+ for (const query of ['?reader=1', '?reader=0.1.0-trial.2', '?reader=0.1.0-trial.3']) assert.equal(readerEntryRequested(query), true);
41
+ for (const query of ['', '?reader=0', '?reader=unrelated', '?other=reader']) assert.equal(readerEntryRequested(query), false);
42
+ });
@@ -0,0 +1,31 @@
1
+ <!doctype html>
2
+ <meta charset="utf-8">
3
+ <title>Reader chat-flow hook vs maid-atelier composer hide</title>
4
+ <style>
5
+ [data-conversation-scroll] { display: flex; flex-direction: column; min-height: 120px; }
6
+ [data-composer-seat] { display: flex; min-height: 28px; }
7
+ /* Exact maid-atelier / phoebe-atelier empty-state hide (issue #10). */
8
+ [data-phase="active"]
9
+ [data-conversation-scroll]:not(:has([data-chat-flow]))
10
+ > [data-composer-seat] { display: none; }
11
+ </style>
12
+ <body>
13
+ <section id="inspect" data-phase="active">
14
+ <div data-conversation-scroll>
15
+ <div data-dsh-better-display="fixture"><section data-reader-turn="1">turn</section></div>
16
+ <div data-composer-seat id="inspect-seat">composer</div>
17
+ </div>
18
+ </section>
19
+ <section id="reader" data-phase="active">
20
+ <div data-conversation-scroll>
21
+ <div data-dsh-better-display="0.1.1">
22
+ <div data-chat-flow=""><section data-reader-turn="1">turn</section></div>
23
+ </div>
24
+ <div data-composer-seat id="reader-seat">composer</div>
25
+ </div>
26
+ </section>
27
+ <script>
28
+ document.body.dataset.inspectDisplay = getComputedStyle(document.getElementById('inspect-seat')).display;
29
+ document.body.dataset.readerDisplay = getComputedStyle(document.getElementById('reader-seat')).display;
30
+ </script>
31
+ </body>
@@ -0,0 +1,54 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { collapseRows, containsNewUser, flowRows, FOLD_TIMING, retiringKeys } from '../src/client/fold-choreography.js';
4
+ import { presentLiveTurn } from '../src/client/live-turn.js';
5
+ import type { LiveStep } from '../src/client/live-turn.js';
6
+ const reasoning = (n: number): LiveStep => ({ kind: 'reasoning', key: `r${n}`, nodeKey: `n${n}`, start: 0, blocks: [], step: n });
7
+ const body: LiveStep = { kind: 'body', key: 'b1', nodeKey: 'n1', start: 1, blocks: [], step: 1 };
8
+ const open = { status: 'open' } as const;
9
+ test('first fold retains every visible step key and admits no incoming reasoning', () => {
10
+ const before = presentLiveTurn([reasoning(1), body], open);
11
+ const after = presentLiveTurn([reasoning(1), body, reasoning(2)], open);
12
+ assert.deepEqual(retiringKeys(before, after, {}), ['r1', 'b1']);
13
+ const rows = collapseRows(before, after);
14
+ assert.deepEqual(rows.filter(x => x.kind === 'step').map(x => x.key), ['r1', 'b1']);
15
+ assert.equal(rows[0].kind, 'summary');
16
+ assert.equal(rows[0].kind === 'summary' && rows[0].item.summary, '思考×0 · 输出×0');
17
+ assert.equal(new Set(flowRows(after).map(row => row.key)).size, flowRows(after).length);
18
+ });
19
+ test('repeat fold retires only displayed open rows, retaining the same summary identity', () => {
20
+ const before = presentLiveTurn([reasoning(1), body, reasoning(2)], open);
21
+ const after = presentLiveTurn([reasoning(1), body, reasoning(2), reasoning(3)], open);
22
+ assert.deepEqual(retiringKeys(before, after, {}), ['r2']);
23
+ assert.equal(collapseRows(before, after)[0].key, flowRows(before)[0].key);
24
+ assert.deepEqual(retiringKeys(before, after, { 'live-fold:r1': true }), []);
25
+ });
26
+ test('user admission bypasses buffered choreography and preserves source ordering', () => {
27
+ const before = presentLiveTurn([reasoning(1)], open);
28
+ const user: LiveStep = { kind: 'user', key: 'u2', nodeKey: 'u2' };
29
+ const after = presentLiveTurn([reasoning(1), user, reasoning(2)], open);
30
+ assert.equal(containsNewUser(before, after), true);
31
+ assert.deepEqual(retiringKeys(before, after, {}), []);
32
+ assert.deepEqual(flowRows(after).map(row => row.key), ['r1', 'u2', 'r2']);
33
+ });
34
+ test('disabling fold preserves all source steps; phase budget is bounded', () => {
35
+ const items = presentLiveTurn([reasoning(1), body, reasoning(2)], open, false);
36
+ assert.equal(items.some(item => item.kind === 'fold'), false);
37
+ assert.equal(FOLD_TIMING.collapse + FOLD_TIMING.count + FOLD_TIMING.settle, 560);
38
+ });
39
+
40
+ // Every phase must be escapable without cooperating animations or frames. A
41
+ // non-idle phase holds the frame source at `shown` and pauses the text reveal, so
42
+ // a phase that cannot advance freezes the whole turn until the view remounts —
43
+ // which is exactly the reported "output stalls at the end, refresh fixes it".
44
+ // The slack is what makes the deadline strictly later than the nominal phase.
45
+ test('every phase has a bounded budget and a watchdog strictly beyond it', () => {
46
+ assert.ok(FOLD_TIMING.watchdogSlack > 0, 'watchdog slack must be positive');
47
+ for (const phase of ['collapse', 'count', 'settle', 'reveal'] as const) {
48
+ const budget = FOLD_TIMING[phase];
49
+ assert.ok(Number.isFinite(budget) && budget > 0, phase + ' needs a positive budget');
50
+ assert.ok(budget + FOLD_TIMING.watchdogSlack > budget, phase + ' deadline must exceed its budget');
51
+ // Far below any plausible user perception of "stuck".
52
+ assert.ok(budget + FOLD_TIMING.watchdogSlack < 1000, phase + ' must not read as a stall');
53
+ }
54
+ });
@@ -0,0 +1,94 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ FOLD_INTENSITY_DEFAULT,
8
+ autoFoldFromIntensity,
9
+ foldIntensityOf,
10
+ frostedGlassOf,
11
+ processOnlyFromIntensity,
12
+ } from '../src/client/fold-intensity.ts';
13
+ import { presentLiveTurn } from '../src/client/live-turn.ts';
14
+ import type { LiveStep } from '../src/client/live-turn.ts';
15
+ import { en, zh } from '../src/client/settings-copy.ts';
16
+
17
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
18
+ const reasoning = (n: number): LiveStep => ({ kind: 'reasoning', key: `r${n}`, nodeKey: `n${n}`, start: 0, blocks: [], step: n });
19
+ const body: LiveStep = { kind: 'body', key: 'b1', nodeKey: 'n1', start: 1, blocks: [{ kind: 'text', text: 'answer' }], step: 1 };
20
+ const tool: LiveStep = {
21
+ kind: 'tool',
22
+ key: 't1',
23
+ entry: {
24
+ kind: 'tool',
25
+ key: 't1',
26
+ callId: 'c1',
27
+ step: 0,
28
+ order: 0,
29
+ draft: { kind: 'tool-call', callId: 'c1', name: 'read', argsRaw: '{"path":"Reader.tsx"}' },
30
+ },
31
+ };
32
+ const open = { status: 'open' } as const;
33
+ const closed = { status: 'closed', reason: 'completed', latestStep: 2, closingStep: 2 } as const;
34
+
35
+ test('fold intensity defaults to standard (1) and glass defaults off', () => {
36
+ assert.equal(FOLD_INTENSITY_DEFAULT, 1);
37
+ assert.equal(foldIntensityOf(undefined), 1);
38
+ assert.equal(foldIntensityOf({}), 1);
39
+ assert.equal(frostedGlassOf(undefined), false);
40
+ assert.equal(frostedGlassOf({}), false);
41
+ assert.equal(frostedGlassOf({ frostedGlass: false }), false);
42
+ assert.equal(frostedGlassOf({ frostedGlass: true }), true);
43
+ });
44
+
45
+ test('fold slider maps 0/1/2 and migrates older persist flags', () => {
46
+ assert.equal(foldIntensityOf({ foldIntensity: 0 }), 0);
47
+ assert.equal(foldIntensityOf({ foldIntensity: 1 }), 1);
48
+ assert.equal(foldIntensityOf({ foldIntensity: 2 }), 2);
49
+ assert.equal(foldIntensityOf({ autoFold: false }), 0);
50
+ assert.equal(foldIntensityOf({ processOnly: true }), 2);
51
+ assert.equal(foldIntensityOf({ autoFold: true, processOnly: false }), 1);
52
+ assert.equal(autoFoldFromIntensity(0), false);
53
+ assert.equal(autoFoldFromIntensity(1), true);
54
+ assert.equal(autoFoldFromIntensity(2), true);
55
+ assert.equal(processOnlyFromIntensity(0), false);
56
+ assert.equal(processOnlyFromIntensity(1), false);
57
+ assert.equal(processOnlyFromIntensity(2), true);
58
+ });
59
+
60
+ test('autoFold false keeps every step open; autoFold true is current-main fold-on-next-reasoning', () => {
61
+ const steps = [reasoning(1), body, tool, reasoning(2)];
62
+ const none = presentLiveTurn(steps, open, false);
63
+ assert.equal(none.some(item => item.kind === 'fold'), false);
64
+ assert.deepEqual(none.map(item => item.key), ['r1', 'b1', 't1', 'r2']);
65
+
66
+ const standard = presentLiveTurn(steps, open, true);
67
+ assert.equal(standard[0]?.kind, 'fold');
68
+ if (standard[0]?.kind !== 'fold') throw new Error('expected standard fold');
69
+ assert.deepEqual(standard[0].steps.map(step => step.key), ['r1', 'b1', 't1']);
70
+ assert.deepEqual(standard.filter(item => item.kind === 'open').map(item => item.key), ['r2']);
71
+ });
72
+
73
+ test('root reader store persists glass off and standard fold on dsh.reader.v1', () => {
74
+ const store = readFileSync(resolve(root, 'src/client/store.ts'), 'utf8');
75
+ assert.match(store, /persist:\s*'dsh\.reader\.v1'/);
76
+ assert.match(store, /frostedGlass:\s*false/);
77
+ assert.match(store, /foldIntensity:\s*FOLD_INTENSITY_DEFAULT/);
78
+ assert.match(store, /deliverableOpenMode:\s*'external'/);
79
+ assert.match(store, /setFrostedGlass/);
80
+ assert.match(store, /setFoldIntensity/);
81
+ assert.match(store, /setDeliverableOpenMode/);
82
+ });
83
+
84
+ test('settings copy names fold switch and glass without leaking home paths', () => {
85
+ for (const copy of [en, zh]) {
86
+ assert.match(copy.glassTitle, /毛玻璃|Frosted glass/i);
87
+ assert.match(copy.foldTitle, /折叠|fold/i);
88
+ assert.doesNotMatch(Object.values(copy).join('\n'), /\/Users\/|\$HOME\b/);
89
+ }
90
+ const reader = readFileSync(resolve(root, 'src/client/Reader.tsx'), 'utf8');
91
+ assert.match(reader, /data-reader-glass=\{frostedGlass/);
92
+ assert.match(reader, /data-reader-auto-fold=\{autoFold/);
93
+ assert.doesNotMatch(reader, /setProcessOnly/);
94
+ });
@@ -0,0 +1,129 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { test } from 'node:test';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const css = readFileSync(resolve(root, 'src/client/Reader.module.css'), 'utf8');
9
+ const bundle = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
10
+
11
+ /**
12
+ * Host ConversationRoot owns the footer band: `.composerSeat` is `position:
13
+ * sticky; bottom: 0; z-index: 7`, its own markdown code banner is 6 and the
14
+ * floating controls are 8. A scrolling reader clamps every turn-level sticky
15
+ * lane down into that band (the lane stops at its containing block's bottom,
16
+ * which passes behind the input card), so a lane at 7 or above paints the
17
+ * reader's own opaque plate over the composer. Everything the reader pins
18
+ * inside the transcript therefore has to stay strictly below 7.
19
+ */
20
+ const HOST_FOOTER_Z = 7;
21
+
22
+ /** Lanes a scroll can clamp into the footer band. */
23
+ const CLAMPED_LANES = [
24
+ '.turnProcessSticky',
25
+ '.liveFoldContainer',
26
+ '.flowCell[data-flow-summary]',
27
+ '.closedProcessSummary',
28
+ ];
29
+
30
+ /** The one lane allowed at 7: it pins to the scrollport's first lane and can never reach the band. */
31
+ const TOP_LANE = '.toolbar';
32
+
33
+ function mergedDeclarations(source: string, selector: string, { hashed = false } = {}): Map<string, string> {
34
+ const declarations = new Map<string, string>();
35
+ const body = source.replace(/\/\*[\s\S]*?\*\//g, '');
36
+ const rule = /([^{}]+)\{([^{}]*)\}/g;
37
+ let match: RegExpExecArray | null;
38
+ while ((match = rule.exec(body)) !== null) {
39
+ const selectors = match[1]!.split(',').map(part => part.trim());
40
+ const wanted = hashed
41
+ ? selectors.some(part => part.endsWith(selector) || part.endsWith(selector.replace(/^\./, '')))
42
+ : selectors.includes(selector);
43
+ if (!wanted) continue;
44
+ for (const declaration of match[2]!.split(';')) {
45
+ const separator = declaration.indexOf(':');
46
+ if (separator < 0) continue;
47
+ const property = declaration.slice(0, separator).trim();
48
+ const value = declaration.slice(separator + 1).trim();
49
+ if (property) declarations.set(property, value);
50
+ }
51
+ }
52
+ return declarations;
53
+ }
54
+
55
+ /** Minified CSS inside the committed bundle keeps the hashed prefix on each class. */
56
+ function bundledDeclarations(className: string): Map<string, string> {
57
+ const declarations = new Map<string, string>();
58
+ const rule = new RegExp(`(?:^|[.,}])[.#]?[\\w-]*${className}\\{([^{}]*)\\}`, 'g');
59
+ let match: RegExpExecArray | null;
60
+ while ((match = rule.exec(bundle)) !== null) {
61
+ for (const declaration of match[1]!.split(';')) {
62
+ const separator = declaration.indexOf(':');
63
+ if (separator < 0) continue;
64
+ declarations.set(declaration.slice(0, separator).trim(), declaration.slice(separator + 1).trim());
65
+ }
66
+ }
67
+ return declarations;
68
+ }
69
+
70
+ test('clamped reader lanes stay strictly below the host footer band', () => {
71
+ for (const lane of CLAMPED_LANES) {
72
+ const declarations = mergedDeclarations(css, lane);
73
+ const zIndex = declarations.get('z-index');
74
+ assert.ok(zIndex, `${lane} still declares its lane z-index`);
75
+ assert.ok(
76
+ Number(zIndex) < HOST_FOOTER_Z,
77
+ `${lane} must stay below the host composer seat (z-index ${HOST_FOOTER_Z}), got ${zIndex}`,
78
+ );
79
+ }
80
+ });
81
+
82
+ test('the top toolbar lane stays at 7 and pins to the scrollport top', () => {
83
+ const declarations = mergedDeclarations(css, TOP_LANE);
84
+ assert.equal(declarations.get('z-index'), String(HOST_FOOTER_Z));
85
+ assert.equal(declarations.get('top'), '0');
86
+ });
87
+
88
+ test('the live status lane pins below the toolbar lane instead of over it', () => {
89
+ const declarations = mergedDeclarations(css, '.turnProcessSticky');
90
+ assert.match(
91
+ declarations.get('top') ?? '',
92
+ /var\(--reader-toolbar-height/,
93
+ 'the status lane shares the measured lane below the toolbar',
94
+ );
95
+ });
96
+
97
+ test('the closed summary lane shares that same measured lane', () => {
98
+ const declarations = mergedDeclarations(css, '.flowCell[data-flow-summary]');
99
+ assert.match(declarations.get('top') ?? '', /var\(--reader-toolbar-height/);
100
+ });
101
+
102
+ test('skin mode does not reintroduce a lane above the footer band', () => {
103
+ for (const lane of CLAMPED_LANES) {
104
+ const zIndex = mergedDeclarations(css, `.root[data-reader-glass] ${lane}`).get('z-index');
105
+ if (zIndex === undefined || zIndex === 'auto') continue;
106
+ assert.ok(
107
+ Number(zIndex) < HOST_FOOTER_Z,
108
+ `glass override for ${lane} must not lift above ${HOST_FOOTER_Z}, got ${zIndex}`,
109
+ );
110
+ }
111
+ assert.equal(
112
+ mergedDeclarations(css, '.root[data-reader-glass] .toolbar').get('z-index'),
113
+ undefined,
114
+ 'the toolbar keeps its single ladder value in both modes',
115
+ );
116
+ });
117
+
118
+ test('the committed client bundle carries the same lane ladder', () => {
119
+ for (const lane of ['turnProcessSticky', 'liveFoldContainer']) {
120
+ const bundled = bundledDeclarations(lane);
121
+ const zIndex = bundled.get('z-index');
122
+ assert.ok(zIndex, `${lane} is present in the built bundle`);
123
+ assert.ok(
124
+ Number(zIndex) < HOST_FOOTER_Z,
125
+ `${lane} in lib/client.js must stay below ${HOST_FOOTER_Z}, got ${zIndex}`,
126
+ );
127
+ }
128
+ assert.equal(bundledDeclarations('toolbar').get('z-index'), String(HOST_FOOTER_Z));
129
+ });
@@ -0,0 +1,16 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { WordTimeline } from '../src/client/word-timeline.js';
4
+
5
+ test('opening a long existing answer does not animate every historical word', () => {
6
+ const timeline = new WordTimeline();
7
+ const history = '已经接收的历史文字。'.repeat(20000);
8
+ timeline.begin(history, true, 0, 100);
9
+ assert.equal(timeline.hasLiveText, false, 'history must bypass per-word components');
10
+
11
+ timeline.begin(history + '新内容', true, 0, 200);
12
+ assert.equal(timeline.hasLiveText, true);
13
+ const words = timeline.words(history + '新内容', 0);
14
+ assert.ok(words.length < 10, `historical prefix expanded into ${words.length} words`);
15
+ assert.equal(words.map(word => word.text).join(''), history + '新内容');
16
+ });