@jcy2387/dsh-conversation-ui 0.1.0-rc.12
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/LICENSE +21 -0
- package/README.en.md +123 -0
- package/README.md +123 -0
- package/conversation-ui-off.yml +3 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +3672 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +302 -0
- package/package.json +182 -0
- package/src/boot-config.ts +28 -0
- package/src/client/AnimatedDisclosure.tsx +89 -0
- package/src/client/ConversationCard.module.css +253 -0
- package/src/client/ConversationCard.tsx +122 -0
- package/src/client/DeliverablesCard.module.css +187 -0
- package/src/client/DeliverablesCard.tsx +142 -0
- package/src/client/FollowHost.tsx +25 -0
- package/src/client/ToolActivityGroup.tsx +341 -0
- package/src/client/TurnPreludeUserNodeView.tsx +258 -0
- package/src/client/TurnProcessMember.tsx +92 -0
- package/src/client/TypewriterAssistantNodeView.module.css +1260 -0
- package/src/client/TypewriterAssistantNodeView.tsx +390 -0
- package/src/client/TypewriterToolNodeView.tsx +916 -0
- package/src/client/conversation-ui-card-controller.ts +162 -0
- package/src/client/conversation-ui-settings-api.ts +65 -0
- package/src/client/deliverables.ts +263 -0
- package/src/client/index.ts +242 -0
- package/src/client/locales.ts +74 -0
- package/src/client/teleprompterGlide.ts +590 -0
- package/src/client/turnElapsed.ts +27 -0
- package/src/client/turnProcessFold.ts +224 -0
- package/src/client/useConversationContent.ts +471 -0
- package/src/client/useFpsGuard.ts +84 -0
- package/src/config.ts +44 -0
- package/src/css-modules.d.ts +9 -0
- package/src/index.ts +2 -0
- package/src/package-meta.ts +39 -0
- package/src/plugin.ts +155 -0
- package/src/profile-installation.ts +121 -0
- package/src/settings-api.ts +33 -0
- package/src/settings.ts +30 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,3672 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@jcy2387/dsh-conversation-ui",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
29
|
+
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
31
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
32
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
33
|
+
let react_dom = require("react-dom");
|
|
34
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
35
|
+
//#region src/client/useConversationContent.ts
|
|
36
|
+
/**
|
|
37
|
+
* Stream-smoothing reveal hook.
|
|
38
|
+
*
|
|
39
|
+
* Buffers the model's chunked text and reveals it at a cadence that tracks
|
|
40
|
+
* the observed arrival rate, so a long reply never dumps whole paragraphs at
|
|
41
|
+
* once and a fast stream never stutters. Port of lobe-ui's smoother: EMA
|
|
42
|
+
* arrival cps + chunk size, backlog pressure, commit-interval widening with
|
|
43
|
+
* tail length, and a flush-speed settle drain once the input idles. The
|
|
44
|
+
* reveal decision is the pure {@link computeRevealStep} for unit tests.
|
|
45
|
+
*
|
|
46
|
+
* `shouldHoldBack` is the performance guard's veto: while it returns true the
|
|
47
|
+
* loop keeps measuring but skips the DOM commit, so an offscreen reply never
|
|
48
|
+
* competes with visible frames when the frame rate is degraded.
|
|
49
|
+
*/
|
|
50
|
+
const PRESET_CONFIG = {
|
|
51
|
+
balanced: {
|
|
52
|
+
activeInputWindowMs: 220,
|
|
53
|
+
defaultCps: 80,
|
|
54
|
+
emaAlpha: .35,
|
|
55
|
+
flushCps: 180,
|
|
56
|
+
largeAppendChars: 120,
|
|
57
|
+
maxActiveCps: 360,
|
|
58
|
+
maxCps: 240,
|
|
59
|
+
maxFlushCps: 480,
|
|
60
|
+
minCommitIntervalMs: 16,
|
|
61
|
+
minCps: 24,
|
|
62
|
+
settleAfterMs: 280,
|
|
63
|
+
settleDrainMaxMs: 420,
|
|
64
|
+
settleDrainMinMs: 120,
|
|
65
|
+
targetBufferMs: 40
|
|
66
|
+
},
|
|
67
|
+
realtime: {
|
|
68
|
+
activeInputWindowMs: 140,
|
|
69
|
+
defaultCps: 120,
|
|
70
|
+
emaAlpha: .45,
|
|
71
|
+
flushCps: 240,
|
|
72
|
+
largeAppendChars: 180,
|
|
73
|
+
maxActiveCps: 480,
|
|
74
|
+
maxCps: 320,
|
|
75
|
+
maxFlushCps: 640,
|
|
76
|
+
minCommitIntervalMs: 16,
|
|
77
|
+
minCps: 32,
|
|
78
|
+
settleAfterMs: 200,
|
|
79
|
+
settleDrainMaxMs: 280,
|
|
80
|
+
settleDrainMinMs: 100,
|
|
81
|
+
targetBufferMs: 24
|
|
82
|
+
},
|
|
83
|
+
silky: {
|
|
84
|
+
activeInputWindowMs: 280,
|
|
85
|
+
defaultCps: 64,
|
|
86
|
+
emaAlpha: .28,
|
|
87
|
+
flushCps: 140,
|
|
88
|
+
largeAppendChars: 100,
|
|
89
|
+
maxActiveCps: 280,
|
|
90
|
+
maxCps: 180,
|
|
91
|
+
maxFlushCps: 400,
|
|
92
|
+
minCommitIntervalMs: 16,
|
|
93
|
+
minCps: 20,
|
|
94
|
+
settleAfterMs: 360,
|
|
95
|
+
settleDrainMaxMs: 520,
|
|
96
|
+
settleDrainMinMs: 160,
|
|
97
|
+
targetBufferMs: 56
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const MAX_COMMIT_INTERVAL_MS = 16;
|
|
101
|
+
const QUEUE_FRAME_MS = 16.67;
|
|
102
|
+
const CATCHUP_SECONDS = .15;
|
|
103
|
+
const clamp$1 = (value, min, max) => {
|
|
104
|
+
return Math.min(max, Math.max(min, value));
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Demo per-frame reveal: drain `backlog / 8` characters, at least 1, scaled
|
|
108
|
+
* by frame time. A small queue types one glyph per frame; a burst raises the
|
|
109
|
+
* step so the display does not fall behind.
|
|
110
|
+
* @param backlog - Unrevealed characters.
|
|
111
|
+
* @param dtMs - Frame delta in ms.
|
|
112
|
+
* @returns Characters to reveal this frame.
|
|
113
|
+
*/
|
|
114
|
+
function computeQueueReveal(backlog, dtMs) {
|
|
115
|
+
if (backlog <= 0 || dtMs <= 0) return 0;
|
|
116
|
+
return Math.min(backlog, Math.max(1, Math.ceil(backlog / 8 * (dtMs / QUEUE_FRAME_MS))));
|
|
117
|
+
}
|
|
118
|
+
const graphemeSegmenter = typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : void 0;
|
|
119
|
+
/** Split text into user-perceived characters without tearing emoji or combining marks. */
|
|
120
|
+
function splitGraphemes(text) {
|
|
121
|
+
if (graphemeSegmenter === void 0) return [...text];
|
|
122
|
+
return [...graphemeSegmenter.segment(text)].map((item) => item.segment);
|
|
123
|
+
}
|
|
124
|
+
/** Counts user-perceived characters, not UTF-16 units or Unicode code points. */
|
|
125
|
+
const countChars = (text) => splitGraphemes(text).length;
|
|
126
|
+
/** Pure settle-drain decision shared by the frame loop and its tests. */
|
|
127
|
+
function computeSettleDrain(config, input) {
|
|
128
|
+
if (input.inputActive || !input.settling) return 0;
|
|
129
|
+
const overflowCps = Math.max(0, input.backlog - 300) * 1e3 / 2;
|
|
130
|
+
const drainTargetMs = clamp$1(input.backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);
|
|
131
|
+
const settleCps = input.backlog * 1e3 / drainTargetMs;
|
|
132
|
+
return clamp$1(Math.max(settleCps, overflowCps), config.flushCps, config.maxFlushCps);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Drain rate multiplier once the input ends: leftover backlog reveals at
|
|
136
|
+
* this multiple of the steady rate, so the end never drags.
|
|
137
|
+
*/
|
|
138
|
+
const SETTLE_DRAIN_MULTIPLIER = 1.8;
|
|
139
|
+
/** Pure per-frame reveal decision shared by the loop and its tests. */
|
|
140
|
+
function computeRevealStep(config, input, dtSeconds) {
|
|
141
|
+
const trackedCps = Math.max(input.emaCps, input.arrivalCpsEma);
|
|
142
|
+
const baseCps = clamp$1(trackedCps, config.minCps, config.maxFlushCps);
|
|
143
|
+
const targetLagChars = input.inputActive ? Math.max(2, Math.round(baseCps * config.targetBufferMs / 1e3)) : 0;
|
|
144
|
+
let currentCps;
|
|
145
|
+
if (input.steadyCps !== void 0) currentCps = input.inputActive || input.settling ? clamp$1(input.steadyCps * (input.inputActive ? 1 : SETTLE_DRAIN_MULTIPLIER), config.minCps, config.maxFlushCps) : 0;
|
|
146
|
+
else if (input.inputActive) {
|
|
147
|
+
const overflow = Math.max(0, input.backlog - 32);
|
|
148
|
+
const catchup = overflow > 0 ? overflow / CATCHUP_SECONDS : 0;
|
|
149
|
+
currentCps = clamp$1(baseCps * 1.08 + catchup, config.minCps, config.maxFlushCps);
|
|
150
|
+
} else if (input.settling) currentCps = computeSettleDrain(config, input);
|
|
151
|
+
else {
|
|
152
|
+
const idleFlushCps = Math.max(config.flushCps, baseCps * 1.8, input.arrivalCpsEma * .8);
|
|
153
|
+
currentCps = clamp$1(idleFlushCps, config.flushCps, config.maxFlushCps);
|
|
154
|
+
}
|
|
155
|
+
const minRevealChars = input.inputActive ? 1 : 2;
|
|
156
|
+
return {
|
|
157
|
+
revealChars: Math.max(minRevealChars, Math.round(currentCps * dtSeconds)),
|
|
158
|
+
targetLagChars
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Smooth a chunked content stream into a reveal-paced display string.
|
|
163
|
+
*
|
|
164
|
+
* @param content - The full accumulated input so far.
|
|
165
|
+
* @param options - Preset, guard, and steady-rate wiring.
|
|
166
|
+
* @returns The displayed content, revealed at the smoothed cadence.
|
|
167
|
+
*/
|
|
168
|
+
function useConversationContent(content, { enabled = true, preset = "balanced", shouldHoldBack, steadyCps, defaultCps, speedCpsRef } = {}) {
|
|
169
|
+
const config = PRESET_CONFIG[preset];
|
|
170
|
+
const seedCps = defaultCps ?? config.defaultCps;
|
|
171
|
+
const [displayedContent, setDisplayedContent] = (0, react.useState)(content);
|
|
172
|
+
const displayedContentRef = (0, react.useRef)(content);
|
|
173
|
+
const displayedCountRef = (0, react.useRef)(countChars(content));
|
|
174
|
+
const targetContentRef = (0, react.useRef)(content);
|
|
175
|
+
const targetCharsRef = (0, react.useRef)(splitGraphemes(content));
|
|
176
|
+
const targetCountRef = (0, react.useRef)(countChars(content));
|
|
177
|
+
const emaCpsRef = (0, react.useRef)(seedCps);
|
|
178
|
+
const lastInputTsRef = (0, react.useRef)(0);
|
|
179
|
+
const lastInputCountRef = (0, react.useRef)(countChars(content));
|
|
180
|
+
const chunkSizeEmaRef = (0, react.useRef)(1);
|
|
181
|
+
const arrivalCpsEmaRef = (0, react.useRef)(seedCps);
|
|
182
|
+
const rafRef = (0, react.useRef)(null);
|
|
183
|
+
const lastFrameTsRef = (0, react.useRef)(null);
|
|
184
|
+
const wakeTimerRef = (0, react.useRef)(null);
|
|
185
|
+
const holdBackRef = (0, react.useRef)(shouldHoldBack);
|
|
186
|
+
const speedOutRef = (0, react.useRef)(speedCpsRef);
|
|
187
|
+
speedOutRef.current = speedCpsRef;
|
|
188
|
+
(0, react.useEffect)(() => {
|
|
189
|
+
holdBackRef.current = shouldHoldBack;
|
|
190
|
+
}, [shouldHoldBack]);
|
|
191
|
+
const clearWakeTimer = (0, react.useCallback)(() => {
|
|
192
|
+
if (wakeTimerRef.current !== null) {
|
|
193
|
+
clearTimeout(wakeTimerRef.current);
|
|
194
|
+
wakeTimerRef.current = null;
|
|
195
|
+
}
|
|
196
|
+
}, []);
|
|
197
|
+
const stopFrameLoop = (0, react.useCallback)(() => {
|
|
198
|
+
if (rafRef.current !== null) {
|
|
199
|
+
cancelAnimationFrame(rafRef.current);
|
|
200
|
+
rafRef.current = null;
|
|
201
|
+
}
|
|
202
|
+
lastFrameTsRef.current = null;
|
|
203
|
+
}, []);
|
|
204
|
+
const stopScheduling = (0, react.useCallback)(() => {
|
|
205
|
+
stopFrameLoop();
|
|
206
|
+
clearWakeTimer();
|
|
207
|
+
}, [clearWakeTimer, stopFrameLoop]);
|
|
208
|
+
const startFrameLoopRef = (0, react.useRef)(() => {});
|
|
209
|
+
const scheduleFrameWake = (0, react.useCallback)((delayMs) => {
|
|
210
|
+
clearWakeTimer();
|
|
211
|
+
wakeTimerRef.current = setTimeout(() => {
|
|
212
|
+
wakeTimerRef.current = null;
|
|
213
|
+
startFrameLoopRef.current();
|
|
214
|
+
}, Math.max(1, Math.ceil(delayMs)));
|
|
215
|
+
}, [clearWakeTimer]);
|
|
216
|
+
const syncImmediate = (0, react.useCallback)((nextContent) => {
|
|
217
|
+
stopScheduling();
|
|
218
|
+
const chars = splitGraphemes(nextContent);
|
|
219
|
+
const now = performance.now();
|
|
220
|
+
targetContentRef.current = nextContent;
|
|
221
|
+
targetCharsRef.current = chars;
|
|
222
|
+
targetCountRef.current = chars.length;
|
|
223
|
+
displayedContentRef.current = nextContent;
|
|
224
|
+
displayedCountRef.current = chars.length;
|
|
225
|
+
setDisplayedContent(nextContent);
|
|
226
|
+
emaCpsRef.current = seedCps;
|
|
227
|
+
chunkSizeEmaRef.current = 1;
|
|
228
|
+
arrivalCpsEmaRef.current = seedCps;
|
|
229
|
+
lastInputTsRef.current = now;
|
|
230
|
+
lastInputCountRef.current = chars.length;
|
|
231
|
+
}, [seedCps, stopScheduling]);
|
|
232
|
+
const startFrameLoop = (0, react.useCallback)(() => {
|
|
233
|
+
clearWakeTimer();
|
|
234
|
+
if (rafRef.current !== null) return;
|
|
235
|
+
const tick = () => {
|
|
236
|
+
const targetCount = targetCountRef.current;
|
|
237
|
+
const displayedCount = displayedCountRef.current;
|
|
238
|
+
const backlog = targetCount - displayedCount;
|
|
239
|
+
if (backlog <= 0) {
|
|
240
|
+
stopFrameLoop();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const now = performance.now();
|
|
244
|
+
if (lastFrameTsRef.current === null) {
|
|
245
|
+
lastFrameTsRef.current = now;
|
|
246
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const commitIntervalMs = Math.min(MAX_COMMIT_INTERVAL_MS, config.minCommitIntervalMs);
|
|
250
|
+
const frameIntervalMs = Math.max(0, now - lastFrameTsRef.current);
|
|
251
|
+
if (frameIntervalMs < commitIntervalMs) {
|
|
252
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .12));
|
|
256
|
+
lastFrameTsRef.current = now;
|
|
257
|
+
const idleMs = now - lastInputTsRef.current;
|
|
258
|
+
const inputActive = idleMs <= config.activeInputWindowMs;
|
|
259
|
+
const settling = !inputActive && idleMs >= config.settleAfterMs;
|
|
260
|
+
let revealChars;
|
|
261
|
+
if (steadyCps !== void 0) {
|
|
262
|
+
const step = computeRevealStep(config, {
|
|
263
|
+
backlog,
|
|
264
|
+
chunkSizeEma: chunkSizeEmaRef.current,
|
|
265
|
+
arrivalCpsEma: arrivalCpsEmaRef.current,
|
|
266
|
+
emaCps: emaCpsRef.current,
|
|
267
|
+
inputActive,
|
|
268
|
+
settling,
|
|
269
|
+
steadyCps
|
|
270
|
+
}, dtSeconds);
|
|
271
|
+
revealChars = Math.min(step.revealChars, backlog);
|
|
272
|
+
} else revealChars = computeQueueReveal(backlog, frameIntervalMs);
|
|
273
|
+
if (holdBackRef.current?.() === true) {
|
|
274
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const speedOut = speedOutRef.current;
|
|
278
|
+
if (speedOut !== void 0 && frameIntervalMs > 0) {
|
|
279
|
+
const instantCps = revealChars * 1e3 / frameIntervalMs;
|
|
280
|
+
speedOut.current = speedOut.current * .92 + instantCps * .08;
|
|
281
|
+
}
|
|
282
|
+
const nextCount = displayedCount + revealChars;
|
|
283
|
+
const segment = targetCharsRef.current.slice(displayedCount, nextCount).join("");
|
|
284
|
+
if (segment) {
|
|
285
|
+
const nextDisplayed = displayedContentRef.current + segment;
|
|
286
|
+
displayedContentRef.current = nextDisplayed;
|
|
287
|
+
displayedCountRef.current = nextCount;
|
|
288
|
+
setDisplayedContent(nextDisplayed);
|
|
289
|
+
} else {
|
|
290
|
+
displayedContentRef.current = targetContentRef.current;
|
|
291
|
+
displayedCountRef.current = targetCount;
|
|
292
|
+
setDisplayedContent(targetContentRef.current);
|
|
293
|
+
}
|
|
294
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
295
|
+
};
|
|
296
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
297
|
+
}, [
|
|
298
|
+
config,
|
|
299
|
+
scheduleFrameWake,
|
|
300
|
+
stopFrameLoop,
|
|
301
|
+
clearWakeTimer,
|
|
302
|
+
steadyCps
|
|
303
|
+
]);
|
|
304
|
+
(0, react.useEffect)(() => {
|
|
305
|
+
startFrameLoopRef.current = startFrameLoop;
|
|
306
|
+
}, [startFrameLoop]);
|
|
307
|
+
(0, react.useEffect)(() => {
|
|
308
|
+
if (!enabled) {
|
|
309
|
+
syncImmediate(content);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const prevTargetContent = targetContentRef.current;
|
|
313
|
+
if (content === prevTargetContent) return;
|
|
314
|
+
const now = performance.now();
|
|
315
|
+
if (!content.startsWith(prevTargetContent)) {
|
|
316
|
+
syncImmediate(content);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const appendedChars = splitGraphemes(content.slice(prevTargetContent.length));
|
|
320
|
+
const appendedCount = appendedChars.length;
|
|
321
|
+
targetContentRef.current = content;
|
|
322
|
+
targetCharsRef.current.push(...appendedChars);
|
|
323
|
+
targetCountRef.current += appendedCount;
|
|
324
|
+
const hadSample = lastInputTsRef.current > 0;
|
|
325
|
+
const deltaChars = targetCountRef.current - lastInputCountRef.current;
|
|
326
|
+
const deltaMs = Math.max(1, now - lastInputTsRef.current);
|
|
327
|
+
if (hadSample && deltaChars > 0) {
|
|
328
|
+
const instantCps = deltaChars * 1e3 / deltaMs;
|
|
329
|
+
const normalizedInstantCps = clamp$1(instantCps, config.minCps, config.maxFlushCps * 3);
|
|
330
|
+
const chunkEmaAlpha = .45;
|
|
331
|
+
chunkSizeEmaRef.current = chunkSizeEmaRef.current * .55 + appendedCount * chunkEmaAlpha;
|
|
332
|
+
arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * .55 + normalizedInstantCps * chunkEmaAlpha;
|
|
333
|
+
emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + normalizedInstantCps * config.emaAlpha;
|
|
334
|
+
}
|
|
335
|
+
lastInputTsRef.current = now;
|
|
336
|
+
lastInputCountRef.current = targetCountRef.current;
|
|
337
|
+
startFrameLoop();
|
|
338
|
+
}, [
|
|
339
|
+
content,
|
|
340
|
+
enabled,
|
|
341
|
+
config,
|
|
342
|
+
startFrameLoop,
|
|
343
|
+
syncImmediate
|
|
344
|
+
]);
|
|
345
|
+
(0, react.useEffect)(() => {
|
|
346
|
+
return () => {
|
|
347
|
+
stopScheduling();
|
|
348
|
+
};
|
|
349
|
+
}, [stopScheduling]);
|
|
350
|
+
return displayedContent;
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/client/useFpsGuard.ts
|
|
354
|
+
/**
|
|
355
|
+
* Performance guard for the streaming reveal.
|
|
356
|
+
*
|
|
357
|
+
* Feeds an EMA-smoothed frame-rate monitor from a rAF loop while streaming
|
|
358
|
+
* and tracks whether the reply is on-screen. The returned `shouldHoldBack`
|
|
359
|
+
* predicate is true only while the frame rate is below the threshold AND the
|
|
360
|
+
* reply is offscreen — exactly the spec's "skip offscreen DOM updates when
|
|
361
|
+
* FPS < 30" rule. The smoother consumes the predicate as its commit veto.
|
|
362
|
+
*/
|
|
363
|
+
const FPS_THRESHOLD = 30;
|
|
364
|
+
const FPS_ALPHA = .12;
|
|
365
|
+
const RECOVER_FRAMES = 6;
|
|
366
|
+
const MAX_FRAME_MS = 100;
|
|
367
|
+
function useFpsGuard(active) {
|
|
368
|
+
const fpsRef = (0, react.useRef)({
|
|
369
|
+
emaMs: 0,
|
|
370
|
+
lastMs: 0,
|
|
371
|
+
healthyRun: 0,
|
|
372
|
+
degraded: false
|
|
373
|
+
});
|
|
374
|
+
const visibleRef = (0, react.useRef)(true);
|
|
375
|
+
const elementRef = (0, react.useRef)(null);
|
|
376
|
+
(0, react.useEffect)(() => {
|
|
377
|
+
if (!active) return;
|
|
378
|
+
let rafId = 0;
|
|
379
|
+
const frame = (now) => {
|
|
380
|
+
rafId = requestAnimationFrame(frame);
|
|
381
|
+
const fps = fpsRef.current;
|
|
382
|
+
if (fps.lastMs === 0) {
|
|
383
|
+
fps.lastMs = now;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const delta = Math.min(MAX_FRAME_MS, Math.max(1, now - fps.lastMs));
|
|
387
|
+
fps.lastMs = now;
|
|
388
|
+
fps.emaMs = fps.emaMs === 0 ? delta : fps.emaMs + FPS_ALPHA * (delta - fps.emaMs);
|
|
389
|
+
if (1e3 / fps.emaMs < FPS_THRESHOLD) {
|
|
390
|
+
fps.healthyRun = 0;
|
|
391
|
+
fps.degraded = true;
|
|
392
|
+
} else if (fps.degraded) {
|
|
393
|
+
fps.healthyRun += 1;
|
|
394
|
+
if (fps.healthyRun >= RECOVER_FRAMES) fps.degraded = false;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
rafId = requestAnimationFrame(frame);
|
|
398
|
+
return () => {
|
|
399
|
+
cancelAnimationFrame(rafId);
|
|
400
|
+
fpsRef.current = {
|
|
401
|
+
emaMs: 0,
|
|
402
|
+
lastMs: 0,
|
|
403
|
+
healthyRun: 0,
|
|
404
|
+
degraded: false
|
|
405
|
+
};
|
|
406
|
+
};
|
|
407
|
+
}, [active]);
|
|
408
|
+
const ref = (0, react.useCallback)((element) => {
|
|
409
|
+
elementRef.current = element;
|
|
410
|
+
}, []);
|
|
411
|
+
(0, react.useEffect)(() => {
|
|
412
|
+
const element = elementRef.current;
|
|
413
|
+
if (element === null || typeof IntersectionObserver === "undefined") return;
|
|
414
|
+
const observer = new IntersectionObserver((entries) => {
|
|
415
|
+
for (const entry of entries) visibleRef.current = entry.isIntersecting;
|
|
416
|
+
}, { rootMargin: "120px 0px" });
|
|
417
|
+
observer.observe(element);
|
|
418
|
+
return () => observer.disconnect();
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
ref,
|
|
422
|
+
shouldHoldBack: (0, react.useCallback)(() => {
|
|
423
|
+
return active && fpsRef.current.degraded && !visibleRef.current;
|
|
424
|
+
}, [active])
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/client/teleprompterGlide.ts
|
|
429
|
+
/**
|
|
430
|
+
* Conversation-port follow while an assistant reply streams.
|
|
431
|
+
*
|
|
432
|
+
* The demo's damped-spring lerp is driven from a float `animatedH` — the
|
|
433
|
+
* smoothed content height — never from the rounded `scrollTop` the browser
|
|
434
|
+
* reports back. A wrap raises the content height by a line; if the engine
|
|
435
|
+
* were allowed to snap `scrollTop` to the new floor, the previous line would
|
|
436
|
+
* hop up in one frame. This overlay:
|
|
437
|
+
*
|
|
438
|
+
* - exposes its current follow write through `data-follow-owned` for styling,
|
|
439
|
+
* diagnostics, and same-port coordination;
|
|
440
|
+
* - sets `overflow-anchor: none` so CSS scroll-anchoring does not snap;
|
|
441
|
+
* - restores `animatedH` in a ResizeObserver (before paint) so a layout
|
|
442
|
+
* pass cannot flash a snapped frame;
|
|
443
|
+
* - while the port has scroll room, writes the interpolated lag as
|
|
444
|
+
* `translate3d` on `[data-chat-transcript]` (message rows only), so the
|
|
445
|
+
* turn-status chrome stays pinned at the floor;
|
|
446
|
+
* - before the port has scroll room (content shorter than the viewport), the
|
|
447
|
+
* same lag is a content-height lag written as a negative translate on
|
|
448
|
+
* `[data-chat-turn-status]`, so the status label descends smoothly with
|
|
449
|
+
* each wrap instead of hopping a line at a time.
|
|
450
|
+
*
|
|
451
|
+
* A reader gesture that unpins the port first writes the effective visual top
|
|
452
|
+
* (`engine - lag`) into `scrollTop` before clearing the transform, so that
|
|
453
|
+
* frame stays continuous. Lifecycle teardown may retain the last same-geometry
|
|
454
|
+
* visual snapshot for a small-lag close, but never derives a new handback from
|
|
455
|
+
* stale `animatedH`: a changed geometry or near-top snapshot in a long Session
|
|
456
|
+
* settles at the physical floor. Port migration does not rewrite the old port.
|
|
457
|
+
* These guards prevent a newer `scrollHeight` from turning old lag into
|
|
458
|
+
* `scrollTop = 0` and manufacturing reader intent in ChatView's ledger.
|
|
459
|
+
*
|
|
460
|
+
* Unpin is a real gesture (wheel / touch / pointer / key) that leaves the
|
|
461
|
+
* floor; a `scrollTop` delta from our own write must not release the pin.
|
|
462
|
+
*/
|
|
463
|
+
/**
|
|
464
|
+
* Plugin-local marker containing the last programmatic `scrollTop`. DSH rc.6
|
|
465
|
+
* and rc.7 do not consume this attribute; the actual write ownership is
|
|
466
|
+
* enforced by {@link followDrivers} and {@link followOwners} below.
|
|
467
|
+
*/
|
|
468
|
+
const FOLLOW_OWNED_ATTR = "data-follow-owned";
|
|
469
|
+
/** Floor / ceiling of the per-frame lerp fraction before the dt term. */
|
|
470
|
+
const FOLLOW_LERP_MIN = .05;
|
|
471
|
+
const FOLLOW_LERP_MAX = .25;
|
|
472
|
+
const FOLLOW_SPEED_FACTOR_MIN = .7;
|
|
473
|
+
const FOLLOW_SPEED_FACTOR_MAX = 2.2;
|
|
474
|
+
const GESTURE_EVENTS = [
|
|
475
|
+
"wheel",
|
|
476
|
+
"touchmove",
|
|
477
|
+
"pointerdown",
|
|
478
|
+
"keydown"
|
|
479
|
+
];
|
|
480
|
+
function clamp(value, min, max) {
|
|
481
|
+
return Math.min(max, Math.max(min, value));
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* One spring-lerp frame from the silky markdown demo.
|
|
485
|
+
* @param dtMs - Frame delta in ms.
|
|
486
|
+
* @param input - Current lag (from the float top, not the rounded engine top) and reveal-speed EMA.
|
|
487
|
+
* @returns The fractional advance and the lerp fraction.
|
|
488
|
+
*/
|
|
489
|
+
function computeFollowStep(dtMs, input) {
|
|
490
|
+
if (input.lag <= .1 || dtMs <= 0) return {
|
|
491
|
+
advancePx: 0,
|
|
492
|
+
lerpStep: 0
|
|
493
|
+
};
|
|
494
|
+
const speedFactor = clamp((input.speedEma > 0 ? input.speedEma : 35) / 35, FOLLOW_SPEED_FACTOR_MIN, FOLLOW_SPEED_FACTOR_MAX);
|
|
495
|
+
const lerpStep = clamp(input.lag / 160 * speedFactor, FOLLOW_LERP_MIN, FOLLOW_LERP_MAX) * (1 - Math.exp(-dtMs / 18));
|
|
496
|
+
const desired = input.lag * lerpStep;
|
|
497
|
+
const minAdvance = Math.max(0, input.minSpeedPxPerSec ?? 0) * dtMs / 1e3;
|
|
498
|
+
const maxAdvance = Math.max(0, input.maxSpeedPxPerSec ?? Number.POSITIVE_INFINITY) * dtMs / 1e3;
|
|
499
|
+
const upper = Math.min(input.lag, maxAdvance);
|
|
500
|
+
return {
|
|
501
|
+
advancePx: clamp(desired, Math.min(upper, minAdvance), upper),
|
|
502
|
+
lerpStep
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/** The message-rows box the lag transform rides on, when the host has one. */
|
|
506
|
+
function shiftRootOf(port) {
|
|
507
|
+
return port.querySelector("[data-chat-transcript]");
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Row wrappers that carry only messages/tools. Hosts without a transcript
|
|
511
|
+
* box keep the turn-status chrome as a sibling of the rows inside
|
|
512
|
+
* `[data-chat-flow]`, so shifting that whole flow would drag the chrome
|
|
513
|
+
* along; shifting the rows individually leaves every non-message sibling
|
|
514
|
+
* (turn status, steering bubbles) pinned while the text glides.
|
|
515
|
+
*
|
|
516
|
+
* Only the outermost rows are shifted: a tool call nests its subcalls as
|
|
517
|
+
* descendant `[data-chat-anchor-key]` rows, and writing the lag on each of
|
|
518
|
+
* them would double (or further multiply) the shift, tearing the subcalls
|
|
519
|
+
* away from their parent every frame. Descendants ride the parent's
|
|
520
|
+
* transform instead.
|
|
521
|
+
*/
|
|
522
|
+
function shiftRowsOf(port) {
|
|
523
|
+
return [...port.querySelectorAll("[data-chat-anchor-key]")].filter((row) => row.parentElement?.closest("[data-chat-anchor-key]") === null);
|
|
524
|
+
}
|
|
525
|
+
/** Element whose resize signals flow growth for the before-paint restore. */
|
|
526
|
+
function resizeProxyOf(port) {
|
|
527
|
+
return port.querySelector("[data-chat-transcript]") ?? port.querySelector("[data-chat-flow]");
|
|
528
|
+
}
|
|
529
|
+
/** True when the glide expresses its lag as transforms instead of engine top. */
|
|
530
|
+
function hasShiftSurface(port) {
|
|
531
|
+
return shiftRootOf(port) !== null || shiftRowsOf(port).length > 0;
|
|
532
|
+
}
|
|
533
|
+
function setShift(element, px, negative = false) {
|
|
534
|
+
const value = negative ? -px : px;
|
|
535
|
+
if (Math.abs(value) > .01) {
|
|
536
|
+
element.style.transform = `translate3d(0, ${value}px, 0)`;
|
|
537
|
+
element.style.willChange = "transform";
|
|
538
|
+
} else {
|
|
539
|
+
element.style.transform = "";
|
|
540
|
+
element.style.willChange = "";
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The running-turn label. Before the port scrolls (content shorter than the
|
|
545
|
+
* viewport) the content-height lag has no `scrollTop` room to ride, so it is
|
|
546
|
+
* expressed as a negative translate on this sibling: the label descends
|
|
547
|
+
* smoothly with each wrap instead of hopping down a line at a time.
|
|
548
|
+
*/
|
|
549
|
+
function chromeShiftOf(port) {
|
|
550
|
+
return port.querySelector("[data-chat-turn-status]");
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Render the single smoothed extent `animatedH`. Before the port scrolls
|
|
554
|
+
* (content shorter than the viewport) `animatedH` is clamped to the content
|
|
555
|
+
* height and the content-height lag rides a negative translate on the
|
|
556
|
+
* turn-status label; once the port has scroll room the engine is pinned at
|
|
557
|
+
* the floor and the lag rides `[data-chat-transcript]` (or each message row
|
|
558
|
+
* when the host has no transcript box) so the chrome stays put. Without any
|
|
559
|
+
* shift surface the engine itself carries the interpolated top. Returns the
|
|
560
|
+
* effective visual top produced by the same geometry write.
|
|
561
|
+
*/
|
|
562
|
+
function applyVisual(port, animatedH) {
|
|
563
|
+
const contentHeight = Math.max(0, port.scrollHeight);
|
|
564
|
+
const floor = Math.max(0, contentHeight - port.clientHeight);
|
|
565
|
+
const extent = Math.min(contentHeight, Math.max(0, animatedH));
|
|
566
|
+
const lag = contentHeight - extent;
|
|
567
|
+
port.style.overflowAnchor = "none";
|
|
568
|
+
port.style.scrollBehavior = "auto";
|
|
569
|
+
const shiftRoot = shiftRootOf(port);
|
|
570
|
+
if (shiftRoot !== null) {
|
|
571
|
+
if (floor <= 0) {
|
|
572
|
+
port.scrollTop = 0;
|
|
573
|
+
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
574
|
+
const chrome = chromeShiftOf(port);
|
|
575
|
+
if (chrome !== null) setShift(chrome, lag, true);
|
|
576
|
+
setShift(shiftRoot, 0);
|
|
577
|
+
return port.scrollTop;
|
|
578
|
+
}
|
|
579
|
+
port.scrollTop = floor;
|
|
580
|
+
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
581
|
+
setShift(shiftRoot, lag);
|
|
582
|
+
return Math.min(floor, Math.max(0, port.scrollTop - lag));
|
|
583
|
+
}
|
|
584
|
+
const rows = shiftRowsOf(port);
|
|
585
|
+
if (rows.length > 0) {
|
|
586
|
+
if (floor <= 0) {
|
|
587
|
+
port.scrollTop = 0;
|
|
588
|
+
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
589
|
+
const chrome = chromeShiftOf(port);
|
|
590
|
+
if (chrome !== null) setShift(chrome, lag, true);
|
|
591
|
+
for (const row of rows) setShift(row, 0);
|
|
592
|
+
return port.scrollTop;
|
|
593
|
+
}
|
|
594
|
+
port.scrollTop = floor;
|
|
595
|
+
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
596
|
+
for (const row of rows) setShift(row, lag);
|
|
597
|
+
return Math.min(floor, Math.max(0, port.scrollTop - lag));
|
|
598
|
+
}
|
|
599
|
+
port.scrollTop = Math.min(floor, Math.max(0, extent));
|
|
600
|
+
port.setAttribute(FOLLOW_OWNED_ATTR, String(port.scrollTop));
|
|
601
|
+
return port.scrollTop;
|
|
602
|
+
}
|
|
603
|
+
function clearVisual(port) {
|
|
604
|
+
port.removeAttribute(FOLLOW_OWNED_ATTR);
|
|
605
|
+
port.style.overflowAnchor = "";
|
|
606
|
+
port.style.scrollBehavior = "";
|
|
607
|
+
const shiftRoot = shiftRootOf(port);
|
|
608
|
+
if (shiftRoot !== null) {
|
|
609
|
+
shiftRoot.style.transform = "";
|
|
610
|
+
shiftRoot.style.willChange = "";
|
|
611
|
+
} else for (const row of shiftRowsOf(port)) {
|
|
612
|
+
row.style.transform = "";
|
|
613
|
+
row.style.willChange = "";
|
|
614
|
+
}
|
|
615
|
+
const chrome = chromeShiftOf(port);
|
|
616
|
+
if (chrome !== null) {
|
|
617
|
+
chrome.style.transform = "";
|
|
618
|
+
chrome.style.willChange = "";
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** Active visual holders per port, normally zero or the elected driver. */
|
|
622
|
+
const followOwners = /* @__PURE__ */ new WeakMap();
|
|
623
|
+
/** Bound active FollowHosts, including loops that did not win the driver token. */
|
|
624
|
+
const followParticipants = /* @__PURE__ */ new WeakMap();
|
|
625
|
+
/** Safe final snapshot retained while a driver token is changing hands. */
|
|
626
|
+
const pendingHandoffs = /* @__PURE__ */ new WeakMap();
|
|
627
|
+
/**
|
|
628
|
+
* Only one animation loop may write a conversation port at a time. Multiple
|
|
629
|
+
* active FollowHosts can exist during a streamed turn (for example, the
|
|
630
|
+
* assistant body and a tool row); independent loops would fight over
|
|
631
|
+
* scrollTop and transforms and make the transcript jump.
|
|
632
|
+
*/
|
|
633
|
+
const followDrivers = /* @__PURE__ */ new WeakMap();
|
|
634
|
+
function acquireFollow(port) {
|
|
635
|
+
followOwners.set(port, (followOwners.get(port) ?? 0) + 1);
|
|
636
|
+
}
|
|
637
|
+
function releaseFollow(port) {
|
|
638
|
+
const next = (followOwners.get(port) ?? 1) - 1;
|
|
639
|
+
if (next <= 0) {
|
|
640
|
+
followOwners.delete(port);
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
followOwners.set(port, next);
|
|
644
|
+
return next;
|
|
645
|
+
}
|
|
646
|
+
function acquireParticipant(port) {
|
|
647
|
+
followParticipants.set(port, (followParticipants.get(port) ?? 0) + 1);
|
|
648
|
+
}
|
|
649
|
+
function releaseParticipant(port) {
|
|
650
|
+
const next = (followParticipants.get(port) ?? 1) - 1;
|
|
651
|
+
if (next <= 0) {
|
|
652
|
+
followParticipants.delete(port);
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
followParticipants.set(port, next);
|
|
656
|
+
return next;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Own the conversation scrollport's bottom-follow while `active` is true.
|
|
660
|
+
*
|
|
661
|
+
* @param rootRef - An element inside the conversation scrollport.
|
|
662
|
+
* @param active - True while the reply is still revealing.
|
|
663
|
+
* @param speedCpsRef - Live reveal-rate EMA from the smoother.
|
|
664
|
+
*/
|
|
665
|
+
function useConversationFollow(rootRef, active, speedCpsRef, minSpeedPxPerSec, maxSpeedPxPerSec) {
|
|
666
|
+
const activeRef = (0, react.useRef)(active);
|
|
667
|
+
(0, react.useEffect)(() => {
|
|
668
|
+
activeRef.current = active;
|
|
669
|
+
}, [active]);
|
|
670
|
+
(0, react.useEffect)(() => {
|
|
671
|
+
if (!active) return;
|
|
672
|
+
const driverToken = Symbol("conversation-follow");
|
|
673
|
+
let rafId = 0;
|
|
674
|
+
let last = performance.now();
|
|
675
|
+
let following = true;
|
|
676
|
+
let primed = false;
|
|
677
|
+
let animatedH = 0;
|
|
678
|
+
let interacting = false;
|
|
679
|
+
let interactTimer = null;
|
|
680
|
+
let port = null;
|
|
681
|
+
let resize = null;
|
|
682
|
+
let holding = null;
|
|
683
|
+
let awayPx = 0;
|
|
684
|
+
let readerIntent = false;
|
|
685
|
+
let lastVisual = null;
|
|
686
|
+
const hold = (next) => {
|
|
687
|
+
if (holding === next) return;
|
|
688
|
+
if (holding !== null) {
|
|
689
|
+
const previous = holding;
|
|
690
|
+
if (releaseFollow(previous) === 0) clearVisual(previous);
|
|
691
|
+
}
|
|
692
|
+
acquireFollow(next);
|
|
693
|
+
holding = next;
|
|
694
|
+
};
|
|
695
|
+
const drop = (next) => {
|
|
696
|
+
if (holding !== next) return;
|
|
697
|
+
const remaining = releaseFollow(next);
|
|
698
|
+
holding = null;
|
|
699
|
+
if (remaining === 0) clearVisual(next);
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Give the reader the visual position the glide was showing before the
|
|
703
|
+
* transforms go away. While transforms carry the lag, the engine sits at
|
|
704
|
+
* the floor and the effective visual top is `engine - lag`; writing that
|
|
705
|
+
* keeps the handover frame-continuous, and the write shows up in
|
|
706
|
+
* ChatView's ledger as reader movement, disarming its snap-follow.
|
|
707
|
+
* Without a shift surface the engine already holds the interpolated top,
|
|
708
|
+
* so there is nothing to compensate.
|
|
709
|
+
*/
|
|
710
|
+
const handBackVisual = (next) => {
|
|
711
|
+
if (!hasShiftSurface(next)) return;
|
|
712
|
+
const floor = Math.max(0, next.scrollHeight - next.clientHeight);
|
|
713
|
+
if (floor <= 0) return;
|
|
714
|
+
const lag = Math.max(0, next.scrollHeight - animatedH);
|
|
715
|
+
next.scrollTop = Math.min(floor, Math.max(0, next.scrollTop - lag));
|
|
716
|
+
};
|
|
717
|
+
const applyCurrentVisual = (next) => {
|
|
718
|
+
const top = applyVisual(next, animatedH);
|
|
719
|
+
lastVisual = {
|
|
720
|
+
scrollHeight: next.scrollHeight,
|
|
721
|
+
clientHeight: next.clientHeight,
|
|
722
|
+
top
|
|
723
|
+
};
|
|
724
|
+
};
|
|
725
|
+
const markGesture = (event) => {
|
|
726
|
+
interacting = true;
|
|
727
|
+
if (event instanceof WheelEvent && event.deltaY < 0) {
|
|
728
|
+
readerIntent = true;
|
|
729
|
+
awayPx += -event.deltaY;
|
|
730
|
+
} else if (event.type === "touchmove" || event.type === "pointerdown" || event.type === "keydown") {
|
|
731
|
+
readerIntent = true;
|
|
732
|
+
awayPx += 8;
|
|
733
|
+
}
|
|
734
|
+
if (readerIntent && port !== null) pendingHandoffs.delete(port);
|
|
735
|
+
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
736
|
+
interactTimer = setTimeout(() => {
|
|
737
|
+
interacting = false;
|
|
738
|
+
interactTimer = null;
|
|
739
|
+
awayPx = 0;
|
|
740
|
+
}, 800);
|
|
741
|
+
};
|
|
742
|
+
const restoreBeforePaint = () => {
|
|
743
|
+
if (!primed || !following || port === null || holding !== port || followDrivers.get(port) !== driverToken) return;
|
|
744
|
+
applyCurrentVisual(port);
|
|
745
|
+
};
|
|
746
|
+
const bindPort = (next) => {
|
|
747
|
+
if (port === next) return;
|
|
748
|
+
if (port !== null) {
|
|
749
|
+
const previous = port;
|
|
750
|
+
const drovePrevious = followDrivers.get(previous) === driverToken;
|
|
751
|
+
for (const name of GESTURE_EVENTS) previous.removeEventListener(name, markGesture);
|
|
752
|
+
resize?.disconnect();
|
|
753
|
+
resize = null;
|
|
754
|
+
if (holding === previous) {
|
|
755
|
+
const remaining = releaseFollow(previous);
|
|
756
|
+
holding = null;
|
|
757
|
+
if (remaining === 0) clearVisual(previous);
|
|
758
|
+
}
|
|
759
|
+
releaseParticipant(previous);
|
|
760
|
+
if (drovePrevious) followDrivers.delete(previous);
|
|
761
|
+
primed = false;
|
|
762
|
+
following = true;
|
|
763
|
+
animatedH = 0;
|
|
764
|
+
awayPx = 0;
|
|
765
|
+
readerIntent = false;
|
|
766
|
+
lastVisual = null;
|
|
767
|
+
}
|
|
768
|
+
port = next;
|
|
769
|
+
acquireParticipant(next);
|
|
770
|
+
for (const name of GESTURE_EVENTS) port.addEventListener(name, markGesture, { passive: true });
|
|
771
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
772
|
+
resize = new ResizeObserver(restoreBeforePaint);
|
|
773
|
+
resize.observe(port);
|
|
774
|
+
const proxy = resizeProxyOf(port);
|
|
775
|
+
if (proxy !== null) resize.observe(proxy);
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
const frame = (now) => {
|
|
779
|
+
rafId = requestAnimationFrame(frame);
|
|
780
|
+
const dt = Math.min(50, now - last);
|
|
781
|
+
last = now;
|
|
782
|
+
const root = rootRef.current;
|
|
783
|
+
if (root === null) return;
|
|
784
|
+
const nextPort = root.closest("[data-conversation-scroll]");
|
|
785
|
+
if (nextPort === null) return;
|
|
786
|
+
bindPort(nextPort);
|
|
787
|
+
const owner = followDrivers.get(nextPort);
|
|
788
|
+
const inherited = owner === void 0 ? pendingHandoffs.get(nextPort) ?? null : null;
|
|
789
|
+
if (owner === void 0) followDrivers.set(nextPort, driverToken);
|
|
790
|
+
if (followDrivers.get(nextPort) !== driverToken) return;
|
|
791
|
+
if (!primed && inherited !== null && !readerIntent) {
|
|
792
|
+
if (inherited.scrollHeight === nextPort.scrollHeight && inherited.clientHeight === nextPort.clientHeight) {
|
|
793
|
+
animatedH = hasShiftSurface(nextPort) ? Math.min(nextPort.scrollHeight, nextPort.clientHeight + inherited.top) : inherited.top;
|
|
794
|
+
following = true;
|
|
795
|
+
hold(nextPort);
|
|
796
|
+
applyCurrentVisual(nextPort);
|
|
797
|
+
primed = true;
|
|
798
|
+
pendingHandoffs.delete(nextPort);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
pendingHandoffs.delete(nextPort);
|
|
803
|
+
if (owner === void 0 && (followOwners.get(nextPort) ?? 0) === 0) clearVisual(nextPort);
|
|
804
|
+
const reportedLag = Math.max(0, nextPort.scrollHeight - nextPort.clientHeight) - nextPort.scrollTop;
|
|
805
|
+
const extent = Math.min(nextPort.scrollHeight, Math.max(0, nextPort.scrollHeight - reportedLag));
|
|
806
|
+
if (!primed) {
|
|
807
|
+
animatedH = extent;
|
|
808
|
+
following = reportedLag <= 25;
|
|
809
|
+
if (following) {
|
|
810
|
+
hold(nextPort);
|
|
811
|
+
applyCurrentVisual(nextPort);
|
|
812
|
+
}
|
|
813
|
+
primed = true;
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (!following && !interacting && reportedLag <= 25) {
|
|
817
|
+
following = true;
|
|
818
|
+
readerIntent = false;
|
|
819
|
+
animatedH = extent;
|
|
820
|
+
hold(nextPort);
|
|
821
|
+
} else if (following && interacting && awayPx >= 8) {
|
|
822
|
+
following = false;
|
|
823
|
+
awayPx = 0;
|
|
824
|
+
handBackVisual(nextPort);
|
|
825
|
+
animatedH = nextPort.scrollHeight;
|
|
826
|
+
drop(nextPort);
|
|
827
|
+
}
|
|
828
|
+
if (!activeRef.current || !following) return;
|
|
829
|
+
hold(nextPort);
|
|
830
|
+
const lag = nextPort.scrollHeight - animatedH;
|
|
831
|
+
const step = computeFollowStep(dt, {
|
|
832
|
+
lag,
|
|
833
|
+
speedEma: speedCpsRef.current,
|
|
834
|
+
minSpeedPxPerSec,
|
|
835
|
+
maxSpeedPxPerSec
|
|
836
|
+
});
|
|
837
|
+
if (lag <= .1) animatedH = nextPort.scrollHeight;
|
|
838
|
+
else animatedH = Math.min(nextPort.scrollHeight, animatedH + step.advancePx);
|
|
839
|
+
applyCurrentVisual(nextPort);
|
|
840
|
+
};
|
|
841
|
+
rafId = requestAnimationFrame(frame);
|
|
842
|
+
return () => {
|
|
843
|
+
cancelAnimationFrame(rafId);
|
|
844
|
+
if (interactTimer !== null) clearTimeout(interactTimer);
|
|
845
|
+
resize?.disconnect();
|
|
846
|
+
const rootHost = rootRef.current?.closest("[data-conversation-scroll]") ?? null;
|
|
847
|
+
const host = port ?? rootHost;
|
|
848
|
+
const droveHost = host !== null && followDrivers.get(host) === driverToken;
|
|
849
|
+
let remainingParticipants = host === null ? 0 : followParticipants.get(host) ?? 0;
|
|
850
|
+
if (port !== null) {
|
|
851
|
+
for (const name of GESTURE_EVENTS) port.removeEventListener(name, markGesture);
|
|
852
|
+
remainingParticipants = releaseParticipant(port);
|
|
853
|
+
}
|
|
854
|
+
const ownSnapshot = host !== null && rootHost === host && droveHost && primed && following && holding === host && !readerIntent ? lastVisual : null;
|
|
855
|
+
if (host !== null) {
|
|
856
|
+
if (readerIntent) pendingHandoffs.delete(host);
|
|
857
|
+
else if (ownSnapshot !== null && remainingParticipants > 0) pendingHandoffs.set(host, ownSnapshot);
|
|
858
|
+
if (remainingParticipants === 0) {
|
|
859
|
+
const snapshot = ownSnapshot ?? pendingHandoffs.get(host) ?? null;
|
|
860
|
+
pendingHandoffs.delete(host);
|
|
861
|
+
if (rootHost === host && !readerIntent && snapshot !== null) {
|
|
862
|
+
const floor = Math.max(0, host.scrollHeight - host.clientHeight);
|
|
863
|
+
const snapshotValid = snapshot.scrollHeight === host.scrollHeight && snapshot.clientHeight === host.clientHeight;
|
|
864
|
+
const snapshotTop = snapshotValid ? Math.min(floor, Math.max(0, snapshot.top)) : floor;
|
|
865
|
+
host.scrollTop = snapshotValid && !(floor > 25 && snapshotTop <= 25) ? snapshotTop : floor;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (droveHost) followDrivers.delete(host);
|
|
870
|
+
if (holding !== null) {
|
|
871
|
+
const held = holding;
|
|
872
|
+
const remaining = releaseFollow(held);
|
|
873
|
+
holding = null;
|
|
874
|
+
const preserveForSuccessor = held === host && remainingParticipants > 0 && pendingHandoffs.has(held);
|
|
875
|
+
if (remaining === 0 && !preserveForSuccessor) clearVisual(held);
|
|
876
|
+
}
|
|
877
|
+
if (host !== null && remainingParticipants === 0 && (followOwners.get(host) ?? 0) === 0) clearVisual(host);
|
|
878
|
+
};
|
|
879
|
+
}, [
|
|
880
|
+
active,
|
|
881
|
+
rootRef,
|
|
882
|
+
speedCpsRef,
|
|
883
|
+
minSpeedPxPerSec,
|
|
884
|
+
maxSpeedPxPerSec
|
|
885
|
+
]);
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region \0dsh-css:/Users/baojie/Documents/Projects/github/dsh-codex-provider-plugin/packages/conversation-ui/src/client/TypewriterAssistantNodeView.module.css.mjs
|
|
889
|
+
const css$2 = ".lYIxha_root{min-width:0;color:var(--dsw-alias-label-primary);flex-direction:column;font-size:16px;line-height:28px;animation:.14s ease-out both lYIxha_dsh-codex-stream-enter;display:flex}.lYIxha_body{flex-direction:column;gap:16px;min-width:0;display:flex}.lYIxha_markdownFlow{min-width:0}.lYIxha_root .lYIxha_markdownFlow pre{overflow-wrap:normal;white-space:pre;word-break:normal;tab-size:2;max-height:min(60vh,480px);overflow:auto}.lYIxha_root .lYIxha_markdownFlow pre code{white-space:inherit;word-break:inherit}.lYIxha_markdownFlow>div>:where(ul,ol)+p{margin-block-start:24px}.lYIxha_root[data-stream-phase=streaming],.lYIxha_root[data-stream-phase=process]{color:var(--dsw-alias-label-primary)}.lYIxha_root[data-stream-phase=streaming] .lYIxha_markdownFlow>div,.lYIxha_root[data-stream-phase=process] .lYIxha_markdownFlow>div{font-family:inherit;font-size:16px;font-weight:400;line-height:28px}.lYIxha_root[data-stream-phase=streaming] .lYIxha_markdownFlow strong,.lYIxha_root[data-stream-phase=process] .lYIxha_markdownFlow strong{font-weight:400!important}.lYIxha_root[data-stream-phase=final]{margin-block-start:2px}.lYIxha_turnFoldRow{color:var(--dsw-alias-label-secondary);flex-direction:column;align-items:stretch;gap:6px;margin-block:0 14px;font-size:14px;line-height:24px;display:flex}.lYIxha_turnPrelude{margin-block-start:16px}.lYIxha_turnWaiting{background:linear-gradient(105deg, var(--dsw-alias-label-tertiary) 15%, var(--dsw-alias-label-primary) 42%, var(--dsw-alias-label-tertiary) 68%);color:#0000;background-size:220% 100%;background-clip:text;width:fit-content;margin-block-start:0;font-size:14px;line-height:24px;animation:1.8s linear infinite lYIxha_dsh-turn-waiting-shimmer}@keyframes lYIxha_dsh-turn-waiting-shimmer{0%{background-position:130% 0}to{background-position:-90% 0}}[data-chat-flow]>[role=status]{display:none!important}.lYIxha_turnFoldRow:after{background:var(--dsw-alias-border-l2);content:\"\";flex:none;width:100%;height:1px}.lYIxha_turnFoldButton,.lYIxha_turnFoldLabel{flex:none;align-self:flex-start;align-items:center;gap:6px;display:inline-flex}.lYIxha_turnFoldButton{color:inherit;cursor:pointer;font:inherit;background:0 0;border:0;border-radius:4px;padding:2px 0}.lYIxha_turnFoldButton:hover{color:var(--dsw-alias-label-primary)}.lYIxha_turnFoldButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:3px}.lYIxha_turnFoldButton>svg{flex:none}[data-turn-process][hidden]{display:none!important}[data-chat-anchor-key]:has([data-turn-process][hidden]):not(:has([data-turn-fold-row])):not(:has([data-stream-phase=final])):not(:has([data-stream-tool-group-frame])){display:none!important}[data-chat-anchor-key]:has([data-tool-group-proxy]){display:none!important}[data-chat-anchor-key]:has([data-stream-tool-group-anchor][hidden]){display:none!important}.lYIxha_root[data-stream-phase=interrupted]{color:var(--dsw-alias-label-secondary)}.lYIxha_eventRow{min-width:0;color:var(--dsw-alias-label-secondary);font-size:14px;line-height:24px;animation:.14s ease-out both lYIxha_dsh-codex-stream-enter}.lYIxha_eventRow[data-stream-node=tool-call],.lYIxha_eventRow[data-stream-node=workflow-run],.lYIxha_eventRow[data-stream-node=model-retry],.lYIxha_eventRow[data-stream-node=compaction],.lYIxha_eventRow[data-stream-node=manual-compaction]{margin-block:2px}.lYIxha_eventRow[data-stream-state=running]{color:var(--dsw-alias-label-tertiary)}.lYIxha_eventRow[data-stream-node=compaction] [data-compaction-disclosure],.lYIxha_eventRow[data-stream-node=manual-compaction] [data-compaction-disclosure]{display:none!important}.lYIxha_eventRow[data-stream-node=compaction] button:has([data-compaction-icon]),.lYIxha_eventRow[data-stream-node=manual-compaction] button:has([data-compaction-icon]){cursor:default}.lYIxha_eventRow[data-stream-node=compaction] button:has([data-compaction-icon]):hover,.lYIxha_eventRow[data-stream-node=compaction] button:has([data-compaction-icon]):focus-visible,.lYIxha_eventRow[data-stream-node=manual-compaction] button:has([data-compaction-icon]):hover,.lYIxha_eventRow[data-stream-node=manual-compaction] button:has([data-compaction-icon]):focus-visible{background:0 0!important}.lYIxha_eventRow[data-stream-node=compaction] [data-compaction-icon],.lYIxha_eventRow[data-stream-node=manual-compaction] [data-compaction-icon]{color:var(--dsw-alias-label-secondary)!important;opacity:1!important}.lYIxha_eventRow[data-stream-node=compaction] button:has([data-compaction-icon]),.lYIxha_eventRow[data-stream-node=manual-compaction] button:has([data-compaction-icon]){color:var(--dsw-alias-label-secondary)!important}.lYIxha_eventRow[data-stream-node=compaction] button:has([data-compaction-icon])>span,.lYIxha_eventRow[data-stream-node=manual-compaction] button:has([data-compaction-icon])>span{color:var(--dsw-alias-label-secondary)!important}button:has([data-compaction-icon]),button:has([data-compaction-icon])>span{color:var(--dsw-alias-label-secondary)!important;opacity:1!important}.lYIxha_toolGroupAnchor{min-width:0}.lYIxha_toolGroupFrame{background:0 0;min-width:0;margin-block:2px 8px;overflow:hidden}.lYIxha_toolGroupHeader{width:100%;min-height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit;text-align:start;background:0 0;border:0;align-items:center;gap:6px;padding:2px 8px 2px 22px;font-size:14px;line-height:24px;display:flex;position:relative}.lYIxha_toolGroupHeader:hover{color:var(--dsw-alias-label-primary);background:0 0}.lYIxha_toolGroupHeader:focus-visible{background:0 0;outline:none}.lYIxha_toolGroupHeaderIcon{width:14px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex;position:absolute;inset-block-start:50%;inset-inline-start:0;transform:translateY(-50%)}.lYIxha_toolGroupHeaderGlyph{justify-content:center;align-items:center;width:14px;height:14px;display:inline-flex}.lYIxha_toolGroupHeaderLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.lYIxha_toolGroupHeaderChevron{color:var(--dsw-alias-label-tertiary);flex:none;margin-inline-start:0;display:inline-flex}.lYIxha_editStats{color:var(--dsw-alias-label-tertiary);white-space:nowrap;flex:none;margin-inline-start:4px;font-size:14px;line-height:24px}.lYIxha_toolGroupFrame>.lYIxha_eventRow[data-stream-tool-group-member],.lYIxha_toolGroupFrame>[data-tool-group-member]>.lYIxha_eventRow[data-stream-tool-group-member]{background:0 0;border:0;border-radius:0;margin-block:0;padding-inline:8px 6px}.lYIxha_toolGroupFrame .lYIxha_toolSemanticIcon,.lYIxha_toolGroupFrame .lYIxha_streamSemanticIcon{inset-block-start:4px;transform:none}.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>.lYIxha_eventRow[data-stream-tool-group-member]>[data-chat-call-id],.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>.lYIxha_eventRow[data-stream-tool-group-member]>*>[data-sample=bash],.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>[data-tool-group-member]>.lYIxha_eventRow[data-stream-tool-group-member]>[data-chat-call-id],.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>[data-tool-group-member]>.lYIxha_eventRow[data-stream-tool-group-member]>*>[data-sample=bash]{display:none!important}.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>.lYIxha_eventRow[data-stream-tool-group-member]>.lYIxha_toolSemanticIcon,.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>[data-tool-group-member]>.lYIxha_eventRow[data-stream-tool-group-member]>.lYIxha_toolSemanticIcon{display:none}.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>.lYIxha_eventRow[data-stream-tool-group-member]>:not(.lYIxha_toolSemanticIcon):not(.lYIxha_streamSemanticIcon),.lYIxha_toolGroupFrame[data-stream-tool-group-collapsed]>[data-tool-group-member]>.lYIxha_eventRow[data-stream-tool-group-member]>:not(.lYIxha_toolSemanticIcon):not(.lYIxha_streamSemanticIcon){display:none!important}.lYIxha_eventRow [data-disclosure-row]{min-width:0}.lYIxha_eventRow[data-stream-tool-row]{position:relative}.lYIxha_eventRow[data-stream-tool-row][data-stream-progress=active]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row],.lYIxha_eventRow[data-stream-tool-row][data-stream-progress=active]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash]{position:relative;overflow:hidden}.lYIxha_eventRow[data-stream-tool-row][data-stream-progress=active]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row]:after,.lYIxha_eventRow[data-stream-tool-row][data-stream-progress=active]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash]:after{inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);content:\"\";pointer-events:none;width:300px;animation:2.6s ease-out infinite lYIxha_dsh-conversation-ui-think-sweep;position:absolute;left:-300px}.lYIxha_toolSemanticIcon,.lYIxha_streamSemanticIcon{z-index:1;width:14px;height:14px;color:var(--dsw-alias-label-tertiary);pointer-events:none;justify-content:center;align-items:center;display:inline-flex;position:absolute;inset-block-start:4px;inset-inline-start:0}.lYIxha_eventRow[data-stream-semantic-icon-owned]{position:relative}.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]>:first-child{display:contents}.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]>:first-child>:first-child:not(:last-child){opacity:1}.lYIxha_eventRow[data-stream-semantic-icon-owned]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]{padding-inline-start:22px}.lYIxha_eventRow[data-stream-semantic-icon-owned]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]>:first-child>:first-child:not(:last-child){display:none}.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]>:first-child>svg{opacity:0;color:var(--dsw-alias-label-secondary);flex:none;order:4;margin:0 0 0 6px;transition:opacity .1s,transform .12s;position:static}.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]:hover>:first-child>svg,.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded]:focus-visible>:first-child>svg{opacity:1}.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded=false]>:first-child>svg{transform:rotate(-90deg)}.lYIxha_eventRow[data-stream-native-disclosure]>:not(.lYIxha_streamSemanticIcon)>[data-disclosure-row][aria-expanded=true]>:first-child>svg{transform:rotate(0)}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row]{padding-inline-start:22px}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row]:not([aria-expanded])>:first-child{display:none}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:first-child{display:contents}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:first-child>:first-child:not(:last-child){display:none}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:nth-child(n+3){flex-grow:0}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:first-child>svg{opacity:0;color:var(--dsw-alias-label-secondary);flex:none;order:4;margin:0 0 0 6px;transition:opacity .1s,transform .12s;position:static}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]:hover>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]:focus-visible>:first-child>svg{opacity:1}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded=false]>:first-child>svg{transform:rotate(-90deg)}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded=true]>:first-child>svg{transform:rotate(0)}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash]{padding-inline-start:22px}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash]:not([aria-expanded])>:first-child{display:none}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:first-child{display:contents}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:first-child>:first-child:not(:last-child){display:none}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:nth-child(n+3){flex-grow:0}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:first-child>svg{opacity:0;color:var(--dsw-alias-label-secondary);flex:none;order:4;margin:0 0 0 6px;transition:opacity .1s,transform .12s;position:static}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]:hover>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]:focus-visible>:first-child>svg{opacity:1}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded=false]>:first-child>svg{transform:rotate(-90deg)}.lYIxha_eventRow[data-stream-tool-row]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded=true]>:first-child>svg{transform:rotate(0)}.lYIxha_nestedToolSemanticIcon{z-index:1;width:14px;height:14px;color:var(--dsw-alias-label-tertiary);pointer-events:none;justify-content:center;align-items:center;display:inline-flex;position:absolute;inset-block-start:5px;inset-inline-start:1px}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]:has(>.lYIxha_nestedToolSemanticIcon),.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]:has(>.lYIxha_nestedToolSemanticIcon){padding-inline-start:22px}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]:has(>.lYIxha_nestedToolSemanticIcon)>:first-child>:first-child:not(:last-child),.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]:has(>.lYIxha_nestedToolSemanticIcon)>:first-child>:first-child:not(:last-child){display:none}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:first-child,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:first-child{display:contents}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:nth-child(n+3),.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:nth-child(n+3){flex-grow:0}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]>:first-child>svg{opacity:0;color:var(--dsw-alias-label-secondary);flex:none;order:4;margin:0 0 0 6px;transition:opacity .1s,transform .12s;position:static}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]:hover>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded]:focus-visible>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]:hover>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded]:focus-visible>:first-child>svg{opacity:1}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded=false]>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded=false]>:first-child>svg{transform:rotate(-90deg)}.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row][aria-expanded=true]>:first-child>svg,.lYIxha_eventRow[data-stream-tool-row] [data-subcalls]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash][aria-expanded=true]>:first-child>svg{transform:rotate(0)}@keyframes lYIxha_dsh-codex-stream-enter{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.lYIxha_reasoningText{min-width:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:24px}.lYIxha_reasoningText>div{font-family:inherit;font-size:14px;font-weight:400;line-height:24px}.lYIxha_reasoningText p,.lYIxha_reasoningText li,.lYIxha_reasoningText blockquote,.lYIxha_reasoningText strong,.lYIxha_reasoningText h1,.lYIxha_reasoningText h2,.lYIxha_reasoningText h3,.lYIxha_reasoningText h4,.lYIxha_reasoningText h5,.lYIxha_reasoningText h6{font-size:14px;font-weight:400;line-height:24px}.lYIxha_reasoningText strong{font-weight:400!important}.lYIxha_reasoningText h1,.lYIxha_reasoningText h2,.lYIxha_reasoningText h3,.lYIxha_reasoningText h4,.lYIxha_reasoningText h5,.lYIxha_reasoningText h6{margin-block:8px}.lYIxha_reasoningText[data-stream-state=running]{color:var(--dsw-alias-label-secondary)}.lYIxha_think{flex-direction:column;display:flex}.lYIxha_thinkRow{position:relative;overflow:hidden}.lYIxha_think[data-state=running] .lYIxha_thinkRow:after{content:\"\";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite lYIxha_dsh-conversation-ui-think-sweep;position:absolute;left:0}@keyframes lYIxha_dsh-conversation-ui-think-sweep{0%{left:-300px}90%,to{left:100%}}.lYIxha_thinkLeading{flex-shrink:0}.lYIxha_thinkChevron{color:var(--dsw-alias-label-secondary)}.lYIxha_thinkTitle{font-weight:400}.lYIxha_thinkSeparator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.lYIxha_thinkSummary{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:0 auto;font-size:14px;line-height:24px;overflow:hidden}.lYIxha_thinkSummary[data-follow-end]{text-overflow:clip}.lYIxha_thinkBody{color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;padding:4px 0 4px 22px;font-size:14px;line-height:24px}.lYIxha_disclosureRoot{flex-direction:column;width:100%;min-width:0;display:flex}.lYIxha_disclosureRow{cursor:pointer;align-items:center;min-width:0;height:24px;display:flex;position:relative;overflow:hidden}.lYIxha_disclosureLeading{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex;position:relative}.lYIxha_disclosureTitle{color:var(--dsw-alias-label-secondary);flex:none;font-size:14px;line-height:24px}.lYIxha_disclosureTrailing{opacity:0;width:14px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-left:6px;transition:opacity .1s;display:inline-flex}.lYIxha_disclosureRow:hover .lYIxha_disclosureTrailing,.lYIxha_disclosureRow:focus-visible .lYIxha_disclosureTrailing{opacity:1}.lYIxha_disclosureContent{visibility:visible;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s;grid-template-rows:1fr;display:grid}.lYIxha_disclosureContent[data-collapsed]{visibility:hidden;transition:grid-template-rows var(--ds-transition-duration,.2s) var(--ds-ease-in-out,cubic-bezier(.4, 0, .2, 1)), visibility 0s var(--ds-transition-duration,.2s);grid-template-rows:0fr}.lYIxha_disclosureContent>*{min-height:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.lYIxha_root,.lYIxha_eventRow,.lYIxha_think[data-state=running] .lYIxha_thinkRow:after{animation:none}.lYIxha_eventRow[data-stream-tool-row][data-stream-progress=active]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>[data-tool]>*>[data-disclosure-row]:after,.lYIxha_eventRow[data-stream-tool-row][data-stream-progress=active]>[data-chat-call-id]>[data-slot=\"tool.call.toolview\"]>*>[data-sample=bash]:after{animation:none}.lYIxha_turnWaiting{color:var(--dsw-alias-label-secondary);background:0 0;animation:none}.lYIxha_disclosureContent,.lYIxha_disclosureContent[data-collapsed]{transition:none}}.lYIxha_stopped{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);border-radius:6px;align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}.lYIxha_visuallyHidden{clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.lYIxha_follow{min-width:0}@supports (text-box-trim:trim-both){.lYIxha_root :is(p,h1,h2,h3,h4,h5,h6,blockquote):not(:has(code)){text-box-trim:trim-both;text-box-edge:text}}";
|
|
890
|
+
const tagId$2 = "@jcy2387/dsh-conversation-ui/TypewriterAssistantNodeView.module.css";
|
|
891
|
+
if (typeof document !== "undefined") {
|
|
892
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]");
|
|
893
|
+
if (tag === null) {
|
|
894
|
+
tag = document.createElement("style");
|
|
895
|
+
tag.dataset.plugin = "@jcy2387/dsh-conversation-ui";
|
|
896
|
+
tag.dataset.pluginCss = tagId$2;
|
|
897
|
+
document.head.appendChild(tag);
|
|
898
|
+
}
|
|
899
|
+
tag.textContent = css$2;
|
|
900
|
+
}
|
|
901
|
+
var TypewriterAssistantNodeView_module_css_default = {
|
|
902
|
+
"thinkTitle": "lYIxha_thinkTitle",
|
|
903
|
+
"turnPrelude": "lYIxha_turnPrelude",
|
|
904
|
+
"markdownFlow": "lYIxha_markdownFlow",
|
|
905
|
+
"toolGroupHeaderLabel": "lYIxha_toolGroupHeaderLabel",
|
|
906
|
+
"thinkChevron": "lYIxha_thinkChevron",
|
|
907
|
+
"root": "lYIxha_root",
|
|
908
|
+
"turnFoldLabel": "lYIxha_turnFoldLabel",
|
|
909
|
+
"thinkRow": "lYIxha_thinkRow",
|
|
910
|
+
"thinkSeparator": "lYIxha_thinkSeparator",
|
|
911
|
+
"disclosureRoot": "lYIxha_disclosureRoot",
|
|
912
|
+
"disclosureLeading": "lYIxha_disclosureLeading",
|
|
913
|
+
"visuallyHidden": "lYIxha_visuallyHidden",
|
|
914
|
+
"disclosureRow": "lYIxha_disclosureRow",
|
|
915
|
+
"toolGroupHeader": "lYIxha_toolGroupHeader",
|
|
916
|
+
"stopped": "lYIxha_stopped",
|
|
917
|
+
"thinkLeading": "lYIxha_thinkLeading",
|
|
918
|
+
"body": "lYIxha_body",
|
|
919
|
+
"dsh-turn-waiting-shimmer": "lYIxha_dsh-turn-waiting-shimmer",
|
|
920
|
+
"toolGroupFrame": "lYIxha_toolGroupFrame",
|
|
921
|
+
"toolGroupHeaderGlyph": "lYIxha_toolGroupHeaderGlyph",
|
|
922
|
+
"toolGroupHeaderChevron": "lYIxha_toolGroupHeaderChevron",
|
|
923
|
+
"dsh-codex-stream-enter": "lYIxha_dsh-codex-stream-enter",
|
|
924
|
+
"disclosureContent": "lYIxha_disclosureContent",
|
|
925
|
+
"streamSemanticIcon": "lYIxha_streamSemanticIcon",
|
|
926
|
+
"toolSemanticIcon": "lYIxha_toolSemanticIcon",
|
|
927
|
+
"disclosureTitle": "lYIxha_disclosureTitle",
|
|
928
|
+
"follow": "lYIxha_follow",
|
|
929
|
+
"disclosureTrailing": "lYIxha_disclosureTrailing",
|
|
930
|
+
"reasoningText": "lYIxha_reasoningText",
|
|
931
|
+
"think": "lYIxha_think",
|
|
932
|
+
"turnWaiting": "lYIxha_turnWaiting",
|
|
933
|
+
"turnFoldRow": "lYIxha_turnFoldRow",
|
|
934
|
+
"thinkSummary": "lYIxha_thinkSummary",
|
|
935
|
+
"eventRow": "lYIxha_eventRow",
|
|
936
|
+
"thinkBody": "lYIxha_thinkBody",
|
|
937
|
+
"editStats": "lYIxha_editStats",
|
|
938
|
+
"toolGroupAnchor": "lYIxha_toolGroupAnchor",
|
|
939
|
+
"turnFoldButton": "lYIxha_turnFoldButton",
|
|
940
|
+
"dsh-conversation-ui-think-sweep": "lYIxha_dsh-conversation-ui-think-sweep",
|
|
941
|
+
"nestedToolSemanticIcon": "lYIxha_nestedToolSemanticIcon",
|
|
942
|
+
"toolGroupHeaderIcon": "lYIxha_toolGroupHeaderIcon"
|
|
943
|
+
};
|
|
944
|
+
//#endregion
|
|
945
|
+
//#region src/client/FollowHost.tsx
|
|
946
|
+
/**
|
|
947
|
+
* Document-flow host that owns conversation-port follow while `active`.
|
|
948
|
+
* Shared by assistant blocks and every other growing Chat row.
|
|
949
|
+
*/
|
|
950
|
+
function FollowHost({ active, speedCpsRef, minSpeedPxPerSec, maxSpeedPxPerSec, children }) {
|
|
951
|
+
const rootRef = (0, react.useRef)(null);
|
|
952
|
+
useConversationFollow(rootRef, active, speedCpsRef, minSpeedPxPerSec, maxSpeedPxPerSec);
|
|
953
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
954
|
+
ref: rootRef,
|
|
955
|
+
className: TypewriterAssistantNodeView_module_css_default.follow,
|
|
956
|
+
children
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/config.ts
|
|
961
|
+
/** Defaults shared by the Host schema and the client-side fallback. */
|
|
962
|
+
const DEFAULT_CONVERSATION_CONFIG = {
|
|
963
|
+
mode: "teleprompter",
|
|
964
|
+
preset: "balanced",
|
|
965
|
+
revealCharsPerSec: 80,
|
|
966
|
+
scrollSpeedPxPerSec: 48,
|
|
967
|
+
maxScrollSpeedPxPerSec: 1e3
|
|
968
|
+
};
|
|
969
|
+
/**
|
|
970
|
+
* Window global the Host writes into the served index HTML. The browser boot
|
|
971
|
+
* graph carries no per-entry config, so this inline script is the only
|
|
972
|
+
* Host-to-client configuration channel for a composed web plugin.
|
|
973
|
+
*/
|
|
974
|
+
const CONVERSATION_BOOT_GLOBAL = "__DSH_CONVERSATION_UI_CONFIG__";
|
|
975
|
+
//#endregion
|
|
976
|
+
//#region src/client/turnProcessFold.ts
|
|
977
|
+
const entries = /* @__PURE__ */ new Map();
|
|
978
|
+
function addressKey(address) {
|
|
979
|
+
return `${address.sessionId}\u0000${address.turn}`;
|
|
980
|
+
}
|
|
981
|
+
function entryFor(key) {
|
|
982
|
+
const current = entries.get(key);
|
|
983
|
+
if (current !== void 0) return current;
|
|
984
|
+
const created = {
|
|
985
|
+
collapsed: false,
|
|
986
|
+
completed: false,
|
|
987
|
+
label: void 0,
|
|
988
|
+
boundaries: /* @__PURE__ */ new Map(),
|
|
989
|
+
members: /* @__PURE__ */ new Map(),
|
|
990
|
+
listeners: /* @__PURE__ */ new Set()
|
|
991
|
+
};
|
|
992
|
+
entries.set(key, created);
|
|
993
|
+
return created;
|
|
994
|
+
}
|
|
995
|
+
function notify$1(entry) {
|
|
996
|
+
for (const listener of entry.listeners) listener();
|
|
997
|
+
}
|
|
998
|
+
function discardUnused(key, entry) {
|
|
999
|
+
if (entry.listeners.size === 0 && entry.boundaries.size === 0 && entry.members.size === 0) entries.delete(key);
|
|
1000
|
+
}
|
|
1001
|
+
function subscribe(key, listener) {
|
|
1002
|
+
const entry = entryFor(key);
|
|
1003
|
+
entry.listeners.add(listener);
|
|
1004
|
+
return () => {
|
|
1005
|
+
entry.listeners.delete(listener);
|
|
1006
|
+
discardUnused(key, entry);
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function orderedMembers(entry) {
|
|
1010
|
+
return [...entry.members].sort(([leftId, leftOrder], [rightId, rightOrder]) => leftOrder - rightOrder || leftId.localeCompare(rightId)).map(([id]) => id);
|
|
1011
|
+
}
|
|
1012
|
+
function orderedBoundaries(entry) {
|
|
1013
|
+
return [...entry.boundaries].sort(([leftId, leftOrder], [rightId, rightOrder]) => leftOrder - rightOrder || leftId.localeCompare(rightId)).map(([id]) => id);
|
|
1014
|
+
}
|
|
1015
|
+
function complete(key, label) {
|
|
1016
|
+
const entry = entryFor(key);
|
|
1017
|
+
if (entry.completed) {
|
|
1018
|
+
if (entry.label === label) return;
|
|
1019
|
+
entry.label = label;
|
|
1020
|
+
notify$1(entry);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
entry.completed = true;
|
|
1024
|
+
entry.label = label;
|
|
1025
|
+
entry.collapsed = true;
|
|
1026
|
+
notify$1(entry);
|
|
1027
|
+
}
|
|
1028
|
+
function registerMember(key, memberId, memberOrder) {
|
|
1029
|
+
const entry = entryFor(key);
|
|
1030
|
+
entry.members.set(memberId, memberOrder);
|
|
1031
|
+
notify$1(entry);
|
|
1032
|
+
return () => {
|
|
1033
|
+
entry.members.delete(memberId);
|
|
1034
|
+
notify$1(entry);
|
|
1035
|
+
discardUnused(key, entry);
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function registerBoundary(key, boundaryId, boundaryOrder) {
|
|
1039
|
+
const entry = entryFor(key);
|
|
1040
|
+
entry.boundaries.set(boundaryId, boundaryOrder);
|
|
1041
|
+
notify$1(entry);
|
|
1042
|
+
return () => {
|
|
1043
|
+
entry.boundaries.delete(boundaryId);
|
|
1044
|
+
notify$1(entry);
|
|
1045
|
+
discardUnused(key, entry);
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function hash$1(value) {
|
|
1049
|
+
let result = 2166136261;
|
|
1050
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1051
|
+
result ^= value.charCodeAt(index);
|
|
1052
|
+
result = Math.imul(result, 16777619);
|
|
1053
|
+
}
|
|
1054
|
+
return (result >>> 0).toString(36);
|
|
1055
|
+
}
|
|
1056
|
+
/** Stable DOM id for one flat Chat node controlled by a Turn disclosure. */
|
|
1057
|
+
function turnProcessMemberId(address, nodeKey) {
|
|
1058
|
+
return `dsh-turn-process-${address.turn}-${hash$1(`${address.sessionId}\u0000${nodeKey}`)}`;
|
|
1059
|
+
}
|
|
1060
|
+
/** Stable DOM/store identity for the user-anchored Turn disclosure. */
|
|
1061
|
+
function turnProcessBoundaryId(sessionId, nodeKey) {
|
|
1062
|
+
return `dsh-turn-boundary-${hash$1(`${sessionId}\u0000${nodeKey}`)}`;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Share one collapse state across flat Chat siblings belonging to the same
|
|
1066
|
+
* Session Turn. A successful final node marks the Turn complete; process
|
|
1067
|
+
* members contribute their DOM ids to the disclosure's `aria-controls` list.
|
|
1068
|
+
*/
|
|
1069
|
+
function useTurnProcessFold(address, options = {}) {
|
|
1070
|
+
const key = address === void 0 ? void 0 : addressKey(address);
|
|
1071
|
+
const subscribeToKey = (0, react.useCallback)((listener) => key === void 0 ? () => {} : subscribe(key, listener), [key]);
|
|
1072
|
+
const collapsed = (0, react.useSyncExternalStore)(subscribeToKey, () => key === void 0 ? false : entries.get(key)?.collapsed ?? false, () => false);
|
|
1073
|
+
const completed = (0, react.useSyncExternalStore)(subscribeToKey, () => key === void 0 ? false : entries.get(key)?.completed ?? false, () => false);
|
|
1074
|
+
const controls = (0, react.useSyncExternalStore)(subscribeToKey, () => {
|
|
1075
|
+
if (key === void 0) return "";
|
|
1076
|
+
const entry = entries.get(key);
|
|
1077
|
+
return entry === void 0 ? "" : orderedMembers(entry).join(" ");
|
|
1078
|
+
}, () => "");
|
|
1079
|
+
const boundaryId = (0, react.useSyncExternalStore)(subscribeToKey, () => {
|
|
1080
|
+
if (key === void 0) return void 0;
|
|
1081
|
+
const entry = entries.get(key);
|
|
1082
|
+
if (entry === void 0) return void 0;
|
|
1083
|
+
return orderedBoundaries(entry)[0] ?? orderedMembers(entry)[0];
|
|
1084
|
+
}, () => void 0);
|
|
1085
|
+
const hasMembers = (0, react.useSyncExternalStore)(subscribeToKey, () => key !== void 0 && (entries.get(key)?.members.size ?? 0) > 0, () => false);
|
|
1086
|
+
const label = (0, react.useSyncExternalStore)(subscribeToKey, () => key === void 0 ? void 0 : entries.get(key)?.label, () => void 0);
|
|
1087
|
+
(0, react.useLayoutEffect)(() => {
|
|
1088
|
+
if (key === void 0 || options.boundaryId === void 0 || options.boundaryOrder === void 0) return;
|
|
1089
|
+
return registerBoundary(key, options.boundaryId, options.boundaryOrder);
|
|
1090
|
+
}, [
|
|
1091
|
+
key,
|
|
1092
|
+
options.boundaryId,
|
|
1093
|
+
options.boundaryOrder
|
|
1094
|
+
]);
|
|
1095
|
+
(0, react.useLayoutEffect)(() => {
|
|
1096
|
+
if (key === void 0 || options.memberId === void 0 || options.memberOrder === void 0) return;
|
|
1097
|
+
return registerMember(key, options.memberId, options.memberOrder);
|
|
1098
|
+
}, [
|
|
1099
|
+
key,
|
|
1100
|
+
options.memberId,
|
|
1101
|
+
options.memberOrder
|
|
1102
|
+
]);
|
|
1103
|
+
(0, react.useLayoutEffect)(() => {
|
|
1104
|
+
if (key === void 0 || options.completedLabel === void 0) return;
|
|
1105
|
+
complete(key, options.completedLabel);
|
|
1106
|
+
}, [key, options.completedLabel]);
|
|
1107
|
+
const toggle = (0, react.useCallback)(() => {
|
|
1108
|
+
if (key === void 0) return;
|
|
1109
|
+
const entry = entryFor(key);
|
|
1110
|
+
if (!entry.completed || entry.members.size === 0) return;
|
|
1111
|
+
entry.collapsed = !entry.collapsed;
|
|
1112
|
+
notify$1(entry);
|
|
1113
|
+
}, [key]);
|
|
1114
|
+
return {
|
|
1115
|
+
collapsed,
|
|
1116
|
+
completed,
|
|
1117
|
+
boundary: options.boundaryId !== void 0 && boundaryId === options.boundaryId || options.memberId !== void 0 && boundaryId === options.memberId,
|
|
1118
|
+
controls,
|
|
1119
|
+
hasMembers,
|
|
1120
|
+
label,
|
|
1121
|
+
toggle
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/client/TurnProcessMember.tsx
|
|
1126
|
+
function useElapsedNow$1(active) {
|
|
1127
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
1128
|
+
(0, react.useEffect)(() => {
|
|
1129
|
+
if (!active) return;
|
|
1130
|
+
setNow(Date.now());
|
|
1131
|
+
const timer = window.setInterval(() => setNow(Date.now()), 1e3);
|
|
1132
|
+
return () => window.clearInterval(timer);
|
|
1133
|
+
}, [active]);
|
|
1134
|
+
return now;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* One process row in a flat Harness Turn. The earliest registered row owns the
|
|
1138
|
+
* Turn disclosure, while this row's own content participates in the shared
|
|
1139
|
+
* collapsed state.
|
|
1140
|
+
*/
|
|
1141
|
+
function TurnProcessMember({ address, memberId, memberOrder, memberRef, turnStartTime, formatRunningElapsed, children, ...attributes }) {
|
|
1142
|
+
const fold = useTurnProcessFold(address, {
|
|
1143
|
+
memberId,
|
|
1144
|
+
memberOrder
|
|
1145
|
+
});
|
|
1146
|
+
const running = fold.boundary && !fold.completed && turnStartTime !== void 0 && formatRunningElapsed !== void 0;
|
|
1147
|
+
const now = useElapsedNow$1(running);
|
|
1148
|
+
const label = fold.completed ? fold.label : running ? formatRunningElapsed(now - turnStartTime) : void 0;
|
|
1149
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [fold.boundary && label !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1150
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldRow,
|
|
1151
|
+
"data-turn-fold-row": "",
|
|
1152
|
+
"data-turn-fold-state": fold.completed ? "completed" : "running",
|
|
1153
|
+
children: fold.completed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1154
|
+
type: "button",
|
|
1155
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldButton,
|
|
1156
|
+
"aria-expanded": !fold.collapsed,
|
|
1157
|
+
"aria-controls": fold.controls,
|
|
1158
|
+
onClick: fold.toggle,
|
|
1159
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), fold.collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
|
|
1160
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1161
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldLabel,
|
|
1162
|
+
"aria-live": "off",
|
|
1163
|
+
children: label
|
|
1164
|
+
})
|
|
1165
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1166
|
+
...attributes,
|
|
1167
|
+
ref: memberRef,
|
|
1168
|
+
id: memberId,
|
|
1169
|
+
"data-turn-process": "",
|
|
1170
|
+
hidden: fold.collapsed,
|
|
1171
|
+
children
|
|
1172
|
+
})] });
|
|
1173
|
+
}
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region src/client/turnElapsed.ts
|
|
1176
|
+
function formatRunDuration(ms, t) {
|
|
1177
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
1178
|
+
const minutes = Math.floor(total / 60);
|
|
1179
|
+
const seconds = total % 60;
|
|
1180
|
+
return minutes > 0 ? t("duration.minutes", {
|
|
1181
|
+
minutes,
|
|
1182
|
+
seconds: String(seconds).padStart(2, "0")
|
|
1183
|
+
}) : t("duration.seconds", { seconds });
|
|
1184
|
+
}
|
|
1185
|
+
function formatTurnElapsed(ms, t) {
|
|
1186
|
+
const duration = formatRunDuration(ms, t);
|
|
1187
|
+
const label = t("message.ranFor", { duration });
|
|
1188
|
+
return label === `用时 ${duration}` ? `耗时 ${duration}` : label;
|
|
1189
|
+
}
|
|
1190
|
+
function formatTurnProcessed(ms, t) {
|
|
1191
|
+
const duration = formatRunDuration(ms, t);
|
|
1192
|
+
const label = t("message.ranFor", { duration });
|
|
1193
|
+
return label === `用时 ${duration}` ? `已处理 ${duration}` : label;
|
|
1194
|
+
}
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/client/TypewriterAssistantNodeView.tsx
|
|
1197
|
+
function usePrefersReducedMotion() {
|
|
1198
|
+
const [reduced, setReduced] = (0, react.useState)(() => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true);
|
|
1199
|
+
(0, react.useEffect)(() => {
|
|
1200
|
+
if (typeof window === "undefined" || window.matchMedia === void 0) return;
|
|
1201
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
1202
|
+
const onChange = () => setReduced(query.matches);
|
|
1203
|
+
query.addEventListener("change", onChange);
|
|
1204
|
+
return () => query.removeEventListener("change", onChange);
|
|
1205
|
+
}, []);
|
|
1206
|
+
return reduced;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Smooth streaming text arm. While the reply runs, the accumulated source is
|
|
1210
|
+
* revealed through the smoother at a rate that tracks the model's arrival
|
|
1211
|
+
* and rendered by the Harness `MarkdownText`
|
|
1212
|
+
* streaming arm (incremental parse, frozen non-tail blocks), so there is no
|
|
1213
|
+
* raw-text tail and no text-to-markdown swap: the tree stays markdown
|
|
1214
|
+
* throughout. The last text block owns conversation-port follow so wraps
|
|
1215
|
+
* glide instead of snapping. Once the stream closes and the reveal queue
|
|
1216
|
+
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
1217
|
+
* mentions) swaps in exactly once.
|
|
1218
|
+
*/
|
|
1219
|
+
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, announce, ownFollow, mode, preset, revealCharsPerSec, scrollSpeedPxPerSec, maxScrollSpeedPxPerSec, shouldHoldBack }) {
|
|
1220
|
+
const reduced = usePrefersReducedMotion();
|
|
1221
|
+
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
1222
|
+
const speedCpsRef = (0, react.useRef)(35);
|
|
1223
|
+
const displayed = useConversationContent(text, {
|
|
1224
|
+
enabled: mode === "typewriter" && typing && !reduced,
|
|
1225
|
+
preset,
|
|
1226
|
+
shouldHoldBack,
|
|
1227
|
+
steadyCps: mode === "typewriter" ? revealCharsPerSec : void 0,
|
|
1228
|
+
speedCpsRef
|
|
1229
|
+
});
|
|
1230
|
+
const shown = reduced || mode === "teleprompter" ? text : displayed;
|
|
1231
|
+
const live = typing && !reduced;
|
|
1232
|
+
(0, react.useEffect)(() => {
|
|
1233
|
+
if (typing && !streaming && shown.length === text.length) setTyping(false);
|
|
1234
|
+
}, [
|
|
1235
|
+
shown,
|
|
1236
|
+
streaming,
|
|
1237
|
+
text,
|
|
1238
|
+
typing
|
|
1239
|
+
]);
|
|
1240
|
+
if (live) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [announce && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1241
|
+
className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
|
|
1242
|
+
"aria-live": "polite",
|
|
1243
|
+
children: text
|
|
1244
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1245
|
+
active: ownFollow,
|
|
1246
|
+
speedCpsRef,
|
|
1247
|
+
minSpeedPxPerSec: scrollSpeedPxPerSec,
|
|
1248
|
+
maxSpeedPxPerSec: maxScrollSpeedPxPerSec,
|
|
1249
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1250
|
+
className: TypewriterAssistantNodeView_module_css_default.markdownFlow,
|
|
1251
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
1252
|
+
text: shown,
|
|
1253
|
+
streaming: true,
|
|
1254
|
+
codeLabels
|
|
1255
|
+
})
|
|
1256
|
+
})
|
|
1257
|
+
})] });
|
|
1258
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1259
|
+
className: TypewriterAssistantNodeView_module_css_default.markdownFlow,
|
|
1260
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
1261
|
+
text,
|
|
1262
|
+
codeLabels,
|
|
1263
|
+
fileMentions
|
|
1264
|
+
})
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Think is part of the Turn document flow rather than a second disclosure
|
|
1269
|
+
* card. It uses the same streaming smoother as ordinary assistant prose, so
|
|
1270
|
+
* reasoning reads as a continuous body while the surrounding Turn still owns
|
|
1271
|
+
* process folding and viewport follow.
|
|
1272
|
+
*/
|
|
1273
|
+
function AnimatedReasoning({ text, running, mode, preset, revealCharsPerSec, shouldHoldBack }) {
|
|
1274
|
+
const reduced = usePrefersReducedMotion();
|
|
1275
|
+
const [typing, setTyping] = (0, react.useState)(running);
|
|
1276
|
+
const speedCpsRef = (0, react.useRef)(35);
|
|
1277
|
+
const displayed = useConversationContent(text, {
|
|
1278
|
+
enabled: mode === "typewriter" && typing && !reduced,
|
|
1279
|
+
preset,
|
|
1280
|
+
shouldHoldBack,
|
|
1281
|
+
steadyCps: mode === "typewriter" ? revealCharsPerSec : void 0,
|
|
1282
|
+
speedCpsRef
|
|
1283
|
+
});
|
|
1284
|
+
const shown = reduced || mode === "teleprompter" ? text : displayed;
|
|
1285
|
+
(0, react.useEffect)(() => {
|
|
1286
|
+
if (typing && !running && shown.length === text.length) setTyping(false);
|
|
1287
|
+
}, [
|
|
1288
|
+
shown,
|
|
1289
|
+
running,
|
|
1290
|
+
text,
|
|
1291
|
+
typing
|
|
1292
|
+
]);
|
|
1293
|
+
const live = typing && !reduced;
|
|
1294
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1295
|
+
className: TypewriterAssistantNodeView_module_css_default.reasoningText,
|
|
1296
|
+
"data-stream-node": "reasoning",
|
|
1297
|
+
"data-stream-state": running ? "running" : "settled",
|
|
1298
|
+
"data-stream-reasoning": "",
|
|
1299
|
+
children: live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
1300
|
+
text: shown,
|
|
1301
|
+
streaming: true
|
|
1302
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { text })
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Assistant renderer for the Codex-style event stream. Teleprompter mode
|
|
1307
|
+
* publishes each latest model snapshot immediately; typewriter mode reveals
|
|
1308
|
+
* complete grapheme clusters at the configured rate. Reasoning stays in the
|
|
1309
|
+
* same document flow as ordinary prose, and the final Assistant message is marked separately
|
|
1310
|
+
* from intermediate process updates. The FPS guard holds offscreen typewriter
|
|
1311
|
+
* commits when the frame rate is degraded. Settled text uses the full Markdown
|
|
1312
|
+
* pipeline.
|
|
1313
|
+
*/
|
|
1314
|
+
const TypewriterAssistantNodeView = (0, react.memo)(function TypewriterAssistantNodeView({ mode = DEFAULT_CONVERSATION_CONFIG.mode, preset = DEFAULT_CONVERSATION_CONFIG.preset, revealCharsPerSec = DEFAULT_CONVERSATION_CONFIG.revealCharsPerSec, scrollSpeedPxPerSec = DEFAULT_CONVERSATION_CONFIG.scrollSpeedPxPerSec, maxScrollSpeedPxPerSec = DEFAULT_CONVERSATION_CONFIG.maxScrollSpeedPxPerSec, node, sessionId, useTurnData, openFile, renderMessageImages, fileMentions, t }) {
|
|
1315
|
+
const data = node.data;
|
|
1316
|
+
const streaming = data.status === "running";
|
|
1317
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
1318
|
+
const { ref: guardRef, shouldHoldBack } = useFpsGuard(streaming);
|
|
1319
|
+
const rootSpeedRef = (0, react.useRef)(35);
|
|
1320
|
+
const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn : void 0;
|
|
1321
|
+
const tail = useTurnData("turn-tail");
|
|
1322
|
+
const finalAnswer = data.status === "settled" && data.finalNode !== void 0 && tail?.closing?.finalNode.seq === data.finalNode.seq;
|
|
1323
|
+
const phase = streaming ? "streaming" : data.status === "interrupted" ? "interrupted" : finalAnswer ? "final" : "process";
|
|
1324
|
+
const owner = (0, react.useMemo)(() => {
|
|
1325
|
+
if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
|
|
1326
|
+
if (tail?.closing?.finalNode.seq !== data.finalNode.seq) return void 0;
|
|
1327
|
+
return {
|
|
1328
|
+
turn,
|
|
1329
|
+
seq: data.finalNode.seq,
|
|
1330
|
+
openFile
|
|
1331
|
+
};
|
|
1332
|
+
}, [
|
|
1333
|
+
data.finalNode,
|
|
1334
|
+
openFile,
|
|
1335
|
+
tail,
|
|
1336
|
+
turn
|
|
1337
|
+
]);
|
|
1338
|
+
const mentions = (0, react.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
|
|
1339
|
+
const codeLabels = (0, react.useMemo)(() => ({
|
|
1340
|
+
copyLabel: t("copy"),
|
|
1341
|
+
copiedLabel: t("copied")
|
|
1342
|
+
}), [t]);
|
|
1343
|
+
const hasVisible = streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call");
|
|
1344
|
+
const address = turn === void 0 ? void 0 : {
|
|
1345
|
+
sessionId,
|
|
1346
|
+
turn: turn.turn
|
|
1347
|
+
};
|
|
1348
|
+
const processMemberId = (phase === "streaming" || phase === "process") && address !== void 0 && hasVisible ? turnProcessMemberId(address, node.key) : void 0;
|
|
1349
|
+
const successfulFinal = finalAnswer && turn?.status === "closed" && turn.start !== void 0 && turn.end !== void 0 && turn.end.data.reason.kind === "completed";
|
|
1350
|
+
const completedLabel = successfulFinal && turn?.start !== void 0 && turn.end !== void 0 ? formatTurnElapsed(turn.end.time - turn.start.time, t) : void 0;
|
|
1351
|
+
const fold = useTurnProcessFold(address, { completedLabel });
|
|
1352
|
+
const memberOrder = typeof node.anchorSeq === "number" ? node.anchorSeq : data.finalNode?.seq ?? 0;
|
|
1353
|
+
const formatRunningElapsed = (0, react.useMemo)(() => (ms) => formatTurnProcessed(ms, t), [t]);
|
|
1354
|
+
if (!hasVisible) return null;
|
|
1355
|
+
const rendered = [];
|
|
1356
|
+
const last = data.blocks.length - 1;
|
|
1357
|
+
let lastFollow = -1;
|
|
1358
|
+
for (let index = 0; index < data.blocks.length; index += 1) {
|
|
1359
|
+
const kind = data.blocks[index]?.kind;
|
|
1360
|
+
if (kind === "text" || kind === "reasoning") lastFollow = index;
|
|
1361
|
+
}
|
|
1362
|
+
for (let index = 0; index < data.blocks.length; index += 1) {
|
|
1363
|
+
const block = data.blocks[index];
|
|
1364
|
+
if (block === void 0) continue;
|
|
1365
|
+
switch (block.kind) {
|
|
1366
|
+
case "text":
|
|
1367
|
+
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedMarkdownText, {
|
|
1368
|
+
text: block.text,
|
|
1369
|
+
codeLabels,
|
|
1370
|
+
fileMentions: mentions,
|
|
1371
|
+
streaming,
|
|
1372
|
+
announce: index === last,
|
|
1373
|
+
ownFollow: !streaming && index === lastFollow,
|
|
1374
|
+
mode,
|
|
1375
|
+
preset,
|
|
1376
|
+
revealCharsPerSec,
|
|
1377
|
+
scrollSpeedPxPerSec,
|
|
1378
|
+
maxScrollSpeedPxPerSec,
|
|
1379
|
+
shouldHoldBack
|
|
1380
|
+
}, index));
|
|
1381
|
+
break;
|
|
1382
|
+
case "reasoning":
|
|
1383
|
+
{
|
|
1384
|
+
const reasoning = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedReasoning, {
|
|
1385
|
+
text: block.text,
|
|
1386
|
+
running: streaming && index === last,
|
|
1387
|
+
mode,
|
|
1388
|
+
preset,
|
|
1389
|
+
revealCharsPerSec,
|
|
1390
|
+
shouldHoldBack
|
|
1391
|
+
}, index);
|
|
1392
|
+
if (finalAnswer && address !== void 0) rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnProcessMember, {
|
|
1393
|
+
address,
|
|
1394
|
+
memberId: turnProcessMemberId(address, `${node.key}:reasoning:${index}`),
|
|
1395
|
+
memberOrder: memberOrder + .5 + index,
|
|
1396
|
+
children: reasoning
|
|
1397
|
+
}, index));
|
|
1398
|
+
else rendered.push(reasoning);
|
|
1399
|
+
}
|
|
1400
|
+
break;
|
|
1401
|
+
case "image": {
|
|
1402
|
+
const start = index;
|
|
1403
|
+
const group = [block];
|
|
1404
|
+
while (index + 1 < data.blocks.length) {
|
|
1405
|
+
const next = data.blocks[index + 1];
|
|
1406
|
+
if (next === void 0 || next.kind !== "image") break;
|
|
1407
|
+
group.push(next);
|
|
1408
|
+
index += 1;
|
|
1409
|
+
}
|
|
1410
|
+
rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: renderMessageImages({
|
|
1411
|
+
images: group,
|
|
1412
|
+
align: "start"
|
|
1413
|
+
}) }, start));
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
case "tool-call": break;
|
|
1417
|
+
case "other": rendered.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.JsonBlock, {
|
|
1418
|
+
label: t("message.unknownBlock"),
|
|
1419
|
+
payload: block.block,
|
|
1420
|
+
truncatedLabel: (total) => t("json.truncated", { total })
|
|
1421
|
+
}, index));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const content = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [successfulFinal && fold.controls === "" && completedLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1425
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldRow,
|
|
1426
|
+
"data-turn-fold-row": "",
|
|
1427
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1428
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldLabel,
|
|
1429
|
+
children: completedLabel
|
|
1430
|
+
})
|
|
1431
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1432
|
+
active: streaming && !reducedMotion,
|
|
1433
|
+
speedCpsRef: rootSpeedRef,
|
|
1434
|
+
minSpeedPxPerSec: scrollSpeedPxPerSec,
|
|
1435
|
+
maxSpeedPxPerSec: maxScrollSpeedPxPerSec,
|
|
1436
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1437
|
+
className: TypewriterAssistantNodeView_module_css_default.body,
|
|
1438
|
+
children: [rendered, data.status === "interrupted" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1439
|
+
className: TypewriterAssistantNodeView_module_css_default.stopped,
|
|
1440
|
+
children: t("message.stopped")
|
|
1441
|
+
})]
|
|
1442
|
+
})
|
|
1443
|
+
})] });
|
|
1444
|
+
if (processMemberId !== void 0 && address !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnProcessMember, {
|
|
1445
|
+
address,
|
|
1446
|
+
memberId: processMemberId,
|
|
1447
|
+
memberOrder,
|
|
1448
|
+
turnStartTime: turn?.start?.time,
|
|
1449
|
+
formatRunningElapsed,
|
|
1450
|
+
memberRef: guardRef,
|
|
1451
|
+
className: TypewriterAssistantNodeView_module_css_default.root,
|
|
1452
|
+
"data-stream-node": "assistant-step",
|
|
1453
|
+
"data-stream-phase": phase,
|
|
1454
|
+
"data-streaming": streaming || void 0,
|
|
1455
|
+
children: content
|
|
1456
|
+
});
|
|
1457
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1458
|
+
ref: guardRef,
|
|
1459
|
+
className: TypewriterAssistantNodeView_module_css_default.root,
|
|
1460
|
+
"data-stream-node": "assistant-step",
|
|
1461
|
+
"data-stream-phase": phase,
|
|
1462
|
+
"data-streaming": streaming || void 0,
|
|
1463
|
+
children: content
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region ../../node_modules/.pnpm/react-icons@5.5.0_react@18.3.1/node_modules/react-icons/lib/iconContext.mjs
|
|
1468
|
+
var DefaultContext = {
|
|
1469
|
+
color: void 0,
|
|
1470
|
+
size: void 0,
|
|
1471
|
+
className: void 0,
|
|
1472
|
+
style: void 0,
|
|
1473
|
+
attr: void 0
|
|
1474
|
+
};
|
|
1475
|
+
var IconContext = react.default.createContext && /*#__PURE__*/ react.default.createContext(DefaultContext);
|
|
1476
|
+
//#endregion
|
|
1477
|
+
//#region ../../node_modules/.pnpm/react-icons@5.5.0_react@18.3.1/node_modules/react-icons/lib/iconBase.mjs
|
|
1478
|
+
var _excluded = [
|
|
1479
|
+
"attr",
|
|
1480
|
+
"size",
|
|
1481
|
+
"title"
|
|
1482
|
+
];
|
|
1483
|
+
function _objectWithoutProperties(source, excluded) {
|
|
1484
|
+
if (source == null) return {};
|
|
1485
|
+
var target = _objectWithoutPropertiesLoose(source, excluded);
|
|
1486
|
+
var key, i;
|
|
1487
|
+
if (Object.getOwnPropertySymbols) {
|
|
1488
|
+
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
|
|
1489
|
+
for (i = 0; i < sourceSymbolKeys.length; i++) {
|
|
1490
|
+
key = sourceSymbolKeys[i];
|
|
1491
|
+
if (excluded.indexOf(key) >= 0) continue;
|
|
1492
|
+
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
|
|
1493
|
+
target[key] = source[key];
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return target;
|
|
1497
|
+
}
|
|
1498
|
+
function _objectWithoutPropertiesLoose(source, excluded) {
|
|
1499
|
+
if (source == null) return {};
|
|
1500
|
+
var target = {};
|
|
1501
|
+
for (var key in source) if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
1502
|
+
if (excluded.indexOf(key) >= 0) continue;
|
|
1503
|
+
target[key] = source[key];
|
|
1504
|
+
}
|
|
1505
|
+
return target;
|
|
1506
|
+
}
|
|
1507
|
+
function _extends() {
|
|
1508
|
+
_extends = Object.assign ? Object.assign.bind() : function(target) {
|
|
1509
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
1510
|
+
var source = arguments[i];
|
|
1511
|
+
for (var key in source) if (Object.prototype.hasOwnProperty.call(source, key)) target[key] = source[key];
|
|
1512
|
+
}
|
|
1513
|
+
return target;
|
|
1514
|
+
};
|
|
1515
|
+
return _extends.apply(this, arguments);
|
|
1516
|
+
}
|
|
1517
|
+
function ownKeys(e, r) {
|
|
1518
|
+
var t = Object.keys(e);
|
|
1519
|
+
if (Object.getOwnPropertySymbols) {
|
|
1520
|
+
var o = Object.getOwnPropertySymbols(e);
|
|
1521
|
+
r && (o = o.filter(function(r) {
|
|
1522
|
+
return Object.getOwnPropertyDescriptor(e, r).enumerable;
|
|
1523
|
+
})), t.push.apply(t, o);
|
|
1524
|
+
}
|
|
1525
|
+
return t;
|
|
1526
|
+
}
|
|
1527
|
+
function _objectSpread(e) {
|
|
1528
|
+
for (var r = 1; r < arguments.length; r++) {
|
|
1529
|
+
var t = null != arguments[r] ? arguments[r] : {};
|
|
1530
|
+
r % 2 ? ownKeys(Object(t), !0).forEach(function(r) {
|
|
1531
|
+
_defineProperty(e, r, t[r]);
|
|
1532
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r) {
|
|
1533
|
+
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
return e;
|
|
1537
|
+
}
|
|
1538
|
+
function _defineProperty(obj, key, value) {
|
|
1539
|
+
key = _toPropertyKey(key);
|
|
1540
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
1541
|
+
value,
|
|
1542
|
+
enumerable: true,
|
|
1543
|
+
configurable: true,
|
|
1544
|
+
writable: true
|
|
1545
|
+
});
|
|
1546
|
+
else obj[key] = value;
|
|
1547
|
+
return obj;
|
|
1548
|
+
}
|
|
1549
|
+
function _toPropertyKey(t) {
|
|
1550
|
+
var i = _toPrimitive(t, "string");
|
|
1551
|
+
return "symbol" == typeof i ? i : i + "";
|
|
1552
|
+
}
|
|
1553
|
+
function _toPrimitive(t, r) {
|
|
1554
|
+
if ("object" != typeof t || !t) return t;
|
|
1555
|
+
var e = t[Symbol.toPrimitive];
|
|
1556
|
+
if (void 0 !== e) {
|
|
1557
|
+
var i = e.call(t, r || "default");
|
|
1558
|
+
if ("object" != typeof i) return i;
|
|
1559
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
1560
|
+
}
|
|
1561
|
+
return ("string" === r ? String : Number)(t);
|
|
1562
|
+
}
|
|
1563
|
+
function Tree2Element(tree) {
|
|
1564
|
+
return tree && tree.map((node, i) => /*#__PURE__*/ react.default.createElement(node.tag, _objectSpread({ key: i }, node.attr), Tree2Element(node.child)));
|
|
1565
|
+
}
|
|
1566
|
+
function GenIcon(data) {
|
|
1567
|
+
return (props) => /*#__PURE__*/ react.default.createElement(IconBase, _extends({ attr: _objectSpread({}, data.attr) }, props), Tree2Element(data.child));
|
|
1568
|
+
}
|
|
1569
|
+
function IconBase(props) {
|
|
1570
|
+
var elem = (conf) => {
|
|
1571
|
+
var { attr, size, title } = props, svgProps = _objectWithoutProperties(props, _excluded);
|
|
1572
|
+
var computedSize = size || conf.size || "1em";
|
|
1573
|
+
var className;
|
|
1574
|
+
if (conf.className) className = conf.className;
|
|
1575
|
+
if (props.className) className = (className ? className + " " : "") + props.className;
|
|
1576
|
+
return /*#__PURE__*/ react.default.createElement("svg", _extends({
|
|
1577
|
+
stroke: "currentColor",
|
|
1578
|
+
fill: "currentColor",
|
|
1579
|
+
strokeWidth: "0"
|
|
1580
|
+
}, conf.attr, attr, svgProps, {
|
|
1581
|
+
className,
|
|
1582
|
+
style: _objectSpread(_objectSpread({ color: props.color || conf.color }, conf.style), props.style),
|
|
1583
|
+
height: computedSize,
|
|
1584
|
+
width: computedSize,
|
|
1585
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
1586
|
+
}), title && /*#__PURE__*/ react.default.createElement("title", null, title), props.children);
|
|
1587
|
+
};
|
|
1588
|
+
return IconContext !== void 0 ? /*#__PURE__*/ react.default.createElement(IconContext.Consumer, null, (conf) => elem(conf)) : elem(DefaultContext);
|
|
1589
|
+
}
|
|
1590
|
+
//#endregion
|
|
1591
|
+
//#region ../../node_modules/.pnpm/react-icons@5.5.0_react@18.3.1/node_modules/react-icons/fi/index.mjs
|
|
1592
|
+
function FiTool(props) {
|
|
1593
|
+
return GenIcon({
|
|
1594
|
+
"tag": "svg",
|
|
1595
|
+
"attr": {
|
|
1596
|
+
"viewBox": "0 0 24 24",
|
|
1597
|
+
"fill": "none",
|
|
1598
|
+
"stroke": "currentColor",
|
|
1599
|
+
"strokeWidth": "2",
|
|
1600
|
+
"strokeLinecap": "round",
|
|
1601
|
+
"strokeLinejoin": "round"
|
|
1602
|
+
},
|
|
1603
|
+
"child": [{
|
|
1604
|
+
"tag": "path",
|
|
1605
|
+
"attr": { "d": "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" },
|
|
1606
|
+
"child": []
|
|
1607
|
+
}]
|
|
1608
|
+
})(props);
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/client/ToolActivityGroup.tsx
|
|
1612
|
+
const groupStates = /* @__PURE__ */ new Map();
|
|
1613
|
+
const groupMounts = /* @__PURE__ */ new Map();
|
|
1614
|
+
function groupState(id) {
|
|
1615
|
+
const current = groupStates.get(id);
|
|
1616
|
+
if (current !== void 0) return current;
|
|
1617
|
+
const created = {
|
|
1618
|
+
collapsed: false,
|
|
1619
|
+
listeners: /* @__PURE__ */ new Set()
|
|
1620
|
+
};
|
|
1621
|
+
groupStates.set(id, created);
|
|
1622
|
+
return created;
|
|
1623
|
+
}
|
|
1624
|
+
function groupMount(id) {
|
|
1625
|
+
const current = groupMounts.get(id);
|
|
1626
|
+
if (current !== void 0) return current;
|
|
1627
|
+
const created = {
|
|
1628
|
+
target: null,
|
|
1629
|
+
listeners: /* @__PURE__ */ new Set()
|
|
1630
|
+
};
|
|
1631
|
+
groupMounts.set(id, created);
|
|
1632
|
+
return created;
|
|
1633
|
+
}
|
|
1634
|
+
function notify(listeners) {
|
|
1635
|
+
for (const listener of listeners) listener();
|
|
1636
|
+
}
|
|
1637
|
+
function subscribeGroup(id, listener) {
|
|
1638
|
+
const state = groupState(id);
|
|
1639
|
+
state.listeners.add(listener);
|
|
1640
|
+
return () => {
|
|
1641
|
+
state.listeners.delete(listener);
|
|
1642
|
+
if (state.listeners.size === 0) groupStates.delete(id);
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function subscribeMount(id, listener) {
|
|
1646
|
+
const mount = groupMount(id);
|
|
1647
|
+
mount.listeners.add(listener);
|
|
1648
|
+
return () => {
|
|
1649
|
+
mount.listeners.delete(listener);
|
|
1650
|
+
if (mount.listeners.size === 0 && mount.target === null) groupMounts.delete(id);
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
function setMount(id, target) {
|
|
1654
|
+
const mount = groupMount(id);
|
|
1655
|
+
if (mount.target === target) return;
|
|
1656
|
+
mount.target = target;
|
|
1657
|
+
notify(mount.listeners);
|
|
1658
|
+
}
|
|
1659
|
+
function hash(value) {
|
|
1660
|
+
let result = 2166136261;
|
|
1661
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1662
|
+
result ^= value.charCodeAt(index);
|
|
1663
|
+
result = Math.imul(result, 16777619);
|
|
1664
|
+
}
|
|
1665
|
+
return (result >>> 0).toString(36);
|
|
1666
|
+
}
|
|
1667
|
+
/** Stable identity for one contiguous semantic tool group in a Turn. */
|
|
1668
|
+
function toolActivityGroupId(sessionId, turn, semantic, firstNodeKey) {
|
|
1669
|
+
return `dsh-tool-group-${hash(`${sessionId}\u0000${turn}\u0000${semantic}\u0000${firstNodeKey}`)}`;
|
|
1670
|
+
}
|
|
1671
|
+
function useToolActivityFold(id) {
|
|
1672
|
+
const subscribe = (0, react.useCallback)((listener) => subscribeGroup(id, listener), [id]);
|
|
1673
|
+
return {
|
|
1674
|
+
collapsed: (0, react.useSyncExternalStore)(subscribe, () => groupState(id).collapsed, () => false),
|
|
1675
|
+
toggle: (0, react.useCallback)(() => {
|
|
1676
|
+
const state = groupState(id);
|
|
1677
|
+
state.collapsed = !state.collapsed;
|
|
1678
|
+
notify(state.listeners);
|
|
1679
|
+
}, [id]),
|
|
1680
|
+
collapse: (0, react.useCallback)(() => {
|
|
1681
|
+
const state = groupState(id);
|
|
1682
|
+
if (state.collapsed) return;
|
|
1683
|
+
state.collapsed = true;
|
|
1684
|
+
notify(state.listeners);
|
|
1685
|
+
}, [id])
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
function useToolActivityMount(id) {
|
|
1689
|
+
const subscribe = (0, react.useCallback)((listener) => subscribeMount(id, listener), [id]);
|
|
1690
|
+
return (0, react.useSyncExternalStore)(subscribe, () => groupMount(id).target, () => null);
|
|
1691
|
+
}
|
|
1692
|
+
function restoreScrollPosition(snapshot) {
|
|
1693
|
+
const floor = Math.max(0, snapshot.port.scrollHeight - snapshot.port.clientHeight);
|
|
1694
|
+
const target = snapshot.bottomGap <= 25 ? floor : Math.min(snapshot.top, floor);
|
|
1695
|
+
snapshot.port.scrollTop = target;
|
|
1696
|
+
}
|
|
1697
|
+
function ToolActivityGroupFrame({ group, collapsed, toggle, header, children }) {
|
|
1698
|
+
const frameRef = (0, react.useRef)(null);
|
|
1699
|
+
const pendingScroll = (0, react.useRef)(null);
|
|
1700
|
+
const register = (0, react.useCallback)((element) => {
|
|
1701
|
+
frameRef.current = element;
|
|
1702
|
+
setMount(group.id, element);
|
|
1703
|
+
}, [group.id]);
|
|
1704
|
+
const toggleWithScroll = (0, react.useCallback)(() => {
|
|
1705
|
+
const port = frameRef.current?.closest("[data-conversation-scroll]");
|
|
1706
|
+
if (port !== null && port !== void 0) {
|
|
1707
|
+
const floor = Math.max(0, port.scrollHeight - port.clientHeight);
|
|
1708
|
+
pendingScroll.current = {
|
|
1709
|
+
port,
|
|
1710
|
+
top: port.scrollTop,
|
|
1711
|
+
bottomGap: floor - port.scrollTop
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
toggle();
|
|
1715
|
+
}, [toggle]);
|
|
1716
|
+
(0, react.useLayoutEffect)(() => {
|
|
1717
|
+
const snapshot = pendingScroll.current;
|
|
1718
|
+
if (snapshot === null) return;
|
|
1719
|
+
pendingScroll.current = null;
|
|
1720
|
+
restoreScrollPosition(snapshot);
|
|
1721
|
+
if (typeof window === "undefined") return;
|
|
1722
|
+
const frame = window.requestAnimationFrame(() => restoreScrollPosition(snapshot));
|
|
1723
|
+
return () => window.cancelAnimationFrame(frame);
|
|
1724
|
+
}, [collapsed]);
|
|
1725
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1726
|
+
ref: register,
|
|
1727
|
+
id: group.domId,
|
|
1728
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupFrame,
|
|
1729
|
+
"data-stream-tool-group": group.semantic,
|
|
1730
|
+
"data-stream-tool-group-frame": "",
|
|
1731
|
+
"data-stream-tool-group-position": group.position,
|
|
1732
|
+
"data-stream-tool-group-count": group.count,
|
|
1733
|
+
"data-stream-tool-group-collapsed": collapsed || void 0,
|
|
1734
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1735
|
+
type: "button",
|
|
1736
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupHeader,
|
|
1737
|
+
"aria-expanded": !collapsed,
|
|
1738
|
+
"aria-controls": group.domId,
|
|
1739
|
+
"data-stream-tool-group-toggle": "",
|
|
1740
|
+
onClick: toggleWithScroll,
|
|
1741
|
+
children: [header, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1742
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupHeaderChevron,
|
|
1743
|
+
"aria-hidden": true,
|
|
1744
|
+
children: collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
|
|
1745
|
+
})]
|
|
1746
|
+
}), children]
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Mount the first member as a group host and portal subsequent tool members
|
|
1751
|
+
* into the same host. The Turn fold remains shared with ordinary process rows,
|
|
1752
|
+
* while a contiguous run of commands gets one Codex-style disclosure block.
|
|
1753
|
+
*/
|
|
1754
|
+
function ToolActivityGroupMember({ group, address, memberId, memberOrder, turnStartTime, formatRunningElapsed, memberRef, eventAttributes, header, children }) {
|
|
1755
|
+
const fold = useTurnProcessFold(address, {
|
|
1756
|
+
memberId,
|
|
1757
|
+
memberOrder
|
|
1758
|
+
});
|
|
1759
|
+
const activity = useToolActivityFold(group.id);
|
|
1760
|
+
const mount = useToolActivityMount(group.id);
|
|
1761
|
+
(0, react.useEffect)(() => {
|
|
1762
|
+
if (group.position === "start" && !group.hasGrowing) activity.collapse();
|
|
1763
|
+
}, [
|
|
1764
|
+
activity.collapse,
|
|
1765
|
+
group.hasGrowing,
|
|
1766
|
+
group.position
|
|
1767
|
+
]);
|
|
1768
|
+
if (group.position === "start") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnProcessMember, {
|
|
1769
|
+
address,
|
|
1770
|
+
memberId,
|
|
1771
|
+
memberOrder,
|
|
1772
|
+
turnStartTime,
|
|
1773
|
+
formatRunningElapsed,
|
|
1774
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupAnchor,
|
|
1775
|
+
"data-stream-tool-group-anchor": "",
|
|
1776
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolActivityGroupFrame, {
|
|
1777
|
+
group,
|
|
1778
|
+
collapsed: activity.collapsed,
|
|
1779
|
+
toggle: activity.toggle,
|
|
1780
|
+
header,
|
|
1781
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1782
|
+
...eventAttributes,
|
|
1783
|
+
ref: memberRef,
|
|
1784
|
+
className: TypewriterAssistantNodeView_module_css_default.eventRow,
|
|
1785
|
+
"data-stream-tool-group-member": "",
|
|
1786
|
+
"data-stream-tool-group-position": "start",
|
|
1787
|
+
"data-stream-tool-group-collapsed": activity.collapsed || void 0,
|
|
1788
|
+
children
|
|
1789
|
+
})
|
|
1790
|
+
})
|
|
1791
|
+
});
|
|
1792
|
+
const hidden = fold.collapsed || activity.collapsed;
|
|
1793
|
+
const member = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1794
|
+
id: memberId,
|
|
1795
|
+
"data-turn-process": "",
|
|
1796
|
+
"data-tool-group-member": "",
|
|
1797
|
+
"data-tool-group-hidden": hidden || void 0,
|
|
1798
|
+
hidden,
|
|
1799
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1800
|
+
...eventAttributes,
|
|
1801
|
+
ref: memberRef,
|
|
1802
|
+
className: TypewriterAssistantNodeView_module_css_default.eventRow,
|
|
1803
|
+
"data-stream-tool-group-member": "",
|
|
1804
|
+
"data-stream-tool-group-position": group.position,
|
|
1805
|
+
"data-stream-tool-group-collapsed": activity.collapsed || void 0,
|
|
1806
|
+
children
|
|
1807
|
+
})
|
|
1808
|
+
});
|
|
1809
|
+
if (mount === null) return member;
|
|
1810
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1811
|
+
"data-tool-group-proxy": "",
|
|
1812
|
+
"aria-hidden": true,
|
|
1813
|
+
children: (0, react_dom.createPortal)(member, mount)
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
/** Render the group heading with the semantic icon supplied by the tool view. */
|
|
1817
|
+
function ToolActivityGroupHeader({ label, icon }) {
|
|
1818
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1819
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupHeaderIcon,
|
|
1820
|
+
"aria-hidden": true,
|
|
1821
|
+
children: icon
|
|
1822
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1823
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupHeaderLabel,
|
|
1824
|
+
children: label
|
|
1825
|
+
})] });
|
|
1826
|
+
}
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region src/client/TypewriterToolNodeView.tsx
|
|
1829
|
+
const TOOL_ICONS = {
|
|
1830
|
+
search: _deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16,
|
|
1831
|
+
read: _deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16,
|
|
1832
|
+
edit: _deepseek_ai_dsh_client_ui_primitives.IconEditOutline16,
|
|
1833
|
+
terminal: _deepseek_ai_dsh_client_ui_primitives.IconApiOutline14,
|
|
1834
|
+
data: _deepseek_ai_dsh_client_ui_primitives.IconDataOutline16,
|
|
1835
|
+
web: _deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14,
|
|
1836
|
+
code: FiTool,
|
|
1837
|
+
skill: _deepseek_ai_dsh_client_ui_primitives.IconSkillOutline16,
|
|
1838
|
+
agent: _deepseek_ai_dsh_client_ui_primitives.IconAgentPresetOutline16,
|
|
1839
|
+
plugin: _deepseek_ai_dsh_client_ui_primitives.IconCordisPluginOutline14,
|
|
1840
|
+
question: _deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14,
|
|
1841
|
+
settings: _deepseek_ai_dsh_client_ui_primitives.IconSettingsOutline16,
|
|
1842
|
+
checklist: _deepseek_ai_dsh_client_ui_primitives.IconChecklistOutline14,
|
|
1843
|
+
other: FiTool
|
|
1844
|
+
};
|
|
1845
|
+
const CONVERSATION_NODE_ICONS = { context: _deepseek_ai_dsh_client_ui_primitives.IconBrowseOutline16 };
|
|
1846
|
+
function toolTokens(toolName) {
|
|
1847
|
+
return toolName.replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z\d]+/).filter(Boolean);
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Map a wire Tool name onto the closest Harness icon family. Custom plugins
|
|
1851
|
+
* do not have to register UI code: descriptive names such as
|
|
1852
|
+
* `smops_knowledge_search` still receive a meaningful search glyph.
|
|
1853
|
+
*/
|
|
1854
|
+
function semanticToolKind(toolName) {
|
|
1855
|
+
const tokens = toolTokens(toolName);
|
|
1856
|
+
const has = (...terms) => terms.some((term) => tokens.includes(term));
|
|
1857
|
+
if (has("skill", "skills")) return "skill";
|
|
1858
|
+
if (has("agent", "subagent", "delegate")) return "agent";
|
|
1859
|
+
if (has("cordis", "plugin", "plugins")) return "plugin";
|
|
1860
|
+
if (has("question", "ask", "clarify", "input")) return "question";
|
|
1861
|
+
if (has("scope", "config", "settings", "setting")) return "settings";
|
|
1862
|
+
if (has("database", "db", "sql", "schema", "table", "record", "records", "dataset", "memory")) return "data";
|
|
1863
|
+
if (has("plan", "todo", "task", "checklist", "goal")) return "checklist";
|
|
1864
|
+
if (has("edit", "write", "patch", "replace", "create", "delete", "remove", "move", "copy")) return "edit";
|
|
1865
|
+
if (has("bash", "shell", "terminal", "exec", "command", "cmd", "powershell", "pwsh")) return "terminal";
|
|
1866
|
+
if (has("web", "browser", "url", "http", "https", "crawl", "scrape")) return "web";
|
|
1867
|
+
if (has("read", "load", "open", "file", "files")) return "read";
|
|
1868
|
+
if (has("search", "find", "grep", "glob", "query", "lookup", "knowledge", "retrieve", "index", "log", "logs", "trace")) return "search";
|
|
1869
|
+
if (has("code", "codegraph", "graph", "python", "javascript", "typescript", "node", "program")) return "code";
|
|
1870
|
+
return "other";
|
|
1871
|
+
}
|
|
1872
|
+
function toolPresentation(node) {
|
|
1873
|
+
if (node === null || typeof node !== "object" || !("data" in node)) return null;
|
|
1874
|
+
const data = node.data;
|
|
1875
|
+
if (data === null || typeof data !== "object" || !("root" in data)) return null;
|
|
1876
|
+
const root = data.root;
|
|
1877
|
+
if (root === null || typeof root !== "object") return null;
|
|
1878
|
+
const name = "name" in root && typeof root.name === "string" ? root.name : "call" in root && root.call !== null && typeof root.call === "object" && "name" in root.call && typeof root.call.name === "string" ? root.call.name : "";
|
|
1879
|
+
if (name === "") return null;
|
|
1880
|
+
const isError = "isError" in root && root.isError === true || "error" in root && root.error !== null && root.error !== void 0;
|
|
1881
|
+
const errorCode = "error" in root && root.error !== null && typeof root.error === "object" && "code" in root.error && typeof root.error.code === "string" ? root.error.code.toLowerCase() : "";
|
|
1882
|
+
const stopped = /cancel|interrupt|abort|stop/.test(errorCode);
|
|
1883
|
+
return {
|
|
1884
|
+
name,
|
|
1885
|
+
semantic: semanticToolKind(name),
|
|
1886
|
+
state: stopped ? "stopped" : isError ? "error" : null
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
function diffLineCount(value) {
|
|
1890
|
+
if (typeof value !== "string" || value === "") return 0;
|
|
1891
|
+
const body = value.endsWith("\n") ? value.slice(0, -1) : value.endsWith("\\n") ? value.slice(0, -2) : value;
|
|
1892
|
+
return body === "" ? 0 : body.split(/\r?\n|\\n/).length;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Derive the compact +/- suffix shown beside an edit file path. The native
|
|
1896
|
+
* edit card already owns the actual diff body; this only mirrors its counts
|
|
1897
|
+
* into the collapsed summary row.
|
|
1898
|
+
*/
|
|
1899
|
+
function editDiffFilePath(node) {
|
|
1900
|
+
const presentation = toolPresentation(node);
|
|
1901
|
+
if (presentation?.semantic !== "edit" || presentation.state !== null || node === null || typeof node !== "object") return void 0;
|
|
1902
|
+
const data = node.data;
|
|
1903
|
+
if (data === null || typeof data !== "object" || !("root" in data)) return void 0;
|
|
1904
|
+
const root = data.root;
|
|
1905
|
+
if (root === null || typeof root !== "object") return void 0;
|
|
1906
|
+
const view = "resultView" in root ? root.resultView ?? root.callView : "callView" in root ? root.callView : void 0;
|
|
1907
|
+
if (view === null || typeof view !== "object" || view.card !== "diff") return void 0;
|
|
1908
|
+
const diffs = view.diffs;
|
|
1909
|
+
if (!Array.isArray(diffs)) return void 0;
|
|
1910
|
+
const first = diffs.find((diff) => diff !== null && typeof diff === "object" && typeof diff.path === "string");
|
|
1911
|
+
return first === void 0 ? void 0 : first.path;
|
|
1912
|
+
}
|
|
1913
|
+
function editDiffStats(node) {
|
|
1914
|
+
const presentation = toolPresentation(node);
|
|
1915
|
+
if (presentation?.semantic !== "edit" || presentation.state !== null || node === null || typeof node !== "object") return void 0;
|
|
1916
|
+
const data = node.data;
|
|
1917
|
+
if (data === null || typeof data !== "object" || !("root" in data)) return void 0;
|
|
1918
|
+
const root = data.root;
|
|
1919
|
+
if (root === null || typeof root !== "object") return void 0;
|
|
1920
|
+
const view = "resultView" in root ? root.resultView ?? root.callView : "callView" in root ? root.callView : void 0;
|
|
1921
|
+
if (view === null || typeof view !== "object" || view.card !== "diff") return void 0;
|
|
1922
|
+
const diffs = view.diffs;
|
|
1923
|
+
if (!Array.isArray(diffs)) return void 0;
|
|
1924
|
+
let added = 0;
|
|
1925
|
+
let removed = 0;
|
|
1926
|
+
for (const diff of diffs) {
|
|
1927
|
+
if (diff === null || typeof diff !== "object") continue;
|
|
1928
|
+
added += diffLineCount(diff.newText);
|
|
1929
|
+
removed += diffLineCount(diff.oldText);
|
|
1930
|
+
}
|
|
1931
|
+
return added === 0 && removed === 0 ? void 0 : `+${added} -${removed}`;
|
|
1932
|
+
}
|
|
1933
|
+
const TOOL_GROUP_LABELS = {
|
|
1934
|
+
search: "搜索",
|
|
1935
|
+
read: "读取了文件",
|
|
1936
|
+
edit: "编辑了文件",
|
|
1937
|
+
terminal: "运行了命令",
|
|
1938
|
+
data: "查询了数据",
|
|
1939
|
+
web: "浏览了网页",
|
|
1940
|
+
code: "运行了代码",
|
|
1941
|
+
skill: "使用了技能",
|
|
1942
|
+
agent: "调用了智能体",
|
|
1943
|
+
plugin: "调用了插件",
|
|
1944
|
+
question: "发起了提问",
|
|
1945
|
+
settings: "更新了设置",
|
|
1946
|
+
checklist: "更新了清单",
|
|
1947
|
+
other: "执行了操作"
|
|
1948
|
+
};
|
|
1949
|
+
const NATIVE_GROUP_LABELS = { context: "注入了 Prompt" };
|
|
1950
|
+
function visibleNode(node) {
|
|
1951
|
+
if (node === null || typeof node !== "object") return false;
|
|
1952
|
+
return !("visibility" in node) || node.visibility === "visible";
|
|
1953
|
+
}
|
|
1954
|
+
function sameActivityGroupNode(node, nodeKind, turn, semantic, requiresTurn) {
|
|
1955
|
+
return visibleNode(node) && chatNodeKind(node) === nodeKind && (!requiresTurn || turnDetailsFromNode(node)?.turn === turn) && (nodeKind !== "tool-call" || toolPresentation(node)?.semantic === semantic);
|
|
1956
|
+
}
|
|
1957
|
+
function activityGroup(snapshot, currentKey, sessionId, turn, nodeKind, semantic, label, requiresTurn) {
|
|
1958
|
+
if (snapshot === null || typeof snapshot !== "object" || !("chat" in snapshot)) return void 0;
|
|
1959
|
+
const chat = snapshot.chat;
|
|
1960
|
+
if (chat === null || typeof chat !== "object" || !("order" in chat) || !("nodes" in chat)) return void 0;
|
|
1961
|
+
const order = chat.order;
|
|
1962
|
+
const nodes = chat.nodes;
|
|
1963
|
+
if (!Array.isArray(order) || nodes === null || typeof nodes !== "object" || !("get" in nodes)) return void 0;
|
|
1964
|
+
const get = nodes.get;
|
|
1965
|
+
if (typeof get !== "function") return void 0;
|
|
1966
|
+
const currentIndex = order.indexOf(currentKey);
|
|
1967
|
+
if (currentIndex === -1) return void 0;
|
|
1968
|
+
const read = (index) => {
|
|
1969
|
+
const key = order[index];
|
|
1970
|
+
return typeof key === "string" ? get.call(nodes, key) : void 0;
|
|
1971
|
+
};
|
|
1972
|
+
const same = (node) => sameActivityGroupNode(node, nodeKind, turn, semantic, requiresTurn);
|
|
1973
|
+
if (!same(read(currentIndex))) return void 0;
|
|
1974
|
+
let first = currentIndex;
|
|
1975
|
+
let last = currentIndex;
|
|
1976
|
+
while (first > 0 && same(read(first - 1))) first -= 1;
|
|
1977
|
+
while (last + 1 < order.length && same(read(last + 1))) last += 1;
|
|
1978
|
+
const count = last - first + 1;
|
|
1979
|
+
if (count < 2) return void 0;
|
|
1980
|
+
const firstKey = order[first];
|
|
1981
|
+
if (typeof firstKey !== "string") return void 0;
|
|
1982
|
+
let hasGrowing = false;
|
|
1983
|
+
for (let index = first; index <= last; index += 1) if (isGrowingChatNode(read(index))) {
|
|
1984
|
+
hasGrowing = true;
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
const groupId = toolActivityGroupId(sessionId, turn, semantic, firstKey);
|
|
1988
|
+
return {
|
|
1989
|
+
id: groupId,
|
|
1990
|
+
domId: `${groupId}-frame`,
|
|
1991
|
+
semantic,
|
|
1992
|
+
label,
|
|
1993
|
+
count,
|
|
1994
|
+
position: currentIndex === first ? "start" : currentIndex === last ? "end" : "middle",
|
|
1995
|
+
hasGrowing
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
/** Find one contiguous same-kind Tool run inside a Turn. */
|
|
1999
|
+
function toolActivityGroup(snapshot, currentKey, sessionId, turn, semantic) {
|
|
2000
|
+
return activityGroup(snapshot, currentKey, sessionId, turn, "tool-call", semantic, TOOL_GROUP_LABELS[semantic], true);
|
|
2001
|
+
}
|
|
2002
|
+
/** Find one contiguous run of a native Stream activity such as Prompt context. */
|
|
2003
|
+
function nativeActivityGroup(snapshot, currentKey, sessionId, turn, kind) {
|
|
2004
|
+
const label = NATIVE_GROUP_LABELS[kind];
|
|
2005
|
+
return label === void 0 ? void 0 : activityGroup(snapshot, currentKey, sessionId, turn, kind, kind, label, false);
|
|
2006
|
+
}
|
|
2007
|
+
function ToolSemanticIcon({ presentation, className = TypewriterAssistantNodeView_module_css_default.toolSemanticIcon }) {
|
|
2008
|
+
const Icon = TOOL_ICONS[presentation.semantic];
|
|
2009
|
+
const glyph = presentation.semantic === "code" || presentation.semantic === "other" ? "wrench" : presentation.semantic;
|
|
2010
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2011
|
+
className,
|
|
2012
|
+
"data-tool-semantic-icon": presentation.semantic,
|
|
2013
|
+
"data-tool-glyph": glyph,
|
|
2014
|
+
"aria-hidden": true,
|
|
2015
|
+
children: presentation.state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "error" }) : presentation.state === "stopped" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { size: 14 })
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
function sameNestedToolIconTargets(current, next) {
|
|
2019
|
+
return current.length === next.length && current.every((item, index) => {
|
|
2020
|
+
const candidate = next[index];
|
|
2021
|
+
return candidate !== void 0 && item.key === candidate.key && item.target === candidate.target && item.presentation.name === candidate.presentation.name && item.presentation.semantic === candidate.presentation.semantic && item.presentation.state === candidate.presentation.state;
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
/** Find expandable nested Tool headers rendered by the Harness Tool tree. */
|
|
2025
|
+
function nestedToolIconTargets(root) {
|
|
2026
|
+
const targets = [];
|
|
2027
|
+
root.querySelectorAll("[data-subcalls] [data-chat-call-id]").forEach((callRow, index) => {
|
|
2028
|
+
const slot = Array.from(callRow.children).find((child) => child instanceof HTMLElement && child.matches("[data-slot=\"tool.call.toolview\"]"));
|
|
2029
|
+
if (slot === void 0) return;
|
|
2030
|
+
const toolRoot = slot.querySelector(":scope > [data-tool]");
|
|
2031
|
+
const bashRoot = slot.querySelector(":scope > * > [data-sample=\"bash\"]");
|
|
2032
|
+
const target = toolRoot?.querySelector(":scope > * > [data-disclosure-row][aria-expanded]") ?? (bashRoot?.matches("[aria-expanded]") === true ? bashRoot : null);
|
|
2033
|
+
if (target === null) return;
|
|
2034
|
+
const name = toolRoot?.getAttribute("data-tool") ?? "bash";
|
|
2035
|
+
const stateName = toolRoot?.getAttribute("data-state") ?? bashRoot?.getAttribute("data-state");
|
|
2036
|
+
const state = stateName === "error" ? "error" : stateName === "stopped" ? "stopped" : null;
|
|
2037
|
+
targets.push({
|
|
2038
|
+
key: callRow.getAttribute("data-chat-call-id") ?? `${name}-${index}`,
|
|
2039
|
+
target,
|
|
2040
|
+
presentation: {
|
|
2041
|
+
name,
|
|
2042
|
+
semantic: semanticToolKind(name),
|
|
2043
|
+
state
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
return targets;
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* Nested DisclosureRows replace their leading icon with a chevron when open.
|
|
2051
|
+
* A portal owned by this plugin keeps a semantic icon mounted in each header
|
|
2052
|
+
* across that internal state change while Harness continues to own the body.
|
|
2053
|
+
*/
|
|
2054
|
+
function NestedToolSemanticIcons({ owner, revision }) {
|
|
2055
|
+
const [targets, setTargets] = (0, react.useState)([]);
|
|
2056
|
+
(0, react.useLayoutEffect)(() => {
|
|
2057
|
+
const next = owner === null ? [] : nestedToolIconTargets(owner);
|
|
2058
|
+
setTargets((current) => sameNestedToolIconTargets(current, next) ? current : next);
|
|
2059
|
+
}, [owner, revision]);
|
|
2060
|
+
return targets.map(({ key, target, presentation }) => {
|
|
2061
|
+
const Icon = TOOL_ICONS[presentation.semantic];
|
|
2062
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2063
|
+
className: TypewriterAssistantNodeView_module_css_default.nestedToolSemanticIcon,
|
|
2064
|
+
"data-stream-nested-tool-icon": presentation.semantic,
|
|
2065
|
+
"aria-hidden": true,
|
|
2066
|
+
children: presentation.state === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "error" }) : presentation.state === "stopped" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "warning" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { size: 14 })
|
|
2067
|
+
}), target, key);
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
function ConversationNodeSemanticIcon({ kind, className = TypewriterAssistantNodeView_module_css_default.streamSemanticIcon }) {
|
|
2071
|
+
const Icon = CONVERSATION_NODE_ICONS[kind];
|
|
2072
|
+
if (Icon === void 0) return null;
|
|
2073
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2074
|
+
className,
|
|
2075
|
+
"data-stream-semantic-icon": kind,
|
|
2076
|
+
"aria-hidden": true,
|
|
2077
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { size: 14 })
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
function chatNodeKind(node) {
|
|
2081
|
+
if (node === null || typeof node !== "object" || !("kind" in node)) return "unknown";
|
|
2082
|
+
const kind = node.kind;
|
|
2083
|
+
return typeof kind === "string" ? kind : "unknown";
|
|
2084
|
+
}
|
|
2085
|
+
const KEEP_VISIBLE_KINDS = /* @__PURE__ */ new Set([
|
|
2086
|
+
"turn-tail",
|
|
2087
|
+
"turn-error",
|
|
2088
|
+
"turn-max-tokens"
|
|
2089
|
+
]);
|
|
2090
|
+
function turnDetailsFromNode(node) {
|
|
2091
|
+
if (node === null || typeof node !== "object" || !("location" in node)) return void 0;
|
|
2092
|
+
const location = node.location;
|
|
2093
|
+
if (location === null || typeof location !== "object" || !("kind" in location)) return void 0;
|
|
2094
|
+
const kind = location.kind;
|
|
2095
|
+
if (kind !== "turn" && kind !== "step") return void 0;
|
|
2096
|
+
const turn = location.turn;
|
|
2097
|
+
if (turn === null || typeof turn !== "object" || !("turn" in turn)) return void 0;
|
|
2098
|
+
const turnNumber = turn.turn;
|
|
2099
|
+
if (typeof turnNumber !== "number") return void 0;
|
|
2100
|
+
const start = "start" in turn ? turn.start : void 0;
|
|
2101
|
+
return {
|
|
2102
|
+
turn: turnNumber,
|
|
2103
|
+
startTime: start !== null && typeof start === "object" && "time" in start && typeof start.time === "number" ? start.time : void 0
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function processAddress(node, sessionId) {
|
|
2107
|
+
if (typeof sessionId !== "string") return void 0;
|
|
2108
|
+
const details = turnDetailsFromNode(node);
|
|
2109
|
+
return details === void 0 ? void 0 : {
|
|
2110
|
+
sessionId,
|
|
2111
|
+
turn: details.turn
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function nodeKey$1(node) {
|
|
2115
|
+
if (node === null || typeof node !== "object" || !("key" in node)) return void 0;
|
|
2116
|
+
const key = node.key;
|
|
2117
|
+
return typeof key === "string" ? key : void 0;
|
|
2118
|
+
}
|
|
2119
|
+
function nodeOrder(node) {
|
|
2120
|
+
if (node === null || typeof node !== "object" || !("anchorSeq" in node)) return 0;
|
|
2121
|
+
const anchorSeq = node.anchorSeq;
|
|
2122
|
+
return typeof anchorSeq === "number" ? anchorSeq : 0;
|
|
2123
|
+
}
|
|
2124
|
+
function followingTurn(snapshot, currentKey) {
|
|
2125
|
+
if (snapshot === null || typeof snapshot !== "object" || !("chat" in snapshot)) return void 0;
|
|
2126
|
+
const chat = snapshot.chat;
|
|
2127
|
+
if (chat === null || typeof chat !== "object" || !("order" in chat) || !("nodes" in chat)) return void 0;
|
|
2128
|
+
const order = chat.order;
|
|
2129
|
+
const nodes = chat.nodes;
|
|
2130
|
+
if (!Array.isArray(order) || nodes === null || typeof nodes !== "object" || !("get" in nodes)) return void 0;
|
|
2131
|
+
const get = nodes.get;
|
|
2132
|
+
if (typeof get !== "function") return void 0;
|
|
2133
|
+
const start = order.indexOf(currentKey);
|
|
2134
|
+
if (start === -1) return void 0;
|
|
2135
|
+
for (let index = start + 1; index < order.length; index += 1) {
|
|
2136
|
+
const key = order[index];
|
|
2137
|
+
if (typeof key !== "string") continue;
|
|
2138
|
+
const turn = turnDetailsFromNode(get.call(nodes, key));
|
|
2139
|
+
if (turn !== void 0) return turn;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Keep the latest settled Tool visibly active while the Agent is still
|
|
2144
|
+
* running but has not published its next Chat row yet. A later visible row
|
|
2145
|
+
* ends this handoff state; `turn-tail` is presentation metadata rather than
|
|
2146
|
+
* the next piece of model/process output.
|
|
2147
|
+
*/
|
|
2148
|
+
function isAwaitingNextTurnNode(snapshot, currentKey, turnNumber) {
|
|
2149
|
+
if (snapshot === null || typeof snapshot !== "object" || !("running" in snapshot) || snapshot.running !== true || !("chat" in snapshot)) return false;
|
|
2150
|
+
const chat = snapshot.chat;
|
|
2151
|
+
if (chat === null || typeof chat !== "object" || !("order" in chat) || !("nodes" in chat)) return false;
|
|
2152
|
+
const order = chat.order;
|
|
2153
|
+
const nodes = chat.nodes;
|
|
2154
|
+
if (!Array.isArray(order) || nodes === null || typeof nodes !== "object" || !("get" in nodes)) return false;
|
|
2155
|
+
const get = nodes.get;
|
|
2156
|
+
if (typeof get !== "function") return false;
|
|
2157
|
+
if ("timeline" in chat) {
|
|
2158
|
+
const timeline = chat.timeline;
|
|
2159
|
+
if (timeline !== null && typeof timeline === "object" && "turns" in timeline) {
|
|
2160
|
+
const turns = timeline.turns;
|
|
2161
|
+
if (turns instanceof Map) {
|
|
2162
|
+
const turn = turns.get(turnNumber);
|
|
2163
|
+
if (turn !== void 0 && turn !== null && typeof turn === "object" && "status" in turn && turn.status !== "open") return false;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
const currentIndex = order.indexOf(currentKey);
|
|
2168
|
+
if (currentIndex === -1) return false;
|
|
2169
|
+
for (let index = currentIndex + 1; index < order.length; index += 1) {
|
|
2170
|
+
const key = order[index];
|
|
2171
|
+
if (typeof key !== "string") continue;
|
|
2172
|
+
const candidate = get.call(nodes, key);
|
|
2173
|
+
if (candidate === null || typeof candidate !== "object") continue;
|
|
2174
|
+
if ("visibility" in candidate && candidate.visibility !== "visible") continue;
|
|
2175
|
+
if ("kind" in candidate && candidate.kind === "turn-tail") continue;
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* True while a Chat node is still growing: an assistant/workflow `status:
|
|
2182
|
+
* 'running'` payload, a Tool root that has not settled (`kind` absent), or a
|
|
2183
|
+
* model-retry whose current attempt is still `scheduled`.
|
|
2184
|
+
* @param node - The Chat node's view `node` prop.
|
|
2185
|
+
* @returns whether this row should own conversation follow.
|
|
2186
|
+
*/
|
|
2187
|
+
function isGrowingChatNode(node) {
|
|
2188
|
+
if (node === null || typeof node !== "object" || !("data" in node)) return false;
|
|
2189
|
+
const data = node.data;
|
|
2190
|
+
if (data === null || typeof data !== "object") return false;
|
|
2191
|
+
if ("status" in data && data.status === "running") return true;
|
|
2192
|
+
if ("root" in data) {
|
|
2193
|
+
const root = data.root;
|
|
2194
|
+
if (root !== null && typeof root === "object" && !("kind" in root)) return true;
|
|
2195
|
+
}
|
|
2196
|
+
if ("current" in data) {
|
|
2197
|
+
const current = data.current;
|
|
2198
|
+
if (current !== null && typeof current === "object" && "retryState" in current && current.retryState === "scheduled") return true;
|
|
2199
|
+
}
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Wrap a prior Chat node renderer so a growing row shares conversation
|
|
2204
|
+
* follow. Presentation stays with the wrapped component; kit seats
|
|
2205
|
+
* (`renderSlot`, locale, inject) pass through unchanged.
|
|
2206
|
+
* @param Inner - The already-registered row component.
|
|
2207
|
+
* @returns A follow-hosted row.
|
|
2208
|
+
*/
|
|
2209
|
+
function wrapFollowNodeView(Inner, motion = {}) {
|
|
2210
|
+
return function TypewriterFollowNodeView(props) {
|
|
2211
|
+
const kind = chatNodeKind(props.node);
|
|
2212
|
+
const directAddress = KEEP_VISIBLE_KINDS.has(kind) ? void 0 : processAddress(props.node, props.sessionId);
|
|
2213
|
+
const key = nodeKey$1(props.node);
|
|
2214
|
+
const useSession = props.useSession;
|
|
2215
|
+
if (directAddress !== void 0 && kind === "tool-call" && key !== void 0 && typeof useSession === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TailAwareToolFollowNodeView, {
|
|
2216
|
+
Inner,
|
|
2217
|
+
motion,
|
|
2218
|
+
props,
|
|
2219
|
+
address: directAddress,
|
|
2220
|
+
nodeKey: key,
|
|
2221
|
+
useSession
|
|
2222
|
+
});
|
|
2223
|
+
if (directAddress === void 0 && !KEEP_VISIBLE_KINDS.has(kind) && key !== void 0 && typeof props.sessionId === "string" && typeof useSession === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InferredTurnFollowNodeView, {
|
|
2224
|
+
Inner,
|
|
2225
|
+
motion,
|
|
2226
|
+
props,
|
|
2227
|
+
nodeKey: key,
|
|
2228
|
+
sessionId: props.sessionId,
|
|
2229
|
+
useSession
|
|
2230
|
+
});
|
|
2231
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowNodeView, {
|
|
2232
|
+
Inner,
|
|
2233
|
+
motion,
|
|
2234
|
+
props,
|
|
2235
|
+
address: directAddress
|
|
2236
|
+
});
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
function TailAwareToolFollowNodeView({ Inner, motion, props, address, nodeKey: currentKey, useSession }) {
|
|
2240
|
+
const awaitingNext = useSession((snapshot) => isAwaitingNextTurnNode(snapshot, currentKey, address.turn));
|
|
2241
|
+
const presentation = toolPresentation(props.node);
|
|
2242
|
+
const group = typeof props.sessionId === "string" && presentation !== null ? useSession((snapshot) => toolActivityGroup(snapshot, currentKey, props.sessionId, address.turn, presentation.semantic)) : void 0;
|
|
2243
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowNodeView, {
|
|
2244
|
+
Inner,
|
|
2245
|
+
motion,
|
|
2246
|
+
props,
|
|
2247
|
+
address,
|
|
2248
|
+
awaitingNext,
|
|
2249
|
+
toolGroup: group
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
function InferredTurnFollowNodeView({ Inner, motion, props, nodeKey: currentKey, sessionId, useSession }) {
|
|
2253
|
+
const turn = useSession((snapshot) => followingTurn(snapshot, currentKey));
|
|
2254
|
+
const address = turn === void 0 ? void 0 : {
|
|
2255
|
+
sessionId,
|
|
2256
|
+
turn: turn.turn
|
|
2257
|
+
};
|
|
2258
|
+
const kind = chatNodeKind(props.node);
|
|
2259
|
+
const group = useSession((snapshot) => kind === "context" && turn !== void 0 ? nativeActivityGroup(snapshot, currentKey, sessionId, turn.turn, kind) : void 0);
|
|
2260
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowNodeView, {
|
|
2261
|
+
Inner,
|
|
2262
|
+
motion,
|
|
2263
|
+
props,
|
|
2264
|
+
address,
|
|
2265
|
+
toolGroup: group,
|
|
2266
|
+
turnStartTime: turn?.startTime
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
function FollowNodeView({ Inner, motion, props, address, awaitingNext = false, toolGroup, turnStartTime = turnDetailsFromNode(props.node)?.startTime }) {
|
|
2270
|
+
const speedCpsRef = (0, react.useRef)(35);
|
|
2271
|
+
const [eventElement, setEventElement] = (0, react.useState)(null);
|
|
2272
|
+
const eventRef = (0, react.useCallback)((element) => {
|
|
2273
|
+
setEventElement(element);
|
|
2274
|
+
}, []);
|
|
2275
|
+
const growing = isGrowingChatNode(props.node);
|
|
2276
|
+
const kind = chatNodeKind(props.node);
|
|
2277
|
+
const staticCompaction = kind === "compaction" || kind === "manual-compaction";
|
|
2278
|
+
(0, react.useLayoutEffect)(() => {
|
|
2279
|
+
if (!staticCompaction || eventElement === null) return;
|
|
2280
|
+
const button = eventElement.querySelector("[data-compaction-disclosure]")?.closest("button");
|
|
2281
|
+
if (button === null || button === void 0) return;
|
|
2282
|
+
const previousDisabled = button.disabled;
|
|
2283
|
+
const previousExpanded = button.getAttribute("aria-expanded");
|
|
2284
|
+
button.disabled = true;
|
|
2285
|
+
button.removeAttribute("aria-expanded");
|
|
2286
|
+
return () => {
|
|
2287
|
+
button.disabled = previousDisabled;
|
|
2288
|
+
if (previousExpanded === null) button.removeAttribute("aria-expanded");
|
|
2289
|
+
else button.setAttribute("aria-expanded", previousExpanded);
|
|
2290
|
+
};
|
|
2291
|
+
}, [
|
|
2292
|
+
eventElement,
|
|
2293
|
+
props.node,
|
|
2294
|
+
staticCompaction
|
|
2295
|
+
]);
|
|
2296
|
+
const preventCompactionExpand = staticCompaction ? (event) => {
|
|
2297
|
+
event.preventDefault();
|
|
2298
|
+
event.stopPropagation();
|
|
2299
|
+
} : void 0;
|
|
2300
|
+
const tool = kind === "tool-call" ? toolPresentation(props.node) : null;
|
|
2301
|
+
const editStats = tool === null ? void 0 : editDiffStats(props.node);
|
|
2302
|
+
const [editStatsRow, setEditStatsRow] = (0, react.useState)(null);
|
|
2303
|
+
(0, react.useLayoutEffect)(() => {
|
|
2304
|
+
const row = editStats === void 0 || eventElement === null ? null : eventElement.querySelector("[data-disclosure-row]");
|
|
2305
|
+
setEditStatsRow((previous) => previous === row ? previous : row);
|
|
2306
|
+
}, [
|
|
2307
|
+
editStats,
|
|
2308
|
+
eventElement,
|
|
2309
|
+
props.node
|
|
2310
|
+
]);
|
|
2311
|
+
const ownsStreamSemanticIcon = tool === null && CONVERSATION_NODE_ICONS[kind] !== void 0;
|
|
2312
|
+
const toolProgressActive = tool !== null && (growing || awaitingNext);
|
|
2313
|
+
const key = nodeKey$1(props.node);
|
|
2314
|
+
const memberId = address === void 0 || key === void 0 ? void 0 : turnProcessMemberId(address, key);
|
|
2315
|
+
const translate = typeof props.t === "function" ? props.t : void 0;
|
|
2316
|
+
const formatRunningElapsed = translate === void 0 ? void 0 : (ms) => formatTurnProcessed(ms, translate);
|
|
2317
|
+
const inner = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2318
|
+
tool !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolSemanticIcon, { presentation: tool }),
|
|
2319
|
+
ownsStreamSemanticIcon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationNodeSemanticIcon, { kind }),
|
|
2320
|
+
(0, react.createElement)(Inner, props),
|
|
2321
|
+
tool !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NestedToolSemanticIcons, {
|
|
2322
|
+
owner: eventElement,
|
|
2323
|
+
revision: props.node
|
|
2324
|
+
}),
|
|
2325
|
+
editStats !== void 0 && editStatsRow !== null && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2326
|
+
className: TypewriterAssistantNodeView_module_css_default.editStats,
|
|
2327
|
+
"data-stream-edit-stats": "",
|
|
2328
|
+
"data-stream-edit-file": editDiffFilePath(props.node),
|
|
2329
|
+
onClick: (event) => {
|
|
2330
|
+
event.stopPropagation();
|
|
2331
|
+
event.currentTarget.closest("[data-disclosure-row][data-expandable]")?.click();
|
|
2332
|
+
},
|
|
2333
|
+
children: editStats
|
|
2334
|
+
}), editStatsRow)
|
|
2335
|
+
] });
|
|
2336
|
+
const activityRunning = growing || toolGroup?.hasGrowing === true;
|
|
2337
|
+
const headerIcon = tool !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolSemanticIcon, {
|
|
2338
|
+
presentation: tool,
|
|
2339
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupHeaderGlyph
|
|
2340
|
+
}) : ownsStreamSemanticIcon ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationNodeSemanticIcon, {
|
|
2341
|
+
kind,
|
|
2342
|
+
className: TypewriterAssistantNodeView_module_css_default.toolGroupHeaderGlyph
|
|
2343
|
+
}) : null;
|
|
2344
|
+
const eventAttributes = {
|
|
2345
|
+
"data-stream-node": kind,
|
|
2346
|
+
"data-stream-turn": address === void 0 ? void 0 : String(address.turn),
|
|
2347
|
+
"data-stream-state": activityRunning ? "running" : "settled",
|
|
2348
|
+
"data-stream-progress": toolProgressActive ? "active" : void 0,
|
|
2349
|
+
"data-stream-tool-row": tool === null ? void 0 : "",
|
|
2350
|
+
"data-stream-native-disclosure": tool === null ? "" : void 0,
|
|
2351
|
+
"data-stream-semantic-icon-owned": ownsStreamSemanticIcon ? kind : void 0,
|
|
2352
|
+
"data-tool-semantic": tool?.semantic,
|
|
2353
|
+
"data-tool-name": tool?.name
|
|
2354
|
+
};
|
|
2355
|
+
const activityHeader = toolGroup !== void 0 && headerIcon !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolActivityGroupHeader, {
|
|
2356
|
+
label: toolGroup.label,
|
|
2357
|
+
icon: headerIcon
|
|
2358
|
+
}) : null;
|
|
2359
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
2360
|
+
active: activityRunning,
|
|
2361
|
+
speedCpsRef,
|
|
2362
|
+
minSpeedPxPerSec: motion.minSpeedPxPerSec,
|
|
2363
|
+
maxSpeedPxPerSec: motion.maxSpeedPxPerSec,
|
|
2364
|
+
children: memberId !== void 0 && address !== void 0 && toolGroup !== void 0 && activityHeader !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolActivityGroupMember, {
|
|
2365
|
+
group: toolGroup,
|
|
2366
|
+
address,
|
|
2367
|
+
memberId,
|
|
2368
|
+
memberOrder: nodeOrder(props.node),
|
|
2369
|
+
turnStartTime,
|
|
2370
|
+
formatRunningElapsed,
|
|
2371
|
+
memberRef: eventRef,
|
|
2372
|
+
eventAttributes,
|
|
2373
|
+
header: activityHeader,
|
|
2374
|
+
children: inner
|
|
2375
|
+
}) : memberId !== void 0 && address !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnProcessMember, {
|
|
2376
|
+
address,
|
|
2377
|
+
memberId,
|
|
2378
|
+
memberOrder: nodeOrder(props.node),
|
|
2379
|
+
turnStartTime,
|
|
2380
|
+
formatRunningElapsed,
|
|
2381
|
+
memberRef: eventRef,
|
|
2382
|
+
className: TypewriterAssistantNodeView_module_css_default.eventRow,
|
|
2383
|
+
onClickCapture: preventCompactionExpand,
|
|
2384
|
+
"data-stream-node": kind,
|
|
2385
|
+
"data-stream-turn": address.turn,
|
|
2386
|
+
"data-stream-state": growing ? "running" : "settled",
|
|
2387
|
+
"data-stream-progress": toolProgressActive ? "active" : void 0,
|
|
2388
|
+
"data-stream-tool-row": tool === null ? void 0 : "",
|
|
2389
|
+
"data-stream-native-disclosure": tool === null ? "" : void 0,
|
|
2390
|
+
"data-stream-semantic-icon-owned": ownsStreamSemanticIcon ? kind : void 0,
|
|
2391
|
+
"data-tool-semantic": tool?.semantic,
|
|
2392
|
+
"data-tool-name": tool?.name,
|
|
2393
|
+
children: inner
|
|
2394
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2395
|
+
ref: eventRef,
|
|
2396
|
+
className: TypewriterAssistantNodeView_module_css_default.eventRow,
|
|
2397
|
+
onClickCapture: preventCompactionExpand,
|
|
2398
|
+
"data-stream-node": kind,
|
|
2399
|
+
"data-stream-state": growing ? "running" : "settled",
|
|
2400
|
+
"data-stream-progress": toolProgressActive ? "active" : void 0,
|
|
2401
|
+
"data-stream-tool-row": tool === null ? void 0 : "",
|
|
2402
|
+
"data-stream-native-disclosure": tool === null ? "" : void 0,
|
|
2403
|
+
"data-stream-semantic-icon-owned": ownsStreamSemanticIcon ? kind : void 0,
|
|
2404
|
+
"data-tool-semantic": tool?.semantic,
|
|
2405
|
+
"data-tool-name": tool?.name,
|
|
2406
|
+
children: inner
|
|
2407
|
+
})
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
//#endregion
|
|
2411
|
+
//#region src/client/TurnPreludeUserNodeView.tsx
|
|
2412
|
+
function nodeKey(node) {
|
|
2413
|
+
if (node === null || typeof node !== "object" || !("key" in node)) return void 0;
|
|
2414
|
+
return typeof node.key === "string" ? node.key : void 0;
|
|
2415
|
+
}
|
|
2416
|
+
function userSeq(node) {
|
|
2417
|
+
if (node === null || typeof node !== "object" || !("data" in node)) return void 0;
|
|
2418
|
+
const data = node.data;
|
|
2419
|
+
if (data === null || typeof data !== "object" || !("seq" in data)) return void 0;
|
|
2420
|
+
return typeof data.seq === "number" ? data.seq : void 0;
|
|
2421
|
+
}
|
|
2422
|
+
function userTime(node) {
|
|
2423
|
+
if (node === null || typeof node !== "object" || !("data" in node)) return void 0;
|
|
2424
|
+
const data = node.data;
|
|
2425
|
+
if (data === null || typeof data !== "object" || !("time" in data)) return void 0;
|
|
2426
|
+
return typeof data.time === "number" ? data.time : void 0;
|
|
2427
|
+
}
|
|
2428
|
+
function chatParts(snapshot) {
|
|
2429
|
+
if (snapshot === null || typeof snapshot !== "object" || !("chat" in snapshot)) return void 0;
|
|
2430
|
+
const chat = snapshot.chat;
|
|
2431
|
+
if (chat === null || typeof chat !== "object") return void 0;
|
|
2432
|
+
if (!("order" in chat) || !Array.isArray(chat.order)) return void 0;
|
|
2433
|
+
if (!("nodes" in chat) || !("timeline" in chat)) return void 0;
|
|
2434
|
+
const nodes = chat.nodes;
|
|
2435
|
+
const timeline = chat.timeline;
|
|
2436
|
+
if (nodes === null || typeof nodes !== "object" || !("get" in nodes)) return void 0;
|
|
2437
|
+
if (timeline === null || typeof timeline !== "object" || !("turns" in timeline) || !("turnOrder" in timeline)) return void 0;
|
|
2438
|
+
const get = nodes.get;
|
|
2439
|
+
const turns = timeline.turns;
|
|
2440
|
+
const turnOrder = timeline.turnOrder;
|
|
2441
|
+
if (typeof get !== "function" || !(turns instanceof Map) || !Array.isArray(turnOrder)) return void 0;
|
|
2442
|
+
return {
|
|
2443
|
+
order: chat.order,
|
|
2444
|
+
get: (key) => get.call(nodes, key),
|
|
2445
|
+
turns,
|
|
2446
|
+
turnOrder
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
function associatedTurn(snapshot, currentKey, currentSeq) {
|
|
2450
|
+
const chat = chatParts(snapshot);
|
|
2451
|
+
if (chat === void 0) return void 0;
|
|
2452
|
+
if (chat.order.indexOf(currentKey) === -1) return void 0;
|
|
2453
|
+
const current = chat.get(currentKey);
|
|
2454
|
+
if (current !== null && typeof current === "object" && "location" in current) {
|
|
2455
|
+
const location = current.location;
|
|
2456
|
+
if (location !== null && typeof location === "object" && "kind" in location && (location.kind === "turn" || location.kind === "step") && "turn" in location) {
|
|
2457
|
+
const turn = location.turn;
|
|
2458
|
+
if (turn !== null && typeof turn === "object" && "turn" in turn) return turn;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
let closest;
|
|
2462
|
+
let closestStartSeq = Number.NEGATIVE_INFINITY;
|
|
2463
|
+
for (const turnNumber of chat.turnOrder) {
|
|
2464
|
+
const turn = chat.turns.get(turnNumber);
|
|
2465
|
+
const startSeq = turn?.start?.seq;
|
|
2466
|
+
const endSeq = turn?.end?.seq;
|
|
2467
|
+
if (turn !== void 0 && typeof startSeq === "number" && startSeq < currentSeq && (typeof endSeq !== "number" || currentSeq < endSeq) && startSeq > closestStartSeq) {
|
|
2468
|
+
closest = turn;
|
|
2469
|
+
closestStartSeq = startSeq;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
return closest;
|
|
2473
|
+
}
|
|
2474
|
+
function isLatestUser(snapshot, currentKey) {
|
|
2475
|
+
const chat = chatParts(snapshot);
|
|
2476
|
+
if (chat === void 0) return false;
|
|
2477
|
+
for (let index = chat.order.length - 1; index >= 0; index -= 1) {
|
|
2478
|
+
const key = chat.order[index];
|
|
2479
|
+
if (typeof key !== "string") continue;
|
|
2480
|
+
const candidate = chat.get(key);
|
|
2481
|
+
if (candidate === null || typeof candidate !== "object" || !("kind" in candidate)) continue;
|
|
2482
|
+
if (candidate.kind === "user") return key === currentKey;
|
|
2483
|
+
}
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
function isRunning(snapshot) {
|
|
2487
|
+
return snapshot !== null && typeof snapshot === "object" && "running" in snapshot && snapshot.running === true;
|
|
2488
|
+
}
|
|
2489
|
+
function useElapsedNow(active) {
|
|
2490
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
2491
|
+
(0, react.useEffect)(() => {
|
|
2492
|
+
if (!active) return;
|
|
2493
|
+
setNow(Date.now());
|
|
2494
|
+
const timer = window.setInterval(() => setNow(Date.now()), 1e3);
|
|
2495
|
+
return () => window.clearInterval(timer);
|
|
2496
|
+
}, [active]);
|
|
2497
|
+
return now;
|
|
2498
|
+
}
|
|
2499
|
+
function TurnPrelude({ active, address, boundaryId, boundaryOrder, startTime, t }) {
|
|
2500
|
+
const fold = useTurnProcessFold(address, {
|
|
2501
|
+
boundaryId,
|
|
2502
|
+
boundaryOrder
|
|
2503
|
+
});
|
|
2504
|
+
const running = active && !fold.completed;
|
|
2505
|
+
const now = useElapsedNow(running);
|
|
2506
|
+
if (!running && !fold.completed) return null;
|
|
2507
|
+
const label = fold.completed ? fold.label : formatTurnProcessed(now - startTime, t);
|
|
2508
|
+
if (label === void 0) return null;
|
|
2509
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2510
|
+
className: TypewriterAssistantNodeView_module_css_default.turnPrelude,
|
|
2511
|
+
"data-turn-prelude": "",
|
|
2512
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2513
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldRow,
|
|
2514
|
+
"data-turn-fold-row": "",
|
|
2515
|
+
"data-turn-fold-state": fold.completed ? "completed" : "running",
|
|
2516
|
+
children: fold.completed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2517
|
+
type: "button",
|
|
2518
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldButton,
|
|
2519
|
+
"aria-expanded": !fold.collapsed,
|
|
2520
|
+
"aria-controls": fold.controls,
|
|
2521
|
+
onClick: fold.toggle,
|
|
2522
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), fold.collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
|
|
2523
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2524
|
+
className: TypewriterAssistantNodeView_module_css_default.turnFoldLabel,
|
|
2525
|
+
"aria-live": "off",
|
|
2526
|
+
children: label
|
|
2527
|
+
})
|
|
2528
|
+
}), running && !fold.hasMembers && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2529
|
+
className: TypewriterAssistantNodeView_module_css_default.turnWaiting,
|
|
2530
|
+
"data-turn-waiting": "",
|
|
2531
|
+
role: "status",
|
|
2532
|
+
"aria-live": "polite",
|
|
2533
|
+
children: "思考中"
|
|
2534
|
+
})]
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
/** Add an immediate Turn status boundary directly after an ordinary user row. */
|
|
2538
|
+
function wrapTurnPreludeNodeView(Inner) {
|
|
2539
|
+
return function TurnPreludeUserNodeView(props) {
|
|
2540
|
+
const key = nodeKey(props.node);
|
|
2541
|
+
const seq = userSeq(props.node);
|
|
2542
|
+
const time = userTime(props.node);
|
|
2543
|
+
const useSession = props.useSession;
|
|
2544
|
+
if (key === void 0 || seq === void 0 || time === void 0 || typeof props.sessionId !== "string" || typeof useSession !== "function" || typeof props.t !== "function") return (0, react.createElement)(Inner, props);
|
|
2545
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnPreludeUserWithSession, {
|
|
2546
|
+
Inner,
|
|
2547
|
+
props,
|
|
2548
|
+
nodeKey: key,
|
|
2549
|
+
nodeSeq: seq,
|
|
2550
|
+
nodeTime: time,
|
|
2551
|
+
sessionId: props.sessionId,
|
|
2552
|
+
useSession,
|
|
2553
|
+
t: props.t
|
|
2554
|
+
});
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
function TurnPreludeUserWithSession({ Inner, props, nodeKey: currentKey, nodeSeq, nodeTime, sessionId, useSession, t }) {
|
|
2558
|
+
const turn = useSession((snapshot) => associatedTurn(snapshot, currentKey, nodeSeq));
|
|
2559
|
+
const running = useSession(isRunning);
|
|
2560
|
+
const latestUser = useSession((snapshot) => isLatestUser(snapshot, currentKey));
|
|
2561
|
+
const address = turn === void 0 ? void 0 : {
|
|
2562
|
+
sessionId,
|
|
2563
|
+
turn: turn.turn
|
|
2564
|
+
};
|
|
2565
|
+
const active = turn?.status === "open" || turn === void 0 && running && latestUser;
|
|
2566
|
+
const startTime = typeof turn?.start?.time === "number" ? turn.start.time : nodeTime;
|
|
2567
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react.createElement)(Inner, props), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnPrelude, {
|
|
2568
|
+
active,
|
|
2569
|
+
address,
|
|
2570
|
+
boundaryId: turnProcessBoundaryId(sessionId, currentKey),
|
|
2571
|
+
boundaryOrder: nodeSeq,
|
|
2572
|
+
startTime,
|
|
2573
|
+
t
|
|
2574
|
+
})] });
|
|
2575
|
+
}
|
|
2576
|
+
//#endregion
|
|
2577
|
+
//#region \0dsh-css:/Users/baojie/Documents/Projects/github/dsh-codex-provider-plugin/packages/conversation-ui/src/client/ConversationCard.module.css.mjs
|
|
2578
|
+
const css$1 = ".y_GtGG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;flex-direction:column;list-style:none;transition:border-color .16s,background .16s;display:flex}.y_GtGG_card:hover{border-color:var(--dsw-alias-label-dimmed)}.y_GtGG_cardOpen{border-color:var(--dsw-alias-label-dimmed);background:var(--dsw-alias-bg-layer-2)}.y_GtGG_header{appearance:none;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.y_GtGG_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.y_GtGG_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.y_GtGG_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.y_GtGG_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.y_GtGG_version{text-overflow:ellipsis;white-space:nowrap;max-width:16rem;color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:1.5;overflow:hidden}.y_GtGG_pending{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.y_GtGG_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.y_GtGG_chevronOpen{transform:rotate(180deg)}.y_GtGG_body{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;margin:0 16px;padding-bottom:8px;display:flex}.y_GtGG_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.y_GtGG_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.y_GtGG_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.y_GtGG_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.y_GtGG_toggle{width:16px;height:16px;accent-color:var(--dsw-alias-brand-primary);cursor:pointer}.y_GtGG_toggle:disabled{cursor:default}.y_GtGG_toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.y_GtGG_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}.y_GtGG_failure{justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.y_GtGG_updateRow{border-top:1px solid var(--dsw-alias-border-l2);justify-content:space-between;align-items:center;gap:12px;padding:12px 0;display:flex}.y_GtGG_updateCopy{flex-direction:column;gap:4px;min-width:0;display:flex}.y_GtGG_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.y_GtGG_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.y_GtGG_save,.y_GtGG_discard,.y_GtGG_update{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.y_GtGG_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.y_GtGG_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.y_GtGG_discard:hover:not(:disabled),.y_GtGG_update:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}.y_GtGG_update{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0;flex:none;align-items:center;gap:6px;display:inline-flex}.y_GtGG_discard:disabled,.y_GtGG_save:disabled,.y_GtGG_update:disabled{opacity:.4;cursor:default}.y_GtGG_discard:focus-visible,.y_GtGG_save:focus-visible,.y_GtGG_update:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}";
|
|
2579
|
+
const tagId$1 = "@jcy2387/dsh-conversation-ui/ConversationCard.module.css";
|
|
2580
|
+
if (typeof document !== "undefined") {
|
|
2581
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]");
|
|
2582
|
+
if (tag === null) {
|
|
2583
|
+
tag = document.createElement("style");
|
|
2584
|
+
tag.dataset.plugin = "@jcy2387/dsh-conversation-ui";
|
|
2585
|
+
tag.dataset.pluginCss = tagId$1;
|
|
2586
|
+
document.head.appendChild(tag);
|
|
2587
|
+
}
|
|
2588
|
+
tag.textContent = css$1;
|
|
2589
|
+
}
|
|
2590
|
+
var ConversationCard_module_css_default = {
|
|
2591
|
+
"cardOpen": "y_GtGG_cardOpen",
|
|
2592
|
+
"card": "y_GtGG_card",
|
|
2593
|
+
"pending": "y_GtGG_pending",
|
|
2594
|
+
"readOnly": "y_GtGG_readOnly",
|
|
2595
|
+
"discard": "y_GtGG_discard",
|
|
2596
|
+
"body": "y_GtGG_body",
|
|
2597
|
+
"description": "y_GtGG_description",
|
|
2598
|
+
"label": "y_GtGG_label",
|
|
2599
|
+
"headText": "y_GtGG_headText",
|
|
2600
|
+
"fieldHead": "y_GtGG_fieldHead",
|
|
2601
|
+
"name": "y_GtGG_name",
|
|
2602
|
+
"hint": "y_GtGG_hint",
|
|
2603
|
+
"header": "y_GtGG_header",
|
|
2604
|
+
"failure": "y_GtGG_failure",
|
|
2605
|
+
"update": "y_GtGG_update",
|
|
2606
|
+
"failed": "y_GtGG_failed",
|
|
2607
|
+
"footer": "y_GtGG_footer",
|
|
2608
|
+
"version": "y_GtGG_version",
|
|
2609
|
+
"save": "y_GtGG_save",
|
|
2610
|
+
"chevronOpen": "y_GtGG_chevronOpen",
|
|
2611
|
+
"field": "y_GtGG_field",
|
|
2612
|
+
"toggle": "y_GtGG_toggle",
|
|
2613
|
+
"updateRow": "y_GtGG_updateRow",
|
|
2614
|
+
"updateCopy": "y_GtGG_updateCopy",
|
|
2615
|
+
"chevron": "y_GtGG_chevron"
|
|
2616
|
+
};
|
|
2617
|
+
//#endregion
|
|
2618
|
+
//#region src/client/ConversationCard.tsx
|
|
2619
|
+
/**
|
|
2620
|
+
* The conversation-ui plugin configuration card, rendered inside the Web
|
|
2621
|
+
* Settings "plugin configuration" page. One toggle, staged until the user
|
|
2622
|
+
* saves — the same shape as the Host-shipped cards, hand-drawn because the
|
|
2623
|
+
* Host cards' chrome is not exported for reuse.
|
|
2624
|
+
*/
|
|
2625
|
+
/** Render the conversation-ui card independently of the core settings namespace allowlist. */
|
|
2626
|
+
function ConversationCard(props) {
|
|
2627
|
+
const { t } = props;
|
|
2628
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2629
|
+
const state = props.useConversationUiCard((snapshot) => snapshot);
|
|
2630
|
+
const blocked = !state.dirty || state.saving || state.status !== "ready";
|
|
2631
|
+
const versionLabel = state.version === void 0 ? null : t(state.installation === "development" ? "developmentVersion" : "version").replace("{version}", state.version);
|
|
2632
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2633
|
+
className: open ? `${ConversationCard_module_css_default.card} ${ConversationCard_module_css_default.cardOpen}` : ConversationCard_module_css_default.card,
|
|
2634
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2635
|
+
type: "button",
|
|
2636
|
+
className: ConversationCard_module_css_default.header,
|
|
2637
|
+
"aria-expanded": open,
|
|
2638
|
+
onClick: () => {
|
|
2639
|
+
setOpen(!open);
|
|
2640
|
+
},
|
|
2641
|
+
children: [
|
|
2642
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2643
|
+
className: ConversationCard_module_css_default.headText,
|
|
2644
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2645
|
+
className: ConversationCard_module_css_default.name,
|
|
2646
|
+
children: t("title")
|
|
2647
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2648
|
+
className: ConversationCard_module_css_default.description,
|
|
2649
|
+
children: t("description")
|
|
2650
|
+
})]
|
|
2651
|
+
}),
|
|
2652
|
+
versionLabel === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2653
|
+
className: ConversationCard_module_css_default.version,
|
|
2654
|
+
children: versionLabel
|
|
2655
|
+
}),
|
|
2656
|
+
state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2657
|
+
className: ConversationCard_module_css_default.pending,
|
|
2658
|
+
children: t("unsaved")
|
|
2659
|
+
}) : null,
|
|
2660
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: open ? `${ConversationCard_module_css_default.chevron} ${ConversationCard_module_css_default.chevronOpen}` : ConversationCard_module_css_default.chevron })
|
|
2661
|
+
]
|
|
2662
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2663
|
+
className: ConversationCard_module_css_default.body,
|
|
2664
|
+
children: [
|
|
2665
|
+
state.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2666
|
+
className: ConversationCard_module_css_default.readOnly,
|
|
2667
|
+
role: "status",
|
|
2668
|
+
children: t("loading")
|
|
2669
|
+
}) : null,
|
|
2670
|
+
state.status === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2671
|
+
className: ConversationCard_module_css_default.failure,
|
|
2672
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2673
|
+
className: ConversationCard_module_css_default.readOnly,
|
|
2674
|
+
role: "status",
|
|
2675
|
+
children: t("unavailable")
|
|
2676
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2677
|
+
type: "button",
|
|
2678
|
+
className: ConversationCard_module_css_default.discard,
|
|
2679
|
+
onClick: props.reload,
|
|
2680
|
+
children: t("retry")
|
|
2681
|
+
})]
|
|
2682
|
+
}) : null,
|
|
2683
|
+
state.status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2684
|
+
!state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2685
|
+
className: ConversationCard_module_css_default.readOnly,
|
|
2686
|
+
role: "status",
|
|
2687
|
+
children: t("readOnly")
|
|
2688
|
+
}) : null,
|
|
2689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2690
|
+
className: ConversationCard_module_css_default.field,
|
|
2691
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2692
|
+
className: ConversationCard_module_css_default.fieldHead,
|
|
2693
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2694
|
+
className: ConversationCard_module_css_default.label,
|
|
2695
|
+
children: t("thinkAutoExpand")
|
|
2696
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2697
|
+
type: "checkbox",
|
|
2698
|
+
className: ConversationCard_module_css_default.toggle,
|
|
2699
|
+
checked: state.thinkAutoExpand,
|
|
2700
|
+
disabled: !state.writable,
|
|
2701
|
+
onChange: (event) => {
|
|
2702
|
+
props.edit(event.target.checked);
|
|
2703
|
+
}
|
|
2704
|
+
})]
|
|
2705
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2706
|
+
className: ConversationCard_module_css_default.hint,
|
|
2707
|
+
children: t("thinkAutoExpandHint")
|
|
2708
|
+
})]
|
|
2709
|
+
}),
|
|
2710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2711
|
+
className: ConversationCard_module_css_default.updateRow,
|
|
2712
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2713
|
+
className: ConversationCard_module_css_default.updateCopy,
|
|
2714
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2715
|
+
className: ConversationCard_module_css_default.label,
|
|
2716
|
+
children: t("updates")
|
|
2717
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2718
|
+
className: ConversationCard_module_css_default.hint,
|
|
2719
|
+
children: state.restartRequired ? t("restartRequired") : state.installation === "npm" ? t("updateHint") : state.installation === "development" ? t("developmentBuild") : t("updateUnavailable")
|
|
2720
|
+
})]
|
|
2721
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2722
|
+
type: "button",
|
|
2723
|
+
className: ConversationCard_module_css_default.update,
|
|
2724
|
+
disabled: !state.canUpgrade || state.upgrading || state.restartRequired,
|
|
2725
|
+
title: state.canUpgrade ? void 0 : t("updateUnavailable"),
|
|
2726
|
+
onClick: props.upgrade,
|
|
2727
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2728
|
+
"aria-hidden": "true",
|
|
2729
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline14, {})
|
|
2730
|
+
}), t(state.upgrading ? "updating" : "update")]
|
|
2731
|
+
})]
|
|
2732
|
+
}),
|
|
2733
|
+
state.upgradeFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2734
|
+
className: ConversationCard_module_css_default.failed,
|
|
2735
|
+
role: "status",
|
|
2736
|
+
children: t("updateFailed")
|
|
2737
|
+
}) : null,
|
|
2738
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2739
|
+
className: ConversationCard_module_css_default.footer,
|
|
2740
|
+
children: [
|
|
2741
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2742
|
+
className: ConversationCard_module_css_default.failed,
|
|
2743
|
+
role: "status",
|
|
2744
|
+
children: t("saveFailed")
|
|
2745
|
+
}) : null,
|
|
2746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2747
|
+
type: "button",
|
|
2748
|
+
className: ConversationCard_module_css_default.discard,
|
|
2749
|
+
disabled: !state.dirty || state.saving,
|
|
2750
|
+
onClick: props.discard,
|
|
2751
|
+
children: t("discard")
|
|
2752
|
+
}),
|
|
2753
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2754
|
+
type: "button",
|
|
2755
|
+
className: ConversationCard_module_css_default.save,
|
|
2756
|
+
disabled: blocked,
|
|
2757
|
+
onClick: props.save,
|
|
2758
|
+
children: t(state.saving ? "saving" : "save")
|
|
2759
|
+
})
|
|
2760
|
+
]
|
|
2761
|
+
})
|
|
2762
|
+
] }) : null
|
|
2763
|
+
]
|
|
2764
|
+
}) : null]
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
//#endregion
|
|
2768
|
+
//#region src/settings.ts
|
|
2769
|
+
/** Defaults shared by the Host schema and the client-side fallback. */
|
|
2770
|
+
const DEFAULT_CONVERSATION_SETTINGS = { thinkAutoExpand: true };
|
|
2771
|
+
//#endregion
|
|
2772
|
+
//#region src/client/conversation-ui-card-controller.ts
|
|
2773
|
+
/** Staged form state for the plugin-owned conversation-ui settings RPC. */
|
|
2774
|
+
/** Bridges the plugin's protected Host interface onto a staged settings form. */
|
|
2775
|
+
var ConversationCardController = class {
|
|
2776
|
+
api;
|
|
2777
|
+
store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(this.projection());
|
|
2778
|
+
loaded;
|
|
2779
|
+
staged;
|
|
2780
|
+
saving = false;
|
|
2781
|
+
failed = false;
|
|
2782
|
+
upgrading = false;
|
|
2783
|
+
upgradeFailed = false;
|
|
2784
|
+
restartRequired = false;
|
|
2785
|
+
loadGeneration = 0;
|
|
2786
|
+
loadStatus = "loading";
|
|
2787
|
+
constructor(api) {
|
|
2788
|
+
this.api = api;
|
|
2789
|
+
}
|
|
2790
|
+
/** Begin the background read after the card has been registered. */
|
|
2791
|
+
start() {
|
|
2792
|
+
this.load();
|
|
2793
|
+
}
|
|
2794
|
+
/** Ignore a late response after the surrounding optional services unload. */
|
|
2795
|
+
stop() {
|
|
2796
|
+
this.loadGeneration += 1;
|
|
2797
|
+
}
|
|
2798
|
+
/** Current card snapshot, also consumed by the streaming preference cell. */
|
|
2799
|
+
getSnapshot() {
|
|
2800
|
+
return this.store.getSnapshot();
|
|
2801
|
+
}
|
|
2802
|
+
/** Subscribe to state changes. */
|
|
2803
|
+
subscribe(listener) {
|
|
2804
|
+
return this.store.subscribe(listener);
|
|
2805
|
+
}
|
|
2806
|
+
/** Build the face consumed by the settings slot renderer. */
|
|
2807
|
+
inject() {
|
|
2808
|
+
return {
|
|
2809
|
+
hooks: { conversationUiCard: this.store },
|
|
2810
|
+
edit: (value) => {
|
|
2811
|
+
this.staged = value;
|
|
2812
|
+
this.failed = false;
|
|
2813
|
+
this.publish();
|
|
2814
|
+
},
|
|
2815
|
+
save: () => {
|
|
2816
|
+
this.save();
|
|
2817
|
+
},
|
|
2818
|
+
discard: () => {
|
|
2819
|
+
if (this.staged === void 0 && !this.failed) return;
|
|
2820
|
+
this.staged = void 0;
|
|
2821
|
+
this.failed = false;
|
|
2822
|
+
this.publish();
|
|
2823
|
+
},
|
|
2824
|
+
reload: () => {
|
|
2825
|
+
this.load();
|
|
2826
|
+
},
|
|
2827
|
+
upgrade: () => {
|
|
2828
|
+
this.upgrade();
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
projection() {
|
|
2833
|
+
return {
|
|
2834
|
+
status: this.loadStatus,
|
|
2835
|
+
writable: this.loaded?.writable ?? false,
|
|
2836
|
+
dirty: this.staged !== void 0,
|
|
2837
|
+
saving: this.saving,
|
|
2838
|
+
failed: this.failed,
|
|
2839
|
+
thinkAutoExpand: this.staged ?? this.loaded?.thinkAutoExpand ?? DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand,
|
|
2840
|
+
version: this.loaded?.version,
|
|
2841
|
+
installation: this.loaded?.installation ?? "unmanaged",
|
|
2842
|
+
canUpgrade: this.loaded?.canUpgrade ?? false,
|
|
2843
|
+
upgrading: this.upgrading,
|
|
2844
|
+
upgradeFailed: this.upgradeFailed,
|
|
2845
|
+
restartRequired: this.restartRequired
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
async load() {
|
|
2849
|
+
const generation = ++this.loadGeneration;
|
|
2850
|
+
this.loadStatus = "loading";
|
|
2851
|
+
this.loaded = void 0;
|
|
2852
|
+
this.publish();
|
|
2853
|
+
try {
|
|
2854
|
+
const view = await this.api.read();
|
|
2855
|
+
if (generation !== this.loadGeneration) return;
|
|
2856
|
+
this.loaded = view;
|
|
2857
|
+
this.loadStatus = "ready";
|
|
2858
|
+
} catch {
|
|
2859
|
+
if (generation !== this.loadGeneration) return;
|
|
2860
|
+
this.loaded = void 0;
|
|
2861
|
+
this.loadStatus = "unavailable";
|
|
2862
|
+
}
|
|
2863
|
+
this.publish();
|
|
2864
|
+
}
|
|
2865
|
+
async save() {
|
|
2866
|
+
if (this.staged === void 0 || this.saving || this.loaded?.writable !== true) return;
|
|
2867
|
+
const value = this.staged;
|
|
2868
|
+
this.saving = true;
|
|
2869
|
+
this.failed = false;
|
|
2870
|
+
this.publish();
|
|
2871
|
+
try {
|
|
2872
|
+
this.loaded = await this.api.write(value);
|
|
2873
|
+
this.staged = void 0;
|
|
2874
|
+
} catch {
|
|
2875
|
+
this.failed = true;
|
|
2876
|
+
}
|
|
2877
|
+
this.saving = false;
|
|
2878
|
+
this.publish();
|
|
2879
|
+
}
|
|
2880
|
+
async upgrade() {
|
|
2881
|
+
if (this.loaded?.canUpgrade !== true || this.upgrading) return;
|
|
2882
|
+
this.upgrading = true;
|
|
2883
|
+
this.upgradeFailed = false;
|
|
2884
|
+
this.restartRequired = false;
|
|
2885
|
+
this.publish();
|
|
2886
|
+
try {
|
|
2887
|
+
const result = await this.api.upgrade();
|
|
2888
|
+
this.restartRequired = result.restartRequired;
|
|
2889
|
+
} catch {
|
|
2890
|
+
this.upgradeFailed = true;
|
|
2891
|
+
}
|
|
2892
|
+
this.upgrading = false;
|
|
2893
|
+
this.publish();
|
|
2894
|
+
}
|
|
2895
|
+
publish() {
|
|
2896
|
+
this.store.set(this.projection());
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
//#endregion
|
|
2900
|
+
//#region \0dsh-css:/Users/baojie/Documents/Projects/github/dsh-codex-provider-plugin/packages/conversation-ui/src/client/DeliverablesCard.module.css.mjs
|
|
2901
|
+
const css = "[data-turn-tail]:has([data-stream-deliverables-card])>:has([data-produced-files-row]){display:none!important}._9Fvu4a_root{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-primary);color:var(--dsw-alias-label-secondary);box-sizing:border-box;border-radius:12px;flex-direction:column;gap:0;margin-top:16px;padding:12px 12px 10px;font-size:14px;line-height:22px;display:flex}._9Fvu4a_header{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:flex-start;gap:12px;min-width:0;padding:4px 8px 12px;display:flex}._9Fvu4a_headerIcon{background:var(--dsw-alias-interactive-bg-hover);width:40px;height:40px;color:var(--dsw-alias-label-secondary);border-radius:10px;flex:none;justify-content:center;align-items:center;display:inline-flex}._9Fvu4a_heading{flex-direction:column;gap:1px;min-width:0;display:flex}._9Fvu4a_title{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:24px}._9Fvu4a_subtitle{color:var(--dsw-alias-label-tertiary);font:inherit;text-align:start;cursor:pointer;background:0 0;border:0;margin:0;padding:0;font-size:14px;line-height:20px}._9Fvu4a_subtitle:hover{color:var(--dsw-alias-label-secondary);text-decoration:underline}._9Fvu4a_list{flex-direction:column;min-width:0;padding:10px 8px 0;display:flex}._9Fvu4a_item{width:100%;min-width:0;min-height:28px;color:inherit;font:inherit;text-align:start;cursor:pointer;background:0 0;border:0;border-radius:5px;align-items:center;gap:0;margin:0;padding:2px 0;line-height:22px;display:flex}._9Fvu4a_item:hover{color:var(--dsw-alias-label-primary)}._9Fvu4a_item:focus-visible,._9Fvu4a_more:focus-visible,._9Fvu4a_subtitle:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}._9Fvu4a_filePath,._9Fvu4a_websitePath{white-space:nowrap;flex:auto;min-width:0;display:flex;overflow:hidden}._9Fvu4a_parentPath{min-width:0;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}._9Fvu4a_fileName{color:var(--dsw-alias-label-secondary);flex:none}._9Fvu4a_websitePath{color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;overflow:hidden}._9Fvu4a_stats{white-space:nowrap;flex:none;gap:6px;margin-inline-start:8px;font-size:13px;line-height:22px;display:inline-flex}._9Fvu4a_added{color:var(--dsw-alias-label-success,#2f9e44)}._9Fvu4a_removed{color:var(--dsw-alias-label-danger,#d94841)}._9Fvu4a_external{color:var(--dsw-alias-label-tertiary);flex:none}._9Fvu4a_more{color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;align-self:flex-start;align-items:center;gap:5px;margin:8px 8px 0;padding:0;line-height:22px;display:inline-flex}._9Fvu4a_more:hover{color:var(--dsw-alias-label-primary)}._9Fvu4a_moreIcon{flex:none;justify-content:center;align-items:center;width:14px;height:14px;display:inline-flex}";
|
|
2902
|
+
const tagId = "@jcy2387/dsh-conversation-ui/DeliverablesCard.module.css";
|
|
2903
|
+
if (typeof document !== "undefined") {
|
|
2904
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
2905
|
+
if (tag === null) {
|
|
2906
|
+
tag = document.createElement("style");
|
|
2907
|
+
tag.dataset.plugin = "@jcy2387/dsh-conversation-ui";
|
|
2908
|
+
tag.dataset.pluginCss = tagId;
|
|
2909
|
+
document.head.appendChild(tag);
|
|
2910
|
+
}
|
|
2911
|
+
tag.textContent = css;
|
|
2912
|
+
}
|
|
2913
|
+
var DeliverablesCard_module_css_default = {
|
|
2914
|
+
"more": "_9Fvu4a_more",
|
|
2915
|
+
"fileName": "_9Fvu4a_fileName",
|
|
2916
|
+
"root": "_9Fvu4a_root",
|
|
2917
|
+
"heading": "_9Fvu4a_heading",
|
|
2918
|
+
"subtitle": "_9Fvu4a_subtitle",
|
|
2919
|
+
"removed": "_9Fvu4a_removed",
|
|
2920
|
+
"parentPath": "_9Fvu4a_parentPath",
|
|
2921
|
+
"stats": "_9Fvu4a_stats",
|
|
2922
|
+
"item": "_9Fvu4a_item",
|
|
2923
|
+
"list": "_9Fvu4a_list",
|
|
2924
|
+
"added": "_9Fvu4a_added",
|
|
2925
|
+
"external": "_9Fvu4a_external",
|
|
2926
|
+
"filePath": "_9Fvu4a_filePath",
|
|
2927
|
+
"header": "_9Fvu4a_header",
|
|
2928
|
+
"moreIcon": "_9Fvu4a_moreIcon",
|
|
2929
|
+
"headerIcon": "_9Fvu4a_headerIcon",
|
|
2930
|
+
"websitePath": "_9Fvu4a_websitePath",
|
|
2931
|
+
"title": "_9Fvu4a_title"
|
|
2932
|
+
};
|
|
2933
|
+
//#endregion
|
|
2934
|
+
//#region src/client/DeliverablesCard.tsx
|
|
2935
|
+
function isWebsite(entry) {
|
|
2936
|
+
return entry.kind === "website" || /^https?:\/\//i.test(entry.path);
|
|
2937
|
+
}
|
|
2938
|
+
function openWebsite(path) {
|
|
2939
|
+
if (typeof window !== "undefined") window.open(path, "_blank", "noopener,noreferrer");
|
|
2940
|
+
}
|
|
2941
|
+
function pathParts(path) {
|
|
2942
|
+
const separator = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
2943
|
+
if (separator < 0) return {
|
|
2944
|
+
parent: "",
|
|
2945
|
+
name: path
|
|
2946
|
+
};
|
|
2947
|
+
return {
|
|
2948
|
+
parent: path.slice(0, separator + 1),
|
|
2949
|
+
name: path.slice(separator + 1)
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
function hiddenKindLabel(entries) {
|
|
2953
|
+
if (entries.every(isWebsite)) return "网站";
|
|
2954
|
+
if (entries.every((entry) => !isWebsite(entry))) return "文件";
|
|
2955
|
+
return "产物";
|
|
2956
|
+
}
|
|
2957
|
+
function countLabel(entries) {
|
|
2958
|
+
const files = entries.filter((entry) => !isWebsite(entry)).length;
|
|
2959
|
+
const websites = entries.length - files;
|
|
2960
|
+
if (files > 0 && websites === 0) return `已编辑 ${files} 个文件`;
|
|
2961
|
+
if (files === 0) return `交付 ${websites} 个网站`;
|
|
2962
|
+
return `已交付 ${entries.length} 项产物`;
|
|
2963
|
+
}
|
|
2964
|
+
function collectEditStats(turn) {
|
|
2965
|
+
const result = /* @__PURE__ */ new Map();
|
|
2966
|
+
if (typeof document === "undefined") return result;
|
|
2967
|
+
for (const element of document.querySelectorAll("[data-stream-edit-stats]")) {
|
|
2968
|
+
if (element.closest("[data-stream-turn]")?.getAttribute("data-stream-turn") !== String(turn)) continue;
|
|
2969
|
+
const path = element.getAttribute("data-stream-edit-file");
|
|
2970
|
+
const match = /\+(\d+)\s+-\s*(\d+)/.exec(element.textContent ?? "");
|
|
2971
|
+
if (path === null || match === null) continue;
|
|
2972
|
+
result.set(path, {
|
|
2973
|
+
added: Number(match[1]),
|
|
2974
|
+
removed: Number(match[2])
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
return result;
|
|
2978
|
+
}
|
|
2979
|
+
/** Codex-style turn-tail card for edited files and deployed websites. */
|
|
2980
|
+
function DeliverablesCard({ matched, turn, openFile }) {
|
|
2981
|
+
const [expanded, setExpanded] = (0, react.useState)(false);
|
|
2982
|
+
const [domStats, setDomStats] = (0, react.useState)(() => /* @__PURE__ */ new Map());
|
|
2983
|
+
(0, react.useLayoutEffect)(() => {
|
|
2984
|
+
setDomStats(collectEditStats(turn.turn));
|
|
2985
|
+
}, [turn.turn, matched]);
|
|
2986
|
+
const entries = matched.map((entry) => {
|
|
2987
|
+
const stats = domStats.get(entry.path);
|
|
2988
|
+
return stats === void 0 || entry.added > 0 || entry.removed > 0 ? entry : {
|
|
2989
|
+
...entry,
|
|
2990
|
+
...stats
|
|
2991
|
+
};
|
|
2992
|
+
});
|
|
2993
|
+
const visible = expanded ? entries : entries.slice(0, 3);
|
|
2994
|
+
const hidden = entries.length - visible.length;
|
|
2995
|
+
const summary = countLabel(entries);
|
|
2996
|
+
const hasFiles = entries.some((entry) => !isWebsite(entry));
|
|
2997
|
+
const firstWebsite = entries.find(isWebsite);
|
|
2998
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2999
|
+
className: DeliverablesCard_module_css_default.root,
|
|
3000
|
+
"data-stream-deliverables-card": true,
|
|
3001
|
+
"data-stream-deliverables-count": entries.length,
|
|
3002
|
+
children: [
|
|
3003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3004
|
+
className: DeliverablesCard_module_css_default.header,
|
|
3005
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3006
|
+
className: DeliverablesCard_module_css_default.headerIcon,
|
|
3007
|
+
"aria-hidden": true,
|
|
3008
|
+
children: hasFiles ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconProjectAddOutline16, { size: 20 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGlobeOutline14, { size: 20 })
|
|
3009
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3010
|
+
className: DeliverablesCard_module_css_default.heading,
|
|
3011
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
3012
|
+
className: DeliverablesCard_module_css_default.title,
|
|
3013
|
+
children: summary
|
|
3014
|
+
}), hasFiles ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3015
|
+
type: "button",
|
|
3016
|
+
className: DeliverablesCard_module_css_default.subtitle,
|
|
3017
|
+
onClick: () => {
|
|
3018
|
+
setExpanded(true);
|
|
3019
|
+
},
|
|
3020
|
+
children: "查看更改 ↗"
|
|
3021
|
+
}) : firstWebsite !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3022
|
+
type: "button",
|
|
3023
|
+
className: DeliverablesCard_module_css_default.subtitle,
|
|
3024
|
+
onClick: () => {
|
|
3025
|
+
openWebsite(firstWebsite.path);
|
|
3026
|
+
},
|
|
3027
|
+
children: "打开网站 ↗"
|
|
3028
|
+
}) : null]
|
|
3029
|
+
})]
|
|
3030
|
+
}),
|
|
3031
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3032
|
+
className: DeliverablesCard_module_css_default.list,
|
|
3033
|
+
children: visible.map((entry) => {
|
|
3034
|
+
const website = isWebsite(entry);
|
|
3035
|
+
const parts = pathParts(entry.path);
|
|
3036
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3037
|
+
type: "button",
|
|
3038
|
+
className: DeliverablesCard_module_css_default.item,
|
|
3039
|
+
title: entry.path,
|
|
3040
|
+
onClick: () => {
|
|
3041
|
+
website ? openWebsite(entry.path) : openFile(entry.path);
|
|
3042
|
+
},
|
|
3043
|
+
children: [
|
|
3044
|
+
website ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3045
|
+
className: DeliverablesCard_module_css_default.websitePath,
|
|
3046
|
+
children: entry.path
|
|
3047
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3048
|
+
className: DeliverablesCard_module_css_default.filePath,
|
|
3049
|
+
children: [parts.parent !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3050
|
+
className: DeliverablesCard_module_css_default.parentPath,
|
|
3051
|
+
children: parts.parent
|
|
3052
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3053
|
+
className: DeliverablesCard_module_css_default.fileName,
|
|
3054
|
+
children: parts.name || entry.path
|
|
3055
|
+
})]
|
|
3056
|
+
}),
|
|
3057
|
+
!website && (entry.added > 0 || entry.removed > 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3058
|
+
className: DeliverablesCard_module_css_default.stats,
|
|
3059
|
+
children: [entry.added > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3060
|
+
className: DeliverablesCard_module_css_default.added,
|
|
3061
|
+
children: ["+", entry.added]
|
|
3062
|
+
}), entry.removed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3063
|
+
className: DeliverablesCard_module_css_default.removed,
|
|
3064
|
+
children: ["-", entry.removed]
|
|
3065
|
+
})]
|
|
3066
|
+
}),
|
|
3067
|
+
website && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3068
|
+
className: DeliverablesCard_module_css_default.external,
|
|
3069
|
+
"aria-hidden": true,
|
|
3070
|
+
children: "↗"
|
|
3071
|
+
})
|
|
3072
|
+
]
|
|
3073
|
+
}, `${entry.kind}:${entry.path}`);
|
|
3074
|
+
})
|
|
3075
|
+
}),
|
|
3076
|
+
hidden > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3077
|
+
type: "button",
|
|
3078
|
+
className: DeliverablesCard_module_css_default.more,
|
|
3079
|
+
onClick: () => {
|
|
3080
|
+
setExpanded(true);
|
|
3081
|
+
},
|
|
3082
|
+
children: [
|
|
3083
|
+
"再显示 ",
|
|
3084
|
+
hidden,
|
|
3085
|
+
" 个",
|
|
3086
|
+
hiddenKindLabel(entries.slice(visible.length)),
|
|
3087
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3088
|
+
className: DeliverablesCard_module_css_default.moreIcon,
|
|
3089
|
+
"aria-hidden": true,
|
|
3090
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
3091
|
+
})
|
|
3092
|
+
]
|
|
3093
|
+
}),
|
|
3094
|
+
expanded && entries.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3095
|
+
type: "button",
|
|
3096
|
+
className: DeliverablesCard_module_css_default.more,
|
|
3097
|
+
onClick: () => {
|
|
3098
|
+
setExpanded(false);
|
|
3099
|
+
},
|
|
3100
|
+
children: ["收起 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3101
|
+
className: DeliverablesCard_module_css_default.moreIcon,
|
|
3102
|
+
"aria-hidden": true,
|
|
3103
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, {})
|
|
3104
|
+
})]
|
|
3105
|
+
})
|
|
3106
|
+
]
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
//#endregion
|
|
3110
|
+
//#region src/client/deliverables.ts
|
|
3111
|
+
const DELIVERABLES_DATA_KEY = "dsh-conversation-ui-deliverables";
|
|
3112
|
+
function record$1(value) {
|
|
3113
|
+
return value !== null && typeof value === "object" ? value : void 0;
|
|
3114
|
+
}
|
|
3115
|
+
function stringValue(value) {
|
|
3116
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
3117
|
+
}
|
|
3118
|
+
function diffLines(value) {
|
|
3119
|
+
const text = stringValue(value);
|
|
3120
|
+
if (text === void 0) return 0;
|
|
3121
|
+
const body = text.endsWith("\n") ? text.slice(0, -1) : text.endsWith("\\n") ? text.slice(0, -2) : text;
|
|
3122
|
+
return body === "" ? 0 : body.split(/\r?\n|\\n/).length;
|
|
3123
|
+
}
|
|
3124
|
+
function jsonArgs(argsRaw) {
|
|
3125
|
+
if (argsRaw === void 0) return {};
|
|
3126
|
+
try {
|
|
3127
|
+
return record$1(JSON.parse(argsRaw)) ?? {};
|
|
3128
|
+
} catch {
|
|
3129
|
+
return {};
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
function viewEntries(view) {
|
|
3133
|
+
const object = record$1(view);
|
|
3134
|
+
if (object === void 0) return [];
|
|
3135
|
+
const card = object.card;
|
|
3136
|
+
const locationPaths = (Array.isArray(object.locations) ? object.locations : []).flatMap((location) => {
|
|
3137
|
+
const path = stringValue(record$1(location)?.path);
|
|
3138
|
+
return path === void 0 ? [] : [path];
|
|
3139
|
+
});
|
|
3140
|
+
if (card === "diff") {
|
|
3141
|
+
const diffs = Array.isArray(object.diffs) ? object.diffs : [];
|
|
3142
|
+
if (diffs.length === 0) return locationPaths.map((path) => ({
|
|
3143
|
+
path,
|
|
3144
|
+
added: 0,
|
|
3145
|
+
removed: 0,
|
|
3146
|
+
kind: "file"
|
|
3147
|
+
}));
|
|
3148
|
+
return diffs.flatMap((diff) => {
|
|
3149
|
+
const item = record$1(diff);
|
|
3150
|
+
const path = stringValue(item?.path);
|
|
3151
|
+
if (path === void 0) return [];
|
|
3152
|
+
return [{
|
|
3153
|
+
path,
|
|
3154
|
+
added: diffLines(item?.newText),
|
|
3155
|
+
removed: diffLines(item?.oldText),
|
|
3156
|
+
kind: "file"
|
|
3157
|
+
}];
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
if (card === "generic" && object.kind === "edit") return locationPaths.map((path) => ({
|
|
3161
|
+
path,
|
|
3162
|
+
added: 0,
|
|
3163
|
+
removed: 0,
|
|
3164
|
+
kind: "file"
|
|
3165
|
+
}));
|
|
3166
|
+
const url = stringValue(object.url) ?? stringValue(object.href);
|
|
3167
|
+
return url === void 0 || !/^https?:\/\//i.test(url) ? [] : [{
|
|
3168
|
+
path: url,
|
|
3169
|
+
added: 0,
|
|
3170
|
+
removed: 0,
|
|
3171
|
+
kind: "website"
|
|
3172
|
+
}];
|
|
3173
|
+
}
|
|
3174
|
+
function mutationPath(name, argsRaw) {
|
|
3175
|
+
const args = jsonArgs(argsRaw);
|
|
3176
|
+
const path = stringValue(args.file_path) ?? stringValue(args.path) ?? stringValue(args.filename);
|
|
3177
|
+
if (path === void 0 || /^https?:\/\//i.test(path)) return void 0;
|
|
3178
|
+
return name.toLowerCase().split(/[^a-z\d]+/).filter(Boolean).some((token) => [
|
|
3179
|
+
"edit",
|
|
3180
|
+
"write",
|
|
3181
|
+
"patch",
|
|
3182
|
+
"replace",
|
|
3183
|
+
"create",
|
|
3184
|
+
"insert",
|
|
3185
|
+
"append"
|
|
3186
|
+
].includes(token)) ? path : void 0;
|
|
3187
|
+
}
|
|
3188
|
+
function websiteTool(name) {
|
|
3189
|
+
return name.toLowerCase().split(/[^a-z\d]+/).filter(Boolean).some((token) => [
|
|
3190
|
+
"deploy",
|
|
3191
|
+
"publish",
|
|
3192
|
+
"preview",
|
|
3193
|
+
"website",
|
|
3194
|
+
"site",
|
|
3195
|
+
"artifact"
|
|
3196
|
+
].includes(token));
|
|
3197
|
+
}
|
|
3198
|
+
function resultWebsiteEntries(name, value) {
|
|
3199
|
+
if (!websiteTool(name)) return [];
|
|
3200
|
+
let text = "";
|
|
3201
|
+
try {
|
|
3202
|
+
text = JSON.stringify(value) ?? "";
|
|
3203
|
+
} catch {
|
|
3204
|
+
return [];
|
|
3205
|
+
}
|
|
3206
|
+
const urls = text.match(/https?:\/\/[^\s"'<>\\]+/gi) ?? [];
|
|
3207
|
+
return [...new Set(urls.map((url) => url.replace(/[),.;]+$/g, "")))].map((path) => ({
|
|
3208
|
+
path,
|
|
3209
|
+
added: 0,
|
|
3210
|
+
removed: 0,
|
|
3211
|
+
kind: "website"
|
|
3212
|
+
}));
|
|
3213
|
+
}
|
|
3214
|
+
function websiteUrl(name, argsRaw) {
|
|
3215
|
+
const args = jsonArgs(argsRaw);
|
|
3216
|
+
const candidate = stringValue(args.url) ?? stringValue(args.href) ?? stringValue(args.preview_url) ?? stringValue(args.website_url);
|
|
3217
|
+
if (candidate === void 0 || !/^https?:\/\//i.test(candidate)) return void 0;
|
|
3218
|
+
return name.toLowerCase().split(/[^a-z\d]+/).filter(Boolean).some((token) => [
|
|
3219
|
+
"deploy",
|
|
3220
|
+
"publish",
|
|
3221
|
+
"preview",
|
|
3222
|
+
"website",
|
|
3223
|
+
"site",
|
|
3224
|
+
"artifact"
|
|
3225
|
+
].includes(token)) ? candidate : void 0;
|
|
3226
|
+
}
|
|
3227
|
+
function appendEntries(current, next, seq) {
|
|
3228
|
+
const entries = [...current];
|
|
3229
|
+
for (const candidate of next) {
|
|
3230
|
+
const existing = entries.findIndex((entry) => entry.path === candidate.path && entry.kind === candidate.kind);
|
|
3231
|
+
if (existing === -1) {
|
|
3232
|
+
entries.push({
|
|
3233
|
+
...candidate,
|
|
3234
|
+
seq
|
|
3235
|
+
});
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
const previous = entries[existing];
|
|
3239
|
+
if (previous === void 0) continue;
|
|
3240
|
+
entries[existing] = {
|
|
3241
|
+
...previous,
|
|
3242
|
+
seq,
|
|
3243
|
+
added: previous.added + candidate.added,
|
|
3244
|
+
removed: previous.removed + candidate.removed
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
return entries;
|
|
3248
|
+
}
|
|
3249
|
+
function entriesForCall(call) {
|
|
3250
|
+
const fromView = viewEntries(call.view);
|
|
3251
|
+
if (fromView.length > 0) return fromView;
|
|
3252
|
+
const path = mutationPath(call.name, call.argsRaw);
|
|
3253
|
+
if (path !== void 0) {
|
|
3254
|
+
const args = jsonArgs(call.argsRaw);
|
|
3255
|
+
return [{
|
|
3256
|
+
path,
|
|
3257
|
+
added: diffLines(args.new_string ?? args.new_text ?? args.content),
|
|
3258
|
+
removed: diffLines(args.old_string ?? args.old_text),
|
|
3259
|
+
kind: "file"
|
|
3260
|
+
}];
|
|
3261
|
+
}
|
|
3262
|
+
const url = websiteUrl(call.name, call.argsRaw);
|
|
3263
|
+
return url === void 0 ? [] : [{
|
|
3264
|
+
path: url,
|
|
3265
|
+
added: 0,
|
|
3266
|
+
removed: 0,
|
|
3267
|
+
kind: "website"
|
|
3268
|
+
}];
|
|
3269
|
+
}
|
|
3270
|
+
/** Conversation data used by the Codex-style delivery card. */
|
|
3271
|
+
const deliverablesDefinition = {
|
|
3272
|
+
kind: DELIVERABLES_DATA_KEY,
|
|
3273
|
+
match: (event) => {
|
|
3274
|
+
if (event.type === "turn/start") return {
|
|
3275
|
+
id: String(event.data.turn),
|
|
3276
|
+
role: "start"
|
|
3277
|
+
};
|
|
3278
|
+
if (event.type === "tool/call") return {
|
|
3279
|
+
id: String(event.data.turn),
|
|
3280
|
+
role: "update"
|
|
3281
|
+
};
|
|
3282
|
+
if (event.type === "tool/result" && (0, _deepseek_ai_dsh_client_runtime_client.isAppendSurfaceEvent)(event)) return {
|
|
3283
|
+
id: String(event.data.turn),
|
|
3284
|
+
role: "update"
|
|
3285
|
+
};
|
|
3286
|
+
return null;
|
|
3287
|
+
},
|
|
3288
|
+
start: (_context, match) => {
|
|
3289
|
+
if (match.event.type !== "turn/start") throw new Error("deliverables start requires turn/start");
|
|
3290
|
+
return {
|
|
3291
|
+
turn: match.event.data.turn,
|
|
3292
|
+
calls: /* @__PURE__ */ new Map(),
|
|
3293
|
+
entries: []
|
|
3294
|
+
};
|
|
3295
|
+
},
|
|
3296
|
+
update: (context, match) => {
|
|
3297
|
+
if (match.event.type === "tool/call") {
|
|
3298
|
+
const calls = new Map(context.state.calls);
|
|
3299
|
+
const view = match.view?.for === "call" ? match.view.view : null;
|
|
3300
|
+
const name = stringValue(match.event.data.name) ?? "";
|
|
3301
|
+
const argsRaw = stringValue(match.event.data.arguments);
|
|
3302
|
+
calls.set(String(match.event.data.callId), {
|
|
3303
|
+
name,
|
|
3304
|
+
argsRaw,
|
|
3305
|
+
view
|
|
3306
|
+
});
|
|
3307
|
+
return {
|
|
3308
|
+
...context.state,
|
|
3309
|
+
calls
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
if (match.event.type !== "tool/result") return context.state;
|
|
3313
|
+
if (match.event.data.message.content[0]?.isError === true) return context.state;
|
|
3314
|
+
const callId = String(match.event.data.message.source.callId);
|
|
3315
|
+
const call = context.state.calls.get(callId);
|
|
3316
|
+
if (call === void 0) return context.state;
|
|
3317
|
+
const resultView = match.view?.for === "result" ? match.view.view : call.view;
|
|
3318
|
+
const fromView = viewEntries(resultView);
|
|
3319
|
+
const fromCall = fromView.length > 0 ? fromView : entriesForCall({
|
|
3320
|
+
...call,
|
|
3321
|
+
view: resultView
|
|
3322
|
+
});
|
|
3323
|
+
const fromResult = resultWebsiteEntries(call.name, match.event.data.message);
|
|
3324
|
+
const entries = appendEntries(context.state.entries, [...fromCall, ...fromResult], match.event.seq);
|
|
3325
|
+
return entries.length === context.state.entries.length && entries.every((entry, index) => {
|
|
3326
|
+
const previous = context.state.entries[index];
|
|
3327
|
+
return previous?.path === entry.path && previous.seq === entry.seq && previous.added === entry.added && previous.removed === entry.removed && previous.kind === entry.kind;
|
|
3328
|
+
}) ? context.state : {
|
|
3329
|
+
...context.state,
|
|
3330
|
+
entries
|
|
3331
|
+
};
|
|
3332
|
+
},
|
|
3333
|
+
buildLocationData: (context, scope) => scope !== "turn" || context.state === void 0 || context.state.entries.length === 0 ? null : {
|
|
3334
|
+
kind: "turn",
|
|
3335
|
+
turn: context.state.turn,
|
|
3336
|
+
key: DELIVERABLES_DATA_KEY,
|
|
3337
|
+
value: { entries: context.state.entries }
|
|
3338
|
+
}
|
|
3339
|
+
};
|
|
3340
|
+
function nativeProducedEntries(owner) {
|
|
3341
|
+
const produced = record$1(owner.turn.data.get("deliverables"))?.produced;
|
|
3342
|
+
if (!Array.isArray(produced)) return [];
|
|
3343
|
+
const entries = [];
|
|
3344
|
+
for (const item of produced) {
|
|
3345
|
+
const value = record$1(item);
|
|
3346
|
+
const path = stringValue(value?.path);
|
|
3347
|
+
const seq = typeof value?.seq === "number" ? value.seq : Number.POSITIVE_INFINITY;
|
|
3348
|
+
if (path === void 0 || seq > owner.seq || entries.some((entry) => entry.path === path)) continue;
|
|
3349
|
+
entries.push({
|
|
3350
|
+
path,
|
|
3351
|
+
seq,
|
|
3352
|
+
added: 0,
|
|
3353
|
+
removed: 0,
|
|
3354
|
+
kind: "file"
|
|
3355
|
+
});
|
|
3356
|
+
}
|
|
3357
|
+
return entries;
|
|
3358
|
+
}
|
|
3359
|
+
/** Select entries that were delivered before the closing assistant message. */
|
|
3360
|
+
function selectDeliverables(owner) {
|
|
3361
|
+
const value = owner.turn.data.get(DELIVERABLES_DATA_KEY);
|
|
3362
|
+
if (value !== void 0) {
|
|
3363
|
+
const entries = value.entries.filter((entry) => entry.seq <= owner.seq);
|
|
3364
|
+
return entries.length === 0 ? null : entries;
|
|
3365
|
+
}
|
|
3366
|
+
const nativeEntries = nativeProducedEntries(owner);
|
|
3367
|
+
return nativeEntries.length === 0 ? null : nativeEntries;
|
|
3368
|
+
}
|
|
3369
|
+
//#endregion
|
|
3370
|
+
//#region src/settings-api.ts
|
|
3371
|
+
/** Shared wire vocabulary for the plugin-owned settings RPC channel. */
|
|
3372
|
+
/** Dedicated, loopback-only RPC channel registered by the Host half. */
|
|
3373
|
+
const CONVERSATION_SETTINGS_RPC_CHANNEL = "/conversation-ui";
|
|
3374
|
+
/** Endpoints accepted by {@link CONVERSATION_SETTINGS_RPC_CHANNEL}. */
|
|
3375
|
+
const CONVERSATION_SETTINGS_RPC = {
|
|
3376
|
+
read: "settings.read",
|
|
3377
|
+
write: "settings.write",
|
|
3378
|
+
upgrade: "plugin.upgrade"
|
|
3379
|
+
};
|
|
3380
|
+
//#endregion
|
|
3381
|
+
//#region src/client/conversation-ui-settings-api.ts
|
|
3382
|
+
function record(value) {
|
|
3383
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
3384
|
+
}
|
|
3385
|
+
function settingsView(value) {
|
|
3386
|
+
const data = record(value);
|
|
3387
|
+
if (data === void 0 || typeof data.version !== "string" || ![
|
|
3388
|
+
"npm",
|
|
3389
|
+
"development",
|
|
3390
|
+
"unmanaged"
|
|
3391
|
+
].includes(data.installation) || typeof data.writable !== "boolean" || typeof data.thinkAutoExpand !== "boolean" || typeof data.canUpgrade !== "boolean") throw new Error("dsh-conversation-ui: malformed settings response");
|
|
3392
|
+
return data;
|
|
3393
|
+
}
|
|
3394
|
+
function upgradeView(value) {
|
|
3395
|
+
if (record(value)?.restartRequired !== true) throw new Error("dsh-conversation-ui: malformed update response");
|
|
3396
|
+
return { restartRequired: true };
|
|
3397
|
+
}
|
|
3398
|
+
function accepted(result) {
|
|
3399
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
3400
|
+
return result.value;
|
|
3401
|
+
}
|
|
3402
|
+
/** Build the typed facade over the generic Connection RPC service. */
|
|
3403
|
+
function createConversationSettingsApi(connection) {
|
|
3404
|
+
return {
|
|
3405
|
+
async read() {
|
|
3406
|
+
return settingsView(accepted(await connection.rpc.call(CONVERSATION_SETTINGS_RPC_CHANNEL, CONVERSATION_SETTINGS_RPC.read, {})));
|
|
3407
|
+
},
|
|
3408
|
+
async write(thinkAutoExpand) {
|
|
3409
|
+
return settingsView(accepted(await connection.rpc.call(CONVERSATION_SETTINGS_RPC_CHANNEL, CONVERSATION_SETTINGS_RPC.write, { thinkAutoExpand })));
|
|
3410
|
+
},
|
|
3411
|
+
async upgrade() {
|
|
3412
|
+
return upgradeView(accepted(await connection.rpc.call(CONVERSATION_SETTINGS_RPC_CHANNEL, CONVERSATION_SETTINGS_RPC.upgrade, {})));
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
//#endregion
|
|
3417
|
+
//#region src/client/locales.ts
|
|
3418
|
+
/** Locale bundles for the conversation-ui plugin configuration card. */
|
|
3419
|
+
/** Dictionary namespace owned by this plugin's settings card. */
|
|
3420
|
+
const NS = "settings.conversationUi";
|
|
3421
|
+
/** English copy. */
|
|
3422
|
+
const en = {
|
|
3423
|
+
title: "Conversation UI",
|
|
3424
|
+
description: "Codex-style progress, tool activity, and final answers.",
|
|
3425
|
+
thinkAutoExpand: "Auto-expand thinking",
|
|
3426
|
+
thinkAutoExpandHint: "Open the thinking block while it streams. Turn off to keep it collapsed.",
|
|
3427
|
+
readOnly: "This deployment stores settings read-only.",
|
|
3428
|
+
loading: "Loading plugin settings…",
|
|
3429
|
+
unavailable: "Plugin settings are unavailable in this connection.",
|
|
3430
|
+
retry: "Retry",
|
|
3431
|
+
version: "Version {version}",
|
|
3432
|
+
developmentVersion: "Development version {version}",
|
|
3433
|
+
updates: "Updates",
|
|
3434
|
+
updateHint: "Install the newest npm version, then restart Harness.",
|
|
3435
|
+
developmentBuild: "Linked source; updates are managed in the checkout.",
|
|
3436
|
+
updateUnavailable: "Updates are available only for an npm profile installation.",
|
|
3437
|
+
update: "Update",
|
|
3438
|
+
updating: "Updating…",
|
|
3439
|
+
restartRequired: "Updated. Restart Harness to load the new version.",
|
|
3440
|
+
updateFailed: "The package update failed; your current version is unchanged.",
|
|
3441
|
+
save: "Save",
|
|
3442
|
+
saving: "Saving…",
|
|
3443
|
+
discard: "Discard",
|
|
3444
|
+
unsaved: "Unsaved",
|
|
3445
|
+
saveFailed: "The deployment did not accept these values; they were left for you to correct."
|
|
3446
|
+
};
|
|
3447
|
+
/** Simplified Chinese copy. */
|
|
3448
|
+
const zh = {
|
|
3449
|
+
title: "对话界面增强",
|
|
3450
|
+
description: "按 Codex 风格展示过程答复、工具活动和最终答案。",
|
|
3451
|
+
thinkAutoExpand: "自动展开思考",
|
|
3452
|
+
thinkAutoExpandHint: "思考块在流式时自动展开;关闭后保持折叠,可手动展开。",
|
|
3453
|
+
readOnly: "本部署的设置为只读。",
|
|
3454
|
+
loading: "正在加载插件设置…",
|
|
3455
|
+
unavailable: "当前连接无法访问插件设置。",
|
|
3456
|
+
retry: "重试",
|
|
3457
|
+
version: "版本 {version}",
|
|
3458
|
+
developmentVersion: "开发版本 {version}",
|
|
3459
|
+
updates: "更新",
|
|
3460
|
+
updateHint: "安装最新 npm 版本后重启 Harness。",
|
|
3461
|
+
developmentBuild: "当前为本地链接版本,请在源码目录管理更新。",
|
|
3462
|
+
updateUnavailable: "只有 profile 使用 npm 包时才能更新。",
|
|
3463
|
+
update: "更新",
|
|
3464
|
+
updating: "更新中…",
|
|
3465
|
+
restartRequired: "已更新;重启 Harness 后加载新版本。",
|
|
3466
|
+
updateFailed: "包更新失败,当前版本未改变。",
|
|
3467
|
+
save: "保存",
|
|
3468
|
+
saving: "保存中…",
|
|
3469
|
+
discard: "放弃修改",
|
|
3470
|
+
unsaved: "未保存",
|
|
3471
|
+
saveFailed: "本部署没有接受这些值,已保留供你修改。"
|
|
3472
|
+
};
|
|
3473
|
+
//#endregion
|
|
3474
|
+
//#region src/client/index.ts
|
|
3475
|
+
/**
|
|
3476
|
+
* Cordis services required by the browser half. Only `slots` is load-bearing
|
|
3477
|
+
* for the stream itself; locale and Connection power the configuration card
|
|
3478
|
+
* and are wired through `ctx.inject` below so a deployment without them still
|
|
3479
|
+
* streams with defaults.
|
|
3480
|
+
*/
|
|
3481
|
+
const inject = ["slots"];
|
|
3482
|
+
const CONVERSATION_MODES = ["typewriter", "teleprompter"];
|
|
3483
|
+
const CONVERSATION_PRESETS = [
|
|
3484
|
+
"realtime",
|
|
3485
|
+
"balanced",
|
|
3486
|
+
"silky"
|
|
3487
|
+
];
|
|
3488
|
+
/** Steering/command rows stay untouched; `assistant-step` is replaced. */
|
|
3489
|
+
const SKIP_WRAP = /* @__PURE__ */ new Set([
|
|
3490
|
+
"assistant-step",
|
|
3491
|
+
"steering",
|
|
3492
|
+
"command-input"
|
|
3493
|
+
]);
|
|
3494
|
+
/**
|
|
3495
|
+
* Read the Host-bridged boot config. The inline script is produced by this
|
|
3496
|
+
* plugin's Host half from a schema-validated value, so only the structural
|
|
3497
|
+
* guarantees that could break between the two halves are re-checked: the
|
|
3498
|
+
* global is absent when the client runs without its Host entry (defaults
|
|
3499
|
+
* apply), and any present-but-malformed value fails loudly instead of
|
|
3500
|
+
* rendering a half-configured view.
|
|
3501
|
+
* @returns The resolved configuration for the assistant node view.
|
|
3502
|
+
*/
|
|
3503
|
+
function readBootConfig() {
|
|
3504
|
+
const raw = globalThis[CONVERSATION_BOOT_GLOBAL];
|
|
3505
|
+
if (raw === void 0) {
|
|
3506
|
+
console.info("[dsh-conversation-ui] no host config bridge; using defaults");
|
|
3507
|
+
return DEFAULT_CONVERSATION_CONFIG;
|
|
3508
|
+
}
|
|
3509
|
+
if (typeof raw !== "object" || raw === null || !CONVERSATION_MODES.includes(raw.mode) || !CONVERSATION_PRESETS.includes(raw.preset) || typeof raw.revealCharsPerSec !== "number" || typeof raw.scrollSpeedPxPerSec !== "number" || typeof raw.maxScrollSpeedPxPerSec !== "number") throw new Error(`[dsh-conversation-ui] malformed ${CONVERSATION_BOOT_GLOBAL} boot global: ${JSON.stringify(raw)}`);
|
|
3510
|
+
return raw;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Wrap every keyed Chat row except `assistant-step` in place. A second
|
|
3514
|
+
* register with the same `children` table throws because the child slot is
|
|
3515
|
+
* already declared, and only the winning entry receives `renderSlot`;
|
|
3516
|
+
* swapping `entry.component` keeps the original children, locale, and inject
|
|
3517
|
+
* seats. `assistant-step` is replaced below so text and Think use the
|
|
3518
|
+
* Codex-style Assistant presentation.
|
|
3519
|
+
* @param ctx - Browser context carrying the slot registry.
|
|
3520
|
+
* @returns Restorer that puts the original components back.
|
|
3521
|
+
*/
|
|
3522
|
+
function wrapGrowingChatRows(ctx, config) {
|
|
3523
|
+
const restores = [];
|
|
3524
|
+
const wrapped = /* @__PURE__ */ new WeakSet();
|
|
3525
|
+
const wrapAll = () => {
|
|
3526
|
+
for (const entry of ctx.slots.entries("conversation.chat.node")) {
|
|
3527
|
+
const key = entry.options.key;
|
|
3528
|
+
if (key === void 0 || SKIP_WRAP.has(key)) continue;
|
|
3529
|
+
const current = entry.component;
|
|
3530
|
+
if (typeof current !== "function" && (typeof current !== "object" || current === null) || wrapped.has(current)) continue;
|
|
3531
|
+
const inner = current;
|
|
3532
|
+
const next = key === "user" ? wrapTurnPreludeNodeView(inner) : wrapFollowNodeView(inner, {
|
|
3533
|
+
minSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
3534
|
+
maxSpeedPxPerSec: config.maxScrollSpeedPxPerSec
|
|
3535
|
+
});
|
|
3536
|
+
wrapped.add(next);
|
|
3537
|
+
entry.component = next;
|
|
3538
|
+
restores.push(() => {
|
|
3539
|
+
if (entry.component === next) entry.component = inner;
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
wrapAll();
|
|
3544
|
+
const off = ctx.on("slots/changed", (key) => {
|
|
3545
|
+
if (key === "conversation.chat.node") wrapAll();
|
|
3546
|
+
});
|
|
3547
|
+
return () => {
|
|
3548
|
+
off();
|
|
3549
|
+
for (const restore of restores) restore();
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* A live preference cell read by `useSyncExternalStore`. It starts on the
|
|
3554
|
+
* shared default and, once the plugin-owned settings controller is attached,
|
|
3555
|
+
* tracks the resolved `thinkAutoExpand` value. A composition (or test) that
|
|
3556
|
+
* ships no settings surface keeps the cell on the default.
|
|
3557
|
+
*/
|
|
3558
|
+
var PreferenceCell = class {
|
|
3559
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3560
|
+
card;
|
|
3561
|
+
value = DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand;
|
|
3562
|
+
/** Re-point the cell at the plugin-owned settings controller. */
|
|
3563
|
+
attach(card) {
|
|
3564
|
+
this.card = card;
|
|
3565
|
+
this.refresh();
|
|
3566
|
+
const unsubscribe = card.subscribe(() => {
|
|
3567
|
+
this.refresh();
|
|
3568
|
+
});
|
|
3569
|
+
return () => {
|
|
3570
|
+
unsubscribe();
|
|
3571
|
+
if (this.card !== card) return;
|
|
3572
|
+
this.card = void 0;
|
|
3573
|
+
this.refresh();
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
read() {
|
|
3577
|
+
return this.card?.getSnapshot().thinkAutoExpand ?? DEFAULT_CONVERSATION_SETTINGS.thinkAutoExpand;
|
|
3578
|
+
}
|
|
3579
|
+
refresh() {
|
|
3580
|
+
const next = this.read();
|
|
3581
|
+
if (next === this.value) return;
|
|
3582
|
+
this.value = next;
|
|
3583
|
+
for (const listener of this.listeners) listener();
|
|
3584
|
+
}
|
|
3585
|
+
getSnapshot = () => this.value;
|
|
3586
|
+
subscribe = (listener) => {
|
|
3587
|
+
this.listeners.add(listener);
|
|
3588
|
+
return () => {
|
|
3589
|
+
this.listeners.delete(listener);
|
|
3590
|
+
};
|
|
3591
|
+
};
|
|
3592
|
+
};
|
|
3593
|
+
/**
|
|
3594
|
+
* Register the Codex-style renderer after the conversation package declares the
|
|
3595
|
+
* keyed Chat node seat. A lower priority shadows the built-in assistant row;
|
|
3596
|
+
* every other growing row is wrapped in place so Tool cards, retries, and
|
|
3597
|
+
* workflow runs share conversation follow. The Host-bridged configuration
|
|
3598
|
+
* selects immediate or typewriter reveal, smoothing preset, and glide speed; the
|
|
3599
|
+
* plugin-owned settings RPC supplies the live auto-expand preference when the
|
|
3600
|
+
* settings surface is composed.
|
|
3601
|
+
* @param ctx - Browser context carrying the shared slot registry.
|
|
3602
|
+
*/
|
|
3603
|
+
function apply(ctx) {
|
|
3604
|
+
const config = readBootConfig();
|
|
3605
|
+
const preference = new PreferenceCell();
|
|
3606
|
+
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
3607
|
+
name: "conversation.chat.turnTail",
|
|
3608
|
+
priority: -100,
|
|
3609
|
+
select: selectDeliverables,
|
|
3610
|
+
registrant: "dsh-conversation-ui"
|
|
3611
|
+
}, DeliverablesCard));
|
|
3612
|
+
ctx.inject(["conversationEvents"], (deliverablesCtx) => {
|
|
3613
|
+
return deliverablesCtx.get("conversationEvents").register(deliverablesDefinition);
|
|
3614
|
+
});
|
|
3615
|
+
ctx.inject([
|
|
3616
|
+
"slots",
|
|
3617
|
+
"locale",
|
|
3618
|
+
"connection"
|
|
3619
|
+
], (settingsCtx) => {
|
|
3620
|
+
const card = new ConversationCardController(createConversationSettingsApi(settingsCtx.get("connection")));
|
|
3621
|
+
const detachPreference = preference.attach(card);
|
|
3622
|
+
card.start();
|
|
3623
|
+
settingsCtx.effect(() => settingsCtx.locale.register(NS, {
|
|
3624
|
+
zh,
|
|
3625
|
+
en
|
|
3626
|
+
}), "dsh-conversation-ui: settings dictionaries");
|
|
3627
|
+
settingsCtx.slots.inject("settings.plugin.item", () => settingsCtx.slots.register({
|
|
3628
|
+
name: "settings.plugin.item",
|
|
3629
|
+
key: "conversation-ui",
|
|
3630
|
+
locale: NS,
|
|
3631
|
+
inject: () => card.inject()
|
|
3632
|
+
}, ConversationCard));
|
|
3633
|
+
return () => {
|
|
3634
|
+
card.stop();
|
|
3635
|
+
detachPreference();
|
|
3636
|
+
};
|
|
3637
|
+
});
|
|
3638
|
+
const configured = function ConversationConfiguredView(props) {
|
|
3639
|
+
const thinkAutoExpand = (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot, preference.getSnapshot);
|
|
3640
|
+
return (0, react.createElement)(TypewriterAssistantNodeView, {
|
|
3641
|
+
...props,
|
|
3642
|
+
mode: config.mode,
|
|
3643
|
+
preset: config.preset,
|
|
3644
|
+
revealCharsPerSec: config.revealCharsPerSec,
|
|
3645
|
+
scrollSpeedPxPerSec: config.scrollSpeedPxPerSec,
|
|
3646
|
+
maxScrollSpeedPxPerSec: config.maxScrollSpeedPxPerSec,
|
|
3647
|
+
thinkAutoExpand
|
|
3648
|
+
});
|
|
3649
|
+
};
|
|
3650
|
+
ctx.slots.inject("conversation.chat.node", () => {
|
|
3651
|
+
const unwrap = wrapGrowingChatRows(ctx, config);
|
|
3652
|
+
const unshadow = ctx.slots.register({
|
|
3653
|
+
name: "conversation.chat.node",
|
|
3654
|
+
key: "assistant-step",
|
|
3655
|
+
priority: -100,
|
|
3656
|
+
locale: "conversation",
|
|
3657
|
+
registrant: "dsh-conversation-ui"
|
|
3658
|
+
}, configured);
|
|
3659
|
+
return () => {
|
|
3660
|
+
unshadow();
|
|
3661
|
+
unwrap();
|
|
3662
|
+
};
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
//#endregion
|
|
3666
|
+
exports.apply = apply;
|
|
3667
|
+
exports.inject = inject;
|
|
3668
|
+
return module.exports;
|
|
3669
|
+
}
|
|
3670
|
+
});
|
|
3671
|
+
|
|
3672
|
+
//# sourceMappingURL=client.js.map
|