@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,218 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import type { AssistantChatData, ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
4
|
+
import type { AssistantBlock, TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
5
|
+
import { assistantSegments, processExpanded } from '../src/client/projection.ts';
|
|
6
|
+
import type { TurnBoundary } from '../src/client/projection.ts';
|
|
7
|
+
import { foldSummary, presentLiveTurn, segmentLiveTurn, splitChain } from '../src/client/live-turn.ts';
|
|
8
|
+
import type { LiveStep } from '../src/client/live-turn.ts';
|
|
9
|
+
import { readerFlow } from '../src/client/tool-activity.ts';
|
|
10
|
+
|
|
11
|
+
const open: TurnBoundary = { status: 'open', reason: null, latestStep: 4, closingStep: null };
|
|
12
|
+
const completed: TurnBoundary = { status: 'closed', reason: 'completed', latestStep: 4, closingStep: 4 };
|
|
13
|
+
|
|
14
|
+
function reasoning(id: string, text = `思考 ${id}`): LiveStep {
|
|
15
|
+
return { kind: 'reasoning', key: id, nodeKey: id, start: 0, blocks: [{ kind: 'reasoning', text }], step: Number(id.replace(/\D/g, '') || 0) };
|
|
16
|
+
}
|
|
17
|
+
function body(id: string, text = `输出 ${id}`): LiveStep {
|
|
18
|
+
return { kind: 'body', key: id, nodeKey: id, start: 1, blocks: [{ kind: 'text', text }], step: 0 };
|
|
19
|
+
}
|
|
20
|
+
function tool(id: string): LiveStep {
|
|
21
|
+
return { kind: 'tool', key: id, entry: { kind: 'tool', key: id, callId: id, step: 0, order: 0, draft: { kind: 'tool-call', callId: id, name: 'bash', argsRaw: '{}' } } };
|
|
22
|
+
}
|
|
23
|
+
function user(id: string): LiveStep {
|
|
24
|
+
return { kind: 'user', key: id, nodeKey: id };
|
|
25
|
+
}
|
|
26
|
+
function other(id: string): LiveStep {
|
|
27
|
+
return { kind: 'other', key: id, nodeKey: id };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function kinds(items: ReturnType<typeof presentLiveTurn>) {
|
|
31
|
+
return items.map(item => item.kind === 'fold' ? ['fold', item.summary, item.steps.map(step => step.key)] : [item.kind, item.key]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('body or tool alone never folds, including after the first reasoning', () => {
|
|
35
|
+
assert.equal(splitChain([reasoning('1')]).fold, null);
|
|
36
|
+
assert.equal(splitChain([reasoning('1'), body('2')]).fold, null);
|
|
37
|
+
assert.equal(splitChain([reasoning('1'), body('2'), tool('3')]).fold, null);
|
|
38
|
+
assert.equal(splitChain([body('2'), tool('3')]).fold, null);
|
|
39
|
+
assert.equal(splitChain([other('ctx'), reasoning('1'), body('2')]).fold, null);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a second reasoning folds every prior step into one box, then a third folds everything before it', () => {
|
|
43
|
+
const first = splitChain([reasoning('1'), body('2'), tool('3')]);
|
|
44
|
+
assert.equal(first.fold, null);
|
|
45
|
+
assert.deepEqual(first.open.map(step => step.key), ['1', '2', '3']);
|
|
46
|
+
|
|
47
|
+
const second = splitChain([reasoning('1'), body('2'), tool('3'), reasoning('4')]);
|
|
48
|
+
assert.deepEqual(second.fold?.map(step => step.key), ['1', '2', '3']);
|
|
49
|
+
assert.deepEqual(second.open.map(step => step.key), ['4']);
|
|
50
|
+
assert.equal(foldSummary(second.fold!), '思考×1 · 输出×1 · 工具×1');
|
|
51
|
+
if (second.fold?.[0]?.kind !== 'reasoning') throw new Error('folded reasoning missing');
|
|
52
|
+
assert.equal(second.fold[0].blocks[0]?.kind === 'reasoning' && second.fold[0].blocks[0].text, '思考 1');
|
|
53
|
+
assert.equal(foldSummary(second.fold).includes('思考 1'), false);
|
|
54
|
+
|
|
55
|
+
const withTool = splitChain([reasoning('1'), body('2'), tool('3'), reasoning('4'), tool('5')]);
|
|
56
|
+
assert.deepEqual(withTool.fold?.map(step => step.key), ['1', '2', '3']);
|
|
57
|
+
assert.deepEqual(withTool.open.map(step => step.key), ['4', '5']);
|
|
58
|
+
|
|
59
|
+
const third = splitChain([reasoning('1'), body('2'), tool('3'), reasoning('4'), tool('5'), reasoning('6')]);
|
|
60
|
+
assert.deepEqual(third.fold?.map(step => step.key), ['1', '2', '3', '4', '5']);
|
|
61
|
+
assert.deepEqual(third.open.map(step => step.key), ['6']);
|
|
62
|
+
assert.equal(foldSummary(third.fold!), '思考×2 · 输出×1 · 工具×2');
|
|
63
|
+
|
|
64
|
+
const latest = splitChain([reasoning('1'), body('2'), tool('3'), reasoning('4'), tool('5'), reasoning('6'), body('7')]);
|
|
65
|
+
assert.deepEqual(latest.fold?.map(step => step.key), ['1', '2', '3', '4', '5']);
|
|
66
|
+
assert.deepEqual(latest.open.map(step => step.key), ['6', '7']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('fold presentation keeps prior step keys so the reader can collapse the same process fragments in place', () => {
|
|
70
|
+
const chain = [reasoning('1'), body('2'), tool('3')];
|
|
71
|
+
const before = presentLiveTurn(chain, open);
|
|
72
|
+
assert.deepEqual(before.map(item => item.key), ['1', '2', '3']);
|
|
73
|
+
|
|
74
|
+
const second = presentLiveTurn([...chain, reasoning('4')], open);
|
|
75
|
+
assert.equal(second[0]?.kind, 'fold');
|
|
76
|
+
if (second[0]?.kind !== 'fold') throw new Error('missing fold');
|
|
77
|
+
assert.equal(second[0].key, 'live-fold:1');
|
|
78
|
+
assert.deepEqual(second[0].steps.map(step => step.key), before.map(item => item.key));
|
|
79
|
+
assert.deepEqual(second.filter(item => item.kind === 'open').map(item => item.key), ['4']);
|
|
80
|
+
|
|
81
|
+
const third = presentLiveTurn([...chain, reasoning('4'), tool('5'), reasoning('6')], open);
|
|
82
|
+
if (third[0]?.kind !== 'fold') throw new Error('missing grown fold');
|
|
83
|
+
assert.equal(third[0].key, 'live-fold:1');
|
|
84
|
+
assert.deepEqual(third[0].steps.map(step => step.key), ['1', '2', '3', '4', '5']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('presentLiveTurn keeps a single prior box rather than one fold per step', () => {
|
|
88
|
+
const items = presentLiveTurn([reasoning('1'), body('2'), tool('3'), reasoning('4'), tool('5'), reasoning('6'), body('7')], open);
|
|
89
|
+
const folds = items.filter(item => item.kind === 'fold');
|
|
90
|
+
assert.equal(folds.length, 1);
|
|
91
|
+
if (folds[0]?.kind !== 'fold') throw new Error('missing fold');
|
|
92
|
+
assert.equal(folds[0].steps.length, 5);
|
|
93
|
+
assert.deepEqual(items.filter(item => item.kind === 'open').map(item => item.key), ['6', '7']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('mid-turn user insert resets the chain; only the next reasoning folds post-insert priors', () => {
|
|
97
|
+
const before = presentLiveTurn([reasoning('1'), body('2'), tool('3'), user('insert'), body('4'), tool('5')], open);
|
|
98
|
+
assert.deepEqual(kinds(before), [
|
|
99
|
+
['open', '1'], ['open', '2'], ['open', '3'],
|
|
100
|
+
['user', 'insert'],
|
|
101
|
+
['open', '4'], ['open', '5'],
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const after = presentLiveTurn([reasoning('1'), body('2'), tool('3'), user('insert'), body('4'), tool('5'), reasoning('6')], open);
|
|
105
|
+
assert.deepEqual(kinds(after), [
|
|
106
|
+
['open', '1'], ['open', '2'], ['open', '3'],
|
|
107
|
+
['user', 'insert'],
|
|
108
|
+
['fold', '输出×1 · 工具×1', ['4', '5']],
|
|
109
|
+
['open', '6'],
|
|
110
|
+
]);
|
|
111
|
+
const folds = after.filter(item => item.kind === 'fold');
|
|
112
|
+
assert.equal(folds.length, 1);
|
|
113
|
+
if (folds[0]?.kind !== 'fold') throw new Error('missing insert fold');
|
|
114
|
+
assert.ok(!folds[0].steps.some(step => ['1', '2', '3', 'insert'].includes(step.key)));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('each chain has at most one fold box; a later chain does not split priors into per-step folds', () => {
|
|
118
|
+
const items = presentLiveTurn([
|
|
119
|
+
reasoning('1'), body('2'), tool('3'), reasoning('4'),
|
|
120
|
+
user('insert'),
|
|
121
|
+
body('5'), tool('6'), reasoning('7'),
|
|
122
|
+
], open);
|
|
123
|
+
const folds = items.filter(item => item.kind === 'fold');
|
|
124
|
+
assert.equal(folds.length, 2);
|
|
125
|
+
if (folds[0]?.kind !== 'fold' || folds[1]?.kind !== 'fold') throw new Error('expected one box per chain');
|
|
126
|
+
assert.deepEqual(folds[0].steps.map(step => step.key), ['1', '2', '3']);
|
|
127
|
+
assert.deepEqual(folds[1].steps.map(step => step.key), ['5', '6']);
|
|
128
|
+
assert.equal(folds[0].summary, '思考×1 · 输出×1 · 工具×1');
|
|
129
|
+
assert.equal(folds[1].summary, '输出×1 · 工具×1');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('successful turn close disables live fold so the existing final-answer rules still apply', () => {
|
|
133
|
+
const items = presentLiveTurn([reasoning('1'), body('2'), tool('3'), reasoning('4'), body('final')], completed);
|
|
134
|
+
assert.equal(items.some(item => item.kind === 'fold'), false);
|
|
135
|
+
assert.deepEqual(items.map(item => item.kind), ['open', 'open', 'open', 'open', 'open']);
|
|
136
|
+
assert.equal(processExpanded(undefined, completed), false);
|
|
137
|
+
assert.equal(processExpanded(undefined, { ...completed, reason: 'error' }), true);
|
|
138
|
+
assert.equal(processExpanded(undefined, { ...completed, reason: 'interrupted' }), true);
|
|
139
|
+
assert.equal(processExpanded(undefined, { ...completed, reason: 'blocked' }), true);
|
|
140
|
+
assert.equal(processExpanded(undefined, { status: 'unknown', reason: null, latestStep: -1, closingStep: null }), true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('error, interrupt, approval-blocked and unknown terminals do not live-fold away the transcript', () => {
|
|
144
|
+
const steps = [reasoning('1'), body('2'), tool('3'), reasoning('4')];
|
|
145
|
+
for (const reason of ['error', 'interrupted', 'aborted', 'blocked', 'max-tokens', 'future-terminal']) {
|
|
146
|
+
const items = presentLiveTurn(steps, { status: 'closed', reason, latestStep: 4, closingStep: 4 });
|
|
147
|
+
assert.equal(items.some(item => item.kind === 'fold'), false, reason);
|
|
148
|
+
assert.equal(processExpanded(undefined, { status: 'closed', reason, latestStep: 4, closingStep: 4 }), true, reason);
|
|
149
|
+
}
|
|
150
|
+
assert.equal(presentLiveTurn(steps, { status: 'unknown', reason: null, latestStep: -1, closingStep: null }).some(item => item.kind === 'fold'), false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('segmentation uses existing assistantSegments and tool boundaries and does not invent empty reasoning', () => {
|
|
154
|
+
const think: AssistantBlock = { kind: 'reasoning', text: '第一段思考' };
|
|
155
|
+
const moreThink: AssistantBlock = { kind: 'reasoning', text: '同一思考步骤' };
|
|
156
|
+
const text: AssistantBlock = { kind: 'text', text: '中途说明' };
|
|
157
|
+
const call: AssistantBlock = { kind: 'tool-call', callId: 'c1', name: 'bash', argsRaw: '{"command":"ls"}' };
|
|
158
|
+
const empty: AssistantBlock = { kind: 'reasoning', text: ' ' };
|
|
159
|
+
assert.deepEqual(assistantSegments([think, moreThink, text, call, empty]).map(part => [part.kind, part.start]), [
|
|
160
|
+
['reasoning', 0], ['body', 2], ['reasoning', 4],
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const data: AssistantChatData = { status: 'running', turn: 1, step: 0, blocks: [think, moreThink, text, call], time: 1 };
|
|
164
|
+
const node = {
|
|
165
|
+
key: 'a', id: 'a', kind: 'assistant-step', target: 'chat', data, anchorSeq: 10, visibility: 'visible',
|
|
166
|
+
location: { kind: 'step', step: { step: 0 } },
|
|
167
|
+
} as unknown as ChatConversationViewNode;
|
|
168
|
+
const turn = { turn: 1, steps: [{ step: 0, start: { seq: 10 }, data: { get: () => data } }] } as unknown as TurnLocation;
|
|
169
|
+
const flow = readerFlow({ key: 'turn:1', turn: 1, keys: ['a'] }, turn, key => key === 'a' ? node : undefined);
|
|
170
|
+
const steps = segmentLiveTurn(flow, key => key === 'a' ? node : undefined);
|
|
171
|
+
assert.deepEqual(steps.map(step => step.kind), ['reasoning', 'body', 'tool']);
|
|
172
|
+
assert.equal(steps[0]?.kind === 'reasoning' && steps[0].blocks.length, 2);
|
|
173
|
+
if (steps[0]?.kind !== 'reasoning') throw new Error('missing reasoning');
|
|
174
|
+
assert.equal(steps[0].blocks[0], think);
|
|
175
|
+
assert.equal(steps[0].blocks[1], moreThink);
|
|
176
|
+
|
|
177
|
+
const blankData: AssistantChatData = { ...data, blocks: [empty, text] };
|
|
178
|
+
const blank = { ...node, data: blankData } as unknown as ChatConversationViewNode;
|
|
179
|
+
const blankTurn = { turn: 1, steps: [{ step: 0, start: { seq: 10 }, data: { get: () => blankData } }] } as unknown as TurnLocation;
|
|
180
|
+
const blankFlow = readerFlow({ key: 'turn:1', turn: 1, keys: ['a'] }, blankTurn, () => blank);
|
|
181
|
+
assert.deepEqual(segmentLiveTurn(blankFlow, () => blank).map(step => step.kind), ['body']);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('steering in the flow is a chain reset, same as a mid-turn user message', () => {
|
|
185
|
+
const think: AssistantBlock = { kind: 'reasoning', text: '先想' };
|
|
186
|
+
const text: AssistantBlock = { kind: 'text', text: '先说' };
|
|
187
|
+
const later: AssistantBlock = { kind: 'reasoning', text: '插入后再想' };
|
|
188
|
+
const first: AssistantChatData = { status: 'settled', turn: 1, step: 0, blocks: [think, text], time: 1 };
|
|
189
|
+
const second: AssistantChatData = { status: 'running', turn: 1, step: 1, blocks: [later], time: 2 };
|
|
190
|
+
const assistant = (key: string, seq: number, data: AssistantChatData) => ({
|
|
191
|
+
key, id: key, kind: 'assistant-step', target: 'chat', data, anchorSeq: seq, visibility: 'visible',
|
|
192
|
+
location: { kind: 'step', step: { step: data.step } },
|
|
193
|
+
}) as unknown as ChatConversationViewNode;
|
|
194
|
+
const steer = {
|
|
195
|
+
key: 'steer', id: 'steer', kind: 'steering', target: 'chat', data: { content: [{ type: 'text', text: '请改方向' }] },
|
|
196
|
+
anchorSeq: 15, visibility: 'visible', location: { kind: 'turn', turn: { turn: 1 } },
|
|
197
|
+
} as unknown as ChatConversationViewNode;
|
|
198
|
+
const nodes = new Map<string, ChatConversationViewNode>([
|
|
199
|
+
['a', assistant('a', 10, first)],
|
|
200
|
+
['steer', steer],
|
|
201
|
+
['b', assistant('b', 20, second)],
|
|
202
|
+
]);
|
|
203
|
+
const flow = readerFlow({ key: 'turn:1', turn: 1, keys: ['a', 'steer', 'b'] }, undefined, key => nodes.get(key));
|
|
204
|
+
const items = presentLiveTurn(segmentLiveTurn(flow, key => nodes.get(key)), open);
|
|
205
|
+
assert.deepEqual(kinds(items), [
|
|
206
|
+
['open', 'a:reasoning:0'], ['open', 'a:body:1'],
|
|
207
|
+
['user', 'steer'],
|
|
208
|
+
['open', 'b:reasoning:0'],
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('disabling auto-fold keeps all steps open as plain streaming steps', () => {
|
|
213
|
+
const steps = [reasoning('1'), body('2'), tool('3'), reasoning('4'), tool('5'), reasoning('6')];
|
|
214
|
+
const items = presentLiveTurn(steps, open, false);
|
|
215
|
+
assert.equal(items.some(item => item.kind === 'fold'), false);
|
|
216
|
+
assert.deepEqual(items.map(item => item.key), ['1', '2', '3', '4', '5', '6']);
|
|
217
|
+
assert.deepEqual(items.map(item => item.kind), ['open', 'open', 'open', 'open', 'open', 'open']);
|
|
218
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import type { Code } from 'mdast';
|
|
4
|
+
import {
|
|
5
|
+
isMcpAppCodeBlock,
|
|
6
|
+
extractMcpAppTitle,
|
|
7
|
+
extractMcpAppHeight,
|
|
8
|
+
ensureHtmlDocument,
|
|
9
|
+
extractHtmlTitle,
|
|
10
|
+
formatReceiptPrompt,
|
|
11
|
+
findComposerTextarea,
|
|
12
|
+
fillComposerDom,
|
|
13
|
+
} from '../src/client/mcp-app.js';
|
|
14
|
+
import { parseGfmWithMath } from '../src/client/markdown/parse.js';
|
|
15
|
+
|
|
16
|
+
test('isMcpAppCodeBlock detects all valid MCP App code block formats', () => {
|
|
17
|
+
// Direct lang tags
|
|
18
|
+
assert.equal(isMcpAppCodeBlock('mcp-app', null, '<div>test</div>'), true);
|
|
19
|
+
assert.equal(isMcpAppCodeBlock('mcpapp', null, '<div>test</div>'), true);
|
|
20
|
+
assert.equal(isMcpAppCodeBlock('mcp-ui', null, '<div>test</div>'), true);
|
|
21
|
+
assert.equal(isMcpAppCodeBlock('mcp', null, '<div>test</div>'), true);
|
|
22
|
+
assert.equal(isMcpAppCodeBlock('mcp-app:html', null, '<div>test</div>'), true);
|
|
23
|
+
assert.equal(isMcpAppCodeBlock('html:mcp-app', null, '<div>test</div>'), true);
|
|
24
|
+
|
|
25
|
+
// Meta indicators
|
|
26
|
+
assert.equal(isMcpAppCodeBlock('html', 'mcp-app title="测试"', '<div>test</div>'), true);
|
|
27
|
+
assert.equal(isMcpAppCodeBlock(null, 'sep-1865', '<div>test</div>'), true);
|
|
28
|
+
|
|
29
|
+
// Content heuristic for SEP-1865 JSON-RPC
|
|
30
|
+
const quizSnippet = `
|
|
31
|
+
window.parent.postMessage({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
method: "ui/submit",
|
|
34
|
+
params: { choice: "A" }
|
|
35
|
+
}, "*");
|
|
36
|
+
`;
|
|
37
|
+
assert.equal(isMcpAppCodeBlock('html', null, quizSnippet), true);
|
|
38
|
+
|
|
39
|
+
// Negative cases
|
|
40
|
+
assert.equal(isMcpAppCodeBlock('typescript', null, 'const x = 1;'), false);
|
|
41
|
+
assert.equal(isMcpAppCodeBlock('json', null, '{"a": 1}'), false);
|
|
42
|
+
assert.equal(isMcpAppCodeBlock('html', null, '<h1>Hello World</h1>'), false);
|
|
43
|
+
assert.equal(isMcpAppCodeBlock(null, null, ''), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('extractMcpAppTitle extracts title from meta or html content', () => {
|
|
47
|
+
assert.equal(extractMcpAppTitle('title="双盲方案盲选评测器"', '<div>test</div>'), '双盲方案盲选评测器');
|
|
48
|
+
assert.equal(extractMcpAppTitle("title='架构方案对比'", '<div>test</div>'), '架构方案对比');
|
|
49
|
+
assert.equal(extractMcpAppTitle(null, '<!DOCTYPE html><html><head><title>网页标题评测</title></head></html>'), '网页标题评测');
|
|
50
|
+
assert.equal(extractMcpAppTitle(null, '<div>无标题内容</div>'), undefined);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('extractMcpAppHeight parses explicit height budget from meta', () => {
|
|
54
|
+
assert.equal(extractMcpAppHeight('title="看板" height=600'), 600);
|
|
55
|
+
assert.equal(extractMcpAppHeight('height="550px"'), 550);
|
|
56
|
+
assert.equal(extractMcpAppHeight('height=50'), undefined); // too small
|
|
57
|
+
assert.equal(extractMcpAppHeight('title="普通"'), undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('extractHtmlTitle extracts document title accurately', () => {
|
|
61
|
+
assert.equal(extractHtmlTitle('<title>我的测试 App</title>'), '我的测试 App');
|
|
62
|
+
assert.equal(extractHtmlTitle('<title class="main" > 性能看板 </title>'), '性能看板');
|
|
63
|
+
assert.equal(extractHtmlTitle('<div>没有标题</div>'), undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('ensureHtmlDocument wraps bare fragments and preserves complete documents with theme bridge', () => {
|
|
67
|
+
const completeHtml = '<!DOCTYPE html>\n<html><head><title>Test</title></head><body>Content</body></html>';
|
|
68
|
+
const processed = ensureHtmlDocument(completeHtml, 'dark');
|
|
69
|
+
assert.ok(processed.includes('<title>Test</title>'));
|
|
70
|
+
assert.ok(processed.includes('<body>Content</body>'));
|
|
71
|
+
assert.ok(processed.includes('applyTheme'));
|
|
72
|
+
|
|
73
|
+
const fragment = '<div class="quiz"><h2>Question</h2></div>';
|
|
74
|
+
const wrapped = ensureHtmlDocument(fragment, 'dark');
|
|
75
|
+
assert.ok(wrapped.startsWith('<!DOCTYPE html>'));
|
|
76
|
+
assert.ok(wrapped.includes('<html lang="zh-CN" data-theme="dark" class="dark"'));
|
|
77
|
+
assert.ok(wrapped.includes(fragment));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('markdown parsing correctly extracts MCP App code fence and prepares for McpAppFrame mounting', () => {
|
|
81
|
+
const markdown = `
|
|
82
|
+
这里是为您生成的方案评测器,请在下方直接点击交互:
|
|
83
|
+
|
|
84
|
+
\`\`\`mcp-app title="双盲方案盲选评测器"
|
|
85
|
+
<!DOCTYPE html>
|
|
86
|
+
<html lang="zh-CN">
|
|
87
|
+
<head>
|
|
88
|
+
<meta charset="UTF-8">
|
|
89
|
+
<title>双盲方案盲选评测器</title>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<div class="quiz-card">
|
|
93
|
+
<h3>题目</h3>
|
|
94
|
+
<button onclick="submitChoice('B')">B: 延迟消息队列</button>
|
|
95
|
+
</div>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
如有其他疑问,请随时告诉我。
|
|
101
|
+
`.trim();
|
|
102
|
+
|
|
103
|
+
const root = parseGfmWithMath(markdown);
|
|
104
|
+
const codeNode = root.children.find((child): child is Code => child.type === 'code');
|
|
105
|
+
assert.ok(codeNode, 'Must contain a code block node');
|
|
106
|
+
assert.equal(codeNode.lang, 'mcp-app');
|
|
107
|
+
assert.equal(codeNode.meta, 'title="双盲方案盲选评测器"');
|
|
108
|
+
assert.equal(isMcpAppCodeBlock(codeNode.lang, codeNode.meta, codeNode.value), true);
|
|
109
|
+
assert.equal(extractMcpAppTitle(codeNode.meta, codeNode.value), '双盲方案盲选评测器');
|
|
110
|
+
assert.ok(ensureHtmlDocument(codeNode.value).includes('延迟消息队列'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('formatReceiptPrompt formats clean, natural conversational text without extra whitespace or raw JSON dump', () => {
|
|
114
|
+
const quizParams = {
|
|
115
|
+
task: 'architecture_quiz_completed',
|
|
116
|
+
choice: 'B',
|
|
117
|
+
desc: '延迟消息队列',
|
|
118
|
+
};
|
|
119
|
+
const prompt = formatReceiptPrompt(quizParams, '双盲方案盲选评测器');
|
|
120
|
+
assert.equal(prompt, '我在「双盲方案盲选评测器」中选择了:B(延迟消息队列)。请根据我的选择继续。');
|
|
121
|
+
assert.ok(!prompt.includes('\n'), 'Must not contain multiline newlines');
|
|
122
|
+
assert.ok(!prompt.includes('{'), 'Must not dump raw JSON braces');
|
|
123
|
+
|
|
124
|
+
const variantParams = {
|
|
125
|
+
selectedVariant: 'Skill_v2',
|
|
126
|
+
score: 95,
|
|
127
|
+
};
|
|
128
|
+
assert.equal(formatReceiptPrompt(variantParams), '我在方案评测中选择了:Skill_v2,得分:95。请根据该方案继续分析。');
|
|
129
|
+
|
|
130
|
+
const actionParams = {
|
|
131
|
+
action: 'reset_counter',
|
|
132
|
+
payload: { count: 0 },
|
|
133
|
+
};
|
|
134
|
+
assert.equal(formatReceiptPrompt(actionParams, '计数器'), '[计数器] 已完成 reset_counter: {"count":0}');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('ensureHtmlDocument injects theme bridge and auto-height reporter into complete documents', () => {
|
|
138
|
+
const completeHtml = '<!DOCTYPE html>\n<html><head><title>Demo</title></head><body><div class="box">hi</div></body></html>';
|
|
139
|
+
const processed = ensureHtmlDocument(completeHtml, 'dark');
|
|
140
|
+
assert.ok(processed.includes('<title>Demo</title>'));
|
|
141
|
+
assert.ok(processed.includes('<body><div class="box">hi</div></body>'));
|
|
142
|
+
// Theme bridge must apply the initial theme and listen for live host updates
|
|
143
|
+
// (host pushes ui/initialize result / host-context-changed carrying hostContext.theme).
|
|
144
|
+
assert.ok(processed.includes('applyTheme("dark")'));
|
|
145
|
+
assert.ok(processed.includes('hostContext'));
|
|
146
|
+
// Auto-height reporter must observe the body and emit SEP-1865 resize events.
|
|
147
|
+
assert.ok(processed.includes('ResizeObserver'));
|
|
148
|
+
assert.ok(processed.includes('ui/resize'));
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('findComposerTextarea is importable and null-safe without a DOM', () => {
|
|
152
|
+
assert.equal(findComposerTextarea(undefined as unknown as Document), null);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('fillComposerDom refuses without a DOM instead of throwing', () => {
|
|
156
|
+
assert.equal(fillComposerDom('hello'), false);
|
|
157
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { formatMessageClock, formatRanFor, formatRunDuration } from '../src/client/message-chrome.ts';
|
|
4
|
+
|
|
5
|
+
test('run duration matches official Chat labels', () => {
|
|
6
|
+
assert.equal(formatRunDuration(0), '0秒');
|
|
7
|
+
assert.equal(formatRunDuration(19_000), '19秒');
|
|
8
|
+
assert.equal(formatRunDuration(118_000), '1分58秒');
|
|
9
|
+
assert.equal(formatRunDuration(65_000), '1分05秒');
|
|
10
|
+
assert.equal(formatRanFor(118_000), '用时 1分58秒');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('same calendar day is HH:mm', () => {
|
|
14
|
+
const now = new Date(2026, 8, 11, 16, 40).getTime();
|
|
15
|
+
const time = new Date(2026, 8, 11, 16, 36).getTime();
|
|
16
|
+
assert.equal(formatMessageClock(time, now), '16:36');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('earlier this year includes month and day', () => {
|
|
20
|
+
const now = new Date(2026, 8, 11, 16, 40).getTime();
|
|
21
|
+
const time = new Date(2026, 7, 3, 9, 5).getTime();
|
|
22
|
+
assert.equal(formatMessageClock(time, now), '8月3日 09:05');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('other years include the year', () => {
|
|
26
|
+
const now = new Date(2026, 8, 11, 16, 40).getTime();
|
|
27
|
+
const time = new Date(2025, 11, 31, 23, 4).getTime();
|
|
28
|
+
assert.equal(formatMessageClock(time, now), '2025年12月31日 23:04');
|
|
29
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { memo } from 'react';
|
|
4
|
+
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import { mirrorOfficialSlot, readerTailMatch, type CompositionRegistry } from '../src/client/official-slots.js';
|
|
6
|
+
|
|
7
|
+
function registry(kind: 'keyed' | 'chain' | 'list' = 'keyed') {
|
|
8
|
+
const core = new SlotCore();
|
|
9
|
+
const register = (options: object, component: unknown) => (core.register as Function)(options, component) as () => void;
|
|
10
|
+
register({ name: 'root', children: {
|
|
11
|
+
source: { kind, scope: 'session' },
|
|
12
|
+
reader: { kind, scope: 'session' },
|
|
13
|
+
} }, () => null);
|
|
14
|
+
const slots: CompositionRegistry = {
|
|
15
|
+
register,
|
|
16
|
+
spec: key => core.spec(key as never),
|
|
17
|
+
entriesOfSlot: key => core.entriesOfSlot(key),
|
|
18
|
+
subscribe: (key, fn) => core.subscribe(key, fn),
|
|
19
|
+
inject: () => { throw new Error('not used by the subtree test'); },
|
|
20
|
+
};
|
|
21
|
+
return { core, slots, register };
|
|
22
|
+
}
|
|
23
|
+
const changed = () => new Promise(resolve => queueMicrotask(resolve));
|
|
24
|
+
|
|
25
|
+
test('official present cards retain their data while the existing Reader produced row stays unique', () => {
|
|
26
|
+
const original = Object.freeze({ produced: Object.freeze(['/work/code.ts']), presented: Object.freeze([{ path: '/work/report.pdf', seq: 42 }]), future: 'retained' });
|
|
27
|
+
assert.deepEqual(readerTailMatch(original, ['/work/code.ts']), { produced: [], presented: original.presented, future: 'retained' });
|
|
28
|
+
assert.equal(readerTailMatch(original), original);
|
|
29
|
+
assert.deepEqual(readerTailMatch({ produced: ['/work/code.ts', '/work/new.ts'], presented: [] }, ['/work/code.ts']), { produced: ['/work/new.ts'], presented: [] });
|
|
30
|
+
assert.deepEqual(original.produced, ['/work/code.ts']);
|
|
31
|
+
const future = { artifacts: ['/work/report.pdf'] };
|
|
32
|
+
assert.equal(readerTailMatch(future), future);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('official memo components retain injection, store, locale and selector metadata', () => {
|
|
36
|
+
const { core, slots, register } = registry('chain');
|
|
37
|
+
const component = memo(() => null);
|
|
38
|
+
const inject = () => ({ hooks: { status: { getSnapshot: () => 1, subscribe: () => () => {} } } });
|
|
39
|
+
const select = (owner: unknown) => owner;
|
|
40
|
+
const store = { create: () => ({}) };
|
|
41
|
+
register({ name: 'source', select, inject, store, locale: 'feedback', priority: -7 }, component);
|
|
42
|
+
const before = core.entriesOfSlot('source')[0];
|
|
43
|
+
const stop = mirrorOfficialSlot(slots, 'source', 'reader', 'reader.children');
|
|
44
|
+
const mirrored = core.entriesOfSlot('reader')[0];
|
|
45
|
+
assert.equal(mirrored.component, component);
|
|
46
|
+
assert.equal(mirrored.inject, inject);
|
|
47
|
+
assert.equal(mirrored.select, select);
|
|
48
|
+
assert.equal(mirrored.store, store);
|
|
49
|
+
assert.equal(mirrored.locale, 'feedback');
|
|
50
|
+
assert.equal(mirrored.options.priority, -7);
|
|
51
|
+
assert.equal(core.entriesOfSlot('source')[0], before);
|
|
52
|
+
stop();
|
|
53
|
+
assert.equal(core.entriesOfSlot('reader').length, 0);
|
|
54
|
+
assert.equal(core.entriesOfSlot('source')[0], before);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('nested declarations use distinct seats and are disposed without touching the source', () => {
|
|
58
|
+
const { core, slots, register } = registry();
|
|
59
|
+
const hookContext = { hooks: { turnData: () => () => 'live' } };
|
|
60
|
+
register({ name: 'source', key: 'read-image', children: { images: { kind: 'single', scope: 'session', inject: hookContext } } }, () => null);
|
|
61
|
+
const child = memo(() => null);
|
|
62
|
+
register({ name: 'images', locale: 'images' }, child);
|
|
63
|
+
const before = core.snapshot('source');
|
|
64
|
+
const stop = mirrorOfficialSlot(slots, 'source', 'reader', 'reader.children');
|
|
65
|
+
assert.equal(core.entriesOfSlot('reader.children/images')[0].component, child);
|
|
66
|
+
assert.equal(slots.spec('reader.children/images')?.inject, hookContext);
|
|
67
|
+
assert.deepEqual(core.snapshot('source'), before);
|
|
68
|
+
stop();
|
|
69
|
+
assert.equal(slots.spec('reader.children/images'), undefined);
|
|
70
|
+
assert.equal(core.entriesOfSlot('images')[0].component, child);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('new registrations do not remount existing cards; winner changes and unload reconcile', async () => {
|
|
74
|
+
const { core, slots, register } = registry();
|
|
75
|
+
const first = memo(() => null);
|
|
76
|
+
const second = memo(() => null);
|
|
77
|
+
const fallback = memo(() => null);
|
|
78
|
+
register({ name: 'source', key: 'read' }, first);
|
|
79
|
+
const stop = mirrorOfficialSlot(slots, 'source', 'reader', 'reader.children');
|
|
80
|
+
const originalSeat = core.entriesOfSlot('reader')[0];
|
|
81
|
+
const removeNew = register({ name: 'source', key: 'new-official-tool' }, second);
|
|
82
|
+
await changed();
|
|
83
|
+
assert.equal(core.entriesOfSlot('reader').find(e => e.options.key === 'read'), originalSeat);
|
|
84
|
+
assert.equal(core.entriesOfSlot('reader').length, 2);
|
|
85
|
+
const restore = register({ name: 'source', key: 'read', priority: -1 }, fallback);
|
|
86
|
+
await changed();
|
|
87
|
+
assert.equal(core.entriesOfSlot('reader').find(e => e.options.key === 'read')?.component, fallback);
|
|
88
|
+
restore();
|
|
89
|
+
removeNew();
|
|
90
|
+
await changed();
|
|
91
|
+
assert.equal(core.entriesOfSlot('reader').length, 1);
|
|
92
|
+
assert.equal(core.entriesOfSlot('reader')[0].component, first);
|
|
93
|
+
stop();
|
|
94
|
+
register({ name: 'source', key: 'after-reader-unload' }, second);
|
|
95
|
+
await changed();
|
|
96
|
+
assert.equal(core.entriesOfSlot('reader').length, 0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('partial setup rolls back its own registrations on a nested contract failure', () => {
|
|
100
|
+
const { core, slots, register } = registry();
|
|
101
|
+
register({ name: 'source', key: 'nested', children: { detail: { kind: 'single', scope: 'session' } } }, () => null);
|
|
102
|
+
register({ name: 'detail' }, () => null);
|
|
103
|
+
const failing = { ...slots, register(options: Parameters<CompositionRegistry['register']>[0], component: unknown) {
|
|
104
|
+
if (options.name === 'reader.children/detail') throw new Error('fixture failure');
|
|
105
|
+
return slots.register(options, component);
|
|
106
|
+
} };
|
|
107
|
+
assert.throws(() => mirrorOfficialSlot(failing, 'source', 'reader', 'reader.children'), /fixture failure/);
|
|
108
|
+
assert.equal(core.entriesOfSlot('reader').length, 0);
|
|
109
|
+
assert.equal(slots.spec('reader.children/detail'), undefined);
|
|
110
|
+
assert.equal(core.entriesOfSlot('source').length, 1);
|
|
111
|
+
assert.equal(core.entriesOfSlot('detail').length, 1);
|
|
112
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
deliverableOpenModeOf,
|
|
8
|
+
fileAddressFor,
|
|
9
|
+
isFolderOpenPath,
|
|
10
|
+
modeFromSnapshot,
|
|
11
|
+
openDeliverableFile,
|
|
12
|
+
resolveOpenWorkspacePath,
|
|
13
|
+
} from '../src/client/open-file.ts';
|
|
14
|
+
|
|
15
|
+
test('index still routes opens through #13 open-mode, not a forced sidebar', () => {
|
|
16
|
+
const index = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../src/client/index.tsx'), 'utf8');
|
|
17
|
+
assert.match(index, /modeFromSnapshot\(prefs\)/);
|
|
18
|
+
assert.match(index, /openDeliverableFile/);
|
|
19
|
+
assert.doesNotMatch(index, /openOnDesktop/);
|
|
20
|
+
assert.doesNotMatch(index, /sidebarRight\.openResource!\(address\)/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('deliverable open mode defaults to external', () => {
|
|
24
|
+
assert.equal(deliverableOpenModeOf(undefined), 'external');
|
|
25
|
+
assert.equal(deliverableOpenModeOf('external'), 'external');
|
|
26
|
+
assert.equal(deliverableOpenModeOf('sidebar'), 'sidebar');
|
|
27
|
+
assert.equal(deliverableOpenModeOf('other'), 'external');
|
|
28
|
+
assert.equal(modeFromSnapshot(undefined), 'external');
|
|
29
|
+
assert.equal(modeFromSnapshot({ getSnapshot: () => ({}) }), 'external');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('folder affordances stay on the OS path', () => {
|
|
33
|
+
assert.equal(isFolderOpenPath('.'), true);
|
|
34
|
+
assert.equal(isFolderOpenPath(''), true);
|
|
35
|
+
assert.equal(isFolderOpenPath('src/a.ts'), false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('fileAddressFor matches official session-scoped grammar', () => {
|
|
39
|
+
assert.equal(
|
|
40
|
+
fileAddressFor('s1', '/repo', 'src/client/Reader.tsx'),
|
|
41
|
+
'dsh-resource://file/session/s1/src/client/Reader.tsx',
|
|
42
|
+
);
|
|
43
|
+
assert.equal(
|
|
44
|
+
fileAddressFor('s1', '/repo', '/repo/src/a.ts'),
|
|
45
|
+
'dsh-resource://file/session/s1/src/a.ts',
|
|
46
|
+
);
|
|
47
|
+
assert.equal(
|
|
48
|
+
fileAddressFor('id with space', undefined, 'notes and more.md'),
|
|
49
|
+
'dsh-resource://file/session/id%20with%20space/notes%20and%20more.md',
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('sidebar mode opens the right-panel address; external uses the system opener', async () => {
|
|
54
|
+
const opened: string[] = [];
|
|
55
|
+
const sidebar: string[] = [];
|
|
56
|
+
const resolve = (cwd: string | undefined, path: string) => `${cwd ?? ''}/${path}`;
|
|
57
|
+
|
|
58
|
+
assert.equal(await openDeliverableFile({
|
|
59
|
+
path: 'a.ts',
|
|
60
|
+
mode: 'external',
|
|
61
|
+
sessionId: 's1',
|
|
62
|
+
cwd: '/repo',
|
|
63
|
+
resolveWorkspacePath: resolve,
|
|
64
|
+
openExternal: async (path) => { opened.push(path); },
|
|
65
|
+
openSidebar: (address) => { sidebar.push(address); },
|
|
66
|
+
}), 'external');
|
|
67
|
+
assert.deepEqual(opened, ['/repo/a.ts']);
|
|
68
|
+
assert.deepEqual(sidebar, []);
|
|
69
|
+
|
|
70
|
+
assert.equal(await openDeliverableFile({
|
|
71
|
+
path: 'a.ts',
|
|
72
|
+
mode: 'sidebar',
|
|
73
|
+
sessionId: 's1',
|
|
74
|
+
cwd: '/repo',
|
|
75
|
+
resolveWorkspacePath: resolve,
|
|
76
|
+
openExternal: async (path) => { opened.push(path); },
|
|
77
|
+
openSidebar: (address) => { sidebar.push(address); },
|
|
78
|
+
}), 'sidebar');
|
|
79
|
+
assert.deepEqual(sidebar, ['dsh-resource://file/session/s1/a.ts']);
|
|
80
|
+
assert.deepEqual(opened, ['/repo/a.ts']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('missing Sidebar API warns and falls back to the system opener', async () => {
|
|
84
|
+
const warnings: string[] = [];
|
|
85
|
+
const opened: string[] = [];
|
|
86
|
+
assert.equal(await openDeliverableFile({
|
|
87
|
+
path: 'a.ts',
|
|
88
|
+
mode: 'sidebar',
|
|
89
|
+
sessionId: 's1',
|
|
90
|
+
cwd: '/repo',
|
|
91
|
+
resolveWorkspacePath: (_cwd, path) => path,
|
|
92
|
+
openExternal: async (path) => { opened.push(path); },
|
|
93
|
+
warn: (message) => { warnings.push(message); },
|
|
94
|
+
}), 'external');
|
|
95
|
+
assert.deepEqual(opened, ['a.ts']);
|
|
96
|
+
assert.match(warnings[0] ?? '', /sidebarRight.openResource is not available/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('workspace folder clicks stay external even when sidebar mode is on', async () => {
|
|
100
|
+
const sidebar: string[] = [];
|
|
101
|
+
const opened: string[] = [];
|
|
102
|
+
assert.equal(await openDeliverableFile({
|
|
103
|
+
path: '.',
|
|
104
|
+
mode: 'sidebar',
|
|
105
|
+
sessionId: 's1',
|
|
106
|
+
cwd: '/repo',
|
|
107
|
+
resolveWorkspacePath: (cwd, path) => `${cwd}/${path}`,
|
|
108
|
+
openExternal: async (path) => { opened.push(path); },
|
|
109
|
+
openSidebar: (address) => { sidebar.push(address); },
|
|
110
|
+
}), 'external');
|
|
111
|
+
assert.deepEqual(opened, ['/repo']);
|
|
112
|
+
assert.deepEqual(sidebar, []);
|
|
113
|
+
assert.equal(resolveOpenWorkspacePath('/repo', '.', (cwd, path) => `${cwd}/${path}`), '/repo');
|
|
114
|
+
});
|