dsh-milestone 0.4.0 → 0.6.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 +15 -1
- package/lib/client.js +1230 -102
- package/package.json +3 -1
package/lib/client.js
CHANGED
|
@@ -126,6 +126,201 @@ window.__ModuleLoader__.load({
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
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/deep-link-logic.ts
|
|
153
|
+
/**
|
|
154
|
+
* Pure deep-link helpers for the milestone rail: parse the `#msg=<key>` URL
|
|
155
|
+
* hash and rebuild it.
|
|
156
|
+
*
|
|
157
|
+
* The conversation anchor key is treated as an OPAQUE string — it is a
|
|
158
|
+
* length-prefixed node key like `13:input-message<messageId>` or
|
|
159
|
+
* `14:assistant-step3:2` (the length prefix makes naive splitting ambiguous),
|
|
160
|
+
* so the parser never inspects or splits the key itself. Callers match the
|
|
161
|
+
* returned key against the session's marks, which is the real validity check.
|
|
162
|
+
*
|
|
163
|
+
* Percent-encoding: the URL fragment parser percent-encodes `"` `<` `>` `` ` ``
|
|
164
|
+
* and lone `%`, so a hash built with a RAW `<`-containing key would read back
|
|
165
|
+
* from `location.hash` percent-encoded (`13:user%3Cdl-2%3E`) and never match
|
|
166
|
+
* a mark after a refresh. `buildMessageHash` therefore percent-encodes those
|
|
167
|
+
* characters itself (a byte-exact URL), and `parseDeepLinkHash` decodes them
|
|
168
|
+
* back — the key round-trips through the URL untouched.
|
|
169
|
+
*/
|
|
170
|
+
/** The URL hash fragment prefix that carries a message anchor key. */
|
|
171
|
+
const MSG_HASH_PREFIX = "msg=";
|
|
172
|
+
/**
|
|
173
|
+
* Characters the WHATWG URL fragment parser cannot round-trip raw (they are
|
|
174
|
+
* percent-encoded on parse): `"` (0x22), `<` (0x3C), `>` (0x3E), backtick
|
|
175
|
+
* (0x60), and `%` (0x25, so a literal `%` never reads as an escape start).
|
|
176
|
+
*/
|
|
177
|
+
const FRAGMENT_UNSAFE = /["<>\u0060%]/g;
|
|
178
|
+
/**
|
|
179
|
+
* Parse a `location.hash` fragment into the message anchor key it references.
|
|
180
|
+
* @param hash - the raw `location.hash` value: `''` or a fragment starting
|
|
181
|
+
* with `#` (e.g. `#msg=13:input-messageabc`).
|
|
182
|
+
* @returns the anchor key (percent-escapes decoded), or null when the
|
|
183
|
+
* fragment is not a message deep link: empty hash, `#msg=` with an empty
|
|
184
|
+
* value, a `#msg` prefix without the `=`, any other hash shape, a hash
|
|
185
|
+
* missing the leading `#`, or a malformed percent escape.
|
|
186
|
+
*/
|
|
187
|
+
function parseDeepLinkHash(hash) {
|
|
188
|
+
if (!hash.startsWith(`#${MSG_HASH_PREFIX}`)) return null;
|
|
189
|
+
const encoded = hash.slice(5);
|
|
190
|
+
if (encoded === "") return null;
|
|
191
|
+
try {
|
|
192
|
+
return decodeURIComponent(encoded);
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build the URL hash fragment that deep-links to a message anchor key.
|
|
199
|
+
* @param key - the conversation node key (opaque, never parsed here).
|
|
200
|
+
*/
|
|
201
|
+
function buildMessageHash(key) {
|
|
202
|
+
return `#${MSG_HASH_PREFIX}${key.replace(FRAGMENT_UNSAFE, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/client/label-logic.ts
|
|
206
|
+
const MINUTE_MS = 6e4;
|
|
207
|
+
const HOUR_MS = 36e5;
|
|
208
|
+
const DAY_MS = 864e5;
|
|
209
|
+
/**
|
|
210
|
+
* Bucket an elapsed duration into a relative-time label.
|
|
211
|
+
*
|
|
212
|
+
* Buckets on `now - time` in milliseconds: below 60s -> justNow (n=0), below
|
|
213
|
+
* 3600s -> minutes, below 86400s -> hours, otherwise days. `n` is the whole
|
|
214
|
+
* count of the bucket unit (floor). Deterministic for a given `now`.
|
|
215
|
+
*
|
|
216
|
+
* @param time - the event timestamp in ms since epoch.
|
|
217
|
+
* @param now - the reference clock in ms since epoch.
|
|
218
|
+
* @returns the label key and bucket count.
|
|
219
|
+
*/
|
|
220
|
+
function relativeTimeParts(time, now) {
|
|
221
|
+
const diff = now - time;
|
|
222
|
+
if (diff < MINUTE_MS) return {
|
|
223
|
+
key: "time.justNow",
|
|
224
|
+
n: 0
|
|
225
|
+
};
|
|
226
|
+
if (diff < HOUR_MS) return {
|
|
227
|
+
key: "time.minutes",
|
|
228
|
+
n: Math.floor(diff / MINUTE_MS)
|
|
229
|
+
};
|
|
230
|
+
if (diff < DAY_MS) return {
|
|
231
|
+
key: "time.hours",
|
|
232
|
+
n: Math.floor(diff / HOUR_MS)
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
key: "time.days",
|
|
236
|
+
n: Math.floor(diff / DAY_MS)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Map a harness end-reason string to a stable i18n key.
|
|
241
|
+
* @param kind - the raw end-reason string (e.g. 'max-tokens').
|
|
242
|
+
* @returns the i18n key, or the raw kind unchanged when unknown.
|
|
243
|
+
*/
|
|
244
|
+
function reasonKeyOf(kind) {
|
|
245
|
+
switch (kind) {
|
|
246
|
+
case "completed": return "reason.completed";
|
|
247
|
+
case "aborted": return "reason.aborted";
|
|
248
|
+
case "error": return "reason.error";
|
|
249
|
+
case "max-tokens": return "reason.maxTokens";
|
|
250
|
+
case "interrupted": return "reason.interrupted";
|
|
251
|
+
case "blocked": return "reason.blocked";
|
|
252
|
+
default: return kind;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/client/tooltip-logic.ts
|
|
257
|
+
const EMPTY_META = {
|
|
258
|
+
model: null,
|
|
259
|
+
purpose: null,
|
|
260
|
+
inputTokens: null,
|
|
261
|
+
outputTokens: null
|
|
262
|
+
};
|
|
263
|
+
/** True when the value is a plain (non-array, non-null) object. */
|
|
264
|
+
function isRecord(value) {
|
|
265
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Decode a `usage` payload structurally: only numeric `inputTokens` /
|
|
269
|
+
* `outputTokens` survive; anything else (absent, malformed, wrong types)
|
|
270
|
+
* degrades to null — the boundary owns trust, the callers get plain numbers.
|
|
271
|
+
* @param usage - untrusted usage payload (typed `unknown` at runtime).
|
|
272
|
+
* @returns the token counts with null for every missing/malformed field.
|
|
273
|
+
*/
|
|
274
|
+
function decodeUsage(usage) {
|
|
275
|
+
if (!isRecord(usage)) return {
|
|
276
|
+
inputTokens: null,
|
|
277
|
+
outputTokens: null
|
|
278
|
+
};
|
|
279
|
+
return {
|
|
280
|
+
inputTokens: typeof usage.inputTokens === "number" ? usage.inputTokens : null,
|
|
281
|
+
outputTokens: typeof usage.outputTokens === "number" ? usage.outputTokens : null
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/** Resolve model/purpose from a request config, falling back to provenance. */
|
|
285
|
+
function metaFromRecord(record) {
|
|
286
|
+
return {
|
|
287
|
+
model: record.requestConfig?.model ?? record.provenance?.model ?? null,
|
|
288
|
+
purpose: record.requestConfig?.purpose ?? null,
|
|
289
|
+
...decodeUsage(record.usage)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Derive the hover metadata for one turn. Sources, in priority order:
|
|
294
|
+
* 1. the `assistant-step` chat node(s) of the turn — their `data.finalNode`
|
|
295
|
+
* carries the recorded `requestConfig` / `provenance` / `usage`;
|
|
296
|
+
* 2. `trajectoryRequests` — the latest entry whose `turn` matches (used when
|
|
297
|
+
* no assistant-step node yields a model or purpose);
|
|
298
|
+
* 3. all-null when the turn is absent, no node matches, or everything is
|
|
299
|
+
* malformed. Never throws.
|
|
300
|
+
* @param nodes - stable per-key chat node reader (as exposed by the snapshot).
|
|
301
|
+
* @param locations - turn -> ordered node keys index.
|
|
302
|
+
* @param turn - owning turn; undefined yields all-null.
|
|
303
|
+
* @param trajectoryRequests - optional fallback request log.
|
|
304
|
+
* @returns the turn's metadata, null where unknown.
|
|
305
|
+
*/
|
|
306
|
+
function deriveTurnMeta(nodes, locations, turn, trajectoryRequests) {
|
|
307
|
+
if (turn === void 0) return EMPTY_META;
|
|
308
|
+
for (const key of locations.getTurn(turn)) {
|
|
309
|
+
const node = nodes.get(key);
|
|
310
|
+
if (node === void 0 || node.kind !== "assistant-step") continue;
|
|
311
|
+
const finalNode = (isRecord(node.data) ? node.data : void 0)?.finalNode;
|
|
312
|
+
if (!isRecord(finalNode)) continue;
|
|
313
|
+
const meta = metaFromRecord(finalNode);
|
|
314
|
+
if (meta.model !== null || meta.purpose !== null) return meta;
|
|
315
|
+
}
|
|
316
|
+
if (trajectoryRequests !== void 0) {
|
|
317
|
+
let latest;
|
|
318
|
+
for (const request of trajectoryRequests) if (request.turn === turn) latest = request;
|
|
319
|
+
if (latest !== void 0) return metaFromRecord(latest);
|
|
320
|
+
}
|
|
321
|
+
return EMPTY_META;
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
129
324
|
//#region src/client/rail-keyboard.ts
|
|
130
325
|
/**
|
|
131
326
|
* Pure roving-tabindex index math for the milestone rail.
|
|
@@ -242,17 +437,83 @@ window.__ModuleLoader__.load({
|
|
|
242
437
|
return `hsl(218, 88%, ${72 - (total <= 1 ? 0 : index / (total - 1)) * 27}%)`;
|
|
243
438
|
}
|
|
244
439
|
//#endregion
|
|
440
|
+
//#region src/client/turn-group-logic.ts
|
|
441
|
+
/**
|
|
442
|
+
* Partition consecutive marks by turn. Marks with the same numeric turn that
|
|
443
|
+
* appear one after another share a group; each mark with `turn === undefined`
|
|
444
|
+
* becomes its own singleton group with `turn: null`.
|
|
445
|
+
* @param marks - marks in rail order.
|
|
446
|
+
* @returns the groups, in original order, partitioning `marks` exactly.
|
|
447
|
+
*/
|
|
448
|
+
function buildTurnGroups(marks) {
|
|
449
|
+
const groups = [];
|
|
450
|
+
let current;
|
|
451
|
+
for (const mark of marks) {
|
|
452
|
+
if (mark.turn === void 0) {
|
|
453
|
+
current = void 0;
|
|
454
|
+
groups.push({
|
|
455
|
+
turn: null,
|
|
456
|
+
marks: [mark]
|
|
457
|
+
});
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (current !== void 0 && current.turn === mark.turn) current.marks.push(mark);
|
|
461
|
+
else {
|
|
462
|
+
current = {
|
|
463
|
+
turn: mark.turn,
|
|
464
|
+
marks: [mark]
|
|
465
|
+
};
|
|
466
|
+
groups.push(current);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return groups;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Flatten groups into render items, collapsing collapsed turns to their last
|
|
473
|
+
* mark and reporting where separators belong.
|
|
474
|
+
* @param groups - groups from {@link buildTurnGroups} (they partition the
|
|
475
|
+
* original marks array in order, so a running count yields original indices).
|
|
476
|
+
* @param collapsed - turns whose group should collapse to its LAST mark.
|
|
477
|
+
* @returns `items` (one RenderItem per visible dot, in group order) and
|
|
478
|
+
* `separatorsAt` (the index in `items` before which a separator should be
|
|
479
|
+
* inserted at each non-first group boundary; never includes 0).
|
|
480
|
+
*/
|
|
481
|
+
function buildRenderList(groups, collapsed) {
|
|
482
|
+
const items = [];
|
|
483
|
+
const separatorsAt = [];
|
|
484
|
+
let counter = 0;
|
|
485
|
+
for (const group of groups) {
|
|
486
|
+
const startIndex = items.length;
|
|
487
|
+
if (group.turn !== null && collapsed.has(group.turn) && group.marks.length > 1) {
|
|
488
|
+
const last = group.marks[group.marks.length - 1];
|
|
489
|
+
items.push({
|
|
490
|
+
mark: last,
|
|
491
|
+
displayIndex: counter + group.marks.length - 1
|
|
492
|
+
});
|
|
493
|
+
} else for (let i = 0; i < group.marks.length; i++) items.push({
|
|
494
|
+
mark: group.marks[i],
|
|
495
|
+
displayIndex: counter + i
|
|
496
|
+
});
|
|
497
|
+
counter += group.marks.length;
|
|
498
|
+
if (startIndex > 0) separatorsAt.push(startIndex);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
items,
|
|
502
|
+
separatorsAt
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
245
506
|
//#region src/client/MilestoneRailSearch.tsx
|
|
246
507
|
/** Dot diameter (px) — matches the rail's DOT_HIT so the toggle aligns. */
|
|
247
|
-
const DOT_HIT$1 =
|
|
508
|
+
const DOT_HIT$1 = 28;
|
|
248
509
|
/**
|
|
249
510
|
* @param props - the search state slice plus the rail's event handlers.
|
|
250
511
|
*/
|
|
251
|
-
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear }) {
|
|
512
|
+
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear, t }) {
|
|
252
513
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
253
514
|
type: "button",
|
|
254
515
|
"data-search-toggle": true,
|
|
255
|
-
"aria-label": "
|
|
516
|
+
"aria-label": t("search.label"),
|
|
256
517
|
"aria-pressed": panelOpen,
|
|
257
518
|
onClick: onToggle,
|
|
258
519
|
style: {
|
|
@@ -269,8 +530,8 @@ window.__ModuleLoader__.load({
|
|
|
269
530
|
color: panelOpen ? "#9db8ff" : "#8b96ab"
|
|
270
531
|
},
|
|
271
532
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
272
|
-
width: "
|
|
273
|
-
height: "
|
|
533
|
+
width: "16",
|
|
534
|
+
height: "16",
|
|
274
535
|
viewBox: "0 0 24 24",
|
|
275
536
|
fill: "none",
|
|
276
537
|
stroke: "currentColor",
|
|
@@ -288,7 +549,7 @@ window.__ModuleLoader__.load({
|
|
|
288
549
|
position: "fixed",
|
|
289
550
|
top: panelTop,
|
|
290
551
|
right: panelRight,
|
|
291
|
-
width:
|
|
552
|
+
width: "min(220px, calc(100vw - 48px))",
|
|
292
553
|
padding: "10px 12px",
|
|
293
554
|
background: "rgba(20, 24, 32, 0.97)",
|
|
294
555
|
color: "#e6e8ee",
|
|
@@ -304,8 +565,8 @@ window.__ModuleLoader__.load({
|
|
|
304
565
|
},
|
|
305
566
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
306
567
|
"data-rail-search": true,
|
|
307
|
-
"aria-label": "
|
|
308
|
-
placeholder: "
|
|
568
|
+
"aria-label": t("search.label"),
|
|
569
|
+
placeholder: t("search.placeholder"),
|
|
309
570
|
value: query,
|
|
310
571
|
onChange: (e) => onQueryChange(e.target.value),
|
|
311
572
|
onKeyDown: onSearchKeyDown,
|
|
@@ -313,8 +574,9 @@ window.__ModuleLoader__.load({
|
|
|
313
574
|
style: {
|
|
314
575
|
flex: 1,
|
|
315
576
|
minWidth: 0,
|
|
316
|
-
padding: "
|
|
317
|
-
fontSize:
|
|
577
|
+
padding: "6px 10px",
|
|
578
|
+
fontSize: 14,
|
|
579
|
+
lineHeight: 1.4,
|
|
318
580
|
color: "#e6e8ee",
|
|
319
581
|
background: "rgba(255, 255, 255, 0.08)",
|
|
320
582
|
border: "1px solid rgba(255, 255, 255, 0.16)",
|
|
@@ -324,7 +586,7 @@ window.__ModuleLoader__.load({
|
|
|
324
586
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
325
587
|
type: "button",
|
|
326
588
|
"data-search-clear": true,
|
|
327
|
-
"aria-label": "
|
|
589
|
+
"aria-label": t("search.clear"),
|
|
328
590
|
onClick: onClear,
|
|
329
591
|
style: {
|
|
330
592
|
width: 22,
|
|
@@ -340,8 +602,8 @@ window.__ModuleLoader__.load({
|
|
|
340
602
|
color: "#8b96ab"
|
|
341
603
|
},
|
|
342
604
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
343
|
-
width: "
|
|
344
|
-
height: "
|
|
605
|
+
width: "12",
|
|
606
|
+
height: "12",
|
|
345
607
|
viewBox: "0 0 24 24",
|
|
346
608
|
fill: "none",
|
|
347
609
|
stroke: "currentColor",
|
|
@@ -355,7 +617,7 @@ window.__ModuleLoader__.load({
|
|
|
355
617
|
"data-match-count": true,
|
|
356
618
|
style: {
|
|
357
619
|
marginTop: 6,
|
|
358
|
-
fontSize:
|
|
620
|
+
fontSize: 13,
|
|
359
621
|
color: "#8b96ab"
|
|
360
622
|
},
|
|
361
623
|
children: [
|
|
@@ -367,11 +629,109 @@ window.__ModuleLoader__.load({
|
|
|
367
629
|
})] });
|
|
368
630
|
}
|
|
369
631
|
//#endregion
|
|
632
|
+
//#region src/client/MilestoneListPanel.tsx
|
|
633
|
+
/**
|
|
634
|
+
* @param props - the panel anchor, the full marks array, and the rail's jump handler.
|
|
635
|
+
*/
|
|
636
|
+
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, t }) {
|
|
637
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
638
|
+
"data-milestone-list": true,
|
|
639
|
+
style: {
|
|
640
|
+
position: "fixed",
|
|
641
|
+
top: panelTop,
|
|
642
|
+
right: panelRight,
|
|
643
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
644
|
+
padding: "10px 12px",
|
|
645
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
646
|
+
color: "#e6e8ee",
|
|
647
|
+
borderRadius: 8,
|
|
648
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
649
|
+
zIndex: 103
|
|
650
|
+
},
|
|
651
|
+
children: [
|
|
652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-list-item]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
653
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
654
|
+
style: {
|
|
655
|
+
display: "flex",
|
|
656
|
+
alignItems: "center",
|
|
657
|
+
gap: 6,
|
|
658
|
+
marginBottom: 6
|
|
659
|
+
},
|
|
660
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
661
|
+
style: {
|
|
662
|
+
fontSize: 13,
|
|
663
|
+
fontWeight: 600,
|
|
664
|
+
color: "#e6e8ee"
|
|
665
|
+
},
|
|
666
|
+
children: t("list.label")
|
|
667
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
668
|
+
style: {
|
|
669
|
+
fontSize: 12,
|
|
670
|
+
color: "#8b96ab"
|
|
671
|
+
},
|
|
672
|
+
children: marks.length
|
|
673
|
+
})]
|
|
674
|
+
}),
|
|
675
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
676
|
+
style: {
|
|
677
|
+
maxHeight: 300,
|
|
678
|
+
overflowY: "auto",
|
|
679
|
+
display: "flex",
|
|
680
|
+
flexDirection: "column",
|
|
681
|
+
gap: 2
|
|
682
|
+
},
|
|
683
|
+
children: marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
684
|
+
type: "button",
|
|
685
|
+
"data-list-item": true,
|
|
686
|
+
"data-jump-key": mark.key,
|
|
687
|
+
onClick: () => onJump(mark.key),
|
|
688
|
+
title: mark.preview,
|
|
689
|
+
style: {
|
|
690
|
+
display: "block",
|
|
691
|
+
width: "100%",
|
|
692
|
+
minWidth: 0,
|
|
693
|
+
padding: "6px 8px",
|
|
694
|
+
background: "transparent",
|
|
695
|
+
border: "none",
|
|
696
|
+
borderRadius: 6,
|
|
697
|
+
cursor: "pointer",
|
|
698
|
+
color: "#e6e8ee",
|
|
699
|
+
textAlign: "left"
|
|
700
|
+
},
|
|
701
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
702
|
+
style: {
|
|
703
|
+
fontSize: 12,
|
|
704
|
+
color: "#8b96ab",
|
|
705
|
+
whiteSpace: "nowrap"
|
|
706
|
+
},
|
|
707
|
+
children: [t("pos.of", {
|
|
708
|
+
n: i + 1,
|
|
709
|
+
m: marks.length
|
|
710
|
+
}), mark.turn !== void 0 ? ` · ${t("turn.label", { n: mark.turn })}` : null]
|
|
711
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
712
|
+
style: {
|
|
713
|
+
fontSize: 13,
|
|
714
|
+
lineHeight: 1.4,
|
|
715
|
+
overflow: "hidden",
|
|
716
|
+
textOverflow: "ellipsis",
|
|
717
|
+
whiteSpace: "nowrap"
|
|
718
|
+
},
|
|
719
|
+
children: mark.preview || t("no.text")
|
|
720
|
+
})]
|
|
721
|
+
}, mark.key))
|
|
722
|
+
})
|
|
723
|
+
]
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
370
727
|
//#region src/client/MilestoneRailTooltip.tsx
|
|
371
728
|
/**
|
|
372
729
|
* @param props - the hovered mark + bookmark wiring (see {@link MilestoneRailTooltipProps}).
|
|
373
730
|
*/
|
|
374
|
-
function MilestoneRailTooltip({ hover, bookmarked, onToggleBookmark, onMouseEnter, onMouseLeave, panelRight }) {
|
|
731
|
+
function MilestoneRailTooltip({ hover, bookmarked, onToggleBookmark, onCopy, onFork, copied, forked, turnCollapsed, onToggleCollapse, onMouseEnter, onMouseLeave, panelRight, t }) {
|
|
732
|
+
const relativeTime = relativeTimeParts(hover.mark.time, Date.now());
|
|
733
|
+
const turn = hover.mark.turn;
|
|
734
|
+
const showCollapse = turn !== void 0 && hover.turnMarkCount !== null && hover.turnMarkCount > 1;
|
|
375
735
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
376
736
|
onMouseEnter,
|
|
377
737
|
onMouseLeave,
|
|
@@ -380,14 +740,14 @@ window.__ModuleLoader__.load({
|
|
|
380
740
|
right: panelRight,
|
|
381
741
|
top: hover.top,
|
|
382
742
|
transform: "translateY(-50%)",
|
|
383
|
-
maxWidth:
|
|
743
|
+
maxWidth: "min(300px, calc(100vw - 120px))",
|
|
384
744
|
minWidth: 180,
|
|
385
745
|
padding: "8px 12px",
|
|
386
746
|
background: "rgba(20, 24, 32, 0.96)",
|
|
387
747
|
color: "#e6e8ee",
|
|
388
748
|
borderRadius: 8,
|
|
389
|
-
fontSize:
|
|
390
|
-
lineHeight: 1.
|
|
749
|
+
fontSize: "var(--dsw-font-s-14, 14px)",
|
|
750
|
+
lineHeight: 1.5,
|
|
391
751
|
whiteSpace: "pre-wrap",
|
|
392
752
|
wordBreak: "break-word",
|
|
393
753
|
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
@@ -399,24 +759,23 @@ window.__ModuleLoader__.load({
|
|
|
399
759
|
style: {
|
|
400
760
|
display: "flex",
|
|
401
761
|
alignItems: "center",
|
|
762
|
+
flexWrap: "wrap",
|
|
402
763
|
gap: 8,
|
|
403
764
|
color: "#9aa4b8",
|
|
404
|
-
fontSize:
|
|
765
|
+
fontSize: 13,
|
|
766
|
+
lineHeight: 1.4,
|
|
405
767
|
marginBottom: 4
|
|
406
768
|
},
|
|
407
769
|
children: [
|
|
408
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
409
|
-
|
|
410
|
-
hover.
|
|
411
|
-
|
|
412
|
-
hover.total,
|
|
413
|
-
" 条"
|
|
414
|
-
] }),
|
|
770
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pos.of", {
|
|
771
|
+
n: hover.index + 1,
|
|
772
|
+
m: hover.total
|
|
773
|
+
}) }),
|
|
415
774
|
hover.turnLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.turnLabel }),
|
|
416
775
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
417
776
|
type: "button",
|
|
418
777
|
"data-star": true,
|
|
419
|
-
"aria-label": "
|
|
778
|
+
"aria-label": t("bookmark.star"),
|
|
420
779
|
"aria-pressed": bookmarked,
|
|
421
780
|
"data-starred": bookmarked ? "true" : void 0,
|
|
422
781
|
onClick: (e) => {
|
|
@@ -438,8 +797,8 @@ window.__ModuleLoader__.load({
|
|
|
438
797
|
color: bookmarked ? "#ffd166" : "#8b96ab"
|
|
439
798
|
},
|
|
440
799
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
441
|
-
width: "
|
|
442
|
-
height: "
|
|
800
|
+
width: "14",
|
|
801
|
+
height: "14",
|
|
443
802
|
viewBox: "0 0 24 24",
|
|
444
803
|
fill: bookmarked ? "currentColor" : "none",
|
|
445
804
|
stroke: "currentColor",
|
|
@@ -448,12 +807,79 @@ window.__ModuleLoader__.load({
|
|
|
448
807
|
"aria-hidden": "true",
|
|
449
808
|
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" })
|
|
450
809
|
})
|
|
810
|
+
}),
|
|
811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
812
|
+
type: "button",
|
|
813
|
+
"data-copy-message": true,
|
|
814
|
+
"data-copied": copied ? "true" : void 0,
|
|
815
|
+
onClick: (e) => {
|
|
816
|
+
e.stopPropagation();
|
|
817
|
+
onCopy(hover.mark);
|
|
818
|
+
},
|
|
819
|
+
style: {
|
|
820
|
+
flexShrink: 0,
|
|
821
|
+
display: "flex",
|
|
822
|
+
alignItems: "center",
|
|
823
|
+
justifyContent: "center",
|
|
824
|
+
background: "transparent",
|
|
825
|
+
border: "none",
|
|
826
|
+
padding: "3px 8px",
|
|
827
|
+
cursor: "pointer",
|
|
828
|
+
whiteSpace: "nowrap",
|
|
829
|
+
color: copied ? "#7ee2a8" : "#8b96ab"
|
|
830
|
+
},
|
|
831
|
+
children: t("copy.message")
|
|
832
|
+
}),
|
|
833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
834
|
+
type: "button",
|
|
835
|
+
"data-fork-here": true,
|
|
836
|
+
"data-forked": forked ? "true" : void 0,
|
|
837
|
+
onClick: (e) => {
|
|
838
|
+
e.stopPropagation();
|
|
839
|
+
onFork(hover.mark);
|
|
840
|
+
},
|
|
841
|
+
style: {
|
|
842
|
+
flexShrink: 0,
|
|
843
|
+
display: "flex",
|
|
844
|
+
alignItems: "center",
|
|
845
|
+
justifyContent: "center",
|
|
846
|
+
background: "transparent",
|
|
847
|
+
border: "none",
|
|
848
|
+
padding: "3px 8px",
|
|
849
|
+
cursor: "pointer",
|
|
850
|
+
whiteSpace: "nowrap",
|
|
851
|
+
color: forked ? "#7ee2a8" : "#8b96ab"
|
|
852
|
+
},
|
|
853
|
+
children: t("fork.here")
|
|
854
|
+
}),
|
|
855
|
+
showCollapse && turn !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
856
|
+
type: "button",
|
|
857
|
+
"data-toggle-collapse": true,
|
|
858
|
+
"aria-pressed": turnCollapsed,
|
|
859
|
+
"data-collapsed": turnCollapsed ? "true" : void 0,
|
|
860
|
+
onClick: (e) => {
|
|
861
|
+
e.stopPropagation();
|
|
862
|
+
onToggleCollapse(turn);
|
|
863
|
+
},
|
|
864
|
+
style: {
|
|
865
|
+
flexShrink: 0,
|
|
866
|
+
display: "flex",
|
|
867
|
+
alignItems: "center",
|
|
868
|
+
justifyContent: "center",
|
|
869
|
+
background: "transparent",
|
|
870
|
+
border: "none",
|
|
871
|
+
padding: "3px 8px",
|
|
872
|
+
cursor: "pointer",
|
|
873
|
+
whiteSpace: "nowrap",
|
|
874
|
+
color: turnCollapsed ? "#7ee2a8" : "#8b96ab"
|
|
875
|
+
},
|
|
876
|
+
children: turnCollapsed ? t("expand.turn") : t("collapse.turn")
|
|
451
877
|
})
|
|
452
878
|
]
|
|
453
879
|
}),
|
|
454
880
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
455
881
|
style: { color: "#c7cede" },
|
|
456
|
-
children: hover.mark.preview !== "" ? hover.mark.preview : "
|
|
882
|
+
children: hover.mark.preview !== "" ? hover.mark.preview : t("no.text")
|
|
457
883
|
}),
|
|
458
884
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
459
885
|
style: {
|
|
@@ -461,27 +887,224 @@ window.__ModuleLoader__.load({
|
|
|
461
887
|
flexWrap: "wrap",
|
|
462
888
|
gap: 8,
|
|
463
889
|
color: "#8b96ab",
|
|
464
|
-
fontSize:
|
|
890
|
+
fontSize: 13,
|
|
891
|
+
lineHeight: 1.4,
|
|
465
892
|
marginTop: 4
|
|
466
893
|
},
|
|
467
894
|
children: [
|
|
468
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children:
|
|
469
|
-
hover.durationLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(relativeTime.key, { n: relativeTime.n }) }),
|
|
896
|
+
hover.durationLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("duration.label", { name: hover.durationLabel }) }),
|
|
470
897
|
hover.reasonLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.reasonLabel }),
|
|
471
|
-
hover.ttftLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
898
|
+
hover.ttftLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("ttft.label", { name: hover.ttftLabel }) }),
|
|
472
899
|
hover.tpsLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.tpsLabel })
|
|
473
900
|
]
|
|
901
|
+
}),
|
|
902
|
+
(hover.modelLabel !== null || hover.purposeLabel !== null || hover.tokensLabel !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
903
|
+
style: {
|
|
904
|
+
display: "flex",
|
|
905
|
+
flexWrap: "wrap",
|
|
906
|
+
gap: 8,
|
|
907
|
+
color: "#8b96ab",
|
|
908
|
+
fontSize: 13,
|
|
909
|
+
lineHeight: 1.4,
|
|
910
|
+
marginTop: 4
|
|
911
|
+
},
|
|
912
|
+
children: [
|
|
913
|
+
hover.modelLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
914
|
+
"data-model": hover.modelLabel,
|
|
915
|
+
children: hover.modelLabel
|
|
916
|
+
}),
|
|
917
|
+
hover.purposeLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
918
|
+
"data-purpose": hover.purposeLabel,
|
|
919
|
+
children: hover.purposeLabel
|
|
920
|
+
}),
|
|
921
|
+
hover.tokensLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
922
|
+
"data-tokens": hover.tokensLabel,
|
|
923
|
+
children: hover.tokensLabel
|
|
924
|
+
})
|
|
925
|
+
]
|
|
474
926
|
})
|
|
475
927
|
]
|
|
476
928
|
});
|
|
477
929
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region src/client/MilestoneSessionSearch.tsx
|
|
932
|
+
/**
|
|
933
|
+
* MilestoneSessionSearch: the cross-session search panel (P3) — the fixed
|
|
934
|
+
* chrome pinned to the rail's top that searches EVERY session's message
|
|
935
|
+
* content through the injected `searchSessions` action (the harness
|
|
936
|
+
* `session.search` RPC), lists ranked hits (display title + snippet), and
|
|
937
|
+
* opens the clicked session via `openSession` (the same selection path the
|
|
938
|
+
* sidebar uses).
|
|
939
|
+
*
|
|
940
|
+
* Owns its search lifecycle — debounce + AbortController + status — unlike
|
|
941
|
+
* the rail's in-session search (RailSearchUi), which is a pure presentation
|
|
942
|
+
* slice of MilestoneRail's own state. Mirrors MilestoneListPanel's
|
|
943
|
+
* fixed-panel styling. Renders one DOM contract
|
|
944
|
+
* (`data-session-search` root / `data-session-search-input` /
|
|
945
|
+
* `data-session-search-result` rows / `data-session-search-error` /
|
|
946
|
+
* `data-session-search-more`).
|
|
947
|
+
*/
|
|
948
|
+
/** Debounce window for the cross-session query (ms). */
|
|
949
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
950
|
+
/**
|
|
951
|
+
* @param props - the panel anchor, the close/open/search actions, and the locale interpreter.
|
|
952
|
+
*/
|
|
953
|
+
function MilestoneSessionSearch({ panelTop, panelRight, onClose, searchSessions, openSession, t }) {
|
|
954
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
955
|
+
const [status, setStatus] = (0, react.useState)("idle");
|
|
956
|
+
const [hits, setHits] = (0, react.useState)([]);
|
|
957
|
+
const [hasMore, setHasMore] = (0, react.useState)(false);
|
|
958
|
+
(0, react.useEffect)(() => {
|
|
959
|
+
const trimmed = query.trim();
|
|
960
|
+
if (trimmed === "") {
|
|
961
|
+
setStatus("idle");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const controller = new AbortController();
|
|
965
|
+
setStatus("loading");
|
|
966
|
+
const timer = window.setTimeout(() => {
|
|
967
|
+
searchSessions(trimmed, controller.signal).then((result) => {
|
|
968
|
+
if (controller.signal.aborted) return;
|
|
969
|
+
setHits(result.items);
|
|
970
|
+
setHasMore(result.hasMore);
|
|
971
|
+
setStatus(result.items.length > 0 ? "results" : "empty");
|
|
972
|
+
}, () => {
|
|
973
|
+
if (controller.signal.aborted) return;
|
|
974
|
+
setStatus("error");
|
|
975
|
+
});
|
|
976
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
977
|
+
return () => {
|
|
978
|
+
window.clearTimeout(timer);
|
|
979
|
+
controller.abort();
|
|
980
|
+
};
|
|
981
|
+
}, [query, searchSessions]);
|
|
982
|
+
const onInputKeyDown = (e) => {
|
|
983
|
+
if (e.key === "Escape") onClose();
|
|
984
|
+
};
|
|
985
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
986
|
+
"data-session-search": true,
|
|
987
|
+
style: {
|
|
988
|
+
position: "fixed",
|
|
989
|
+
top: panelTop,
|
|
990
|
+
right: panelRight,
|
|
991
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
992
|
+
padding: "10px 12px",
|
|
993
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
994
|
+
color: "#e6e8ee",
|
|
995
|
+
borderRadius: 8,
|
|
996
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
997
|
+
zIndex: 103
|
|
998
|
+
},
|
|
999
|
+
children: [
|
|
1000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-session-search-result]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
1001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1002
|
+
style: {
|
|
1003
|
+
display: "flex",
|
|
1004
|
+
alignItems: "center",
|
|
1005
|
+
gap: 6,
|
|
1006
|
+
marginBottom: 6
|
|
1007
|
+
},
|
|
1008
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1009
|
+
style: {
|
|
1010
|
+
fontSize: 13,
|
|
1011
|
+
fontWeight: 600,
|
|
1012
|
+
color: "#e6e8ee"
|
|
1013
|
+
},
|
|
1014
|
+
children: t("search.cross")
|
|
1015
|
+
})
|
|
1016
|
+
}),
|
|
1017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1018
|
+
"data-session-search-input": true,
|
|
1019
|
+
"aria-label": t("search.cross"),
|
|
1020
|
+
placeholder: t("search.cross"),
|
|
1021
|
+
value: query,
|
|
1022
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1023
|
+
onKeyDown: onInputKeyDown,
|
|
1024
|
+
autoFocus: true,
|
|
1025
|
+
style: {
|
|
1026
|
+
boxSizing: "border-box",
|
|
1027
|
+
width: "100%",
|
|
1028
|
+
padding: "6px 10px",
|
|
1029
|
+
fontSize: 14,
|
|
1030
|
+
lineHeight: 1.4,
|
|
1031
|
+
color: "#e6e8ee",
|
|
1032
|
+
background: "rgba(255, 255, 255, 0.08)",
|
|
1033
|
+
border: "1px solid rgba(255, 255, 255, 0.16)",
|
|
1034
|
+
borderRadius: 6,
|
|
1035
|
+
outline: "none"
|
|
1036
|
+
}
|
|
1037
|
+
}),
|
|
1038
|
+
status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1039
|
+
"data-session-search-error": true,
|
|
1040
|
+
style: {
|
|
1041
|
+
marginTop: 8,
|
|
1042
|
+
fontSize: 13,
|
|
1043
|
+
color: "#f07c7c"
|
|
1044
|
+
},
|
|
1045
|
+
children: t("search.error")
|
|
1046
|
+
}),
|
|
1047
|
+
status === "results" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1048
|
+
style: {
|
|
1049
|
+
maxHeight: 300,
|
|
1050
|
+
overflowY: "auto",
|
|
1051
|
+
display: "flex",
|
|
1052
|
+
flexDirection: "column",
|
|
1053
|
+
gap: 2,
|
|
1054
|
+
marginTop: 8
|
|
1055
|
+
},
|
|
1056
|
+
children: hits.map((hit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1057
|
+
type: "button",
|
|
1058
|
+
"data-session-search-result": true,
|
|
1059
|
+
onClick: () => {
|
|
1060
|
+
openSession(hit.sessionId);
|
|
1061
|
+
onClose();
|
|
1062
|
+
},
|
|
1063
|
+
title: hit.snippet,
|
|
1064
|
+
style: {
|
|
1065
|
+
display: "block",
|
|
1066
|
+
width: "100%",
|
|
1067
|
+
minWidth: 0,
|
|
1068
|
+
padding: "6px 8px",
|
|
1069
|
+
background: "transparent",
|
|
1070
|
+
border: "none",
|
|
1071
|
+
borderRadius: 6,
|
|
1072
|
+
cursor: "pointer",
|
|
1073
|
+
color: "#e6e8ee",
|
|
1074
|
+
textAlign: "left"
|
|
1075
|
+
},
|
|
1076
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1077
|
+
style: {
|
|
1078
|
+
fontSize: 12,
|
|
1079
|
+
color: "#9db8ff",
|
|
1080
|
+
whiteSpace: "nowrap",
|
|
1081
|
+
overflow: "hidden",
|
|
1082
|
+
textOverflow: "ellipsis"
|
|
1083
|
+
},
|
|
1084
|
+
children: hit.title ?? t("search.untitled")
|
|
1085
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1086
|
+
style: {
|
|
1087
|
+
fontSize: 13,
|
|
1088
|
+
lineHeight: 1.4,
|
|
1089
|
+
color: "#c6ccd8",
|
|
1090
|
+
overflow: "hidden",
|
|
1091
|
+
textOverflow: "ellipsis",
|
|
1092
|
+
whiteSpace: "nowrap"
|
|
1093
|
+
},
|
|
1094
|
+
children: hit.snippet
|
|
1095
|
+
})]
|
|
1096
|
+
}, hit.sessionId))
|
|
1097
|
+
}), hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1098
|
+
"data-session-search-more": true,
|
|
1099
|
+
style: {
|
|
1100
|
+
marginTop: 6,
|
|
1101
|
+
fontSize: 12,
|
|
1102
|
+
color: "#8b96ab"
|
|
1103
|
+
},
|
|
1104
|
+
children: t("search.more")
|
|
1105
|
+
})] })
|
|
1106
|
+
]
|
|
1107
|
+
});
|
|
485
1108
|
}
|
|
486
1109
|
//#endregion
|
|
487
1110
|
//#region src/client/useCurrentAnchor.ts
|
|
@@ -598,15 +1221,38 @@ window.__ModuleLoader__.load({
|
|
|
598
1221
|
70% { box-shadow: 0 0 0 5px transparent; opacity: 0.35 }
|
|
599
1222
|
100% { box-shadow: 0 0 0 0 transparent; opacity: 0.85 }
|
|
600
1223
|
}`;
|
|
1224
|
+
/**
|
|
1225
|
+
* P3 focus mode: dims the harness's AI thinking/scratchpad blocks so the
|
|
1226
|
+
* conversation reads cleaner. The rule targets the stable, un-hashed
|
|
1227
|
+
* `data-variant="think"` attribute on the thinking-block ROOT (the harness
|
|
1228
|
+
* renders it as `data-variant="think"` with `data-state="running|ok"`), so an
|
|
1229
|
+
* overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
|
|
1230
|
+
* opening it, `[data-open]`) restores full opacity. Kept in an inline
|
|
1231
|
+
* <style> so the plugin stays zero-asset — same pattern as BADGE_PULSE_CSS.
|
|
1232
|
+
*/
|
|
1233
|
+
const FOCUS_CSS = `[data-variant="think"] { opacity: 0.4; transition: opacity 0.2s; }
|
|
1234
|
+
[data-variant="think"]:hover, [data-variant="think"] [data-open] { opacity: 1; }`;
|
|
601
1235
|
/** Visual dot diameter (px). */
|
|
602
|
-
const DOT_SIZE =
|
|
1236
|
+
const DOT_SIZE = 14;
|
|
603
1237
|
/** Hit area per dot (px) — larger than the dot for comfortable clicking. */
|
|
604
|
-
const DOT_HIT =
|
|
1238
|
+
const DOT_HIT = 28;
|
|
605
1239
|
/** Vertical gap between dot hit areas (px) — fixed pitch, never scaled. */
|
|
606
|
-
const DOT_GAP =
|
|
1240
|
+
const DOT_GAP = 14;
|
|
607
1241
|
/** Inward offset from the scrollport right edge so the rail clears the scrollbar. */
|
|
608
1242
|
const RAIL_INSET = 14;
|
|
609
1243
|
/**
|
|
1244
|
+
* P3 deep links (`#msg=<anchor-key>`): initial delay before the first
|
|
1245
|
+
* deep-link attempt — the harness scrolls the conversation to the bottom on
|
|
1246
|
+
* load, so the deep link must land AFTER the view mounts.
|
|
1247
|
+
*/
|
|
1248
|
+
const DEEP_LINK_INITIAL_DELAY = 100;
|
|
1249
|
+
/** P3: interval between DOM-row polls while waiting for the target to render. */
|
|
1250
|
+
const DEEP_LINK_POLL_DELAY = 150;
|
|
1251
|
+
/** P3: polls before falling back to a single `loadOlder` fetch. */
|
|
1252
|
+
const DEEP_LINK_MAX_POLLS = 5;
|
|
1253
|
+
/** P3: bounded polls after `loadOlder`, then the deep link gives up silently. */
|
|
1254
|
+
const DEEP_LINK_MAX_RETRY_POLLS = 5;
|
|
1255
|
+
/**
|
|
610
1256
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
611
1257
|
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
612
1258
|
*/
|
|
@@ -624,18 +1270,6 @@ window.__ModuleLoader__.load({
|
|
|
624
1270
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
625
1271
|
return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
626
1272
|
}
|
|
627
|
-
/** Human label for a TurnEndReason kind. */
|
|
628
|
-
function reasonLabelOf(kind) {
|
|
629
|
-
switch (kind) {
|
|
630
|
-
case "completed": return "已完成";
|
|
631
|
-
case "aborted": return "已中止";
|
|
632
|
-
case "error": return "出错";
|
|
633
|
-
case "max-tokens": return "达到上限";
|
|
634
|
-
case "interrupted": return "已中断";
|
|
635
|
-
case "blocked": return "已阻塞";
|
|
636
|
-
default: return kind;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
1273
|
/** Read the ui-conversation 'turn-tail' location data (ttftMs/tokensPerSecond). */
|
|
640
1274
|
function turnTailOf(turn) {
|
|
641
1275
|
const data = turn.data;
|
|
@@ -644,13 +1278,21 @@ window.__ModuleLoader__.load({
|
|
|
644
1278
|
}
|
|
645
1279
|
/**
|
|
646
1280
|
* @param props - session standard kit (useSession, sessionId, useProjection),
|
|
647
|
-
* the injected loadOlder
|
|
648
|
-
* actions, injected by the framework from the declared store seat)
|
|
1281
|
+
* the injected loadOlder/forkAt actions, the bookmarks store pair (useStore +
|
|
1282
|
+
* actions, injected by the framework from the declared store seat), and the
|
|
1283
|
+
* framework-synthesized `t` locale interpreter (registered via the entry's
|
|
1284
|
+
* `locale: 'dsh-milestone'`; defaults to a key-pass fallback for renders
|
|
1285
|
+
* outside the slot machinery).
|
|
649
1286
|
*/
|
|
650
|
-
function MilestoneRail({ useSession, loadOlder, useStore, actions
|
|
1287
|
+
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
1288
|
+
items: [],
|
|
1289
|
+
hasMore: false
|
|
1290
|
+
}), openSession = () => {}, t = (key) => key }) {
|
|
651
1291
|
const order = useSession((s) => s.chat.order);
|
|
652
1292
|
const nodes = useSession((s) => s.chat.nodes);
|
|
1293
|
+
const locations = useSession((s) => s.chat.locations);
|
|
653
1294
|
const timeline = useSession((s) => s.chat.timeline);
|
|
1295
|
+
const trajectoryRequests = useSession((s) => s.views.get("trajectory")?.requests);
|
|
654
1296
|
const hasMore = useSession((s) => s.hasMore);
|
|
655
1297
|
const loadingOlder = useSession((s) => s.loadingOlder);
|
|
656
1298
|
const bookmarkedKeys = useStore?.((s) => s.keys) ?? NO_BOOKMARKS;
|
|
@@ -696,9 +1338,33 @@ window.__ModuleLoader__.load({
|
|
|
696
1338
|
panelOpen: false
|
|
697
1339
|
});
|
|
698
1340
|
const [bookmarksOnly, setBookmarksOnly] = (0, react.useState)(false);
|
|
1341
|
+
const [focusActive, setFocusActive] = (0, react.useState)(false);
|
|
1342
|
+
const [listOpen, setListOpen] = (0, react.useState)(false);
|
|
1343
|
+
const [crossOpen, setCrossOpen] = (0, react.useState)(false);
|
|
1344
|
+
const [copiedKey, setCopiedKey] = (0, react.useState)(null);
|
|
1345
|
+
const [forkedKey, setForkedKey] = (0, react.useState)(null);
|
|
1346
|
+
const [collapsedTurns, setCollapsedTurns] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
699
1347
|
const [focusIndex, setFocusIndex] = (0, react.useState)(0);
|
|
700
1348
|
const listRef = (0, react.useRef)(null);
|
|
701
1349
|
const currentKey = useCurrentAnchor(order);
|
|
1350
|
+
/**
|
|
1351
|
+
* P3: jump to the chat row with the given node key — smooth-scroll it into
|
|
1352
|
+
* view and write the position back into the URL hash (`#msg=<key>`) so
|
|
1353
|
+
* refresh and share preserve it. `history.replaceState` (not a
|
|
1354
|
+
* `location.hash` assignment) keeps the history stack clean, and it never
|
|
1355
|
+
* fires `hashchange`, so the deep-link listeners below never echo the
|
|
1356
|
+
* rail's own updates. No-op when the row is not (yet) rendered — the
|
|
1357
|
+
* deep-link mount retry and the load-older flow cover that case.
|
|
1358
|
+
*/
|
|
1359
|
+
const jump = (key) => {
|
|
1360
|
+
const row = findRow(key);
|
|
1361
|
+
if (row === null) return;
|
|
1362
|
+
row.scrollIntoView({
|
|
1363
|
+
behavior: "smooth",
|
|
1364
|
+
block: "start"
|
|
1365
|
+
});
|
|
1366
|
+
history.replaceState(null, "", buildMessageHash(key));
|
|
1367
|
+
};
|
|
702
1368
|
const displayMarks = (0, react.useMemo)(() => {
|
|
703
1369
|
if (!bookmarksOnly) return marks;
|
|
704
1370
|
return filterByBookmarks(marks, bookmarkedKeys).visible.map((i) => marks[i]);
|
|
@@ -710,6 +1376,22 @@ window.__ModuleLoader__.load({
|
|
|
710
1376
|
const { matches } = (0, react.useMemo)(() => filterMarks(displayMarks, search.query), [displayMarks, search.query]);
|
|
711
1377
|
const hasQuery = search.query.trim() !== "";
|
|
712
1378
|
const activeMarkIndex = hasQuery && matches.length > 0 ? matches[Math.min(search.activePos, matches.length - 1)] : -1;
|
|
1379
|
+
const groups = (0, react.useMemo)(() => buildTurnGroups(displayMarks), [displayMarks]);
|
|
1380
|
+
const render = (0, react.useMemo)(() => buildRenderList(groups, collapsedTurns), [groups, collapsedTurns]);
|
|
1381
|
+
const separatorIndices = (0, react.useMemo)(() => new Set(render.separatorsAt), [render]);
|
|
1382
|
+
const collapsedSummaries = (0, react.useMemo)(() => {
|
|
1383
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
1384
|
+
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);
|
|
1385
|
+
return summaries;
|
|
1386
|
+
}, [groups, collapsedTurns]);
|
|
1387
|
+
const turnMarkCounts = (0, react.useMemo)(() => {
|
|
1388
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1389
|
+
for (const mark of displayMarks) {
|
|
1390
|
+
if (mark.turn === void 0) continue;
|
|
1391
|
+
counts.set(mark.turn, (counts.get(mark.turn) ?? 0) + 1);
|
|
1392
|
+
}
|
|
1393
|
+
return counts;
|
|
1394
|
+
}, [displayMarks]);
|
|
713
1395
|
(0, react.useLayoutEffect)(() => {
|
|
714
1396
|
if (marks.length < MIN_MARKS) {
|
|
715
1397
|
setRailBox(null);
|
|
@@ -735,15 +1417,61 @@ window.__ModuleLoader__.load({
|
|
|
735
1417
|
};
|
|
736
1418
|
}, [marks.length]);
|
|
737
1419
|
(0, react.useLayoutEffect)(() => {
|
|
738
|
-
setFocusIndex((f) => clampIndex(f,
|
|
739
|
-
}, [
|
|
1420
|
+
setFocusIndex((f) => clampIndex(f, render.items.length));
|
|
1421
|
+
}, [render.items.length]);
|
|
1422
|
+
(0, react.useEffect)(() => {
|
|
1423
|
+
if (!listOpen && !crossOpen) return;
|
|
1424
|
+
const onKey = (e) => {
|
|
1425
|
+
if (e.key !== "Escape") return;
|
|
1426
|
+
setListOpen(false);
|
|
1427
|
+
setCrossOpen(false);
|
|
1428
|
+
};
|
|
1429
|
+
window.addEventListener("keydown", onKey);
|
|
1430
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1431
|
+
}, [listOpen, crossOpen]);
|
|
1432
|
+
const marksRef = (0, react.useRef)(marks);
|
|
1433
|
+
(0, react.useEffect)(() => {
|
|
1434
|
+
marksRef.current = marks;
|
|
1435
|
+
});
|
|
1436
|
+
(0, react.useEffect)(() => {
|
|
1437
|
+
const key = parseDeepLinkHash(window.location.hash);
|
|
1438
|
+
if (key === null) return;
|
|
1439
|
+
let cancelled = false;
|
|
1440
|
+
let timer;
|
|
1441
|
+
const attempt = (pollsLeft, canLoadOlder) => {
|
|
1442
|
+
if (cancelled) return;
|
|
1443
|
+
if (findRow(key) !== null) {
|
|
1444
|
+
jump(key);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (marksRef.current.length > 0 && !marksRef.current.some((m) => m.key === key)) return;
|
|
1448
|
+
if (pollsLeft > 0) {
|
|
1449
|
+
timer = window.setTimeout(() => attempt(pollsLeft - 1, canLoadOlder), DEEP_LINK_POLL_DELAY);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (canLoadOlder) {
|
|
1453
|
+
loadOlder().then(() => {
|
|
1454
|
+
timer = window.setTimeout(() => attempt(DEEP_LINK_MAX_RETRY_POLLS, false), DEEP_LINK_POLL_DELAY);
|
|
1455
|
+
}, () => {});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
timer = window.setTimeout(() => attempt(DEEP_LINK_MAX_POLLS, true), DEEP_LINK_INITIAL_DELAY);
|
|
1460
|
+
return () => {
|
|
1461
|
+
cancelled = true;
|
|
1462
|
+
if (timer !== void 0) window.clearTimeout(timer);
|
|
1463
|
+
};
|
|
1464
|
+
}, []);
|
|
1465
|
+
(0, react.useEffect)(() => {
|
|
1466
|
+
const onHashChange = () => {
|
|
1467
|
+
const key = parseDeepLinkHash(window.location.hash);
|
|
1468
|
+
if (key === null) return;
|
|
1469
|
+
if (marksRef.current.some((m) => m.key === key)) jump(key);
|
|
1470
|
+
};
|
|
1471
|
+
window.addEventListener("hashchange", onHashChange);
|
|
1472
|
+
return () => window.removeEventListener("hashchange", onHashChange);
|
|
1473
|
+
}, []);
|
|
740
1474
|
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
741
|
-
const jump = (key) => {
|
|
742
|
-
findRow(key)?.scrollIntoView({
|
|
743
|
-
behavior: "smooth",
|
|
744
|
-
block: "start"
|
|
745
|
-
});
|
|
746
|
-
};
|
|
747
1475
|
const updateQuery = (query) => {
|
|
748
1476
|
setSearch({
|
|
749
1477
|
query,
|
|
@@ -786,16 +1514,17 @@ window.__ModuleLoader__.load({
|
|
|
786
1514
|
/** Tab lands on the list itself: hand focus to the dot owning the tab stop. */
|
|
787
1515
|
const onListFocus = (e) => {
|
|
788
1516
|
if (e.target !== e.currentTarget) return;
|
|
789
|
-
focusDotAt(clampIndex(focusIndex,
|
|
1517
|
+
focusDotAt(clampIndex(focusIndex, render.items.length));
|
|
790
1518
|
};
|
|
791
1519
|
/**
|
|
792
1520
|
* Roving-tabindex keys: ArrowDown/ArrowUp move focus (wrapping), Home/End
|
|
793
1521
|
* jump to first/last. Enter/Space are deliberately NOT handled — the dots
|
|
794
1522
|
* are real buttons, so native activation fires the jump click untouched
|
|
795
|
-
* (preventDefault here would swallow it).
|
|
1523
|
+
* (preventDefault here would swallow it). The rover counts RENDERED dots
|
|
1524
|
+
* (collapsed turns shrink the list).
|
|
796
1525
|
*/
|
|
797
1526
|
const onListKeyDown = (e) => {
|
|
798
|
-
const count =
|
|
1527
|
+
const count = render.items.length;
|
|
799
1528
|
let next = null;
|
|
800
1529
|
switch (e.key) {
|
|
801
1530
|
case "ArrowDown":
|
|
@@ -818,6 +1547,8 @@ window.__ModuleLoader__.load({
|
|
|
818
1547
|
focusDotAt(target);
|
|
819
1548
|
};
|
|
820
1549
|
const buildHover = (mark, index) => {
|
|
1550
|
+
if (copiedKey !== null && mark.key !== copiedKey) setCopiedKey(null);
|
|
1551
|
+
if (forkedKey !== null && mark.key !== forkedKey) setForkedKey(null);
|
|
821
1552
|
const turn = mark.turn !== void 0 ? timeline.turns.get(mark.turn) : void 0;
|
|
822
1553
|
let durationLabel = null;
|
|
823
1554
|
let reasonLabel = null;
|
|
@@ -827,7 +1558,7 @@ window.__ModuleLoader__.load({
|
|
|
827
1558
|
if (turn.start !== void 0 && turn.end !== void 0) durationLabel = formatDuration(turn.end.time - turn.start.time);
|
|
828
1559
|
if (turn.end !== void 0) {
|
|
829
1560
|
const reason = turn.end.data.reason;
|
|
830
|
-
if (reason?.kind !== void 0) reasonLabel =
|
|
1561
|
+
if (reason?.kind !== void 0) reasonLabel = t(reasonKeyOf(reason.kind));
|
|
831
1562
|
}
|
|
832
1563
|
const tail = turnTailOf(turn);
|
|
833
1564
|
if (tail !== void 0) {
|
|
@@ -835,18 +1566,36 @@ window.__ModuleLoader__.load({
|
|
|
835
1566
|
if (tail.tokensPerSecond !== void 0) tpsLabel = `${tail.tokensPerSecond.toFixed(1)} tok/s`;
|
|
836
1567
|
}
|
|
837
1568
|
}
|
|
1569
|
+
const meta = deriveTurnMeta(nodes, locations, mark.turn, trajectoryRequests);
|
|
838
1570
|
return {
|
|
839
1571
|
mark,
|
|
840
1572
|
index,
|
|
841
1573
|
total: displayMarks.length,
|
|
842
|
-
turnLabel: mark.turn !== void 0 ?
|
|
1574
|
+
turnLabel: mark.turn !== void 0 ? t("turn.label", { n: mark.turn }) : null,
|
|
843
1575
|
durationLabel,
|
|
844
1576
|
reasonLabel,
|
|
845
1577
|
ttftLabel,
|
|
846
|
-
tpsLabel
|
|
1578
|
+
tpsLabel,
|
|
1579
|
+
modelLabel: meta.model,
|
|
1580
|
+
purposeLabel: meta.purpose,
|
|
1581
|
+
tokensLabel: meta.inputTokens !== null && meta.outputTokens !== null ? `${meta.inputTokens} / ${meta.outputTokens} tok` : null,
|
|
1582
|
+
turnMarkCount: mark.turn !== void 0 ? turnMarkCounts.get(mark.turn) ?? 0 : null
|
|
847
1583
|
};
|
|
848
1584
|
};
|
|
849
1585
|
/**
|
|
1586
|
+
* C4: collapse/expand the hovered mark's turn in the rail. The set is
|
|
1587
|
+
* replaced immutably (a turn toggles out when already present); collapsing
|
|
1588
|
+
* keeps the turn's LAST mark visible via buildRenderList.
|
|
1589
|
+
*/
|
|
1590
|
+
const onToggleCollapse = (turn) => {
|
|
1591
|
+
setCollapsedTurns((prev) => {
|
|
1592
|
+
const next = new Set(prev);
|
|
1593
|
+
if (next.has(turn)) next.delete(turn);
|
|
1594
|
+
else next.add(turn);
|
|
1595
|
+
return next;
|
|
1596
|
+
});
|
|
1597
|
+
};
|
|
1598
|
+
/**
|
|
850
1599
|
* T10: flip a mark's bookmark in the persisted store. The store action is
|
|
851
1600
|
* the write path (the engine persists synchronously). The hover re-assert
|
|
852
1601
|
* forces a re-render so the star reflects the toggled state — production
|
|
@@ -858,6 +1607,20 @@ window.__ModuleLoader__.load({
|
|
|
858
1607
|
actions?.toggle(key);
|
|
859
1608
|
setHover((h) => h === null ? h : { ...h });
|
|
860
1609
|
};
|
|
1610
|
+
/**
|
|
1611
|
+
* C3: copy the hovered mark's FULL message text to the system clipboard.
|
|
1612
|
+
* The acknowledgement only shows when the write actually succeeded.
|
|
1613
|
+
*/
|
|
1614
|
+
const onCopy = async (mark) => {
|
|
1615
|
+
if (await copyText(mark.text)) setCopiedKey(mark.key);
|
|
1616
|
+
};
|
|
1617
|
+
/**
|
|
1618
|
+
* C3: fork the session at the hovered mark, anchoring the cut at its event
|
|
1619
|
+
* seq. The acknowledgement only shows once the fork resolved.
|
|
1620
|
+
*/
|
|
1621
|
+
const onFork = (mark) => {
|
|
1622
|
+
forkAt(mark.seq).then(() => setForkedKey(mark.key));
|
|
1623
|
+
};
|
|
861
1624
|
const showLoadOlder = hasMore && marks.length >= MIN_MARKS;
|
|
862
1625
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
863
1626
|
style: {
|
|
@@ -869,17 +1632,21 @@ window.__ModuleLoader__.load({
|
|
|
869
1632
|
pointerEvents: "auto",
|
|
870
1633
|
zIndex: 100,
|
|
871
1634
|
display: "flex",
|
|
872
|
-
flexDirection: "column"
|
|
1635
|
+
flexDirection: "column",
|
|
1636
|
+
gap: 6,
|
|
1637
|
+
paddingTop: 6
|
|
873
1638
|
},
|
|
874
|
-
"aria-label": "
|
|
1639
|
+
"aria-label": t("rail.label"),
|
|
1640
|
+
"data-focus-active": focusActive ? "true" : void 0,
|
|
875
1641
|
children: [
|
|
876
1642
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BADGE_PULSE_CSS }),
|
|
1643
|
+
focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: FOCUS_CSS }),
|
|
877
1644
|
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
878
1645
|
type: "button",
|
|
879
1646
|
"data-load-older": true,
|
|
880
1647
|
"data-loading-older": loadingOlder ? "true" : void 0,
|
|
881
|
-
title: "
|
|
882
|
-
"aria-label": "
|
|
1648
|
+
title: t("load.older"),
|
|
1649
|
+
"aria-label": t("load.older"),
|
|
883
1650
|
disabled: loadingOlder,
|
|
884
1651
|
onClick: () => {
|
|
885
1652
|
loadOlder();
|
|
@@ -896,7 +1663,7 @@ window.__ModuleLoader__.load({
|
|
|
896
1663
|
padding: 0,
|
|
897
1664
|
cursor: loadingOlder ? "default" : "pointer",
|
|
898
1665
|
color: loadingOlder ? "#5a6375" : "#8b96ab",
|
|
899
|
-
fontSize:
|
|
1666
|
+
fontSize: 13,
|
|
900
1667
|
lineHeight: 1,
|
|
901
1668
|
letterSpacing: 1
|
|
902
1669
|
},
|
|
@@ -905,7 +1672,7 @@ window.__ModuleLoader__.load({
|
|
|
905
1672
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
906
1673
|
type: "button",
|
|
907
1674
|
"data-bookmarks-toggle": true,
|
|
908
|
-
"aria-label": "
|
|
1675
|
+
"aria-label": t("bookmark.filter"),
|
|
909
1676
|
"aria-pressed": bookmarksOnly,
|
|
910
1677
|
"data-active": bookmarksOnly ? "true" : void 0,
|
|
911
1678
|
onClick: () => setBookmarksOnly((v) => !v),
|
|
@@ -923,8 +1690,8 @@ window.__ModuleLoader__.load({
|
|
|
923
1690
|
color: bookmarksOnly ? "#9db8ff" : "#8b96ab"
|
|
924
1691
|
},
|
|
925
1692
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
926
|
-
width: "
|
|
927
|
-
height: "
|
|
1693
|
+
width: "16",
|
|
1694
|
+
height: "16",
|
|
928
1695
|
viewBox: "0 0 24 24",
|
|
929
1696
|
fill: bookmarksOnly ? "currentColor" : "none",
|
|
930
1697
|
stroke: "currentColor",
|
|
@@ -934,6 +1701,122 @@ window.__ModuleLoader__.load({
|
|
|
934
1701
|
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" })
|
|
935
1702
|
})
|
|
936
1703
|
}),
|
|
1704
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1705
|
+
type: "button",
|
|
1706
|
+
"data-focus-toggle": true,
|
|
1707
|
+
"aria-label": focusActive ? t("focus.off") : t("focus.on"),
|
|
1708
|
+
title: focusActive ? t("focus.off") : t("focus.on"),
|
|
1709
|
+
"aria-pressed": focusActive,
|
|
1710
|
+
onClick: () => setFocusActive((v) => !v),
|
|
1711
|
+
style: {
|
|
1712
|
+
width: DOT_HIT,
|
|
1713
|
+
height: DOT_HIT,
|
|
1714
|
+
flexShrink: 0,
|
|
1715
|
+
display: "flex",
|
|
1716
|
+
alignItems: "center",
|
|
1717
|
+
justifyContent: "center",
|
|
1718
|
+
background: focusActive ? "rgba(126, 226, 168, 0.14)" : "transparent",
|
|
1719
|
+
border: "none",
|
|
1720
|
+
padding: 0,
|
|
1721
|
+
cursor: "pointer",
|
|
1722
|
+
color: focusActive ? "#7ee2a8" : "#8b96ab"
|
|
1723
|
+
},
|
|
1724
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1725
|
+
width: "16",
|
|
1726
|
+
height: "16",
|
|
1727
|
+
viewBox: "0 0 24 24",
|
|
1728
|
+
fill: "none",
|
|
1729
|
+
stroke: "currentColor",
|
|
1730
|
+
strokeWidth: "2",
|
|
1731
|
+
strokeLinecap: "round",
|
|
1732
|
+
strokeLinejoin: "round",
|
|
1733
|
+
"aria-hidden": "true",
|
|
1734
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1735
|
+
cx: "12",
|
|
1736
|
+
cy: "12",
|
|
1737
|
+
r: "3"
|
|
1738
|
+
})]
|
|
1739
|
+
})
|
|
1740
|
+
}),
|
|
1741
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1742
|
+
type: "button",
|
|
1743
|
+
"data-list-toggle": true,
|
|
1744
|
+
"aria-label": listOpen ? t("list.close") : t("list.open"),
|
|
1745
|
+
title: listOpen ? t("list.close") : t("list.open"),
|
|
1746
|
+
"aria-pressed": listOpen,
|
|
1747
|
+
onClick: () => setListOpen((v) => !v),
|
|
1748
|
+
style: {
|
|
1749
|
+
width: DOT_HIT,
|
|
1750
|
+
height: DOT_HIT,
|
|
1751
|
+
flexShrink: 0,
|
|
1752
|
+
display: "flex",
|
|
1753
|
+
alignItems: "center",
|
|
1754
|
+
justifyContent: "center",
|
|
1755
|
+
background: listOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
1756
|
+
border: "none",
|
|
1757
|
+
padding: 0,
|
|
1758
|
+
cursor: "pointer",
|
|
1759
|
+
color: listOpen ? "#9db8ff" : "#8b96ab"
|
|
1760
|
+
},
|
|
1761
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1762
|
+
width: "16",
|
|
1763
|
+
height: "16",
|
|
1764
|
+
viewBox: "0 0 24 24",
|
|
1765
|
+
fill: "none",
|
|
1766
|
+
stroke: "currentColor",
|
|
1767
|
+
strokeWidth: "2.5",
|
|
1768
|
+
strokeLinecap: "round",
|
|
1769
|
+
"aria-hidden": "true",
|
|
1770
|
+
children: [
|
|
1771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
1772
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h18" }),
|
|
1773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h18" })
|
|
1774
|
+
]
|
|
1775
|
+
})
|
|
1776
|
+
}),
|
|
1777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1778
|
+
type: "button",
|
|
1779
|
+
"data-session-search-toggle": true,
|
|
1780
|
+
"aria-label": crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
1781
|
+
title: crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
1782
|
+
"aria-pressed": crossOpen,
|
|
1783
|
+
onClick: () => setCrossOpen((v) => !v),
|
|
1784
|
+
style: {
|
|
1785
|
+
width: DOT_HIT,
|
|
1786
|
+
height: DOT_HIT,
|
|
1787
|
+
flexShrink: 0,
|
|
1788
|
+
display: "flex",
|
|
1789
|
+
alignItems: "center",
|
|
1790
|
+
justifyContent: "center",
|
|
1791
|
+
background: crossOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
1792
|
+
border: "none",
|
|
1793
|
+
padding: 0,
|
|
1794
|
+
cursor: "pointer",
|
|
1795
|
+
color: crossOpen ? "#9db8ff" : "#8b96ab"
|
|
1796
|
+
},
|
|
1797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1798
|
+
width: "16",
|
|
1799
|
+
height: "16",
|
|
1800
|
+
viewBox: "0 0 24 24",
|
|
1801
|
+
fill: "none",
|
|
1802
|
+
stroke: "currentColor",
|
|
1803
|
+
strokeWidth: "2",
|
|
1804
|
+
strokeLinecap: "round",
|
|
1805
|
+
strokeLinejoin: "round",
|
|
1806
|
+
"aria-hidden": "true",
|
|
1807
|
+
children: [
|
|
1808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h9" }),
|
|
1809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h9" }),
|
|
1810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h9" }),
|
|
1811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1812
|
+
cx: "17",
|
|
1813
|
+
cy: "7",
|
|
1814
|
+
r: "3.5"
|
|
1815
|
+
}),
|
|
1816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m19.5 9.5 2.5 2.5" })
|
|
1817
|
+
]
|
|
1818
|
+
})
|
|
1819
|
+
}),
|
|
937
1820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailSearchUi, {
|
|
938
1821
|
panelTop: railBox.top,
|
|
939
1822
|
panelRight: railBox.right + DOT_HIT + 8,
|
|
@@ -947,13 +1830,29 @@ window.__ModuleLoader__.load({
|
|
|
947
1830
|
})),
|
|
948
1831
|
onQueryChange: updateQuery,
|
|
949
1832
|
onSearchKeyDown,
|
|
950
|
-
onClear: clearSearch
|
|
1833
|
+
onClear: clearSearch,
|
|
1834
|
+
t
|
|
1835
|
+
}),
|
|
1836
|
+
listOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneListPanel, {
|
|
1837
|
+
panelTop: railBox.top,
|
|
1838
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
1839
|
+
marks,
|
|
1840
|
+
onJump: jump,
|
|
1841
|
+
t
|
|
1842
|
+
}),
|
|
1843
|
+
crossOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneSessionSearch, {
|
|
1844
|
+
panelTop: railBox.top,
|
|
1845
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
1846
|
+
onClose: () => setCrossOpen(false),
|
|
1847
|
+
searchSessions,
|
|
1848
|
+
openSession,
|
|
1849
|
+
t
|
|
951
1850
|
}),
|
|
952
1851
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
953
1852
|
ref: listRef,
|
|
954
1853
|
"data-rail-list": true,
|
|
955
1854
|
tabIndex: 0,
|
|
956
|
-
"aria-label": "
|
|
1855
|
+
"aria-label": t("rail.list"),
|
|
957
1856
|
onFocus: onListFocus,
|
|
958
1857
|
onKeyDown: onListKeyDown,
|
|
959
1858
|
style: {
|
|
@@ -967,25 +1866,38 @@ window.__ModuleLoader__.load({
|
|
|
967
1866
|
padding: "6px 0",
|
|
968
1867
|
scrollbarWidth: "none"
|
|
969
1868
|
},
|
|
970
|
-
children:
|
|
1869
|
+
children: render.items.map((item, i) => {
|
|
1870
|
+
const showSeparator = separatorIndices.has(i);
|
|
1871
|
+
const mark = displayMarks[item.displayIndex];
|
|
1872
|
+
const summaryCount = collapsedSummaries.get(mark.key);
|
|
971
1873
|
const bookmarked = isBookmarked(bookmarkedKeys, mark.key);
|
|
972
1874
|
const dotState = markState({
|
|
973
1875
|
key: mark.key,
|
|
974
1876
|
hasQuery,
|
|
975
|
-
isMatch: matches.includes(
|
|
976
|
-
isActive:
|
|
1877
|
+
isMatch: matches.includes(item.displayIndex),
|
|
1878
|
+
isActive: item.displayIndex === activeMarkIndex,
|
|
977
1879
|
isCurrent: !hasQuery && mark.key === currentKey
|
|
978
1880
|
});
|
|
979
1881
|
const isHovered = hover?.mark.key === mark.key;
|
|
980
1882
|
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";
|
|
981
1883
|
const badge = deriveBadge({
|
|
982
1884
|
nodeKinds: mark.turn === void 0 ? NO_KINDS : kindsByTurn.get(mark.turn) ?? NO_KINDS,
|
|
983
|
-
lastMark:
|
|
1885
|
+
lastMark: item.displayIndex === displayMarks.length - 1,
|
|
984
1886
|
running,
|
|
985
1887
|
awaitingInput
|
|
986
1888
|
});
|
|
987
1889
|
const ringStyle = badge === null ? null : badgeRingStyle(badge);
|
|
988
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1890
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [showSeparator && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1891
|
+
"data-turn-separator": true,
|
|
1892
|
+
"data-turn": mark.turn === void 0 ? void 0 : mark.turn,
|
|
1893
|
+
style: {
|
|
1894
|
+
width: DOT_HIT - 8,
|
|
1895
|
+
height: 1,
|
|
1896
|
+
flexShrink: 0,
|
|
1897
|
+
background: "rgba(139, 150, 171, 0.35)",
|
|
1898
|
+
borderRadius: 1
|
|
1899
|
+
}
|
|
1900
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
989
1901
|
type: "button",
|
|
990
1902
|
style: {
|
|
991
1903
|
width: DOT_HIT,
|
|
@@ -1002,15 +1914,17 @@ window.__ModuleLoader__.load({
|
|
|
1002
1914
|
onMouseEnter: (e) => {
|
|
1003
1915
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
1004
1916
|
setHover({
|
|
1005
|
-
...buildHover(mark,
|
|
1917
|
+
...buildHover(mark, item.displayIndex),
|
|
1006
1918
|
top: rect.top + rect.height / 2
|
|
1007
1919
|
});
|
|
1008
1920
|
},
|
|
1009
1921
|
onClick: () => jump(mark.key),
|
|
1010
1922
|
"data-rail-dot": true,
|
|
1923
|
+
"data-collapsed-summary": summaryCount !== void 0 ? "true" : void 0,
|
|
1924
|
+
"data-collapsed-count": summaryCount,
|
|
1011
1925
|
tabIndex: focusIndex === i ? 0 : -1,
|
|
1012
1926
|
onFocus: () => setFocusIndex(i),
|
|
1013
|
-
"aria-label":
|
|
1927
|
+
"aria-label": t("jump.to", { n: item.displayIndex + 1 }),
|
|
1014
1928
|
"aria-current": dotState === "active" ? "true" : void 0,
|
|
1015
1929
|
"data-current": dotState === "current" ? "true" : void 0,
|
|
1016
1930
|
"data-dimmed": dotState === "dimmed" ? "true" : void 0,
|
|
@@ -1020,7 +1934,7 @@ window.__ModuleLoader__.load({
|
|
|
1020
1934
|
width: DOT_SIZE,
|
|
1021
1935
|
height: DOT_SIZE,
|
|
1022
1936
|
borderRadius: "50%",
|
|
1023
|
-
background: dotColor(
|
|
1937
|
+
background: dotColor(item.displayIndex, marks.length),
|
|
1024
1938
|
boxShadow,
|
|
1025
1939
|
transition: "transform 120ms ease, opacity 120ms ease",
|
|
1026
1940
|
transform: `scale(${isHovered ? 1.35 : dotState === "active" || dotState === "current" ? 1.25 : 1})`,
|
|
@@ -1040,7 +1954,7 @@ window.__ModuleLoader__.load({
|
|
|
1040
1954
|
}
|
|
1041
1955
|
})
|
|
1042
1956
|
})
|
|
1043
|
-
}, mark.key);
|
|
1957
|
+
})] }, mark.key);
|
|
1044
1958
|
})
|
|
1045
1959
|
}),
|
|
1046
1960
|
hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneRailTooltip, {
|
|
@@ -1048,10 +1962,17 @@ window.__ModuleLoader__.load({
|
|
|
1048
1962
|
hover,
|
|
1049
1963
|
bookmarked: isBookmarked(bookmarkedKeys, hover.mark.key),
|
|
1050
1964
|
onToggleBookmark: () => onToggleBookmark(hover.mark.key),
|
|
1965
|
+
onCopy,
|
|
1966
|
+
onFork,
|
|
1967
|
+
copied: copiedKey === hover.mark.key,
|
|
1968
|
+
forked: forkedKey === hover.mark.key,
|
|
1969
|
+
turnCollapsed: hover.mark.turn !== void 0 && collapsedTurns.has(hover.mark.turn),
|
|
1970
|
+
onToggleCollapse,
|
|
1051
1971
|
onMouseEnter: () => setHover((h) => h),
|
|
1052
|
-
onMouseLeave: () => setHover(null)
|
|
1972
|
+
onMouseLeave: () => setHover(null),
|
|
1973
|
+
t
|
|
1053
1974
|
}),
|
|
1054
|
-
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1975
|
+
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1055
1976
|
"data-window-hint": true,
|
|
1056
1977
|
style: {
|
|
1057
1978
|
position: "absolute",
|
|
@@ -1059,17 +1980,13 @@ window.__ModuleLoader__.load({
|
|
|
1059
1980
|
right: "100%",
|
|
1060
1981
|
marginRight: 8,
|
|
1061
1982
|
whiteSpace: "nowrap",
|
|
1062
|
-
fontSize:
|
|
1983
|
+
fontSize: 12,
|
|
1063
1984
|
lineHeight: 1,
|
|
1064
1985
|
color: "rgba(139, 150, 171, 0.9)",
|
|
1065
1986
|
pointerEvents: "none",
|
|
1066
1987
|
userSelect: "none"
|
|
1067
1988
|
},
|
|
1068
|
-
children:
|
|
1069
|
-
"已显示 ",
|
|
1070
|
-
marks.length,
|
|
1071
|
-
" 条 · 还有更早"
|
|
1072
|
-
]
|
|
1989
|
+
children: t("window.hint", { n: marks.length })
|
|
1073
1990
|
})
|
|
1074
1991
|
]
|
|
1075
1992
|
});
|
|
@@ -1107,6 +2024,46 @@ window.__ModuleLoader__.load({
|
|
|
1107
2024
|
//#endregion
|
|
1108
2025
|
//#region src/client/railInject.ts
|
|
1109
2026
|
/**
|
|
2027
|
+
* Wrap the `session.search` RPC into a safe cross-session search action.
|
|
2028
|
+
*
|
|
2029
|
+
* - `ok: true` unwraps the value and joins each hit's human display title
|
|
2030
|
+
* from the session list snapshot (a session outside the list keeps no
|
|
2031
|
+
* title — the caller falls back to `search.untitled`).
|
|
2032
|
+
* - `ok: false` throws `new Error(error.message)` so the caller can surface
|
|
2033
|
+
* the business/transport error as the `search.error` state.
|
|
2034
|
+
* - A rejected RPC propagates unchanged.
|
|
2035
|
+
*
|
|
2036
|
+
* The list read happens inside the returned closure (never at factory time),
|
|
2037
|
+
* so the title join always reflects the current list snapshot.
|
|
2038
|
+
*
|
|
2039
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
2040
|
+
* @returns an action that searches all sessions' message content.
|
|
2041
|
+
*/
|
|
2042
|
+
function createSessionSearch(sessions) {
|
|
2043
|
+
return async (query, signal) => {
|
|
2044
|
+
const result = await sessions.search(query, signal);
|
|
2045
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
2046
|
+
const byId = sessions.list.getSnapshot().byId;
|
|
2047
|
+
return {
|
|
2048
|
+
items: result.value.items.map((item) => ({
|
|
2049
|
+
...item,
|
|
2050
|
+
title: byId[item.sessionId]?.displayTitle
|
|
2051
|
+
})),
|
|
2052
|
+
hasMore: result.value.hasMore
|
|
2053
|
+
};
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Wrap a session `open` call into a safe action that selects a listed session
|
|
2058
|
+
* as current — the exact selection path the sidebar uses on click.
|
|
2059
|
+
*
|
|
2060
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
2061
|
+
* @returns an action that opens the given session.
|
|
2062
|
+
*/
|
|
2063
|
+
function createOpenSession(sessions) {
|
|
2064
|
+
return (id) => sessions.open(id);
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
1110
2067
|
* Wrap a session-bound `loadOlder` call into a safe action closure.
|
|
1111
2068
|
*
|
|
1112
2069
|
* - Missing binding: resolves (never throws on an unlisted/unscoped session).
|
|
@@ -1124,10 +2081,171 @@ window.__ModuleLoader__.load({
|
|
|
1124
2081
|
await binding.session.loadOlder();
|
|
1125
2082
|
};
|
|
1126
2083
|
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Wrap a session `fork` call into a safe action closure that anchors the cut
|
|
2086
|
+
* at an event seq and always bumps the inherited title.
|
|
2087
|
+
*
|
|
2088
|
+
* - Delegates to `sessions.fork({ sessionId, atSeq, increaseTitle: true })`;
|
|
2089
|
+
* the resolved child id is passed through.
|
|
2090
|
+
* - A rejection propagates unchanged so callers can surface the fork error.
|
|
2091
|
+
*
|
|
2092
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
2093
|
+
* @param sessionId - the session the rail is scoped to.
|
|
2094
|
+
* @returns an action that forks that session at a given event seq.
|
|
2095
|
+
*/
|
|
2096
|
+
function createForkAt(sessions, sessionId) {
|
|
2097
|
+
return (atSeq) => sessions.fork({
|
|
2098
|
+
sessionId,
|
|
2099
|
+
atSeq,
|
|
2100
|
+
increaseTitle: true
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
//#endregion
|
|
2104
|
+
//#region src/client/locales.ts
|
|
2105
|
+
/**
|
|
2106
|
+
* UI strings for the milestone rail, keyed flat (single-language-per-key,
|
|
2107
|
+
* no nesting) so the later i18n threading stays a mechanical
|
|
2108
|
+
* `value.replace('{name}', n)` substitution.
|
|
2109
|
+
*
|
|
2110
|
+
* `zh` is the source of truth and the key registry: it byte-matches the
|
|
2111
|
+
* current hardcoded output of MilestoneRail / MilestoneRailTooltip /
|
|
2112
|
+
* MilestoneRailSearch exactly (each `{n}`/`{m}`/`{name}` slot stands in for
|
|
2113
|
+
* the interpolated number or label), so swapping in these templates is
|
|
2114
|
+
* behavior-preserving. `en` is typed `Record<MilestoneKey, string>` so a
|
|
2115
|
+
* missing English translation is a compile error, not a runtime miss.
|
|
2116
|
+
*/
|
|
2117
|
+
const zh = {
|
|
2118
|
+
/** aria-label on each dot: `跳转到第 ${i + 1} 条消息`. */
|
|
2119
|
+
"jump.to": "跳转到第 {n} 条消息",
|
|
2120
|
+
/** Load-older coverage hint: `已显示 {marks.length} 条 · 还有更早`. */
|
|
2121
|
+
"window.hint": "已显示 {n} 条 · 还有更早",
|
|
2122
|
+
/** Hover turn badge: `第 ${mark.turn} 轮`. */
|
|
2123
|
+
"turn.label": "第 {n} 轮",
|
|
2124
|
+
/** Hover position: `第 {hover.index + 1} / {hover.total} 条`. */
|
|
2125
|
+
"pos.of": "第 {n} / {m} 条",
|
|
2126
|
+
/** Search input placeholder. */
|
|
2127
|
+
"search.placeholder": "搜索消息内容",
|
|
2128
|
+
/** aria-label on the search toggle button and the search input. */
|
|
2129
|
+
"search.label": "搜索消息",
|
|
2130
|
+
/** aria-label on the bookmarks-only filter toggle. */
|
|
2131
|
+
"bookmark.filter": "只看收藏",
|
|
2132
|
+
/** aria-label + title on the focus-mode toggle when focus is OFF (arm it). */
|
|
2133
|
+
"focus.on": "聚焦模式",
|
|
2134
|
+
/** aria-label + title on the focus-mode toggle when focus is ON (disarm it). */
|
|
2135
|
+
"focus.off": "退出聚焦",
|
|
2136
|
+
/** aria-label on the hover tooltip star toggle. */
|
|
2137
|
+
"bookmark.star": "收藏此消息",
|
|
2138
|
+
/** aria-label on the search clear button. */
|
|
2139
|
+
"search.clear": "清空搜索",
|
|
2140
|
+
/** title + aria-label on the load-older `···` button. */
|
|
2141
|
+
"load.older": "加载更早消息",
|
|
2142
|
+
/** aria-label on the rail root. */
|
|
2143
|
+
"rail.label": "会话里程碑",
|
|
2144
|
+
/** aria-label on the dot list. */
|
|
2145
|
+
"rail.list": "会话里程碑列表",
|
|
2146
|
+
/** Hover preview fallback for empty message text. */
|
|
2147
|
+
"no.text": "(无文本)",
|
|
2148
|
+
/** Relative time: `< 60s`. */
|
|
2149
|
+
"time.justNow": "刚刚",
|
|
2150
|
+
/** Relative time: `< 1h`. */
|
|
2151
|
+
"time.minutes": "{n} 分钟前",
|
|
2152
|
+
/** Relative time: `< 1d`. */
|
|
2153
|
+
"time.hours": "{n} 小时前",
|
|
2154
|
+
/** Relative time: `>= 1d`. */
|
|
2155
|
+
"time.days": "{n} 天前",
|
|
2156
|
+
/** Hover duration: `用时 {durationLabel}`. */
|
|
2157
|
+
"duration.label": "用时 {name}",
|
|
2158
|
+
/** Hover TTFT: `首字 {ttftLabel}`. */
|
|
2159
|
+
"ttft.label": "首字 {name}",
|
|
2160
|
+
/** TurnEndReason `completed`. */
|
|
2161
|
+
"reason.completed": "已完成",
|
|
2162
|
+
/** TurnEndReason `aborted`. */
|
|
2163
|
+
"reason.aborted": "已中止",
|
|
2164
|
+
/** TurnEndReason `error`. */
|
|
2165
|
+
"reason.error": "出错",
|
|
2166
|
+
/** TurnEndReason `max-tokens`. */
|
|
2167
|
+
"reason.maxTokens": "达到上限",
|
|
2168
|
+
/** TurnEndReason `interrupted`. */
|
|
2169
|
+
"reason.interrupted": "已中断",
|
|
2170
|
+
/** TurnEndReason `blocked`. */
|
|
2171
|
+
"reason.blocked": "已阻塞",
|
|
2172
|
+
/** Copy-message tooltip action. */
|
|
2173
|
+
"copy.message": "复制消息",
|
|
2174
|
+
/** Fork-from-here tooltip action. */
|
|
2175
|
+
"fork.here": "从此处 fork",
|
|
2176
|
+
/** Collapse-turn tooltip action. */
|
|
2177
|
+
"collapse.turn": "折叠此轮",
|
|
2178
|
+
/** Expand-turn tooltip action. */
|
|
2179
|
+
"expand.turn": "展开此轮",
|
|
2180
|
+
/** aria-label + title on the milestone-list toggle when the panel is CLOSED. */
|
|
2181
|
+
"list.open": "打开列表",
|
|
2182
|
+
/** aria-label + title on the milestone-list toggle when the panel is OPEN. */
|
|
2183
|
+
"list.close": "收起列表",
|
|
2184
|
+
/** Header title of the all-prompts list panel. */
|
|
2185
|
+
"list.label": "全部提问",
|
|
2186
|
+
/** Header title + input placeholder of the cross-session search panel. */
|
|
2187
|
+
"search.cross": "跨会话搜索",
|
|
2188
|
+
/** aria-label + title on the cross-session search toggle when the panel is CLOSED. */
|
|
2189
|
+
"search.cross.open": "打开跨会话搜索",
|
|
2190
|
+
/** aria-label + title on the cross-session search toggle when the panel is OPEN. */
|
|
2191
|
+
"search.cross.close": "收起跨会话搜索",
|
|
2192
|
+
/** Cross-session result row title fallback for sessions with no display title. */
|
|
2193
|
+
"search.untitled": "(无标题)",
|
|
2194
|
+
/** Cross-session search failure notice. */
|
|
2195
|
+
"search.error": "搜索失败,请重试",
|
|
2196
|
+
/** Cross-session search footer hint when the harness capped the result list. */
|
|
2197
|
+
"search.more": "结果已截断,请细化关键词"
|
|
2198
|
+
};
|
|
2199
|
+
const en = {
|
|
2200
|
+
"jump.to": "Jump to message {n}",
|
|
2201
|
+
"window.hint": "Showing {n} messages · more below",
|
|
2202
|
+
"turn.label": "Turn {n}",
|
|
2203
|
+
"pos.of": "Message {n} of {m}",
|
|
2204
|
+
"search.placeholder": "Search message content",
|
|
2205
|
+
"search.label": "Search messages",
|
|
2206
|
+
"bookmark.filter": "Bookmarks only",
|
|
2207
|
+
"focus.on": "Focus mode",
|
|
2208
|
+
"focus.off": "Exit focus",
|
|
2209
|
+
"bookmark.star": "Bookmark this message",
|
|
2210
|
+
"search.clear": "Clear search",
|
|
2211
|
+
"load.older": "Load older messages",
|
|
2212
|
+
"rail.label": "Session milestones",
|
|
2213
|
+
"rail.list": "Session milestone list",
|
|
2214
|
+
"no.text": "(no text)",
|
|
2215
|
+
"time.justNow": "Just now",
|
|
2216
|
+
"time.minutes": "{n} minutes ago",
|
|
2217
|
+
"time.hours": "{n} hours ago",
|
|
2218
|
+
"time.days": "{n} days ago",
|
|
2219
|
+
"duration.label": "Duration {name}",
|
|
2220
|
+
"ttft.label": "First token {name}",
|
|
2221
|
+
"reason.completed": "Completed",
|
|
2222
|
+
"reason.aborted": "Aborted",
|
|
2223
|
+
"reason.error": "Error",
|
|
2224
|
+
"reason.maxTokens": "Max tokens reached",
|
|
2225
|
+
"reason.interrupted": "Interrupted",
|
|
2226
|
+
"reason.blocked": "Blocked",
|
|
2227
|
+
"copy.message": "Copy message",
|
|
2228
|
+
"fork.here": "Fork from here",
|
|
2229
|
+
"collapse.turn": "Collapse turn",
|
|
2230
|
+
"expand.turn": "Expand turn",
|
|
2231
|
+
"list.open": "Open list",
|
|
2232
|
+
"list.close": "Close list",
|
|
2233
|
+
"list.label": "All prompts",
|
|
2234
|
+
"search.cross": "Cross-session search",
|
|
2235
|
+
"search.cross.open": "Open cross-session search",
|
|
2236
|
+
"search.cross.close": "Close cross-session search",
|
|
2237
|
+
"search.untitled": "(untitled)",
|
|
2238
|
+
"search.error": "Search failed, retry",
|
|
2239
|
+
"search.more": "Results truncated — refine your query"
|
|
2240
|
+
};
|
|
1127
2241
|
//#endregion
|
|
1128
2242
|
//#region src/client/index.ts
|
|
1129
2243
|
/** Required services (cordis fiber inject). */
|
|
1130
|
-
const inject = [
|
|
2244
|
+
const inject = [
|
|
2245
|
+
"slots",
|
|
2246
|
+
"sessions",
|
|
2247
|
+
"locale"
|
|
2248
|
+
];
|
|
1131
2249
|
/**
|
|
1132
2250
|
* Register the overlay and rail once their slot declarations are on the
|
|
1133
2251
|
* ledger. The overlay registers directly against the shipped shell.overlay
|
|
@@ -1136,6 +2254,10 @@ window.__ModuleLoader__.load({
|
|
|
1136
2254
|
* @param ctx - client root context.
|
|
1137
2255
|
*/
|
|
1138
2256
|
function apply(ctx) {
|
|
2257
|
+
ctx.effect(() => ctx.locale.register("dsh-milestone", {
|
|
2258
|
+
zh,
|
|
2259
|
+
en
|
|
2260
|
+
}), "dsh-milestone: dictionaries");
|
|
1139
2261
|
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
1140
2262
|
name: "shell.overlay",
|
|
1141
2263
|
id: "milestone",
|
|
@@ -1148,7 +2270,13 @@ window.__ModuleLoader__.load({
|
|
|
1148
2270
|
ctx.slots.inject("milestone.rail", () => ctx.slots.register({
|
|
1149
2271
|
name: "milestone.rail",
|
|
1150
2272
|
store: createBookmarksStore,
|
|
1151
|
-
|
|
2273
|
+
locale: "dsh-milestone",
|
|
2274
|
+
inject: (sessionId) => ({
|
|
2275
|
+
loadOlder: createLoadOlder(ctx.sessions, sessionId),
|
|
2276
|
+
forkAt: createForkAt(ctx.sessions, sessionId),
|
|
2277
|
+
searchSessions: createSessionSearch(ctx.sessions),
|
|
2278
|
+
openSession: createOpenSession(ctx.sessions)
|
|
2279
|
+
})
|
|
1152
2280
|
}, MilestoneRail));
|
|
1153
2281
|
}
|
|
1154
2282
|
//#endregion
|