@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,306 @@
1
+ import { useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
2
+ import { StreamMotionContext } from './streaming.js';
3
+ import type { ReactNode } from 'react';
4
+ import { REASON_CHASE_HOLD, REASON_HOLD, REASON_LINES, REASON_STEP, reasoningTarget } from './reasoning-follow.js';
5
+ import css from './Reader.module.css';
6
+
7
+ const EASING = 'cubic-bezier(.22,1,.36,1)';
8
+
9
+ /** One real transcript: reference transform while following, native scroll while reading. */
10
+ export function ReasoningCard({ children, step, active, motion, selected, onRead }: {
11
+ children: ReactNode; step: number; active: boolean; motion: boolean; selected: boolean; onRead: () => void;
12
+ }) {
13
+ const viewport = useRef<HTMLDivElement>(null);
14
+ const track = useRef<HTMLDivElement>(null);
15
+ const content = useRef<HTMLDivElement>(null);
16
+ const controls = useId();
17
+ const [expanded, setExpanded] = useState(false);
18
+ const [following, setFollowing] = useState(true);
19
+ const [overflow, setOverflow] = useState(false);
20
+ const [edges, setEdges] = useState('none');
21
+ const lastHeight = useRef(0);
22
+ const resize = useRef<Animation | null>(null);
23
+ const previousExpanded = useRef(expanded);
24
+ const stopFollow = useRef<() => void>(() => {});
25
+ const { paused = false } = useContext(StreamMotionContext);
26
+ const allowed = following && active && motion && !selected && !paused;
27
+
28
+ const pause = useCallback(() => {
29
+ stopFollow.current();
30
+ setFollowing(false);
31
+ onRead();
32
+ }, [onRead]);
33
+
34
+ useLayoutEffect(() => {
35
+ // The parent's selection observer can suspend this card before its own
36
+ // listener runs. Clearing a selection must not silently resume following.
37
+ if (!selected) return;
38
+ stopFollow.current();
39
+ setFollowing(false);
40
+ }, [selected]);
41
+
42
+ useLayoutEffect(() => {
43
+ if (!expanded) return;
44
+ const port = viewport.current;
45
+ const host = port?.closest<HTMLElement>('[data-conversation-scroll]');
46
+ if (!port || !host) return;
47
+ const fit = () => {
48
+ // Public DSH layout variable, updated by the native composer's observer.
49
+ // Leave room for the permanent identity, footer and reading margin.
50
+ const composer = parseFloat(getComputedStyle(host).getPropertyValue('--dsh-composer-height')) || 152;
51
+ const heading = port.parentElement?.querySelector<HTMLElement>('[data-reader-reasoning-heading]')?.offsetHeight || 30;
52
+ const footer = port.parentElement?.querySelector<HTMLElement>('[data-ud-check=reasoning-controls]')?.offsetHeight || 38;
53
+ const available = Math.max(120, host.clientHeight - composer - heading - footer - 32);
54
+ port.style.setProperty('--reason-reading-height', `${available}px`);
55
+ };
56
+ fit();
57
+ const observer = new ResizeObserver(fit); observer.observe(host);
58
+ return () => { observer.disconnect(); };
59
+ }, [expanded]);
60
+
61
+ useLayoutEffect(() => {
62
+ const port = viewport.current;
63
+ const text = content.current;
64
+ const scrollTrack = track.current;
65
+ if (!port || !text || !scrollTrack) return;
66
+ let frame = 0;
67
+ let timer: ReturnType<typeof setTimeout> | undefined;
68
+ let nextAt = performance.now() + REASON_HOLD;
69
+ let alive = true;
70
+ let automatic = false;
71
+ let lastPainted = port.scrollTop;
72
+ let targetOffset = lastPainted;
73
+ const tail = () => Math.max(0, text.offsetHeight - port.clientHeight);
74
+ const clamp = (value: number) => Math.max(0, Math.min(tail(), value));
75
+ const paintedOffset = () => {
76
+ if (!automatic) return port.scrollTop;
77
+ const transform = getComputedStyle(scrollTrack).transform;
78
+ // Reduced-motion CSS may win before React receives the media change.
79
+ // Retain the last painted position rather than snapping back to the top.
80
+ return clamp(transform === 'none' ? lastPainted : port.scrollTop - new DOMMatrixReadOnly(transform).m42);
81
+ };
82
+ const hasSelection = () => {
83
+ const selection = document.getSelection();
84
+ return !!selection && !selection.isCollapsed && !!selection.anchorNode && text.contains(selection.anchorNode);
85
+ };
86
+ const cancel = () => {
87
+ cancelAnimationFrame(frame); frame = 0;
88
+ clearTimeout(timer); timer = undefined;
89
+ delete port.dataset.reasoningMoving;
90
+ };
91
+ const manual = () => {
92
+ if (!automatic) return;
93
+ const top = paintedOffset();
94
+ automatic = false;
95
+ scrollTrack.style.transition = 'none';
96
+ scrollTrack.style.transform = 'none';
97
+ // Set both in the same layout effect/event, before a frame can paint.
98
+ // The first wheel gesture must already have a native scrollable viewport.
99
+ port.style.overflow = 'auto';
100
+ port.scrollTop = top;
101
+ lastPainted = port.scrollTop;
102
+ port.dataset.reasoningMode = 'manual';
103
+ };
104
+ const follow = () => {
105
+ if (automatic) return;
106
+ targetOffset = lastPainted = clamp(port.scrollTop);
107
+ scrollTrack.style.transition = 'none';
108
+ scrollTrack.style.transform = `translateY(-${lastPainted}px)`;
109
+ port.scrollTop = 0;
110
+ port.style.overflow = 'hidden';
111
+ automatic = true;
112
+ port.dataset.reasoningMode = 'transform';
113
+ };
114
+ const measure = (recordHeight = true) => {
115
+ const previewHeight = parseFloat(getComputedStyle(port).getPropertyValue('--reason-preview-height'));
116
+ setOverflow(text.offsetHeight > previewHeight + 1);
117
+ lastPainted = paintedOffset();
118
+ const top = lastPainted > 1;
119
+ const bottom = tail() - lastPainted > 1;
120
+ const next = top ? bottom ? 'both' : 'top' : bottom ? 'bottom' : 'none';
121
+ setEdges(value => value === next ? value : next);
122
+ if (recordHeight && !resize.current) lastHeight.current = port.clientHeight;
123
+ };
124
+ stopFollow.current = () => { cancel(); manual(); measure(false); };
125
+ const canFollow = () => allowed && alive && !document.hidden && !hasSelection();
126
+ const schedule = () => {
127
+ if (!canFollow() || frame || timer !== undefined) return;
128
+ follow();
129
+ if (tail() - paintedOffset() < 1) return;
130
+ timer = setTimeout(start, Math.max(0, nextAt - performance.now()));
131
+ };
132
+ const start = () => {
133
+ timer = undefined;
134
+ if (!canFollow()) return;
135
+ const from = paintedOffset();
136
+ const lineHeight = parseFloat(getComputedStyle(text).lineHeight) || 24;
137
+ // How much unpainted text is waiting. A fast model pushes this far past the
138
+ // reference step, and chasing it is what keeps the scroll continuous.
139
+ const backlogLines = Math.max(0, (tail() - from) / lineHeight);
140
+ const target = reasoningTarget(from, text.offsetHeight, port.clientHeight, lineHeight, backlogLines);
141
+ if (target - from < 1) return;
142
+ const began = performance.now();
143
+ // Still behind after this step: take the next one almost immediately instead
144
+ // of holding the reading beat, so motion tracks the text rather than stalling.
145
+ nextAt = began + (backlogLines > REASON_LINES ? REASON_CHASE_HOLD : REASON_HOLD);
146
+ targetOffset = target;
147
+ // The supplied recipe: commit the start pose, then transition the track.
148
+ // No per-frame scrollTop writes and no catch-up across unread lines.
149
+ scrollTrack.style.transition = 'none';
150
+ scrollTrack.style.transform = `translateY(-${from}px)`;
151
+ void scrollTrack.offsetHeight;
152
+ scrollTrack.style.transition = 'transform var(--reason-step) var(--reason-ease)';
153
+ scrollTrack.style.transform = `translateY(-${target}px)`;
154
+ port.dataset.reasoningMoving = 'true';
155
+ port.dataset.reasoningFrom = String(from);
156
+ port.dataset.reasoningTarget = String(target);
157
+ port.dataset.reasoningBegan = String(began);
158
+ const tick = (now: number) => {
159
+ frame = 0;
160
+ if (!canFollow()) { cancel(); manual(); return; }
161
+ // Observe the browser's actual CSS interpolation for masks and handoff.
162
+ measure();
163
+ if (now - began < REASON_STEP || Math.abs(lastPainted - target) > .05) frame = requestAnimationFrame(tick);
164
+ else { delete port.dataset.reasoningMoving; schedule(); }
165
+ };
166
+ frame = requestAnimationFrame(tick);
167
+ };
168
+ const onScroll = () => {
169
+ measure();
170
+ // Focus/keyboard-induced native scroll wins even while the track moves.
171
+ if (automatic && port.scrollTop > 1) pause();
172
+ };
173
+ const onWheel = (event: WheelEvent) => {
174
+ if (!event.deltaY) return;
175
+ // The compositor picks a wheel scroller before handlers run. A clipped
176
+ // transform viewport would otherwise lose this first gesture or scroll
177
+ // the conversation. Consume only this handoff; later wheels stay native.
178
+ const handoff = automatic && event.cancelable;
179
+ if (handoff) event.preventDefault();
180
+ const unit = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? parseFloat(getComputedStyle(text).lineHeight) || 24
181
+ : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? port.clientHeight : 1;
182
+ pause();
183
+ if (handoff) { port.scrollTop = clamp(port.scrollTop + event.deltaY * unit); measure(); }
184
+ };
185
+ const onSelection = () => { if (hasSelection()) pause(); };
186
+ const onVisibility = () => {
187
+ cancel(); manual();
188
+ nextAt = performance.now() + REASON_HOLD;
189
+ if (!document.hidden) schedule();
190
+ };
191
+ const observer = new ResizeObserver(() => {
192
+ if (automatic && targetOffset > tail() + 1) {
193
+ cancel(); manual();
194
+ nextAt = performance.now() + REASON_HOLD;
195
+ }
196
+ measure(); schedule();
197
+ });
198
+ observer.observe(text); observer.observe(port);
199
+ port.addEventListener('scroll', onScroll, { passive: true });
200
+ port.addEventListener('wheel', onWheel, { passive: false });
201
+ document.addEventListener('selectionchange', onSelection);
202
+ document.addEventListener('visibilitychange', onVisibility);
203
+ // Keep the previous layout height until the resize effect can sample it.
204
+ measure(false); schedule();
205
+ return () => {
206
+ alive = false; cancel(); manual(); observer.disconnect();
207
+ port.removeEventListener('scroll', onScroll); port.removeEventListener('wheel', onWheel);
208
+ document.removeEventListener('selectionchange', onSelection);
209
+ document.removeEventListener('visibilitychange', onVisibility);
210
+ stopFollow.current = () => {};
211
+ };
212
+ }, [allowed, pause]);
213
+
214
+ useLayoutEffect(() => {
215
+ const port = viewport.current;
216
+ if (!port) return;
217
+ const changed = expanded !== previousExpanded.current;
218
+ previousExpanded.current = expanded;
219
+ const from = resize.current ? port.clientHeight : lastHeight.current;
220
+ resize.current?.cancel(); resize.current = null;
221
+ port.style.maxHeight = ''; port.style.height = '';
222
+ const target = port.clientHeight;
223
+ // Explicit reading adjusts only the conversation, never the outer app.
224
+ // The card is also a reader anchor so resizing cannot pin a later answer
225
+ // in place at the expense of this card's visible heading.
226
+ if (changed && !selected) {
227
+ const card = port.parentElement;
228
+ const host = port.closest<HTMLElement>('[data-conversation-scroll]');
229
+ if (card && host) {
230
+ const composer = parseFloat(getComputedStyle(host).getPropertyValue('--dsh-composer-height')) || 152;
231
+ const region = host.getBoundingClientRect();
232
+ const box = card.getBoundingClientRect();
233
+ const top = region.top + 16;
234
+ const bottom = region.bottom - composer - 16;
235
+ const delta = box.top < top ? box.top - top : box.bottom > bottom ? Math.min(box.bottom - bottom, box.top - top) : 0;
236
+ if (Math.abs(delta) > 1) host.scrollTo({ top: Math.max(0, host.scrollTop + delta), behavior: motion ? 'smooth' : 'instant' });
237
+ }
238
+ }
239
+ if (!changed || !motion || from < 1 || Math.abs(target - from) < 1) {
240
+ lastHeight.current = target;
241
+ return;
242
+ }
243
+ // max-height otherwise clamps the very first collapse frame to the new cap.
244
+ port.style.maxHeight = 'none';
245
+ const animation = port.animate([{ height: `${from}px` }, { height: `${target}px` }], { duration: 300, easing: EASING, fill: 'both' });
246
+ resize.current = animation;
247
+ const restore = () => {
248
+ // `max-height` was cleared so the first frame could exceed the old cap; it has
249
+ // to come back even if the animation never reports finishing, or the card keeps
250
+ // the inline styles the animation was driving.
251
+ port.style.maxHeight = ''; port.style.height = '';
252
+ lastHeight.current = port.clientHeight;
253
+ };
254
+ let settled = false;
255
+ const settle = () => {
256
+ if (settled) return;
257
+ settled = true;
258
+ if (resize.current === animation) resize.current = null;
259
+ animation.cancel();
260
+ restore();
261
+ };
262
+ animation.onfinish = settle;
263
+ // `fill: 'both'` holds whichever keyframe the animation stopped on, so a run that
264
+ // never finishes — cancelled by a re-run, unmounted, or skipped by the compositor —
265
+ // would leave the card at the previous height with its inline styles still
266
+ // overridden. Restore on a deadline too.
267
+ const deadline = window.setTimeout(settle, 300 + 240);
268
+ return () => { window.clearTimeout(deadline); };
269
+ }, [expanded, motion, selected]);
270
+ useEffect(() => () => resize.current?.cancel(), []);
271
+
272
+ const toggleReading = () => {
273
+ // Resize the same transcript without changing follow intent or position.
274
+ // Wheel, selection and viewport focus still explicitly pause following.
275
+ onRead();
276
+ setExpanded(value => !value);
277
+ };
278
+
279
+ return <div className={css.reasonCard} data-reader-reasoning-card data-reader-anchor data-expanded={expanded} data-following={allowed} data-overflow={overflow} data-ud-motion="reader-reasoning-size">
280
+ <div className={css.reasonHeading} data-reader-reasoning-heading data-ud-check="reasoning-identity">
281
+ <span className={css.reasonLabel} data-reader-reasoning-label>思考</span>
282
+ <span>步骤 {step}</span>
283
+ </div>
284
+ <div ref={viewport} id={controls} className={css.reasonViewport} data-reader-reasoning-scroll data-edges={edges}
285
+ data-ud-motion="reader-reasoning-scroll" role="region" aria-label={`步骤 ${step} 的思考${overflow ? ',可滚动阅读' : ''}`}
286
+ tabIndex={overflow ? 0 : undefined} onPointerDown={pause} onFocus={pause}>
287
+ <div ref={track} className={css.reasonTrack} data-reader-reasoning-track>
288
+ <div ref={content} className={css.reasonText} data-reader-reasoning-text>{children}</div>
289
+ </div>
290
+ </div>
291
+ {(overflow || expanded) && <div className={css.reasonFooter} data-ud-check="reasoning-controls">
292
+ {active && motion ? <button type="button" className={css.reasonAction} disabled={selected} aria-controls={controls}
293
+ aria-label={following ? '暂停自动跟随思考' : '继续跟随最新思考'}
294
+ title={selected ? '取消文字选择后可继续跟随' : undefined}
295
+ onClick={() => { if (following) pause(); else { onRead(); setFollowing(true); } }}>
296
+ <svg width="12" height="12" viewBox="0 0 16 16" aria-hidden="true">{following ? <path d="M5.5 4v8m5-8v8" /> : <path d="M8 3v10m-4-4 4 4 4-4" />}</svg>
297
+ {following ? '暂停跟随' : '跟随最新'}
298
+ </button> : <span className={css.reasonCaption}>{expanded ? '手动阅读' : '可滚动阅读'}</span>}
299
+ <button type="button" className={css.reasonAction} aria-expanded={expanded} aria-controls={controls}
300
+ aria-label={expanded ? '收起完整思考' : '展开阅读完整思考'} onClick={toggleReading}>
301
+ {expanded ? '收起' : '展开阅读'}
302
+ <svg width="12" height="12" viewBox="0 0 16 16" aria-hidden="true">{expanded ? <path d="m4 10 4-4 4 4" /> : <path d="m4 6 4 4 4-4" />}</svg>
303
+ </button>
304
+ </div>}
305
+ </div>;
306
+ }
@@ -0,0 +1,184 @@
1
+ .section {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 28px;
5
+ width: 100%;
6
+ max-width: 720px;
7
+ }
8
+
9
+ .row {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 8px;
13
+ padding: 16px 0;
14
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
15
+ }
16
+
17
+ .rowText {
18
+ flex: 1;
19
+ min-width: 0;
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 4px;
23
+ padding-right: 24px;
24
+ }
25
+
26
+ .title {
27
+ font-size: 14px;
28
+ font-weight: 400;
29
+ line-height: 22px;
30
+ color: var(--dsw-alias-label-primary);
31
+ }
32
+
33
+ .desc {
34
+ font-size: 12px;
35
+ font-weight: 400;
36
+ line-height: 18px;
37
+ color: var(--dsw-alias-label-tertiary);
38
+ }
39
+
40
+ .switch {
41
+ flex: none;
42
+ width: 40px;
43
+ height: 24px;
44
+ padding: 0;
45
+ border: none;
46
+ border-radius: 12px;
47
+ background: var(--dsw-alias-bg-module-platform);
48
+ position: relative;
49
+ cursor: pointer;
50
+ }
51
+
52
+ .switch::after {
53
+ content: '';
54
+ position: absolute;
55
+ top: 3px;
56
+ left: 3px;
57
+ width: 18px;
58
+ height: 18px;
59
+ border-radius: 50%;
60
+ background: var(--dsw-alias-label-primary);
61
+ transition: transform 160ms ease;
62
+ }
63
+
64
+ .switch[data-on]::after {
65
+ transform: translateX(16px);
66
+ }
67
+
68
+ .switch[data-on] {
69
+ background: var(--dsw-alias-state-business-primary);
70
+ }
71
+
72
+ .block {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 10px;
76
+ }
77
+
78
+ .status {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 8px;
82
+ font-size: 13px;
83
+ line-height: 20px;
84
+ color: var(--dsw-alias-label-secondary);
85
+ }
86
+
87
+ .badge {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ height: 22px;
91
+ padding: 0 8px;
92
+ border-radius: 11px;
93
+ background: var(--dsw-alias-bg-module-platform);
94
+ color: var(--dsw-alias-label-secondary);
95
+ font-size: 12px;
96
+ }
97
+
98
+ .badge[data-ok] {
99
+ background: rgba(16, 185, 129, 0.14);
100
+ color: #059669;
101
+ }
102
+
103
+ .pre {
104
+ margin: 0;
105
+ padding: 10px 12px;
106
+ border-radius: 8px;
107
+ background: var(--dsw-alias-bg-module-platform);
108
+ color: var(--dsw-alias-label-secondary);
109
+ font-size: 12px;
110
+ line-height: 18px;
111
+ overflow-wrap: anywhere;
112
+ white-space: pre-wrap;
113
+ }
114
+
115
+ .roots {
116
+ margin: 0;
117
+ padding-left: 18px;
118
+ color: var(--dsw-alias-label-tertiary);
119
+ font-size: 12px;
120
+ line-height: 18px;
121
+ }
122
+
123
+ .actions {
124
+ display: flex;
125
+ gap: 8px;
126
+ }
127
+
128
+ .button {
129
+ height: 32px;
130
+ padding: 0 12px;
131
+ border: 0.5px solid var(--dsw-alias-border-l2);
132
+ border-radius: 8px;
133
+ background: var(--dsw-alias-bg-module-platform);
134
+ color: var(--dsw-alias-label-primary);
135
+ font: inherit;
136
+ font-size: 13px;
137
+ cursor: pointer;
138
+ }
139
+
140
+ .button:hover {
141
+ background: var(--dsw-alias-interactive-bg-hover);
142
+ }
143
+
144
+ .button:disabled {
145
+ opacity: 0.6;
146
+ cursor: default;
147
+ }
148
+
149
+ .segment {
150
+ display: flex;
151
+ width: 100%;
152
+ margin-top: 8px;
153
+ padding: 2px;
154
+ border-radius: 10px;
155
+ background: var(--dsw-alias-bg-module-platform);
156
+ gap: 2px;
157
+ }
158
+
159
+ .segmentStop {
160
+ flex: 1 1 0;
161
+ min-width: 0;
162
+ height: 32px;
163
+ padding: 0 8px;
164
+ border: 0;
165
+ border-radius: 8px;
166
+ background: transparent;
167
+ color: var(--dsw-alias-label-secondary);
168
+ font: inherit;
169
+ font-size: 12px;
170
+ line-height: 18px;
171
+ cursor: pointer;
172
+ }
173
+
174
+ .segmentStop[data-on] {
175
+ background: var(--dsw-alias-bg-base);
176
+ color: var(--dsw-alias-label-primary);
177
+ box-shadow: 0 0 0 0.5px var(--dsw-alias-border-l2);
178
+ }
179
+
180
+ .slider {
181
+ width: 100%;
182
+ margin: 10px 0 0;
183
+ accent-color: var(--dsw-alias-state-business-primary);
184
+ }
@@ -0,0 +1,194 @@
1
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
2
+ import { bubblesOf, foldIntensityOf, frostedGlassOf, type FoldIntensity } from './fold-intensity.js';
3
+ import { deliverableOpenModeOf, type DeliverableOpenMode } from './open-file.js';
4
+ import { settingsCopyFor, type SettingsCopy, type SettingsCopyKey } from './settings-copy.js';
5
+ import { CONVENTIONAL_SKILL_ROOTS, detectGenerativeMcpappsSkill, shortestInstallCommand, type SkillStatusProbe, type SkillStatusSnapshot } from './skill-status.js';
6
+ import css from './SettingsSection.module.css';
7
+
8
+ export interface ReaderPrefsSnapshot {
9
+ deliverableOpenMode?: DeliverableOpenMode;
10
+ frostedGlass?: boolean;
11
+ bubbles?: boolean;
12
+ foldIntensity?: FoldIntensity;
13
+ autoFold?: boolean;
14
+ processOnly?: boolean;
15
+ }
16
+
17
+ export interface OpenPrefs {
18
+ getSnapshot: () => ReaderPrefsSnapshot;
19
+ subscribe: (fn: () => void) => () => void;
20
+ actions: {
21
+ setDeliverableOpenMode: (value: DeliverableOpenMode) => void;
22
+ setFrostedGlass: (value: boolean) => void;
23
+ setBubbles?: (value: boolean) => void;
24
+ setFoldIntensity?: (value: FoldIntensity) => void;
25
+ setAutoFold?: (value: boolean) => void;
26
+ };
27
+ }
28
+
29
+ export interface BetterDisplaySettingsInjected {
30
+ prefs: OpenPrefs;
31
+ copy?: SettingsCopy;
32
+ languageTag?: string;
33
+ checkSkill: SkillStatusProbe;
34
+ }
35
+
36
+ type SettingsProps = BetterDisplaySettingsInjected & {
37
+ /** Official settings.section owner share; unused here. */
38
+ close?: () => void;
39
+ t?: (key: SettingsCopyKey) => string;
40
+ };
41
+
42
+ function text(props: SettingsProps, copy: SettingsCopy, key: keyof SettingsCopy): string {
43
+ if (typeof props.t === 'function') {
44
+ try {
45
+ const value = props.t(key);
46
+ if (typeof value === 'string' && value.length > 0 && value !== key) return value;
47
+ } catch {
48
+ // Fall through to the bundled dictionaries.
49
+ }
50
+ }
51
+ return copy[key];
52
+ }
53
+
54
+ const FOLD_STOPS: { value: FoldIntensity; key: 'foldNone' | 'foldStandard' | 'foldSummary' }[] = [
55
+ { value: 0, key: 'foldNone' },
56
+ { value: 1, key: 'foldStandard' },
57
+ { value: 2, key: 'foldSummary' },
58
+ ];
59
+
60
+ export function SettingsSection(props: SettingsProps) {
61
+ const copy = props.copy ?? settingsCopyFor(props.languageTag);
62
+ const snap = useSyncExternalStore(
63
+ props.prefs.subscribe,
64
+ () => props.prefs.getSnapshot() ?? ({} as ReaderPrefsSnapshot),
65
+ () => ({} as ReaderPrefsSnapshot),
66
+ );
67
+ const mode = deliverableOpenModeOf(snap.deliverableOpenMode);
68
+ const glass = frostedGlassOf(snap);
69
+ const bubbles = bubblesOf(snap);
70
+ const autoFold = snap.autoFold !== false && snap.foldIntensity !== 0;
71
+ const setMode = (value: DeliverableOpenMode) => {
72
+ props.prefs.actions.setDeliverableOpenMode(value);
73
+ };
74
+ const on = mode === 'sidebar';
75
+ const [skill, setSkill] = useState<SkillStatusSnapshot | undefined>();
76
+ const [checking, setChecking] = useState(false);
77
+
78
+ const recheck = useCallback(async () => {
79
+ setChecking(true);
80
+ try {
81
+ setSkill(await detectGenerativeMcpappsSkill(props.checkSkill));
82
+ } catch {
83
+ setSkill({
84
+ name: 'generative-mcpapps',
85
+ installed: false,
86
+ via: null,
87
+ roots: [],
88
+ hostReached: false,
89
+ });
90
+ } finally {
91
+ setChecking(false);
92
+ }
93
+ }, [props.checkSkill]);
94
+
95
+ useEffect(() => { void recheck(); }, [recheck]);
96
+
97
+ return (
98
+ <div className={css.section} data-better-display-settings>
99
+ <div className={css.row}>
100
+ <div className={css.rowText}>
101
+ <div className={css.title}>{text(props, copy, 'openTitle')}</div>
102
+ <div className={css.desc}>{text(props, copy, 'openDescription')}</div>
103
+ </div>
104
+ <button
105
+ type="button"
106
+ role="switch"
107
+ aria-checked={on}
108
+ className={css.switch}
109
+ data-on={on || undefined}
110
+ data-better-display-open-mode={mode}
111
+ onClick={() => { setMode(on ? 'external' : 'sidebar'); }}
112
+ />
113
+ </div>
114
+
115
+ <div className={css.row}>
116
+ <div className={css.rowText}>
117
+ <div className={css.title}>{text(props, copy, 'glassTitle')}</div>
118
+ <div className={css.desc}>{text(props, copy, 'glassDescription')}</div>
119
+ </div>
120
+ <button
121
+ type="button"
122
+ role="switch"
123
+ aria-checked={glass}
124
+ className={css.switch}
125
+ data-on={glass || undefined}
126
+ data-better-display-glass={glass ? 'on' : 'off'}
127
+ onClick={() => { props.prefs.actions.setFrostedGlass(!glass); }}
128
+ />
129
+ </div>
130
+
131
+ <div className={css.row}>
132
+ <div className={css.rowText}>
133
+ <div className={css.title}>{text(props, copy, 'bubbleTitle')}</div>
134
+ <div className={css.desc}>{text(props, copy, 'bubbleDescription')}</div>
135
+ </div>
136
+ <button
137
+ type="button"
138
+ role="switch"
139
+ aria-checked={bubbles}
140
+ className={css.switch}
141
+ data-on={bubbles || undefined}
142
+ data-better-display-bubbles={bubbles ? 'on' : 'off'}
143
+ onClick={() => { props.prefs.actions.setBubbles?.(!bubbles); }}
144
+ />
145
+ </div>
146
+
147
+ <div className={css.row}>
148
+ <div className={css.rowText}>
149
+ <div className={css.title}>{text(props, copy, 'foldTitle')}</div>
150
+ <div className={css.desc}>{text(props, copy, 'foldDescription')}</div>
151
+ </div>
152
+ <button
153
+ type="button"
154
+ role="switch"
155
+ aria-checked={autoFold}
156
+ className={css.switch}
157
+ data-on={autoFold || undefined}
158
+ data-better-display-auto-fold={autoFold ? 'on' : 'off'}
159
+ onClick={() => {
160
+ props.prefs.actions.setAutoFold?.(!autoFold);
161
+ props.prefs.actions.setFoldIntensity?.(!autoFold ? 1 : 0);
162
+ }}
163
+ />
164
+ </div>
165
+
166
+ <section className={css.block} data-better-display-skill={skill?.installed ? 'installed' : 'missing'}>
167
+ <div className={css.title}>{text(props, copy, 'skillTitle')}</div>
168
+ <div className={css.status}>
169
+ <span className={css.badge} data-ok={skill?.installed || undefined}>
170
+ {skill?.installed ? text(props, copy, 'skillInstalled') : text(props, copy, 'skillMissing')}
171
+ </span>
172
+ </div>
173
+ <p className={css.desc}>{text(props, copy, 'skillPurpose')}</p>
174
+ <p className={css.desc}>{text(props, copy, 'skillPluginNote')}</p>
175
+ {skill && !skill.installed ? (
176
+ <>
177
+ <p className={css.desc}>{skill.hostReached ? text(props, copy, 'skillInstall') : text(props, copy, 'skillUnavailable')}</p>
178
+ <pre className={css.pre}>{shortestInstallCommand()}</pre>
179
+ <ul className={css.roots} data-better-display-skill-roots>
180
+ {CONVENTIONAL_SKILL_ROOTS.map(root => (
181
+ <li key={root}>{root}</li>
182
+ ))}
183
+ </ul>
184
+ </>
185
+ ) : null}
186
+ <div className={css.actions}>
187
+ <button type="button" className={css.button} disabled={checking} onClick={() => { void recheck(); }}>
188
+ {checking ? text(props, copy, 'skillChecking') : text(props, copy, 'skillRecheck')}
189
+ </button>
190
+ </div>
191
+ </section>
192
+ </div>
193
+ );
194
+ }