dsh-better-display 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +41 -0
  5. package/README.md +41 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/cordis.patch.yml +5 -0
  8. package/cordis.yml +4 -0
  9. package/dshx.yml +5 -0
  10. package/lib/client.js +49923 -0
  11. package/lib/client.js.map +1 -0
  12. package/lib/dsh-better-display.js +256 -0
  13. package/lib/types/client/Blocks.d.ts +45 -0
  14. package/lib/types/client/Blocks.d.ts.map +1 -0
  15. package/lib/types/client/Blocks.js +136 -0
  16. package/lib/types/client/Blocks.js.map +1 -0
  17. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  18. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.js +170 -0
  20. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  21. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  22. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.js +15 -0
  24. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  25. package/lib/types/client/LiveFold.d.ts +10 -0
  26. package/lib/types/client/LiveFold.d.ts.map +1 -0
  27. package/lib/types/client/LiveFold.js +39 -0
  28. package/lib/types/client/LiveFold.js.map +1 -0
  29. package/lib/types/client/McpAppFrame.d.ts +32 -0
  30. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  31. package/lib/types/client/McpAppFrame.js +193 -0
  32. package/lib/types/client/McpAppFrame.js.map +1 -0
  33. package/lib/types/client/Reader.d.ts +3 -0
  34. package/lib/types/client/Reader.d.ts.map +1 -0
  35. package/lib/types/client/Reader.js +537 -0
  36. package/lib/types/client/Reader.js.map +1 -0
  37. package/lib/types/client/ReasoningCard.d.ts +11 -0
  38. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  39. package/lib/types/client/ReasoningCard.js +301 -0
  40. package/lib/types/client/ReasoningCard.js.map +1 -0
  41. package/lib/types/client/StickyLane.d.ts +8 -0
  42. package/lib/types/client/StickyLane.d.ts.map +1 -0
  43. package/lib/types/client/StickyLane.js +28 -0
  44. package/lib/types/client/StickyLane.js.map +1 -0
  45. package/lib/types/client/TimelineRail.d.ts +10 -0
  46. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  47. package/lib/types/client/TimelineRail.js +168 -0
  48. package/lib/types/client/TimelineRail.js.map +1 -0
  49. package/lib/types/client/ToolActivity.d.ts +17 -0
  50. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  51. package/lib/types/client/ToolActivity.js +192 -0
  52. package/lib/types/client/ToolActivity.js.map +1 -0
  53. package/lib/types/client/TurnMetrics.d.ts +11 -0
  54. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  55. package/lib/types/client/TurnMetrics.js +44 -0
  56. package/lib/types/client/TurnMetrics.js.map +1 -0
  57. package/lib/types/client/WaitingStatus.d.ts +6 -0
  58. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  59. package/lib/types/client/WaitingStatus.js +19 -0
  60. package/lib/types/client/WaitingStatus.js.map +1 -0
  61. package/lib/types/client/conversation-scroll.d.ts +19 -0
  62. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  63. package/lib/types/client/conversation-scroll.js +25 -0
  64. package/lib/types/client/conversation-scroll.js.map +1 -0
  65. package/lib/types/client/deliverables.d.ts +24 -0
  66. package/lib/types/client/deliverables.d.ts.map +1 -0
  67. package/lib/types/client/deliverables.js +104 -0
  68. package/lib/types/client/deliverables.js.map +1 -0
  69. package/lib/types/client/entry-policy.d.ts +10 -0
  70. package/lib/types/client/entry-policy.d.ts.map +1 -0
  71. package/lib/types/client/entry-policy.js +24 -0
  72. package/lib/types/client/entry-policy.js.map +1 -0
  73. package/lib/types/client/entry.d.ts +4 -0
  74. package/lib/types/client/entry.d.ts.map +1 -0
  75. package/lib/types/client/entry.js +34 -0
  76. package/lib/types/client/entry.js.map +1 -0
  77. package/lib/types/client/fold-choreography.d.ts +28 -0
  78. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  79. package/lib/types/client/fold-choreography.js +31 -0
  80. package/lib/types/client/fold-choreography.js.map +1 -0
  81. package/lib/types/client/index.d.ts +7 -0
  82. package/lib/types/client/index.d.ts.map +1 -0
  83. package/lib/types/client/index.js +153 -0
  84. package/lib/types/client/index.js.map +1 -0
  85. package/lib/types/client/live-turn.d.ts +58 -0
  86. package/lib/types/client/live-turn.d.ts.map +1 -0
  87. package/lib/types/client/live-turn.js +144 -0
  88. package/lib/types/client/live-turn.js.map +1 -0
  89. package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
  90. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  91. package/lib/types/client/markdown/MarkdownText.js +171 -0
  92. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  93. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  94. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  95. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  96. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  97. package/lib/types/client/markdown/incremental.d.ts +69 -0
  98. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  99. package/lib/types/client/markdown/incremental.js +108 -0
  100. package/lib/types/client/markdown/incremental.js.map +1 -0
  101. package/lib/types/client/markdown/katex.d.ts +27 -0
  102. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  103. package/lib/types/client/markdown/katex.js +88 -0
  104. package/lib/types/client/markdown/katex.js.map +1 -0
  105. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  106. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  107. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  108. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  109. package/lib/types/client/markdown/parse.d.ts +25 -0
  110. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  111. package/lib/types/client/markdown/parse.js +42 -0
  112. package/lib/types/client/markdown/parse.js.map +1 -0
  113. package/lib/types/client/markdown/render.d.ts +119 -0
  114. package/lib/types/client/markdown/render.d.ts.map +1 -0
  115. package/lib/types/client/markdown/render.js +435 -0
  116. package/lib/types/client/markdown/render.js.map +1 -0
  117. package/lib/types/client/mcp-app.d.ts +50 -0
  118. package/lib/types/client/mcp-app.d.ts.map +1 -0
  119. package/lib/types/client/mcp-app.js +328 -0
  120. package/lib/types/client/mcp-app.js.map +1 -0
  121. package/lib/types/client/message-chrome.d.ts +19 -0
  122. package/lib/types/client/message-chrome.d.ts.map +1 -0
  123. package/lib/types/client/message-chrome.js +42 -0
  124. package/lib/types/client/message-chrome.js.map +1 -0
  125. package/lib/types/client/motion.d.ts +38 -0
  126. package/lib/types/client/motion.d.ts.map +1 -0
  127. package/lib/types/client/motion.js +367 -0
  128. package/lib/types/client/motion.js.map +1 -0
  129. package/lib/types/client/native/ContextBody.d.ts +130 -0
  130. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  131. package/lib/types/client/native/ContextBody.js +398 -0
  132. package/lib/types/client/native/ContextBody.js.map +1 -0
  133. package/lib/types/client/native/ContextInjectionRow.d.ts +26 -0
  134. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  135. package/lib/types/client/native/ContextInjectionRow.js +27 -0
  136. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  137. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  138. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  139. package/lib/types/client/native/ReferenceIcon.js +16 -0
  140. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  141. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  142. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  143. package/lib/types/client/native/tool-call-model.js +219 -0
  144. package/lib/types/client/native/tool-call-model.js.map +1 -0
  145. package/lib/types/client/open-file.d.ts +24 -0
  146. package/lib/types/client/primitive-labels.d.ts +10 -0
  147. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  148. package/lib/types/client/primitive-labels.js +37 -0
  149. package/lib/types/client/primitive-labels.js.map +1 -0
  150. package/lib/types/client/projection.d.ts +41 -0
  151. package/lib/types/client/projection.d.ts.map +1 -0
  152. package/lib/types/client/projection.js +112 -0
  153. package/lib/types/client/projection.js.map +1 -0
  154. package/lib/types/client/reasoning-follow.d.ts +6 -0
  155. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  156. package/lib/types/client/reasoning-follow.js +12 -0
  157. package/lib/types/client/reasoning-follow.js.map +1 -0
  158. package/lib/types/client/settings-copy.d.ts +6 -0
  159. package/lib/types/client/settings-slots.d.ts +16 -0
  160. package/lib/types/client/store.d.ts +17 -0
  161. package/lib/types/client/store.d.ts.map +1 -0
  162. package/lib/types/client/store.js +13 -0
  163. package/lib/types/client/store.js.map +1 -0
  164. package/lib/types/client/stream-buffer.d.ts +29 -0
  165. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  166. package/lib/types/client/stream-buffer.js +113 -0
  167. package/lib/types/client/stream-buffer.js.map +1 -0
  168. package/lib/types/client/streaming.d.ts +18 -0
  169. package/lib/types/client/streaming.d.ts.map +1 -0
  170. package/lib/types/client/streaming.js +77 -0
  171. package/lib/types/client/streaming.js.map +1 -0
  172. package/lib/types/client/timeline.d.ts +26 -0
  173. package/lib/types/client/timeline.d.ts.map +1 -0
  174. package/lib/types/client/timeline.js +48 -0
  175. package/lib/types/client/timeline.js.map +1 -0
  176. package/lib/types/client/tool-activity.d.ts +52 -0
  177. package/lib/types/client/tool-activity.d.ts.map +1 -0
  178. package/lib/types/client/tool-activity.js +178 -0
  179. package/lib/types/client/tool-activity.js.map +1 -0
  180. package/lib/types/client/types.d.ts +66 -0
  181. package/lib/types/client/types.d.ts.map +1 -0
  182. package/lib/types/client/types.js +2 -0
  183. package/lib/types/client/types.js.map +1 -0
  184. package/lib/types/client/waiting-clock.d.ts +17 -0
  185. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  186. package/lib/types/client/waiting-clock.js +24 -0
  187. package/lib/types/client/waiting-clock.js.map +1 -0
  188. package/lib/types/client/word-motion.d.ts +16 -0
  189. package/lib/types/client/word-motion.d.ts.map +1 -0
  190. package/lib/types/client/word-motion.js +89 -0
  191. package/lib/types/client/word-motion.js.map +1 -0
  192. package/lib/types/client/word-timeline.d.ts +28 -0
  193. package/lib/types/client/word-timeline.d.ts.map +1 -0
  194. package/lib/types/client/word-timeline.js +100 -0
  195. package/lib/types/client/word-timeline.js.map +1 -0
  196. package/lib/types/dsh-better-display.d.ts +17 -0
  197. package/lib/types/dsh-better-display.d.ts.map +1 -0
  198. package/lib/types/dsh-better-display.js +53 -0
  199. package/lib/types/dsh-better-display.js.map +1 -0
  200. package/package.json +121 -0
  201. package/scripts/build-motion-fixture.mjs +26 -0
  202. package/scripts/link-harness-dependencies.mjs +51 -0
  203. package/skills/generative-mcpapps/SKILL.md +47 -0
  204. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  205. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  206. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  207. package/src/client/Blocks.tsx +205 -0
  208. package/src/client/ChoreographedFlow.tsx +182 -0
  209. package/src/client/ClosedProcessSummary.tsx +19 -0
  210. package/src/client/LiveFold.tsx +43 -0
  211. package/src/client/McpAppFrame.module.css +210 -0
  212. package/src/client/McpAppFrame.tsx +317 -0
  213. package/src/client/Reader.module.css +329 -0
  214. package/src/client/Reader.tsx +814 -0
  215. package/src/client/ReasoningCard.tsx +285 -0
  216. package/src/client/SettingsSection.module.css +147 -0
  217. package/src/client/SettingsSection.tsx +117 -0
  218. package/src/client/StickyLane.tsx +24 -0
  219. package/src/client/TimelineRail.module.css +170 -0
  220. package/src/client/TimelineRail.tsx +262 -0
  221. package/src/client/ToolActivity.tsx +214 -0
  222. package/src/client/TurnMetrics.module.css +139 -0
  223. package/src/client/TurnMetrics.tsx +160 -0
  224. package/src/client/WaitingStatus.tsx +22 -0
  225. package/src/client/conversation-scroll.ts +28 -0
  226. package/src/client/deliverables.ts +122 -0
  227. package/src/client/entry-policy.ts +20 -0
  228. package/src/client/entry.tsx +40 -0
  229. package/src/client/fold-choreography.ts +38 -0
  230. package/src/client/index.tsx +198 -0
  231. package/src/client/live-turn.ts +155 -0
  232. package/src/client/markdown/MarkdownText.module.css +304 -0
  233. package/src/client/markdown/MarkdownText.tsx +187 -0
  234. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  235. package/src/client/markdown/incremental.ts +131 -0
  236. package/src/client/markdown/katex.tsx +91 -0
  237. package/src/client/markdown/mathCompatibility.ts +350 -0
  238. package/src/client/markdown/parse.ts +45 -0
  239. package/src/client/markdown/render.tsx +630 -0
  240. package/src/client/markdown/upstream.json +14 -0
  241. package/src/client/mcp-app.ts +323 -0
  242. package/src/client/message-chrome.ts +47 -0
  243. package/src/client/motion.tsx +338 -0
  244. package/src/client/native/ContextBody.module.css +161 -0
  245. package/src/client/native/ContextBody.tsx +591 -0
  246. package/src/client/native/ContextInjectionRow.module.css +62 -0
  247. package/src/client/native/ContextInjectionRow.tsx +72 -0
  248. package/src/client/native/ReferenceIcon.tsx +35 -0
  249. package/src/client/native/tool-call-model.ts +248 -0
  250. package/src/client/native/upstream.json +41 -0
  251. package/src/client/open-file.ts +94 -0
  252. package/src/client/pending-submission.ts +49 -0
  253. package/src/client/primitive-labels.ts +53 -0
  254. package/src/client/projection.ts +118 -0
  255. package/src/client/reasoning-follow.ts +12 -0
  256. package/src/client/settings-copy.ts +53 -0
  257. package/src/client/settings-slots.ts +20 -0
  258. package/src/client/settings.tsx +73 -0
  259. package/src/client/skill-status.ts +78 -0
  260. package/src/client/store.ts +30 -0
  261. package/src/client/stream-buffer.ts +100 -0
  262. package/src/client/streaming.tsx +62 -0
  263. package/src/client/style.d.ts +4 -0
  264. package/src/client/timeline.ts +77 -0
  265. package/src/client/tool-activity.ts +152 -0
  266. package/src/client/types.ts +66 -0
  267. package/src/client/waiting-clock.ts +25 -0
  268. package/src/client/word-motion.tsx +91 -0
  269. package/src/client/word-timeline.ts +96 -0
  270. package/src/dsh-better-display.ts +108 -0
  271. package/src/skill-roots.ts +138 -0
  272. package/src/skill-status.ts +81 -0
  273. package/tests/browser-closed-gap.tsx +20 -0
  274. package/tests/browser-motion.tsx +65 -0
  275. package/tests/browser-sticky-lanes.tsx +22 -0
  276. package/tests/browser-waiting-clock.tsx +14 -0
  277. package/tests/chat-flow-hook.test.ts +66 -0
  278. package/tests/cloud-gate.test.ts +34 -0
  279. package/tests/conversation-scroll.test.ts +47 -0
  280. package/tests/deliverables.test.ts +128 -0
  281. package/tests/entry.test.ts +42 -0
  282. package/tests/fixtures/maid-composer-hide.html +31 -0
  283. package/tests/fold-choreography.test.ts +38 -0
  284. package/tests/history-word-regression.test.ts +16 -0
  285. package/tests/live-turn.test.ts +218 -0
  286. package/tests/mcp-apps.test.ts +157 -0
  287. package/tests/message-chrome.test.ts +29 -0
  288. package/tests/open-file.test.ts +103 -0
  289. package/tests/pending-submission.test.ts +45 -0
  290. package/tests/projection.test.ts +184 -0
  291. package/tests/reasoning-follow.test.ts +34 -0
  292. package/tests/settings-copy.test.ts +63 -0
  293. package/tests/skill-status.test.ts +137 -0
  294. package/tests/stock-install.test.ts +50 -0
  295. package/tests/stream-buffer.test.ts +97 -0
  296. package/tests/timeline.test.ts +59 -0
  297. package/tests/waiting-clock.test.ts +24 -0
  298. package/tests/word-timeline.test.ts +136 -0
  299. package/tsconfig.json +11 -0
  300. package/tsdown.config.ts +49 -0
