@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,75 @@
|
|
|
1
|
+
import { defineStore } from '@deepseek-ai/dsh-client-store';
|
|
2
|
+
import type { EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
|
|
3
|
+
import {
|
|
4
|
+
FOLD_INTENSITY_DEFAULT,
|
|
5
|
+
autoFoldFromIntensity,
|
|
6
|
+
processOnlyFromIntensity,
|
|
7
|
+
type FoldIntensity,
|
|
8
|
+
} from './fold-intensity.js';
|
|
9
|
+
import type { DeliverableOpenMode } from './open-file.js';
|
|
10
|
+
|
|
11
|
+
export interface ReaderState {
|
|
12
|
+
expanded: Record<string, boolean>;
|
|
13
|
+
motion: boolean;
|
|
14
|
+
/** Derived from foldIntensity; kept so older persisted snapshots still read. */
|
|
15
|
+
autoFold: boolean;
|
|
16
|
+
/** Default stays the system app. `sidebar` is opt-in. */
|
|
17
|
+
deliverableOpenMode: DeliverableOpenMode;
|
|
18
|
+
/**
|
|
19
|
+
* 0 = no auto-fold, 1 = current-main fold-on-next-reasoning (default),
|
|
20
|
+
* 2 = process-summary mode from community PR #14.
|
|
21
|
+
*/
|
|
22
|
+
foldIntensity: FoldIntensity;
|
|
23
|
+
/** Translucent frosted chrome. Default off so opaque main chrome stays. */
|
|
24
|
+
frostedGlass: boolean;
|
|
25
|
+
/** Rounded answer bubbles. Default on; off lays answers flat on the page. */
|
|
26
|
+
bubbles: boolean;
|
|
27
|
+
/** Derived from foldIntensity === 2; kept for older #14 snapshots. */
|
|
28
|
+
processOnly: boolean;
|
|
29
|
+
}
|
|
30
|
+
type ReaderActions = {
|
|
31
|
+
setExpanded: (draft: ReaderState, key: string, value: boolean) => void;
|
|
32
|
+
resetExpanded: (draft: ReaderState) => void;
|
|
33
|
+
setMotion: (draft: ReaderState, value: boolean) => void;
|
|
34
|
+
setAutoFold: (draft: ReaderState, value: boolean) => void;
|
|
35
|
+
setDeliverableOpenMode: (draft: ReaderState, value: DeliverableOpenMode) => void;
|
|
36
|
+
setFoldIntensity: (draft: ReaderState, value: FoldIntensity) => void;
|
|
37
|
+
setFrostedGlass: (draft: ReaderState, value: boolean) => void;
|
|
38
|
+
setBubbles: (draft: ReaderState, value: boolean) => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function applyFoldIntensity(draft: ReaderState, value: FoldIntensity): void {
|
|
42
|
+
draft.foldIntensity = value;
|
|
43
|
+
draft.autoFold = autoFoldFromIntensity(value);
|
|
44
|
+
draft.processOnly = processOnlyFromIntensity(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createReaderStore(): EngineStoreHandle<ReaderState, ReaderActions> {
|
|
48
|
+
return defineStore({
|
|
49
|
+
init: (): ReaderState => ({
|
|
50
|
+
expanded: {},
|
|
51
|
+
motion: true,
|
|
52
|
+
autoFold: true,
|
|
53
|
+
deliverableOpenMode: 'external',
|
|
54
|
+
foldIntensity: FOLD_INTENSITY_DEFAULT,
|
|
55
|
+
frostedGlass: false,
|
|
56
|
+
bubbles: true,
|
|
57
|
+
processOnly: false,
|
|
58
|
+
}),
|
|
59
|
+
persist: 'dsh.reader.v1',
|
|
60
|
+
actions: {
|
|
61
|
+
setExpanded: (draft, key: string, value: boolean) => { draft.expanded[key] = value; },
|
|
62
|
+
resetExpanded: draft => { draft.expanded = {}; },
|
|
63
|
+
setMotion: (draft, value: boolean) => { draft.motion = value; },
|
|
64
|
+
setAutoFold: (draft, value: boolean) => {
|
|
65
|
+
applyFoldIntensity(draft, value
|
|
66
|
+
? (draft.foldIntensity === 2 ? 2 : 1)
|
|
67
|
+
: 0);
|
|
68
|
+
},
|
|
69
|
+
setDeliverableOpenMode: (draft, value: DeliverableOpenMode) => { draft.deliverableOpenMode = value; },
|
|
70
|
+
setFoldIntensity: (draft, value: FoldIntensity) => { applyFoldIntensity(draft, value); },
|
|
71
|
+
setFrostedGlass: (draft, value: boolean) => { draft.frostedGlass = value; },
|
|
72
|
+
setBubbles: (draft, value: boolean) => { draft.bubbles = value; },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Presentation only. The original session string remains the source of truth. */
|
|
2
|
+
// The reveal runs at the rate the source is actually producing (feed-forward)
|
|
3
|
+
// plus a proportional term that closes whatever backlog jitter left behind.
|
|
4
|
+
// A proportional-only controller drains exactly one window per window, so the
|
|
5
|
+
// display trails live output by catchUpMs forever — unreadable-but-smooth at
|
|
6
|
+
// 75 chars/s, permanently behind at 1000. The feed-forward term is what makes
|
|
7
|
+
// the pace adapt to the model instead of to a fixed constant.
|
|
8
|
+
export const STREAM_TIMING = {
|
|
9
|
+
catchUpMs: 520,
|
|
10
|
+
maxQueuedMs: 600,
|
|
11
|
+
finishMs: 96,
|
|
12
|
+
revealMs: 350,
|
|
13
|
+
minimumRate: 260,
|
|
14
|
+
/** Weight of the newest arrival sample in the smoothed source rate. */
|
|
15
|
+
rateWeight: 0.35,
|
|
16
|
+
/** A longer gap is a pause between runs, not a slow model: keep the estimate. */
|
|
17
|
+
rateGapMs: 250,
|
|
18
|
+
} as const;
|
|
19
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
20
|
+
|
|
21
|
+
function atOrAfter(values: readonly number[], target: number): number {
|
|
22
|
+
let low = 0;
|
|
23
|
+
let high = values.length - 1;
|
|
24
|
+
while (low < high) {
|
|
25
|
+
const middle = (low + high) >>> 1;
|
|
26
|
+
if (values[middle]! < target) low = middle + 1;
|
|
27
|
+
else high = middle;
|
|
28
|
+
}
|
|
29
|
+
return low;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class StreamBuffer {
|
|
33
|
+
target: string;
|
|
34
|
+
visible: string;
|
|
35
|
+
revision = 0;
|
|
36
|
+
private boundaries = [0];
|
|
37
|
+
private arrivals: { end: number; at: number }[] = [];
|
|
38
|
+
private lastAt: number | null = null;
|
|
39
|
+
private credit = 0;
|
|
40
|
+
private finishAt: number | null = null;
|
|
41
|
+
private rateEwma = 0;
|
|
42
|
+
private lastArrival: number | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(initial = '') {
|
|
45
|
+
this.target = initial;
|
|
46
|
+
this.visible = initial;
|
|
47
|
+
this.segment(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get pending(): boolean { return this.visible.length < this.target.length; }
|
|
51
|
+
|
|
52
|
+
private segment(from: number): void {
|
|
53
|
+
const index = atOrAfter(this.boundaries, from);
|
|
54
|
+
this.boundaries.length = index;
|
|
55
|
+
for (const part of graphemes.segment(this.target.slice(from))) this.boundaries.push(from + part.index);
|
|
56
|
+
if (this.boundaries.at(-1) !== this.target.length) this.boundaries.push(this.target.length);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Non-append updates, cancellations and hidden/reduced views never replay. */
|
|
60
|
+
update(text: string, now: number, options: { immediate?: boolean; finished?: boolean } = {}): void {
|
|
61
|
+
if (options.immediate || !text.startsWith(this.target)) {
|
|
62
|
+
if (text !== this.target) this.revision++;
|
|
63
|
+
this.target = text;
|
|
64
|
+
this.flush();
|
|
65
|
+
this.boundaries = [0];
|
|
66
|
+
this.segment(0);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (text !== this.target) {
|
|
70
|
+
const wasPending = this.pending;
|
|
71
|
+
const grew = text.length - this.target.length;
|
|
72
|
+
const from = this.boundaries.at(-2) ?? 0;
|
|
73
|
+
this.observe(grew, now);
|
|
74
|
+
this.target = text;
|
|
75
|
+
// Re-segment the previous last cluster: an emoji/combining sequence can
|
|
76
|
+
// itself span transport chunks. Frozen source prefixes are not scanned.
|
|
77
|
+
this.segment(from);
|
|
78
|
+
this.arrivals.push({ end: text.length, at: now });
|
|
79
|
+
if (!wasPending) { this.lastAt = now; this.credit = 0; }
|
|
80
|
+
}
|
|
81
|
+
if (options.finished && this.finishAt === null) this.finishAt = now + STREAM_TIMING.finishMs;
|
|
82
|
+
if (!options.finished) this.finishAt = null;
|
|
83
|
+
if (this.target.length - this.visible.length > 8192) { this.revision++; this.flush(); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Smoothed source rate, so one jittery transport chunk cannot set the pace. */
|
|
87
|
+
private observe(grew: number, now: number): void {
|
|
88
|
+
const previous = this.lastArrival;
|
|
89
|
+
this.lastArrival = now;
|
|
90
|
+
if (previous === null || grew <= 0) return;
|
|
91
|
+
const elapsed = now - previous;
|
|
92
|
+
if (elapsed <= 0 || elapsed > STREAM_TIMING.rateGapMs) return;
|
|
93
|
+
const sample = grew * 1000 / elapsed;
|
|
94
|
+
this.rateEwma = this.rateEwma === 0
|
|
95
|
+
? sample
|
|
96
|
+
: this.rateEwma * (1 - STREAM_TIMING.rateWeight) + sample * STREAM_TIMING.rateWeight;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
flush(): void {
|
|
100
|
+
this.visible = this.target;
|
|
101
|
+
this.arrivals = [];
|
|
102
|
+
this.credit = 0;
|
|
103
|
+
this.lastAt = null;
|
|
104
|
+
this.lastArrival = null;
|
|
105
|
+
this.finishAt = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
advance(now: number): boolean {
|
|
109
|
+
if (!this.pending) return false;
|
|
110
|
+
const before = this.visible.length;
|
|
111
|
+
const index = atOrAfter(this.boundaries, before);
|
|
112
|
+
const remaining = this.boundaries.length - index - 1;
|
|
113
|
+
const delta = Math.max(0, now - (this.lastAt ?? now));
|
|
114
|
+
this.lastAt = now;
|
|
115
|
+
const windowMs = this.finishAt === null ? STREAM_TIMING.catchUpMs : Math.max(16, Math.min(STREAM_TIMING.catchUpMs, this.finishAt - now));
|
|
116
|
+
// Feed-forward keeps pace with the source; the proportional term then drains
|
|
117
|
+
// whatever backlog is left, so lag decays instead of settling at one window.
|
|
118
|
+
const rate = Math.max(STREAM_TIMING.minimumRate, this.rateEwma + remaining * 1000 / windowMs);
|
|
119
|
+
this.credit += rate * delta / 1000;
|
|
120
|
+
const count = Math.floor(this.credit);
|
|
121
|
+
this.credit -= count;
|
|
122
|
+
let end = this.boundaries[Math.min(this.boundaries.length - 1, index + count)]!;
|
|
123
|
+
for (const item of this.arrivals) {
|
|
124
|
+
if (now - item.at < STREAM_TIMING.maxQueuedMs) break;
|
|
125
|
+
end = Math.max(end, this.boundaries[atOrAfter(this.boundaries, item.end)]!);
|
|
126
|
+
}
|
|
127
|
+
if (this.finishAt !== null && now >= this.finishAt) end = this.target.length;
|
|
128
|
+
// Never expose half of a surrogate pair while the source can still grow.
|
|
129
|
+
if (end && /[\uD800-\uDBFF]/.test(this.target[end - 1]!) && this.finishAt === null) end--;
|
|
130
|
+
end = Math.max(before, end);
|
|
131
|
+
this.visible = this.target.slice(0, end);
|
|
132
|
+
this.arrivals = this.arrivals.filter(item => item.end > end);
|
|
133
|
+
if (!this.pending) { this.credit = 0; this.lastAt = null; }
|
|
134
|
+
return end !== before;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { StreamBuffer } from './stream-buffer.js';
|
|
3
|
+
import { WORD_MOTION } from './word-timeline.js';
|
|
4
|
+
|
|
5
|
+
export const StreamMotionContext = createContext<{ enabled: boolean; activatedAt: number; paused?: boolean; resumed?: boolean }>({ enabled: false, activatedAt: 0 });
|
|
6
|
+
|
|
7
|
+
export function useStreamingText(source: string, streaming: boolean, options: { startedAt?: number; interrupted: boolean; selected: boolean }) {
|
|
8
|
+
const { enabled, activatedAt, paused = false, resumed = false } = useContext(StreamMotionContext);
|
|
9
|
+
const fresh = resumed || (options.startedAt ?? 0) >= activatedAt;
|
|
10
|
+
const buffer = useRef<StreamBuffer>();
|
|
11
|
+
// Only newly mounted content after a fold uses the catch-up path. History and
|
|
12
|
+
// user-opened drawers stay immediate; already mounted prefixes never replay.
|
|
13
|
+
if (!buffer.current) buffer.current = new StreamBuffer(enabled && (streaming || resumed) && fresh ? '' : source);
|
|
14
|
+
const [display, setDisplay] = useState(() => ({ text: buffer.current!.visible, revision: 0 }));
|
|
15
|
+
const [finalizing, setFinalizing] = useState(streaming);
|
|
16
|
+
const frame = useRef(0);
|
|
17
|
+
const immediate = !enabled || options.interrupted || options.selected;
|
|
18
|
+
const publish = () => {
|
|
19
|
+
const current = buffer.current!;
|
|
20
|
+
setDisplay(previous => previous.text === current.visible && previous.revision === current.revision ? previous : { text: current.visible, revision: current.revision });
|
|
21
|
+
};
|
|
22
|
+
const handoff = useRef(resumed);
|
|
23
|
+
const presentationClock = useRef(performance.now());
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
cancelAnimationFrame(frame.current);
|
|
26
|
+
const current = buffer.current!;
|
|
27
|
+
// While the choreography holds the reveal, keep the buffer absorbing the
|
|
28
|
+
// source instead of returning early: the target must never lag behind what
|
|
29
|
+
// arrived, or the text would keep catching up long after the fold settled.
|
|
30
|
+
// Dropping the frame loop is what pauses the reveal; the target still tracks.
|
|
31
|
+
if (paused && !immediate && !document.hidden) {
|
|
32
|
+
current.update(source, performance.now(), { immediate: false, finished: !streaming });
|
|
33
|
+
publish();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let previous = performance.now();
|
|
37
|
+
current.update(source, handoff.current ? presentationClock.current : previous, { immediate: immediate || document.hidden, finished: !streaming && !resumed });
|
|
38
|
+
publish();
|
|
39
|
+
const tick = (now: number) => {
|
|
40
|
+
// A delayed browser frame must not dump the whole buffered response at
|
|
41
|
+
// first paint. Bound catch-up by visible frame time for this handoff only.
|
|
42
|
+
presentationClock.current += Math.min(32, Math.max(0, now - previous));
|
|
43
|
+
previous = now;
|
|
44
|
+
if (document.hidden) current.flush();
|
|
45
|
+
else current.advance(handoff.current ? presentationClock.current : now);
|
|
46
|
+
publish();
|
|
47
|
+
if (current.pending) frame.current = requestAnimationFrame(tick);
|
|
48
|
+
else handoff.current = false;
|
|
49
|
+
};
|
|
50
|
+
if (current.pending) frame.current = requestAnimationFrame(tick);
|
|
51
|
+
return () => cancelAnimationFrame(frame.current);
|
|
52
|
+
}, [source, streaming, immediate, paused, resumed]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const hidden = () => {
|
|
55
|
+
if (!document.hidden) return;
|
|
56
|
+
cancelAnimationFrame(frame.current);
|
|
57
|
+
buffer.current!.flush(); publish(); setFinalizing(false);
|
|
58
|
+
};
|
|
59
|
+
document.addEventListener('visibilitychange', hidden);
|
|
60
|
+
return () => document.removeEventListener('visibilitychange', hidden);
|
|
61
|
+
}, []);
|
|
62
|
+
const pending = display.text !== source;
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (streaming || pending) { setFinalizing(true); return; }
|
|
65
|
+
if (immediate || document.hidden) { setFinalizing(false); return; }
|
|
66
|
+
const timer = setTimeout(() => setFinalizing(false), WORD_MOTION.duration + WORD_MOTION.maxDelay);
|
|
67
|
+
return () => clearTimeout(timer);
|
|
68
|
+
}, [streaming, pending, immediate]);
|
|
69
|
+
return { text: immediate ? source : display.text, pending: !immediate && pending, revision: display.revision, formatStreaming: streaming || (!immediate && (pending || finalizing)), reveal: enabled && !options.interrupted && !options.selected };
|
|
70
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
2
|
+
|
|
3
|
+
export interface TimelineItem {
|
|
4
|
+
readonly turn: number;
|
|
5
|
+
readonly prompt: string;
|
|
6
|
+
readonly response: string;
|
|
7
|
+
readonly hasDeliverables: boolean;
|
|
8
|
+
readonly anchor:
|
|
9
|
+
| { readonly kind: 'loaded'; readonly key: string }
|
|
10
|
+
| { readonly kind: 'unloaded'; readonly seq: number };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RawOutlineEntry {
|
|
14
|
+
readonly turn?: unknown;
|
|
15
|
+
readonly seq?: unknown;
|
|
16
|
+
readonly prompt?: unknown;
|
|
17
|
+
readonly response?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface LoadedTurnNavigationItem {
|
|
21
|
+
readonly turn: number;
|
|
22
|
+
readonly anchorKey: string;
|
|
23
|
+
readonly prompt: string;
|
|
24
|
+
readonly response: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Merge host turn outline with loaded navigation items and turn deliverables.
|
|
29
|
+
* Guarantees strictly ascending turn order and dedupes entries.
|
|
30
|
+
*/
|
|
31
|
+
export function mergeTimelineItems(
|
|
32
|
+
loaded: readonly LoadedTurnNavigationItem[] | undefined,
|
|
33
|
+
outline: unknown,
|
|
34
|
+
turnsWithDeliverables?: ReadonlySet<number>,
|
|
35
|
+
): readonly TimelineItem[] {
|
|
36
|
+
const byTurn = new Map<number, TimelineItem>();
|
|
37
|
+
|
|
38
|
+
// 1. Process host whole-log outline (if available)
|
|
39
|
+
if (Array.isArray(outline)) {
|
|
40
|
+
for (const raw of outline) {
|
|
41
|
+
if (typeof raw !== 'object' || raw === null) continue;
|
|
42
|
+
const entry = raw as RawOutlineEntry;
|
|
43
|
+
if (typeof entry.turn !== 'number' || !Number.isSafeInteger(entry.turn) || entry.turn < 0) continue;
|
|
44
|
+
if (typeof entry.seq !== 'number' || !Number.isSafeInteger(entry.seq) || entry.seq < 0) continue;
|
|
45
|
+
|
|
46
|
+
byTurn.set(entry.turn, {
|
|
47
|
+
turn: entry.turn,
|
|
48
|
+
prompt: typeof entry.prompt === 'string' ? entry.prompt.trim() : '',
|
|
49
|
+
response: typeof entry.response === 'string' ? entry.response.trim() : '',
|
|
50
|
+
hasDeliverables: turnsWithDeliverables?.has(entry.turn) ?? false,
|
|
51
|
+
anchor: { kind: 'unloaded', seq: entry.seq },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Overlay loaded items (taking loaded anchor, preferring freshest previews)
|
|
57
|
+
if (Array.isArray(loaded)) {
|
|
58
|
+
for (const item of loaded) {
|
|
59
|
+
if (typeof item.turn !== 'number') continue;
|
|
60
|
+
const preview = byTurn.get(item.turn);
|
|
61
|
+
const prompt = item.prompt?.trim() || preview?.prompt || '';
|
|
62
|
+
const response = item.response?.trim() || preview?.response || '';
|
|
63
|
+
const hasDeliverables = turnsWithDeliverables?.has(item.turn) ?? preview?.hasDeliverables ?? false;
|
|
64
|
+
|
|
65
|
+
byTurn.set(item.turn, {
|
|
66
|
+
turn: item.turn,
|
|
67
|
+
prompt,
|
|
68
|
+
response,
|
|
69
|
+
hasDeliverables,
|
|
70
|
+
anchor: { kind: 'loaded', key: item.anchorKey },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (byTurn.size === 0) return [];
|
|
76
|
+
return [...byTurn.values()].sort((a, b) => a.turn - b.turn);
|
|
77
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import type { AssistantBlock, ToolCallBlock, TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
2
|
+
import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-ui-chat/client';
|
|
3
|
+
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
|
+
import type { ReaderGroup } from './projection.js';
|
|
5
|
+
|
|
6
|
+
export type ToolDraft = Extract<AssistantBlock, { kind: 'tool-call' }>;
|
|
7
|
+
export type ToolPhase = 'preparing' | 'running' | 'returned' | 'succeeded' | 'failed' | 'interrupted';
|
|
8
|
+
export type ToolCategory = 'write' | 'read' | 'terminal' | 'search' | 'web' | 'code' | 'other';
|
|
9
|
+
export interface ToolActivityEntry {
|
|
10
|
+
kind: 'tool'; key: string; callId: string; step: number; order: number;
|
|
11
|
+
draft?: ToolDraft; block?: ToolCallBlock;
|
|
12
|
+
}
|
|
13
|
+
export type ReaderFlowEntry = ToolActivityEntry | { kind: 'node'; key: string; nodeKey: string; order: number };
|
|
14
|
+
|
|
15
|
+
/** Public Step data includes tool-only model output before a chat node exists. */
|
|
16
|
+
export function readerFlow(group: ReaderGroup, turn: TurnLocation | undefined, get: (key: string) => ChatConversationViewNode | undefined): ReaderFlowEntry[] {
|
|
17
|
+
const flow: ReaderFlowEntry[] = [];
|
|
18
|
+
const calls = new Map<string, ToolActivityEntry>();
|
|
19
|
+
const assistantOrder = new Map<number, number>();
|
|
20
|
+
for (const key of group.keys) {
|
|
21
|
+
const node = get(key);
|
|
22
|
+
if (!node || node.visibility === 'hidden') continue;
|
|
23
|
+
const step = node.location.kind === 'step' ? node.location.step.step : 0;
|
|
24
|
+
if (node.kind === 'tool-call') {
|
|
25
|
+
const block = (node.data as { root: ToolCallBlock }).root;
|
|
26
|
+
calls.set(block.callId, { kind: 'tool', key: `reader-tool:${block.callId}`, callId: block.callId, step, block, order: node.anchorSeq });
|
|
27
|
+
} else {
|
|
28
|
+
flow.push({ kind: 'node', key, nodeKey: key, order: node.anchorSeq });
|
|
29
|
+
if (node.kind === 'assistant-step') assistantOrder.set(step, node.anchorSeq);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const step of turn?.steps ?? []) {
|
|
33
|
+
const data = step.data.get('assistant-step');
|
|
34
|
+
let index = 0;
|
|
35
|
+
for (const draft of data?.blocks ?? []) {
|
|
36
|
+
if (draft.kind !== 'tool-call' || !draft.callId) continue;
|
|
37
|
+
const previous = calls.get(draft.callId);
|
|
38
|
+
calls.set(draft.callId, previous ? { ...previous, draft } : {
|
|
39
|
+
kind: 'tool', key: `reader-tool:${draft.callId}`, callId: draft.callId, step: step.step, draft,
|
|
40
|
+
order: (assistantOrder.get(step.step) ?? step.start?.seq ?? 0) + .01 + index++ / 10000,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
flow.push(...calls.values());
|
|
45
|
+
return flow.sort((left, right) => left.order - right.order);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function objectValue(value: unknown): Record<string, unknown> | null {
|
|
49
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
50
|
+
}
|
|
51
|
+
export function stringValue(record: Record<string, unknown> | null, ...keys: string[]): string | undefined {
|
|
52
|
+
for (const key of keys) if (typeof record?.[key] === 'string' && record[key]) return record[key] as string;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Read only top-level JSON string values, including an unfinished final string.
|
|
57
|
+
* This never executes input or mistakes escaped/nested content for a path field. */
|
|
58
|
+
export function inputFields(raw: string): Record<string, unknown> {
|
|
59
|
+
try { return objectValue(JSON.parse(raw)) ?? {}; } catch { /* an in-flight argument is normally incomplete */ }
|
|
60
|
+
const fields: Record<string, unknown> = Object.create(null);
|
|
61
|
+
const prefix = raw.slice(0, 262144);
|
|
62
|
+
let depth = 0;
|
|
63
|
+
let key: string | undefined;
|
|
64
|
+
for (let index = 0; index < prefix.length; index++) {
|
|
65
|
+
const char = prefix[index];
|
|
66
|
+
if (char === '{' || char === '[') { depth++; continue; }
|
|
67
|
+
if (char === '}' || char === ']') { depth--; continue; }
|
|
68
|
+
if (char !== '"') continue;
|
|
69
|
+
const start = index;
|
|
70
|
+
let closed = false;
|
|
71
|
+
for (index++; index < prefix.length; index++) {
|
|
72
|
+
if (prefix[index] === '\\') { index++; continue; }
|
|
73
|
+
if (prefix[index] === '"') { closed = true; break; }
|
|
74
|
+
}
|
|
75
|
+
if (depth !== 1) continue;
|
|
76
|
+
const token = prefix.slice(start, closed ? index + 1 : prefix.length);
|
|
77
|
+
let value: string;
|
|
78
|
+
try { value = JSON.parse(token); } catch {
|
|
79
|
+
if (closed) continue;
|
|
80
|
+
// Strip only an unfinished trailing escape/unicode escape, not content.
|
|
81
|
+
let body = token.slice(1);
|
|
82
|
+
const unicode = /(?<!\\)(?:\\\\)*\\u[\da-f]{0,3}$/i.exec(body);
|
|
83
|
+
if (unicode) body = body.slice(0, unicode.index) + unicode[0].replace(/\\u[\da-f]{0,3}$/i, '');
|
|
84
|
+
let slashes = 0; for (let end = body.length - 1; end >= 0 && body[end] === '\\'; end--) slashes++;
|
|
85
|
+
if (slashes % 2) body = body.slice(0, -1);
|
|
86
|
+
try { value = JSON.parse(`"${body}"`); } catch { continue; }
|
|
87
|
+
}
|
|
88
|
+
if (closed && /^\s*:/.test(prefix.slice(index + 1))) key = value;
|
|
89
|
+
else if (key !== undefined) { fields[key] = value; key = undefined; }
|
|
90
|
+
}
|
|
91
|
+
return fields;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function toolIdentity(entry: Pick<ToolActivityEntry, 'block' | 'draft'>) {
|
|
95
|
+
const block = entry.block;
|
|
96
|
+
return {
|
|
97
|
+
name: block ? 'kind' in block ? block.call?.name ?? entry.draft?.name ?? '工具调用' : block.name : entry.draft?.name ?? '工具调用',
|
|
98
|
+
raw: block ? 'kind' in block ? block.call?.argsRaw ?? entry.draft?.argsRaw ?? '' : ('argsRaw' in block ? block.argsRaw : '') : entry.draft?.argsRaw ?? '',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Name and raw arguments of any call block, whether it has landed or is pending. */
|
|
103
|
+
function childIdentity(block: ToolCallBlock): { name?: string; raw: string } {
|
|
104
|
+
if (!block || !('kind' in block)) return { name: block?.name, raw: block && 'argsRaw' in block ? block.argsRaw : '' };
|
|
105
|
+
return { name: block.call?.name, raw: block.call?.argsRaw ?? '' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function executionFacts(block: ToolCallBlock | undefined): { exitCode?: number; signal?: string } {
|
|
109
|
+
if (!block || !('kind' in block)) return {};
|
|
110
|
+
const meta = objectValue(block.meta);
|
|
111
|
+
const code = meta?.exitCode ?? meta?.exit_code;
|
|
112
|
+
const text = block.content.length === 1 && block.content[0]?.type === 'text' ? block.content[0].text : '';
|
|
113
|
+
const exit = /\n\[exit code: (\d+)\]$/.exec(text);
|
|
114
|
+
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
|
|
115
|
+
const parsedCode = exit?.[1] === undefined ? undefined : Number(exit[1]);
|
|
116
|
+
return {
|
|
117
|
+
exitCode: typeof code === 'number' && Number.isFinite(code) ? code : parsedCode,
|
|
118
|
+
signal: stringValue(meta, 'signal') ?? signal?.[1],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Line counts of one text side; a trailing newline does not start a line. */
|
|
123
|
+
function lineCount(text: string | null): number {
|
|
124
|
+
if (text === null) return 0;
|
|
125
|
+
const body = text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
126
|
+
return body === '' ? 0 : body.split('\n').length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Argument fields that carry the text a call is about to write. */
|
|
130
|
+
const NEW_TEXT_FIELDS = ['content', 'new_string', 'new_str', 'newText', 'file_text'] as const;
|
|
131
|
+
/** Argument fields that carry the text a call is about to replace. */
|
|
132
|
+
const OLD_TEXT_FIELDS = ['old_string', 'old_str', 'oldText'] as const;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Added/removed line counts for a mutation call, or null when there is no diff
|
|
136
|
+
* to show.
|
|
137
|
+
*
|
|
138
|
+
* The result metadata is preferred when the host attaches one. Not every host
|
|
139
|
+
* build does — a session log may carry no hunk payload at all — so the call's own
|
|
140
|
+
* arguments are read as the fallback, which is also what the official row uses
|
|
141
|
+
* while a write is still pending. A call whose arguments hold none of these
|
|
142
|
+
* fields (a read, a listing) yields null and shows no badge.
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* One changed file, in the exact shape the official `DiffBlock` primitive takes:
|
|
146
|
+
* `oldText` is null for a pure insertion, and `newText` is always a string. Line
|
|
147
|
+
* counts are derived from these two sides, never stored alongside them.
|
|
148
|
+
*/
|
|
149
|
+
export interface DiffHunk {
|
|
150
|
+
path: string;
|
|
151
|
+
oldText: string | null;
|
|
152
|
+
newText: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Argument fields that name the file a mutation call targets. */
|
|
156
|
+
const PATH_FIELDS = ['file_path', 'path', 'filePath'] as const;
|
|
157
|
+
|
|
158
|
+
function firstString(source: Record<string, unknown> | undefined, fields: readonly string[]): string | null {
|
|
159
|
+
if (!source) return null;
|
|
160
|
+
for (const field of fields) {
|
|
161
|
+
const value = source[field];
|
|
162
|
+
if (typeof value === 'string' && value !== '') return value;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The changed files of a mutation call.
|
|
169
|
+
*
|
|
170
|
+
* Result metadata is preferred when the host attaches it. Not every host build
|
|
171
|
+
* does, so the call's own arguments are the fallback — the same source the
|
|
172
|
+
* official row reads while a write is still pending. A call whose arguments hold
|
|
173
|
+
* none of these fields (a read, a listing) yields nothing and shows no counts.
|
|
174
|
+
*/
|
|
175
|
+
/** Only these tools change a file, so only these may fall back to their arguments. */
|
|
176
|
+
export const DIFF_FALLBACK_TOOLS = ['write', 'edit', 'str_replace_editor'] as const;
|
|
177
|
+
const MUTATION_TOOLS = new Set<string>(DIFF_FALLBACK_TOOLS);
|
|
178
|
+
|
|
179
|
+
export function callDiffHunks(
|
|
180
|
+
block: ToolCallBlock | undefined,
|
|
181
|
+
args?: Record<string, unknown>,
|
|
182
|
+
name?: string,
|
|
183
|
+
): DiffHunk[] {
|
|
184
|
+
// A parent call owns whatever its children changed: a script that writes files
|
|
185
|
+
// reports those writes itself rather than through its own arguments, and the
|
|
186
|
+
// digest must see them too. Without this the counts only ever appeared on the
|
|
187
|
+
// nested row, which is what a reader noticed as a missing badge.
|
|
188
|
+
// Each child contributes through its own arguments, not the parent's: a nested
|
|
189
|
+
// write/edit records its hunks in `argsRaw`, and passing nothing here silently
|
|
190
|
+
// dropped them from the parent total. Errors are contained so one unreadable
|
|
191
|
+
// child cannot blank the whole row.
|
|
192
|
+
const nested = block && 'kind' in block && Array.isArray(block.subCalls)
|
|
193
|
+
? block.subCalls.flatMap(child => {
|
|
194
|
+
try { return callDiffHunks(child, inputFields(childIdentity(child).raw), childIdentity(child).name); }
|
|
195
|
+
catch { return []; }
|
|
196
|
+
})
|
|
197
|
+
: [];
|
|
198
|
+
if (block && 'kind' in block) {
|
|
199
|
+
const diffs = objectValue(block.meta)?.diffs;
|
|
200
|
+
if (Array.isArray(diffs) && diffs.length > 0) {
|
|
201
|
+
const hunks: DiffHunk[] = [];
|
|
202
|
+
for (const raw of diffs) {
|
|
203
|
+
const entry = objectValue(raw);
|
|
204
|
+
if (!entry) continue;
|
|
205
|
+
const oldText = typeof entry.oldText === 'string' ? entry.oldText : null;
|
|
206
|
+
const newText = typeof entry.newText === 'string' ? entry.newText : '';
|
|
207
|
+
if (!lineCount(newText) && !lineCount(oldText)) continue;
|
|
208
|
+
hunks.push({ path: firstString(entry, PATH_FIELDS) ?? '', oldText, newText });
|
|
209
|
+
}
|
|
210
|
+
if (hunks.length) return [...hunks, ...nested];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Several unrelated tools take a field named `content` (a memory note, a typed
|
|
214
|
+
// message). Reading it as a file body invented counts for calls that changed no
|
|
215
|
+
// file at all, so the name decides whether arguments may be read this way.
|
|
216
|
+
if (!name || !MUTATION_TOOLS.has(name)) return nested;
|
|
217
|
+
const path = firstString(args, PATH_FIELDS);
|
|
218
|
+
if (!path) return nested;
|
|
219
|
+
const oldText = firstString(args, OLD_TEXT_FIELDS);
|
|
220
|
+
const newText = firstString(args, NEW_TEXT_FIELDS) ?? '';
|
|
221
|
+
if (!lineCount(newText) && !lineCount(oldText)) return nested;
|
|
222
|
+
return [{ path, oldText, newText }, ...nested];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Added/removed line counts for one call, or null when there is nothing to show. */
|
|
226
|
+
export function diffTotals(
|
|
227
|
+
block: ToolCallBlock | undefined,
|
|
228
|
+
args?: Record<string, unknown>,
|
|
229
|
+
name?: string,
|
|
230
|
+
): { added: number; removed: number } | null {
|
|
231
|
+
const hunks = callDiffHunks(block, args, name);
|
|
232
|
+
if (!hunks.length) return null;
|
|
233
|
+
return diffLineTotals(hunks);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Counts the lines on each side, under the same rule the official block uses. */
|
|
237
|
+
export function diffLineTotals(hunks: readonly DiffHunk[]): { added: number; removed: number } {
|
|
238
|
+
return hunks.reduce((total, hunk) => ({
|
|
239
|
+
added: total.added + lineCount(hunk.newText),
|
|
240
|
+
removed: total.removed + lineCount(hunk.oldText),
|
|
241
|
+
}), { added: 0, removed: 0 });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Every changed file across a folded run of steps, in the order they ran. */
|
|
245
|
+
export function foldDiffHunks(steps: readonly { kind: string; entry?: ToolActivityEntry }[]): DiffHunk[] {
|
|
246
|
+
const hunks: DiffHunk[] = [];
|
|
247
|
+
for (const step of steps) {
|
|
248
|
+
if (step.kind !== 'tool' || !step.entry) continue;
|
|
249
|
+
const model = activitySummary(step.entry);
|
|
250
|
+
hunks.push(...callDiffHunks(step.entry.block, model.args, model.name));
|
|
251
|
+
}
|
|
252
|
+
return hunks;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function activityPhase(entry: Pick<ToolActivityEntry, 'block' | 'draft'>, turnClosed = false): ToolPhase {
|
|
256
|
+
if (!entry.block) return turnClosed ? 'interrupted' : 'preparing';
|
|
257
|
+
if (!('kind' in entry.block)) return turnClosed ? 'interrupted' : 'running';
|
|
258
|
+
// RC1 publishes canonical cancellation as an error result with a typed code.
|
|
259
|
+
if (entry.block.error?.code === 'ABORTED' || entry.block.error?.code === 'interrupted') return 'interrupted';
|
|
260
|
+
const facts = executionFacts(entry.block);
|
|
261
|
+
if (entry.block.isError || facts.signal || (facts.exitCode !== undefined && facts.exitCode !== 0)
|
|
262
|
+
|| entry.block.subCalls.some(block => activityPhase({ block }, turnClosed) === 'failed')) return 'failed';
|
|
263
|
+
if (facts.exitCode === 0) return 'succeeded';
|
|
264
|
+
return 'returned';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function activitySummary(entry: Pick<ToolActivityEntry, 'block' | 'draft'>) {
|
|
268
|
+
const { name, raw } = toolIdentity(entry);
|
|
269
|
+
const args = inputFields(raw);
|
|
270
|
+
const target = stringValue(args, 'file_path', 'path', 'filename', 'filePath');
|
|
271
|
+
const command = stringValue(args, 'command', 'cmd', 'script');
|
|
272
|
+
const description = stringValue(args, 'description');
|
|
273
|
+
const file = target?.split(/[/\\]/).at(-1);
|
|
274
|
+
const category: ToolCategory = /^(write|edit|apply_patch|patch|str_replace_editor)$/.test(name) ? 'write'
|
|
275
|
+
: /^(read|read_file)$/.test(name) ? 'read'
|
|
276
|
+
: /^(bash|shell|terminal|terminal_send|exec_command|pwsh)$/.test(name) ? 'terminal'
|
|
277
|
+
: /^(grep|glob|find|search)$/.test(name) ? 'search'
|
|
278
|
+
: /^(web_search|web_fetch|web_open)$/.test(name) ? 'web'
|
|
279
|
+
// Code interpreters: no file, no shell, no query — the call is a program.
|
|
280
|
+
// Without this branch `run_code` fell through to `other`, which has no
|
|
281
|
+
// renderer of its own, so its result text was emitted as ordinary prose
|
|
282
|
+
// and stayed outside every fold.
|
|
283
|
+
: /^(run_code|execute_code|code_interpreter|python|node|eval|repl)$/.test(name) ? 'code'
|
|
284
|
+
: 'other';
|
|
285
|
+
const title = category === 'write' ? `${name === 'write' ? '写入' : '修改'}${file ? ` ${file}` : name === 'apply_patch' ? '代码补丁' : '文件'}`
|
|
286
|
+
: category === 'read' ? `读取${file ? ` ${file}` : '文件'}`
|
|
287
|
+
: category === 'terminal' ? description || '运行命令'
|
|
288
|
+
: category === 'search' ? name === 'glob' ? '查找文件' : '搜索内容'
|
|
289
|
+
: category === 'web' ? name === 'web_search' ? '搜索网页' : '读取网页'
|
|
290
|
+
: category === 'code' ? (description || '运行代码')
|
|
291
|
+
: name;
|
|
292
|
+
return { name, raw, args, category, title, target: target ?? command ?? stringValue(args, 'query', 'pattern', 'url'), command,
|
|
293
|
+
cwd: stringValue(args, 'workdir', 'cwd'), content: stringValue(args, 'content', 'new_string', 'newText', 'file_text') };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function preparingLabel(name: string): string {
|
|
297
|
+
return /^(write|edit|apply_patch)$/.test(name) ? '正在生成文件内容' : /^(bash|shell|exec_command|pwsh)$/.test(name) ? '正在准备命令' : '正在准备工具输入';
|
|
298
|
+
}
|