@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,81 @@
1
+ /** Shared generative-mcpapps identity. Plugin-tree presence is not installation. */
2
+
3
+ export const GENERATIVE_MCPAPPS_SKILL = 'generative-mcpapps';
4
+
5
+ /** Conventional relative roots only. Never expand $HOME or dump host `root.path`. */
6
+ export const CONVENTIONAL_SKILL_ROOTS = ['.dsh/skills', '.agents/skills'] as const;
7
+
8
+ /** Copy source relative to the plugin / checkout, not an absolute pack path. */
9
+ export const SKILL_PACK_RELATIVE = `skills/${GENERATIVE_MCPAPPS_SKILL}`;
10
+
11
+ export type SkillRootSource =
12
+ | 'project-dsh'
13
+ | 'project-agents'
14
+ | 'user-dsh'
15
+ | 'user-agents'
16
+ | 'custom'
17
+ | 'bundled'
18
+ | (string & {});
19
+
20
+ export interface SkillRootInfo {
21
+ readonly source: SkillRootSource;
22
+ readonly path: string;
23
+ readonly present?: boolean;
24
+ }
25
+
26
+ export interface HostSkillStatus {
27
+ readonly name: typeof GENERATIVE_MCPAPPS_SKILL;
28
+ readonly installed: boolean;
29
+ readonly via: 'skills.list' | 'skill-root' | null;
30
+ readonly roots: readonly SkillRootInfo[];
31
+ /** Plugin-shipped pack path for copy guidance. Not a harness skill root. */
32
+ readonly packPath?: string;
33
+ }
34
+
35
+ export function skillNameMatches(name: unknown): boolean {
36
+ return typeof name === 'string' && name === GENERATIVE_MCPAPPS_SKILL;
37
+ }
38
+
39
+ export function skillListIncludes(entries: readonly { readonly name?: string }[] | undefined): boolean {
40
+ return Array.isArray(entries) && entries.some(entry => skillNameMatches(entry?.name));
41
+ }
42
+
43
+ /** Directory bundle `name/SKILL.md` or flat `name.md` at a scanned root. */
44
+ export function skillRootEntryMatches(entryName: string): boolean {
45
+ return entryName === GENERATIVE_MCPAPPS_SKILL || entryName === `${GENERATIVE_MCPAPPS_SKILL}.md`;
46
+ }
47
+
48
+ export function skillsFromListResult(result: unknown): { name?: string }[] {
49
+ if (Array.isArray(result)) return result;
50
+ if (result === null || typeof result !== 'object') return [];
51
+ const record = result as {
52
+ ok?: boolean;
53
+ value?: { skills?: unknown } | unknown;
54
+ skills?: unknown;
55
+ };
56
+ if (record.ok === false) return [];
57
+ if (Array.isArray(record.skills)) return record.skills as { name?: string }[];
58
+ if (record.value && typeof record.value === 'object') {
59
+ const value = record.value as { skills?: unknown };
60
+ if (Array.isArray(value.skills)) return value.skills as { name?: string }[];
61
+ if (Array.isArray(record.value)) return record.value as { name?: string }[];
62
+ }
63
+ return [];
64
+ }
65
+
66
+ export function publicSkillRoots(): readonly SkillRootInfo[] {
67
+ return CONVENTIONAL_SKILL_ROOTS.map(path => ({ source: 'conventional', path }));
68
+ }
69
+
70
+ /**
71
+ * Strip host home / pack absolutes before anything user-facing (HTTP or Settings).
72
+ * Detection still uses the raw scan; this is the public face.
73
+ */
74
+ export function toPublicSkillStatus(status: HostSkillStatus): HostSkillStatus {
75
+ return {
76
+ name: status.name,
77
+ installed: status.installed,
78
+ via: status.via,
79
+ roots: publicSkillRoots(),
80
+ };
81
+ }
@@ -0,0 +1,64 @@
1
+ import React, { useSyncExternalStore } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
4
+ import { Reader } from '../src/client/Reader.js';
5
+ import { createReaderStore } from '../src/client/store.js';
6
+ import type { ReaderProps } from '../src/client/types.js';
7
+
8
+ // Real Reader and real root/session stores; only conversation data is synthetic.
9
+ const store = createReaderStore();
10
+ const prefs = store.create();
11
+ const session = store.create('auto-fold-fixture');
12
+ const nodes = new Map();
13
+ const turns = new Map();
14
+ for (const [number, closed] of [[1, true], [2, false]] as const) {
15
+ const turn = {
16
+ turn: number, status: closed ? 'closed' : 'open',
17
+ start: { seq: number * 10, time: 1 },
18
+ end: closed ? { time: 1001, data: { reason: { kind: 'completed' } } } : undefined,
19
+ data: new Map(closed ? [['turn-tail', { closing: { step: 1 } }]] : []),
20
+ steps: [] as any[],
21
+ };
22
+ for (let step = 0; step < 2; step++) {
23
+ const key = `turn-${number}-step-${step}`;
24
+ const data = { turn: number, step, status: closed || step === 0 ? 'settled' : 'running', time: 1,
25
+ blocks: [
26
+ { kind: 'reasoning', text: closed ? `已完成思考 ${step}` : `进行中思考 ${step}` },
27
+ { kind: 'text', text: closed && step === 1 ? '最终回答始终保留' : `过程说明 ${number}-${step}` },
28
+ ],
29
+ };
30
+ const location = { step, start: { seq: number * 10 + step }, data: new Map([['assistant-step', data]]) };
31
+ turn.steps.push(location);
32
+ nodes.set(key, { key, kind: 'assistant-step', visibility: 'visible', anchorSeq: number * 10 + step,
33
+ location: { kind: 'step', turn, step: location }, data });
34
+ }
35
+ turns.set(number, turn);
36
+ }
37
+ const chat = { order: [...nodes.keys()], nodes, timeline: { turns } } as unknown as ChatSnapshot;
38
+ const sessionSnapshot = { running: true, openState: 'ready', pendingSubmissions: [], hasMore: false, loadingOlder: false };
39
+ const sessionsSnapshot = { byId: { 'auto-fold-fixture': { cwd: '/fixture' } } };
40
+ const pending = new Map();
41
+ const host = { home: '/fixture' };
42
+ const useReaderStore: ReaderProps['useStore'] = selector => selector(useSyncExternalStore(session.subscribe, session.getSnapshot));
43
+ const props = {
44
+ sessionId: 'auto-fold-fixture', useChat: selector => selector(chat),
45
+ useSession: selector => selector(sessionSnapshot), useSessions: selector => selector(sessionsSnapshot),
46
+ useSessionStatus: selector => selector(new Map([['auto-fold-fixture', { running: true, pendingInteraction: pending.get('auto-fold-fixture'), completionUnread: false }]])),
47
+ useStore: useReaderStore, actions: session.actions, openPrefs: prefs,
48
+ t: key => key, renderSlot: (_name, _owner, options) => options?.fallback ?? null,
49
+ renderSlotChain: (_name, _owner, options) => options?.fallback ?? null,
50
+ loadImage: async () => ({ data: new Uint8Array(), mediaType: 'image/png' }),
51
+ officialImageLoader: Object.assign(async () => null, { peek: () => null }),
52
+ officialFileMentions: () => undefined, officialPreviewFile: () => {},
53
+ officialHost: { getSnapshot: () => host, subscribe: () => () => {} },
54
+ fillComposer: () => true, openFile: () => {}, loadOlder: async () => {}, openView: () => {},
55
+ } as ReaderProps;
56
+
57
+ Object.assign(window, { autoFoldFixture: {
58
+ // Exercise Settings' root preference path without recreating the Reader.
59
+ preference: (value: boolean) => prefs.actions.setAutoFold(value),
60
+ state: () => session.getSnapshot(),
61
+ } });
62
+ createRoot(document.getElementById('app')!).render(
63
+ <div data-conversation-scroll style={{ height: '100vh', overflow: 'auto' }}><Reader {...props} /></div>,
64
+ );
@@ -0,0 +1,20 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
4
+ import { ChoreographedFlow } from '../src/client/ChoreographedFlow.js';
5
+ import { ClosedProcessSummary } from '../src/client/ClosedProcessSummary.js';
6
+ import { ProcessFragment } from '../src/client/motion.js';
7
+ import { presentLiveTurn } from '../src/client/live-turn.js';
8
+ import type { LiveStep } from '../src/client/live-turn.js';
9
+ function Fixture() {
10
+ const [open,setOpen]=useState(false);const button=useRef<HTMLButtonElement>(null);
11
+ const steps=useMemo(()=>Array.from({length:500},(_,i):LiveStep=>({kind:'reasoning',key:`r${i}`,nodeKey:`r${i}`,step:i,start:0,blocks:[]})),[]);
12
+ const frame=useMemo(()=>({items:presentLiveTurn(steps,{status:'closed',reason:'completed',closingStep:501,latestStep:501}),snapshot:{} as ChatSnapshot}),[steps]);
13
+ return <main style={{padding:30}}><h1>完成后 500 条过程记录:实际组件回归</h1>
14
+ <button ref={button} onClick={()=>setOpen(v=>!v)}>切换过程展开</button>
15
+ <ClosedProcessSummary steps={steps} open={open} onChange={setOpen} controls="closed-flow"/>
16
+ <ChoreographedFlow id="closed-flow" frame={frame} motion enabled={false} urgent={false} processOpen={open} open={{}} onOpenChange={()=>{}}
17
+ renderStep={step=><ProcessFragment open={open} motion onRead={()=>{}} returnFocusTo={button} nodeKey={step.key}><p>过程内容 {step.key}</p></ProcessFragment>}/>
18
+ <article data-final-answer>最终回答必须紧随统计,不能隔着 8000px 空白;展开、收起以后仍应如此。</article></main>;
19
+ }
20
+ createRoot(document.getElementById('app')!).render(<Fixture/>);
@@ -0,0 +1,65 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
4
+ import { ChoreographedFlow, useFlowChat } from '../src/client/ChoreographedFlow.js';
5
+ import { presentLiveTurn } from '../src/client/live-turn.js';
6
+ import type { LiveStep } from '../src/client/live-turn.js';
7
+ import { Disclosure, StatusText, useMotionAllowed, useReadingScroll } from '../src/client/motion.js';
8
+ import { ReasoningCard } from '../src/client/ReasoningCard.js';
9
+ import { StreamMotionContext, useStreamingText } from '../src/client/streaming.js';
10
+ import css from '../src/client/Reader.module.css';
11
+
12
+ const sample = '旧内容保持原位,先完成连续收拢,再确认数字,最后继续输出。This is a real component regression fixture, not a replacement animation. ';
13
+ function Text({ step }: { step: LiveStep }) {
14
+ const node = useFlowChat(s => 'nodeKey' in step ? s.nodes.get(step.nodeKey) : undefined);
15
+ const source = (node?.data as { text?: string })?.text ?? '';
16
+ const text = useStreamingText(source, true, { startedAt: 0, interrupted: false, selected: false });
17
+ if (step.kind === 'reasoning') return <ReasoningCard step={step.step} active motion selected={false} onRead={() => {}}>
18
+ <div data-test-text={step.key} style={{ whiteSpace: 'pre-wrap' }}>{text.text}</div>
19
+ </ReasoningCard>;
20
+ return <div data-reader-anchor style={{ padding: '10px 0' }} data-test-text={step.key}>{step.kind === 'body' ? sample : '工具 · 读取源文件,等待下一个步骤'}</div>;
21
+ }
22
+ function Demo() {
23
+ const [n, setN] = useState(1);
24
+ const [tail, setTail] = useState(0);
25
+ const [auto, setAuto] = useState(true);
26
+ const [motionSetting, setMotion] = useState(true);
27
+ const motion = useMotionAllowed(motionSetting);
28
+ const [urgent, setUrgent] = useState(false);
29
+ const [expanded, setExpanded] = useState(true);
30
+ const [open, setOpen] = useState<Record<string, boolean>>({});
31
+ const root = useRef<HTMLDivElement>(null); const button=useRef<HTMLButtonElement>(null);
32
+ useReadingScroll(root, motion);
33
+ const frame = useMemo(() => {
34
+ const steps: LiveStep[] = [];
35
+ const nodes = new Map();
36
+ for (let i=1; i<=n; i++) {
37
+ steps.push({ kind: 'reasoning', key: `r${i}`, nodeKey:`r${i}`, start:0, blocks:[], step:i });
38
+ nodes.set(`r${i}`, { data: { text: sample.repeat(i===n && n>1 ? 3 + tail : 5) } });
39
+ if (i<n || n===1) {
40
+ steps.push({kind:'body',key:`b${i}`,nodeKey:`b${i}`,start:1,blocks:[],step:i});
41
+ steps.push({kind:'other',key:`t${i}`,nodeKey:`t${i}`});
42
+ }
43
+ }
44
+ return {items:presentLiveTurn(steps,{status:'open'},auto),snapshot:{nodes:{get:(key:string)=>nodes.get(key)}} as unknown as ChatSnapshot};
45
+ }, [n,tail,auto]);
46
+ (window as any).fixture = { next:()=>setN(v=>v+1), burst:()=>{setN(v=>v+1); setTimeout(()=>setN(v=>v+2),100); setTimeout(()=>setTail(v=>v+2),180);}, stop:()=>setUrgent(true), reduce:()=>setMotion(false), reset:()=>{setN(1);setTail(0);setUrgent(false);setOpen({});setMotion(true);setAuto(true);} };
47
+ return <><nav style={{padding:12,display:'flex',gap:12,flexWrap:'wrap'}}>
48
+ <b>转场验收 · 真实组件 / 模拟数据</b>
49
+ <button onClick={()=>(window as any).fixture.next()}>下一步</button>
50
+ <button onClick={()=>(window as any).fixture.burst()}>突发三步</button>
51
+ <button onClick={()=>(window as any).fixture.stop()}>停止</button>
52
+ <button onClick={()=>(window as any).fixture.reset()}>重置</button>
53
+ <button onClick={()=>setMotion(v=>!v)}>动效{motion?'开':'关'}</button>
54
+ </nav><div data-conversation-scroll style={{height:'calc(100vh - 70px)',overflow:'auto'}}>
55
+ <StreamMotionContext.Provider value={{enabled:motion,activatedAt:Date.now()}}>
56
+ <div ref={root} className={css.root} data-motion={motion?'on':'off'}><div className={css.column} data-chat-flow="">
57
+ <div className={css.toolbar}><button className={css.textButton} onClick={()=>setAuto(v=>!v)}>自动折叠{auto?'开':'关'}</button></div>
58
+ <div className={css.turnProcessSticky}><Disclosure open={expanded} onChange={setExpanded} buttonRef={button} label={<StatusText text="正在处理" motion={motion}/>} /></div>
59
+ <ChoreographedFlow id="fixture-flow" frame={frame} motion={motion} enabled={auto} urgent={urgent} processOpen={expanded}
60
+ open={open} onOpenChange={(key,value)=>setOpen(s=>({...s,[key]:value}))} renderStep={step=><Text step={step}/>} />
61
+ <div data-test-footer style={{height:72}}>输入区位置参照 · 后台数据不会被修改</div>
62
+ </div></div></StreamMotionContext.Provider>
63
+ </div></>;
64
+ }
65
+ createRoot(document.getElementById('app')!).render(<Demo />);
@@ -0,0 +1,152 @@
1
+ /** Test-only imports resolve to the selected Harness; none enter client.js. */
2
+ import { Context } from '@deepseek-ai/cordis';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { memo, useState } from 'react';
5
+ import { SlotRegistry } from '@fixture/registry';
6
+ import { createSlotRenderer } from '@fixture/renderer';
7
+ import { apply as applyFeedback } from '@fixture/feedback';
8
+ import { ReadRow } from '@fixture/read-row';
9
+ import { readImageToolview } from '@fixture/read-image-row';
10
+ import { en as conversationEn } from '@fixture/tool-locale';
11
+ import { officialChildren, installOfficialSlots, OFFICIAL_SEATS } from '../src/client/official-slots.js';
12
+ import { OfficialActions, OfficialTool } from '../src/client/OfficialContent.js';
13
+ import { DeliverablesTail } from '@fixture/deliverables';
14
+ import { zh as deliverablesZh, en as deliverablesEn } from '@fixture/deliverables-locale';
15
+ import { FileRouteAction } from '@fixture/open-in-app-action';
16
+ import { zh as openInAppZh, en as openInAppEn } from '@fixture/open-in-app-locale';
17
+ import { CopyAnswer } from '../src/client/Blocks.js';
18
+ import { MotionMarkdown } from '../src/client/word-motion.js';
19
+ import css from '../src/client/Reader.module.css';
20
+
21
+ const rootContext = new Context();
22
+ await rootContext.plugin(SlotRegistry);
23
+ await rootContext.plugin({ name: 'bridge-fixture', inject: ['slots'], async apply(ctx: Context) {
24
+ const slots = ctx.slots as any;
25
+ slots.install(createSlotRenderer());
26
+ const observations: any = { opens: [], requests: [], mounts: 0, subscriptions: 0 };
27
+ const source = <T,>(initial: T) => {
28
+ let value = initial;
29
+ const listeners = new Set<() => void>();
30
+ return { getSnapshot: () => value, subscribe: (fn: () => void) => {
31
+ listeners.add(fn); observations.subscriptions++;
32
+ return () => { if (listeners.delete(fn)) observations.subscriptions--; };
33
+ }, set(next: T) { value = next; for (const fn of [...listeners]) fn(); } };
34
+ };
35
+ const localeSource = source({ revision: 0 });
36
+ let language = 'zh';
37
+ const dictionaries: Record<string, any> = { deliverables: { zh: deliverablesZh, en: deliverablesEn }, 'open-in-app': { zh: openInAppZh, en: openInAppEn }, conversation: { zh: conversationEn, en: conversationEn } };
38
+ const locale = {
39
+ ...localeSource,
40
+ register(ns: string, dictionary: any) { dictionaries[ns] = dictionary; return () => { delete dictionaries[ns]; }; },
41
+ bind(ns: string) { return (key: string, values?: Record<string, any>) => {
42
+ let text = dictionaries[ns]?.[language]?.[key] ?? dictionaries[ns]?.en?.[key] ?? key;
43
+ for (const [name, value] of Object.entries(values ?? {})) text = text.replaceAll(`{${name}}`, String(value));
44
+ return text;
45
+ }; },
46
+ };
47
+ ctx.provide('locale', locale);
48
+ slots.installLocale(locale);
49
+ const messageItems = new Map();
50
+ ctx.provide('remote', {
51
+ messageFeedback: {
52
+ async list(request: any) { observations.requests.push(['list', request]); return { ok: true, value: { ok: true, value: { items: [...messageItems.values()] } } }; },
53
+ async put(request: any) {
54
+ observations.requests.push(['put', request]);
55
+ const item = { messageId: request.messageId, rating: request.rating, version: (messageItems.get(request.messageId)?.version ?? 0) + 1, ...request.entry };
56
+ messageItems.set(request.messageId, item);
57
+ return { ok: true, value: { ok: true, value: item } };
58
+ },
59
+ async delete(request: any) { observations.requests.push(['delete', request]); messageItems.delete(request.messageId); return { ok: true, value: { ok: true, value: { item: null } } }; },
60
+ },
61
+ sessionFeedback: { record: async () => { throw new Error('Unexpected session feedback'); } },
62
+ });
63
+ const binding = { key: 'session-a', ctx: new Context(), hooks: {}, keyedHooks: {}, props: { sessionId: 'session-a' } };
64
+ const current = source(binding);
65
+ slots.installScope('session', { current, bindingSource: (target: any) => source(target === binding.key ? binding : undefined), renderArea: (_b: any, props: any) => props.children });
66
+ const toolBlock = {
67
+ kind: 'tool-result', callId: 'read-1', call: { name: 'read', args: { file_path: '/fixture/sample.ts', offset: 12 }, argsRaw: '{"file_path":"/fixture/sample.ts","offset":12}' },
68
+ content: [{ type: 'text', text: '<path>/fixture/sample.ts</path>\n<type>file</type>\n<content>\n12: export const value = 1;\n</content>' }], isError: false, subCalls: [], time: 100, callTime: 10,
69
+ meta: { path: '/fixture/sample.ts', offset: 12, totalLines: 20, lines: [{ number: 12, text: 'export const value = 1;' }], lang: 'typescript' },
70
+ };
71
+ const host = source({ home: '/fixture' });
72
+ const loadImage = Object.assign(async () => '/pixel.png', { peek: () => '/pixel.png' });
73
+ const blockProps = { loadImage: async () => ({ data: new Uint8Array(), mediaType: 'image/png' }), fillComposer: () => true, cwd: '/fixture', openFile: (path: string, options?: any) => observations.opens.push({ path, options }) };
74
+ let stopReader = () => {};
75
+ let stopMirror = () => {};
76
+ slots.register({ name: 'root', children: {
77
+ 'conversation.chat.assistant-actions': { kind: 'list', scope: 'session' },
78
+ 'tool.call.toolview': { kind: 'keyed', scope: 'session' },
79
+ 'conversation.chat.turnTail': { kind: 'list', scope: 'session' },
80
+ 'conversation.chat.node': { kind: 'keyed', scope: 'session', inject: { hooks: { turnData: (_s: any, value: any) => () => value } } },
81
+ 'conversation.message.images': { kind: 'single', scope: 'session' },
82
+ 'conversation.input.overlay': { kind: 'list', scope: 'session' },
83
+ 'fixture.reader': { kind: 'single', scope: 'session' },
84
+ } }, ({ SessionProvider, renderSlot }: any) => <SessionProvider>
85
+ {renderSlot('fixture.reader', {}, { fallback: <div data-unmounted>Reader unloaded</div> })}
86
+ {renderSlot('conversation.input.overlay', {})}
87
+ </SessionProvider>);
88
+ await ctx.plugin({ name: 'fixture-official-feedback', inject: ['slots', 'remote', 'locale'], apply: applyFeedback });
89
+ slots.register({ name: 'tool.call.toolview', key: 'read', locale: 'conversation' }, ReadRow);
90
+ readImageToolview.apply(ctx);
91
+ slots.provideRoot({ hooks: { sessions: source({ byId: { 'session-a': { cwd: '/fixture' } } }) } });
92
+ const artifact = { path: '/fixture/report.pdf', description: '演示报告', seq: 42, index: 0 };
93
+ const presentedOpen = source({});
94
+ const presentedHost = source({ available: true, fileManager: 'finder' });
95
+ const changesDiff = source({});
96
+ const changesSummary = source({});
97
+ const showCodeDiff = source(false);
98
+ slots.register({ name: 'conversation.chat.turnTail', id: '@deepseek-ai/dsh-client-ui-deliverables', locale: 'deliverables',
99
+ children: { 'deliverables.file.actions': { kind: 'list', scope: 'session' } },
100
+ inject: () => ({ hooks: { changesDiff, showCodeDiff, presentedOpen, presentedHost, changesSummary },
101
+ loadChangesDiff: async () => {}, reloadPresentedHost: async () => {}, loadChangesSummary: async () => {},
102
+ openPresented: async (...args: any[]) => { observations.opens.push(['presented', ...args]); },
103
+ openChanged: async () => {}, openChangesReview: () => {},
104
+ }),
105
+ }, DeliverablesTail);
106
+ slots.register({ name: 'deliverables.file.actions', id: 'open-in-app', locale: 'open-in-app' }, FileRouteAction);
107
+ let liveDetail = source('initial');
108
+ const storeHandle = { create: () => {
109
+ observations.mounts++;
110
+ const value = source(0);
111
+ return { ...value, actions: { increment: () => value.set(value.getSnapshot() + 1) }, clearPersisted() {} };
112
+ } };
113
+ slots.register({ name: 'conversation.chat.node', key: 'future-widget', store: storeHandle,
114
+ children: { 'future.detail': { kind: 'single', scope: 'session' } },
115
+ inject: (sessionId: string, actions: any) => ({ hooks: { detail: liveDetail }, injectedSession: sessionId, increment: actions.increment }),
116
+ }, memo(function FutureWidget({ useStore, useDetail, useTurnData, injectedSession, increment, renderSlot }: any) {
117
+ const value = useStore((n: number) => n);
118
+ const detail = useDetail((s: string) => s);
119
+ const context = useTurnData();
120
+ return <div data-memo-widget><button onClick={increment}>Count {value}</button><span>{injectedSession} / {detail} / {context}</span>{renderSlot('future.detail', {})}</div>;
121
+ }));
122
+ slots.register({ name: 'future.detail' }, memo(() => <span data-nested>nested official child</span>));
123
+
124
+ function ReaderFixture({ renderSlot, renderSlotChain }: any) {
125
+ const official = { renderSlot, renderSlotChain, officialHost: host, officialImageLoader: loadImage, officialFileMentions: () => undefined, officialPreviewFile: blockProps.openFile, openView: (...args: any[]) => observations.opens.push(args) };
126
+ const [text, setText] = useState('本地图片:\n\n![本地样例](/fixture/image.png)\n\n普通正文 **加粗** 与 `sample.ts`。');
127
+ return <main className={css.root} data-dsh-better-display="official-fixture"><div className={css.column} data-chat-flow="">
128
+ <article className={css.answer} data-reader-answer>
129
+ <MotionMarkdown text={text} streaming={false} enabled={false} revision={0} />
130
+ <CopyAnswer blocks={[{ kind: 'text', text }]} extraActions={<OfficialActions official={official} messageId={'message-1' as any} />} />
131
+ </article>
132
+ <div data-reader-tool><OfficialTool {...blockProps} official={official} renderSlotChain={renderSlotChain} block={toolBlock as any} toolName="read" fallback={<span>fallback</span>} /></div>
133
+ {renderSlot(OFFICIAL_SEATS.tail, { turn: { data: new Map([['deliverables', { changes: null, presented: [artifact] }]]) }, seq: 43, openFile: blockProps.openFile, readerProducedPaths: ['/fixture/sample.ts'] })}
134
+ {renderSlot(OFFICIAL_SEATS.nodes, {}, { entryKey: 'future-widget', hookContext: 'turn-context' })}
135
+ <button onClick={() => setText(text + '\n\n继续输出。')}>Append text</button>
136
+ </div></main>;
137
+ }
138
+ function mountReader() {
139
+ stopReader = slots.register({ name: 'fixture.reader', children: officialChildren(slots) }, ReaderFixture);
140
+ stopMirror = installOfficialSlots(ctx);
141
+ }
142
+ mountReader();
143
+ const root = createRoot(document.getElementById('app')!);
144
+ root.render(slots.renderSlot('root', {}));
145
+ (window as any).fixture = {
146
+ observations, update: () => liveDetail.set('updated'),
147
+ language: () => { language = 'en'; localeSource.set({ revision: 1 }); },
148
+ addAction: () => slots.register({ name: 'conversation.chat.assistant-actions', id: 'future-action', order: 20 }, memo(() => <button>Future official action</button>)),
149
+ unmount: () => { stopMirror(); stopReader(); }, mount: mountReader,
150
+ };
151
+
152
+ } });
@@ -0,0 +1,22 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
4
+ import { StickyLane } from '../src/client/StickyLane.js';
5
+ import { ChoreographedFlow } from '../src/client/ChoreographedFlow.js';
6
+ import { ClosedProcessSummary } from '../src/client/ClosedProcessSummary.js';
7
+ import { Disclosure, StatusText } from '../src/client/motion.js';
8
+ import { presentLiveTurn } from '../src/client/live-turn.js';
9
+ import type { LiveStep } from '../src/client/live-turn.js';
10
+ import css from '../src/client/Reader.module.css';
11
+ const steps:LiveStep[]=[...Array.from({length:6},(_,i):LiveStep=>({kind:'reasoning',key:`r${i}`,nodeKey:`r${i}`,step:i,start:0,blocks:[]})),{kind:'body',key:'body',nodeKey:'body',step:6,start:0,blocks:[]},{kind:'other',key:'extra',nodeKey:'extra'},{kind:'reasoning',key:'latest',nodeKey:'latest',step:7,start:0,blocks:[]},{kind:'body',key:'answer',nodeKey:'answer',step:7,start:0,blocks:[]}];
12
+ function Fixture(){
13
+ const [expanded,setExpanded]=useState(true),[open,setOpen]=useState<Record<string,boolean>>({}),[closed,setClosed]=useState(false);const button=useRef<HTMLButtonElement>(null);
14
+ const frame=useMemo(()=>({items:presentLiveTurn(steps,{status:'open',reason:null,latestStep:7,closingStep:null}),snapshot:{} as ChatSnapshot}),[]);
15
+ return <><button onClick={()=>setClosed(v=>!v)}>切换完成状态</button><div data-conversation-scroll style={{height:'calc(100vh - 32px)',overflow:'auto'}}><div className={css.root} data-dsh-better-display="fixture"><div className={css.column} data-chat-flow="">
16
+ <StickyLane kind="toolbar" className={css.toolbar}><button className={css.textButton}>自动折叠开</button></StickyLane>
17
+ <section className={css.turn} data-reader-turn="1"><div style={{height:240,background:'#eef2ff',alignSelf:'flex-end',width:'65%'}}>用户消息:滚动抵达顶部才吸顶。</div>
18
+ <StickyLane kind="status" className={css.turnProcessSticky}><Disclosure open={expanded} onChange={setExpanded} buttonRef={button} label={<StatusText text="正在使用工具" motion={false}/>} /></StickyLane>
19
+ {closed?<><ClosedProcessSummary steps={steps.slice(0,-1)} open={expanded} onChange={setExpanded} controls="body"/><div id="body" style={{height:1600}}>最终回答占位,验证完成后统计仍然吸顶。</div></>:<ChoreographedFlow id="flow" frame={frame} motion={false} enabled={false} urgent={false} processOpen={expanded} open={open} onOpenChange={(k,v)=>{setExpanded(true);setOpen(x=>({...x,[k]:v}));}} renderStep={s=>s.key==='answer'?<div style={{height:1600}}>当前输出保持完整,统计不应被推出视野。</div>:expanded?<div style={{height:s.key==='latest'?300:100}}>过程 {s.key}</div>:null}/>}
20
+ </section><div style={{height:180}}>下一轮之前</div></div></div></div></>;
21
+ }
22
+ createRoot(document.getElementById('app')!).render(<Fixture/>);
@@ -0,0 +1,14 @@
1
+ import React, { useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { WaitingStatus } from '../src/client/WaitingStatus.js';
4
+ import { waitingAnchor } from '../src/client/waiting-clock.js';
5
+ function Fixture() {
6
+ const [inputs,setInputs]=useState([{key:'u1',kind:'user',data:{time:Date.now()-300000}}]);
7
+ const [renders,setRenders]=useState(0);
8
+ const anchor=waitingAnchor(inputs.map(x=>x.key),key=>inputs.find(x=>x.key===key));
9
+ return <main style={{padding:30}}><h1>等待计时回归:真实组件、模拟输入</h1>
10
+ <button onClick={()=>setInputs(x=>[...x,{key:`s${x.length}`,kind:'steering',data:{time:Date.now()}}])}>追加 steering</button>
11
+ <button onClick={()=>setRenders(x=>x+1)}>普通输出更新</button><span>更新次数 {renders}</span>
12
+ <WaitingStatus anchor={anchor} label="深度求索中..."/></main>;
13
+ }
14
+ createRoot(document.getElementById('app')!).render(<Fixture/>);
@@ -0,0 +1,66 @@
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
+
9
+ /** Maid-atelier / phoebe-atelier hide rule, reduced to the :has() predicate. */
10
+ const SKIN_HIDE = '[data-conversation-scroll]:not(:has([data-chat-flow])) > [data-composer-seat]';
11
+
12
+ interface AttrNode {
13
+ attrs: Record<string, string>;
14
+ children: AttrNode[];
15
+ }
16
+
17
+ function hasAttr(node: AttrNode, name: string): boolean {
18
+ if (Object.hasOwn(node.attrs, name)) return true;
19
+ return node.children.some(child => hasAttr(child, name));
20
+ }
21
+
22
+ /** True when the skin rule would set the seat to display:none. */
23
+ function skinHidesComposerSeat(scroll: AttrNode): boolean {
24
+ return !hasAttr(scroll, 'data-chat-flow');
25
+ }
26
+
27
+ function readerColumn(hook: boolean): AttrNode {
28
+ return {
29
+ attrs: hook ? { 'data-chat-flow': '' } : {},
30
+ children: [{ attrs: { 'data-reader-turn': '1' }, children: [] }],
31
+ };
32
+ }
33
+
34
+ test('Reader source keeps the native ChatView chat-flow hook on its column', () => {
35
+ const source = readFileSync(resolve(root, 'src/client/Reader.tsx'), 'utf8');
36
+ const column = source.match(/<div className=\{css\.column\}[^>]*>/);
37
+ assert.ok(column, 'Reader still renders css.column');
38
+ assert.match(column[0], /data-chat-flow=""/);
39
+ });
40
+
41
+ test('committed client bundle publishes data-chat-flow for git installs', () => {
42
+ const client = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
43
+ assert.match(client, /"data-chat-flow":\s*""/);
44
+ const compiled = readFileSync(resolve(root, 'lib/types/client/Reader.js'), 'utf8');
45
+ assert.match(compiled, /"data-chat-flow":\s*""/);
46
+ });
47
+
48
+ test('skin empty-state hide stays off when Reader mounts inside the scrollport', () => {
49
+ const inspectOnly: AttrNode = {
50
+ attrs: { 'data-conversation-scroll': '' },
51
+ children: [
52
+ { attrs: { 'data-reader-turn': '1' }, children: [] },
53
+ { attrs: { 'data-composer-seat': '' }, children: [] },
54
+ ],
55
+ };
56
+ const withReader: AttrNode = {
57
+ attrs: { 'data-conversation-scroll': '' },
58
+ children: [readerColumn(true), { attrs: { 'data-composer-seat': '' }, children: [] }],
59
+ };
60
+ assert.equal(skinHidesComposerSeat(inspectOnly), true, SKIN_HIDE);
61
+ assert.equal(skinHidesComposerSeat(withReader), false, SKIN_HIDE);
62
+ assert.equal(skinHidesComposerSeat({
63
+ attrs: { 'data-conversation-scroll': '' },
64
+ children: [readerColumn(false), { attrs: { 'data-composer-seat': '' }, children: [] }],
65
+ }), true);
66
+ });
@@ -0,0 +1,34 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { accessSync, constants, readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+
9
+ test('cloud hard-gate files are committed and ban other models / computer use', () => {
10
+ const hooks = JSON.parse(readFileSync(resolve(root, '.cursor/hooks.json'), 'utf8')) as {
11
+ hooks: { subagentStart: { command: string }[] };
12
+ };
13
+ assert.equal(hooks.hooks.subagentStart[0]?.command, '.cursor/hooks/block-other-models.sh');
14
+
15
+ const script = resolve(root, '.cursor/hooks/block-other-models.sh');
16
+ accessSync(script, constants.X_OK);
17
+ const hook = readFileSync(script, 'utf8');
18
+ assert.match(hook, /computeruse|computerUse/);
19
+ assert.match(hook, /browser/);
20
+ assert.match(hook, /claude|sonnet|opus/);
21
+
22
+ const worker = readFileSync(resolve(root, '.cursor/agents/worker.md'), 'utf8');
23
+ assert.match(worker, /model: inherit/);
24
+ assert.match(worker, /force-default-model: true/);
25
+
26
+ const cloud = readFileSync(resolve(root, '.cursor/CLOUD.md'), 'utf8');
27
+ assert.match(cloud, /Only use Grok 4\.6/);
28
+ assert.match(cloud, /Never Claude, Sonnet, Opus, GPT, or Gemini/);
29
+ assert.match(cloud, /model: inherit/);
30
+ assert.match(cloud, /Do not spawn the Task tool, computerUse, or browser/);
31
+ assert.match(cloud, /Composer is the only fallback/);
32
+ assert.match(cloud, /Do not pass a `model` argument/);
33
+ assert.match(cloud, /Prefer Grok or Composer only/);
34
+ });
@@ -0,0 +1,33 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { toolUpdateRows } from '../src/client/native/tool-update.js';
4
+
5
+ test('a single tool addition is its own closed row', () => {
6
+ assert.deepEqual(toolUpdateRows([{ type: 'tool-addition', toolName: 'search' }]), {
7
+ added: ['search'],
8
+ removed: [],
9
+ single: { type: 'tool-addition', toolName: 'search' },
10
+ });
11
+ });
12
+
13
+ test('mixed tool changes stay one expandable update', () => {
14
+ assert.deepEqual(toolUpdateRows([
15
+ { type: 'tool-addition', toolName: 'search' },
16
+ { type: 'tool-addition', toolName: 'read_file' },
17
+ { type: 'tool-removal', toolName: 'old_search' },
18
+ ]), {
19
+ added: ['search', 'read_file'],
20
+ removed: ['old_search'],
21
+ single: null,
22
+ });
23
+ });
24
+
25
+ test('text or a missing tool name stays a generic context row', () => {
26
+ assert.equal(toolUpdateRows([{ type: 'text', toolName: 'search' }]), null);
27
+ assert.equal(toolUpdateRows([{ type: 'tool-addition' }]), null);
28
+ assert.equal(toolUpdateRows([]), null);
29
+ assert.equal(toolUpdateRows([
30
+ { type: 'tool-addition', toolName: 'search' },
31
+ { type: 'text' },
32
+ ]), null);
33
+ });
@@ -0,0 +1,47 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { flowTop, landTurn, READING_LINE_OFFSET_PX, scrollerOf } from '../src/client/conversation-scroll.ts';
4
+
5
+ interface FakeBox {
6
+ top: number;
7
+ scrollTop: number;
8
+ closest: (selector: string) => FakeBox | null;
9
+ getBoundingClientRect: () => { top: number };
10
+ }
11
+
12
+ function box(top: number, extra: Partial<FakeBox> = {}): FakeBox {
13
+ const node: FakeBox = {
14
+ top,
15
+ scrollTop: 0,
16
+ closest: () => null,
17
+ getBoundingClientRect: () => ({ top: node.top }),
18
+ ...extra,
19
+ };
20
+ return node;
21
+ }
22
+
23
+ test('scrollerOf prefers the conversation scroll host over the reader root', () => {
24
+ const host = box(40);
25
+ const root = box(80, { closest: selector => selector === '[data-conversation-scroll]' ? host : null });
26
+ assert.equal(scrollerOf(root as unknown as HTMLElement), host as unknown as HTMLElement);
27
+ });
28
+
29
+ test('scrollerOf falls back to the reader root when no host exists', () => {
30
+ const root = box(0);
31
+ assert.equal(scrollerOf(root as unknown as HTMLElement), root as unknown as HTMLElement);
32
+ });
33
+
34
+ test('landTurn writes only scrollport.scrollTop using the official reading-line offset', () => {
35
+ const scrollport = box(100, { scrollTop: 50 });
36
+ const row = box(300);
37
+ landTurn(row as unknown as HTMLElement, scrollport as unknown as HTMLElement);
38
+ assert.equal(flowTop(row as unknown as HTMLElement, scrollport as unknown as HTMLElement), 200);
39
+ assert.equal(scrollport.scrollTop, 50 + 200 - READING_LINE_OFFSET_PX);
40
+ });
41
+
42
+ test('landTurn to the last row clamps naturally via the caller scrollport assignment', () => {
43
+ const scrollport = box(0, { scrollTop: 900 });
44
+ const row = box(40);
45
+ landTurn(row as unknown as HTMLElement, scrollport as unknown as HTMLElement);
46
+ assert.equal(scrollport.scrollTop, 900 + 40 - READING_LINE_OFFSET_PX);
47
+ });