@@ -0,0 +1,285 @@
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_HOLD, 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
+ const target = reasoningTarget(from, text.offsetHeight, port.clientHeight, lineHeight);
138
+ if (target - from < 1) return;
139
+ const began = performance.now();
140
+ nextAt = began + REASON_HOLD;
141
+ targetOffset = target;
142
+ // The supplied recipe: commit the start pose, then transition the track.
143
+ // No per-frame scrollTop writes and no catch-up across unread lines.
144
+ scrollTrack.style.transition = 'none';
145
+ scrollTrack.style.transform = `translateY(-${from}px)`;
146
+ void scrollTrack.offsetHeight;
147
+ scrollTrack.style.transition = 'transform var(--reason-step) var(--reason-ease)';
148
+ scrollTrack.style.transform = `translateY(-${target}px)`;
149
+ port.dataset.reasoningMoving = 'true';
150
+ port.dataset.reasoningFrom = String(from);
151
+ port.dataset.reasoningTarget = String(target);
152
+ port.dataset.reasoningBegan = String(began);
153
+ const tick = (now: number) => {
154
+ frame = 0;
155
+ if (!canFollow()) { cancel(); manual(); return; }
156
+ // Observe the browser's actual CSS interpolation for masks and handoff.
157
+ measure();
158
+ if (now - began < REASON_STEP || Math.abs(lastPainted - target) > .05) frame = requestAnimationFrame(tick);
159
+ else { delete port.dataset.reasoningMoving; schedule(); }
160
+ };
161
+ frame = requestAnimationFrame(tick);
162
+ };
163
+ const onScroll = () => {
164
+ measure();
165
+ // Focus/keyboard-induced native scroll wins even while the track moves.
166
+ if (automatic && port.scrollTop > 1) pause();
167
+ };
168
+ const onWheel = (event: WheelEvent) => {
169
+ if (!event.deltaY) return;
170
+ // The compositor picks a wheel scroller before handlers run. A clipped
171
+ // transform viewport would otherwise lose this first gesture or scroll
172
+ // the conversation. Consume only this handoff; later wheels stay native.
173
+ const handoff = automatic && event.cancelable;
174
+ if (handoff) event.preventDefault();
175
+ const unit = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? parseFloat(getComputedStyle(text).lineHeight) || 24
176
+ : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? port.clientHeight : 1;
177
+ pause();
178
+ if (handoff) { port.scrollTop = clamp(port.scrollTop + event.deltaY * unit); measure(); }
179
+ };
180
+ const onSelection = () => { if (hasSelection()) pause(); };
181
+ const onVisibility = () => {
182
+ cancel(); manual();
183
+ nextAt = performance.now() + REASON_HOLD;
184
+ if (!document.hidden) schedule();
185
+ };
186
+ const observer = new ResizeObserver(() => {
187
+ if (automatic && targetOffset > tail() + 1) {
188
+ cancel(); manual();
189
+ nextAt = performance.now() + REASON_HOLD;
190
+ }
191
+ measure(); schedule();
192
+ });
193
+ observer.observe(text); observer.observe(port);
194
+ port.addEventListener('scroll', onScroll, { passive: true });
195
+ port.addEventListener('wheel', onWheel, { passive: false });
196
+ document.addEventListener('selectionchange', onSelection);
197
+ document.addEventListener('visibilitychange', onVisibility);
198
+ // Keep the previous layout height until the resize effect can sample it.
199
+ measure(false); schedule();
200
+ return () => {
201
+ alive = false; cancel(); manual(); observer.disconnect();
202
+ port.removeEventListener('scroll', onScroll); port.removeEventListener('wheel', onWheel);
203
+ document.removeEventListener('selectionchange', onSelection);
204
+ document.removeEventListener('visibilitychange', onVisibility);
205
+ stopFollow.current = () => {};
206
+ };
207
+ }, [allowed, pause]);
208
+
209
+ useLayoutEffect(() => {
210
+ const port = viewport.current;
211
+ if (!port) return;
212
+ const changed = expanded !== previousExpanded.current;
213
+ previousExpanded.current = expanded;
214
+ const from = resize.current ? port.clientHeight : lastHeight.current;
215
+ resize.current?.cancel(); resize.current = null;
216
+ port.style.maxHeight = ''; port.style.height = '';
217
+ const target = port.clientHeight;
218
+ // Explicit reading adjusts only the conversation, never the outer app.
219
+ // The card is also a reader anchor so resizing cannot pin a later answer
220
+ // in place at the expense of this card's visible heading.
221
+ if (changed && !selected) {
222
+ const card = port.parentElement;
223
+ const host = port.closest<HTMLElement>('[data-conversation-scroll]');
224
+ if (card && host) {
225
+ const composer = parseFloat(getComputedStyle(host).getPropertyValue('--dsh-composer-height')) || 152;
226
+ const region = host.getBoundingClientRect();
227
+ const box = card.getBoundingClientRect();
228
+ const top = region.top + 16;
229
+ const bottom = region.bottom - composer - 16;
230
+ const delta = box.top < top ? box.top - top : box.bottom > bottom ? Math.min(box.bottom - bottom, box.top - top) : 0;
231
+ if (Math.abs(delta) > 1) host.scrollTo({ top: Math.max(0, host.scrollTop + delta), behavior: motion ? 'smooth' : 'instant' });
232
+ }
233
+ }
234
+ if (!changed || !motion || from < 1 || Math.abs(target - from) < 1) {
235
+ lastHeight.current = target;
236
+ return;
237
+ }
238
+ // max-height otherwise clamps the very first collapse frame to the new cap.
239
+ port.style.maxHeight = 'none';
240
+ const animation = port.animate([{ height: `${from}px` }, { height: `${target}px` }], { duration: 300, easing: EASING, fill: 'both' });
241
+ resize.current = animation;
242
+ animation.onfinish = () => {
243
+ if (resize.current !== animation) return;
244
+ resize.current = null; animation.cancel();
245
+ port.style.maxHeight = ''; port.style.height = '';
246
+ lastHeight.current = port.clientHeight;
247
+ };
248
+ }, [expanded, motion, selected]);
249
+ useEffect(() => () => resize.current?.cancel(), []);
250
+
251
+ const toggleReading = () => {
252
+ // Resize the same transcript without changing follow intent or position.
253
+ // Wheel, selection and viewport focus still explicitly pause following.
254
+ onRead();
255
+ setExpanded(value => !value);
256
+ };
257
+
258
+ 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">
259
+ <div className={css.reasonHeading} data-reader-reasoning-heading data-ud-check="reasoning-identity">
260
+ <span className={css.reasonLabel} data-reader-reasoning-label>思考</span>
261
+ <span>步骤 {step}</span>
262
+ </div>
263
+ <div ref={viewport} id={controls} className={css.reasonViewport} data-reader-reasoning-scroll data-edges={edges}
264
+ data-ud-motion="reader-reasoning-scroll" role="region" aria-label={`步骤 ${step} 的思考${overflow ? ',可滚动阅读' : ''}`}
265
+ tabIndex={overflow ? 0 : undefined} onPointerDown={pause} onFocus={pause}>
266
+ <div ref={track} className={css.reasonTrack} data-reader-reasoning-track>
267
+ <div ref={content} className={css.reasonText} data-reader-reasoning-text>{children}</div>
268
+ </div>
269
+ </div>
270
+ {(overflow || expanded) && <div className={css.reasonFooter} data-ud-check="reasoning-controls">
271
+ {active && motion ? <button type="button" className={css.reasonAction} disabled={selected} aria-controls={controls}
272
+ aria-label={following ? '暂停自动跟随思考' : '继续跟随最新思考'}
273
+ title={selected ? '取消文字选择后可继续跟随' : undefined}
274
+ onClick={() => { if (following) pause(); else { onRead(); setFollowing(true); } }}>
275
+ <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>
276
+ {following ? '暂停跟随' : '跟随最新'}
277
+ </button> : <span className={css.reasonCaption}>{expanded ? '手动阅读' : '可滚动阅读'}</span>}
278
+ <button type="button" className={css.reasonAction} aria-expanded={expanded} aria-controls={controls}
279
+ aria-label={expanded ? '收起完整思考' : '展开阅读完整思考'} onClick={toggleReading}>
280
+ {expanded ? '收起' : '展开阅读'}
281
+ <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>
282
+ </button>
283
+ </div>}
284
+ </div>;
285
+ }
@@ -0,0 +1,147 @@
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
+ }
@@ -0,0 +1,117 @@
1
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
2
+ import { deliverableOpenModeOf, type DeliverableOpenMode } from './open-file.js';
3
+ import { settingsCopyFor, type SettingsCopy, type SettingsCopyKey } from './settings-copy.js';
4
+ import { CONVENTIONAL_SKILL_ROOTS, detectGenerativeMcpappsSkill, shortestInstallCommand, type SkillStatusProbe, type SkillStatusSnapshot } from './skill-status.js';
5
+ import css from './SettingsSection.module.css';
6
+
7
+ export interface OpenPrefs {
8
+ getSnapshot: () => { deliverableOpenMode?: DeliverableOpenMode };
9
+ subscribe: (fn: () => void) => () => void;
10
+ actions: { setDeliverableOpenMode: (value: DeliverableOpenMode) => void };
11
+ }
12
+
13
+ export interface BetterDisplaySettingsInjected {
14
+ prefs: OpenPrefs;
15
+ copy?: SettingsCopy;
16
+ languageTag?: string;
17
+ checkSkill: SkillStatusProbe;
18
+ }
19
+
20
+ type SettingsProps = BetterDisplaySettingsInjected & {
21
+ /** Official settings.section owner share; unused here. */
22
+ close?: () => void;
23
+ t?: (key: SettingsCopyKey) => string;
24
+ };
25
+
26
+ function text(props: SettingsProps, copy: SettingsCopy, key: keyof SettingsCopy): string {
27
+ if (typeof props.t === 'function') {
28
+ try {
29
+ const value = props.t(key);
30
+ if (typeof value === 'string' && value.length > 0 && value !== key) return value;
31
+ } catch {
32
+ // Fall through to the bundled dictionaries.
33
+ }
34
+ }
35
+ return copy[key];
36
+ }
37
+
38
+ export function SettingsSection(props: SettingsProps) {
39
+ const copy = props.copy ?? settingsCopyFor(props.languageTag);
40
+ const mode = deliverableOpenModeOf(useSyncExternalStore(
41
+ props.prefs.subscribe,
42
+ () => props.prefs.getSnapshot()?.deliverableOpenMode,
43
+ () => 'external',
44
+ ));
45
+ const setMode = (value: DeliverableOpenMode) => {
46
+ props.prefs.actions.setDeliverableOpenMode(value);
47
+ };
48
+ const on = mode === 'sidebar';
49
+ const [skill, setSkill] = useState<SkillStatusSnapshot | undefined>();
50
+ const [checking, setChecking] = useState(false);
51
+
52
+ const recheck = useCallback(async () => {
53
+ setChecking(true);
54
+ try {
55
+ setSkill(await detectGenerativeMcpappsSkill(props.checkSkill));
56
+ } catch {
57
+ setSkill({
58
+ name: 'generative-mcpapps',
59
+ installed: false,
60
+ via: null,
61
+ roots: [],
62
+ hostReached: false,
63
+ });
64
+ } finally {
65
+ setChecking(false);
66
+ }
67
+ }, [props.checkSkill]);
68
+
69
+ useEffect(() => { void recheck(); }, [recheck]);
70
+
71
+ return (
72
+ <div className={css.section} data-better-display-settings>
73
+ <div className={css.row}>
74
+ <div className={css.rowText}>
75
+ <div className={css.title}>{text(props, copy, 'openTitle')}</div>
76
+ <div className={css.desc}>{text(props, copy, 'openDescription')}</div>
77
+ </div>
78
+ <button
79
+ type="button"
80
+ role="switch"
81
+ aria-checked={on}
82
+ className={css.switch}
83
+ data-on={on || undefined}
84
+ data-better-display-open-mode={mode}
85
+ onClick={() => { setMode(on ? 'external' : 'sidebar'); }}
86
+ />
87
+ </div>
88
+
89
+ <section className={css.block} data-better-display-skill={skill?.installed ? 'installed' : 'missing'}>
90
+ <div className={css.title}>{text(props, copy, 'skillTitle')}</div>
91
+ <div className={css.status}>
92
+ <span className={css.badge} data-ok={skill?.installed || undefined}>
93
+ {skill?.installed ? text(props, copy, 'skillInstalled') : text(props, copy, 'skillMissing')}
94
+ </span>
95
+ </div>
96
+ <p className={css.desc}>{text(props, copy, 'skillPurpose')}</p>
97
+ <p className={css.desc}>{text(props, copy, 'skillPluginNote')}</p>
98
+ {skill && !skill.installed ? (
99
+ <>
100
+ <p className={css.desc}>{skill.hostReached ? text(props, copy, 'skillInstall') : text(props, copy, 'skillUnavailable')}</p>
101
+ <pre className={css.pre}>{shortestInstallCommand()}</pre>
102
+ <ul className={css.roots} data-better-display-skill-roots>
103
+ {CONVENTIONAL_SKILL_ROOTS.map(root => (
104
+ <li key={root}>{root}</li>
105
+ ))}
106
+ </ul>
107
+ </>
108
+ ) : null}
109
+ <div className={css.actions}>
110
+ <button type="button" className={css.button} disabled={checking} onClick={() => { void recheck(); }}>
111
+ {checking ? text(props, copy, 'skillChecking') : text(props, copy, 'skillRecheck')}
112
+ </button>
113
+ </div>
114
+ </section>
115
+ </div>
116
+ );
117
+ }
@@ -0,0 +1,24 @@
1
+ import { useLayoutEffect, useRef } from 'react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ /** Publish real lane geometry; wrapped labels must not collide with statistics. */
5
+ export function StickyLane({ kind, className, children }: { kind: 'toolbar' | 'status'; className: string; children: ReactNode }) {
6
+ const ref = useRef<HTMLDivElement>(null);
7
+ useLayoutEffect(() => {
8
+ const el = ref.current;
9
+ const scope = el?.closest<HTMLElement>(kind === 'toolbar' ? '[data-dsh-better-display]' : '[data-reader-turn]');
10
+ if (!el || !scope) return;
11
+ const property = kind === 'toolbar' ? '--reader-toolbar-height' : '--reader-status-height';
12
+ const update = () => {
13
+ scope.style.setProperty(property, `${el.getBoundingClientRect().height}px`);
14
+ if (kind === 'toolbar') scope.style.setProperty('--reader-control-width', `${(el.querySelector('button')?.getBoundingClientRect().width ?? 80) + 20}px`);
15
+ };
16
+ update();
17
+ const observer = new ResizeObserver(update);
18
+ observer.observe(el);
19
+ const control = kind === 'toolbar' ? el.querySelector('button') : null;
20
+ if (control) observer.observe(control);
21
+ return () => { observer.disconnect(); scope.style.removeProperty(property); if (kind === 'toolbar') scope.style.removeProperty('--reader-control-width'); };
22
+ }, [kind]);
23
+ return <div ref={ref} className={className} data-reader-lane={kind} data-ud-check={kind === 'toolbar' ? 'reader-toolbar' : undefined}>{children}</div>;
24
+ }