dsh-better-display 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/DESIGN.md +72 -0
- package/LICENSE +24 -0
- package/README.en.md +41 -0
- package/README.md +41 -0
- package/THIRD_PARTY_NOTICES.md +65 -0
- package/cordis.patch.yml +5 -0
- package/cordis.yml +4 -0
- package/dshx.yml +5 -0
- package/lib/client.js +49923 -0
- package/lib/client.js.map +1 -0
- package/lib/dsh-better-display.js +256 -0
- package/lib/types/client/Blocks.d.ts +45 -0
- package/lib/types/client/Blocks.d.ts.map +1 -0
- package/lib/types/client/Blocks.js +136 -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 +170 -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 +15 -0
- package/lib/types/client/ClosedProcessSummary.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/Reader.d.ts +3 -0
- package/lib/types/client/Reader.d.ts.map +1 -0
- package/lib/types/client/Reader.js +537 -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 +301 -0
- package/lib/types/client/ReasoningCard.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 +28 -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 +192 -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/WaitingStatus.d.ts +6 -0
- package/lib/types/client/WaitingStatus.d.ts.map +1 -0
- package/lib/types/client/WaitingStatus.js +19 -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 +28 -0
- package/lib/types/client/fold-choreography.d.ts.map +1 -0
- package/lib/types/client/fold-choreography.js +31 -0
- package/lib/types/client/fold-choreography.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 +153 -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 +144 -0
- package/lib/types/client/live-turn.js.map +1 -0
- package/lib/types/client/markdown/MarkdownText.d.ts +39 -0
- package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
- package/lib/types/client/markdown/MarkdownText.js +171 -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 +119 -0
- package/lib/types/client/markdown/render.d.ts.map +1 -0
- package/lib/types/client/markdown/render.js +435 -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 +367 -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 +26 -0
- package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
- package/lib/types/client/native/ContextInjectionRow.js +27 -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/open-file.d.ts +24 -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 +37 -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 +6 -0
- package/lib/types/client/reasoning-follow.d.ts.map +1 -0
- package/lib/types/client/reasoning-follow.js +12 -0
- package/lib/types/client/reasoning-follow.js.map +1 -0
- package/lib/types/client/settings-copy.d.ts +6 -0
- package/lib/types/client/settings-slots.d.ts +16 -0
- package/lib/types/client/store.d.ts +17 -0
- package/lib/types/client/store.d.ts.map +1 -0
- package/lib/types/client/store.js +13 -0
- package/lib/types/client/store.js.map +1 -0
- package/lib/types/client/stream-buffer.d.ts +29 -0
- package/lib/types/client/stream-buffer.d.ts.map +1 -0
- package/lib/types/client/stream-buffer.js +113 -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 +77 -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 +52 -0
- package/lib/types/client/tool-activity.d.ts.map +1 -0
- package/lib/types/client/tool-activity.js +178 -0
- package/lib/types/client/tool-activity.js.map +1 -0
- package/lib/types/client/types.d.ts +66 -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 +17 -0
- package/lib/types/client/waiting-clock.d.ts.map +1 -0
- package/lib/types/client/waiting-clock.js +24 -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 +89 -0
- package/lib/types/client/word-motion.js.map +1 -0
- package/lib/types/client/word-timeline.d.ts +28 -0
- package/lib/types/client/word-timeline.d.ts.map +1 -0
- package/lib/types/client/word-timeline.js +100 -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 +53 -0
- package/lib/types/dsh-better-display.js.map +1 -0
- package/package.json +121 -0
- package/scripts/build-motion-fixture.mjs +26 -0
- package/scripts/link-harness-dependencies.mjs +51 -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 +205 -0
- package/src/client/ChoreographedFlow.tsx +182 -0
- package/src/client/ClosedProcessSummary.tsx +19 -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/Reader.module.css +329 -0
- package/src/client/Reader.tsx +814 -0
- package/src/client/ReasoningCard.tsx +285 -0
- package/src/client/SettingsSection.module.css +147 -0
- package/src/client/SettingsSection.tsx +117 -0
- package/src/client/StickyLane.tsx +24 -0
- package/src/client/TimelineRail.module.css +170 -0
- package/src/client/TimelineRail.tsx +262 -0
- package/src/client/ToolActivity.tsx +214 -0
- package/src/client/TurnMetrics.module.css +139 -0
- package/src/client/TurnMetrics.tsx +160 -0
- package/src/client/WaitingStatus.tsx +22 -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 +38 -0
- package/src/client/index.tsx +198 -0
- package/src/client/live-turn.ts +155 -0
- package/src/client/markdown/MarkdownText.module.css +304 -0
- package/src/client/markdown/MarkdownText.tsx +187 -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 +630 -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 +338 -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 +62 -0
- package/src/client/native/ContextInjectionRow.tsx +72 -0
- package/src/client/native/ReferenceIcon.tsx +35 -0
- package/src/client/native/tool-call-model.ts +248 -0
- package/src/client/native/upstream.json +41 -0
- package/src/client/open-file.ts +94 -0
- package/src/client/pending-submission.ts +49 -0
- package/src/client/primitive-labels.ts +53 -0
- package/src/client/projection.ts +118 -0
- package/src/client/reasoning-follow.ts +12 -0
- package/src/client/settings-copy.ts +53 -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 +30 -0
- package/src/client/stream-buffer.ts +100 -0
- package/src/client/streaming.tsx +62 -0
- package/src/client/style.d.ts +4 -0
- package/src/client/timeline.ts +77 -0
- package/src/client/tool-activity.ts +152 -0
- package/src/client/types.ts +66 -0
- package/src/client/waiting-clock.ts +25 -0
- package/src/client/word-motion.tsx +91 -0
- package/src/client/word-timeline.ts +96 -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-closed-gap.tsx +20 -0
- package/tests/browser-motion.tsx +65 -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/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 +38 -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/open-file.test.ts +103 -0
- package/tests/pending-submission.test.ts +45 -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 +50 -0
- package/tests/stream-buffer.test.ts +97 -0
- package/tests/timeline.test.ts +59 -0
- package/tests/waiting-clock.test.ts +24 -0
- package/tests/word-timeline.test.ts +136 -0
- package/tsconfig.json +11 -0
- package/tsdown.config.ts +49 -0
|
@@ -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,22 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { formatRunDuration } from './message-chrome.js';
|
|
3
|
+
import type { WaitingAnchor } from './waiting-clock.js';
|
|
4
|
+
import css from './Reader.module.css';
|
|
5
|
+
|
|
6
|
+
export function WaitingStatus({ anchor, label }: { anchor: WaitingAnchor; label: string }) {
|
|
7
|
+
// A new input gets its own clock even if the waiting indicator stays mounted.
|
|
8
|
+
return <InputClock key={anchor.key} startTime={anchor.time} label={label} />;
|
|
9
|
+
}
|
|
10
|
+
function InputClock({ startTime, label }: { startTime: number | null; label: string }) {
|
|
11
|
+
const [mountedAt] = useState(() => Date.now());
|
|
12
|
+
const [now, setNow] = useState(() => Date.now());
|
|
13
|
+
const anchor = startTime ?? mountedAt;
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
16
|
+
return () => clearInterval(id);
|
|
17
|
+
}, []);
|
|
18
|
+
return <div className={css.turnStatus} role="status" aria-live="polite" data-reader-turn-status data-reader-wait-start={anchor}>
|
|
19
|
+
<span className={css.turnStatusLabel}>{label}</span>
|
|
20
|
+
<span className={css.turnStatusClock} aria-hidden="true">{formatRunDuration(Math.max(0, now - anchor))}</span>
|
|
21
|
+
</div>;
|
|
22
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { TurnLocation } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
2
|
+
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
3
|
+
import type { ReaderFlowEntry } from './tool-activity.js';
|
|
4
|
+
import { inputFields, stringValue, toolIdentity } from './tool-activity.js';
|
|
5
|
+
|
|
6
|
+
interface ProducedEntry {
|
|
7
|
+
readonly seq?: number;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DeliverablesData {
|
|
12
|
+
readonly produced: readonly ProducedEntry[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Extract standard filename without directory components. */
|
|
16
|
+
export function basename(path: string): string {
|
|
17
|
+
const normalized = path.replace(/[/\\]+$/, '');
|
|
18
|
+
const at = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
|
|
19
|
+
return at === -1 ? normalized : normalized.slice(at + 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Extract parent directory of a path (or '.' if top-level). */
|
|
23
|
+
export function dirname(path: string): string {
|
|
24
|
+
const normalized = path.replace(/[/\\]+$/, '');
|
|
25
|
+
const at = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
|
|
26
|
+
return at === -1 ? '.' : normalized.slice(0, at) || '.';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The produced-files chip row waits for turn close. Live writes still
|
|
31
|
+
* accumulate, but the row must not interrupt an in-progress process.
|
|
32
|
+
*/
|
|
33
|
+
export function showDeliverablesRow(status: 'open' | 'closed' | 'unknown', paths: readonly string[]): boolean {
|
|
34
|
+
return status === 'closed' && paths.length > 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract all unique file paths produced/modified in one turn.
|
|
39
|
+
* Respects official deliverables data when available, and falls back to
|
|
40
|
+
* inspecting successful write/edit tool invocations in the turn flow.
|
|
41
|
+
*/
|
|
42
|
+
export function getTurnDeliverables(turn: TurnLocation | undefined, flow?: readonly ReaderFlowEntry[]): readonly string[] {
|
|
43
|
+
const paths: string[] = [];
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
|
|
46
|
+
// 1. Check official deliverables turn data from @deepseek-ai/dsh-client-ui-deliverables
|
|
47
|
+
const deliverables = (turn?.data as { get(key: string): unknown } | undefined)?.get('deliverables') as DeliverablesData | undefined;
|
|
48
|
+
if (deliverables?.produced && Array.isArray(deliverables.produced)) {
|
|
49
|
+
for (const item of deliverables.produced) {
|
|
50
|
+
if (typeof item?.path === 'string' && item.path.trim().length > 0) {
|
|
51
|
+
const clean = item.path.trim();
|
|
52
|
+
if (!seen.has(clean)) {
|
|
53
|
+
seen.add(clean);
|
|
54
|
+
paths.push(clean);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (paths.length > 0) return paths;
|
|
61
|
+
|
|
62
|
+
// 2. Fallback: inspect successful file modification tool calls in the flow
|
|
63
|
+
if (flow) {
|
|
64
|
+
for (const item of flow) {
|
|
65
|
+
if (item.kind !== 'tool' || !item.block) continue;
|
|
66
|
+
// Skip failed tool results
|
|
67
|
+
if ('isError' in item.block && item.block.isError) continue;
|
|
68
|
+
const { name, raw } = toolIdentity(item);
|
|
69
|
+
const toolName = name !== '工具调用' ? name : ((item.block as unknown as { name?: string }).name || (item.block as unknown as { call?: { name?: string } }).call?.name);
|
|
70
|
+
const toolRaw = raw || ((item.block as unknown as { argsRaw?: string }).argsRaw || (item.block as unknown as { call?: { argsRaw?: string } }).call?.argsRaw || '');
|
|
71
|
+
if (toolName === 'write' || toolName === 'edit' || toolName === 'apply_patch') {
|
|
72
|
+
const args = inputFields(toolRaw);
|
|
73
|
+
const target = stringValue(args, 'file_path', 'path', 'filename', 'filePath');
|
|
74
|
+
if (target && !seen.has(target)) {
|
|
75
|
+
seen.add(target);
|
|
76
|
+
paths.push(target);
|
|
77
|
+
}
|
|
78
|
+
} else if (toolName === 'str_replace_editor') {
|
|
79
|
+
const args = inputFields(toolRaw);
|
|
80
|
+
const cmd = stringValue(args, 'command');
|
|
81
|
+
if (cmd === 'create' || cmd === 'str_replace' || cmd === 'insert') {
|
|
82
|
+
const target = stringValue(args, 'path');
|
|
83
|
+
if (target && !seen.has(target)) {
|
|
84
|
+
seen.add(target);
|
|
85
|
+
paths.push(target);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return paths;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The single produced path whose basename is exactly value, or undefined. */
|
|
96
|
+
function onlyPathWithBasename(paths: readonly string[], value: string): string | undefined {
|
|
97
|
+
const matches = paths.filter(path => basename(path) === value);
|
|
98
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* File-mention resolver: converts inline-code tokens matching produced paths
|
|
103
|
+
* into clickable file references that open the corresponding file on the host.
|
|
104
|
+
*/
|
|
105
|
+
export function createProducedFileMentions(
|
|
106
|
+
paths: readonly string[],
|
|
107
|
+
openFile: (path: string) => void,
|
|
108
|
+
): MarkdownFileMentions {
|
|
109
|
+
return {
|
|
110
|
+
resolve(value: string) {
|
|
111
|
+
if (!value || value.includes('\n')) return undefined;
|
|
112
|
+
const clean = value.trim();
|
|
113
|
+
const path = paths.includes(clean) ? clean : onlyPathWithBasename(paths, clean);
|
|
114
|
+
if (path === undefined) return undefined;
|
|
115
|
+
return {
|
|
116
|
+
open: () => { openFile(path); },
|
|
117
|
+
label: `打开 ${path}`,
|
|
118
|
+
title: path,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Select the reading view on entry, without fighting a later explicit tab choice. */
|
|
2
|
+
export class ReaderEntryPolicy {
|
|
3
|
+
private entered = false;
|
|
4
|
+
|
|
5
|
+
constructor(private readonly requested: boolean, private readonly consumeRequest: () => void = () => {}) {}
|
|
6
|
+
|
|
7
|
+
select(view: string | null | undefined): 'reader' | null {
|
|
8
|
+
const requested = !this.entered && this.requested;
|
|
9
|
+
if (!this.entered) {
|
|
10
|
+
this.entered = true;
|
|
11
|
+
if (this.requested) this.consumeRequest();
|
|
12
|
+
}
|
|
13
|
+
return (requested || view == null) && view !== 'reader' ? 'reader' : null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readerEntryRequested(search: string): boolean {
|
|
18
|
+
const value = new URLSearchParams(search).get('reader');
|
|
19
|
+
return value === '1' || /^0\.1\.0-trial\.\d+$/.test(value ?? '');
|
|
20
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useLayoutEffect } from 'react';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { ConversationStore } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
4
|
+
import type { PropsRuntime, PropsStore, StoreDecl } from '@deepseek-ai/dsh-client-ui-slots';
|
|
5
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client';
|
|
6
|
+
import { ReaderEntryPolicy, readerEntryRequested } from './entry-policy.js';
|
|
7
|
+
|
|
8
|
+
function isConversationStore(store: StoreDecl | undefined): store is ConversationStore {
|
|
9
|
+
return typeof store === 'object' && store !== null
|
|
10
|
+
&& store.spec.persist === 'dsh.conversation'
|
|
11
|
+
&& typeof store.spec.actions.setView === 'function';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type EntryProps = PropsRuntime<'conversation.input.dock'> & PropsStore<ConversationStore> & { policy: ReaderEntryPolicy };
|
|
15
|
+
|
|
16
|
+
function ReaderEntry({ useStore, actions, policy }: EntryProps) {
|
|
17
|
+
const view = useStore(state => state.view);
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
const next = policy.select(view);
|
|
20
|
+
if (next) actions.setView(next);
|
|
21
|
+
}, [view, actions, policy]);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Reuse the native store handle; its framework-owned instance preserves drafts. */
|
|
26
|
+
export function installReaderEntry(ctx: Context): () => void {
|
|
27
|
+
const native = ctx.slots.entriesOfSlot('conversation.session')[0]?.store;
|
|
28
|
+
if (!isConversationStore(native)) throw new Error('DSH Reader cannot bind the native conversation view store.');
|
|
29
|
+
const policy = new ReaderEntryPolicy(readerEntryRequested(location.search), () => {
|
|
30
|
+
const url = new URL(location.href);
|
|
31
|
+
url.searchParams.delete('reader');
|
|
32
|
+
history.replaceState(history.state, '', `${url.pathname}${url.search}${url.hash}`);
|
|
33
|
+
});
|
|
34
|
+
return ctx.slots.register({
|
|
35
|
+
name: 'conversation.input.dock',
|
|
36
|
+
id: 'dsh-better-display-entry',
|
|
37
|
+
store: native,
|
|
38
|
+
inject: () => ({ policy }),
|
|
39
|
+
}, ReaderEntry);
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LiveStep, LiveTurnItem } from './live-turn.js';
|
|
2
|
+
|
|
3
|
+
/** Presentation clock, not model/network time. */
|
|
4
|
+
export const FOLD_TIMING = { collapse: 320, count: 160, settle: 80, reveal: 180 } as const;
|
|
5
|
+
export type FoldPhase = 'idle' | 'collapse' | 'count' | 'settle' | 'reveal';
|
|
6
|
+
export type FlowRow =
|
|
7
|
+
| { kind: 'summary'; key: string; item: Extract<LiveTurnItem, { kind: 'fold' }> }
|
|
8
|
+
| { kind: 'step'; key: string; step: LiveStep; foldKey?: string };
|
|
9
|
+
|
|
10
|
+
/** A step always has the same parent and React key, even when its membership changes. */
|
|
11
|
+
export function flowRows(items: readonly LiveTurnItem[]): FlowRow[] {
|
|
12
|
+
return items.flatMap((item): FlowRow[] => item.kind === 'fold'
|
|
13
|
+
? [{ kind: 'summary', key: item.key, item }, ...item.steps.map(step => ({ kind: 'step' as const, key: step.key, step, foldKey: item.key }))]
|
|
14
|
+
: [{ kind: 'step', key: item.key, step: item.step }]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function retiringKeys(before: readonly LiveTurnItem[], after: readonly LiveTurnItem[], open: Readonly<Record<string, boolean>>): string[] {
|
|
18
|
+
const folded = new Set(flowRows(after).filter(row => row.kind === 'step' && row.foldKey && !open[row.foldKey]).map(row => row.key));
|
|
19
|
+
return flowRows(before).filter(row => row.kind === 'step' && !row.foldKey && folded.has(row.key)).map(row => row.key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Insert only new summary slots. Never admit incoming content during shrink. */
|
|
23
|
+
export function collapseRows(before: readonly LiveTurnItem[], target: readonly LiveTurnItem[]): FlowRow[] {
|
|
24
|
+
const rows = flowRows(before);
|
|
25
|
+
const headers = new Set(rows.filter(row => row.kind === 'summary').map(row => row.key));
|
|
26
|
+
for (const item of target) {
|
|
27
|
+
if (item.kind !== 'fold' || headers.has(item.key)) continue;
|
|
28
|
+
const members = new Set(item.steps.map(step => step.key));
|
|
29
|
+
const at = rows.findIndex(row => members.has(row.key));
|
|
30
|
+
if (at >= 0) rows.splice(at, 0, { kind: 'summary', key: item.key, item: { ...item, summary: item.summary.replace(/\d+/g, '0') } });
|
|
31
|
+
}
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function containsNewUser(before: readonly LiveTurnItem[], after: readonly LiveTurnItem[]): boolean {
|
|
36
|
+
const users = new Set(before.filter(item => item.kind === 'user').map(item => item.key));
|
|
37
|
+
return after.some(item => item.kind === 'user' && !users.has(item.key));
|
|
38
|
+
}
|