@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.
- package/CHANGELOG.md +259 -0
- package/DESIGN.md +72 -0
- package/LICENSE +24 -0
- package/README.en.md +55 -0
- package/README.md +55 -0
- package/THIRD_PARTY_NOTICES.md +65 -0
- package/compat/harness-rc2.json +99 -0
- package/cordis.patch.yml +5 -0
- package/cordis.yml +4 -0
- package/docs/official-rendering-bridge.md +65 -0
- package/dshx.yml +5 -0
- package/lib/client.js +49292 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-better-display.js +101 -0
- package/lib/types/client/Blocks.d.ts +49 -0
- package/lib/types/client/Blocks.d.ts.map +1 -0
- package/lib/types/client/Blocks.js +143 -0
- package/lib/types/client/Blocks.js.map +1 -0
- package/lib/types/client/ChoreographedFlow.d.ts +22 -0
- package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
- package/lib/types/client/ChoreographedFlow.js +218 -0
- package/lib/types/client/ChoreographedFlow.js.map +1 -0
- package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
- package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
- package/lib/types/client/ClosedProcessSummary.js +16 -0
- package/lib/types/client/ClosedProcessSummary.js.map +1 -0
- package/lib/types/client/DiffPanel.d.ts +13 -0
- package/lib/types/client/DiffPanel.d.ts.map +1 -0
- package/lib/types/client/DiffPanel.js +115 -0
- package/lib/types/client/DiffPanel.js.map +1 -0
- package/lib/types/client/LiveFold.d.ts +10 -0
- package/lib/types/client/LiveFold.d.ts.map +1 -0
- package/lib/types/client/LiveFold.js +39 -0
- package/lib/types/client/LiveFold.js.map +1 -0
- package/lib/types/client/McpAppFrame.d.ts +32 -0
- package/lib/types/client/McpAppFrame.d.ts.map +1 -0
- package/lib/types/client/McpAppFrame.js +193 -0
- package/lib/types/client/McpAppFrame.js.map +1 -0
- package/lib/types/client/OfficialContent.d.ts +25 -0
- package/lib/types/client/OfficialContent.d.ts.map +1 -0
- package/lib/types/client/OfficialContent.js +53 -0
- package/lib/types/client/OfficialContent.js.map +1 -0
- package/lib/types/client/Reader.d.ts +3 -0
- package/lib/types/client/Reader.d.ts.map +1 -0
- package/lib/types/client/Reader.js +603 -0
- package/lib/types/client/Reader.js.map +1 -0
- package/lib/types/client/ReasoningCard.d.ts +11 -0
- package/lib/types/client/ReasoningCard.d.ts.map +1 -0
- package/lib/types/client/ReasoningCard.js +322 -0
- package/lib/types/client/ReasoningCard.js.map +1 -0
- package/lib/types/client/SettingsSection.d.ts +37 -0
- package/lib/types/client/SettingsSection.d.ts.map +1 -0
- package/lib/types/client/SettingsSection.js +63 -0
- package/lib/types/client/SettingsSection.js.map +1 -0
- package/lib/types/client/StickyLane.d.ts +8 -0
- package/lib/types/client/StickyLane.d.ts.map +1 -0
- package/lib/types/client/StickyLane.js +39 -0
- package/lib/types/client/StickyLane.js.map +1 -0
- package/lib/types/client/TimelineRail.d.ts +10 -0
- package/lib/types/client/TimelineRail.d.ts.map +1 -0
- package/lib/types/client/TimelineRail.js +168 -0
- package/lib/types/client/TimelineRail.js.map +1 -0
- package/lib/types/client/ToolActivity.d.ts +17 -0
- package/lib/types/client/ToolActivity.d.ts.map +1 -0
- package/lib/types/client/ToolActivity.js +244 -0
- package/lib/types/client/ToolActivity.js.map +1 -0
- package/lib/types/client/TurnMetrics.d.ts +11 -0
- package/lib/types/client/TurnMetrics.d.ts.map +1 -0
- package/lib/types/client/TurnMetrics.js +44 -0
- package/lib/types/client/TurnMetrics.js.map +1 -0
- package/lib/types/client/WaitClock.d.ts +16 -0
- package/lib/types/client/WaitClock.d.ts.map +1 -0
- package/lib/types/client/WaitClock.js +29 -0
- package/lib/types/client/WaitClock.js.map +1 -0
- package/lib/types/client/WaitingClock.d.ts +15 -0
- package/lib/types/client/WaitingClock.d.ts.map +1 -0
- package/lib/types/client/WaitingClock.js +30 -0
- package/lib/types/client/WaitingClock.js.map +1 -0
- package/lib/types/client/WaitingStatus.d.ts +6 -0
- package/lib/types/client/WaitingStatus.d.ts.map +1 -0
- package/lib/types/client/WaitingStatus.js +9 -0
- package/lib/types/client/WaitingStatus.js.map +1 -0
- package/lib/types/client/conversation-scroll.d.ts +19 -0
- package/lib/types/client/conversation-scroll.d.ts.map +1 -0
- package/lib/types/client/conversation-scroll.js +25 -0
- package/lib/types/client/conversation-scroll.js.map +1 -0
- package/lib/types/client/deliverables.d.ts +24 -0
- package/lib/types/client/deliverables.d.ts.map +1 -0
- package/lib/types/client/deliverables.js +104 -0
- package/lib/types/client/deliverables.js.map +1 -0
- package/lib/types/client/entry-policy.d.ts +10 -0
- package/lib/types/client/entry-policy.d.ts.map +1 -0
- package/lib/types/client/entry-policy.js +24 -0
- package/lib/types/client/entry-policy.js.map +1 -0
- package/lib/types/client/entry.d.ts +4 -0
- package/lib/types/client/entry.d.ts.map +1 -0
- package/lib/types/client/entry.js +34 -0
- package/lib/types/client/entry.js.map +1 -0
- package/lib/types/client/fold-choreography.d.ts +40 -0
- package/lib/types/client/fold-choreography.d.ts.map +1 -0
- package/lib/types/client/fold-choreography.js +48 -0
- package/lib/types/client/fold-choreography.js.map +1 -0
- package/lib/types/client/fold-intensity.d.ts +11 -0
- package/lib/types/client/fold-intensity.d.ts.map +1 -0
- package/lib/types/client/fold-intensity.js +32 -0
- package/lib/types/client/fold-intensity.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +195 -0
- package/lib/types/client/index.js.map +1 -0
- package/lib/types/client/live-turn.d.ts +58 -0
- package/lib/types/client/live-turn.d.ts.map +1 -0
- package/lib/types/client/live-turn.js +190 -0
- package/lib/types/client/live-turn.js.map +1 -0
- package/lib/types/client/markdown/MarkdownText.d.ts +41 -0
- package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
- package/lib/types/client/markdown/MarkdownText.js +178 -0
- package/lib/types/client/markdown/MarkdownText.js.map +1 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
- package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
- package/lib/types/client/markdown/incremental.d.ts +69 -0
- package/lib/types/client/markdown/incremental.d.ts.map +1 -0
- package/lib/types/client/markdown/incremental.js +108 -0
- package/lib/types/client/markdown/incremental.js.map +1 -0
- package/lib/types/client/markdown/katex.d.ts +27 -0
- package/lib/types/client/markdown/katex.d.ts.map +1 -0
- package/lib/types/client/markdown/katex.js +88 -0
- package/lib/types/client/markdown/katex.js.map +1 -0
- package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
- package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
- package/lib/types/client/markdown/mathCompatibility.js +293 -0
- package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
- package/lib/types/client/markdown/parse.d.ts +25 -0
- package/lib/types/client/markdown/parse.d.ts.map +1 -0
- package/lib/types/client/markdown/parse.js +42 -0
- package/lib/types/client/markdown/parse.js.map +1 -0
- package/lib/types/client/markdown/render.d.ts +121 -0
- package/lib/types/client/markdown/render.d.ts.map +1 -0
- package/lib/types/client/markdown/render.js +452 -0
- package/lib/types/client/markdown/render.js.map +1 -0
- package/lib/types/client/mcp-app.d.ts +50 -0
- package/lib/types/client/mcp-app.d.ts.map +1 -0
- package/lib/types/client/mcp-app.js +328 -0
- package/lib/types/client/mcp-app.js.map +1 -0
- package/lib/types/client/message-chrome.d.ts +19 -0
- package/lib/types/client/message-chrome.d.ts.map +1 -0
- package/lib/types/client/message-chrome.js +42 -0
- package/lib/types/client/message-chrome.js.map +1 -0
- package/lib/types/client/motion.d.ts +38 -0
- package/lib/types/client/motion.d.ts.map +1 -0
- package/lib/types/client/motion.js +453 -0
- package/lib/types/client/motion.js.map +1 -0
- package/lib/types/client/native/ContextBody.d.ts +130 -0
- package/lib/types/client/native/ContextBody.d.ts.map +1 -0
- package/lib/types/client/native/ContextBody.js +398 -0
- package/lib/types/client/native/ContextBody.js.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts +35 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.js +42 -0
- package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
- package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
- package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
- package/lib/types/client/native/ReferenceIcon.js +16 -0
- package/lib/types/client/native/ReferenceIcon.js.map +1 -0
- package/lib/types/client/native/tool-call-model.d.ts +58 -0
- package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
- package/lib/types/client/native/tool-call-model.js +219 -0
- package/lib/types/client/native/tool-call-model.js.map +1 -0
- package/lib/types/client/native/tool-update.d.ts +23 -0
- package/lib/types/client/native/tool-update.d.ts.map +1 -0
- package/lib/types/client/native/tool-update.js +22 -0
- package/lib/types/client/native/tool-update.js.map +1 -0
- package/lib/types/client/official-slots.d.ts +71 -0
- package/lib/types/client/official-slots.d.ts.map +1 -0
- package/lib/types/client/official-slots.js +183 -0
- package/lib/types/client/official-slots.js.map +1 -0
- package/lib/types/client/open-file.d.ts +29 -0
- package/lib/types/client/open-file.d.ts.map +1 -0
- package/lib/types/client/open-file.js +71 -0
- package/lib/types/client/open-file.js.map +1 -0
- package/lib/types/client/pending-submission.d.ts +28 -0
- package/lib/types/client/pending-submission.d.ts.map +1 -0
- package/lib/types/client/pending-submission.js +28 -0
- package/lib/types/client/pending-submission.js.map +1 -0
- package/lib/types/client/platform-media.d.ts +6 -0
- package/lib/types/client/platform-media.d.ts.map +1 -0
- package/lib/types/client/platform-media.js +18 -0
- package/lib/types/client/platform-media.js.map +1 -0
- package/lib/types/client/primitive-labels.d.ts +10 -0
- package/lib/types/client/primitive-labels.d.ts.map +1 -0
- package/lib/types/client/primitive-labels.js +38 -0
- package/lib/types/client/primitive-labels.js.map +1 -0
- package/lib/types/client/projection.d.ts +41 -0
- package/lib/types/client/projection.d.ts.map +1 -0
- package/lib/types/client/projection.js +112 -0
- package/lib/types/client/projection.js.map +1 -0
- package/lib/types/client/reasoning-follow.d.ts +10 -0
- package/lib/types/client/reasoning-follow.d.ts.map +1 -0
- package/lib/types/client/reasoning-follow.js +21 -0
- package/lib/types/client/reasoning-follow.js.map +1 -0
- package/lib/types/client/resource-address.d.ts +3 -0
- package/lib/types/client/resource-address.d.ts.map +1 -0
- package/lib/types/client/resource-address.js +33 -0
- package/lib/types/client/resource-address.js.map +1 -0
- package/lib/types/client/settings-copy.d.ts +7 -0
- package/lib/types/client/settings-copy.d.ts.map +1 -0
- package/lib/types/client/settings-copy.js +53 -0
- package/lib/types/client/settings-copy.js.map +1 -0
- package/lib/types/client/settings-slots.d.ts +23 -0
- package/lib/types/client/settings-slots.d.ts.map +1 -0
- package/lib/types/client/settings-slots.js +2 -0
- package/lib/types/client/settings-slots.js.map +1 -0
- package/lib/types/client/settings.d.ts +4 -0
- package/lib/types/client/settings.d.ts.map +1 -0
- package/lib/types/client/settings.js +60 -0
- package/lib/types/client/settings.js.map +1 -0
- package/lib/types/client/skill-status.d.ts +25 -0
- package/lib/types/client/skill-status.d.ts.map +1 -0
- package/lib/types/client/skill-status.js +44 -0
- package/lib/types/client/skill-status.js.map +1 -0
- package/lib/types/client/store.d.ts +35 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/client/store.js +37 -0
- package/lib/types/client/store.js.map +1 -0
- package/lib/types/client/stream-buffer.d.ts +37 -0
- package/lib/types/client/stream-buffer.d.ts.map +1 -0
- package/lib/types/client/stream-buffer.js +150 -0
- package/lib/types/client/stream-buffer.js.map +1 -0
- package/lib/types/client/streaming.d.ts +18 -0
- package/lib/types/client/streaming.d.ts.map +1 -0
- package/lib/types/client/streaming.js +84 -0
- package/lib/types/client/streaming.js.map +1 -0
- package/lib/types/client/timeline.d.ts +26 -0
- package/lib/types/client/timeline.d.ts.map +1 -0
- package/lib/types/client/timeline.js +48 -0
- package/lib/types/client/timeline.js.map +1 -0
- package/lib/types/client/tool-activity.d.ts +98 -0
- package/lib/types/client/tool-activity.d.ts.map +1 -0
- package/lib/types/client/tool-activity.js +301 -0
- package/lib/types/client/tool-activity.js.map +1 -0
- package/lib/types/client/types.d.ts +91 -0
- package/lib/types/client/types.d.ts.map +1 -0
- package/lib/types/client/types.js +2 -0
- package/lib/types/client/types.js.map +1 -0
- package/lib/types/client/waiting-clock.d.ts +49 -0
- package/lib/types/client/waiting-clock.d.ts.map +1 -0
- package/lib/types/client/waiting-clock.js +80 -0
- package/lib/types/client/waiting-clock.js.map +1 -0
- package/lib/types/client/word-motion.d.ts +16 -0
- package/lib/types/client/word-motion.d.ts.map +1 -0
- package/lib/types/client/word-motion.js +90 -0
- package/lib/types/client/word-motion.js.map +1 -0
- package/lib/types/client/word-timeline.d.ts +32 -0
- package/lib/types/client/word-timeline.d.ts.map +1 -0
- package/lib/types/client/word-timeline.js +119 -0
- package/lib/types/client/word-timeline.js.map +1 -0
- package/lib/types/dsh-better-display.d.ts +17 -0
- package/lib/types/dsh-better-display.d.ts.map +1 -0
- package/lib/types/dsh-better-display.js +93 -0
- package/lib/types/dsh-better-display.js.map +1 -0
- package/lib/types/skill-roots.d.ts +22 -0
- package/lib/types/skill-roots.d.ts.map +1 -0
- package/lib/types/skill-roots.js +126 -0
- package/lib/types/skill-roots.js.map +1 -0
- package/lib/types/skill-status.d.ts +36 -0
- package/lib/types/skill-status.d.ts.map +1 -0
- package/lib/types/skill-status.js +51 -0
- package/lib/types/skill-status.js.map +1 -0
- package/package.json +133 -0
- package/scripts/build-motion-fixture.mjs +26 -0
- package/scripts/check-harness-compat.mjs +71 -0
- package/scripts/link-harness-dependencies.mjs +54 -0
- package/scripts/verify-auto-fold.mjs +114 -0
- package/scripts/verify-official-bridge.mjs +121 -0
- package/scripts/verify-tool-shimmer.mjs +62 -0
- package/skills/generative-mcpapps/SKILL.md +47 -0
- package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
- package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
- package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
- package/src/client/Blocks.tsx +213 -0
- package/src/client/ChoreographedFlow.tsx +232 -0
- package/src/client/ClosedProcessSummary.tsx +23 -0
- package/src/client/DiffPanel.tsx +124 -0
- package/src/client/LiveFold.tsx +43 -0
- package/src/client/McpAppFrame.module.css +210 -0
- package/src/client/McpAppFrame.tsx +317 -0
- package/src/client/OfficialContent.tsx +69 -0
- package/src/client/Reader.module.css +692 -0
- package/src/client/Reader.tsx +878 -0
- package/src/client/ReasoningCard.tsx +306 -0
- package/src/client/SettingsSection.module.css +184 -0
- package/src/client/SettingsSection.tsx +194 -0
- package/src/client/StickyLane.tsx +34 -0
- package/src/client/TimelineRail.module.css +170 -0
- package/src/client/TimelineRail.tsx +262 -0
- package/src/client/ToolActivity.tsx +274 -0
- package/src/client/TurnMetrics.module.css +139 -0
- package/src/client/TurnMetrics.tsx +160 -0
- package/src/client/WaitClock.tsx +33 -0
- package/src/client/WaitingStatus.tsx +13 -0
- package/src/client/conversation-scroll.ts +28 -0
- package/src/client/deliverables.ts +122 -0
- package/src/client/entry-policy.ts +20 -0
- package/src/client/entry.tsx +40 -0
- package/src/client/fold-choreography.ts +55 -0
- package/src/client/fold-intensity.ts +36 -0
- package/src/client/index.tsx +222 -0
- package/src/client/live-turn.ts +198 -0
- package/src/client/markdown/MarkdownText.module.css +304 -0
- package/src/client/markdown/MarkdownText.tsx +195 -0
- package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
- package/src/client/markdown/incremental.ts +131 -0
- package/src/client/markdown/katex.tsx +91 -0
- package/src/client/markdown/mathCompatibility.ts +350 -0
- package/src/client/markdown/parse.ts +45 -0
- package/src/client/markdown/render.tsx +647 -0
- package/src/client/markdown/upstream.json +14 -0
- package/src/client/mcp-app.ts +323 -0
- package/src/client/message-chrome.ts +47 -0
- package/src/client/motion.tsx +410 -0
- package/src/client/native/ContextBody.module.css +161 -0
- package/src/client/native/ContextBody.tsx +591 -0
- package/src/client/native/ContextInjectionRow.module.css +66 -0
- package/src/client/native/ContextInjectionRow.tsx +106 -0
- package/src/client/native/ReferenceIcon.tsx +35 -0
- package/src/client/native/tool-call-model.ts +248 -0
- package/src/client/native/tool-update.ts +32 -0
- package/src/client/native/upstream.json +41 -0
- package/src/client/official-slots.tsx +211 -0
- package/src/client/open-file.ts +94 -0
- package/src/client/pending-submission.ts +49 -0
- package/src/client/platform-media.ts +17 -0
- package/src/client/primitive-labels.ts +54 -0
- package/src/client/projection.ts +118 -0
- package/src/client/reasoning-follow.ts +28 -0
- package/src/client/settings-copy.ts +80 -0
- package/src/client/settings-slots.ts +20 -0
- package/src/client/settings.tsx +73 -0
- package/src/client/skill-status.ts +78 -0
- package/src/client/store.ts +75 -0
- package/src/client/stream-buffer.ts +136 -0
- package/src/client/streaming.tsx +70 -0
- package/src/client/style.d.ts +4 -0
- package/src/client/timeline.ts +77 -0
- package/src/client/tool-activity.ts +298 -0
- package/src/client/types.ts +89 -0
- package/src/client/waiting-clock.ts +83 -0
- package/src/client/word-motion.tsx +92 -0
- package/src/client/word-timeline.ts +113 -0
- package/src/dsh-better-display.ts +108 -0
- package/src/skill-roots.ts +138 -0
- package/src/skill-status.ts +81 -0
- package/tests/browser-auto-fold.tsx +64 -0
- package/tests/browser-closed-gap.tsx +20 -0
- package/tests/browser-motion.tsx +65 -0
- package/tests/browser-official-bridge.tsx +152 -0
- package/tests/browser-sticky-lanes.tsx +22 -0
- package/tests/browser-waiting-clock.tsx +14 -0
- package/tests/chat-flow-hook.test.ts +66 -0
- package/tests/cloud-gate.test.ts +34 -0
- package/tests/context-tool-update.test.ts +33 -0
- package/tests/conversation-scroll.test.ts +47 -0
- package/tests/deliverables.test.ts +128 -0
- package/tests/entry.test.ts +42 -0
- package/tests/fixtures/maid-composer-hide.html +31 -0
- package/tests/fold-choreography.test.ts +54 -0
- package/tests/fold-intensity.test.ts +94 -0
- package/tests/footer-precedence.test.ts +129 -0
- package/tests/history-word-regression.test.ts +16 -0
- package/tests/live-turn.test.ts +218 -0
- package/tests/mcp-apps.test.ts +157 -0
- package/tests/message-chrome.test.ts +29 -0
- package/tests/official-slots.test.ts +112 -0
- package/tests/open-file.test.ts +114 -0
- package/tests/pending-submission.test.ts +45 -0
- package/tests/platform-media.test.ts +23 -0
- package/tests/projection.test.ts +184 -0
- package/tests/reasoning-follow.test.ts +34 -0
- package/tests/settings-copy.test.ts +63 -0
- package/tests/skill-status.test.ts +137 -0
- package/tests/stock-install.test.ts +58 -0
- package/tests/stream-buffer.test.ts +102 -0
- package/tests/timeline.test.ts +59 -0
- package/tests/tool-diff.test.ts +57 -0
- package/tests/waiting-clock.test.ts +83 -0
- package/tests/word-timeline.test.ts +146 -0
- package/tsconfig.json +11 -0
- package/tsdown.config.ts +51 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment';
|
|
2
|
+
import type { AssistantBlock, MessageImageLoader } 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
|
+
import type { OfficialSeat } from './official-slots.js';
|
|
10
|
+
import type { OpenFileOptions, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
11
|
+
import type {} from '@deepseek-ai/dsh-client-ui-tool/client';
|
|
12
|
+
|
|
13
|
+
export interface ReaderBlockOwner {
|
|
14
|
+
block: AssistantBlock;
|
|
15
|
+
streaming: boolean;
|
|
16
|
+
source: 'assistant' | 'user' | 'tool';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
20
|
+
interface SlotMap {
|
|
21
|
+
/** Trusted installed renderers may opt in; unknown model payloads never execute code. */
|
|
22
|
+
'dsh-better-display.block': { kind: 'chain'; scope: 'session'; owner: ReaderBlockOwner };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReaderInjected {
|
|
27
|
+
loadOlder: () => Promise<void>;
|
|
28
|
+
loadImage: (attachment: ImageAttachmentRef) => Promise<{ data: Uint8Array; mediaType: string }>;
|
|
29
|
+
/**
|
|
30
|
+
* Writes text into this session's composer draft via the sanctioned
|
|
31
|
+
* conversation input face, with a DOM fallback. Returns true when the
|
|
32
|
+
* composer accepted the text.
|
|
33
|
+
*/
|
|
34
|
+
fillComposer: (text: string) => boolean;
|
|
35
|
+
/** Open a workspace file or directory; mode comes from the Better Display setting. */
|
|
36
|
+
openFile: (path: string, options?: OpenFileOptions) => Promise<void> | void;
|
|
37
|
+
officialImageLoader: MessageImageLoader;
|
|
38
|
+
officialFileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined;
|
|
39
|
+
officialPreviewFile: (path: string) => void;
|
|
40
|
+
officialHost: { getSnapshot: () => { home?: string }; subscribe: (listener: () => void) => () => void };
|
|
41
|
+
/** Root-scoped Better Display prefs (`dsh.reader.v1`), shared with Settings. */
|
|
42
|
+
openPrefs?: {
|
|
43
|
+
getSnapshot: () => {
|
|
44
|
+
deliverableOpenMode?: import('./open-file.js').DeliverableOpenMode;
|
|
45
|
+
frostedGlass?: boolean;
|
|
46
|
+
bubbles?: boolean;
|
|
47
|
+
foldIntensity?: import('./fold-intensity.js').FoldIntensity;
|
|
48
|
+
autoFold?: boolean;
|
|
49
|
+
processOnly?: boolean;
|
|
50
|
+
};
|
|
51
|
+
subscribe: (fn: () => void) => () => void;
|
|
52
|
+
actions?: {
|
|
53
|
+
setFoldIntensity?: (value: import('./fold-intensity.js').FoldIntensity) => void;
|
|
54
|
+
setAutoFold?: (value: boolean) => void;
|
|
55
|
+
setFrostedGlass?: (value: boolean) => void;
|
|
56
|
+
setBubbles?: (value: boolean) => void;
|
|
57
|
+
setDeliverableOpenMode?: (value: import('./open-file.js').DeliverableOpenMode) => void;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** Reveal and highlight a workspace file in macOS Finder or Windows Explorer. */
|
|
61
|
+
revealFile?: (path: string) => Promise<void> | void;
|
|
62
|
+
/** Fork the conversation at a specific message sequence into a new branch session. */
|
|
63
|
+
forkAt?: (seq: number) => void;
|
|
64
|
+
/** Load session history through a target sequence number. */
|
|
65
|
+
loadThrough?: (seq: unknown) => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export type ReaderProps = PropsRuntime<'conversation.view'>
|
|
68
|
+
& PropsLocale<'chat'>
|
|
69
|
+
& PropsRenderSlots<'dsh-better-display.block' | OfficialSeat>
|
|
70
|
+
& PropsStore<ReturnType<typeof createReaderStore>>
|
|
71
|
+
& ReaderInjected;
|
|
72
|
+
export type BlockRenderProps = Pick<ReaderProps, 'renderSlotChain' | 'loadImage' | 'fillComposer'> & {
|
|
73
|
+
official?: Pick<ReaderProps, 'renderSlot' | 'renderSlotChain' | 'officialImageLoader' | 'officialFileMentions' | 'officialPreviewFile' | 'officialHost' | 'openView'>;
|
|
74
|
+
cwd?: string;
|
|
75
|
+
openFile?: (path: string, options?: OpenFileOptions) => Promise<void> | void;
|
|
76
|
+
revealFile?: (path: string) => Promise<void> | void;
|
|
77
|
+
forkAt?: (seq: number) => void;
|
|
78
|
+
/** Durable closing-message seq of this turn (turn-tail closing), used as the fork anchor. */
|
|
79
|
+
forkSeq?: number;
|
|
80
|
+
fileMentions?: MarkdownFileMentions;
|
|
81
|
+
metrics?: {
|
|
82
|
+
usage?: NonNullable<import('@deepseek-ai/dsh-client-ui-chat/client').TurnTailChatData['tokenUsage']>;
|
|
83
|
+
runMs?: number;
|
|
84
|
+
tokensPerSecond?: number;
|
|
85
|
+
ttftMs?: number;
|
|
86
|
+
/** Closing assistant-message time (turn-tail `closing.time`). */
|
|
87
|
+
endedAt?: number;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ChatNodeKind } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
2
|
+
|
|
3
|
+
export interface WaitingAnchor { key: string; time: number | null }
|
|
4
|
+
type InputNode = { kind: string; data: unknown };
|
|
5
|
+
type Submission = { requestId: string; time?: number; placement?: string };
|
|
6
|
+
const timestamp = (value: number | undefined): number | null => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Chat-layer kinds that hand the turn back to the model unconditionally.
|
|
10
|
+
*
|
|
11
|
+
* These are `ChatNodeDataMap` keys, NOT conversation-layer `ConversationNode` kinds.
|
|
12
|
+
* The two layers name the same idea differently: a returned tool is `tool-result`
|
|
13
|
+
* in the conversation contract, but the row the reader sees is the `tool-call` node.
|
|
14
|
+
* A name copied from the wrong layer silently disables the branch it guards, so
|
|
15
|
+
* the `satisfies` below makes the compiler reject any name that is not a real kind.
|
|
16
|
+
*/
|
|
17
|
+
export const WAIT_AFTER = new Set<ChatNodeKind>(['context', 'model-retry']);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every chat-kind the handover logic reasons about, checked against the host's own
|
|
21
|
+
* kind union at compile time. Adding a name here that the host does not register
|
|
22
|
+
* fails `tsc` instead of failing silently at runtime.
|
|
23
|
+
*/
|
|
24
|
+
export const HANDOVER_KINDS = [
|
|
25
|
+
'user', 'steering', 'context', 'model-retry', 'tool-call', 'command',
|
|
26
|
+
] as const satisfies readonly ChatNodeKind[];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mirrors the host's `isSettledTool`: a running call has no `kind`, a settled one
|
|
30
|
+
* carries `kind: 'tool-result'`. Kept local so this module stays dependency-free,
|
|
31
|
+
* and pinned by a test against the host's own helper contract.
|
|
32
|
+
*/
|
|
33
|
+
function toolSettled(data: unknown): boolean {
|
|
34
|
+
const root = (data as { root?: unknown } | undefined)?.root;
|
|
35
|
+
return typeof root === 'object' && root !== null && (root as { kind?: unknown }).kind === 'tool-result';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A command that is still executing has no outcome yet; only a settled one hands over. */
|
|
39
|
+
function commandSettled(data: unknown): boolean {
|
|
40
|
+
return (data as { outcome?: unknown } | undefined)?.outcome != null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Does this node leave the model on the hook for the next move?
|
|
45
|
+
*
|
|
46
|
+
* A tool or command that is *still running* is not a wait — the tool is the one
|
|
47
|
+
* working. Only its returned form hands control back, which is exactly the moment
|
|
48
|
+
* the model can stall.
|
|
49
|
+
*/
|
|
50
|
+
export function handsBackToModel(node: InputNode): boolean {
|
|
51
|
+
if (node.kind === 'user' || node.kind === 'steering') return true;
|
|
52
|
+
if (node.kind === 'tool-call') return toolSettled(node.data);
|
|
53
|
+
if (node.kind === 'command') return commandSettled(node.data);
|
|
54
|
+
return WAIT_AFTER.has(node.kind as ChatNodeKind);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The moment the current wait began.
|
|
59
|
+
*
|
|
60
|
+
* This is the time of the **last** node that handed control to the model — a
|
|
61
|
+
* returned tool, an injected context, or the user's own message. Anchoring on the
|
|
62
|
+
* user's message alone made the readout count the entire turn, so a wait that had
|
|
63
|
+
* only just begun showed the minutes the tools had already spent.
|
|
64
|
+
*/
|
|
65
|
+
export function waitingAnchor(order: readonly string[], get: (key: string) => InputNode | undefined, pending: readonly Submission[] = []): WaitingAnchor {
|
|
66
|
+
let anchor: WaitingAnchor = { key: 'unresolved', time: null };
|
|
67
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
68
|
+
const node = get(order[i]);
|
|
69
|
+
if (!node) continue;
|
|
70
|
+
if (!handsBackToModel(node)) continue;
|
|
71
|
+
const time = node.data && typeof node.data === 'object' && 'time' in node.data && typeof node.data.time === 'number' ? node.data.time : undefined;
|
|
72
|
+
anchor = { key: order[i], time: timestamp(time) };
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
76
|
+
const input = pending[i];
|
|
77
|
+
if (input.placement === 'queued') continue;
|
|
78
|
+
const time = timestamp(input.time);
|
|
79
|
+
if (time === null || anchor.time === null || time >= anchor.time) return { key: `pending:${input.requestId}`, time };
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return anchor;
|
|
83
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { readerPathImages } from './platform-media.js';
|
|
6
|
+
import { WORD_MOTION, WordTimeline } from './word-timeline.js';
|
|
7
|
+
import css from './Reader.module.css';
|
|
8
|
+
|
|
9
|
+
const WordScope = createContext({ enabled: false, generation: 0 });
|
|
10
|
+
const CODE_LABELS = { copyLabel: '复制代码', copiedLabel: '已复制' };
|
|
11
|
+
|
|
12
|
+
function useSourceReveal(element: RefObject<HTMLElement>, born: number | null, generation: number) {
|
|
13
|
+
const scope = useContext(WordScope);
|
|
14
|
+
const cancelled = useRef(false);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
const target = element.current;
|
|
17
|
+
if (!target) return;
|
|
18
|
+
target.dataset.wordState = 'settled';
|
|
19
|
+
if (!scope.enabled || scope.generation !== generation) { cancelled.current = true; return; }
|
|
20
|
+
if (cancelled.current || born === null || document.hidden) return;
|
|
21
|
+
const age = Number(document.timeline.currentTime ?? performance.now()) - born;
|
|
22
|
+
if (age >= WORD_MOTION.duration || typeof target.animate !== 'function') return;
|
|
23
|
+
// The reference opacity + blur frames, applied only to this new word.
|
|
24
|
+
// No color interpolation, stylesheet mutation, or whole-paragraph wipe.
|
|
25
|
+
const animation = target.animate([
|
|
26
|
+
{ opacity: 0, filter: `blur(${WORD_MOTION.blur}px)` },
|
|
27
|
+
{ opacity: 1, filter: 'blur(0px)' },
|
|
28
|
+
], { duration: WORD_MOTION.duration, easing: WORD_MOTION.easing, fill: 'backwards' });
|
|
29
|
+
// One absolute clock prevents newly mounted/resegmented leaves from getting
|
|
30
|
+
// a fresh delay or running ahead because their layout effects ran later.
|
|
31
|
+
animation.startTime = born;
|
|
32
|
+
target.dataset.wordState = 'resolving';
|
|
33
|
+
const finish = () => { animation.cancel(); target.dataset.wordState = 'settled'; document.removeEventListener('visibilitychange', hidden); };
|
|
34
|
+
animation.onfinish = finish;
|
|
35
|
+
const hidden = () => { if (document.hidden) { cancelled.current = true; finish(); } };
|
|
36
|
+
document.addEventListener('visibilitychange', hidden);
|
|
37
|
+
return () => { animation.cancel(); document.removeEventListener('visibilitychange', hidden); target.dataset.wordState = 'settled'; };
|
|
38
|
+
}, [element, born, generation, scope.enabled, scope.generation]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Word({ children, born, generation, offset, inline }: { children: string; born: number | null; generation: number; offset: number; inline: boolean }) {
|
|
42
|
+
const element = useRef<HTMLSpanElement>(null);
|
|
43
|
+
useSourceReveal(element, born, generation);
|
|
44
|
+
return <span ref={element} className={inline ? css.streamInlineWord : css.streamWord} data-reader-word data-source-start={offset} data-source-birth={born ?? undefined}>{children}</span>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A native code block enters on the same clock, without rebuilding its text. */
|
|
48
|
+
function MotionAtom({ children, born, generation, offset }: { children: ReactNode; born: number | null; generation: number; offset: number }) {
|
|
49
|
+
const element = useRef<HTMLDivElement>(null);
|
|
50
|
+
useSourceReveal(element, born, generation);
|
|
51
|
+
return <div ref={element} className={css.streamAtom} data-reader-atom data-source-start={offset} data-source-birth={born ?? undefined}>{children}</div>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Native Think is literal text, not Markdown. Spans never alter its bytes. */
|
|
55
|
+
export function MotionPlainText({ text, enabled, revision }: { text: string; enabled: boolean; revision: number }) {
|
|
56
|
+
const timeline = useRef<WordTimeline>();
|
|
57
|
+
timeline.current ??= new WordTimeline();
|
|
58
|
+
timeline.current.begin(text, enabled, revision, Number(document.timeline.currentTime ?? performance.now()));
|
|
59
|
+
const current = timeline.current;
|
|
60
|
+
const generation = current.generation;
|
|
61
|
+
const scope = useMemo(() => ({ enabled, generation }), [enabled, generation]);
|
|
62
|
+
return <WordScope.Provider value={scope}><div className={css.reasonPlain}>
|
|
63
|
+
{current.hasLiveText ? current.words(text, 0).map(word => word.text.trim()
|
|
64
|
+
? <Word key={word.key} born={word.born} generation={generation} offset={word.key} inline>{word.text}</Word>
|
|
65
|
+
: word.text) : text}
|
|
66
|
+
</div></WordScope.Provider>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Native DSH Markdown semantics with a stable text-leaf animation hook. */
|
|
70
|
+
export function MotionMarkdown({ text, streaming, enabled, revision, fileMentions }: {
|
|
71
|
+
text: string; streaming: boolean; enabled: boolean; revision: number; fileMentions?: MarkdownFileMentions;
|
|
72
|
+
}) {
|
|
73
|
+
const timeline = useRef<WordTimeline>();
|
|
74
|
+
timeline.current ??= new WordTimeline();
|
|
75
|
+
timeline.current.begin(text, enabled, revision, Number(document.timeline.currentTime ?? performance.now()));
|
|
76
|
+
const generation = timeline.current.generation;
|
|
77
|
+
const scope = useMemo(() => ({ enabled, generation }), [enabled, generation]);
|
|
78
|
+
const renderText = useMemo(() => (value: string, offset: number, inline = false): ReactNode => {
|
|
79
|
+
const current = timeline.current!;
|
|
80
|
+
if (!current.hasLiveText) return value;
|
|
81
|
+
return current.words(value, offset).map(word => word.text.trim()
|
|
82
|
+
? <Word key={word.key} born={word.born} generation={current.generation} offset={word.key} inline={inline || /^\p{P}+$/u.test(word.text)}>{word.text}</Word>
|
|
83
|
+
: word.text);
|
|
84
|
+
}, []);
|
|
85
|
+
const renderAtom = useMemo(() => (children: ReactNode, offset: number): ReactNode => {
|
|
86
|
+
const current = timeline.current!;
|
|
87
|
+
return current.hasLiveText ? <MotionAtom born={current.bornAt(offset)} generation={current.generation} offset={offset}>{children}</MotionAtom> : children;
|
|
88
|
+
}, []);
|
|
89
|
+
return <WordScope.Provider value={scope}>
|
|
90
|
+
<MarkdownText text={text} streaming={streaming} codeLabels={CODE_LABELS} fileMentions={fileMentions} pathImages={readerPathImages} renderText={renderText} renderAtom={renderAtom} />
|
|
91
|
+
</WordScope.Provider>;
|
|
92
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
/** Floor for the per-word cadence once a batch is compressed. */
|
|
9
|
+
minGap: 3,
|
|
10
|
+
/** One paint's words should land inside this window, however many arrive. */
|
|
11
|
+
batchMs: 90,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
|
|
15
|
+
const closingPunctuation = /^[\p{Pe}\p{Pf},.!?:;,。!?、;:%‰…·]+$/u;
|
|
16
|
+
const openingPunctuation = /^[\p{Ps}\p{Pi}]+$/u;
|
|
17
|
+
export interface RevealingWord { key: number; text: string; born: number | null }
|
|
18
|
+
interface SourceBirth { end: number; born: number | null }
|
|
19
|
+
|
|
20
|
+
/** Source-offset identity survives reparsing, frozen blocks, and final formatting. */
|
|
21
|
+
export class WordTimeline {
|
|
22
|
+
generation = 0;
|
|
23
|
+
hasLiveText = false;
|
|
24
|
+
private source: string | null = null;
|
|
25
|
+
private enabled = false;
|
|
26
|
+
private revision = 0;
|
|
27
|
+
private floor = 0;
|
|
28
|
+
private lastBirth = -Infinity;
|
|
29
|
+
private readonly births: SourceBirth[] = [];
|
|
30
|
+
|
|
31
|
+
begin(source: string, enabled: boolean, revision: number, now: number): void {
|
|
32
|
+
if (this.source === source && this.enabled === enabled && this.revision === revision) return;
|
|
33
|
+
const replaced = revision !== this.revision || (this.source !== null && !source.startsWith(this.source));
|
|
34
|
+
if (replaced || !enabled) {
|
|
35
|
+
this.births.length = 0;
|
|
36
|
+
this.lastBirth = -Infinity;
|
|
37
|
+
this.generation++;
|
|
38
|
+
this.floor = source.length;
|
|
39
|
+
} else if (this.source === null) {
|
|
40
|
+
// A mounted historical prefix must not replay; a fresh live buffer mounts empty.
|
|
41
|
+
this.floor = source.length;
|
|
42
|
+
} else {
|
|
43
|
+
// Allocate in source order, before Markdown can split/reorder text leaves.
|
|
44
|
+
// Intervals cover every offset, so resegmented CJK words inherit the time
|
|
45
|
+
// of their characters rather than falling through as instant "history".
|
|
46
|
+
const from = this.source.length;
|
|
47
|
+
const added = source.slice(from);
|
|
48
|
+
// The cadence follows the batch, not a constant. A fixed per-word gap caps
|
|
49
|
+
// the reveal at ~16 words/second however fast the model actually wrote, so
|
|
50
|
+
// a 500 tok/s answer still dripped out one word at a time. A batch of many
|
|
51
|
+
// words is compressed into one short window instead; a lone word keeps the
|
|
52
|
+
// original typing rhythm.
|
|
53
|
+
let arriving = 0;
|
|
54
|
+
for (const part of segmenter.segment(added)) if (part.segment.trim()) arriving++;
|
|
55
|
+
const gap = arriving > 1
|
|
56
|
+
? Math.max(WORD_MOTION.minGap, Math.min(WORD_MOTION.gap, WORD_MOTION.batchMs / arriving))
|
|
57
|
+
: WORD_MOTION.gap;
|
|
58
|
+
for (const part of segmenter.segment(added)) {
|
|
59
|
+
let born = Number.isFinite(this.lastBirth) ? this.lastBirth : null;
|
|
60
|
+
if (part.segment.trim()) {
|
|
61
|
+
// Capping at the batch window is what stops a queue from forming: the
|
|
62
|
+
// clock can never run further ahead than one window past the newest text.
|
|
63
|
+
born = Math.min(now + WORD_MOTION.batchMs, Math.max(now, this.lastBirth + gap));
|
|
64
|
+
this.lastBirth = born;
|
|
65
|
+
}
|
|
66
|
+
const end = from + part.index + part.segment.length;
|
|
67
|
+
const previous = this.births.at(-1);
|
|
68
|
+
if (previous && previous.born === born) previous.end = end;
|
|
69
|
+
else this.births.push({ end, born });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.source = source;
|
|
73
|
+
this.enabled = enabled;
|
|
74
|
+
this.revision = revision;
|
|
75
|
+
this.hasLiveText = enabled && this.births.length > 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
bornAt(offset: number): number | null {
|
|
79
|
+
if (!this.enabled || offset < this.floor || this.births.length === 0) return null;
|
|
80
|
+
let low = 0;
|
|
81
|
+
let high = this.births.length - 1;
|
|
82
|
+
while (low < high) {
|
|
83
|
+
const middle = (low + high) >>> 1;
|
|
84
|
+
if (this.births[middle]!.end <= offset) low = middle + 1;
|
|
85
|
+
else high = middle;
|
|
86
|
+
}
|
|
87
|
+
return this.births[low]!.born;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
words(value: string, offset: number): RevealingWord[] {
|
|
91
|
+
// Atomic inline animation boxes must not turn commas into legal line starts
|
|
92
|
+
// or strand opening quotes at a line end. Keep source-offset identities when
|
|
93
|
+
// punctuation arrives in a later chunk, so the preceding word never replays.
|
|
94
|
+
// Already received text is one inert leaf, not thousands of React Words.
|
|
95
|
+
// Only the newly appended suffix needs word identities and birth times.
|
|
96
|
+
const prefixLength = Math.min(value.length, Math.max(0, this.floor - offset));
|
|
97
|
+
const prefix: RevealingWord[] = prefixLength > 0
|
|
98
|
+
? [{ key: offset, text: value.slice(0, prefixLength), born: null }]
|
|
99
|
+
: [];
|
|
100
|
+
const parts: { index: number; segment: string }[] = [];
|
|
101
|
+
for (const segment of segmenter.segment(value.slice(prefixLength))) {
|
|
102
|
+
const part = { index: prefixLength + segment.index, segment: segment.segment };
|
|
103
|
+
const previous = parts.at(-1);
|
|
104
|
+
if (previous?.segment.trim() && (closingPunctuation.test(part.segment) || openingPunctuation.test(previous.segment))) {
|
|
105
|
+
previous.segment += part.segment;
|
|
106
|
+
} else parts.push({ index: part.index, segment: part.segment });
|
|
107
|
+
}
|
|
108
|
+
return prefix.concat(parts.map(part => {
|
|
109
|
+
const key = offset + part.index;
|
|
110
|
+
return { key, text: part.segment, born: this.bornAt(key) };
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -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 = '@drscrewdriver/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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
GENERATIVE_MCPAPPS_SKILL,
|
|
8
|
+
skillListIncludes,
|
|
9
|
+
skillRootEntryMatches,
|
|
10
|
+
type HostSkillStatus,
|
|
11
|
+
type SkillRootInfo,
|
|
12
|
+
} from './skill-status.js';
|
|
13
|
+
|
|
14
|
+
export function expandHomePath(path: string, home = homedir()): string {
|
|
15
|
+
if (path === '~') return home;
|
|
16
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(home, path.slice(2));
|
|
17
|
+
return path;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `$DSH_HOME` or `~/.dsh`, matching official `resolveDshHome`. */
|
|
21
|
+
export function resolveDshHome(env: Record<string, string | undefined> = process.env, home = homedir()): string {
|
|
22
|
+
const fromEnv = env.DSH_HOME;
|
|
23
|
+
const selected = fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : join(home, '.dsh');
|
|
24
|
+
return resolve(expandHomePath(selected, home));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `$DSH_AGENTS_HOME` or `~/.agents`. */
|
|
28
|
+
export function resolveAgentsHome(env: Record<string, string | undefined> = process.env, home = homedir()): string {
|
|
29
|
+
const fromEnv = env.DSH_AGENTS_HOME;
|
|
30
|
+
const selected = fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : join(home, '.agents');
|
|
31
|
+
return resolve(expandHomePath(selected, home));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function userSkillRoots(
|
|
35
|
+
env: Record<string, string | undefined> = process.env,
|
|
36
|
+
home = homedir(),
|
|
37
|
+
): readonly SkillRootInfo[] {
|
|
38
|
+
return [
|
|
39
|
+
{ source: 'user-dsh', path: join(resolveDshHome(env, home), 'skills') },
|
|
40
|
+
{ source: 'user-agents', path: join(resolveAgentsHome(env, home), 'skills') },
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function projectSkillRoots(projectRoot: string): readonly SkillRootInfo[] {
|
|
45
|
+
return [
|
|
46
|
+
{ source: 'project-dsh', path: join(projectRoot, '.dsh', 'skills') },
|
|
47
|
+
{ source: 'project-agents', path: join(projectRoot, '.agents', 'skills') },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function findProjectRoot(cwd: string): Promise<string> {
|
|
52
|
+
let current = resolve(cwd);
|
|
53
|
+
while (true) {
|
|
54
|
+
if (existsSync(join(current, '.git'))) return current;
|
|
55
|
+
const parent = dirname(current);
|
|
56
|
+
if (parent === current) return resolve(cwd);
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function rootHasGenerativeMcpapps(rootPath: string): Promise<boolean> {
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = await readdir(rootPath, { withFileTypes: true });
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (entry.name === '.system') continue;
|
|
70
|
+
if (!skillRootEntryMatches(entry.name)) continue;
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
try {
|
|
73
|
+
await access(join(rootPath, entry.name, 'SKILL.md'));
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (entry.isFile()) return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolve the plugin-shipped pack. This path is never treated as a skill root. */
|
|
85
|
+
export function pluginSkillPackPath(moduleUrl = import.meta.url): string | undefined {
|
|
86
|
+
const here = dirname(fileURLToPath(moduleUrl));
|
|
87
|
+
const candidates = [
|
|
88
|
+
join(here, '../skills', GENERATIVE_MCPAPPS_SKILL),
|
|
89
|
+
join(here, '../../skills', GENERATIVE_MCPAPPS_SKILL),
|
|
90
|
+
join(here, 'skills', GENERATIVE_MCPAPPS_SKILL),
|
|
91
|
+
];
|
|
92
|
+
return candidates.find(path => existsSync(join(path, 'SKILL.md')));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function scanGenerativeMcpappsStatus(options: {
|
|
96
|
+
cwd?: string;
|
|
97
|
+
env?: Record<string, string | undefined>;
|
|
98
|
+
home?: string;
|
|
99
|
+
packPath?: string;
|
|
100
|
+
listSkills?: () => Promise<readonly { readonly name?: string }[] | undefined>;
|
|
101
|
+
} = {}): Promise<HostSkillStatus> {
|
|
102
|
+
const env = options.env ?? process.env;
|
|
103
|
+
const home = options.home ?? homedir();
|
|
104
|
+
const roots: SkillRootInfo[] = [...userSkillRoots(env, home)];
|
|
105
|
+
const cwd = typeof options.cwd === 'string' && options.cwd.trim().length > 0
|
|
106
|
+
? (isAbsolute(options.cwd) ? options.cwd : resolve(options.cwd))
|
|
107
|
+
: undefined;
|
|
108
|
+
if (cwd !== undefined) roots.unshift(...projectSkillRoots(await findProjectRoot(cwd)));
|
|
109
|
+
|
|
110
|
+
const inspected: SkillRootInfo[] = [];
|
|
111
|
+
let rootHit = false;
|
|
112
|
+
for (const root of roots) {
|
|
113
|
+
const present = await rootHasGenerativeMcpapps(root.path);
|
|
114
|
+
inspected.push({ ...root, present });
|
|
115
|
+
if (present) rootHit = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let via: HostSkillStatus['via'] = null;
|
|
119
|
+
let listHit = false;
|
|
120
|
+
if (options.listSkills) {
|
|
121
|
+
try {
|
|
122
|
+
listHit = skillListIncludes(await options.listSkills());
|
|
123
|
+
if (listHit) via = 'skills.list';
|
|
124
|
+
} catch {
|
|
125
|
+
// Registry/RPC absence falls back to the scanned roots.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!listHit && rootHit) via = 'skill-root';
|
|
129
|
+
|
|
130
|
+
const packPath = options.packPath ?? pluginSkillPackPath();
|
|
131
|
+
return {
|
|
132
|
+
name: GENERATIVE_MCPAPPS_SKILL,
|
|
133
|
+
installed: listHit || rootHit,
|
|
134
|
+
via,
|
|
135
|
+
roots: inspected,
|
|
136
|
+
...packPath !== undefined ? { packPath } : {},
|
|
137
|
+
};
|
|
138
|
+
}
|