dsh-milestone 0.3.1 → 0.5.0
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/README.md +16 -2
- package/lib/client.js +1002 -123
- package/package.json +3 -1
package/lib/client.js
CHANGED
|
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
8
|
let react = require("react");
|
|
9
|
+
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
9
10
|
//#region src/client/MilestoneOverlay.tsx
|
|
10
11
|
/**
|
|
11
12
|
* @param props - runtime share (root kit) + the narrowed renderSlot and the
|
|
@@ -18,6 +19,281 @@ window.__ModuleLoader__.load({
|
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
//#endregion
|
|
22
|
+
//#region src/client/badge-logic.ts
|
|
23
|
+
/**
|
|
24
|
+
* Derive the badge for one mark.
|
|
25
|
+
*
|
|
26
|
+
* Precedence: error > max-tokens > retry > running > awaiting. Node-derived
|
|
27
|
+
* badges ('turn-error' -> error, 'turn-max-tokens' -> max-tokens,
|
|
28
|
+
* 'model-retry' -> retry) fire regardless of `lastMark`; the transient badges
|
|
29
|
+
* (running, awaiting) only apply to the newest mark. Callers must already
|
|
30
|
+
* exclude cancelled retries — a bare 'model-retry' kind is treated as retry.
|
|
31
|
+
*
|
|
32
|
+
* @param input - the mark's snapshot signals.
|
|
33
|
+
* @returns the winning badge kind, or null when no signal applies.
|
|
34
|
+
*/
|
|
35
|
+
function deriveBadge(input) {
|
|
36
|
+
if (input.nodeKinds.includes("turn-error")) return "error";
|
|
37
|
+
if (input.nodeKinds.includes("turn-max-tokens")) return "max-tokens";
|
|
38
|
+
if (input.nodeKinds.includes("model-retry")) return "retry";
|
|
39
|
+
if (input.lastMark) {
|
|
40
|
+
if (input.running) return "running";
|
|
41
|
+
if (input.awaitingInput) return "awaiting";
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** Ring colors and pulse flag per badge kind; running/awaiting pulse. */
|
|
46
|
+
const RING_STYLES = {
|
|
47
|
+
error: {
|
|
48
|
+
color: "#ef4444",
|
|
49
|
+
pulse: false
|
|
50
|
+
},
|
|
51
|
+
"max-tokens": {
|
|
52
|
+
color: "#f59e0b",
|
|
53
|
+
pulse: false
|
|
54
|
+
},
|
|
55
|
+
retry: {
|
|
56
|
+
color: "#f97316",
|
|
57
|
+
pulse: false
|
|
58
|
+
},
|
|
59
|
+
running: {
|
|
60
|
+
color: "#4d7cfe",
|
|
61
|
+
pulse: true
|
|
62
|
+
},
|
|
63
|
+
awaiting: {
|
|
64
|
+
color: "#f59e0b",
|
|
65
|
+
pulse: true
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Style tokens for a badge kind.
|
|
70
|
+
* @param badge - the derived badge kind.
|
|
71
|
+
* @returns the ring color and whether the dot should pulse.
|
|
72
|
+
*/
|
|
73
|
+
function badgeRingStyle(badge) {
|
|
74
|
+
return RING_STYLES[badge];
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/client/bookmark-logic.ts
|
|
78
|
+
/**
|
|
79
|
+
* Pure bookmark logic for the milestone rail: membership, immutable
|
|
80
|
+
* append/remove toggling, bookmark filtering of a mark list, and count.
|
|
81
|
+
*
|
|
82
|
+
* All functions are side-effect free (no React, no DOM) so the rail component
|
|
83
|
+
* can consume them directly and tests can exercise them in isolation. The
|
|
84
|
+
* persisted store engine lives in bookmarkStore.ts; this module only shapes
|
|
85
|
+
* values.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* Whether a key is currently bookmarked.
|
|
89
|
+
* @param keys - the bookmark key list (in toggle order).
|
|
90
|
+
* @param key - the key to look up.
|
|
91
|
+
* @returns true when the key is present.
|
|
92
|
+
*/
|
|
93
|
+
function isBookmarked(keys, key) {
|
|
94
|
+
return keys.includes(key);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Immutable toggle: append the key when it is not bookmarked, remove it when
|
|
98
|
+
* it is. Never mutates the input; returns a fresh list (order preserved).
|
|
99
|
+
* @param keys - the bookmark key list (in toggle order).
|
|
100
|
+
* @param key - the key to flip.
|
|
101
|
+
* @returns a new list with the key toggled.
|
|
102
|
+
*/
|
|
103
|
+
function toggleKey(keys, key) {
|
|
104
|
+
return isBookmarked(keys, key) ? keys.filter((k) => k !== key) : [...keys, key];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Filter a mark list down to the bookmarked marks.
|
|
108
|
+
* @param marks - marks in rail order (only `key` is consulted).
|
|
109
|
+
* @param bookmarked - the bookmark key list.
|
|
110
|
+
* @returns `visible` (ascending indices of marks whose key is bookmarked;
|
|
111
|
+
* empty whenever there are no bookmarks) and `isFiltered` (true exactly when
|
|
112
|
+
* any bookmark exists — callers treat it as "filter active").
|
|
113
|
+
*/
|
|
114
|
+
function filterByBookmarks(marks, bookmarked) {
|
|
115
|
+
if (bookmarked.length === 0) return {
|
|
116
|
+
visible: [],
|
|
117
|
+
isFiltered: false
|
|
118
|
+
};
|
|
119
|
+
const set = new Set(bookmarked);
|
|
120
|
+
return {
|
|
121
|
+
visible: marks.reduce((acc, mark, i) => {
|
|
122
|
+
if (set.has(mark.key)) acc.push(i);
|
|
123
|
+
return acc;
|
|
124
|
+
}, []),
|
|
125
|
+
isFiltered: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/client/clipboard-logic.ts
|
|
130
|
+
/**
|
|
131
|
+
* Clipboard helper for the milestone rail: copies text to the system
|
|
132
|
+
* clipboard via the async Clipboard API. Resolves false (never throws) when
|
|
133
|
+
* the API is unavailable or the write is rejected, so callers can treat the
|
|
134
|
+
* result as a plain boolean.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Copy text to the system clipboard.
|
|
138
|
+
* @param text - the text to copy.
|
|
139
|
+
* @returns a promise resolving to true when the clipboard write succeeded,
|
|
140
|
+
* false when the Clipboard API is unavailable or the write was rejected.
|
|
141
|
+
*/
|
|
142
|
+
async function copyText(text) {
|
|
143
|
+
if (typeof navigator === "undefined" || navigator.clipboard?.writeText === void 0) return false;
|
|
144
|
+
try {
|
|
145
|
+
await navigator.clipboard.writeText(text);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/client/label-logic.ts
|
|
153
|
+
const MINUTE_MS = 6e4;
|
|
154
|
+
const HOUR_MS = 36e5;
|
|
155
|
+
const DAY_MS = 864e5;
|
|
156
|
+
/**
|
|
157
|
+
* Bucket an elapsed duration into a relative-time label.
|
|
158
|
+
*
|
|
159
|
+
* Buckets on `now - time` in milliseconds: below 60s -> justNow (n=0), below
|
|
160
|
+
* 3600s -> minutes, below 86400s -> hours, otherwise days. `n` is the whole
|
|
161
|
+
* count of the bucket unit (floor). Deterministic for a given `now`.
|
|
162
|
+
*
|
|
163
|
+
* @param time - the event timestamp in ms since epoch.
|
|
164
|
+
* @param now - the reference clock in ms since epoch.
|
|
165
|
+
* @returns the label key and bucket count.
|
|
166
|
+
*/
|
|
167
|
+
function relativeTimeParts(time, now) {
|
|
168
|
+
const diff = now - time;
|
|
169
|
+
if (diff < MINUTE_MS) return {
|
|
170
|
+
key: "time.justNow",
|
|
171
|
+
n: 0
|
|
172
|
+
};
|
|
173
|
+
if (diff < HOUR_MS) return {
|
|
174
|
+
key: "time.minutes",
|
|
175
|
+
n: Math.floor(diff / MINUTE_MS)
|
|
176
|
+
};
|
|
177
|
+
if (diff < DAY_MS) return {
|
|
178
|
+
key: "time.hours",
|
|
179
|
+
n: Math.floor(diff / HOUR_MS)
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
key: "time.days",
|
|
183
|
+
n: Math.floor(diff / DAY_MS)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Map a harness end-reason string to a stable i18n key.
|
|
188
|
+
* @param kind - the raw end-reason string (e.g. 'max-tokens').
|
|
189
|
+
* @returns the i18n key, or the raw kind unchanged when unknown.
|
|
190
|
+
*/
|
|
191
|
+
function reasonKeyOf(kind) {
|
|
192
|
+
switch (kind) {
|
|
193
|
+
case "completed": return "reason.completed";
|
|
194
|
+
case "aborted": return "reason.aborted";
|
|
195
|
+
case "error": return "reason.error";
|
|
196
|
+
case "max-tokens": return "reason.maxTokens";
|
|
197
|
+
case "interrupted": return "reason.interrupted";
|
|
198
|
+
case "blocked": return "reason.blocked";
|
|
199
|
+
default: return kind;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/client/tooltip-logic.ts
|
|
204
|
+
const EMPTY_META = {
|
|
205
|
+
model: null,
|
|
206
|
+
purpose: null,
|
|
207
|
+
inputTokens: null,
|
|
208
|
+
outputTokens: null
|
|
209
|
+
};
|
|
210
|
+
/** True when the value is a plain (non-array, non-null) object. */
|
|
211
|
+
function isRecord(value) {
|
|
212
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decode a `usage` payload structurally: only numeric `inputTokens` /
|
|
216
|
+
* `outputTokens` survive; anything else (absent, malformed, wrong types)
|
|
217
|
+
* degrades to null — the boundary owns trust, the callers get plain numbers.
|
|
218
|
+
* @param usage - untrusted usage payload (typed `unknown` at runtime).
|
|
219
|
+
* @returns the token counts with null for every missing/malformed field.
|
|
220
|
+
*/
|
|
221
|
+
function decodeUsage(usage) {
|
|
222
|
+
if (!isRecord(usage)) return {
|
|
223
|
+
inputTokens: null,
|
|
224
|
+
outputTokens: null
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
inputTokens: typeof usage.inputTokens === "number" ? usage.inputTokens : null,
|
|
228
|
+
outputTokens: typeof usage.outputTokens === "number" ? usage.outputTokens : null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Resolve model/purpose from a request config, falling back to provenance. */
|
|
232
|
+
function metaFromRecord(record) {
|
|
233
|
+
return {
|
|
234
|
+
model: record.requestConfig?.model ?? record.provenance?.model ?? null,
|
|
235
|
+
purpose: record.requestConfig?.purpose ?? null,
|
|
236
|
+
...decodeUsage(record.usage)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Derive the hover metadata for one turn. Sources, in priority order:
|
|
241
|
+
* 1. the `assistant-step` chat node(s) of the turn — their `data.finalNode`
|
|
242
|
+
* carries the recorded `requestConfig` / `provenance` / `usage`;
|
|
243
|
+
* 2. `trajectoryRequests` — the latest entry whose `turn` matches (used when
|
|
244
|
+
* no assistant-step node yields a model or purpose);
|
|
245
|
+
* 3. all-null when the turn is absent, no node matches, or everything is
|
|
246
|
+
* malformed. Never throws.
|
|
247
|
+
* @param nodes - stable per-key chat node reader (as exposed by the snapshot).
|
|
248
|
+
* @param locations - turn -> ordered node keys index.
|
|
249
|
+
* @param turn - owning turn; undefined yields all-null.
|
|
250
|
+
* @param trajectoryRequests - optional fallback request log.
|
|
251
|
+
* @returns the turn's metadata, null where unknown.
|
|
252
|
+
*/
|
|
253
|
+
function deriveTurnMeta(nodes, locations, turn, trajectoryRequests) {
|
|
254
|
+
if (turn === void 0) return EMPTY_META;
|
|
255
|
+
for (const key of locations.getTurn(turn)) {
|
|
256
|
+
const node = nodes.get(key);
|
|
257
|
+
if (node === void 0 || node.kind !== "assistant-step") continue;
|
|
258
|
+
const finalNode = (isRecord(node.data) ? node.data : void 0)?.finalNode;
|
|
259
|
+
if (!isRecord(finalNode)) continue;
|
|
260
|
+
const meta = metaFromRecord(finalNode);
|
|
261
|
+
if (meta.model !== null || meta.purpose !== null) return meta;
|
|
262
|
+
}
|
|
263
|
+
if (trajectoryRequests !== void 0) {
|
|
264
|
+
let latest;
|
|
265
|
+
for (const request of trajectoryRequests) if (request.turn === turn) latest = request;
|
|
266
|
+
if (latest !== void 0) return metaFromRecord(latest);
|
|
267
|
+
}
|
|
268
|
+
return EMPTY_META;
|
|
269
|
+
}
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/client/rail-keyboard.ts
|
|
272
|
+
/**
|
|
273
|
+
* Pure roving-tabindex index math for the milestone rail.
|
|
274
|
+
*
|
|
275
|
+
* The dots list becomes a single roving-tabindex widget (ArrowUp/Down moves
|
|
276
|
+
* focus, Home/End jumps to first/last). This module only owns the pure index
|
|
277
|
+
* arithmetic; the widget wiring lives in the component.
|
|
278
|
+
*/
|
|
279
|
+
/**
|
|
280
|
+
* Move `current` by `delta` (1 = forward, -1 = backward), wrapping around
|
|
281
|
+
* `[0, count - 1]`. Returns `-1` when there are no focusable dots.
|
|
282
|
+
*/
|
|
283
|
+
function nextFocusIndex(current, count, delta) {
|
|
284
|
+
if (count <= 0) return -1;
|
|
285
|
+
return (current + delta + count) % count;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Clamp `current` into `[0, count - 1]` — e.g. when the visible dot list
|
|
289
|
+
* shrinks and the focused index no longer exists. Returns `-1` when there
|
|
290
|
+
* are no focusable dots.
|
|
291
|
+
*/
|
|
292
|
+
function clampIndex(current, count) {
|
|
293
|
+
if (count <= 0) return -1;
|
|
294
|
+
return Math.min(Math.max(current, 0), count - 1);
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
21
297
|
//#region src/client/rail-logic.ts
|
|
22
298
|
/**
|
|
23
299
|
* Extract the FULL plain text of a ContentBlock[] payload: the `text` of every
|
|
@@ -108,17 +384,83 @@ window.__ModuleLoader__.load({
|
|
|
108
384
|
return `hsl(218, 88%, ${72 - (total <= 1 ? 0 : index / (total - 1)) * 27}%)`;
|
|
109
385
|
}
|
|
110
386
|
//#endregion
|
|
387
|
+
//#region src/client/turn-group-logic.ts
|
|
388
|
+
/**
|
|
389
|
+
* Partition consecutive marks by turn. Marks with the same numeric turn that
|
|
390
|
+
* appear one after another share a group; each mark with `turn === undefined`
|
|
391
|
+
* becomes its own singleton group with `turn: null`.
|
|
392
|
+
* @param marks - marks in rail order.
|
|
393
|
+
* @returns the groups, in original order, partitioning `marks` exactly.
|
|
394
|
+
*/
|
|
395
|
+
function buildTurnGroups(marks) {
|
|
396
|
+
const groups = [];
|
|
397
|
+
let current;
|
|
398
|
+
for (const mark of marks) {
|
|
399
|
+
if (mark.turn === void 0) {
|
|
400
|
+
current = void 0;
|
|
401
|
+
groups.push({
|
|
402
|
+
turn: null,
|
|
403
|
+
marks: [mark]
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (current !== void 0 && current.turn === mark.turn) current.marks.push(mark);
|
|
408
|
+
else {
|
|
409
|
+
current = {
|
|
410
|
+
turn: mark.turn,
|
|
411
|
+
marks: [mark]
|
|
412
|
+
};
|
|
413
|
+
groups.push(current);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return groups;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Flatten groups into render items, collapsing collapsed turns to their last
|
|
420
|
+
* mark and reporting where separators belong.
|
|
421
|
+
* @param groups - groups from {@link buildTurnGroups} (they partition the
|
|
422
|
+
* original marks array in order, so a running count yields original indices).
|
|
423
|
+
* @param collapsed - turns whose group should collapse to its LAST mark.
|
|
424
|
+
* @returns `items` (one RenderItem per visible dot, in group order) and
|
|
425
|
+
* `separatorsAt` (the index in `items` before which a separator should be
|
|
426
|
+
* inserted at each non-first group boundary; never includes 0).
|
|
427
|
+
*/
|
|
428
|
+
function buildRenderList(groups, collapsed) {
|
|
429
|
+
const items = [];
|
|
430
|
+
const separatorsAt = [];
|
|
431
|
+
let counter = 0;
|
|
432
|
+
for (const group of groups) {
|
|
433
|
+
const startIndex = items.length;
|
|
434
|
+
if (group.turn !== null && collapsed.has(group.turn) && group.marks.length > 1) {
|
|
435
|
+
const last = group.marks[group.marks.length - 1];
|
|
436
|
+
items.push({
|
|
437
|
+
mark: last,
|
|
438
|
+
displayIndex: counter + group.marks.length - 1
|
|
439
|
+
});
|
|
440
|
+
} else for (let i = 0; i < group.marks.length; i++) items.push({
|
|
441
|
+
mark: group.marks[i],
|
|
442
|
+
displayIndex: counter + i
|
|
443
|
+
});
|
|
444
|
+
counter += group.marks.length;
|
|
445
|
+
if (startIndex > 0) separatorsAt.push(startIndex);
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
items,
|
|
449
|
+
separatorsAt
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
111
453
|
//#region src/client/MilestoneRailSearch.tsx
|
|
112
454
|
/** Dot diameter (px) — matches the rail's DOT_HIT so the toggle aligns. */
|
|
113
455
|
const DOT_HIT$1 = 22;
|
|
114
456
|
/**
|
|
115
457
|
* @param props - the search state slice plus the rail's event handlers.
|
|
116
458
|
*/
|
|
117
|
-
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear }) {
|
|
459
|
+
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear, t }) {
|
|
118
460
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
119
461
|
type: "button",
|
|
120
462
|
"data-search-toggle": true,
|
|
121
|
-
"aria-label": "
|
|
463
|
+
"aria-label": t("search.label"),
|
|
122
464
|
"aria-pressed": panelOpen,
|
|
123
465
|
onClick: onToggle,
|
|
124
466
|
style: {
|
|
@@ -170,8 +512,8 @@ window.__ModuleLoader__.load({
|
|
|
170
512
|
},
|
|
171
513
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
172
514
|
"data-rail-search": true,
|
|
173
|
-
"aria-label": "
|
|
174
|
-
placeholder: "
|
|
515
|
+
"aria-label": t("search.label"),
|
|
516
|
+
placeholder: t("search.placeholder"),
|
|
175
517
|
value: query,
|
|
176
518
|
onChange: (e) => onQueryChange(e.target.value),
|
|
177
519
|
onKeyDown: onSearchKeyDown,
|
|
@@ -190,7 +532,7 @@ window.__ModuleLoader__.load({
|
|
|
190
532
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
191
533
|
type: "button",
|
|
192
534
|
"data-search-clear": true,
|
|
193
|
-
"aria-label": "
|
|
535
|
+
"aria-label": t("search.clear"),
|
|
194
536
|
onClick: onClear,
|
|
195
537
|
style: {
|
|
196
538
|
width: 22,
|
|
@@ -233,6 +575,207 @@ window.__ModuleLoader__.load({
|
|
|
233
575
|
})] });
|
|
234
576
|
}
|
|
235
577
|
//#endregion
|
|
578
|
+
//#region src/client/MilestoneRailTooltip.tsx
|
|
579
|
+
/**
|
|
580
|
+
* @param props - the hovered mark + bookmark wiring (see {@link MilestoneRailTooltipProps}).
|
|
581
|
+
*/
|
|
582
|
+
function MilestoneRailTooltip({ hover, bookmarked, onToggleBookmark, onCopy, onFork, copied, forked, turnCollapsed, onToggleCollapse, onMouseEnter, onMouseLeave, panelRight, t }) {
|
|
583
|
+
const relativeTime = relativeTimeParts(hover.mark.time, Date.now());
|
|
584
|
+
const turn = hover.mark.turn;
|
|
585
|
+
const showCollapse = turn !== void 0 && hover.turnMarkCount !== null && hover.turnMarkCount > 1;
|
|
586
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
587
|
+
onMouseEnter,
|
|
588
|
+
onMouseLeave,
|
|
589
|
+
style: {
|
|
590
|
+
position: "fixed",
|
|
591
|
+
right: panelRight,
|
|
592
|
+
top: hover.top,
|
|
593
|
+
transform: "translateY(-50%)",
|
|
594
|
+
maxWidth: 300,
|
|
595
|
+
minWidth: 180,
|
|
596
|
+
padding: "8px 12px",
|
|
597
|
+
background: "rgba(20, 24, 32, 0.96)",
|
|
598
|
+
color: "#e6e8ee",
|
|
599
|
+
borderRadius: 8,
|
|
600
|
+
fontSize: 12,
|
|
601
|
+
lineHeight: 1.6,
|
|
602
|
+
whiteSpace: "pre-wrap",
|
|
603
|
+
wordBreak: "break-word",
|
|
604
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
605
|
+
zIndex: 101,
|
|
606
|
+
pointerEvents: "auto"
|
|
607
|
+
},
|
|
608
|
+
children: [
|
|
609
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
610
|
+
style: {
|
|
611
|
+
display: "flex",
|
|
612
|
+
alignItems: "center",
|
|
613
|
+
flexWrap: "wrap",
|
|
614
|
+
gap: 8,
|
|
615
|
+
color: "#9aa4b8",
|
|
616
|
+
fontSize: 11,
|
|
617
|
+
marginBottom: 4
|
|
618
|
+
},
|
|
619
|
+
children: [
|
|
620
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pos.of", {
|
|
621
|
+
n: hover.index + 1,
|
|
622
|
+
m: hover.total
|
|
623
|
+
}) }),
|
|
624
|
+
hover.turnLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.turnLabel }),
|
|
625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
626
|
+
type: "button",
|
|
627
|
+
"data-star": true,
|
|
628
|
+
"aria-label": t("bookmark.star"),
|
|
629
|
+
"aria-pressed": bookmarked,
|
|
630
|
+
"data-starred": bookmarked ? "true" : void 0,
|
|
631
|
+
onClick: (e) => {
|
|
632
|
+
e.stopPropagation();
|
|
633
|
+
onToggleBookmark();
|
|
634
|
+
},
|
|
635
|
+
style: {
|
|
636
|
+
marginLeft: "auto",
|
|
637
|
+
width: 22,
|
|
638
|
+
height: 22,
|
|
639
|
+
flexShrink: 0,
|
|
640
|
+
display: "flex",
|
|
641
|
+
alignItems: "center",
|
|
642
|
+
justifyContent: "center",
|
|
643
|
+
background: "transparent",
|
|
644
|
+
border: "none",
|
|
645
|
+
padding: 0,
|
|
646
|
+
cursor: "pointer",
|
|
647
|
+
color: bookmarked ? "#ffd166" : "#8b96ab"
|
|
648
|
+
},
|
|
649
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
650
|
+
width: "13",
|
|
651
|
+
height: "13",
|
|
652
|
+
viewBox: "0 0 24 24",
|
|
653
|
+
fill: bookmarked ? "currentColor" : "none",
|
|
654
|
+
stroke: "currentColor",
|
|
655
|
+
strokeWidth: "2",
|
|
656
|
+
strokeLinejoin: "round",
|
|
657
|
+
"aria-hidden": "true",
|
|
658
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
|
|
659
|
+
})
|
|
660
|
+
}),
|
|
661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
662
|
+
type: "button",
|
|
663
|
+
"data-copy-message": true,
|
|
664
|
+
"data-copied": copied ? "true" : void 0,
|
|
665
|
+
onClick: (e) => {
|
|
666
|
+
e.stopPropagation();
|
|
667
|
+
onCopy(hover.mark);
|
|
668
|
+
},
|
|
669
|
+
style: {
|
|
670
|
+
flexShrink: 0,
|
|
671
|
+
display: "flex",
|
|
672
|
+
alignItems: "center",
|
|
673
|
+
justifyContent: "center",
|
|
674
|
+
background: "transparent",
|
|
675
|
+
border: "none",
|
|
676
|
+
padding: "2px 6px",
|
|
677
|
+
cursor: "pointer",
|
|
678
|
+
whiteSpace: "nowrap",
|
|
679
|
+
color: copied ? "#7ee2a8" : "#8b96ab"
|
|
680
|
+
},
|
|
681
|
+
children: t("copy.message")
|
|
682
|
+
}),
|
|
683
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
684
|
+
type: "button",
|
|
685
|
+
"data-fork-here": true,
|
|
686
|
+
"data-forked": forked ? "true" : void 0,
|
|
687
|
+
onClick: (e) => {
|
|
688
|
+
e.stopPropagation();
|
|
689
|
+
onFork(hover.mark);
|
|
690
|
+
},
|
|
691
|
+
style: {
|
|
692
|
+
flexShrink: 0,
|
|
693
|
+
display: "flex",
|
|
694
|
+
alignItems: "center",
|
|
695
|
+
justifyContent: "center",
|
|
696
|
+
background: "transparent",
|
|
697
|
+
border: "none",
|
|
698
|
+
padding: "2px 6px",
|
|
699
|
+
cursor: "pointer",
|
|
700
|
+
whiteSpace: "nowrap",
|
|
701
|
+
color: forked ? "#7ee2a8" : "#8b96ab"
|
|
702
|
+
},
|
|
703
|
+
children: t("fork.here")
|
|
704
|
+
}),
|
|
705
|
+
showCollapse && turn !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
706
|
+
type: "button",
|
|
707
|
+
"data-toggle-collapse": true,
|
|
708
|
+
"aria-pressed": turnCollapsed,
|
|
709
|
+
"data-collapsed": turnCollapsed ? "true" : void 0,
|
|
710
|
+
onClick: (e) => {
|
|
711
|
+
e.stopPropagation();
|
|
712
|
+
onToggleCollapse(turn);
|
|
713
|
+
},
|
|
714
|
+
style: {
|
|
715
|
+
flexShrink: 0,
|
|
716
|
+
display: "flex",
|
|
717
|
+
alignItems: "center",
|
|
718
|
+
justifyContent: "center",
|
|
719
|
+
background: "transparent",
|
|
720
|
+
border: "none",
|
|
721
|
+
padding: "2px 6px",
|
|
722
|
+
cursor: "pointer",
|
|
723
|
+
whiteSpace: "nowrap",
|
|
724
|
+
color: turnCollapsed ? "#7ee2a8" : "#8b96ab"
|
|
725
|
+
},
|
|
726
|
+
children: turnCollapsed ? t("expand.turn") : t("collapse.turn")
|
|
727
|
+
})
|
|
728
|
+
]
|
|
729
|
+
}),
|
|
730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
731
|
+
style: { color: "#c7cede" },
|
|
732
|
+
children: hover.mark.preview !== "" ? hover.mark.preview : t("no.text")
|
|
733
|
+
}),
|
|
734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
735
|
+
style: {
|
|
736
|
+
display: "flex",
|
|
737
|
+
flexWrap: "wrap",
|
|
738
|
+
gap: 8,
|
|
739
|
+
color: "#8b96ab",
|
|
740
|
+
fontSize: 11,
|
|
741
|
+
marginTop: 4
|
|
742
|
+
},
|
|
743
|
+
children: [
|
|
744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(relativeTime.key, { n: relativeTime.n }) }),
|
|
745
|
+
hover.durationLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("duration.label", { name: hover.durationLabel }) }),
|
|
746
|
+
hover.reasonLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.reasonLabel }),
|
|
747
|
+
hover.ttftLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("ttft.label", { name: hover.ttftLabel }) }),
|
|
748
|
+
hover.tpsLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.tpsLabel })
|
|
749
|
+
]
|
|
750
|
+
}),
|
|
751
|
+
(hover.modelLabel !== null || hover.purposeLabel !== null || hover.tokensLabel !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
752
|
+
style: {
|
|
753
|
+
display: "flex",
|
|
754
|
+
flexWrap: "wrap",
|
|
755
|
+
gap: 8,
|
|
756
|
+
color: "#8b96ab",
|
|
757
|
+
fontSize: 11,
|
|
758
|
+
marginTop: 4
|
|
759
|
+
},
|
|
760
|
+
children: [
|
|
761
|
+
hover.modelLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
762
|
+
"data-model": hover.modelLabel,
|
|
763
|
+
children: hover.modelLabel
|
|
764
|
+
}),
|
|
765
|
+
hover.purposeLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
766
|
+
"data-purpose": hover.purposeLabel,
|
|
767
|
+
children: hover.purposeLabel
|
|
768
|
+
}),
|
|
769
|
+
hover.tokensLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
770
|
+
"data-tokens": hover.tokensLabel,
|
|
771
|
+
children: hover.tokensLabel
|
|
772
|
+
})
|
|
773
|
+
]
|
|
774
|
+
})
|
|
775
|
+
]
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
236
779
|
//#region src/client/useCurrentAnchor.ts
|
|
237
780
|
/**
|
|
238
781
|
* useCurrentAnchor: tracks which user-message row sits at/just above the
|
|
@@ -332,6 +875,21 @@ window.__ModuleLoader__.load({
|
|
|
332
875
|
/** Minimum user messages before the rail adds value. */
|
|
333
876
|
const MIN_MARKS = 2;
|
|
334
877
|
const PREVIEW_LENGTH = 80;
|
|
878
|
+
/** Stable no-bookmarks fallback for render paths without the store seat. */
|
|
879
|
+
const NO_BOOKMARKS = [];
|
|
880
|
+
/** Stable no-kinds fallback for marks whose turn carries no badge nodes. */
|
|
881
|
+
const NO_KINDS = [];
|
|
882
|
+
/**
|
|
883
|
+
* Self-contained pulse keyframes for the transient badges (running/awaiting):
|
|
884
|
+
* an expanding currentColor ring on box-shadow plus an opacity beat, driven by
|
|
885
|
+
* `animation` on the badge ring span (kept in an inline <style> so the plugin
|
|
886
|
+
* stays zero-asset).
|
|
887
|
+
*/
|
|
888
|
+
const BADGE_PULSE_CSS = `@keyframes milestone-badge-pulse {
|
|
889
|
+
0% { box-shadow: 0 0 0 0 currentColor; opacity: 0.85 }
|
|
890
|
+
70% { box-shadow: 0 0 0 5px transparent; opacity: 0.35 }
|
|
891
|
+
100% { box-shadow: 0 0 0 0 transparent; opacity: 0.85 }
|
|
892
|
+
}`;
|
|
335
893
|
/** Visual dot diameter (px). */
|
|
336
894
|
const DOT_SIZE = 12;
|
|
337
895
|
/** Hit area per dot (px) — larger than the dot for comfortable clicking. */
|
|
@@ -352,32 +910,12 @@ window.__ModuleLoader__.load({
|
|
|
352
910
|
function extractPreview(content) {
|
|
353
911
|
return extractText(content).slice(0, PREVIEW_LENGTH);
|
|
354
912
|
}
|
|
355
|
-
/** Relative wall-clock label for a Unix-epoch-ms timestamp. */
|
|
356
|
-
function formatRelativeTime(time) {
|
|
357
|
-
const diff = Date.now() - time;
|
|
358
|
-
if (diff < 6e4) return "刚刚";
|
|
359
|
-
if (diff < 36e5) return `${Math.floor(diff / 6e4)} 分钟前`;
|
|
360
|
-
if (diff < 864e5) return `${Math.floor(diff / 36e5)} 小时前`;
|
|
361
|
-
return `${Math.floor(diff / 864e5)} 天前`;
|
|
362
|
-
}
|
|
363
913
|
/** Compact duration label (ms). */
|
|
364
914
|
function formatDuration(ms) {
|
|
365
915
|
if (ms < 1e3) return `${ms}ms`;
|
|
366
916
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
367
917
|
return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
368
918
|
}
|
|
369
|
-
/** Human label for a TurnEndReason kind. */
|
|
370
|
-
function reasonLabelOf(kind) {
|
|
371
|
-
switch (kind) {
|
|
372
|
-
case "completed": return "已完成";
|
|
373
|
-
case "aborted": return "已中止";
|
|
374
|
-
case "error": return "出错";
|
|
375
|
-
case "max-tokens": return "达到上限";
|
|
376
|
-
case "interrupted": return "已中断";
|
|
377
|
-
case "blocked": return "已阻塞";
|
|
378
|
-
default: return kind;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
919
|
/** Read the ui-conversation 'turn-tail' location data (ttftMs/tokensPerSecond). */
|
|
382
920
|
function turnTailOf(turn) {
|
|
383
921
|
const data = turn.data;
|
|
@@ -385,14 +923,22 @@ window.__ModuleLoader__.load({
|
|
|
385
923
|
return data.get("turn-tail");
|
|
386
924
|
}
|
|
387
925
|
/**
|
|
388
|
-
* @param props - session standard kit (useSession, sessionId, useProjection)
|
|
926
|
+
* @param props - session standard kit (useSession, sessionId, useProjection),
|
|
927
|
+
* the injected loadOlder/forkAt actions, the bookmarks store pair (useStore +
|
|
928
|
+
* actions, injected by the framework from the declared store seat), and the
|
|
929
|
+
* framework-synthesized `t` locale interpreter (registered via the entry's
|
|
930
|
+
* `locale: 'dsh-milestone'`; defaults to a key-pass fallback for renders
|
|
931
|
+
* outside the slot machinery).
|
|
389
932
|
*/
|
|
390
|
-
function MilestoneRail({ useSession, loadOlder }) {
|
|
933
|
+
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, t = (key) => key }) {
|
|
391
934
|
const order = useSession((s) => s.chat.order);
|
|
392
935
|
const nodes = useSession((s) => s.chat.nodes);
|
|
936
|
+
const locations = useSession((s) => s.chat.locations);
|
|
393
937
|
const timeline = useSession((s) => s.chat.timeline);
|
|
938
|
+
const trajectoryRequests = useSession((s) => s.views.get("trajectory")?.requests);
|
|
394
939
|
const hasMore = useSession((s) => s.hasMore);
|
|
395
940
|
const loadingOlder = useSession((s) => s.loadingOlder);
|
|
941
|
+
const bookmarkedKeys = useStore?.((s) => s.keys) ?? NO_BOOKMARKS;
|
|
396
942
|
const marks = (0, react.useMemo)(() => {
|
|
397
943
|
const result = [];
|
|
398
944
|
for (const key of order) {
|
|
@@ -411,6 +957,22 @@ window.__ModuleLoader__.load({
|
|
|
411
957
|
}
|
|
412
958
|
return result;
|
|
413
959
|
}, [order, nodes]);
|
|
960
|
+
const kindsByTurn = (0, react.useMemo)(() => {
|
|
961
|
+
const result = /* @__PURE__ */ new Map();
|
|
962
|
+
for (const node of nodes.values()) {
|
|
963
|
+
if (node.kind !== "turn-error" && node.kind !== "turn-max-tokens" && node.kind !== "model-retry") continue;
|
|
964
|
+
if (node.kind === "model-retry") {
|
|
965
|
+
if (node.data?.retryState === "cancelled") continue;
|
|
966
|
+
}
|
|
967
|
+
if (node.location.kind !== "turn" && node.location.kind !== "step") continue;
|
|
968
|
+
const kinds = result.get(node.location.turn.turn) ?? [];
|
|
969
|
+
kinds.push(node.kind);
|
|
970
|
+
result.set(node.location.turn.turn, kinds);
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
}, [order, nodes]);
|
|
974
|
+
const running = useSession((s) => s.running);
|
|
975
|
+
const awaitingInput = useSession((s) => s.pending).length > 0;
|
|
414
976
|
const [railBox, setRailBox] = (0, react.useState)(null);
|
|
415
977
|
const [hover, setHover] = (0, react.useState)(null);
|
|
416
978
|
const [search, setSearch] = (0, react.useState)({
|
|
@@ -418,10 +980,40 @@ window.__ModuleLoader__.load({
|
|
|
418
980
|
activePos: 0,
|
|
419
981
|
panelOpen: false
|
|
420
982
|
});
|
|
983
|
+
const [bookmarksOnly, setBookmarksOnly] = (0, react.useState)(false);
|
|
984
|
+
const [copiedKey, setCopiedKey] = (0, react.useState)(null);
|
|
985
|
+
const [forkedKey, setForkedKey] = (0, react.useState)(null);
|
|
986
|
+
const [collapsedTurns, setCollapsedTurns] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
987
|
+
const [focusIndex, setFocusIndex] = (0, react.useState)(0);
|
|
988
|
+
const listRef = (0, react.useRef)(null);
|
|
421
989
|
const currentKey = useCurrentAnchor(order);
|
|
422
|
-
const
|
|
990
|
+
const displayMarks = (0, react.useMemo)(() => {
|
|
991
|
+
if (!bookmarksOnly) return marks;
|
|
992
|
+
return filterByBookmarks(marks, bookmarkedKeys).visible.map((i) => marks[i]);
|
|
993
|
+
}, [
|
|
994
|
+
bookmarksOnly,
|
|
995
|
+
marks,
|
|
996
|
+
bookmarkedKeys
|
|
997
|
+
]);
|
|
998
|
+
const { matches } = (0, react.useMemo)(() => filterMarks(displayMarks, search.query), [displayMarks, search.query]);
|
|
423
999
|
const hasQuery = search.query.trim() !== "";
|
|
424
1000
|
const activeMarkIndex = hasQuery && matches.length > 0 ? matches[Math.min(search.activePos, matches.length - 1)] : -1;
|
|
1001
|
+
const groups = (0, react.useMemo)(() => buildTurnGroups(displayMarks), [displayMarks]);
|
|
1002
|
+
const render = (0, react.useMemo)(() => buildRenderList(groups, collapsedTurns), [groups, collapsedTurns]);
|
|
1003
|
+
const separatorIndices = (0, react.useMemo)(() => new Set(render.separatorsAt), [render]);
|
|
1004
|
+
const collapsedSummaries = (0, react.useMemo)(() => {
|
|
1005
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
1006
|
+
for (const group of groups) if (group.turn !== null && group.marks.length > 1 && collapsedTurns.has(group.turn)) summaries.set(group.marks[group.marks.length - 1].key, group.marks.length);
|
|
1007
|
+
return summaries;
|
|
1008
|
+
}, [groups, collapsedTurns]);
|
|
1009
|
+
const turnMarkCounts = (0, react.useMemo)(() => {
|
|
1010
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1011
|
+
for (const mark of displayMarks) {
|
|
1012
|
+
if (mark.turn === void 0) continue;
|
|
1013
|
+
counts.set(mark.turn, (counts.get(mark.turn) ?? 0) + 1);
|
|
1014
|
+
}
|
|
1015
|
+
return counts;
|
|
1016
|
+
}, [displayMarks]);
|
|
425
1017
|
(0, react.useLayoutEffect)(() => {
|
|
426
1018
|
if (marks.length < MIN_MARKS) {
|
|
427
1019
|
setRailBox(null);
|
|
@@ -446,6 +1038,9 @@ window.__ModuleLoader__.load({
|
|
|
446
1038
|
window.removeEventListener("resize", compute);
|
|
447
1039
|
};
|
|
448
1040
|
}, [marks.length]);
|
|
1041
|
+
(0, react.useLayoutEffect)(() => {
|
|
1042
|
+
setFocusIndex((f) => clampIndex(f, render.items.length));
|
|
1043
|
+
}, [render.items.length]);
|
|
449
1044
|
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
450
1045
|
const jump = (key) => {
|
|
451
1046
|
findRow(key)?.scrollIntoView({
|
|
@@ -482,13 +1077,54 @@ window.__ModuleLoader__.load({
|
|
|
482
1077
|
...s,
|
|
483
1078
|
activePos: next
|
|
484
1079
|
}));
|
|
485
|
-
jump(
|
|
1080
|
+
jump(displayMarks[matches[next]].key);
|
|
486
1081
|
};
|
|
487
1082
|
const onSearchKeyDown = (e) => {
|
|
488
1083
|
if (e.key === "Enter") advanceMatch();
|
|
489
1084
|
if (e.key === "Escape") closeSearch();
|
|
490
1085
|
};
|
|
1086
|
+
/** Focus the dot at `index` (no-op while the list is unmounted). */
|
|
1087
|
+
const focusDotAt = (index) => {
|
|
1088
|
+
listRef.current?.querySelectorAll("[data-rail-dot]")[index]?.focus();
|
|
1089
|
+
};
|
|
1090
|
+
/** Tab lands on the list itself: hand focus to the dot owning the tab stop. */
|
|
1091
|
+
const onListFocus = (e) => {
|
|
1092
|
+
if (e.target !== e.currentTarget) return;
|
|
1093
|
+
focusDotAt(clampIndex(focusIndex, render.items.length));
|
|
1094
|
+
};
|
|
1095
|
+
/**
|
|
1096
|
+
* Roving-tabindex keys: ArrowDown/ArrowUp move focus (wrapping), Home/End
|
|
1097
|
+
* jump to first/last. Enter/Space are deliberately NOT handled — the dots
|
|
1098
|
+
* are real buttons, so native activation fires the jump click untouched
|
|
1099
|
+
* (preventDefault here would swallow it). The rover counts RENDERED dots
|
|
1100
|
+
* (collapsed turns shrink the list).
|
|
1101
|
+
*/
|
|
1102
|
+
const onListKeyDown = (e) => {
|
|
1103
|
+
const count = render.items.length;
|
|
1104
|
+
let next = null;
|
|
1105
|
+
switch (e.key) {
|
|
1106
|
+
case "ArrowDown":
|
|
1107
|
+
next = nextFocusIndex(focusIndex, count, 1);
|
|
1108
|
+
break;
|
|
1109
|
+
case "ArrowUp":
|
|
1110
|
+
next = nextFocusIndex(focusIndex, count, -1);
|
|
1111
|
+
break;
|
|
1112
|
+
case "Home":
|
|
1113
|
+
next = 0;
|
|
1114
|
+
break;
|
|
1115
|
+
case "End":
|
|
1116
|
+
next = count - 1;
|
|
1117
|
+
break;
|
|
1118
|
+
default: return;
|
|
1119
|
+
}
|
|
1120
|
+
e.preventDefault();
|
|
1121
|
+
const target = clampIndex(next, count);
|
|
1122
|
+
setFocusIndex(target);
|
|
1123
|
+
focusDotAt(target);
|
|
1124
|
+
};
|
|
491
1125
|
const buildHover = (mark, index) => {
|
|
1126
|
+
if (copiedKey !== null && mark.key !== copiedKey) setCopiedKey(null);
|
|
1127
|
+
if (forkedKey !== null && mark.key !== forkedKey) setForkedKey(null);
|
|
492
1128
|
const turn = mark.turn !== void 0 ? timeline.turns.get(mark.turn) : void 0;
|
|
493
1129
|
let durationLabel = null;
|
|
494
1130
|
let reasonLabel = null;
|
|
@@ -498,7 +1134,7 @@ window.__ModuleLoader__.load({
|
|
|
498
1134
|
if (turn.start !== void 0 && turn.end !== void 0) durationLabel = formatDuration(turn.end.time - turn.start.time);
|
|
499
1135
|
if (turn.end !== void 0) {
|
|
500
1136
|
const reason = turn.end.data.reason;
|
|
501
|
-
if (reason?.kind !== void 0) reasonLabel =
|
|
1137
|
+
if (reason?.kind !== void 0) reasonLabel = t(reasonKeyOf(reason.kind));
|
|
502
1138
|
}
|
|
503
1139
|
const tail = turnTailOf(turn);
|
|
504
1140
|
if (tail !== void 0) {
|
|
@@ -506,17 +1142,61 @@ window.__ModuleLoader__.load({
|
|
|
506
1142
|
if (tail.tokensPerSecond !== void 0) tpsLabel = `${tail.tokensPerSecond.toFixed(1)} tok/s`;
|
|
507
1143
|
}
|
|
508
1144
|
}
|
|
1145
|
+
const meta = deriveTurnMeta(nodes, locations, mark.turn, trajectoryRequests);
|
|
509
1146
|
return {
|
|
510
1147
|
mark,
|
|
511
1148
|
index,
|
|
512
|
-
total:
|
|
513
|
-
turnLabel: mark.turn !== void 0 ?
|
|
1149
|
+
total: displayMarks.length,
|
|
1150
|
+
turnLabel: mark.turn !== void 0 ? t("turn.label", { n: mark.turn }) : null,
|
|
514
1151
|
durationLabel,
|
|
515
1152
|
reasonLabel,
|
|
516
1153
|
ttftLabel,
|
|
517
|
-
tpsLabel
|
|
1154
|
+
tpsLabel,
|
|
1155
|
+
modelLabel: meta.model,
|
|
1156
|
+
purposeLabel: meta.purpose,
|
|
1157
|
+
tokensLabel: meta.inputTokens !== null && meta.outputTokens !== null ? `${meta.inputTokens} / ${meta.outputTokens} tok` : null,
|
|
1158
|
+
turnMarkCount: mark.turn !== void 0 ? turnMarkCounts.get(mark.turn) ?? 0 : null
|
|
518
1159
|
};
|
|
519
1160
|
};
|
|
1161
|
+
/**
|
|
1162
|
+
* C4: collapse/expand the hovered mark's turn in the rail. The set is
|
|
1163
|
+
* replaced immutably (a turn toggles out when already present); collapsing
|
|
1164
|
+
* keeps the turn's LAST mark visible via buildRenderList.
|
|
1165
|
+
*/
|
|
1166
|
+
const onToggleCollapse = (turn) => {
|
|
1167
|
+
setCollapsedTurns((prev) => {
|
|
1168
|
+
const next = new Set(prev);
|
|
1169
|
+
if (next.has(turn)) next.delete(turn);
|
|
1170
|
+
else next.add(turn);
|
|
1171
|
+
return next;
|
|
1172
|
+
});
|
|
1173
|
+
};
|
|
1174
|
+
/**
|
|
1175
|
+
* T10: flip a mark's bookmark in the persisted store. The store action is
|
|
1176
|
+
* the write path (the engine persists synchronously). The hover re-assert
|
|
1177
|
+
* forces a re-render so the star reflects the toggled state — production
|
|
1178
|
+
* re-renders through the framework's uSES-bound useStore; the component
|
|
1179
|
+
* test harness injects an unsubscribed selector, so this local re-render is
|
|
1180
|
+
* what syncs the DOM there. Both paths converge on the same fresh snapshot.
|
|
1181
|
+
*/
|
|
1182
|
+
const onToggleBookmark = (key) => {
|
|
1183
|
+
actions?.toggle(key);
|
|
1184
|
+
setHover((h) => h === null ? h : { ...h });
|
|
1185
|
+
};
|
|
1186
|
+
/**
|
|
1187
|
+
* C3: copy the hovered mark's FULL message text to the system clipboard.
|
|
1188
|
+
* The acknowledgement only shows when the write actually succeeded.
|
|
1189
|
+
*/
|
|
1190
|
+
const onCopy = async (mark) => {
|
|
1191
|
+
if (await copyText(mark.text)) setCopiedKey(mark.key);
|
|
1192
|
+
};
|
|
1193
|
+
/**
|
|
1194
|
+
* C3: fork the session at the hovered mark, anchoring the cut at its event
|
|
1195
|
+
* seq. The acknowledgement only shows once the fork resolved.
|
|
1196
|
+
*/
|
|
1197
|
+
const onFork = (mark) => {
|
|
1198
|
+
forkAt(mark.seq).then(() => setForkedKey(mark.key));
|
|
1199
|
+
};
|
|
520
1200
|
const showLoadOlder = hasMore && marks.length >= MIN_MARKS;
|
|
521
1201
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
522
1202
|
style: {
|
|
@@ -530,14 +1210,15 @@ window.__ModuleLoader__.load({
|
|
|
530
1210
|
display: "flex",
|
|
531
1211
|
flexDirection: "column"
|
|
532
1212
|
},
|
|
533
|
-
"aria-label": "
|
|
1213
|
+
"aria-label": t("rail.label"),
|
|
534
1214
|
children: [
|
|
1215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BADGE_PULSE_CSS }),
|
|
535
1216
|
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
536
1217
|
type: "button",
|
|
537
1218
|
"data-load-older": true,
|
|
538
1219
|
"data-loading-older": loadingOlder ? "true" : void 0,
|
|
539
|
-
title: "
|
|
540
|
-
"aria-label": "
|
|
1220
|
+
title: t("load.older"),
|
|
1221
|
+
"aria-label": t("load.older"),
|
|
541
1222
|
disabled: loadingOlder,
|
|
542
1223
|
onClick: () => {
|
|
543
1224
|
loadOlder();
|
|
@@ -560,22 +1241,61 @@ window.__ModuleLoader__.load({
|
|
|
560
1241
|
},
|
|
561
1242
|
children: "···"
|
|
562
1243
|
}),
|
|
1244
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1245
|
+
type: "button",
|
|
1246
|
+
"data-bookmarks-toggle": true,
|
|
1247
|
+
"aria-label": t("bookmark.filter"),
|
|
1248
|
+
"aria-pressed": bookmarksOnly,
|
|
1249
|
+
"data-active": bookmarksOnly ? "true" : void 0,
|
|
1250
|
+
onClick: () => setBookmarksOnly((v) => !v),
|
|
1251
|
+
style: {
|
|
1252
|
+
width: DOT_HIT,
|
|
1253
|
+
height: DOT_HIT,
|
|
1254
|
+
flexShrink: 0,
|
|
1255
|
+
display: "flex",
|
|
1256
|
+
alignItems: "center",
|
|
1257
|
+
justifyContent: "center",
|
|
1258
|
+
background: bookmarksOnly ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
1259
|
+
border: "none",
|
|
1260
|
+
padding: 0,
|
|
1261
|
+
cursor: "pointer",
|
|
1262
|
+
color: bookmarksOnly ? "#9db8ff" : "#8b96ab"
|
|
1263
|
+
},
|
|
1264
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1265
|
+
width: "13",
|
|
1266
|
+
height: "13",
|
|
1267
|
+
viewBox: "0 0 24 24",
|
|
1268
|
+
fill: bookmarksOnly ? "currentColor" : "none",
|
|
1269
|
+
stroke: "currentColor",
|
|
1270
|
+
strokeWidth: "2",
|
|
1271
|
+
strokeLinejoin: "round",
|
|
1272
|
+
"aria-hidden": "true",
|
|
1273
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
|
|
1274
|
+
})
|
|
1275
|
+
}),
|
|
563
1276
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailSearchUi, {
|
|
564
1277
|
panelTop: railBox.top,
|
|
565
1278
|
panelRight: railBox.right + DOT_HIT + 8,
|
|
566
1279
|
query: search.query,
|
|
567
1280
|
panelOpen: search.panelOpen,
|
|
568
1281
|
matches: matches.length,
|
|
569
|
-
total:
|
|
1282
|
+
total: displayMarks.length,
|
|
570
1283
|
onToggle: () => setSearch((s) => ({
|
|
571
1284
|
...s,
|
|
572
1285
|
panelOpen: !s.panelOpen
|
|
573
1286
|
})),
|
|
574
1287
|
onQueryChange: updateQuery,
|
|
575
1288
|
onSearchKeyDown,
|
|
576
|
-
onClear: clearSearch
|
|
1289
|
+
onClear: clearSearch,
|
|
1290
|
+
t
|
|
577
1291
|
}),
|
|
578
1292
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1293
|
+
ref: listRef,
|
|
1294
|
+
"data-rail-list": true,
|
|
1295
|
+
tabIndex: 0,
|
|
1296
|
+
"aria-label": t("rail.list"),
|
|
1297
|
+
onFocus: onListFocus,
|
|
1298
|
+
onKeyDown: onListKeyDown,
|
|
579
1299
|
style: {
|
|
580
1300
|
flex: 1,
|
|
581
1301
|
minHeight: 0,
|
|
@@ -587,17 +1307,38 @@ window.__ModuleLoader__.load({
|
|
|
587
1307
|
padding: "6px 0",
|
|
588
1308
|
scrollbarWidth: "none"
|
|
589
1309
|
},
|
|
590
|
-
children:
|
|
1310
|
+
children: render.items.map((item, i) => {
|
|
1311
|
+
const showSeparator = separatorIndices.has(i);
|
|
1312
|
+
const mark = displayMarks[item.displayIndex];
|
|
1313
|
+
const summaryCount = collapsedSummaries.get(mark.key);
|
|
1314
|
+
const bookmarked = isBookmarked(bookmarkedKeys, mark.key);
|
|
591
1315
|
const dotState = markState({
|
|
592
1316
|
key: mark.key,
|
|
593
1317
|
hasQuery,
|
|
594
|
-
isMatch: matches.includes(
|
|
595
|
-
isActive:
|
|
1318
|
+
isMatch: matches.includes(item.displayIndex),
|
|
1319
|
+
isActive: item.displayIndex === activeMarkIndex,
|
|
596
1320
|
isCurrent: !hasQuery && mark.key === currentKey
|
|
597
1321
|
});
|
|
598
1322
|
const isHovered = hover?.mark.key === mark.key;
|
|
599
1323
|
const boxShadow = isHovered ? "0 0 0 3px rgba(77, 124, 254, 0.35)" : dotState === "active" ? "0 0 0 3px rgba(255, 255, 255, 0.9)" : dotState === "current" ? "0 0 0 3px rgba(255, 255, 255, 0.75)" : "none";
|
|
600
|
-
|
|
1324
|
+
const badge = deriveBadge({
|
|
1325
|
+
nodeKinds: mark.turn === void 0 ? NO_KINDS : kindsByTurn.get(mark.turn) ?? NO_KINDS,
|
|
1326
|
+
lastMark: item.displayIndex === displayMarks.length - 1,
|
|
1327
|
+
running,
|
|
1328
|
+
awaitingInput
|
|
1329
|
+
});
|
|
1330
|
+
const ringStyle = badge === null ? null : badgeRingStyle(badge);
|
|
1331
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showSeparator && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1332
|
+
"data-turn-separator": true,
|
|
1333
|
+
"data-turn": mark.turn === void 0 ? void 0 : mark.turn,
|
|
1334
|
+
style: {
|
|
1335
|
+
width: DOT_HIT - 8,
|
|
1336
|
+
height: 1,
|
|
1337
|
+
flexShrink: 0,
|
|
1338
|
+
background: "rgba(139, 150, 171, 0.35)",
|
|
1339
|
+
borderRadius: 1
|
|
1340
|
+
}
|
|
1341
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
601
1342
|
type: "button",
|
|
602
1343
|
style: {
|
|
603
1344
|
width: DOT_HIT,
|
|
@@ -614,90 +1355,65 @@ window.__ModuleLoader__.load({
|
|
|
614
1355
|
onMouseEnter: (e) => {
|
|
615
1356
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
616
1357
|
setHover({
|
|
617
|
-
...buildHover(mark,
|
|
1358
|
+
...buildHover(mark, item.displayIndex),
|
|
618
1359
|
top: rect.top + rect.height / 2
|
|
619
1360
|
});
|
|
620
1361
|
},
|
|
621
|
-
onMouseLeave: () => setHover(null),
|
|
622
1362
|
onClick: () => jump(mark.key),
|
|
623
|
-
"
|
|
1363
|
+
"data-rail-dot": true,
|
|
1364
|
+
"data-collapsed-summary": summaryCount !== void 0 ? "true" : void 0,
|
|
1365
|
+
"data-collapsed-count": summaryCount,
|
|
1366
|
+
tabIndex: focusIndex === i ? 0 : -1,
|
|
1367
|
+
onFocus: () => setFocusIndex(i),
|
|
1368
|
+
"aria-label": t("jump.to", { n: item.displayIndex + 1 }),
|
|
624
1369
|
"aria-current": dotState === "active" ? "true" : void 0,
|
|
625
1370
|
"data-current": dotState === "current" ? "true" : void 0,
|
|
626
1371
|
"data-dimmed": dotState === "dimmed" ? "true" : void 0,
|
|
627
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1372
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1373
|
+
style: {
|
|
1374
|
+
position: "relative",
|
|
1375
|
+
width: DOT_SIZE,
|
|
1376
|
+
height: DOT_SIZE,
|
|
1377
|
+
borderRadius: "50%",
|
|
1378
|
+
background: dotColor(item.displayIndex, marks.length),
|
|
1379
|
+
boxShadow,
|
|
1380
|
+
transition: "transform 120ms ease, opacity 120ms ease",
|
|
1381
|
+
transform: `scale(${isHovered ? 1.35 : dotState === "active" || dotState === "current" ? 1.25 : 1})`,
|
|
1382
|
+
opacity: isHovered || dotState !== "dimmed" ? 1 : .22
|
|
1383
|
+
},
|
|
1384
|
+
"data-bookmarked": bookmarked ? "true" : void 0,
|
|
1385
|
+
children: ringStyle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1386
|
+
"data-badge": badge,
|
|
1387
|
+
style: {
|
|
1388
|
+
position: "absolute",
|
|
1389
|
+
inset: -3,
|
|
1390
|
+
borderRadius: "50%",
|
|
1391
|
+
border: `2px solid ${ringStyle.color}`,
|
|
1392
|
+
color: ringStyle.color,
|
|
1393
|
+
pointerEvents: "none",
|
|
1394
|
+
animation: ringStyle.pulse ? "milestone-badge-pulse 1.4s ease-out infinite" : void 0
|
|
1395
|
+
}
|
|
1396
|
+
})
|
|
1397
|
+
})
|
|
1398
|
+
})] }, mark.key);
|
|
638
1399
|
})
|
|
639
1400
|
}),
|
|
640
|
-
hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
whiteSpace: "pre-wrap",
|
|
655
|
-
wordBreak: "break-word",
|
|
656
|
-
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
657
|
-
zIndex: 101,
|
|
658
|
-
pointerEvents: "none"
|
|
659
|
-
},
|
|
660
|
-
children: [
|
|
661
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
662
|
-
style: {
|
|
663
|
-
display: "flex",
|
|
664
|
-
gap: 8,
|
|
665
|
-
color: "#9aa4b8",
|
|
666
|
-
fontSize: 11,
|
|
667
|
-
marginBottom: 4
|
|
668
|
-
},
|
|
669
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
670
|
-
"第 ",
|
|
671
|
-
hover.index + 1,
|
|
672
|
-
" / ",
|
|
673
|
-
hover.total,
|
|
674
|
-
" 条"
|
|
675
|
-
] }), hover.turnLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.turnLabel })]
|
|
676
|
-
}),
|
|
677
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
678
|
-
style: { color: "#c7cede" },
|
|
679
|
-
children: hover.mark.preview !== "" ? hover.mark.preview : "(无文本)"
|
|
680
|
-
}),
|
|
681
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
682
|
-
style: {
|
|
683
|
-
display: "flex",
|
|
684
|
-
flexWrap: "wrap",
|
|
685
|
-
gap: 8,
|
|
686
|
-
color: "#8b96ab",
|
|
687
|
-
fontSize: 11,
|
|
688
|
-
marginTop: 4
|
|
689
|
-
},
|
|
690
|
-
children: [
|
|
691
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime(hover.mark.time) }),
|
|
692
|
-
hover.durationLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["用时 ", hover.durationLabel] }),
|
|
693
|
-
hover.reasonLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.reasonLabel }),
|
|
694
|
-
hover.ttftLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["首字 ", hover.ttftLabel] }),
|
|
695
|
-
hover.tpsLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.tpsLabel })
|
|
696
|
-
]
|
|
697
|
-
})
|
|
698
|
-
]
|
|
1401
|
+
hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneRailTooltip, {
|
|
1402
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
1403
|
+
hover,
|
|
1404
|
+
bookmarked: isBookmarked(bookmarkedKeys, hover.mark.key),
|
|
1405
|
+
onToggleBookmark: () => onToggleBookmark(hover.mark.key),
|
|
1406
|
+
onCopy,
|
|
1407
|
+
onFork,
|
|
1408
|
+
copied: copiedKey === hover.mark.key,
|
|
1409
|
+
forked: forkedKey === hover.mark.key,
|
|
1410
|
+
turnCollapsed: hover.mark.turn !== void 0 && collapsedTurns.has(hover.mark.turn),
|
|
1411
|
+
onToggleCollapse,
|
|
1412
|
+
onMouseEnter: () => setHover((h) => h),
|
|
1413
|
+
onMouseLeave: () => setHover(null),
|
|
1414
|
+
t
|
|
699
1415
|
}),
|
|
700
|
-
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1416
|
+
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
701
1417
|
"data-window-hint": true,
|
|
702
1418
|
style: {
|
|
703
1419
|
position: "absolute",
|
|
@@ -711,16 +1427,42 @@ window.__ModuleLoader__.load({
|
|
|
711
1427
|
pointerEvents: "none",
|
|
712
1428
|
userSelect: "none"
|
|
713
1429
|
},
|
|
714
|
-
children:
|
|
715
|
-
"已显示 ",
|
|
716
|
-
marks.length,
|
|
717
|
-
" 条 · 还有更早"
|
|
718
|
-
]
|
|
1430
|
+
children: t("window.hint", { n: marks.length })
|
|
719
1431
|
})
|
|
720
1432
|
]
|
|
721
1433
|
});
|
|
722
1434
|
}
|
|
723
1435
|
//#endregion
|
|
1436
|
+
//#region src/client/bookmarkStore.ts
|
|
1437
|
+
/**
|
|
1438
|
+
* Persisted per-session bookmarks store for the milestone rail.
|
|
1439
|
+
*
|
|
1440
|
+
* A thin declarative shell over the harness snapshot-store engine: pure
|
|
1441
|
+
* draft-mutator actions, persisted to localStorage under the key
|
|
1442
|
+
* `dsh-milestone.bookmarks` (+ `.${scopeKey}` for session-scope instances,
|
|
1443
|
+
* resolved by the engine's `create(scopeKey)`). Consumers must call the
|
|
1444
|
+
* FACTORY (never a module-level handle — module-cache identity is a disguised
|
|
1445
|
+
* singleton across plugin reloads).
|
|
1446
|
+
*/
|
|
1447
|
+
/**
|
|
1448
|
+
* Declare the bookmarks store handle. Returns a fresh handle per call; the
|
|
1449
|
+
* framework (or tests) create per-session instances via `create(scopeKey)`.
|
|
1450
|
+
*/
|
|
1451
|
+
function createBookmarksStore() {
|
|
1452
|
+
return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
1453
|
+
init: () => ({ keys: [] }),
|
|
1454
|
+
persist: "dsh-milestone.bookmarks",
|
|
1455
|
+
actions: {
|
|
1456
|
+
toggle: (draft, key) => {
|
|
1457
|
+
draft.keys = toggleKey(draft.keys, key);
|
|
1458
|
+
},
|
|
1459
|
+
clear: (draft) => {
|
|
1460
|
+
draft.keys = [];
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
//#endregion
|
|
724
1466
|
//#region src/client/railInject.ts
|
|
725
1467
|
/**
|
|
726
1468
|
* Wrap a session-bound `loadOlder` call into a safe action closure.
|
|
@@ -740,10 +1482,138 @@ window.__ModuleLoader__.load({
|
|
|
740
1482
|
await binding.session.loadOlder();
|
|
741
1483
|
};
|
|
742
1484
|
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Wrap a session `fork` call into a safe action closure that anchors the cut
|
|
1487
|
+
* at an event seq and always bumps the inherited title.
|
|
1488
|
+
*
|
|
1489
|
+
* - Delegates to `sessions.fork({ sessionId, atSeq, increaseTitle: true })`;
|
|
1490
|
+
* the resolved child id is passed through.
|
|
1491
|
+
* - A rejection propagates unchanged so callers can surface the fork error.
|
|
1492
|
+
*
|
|
1493
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
1494
|
+
* @param sessionId - the session the rail is scoped to.
|
|
1495
|
+
* @returns an action that forks that session at a given event seq.
|
|
1496
|
+
*/
|
|
1497
|
+
function createForkAt(sessions, sessionId) {
|
|
1498
|
+
return (atSeq) => sessions.fork({
|
|
1499
|
+
sessionId,
|
|
1500
|
+
atSeq,
|
|
1501
|
+
increaseTitle: true
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
//#endregion
|
|
1505
|
+
//#region src/client/locales.ts
|
|
1506
|
+
/**
|
|
1507
|
+
* UI strings for the milestone rail, keyed flat (single-language-per-key,
|
|
1508
|
+
* no nesting) so the later i18n threading stays a mechanical
|
|
1509
|
+
* `value.replace('{name}', n)` substitution.
|
|
1510
|
+
*
|
|
1511
|
+
* `zh` is the source of truth and the key registry: it byte-matches the
|
|
1512
|
+
* current hardcoded output of MilestoneRail / MilestoneRailTooltip /
|
|
1513
|
+
* MilestoneRailSearch exactly (each `{n}`/`{m}`/`{name}` slot stands in for
|
|
1514
|
+
* the interpolated number or label), so swapping in these templates is
|
|
1515
|
+
* behavior-preserving. `en` is typed `Record<MilestoneKey, string>` so a
|
|
1516
|
+
* missing English translation is a compile error, not a runtime miss.
|
|
1517
|
+
*/
|
|
1518
|
+
const zh = {
|
|
1519
|
+
/** aria-label on each dot: `跳转到第 ${i + 1} 条消息`. */
|
|
1520
|
+
"jump.to": "跳转到第 {n} 条消息",
|
|
1521
|
+
/** Load-older coverage hint: `已显示 {marks.length} 条 · 还有更早`. */
|
|
1522
|
+
"window.hint": "已显示 {n} 条 · 还有更早",
|
|
1523
|
+
/** Hover turn badge: `第 ${mark.turn} 轮`. */
|
|
1524
|
+
"turn.label": "第 {n} 轮",
|
|
1525
|
+
/** Hover position: `第 {hover.index + 1} / {hover.total} 条`. */
|
|
1526
|
+
"pos.of": "第 {n} / {m} 条",
|
|
1527
|
+
/** Search input placeholder. */
|
|
1528
|
+
"search.placeholder": "搜索消息内容",
|
|
1529
|
+
/** aria-label on the search toggle button and the search input. */
|
|
1530
|
+
"search.label": "搜索消息",
|
|
1531
|
+
/** aria-label on the bookmarks-only filter toggle. */
|
|
1532
|
+
"bookmark.filter": "只看收藏",
|
|
1533
|
+
/** aria-label on the hover tooltip star toggle. */
|
|
1534
|
+
"bookmark.star": "收藏此消息",
|
|
1535
|
+
/** aria-label on the search clear button. */
|
|
1536
|
+
"search.clear": "清空搜索",
|
|
1537
|
+
/** title + aria-label on the load-older `···` button. */
|
|
1538
|
+
"load.older": "加载更早消息",
|
|
1539
|
+
/** aria-label on the rail root. */
|
|
1540
|
+
"rail.label": "会话里程碑",
|
|
1541
|
+
/** aria-label on the dot list. */
|
|
1542
|
+
"rail.list": "会话里程碑列表",
|
|
1543
|
+
/** Hover preview fallback for empty message text. */
|
|
1544
|
+
"no.text": "(无文本)",
|
|
1545
|
+
/** Relative time: `< 60s`. */
|
|
1546
|
+
"time.justNow": "刚刚",
|
|
1547
|
+
/** Relative time: `< 1h`. */
|
|
1548
|
+
"time.minutes": "{n} 分钟前",
|
|
1549
|
+
/** Relative time: `< 1d`. */
|
|
1550
|
+
"time.hours": "{n} 小时前",
|
|
1551
|
+
/** Relative time: `>= 1d`. */
|
|
1552
|
+
"time.days": "{n} 天前",
|
|
1553
|
+
/** Hover duration: `用时 {durationLabel}`. */
|
|
1554
|
+
"duration.label": "用时 {name}",
|
|
1555
|
+
/** Hover TTFT: `首字 {ttftLabel}`. */
|
|
1556
|
+
"ttft.label": "首字 {name}",
|
|
1557
|
+
/** TurnEndReason `completed`. */
|
|
1558
|
+
"reason.completed": "已完成",
|
|
1559
|
+
/** TurnEndReason `aborted`. */
|
|
1560
|
+
"reason.aborted": "已中止",
|
|
1561
|
+
/** TurnEndReason `error`. */
|
|
1562
|
+
"reason.error": "出错",
|
|
1563
|
+
/** TurnEndReason `max-tokens`. */
|
|
1564
|
+
"reason.maxTokens": "达到上限",
|
|
1565
|
+
/** TurnEndReason `interrupted`. */
|
|
1566
|
+
"reason.interrupted": "已中断",
|
|
1567
|
+
/** TurnEndReason `blocked`. */
|
|
1568
|
+
"reason.blocked": "已阻塞",
|
|
1569
|
+
/** Copy-message tooltip action. */
|
|
1570
|
+
"copy.message": "复制消息",
|
|
1571
|
+
/** Fork-from-here tooltip action. */
|
|
1572
|
+
"fork.here": "从此处 fork",
|
|
1573
|
+
/** Collapse-turn tooltip action. */
|
|
1574
|
+
"collapse.turn": "折叠此轮",
|
|
1575
|
+
/** Expand-turn tooltip action. */
|
|
1576
|
+
"expand.turn": "展开此轮"
|
|
1577
|
+
};
|
|
1578
|
+
const en = {
|
|
1579
|
+
"jump.to": "Jump to message {n}",
|
|
1580
|
+
"window.hint": "Showing {n} messages · more below",
|
|
1581
|
+
"turn.label": "Turn {n}",
|
|
1582
|
+
"pos.of": "Message {n} of {m}",
|
|
1583
|
+
"search.placeholder": "Search message content",
|
|
1584
|
+
"search.label": "Search messages",
|
|
1585
|
+
"bookmark.filter": "Bookmarks only",
|
|
1586
|
+
"bookmark.star": "Bookmark this message",
|
|
1587
|
+
"search.clear": "Clear search",
|
|
1588
|
+
"load.older": "Load older messages",
|
|
1589
|
+
"rail.label": "Session milestones",
|
|
1590
|
+
"rail.list": "Session milestone list",
|
|
1591
|
+
"no.text": "(no text)",
|
|
1592
|
+
"time.justNow": "Just now",
|
|
1593
|
+
"time.minutes": "{n} minutes ago",
|
|
1594
|
+
"time.hours": "{n} hours ago",
|
|
1595
|
+
"time.days": "{n} days ago",
|
|
1596
|
+
"duration.label": "Duration {name}",
|
|
1597
|
+
"ttft.label": "First token {name}",
|
|
1598
|
+
"reason.completed": "Completed",
|
|
1599
|
+
"reason.aborted": "Aborted",
|
|
1600
|
+
"reason.error": "Error",
|
|
1601
|
+
"reason.maxTokens": "Max tokens reached",
|
|
1602
|
+
"reason.interrupted": "Interrupted",
|
|
1603
|
+
"reason.blocked": "Blocked",
|
|
1604
|
+
"copy.message": "Copy message",
|
|
1605
|
+
"fork.here": "Fork from here",
|
|
1606
|
+
"collapse.turn": "Collapse turn",
|
|
1607
|
+
"expand.turn": "Expand turn"
|
|
1608
|
+
};
|
|
743
1609
|
//#endregion
|
|
744
1610
|
//#region src/client/index.ts
|
|
745
1611
|
/** Required services (cordis fiber inject). */
|
|
746
|
-
const inject = [
|
|
1612
|
+
const inject = [
|
|
1613
|
+
"slots",
|
|
1614
|
+
"sessions",
|
|
1615
|
+
"locale"
|
|
1616
|
+
];
|
|
747
1617
|
/**
|
|
748
1618
|
* Register the overlay and rail once their slot declarations are on the
|
|
749
1619
|
* ledger. The overlay registers directly against the shipped shell.overlay
|
|
@@ -752,6 +1622,10 @@ window.__ModuleLoader__.load({
|
|
|
752
1622
|
* @param ctx - client root context.
|
|
753
1623
|
*/
|
|
754
1624
|
function apply(ctx) {
|
|
1625
|
+
ctx.effect(() => ctx.locale.register("dsh-milestone", {
|
|
1626
|
+
zh,
|
|
1627
|
+
en
|
|
1628
|
+
}), "dsh-milestone: dictionaries");
|
|
755
1629
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
756
1630
|
name: "shell.overlay",
|
|
757
1631
|
id: "milestone",
|
|
@@ -763,7 +1637,12 @@ window.__ModuleLoader__.load({
|
|
|
763
1637
|
}, MilestoneOverlay));
|
|
764
1638
|
ctx.slots.inject("milestone.rail", () => ctx.slots.register({
|
|
765
1639
|
name: "milestone.rail",
|
|
766
|
-
|
|
1640
|
+
store: createBookmarksStore,
|
|
1641
|
+
locale: "dsh-milestone",
|
|
1642
|
+
inject: (sessionId) => ({
|
|
1643
|
+
loadOlder: createLoadOlder(ctx.sessions, sessionId),
|
|
1644
|
+
forkAt: createForkAt(ctx.sessions, sessionId)
|
|
1645
|
+
})
|
|
767
1646
|
}, MilestoneRail));
|
|
768
1647
|
}
|
|
769
1648
|
//#endregion
|