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,152 @@
1
+ import type { AssistantBlock, ToolCallBlock, TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
2
+ import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
3
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client';
4
+ import type { ReaderGroup } from './projection.js';
5
+
6
+ export type ToolDraft = Extract<AssistantBlock, { kind: 'tool-call' }>;
7
+ export type ToolPhase = 'preparing' | 'running' | 'returned' | 'succeeded' | 'failed' | 'interrupted';
8
+ export type ToolCategory = 'write' | 'read' | 'terminal' | 'search' | 'web' | 'other';
9
+ export interface ToolActivityEntry {
10
+ kind: 'tool'; key: string; callId: string; step: number; order: number;
11
+ draft?: ToolDraft; block?: ToolCallBlock;
12
+ }
13
+ export type ReaderFlowEntry = ToolActivityEntry | { kind: 'node'; key: string; nodeKey: string; order: number };
14
+
15
+ /** Public Step data includes tool-only model output before a chat node exists. */
16
+ export function readerFlow(group: ReaderGroup, turn: TurnLocation | undefined, get: (key: string) => ChatConversationViewNode | undefined): ReaderFlowEntry[] {
17
+ const flow: ReaderFlowEntry[] = [];
18
+ const calls = new Map<string, ToolActivityEntry>();
19
+ const assistantOrder = new Map<number, number>();
20
+ for (const key of group.keys) {
21
+ const node = get(key);
22
+ if (!node || node.visibility === 'hidden') continue;
23
+ const step = node.location.kind === 'step' ? node.location.step.step : 0;
24
+ if (node.kind === 'tool-call') {
25
+ const block = (node.data as { root: ToolCallBlock }).root;
26
+ calls.set(block.callId, { kind: 'tool', key: `reader-tool:${block.callId}`, callId: block.callId, step, block, order: node.anchorSeq });
27
+ } else {
28
+ flow.push({ kind: 'node', key, nodeKey: key, order: node.anchorSeq });
29
+ if (node.kind === 'assistant-step') assistantOrder.set(step, node.anchorSeq);
30
+ }
31
+ }
32
+ for (const step of turn?.steps ?? []) {
33
+ const data = step.data.get('assistant-step');
34
+ let index = 0;
35
+ for (const draft of data?.blocks ?? []) {
36
+ if (draft.kind !== 'tool-call' || !draft.callId) continue;
37
+ const previous = calls.get(draft.callId);
38
+ calls.set(draft.callId, previous ? { ...previous, draft } : {
39
+ kind: 'tool', key: `reader-tool:${draft.callId}`, callId: draft.callId, step: step.step, draft,
40
+ order: (assistantOrder.get(step.step) ?? step.start?.seq ?? 0) + .01 + index++ / 10000,
41
+ });
42
+ }
43
+ }
44
+ flow.push(...calls.values());
45
+ return flow.sort((left, right) => left.order - right.order);
46
+ }
47
+
48
+ export function objectValue(value: unknown): Record<string, unknown> | null {
49
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
50
+ }
51
+ export function stringValue(record: Record<string, unknown> | null, ...keys: string[]): string | undefined {
52
+ for (const key of keys) if (typeof record?.[key] === 'string' && record[key]) return record[key] as string;
53
+ return undefined;
54
+ }
55
+
56
+ /** Read only top-level JSON string values, including an unfinished final string.
57
+ * This never executes input or mistakes escaped/nested content for a path field. */
58
+ export function inputFields(raw: string): Record<string, unknown> {
59
+ try { return objectValue(JSON.parse(raw)) ?? {}; } catch { /* an in-flight argument is normally incomplete */ }
60
+ const fields: Record<string, unknown> = Object.create(null);
61
+ const prefix = raw.slice(0, 262144);
62
+ let depth = 0;
63
+ let key: string | undefined;
64
+ for (let index = 0; index < prefix.length; index++) {
65
+ const char = prefix[index];
66
+ if (char === '{' || char === '[') { depth++; continue; }
67
+ if (char === '}' || char === ']') { depth--; continue; }
68
+ if (char !== '"') continue;
69
+ const start = index;
70
+ let closed = false;
71
+ for (index++; index < prefix.length; index++) {
72
+ if (prefix[index] === '\\') { index++; continue; }
73
+ if (prefix[index] === '"') { closed = true; break; }
74
+ }
75
+ if (depth !== 1) continue;
76
+ const token = prefix.slice(start, closed ? index + 1 : prefix.length);
77
+ let value: string;
78
+ try { value = JSON.parse(token); } catch {
79
+ if (closed) continue;
80
+ // Strip only an unfinished trailing escape/unicode escape, not content.
81
+ let body = token.slice(1);
82
+ const unicode = /(?<!\\)(?:\\\\)*\\u[\da-f]{0,3}$/i.exec(body);
83
+ if (unicode) body = body.slice(0, unicode.index) + unicode[0].replace(/\\u[\da-f]{0,3}$/i, '');
84
+ let slashes = 0; for (let end = body.length - 1; end >= 0 && body[end] === '\\'; end--) slashes++;
85
+ if (slashes % 2) body = body.slice(0, -1);
86
+ try { value = JSON.parse(`"${body}"`); } catch { continue; }
87
+ }
88
+ if (closed && /^\s*:/.test(prefix.slice(index + 1))) key = value;
89
+ else if (key !== undefined) { fields[key] = value; key = undefined; }
90
+ }
91
+ return fields;
92
+ }
93
+
94
+ export function toolIdentity(entry: Pick<ToolActivityEntry, 'block' | 'draft'>) {
95
+ const block = entry.block;
96
+ return {
97
+ name: block ? 'kind' in block ? block.call?.name ?? entry.draft?.name ?? '工具调用' : block.name : entry.draft?.name ?? '工具调用',
98
+ raw: block ? 'kind' in block ? block.call?.argsRaw ?? entry.draft?.argsRaw ?? '' : block.argsRaw : entry.draft?.argsRaw ?? '',
99
+ };
100
+ }
101
+
102
+ export function executionFacts(block: ToolCallBlock | undefined): { exitCode?: number; signal?: string } {
103
+ if (!block || !('kind' in block)) return {};
104
+ const meta = objectValue(block.meta);
105
+ const code = meta?.exitCode ?? meta?.exit_code;
106
+ const text = block.content.length === 1 && block.content[0]?.type === 'text' ? block.content[0].text : '';
107
+ const exit = /\n\[exit code: (\d+)\]$/.exec(text);
108
+ const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
109
+ const parsedCode = exit?.[1] === undefined ? undefined : Number(exit[1]);
110
+ return {
111
+ exitCode: typeof code === 'number' && Number.isFinite(code) ? code : parsedCode,
112
+ signal: stringValue(meta, 'signal') ?? signal?.[1],
113
+ };
114
+ }
115
+
116
+ export function activityPhase(entry: Pick<ToolActivityEntry, 'block' | 'draft'>, turnClosed = false): ToolPhase {
117
+ if (!entry.block) return turnClosed ? 'interrupted' : 'preparing';
118
+ if (!('kind' in entry.block)) return turnClosed ? 'interrupted' : 'running';
119
+ // RC1 publishes canonical cancellation as an error result with a typed code.
120
+ if (entry.block.error?.code === 'ABORTED' || entry.block.error?.code === 'interrupted') return 'interrupted';
121
+ const facts = executionFacts(entry.block);
122
+ if (entry.block.isError || facts.signal || (facts.exitCode !== undefined && facts.exitCode !== 0)
123
+ || entry.block.subCalls.some(block => activityPhase({ block }, turnClosed) === 'failed')) return 'failed';
124
+ if (facts.exitCode === 0) return 'succeeded';
125
+ return 'returned';
126
+ }
127
+
128
+ export function activitySummary(entry: Pick<ToolActivityEntry, 'block' | 'draft'>) {
129
+ const { name, raw } = toolIdentity(entry);
130
+ const args = inputFields(raw);
131
+ const target = stringValue(args, 'file_path', 'path', 'filename', 'filePath');
132
+ const command = stringValue(args, 'command', 'cmd', 'script');
133
+ const description = stringValue(args, 'description');
134
+ const file = target?.split(/[/\\]/).at(-1);
135
+ const category: ToolCategory = /^(write|edit|apply_patch|patch|str_replace_editor)$/.test(name) ? 'write'
136
+ : /^(read|read_file)$/.test(name) ? 'read'
137
+ : /^(bash|shell|terminal|terminal_send|exec_command|pwsh)$/.test(name) ? 'terminal'
138
+ : /^(grep|glob|find|search)$/.test(name) ? 'search'
139
+ : /^(web_search|web_fetch|web_open)$/.test(name) ? 'web' : 'other';
140
+ const title = category === 'write' ? `${name === 'write' ? '写入' : '修改'}${file ? ` ${file}` : name === 'apply_patch' ? '代码补丁' : '文件'}`
141
+ : category === 'read' ? `读取${file ? ` ${file}` : '文件'}`
142
+ : category === 'terminal' ? description || '运行命令'
143
+ : category === 'search' ? name === 'glob' ? '查找文件' : '搜索内容'
144
+ : category === 'web' ? name === 'web_search' ? '搜索网页' : '读取网页'
145
+ : name;
146
+ return { name, raw, args, category, title, target: target ?? command ?? stringValue(args, 'query', 'pattern', 'url'), command,
147
+ cwd: stringValue(args, 'workdir', 'cwd'), content: stringValue(args, 'content', 'new_string', 'newText', 'file_text') };
148
+ }
149
+
150
+ export function preparingLabel(name: string): string {
151
+ return /^(write|edit|apply_patch)$/.test(name) ? '正在生成文件内容' : /^(bash|shell|exec_command|pwsh)$/.test(name) ? '正在准备命令' : '正在准备工具输入';
152
+ }
@@ -0,0 +1,66 @@
1
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
2
+ import type { AssistantBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
+ import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
5
+ import type {} from '@deepseek-ai/dsh-client-ui-chat/client';
6
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client';
7
+ import type {} from '@deepseek-ai/dsh-client-ui-session/client';
8
+ import type { createReaderStore } from './store.js';
9
+
10
+ export interface ReaderBlockOwner {
11
+ block: AssistantBlock;
12
+ streaming: boolean;
13
+ source: 'assistant' | 'user' | 'tool';
14
+ }
15
+
16
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
+ interface SlotMap {
18
+ /** Trusted installed renderers may opt in; unknown model payloads never execute code. */
19
+ 'dsh-better-display.block': { kind: 'chain'; scope: 'session'; owner: ReaderBlockOwner };
20
+ }
21
+ }
22
+
23
+ export interface ReaderInjected {
24
+ loadOlder: () => Promise<void>;
25
+ loadImage: (attachment: ImageAttachmentRef) => Promise<{ data: Uint8Array; mediaType: string }>;
26
+ /**
27
+ * Writes text into this session's composer draft via the sanctioned
28
+ * conversation input face, with a DOM fallback. Returns true when the
29
+ * composer accepted the text.
30
+ */
31
+ fillComposer: (text: string) => boolean;
32
+ /** Open a workspace file or directory; mode comes from the Better Display setting. */
33
+ openFile: (path: string) => Promise<void> | void;
34
+ /** Root-scoped open-mode snapshot (`dsh.reader.v1`), shared with Settings. */
35
+ openPrefs?: {
36
+ getSnapshot: () => { deliverableOpenMode?: import('./open-file.js').DeliverableOpenMode };
37
+ subscribe: (fn: () => void) => () => void;
38
+ };
39
+ /** Reveal and highlight a workspace file in macOS Finder or Windows Explorer. */
40
+ revealFile?: (path: string) => Promise<void> | void;
41
+ /** Fork the conversation at a specific message sequence into a new branch session. */
42
+ forkAt?: (seq: number) => void;
43
+ /** Load session history through a target sequence number. */
44
+ loadThrough?: (seq: unknown) => Promise<void>;
45
+ }
46
+ export type ReaderProps = PropsRuntime<'conversation.view'>
47
+ & PropsLocale<'chat'>
48
+ & PropsRenderSlots<'dsh-better-display.block'>
49
+ & PropsStore<ReturnType<typeof createReaderStore>>
50
+ & ReaderInjected;
51
+ export type BlockRenderProps = Pick<ReaderProps, 'renderSlotChain' | 'loadImage' | 'fillComposer'> & {
52
+ openFile?: (path: string) => Promise<void> | void;
53
+ revealFile?: (path: string) => Promise<void> | void;
54
+ forkAt?: (seq: number) => void;
55
+ /** Durable closing-message seq of this turn (turn-tail closing), used as the fork anchor. */
56
+ forkSeq?: number;
57
+ fileMentions?: MarkdownFileMentions;
58
+ metrics?: {
59
+ usage?: NonNullable<import('@deepseek-ai/dsh-client-ui-chat/client').TurnTailChatData['tokenUsage']>;
60
+ runMs?: number;
61
+ tokensPerSecond?: number;
62
+ ttftMs?: number;
63
+ /** Closing assistant-message time (turn-tail `closing.time`). */
64
+ endedAt?: number;
65
+ };
66
+ };
@@ -0,0 +1,25 @@
1
+ export interface WaitingAnchor { key: string; time: number | null }
2
+ type InputNode = { kind: string; data: unknown };
3
+ type Submission = { requestId: string; time?: number; placement?: string };
4
+ const timestamp = (value: number | undefined): number | null => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
5
+
6
+ /** Waiting is scoped to the latest human input, never the enclosing turn. */
7
+ export function waitingAnchor(order: readonly string[], get: (key: string) => InputNode | undefined, pending: readonly Submission[] = []): WaitingAnchor {
8
+ let anchor: WaitingAnchor = { key: 'unresolved', time: null };
9
+ for (let i = order.length - 1; i >= 0; i--) {
10
+ const node = get(order[i]);
11
+ if (node?.kind === 'user' || node?.kind === 'steering') {
12
+ const time = node.data && typeof node.data === 'object' && 'time' in node.data && typeof node.data.time === 'number' ? node.data.time : undefined;
13
+ anchor = { key: order[i], time: timestamp(time) };
14
+ break;
15
+ }
16
+ }
17
+ for (let i = pending.length - 1; i >= 0; i--) {
18
+ const input = pending[i];
19
+ if (input.placement === 'queued') continue;
20
+ const time = timestamp(input.time);
21
+ if (time === null || anchor.time === null || time >= anchor.time) return { key: `pending:${input.requestId}`, time };
22
+ break;
23
+ }
24
+ return anchor;
25
+ }
@@ -0,0 +1,91 @@
1
+ import { createContext, useContext, useLayoutEffect, useMemo, useRef } from 'react';
2
+ import type { ReactNode, RefObject } from 'react';
3
+ import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import { MarkdownText } from './markdown/MarkdownText.js';
5
+ import { WORD_MOTION, WordTimeline } from './word-timeline.js';
6
+ import css from './Reader.module.css';
7
+
8
+ const WordScope = createContext({ enabled: false, generation: 0 });
9
+ const CODE_LABELS = { copyLabel: '复制代码', copiedLabel: '已复制' };
10
+
11
+ function useSourceReveal(element: RefObject<HTMLElement>, born: number | null, generation: number) {
12
+ const scope = useContext(WordScope);
13
+ const cancelled = useRef(false);
14
+ useLayoutEffect(() => {
15
+ const target = element.current;
16
+ if (!target) return;
17
+ target.dataset.wordState = 'settled';
18
+ if (!scope.enabled || scope.generation !== generation) { cancelled.current = true; return; }
19
+ if (cancelled.current || born === null || document.hidden) return;
20
+ const age = Number(document.timeline.currentTime ?? performance.now()) - born;
21
+ if (age >= WORD_MOTION.duration || typeof target.animate !== 'function') return;
22
+ // The reference opacity + blur frames, applied only to this new word.
23
+ // No color interpolation, stylesheet mutation, or whole-paragraph wipe.
24
+ const animation = target.animate([
25
+ { opacity: 0, filter: `blur(${WORD_MOTION.blur}px)` },
26
+ { opacity: 1, filter: 'blur(0px)' },
27
+ ], { duration: WORD_MOTION.duration, easing: WORD_MOTION.easing, fill: 'backwards' });
28
+ // One absolute clock prevents newly mounted/resegmented leaves from getting
29
+ // a fresh delay or running ahead because their layout effects ran later.
30
+ animation.startTime = born;
31
+ target.dataset.wordState = 'resolving';
32
+ const finish = () => { animation.cancel(); target.dataset.wordState = 'settled'; document.removeEventListener('visibilitychange', hidden); };
33
+ animation.onfinish = finish;
34
+ const hidden = () => { if (document.hidden) { cancelled.current = true; finish(); } };
35
+ document.addEventListener('visibilitychange', hidden);
36
+ return () => { animation.cancel(); document.removeEventListener('visibilitychange', hidden); target.dataset.wordState = 'settled'; };
37
+ }, [element, born, generation, scope.enabled, scope.generation]);
38
+ }
39
+
40
+ function Word({ children, born, generation, offset, inline }: { children: string; born: number | null; generation: number; offset: number; inline: boolean }) {
41
+ const element = useRef<HTMLSpanElement>(null);
42
+ useSourceReveal(element, born, generation);
43
+ return <span ref={element} className={inline ? css.streamInlineWord : css.streamWord} data-reader-word data-source-start={offset} data-source-birth={born ?? undefined}>{children}</span>;
44
+ }
45
+
46
+ /** A native code block enters on the same clock, without rebuilding its text. */
47
+ function MotionAtom({ children, born, generation, offset }: { children: ReactNode; born: number | null; generation: number; offset: number }) {
48
+ const element = useRef<HTMLDivElement>(null);
49
+ useSourceReveal(element, born, generation);
50
+ return <div ref={element} className={css.streamAtom} data-reader-atom data-source-start={offset} data-source-birth={born ?? undefined}>{children}</div>;
51
+ }
52
+
53
+ /** Native Think is literal text, not Markdown. Spans never alter its bytes. */
54
+ export function MotionPlainText({ text, enabled, revision }: { text: string; enabled: boolean; revision: number }) {
55
+ const timeline = useRef<WordTimeline>();
56
+ timeline.current ??= new WordTimeline();
57
+ timeline.current.begin(text, enabled, revision, Number(document.timeline.currentTime ?? performance.now()));
58
+ const current = timeline.current;
59
+ const generation = current.generation;
60
+ const scope = useMemo(() => ({ enabled, generation }), [enabled, generation]);
61
+ return <WordScope.Provider value={scope}><div className={css.reasonPlain}>
62
+ {current.hasLiveText ? current.words(text, 0).map(word => word.text.trim()
63
+ ? <Word key={word.key} born={word.born} generation={generation} offset={word.key} inline>{word.text}</Word>
64
+ : word.text) : text}
65
+ </div></WordScope.Provider>;
66
+ }
67
+
68
+ /** Native DSH Markdown semantics with a stable text-leaf animation hook. */
69
+ export function MotionMarkdown({ text, streaming, enabled, revision, fileMentions }: {
70
+ text: string; streaming: boolean; enabled: boolean; revision: number; fileMentions?: MarkdownFileMentions;
71
+ }) {
72
+ const timeline = useRef<WordTimeline>();
73
+ timeline.current ??= new WordTimeline();
74
+ timeline.current.begin(text, enabled, revision, Number(document.timeline.currentTime ?? performance.now()));
75
+ const generation = timeline.current.generation;
76
+ const scope = useMemo(() => ({ enabled, generation }), [enabled, generation]);
77
+ const renderText = useMemo(() => (value: string, offset: number, inline = false): ReactNode => {
78
+ const current = timeline.current!;
79
+ if (!current.hasLiveText) return value;
80
+ return current.words(value, offset).map(word => word.text.trim()
81
+ ? <Word key={word.key} born={word.born} generation={current.generation} offset={word.key} inline={inline || /^\p{P}+$/u.test(word.text)}>{word.text}</Word>
82
+ : word.text);
83
+ }, []);
84
+ const renderAtom = useMemo(() => (children: ReactNode, offset: number): ReactNode => {
85
+ const current = timeline.current!;
86
+ return current.hasLiveText ? <MotionAtom born={current.bornAt(offset)} generation={current.generation} offset={offset}>{children}</MotionAtom> : children;
87
+ }, []);
88
+ return <WordScope.Provider value={scope}>
89
+ <MarkdownText text={text} streaming={streaming} codeLabels={CODE_LABELS} fileMentions={fileMentions} renderText={renderText} renderAtom={renderAtom} />
90
+ </WordScope.Provider>;
91
+ }
@@ -0,0 +1,96 @@
1
+ /** Motion parameters from the public transitions.dev streaming-text recipe. */
2
+ export const WORD_MOTION = {
3
+ duration: 350,
4
+ gap: 60,
5
+ blur: 1,
6
+ easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
7
+ maxDelay: 240,
8
+ } as const;
9
+
10
+ const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
11
+ const closingPunctuation = /^[\p{Pe}\p{Pf},.!?:;,。!?、;:%‰…·]+$/u;
12
+ const openingPunctuation = /^[\p{Ps}\p{Pi}]+$/u;
13
+ export interface RevealingWord { key: number; text: string; born: number | null }
14
+ interface SourceBirth { end: number; born: number | null }
15
+
16
+ /** Source-offset identity survives reparsing, frozen blocks, and final formatting. */
17
+ export class WordTimeline {
18
+ generation = 0;
19
+ hasLiveText = false;
20
+ private source: string | null = null;
21
+ private enabled = false;
22
+ private revision = 0;
23
+ private floor = 0;
24
+ private lastBirth = -Infinity;
25
+ private readonly births: SourceBirth[] = [];
26
+
27
+ begin(source: string, enabled: boolean, revision: number, now: number): void {
28
+ if (this.source === source && this.enabled === enabled && this.revision === revision) return;
29
+ const replaced = revision !== this.revision || (this.source !== null && !source.startsWith(this.source));
30
+ if (replaced || !enabled) {
31
+ this.births.length = 0;
32
+ this.lastBirth = -Infinity;
33
+ this.generation++;
34
+ this.floor = source.length;
35
+ } else if (this.source === null) {
36
+ // A mounted historical prefix must not replay; a fresh live buffer mounts empty.
37
+ this.floor = source.length;
38
+ } else {
39
+ // Allocate in source order, before Markdown can split/reorder text leaves.
40
+ // Intervals cover every offset, so resegmented CJK words inherit the time
41
+ // of their characters rather than falling through as instant "history".
42
+ const from = this.source.length;
43
+ for (const part of segmenter.segment(source.slice(from))) {
44
+ let born = Number.isFinite(this.lastBirth) ? this.lastBirth : null;
45
+ if (part.segment.trim()) {
46
+ born = Math.min(now + WORD_MOTION.maxDelay, Math.max(now, this.lastBirth + WORD_MOTION.gap));
47
+ this.lastBirth = born;
48
+ }
49
+ const end = from + part.index + part.segment.length;
50
+ const previous = this.births.at(-1);
51
+ if (previous && previous.born === born) previous.end = end;
52
+ else this.births.push({ end, born });
53
+ }
54
+ }
55
+ this.source = source;
56
+ this.enabled = enabled;
57
+ this.revision = revision;
58
+ this.hasLiveText = enabled && this.births.length > 0;
59
+ }
60
+
61
+ bornAt(offset: number): number | null {
62
+ if (!this.enabled || offset < this.floor || this.births.length === 0) return null;
63
+ let low = 0;
64
+ let high = this.births.length - 1;
65
+ while (low < high) {
66
+ const middle = (low + high) >>> 1;
67
+ if (this.births[middle]!.end <= offset) low = middle + 1;
68
+ else high = middle;
69
+ }
70
+ return this.births[low]!.born;
71
+ }
72
+
73
+ words(value: string, offset: number): RevealingWord[] {
74
+ // Atomic inline animation boxes must not turn commas into legal line starts
75
+ // or strand opening quotes at a line end. Keep source-offset identities when
76
+ // punctuation arrives in a later chunk, so the preceding word never replays.
77
+ // Already received text is one inert leaf, not thousands of React Words.
78
+ // Only the newly appended suffix needs word identities and birth times.
79
+ const prefixLength = Math.min(value.length, Math.max(0, this.floor - offset));
80
+ const prefix: RevealingWord[] = prefixLength > 0
81
+ ? [{ key: offset, text: value.slice(0, prefixLength), born: null }]
82
+ : [];
83
+ const parts: { index: number; segment: string }[] = [];
84
+ for (const segment of segmenter.segment(value.slice(prefixLength))) {
85
+ const part = { index: prefixLength + segment.index, segment: segment.segment };
86
+ const previous = parts.at(-1);
87
+ if (previous?.segment.trim() && (closingPunctuation.test(part.segment) || openingPunctuation.test(previous.segment))) {
88
+ previous.segment += part.segment;
89
+ } else parts.push({ index: part.index, segment: part.segment });
90
+ }
91
+ return prefix.concat(parts.map(part => {
92
+ const key = offset + part.index;
93
+ return { key, text: part.segment, born: this.bornAt(key) };
94
+ }));
95
+ }
96
+ }
@@ -0,0 +1,108 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import type { Context } from '@deepseek-ai/cordis';
4
+ import { scanGenerativeMcpappsStatus } from './skill-roots.js';
5
+ import { skillsFromListResult, toPublicSkillStatus } from './skill-status.js';
6
+
7
+ declare module '@deepseek-ai/cordis' {
8
+ interface Context {
9
+ webServer?: {
10
+ register: (route: {
11
+ kind: 'exact' | 'prefix';
12
+ path: string;
13
+ handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
14
+ }) => () => void;
15
+ };
16
+ }
17
+ }
18
+
19
+ export const name = 'dsh-better-display';
20
+ export const inject = ['webServer'];
21
+
22
+ function writeJson(res: ServerResponse, status: number, body: unknown): void {
23
+ res.setHeader('Content-Type', 'application/json');
24
+ res.statusCode = status;
25
+ res.end(JSON.stringify(body));
26
+ }
27
+
28
+ function skillLister(ctx: Context): (() => Promise<readonly { readonly name?: string }[] | undefined>) | undefined {
29
+ const skills = ctx.get?.('skills') as { list?: (options?: object) => Promise<unknown> } | undefined;
30
+ if (typeof skills?.list !== 'function') return undefined;
31
+ return async () => skillsFromListResult(await skills.list!({}));
32
+ }
33
+
34
+ export function apply(ctx: Context): void {
35
+ console.log('[my-plugins/dsh-better-display] loaded');
36
+
37
+ if (ctx.webServer) {
38
+ ctx.effect(() => {
39
+ const disposeReveal = ctx.webServer!.register({
40
+ kind: 'exact',
41
+ path: '/better-display/reveal',
42
+ handler: async (req, res) => {
43
+ if (req.method !== 'POST') {
44
+ res.statusCode = 405;
45
+ res.end();
46
+ return;
47
+ }
48
+ let body = '';
49
+ req.on('data', chunk => { body += chunk; });
50
+ req.on('end', () => {
51
+ try {
52
+ const data = JSON.parse(body);
53
+ const targetPath = typeof data.path === 'string' ? data.path.trim() : '';
54
+ if (!targetPath) {
55
+ res.statusCode = 400;
56
+ res.end(JSON.stringify({ ok: false, error: 'Empty path' }));
57
+ return;
58
+ }
59
+
60
+ if (process.platform === 'darwin') {
61
+ // Exact file reveal in macOS Finder with selection highlight
62
+ spawn('open', ['-R', targetPath], { detached: true, stdio: 'ignore' });
63
+ } else if (process.platform === 'win32') {
64
+ // Exact file selection in Windows Explorer
65
+ spawn('explorer.exe', [`/select,${targetPath}`], { detached: true, stdio: 'ignore' });
66
+ } else {
67
+ spawn('xdg-open', [targetPath], { detached: true, stdio: 'ignore' });
68
+ }
69
+
70
+ res.setHeader('Content-Type', 'application/json');
71
+ res.statusCode = 200;
72
+ res.end(JSON.stringify({ ok: true }));
73
+ } catch (err) {
74
+ res.statusCode = 400;
75
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
76
+ }
77
+ });
78
+ },
79
+ });
80
+ const disposeSkill = ctx.webServer!.register({
81
+ kind: 'exact',
82
+ path: '/better-display/skill-status',
83
+ handler: async (req, res) => {
84
+ if (req.method !== 'GET') {
85
+ res.statusCode = 405;
86
+ res.end();
87
+ return;
88
+ }
89
+ try {
90
+ const url = new URL(req.url ?? '', 'http://127.0.0.1');
91
+ const cwd = url.searchParams.get('cwd') ?? undefined;
92
+ const status = await scanGenerativeMcpappsStatus({
93
+ cwd,
94
+ listSkills: skillLister(ctx),
95
+ });
96
+ writeJson(res, 200, toPublicSkillStatus(status));
97
+ } catch (err) {
98
+ writeJson(res, 500, { ok: false, error: String(err) });
99
+ }
100
+ },
101
+ });
102
+ return () => {
103
+ disposeReveal();
104
+ disposeSkill();
105
+ };
106
+ }, 'dsh-better-display: reveal and skill-status routes');
107
+ }
108
+ }