dsh-better-display 0.1.1 → 0.2.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +177 -4
  2. package/README.en.md +17 -3
  3. package/README.md +17 -3
  4. package/lib/client.js +42319 -41533
  5. package/lib/client.js.map +1 -1
  6. package/lib/dsh-better-display.js +51 -55
  7. package/lib/types/client/Blocks.d.ts +4 -1
  8. package/lib/types/client/Blocks.d.ts.map +1 -1
  9. package/lib/types/client/Blocks.js +11 -4
  10. package/lib/types/client/Blocks.js.map +1 -1
  11. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -1
  12. package/lib/types/client/ChoreographedFlow.js +68 -20
  13. package/lib/types/client/ChoreographedFlow.js.map +1 -1
  14. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -1
  15. package/lib/types/client/ClosedProcessSummary.js +3 -2
  16. package/lib/types/client/ClosedProcessSummary.js.map +1 -1
  17. package/lib/types/client/DiffPanel.d.ts +13 -0
  18. package/lib/types/client/DiffPanel.d.ts.map +1 -0
  19. package/lib/types/client/DiffPanel.js +115 -0
  20. package/lib/types/client/DiffPanel.js.map +1 -0
  21. package/lib/types/client/Reader.d.ts.map +1 -1
  22. package/lib/types/client/Reader.js +88 -54
  23. package/lib/types/client/Reader.js.map +1 -1
  24. package/lib/types/client/ReasoningCard.d.ts.map +1 -1
  25. package/lib/types/client/ReasoningCard.js +29 -8
  26. package/lib/types/client/ReasoningCard.js.map +1 -1
  27. package/lib/types/client/SettingsSection.d.ts +35 -0
  28. package/lib/types/client/SettingsSection.d.ts.map +1 -0
  29. package/lib/types/client/SettingsSection.js +62 -0
  30. package/lib/types/client/SettingsSection.js.map +1 -0
  31. package/lib/types/client/StickyLane.d.ts.map +1 -1
  32. package/lib/types/client/StickyLane.js +16 -5
  33. package/lib/types/client/StickyLane.js.map +1 -1
  34. package/lib/types/client/ToolActivity.d.ts.map +1 -1
  35. package/lib/types/client/ToolActivity.js +53 -5
  36. package/lib/types/client/ToolActivity.js.map +1 -1
  37. package/lib/types/client/WaitClock.d.ts +16 -0
  38. package/lib/types/client/WaitClock.d.ts.map +1 -0
  39. package/lib/types/client/WaitClock.js +29 -0
  40. package/lib/types/client/WaitClock.js.map +1 -0
  41. package/lib/types/client/WaitingClock.d.ts +15 -0
  42. package/lib/types/client/WaitingClock.d.ts.map +1 -0
  43. package/lib/types/client/WaitingClock.js +30 -0
  44. package/lib/types/client/WaitingClock.js.map +1 -0
  45. package/lib/types/client/WaitingStatus.d.ts.map +1 -1
  46. package/lib/types/client/WaitingStatus.js +4 -14
  47. package/lib/types/client/WaitingStatus.js.map +1 -1
  48. package/lib/types/client/fold-choreography.d.ts +12 -0
  49. package/lib/types/client/fold-choreography.d.ts.map +1 -1
  50. package/lib/types/client/fold-choreography.js +18 -1
  51. package/lib/types/client/fold-choreography.js.map +1 -1
  52. package/lib/types/client/fold-intensity.d.ts +9 -0
  53. package/lib/types/client/fold-intensity.d.ts.map +1 -0
  54. package/lib/types/client/fold-intensity.js +25 -0
  55. package/lib/types/client/fold-intensity.js.map +1 -0
  56. package/lib/types/client/index.d.ts.map +1 -1
  57. package/lib/types/client/index.js +68 -16
  58. package/lib/types/client/index.js.map +1 -1
  59. package/lib/types/client/live-turn.d.ts.map +1 -1
  60. package/lib/types/client/live-turn.js +46 -0
  61. package/lib/types/client/live-turn.js.map +1 -1
  62. package/lib/types/client/motion.d.ts.map +1 -1
  63. package/lib/types/client/motion.js +89 -12
  64. package/lib/types/client/motion.js.map +1 -1
  65. package/lib/types/client/open-file.d.ts +6 -1
  66. package/lib/types/client/open-file.d.ts.map +1 -0
  67. package/lib/types/client/open-file.js +71 -0
  68. package/lib/types/client/open-file.js.map +1 -0
  69. package/lib/types/client/pending-submission.d.ts +28 -0
  70. package/lib/types/client/pending-submission.d.ts.map +1 -0
  71. package/lib/types/client/pending-submission.js +28 -0
  72. package/lib/types/client/pending-submission.js.map +1 -0
  73. package/lib/types/client/reasoning-follow.d.ts +5 -1
  74. package/lib/types/client/reasoning-follow.d.ts.map +1 -1
  75. package/lib/types/client/reasoning-follow.js +13 -4
  76. package/lib/types/client/reasoning-follow.js.map +1 -1
  77. package/lib/types/client/resource-address.d.ts +3 -0
  78. package/lib/types/client/resource-address.d.ts.map +1 -0
  79. package/lib/types/client/resource-address.js +33 -0
  80. package/lib/types/client/resource-address.js.map +1 -0
  81. package/lib/types/client/settings-copy.d.ts +3 -2
  82. package/lib/types/client/settings-copy.d.ts.map +1 -0
  83. package/lib/types/client/settings-copy.js +49 -0
  84. package/lib/types/client/settings-copy.js.map +1 -0
  85. package/lib/types/client/settings-slots.d.ts +7 -0
  86. package/lib/types/client/settings-slots.d.ts.map +1 -0
  87. package/lib/types/client/settings-slots.js +2 -0
  88. package/lib/types/client/settings-slots.js.map +1 -0
  89. package/lib/types/client/settings.d.ts +4 -0
  90. package/lib/types/client/settings.d.ts.map +1 -0
  91. package/lib/types/client/settings.js +60 -0
  92. package/lib/types/client/settings.js.map +1 -0
  93. package/lib/types/client/skill-status.d.ts +25 -0
  94. package/lib/types/client/skill-status.d.ts.map +1 -0
  95. package/lib/types/client/skill-status.js +44 -0
  96. package/lib/types/client/skill-status.js.map +1 -0
  97. package/lib/types/client/store.d.ts +14 -0
  98. package/lib/types/client/store.d.ts.map +1 -1
  99. package/lib/types/client/store.js +23 -2
  100. package/lib/types/client/store.js.map +1 -1
  101. package/lib/types/client/stream-buffer.d.ts +11 -3
  102. package/lib/types/client/stream-buffer.d.ts.map +1 -1
  103. package/lib/types/client/stream-buffer.js +39 -2
  104. package/lib/types/client/stream-buffer.js.map +1 -1
  105. package/lib/types/client/streaming.d.ts.map +1 -1
  106. package/lib/types/client/streaming.js +9 -2
  107. package/lib/types/client/streaming.js.map +1 -1
  108. package/lib/types/client/tool-activity.d.ts +46 -0
  109. package/lib/types/client/tool-activity.d.ts.map +1 -1
  110. package/lib/types/client/tool-activity.js +116 -0
  111. package/lib/types/client/tool-activity.js.map +1 -1
  112. package/lib/types/client/types.d.ts +15 -2
  113. package/lib/types/client/types.d.ts.map +1 -1
  114. package/lib/types/client/waiting-clock.d.ts +34 -2
  115. package/lib/types/client/waiting-clock.d.ts.map +1 -1
  116. package/lib/types/client/waiting-clock.js +62 -6
  117. package/lib/types/client/waiting-clock.js.map +1 -1
  118. package/lib/types/client/word-timeline.d.ts +4 -0
  119. package/lib/types/client/word-timeline.d.ts.map +1 -1
  120. package/lib/types/client/word-timeline.js +21 -2
  121. package/lib/types/client/word-timeline.js.map +1 -1
  122. package/lib/types/dsh-better-display.d.ts.map +1 -1
  123. package/lib/types/dsh-better-display.js +42 -2
  124. package/lib/types/dsh-better-display.js.map +1 -1
  125. package/lib/types/skill-roots.d.ts +22 -0
  126. package/lib/types/skill-roots.d.ts.map +1 -0
  127. package/lib/types/skill-roots.js +126 -0
  128. package/lib/types/skill-roots.js.map +1 -0
  129. package/lib/types/skill-status.d.ts +36 -0
  130. package/lib/types/skill-status.d.ts.map +1 -0
  131. package/lib/types/skill-status.js +51 -0
  132. package/lib/types/skill-status.js.map +1 -0
  133. package/package.json +1 -1
  134. package/src/client/Blocks.tsx +11 -4
  135. package/src/client/ChoreographedFlow.tsx +68 -18
  136. package/src/client/ClosedProcessSummary.tsx +6 -2
  137. package/src/client/DiffPanel.tsx +124 -0
  138. package/src/client/Reader.module.css +319 -6
  139. package/src/client/Reader.tsx +78 -42
  140. package/src/client/ReasoningCard.tsx +27 -6
  141. package/src/client/SettingsSection.module.css +37 -0
  142. package/src/client/SettingsSection.tsx +64 -6
  143. package/src/client/StickyLane.tsx +13 -3
  144. package/src/client/ToolActivity.tsx +70 -5
  145. package/src/client/WaitClock.tsx +33 -0
  146. package/src/client/WaitingStatus.tsx +6 -15
  147. package/src/client/fold-choreography.ts +18 -1
  148. package/src/client/fold-intensity.ts +28 -0
  149. package/src/client/index.tsx +28 -2
  150. package/src/client/live-turn.ts +43 -0
  151. package/src/client/motion.tsx +80 -15
  152. package/src/client/reasoning-follow.ts +20 -4
  153. package/src/client/settings-copy.ts +21 -0
  154. package/src/client/store.ts +40 -2
  155. package/src/client/stream-buffer.ts +38 -2
  156. package/src/client/streaming.tsx +9 -1
  157. package/src/client/tool-activity.ts +139 -0
  158. package/src/client/types.ts +18 -3
  159. package/src/client/waiting-clock.ts +64 -6
  160. package/src/client/word-timeline.ts +19 -2
  161. package/tests/fold-choreography.test.ts +16 -0
  162. package/tests/fold-intensity.test.ts +94 -0
  163. package/tests/footer-precedence.test.ts +129 -0
  164. package/tests/open-file.test.ts +11 -0
  165. package/tests/stock-install.test.ts +8 -0
  166. package/tests/stream-buffer.test.ts +8 -3
  167. package/tests/tool-diff.test.ts +57 -0
  168. package/tests/waiting-clock.test.ts +78 -19
  169. package/tests/word-timeline.test.ts +14 -4
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { DiffBlock, diffTotals } from '@deepseek-ai/dsh-client-ui-primitives';
4
+ import css from './Reader.module.css';
5
+ import { diffBlockLabels } from './primitive-labels.js';
6
+ import { FOLD_TIMING } from './fold-choreography.js';
7
+ import { foldDiffHunks } from './tool-activity.js';
8
+ /**
9
+ * Reveals and withdraws its child by animating real height, in the plugin's own
10
+ * motion language. Height is the animated property rather than transform because
11
+ * the panel pushes the transcript down and the reading scroll measures that
12
+ * growth; the curve and duration are the fold choreography's, so this reads as
13
+ * the same gesture as opening a digest.
14
+ *
15
+ * Closing animates too: the child stays mounted until the shrink finishes, which
16
+ * is why the owner keeps it alive on an `open` flag instead of unmounting it.
17
+ */
18
+ function MorphPanel({ open, children, onSettled, onClosed }) {
19
+ const ref = useRef(null);
20
+ useLayoutEffect(() => {
21
+ const element = ref.current;
22
+ if (!element)
23
+ return;
24
+ element.dispatchEvent(new CustomEvent('reader-layout-start', { bubbles: true }));
25
+ let released = false;
26
+ const release = () => { if (!released) {
27
+ released = true;
28
+ element.dispatchEvent(new CustomEvent('reader-layout-end', { bubbles: true }));
29
+ } };
30
+ if (typeof element.animate !== 'function') {
31
+ release();
32
+ if (!open)
33
+ onClosed();
34
+ return;
35
+ }
36
+ const height = element.scrollHeight;
37
+ const frames = open
38
+ ? [{ height: '0px', opacity: 0, transform: 'translateY(-6px)' },
39
+ { height: `${height}px`, opacity: 1, transform: 'translateY(0)' }]
40
+ : [{ height: `${height}px`, opacity: 1 },
41
+ { height: '0px', opacity: 0 }];
42
+ const animation = element.animate(frames, {
43
+ duration: FOLD_TIMING.reveal + FOLD_TIMING.settle,
44
+ easing: 'cubic-bezier(.4,0,.2,1)',
45
+ });
46
+ let settled = false;
47
+ const settle = () => {
48
+ if (settled)
49
+ return;
50
+ settled = true;
51
+ release();
52
+ if (open) {
53
+ element.style.height = 'auto';
54
+ element.style.overflow = 'visible';
55
+ onSettled?.(element);
56
+ }
57
+ else {
58
+ onClosed();
59
+ }
60
+ };
61
+ animation.onfinish = settle;
62
+ const deadline = window.setTimeout(settle, FOLD_TIMING.reveal + FOLD_TIMING.settle + 240);
63
+ return () => { window.clearTimeout(deadline); animation.cancel(); release(); };
64
+ }, [open]);
65
+ return _jsx("div", { ref: ref, className: css.diffOverlay, children: children });
66
+ }
67
+ /**
68
+ * Keeps a newly opened panel on screen.
69
+ *
70
+ * Opening grows the row downward, so a badge near the fold reveals its card below
71
+ * the viewport and the reader has to chase it. The scroll container is not known
72
+ * here, so the nearest scrollable ancestor is found and the card's head is brought
73
+ * to the top of it — the same thing a jump-to-anchor does.
74
+ */
75
+ function revealPanel(element) {
76
+ let port = element.parentElement;
77
+ while (port && port.scrollHeight <= port.clientHeight + 1)
78
+ port = port.parentElement;
79
+ if (!port) {
80
+ element.scrollIntoView({ block: 'nearest' });
81
+ return;
82
+ }
83
+ const gap = element.getBoundingClientRect().top - port.getBoundingClientRect().top;
84
+ // Already fully visible: leave the scroll alone rather than jumping on every open.
85
+ if (gap >= 0 && element.getBoundingClientRect().bottom <= port.getBoundingClientRect().bottom)
86
+ return;
87
+ port.scrollTop += gap - 8;
88
+ }
89
+ /**
90
+ * Line counts for everything a folded run of steps changed.
91
+ *
92
+ * A run that touched no files renders nothing, so an ordinary process digest is
93
+ * unchanged. Clicking the counts opens the same diff surface the official tool
94
+ * row uses — one file at a time, red removed / green added, expandable.
95
+ */
96
+ export function DiffStat({ steps, label }) {
97
+ const hunks = useMemo(() => foldDiffHunks(steps), [steps]);
98
+ const [open, setOpen] = useState(false);
99
+ const [visible, setVisible] = useState(false);
100
+ const totals = useMemo(() => diffTotals(hunks), [hunks]);
101
+ const [active, setActive] = useState(0);
102
+ if (!hunks.length)
103
+ return null;
104
+ const current = hunks[Math.min(active, hunks.length - 1)];
105
+ return _jsxs(_Fragment, { children: [_jsx("span", { className: css.diffStatRoot, children: _jsxs("button", { type: "button", className: css.diffStatButton, "aria-expanded": open, "aria-label": `${label} · 改动 ${totals.added} 行,删除 ${totals.removed} 行`, onClick: event => {
106
+ event.stopPropagation();
107
+ if (open) {
108
+ setOpen(false);
109
+ return;
110
+ }
111
+ setVisible(true);
112
+ setOpen(true);
113
+ }, children: [_jsx("span", { className: css.diffCaret, "aria-hidden": true, children: open ? '\u25be' : '\u25b8' }), totals.added > 0 && _jsxs("span", { className: css.diffAdded, children: ["+", totals.added] }), totals.removed > 0 && _jsxs("span", { className: css.diffRemoved, children: ["-", totals.removed] })] }) }), visible && _jsx("div", { className: css.diffOverlayRow, children: _jsxs(MorphPanel, { open: open, onSettled: revealPanel, onClosed: () => setVisible(false), children: [hunks.length > 1 && _jsx("div", { className: css.diffTabs, children: hunks.map((hunk, index) => _jsx("button", { type: "button", className: css.diffTab, "data-active": index === active || undefined, onClick: () => setActive(index), title: hunk.path, children: hunk.path.split(/[/\\]+/).filter(Boolean).slice(-1)[0] ?? hunk.path }, `${hunk.path}:${index}`)) }), _jsx("div", { className: css.diffScrollArea, children: _jsx(DiffBlock, { diffs: [current], maxLines: 36, labels: diffBlockLabels }) })] }) })] });
114
+ }
115
+ //# sourceMappingURL=DiffPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiffPanel.js","sourceRoot":"","sources":["../../../src/client/DiffPanel.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Reader.d.ts","sourceRoot":"","sources":["../../../src/client/Reader.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAoB,WAAW,EAAE,MAAM,YAAY,CAAC;AA4hBhE,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,+BAyOxC"}
1
+ {"version":3,"file":"Reader.d.ts","sourceRoot":"","sources":["../../../src/client/Reader.tsx"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAoB,WAAW,EAAE,MAAM,YAAY,CAAC;AA4jBhE,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,+BAyPxC"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { Fragment, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { JsonBlock, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
4
4
  import { BlockBoundary, Blocks, contentBlocks, CopyAnswer, UserMessageActions } from './Blocks.js';
5
5
  import { ReasoningCard } from './ReasoningCard.js';
@@ -9,13 +9,16 @@ import { Disclosure, ProcessFragment, RetiringContent, StatusText, useMotionAllo
9
9
  import { StreamMotionContext } from './streaming.js';
10
10
  import { assistantSegments, boundaryOf, forkAnchorSeq, groupNodes, hasProcessContent, hasVisibleBody, isEarlierNarration, processChoiceKey, processExpanded, terminalLabel } from './projection.js';
11
11
  import { basename, createProducedFileMentions, dirname, getTurnDeliverables, showDeliverablesRow } from './deliverables.js';
12
+ import { deliverableOpenModeOf } from './open-file.js';
13
+ import { asReadonlyArray, pendingSubmissionImages } from './pending-submission.js';
12
14
  import { WaitingStatus } from './WaitingStatus.js';
13
- import { waitingAnchor } from './waiting-clock.js';
15
+ import { handsBackToModel, waitingAnchor } from './waiting-clock.js';
14
16
  import { ContextInjectionRow } from './native/ContextInjectionRow.js';
15
17
  import { TimelineRail } from './TimelineRail.js';
16
18
  import { landTurn, scrollerOf } from './conversation-scroll.js';
17
19
  import { mergeTimelineItems } from './timeline.js';
18
20
  import { presentLiveTurn, segmentLiveTurn } from './live-turn.js';
21
+ import { frostedGlassOf } from './fold-intensity.js';
19
22
  import { ChoreographedFlow, useFlowChat } from './ChoreographedFlow.js';
20
23
  import { ClosedProcessSummary } from './ClosedProcessSummary.js';
21
24
  import { StickyLane } from './StickyLane.js';
@@ -146,12 +149,31 @@ const MainNode = memo(function MainNode({ useChat, nodeKey, boundary, pinned, pr
146
149
  return null;
147
150
  if (node.kind === 'context' || node.kind === 'turn-tail' || node.kind === 'system-prompt' || node.kind === 'turn-process')
148
151
  return null;
149
- if (node.kind === 'command-input') {
150
- const data = node.data;
151
- return _jsxs("div", { className: css.user, "data-reader-anchor": true, "data-reader-key": nodeKey, children: [_jsx("p", { className: css.meta, children: "\u547D\u4EE4\u8F93\u5165" }), _jsx("p", { className: css.commandInput, children: data.text })] });
152
- }
152
+ // No 'command-input' branch: that string is not a chat node kind in any released
153
+ // host (it appears nowhere in the installed packages), so a slash command arrives
154
+ // as 'command' above and is rendered there. Keeping the branch only made the file
155
+ // look like it handled a case that cannot occur.
153
156
  return _jsxs("div", { className: css.unknown, "data-reader-anchor": true, children: [_jsxs("p", { children: ["\u6B64\u8BB0\u5F55\u7C7B\u578B\u6682\u672A\u63A5\u5165\u9605\u8BFB\u9875\uFF1A", node.kind] }), _jsx(JsonBlock, { label: "\u67E5\u770B\u539F\u59CB\u8BB0\u5F55", payload: node.data, truncatedLabel: truncatedJsonLabel })] });
154
157
  });
158
+ /**
159
+ * Sub-agents this turn dispatched, read from the host's own Turn-process row.
160
+ *
161
+ * The host already derives this from the dispatch tree (`subagentCount` on
162
+ * `TurnProcessChatData`), so counting calls here would only risk disagreeing with
163
+ * it. The row is otherwise hidden by the reader, which is why this is the one place
164
+ * its counters are read.
165
+ */
166
+ function subagentCount(snapshot, keys) {
167
+ for (const key of keys) {
168
+ const node = snapshot.nodes.get(key);
169
+ if (!node || node.kind !== 'turn-process')
170
+ continue;
171
+ const value = node.data.subagentCount;
172
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
173
+ return value;
174
+ }
175
+ return 0;
176
+ }
155
177
  function GroupStatus({ group, sessionId, useChat, useSessionPendingInteraction, motion }) {
156
178
  const pending = useSessionPendingInteraction(snapshot => snapshot.get(sessionId));
157
179
  const text = useChat(snapshot => {
@@ -168,8 +190,10 @@ function GroupStatus({ group, sessionId, useChat, useSessionPendingInteraction,
168
190
  return '等待你的操作';
169
191
  const current = turn.steps.at(-1)?.data.get('assistant-step');
170
192
  const last = current?.blocks.at(-1);
171
- if (current?.status === 'running' && last?.kind === 'tool-call')
172
- return preparingLabel(last.name);
193
+ if (current?.status === 'running' && last?.kind === 'tool-call') {
194
+ const spawned = subagentCount(snapshot, group.keys);
195
+ return spawned ? `${preparingLabel(last.name)}·${spawned} 个子代理` : preparingLabel(last.name);
196
+ }
173
197
  for (let index = group.keys.length - 1; index >= 0; index--) {
174
198
  const node = snapshot.nodes.get(group.keys[index]);
175
199
  if (!node)
@@ -186,7 +210,7 @@ function GroupStatus({ group, sessionId, useChat, useSessionPendingInteraction,
186
210
  const busy = useChat(snapshot => group.turn !== null && snapshot.timeline.turns.get(group.turn)?.status === 'open' && pending === undefined);
187
211
  return _jsx(StatusText, { text: text, motion: motion, shimmer: busy });
188
212
  }
189
- const DeliverableChip = memo(function DeliverableChip({ path, openFile, revealFile }) {
213
+ const DeliverableChip = memo(function DeliverableChip({ path, openFile, revealFile, openMode }) {
190
214
  const [status, setStatus] = useState('idle');
191
215
  const timer = useRef();
192
216
  const flash = (next) => {
@@ -231,9 +255,9 @@ const DeliverableChip = memo(function DeliverableChip({ path, openFile, revealFi
231
255
  };
232
256
  const name = basename(path);
233
257
  const folder = dirname(path);
234
- return (_jsxs("div", { className: css.deliverableChip, "data-status": status, title: path, children: [_jsxs("button", { type: "button", className: css.chipMain, onClick: onOpen, onDoubleClick: onOpen, "aria-label": `直接在编辑器中打开 ${path}`, children: [status === 'opened' ? (_jsx("svg", { className: css.statusIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (_jsxs("svg", { className: css.deliverableIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: [_jsx("path", { d: "M4 2.5h5l3 3V13.5H4V2.5z", strokeWidth: "1.2", strokeLinejoin: "round" }), _jsx("path", { d: "M9 2.5v3h3", strokeWidth: "1.2", strokeLinejoin: "round" })] })), _jsx("span", { className: css.deliverableName, children: status === 'opened' ? '已在外部打开' : name })] }), _jsxs("div", { className: css.chipActions, "aria-label": "\u6587\u4EF6\u64CD\u4F5C", children: [_jsx("button", { type: "button", className: css.chipActionBtn, title: `在访达中定位所在目录 (${folder})`, "aria-label": "\u5728\u8BBF\u8FBE\u4E2D\u663E\u793A\u6240\u5728\u76EE\u5F55", onClick: onReveal, children: status === 'revealed' ? (_jsx("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (_jsx("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M2 4.5h4l1.5 2H14v6.5H2V4.5z", strokeWidth: "1.2", strokeLinejoin: "round" }) })) }), _jsx("button", { type: "button", className: css.chipActionBtn, title: "\u590D\u5236\u76F8\u5BF9\u8DEF\u5F84", "aria-label": "\u590D\u5236\u76F8\u5BF9\u8DEF\u5F84", onClick: onCopy, children: status === 'copied' ? (_jsx("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (_jsxs("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: [_jsx("rect", { x: "5.5", y: "5.5", width: "8", height: "8", rx: "1.5", strokeWidth: "1.2" }), _jsx("path", { d: "M4 10.5H3a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v1", strokeWidth: "1.2", strokeLinecap: "round" })] })) })] })] }));
258
+ return (_jsxs("div", { className: css.deliverableChip, "data-status": status, title: path, children: [_jsxs("button", { type: "button", className: css.chipMain, onClick: onOpen, onDoubleClick: onOpen, "aria-label": openMode === 'sidebar' ? `在内置面板中打开 ${path}` : `用系统应用打开 ${path}`, children: [status === 'opened' ? (_jsx("svg", { className: css.statusIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (_jsxs("svg", { className: css.deliverableIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: [_jsx("path", { d: "M4 2.5h5l3 3V13.5H4V2.5z", strokeWidth: "1.2", strokeLinejoin: "round" }), _jsx("path", { d: "M9 2.5v3h3", strokeWidth: "1.2", strokeLinejoin: "round" })] })), _jsx("span", { className: css.deliverableName, children: status === 'opened' ? (openMode === 'sidebar' ? '已在面板打开' : '已在外部打开') : name })] }), _jsxs("div", { className: css.chipActions, "aria-label": "\u6587\u4EF6\u64CD\u4F5C", children: [_jsx("button", { type: "button", className: css.chipActionBtn, title: `在访达中定位所在目录 (${folder})`, "aria-label": "\u5728\u8BBF\u8FBE\u4E2D\u663E\u793A\u6240\u5728\u76EE\u5F55", onClick: onReveal, children: status === 'revealed' ? (_jsx("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (_jsx("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M2 4.5h4l1.5 2H14v6.5H2V4.5z", strokeWidth: "1.2", strokeLinejoin: "round" }) })) }), _jsx("button", { type: "button", className: css.chipActionBtn, title: "\u590D\u5236\u76F8\u5BF9\u8DEF\u5F84", "aria-label": "\u590D\u5236\u76F8\u5BF9\u8DEF\u5F84", onClick: onCopy, children: status === 'copied' ? (_jsx("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })) : (_jsxs("svg", { className: css.actionIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: [_jsx("rect", { x: "5.5", y: "5.5", width: "8", height: "8", rx: "1.5", strokeWidth: "1.2" }), _jsx("path", { d: "M4 10.5H3a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v1", strokeWidth: "1.2", strokeLinecap: "round" })] })) })] })] }));
235
259
  });
236
- function DeliverablesRow({ deliverables, openFile, revealFile }) {
260
+ function DeliverablesRow({ deliverables, openFile, revealFile, openMode }) {
237
261
  const [folderStatus, setFolderStatus] = useState('idle');
238
262
  const onOpenWorkspace = () => {
239
263
  try {
@@ -245,7 +269,7 @@ function DeliverablesRow({ deliverables, openFile, revealFile }) {
245
269
  // ignore
246
270
  }
247
271
  };
248
- return (_jsxs("div", { className: css.deliverablesRoot, "data-reader-deliverables": true, children: [_jsx("span", { className: css.deliverablesLabel, children: "\u4EA7\u7269" }), _jsx("div", { className: css.deliverablesLane, children: _jsxs("div", { className: css.deliverablesRow, children: [deliverables.slice(0, 8).map(path => (_jsx(DeliverableChip, { path: path, openFile: openFile, revealFile: revealFile }, path))), deliverables.length > 8 && (_jsxs("span", { className: css.deliverablesMore, children: ["+ ", deliverables.length - 8, " \u4E2A\u6587\u4EF6"] })), deliverables.length > 1 && (_jsxs("button", { type: "button", className: css.deliverablesShowFolder, "data-status": folderStatus, onClick: onOpenWorkspace, title: "\u5728\u8BBF\u8FBE\u4E2D\u6253\u5F00\u6574\u4E2A\u5DE5\u4F5C\u533A\u76EE\u5F55", children: [folderStatus === 'opened' && (_jsx("svg", { className: css.statusIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })), _jsx("span", { children: folderStatus === 'opened' ? '已打开访达' : '在文件夹中显示' })] }))] }) })] }));
272
+ return (_jsxs("div", { className: css.deliverablesRoot, "data-reader-deliverables": true, children: [_jsx("span", { className: css.deliverablesLabel, children: "\u4EA7\u7269" }), _jsx("div", { className: css.deliverablesLane, children: _jsxs("div", { className: css.deliverablesRow, children: [deliverables.slice(0, 8).map(path => (_jsx(DeliverableChip, { path: path, openFile: openFile, revealFile: revealFile, openMode: openMode }, path))), deliverables.length > 8 && (_jsxs("span", { className: css.deliverablesMore, children: ["+ ", deliverables.length - 8, " \u4E2A\u6587\u4EF6"] })), deliverables.length > 1 && (_jsxs("button", { type: "button", className: css.deliverablesShowFolder, "data-status": folderStatus, onClick: onOpenWorkspace, title: "\u5728\u8BBF\u8FBE\u4E2D\u6253\u5F00\u6574\u4E2A\u5DE5\u4F5C\u533A\u76EE\u5F55", children: [folderStatus === 'opened' && (_jsx("svg", { className: css.statusIcon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", children: _jsx("path", { d: "M3.5 8.5l3 3 6-7", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) })), _jsx("span", { children: folderStatus === 'opened' ? '已打开访达' : '在文件夹中显示' })] }))] }) })] }));
249
273
  }
250
274
  const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys, selectedProcessKeys, isAwaitingModel = false, ...props }) {
251
275
  const snapshot = props.useChat(snapshot => snapshot);
@@ -266,6 +290,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
266
290
  const flow = useMemo(() => readerFlow({ ...group, keys: mainKeys }, turn, key => nodes.get(key)), [nodes, group, mainKeys, turn]);
267
291
  const steps = useMemo(() => segmentLiveTurn(flow, key => nodes.get(key)), [flow, nodes]);
268
292
  const liveItems = useMemo(() => presentLiveTurn(steps, boundary, autoFold), [steps, boundary, autoFold]);
293
+ const openMode = deliverableOpenModeOf(useSyncExternalStore(props.openPrefs?.subscribe ?? ((fn) => { void fn; return () => { }; }), () => props.openPrefs?.getSnapshot()?.deliverableOpenMode, () => 'external'));
269
294
  const hasProcess = flow.some(item => item.kind === 'tool' || hasProcessContent(nodes.get(item.nodeKey), boundary));
270
295
  // Only a real, still-active text selection delays folding. Merely clicking,
271
296
  // focusing or scrolling the live card does not create a permanent override.
@@ -273,7 +298,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
273
298
  || liveItems.some(item => item.kind === 'fold'
274
299
  ? item.key === key || item.steps.some(step => step.key === key || ('nodeKey' in step && step.nodeKey === key))
275
300
  : item.key === key || ('nodeKey' in item.step && item.step.nodeKey === key)));
276
- const expanded = holdingSelection || processExpanded(expansionChoice, boundary);
301
+ const expanded = !autoFold || holdingSelection || processExpanded(expansionChoice, boundary);
277
302
  const [foldOpenByKey, setFoldOpenByKey] = useState({});
278
303
  const deliverables = useMemo(() => getTurnDeliverables(turn, flow), [turn, flow]);
279
304
  const fileMentions = useMemo(() => deliverables.length > 0 && props.openFile ? createProducedFileMentions(deliverables, props.openFile) : undefined, [deliverables, props.openFile]);
@@ -315,10 +340,17 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
315
340
  forkSeq,
316
341
  fileMentions,
317
342
  metrics,
343
+ getToolView: props.getToolView,
318
344
  };
319
345
  const terminal = terminalLabel(boundary.reason);
320
346
  const hasTurnError = flow.some(item => item.kind === 'node' && nodes.get(item.nodeKey)?.kind === 'turn-error');
321
- const showTerminalNotice = terminal && !hasTurnError && boundary.reason !== 'interrupted' && boundary.reason !== 'aborted';
347
+ // A stopped turn normally stays quiet, but a stopped turn that produced no
348
+ // prose at all reads as if its answer vanished into the process fold. Say so.
349
+ const hasAnswerProse = steps.some(step => step.kind === 'body'
350
+ && step.blocks.some(block => block.kind === 'text' && block.text.trim() !== ''));
351
+ const stoppedWithoutAnswer = (boundary.reason === 'interrupted' || boundary.reason === 'aborted') && !hasAnswerProse;
352
+ const showTerminalNotice = terminal && !hasTurnError
353
+ && (stoppedWithoutAnswer || (boundary.reason !== 'interrupted' && boundary.reason !== 'aborted'));
322
354
  const renderStep = (step, folded) => {
323
355
  const processOpen = folded || expanded;
324
356
  if (step.kind === 'reasoning' || step.kind === 'body')
@@ -329,7 +361,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
329
361
  return _jsx(BlockBoundary, { children: _jsx(MainNode, { ...shared, boundary: boundary, nodeKey: step.nodeKey }) });
330
362
  return _jsxs(Fragment, { children: [_jsx(BlockBoundary, { children: _jsx(ProcessNode, { useChat: useFlowChat, t: props.t, nodeKey: step.nodeKey, open: processOpen, motion: motion, onRead: pinProcess, returnFocusTo: processButton }) }), _jsx(BlockBoundary, { children: _jsx(MainNode, { ...shared, boundary: boundary, nodeKey: step.nodeKey, pinned: pinnedKeys.includes(step.nodeKey), processOpen: processOpen }) })] });
331
363
  };
332
- return _jsxs("section", { className: css.turn, "data-reader-turn": group.turn ?? 'unresolved', "data-reader-turn-state": boundary.status, "data-reader-turn-result": boundary.reason ?? undefined, children: [startsWithUser && _jsx(BlockBoundary, { children: _jsx(MainNode, { ...shared, useChat: props.useChat, boundary: boundary, nodeKey: group.keys[0] }) }), hasProcess && !isAwaitingModel && _jsx(StickyLane, { kind: "status", className: css.turnProcessSticky, children: _jsx(Disclosure, { open: expanded, onChange: setExpanded, controls: flowId, buttonRef: processButton, label: _jsx(GroupStatus, { group: group, sessionId: props.sessionId, useChat: props.useChat, useSessionPendingInteraction: props.useSessionPendingInteraction, motion: motion }) }) }), boundary.status === 'closed' && hasProcess && _jsx(ClosedProcessSummary, { open: expanded, onChange: setExpanded, controls: flowId, steps: steps.filter(step => {
364
+ return _jsxs("section", { className: css.turn, "data-reader-turn": group.turn ?? 'unresolved', "data-reader-turn-state": boundary.status, "data-reader-turn-result": boundary.reason ?? undefined, children: [startsWithUser && _jsx(BlockBoundary, { children: _jsx(MainNode, { ...shared, useChat: props.useChat, boundary: boundary, nodeKey: group.keys[0] }) }), hasProcess && !isAwaitingModel && _jsx(StickyLane, { kind: "status", className: css.turnProcessSticky, children: _jsx(Disclosure, { open: expanded, onChange: setExpanded, controls: flowId, buttonRef: processButton, label: _jsx(GroupStatus, { group: group, sessionId: props.sessionId, useChat: props.useChat, useSessionPendingInteraction: props.useSessionPendingInteraction, motion: motion }) }) }), boundary.status === 'closed' && hasProcess && autoFold && _jsx(ClosedProcessSummary, { open: expanded, onChange: setExpanded, controls: flowId, steps: steps.filter(step => {
333
365
  if (step.kind === 'user')
334
366
  return false;
335
367
  if (step.kind !== 'body')
@@ -338,7 +370,7 @@ const TurnGroup = memo(function TurnGroup({ group, motion, autoFold, pinnedKeys,
338
370
  return !!node && isNode(node, 'assistant-step') && (isEarlierNarration(node.data, boundary)
339
371
  || node.data.blocks.some(block => block.kind === 'tool-call')
340
372
  || (boundary.latestStep > 0 && node.data.step < boundary.latestStep));
341
- }) }), _jsx(ChoreographedFlow, { id: flowId, frame: presentation, motion: motion, enabled: autoFold && boundary.status === 'open' && !holdingSelection, urgent: hasTurnError || interaction !== undefined || !sessionRunning, open: foldOpenByKey, processOpen: expanded, onOpenChange: (key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }, renderStep: renderStep }), !hasProcess && boundary.status === 'open' && !isAwaitingModel && _jsx("div", { className: css.disclosure, "data-reader-status-only": true, children: _jsx(GroupStatus, { group: group, sessionId: props.sessionId, useChat: props.useChat, useSessionPendingInteraction: props.useSessionPendingInteraction, motion: motion }) }), showDeliverablesRow(boundary.status, deliverables) && _jsx(DeliverablesRow, { deliverables: deliverables, openFile: props.openFile, revealFile: props.revealFile }), showTerminalNotice && _jsx("div", { className: css.notice, "data-reader-terminal": true, children: terminal })] });
373
+ }) }), _jsx(ChoreographedFlow, { id: flowId, frame: presentation, motion: motion, enabled: autoFold && boundary.status === 'open' && !holdingSelection, urgent: hasTurnError || interaction !== undefined || !sessionRunning, open: foldOpenByKey, processOpen: expanded, onOpenChange: (key, value) => { pinProcess(); setFoldOpenByKey(current => ({ ...current, [key]: value })); }, renderStep: renderStep }), !hasProcess && boundary.status === 'open' && !isAwaitingModel && _jsx("div", { className: css.disclosure, "data-reader-status-only": true, children: _jsx(GroupStatus, { group: group, sessionId: props.sessionId, useChat: props.useChat, useSessionPendingInteraction: props.useSessionPendingInteraction, motion: motion }) }), showDeliverablesRow(boundary.status, deliverables) && _jsx(DeliverablesRow, { deliverables: deliverables, openFile: props.openFile, revealFile: props.revealFile, openMode: openMode }), showTerminalNotice && _jsx("div", { className: css.notice, "data-reader-terminal": true, children: terminal })] });
342
374
  });
343
375
  export function Reader(props) {
344
376
  const root = useRef(null);
@@ -353,51 +385,47 @@ export function Reader(props) {
353
385
  const hasMore = props.useSession(snapshot => snapshot.hasMore);
354
386
  const loadingOlder = props.useSession(snapshot => snapshot.loadingOlder);
355
387
  const pendingSubmissions = props.useSession(snapshot => snapshot.pendingSubmissions);
356
- const waitAnchor = waitingAnchor(order, key => nodes.get(key), pendingSubmissions);
388
+ const pendingList = asReadonlyArray(pendingSubmissions);
389
+ const waitAnchor = waitingAnchor(order, key => nodes.get(key), pendingList);
357
390
  const motionPreference = props.useStore(state => state.motion);
358
391
  const motion = useMotionAllowed(motionPreference);
359
- const autoFold = props.useStore(state => state.autoFold ?? true);
392
+ const prefsSnap = useSyncExternalStore(props.openPrefs?.subscribe ?? ((fn) => { void fn; return () => { }; }), () => props.openPrefs?.getSnapshot?.(), () => undefined);
393
+ const storeAutoFold = props.useStore(state => state.autoFold);
394
+ const autoFold = prefsSnap?.autoFold ?? (prefsSnap?.foldIntensity !== undefined ? prefsSnap.foldIntensity !== 0 : undefined) ?? storeAutoFold ?? true;
395
+ const frostedGlass = frostedGlassOf(prefsSnap);
360
396
  const streamMotion = useMemo(() => ({ enabled: motion, activatedAt: activatedAt.current }), [motion]);
361
397
  const groups = useMemo(() => groupNodes(order, key => nodes.get(key)), [order, nodes, timeline]);
362
398
  const isAwaitingModel = useMemo(() => {
363
399
  // 状态机铁律:「深度求索中」与「正在处理/思考」永远互斥。
364
400
  // 1. 仅空闲发送(!running)的本地回显窗口:用户按下回车第 0 毫秒立即反馈。
365
401
  // 忙碌时的 queued/steering 回显不算等待——模型本来就在工作。
366
- if (!running && pendingSubmissions && pendingSubmissions.length > 0)
402
+ if (!running && pendingList.length > 0)
367
403
  return true;
368
404
  const lastKey = order.at(-1);
369
405
  const lastNode = lastKey ? nodes.get(lastKey) : undefined;
370
- const lastIsUser = lastNode?.kind === 'user' || lastNode?.kind === 'steering';
371
- // 2. 最后一个节点是用户发言(含补发消息):检查该用户节点**之后**是否已有模型产出。
372
- // 之前用 slice(1) 是 Bug——补发消息在组中部,前面的旧输出会让等待永远不出现。
373
- if (lastIsUser && lastKey) {
374
- const lastGroup = groups.at(-1);
375
- const turn = lastGroup?.turn === null || lastGroup?.turn === undefined ? undefined : timeline.turns.get(lastGroup.turn);
376
- if (turn?.status === 'closed')
377
- return false;
378
- const keys = lastGroup?.keys ?? [];
379
- const userIdx = keys.lastIndexOf(lastKey);
380
- const hasOutputAfter = keys.slice(userIdx + 1).some(key => {
381
- const node = nodes.get(key);
382
- if (!node || node.visibility === 'hidden')
383
- return false;
384
- if (node.kind === 'assistant-step') {
385
- const data = node.data;
386
- return Array.isArray(data.blocks) && data.blocks.length > 0;
387
- }
388
- return node.kind === 'tool-call' || node.kind === 'tool-return' || node.kind === 'command-input';
389
- });
390
- return !hasOutputAfter;
391
- }
392
- // 3. assistant-step 已创建但一个 block 都没有:模型收到请求、内容未吐出的瞬间。
393
- if (lastNode?.kind === 'assistant-step') {
406
+ if (!lastKey || !lastNode)
407
+ return false;
408
+ // 2. 模型已经产出内容:等待结束,计时器必须立刻消失。
409
+ // 「产出」= assistant-step 已带 block(正文/思维链/工具调用都算)。
410
+ if (lastNode.kind === 'assistant-step') {
394
411
  const data = lastNode.data;
395
- if (data.status === 'running' && (!data.blocks || data.blocks.length === 0)) {
396
- return true;
397
- }
412
+ const blocks = data.blocks ?? [];
413
+ // 空 block 且仍在跑 = 请求已发出、模型还没吐字,正是要计时的那一刻。
414
+ return blocks.length === 0 && data.status === 'running';
398
415
  }
399
- return false;
400
- }, [running, pendingSubmissions, order, nodes, groups, timeline]);
416
+ // 3. 球在模型脚下:用户刚发言,或工具已返回、上下文已注入、命令已执行。
417
+ // 这些时刻模型随时可能卡住,正是要计时的地方;工具执行期间不计时,
418
+ // 因为那时忙的是工具而不是模型。
419
+ // handsBackToModel 内部按 chat 层 kind 判定,并区分「工具已返回」与
420
+ // 「工具仍在跑」——后者不算等待。
421
+ const modelOwesResponse = handsBackToModel(lastNode);
422
+ if (!modelOwesResponse)
423
+ return false;
424
+ // 轮次已结束就没什么可等的。
425
+ const lastGroup = groups.at(-1);
426
+ const turn = lastGroup?.turn === null || lastGroup?.turn === undefined ? undefined : timeline.turns.get(lastGroup.turn);
427
+ return turn?.status !== 'closed';
428
+ }, [running, pendingList, order, nodes, groups, timeline]);
401
429
  const scroll = useReadingScroll(root, motion);
402
430
  const pinnedKeys = usePinnedSelection(root);
403
431
  const selectedProcessKeys = usePinnedSelection(root, '[data-reader-process]');
@@ -505,7 +533,7 @@ export function Reader(props) {
505
533
  }, [props.loadThrough, props.loadOlder, scroll.jump, scroll.release, timelineItems]);
506
534
  const lastKey = order.at(-1);
507
535
  const lastNode = lastKey ? nodes.get(lastKey) : undefined;
508
- const lastSubmissionId = pendingSubmissions?.length ? pendingSubmissions[pendingSubmissions.length - 1].requestId : null;
536
+ const lastSubmissionId = pendingList.length ? pendingList[pendingList.length - 1].requestId : null;
509
537
  const lastOrderKeyRef = useRef(lastKey);
510
538
  const lastSubmissionRef = useRef(lastSubmissionId);
511
539
  useLayoutEffect(() => {
@@ -518,13 +546,16 @@ export function Reader(props) {
518
546
  }
519
547
  }, [lastKey, lastNode?.kind, lastSubmissionId, scroll]);
520
548
  const visibleSubmissions = useMemo(() => {
521
- if (!pendingSubmissions || pendingSubmissions.length === 0)
549
+ if (pendingList.length === 0)
522
550
  return [];
523
- return pendingSubmissions.filter(sub => sub.placement !== 'queued');
524
- }, [pendingSubmissions]);
551
+ return pendingList.filter(sub => sub.placement !== 'queued');
552
+ }, [pendingList]);
525
553
  // ChatView publishes data-chat-flow="" on its column. Skins treat a
526
554
  // scrollport without that hook as inspect-only and hide [data-composer-seat].
527
- return _jsx(StreamMotionContext.Provider, { value: streamMotion, children: _jsxs("div", { ref: root, className: css.root, "data-dsh-better-display": "0.1.1", "data-reader-wait-clock-version": "input-v1", "data-reader-wait-start": waitAnchor.time ?? undefined, "data-motion": motion ? 'on' : 'off', children: [_jsx(TimelineRail, { items: timelineItems, activeTurn: activeTurn, busyTurn: busyTurn, onNavigate: onNavigateTurn }), _jsxs("div", { className: css.column, "data-chat-flow": "", children: [_jsx(StickyLane, { kind: "toolbar", className: css.toolbar, children: _jsx("button", { type: "button", className: css.textButton, "aria-pressed": autoFold, onClick: () => props.actions.setAutoFold(!autoFold), title: "\u65B0\u601D\u8003\u4EA7\u751F\u65F6\uFF0C\u662F\u5426\u81EA\u52A8\u5C06\u6B64\u524D\u6B65\u9AA4\u6536\u62E2\u4E3A\u4E00\u884C\u6C47\u603B\u3002\u5173\u95ED\u540E\u5B8C\u6574\u4FDD\u7559\u539F\u59CB\u8FC7\u7A0B\u4E0E\u6D41\u5F0F\u8F93\u51FA\u3002", children: `自动折叠${autoFold ? '开' : '关'}` }) }), hasMore && _jsx("button", { type: "button", className: css.historyButton, disabled: loadingOlder, onClick: async () => {
555
+ return _jsx(StreamMotionContext.Provider, { value: streamMotion, children: _jsxs("div", { ref: root, className: css.root, "data-dsh-better-display": "0.2.1", "data-reader-wait-clock-version": "input-v1", "data-reader-wait-start": waitAnchor.time ?? undefined, "data-motion": motion ? 'on' : 'off', "data-reader-glass": frostedGlass || undefined, "data-reader-auto-fold": autoFold ? 'on' : 'off', children: [_jsx(TimelineRail, { items: timelineItems, activeTurn: activeTurn, busyTurn: busyTurn, onNavigate: onNavigateTurn }), _jsxs("div", { className: css.column, "data-chat-flow": "", children: [_jsx(StickyLane, { kind: "toolbar", className: css.toolbar, children: _jsx("button", { type: "button", className: css.textButton, "aria-pressed": autoFold, onClick: () => {
556
+ props.actions.setAutoFold(!autoFold);
557
+ props.openPrefs?.actions?.setAutoFold?.(!autoFold);
558
+ }, title: "\u65B0\u601D\u8003\u4EA7\u751F\u65F6\uFF0C\u662F\u5426\u81EA\u52A8\u5C06\u6B64\u524D\u6B65\u9AA4\u6536\u62E2\u4E3A\u4E00\u884C\u6C47\u603B\u3002\u5173\u95ED\u540E\u5B8C\u6574\u4FDD\u7559\u539F\u59CB\u8FC7\u7A0B\u4E0E\u6D41\u5F0F\u8F93\u51FA\u3002", children: `自动折叠${autoFold ? '开' : '关'}` }) }), hasMore && _jsx("button", { type: "button", className: css.historyButton, disabled: loadingOlder, onClick: async () => {
528
559
  setHistoryError(false);
529
560
  try {
530
561
  await props.loadOlder();
@@ -532,6 +563,9 @@ export function Reader(props) {
532
563
  catch {
533
564
  setHistoryError(true);
534
565
  }
535
- }, children: loadingOlder ? '正在加载更早记录' : '加载更早记录' }), historyError && _jsx("div", { className: css.notice, children: "\u5386\u53F2\u8BB0\u5F55\u52A0\u8F7D\u5931\u8D25\uFF0C\u53EF\u518D\u6B21\u5C1D\u8BD5\uFF1B\u73B0\u6709\u5185\u5BB9\u672A\u6539\u53D8\u3002" }), openError && _jsxs("div", { className: css.error, role: "alert", children: ["\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\uFF1A", openError.message] }), loading && groups.length === 0 && _jsx("p", { className: css.empty, role: "status", children: "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u2026" }), groups.map(group => _jsx(TurnGroup, { ...props, group: group, motion: motion, autoFold: autoFold, pinnedKeys: pinnedKeys, selectedProcessKeys: selectedProcessKeys, isAwaitingModel: isAwaitingModel && group.key === groups.at(-1)?.key }, group.key)), visibleSubmissions.map(submission => (_jsxs("div", { className: css.userCluster, "data-reader-pending-submission": true, children: [submission.attachments.some(item => item.type === 'image') && (_jsx("div", { className: css.userImages, children: submission.attachments.map((item, idx) => item.type === 'image' ? (_jsx("figure", { className: css.imageFigure, children: _jsx("div", { className: css.imageFrame, style: { aspectRatio: `${item.value.width || 4} / ${item.value.height || 3}` }, children: _jsx("img", { src: item.value.previewUrl, alt: item.value.name ?? '发送的图片', className: css.pendingImage }) }) }, idx)) : null) })), submission.text ? (_jsx("div", { className: css.user, children: _jsx("div", { className: css.blocks, children: submission.text }) })) : null, _jsx(UserMessageActions, { text: submission.text, time: submission.time })] }, submission.requestId))), isAwaitingModel && _jsx(WaitingStatus, { anchor: waitAnchor, label: props.t ? props.t('chat.deepDiving') : '深度求索中...' }), pending !== undefined && _jsxs("div", { className: css.attention, role: "alert", "data-reader-attention": true, children: [_jsx("strong", { children: pending.kind === 'question' ? '需要你回答一个问题' : '需要你的确认' }), _jsx("span", { children: "\u8BF7\u5728\u4E0B\u65B9\u539F\u751F\u64CD\u4F5C\u533A\u5904\u7406\u3002\u6B64\u63D0\u793A\u4E0D\u4F1A\u6536\u8FDB\u6267\u884C\u8FC7\u7A0B\u3002" })] }), scroll.detached && _jsx("div", { className: css.jumpDock, children: _jsx("button", { type: "button", className: css.jump, "aria-label": "\u56DE\u5230\u5E95\u90E8", title: "\u56DE\u5230\u5E95\u90E8", onClick: scroll.jump, children: _jsx("svg", { viewBox: "0 0 14 14", width: "14", height: "14", "aria-hidden": "true", children: _jsx("path", { d: "M3 5.5 7 9.5 11 5.5", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) })] })] }) });
566
+ }, children: loadingOlder ? '正在加载更早记录' : '加载更早记录' }), historyError && _jsx("div", { className: css.notice, children: "\u5386\u53F2\u8BB0\u5F55\u52A0\u8F7D\u5931\u8D25\uFF0C\u53EF\u518D\u6B21\u5C1D\u8BD5\uFF1B\u73B0\u6709\u5185\u5BB9\u672A\u6539\u53D8\u3002" }), openError && _jsxs("div", { className: css.error, role: "alert", children: ["\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\uFF1A", openError.message] }), loading && groups.length === 0 && _jsx("p", { className: css.empty, role: "status", children: "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u2026" }), groups.map(group => _jsx(TurnGroup, { ...props, group: group, motion: motion, autoFold: autoFold, pinnedKeys: pinnedKeys, selectedProcessKeys: selectedProcessKeys, isAwaitingModel: isAwaitingModel && group.key === groups.at(-1)?.key }, group.key)), visibleSubmissions.map(submission => {
567
+ const images = pendingSubmissionImages(submission);
568
+ return (_jsxs("div", { className: css.userCluster, "data-reader-pending-submission": true, children: [images.length > 0 && (_jsx("div", { className: css.userImages, children: images.map((item, idx) => (_jsx("figure", { className: css.imageFigure, children: _jsx("div", { className: css.imageFrame, style: { aspectRatio: `${item.width || 4} / ${item.height || 3}` }, children: _jsx("img", { src: item.previewUrl, alt: item.name ?? '发送的图片', className: css.pendingImage }) }) }, idx))) })), submission.text ? (_jsx("div", { className: css.user, children: _jsx("div", { className: css.blocks, children: submission.text }) })) : null, _jsx(UserMessageActions, { text: submission.text ?? '', time: submission.time })] }, submission.requestId));
569
+ }), isAwaitingModel && _jsx(WaitingStatus, { anchor: waitAnchor, label: props.t ? props.t('chat.deepDiving') : '深度求索中...' }), pending !== undefined && _jsxs("div", { className: css.attention, role: "alert", "data-reader-attention": true, children: [_jsx("strong", { children: pending.kind === 'question' ? '需要你回答一个问题' : '需要你的确认' }), _jsx("span", { children: "\u8BF7\u5728\u4E0B\u65B9\u539F\u751F\u64CD\u4F5C\u533A\u5904\u7406\u3002\u6B64\u63D0\u793A\u4E0D\u4F1A\u6536\u8FDB\u6267\u884C\u8FC7\u7A0B\u3002" })] }), scroll.detached && _jsx("div", { className: css.jumpDock, children: _jsx("button", { type: "button", className: css.jump, "aria-label": "\u56DE\u5230\u5E95\u90E8", title: "\u56DE\u5230\u5E95\u90E8", onClick: scroll.jump, children: _jsx("svg", { viewBox: "0 0 14 14", width: "14", height: "14", "aria-hidden": "true", children: _jsx("path", { d: "M3 5.5 7 9.5 11 5.5", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }) }) })] })] }) });
536
570
  }
537
571
  //# sourceMappingURL=Reader.js.map