@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,274 @@
|
|
|
1
|
+
import { memo, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { ToolCallBlock, ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
3
|
+
import type { DiffHunk, ReadBlockLine, SearchFileGroup } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import { DiffBlock, DisclosureRow, JsonTree, ReadBlock, SearchBlock, TerminalBlock, WebBlock,
|
|
5
|
+
IconApiOutlineRegular, IconBrowseOutlineRegular, IconEditOutlineRegular, IconSearchOutlineRegular, IconSkillOutlineRegular, IconSparkleRegular } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
6
|
+
import { Blocks, contentBlocks } from './Blocks.js';
|
|
7
|
+
import { OfficialTool } from './OfficialContent.js';
|
|
8
|
+
import { ProcessFragment } from './motion.js';
|
|
9
|
+
import { activityPhase, activitySummary, callDiffHunks, diffTotals, executionFacts, objectValue, toolIdentity } from './tool-activity.js';
|
|
10
|
+
import type { ToolActivityEntry, ToolCategory, ToolPhase } from './tool-activity.js';
|
|
11
|
+
import type { BlockRenderProps } from './types.js';
|
|
12
|
+
import { classifyTool, toolRowModel, VARIANT_TITLES } from './native/tool-call-model.js';
|
|
13
|
+
import { McpAppFrame, StreamingMcpAppPlaceholder } from './McpAppFrame.js';
|
|
14
|
+
import { diffBlockLabels, jsonTreeLabels, readBlockLabels, searchBlockLabels, terminalBlockLabels, webBlockLabels } from './primitive-labels.js';
|
|
15
|
+
import css from './Reader.module.css';
|
|
16
|
+
|
|
17
|
+
const LABEL: Record<ToolPhase, string> = { preparing: '输入生成中', running: '执行中', returned: '已返回', succeeded: '已完成', failed: '失败', interrupted: '已中断' };
|
|
18
|
+
const ICONS = { write: IconEditOutlineRegular, read: IconBrowseOutlineRegular, terminal: IconApiOutlineRegular, search: IconSearchOutlineRegular, web: IconSearchOutlineRegular, code: IconApiOutlineRegular, other: IconSparkleRegular } satisfies Record<ToolCategory, unknown>;
|
|
19
|
+
const number = new Intl.NumberFormat('zh-CN');
|
|
20
|
+
const language = (path: string | undefined) => path?.split('.').at(-1);
|
|
21
|
+
const duration = (ms: number) => ms < 1000 ? `${Math.round(ms)} 毫秒` : `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} 秒`;
|
|
22
|
+
|
|
23
|
+
function generatedInput(content: string, target: string | undefined, preparing: boolean) {
|
|
24
|
+
const lines = content.split('\n').map((text, index) => ({ number: index + 1, text }));
|
|
25
|
+
const visible = preparing ? lines.slice(-12) : lines.slice(0, 1600);
|
|
26
|
+
return <div data-reader-tool-file><p className={css.toolDetailNote}>{preparing ? '正在生成的输入 · 尚未执行 · 末尾 12 行' : '工具输入中的文件内容'}{!preparing && lines.length > visible.length ? ' · 预览前 1,600 行,完整内容在原始数据中' : ''}</p>
|
|
27
|
+
<ReadBlock label={target ?? '文件内容'} lang={language(target)} lines={visible} totalLines={lines.length} maxLines={16} labels={readBlockLabels} />
|
|
28
|
+
</div>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function InputView({ model, preparing, fillComposer }: { model: ReturnType<typeof activitySummary>; preparing: boolean; fillComposer: BlockRenderProps['fillComposer'] }) {
|
|
32
|
+
if ((model.name === 'render_ui' || model.name === 'show_widget') && typeof model.args?.html === 'string') {
|
|
33
|
+
return preparing
|
|
34
|
+
? <StreamingMcpAppPlaceholder title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} />
|
|
35
|
+
: <McpAppFrame html={model.args.html as string} title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} fillComposer={fillComposer} />;
|
|
36
|
+
}
|
|
37
|
+
if (model.content) return generatedInput(model.content, model.target, preparing);
|
|
38
|
+
if (model.command) return <div data-reader-tool-terminal><p className={css.toolDetailNote}>{preparing ? '正在生成命令 · 尚未执行' : '提交的命令'}</p><TerminalBlock command={model.command} cwd={model.cwd} labels={terminalBlockLabels} /></div>;
|
|
39
|
+
return <JsonTree data={model.args} label={preparing ? '已收到的输入字段' : '工具输入'} labels={jsonTreeLabels} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readLines(value: unknown): ReadBlockLine[] | null {
|
|
43
|
+
if (!Array.isArray(value)) return null;
|
|
44
|
+
const lines: ReadBlockLine[] = [];
|
|
45
|
+
for (const item of value) {
|
|
46
|
+
const row = objectValue(item);
|
|
47
|
+
if (typeof row?.number !== 'number' || !Number.isInteger(row.number) || typeof row.text !== 'string') return null;
|
|
48
|
+
lines.push({ number: row.number, text: row.text });
|
|
49
|
+
}
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A step past this reads as slow rather than merely in progress. */
|
|
54
|
+
const SLOW_TOOL_MS = 10_000;
|
|
55
|
+
|
|
56
|
+
function diffHunks(value: unknown): DiffHunk[] | null {
|
|
57
|
+
if (!Array.isArray(value) || value.length === 0) return null;
|
|
58
|
+
const diffs: DiffHunk[] = [];
|
|
59
|
+
for (const item of value) {
|
|
60
|
+
const row = objectValue(item);
|
|
61
|
+
if (typeof row?.path !== 'string' || (row.oldText !== null && typeof row.oldText !== 'string') || typeof row.newText !== 'string') return null;
|
|
62
|
+
diffs.push({ path: row.path, oldText: row.oldText, newText: row.newText });
|
|
63
|
+
}
|
|
64
|
+
return diffs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function searchFiles(value: unknown): SearchFileGroup[] | null {
|
|
68
|
+
if (!Array.isArray(value)) return null;
|
|
69
|
+
const files: SearchFileGroup[] = [];
|
|
70
|
+
for (const item of value) {
|
|
71
|
+
const row = objectValue(item);
|
|
72
|
+
if (typeof row?.path !== 'string' || !Array.isArray(row.matches)) return null;
|
|
73
|
+
const matches: { lineNumber: number; line: string }[] = [];
|
|
74
|
+
for (const itemMatch of row.matches) {
|
|
75
|
+
const match = objectValue(itemMatch);
|
|
76
|
+
if (typeof match?.lineNumber !== 'number' || !Number.isInteger(match.lineNumber) || typeof match.line !== 'string') return null;
|
|
77
|
+
matches.push({ lineNumber: match.lineNumber, line: match.line });
|
|
78
|
+
}
|
|
79
|
+
files.push({ path: row.path, matches });
|
|
80
|
+
}
|
|
81
|
+
return files;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ResultView(props: BlockRenderProps & { entry: ToolActivityEntry; model: ReturnType<typeof activitySummary>; phase: ToolPhase }) {
|
|
85
|
+
const { official, entry, model } = props;
|
|
86
|
+
const fallback = <ResultFallback {...props} />;
|
|
87
|
+
if (!official || !entry.block || model.name === 'render_ui' || model.name === 'show_widget') return fallback;
|
|
88
|
+
return <OfficialTool {...props} official={official} block={entry.block} toolName={model.name} cwd={props.cwd ?? model.cwd} fallback={fallback} />;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ResultFallback({ entry, model, phase, ...render }: BlockRenderProps & { entry: ToolActivityEntry; model: ReturnType<typeof activitySummary>; phase: ToolPhase }) {
|
|
92
|
+
if ((model.name === 'render_ui' || model.name === 'show_widget') && typeof model.args?.html === 'string') {
|
|
93
|
+
return <McpAppFrame html={model.args.html as string} title={typeof model.args.title === 'string' ? (model.args.title as string) : undefined} fillComposer={render.fillComposer} />;
|
|
94
|
+
}
|
|
95
|
+
const block = entry.block;
|
|
96
|
+
if (!block || !('kind' in block)) return <>
|
|
97
|
+
<p className={css.toolDetailNote}>{phase === 'interrupted' ? '已中断,没有工具结果。已生成的输入仍可查看。' : phase === 'preparing' ? '模型正在生成工具输入,工具还未开始执行。' : '工具已开始执行,正在等待结果。'}</p>
|
|
98
|
+
<InputView model={model} preparing={phase === 'preparing'} fillComposer={render.fillComposer} />
|
|
99
|
+
</>;
|
|
100
|
+
const meta = objectValue(block.meta);
|
|
101
|
+
const text = block.content.filter(item => item.type === 'text').map(item => item.text).join('\n');
|
|
102
|
+
if (phase === 'interrupted') return <><p className={css.toolDetailNote}>工具已取消,未正常完成。输入和原始返回记录仍可查看。</p><InputView model={model} preparing={false} fillComposer={render.fillComposer} /><pre className={css.toolRaw}>{text}</pre></>;
|
|
103
|
+
|
|
104
|
+
if (model.category === 'terminal') {
|
|
105
|
+
const facts = executionFacts(block);
|
|
106
|
+
const output = text.replace(/\n\[(?:exit code: \d+|killed by signal: [^\]\n]+)\]$/, '');
|
|
107
|
+
return <div data-reader-tool-terminal><TerminalBlock command={model.command ?? model.name} cwd={model.cwd}
|
|
108
|
+
output={output} exitCode={facts.exitCode} signal={facts.signal} maxLines={18} labels={terminalBlockLabels} /></div>;
|
|
109
|
+
}
|
|
110
|
+
const lines = readLines(meta?.lines);
|
|
111
|
+
if (model.category === 'read' && typeof meta?.path === 'string' && typeof meta.totalLines === 'number' && lines) return <div data-reader-tool-file><ReadBlock label={meta.path} lang={typeof meta.lang === 'string' ? meta.lang : undefined} lines={lines} totalLines={meta.totalLines} maxLines={18} labels={readBlockLabels} /></div>;
|
|
112
|
+
// The host's own hunks when it sent them; otherwise derive them from the call's
|
|
113
|
+
// arguments, which is also what the official row does while a write is pending.
|
|
114
|
+
// Both the counts and this card read the same source, so they cannot disagree.
|
|
115
|
+
const diffs = diffHunks(meta?.diffs) ?? callDiffHunks(block, model.args, model.name);
|
|
116
|
+
if (diffs.length) return <div data-reader-tool-diff><DiffBlock diffs={diffs} maxLines={18} labels={diffBlockLabels} /></div>;
|
|
117
|
+
if (model.category === 'search' && typeof meta?.total === 'number' && typeof meta.truncated === 'boolean') {
|
|
118
|
+
if (meta.shape === 'paths' && Array.isArray(meta.paths) && meta.paths.every((path): path is string => typeof path === 'string')) return <div data-reader-tool-search><SearchBlock kind="paths" paths={meta.paths} total={meta.total} truncated={meta.truncated} maxLines={18} labels={searchBlockLabels} /></div>;
|
|
119
|
+
const files = searchFiles(meta.files);
|
|
120
|
+
if (meta.shape === 'matches' && files) return <div data-reader-tool-search><SearchBlock kind="matches" files={files} total={meta.total} truncated={meta.truncated} maxLines={18} labels={searchBlockLabels} /></div>;
|
|
121
|
+
}
|
|
122
|
+
if (model.category === 'web' && typeof meta?.truncated === 'boolean') {
|
|
123
|
+
if (model.name === 'web_fetch' && typeof meta.url === 'string' && typeof meta.statusCode === 'number') return <div data-reader-tool-web><WebBlock kind="fetch" url={meta.url} statusCode={meta.statusCode} truncated={meta.truncated} labels={webBlockLabels} /></div>;
|
|
124
|
+
if (model.name === 'web_search' && Array.isArray(meta.sources)) {
|
|
125
|
+
const sources = meta.sources.flatMap(source => {
|
|
126
|
+
const item = objectValue(source);
|
|
127
|
+
return typeof item?.url === 'string' ? [{ url: item.url, ...(typeof item.title === 'string' ? { title: item.title } : {}), ...(typeof item.snippet === 'string' ? { snippet: item.snippet } : {}), ...(typeof item.publishedAt === 'string' ? { publishedAt: item.publishedAt } : {}) }] : [];
|
|
128
|
+
});
|
|
129
|
+
if (sources.length === meta.sources.length) return <div data-reader-tool-web><WebBlock kind="search" sources={sources} answer={typeof meta.answer === 'string' ? meta.answer : undefined} truncated={meta.truncated} labels={webBlockLabels} /></div>;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// A trace/export may omit wire presentation. Keep the generated input clearly
|
|
133
|
+
// labelled; it is not proof of an applied diff or a successful file mutation.
|
|
134
|
+
if (model.category === 'write' && model.content && !block.isError) return <>
|
|
135
|
+
<p className={css.toolDetailNote}>文件工具已返回。以下为提交的内容;完整返回记录可在「原始数据」查看。</p>
|
|
136
|
+
{generatedInput(model.content, model.target, false)}
|
|
137
|
+
</>;
|
|
138
|
+
const content: ToolResultNode['content'] = block.content;
|
|
139
|
+
// A code interpreter's product is its result, not prose. Without this branch
|
|
140
|
+
// the output fell through to the generic document path below, which emits it
|
|
141
|
+
// as reader answer text — so a run_code card sat in the transcript outside
|
|
142
|
+
// every fold, the exact block the reader wanted to be able to close.
|
|
143
|
+
if (model.category === 'code') {
|
|
144
|
+
const output = content.filter(item => item.type === 'text').map(item => item.text).join('\n');
|
|
145
|
+
const facts = executionFacts(block);
|
|
146
|
+
if (output.trim()) return <div data-reader-tool-code>
|
|
147
|
+
<TerminalBlock command={model.command ?? model.name} cwd={model.cwd} output={output}
|
|
148
|
+
exitCode={facts.exitCode} signal={facts.signal} maxLines={18} labels={terminalBlockLabels} />
|
|
149
|
+
</div>;
|
|
150
|
+
return <p className={css.toolDetailNote}>代码已执行,没有可展示的输出。</p>;
|
|
151
|
+
}
|
|
152
|
+
if (content.some(item => item.type === 'text')) return <div className={css.toolDocument}><Blocks {...render} blocks={contentBlocks(content).filter(item => item.kind === 'text')} source="tool" /></div>;
|
|
153
|
+
if (content.length) return <p className={css.toolDetailNote}>图片或扩展内容已在对话中单独展示。</p>;
|
|
154
|
+
return <p className={css.toolDetailNote}>工具没有返回可展示的内容。</p>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** One occurrence, keyed by call id all the way from generation to result. */
|
|
158
|
+
export const ToolActivity = memo(function ToolActivityView({ entry, motion, turnClosed, onRead, depth = 0, ...render }: BlockRenderProps & {
|
|
159
|
+
entry: ToolActivityEntry; motion: boolean; turnClosed: boolean; onRead: () => void; depth?: number;
|
|
160
|
+
}) {
|
|
161
|
+
const [open, setOpen] = useState(false);
|
|
162
|
+
const [tab, setTab] = useState<'result' | 'input' | 'raw'>('result');
|
|
163
|
+
const control = useRef<HTMLElement | null>(null);
|
|
164
|
+
const panel = useRef<HTMLDivElement>(null);
|
|
165
|
+
const [selected, setSelected] = useState(false);
|
|
166
|
+
const detailId = useId();
|
|
167
|
+
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
168
|
+
const model = useMemo(() => activitySummary(entry), [entry.block, entry.draft]);
|
|
169
|
+
const phase = activityPhase(entry, turnClosed);
|
|
170
|
+
const heldPreview = useRef({ entry, model, phase });
|
|
171
|
+
if (!selected) heldPreview.current = { entry, model, phase };
|
|
172
|
+
const preview = heldPreview.current;
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
const track = () => {
|
|
175
|
+
const selection = document.getSelection();
|
|
176
|
+
setSelected(!!selection && !selection.isCollapsed && !!selection.anchorNode && !!panel.current?.contains(selection.anchorNode));
|
|
177
|
+
};
|
|
178
|
+
document.addEventListener('selectionchange', track);
|
|
179
|
+
return () => document.removeEventListener('selectionchange', track);
|
|
180
|
+
}, []);
|
|
181
|
+
const facts = executionFacts(entry.block);
|
|
182
|
+
const Icon = model.name === 'skill' ? IconSkillOutlineRegular : ICONS[model.category];
|
|
183
|
+
const block = entry.block;
|
|
184
|
+
const native = block ? toolRowModel(model.name, block) : null;
|
|
185
|
+
const skillName = typeof model.args?.name === 'string' ? model.args.name.split('\n')[0] : model.raw.split('\n')[0];
|
|
186
|
+
const rowTitle = model.name === 'skill' ? 'Skill' : native?.title ?? VARIANT_TITLES[classifyTool(model.name)];
|
|
187
|
+
const rowSummary = phase === 'interrupted' ? '已停止 · 调用记录保留' : model.name === 'skill' ? skillName : native?.errorSummary ?? native?.summary
|
|
188
|
+
?? (classifyTool(model.name) === 'others' ? `${model.name} · ${model.target ?? model.title}` : model.target ?? model.title);
|
|
189
|
+
// Files that changed get the same +/- line counts the official tool row shows,
|
|
190
|
+
// read straight from the diff the host attached to the result.
|
|
191
|
+
const diff = useMemo(() => diffTotals(block, model.args, model.name), [block, model.args, model.name]);
|
|
192
|
+
// The changed lines ride in the row's own expansion, the way the official tool
|
|
193
|
+
// row shows them — one disclosure, not a second floating widget.
|
|
194
|
+
const showState = phase === 'preparing' || phase === 'running' || phase === 'failed' || phase === 'interrupted';
|
|
195
|
+
const running = phase === 'preparing' || phase === 'running';
|
|
196
|
+
// A step that has not returned yet counts its own seconds, so a long command
|
|
197
|
+
// reads as progress rather than as a stall. The clock is idle-rendered off the
|
|
198
|
+
// row's own call time, and disappears the moment the result arrives.
|
|
199
|
+
const [liveMs, setLiveMs] = useState<number | null>(null);
|
|
200
|
+
const openedAt = useRef<number | null>(null);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!running) { openedAt.current = null; setLiveMs(null); return; }
|
|
203
|
+
const stamped = block && 'kind' in block && block.callTime != null ? block.callTime : null;
|
|
204
|
+
const base = stamped ?? (openedAt.current ??= Date.now());
|
|
205
|
+
const tick = () => setLiveMs(Math.max(0, Date.now() - base));
|
|
206
|
+
tick();
|
|
207
|
+
const timer = setInterval(tick, 200);
|
|
208
|
+
return () => clearInterval(timer);
|
|
209
|
+
}, [running, block]);
|
|
210
|
+
const elapsed = block && 'kind' in block && block.callTime != null ? Math.max(0, block.time - block.callTime) : null;
|
|
211
|
+
// A step that ran long keeps showing how long it took after it returns: the
|
|
212
|
+
// number is the point of the readout. A fast step shows nothing once it is done.
|
|
213
|
+
const shownMs = liveMs ?? (elapsed !== null && elapsed >= SLOW_TOOL_MS ? elapsed : null);
|
|
214
|
+
const rawResult = useMemo(() => {
|
|
215
|
+
const value = preview.entry.block;
|
|
216
|
+
return value && 'kind' in value ? JSON.stringify({ content: value.content, isError: value.isError, meta: value.meta }, null, 2) : '';
|
|
217
|
+
}, [preview.entry.block]);
|
|
218
|
+
const tabs = [['result', phase === 'preparing' ? '生成预览' : '结果'], ['input', '输入'], ['raw', '原始数据']] as const;
|
|
219
|
+
const activate = (index: number) => { const item = tabs[(index + tabs.length) % tabs.length]!; setTab(item[0]); tabRefs.current[(index + tabs.length) % tabs.length]?.focus(); };
|
|
220
|
+
if (depth > 6) return <p className={css.meta}>更深的嵌套调用可在原对话查看。</p>;
|
|
221
|
+
return <div ref={element => { control.current = element?.querySelector<HTMLElement>('[data-disclosure-row]') ?? null; }} className={css.toolActivity} data-reader-tool-call={entry.callId} data-tool-phase={phase} data-tool-args-length={model.raw.length} data-tool-category={model.category} data-expanded={open} data-ud-check="reader-tool-activity">
|
|
222
|
+
<DisclosureRow icon={<Icon size={14} />} title={rowTitle} open={open} expandable expandOnRowClick keepContentWhenOpen
|
|
223
|
+
onToggle={() => { onRead(); setOpen(value => !value); }} rowClassName={css.nativeToolRow}
|
|
224
|
+
collapsedContent={<><span className={css.rowSeparator} aria-hidden /><span className={css.nativeToolSummary} title={rowSummary} data-reader-tool-summary
|
|
225
|
+
{...(running ? { 'data-running': '' } : {})}>{rowSummary}</span>
|
|
226
|
+
{shownMs !== null && <span className={css.toolElapsed} data-reader-tool-elapsed
|
|
227
|
+
{...(shownMs >= SLOW_TOOL_MS ? { 'data-slow': '' } : {})}>
|
|
228
|
+
{(shownMs / 1000).toFixed(shownMs < 10000 ? 1 : 0)}s</span>}
|
|
229
|
+
{diff && <span className={css.diffStat} data-reader-diff-stat>
|
|
230
|
+
{diff.added > 0 && <span className={css.diffAdded}>+{number.format(diff.added)}</span>}
|
|
231
|
+
{diff.removed > 0 && <span className={css.diffRemoved}>-{number.format(diff.removed)}</span>}
|
|
232
|
+
</span>}
|
|
233
|
+
{showState && <span className={css.toolState} data-phase={phase}>{LABEL[phase]}</span>}</>} />
|
|
234
|
+
<ProcessFragment open={open} motion={motion} onRead={onRead} returnFocusTo={control} nodeKey={`${entry.key}:detail`} framed>
|
|
235
|
+
<div id={detailId} className={css.toolDetails}>
|
|
236
|
+
<div className={css.toolLedger} aria-live="off">
|
|
237
|
+
<span>工具 · <span className={css.toolEngine}>{model.name}</span></span>
|
|
238
|
+
<span data-reader-tool-progress>{phase === 'preparing' ? `已接收 ${number.format(model.raw.length)} 字符输入` : phase === 'running' ? '已提交 · 等待工具返回' : phase === 'interrupted' ? '已停止 · 输入记录保留' : elapsed !== null ? `执行 ${duration(elapsed)}` : '结果已记录'}</span>
|
|
239
|
+
{facts.exitCode !== undefined && <span>退出码 {facts.exitCode}</span>}
|
|
240
|
+
{facts.signal && <span>信号 {facts.signal}</span>}
|
|
241
|
+
</div>
|
|
242
|
+
<div className={css.toolTabs} role="tablist" aria-label={`${model.title}的执行数据`} onKeyDown={event => {
|
|
243
|
+
const index = tabs.findIndex(item => item[0] === tab);
|
|
244
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') { event.preventDefault(); activate(index + (event.key === 'ArrowRight' ? 1 : -1)); }
|
|
245
|
+
else if (event.key === 'Home' || event.key === 'End') { event.preventDefault(); activate(event.key === 'Home' ? 0 : tabs.length - 1); }
|
|
246
|
+
}}>{tabs.map(([id, title], index) => <button key={id} ref={element => { tabRefs.current[index] = element; }} type="button" role="tab" id={`${detailId}-${id}`} aria-selected={tab === id} aria-controls={`${detailId}-panel`} tabIndex={tab === id ? 0 : -1} onClick={() => setTab(id)}>{title}</button>)}</div>
|
|
247
|
+
<div ref={panel} id={`${detailId}-panel`} className={css.toolPanel} role="tabpanel" aria-labelledby={`${detailId}-${tab}`} tabIndex={0}>
|
|
248
|
+
{selected && <p className={css.toolDetailNote}>为保留选区,预览暂停更新;当前状态见卡片标题。</p>}
|
|
249
|
+
{tab === 'result' && <ResultView {...render} {...preview} />}
|
|
250
|
+
{tab === 'input' && <><InputView model={preview.model} preparing={preview.phase === 'preparing'} fillComposer={render.fillComposer} /><details className={css.detail}><summary>全部输入字段</summary><JsonTree data={preview.model.args} label="输入字段" labels={jsonTreeLabels} /></details></>}
|
|
251
|
+
{tab === 'raw' && <><p className={css.toolDetailNote}>完整记录 · 只读 · 不执行其中的代码</p><h4 className={css.toolRawLabel}>工具输入</h4><pre className={css.toolRaw}>{preview.model.raw || '输入尚未到达'}</pre>{rawResult && <><h4 className={css.toolRawLabel}>工具结果</h4><pre className={css.toolRaw}>{rawResult}</pre></>}</>}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</ProcessFragment>
|
|
255
|
+
{!!block?.subCalls.length && <div className={css.toolChildren} aria-label="子调用">{block.subCalls.map((child, index) => <ToolActivity key={child.callId} {...render}
|
|
256
|
+
entry={{ kind: 'tool', key: `reader-tool:${child.callId}`, callId: child.callId, step: entry.step, order: index, block: child }}
|
|
257
|
+
motion={motion} turnClosed={turnClosed} onRead={onRead} depth={depth + 1} />)}</div>}
|
|
258
|
+
</div>;
|
|
259
|
+
}, (previous, next) => previous.entry.callId === next.entry.callId && previous.entry.block === next.entry.block
|
|
260
|
+
&& previous.entry.draft === next.entry.draft && previous.entry.step === next.entry.step
|
|
261
|
+
&& previous.motion === next.motion && previous.turnClosed === next.turnClosed && previous.depth === next.depth
|
|
262
|
+
&& previous.onRead === next.onRead && previous.renderSlotChain === next.renderSlotChain && previous.loadImage === next.loadImage && previous.fillComposer === next.fillComposer
|
|
263
|
+
&& previous.official === next.official && previous.cwd === next.cwd && previous.openFile === next.openFile);
|
|
264
|
+
|
|
265
|
+
/** Rich media (images, MCP widgets) rendered outside the folded tool ledger. */
|
|
266
|
+
export function ToolMedia({ block, depth = 0, ...render }: BlockRenderProps & { block: ToolCallBlock; depth?: number }) {
|
|
267
|
+
if (depth > 6) return null;
|
|
268
|
+
const settled = 'kind' in block;
|
|
269
|
+
const visible = settled ? contentBlocks(block.content).filter(item => item.kind === 'image' || item.kind === 'other') : [];
|
|
270
|
+
return <>
|
|
271
|
+
{visible.length > 0 && <Blocks {...render} blocks={visible} source="tool" />}
|
|
272
|
+
{block.subCalls.map(child => <ToolMedia key={child.callId} {...render} block={child} depth={depth + 1} />)}
|
|
273
|
+
</>;
|
|
274
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 2px;
|
|
5
|
+
position: relative;
|
|
6
|
+
font-size: 13px;
|
|
7
|
+
line-height: 24px;
|
|
8
|
+
color: var(--dsw-alias-label-tertiary);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.pillButton {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 5px;
|
|
15
|
+
padding: 2px 8px;
|
|
16
|
+
border-radius: 12px;
|
|
17
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
18
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
19
|
+
color: var(--dsw-alias-label-tertiary);
|
|
20
|
+
font-size: 11px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
outline: none;
|
|
23
|
+
transition: all 140ms ease;
|
|
24
|
+
font-family: inherit;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.pillButton:hover, .pillButton[data-active=true] {
|
|
28
|
+
color: var(--dsw-alias-label-primary);
|
|
29
|
+
border-color: var(--dsw-alias-border-l2);
|
|
30
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.timeButton {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 4px;
|
|
37
|
+
height: 28px;
|
|
38
|
+
padding: 6px 8px;
|
|
39
|
+
border: none;
|
|
40
|
+
border-radius: 28px;
|
|
41
|
+
background: transparent;
|
|
42
|
+
color: var(--dsw-alias-label-tertiary);
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
font-variant-numeric: tabular-nums;
|
|
45
|
+
line-height: 24px;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
font-family: inherit;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.timeButton:hover, .timeButton[data-active=true] {
|
|
52
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
53
|
+
color: var(--dsw-alias-label-secondary);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.pillIcon {
|
|
57
|
+
width: 12px;
|
|
58
|
+
height: 12px;
|
|
59
|
+
flex: none;
|
|
60
|
+
opacity: .8;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.metricsPop {
|
|
64
|
+
position: absolute;
|
|
65
|
+
bottom: calc(100% + 8px);
|
|
66
|
+
left: 0;
|
|
67
|
+
width: 260px;
|
|
68
|
+
padding: 12px 14px;
|
|
69
|
+
border-radius: 10px;
|
|
70
|
+
background: var(--dsw-alias-bg-base);
|
|
71
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
72
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);
|
|
73
|
+
backdrop-filter: blur(10px);
|
|
74
|
+
z-index: 50;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
text-align: left;
|
|
79
|
+
animation: popIn 140ms ease-out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes popIn {
|
|
83
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
84
|
+
to { opacity: 1; transform: translateY(0); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.popHeader {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: space-between;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
color: var(--dsw-alias-label-primary);
|
|
94
|
+
padding-bottom: 6px;
|
|
95
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.popSection {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 4px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.popSectionTitle {
|
|
105
|
+
font-size: 10px;
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
text-transform: uppercase;
|
|
108
|
+
letter-spacing: .5px;
|
|
109
|
+
color: var(--dsw-alias-label-tertiary);
|
|
110
|
+
margin-top: 2px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.popGrid {
|
|
114
|
+
display: grid;
|
|
115
|
+
grid-template-columns: 1fr auto;
|
|
116
|
+
gap: 3px 12px;
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
line-height: 18px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.popLabel {
|
|
122
|
+
color: var(--dsw-alias-label-secondary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.popValue {
|
|
126
|
+
color: var(--dsw-alias-label-primary);
|
|
127
|
+
font-variant-numeric: tabular-nums;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
text-align: right;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.popBadge {
|
|
133
|
+
font-size: 10px;
|
|
134
|
+
padding: 0 4px;
|
|
135
|
+
border-radius: 3px;
|
|
136
|
+
background: var(--dsw-alias-border-l1);
|
|
137
|
+
color: var(--dsw-alias-label-secondary);
|
|
138
|
+
margin-left: 4px;
|
|
139
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { memo, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { TurnTailChatData } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
3
|
+
import { formatRanFor, formatRunDuration } from './message-chrome.js';
|
|
4
|
+
import css from './TurnMetrics.module.css';
|
|
5
|
+
|
|
6
|
+
type TurnTokenUsage = NonNullable<TurnTailChatData['tokenUsage']>;
|
|
7
|
+
|
|
8
|
+
export interface TurnMetricsProps {
|
|
9
|
+
usage?: TurnTokenUsage;
|
|
10
|
+
runMs?: number;
|
|
11
|
+
tokensPerSecond?: number;
|
|
12
|
+
ttftMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatTokens(count: number): string {
|
|
16
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(2)}M tok`;
|
|
17
|
+
if (count >= 1000) return `${(count / 1000).toFixed(1)}k tok`;
|
|
18
|
+
return `${count} tok`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const TurnMetrics = memo(function TurnMetrics({
|
|
22
|
+
usage,
|
|
23
|
+
runMs,
|
|
24
|
+
tokensPerSecond,
|
|
25
|
+
ttftMs,
|
|
26
|
+
}: TurnMetricsProps) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const containerRef = useRef<HTMLSpanElement>(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!open) return;
|
|
32
|
+
const onClickOutside = (e: MouseEvent) => {
|
|
33
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
34
|
+
setOpen(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
38
|
+
if (e.key === 'Escape') setOpen(false);
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener('pointerdown', onClickOutside);
|
|
41
|
+
document.addEventListener('keydown', onKeyDown);
|
|
42
|
+
return () => {
|
|
43
|
+
document.removeEventListener('pointerdown', onClickOutside);
|
|
44
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
45
|
+
};
|
|
46
|
+
}, [open]);
|
|
47
|
+
|
|
48
|
+
const totalTokens = usage?.totalTokens;
|
|
49
|
+
const hasTiming = typeof runMs === 'number' && runMs > 0;
|
|
50
|
+
const hasTokens = typeof totalTokens === 'number' && totalTokens > 0;
|
|
51
|
+
if (!hasTiming && !hasTokens) return null;
|
|
52
|
+
|
|
53
|
+
const cacheHitPercent = usage && usage.cacheReadTokens && usage.totalTokens > usage.outputTokens
|
|
54
|
+
? Math.round((usage.cacheReadTokens / (usage.totalTokens - usage.outputTokens)) * 100)
|
|
55
|
+
: null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<span ref={containerRef} className={css.container}>
|
|
59
|
+
{hasTokens && typeof totalTokens === 'number' && (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className={css.pillButton}
|
|
63
|
+
data-active={open}
|
|
64
|
+
onClick={() => setOpen(v => !v)}
|
|
65
|
+
aria-expanded={open}
|
|
66
|
+
title="查看本轮 Token 消耗"
|
|
67
|
+
>
|
|
68
|
+
<svg className={css.pillIcon} viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
|
69
|
+
<ellipse cx="8" cy="4.2" rx="5" ry="2.2" strokeWidth="1.2" />
|
|
70
|
+
<path d="M3 4.2v7.6c0 1.2 2.2 2.2 5 2.2s5-1 5-2.2V4.2" strokeWidth="1.2" />
|
|
71
|
+
<path d="M3 8c0 1.2 2.2 2.2 5 2.2s5-1 5-2.2" strokeWidth="1.2" />
|
|
72
|
+
</svg>
|
|
73
|
+
<span>用量 {formatTokens(totalTokens)}</span>
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
{hasTiming && typeof runMs === 'number' && (
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
className={css.timeButton}
|
|
80
|
+
data-active={open}
|
|
81
|
+
onClick={() => setOpen(v => !v)}
|
|
82
|
+
aria-expanded={open}
|
|
83
|
+
aria-label={formatRanFor(runMs)}
|
|
84
|
+
title="查看本轮用时和速度"
|
|
85
|
+
>
|
|
86
|
+
<svg className={css.pillIcon} viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
|
87
|
+
<circle cx="8" cy="8" r="6.5" strokeWidth="1.2" />
|
|
88
|
+
<path d="M8 4.5v3.8l2.5 1.5" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
89
|
+
</svg>
|
|
90
|
+
<span>{formatRanFor(runMs)}</span>
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{open && (
|
|
95
|
+
<div className={css.metricsPop} role="dialog" aria-label="本轮概况">
|
|
96
|
+
<div className={css.popHeader}>
|
|
97
|
+
<span>本轮性能与用量概况</span>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{hasTiming && typeof runMs === 'number' && (
|
|
101
|
+
<div className={css.popSection}>
|
|
102
|
+
<div className={css.popSectionTitle}>耗时与生成速度</div>
|
|
103
|
+
<div className={css.popGrid}>
|
|
104
|
+
<span className={css.popLabel}>总用时</span>
|
|
105
|
+
<span className={css.popValue}>{formatRunDuration(runMs)}</span>
|
|
106
|
+
|
|
107
|
+
{typeof tokensPerSecond === 'number' && tokensPerSecond > 0 && (
|
|
108
|
+
<>
|
|
109
|
+
<span className={css.popLabel}>生成吞吐 (TPS)</span>
|
|
110
|
+
<span className={css.popValue}>{tokensPerSecond.toFixed(1)} tok/s</span>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{typeof ttftMs === 'number' && ttftMs > 0 && (
|
|
115
|
+
<>
|
|
116
|
+
<span className={css.popLabel}>首字延迟 (TTFT)</span>
|
|
117
|
+
<span className={css.popValue}>{(ttftMs / 1000).toFixed(2)}s</span>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{hasTokens && usage && (
|
|
125
|
+
<div className={css.popSection}>
|
|
126
|
+
<div className={css.popSectionTitle}>Token 消耗分解</div>
|
|
127
|
+
<div className={css.popGrid}>
|
|
128
|
+
<span className={css.popLabel}>总消耗</span>
|
|
129
|
+
<span className={css.popValue}>{usage.totalTokens.toLocaleString()} tok</span>
|
|
130
|
+
|
|
131
|
+
<span className={css.popLabel}>
|
|
132
|
+
输入
|
|
133
|
+
{cacheHitPercent !== null && (
|
|
134
|
+
<span className={css.popBadge}>命中 {cacheHitPercent}%</span>
|
|
135
|
+
)}
|
|
136
|
+
</span>
|
|
137
|
+
<span className={css.popValue}>{(usage.totalTokens - usage.outputTokens).toLocaleString()} tok</span>
|
|
138
|
+
|
|
139
|
+
{typeof usage.cacheReadTokens === 'number' && usage.cacheReadTokens > 0 && (
|
|
140
|
+
<>
|
|
141
|
+
<span className={css.popLabel}>缓存读取</span>
|
|
142
|
+
<span className={css.popValue}>{usage.cacheReadTokens.toLocaleString()} tok</span>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
<span className={css.popLabel}>
|
|
147
|
+
输出
|
|
148
|
+
{typeof usage.reasoningTokens === 'number' && usage.reasoningTokens > 0 && (
|
|
149
|
+
<span className={css.popBadge}>思考 {usage.reasoningTokens.toLocaleString()}</span>
|
|
150
|
+
)}
|
|
151
|
+
</span>
|
|
152
|
+
<span className={css.popValue}>{usage.outputTokens?.toLocaleString() ?? 0} tok</span>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</span>
|
|
159
|
+
);
|
|
160
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { formatRunDuration } from './message-chrome.js';
|
|
3
|
+
import css from './Reader.module.css';
|
|
4
|
+
|
|
5
|
+
/** Past this, a wait stops being a pause and reads as the model not answering. */
|
|
6
|
+
export const WAIT_OVERTIME_MS = 10_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* How long the model has been given the turn, counted in plain seconds.
|
|
10
|
+
*
|
|
11
|
+
* Just the number and, past ten seconds, a「暂未响应」badge — no bar, because the
|
|
12
|
+
* question the reader has is「how long has this been hanging」, not「how far
|
|
13
|
+
* along」: there is no known total to be a fraction of.
|
|
14
|
+
*
|
|
15
|
+
* The clock restarts whenever the anchor changes, so each wait is timed from the
|
|
16
|
+
* event that started it rather than from the beginning of the turn.
|
|
17
|
+
*/
|
|
18
|
+
export function WaitClock({ startTime }: { startTime: number | null }) {
|
|
19
|
+
const [mountedAt] = useState(() => Date.now());
|
|
20
|
+
const [now, setNow] = useState(() => Date.now());
|
|
21
|
+
const anchor = startTime ?? mountedAt;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const id = setInterval(() => setNow(Date.now()), 250);
|
|
24
|
+
return () => clearInterval(id);
|
|
25
|
+
}, []);
|
|
26
|
+
const waited = Math.max(0, now - anchor);
|
|
27
|
+
const overtime = waited >= WAIT_OVERTIME_MS;
|
|
28
|
+
return <span className={css.waitClock} data-reader-wait-clock
|
|
29
|
+
{...(overtime ? { 'data-overtime': '' } : {})}>
|
|
30
|
+
<span className={css.waitSeconds}>{formatRunDuration(waited)}</span>
|
|
31
|
+
{overtime && <span className={css.waitOvertime} data-reader-wait-badge>暂未响应</span>}
|
|
32
|
+
</span>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WaitClock } from './WaitClock.js';
|
|
2
|
+
import type { WaitingAnchor } from './waiting-clock.js';
|
|
3
|
+
import css from './Reader.module.css';
|
|
4
|
+
|
|
5
|
+
export function WaitingStatus({ anchor, label }: { anchor: WaitingAnchor; label: string }) {
|
|
6
|
+
// A new input gets its own clock even if the waiting indicator stays mounted;
|
|
7
|
+
// the readout itself is shared with the group status so the two never disagree.
|
|
8
|
+
return <div className={css.turnStatus} role="status" aria-live="polite" data-reader-turn-status
|
|
9
|
+
data-reader-wait-start={anchor.time ?? ''}>
|
|
10
|
+
<span className={css.turnStatusLabel}>{label}</span>
|
|
11
|
+
<WaitClock key={anchor.key} startTime={anchor.time} />
|
|
12
|
+
</div>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation-column scroll helpers. Official ChatView lands Turns by writing
|
|
3
|
+
* the enclosing `[data-conversation-scroll]` box; `Element.scrollIntoView`
|
|
4
|
+
* also walks ancestor scrollers and can lift the sticky composer off the
|
|
5
|
+
* bottom of the viewport (rail jump to top, then to bottom).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Reading-line offset used by ui-chat ChatView.landOnRow. */
|
|
9
|
+
export const READING_LINE_OFFSET_PX = 24;
|
|
10
|
+
|
|
11
|
+
/** Active column host when present; otherwise the view-local scroller. */
|
|
12
|
+
export function scrollerOf(from: HTMLElement): HTMLElement {
|
|
13
|
+
return from.closest('[data-conversation-scroll]') ?? from;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Row position in scrollport coordinates (viewport-independent). */
|
|
17
|
+
export function flowTop(row: HTMLElement, scrollport: HTMLElement): number {
|
|
18
|
+
return row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Place `row` on the conversation scroller only. Never calls scrollIntoView.
|
|
23
|
+
* @param row - a rendered `[data-reader-turn]` row.
|
|
24
|
+
* @param scrollport - `scrollerOf` of the reader root.
|
|
25
|
+
*/
|
|
26
|
+
export function landTurn(row: HTMLElement, scrollport: HTMLElement, offsetPx = READING_LINE_OFFSET_PX): void {
|
|
27
|
+
scrollport.scrollTop += flowTop(row, scrollport) - offsetPx;
|
|
28
|
+
}
|