@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,45 @@
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 { asReadonlyArray, pendingSubmissionImages } from '../src/client/pending-submission.ts';
7
+
8
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
+
10
+ test('text-only pending submission does not throw on missing images', () => {
11
+ const echo = { requestId: 'r1', text: 'hello', time: 1 };
12
+ assert.doesNotThrow(() => pendingSubmissionImages(echo).length);
13
+ assert.deepEqual(pendingSubmissionImages(echo), []);
14
+ assert.deepEqual(pendingSubmissionImages({ images: undefined }), []);
15
+ assert.deepEqual(pendingSubmissionImages({ attachments: undefined }), []);
16
+ assert.deepEqual(pendingSubmissionImages(undefined), []);
17
+ assert.deepEqual(pendingSubmissionImages(null), []);
18
+ });
19
+
20
+ test('official images and legacy attachments both resolve', () => {
21
+ assert.deepEqual(
22
+ pendingSubmissionImages({ images: [{ previewUrl: 'blob:a', width: 4, height: 3, name: 'a.png' }] }),
23
+ [{ previewUrl: 'blob:a', width: 4, height: 3, name: 'a.png' }],
24
+ );
25
+ assert.deepEqual(
26
+ pendingSubmissionImages({ attachments: [{ type: 'image', value: { previewUrl: 'blob:b' } }, { type: 'file' }] }),
27
+ [{ previewUrl: 'blob:b' }],
28
+ );
29
+ });
30
+
31
+ test('pendingSubmissions that is not an array is treated as empty', () => {
32
+ assert.deepEqual(asReadonlyArray(undefined), []);
33
+ assert.deepEqual(asReadonlyArray(null), []);
34
+ assert.deepEqual(asReadonlyArray({ length: 2 }), []);
35
+ assert.deepEqual(asReadonlyArray([{ requestId: 'a' }]), [{ requestId: 'a' }]);
36
+ });
37
+
38
+ test('Reader and committed client bundle do not read unguarded submission.images.length', () => {
39
+ const source = readFileSync(resolve(root, 'src/client/Reader.tsx'), 'utf8');
40
+ assert.match(source, /pendingSubmissionImages/);
41
+ assert.doesNotMatch(source, /submission\.images\.length/);
42
+ const client = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
43
+ assert.doesNotMatch(client, /submission\.images\.length/);
44
+ assert.match(client, /pendingSubmissionImages|function pendingSubmissionImages/);
45
+ });
@@ -0,0 +1,23 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { localPathMediaUrl } from '../src/client/platform-media.js';
4
+
5
+ test('absolute local image paths use the authenticated Host file API without losing authored bytes', () => {
6
+ for (const path of ['/tmp/a.png', '/tmp/中文 图 #1.png', '/tmp/a%20b.png', '/tmp/a?x=1&y=2.png']) {
7
+ const url = localPathMediaUrl('http:', 'http://127.0.0.1:43127', path)!;
8
+ assert.equal(new URL(url).pathname, '/api/file');
9
+ assert.equal(new URL(url).searchParams.get('path'), path);
10
+ }
11
+ });
12
+ test('relative, protocol-relative and non-HTTP page paths are not sent to the Host file API', () => {
13
+ for (const path of ['', 'relative.png', '//host/image.png', 'https://host/image.png', 'data:image/png,x', 'javascript:alert(1)']) {
14
+ assert.equal(localPathMediaUrl('http:', 'http://local', path), undefined);
15
+ }
16
+ assert.equal(localPathMediaUrl('file:', 'null', '/tmp/image.png'), undefined);
17
+ assert.equal(localPathMediaUrl('dsh-app:', 'dsh-app://other', '/tmp/image.png'), undefined);
18
+ });
19
+ test('Desktop file routes use the application file API', () => {
20
+ const url = localPathMediaUrl('dsh-app:', 'dsh-app://app', '/tmp/a.png')!;
21
+ assert.equal(url.startsWith('dsh-app://app/api/file?'), true);
22
+ assert.equal(new URL(url).searchParams.get('path'), '/tmp/a.png');
23
+ });
@@ -0,0 +1,184 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import type { AssistantBlock, ToolCallBlock, TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
4
+ import type { AssistantChatData, ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
5
+ import { assistantSegments, boundaryOf, forkAnchorSeq, groupNodes, hasProcessContent, hasVisibleBody, isEarlierNarration, processChoiceKey, processExpanded, terminalLabel, toolFailed } from '../src/client/projection.ts';
6
+ import type { TurnBoundary } from '../src/client/projection.ts';
7
+ import { activityPhase, activitySummary, inputFields, readerFlow } from '../src/client/tool-activity.ts';
8
+
9
+ const text: AssistantBlock = { kind: 'text', text: '前序说明或回答:不能靠关键词判断。' };
10
+ function assistant(values: Partial<AssistantChatData> = {}): AssistantChatData {
11
+ return { status: 'settled', turn: 1, step: 0, blocks: [text], time: 1, ...values };
12
+ }
13
+ const active: TurnBoundary = { status: 'open', reason: null, latestStep: 2, closingStep: null };
14
+ const completed: TurnBoundary = { ...active, status: 'closed', reason: 'completed', closingStep: 2 };
15
+
16
+ test('RC1 system prompts belong to process details', () => {
17
+ assert.equal(hasProcessContent({ kind: 'system-prompt', visibility: 'visible', data: { text: 'system' } } as ChatConversationViewNode, completed), true);
18
+ });
19
+
20
+ test('canonical cancellation is interrupted while genuine failures stay failed', () => {
21
+ const cancelled = { kind: 'tool-result', isError: true, error: { code: 'ABORTED', name: 'AbortError' }, content: [{ type: 'text', text: 'Error: tool call aborted' }], subCalls: [] } as unknown as ToolCallBlock;
22
+ assert.equal(activityPhase({ block: cancelled }), 'interrupted');
23
+ assert.equal(toolFailed(cancelled), false);
24
+ const failure = { ...cancelled, error: { code: 'OTHER' }, content: [{ type: 'text', text: 'failure' }], meta: { exitCode: 7 } } as ToolCallBlock;
25
+ assert.equal(activityPhase({ block: failure }, true), 'failed');
26
+ assert.equal(toolFailed(failure), true);
27
+ });
28
+
29
+ test('reasoning and body retain original order, content and identities across streaming appends', () => {
30
+ const first: AssistantBlock = { kind: 'reasoning', text: '**原始标点**\n 原始空格\n' };
31
+ const later: AssistantBlock = { kind: 'reasoning', text: '正文之后的思考' };
32
+ const segments = assistantSegments([first, text, later]);
33
+ assert.deepEqual(segments.map(part => [part.kind, part.start]), [['reasoning', 0], ['body', 1], ['reasoning', 2]]);
34
+ assert.deepEqual(segments.flatMap(part => part.blocks), [first, text, later]);
35
+ assert.equal(segments[0].blocks[0], first);
36
+ const append = assistantSegments([first, text, later, { kind: 'text', text: '最终正文' }]);
37
+ assert.deepEqual(append.slice(0, 3), segments);
38
+ });
39
+
40
+ test('a settled step alone never means final or disposable', () => {
41
+ assert.equal(isEarlierNarration(assistant(), { ...active, latestStep: 0 }), false);
42
+ assert.equal(isEarlierNarration(assistant(), { ...active, status: 'unknown' }), false);
43
+ assert.equal(isEarlierNarration(assistant({ status: 'running' }), active), false);
44
+ assert.equal(isEarlierNarration(assistant(), active), false);
45
+ });
46
+
47
+ test('process stays open through body output and later steps until successful turn completion', () => {
48
+ assert.equal(processExpanded(undefined, active), true);
49
+ assert.equal(processExpanded(undefined, { ...active, latestStep: 7 }), true);
50
+ assert.equal(processExpanded(undefined, completed), false);
51
+ assert.equal(processExpanded(undefined, { ...active, status: 'unknown' }), true);
52
+ for (const reason of ['error', 'aborted', 'interrupted', 'blocked', 'max-tokens', 'future-terminal']) {
53
+ assert.equal(processExpanded(undefined, { ...completed, reason }), true, reason);
54
+ }
55
+ });
56
+
57
+ test('deliberate process expansion or collapse overrides the automatic lifecycle', () => {
58
+ assert.equal(processExpanded(true, completed), true);
59
+ assert.equal(processExpanded(false, active), false);
60
+ assert.equal(processExpanded(false, { ...completed, reason: 'error' }), false);
61
+ });
62
+
63
+ test('reading during a run does not keep the completed process open; reopening after completion is deliberate', () => {
64
+ const choices: Record<string, boolean> = {};
65
+ choices[processChoiceKey('turn:1', active)] = true;
66
+ assert.equal(processExpanded(choices[processChoiceKey('turn:1', completed)], completed), false);
67
+ assert.equal(processChoiceKey('turn:1', active), processChoiceKey('turn:1', { ...active, latestStep: 9 }));
68
+ choices[processChoiceKey('turn:1', completed)] = true;
69
+ assert.equal(processExpanded(choices[processChoiceKey('turn:1', completed)], completed), true);
70
+ assert.equal(processExpanded(choices[processChoiceKey('turn:2', completed)], completed), false);
71
+ });
72
+
73
+ test('body-only steps do not advertise empty thinking, while real reasoning and earlier progress remain accessible', () => {
74
+ const node = (data: AssistantChatData) => ({ kind: 'assistant-step', visibility: 'visible', data }) as ChatConversationViewNode;
75
+ const closing = node(assistant({ step: 2 }));
76
+ assert.equal(hasProcessContent(closing, active), false);
77
+ assert.equal(hasProcessContent(closing, completed), false);
78
+ assert.equal(hasProcessContent(node(assistant({ step: 2, blocks: [{ kind: 'reasoning', text: '' }, text] })), completed), false);
79
+ assert.equal(hasProcessContent(node(assistant({ step: 2, blocks: [{ kind: 'reasoning', text: '真实思考' }, text] })), active), true);
80
+ assert.equal(hasProcessContent(node(assistant({ step: 1 })), completed), true);
81
+ assert.equal(hasProcessContent({ ...closing, visibility: 'hidden' }, completed), false);
82
+ });
83
+
84
+ test('completed turns preserve the public closing message, even with a later nontext step', () => {
85
+ assert.equal(isEarlierNarration(assistant({ step: 2 }), completed), false);
86
+ assert.equal(isEarlierNarration(assistant({ step: 1 }), { ...completed, closingStep: 1 }), false);
87
+ assert.equal(isEarlierNarration(assistant({ step: 0 }), { ...completed, closingStep: 1 }), true);
88
+ assert.equal(isEarlierNarration(assistant(), { ...completed, closingStep: null }), false);
89
+ });
90
+
91
+ test('all abnormal and unknown terminal states preserve generated prefixes', () => {
92
+ for (const reason of ['error', 'aborted', 'interrupted', 'blocked', 'max-tokens', 'future-terminal']) {
93
+ assert.equal(isEarlierNarration(assistant(), { ...completed, reason }), false, reason);
94
+ assert.ok(terminalLabel(reason), reason);
95
+ }
96
+ assert.equal(isEarlierNarration(assistant({ status: 'interrupted' }), completed), false);
97
+ assert.equal(terminalLabel('completed'), null);
98
+ });
99
+
100
+ test('images, future modalities, and mixed body blocks never disappear into process', () => {
101
+ const unknown: AssistantBlock = { kind: 'other', block: { type: 'mcp-app', html: '<script>window.bad=true</script>' } };
102
+ const image: AssistantBlock = { kind: 'image', attachment: { attachmentId: 'test-attachment' as never, width: 100, height: 100, mediaType: 'image/png', bytes: 10 } };
103
+ for (const blocks of [[unknown], [text, unknown], [image], [text, image]]) {
104
+ assert.equal(isEarlierNarration(assistant({ blocks }), completed), false);
105
+ assert.equal(hasVisibleBody(blocks), true);
106
+ }
107
+ assert.equal(hasVisibleBody([{ kind: 'reasoning', text: 'private display trace' }]), false);
108
+ });
109
+
110
+ test('classification never inspects wording', () => {
111
+ for (const value of ['Final answer:', '正在处理', '请你确认再继续', 'All done', 'Think']) {
112
+ assert.equal(isEarlierNarration(assistant({ step: 2, blocks: [{ kind: 'text', text: value }] }), completed), false);
113
+ }
114
+ });
115
+
116
+ test('structural grouping retains keys and source order and excludes host-hidden rows', () => {
117
+ const turn = { turn: 1, status: 'open', steps: [] } as unknown as TurnLocation;
118
+ const node = (key: string, kind: string, location: ChatConversationViewNode['location'], visibility: 'visible' | 'hidden' = 'visible'): ChatConversationViewNode => ({ key, id: key, kind, target: 'chat', data: {}, anchorSeq: Number(key), visibility, location });
119
+ const nodes = [node('1', 'user', { kind: 'unresolved' }), node('2', 'assistant-step', { kind: 'turn', turn }), node('3', 'tool-call', { kind: 'turn', turn }), node('4', 'context', { kind: 'turn', turn }, 'hidden'), node('5', 'custom', { kind: 'unresolved' })];
120
+ const byKey = new Map(nodes.map(row => [row.key, row]));
121
+ const result = groupNodes(nodes.map(row => row.key), key => byKey.get(key));
122
+ assert.deepEqual(result.map(group => group.keys), [['1'], ['2', '3'], ['5']]);
123
+ assert.deepEqual(result.flatMap(group => group.keys), ['1', '2', '3', '5']);
124
+ assert.equal(result[1]?.key, groupNodes([...nodes.map(row => row.key), 'missing'], key => byKey.get(key))[1]?.key);
125
+ });
126
+
127
+ test('missing historical boundaries remain explicit uncertainty', () => {
128
+ const boundary = boundaryOf(undefined);
129
+ assert.deepEqual(boundary, { status: 'unknown', reason: null, latestStep: -1, closingStep: null });
130
+ assert.equal(isEarlierNarration(assistant(), boundary), false);
131
+ });
132
+
133
+ test('a failed child tool is not hidden by a successful parent summary', () => {
134
+ const failed = { kind: 'tool-result', isError: true, content: [], subCalls: [] } as unknown as ToolCallBlock;
135
+ const parent = { kind: 'tool-result', isError: false, content: [], subCalls: [failed] } as unknown as ToolCallBlock;
136
+ assert.equal(toolFailed(parent), true);
137
+ });
138
+
139
+ test('tool input is visible before execution, including native-hidden tool-only steps', () => {
140
+ const draft = { kind: 'tool-call' as const, callId: 'draft-call', name: 'write', argsRaw: '{"file_path":"/work/view.html","content":"<html>\\n' };
141
+ const data = assistant({ status: 'running', blocks: [draft] });
142
+ const turn = { turn: 1, steps: [{ step: 0, start: { seq: 1 }, data: { get: () => data } }] } as unknown as TurnLocation;
143
+ const group = { key: 'turn:1', turn: 1, keys: [] };
144
+ const pending = readerFlow(group, turn, () => undefined);
145
+ assert.equal(pending.length, 1);
146
+ assert.equal(pending[0]?.kind, 'tool');
147
+ if (pending[0]?.kind !== 'tool') throw new Error('missing tool');
148
+ assert.equal(activityPhase(pending[0]), 'preparing');
149
+ assert.equal(activitySummary(pending[0]).title, '写入 view.html');
150
+ assert.equal(activitySummary(pending[0]).content, '<html>\n');
151
+ const block = { callId: draft.callId, name: 'write', argsRaw: draft.argsRaw, subCalls: [] } as unknown as ToolCallBlock;
152
+ const node = { key: 'tool-native', kind: 'tool-call', visibility: 'visible', anchorSeq: 10, data: { root: block }, location: { kind: 'step', step: { step: 0 } } } as unknown as ChatConversationViewNode;
153
+ const running = readerFlow({ ...group, keys: [node.key] }, turn, () => node);
154
+ assert.equal(running.length, 1);
155
+ assert.equal(running[0]?.key, pending[0].key, 'one call keeps the same React key at execution');
156
+ });
157
+
158
+ test('partial argument parsing respects JSON nesting, escapes and unfinished unicode', () => {
159
+ const source = JSON.stringify({ file_path: '/work/真实.html', content: '"file_path":"fake"\n你好😀', nested: { file_path: 'also fake' } });
160
+ for (let cut = source.indexOf('content') + 10; cut <= source.length; cut++) {
161
+ const fields = inputFields(source.slice(0, cut));
162
+ assert.equal(fields.file_path, '/work/真实.html');
163
+ if (typeof fields.content === 'string') assert.ok('"file_path":"fake"\n你好😀'.startsWith(fields.content));
164
+ }
165
+ assert.equal(inputFields('{"content":"start\\u4f').content, 'start');
166
+ assert.equal(inputFields('{"content":"slash\\\\').content, 'slash\\');
167
+ assert.equal(inputFields('{"nested":{"file_path":"fake"},"file_path":"real').file_path, 'real');
168
+ });
169
+
170
+ test('a nonzero terminal exit is a failure even when the tool transport is non-error', () => {
171
+ const block = { kind: 'tool-result', isError: false, content: [], meta: { exitCode: 7 }, subCalls: [] } as unknown as ToolCallBlock;
172
+ assert.equal(toolFailed(block), true);
173
+ assert.equal(activityPhase({ block }), 'failed');
174
+ assert.equal(activityPhase({ block: { ...block, meta: null } as ToolCallBlock }), 'returned');
175
+ assert.equal(activityPhase({}, true), 'interrupted');
176
+ });
177
+
178
+ test('fork anchor prefers the durable closing seq and never yields a missing anchor', () => {
179
+ assert.equal(forkAnchorSeq([{ seq: 42 }]), 42);
180
+ assert.equal(forkAnchorSeq([null, undefined, {}, { seq: '42' as unknown as number }, { seq: 7 }]), 7);
181
+ assert.equal(forkAnchorSeq([{ seq: NaN }, { seq: Infinity }, { seq: 100.5 }]), 100.5);
182
+ assert.equal(forkAnchorSeq([]), undefined);
183
+ assert.equal(forkAnchorSeq([null, {}, { seq: undefined }]), undefined);
184
+ });
@@ -0,0 +1,34 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { reasoningTarget } from '../src/client/reasoning-follow.js';
4
+
5
+ test('reasoning follows ordinary growth in two-line steps without passing the tail', () => {
6
+ assert.equal(reasoningTarget(0, 284, 224, 24), 48);
7
+ assert.equal(reasoningTarget(48, 284, 224, 24), 60);
8
+ assert.equal(reasoningTarget(0, 100, 224, 24), 0);
9
+ assert.equal(reasoningTarget(999, 284, 224, 24), 60);
10
+ });
11
+
12
+ test('a large reasoning burst still advances exactly two lines without skipping the middle', () => {
13
+ let top = 0;
14
+ for (const content of [400, 1200, 1500, 1500]) {
15
+ const target = reasoningTarget(top, content, 224, 24);
16
+ assert.equal(target - top, 48, 'a transport burst must not change the reference step distance');
17
+ assert.ok(target <= content - 224);
18
+ top = target;
19
+ }
20
+ while (top < 1500 - 224) {
21
+ const target = reasoningTarget(top, 1500, 224, 24);
22
+ assert.ok(target > top && target - top <= 48);
23
+ top = target;
24
+ }
25
+ assert.equal(top, 1500 - 224);
26
+ assert.equal(reasoningTarget(top, 1500, 224, 24), top, 'reaching the end must not replay the transcript');
27
+ });
28
+
29
+ test('two-line movement follows actual typography and clamps a resized viewport', () => {
30
+ assert.equal(reasoningTarget(28, 900, 192, 30), 88);
31
+ assert.equal(reasoningTarget(-50, 900, 192, 30), 60);
32
+ assert.equal(reasoningTarget(708, 900, 400, 30), 500);
33
+ assert.equal(reasoningTarget(0, 900, 192, 0), 2);
34
+ });
@@ -0,0 +1,63 @@
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 { en, zh } from '../src/client/settings-copy.ts';
7
+ import { CONVENTIONAL_SKILL_ROOTS, GENERATIVE_MCPAPPS_SKILL } from '../src/skill-status.ts';
8
+ import { shortestInstallCommand } from '../src/client/skill-status.ts';
9
+
10
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
+
12
+ /** User-facing surfaces must never print home/profile absolutes. */
13
+ const LEAKED_ABSOLUTE_PATH = /\/Users\/|[A-Za-z]:\\|(?:^|[\s"'`])\/home\/|\$HOME\b|\$DSH_HOME\b|~\//;
14
+
15
+ function installGuidanceSurface(): string {
16
+ return [
17
+ ...Object.values(en),
18
+ ...Object.values(zh),
19
+ shortestInstallCommand({
20
+ packPath: '/Users/cola/plugin/skills/generative-mcpapps',
21
+ roots: [
22
+ { source: 'user-dsh', path: '/Users/cola/.dsh/skills' },
23
+ { source: 'user-agents', path: 'C:\\Users\\cola\\.agents\\skills' },
24
+ ],
25
+ }),
26
+ ...CONVENTIONAL_SKILL_ROOTS,
27
+ ].join('\n');
28
+ }
29
+
30
+ test('install guidance names only relative skill roots', () => {
31
+ const surface = installGuidanceSurface();
32
+ assert.match(surface, /\.dsh\/skills/);
33
+ assert.match(surface, /\.agents\/skills/);
34
+ assert.match(surface, new RegExp(GENERATIVE_MCPAPPS_SKILL));
35
+ assert.doesNotMatch(surface, LEAKED_ABSOLUTE_PATH);
36
+ assert.doesNotMatch(surface, /\/Users\//);
37
+ assert.doesNotMatch(surface, /C:\\/);
38
+ });
39
+
40
+ test('Settings dictionaries mention conventional roots and the skill folder', () => {
41
+ for (const copy of [en, zh]) {
42
+ const blob = `${copy.skillInstall}\n${copy.skillUnavailable}`;
43
+ assert.match(blob, /\.dsh\/skills/);
44
+ assert.match(blob, /\.agents\/skills/);
45
+ assert.match(blob, /generative-mcpapps/);
46
+ assert.doesNotMatch(blob, LEAKED_ABSOLUTE_PATH);
47
+ }
48
+ });
49
+
50
+ test('committed Settings UI copy and command stay relative', () => {
51
+ const client = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
52
+ const section = readFileSync(resolve(root, 'src/client/SettingsSection.tsx'), 'utf8');
53
+ assert.match(section, /CONVENTIONAL_SKILL_ROOTS/);
54
+ assert.doesNotMatch(section, /root\.path/);
55
+ assert.doesNotMatch(section, /shortestInstallCommand\(skill\)/);
56
+ assert.match(client, /\.dsh\/skills/);
57
+ assert.match(client, /\.agents\/skills/);
58
+ assert.doesNotMatch(client, /mkdir -p "\$DSH_HOME\/skills"/);
59
+ assert.doesNotMatch(client, /status\.packPath \?\?/);
60
+ assert.doesNotMatch(client, /root\.source, ": ", root\.path/);
61
+ assert.doesNotMatch(client, /\/Users\//);
62
+ assert.doesNotMatch(client, /C:\\\\/);
63
+ });
@@ -0,0 +1,137 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import {
7
+ CONVENTIONAL_SKILL_ROOTS,
8
+ GENERATIVE_MCPAPPS_SKILL,
9
+ SKILL_PACK_RELATIVE,
10
+ skillListIncludes,
11
+ skillNameMatches,
12
+ skillRootEntryMatches,
13
+ skillsFromListResult,
14
+ toPublicSkillStatus,
15
+ } from '../src/skill-status.ts';
16
+ import {
17
+ projectSkillRoots,
18
+ rootHasGenerativeMcpapps,
19
+ scanGenerativeMcpappsStatus,
20
+ userSkillRoots,
21
+ } from '../src/skill-roots.ts';
22
+ import { detectGenerativeMcpappsSkill, firstSessionId, shortestInstallCommand } from '../src/client/skill-status.ts';
23
+
24
+ test('only the kebab-case skill name counts as installed', () => {
25
+ assert.equal(skillNameMatches(GENERATIVE_MCPAPPS_SKILL), true);
26
+ assert.equal(skillNameMatches('other'), false);
27
+ assert.equal(skillRootEntryMatches('generative-mcpapps'), true);
28
+ assert.equal(skillRootEntryMatches('generative-mcpapps.md'), true);
29
+ assert.equal(skillRootEntryMatches('skills'), false);
30
+ assert.equal(skillListIncludes([{ name: 'generative-mcpapps' }]), true);
31
+ assert.equal(skillListIncludes(skillsFromListResult({ ok: true, value: { skills: [{ name: 'generative-mcpapps' }] } })), true);
32
+ });
33
+
34
+ test('plugin-tree skills/ is not a harness skill root', () => {
35
+ const roots = [
36
+ ...projectSkillRoots('/plugin/dsh-better-display'),
37
+ ...userSkillRoots({ DSH_HOME: '/home/me/.dsh', DSH_AGENTS_HOME: '/home/me/.agents' }, '/home/me'),
38
+ ];
39
+ assert.deepEqual(roots.map(root => root.path), [
40
+ '/plugin/dsh-better-display/.dsh/skills',
41
+ '/plugin/dsh-better-display/.agents/skills',
42
+ '/home/me/.dsh/skills',
43
+ '/home/me/.agents/skills',
44
+ ]);
45
+ assert.equal(roots.some(root => root.path.endsWith('/dsh-better-display/skills')), false);
46
+ });
47
+
48
+ test('a real user skill root with the pack is installed; the plugin pack path is not', async () => {
49
+ const emptyHome = await mkdtemp(join(tmpdir(), 'dsh-bd-empty-'));
50
+ const skillHome = await mkdtemp(join(tmpdir(), 'dsh-bd-skill-'));
51
+ const userRoot = join(skillHome, '.dsh', 'skills', 'generative-mcpapps');
52
+ await mkdir(userRoot, { recursive: true });
53
+ await writeFile(join(userRoot, 'SKILL.md'), '---\nname: generative-mcpapps\ndescription: x\n---\n');
54
+
55
+ const missing = await scanGenerativeMcpappsStatus({
56
+ home: emptyHome,
57
+ env: {},
58
+ packPath: '/plugin/dsh-better-display/skills/generative-mcpapps',
59
+ });
60
+ const found = await scanGenerativeMcpappsStatus({
61
+ home: skillHome,
62
+ env: { DSH_HOME: join(skillHome, '.dsh') },
63
+ packPath: '/plugin/dsh-better-display/skills/generative-mcpapps',
64
+ });
65
+
66
+ assert.equal(await rootHasGenerativeMcpapps('/plugin/dsh-better-display/skills'), false);
67
+ assert.equal(missing.installed, false);
68
+ assert.equal(found.installed, true);
69
+ assert.equal(found.via, 'skill-root');
70
+ assert.equal(found.packPath, '/plugin/dsh-better-display/skills/generative-mcpapps');
71
+ });
72
+
73
+ test('client detect snapshot never carries host packPath or home roots', async () => {
74
+ const status = await detectGenerativeMcpappsSkill({
75
+ fetchHostStatus: async () => ({
76
+ name: 'generative-mcpapps',
77
+ installed: false,
78
+ via: null,
79
+ packPath: '/Users/cola/plugin/skills/generative-mcpapps',
80
+ roots: [{ source: 'user-dsh', path: '/Users/cola/.dsh/skills', present: false }],
81
+ }),
82
+ });
83
+ assert.equal(status.packPath, undefined);
84
+ assert.deepEqual(status.roots.map(root => root.path), [...CONVENTIONAL_SKILL_ROOTS]);
85
+ assert.equal(status.roots.some(root => /\/Users\/|[A-Za-z]:\\/.test(root.path)), false);
86
+ });
87
+
88
+ test('skills/list wins when the registry can see the skill', async () => {
89
+ const status = await detectGenerativeMcpappsSkill({
90
+ fetchHostStatus: async () => ({
91
+ name: 'generative-mcpapps',
92
+ installed: false,
93
+ via: null,
94
+ roots: [{ source: 'user-dsh', path: '/tmp/.dsh/skills', present: false }],
95
+ }),
96
+ listRemoteSkills: async () => [{ name: 'generative-mcpapps' }],
97
+ });
98
+ assert.equal(status.installed, true);
99
+ assert.equal(status.via, 'skills.list');
100
+ });
101
+
102
+ test('public skill status strips packPath and host home absolutes', () => {
103
+ const published = toPublicSkillStatus({
104
+ name: 'generative-mcpapps',
105
+ installed: false,
106
+ via: null,
107
+ packPath: '/Users/cola/.dsh/plugins/skills/generative-mcpapps',
108
+ roots: [
109
+ { source: 'user-dsh', path: '/Users/cola/.dsh/skills', present: false },
110
+ { source: 'user-agents', path: 'C:\\Users\\cola\\.agents\\skills', present: false },
111
+ ],
112
+ });
113
+ assert.equal(published.packPath, undefined);
114
+ assert.deepEqual(published.roots.map(root => root.path), [...CONVENTIONAL_SKILL_ROOTS]);
115
+ assert.equal(published.roots.some(root => /\/Users\/|[A-Za-z]:\\/.test(root.path)), false);
116
+ });
117
+
118
+ test('install command uses conventional relative roots only', () => {
119
+ const command = shortestInstallCommand({
120
+ packPath: '/opt/plugin/skills/generative-mcpapps',
121
+ roots: [{ source: 'user-dsh', path: '/home/me/.dsh/skills' }],
122
+ });
123
+ assert.match(command, /mkdir -p \.dsh\/skills/);
124
+ assert.match(command, new RegExp(`cp -R ${SKILL_PACK_RELATIVE.replace('/', '\\/')}`));
125
+ assert.match(command, /generative-mcpapps/);
126
+ assert.doesNotMatch(command, /\/Users\//);
127
+ assert.doesNotMatch(command, /[A-Za-z]:\\/);
128
+ assert.doesNotMatch(command, /\/home\//);
129
+ assert.doesNotMatch(command, /\$HOME|\$DSH_HOME/);
130
+ assert.doesNotMatch(command, /\/opt\//);
131
+ });
132
+
133
+ test('firstSessionId reads ids or byId keys', () => {
134
+ assert.equal(firstSessionId({ ids: ['a', 'b'] }), 'a');
135
+ assert.equal(firstSessionId({ byId: { z: {} } }), 'z');
136
+ assert.equal(firstSessionId(undefined), undefined);
137
+ });
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, 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 pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
9
+ main: string;
10
+ scripts?: { prepare?: string };
11
+ exports: Record<string, { default?: string } | string>;
12
+ files: string[];
13
+ dsh: { bundle?: { patch?: string } };
14
+ };
15
+
16
+ test('declares dsh.bundle.patch so official add joins the profile layer stack', () => {
17
+ assert.equal(pkg.dsh.bundle?.patch, './cordis.patch.yml');
18
+ assert.equal(existsSync(resolve(root, 'cordis.patch.yml')), true);
19
+ const patch = readFileSync(resolve(root, 'cordis.patch.yml'), 'utf8');
20
+ assert.match(patch, /id: dsh-better-display/);
21
+ assert.match(patch, /name: dsh-better-display/);
22
+ assert.equal(pkg.files.includes('cordis.patch.yml'), true);
23
+ });
24
+
25
+ test('commits compiled lib entries and does not require a prepare script', () => {
26
+ assert.equal(pkg.scripts?.prepare, undefined);
27
+ assert.equal(pkg.main, 'lib/dsh-better-display.js');
28
+ const client = pkg.exports['./client'];
29
+ assert.equal(typeof client === 'object' && client !== null ? client.default : client, './lib/client.js');
30
+ assert.equal(existsSync(resolve(root, 'lib/dsh-better-display.js')), true);
31
+ assert.equal(existsSync(resolve(root, 'lib/client.js')), true);
32
+ const clientJs = readFileSync(resolve(root, 'lib/client.js'), 'utf8');
33
+ assert.match(clientJs, /window\.__ModuleLoader__\.load/);
34
+ assert.match(clientJs, /id:\s*"dsh-better-display"/);
35
+ assert.match(clientJs, /settings\.section/);
36
+ assert.match(clientJs, /deliverableOpenMode/);
37
+ assert.match(clientJs, /frostedGlass/);
38
+ assert.match(clientJs, /foldIntensity/);
39
+ assert.match(clientJs, /setFrostedGlass/);
40
+ assert.match(clientJs, /setFoldIntensity/);
41
+ assert.match(clientJs, /data-reader-glass/);
42
+ assert.match(clientJs, /modeFromSnapshot/);
43
+ assert.match(clientJs, /str_replace_editor/);
44
+ assert.doesNotMatch(clientJs, /只折叠过程/);
45
+ assert.match(clientJs, /\.dsh\/skills/);
46
+ assert.doesNotMatch(clientJs, /submission\.images\.length/);
47
+ });
48
+
49
+ test('README leads with the official stock one-liner and names pnpm', () => {
50
+ for (const name of ['README.md', 'README.en.md']) {
51
+ const text = readFileSync(resolve(root, name), 'utf8');
52
+ assert.match(text, /dsh plugin --profile web add github:aa2246740\/dsh-better-display/);
53
+ assert.match(text, /pnpm/);
54
+ assert.doesNotMatch(text, /activate-new-client/);
55
+ assert.doesNotMatch(text, /my-plugins/);
56
+ assert.doesNotMatch(text, /DSHX_HARNESS/);
57
+ }
58
+ });
@@ -0,0 +1,102 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import { StreamBuffer, STREAM_TIMING } from '../src/client/stream-buffer.js';
4
+
5
+ test('a transport burst becomes multiple bounded, monotonically growing frames', () => {
6
+ const buffer = new StreamBuffer();
7
+ const text = '让新到的文字柔和显现,已经读过的内容保持稳定。'.repeat(6);
8
+ buffer.update(text, 0);
9
+ const frames: string[] = [];
10
+ let firstFrameAt: number | null = null;
11
+ // The reveal is paced by STREAM_TIMING, so the test asserts the invariants
12
+ // (many bounded frames, monotonic growth, full convergence in bounded time)
13
+ // rather than pinning numbers that a pacing change is expected to move.
14
+ for (let time = 16; time <= STREAM_TIMING.catchUpMs * 2; time += 16) {
15
+ const previous = buffer.visible;
16
+ if (buffer.advance(time)) { frames.push(buffer.visible); firstFrameAt ??= time; }
17
+ assert.ok(buffer.visible.startsWith(previous));
18
+ assert.ok(text.startsWith(buffer.visible));
19
+ }
20
+ assert.ok(frames.length > 8, `expected many reveal frames, got ${frames.length}`);
21
+ assert.ok(frames[0]!.length < text.length / 4);
22
+ assert.ok(firstFrameAt !== null && firstFrameAt <= 32, 'the first characters must appear immediately');
23
+ assert.equal(buffer.visible, text);
24
+ assert.equal(buffer.pending, false);
25
+ });
26
+
27
+ test('each received batch has a hard queue-age ceiling during continuing input', () => {
28
+ const buffer = new StreamBuffer();
29
+ const deliveries: { at: number; end: number }[] = [];
30
+ let text = '';
31
+ for (let time = 0; time <= 1200; time += 16) {
32
+ if (time % 64 === 0) {
33
+ text += '中文 English 输出片段,'.repeat(3);
34
+ buffer.update(text, time);
35
+ deliveries.push({ at: time, end: text.length });
36
+ }
37
+ buffer.advance(time);
38
+ for (const item of deliveries) if (time - item.at >= STREAM_TIMING.maxQueuedMs) assert.ok(buffer.visible.length >= item.end);
39
+ }
40
+ });
41
+
42
+ test('grapheme-safe updates do not split combined emoji, flags or accents', () => {
43
+ const buffer = new StreamBuffer();
44
+ const text = '你好👩🏽‍💻e\u0301🇨🇳👨‍👩‍👧‍👦完整。'.repeat(4);
45
+ const boundaries = new Set([...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(text)].map(part => part.index));
46
+ boundaries.add(text.length);
47
+ buffer.update(text, 0);
48
+ for (let time = 8; time <= 256; time += 8) {
49
+ buffer.advance(time);
50
+ assert.ok(boundaries.has(buffer.visible.length), `split at ${buffer.visible.length}`);
51
+ }
52
+ assert.equal(buffer.visible, text);
53
+ });
54
+
55
+ test('a surrogate pair split across transport deliveries is never exposed half-written', () => {
56
+ const buffer = new StreamBuffer();
57
+ buffer.update('你好\uD83D', 0);
58
+ buffer.advance(250);
59
+ assert.equal(buffer.visible, '你好');
60
+ buffer.update('你好\uD83D\uDE00继续', 260);
61
+ buffer.advance(520);
62
+ assert.equal(buffer.visible, '你好😀继续');
63
+ });
64
+
65
+ test('completion drains the last bytes within the short finish budget', () => {
66
+ const buffer = new StreamBuffer();
67
+ const text = '最终文本'.repeat(40);
68
+ buffer.update(text, 0);
69
+ buffer.advance(16);
70
+ buffer.update(text, 20, { finished: true });
71
+ buffer.advance(20 + STREAM_TIMING.finishMs);
72
+ assert.equal(buffer.visible, text);
73
+ });
74
+
75
+ test('cancel, reduced motion, selected text and hidden views share an immediate flush path', () => {
76
+ const buffer = new StreamBuffer();
77
+ buffer.update('所有已经收到的文字必须保留。'.repeat(20), 0);
78
+ buffer.advance(16);
79
+ buffer.update(buffer.target, 17, { immediate: true });
80
+ assert.equal(buffer.visible, buffer.target);
81
+ assert.equal(buffer.pending, false);
82
+ });
83
+
84
+ test('history is immediate and replacement/shrink does not replay an old answer', () => {
85
+ const buffer = new StreamBuffer('这是已完成的历史文本。');
86
+ assert.equal(buffer.visible, buffer.target);
87
+ buffer.update('修正后的权威文本。', 0);
88
+ assert.equal(buffer.visible, '修正后的权威文本。');
89
+ assert.equal(buffer.revision, 1);
90
+ buffer.update('修正。', 1);
91
+ assert.equal(buffer.visible, '修正。');
92
+ });
93
+
94
+ test('large payloads and delayed background frames do not create a typing backlog', () => {
95
+ const large = new StreamBuffer();
96
+ large.update('x'.repeat(12000), 0);
97
+ assert.equal(large.visible, large.target);
98
+ const delayed = new StreamBuffer();
99
+ delayed.update('一次长时间暂停后应立刻赶上全部已收到的内容。'.repeat(20), 0);
100
+ delayed.advance(10000);
101
+ assert.equal(delayed.visible, delayed.target);
102
+ });