dsh-milestone 0.6.5 → 0.7.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 +182 -235
- package/lib/client.js +652 -185
- package/lib/index.js +203 -6
- package/package.json +18 -9
package/lib/client.js
CHANGED
|
@@ -6,7 +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
|
|
9
|
+
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
10
10
|
//#region src/client/MilestoneOverlay.tsx
|
|
11
11
|
/**
|
|
12
12
|
* @param props - runtime share (root kit) + the narrowed renderSlot and the
|
|
@@ -15,7 +15,7 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
function MilestoneOverlay({ SessionProvider, renderSlot }) {
|
|
16
16
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionProvider, {
|
|
17
17
|
empty: () => null,
|
|
18
|
-
children:
|
|
18
|
+
children: renderSlot("milestone.rail", {})
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
//#endregion
|
|
@@ -198,6 +198,82 @@ window.__ModuleLoader__.load({
|
|
|
198
198
|
50% { opacity: 0.45; box-shadow: ${`0 0 0 2px ${layer(.35)}, 0 0 4px 1px ${layer(.25)}, 0 0 9px 3px ${layer(.12)}`}; }
|
|
199
199
|
}`;
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Clamp a ball's top-left so the WHOLE circle stays inside the viewport.
|
|
203
|
+
*
|
|
204
|
+
* Per axis: `max = Math.max(0, viewportSize - ballSize - margin)` and
|
|
205
|
+
* `min = Math.min(margin, max)`. When there IS room, `min` is the margin, so
|
|
206
|
+
* the ball keeps its preferred distance from the edge; when the viewport is
|
|
207
|
+
* smaller than ball + margins, `max` collapses to 0 (or below) and `min`
|
|
208
|
+
* follows it, pinning the ball to the top/left outer edge instead of letting
|
|
209
|
+
* it escape the viewport.
|
|
210
|
+
*
|
|
211
|
+
* Non-finite `pos` values degrade to `0` BEFORE clamping, so a corrupt call
|
|
212
|
+
* still lands on a finite, fully-visible position.
|
|
213
|
+
*/
|
|
214
|
+
function clampBallPosition(pos, viewport, ballSize = 40, margin = 8) {
|
|
215
|
+
const xRaw = Number.isFinite(pos.x) ? pos.x : 0;
|
|
216
|
+
const yRaw = Number.isFinite(pos.y) ? pos.y : 0;
|
|
217
|
+
const maxX = Math.max(0, viewport.width - ballSize - margin);
|
|
218
|
+
const minX = Math.min(margin, maxX);
|
|
219
|
+
const maxY = Math.max(0, viewport.height - ballSize - margin);
|
|
220
|
+
return {
|
|
221
|
+
x: Math.min(maxX, Math.max(minX, xRaw)),
|
|
222
|
+
y: Math.min(maxY, Math.max(Math.min(margin, maxY), yRaw))
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* True when pointer travel from `start` to `current` exceeds the drag
|
|
227
|
+
* threshold — the gesture vocabulary of the ball: a press that stays put (or
|
|
228
|
+
* wiggles less than the threshold) is a CLICK (toggle the rail), a press that
|
|
229
|
+
* travels farther is a DRAG (move the ball).
|
|
230
|
+
*
|
|
231
|
+
* Distance is Euclidean, so a diagonal move counts the same as a straight one.
|
|
232
|
+
* EXACTLY at the threshold is still a click (strictly greater than).
|
|
233
|
+
*/
|
|
234
|
+
function isDragGesture(start, current, threshold = 5) {
|
|
235
|
+
const dx = current.x - start.x;
|
|
236
|
+
const dy = current.y - start.y;
|
|
237
|
+
return Math.hypot(dx, dy) > threshold;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Sanitize a persisted ball position; return null when unusable.
|
|
241
|
+
*
|
|
242
|
+
* Accepts only a plain object carrying FINITE numeric `x` and `y` (extra keys
|
|
243
|
+
* are ignored, so a future blob extension stays readable). Everything else —
|
|
244
|
+
* `null`, arrays, strings, numbers, NaN/Infinity, missing keys — degrades to
|
|
245
|
+
* `null`, which callers read as "no stored position, use the computed default
|
|
246
|
+
* resting spot" (see {@link defaultBallPosition}).
|
|
247
|
+
*
|
|
248
|
+
* No clamping happens here: the stored value is resolution-agnostic and gets
|
|
249
|
+
* clamped against the live viewport by {@link clampBallPosition} at render
|
|
250
|
+
* time.
|
|
251
|
+
*/
|
|
252
|
+
function sanitizeBallPosition(raw) {
|
|
253
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
254
|
+
const { x, y } = raw;
|
|
255
|
+
if (typeof x !== "number" || !Number.isFinite(x)) return null;
|
|
256
|
+
if (typeof y !== "number" || !Number.isFinite(y)) return null;
|
|
257
|
+
return {
|
|
258
|
+
x,
|
|
259
|
+
y
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Default resting spot: hugging `side` at `inset` from the edge, vertically
|
|
264
|
+
* centered. `inset` is measured from the nearest screen edge; the vertical
|
|
265
|
+
* axis is simply centered on the viewport.
|
|
266
|
+
*
|
|
267
|
+
* The raw spot is always passed through {@link clampBallPosition} with the
|
|
268
|
+
* default margin, so the resting spot is fully visible even when `inset`
|
|
269
|
+
* overflows or the viewport is smaller than the ball.
|
|
270
|
+
*/
|
|
271
|
+
function defaultBallPosition(viewport, side, inset, ballSize = 40) {
|
|
272
|
+
return clampBallPosition({
|
|
273
|
+
x: side === "left" ? inset : viewport.width - ballSize - inset,
|
|
274
|
+
y: (viewport.height - ballSize) / 2
|
|
275
|
+
}, viewport, ballSize, 8);
|
|
276
|
+
}
|
|
201
277
|
//#endregion
|
|
202
278
|
//#region src/client/bookmark-logic.ts
|
|
203
279
|
/**
|
|
@@ -385,66 +461,22 @@ window.__ModuleLoader__.load({
|
|
|
385
461
|
inputTokens: null,
|
|
386
462
|
outputTokens: null
|
|
387
463
|
};
|
|
388
|
-
/** True when the value is a plain (non-array, non-null) object. */
|
|
389
|
-
function isRecord(value) {
|
|
390
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
391
|
-
}
|
|
392
464
|
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* @param
|
|
397
|
-
* @returns the
|
|
465
|
+
* 0.1.2: derive hover metadata from the `milestone.messages` projection's
|
|
466
|
+
* per-turn fold state. The fold carries token usage and timing; provider /
|
|
467
|
+
* model provenance is not folded yet, so those degrade to null.
|
|
468
|
+
* @param turn - the projection's turn meta, or undefined when absent.
|
|
469
|
+
* @returns the turn's metadata, null where unknown.
|
|
398
470
|
*/
|
|
399
|
-
function
|
|
400
|
-
if (
|
|
401
|
-
inputTokens: null,
|
|
402
|
-
outputTokens: null
|
|
403
|
-
};
|
|
471
|
+
function deriveTurnMetaFromProjection(turn) {
|
|
472
|
+
if (turn?.usage === void 0) return EMPTY_META;
|
|
404
473
|
return {
|
|
405
|
-
|
|
406
|
-
|
|
474
|
+
model: null,
|
|
475
|
+
purpose: null,
|
|
476
|
+
inputTokens: turn.usage.input,
|
|
477
|
+
outputTokens: turn.usage.output
|
|
407
478
|
};
|
|
408
479
|
}
|
|
409
|
-
/** Resolve model/purpose from a request config, falling back to provenance. */
|
|
410
|
-
function metaFromRecord(record) {
|
|
411
|
-
return {
|
|
412
|
-
model: record.requestConfig?.model ?? record.provenance?.model ?? null,
|
|
413
|
-
purpose: record.requestConfig?.purpose ?? null,
|
|
414
|
-
...decodeUsage(record.usage)
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Derive the hover metadata for one turn. Sources, in priority order:
|
|
419
|
-
* 1. the `assistant-step` chat node(s) of the turn — their `data.finalNode`
|
|
420
|
-
* carries the recorded `requestConfig` / `provenance` / `usage`;
|
|
421
|
-
* 2. `trajectoryRequests` — the latest entry whose `turn` matches (used when
|
|
422
|
-
* no assistant-step node yields a model or purpose);
|
|
423
|
-
* 3. all-null when the turn is absent, no node matches, or everything is
|
|
424
|
-
* malformed. Never throws.
|
|
425
|
-
* @param nodes - stable per-key chat node reader (as exposed by the snapshot).
|
|
426
|
-
* @param locations - turn -> ordered node keys index.
|
|
427
|
-
* @param turn - owning turn; undefined yields all-null.
|
|
428
|
-
* @param trajectoryRequests - optional fallback request log.
|
|
429
|
-
* @returns the turn's metadata, null where unknown.
|
|
430
|
-
*/
|
|
431
|
-
function deriveTurnMeta(nodes, locations, turn, trajectoryRequests) {
|
|
432
|
-
if (turn === void 0) return EMPTY_META;
|
|
433
|
-
for (const key of locations.getTurn(turn)) {
|
|
434
|
-
const node = nodes.get(key);
|
|
435
|
-
if (node === void 0 || node.kind !== "assistant-step") continue;
|
|
436
|
-
const finalNode = (isRecord(node.data) ? node.data : void 0)?.finalNode;
|
|
437
|
-
if (!isRecord(finalNode)) continue;
|
|
438
|
-
const meta = metaFromRecord(finalNode);
|
|
439
|
-
if (meta.model !== null || meta.purpose !== null) return meta;
|
|
440
|
-
}
|
|
441
|
-
if (trajectoryRequests !== void 0) {
|
|
442
|
-
let latest;
|
|
443
|
-
for (const request of trajectoryRequests) if (request.turn === turn) latest = request;
|
|
444
|
-
if (latest !== void 0) return metaFromRecord(latest);
|
|
445
|
-
}
|
|
446
|
-
return EMPTY_META;
|
|
447
|
-
}
|
|
448
480
|
//#endregion
|
|
449
481
|
//#region src/client/rail-keyboard.ts
|
|
450
482
|
/**
|
|
@@ -481,22 +513,6 @@ window.__ModuleLoader__.load({
|
|
|
481
513
|
* can consume them directly and tests can exercise them in isolation.
|
|
482
514
|
*/
|
|
483
515
|
/**
|
|
484
|
-
* Extract the FULL plain text of a ContentBlock[] payload: the `text` of every
|
|
485
|
-
* `{ type: 'text', text: string }` block, joined with a single space and
|
|
486
|
-
* trimmed. Unlike the rail's hover preview this is NOT truncated — callers use
|
|
487
|
-
* it for search matching, so the entire message must be searchable.
|
|
488
|
-
* @param content - untrusted payload; anything that is not an array yields ''.
|
|
489
|
-
*/
|
|
490
|
-
function extractText(content) {
|
|
491
|
-
if (!Array.isArray(content)) return "";
|
|
492
|
-
const parts = [];
|
|
493
|
-
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
|
|
494
|
-
const text = block.text;
|
|
495
|
-
if (typeof text === "string") parts.push(text);
|
|
496
|
-
}
|
|
497
|
-
return parts.join(" ").trim();
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
516
|
* Case-insensitive substring filter over mark texts.
|
|
501
517
|
* @param marks - marks in rail order.
|
|
502
518
|
* @param query - the search query; empty/whitespace matches everything.
|
|
@@ -598,6 +614,9 @@ window.__ModuleLoader__.load({
|
|
|
598
614
|
"turn.label": "第 {n} 轮",
|
|
599
615
|
/** Hover position: `第 {hover.index + 1} / {hover.total} 条`. */
|
|
600
616
|
"pos.of": "第 {n} / {m} 条",
|
|
617
|
+
/** Hover position of a collapsed-turn summary dot: `第 {a}–{b} / {m} 条`
|
|
618
|
+
* (the range of messages the summary dot represents). */
|
|
619
|
+
"pos.range": "第 {a}–{b} / {m} 条",
|
|
601
620
|
/** Search input placeholder. */
|
|
602
621
|
"search.placeholder": "搜索消息内容",
|
|
603
622
|
/** aria-label on the search toggle button and the search input. */
|
|
@@ -618,6 +637,10 @@ window.__ModuleLoader__.load({
|
|
|
618
637
|
"rail.label": "会话里程碑",
|
|
619
638
|
/** aria-label on the dot list. */
|
|
620
639
|
"rail.list": "会话里程碑列表",
|
|
640
|
+
/** aria-label on the rail-collapse control (issue #4: fold the rail into the floating ball). */
|
|
641
|
+
"rail.collapse": "收起为悬浮球",
|
|
642
|
+
/** aria-label + title on the collapsed rail's floating ball (click = expand). */
|
|
643
|
+
"ball.expand": "展开里程碑条",
|
|
621
644
|
/** Hover preview fallback for empty message text. */
|
|
622
645
|
"no.text": "(无文本)",
|
|
623
646
|
/** Relative time: `< 60s`. */
|
|
@@ -658,6 +681,8 @@ window.__ModuleLoader__.load({
|
|
|
658
681
|
"list.close": "收起列表",
|
|
659
682
|
/** Header title of the all-prompts list panel. */
|
|
660
683
|
"list.label": "全部提问",
|
|
684
|
+
/** Bottom hint of the all-prompts list while it drains older pages. */
|
|
685
|
+
"list.loading": "正在加载更早消息…",
|
|
661
686
|
/** Header title + input placeholder of the cross-session search panel. */
|
|
662
687
|
"search.cross": "跨会话搜索",
|
|
663
688
|
/** aria-label + title on the cross-session search toggle when the panel is CLOSED. */
|
|
@@ -706,6 +731,8 @@ window.__ModuleLoader__.load({
|
|
|
706
731
|
"settings.section.personal": "个性化",
|
|
707
732
|
/** Settings modal: section heading for the focus-mode controls (0.6.3). */
|
|
708
733
|
"settings.section.focus": "聚焦",
|
|
734
|
+
/** Settings modal: section heading for the floating-ball controls (issue #4). */
|
|
735
|
+
"settings.section.ball": "悬浮球",
|
|
709
736
|
/** Settings: personalization-section hint shown inside the expanded block. */
|
|
710
737
|
"settings.personal.hint": "圆点、强调色与位置,即调即存",
|
|
711
738
|
/** Settings: aria-label on the personalization block toggle while COLLAPSED. */
|
|
@@ -742,6 +769,16 @@ window.__ModuleLoader__.load({
|
|
|
742
769
|
"settings.side.left": "左侧",
|
|
743
770
|
/** Settings personalization: side radio — hug the right edge. */
|
|
744
771
|
"settings.side.right": "右侧",
|
|
772
|
+
/** Settings: floating-ball block — hint shown inside the expanded block. */
|
|
773
|
+
"settings.ball.hint": "收起后变为悬浮球;可固定,也可自由拖动到任意位置",
|
|
774
|
+
/** Settings: floating-ball behavior row label + radiogroup aria-label. */
|
|
775
|
+
"settings.ball.mode": "行为",
|
|
776
|
+
/** Settings: floating-ball behavior radio — pinned to the resting spot. */
|
|
777
|
+
"settings.ball.mode.fixed": "固定",
|
|
778
|
+
/** Settings: floating-ball behavior radio — free drag anywhere. */
|
|
779
|
+
"settings.ball.mode.draggable": "可拖动",
|
|
780
|
+
/** Settings: floating-ball action — drop the persisted drag position. */
|
|
781
|
+
"settings.ball.reset": "重置位置",
|
|
745
782
|
/** Settings: focus block — hint shown inside the expanded block. */
|
|
746
783
|
"settings.focus.hint": "这些选项自由组合成你的「聚焦搭配」;总开关仍是工具栏的眼睛按钮",
|
|
747
784
|
/** Settings: aria-label on the focus block toggle while COLLAPSED. */
|
|
@@ -836,6 +873,8 @@ window.__ModuleLoader__.load({
|
|
|
836
873
|
"window.hint": "Showing {n} messages · more below",
|
|
837
874
|
"turn.label": "Turn {n}",
|
|
838
875
|
"pos.of": "Message {n} of {m}",
|
|
876
|
+
/** Collapsed-summary dot position: the message RANGE the dot represents. */
|
|
877
|
+
"pos.range": "Messages {a}–{b} of {m}",
|
|
839
878
|
"search.placeholder": "Search message content",
|
|
840
879
|
"search.label": "Search messages",
|
|
841
880
|
"bookmark.filter": "Bookmarks only",
|
|
@@ -846,6 +885,8 @@ window.__ModuleLoader__.load({
|
|
|
846
885
|
"load.older": "Load older messages",
|
|
847
886
|
"rail.label": "Session milestones",
|
|
848
887
|
"rail.list": "Session milestone list",
|
|
888
|
+
"rail.collapse": "Collapse to floating ball",
|
|
889
|
+
"ball.expand": "Expand milestone rail",
|
|
849
890
|
"no.text": "(no text)",
|
|
850
891
|
"time.justNow": "Just now",
|
|
851
892
|
"time.minutes": "{n} minutes ago",
|
|
@@ -866,6 +907,8 @@ window.__ModuleLoader__.load({
|
|
|
866
907
|
"list.open": "Open list",
|
|
867
908
|
"list.close": "Close list",
|
|
868
909
|
"list.label": "All prompts",
|
|
910
|
+
/** Bottom hint of the all-prompts list while it drains older pages. */
|
|
911
|
+
"list.loading": "Loading earlier messages…",
|
|
869
912
|
"search.cross": "Cross-session search",
|
|
870
913
|
"search.cross.open": "Open cross-session search",
|
|
871
914
|
"search.cross.close": "Close cross-session search",
|
|
@@ -890,6 +933,7 @@ window.__ModuleLoader__.load({
|
|
|
890
933
|
"settings.section.features": "Features & Shortcuts",
|
|
891
934
|
"settings.section.personal": "Personalization",
|
|
892
935
|
"settings.section.focus": "Focus",
|
|
936
|
+
"settings.section.ball": "Floating ball",
|
|
893
937
|
"settings.personal.hint": "Dot size, accent color, and position — saved as you adjust",
|
|
894
938
|
"settings.personal.expand": "Expand personalization",
|
|
895
939
|
"settings.personal.collapse": "Collapse personalization",
|
|
@@ -908,6 +952,11 @@ window.__ModuleLoader__.load({
|
|
|
908
952
|
"settings.side": "Position",
|
|
909
953
|
"settings.side.left": "Left",
|
|
910
954
|
"settings.side.right": "Right",
|
|
955
|
+
"settings.ball.hint": "Collapses into a floating ball — pin it, or drag it anywhere",
|
|
956
|
+
"settings.ball.mode": "Behavior",
|
|
957
|
+
"settings.ball.mode.fixed": "Fixed",
|
|
958
|
+
"settings.ball.mode.draggable": "Draggable",
|
|
959
|
+
"settings.ball.reset": "Reset position",
|
|
911
960
|
"settings.focus.hint": "Combine these options into your own focus recipe; the eye button on the toolbar stays the master switch",
|
|
912
961
|
"settings.focus.expand": "Expand focus settings",
|
|
913
962
|
"settings.focus.collapse": "Collapse focus settings",
|
|
@@ -974,6 +1023,31 @@ window.__ModuleLoader__.load({
|
|
|
974
1023
|
//#endregion
|
|
975
1024
|
//#region src/client/turn-group-logic.ts
|
|
976
1025
|
/**
|
|
1026
|
+
* Renumber the raw harness turn numbers into a compact 1-based DISPLAY
|
|
1027
|
+
* sequence over the marks that actually render. The harness numbers every
|
|
1028
|
+
* engine turn (subagent/injected turns included), so the raw numbers show
|
|
1029
|
+
* gaps (turns that produced no user mark) and repeats (several marks sharing
|
|
1030
|
+
* one turn); labels fed through this map read as clean 1, 2, 3, … rounds.
|
|
1031
|
+
*
|
|
1032
|
+
* Grouping and collapse logic keep operating on the RAW turn (same-turn marks
|
|
1033
|
+
* are contiguous in rail order, so first-appearance ranking preserves the
|
|
1034
|
+
* partition) — only labels consume this map.
|
|
1035
|
+
*
|
|
1036
|
+
* @param marks - marks in rail order (only `turn` is consulted).
|
|
1037
|
+
* @returns raw turn -> display round (1-based), in first-appearance order;
|
|
1038
|
+
* turns that never appear (or marks without turn info) get no entry.
|
|
1039
|
+
*/
|
|
1040
|
+
function buildDisplayTurns(marks) {
|
|
1041
|
+
const display = /* @__PURE__ */ new Map();
|
|
1042
|
+
let rank = 0;
|
|
1043
|
+
for (const mark of marks) {
|
|
1044
|
+
if (mark.turn === void 0) continue;
|
|
1045
|
+
if (display.has(mark.turn)) continue;
|
|
1046
|
+
display.set(mark.turn, ++rank);
|
|
1047
|
+
}
|
|
1048
|
+
return display;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
977
1051
|
* Partition consecutive marks by turn. Marks with the same numeric turn that
|
|
978
1052
|
* appear one after another share a group; each mark with `turn === undefined`
|
|
979
1053
|
* becomes its own singleton group with `turn: null`.
|
|
@@ -1286,9 +1360,10 @@ window.__ModuleLoader__.load({
|
|
|
1286
1360
|
/**
|
|
1287
1361
|
* @param props - the panel anchor, the full marks array, and the rail's jump handler.
|
|
1288
1362
|
*/
|
|
1289
|
-
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, onClose, t }) {
|
|
1363
|
+
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, onClose, loading = false, t }) {
|
|
1290
1364
|
const panelRef = (0, react.useRef)(null);
|
|
1291
1365
|
useOutsideDismiss(panelRef, true, onClose, { exclude: (target) => outsideDismissMatches(target, "[data-list-toggle]") });
|
|
1366
|
+
const displayTurns = (0, react.useMemo)(() => buildDisplayTurns(marks), [marks]);
|
|
1292
1367
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1293
1368
|
ref: panelRef,
|
|
1294
1369
|
"data-milestone-list": true,
|
|
@@ -1328,7 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
1328
1403
|
children: marks.length
|
|
1329
1404
|
})]
|
|
1330
1405
|
}),
|
|
1331
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1332
1407
|
style: {
|
|
1333
1408
|
maxHeight: 300,
|
|
1334
1409
|
overflowY: "auto",
|
|
@@ -1336,7 +1411,7 @@ window.__ModuleLoader__.load({
|
|
|
1336
1411
|
flexDirection: "column",
|
|
1337
1412
|
gap: 2
|
|
1338
1413
|
},
|
|
1339
|
-
children: marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1414
|
+
children: [marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1340
1415
|
type: "button",
|
|
1341
1416
|
"data-list-item": true,
|
|
1342
1417
|
"data-jump-key": mark.key,
|
|
@@ -1363,7 +1438,7 @@ window.__ModuleLoader__.load({
|
|
|
1363
1438
|
children: [t("pos.of", {
|
|
1364
1439
|
n: i + 1,
|
|
1365
1440
|
m: marks.length
|
|
1366
|
-
}), mark.turn !== void 0 ? ` · ${t("turn.label", { n: mark.turn })}` : null]
|
|
1441
|
+
}), mark.turn !== void 0 ? ` · ${t("turn.label", { n: displayTurns.get(mark.turn) ?? mark.turn })}` : null]
|
|
1367
1442
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1368
1443
|
style: {
|
|
1369
1444
|
fontSize: 13,
|
|
@@ -1374,7 +1449,16 @@ window.__ModuleLoader__.load({
|
|
|
1374
1449
|
},
|
|
1375
1450
|
children: mark.preview || t("no.text")
|
|
1376
1451
|
})]
|
|
1377
|
-
}, mark.key))
|
|
1452
|
+
}, mark.key)), loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1453
|
+
"data-list-loading": true,
|
|
1454
|
+
style: {
|
|
1455
|
+
fontSize: 12,
|
|
1456
|
+
color: "#8b96ab",
|
|
1457
|
+
textAlign: "center",
|
|
1458
|
+
padding: "6px 8px"
|
|
1459
|
+
},
|
|
1460
|
+
children: t("list.loading")
|
|
1461
|
+
})]
|
|
1378
1462
|
})
|
|
1379
1463
|
]
|
|
1380
1464
|
});
|
|
@@ -1831,10 +1915,7 @@ window.__ModuleLoader__.load({
|
|
|
1831
1915
|
marginBottom: 4
|
|
1832
1916
|
},
|
|
1833
1917
|
children: [
|
|
1834
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children:
|
|
1835
|
-
n: hover.index + 1,
|
|
1836
|
-
m: hover.total
|
|
1837
|
-
}) }),
|
|
1918
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.posLabel }),
|
|
1838
1919
|
hover.turnLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.turnLabel }),
|
|
1839
1920
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1840
1921
|
type: "button",
|
|
@@ -2246,7 +2327,8 @@ window.__ModuleLoader__.load({
|
|
|
2246
2327
|
* toolbar-prefs: the persistence layer for the milestone rail's toolbar
|
|
2247
2328
|
* personalization — WHICH function keys stay visible outside the collapse
|
|
2248
2329
|
* (pinned) plus the settings-module appearance prefs (accent color, icon/dot
|
|
2249
|
-
* size, distance from the rail's screen edge,
|
|
2330
|
+
* size, distance from the rail's screen edge, rail side) and the collapsed
|
|
2331
|
+
* rail's floating ball (mode + last position).
|
|
2250
2332
|
*
|
|
2251
2333
|
* Storage contract: one localStorage key (`dsh-milestone.toolbar`) holding a
|
|
2252
2334
|
* JSON object:
|
|
@@ -2254,13 +2336,17 @@ window.__ModuleLoader__.load({
|
|
|
2254
2336
|
* { "pinned": string[], "accent": "#rrggbb", "iconSize": number,
|
|
2255
2337
|
* "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en",
|
|
2256
2338
|
* "focus": { "dimThink": boolean, "dimTools": boolean,
|
|
2257
|
-
* "collapseThink": boolean, "opacity": number }
|
|
2339
|
+
* "collapseThink": boolean, "opacity": number },
|
|
2340
|
+
* "ballMode": "fixed" | "draggable",
|
|
2341
|
+
* "ball": { "x": number, "y": number } | null }
|
|
2258
2342
|
*
|
|
2259
2343
|
* Backward compatibility: the pre-personalization blob `{ "pinned": string[] }`
|
|
2260
2344
|
* (and an entirely absent value) parses to the DEFAULT prefs with the new
|
|
2261
2345
|
* fields at their defaults — old users keep their pins untouched. The same
|
|
2262
2346
|
* rule covers the `focus` object: a blob stored before 0.6.3 (no `focus`
|
|
2263
|
-
* field) gains the default focus mix
|
|
2347
|
+
* field) gains the default focus mix, and a blob stored before the floating
|
|
2348
|
+
* ball (no `ballMode` / `ball`) gains `ballMode: 'draggable'` + `ball: null`
|
|
2349
|
+
* (the computed default resting spot).
|
|
2264
2350
|
*
|
|
2265
2351
|
* All reads are sanitized per field:
|
|
2266
2352
|
* - `pinned`: whitelisted ids only (`TOOLBAR_PIN_IDS`), duplicates dropped,
|
|
@@ -2272,7 +2358,11 @@ window.__ModuleLoader__.load({
|
|
|
2272
2358
|
* - `side`: exactly `'left'` or `'right'`;
|
|
2273
2359
|
* - `focus`: three booleans (`dimThink` / `dimTools` / `collapseThink`)
|
|
2274
2360
|
* defaulting to `true` / `false` / `false`, plus the dim `opacity`
|
|
2275
|
-
* snapped to the 0.1 step and clamped to [0.2, 0.8]
|
|
2361
|
+
* snapped to the 0.1 step and clamped to [0.2, 0.8];
|
|
2362
|
+
* - `ballMode`: exactly `'fixed'`, otherwise `'draggable'` (including a
|
|
2363
|
+
* legacy blob stored without the field);
|
|
2364
|
+
* - `ball`: a finite `{x, y}` pair, otherwise `null` — viewport clamping
|
|
2365
|
+
* happens at render time (`clampBallPosition`), never in storage.
|
|
2276
2366
|
*
|
|
2277
2367
|
* The whitelist lives HERE (not in MilestoneRail) so the pure functions stay
|
|
2278
2368
|
* dependency-free and unit-testable; MilestoneRail's feature registry keys
|
|
@@ -2316,7 +2406,9 @@ window.__ModuleLoader__.load({
|
|
|
2316
2406
|
inset: 14,
|
|
2317
2407
|
side: "right",
|
|
2318
2408
|
locale: "system",
|
|
2319
|
-
focus: { ...DEFAULT_FOCUS_PREFS }
|
|
2409
|
+
focus: { ...DEFAULT_FOCUS_PREFS },
|
|
2410
|
+
ballMode: "draggable",
|
|
2411
|
+
ball: null
|
|
2320
2412
|
};
|
|
2321
2413
|
/** Type guard for registry ids — unknown strings never survive a parse. */
|
|
2322
2414
|
function isToolbarPinId(id) {
|
|
@@ -2384,7 +2476,7 @@ window.__ModuleLoader__.load({
|
|
|
2384
2476
|
return { ...DEFAULT_PREFS };
|
|
2385
2477
|
}
|
|
2386
2478
|
if (typeof parsed !== "object" || parsed === null) return { ...DEFAULT_PREFS };
|
|
2387
|
-
const { pinned, accent, iconSize, inset, side, locale, focus } = parsed;
|
|
2479
|
+
const { pinned, accent, iconSize, inset, side, locale, focus, ballMode, ball } = parsed;
|
|
2388
2480
|
return {
|
|
2389
2481
|
pinned: sanitizePinned(pinned),
|
|
2390
2482
|
accent: typeof accent === "string" && isHexColor(accent) ? accent.toLowerCase() : DEFAULT_PREFS.accent,
|
|
@@ -2392,7 +2484,9 @@ window.__ModuleLoader__.load({
|
|
|
2392
2484
|
inset: clampStep(inset, 0, 40, 2, DEFAULT_PREFS.inset),
|
|
2393
2485
|
side: side === "left" || side === "right" ? side : DEFAULT_PREFS.side,
|
|
2394
2486
|
locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale,
|
|
2395
|
-
focus: sanitizeFocus(focus)
|
|
2487
|
+
focus: sanitizeFocus(focus),
|
|
2488
|
+
ballMode: ballMode === "fixed" ? "fixed" : "draggable",
|
|
2489
|
+
ball: sanitizeBallPosition(ball)
|
|
2396
2490
|
};
|
|
2397
2491
|
}
|
|
2398
2492
|
/**
|
|
@@ -2738,7 +2832,7 @@ window.__ModuleLoader__.load({
|
|
|
2738
2832
|
* Installed plugin version. Injected at build time as
|
|
2739
2833
|
* `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
|
|
2740
2834
|
*/
|
|
2741
|
-
const PLUGIN_VERSION = "0.
|
|
2835
|
+
const PLUGIN_VERSION = "0.7.0";
|
|
2742
2836
|
//#endregion
|
|
2743
2837
|
//#region src/client/MilestoneRail.tsx
|
|
2744
2838
|
/**
|
|
@@ -2792,9 +2886,12 @@ window.__ModuleLoader__.load({
|
|
|
2792
2886
|
*/
|
|
2793
2887
|
/** Minimum user messages before the rail adds value. */
|
|
2794
2888
|
const MIN_MARKS = 2;
|
|
2795
|
-
const PREVIEW_LENGTH = 80;
|
|
2796
2889
|
/** Stable no-bookmarks fallback for render paths without the store seat. */
|
|
2797
2890
|
const NO_BOOKMARKS = [];
|
|
2891
|
+
/** Stable empty projection fallback before the host unit mounts. */
|
|
2892
|
+
const EMPTY_PROJECTION_MESSAGES = Object.freeze([]);
|
|
2893
|
+
/** 0.1.2: per-turn badge kinds are not projected yet; stays empty. */
|
|
2894
|
+
const EMPTY_KINDS_BY_TURN = /* @__PURE__ */ new Map();
|
|
2798
2895
|
/** Stable no-kinds fallback for marks whose turn carries no badge nodes. */
|
|
2799
2896
|
const NO_KINDS = [];
|
|
2800
2897
|
/** Visual dot diameter at the default icon size (px). */
|
|
@@ -2917,24 +3014,24 @@ window.__ModuleLoader__.load({
|
|
|
2917
3014
|
/* Row and header washes (the inline styles deliberately leave backgrounds
|
|
2918
3015
|
unset so these rules win over the default padding-box background). */
|
|
2919
3016
|
[data-toolbar-pin-toggle]:hover, [data-toolbar-pin-toggle]:focus-visible { background: rgba(255, 255, 255, 0.06); }
|
|
2920
|
-
[data-personal-toggle]:hover, [data-focus-toggle-settings]:hover { background: rgba(255, 255, 255, 0.05); }
|
|
3017
|
+
[data-personal-toggle]:hover, [data-focus-toggle-settings]:hover, [data-ball-toggle]:hover { background: rgba(255, 255, 255, 0.05); }
|
|
2921
3018
|
[data-focus-option]:hover { background: rgba(255, 255, 255, 0.04); }
|
|
2922
3019
|
[data-toolbar-settings-close]:hover { background: rgba(255, 255, 255, 0.08); }
|
|
2923
|
-
[data-toolbar-settings-reset], [data-onboarding-reopen] { background: rgba(255, 255, 255, 0.06); }
|
|
2924
|
-
[data-toolbar-settings-reset]:hover, [data-onboarding-reopen]:hover { background: rgba(255, 255, 255, 0.1); }
|
|
3020
|
+
[data-toolbar-settings-reset], [data-onboarding-reopen], [data-ball-reset] { background: rgba(255, 255, 255, 0.06); }
|
|
3021
|
+
[data-toolbar-settings-reset]:hover, [data-onboarding-reopen]:hover, [data-ball-reset]:hover { background: rgba(255, 255, 255, 0.1); }
|
|
2925
3022
|
/* BASE state reset: every modal surface must sit transparent on the dark
|
|
2926
3023
|
panel — without it the UA default button face (light gray) floods through
|
|
2927
3024
|
and rows become unreadable light-on-light. Hover washes above take over
|
|
2928
3025
|
on interaction. */
|
|
2929
|
-
[data-toolbar-pin-toggle], [data-personal-toggle], [data-focus-toggle-settings],
|
|
3026
|
+
[data-toolbar-pin-toggle], [data-personal-toggle], [data-focus-toggle-settings], [data-ball-toggle],
|
|
2930
3027
|
[data-toolbar-settings-close], [data-focus-option] {
|
|
2931
3028
|
background: transparent;
|
|
2932
3029
|
}
|
|
2933
3030
|
/* ONE accent ring for keyboard focus on every modal control. */
|
|
2934
3031
|
[data-toolbar-pin-toggle]:focus-visible, [data-personal-toggle]:focus-visible,
|
|
2935
|
-
[data-focus-toggle-settings]:focus-visible,
|
|
3032
|
+
[data-focus-toggle-settings]:focus-visible, [data-ball-toggle]:focus-visible,
|
|
2936
3033
|
[data-toolbar-settings-close]:focus-visible, [data-toolbar-settings-reset]:focus-visible,
|
|
2937
|
-
[data-onboarding-reopen]:focus-visible {
|
|
3034
|
+
[data-onboarding-reopen]:focus-visible, [data-ball-reset]:focus-visible {
|
|
2938
3035
|
box-shadow: 0 0 0 2px var(--ms-accent-soft);
|
|
2939
3036
|
}
|
|
2940
3037
|
/* Near-row description tip: a rotated square peeks out of the LEFT edge so
|
|
@@ -2948,10 +3045,10 @@ window.__ModuleLoader__.load({
|
|
|
2948
3045
|
}
|
|
2949
3046
|
/* Tip + chevron motion lives here (not inline) so reduced-motion can kill it. */
|
|
2950
3047
|
[data-settings-tip] { transition: opacity 140ms ease, transform 140ms ease, visibility 140ms; }
|
|
2951
|
-
[data-personal-toggle] svg, [data-focus-toggle-settings] svg { transition: transform 150ms ease; }
|
|
2952
|
-
/* One authored reveal: the personalization/focus bodies fade in on expand. */
|
|
3048
|
+
[data-personal-toggle] svg, [data-focus-toggle-settings] svg, [data-ball-toggle] svg { transition: transform 150ms ease; }
|
|
3049
|
+
/* One authored reveal: the personalization/focus/ball bodies fade in on expand. */
|
|
2953
3050
|
@keyframes ms-settings-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
|
|
2954
|
-
[data-settings-personal-body], [data-settings-focus-body] { animation: ms-settings-fade 140ms ease; }
|
|
3051
|
+
[data-settings-personal-body], [data-settings-focus-body], [data-settings-ball-body] { animation: ms-settings-fade 140ms ease; }
|
|
2955
3052
|
/* Thin themed scrollbar for the scrollable modal panel. */
|
|
2956
3053
|
[data-toolbar-settings-panel]::-webkit-scrollbar { width: 10px; }
|
|
2957
3054
|
[data-toolbar-settings-panel]::-webkit-scrollbar-thumb {
|
|
@@ -2959,8 +3056,8 @@ window.__ModuleLoader__.load({
|
|
|
2959
3056
|
}
|
|
2960
3057
|
[data-toolbar-settings-panel]::-webkit-scrollbar-track { background: transparent; }
|
|
2961
3058
|
@media (prefers-reduced-motion: reduce) {
|
|
2962
|
-
[data-settings-tip], [data-personal-toggle] svg, [data-focus-toggle-settings] svg, [data-support-card] { transition: none; }
|
|
2963
|
-
[data-settings-personal-body], [data-settings-focus-body] { animation: none; }
|
|
3059
|
+
[data-settings-tip], [data-personal-toggle] svg, [data-focus-toggle-settings] svg, [data-ball-toggle] svg, [data-support-card] { transition: none; }
|
|
3060
|
+
[data-settings-personal-body], [data-settings-focus-body], [data-settings-ball-body] { animation: none; }
|
|
2964
3061
|
}
|
|
2965
3062
|
[data-search-toggle] { color: #8b96ab !important; }
|
|
2966
3063
|
[data-search-toggle][aria-pressed="true"] { background: var(--ms-accent-bg) !important; color: var(--ms-accent-soft) !important; }
|
|
@@ -3034,26 +3131,21 @@ window.__ModuleLoader__.load({
|
|
|
3034
3131
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
3035
3132
|
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
3036
3133
|
*/
|
|
3037
|
-
function findRow(key) {
|
|
3038
|
-
|
|
3134
|
+
function findRow(key, messageId) {
|
|
3135
|
+
const rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
3136
|
+
for (const row of rows) if (row.dataset.chatAnchorKey === key) return row;
|
|
3137
|
+
if (messageId !== void 0 && messageId !== "") for (const row of rows) {
|
|
3138
|
+
const anchor = row.dataset.chatAnchorKey;
|
|
3139
|
+
if (anchor !== void 0 && anchor.endsWith(messageId)) return row;
|
|
3140
|
+
}
|
|
3039
3141
|
return null;
|
|
3040
3142
|
}
|
|
3041
|
-
/** Extract a plain-text hover preview (first 80 chars) from a ContentBlock[]. */
|
|
3042
|
-
function extractPreview(content) {
|
|
3043
|
-
return extractText(content).slice(0, PREVIEW_LENGTH);
|
|
3044
|
-
}
|
|
3045
3143
|
/** Compact duration label (ms). */
|
|
3046
3144
|
function formatDuration(ms) {
|
|
3047
3145
|
if (ms < 1e3) return `${ms}ms`;
|
|
3048
3146
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
3049
3147
|
return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
3050
3148
|
}
|
|
3051
|
-
/** Read the ui-conversation 'turn-tail' location data (ttftMs/tokensPerSecond). */
|
|
3052
|
-
function turnTailOf(turn) {
|
|
3053
|
-
const data = turn.data;
|
|
3054
|
-
if (data?.get === void 0) return void 0;
|
|
3055
|
-
return data.get("turn-tail");
|
|
3056
|
-
}
|
|
3057
3149
|
/**
|
|
3058
3150
|
* @param props - session standard kit (useSession, sessionId, useProjection),
|
|
3059
3151
|
* the injected loadOlder/forkAt actions, the bookmarks store pair (useStore +
|
|
@@ -3062,52 +3154,28 @@ window.__ModuleLoader__.load({
|
|
|
3062
3154
|
* `locale: 'dsh-milestone'`; defaults to a key-pass fallback for renders
|
|
3063
3155
|
* outside the slot machinery).
|
|
3064
3156
|
*/
|
|
3065
|
-
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
3157
|
+
function MilestoneRail({ useSession, useProjection, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
3066
3158
|
items: [],
|
|
3067
3159
|
hasMore: false
|
|
3068
3160
|
}), openSession = () => {}, t: frameworkT = (key) => key }) {
|
|
3069
|
-
const
|
|
3070
|
-
const nodes = useSession((s) => s.chat.nodes);
|
|
3071
|
-
const locations = useSession((s) => s.chat.locations);
|
|
3072
|
-
const timeline = useSession((s) => s.chat.timeline);
|
|
3073
|
-
const trajectoryRequests = useSession((s) => s.views.get("trajectory")?.requests);
|
|
3161
|
+
const projection = useProjection?.("milestone.messages");
|
|
3074
3162
|
const hasMore = useSession((s) => s.hasMore);
|
|
3075
3163
|
const loadingOlder = useSession((s) => s.loadingOlder);
|
|
3076
3164
|
const bookmarkedKeys = useStore?.((s) => s.keys) ?? NO_BOOKMARKS;
|
|
3077
3165
|
const marks = (0, react.useMemo)(() => {
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
text: extractText(data.content),
|
|
3090
|
-
preview: extractPreview(data.content)
|
|
3091
|
-
});
|
|
3092
|
-
}
|
|
3093
|
-
return result;
|
|
3094
|
-
}, [order, nodes]);
|
|
3095
|
-
const kindsByTurn = (0, react.useMemo)(() => {
|
|
3096
|
-
const result = /* @__PURE__ */ new Map();
|
|
3097
|
-
for (const node of nodes.values()) {
|
|
3098
|
-
if (node.kind !== "turn-error" && node.kind !== "turn-max-tokens" && node.kind !== "model-retry") continue;
|
|
3099
|
-
if (node.kind === "model-retry") {
|
|
3100
|
-
if (node.data?.retryState === "cancelled") continue;
|
|
3101
|
-
}
|
|
3102
|
-
if (node.location.kind !== "turn" && node.location.kind !== "step") continue;
|
|
3103
|
-
const kinds = result.get(node.location.turn.turn) ?? [];
|
|
3104
|
-
kinds.push(node.kind);
|
|
3105
|
-
result.set(node.location.turn.turn, kinds);
|
|
3106
|
-
}
|
|
3107
|
-
return result;
|
|
3108
|
-
}, [order, nodes]);
|
|
3166
|
+
return (projection?.messages ?? EMPTY_PROJECTION_MESSAGES).map((m) => ({
|
|
3167
|
+
key: String(m.seq),
|
|
3168
|
+
turn: m.turn,
|
|
3169
|
+
seq: m.seq,
|
|
3170
|
+
messageId: m.messageId,
|
|
3171
|
+
time: m.time,
|
|
3172
|
+
text: m.text,
|
|
3173
|
+
preview: m.preview
|
|
3174
|
+
}));
|
|
3175
|
+
}, [projection]);
|
|
3176
|
+
const kindsByTurn = EMPTY_KINDS_BY_TURN;
|
|
3109
3177
|
const running = useSession((s) => s.running);
|
|
3110
|
-
const awaitingInput = useSession((s) => s.
|
|
3178
|
+
const awaitingInput = useSession((s) => s.queue).length > 0;
|
|
3111
3179
|
const [railBox, setRailBox] = (0, react.useState)(null);
|
|
3112
3180
|
const [hover, setHover] = (0, react.useState)(null);
|
|
3113
3181
|
const [search, setSearch] = (0, react.useState)({
|
|
@@ -3118,13 +3186,15 @@ window.__ModuleLoader__.load({
|
|
|
3118
3186
|
const [bookmarksOnly, setBookmarksOnly] = (0, react.useState)(false);
|
|
3119
3187
|
const [focusActive, setFocusActive] = (0, react.useState)(false);
|
|
3120
3188
|
const [listOpen, setListOpen] = (0, react.useState)(false);
|
|
3189
|
+
const [drainPage, setDrainPage] = (0, react.useState)(false);
|
|
3190
|
+
const [drainFailed, setDrainFailed] = (0, react.useState)(false);
|
|
3121
3191
|
const [crossOpen, setCrossOpen] = (0, react.useState)(false);
|
|
3122
3192
|
const [copiedKey, setCopiedKey] = (0, react.useState)(null);
|
|
3123
3193
|
const [forkedKey, setForkedKey] = (0, react.useState)(null);
|
|
3124
3194
|
const [collapsedTurns, setCollapsedTurns] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3125
3195
|
const [focusIndex, setFocusIndex] = (0, react.useState)(0);
|
|
3126
3196
|
const listRef = (0, react.useRef)(null);
|
|
3127
|
-
const currentKey = useCurrentAnchor(
|
|
3197
|
+
const currentKey = useCurrentAnchor(marks.map((m) => m.key));
|
|
3128
3198
|
/**
|
|
3129
3199
|
* P3: jump to the chat row with the given node key — smooth-scroll it into
|
|
3130
3200
|
* view and write the position back into the URL hash (`#msg=<key>`) so
|
|
@@ -3134,8 +3204,8 @@ window.__ModuleLoader__.load({
|
|
|
3134
3204
|
* rail's own updates. No-op when the row is not (yet) rendered — the
|
|
3135
3205
|
* deep-link mount retry and the load-older flow cover that case.
|
|
3136
3206
|
*/
|
|
3137
|
-
const jump = (key) => {
|
|
3138
|
-
const row = findRow(key);
|
|
3207
|
+
const jump = (key, messageId) => {
|
|
3208
|
+
const row = findRow(key, messageId);
|
|
3139
3209
|
if (row === null) return;
|
|
3140
3210
|
row.scrollIntoView({
|
|
3141
3211
|
behavior: "smooth",
|
|
@@ -3170,8 +3240,9 @@ window.__ModuleLoader__.load({
|
|
|
3170
3240
|
}
|
|
3171
3241
|
return counts;
|
|
3172
3242
|
}, [displayMarks]);
|
|
3243
|
+
const displayTurns = (0, react.useMemo)(() => buildDisplayTurns(marks), [marks]);
|
|
3173
3244
|
const [prefs, setPrefs] = (0, react.useState)(() => loadPrefs());
|
|
3174
|
-
const { pinned, accent, iconSize, inset, side } = prefs;
|
|
3245
|
+
const { pinned, accent, iconSize, inset, side, ballMode, ball } = prefs;
|
|
3175
3246
|
const scale = iconSize / DOT_HIT;
|
|
3176
3247
|
const hit = iconSize;
|
|
3177
3248
|
const size = DOT_SIZE * scale;
|
|
@@ -3251,6 +3322,27 @@ window.__ModuleLoader__.load({
|
|
|
3251
3322
|
/** B-design (0.6.3): the focus block mirrors the personalization block —
|
|
3252
3323
|
* collapsed by default, the header leads with a live option summary. */
|
|
3253
3324
|
const [focusOpen, setFocusOpen] = (0, react.useState)(false);
|
|
3325
|
+
const [railCollapsed, setRailCollapsed] = (0, react.useState)(false);
|
|
3326
|
+
const [railCollapseHovered, setRailCollapseHovered] = (0, react.useState)(false);
|
|
3327
|
+
/** Issue #4 settings: the floating-ball block mirrors the personalization /
|
|
3328
|
+
* focus collapsibles — collapsed by default, the header leads with the mode. */
|
|
3329
|
+
const [ballOpen, setBallOpen] = (0, react.useState)(false);
|
|
3330
|
+
/** The live ball position while a press is in flight (`null` = resting). */
|
|
3331
|
+
const [dragPos, setDragPos] = (0, react.useState)(null);
|
|
3332
|
+
const dragRef = (0, react.useRef)(null);
|
|
3333
|
+
/** Detaches the in-flight press's window listeners (set on pointerdown). */
|
|
3334
|
+
const dragListenersRef = (0, react.useRef)(null);
|
|
3335
|
+
(0, react.useEffect)(() => () => {
|
|
3336
|
+
dragListenersRef.current?.();
|
|
3337
|
+
dragListenersRef.current = null;
|
|
3338
|
+
}, []);
|
|
3339
|
+
const [, setBallTick] = (0, react.useState)(0);
|
|
3340
|
+
(0, react.useEffect)(() => {
|
|
3341
|
+
if (!railCollapsed) return;
|
|
3342
|
+
const onResize = () => setBallTick((n) => n + 1);
|
|
3343
|
+
window.addEventListener("resize", onResize);
|
|
3344
|
+
return () => window.removeEventListener("resize", onResize);
|
|
3345
|
+
}, [railCollapsed]);
|
|
3254
3346
|
const settingsRef = (0, react.useRef)(null);
|
|
3255
3347
|
const settingsBtnRef = (0, react.useRef)(null);
|
|
3256
3348
|
const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
|
|
@@ -3352,11 +3444,12 @@ window.__ModuleLoader__.load({
|
|
|
3352
3444
|
let timer;
|
|
3353
3445
|
const attempt = (pollsLeft, canLoadOlder) => {
|
|
3354
3446
|
if (cancelled) return;
|
|
3355
|
-
|
|
3356
|
-
|
|
3447
|
+
const mark = marksRef.current.find((m) => m.key === key);
|
|
3448
|
+
if (findRow(key, mark?.messageId) !== null) {
|
|
3449
|
+
jump(key, mark?.messageId);
|
|
3357
3450
|
return;
|
|
3358
3451
|
}
|
|
3359
|
-
if (marksRef.current.length > 0 &&
|
|
3452
|
+
if (marksRef.current.length > 0 && mark === void 0) return;
|
|
3360
3453
|
if (pollsLeft > 0) {
|
|
3361
3454
|
timer = window.setTimeout(() => attempt(pollsLeft - 1, canLoadOlder), DEEP_LINK_POLL_DELAY);
|
|
3362
3455
|
return;
|
|
@@ -3378,7 +3471,8 @@ window.__ModuleLoader__.load({
|
|
|
3378
3471
|
const onHashChange = () => {
|
|
3379
3472
|
const key = parseDeepLinkHash(window.location.hash);
|
|
3380
3473
|
if (key === null) return;
|
|
3381
|
-
|
|
3474
|
+
const mark = marksRef.current.find((m) => m.key === key);
|
|
3475
|
+
if (mark !== void 0) jump(key, mark.messageId);
|
|
3382
3476
|
};
|
|
3383
3477
|
window.addEventListener("hashchange", onHashChange);
|
|
3384
3478
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
@@ -3441,6 +3535,22 @@ window.__ModuleLoader__.load({
|
|
|
3441
3535
|
window.addEventListener("keydown", onKey);
|
|
3442
3536
|
return () => window.removeEventListener("keydown", onKey);
|
|
3443
3537
|
}, [listOpen, crossOpen]);
|
|
3538
|
+
const listDraining = listOpen && (drainPage || hasMore && !drainFailed);
|
|
3539
|
+
(0, react.useEffect)(() => {
|
|
3540
|
+
if (!listOpen) return;
|
|
3541
|
+
setDrainFailed(false);
|
|
3542
|
+
}, [listOpen]);
|
|
3543
|
+
(0, react.useEffect)(() => {
|
|
3544
|
+
if (!listOpen || !hasMore || drainPage || drainFailed) return;
|
|
3545
|
+
setDrainPage(true);
|
|
3546
|
+
loadOlder().catch(() => setDrainFailed(true)).finally(() => setDrainPage(false));
|
|
3547
|
+
}, [
|
|
3548
|
+
listOpen,
|
|
3549
|
+
hasMore,
|
|
3550
|
+
drainPage,
|
|
3551
|
+
drainFailed,
|
|
3552
|
+
loadOlder
|
|
3553
|
+
]);
|
|
3444
3554
|
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
3445
3555
|
const updateQuery = (query) => {
|
|
3446
3556
|
setSearch({
|
|
@@ -3471,7 +3581,8 @@ window.__ModuleLoader__.load({
|
|
|
3471
3581
|
...s,
|
|
3472
3582
|
activePos: next
|
|
3473
3583
|
}));
|
|
3474
|
-
|
|
3584
|
+
const mark = displayMarks[matches[next]];
|
|
3585
|
+
jump(mark.key, mark.messageId);
|
|
3475
3586
|
};
|
|
3476
3587
|
const onSearchKeyDown = (e) => {
|
|
3477
3588
|
if (e.key === "Enter") advanceMatch();
|
|
@@ -3801,29 +3912,35 @@ window.__ModuleLoader__.load({
|
|
|
3801
3912
|
const buildHover = (mark, index) => {
|
|
3802
3913
|
if (copiedKey !== null && mark.key !== copiedKey) setCopiedKey(null);
|
|
3803
3914
|
if (forkedKey !== null && mark.key !== forkedKey) setForkedKey(null);
|
|
3804
|
-
const
|
|
3915
|
+
const turnMeta = mark.turn !== void 0 ? projection?.turns.find((turn) => turn.turn === mark.turn) : void 0;
|
|
3805
3916
|
let durationLabel = null;
|
|
3806
3917
|
let reasonLabel = null;
|
|
3807
3918
|
let ttftLabel = null;
|
|
3808
3919
|
let tpsLabel = null;
|
|
3809
|
-
if (
|
|
3810
|
-
if (
|
|
3811
|
-
if (
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
if (tail !== void 0) {
|
|
3817
|
-
if (tail.ttftMs !== void 0) ttftLabel = formatDuration(tail.ttftMs);
|
|
3818
|
-
if (tail.tokensPerSecond !== void 0) tpsLabel = `${tail.tokensPerSecond.toFixed(1)} tok/s`;
|
|
3920
|
+
if (turnMeta !== void 0) {
|
|
3921
|
+
if (turnMeta.startTime !== void 0 && turnMeta.endTime !== void 0) durationLabel = formatDuration(turnMeta.endTime - turnMeta.startTime);
|
|
3922
|
+
if (turnMeta.endReason !== void 0) reasonLabel = t(reasonKeyOf(turnMeta.endReason));
|
|
3923
|
+
if (turnMeta.firstChunkTime !== void 0 && turnMeta.startTime !== void 0) ttftLabel = formatDuration(turnMeta.firstChunkTime - turnMeta.startTime);
|
|
3924
|
+
if (turnMeta.usage !== void 0 && durationLabel !== null) {
|
|
3925
|
+
const seconds = (turnMeta.endTime - turnMeta.startTime) / 1e3;
|
|
3926
|
+
if (seconds > 0) tpsLabel = `${(turnMeta.usage.output / seconds).toFixed(1)} tok/s`;
|
|
3819
3927
|
}
|
|
3820
3928
|
}
|
|
3821
|
-
const meta =
|
|
3929
|
+
const meta = deriveTurnMetaFromProjection(turnMeta);
|
|
3930
|
+
const summaryCount = collapsedSummaries.get(mark.key);
|
|
3822
3931
|
return {
|
|
3823
3932
|
mark,
|
|
3824
3933
|
index,
|
|
3825
3934
|
total: displayMarks.length,
|
|
3826
|
-
|
|
3935
|
+
posLabel: summaryCount !== void 0 ? t("pos.range", {
|
|
3936
|
+
a: index - summaryCount + 2,
|
|
3937
|
+
b: index + 1,
|
|
3938
|
+
m: displayMarks.length
|
|
3939
|
+
}) : t("pos.of", {
|
|
3940
|
+
n: index + 1,
|
|
3941
|
+
m: displayMarks.length
|
|
3942
|
+
}),
|
|
3943
|
+
turnLabel: mark.turn !== void 0 ? t("turn.label", { n: displayTurns.get(mark.turn) ?? mark.turn }) : null,
|
|
3827
3944
|
durationLabel,
|
|
3828
3945
|
reasonLabel,
|
|
3829
3946
|
ttftLabel,
|
|
@@ -3874,6 +3991,157 @@ window.__ModuleLoader__.load({
|
|
|
3874
3991
|
forkAt(mark.seq).then(() => setForkedKey(mark.key));
|
|
3875
3992
|
};
|
|
3876
3993
|
const showLoadOlder = hasMore && marks.length >= MIN_MARKS;
|
|
3994
|
+
if (railCollapsed) {
|
|
3995
|
+
const viewport = {
|
|
3996
|
+
width: window.innerWidth,
|
|
3997
|
+
height: window.innerHeight
|
|
3998
|
+
};
|
|
3999
|
+
const resting = clampBallPosition(ball ?? defaultBallPosition(viewport, side, inset), viewport);
|
|
4000
|
+
const pos = dragPos ?? resting;
|
|
4001
|
+
/** Detach the in-flight press's window listeners (idempotent). */
|
|
4002
|
+
const removeDragListeners = () => {
|
|
4003
|
+
const detach = dragListenersRef.current;
|
|
4004
|
+
if (detach === null) return;
|
|
4005
|
+
dragListenersRef.current = null;
|
|
4006
|
+
detach();
|
|
4007
|
+
};
|
|
4008
|
+
/** Release the pointer capture when the DOM implementation supports it. */
|
|
4009
|
+
const releaseBallCapture = (el, pointerId) => {
|
|
4010
|
+
if (typeof el.releasePointerCapture !== "function") return;
|
|
4011
|
+
try {
|
|
4012
|
+
el.releasePointerCapture(pointerId);
|
|
4013
|
+
} catch {}
|
|
4014
|
+
};
|
|
4015
|
+
/**
|
|
4016
|
+
* Press start: in draggable mode record the gesture anchor and arm the
|
|
4017
|
+
* window pointer listeners; in fixed mode do nothing drag-related — the
|
|
4018
|
+
* plain onClick fallback below still expands the rail.
|
|
4019
|
+
*/
|
|
4020
|
+
const onBallPointerDown = (e) => {
|
|
4021
|
+
if (ballMode !== "draggable") return;
|
|
4022
|
+
const el = e.currentTarget;
|
|
4023
|
+
if (typeof el.setPointerCapture === "function") try {
|
|
4024
|
+
el.setPointerCapture(e.pointerId);
|
|
4025
|
+
} catch {}
|
|
4026
|
+
if (dragRef.current !== null) removeDragListeners();
|
|
4027
|
+
dragRef.current = {
|
|
4028
|
+
pointerId: e.pointerId,
|
|
4029
|
+
startPointer: {
|
|
4030
|
+
x: e.clientX,
|
|
4031
|
+
y: e.clientY
|
|
4032
|
+
},
|
|
4033
|
+
startPos: pos,
|
|
4034
|
+
moved: false
|
|
4035
|
+
};
|
|
4036
|
+
const onMove = (ev) => {
|
|
4037
|
+
const drag = dragRef.current;
|
|
4038
|
+
if (drag === null) return;
|
|
4039
|
+
const live = {
|
|
4040
|
+
width: window.innerWidth,
|
|
4041
|
+
height: window.innerHeight
|
|
4042
|
+
};
|
|
4043
|
+
setDragPos(clampBallPosition({
|
|
4044
|
+
x: drag.startPos.x + (ev.clientX - drag.startPointer.x),
|
|
4045
|
+
y: drag.startPos.y + (ev.clientY - drag.startPointer.y)
|
|
4046
|
+
}, live));
|
|
4047
|
+
drag.moved = drag.moved || isDragGesture(drag.startPointer, {
|
|
4048
|
+
x: ev.clientX,
|
|
4049
|
+
y: ev.clientY
|
|
4050
|
+
});
|
|
4051
|
+
};
|
|
4052
|
+
const onUp = (ev) => {
|
|
4053
|
+
const drag = dragRef.current;
|
|
4054
|
+
if (drag === null) return;
|
|
4055
|
+
removeDragListeners();
|
|
4056
|
+
releaseBallCapture(el, drag.pointerId);
|
|
4057
|
+
if (drag.moved) {
|
|
4058
|
+
const live = {
|
|
4059
|
+
width: window.innerWidth,
|
|
4060
|
+
height: window.innerHeight
|
|
4061
|
+
};
|
|
4062
|
+
updatePrefs({ ball: clampBallPosition({
|
|
4063
|
+
x: drag.startPos.x + (ev.clientX - drag.startPointer.x),
|
|
4064
|
+
y: drag.startPos.y + (ev.clientY - drag.startPointer.y)
|
|
4065
|
+
}, live) });
|
|
4066
|
+
} else setRailCollapsed(false);
|
|
4067
|
+
setDragPos(null);
|
|
4068
|
+
dragRef.current = null;
|
|
4069
|
+
};
|
|
4070
|
+
const onCancel = () => {
|
|
4071
|
+
const drag = dragRef.current;
|
|
4072
|
+
if (drag === null) return;
|
|
4073
|
+
removeDragListeners();
|
|
4074
|
+
releaseBallCapture(el, drag.pointerId);
|
|
4075
|
+
setDragPos(null);
|
|
4076
|
+
dragRef.current = null;
|
|
4077
|
+
};
|
|
4078
|
+
window.addEventListener("pointermove", onMove);
|
|
4079
|
+
window.addEventListener("pointerup", onUp);
|
|
4080
|
+
window.addEventListener("pointercancel", onCancel);
|
|
4081
|
+
dragListenersRef.current = () => {
|
|
4082
|
+
window.removeEventListener("pointermove", onMove);
|
|
4083
|
+
window.removeEventListener("pointerup", onUp);
|
|
4084
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
4085
|
+
};
|
|
4086
|
+
};
|
|
4087
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4088
|
+
"data-milestone-ball": true,
|
|
4089
|
+
role: "button",
|
|
4090
|
+
tabIndex: 0,
|
|
4091
|
+
"aria-label": t("ball.expand"),
|
|
4092
|
+
title: t("ball.expand"),
|
|
4093
|
+
"data-ball-x": String(Math.round(pos.x)),
|
|
4094
|
+
"data-ball-y": String(Math.round(pos.y)),
|
|
4095
|
+
onPointerDown: onBallPointerDown,
|
|
4096
|
+
onClick: ballMode === "fixed" ? () => setRailCollapsed(false) : void 0,
|
|
4097
|
+
onKeyDown: (e) => {
|
|
4098
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
4099
|
+
e.preventDefault();
|
|
4100
|
+
setRailCollapsed(false);
|
|
4101
|
+
},
|
|
4102
|
+
style: {
|
|
4103
|
+
position: "fixed",
|
|
4104
|
+
left: pos.x,
|
|
4105
|
+
top: pos.y,
|
|
4106
|
+
width: 40,
|
|
4107
|
+
height: 40,
|
|
4108
|
+
borderRadius: "50%",
|
|
4109
|
+
background: accent,
|
|
4110
|
+
color: "#ffffff",
|
|
4111
|
+
opacity: .9,
|
|
4112
|
+
zIndex: 100,
|
|
4113
|
+
display: "flex",
|
|
4114
|
+
alignItems: "center",
|
|
4115
|
+
justifyContent: "center",
|
|
4116
|
+
touchAction: "none",
|
|
4117
|
+
userSelect: "none",
|
|
4118
|
+
cursor: ballMode === "draggable" ? "grab" : "pointer",
|
|
4119
|
+
boxShadow: "0 4px 14px rgba(0, 0, 0, 0.35)"
|
|
4120
|
+
},
|
|
4121
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
4122
|
+
width: "18",
|
|
4123
|
+
height: "18",
|
|
4124
|
+
viewBox: "0 0 24 24",
|
|
4125
|
+
fill: "none",
|
|
4126
|
+
stroke: "currentColor",
|
|
4127
|
+
strokeWidth: "2",
|
|
4128
|
+
strokeLinecap: "round",
|
|
4129
|
+
strokeLinejoin: "round",
|
|
4130
|
+
"aria-hidden": "true",
|
|
4131
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4132
|
+
cx: "12",
|
|
4133
|
+
cy: "12",
|
|
4134
|
+
r: "8"
|
|
4135
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4136
|
+
cx: "12",
|
|
4137
|
+
cy: "12",
|
|
4138
|
+
r: "2.5",
|
|
4139
|
+
fill: "currentColor",
|
|
4140
|
+
stroke: "none"
|
|
4141
|
+
})]
|
|
4142
|
+
})
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
3877
4145
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3878
4146
|
style: {
|
|
3879
4147
|
position: "fixed",
|
|
@@ -3931,6 +4199,40 @@ window.__ModuleLoader__.load({
|
|
|
3931
4199
|
},
|
|
3932
4200
|
children: "···"
|
|
3933
4201
|
}),
|
|
4202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4203
|
+
type: "button",
|
|
4204
|
+
"data-rail-collapse": true,
|
|
4205
|
+
"aria-label": t("rail.collapse"),
|
|
4206
|
+
title: t("rail.collapse"),
|
|
4207
|
+
onClick: () => setRailCollapsed(true),
|
|
4208
|
+
onMouseEnter: () => setRailCollapseHovered(true),
|
|
4209
|
+
onMouseLeave: () => setRailCollapseHovered(false),
|
|
4210
|
+
onFocus: () => setRailCollapseHovered(true),
|
|
4211
|
+
onBlur: () => setRailCollapseHovered(false),
|
|
4212
|
+
style: chromeButtonStyle(railCollapseHovered),
|
|
4213
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
4214
|
+
width: "16",
|
|
4215
|
+
height: "16",
|
|
4216
|
+
viewBox: "0 0 24 24",
|
|
4217
|
+
fill: "none",
|
|
4218
|
+
stroke: "currentColor",
|
|
4219
|
+
strokeWidth: "2",
|
|
4220
|
+
strokeLinecap: "round",
|
|
4221
|
+
strokeLinejoin: "round",
|
|
4222
|
+
"aria-hidden": "true",
|
|
4223
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4224
|
+
cx: "12",
|
|
4225
|
+
cy: "12",
|
|
4226
|
+
r: "8"
|
|
4227
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4228
|
+
cx: "12",
|
|
4229
|
+
cy: "12",
|
|
4230
|
+
r: "2.5",
|
|
4231
|
+
fill: "currentColor",
|
|
4232
|
+
stroke: "none"
|
|
4233
|
+
})]
|
|
4234
|
+
})
|
|
4235
|
+
}),
|
|
3934
4236
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3935
4237
|
type: "button",
|
|
3936
4238
|
"data-toolbar-expand": true,
|
|
@@ -4440,6 +4742,139 @@ window.__ModuleLoader__.load({
|
|
|
4440
4742
|
]
|
|
4441
4743
|
})]
|
|
4442
4744
|
}),
|
|
4745
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4746
|
+
"data-settings-section": true,
|
|
4747
|
+
"data-settings-ball": true,
|
|
4748
|
+
style: { marginBottom: 20 },
|
|
4749
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4750
|
+
type: "button",
|
|
4751
|
+
"data-ball-toggle": true,
|
|
4752
|
+
"aria-expanded": ballOpen,
|
|
4753
|
+
"aria-label": t("settings.section.ball"),
|
|
4754
|
+
title: t("settings.section.ball"),
|
|
4755
|
+
onClick: () => setBallOpen((v) => !v),
|
|
4756
|
+
style: SECTION_TOGGLE_STYLE,
|
|
4757
|
+
children: [
|
|
4758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
4759
|
+
width: "14",
|
|
4760
|
+
height: "14",
|
|
4761
|
+
viewBox: "0 0 24 24",
|
|
4762
|
+
fill: "none",
|
|
4763
|
+
stroke: "currentColor",
|
|
4764
|
+
strokeWidth: "2.5",
|
|
4765
|
+
strokeLinecap: "round",
|
|
4766
|
+
strokeLinejoin: "round",
|
|
4767
|
+
"aria-hidden": "true",
|
|
4768
|
+
style: {
|
|
4769
|
+
flexShrink: 0,
|
|
4770
|
+
transform: ballOpen ? "rotate(90deg)" : "none"
|
|
4771
|
+
},
|
|
4772
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m9 18 6-6-6-6" })
|
|
4773
|
+
}),
|
|
4774
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4775
|
+
"data-settings-section-title": true,
|
|
4776
|
+
style: { flexShrink: 0 },
|
|
4777
|
+
children: t("settings.section.ball")
|
|
4778
|
+
}),
|
|
4779
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4780
|
+
"data-ball-summary": true,
|
|
4781
|
+
style: SECTION_SUMMARY_STYLE,
|
|
4782
|
+
children: ballMode === "fixed" ? t("settings.ball.mode.fixed") : t("settings.ball.mode.draggable")
|
|
4783
|
+
})
|
|
4784
|
+
]
|
|
4785
|
+
}), ballOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4786
|
+
"data-settings-ball-body": true,
|
|
4787
|
+
style: {
|
|
4788
|
+
padding: "10px 4px 8px",
|
|
4789
|
+
display: "flex",
|
|
4790
|
+
flexDirection: "column",
|
|
4791
|
+
gap: 12
|
|
4792
|
+
},
|
|
4793
|
+
children: [
|
|
4794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4795
|
+
"data-settings-ball-hint": true,
|
|
4796
|
+
style: {
|
|
4797
|
+
fontSize: 12,
|
|
4798
|
+
color: "#8b96ab",
|
|
4799
|
+
lineHeight: 1.5,
|
|
4800
|
+
padding: "0 6px"
|
|
4801
|
+
},
|
|
4802
|
+
children: t("settings.ball.hint")
|
|
4803
|
+
}),
|
|
4804
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4805
|
+
role: "radiogroup",
|
|
4806
|
+
"aria-label": t("settings.ball.mode"),
|
|
4807
|
+
style: {
|
|
4808
|
+
display: "flex",
|
|
4809
|
+
alignItems: "center",
|
|
4810
|
+
gap: 10,
|
|
4811
|
+
flexWrap: "wrap"
|
|
4812
|
+
},
|
|
4813
|
+
children: [
|
|
4814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4815
|
+
style: {
|
|
4816
|
+
fontSize: 12.5,
|
|
4817
|
+
color: "#8b96ab",
|
|
4818
|
+
width: 90,
|
|
4819
|
+
flexShrink: 0
|
|
4820
|
+
},
|
|
4821
|
+
children: t("settings.ball.mode")
|
|
4822
|
+
}),
|
|
4823
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4824
|
+
style: {
|
|
4825
|
+
display: "inline-flex",
|
|
4826
|
+
alignItems: "center",
|
|
4827
|
+
gap: 5,
|
|
4828
|
+
fontSize: 13,
|
|
4829
|
+
color: "#e6e8ee",
|
|
4830
|
+
cursor: "pointer"
|
|
4831
|
+
},
|
|
4832
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4833
|
+
type: "radio",
|
|
4834
|
+
name: "ms-ball-mode",
|
|
4835
|
+
"data-ball-mode-radio": true,
|
|
4836
|
+
value: "fixed",
|
|
4837
|
+
checked: ballMode === "fixed",
|
|
4838
|
+
onChange: () => updatePrefs({ ballMode: "fixed" })
|
|
4839
|
+
}), t("settings.ball.mode.fixed")]
|
|
4840
|
+
}),
|
|
4841
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4842
|
+
style: {
|
|
4843
|
+
display: "inline-flex",
|
|
4844
|
+
alignItems: "center",
|
|
4845
|
+
gap: 5,
|
|
4846
|
+
fontSize: 13,
|
|
4847
|
+
color: "#e6e8ee",
|
|
4848
|
+
cursor: "pointer"
|
|
4849
|
+
},
|
|
4850
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4851
|
+
type: "radio",
|
|
4852
|
+
name: "ms-ball-mode",
|
|
4853
|
+
"data-ball-mode-radio": true,
|
|
4854
|
+
value: "draggable",
|
|
4855
|
+
checked: ballMode === "draggable",
|
|
4856
|
+
onChange: () => updatePrefs({ ballMode: "draggable" })
|
|
4857
|
+
}), t("settings.ball.mode.draggable")]
|
|
4858
|
+
})
|
|
4859
|
+
]
|
|
4860
|
+
}),
|
|
4861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4862
|
+
type: "button",
|
|
4863
|
+
"data-ball-reset": true,
|
|
4864
|
+
onClick: () => updatePrefs({ ball: null }),
|
|
4865
|
+
style: {
|
|
4866
|
+
padding: "7px 16px",
|
|
4867
|
+
border: `1px solid rgba(255, 255, 255, 0.14)`,
|
|
4868
|
+
borderRadius: 8,
|
|
4869
|
+
cursor: "pointer",
|
|
4870
|
+
color: "#b9c2d4",
|
|
4871
|
+
fontSize: 12.5
|
|
4872
|
+
},
|
|
4873
|
+
children: t("settings.ball.reset")
|
|
4874
|
+
}) })
|
|
4875
|
+
]
|
|
4876
|
+
})]
|
|
4877
|
+
}),
|
|
4443
4878
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4444
4879
|
"data-settings-section": true,
|
|
4445
4880
|
"data-focus-settings": true,
|
|
@@ -4838,7 +5273,7 @@ window.__ModuleLoader__.load({
|
|
|
4838
5273
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4839
5274
|
style: { color: "#8b96ab" },
|
|
4840
5275
|
children: [t("update.current"), ": "]
|
|
4841
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.
|
|
5276
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.7.0" })] }),
|
|
4842
5277
|
updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4843
5278
|
"data-update-latest": true,
|
|
4844
5279
|
children: [
|
|
@@ -4943,6 +5378,8 @@ window.__ModuleLoader__.load({
|
|
|
4943
5378
|
panelRight: panelRightFor(PANEL_WIDTH_STANDARD),
|
|
4944
5379
|
marks,
|
|
4945
5380
|
onJump: jump,
|
|
5381
|
+
onClose: () => setListOpen(false),
|
|
5382
|
+
loading: listDraining,
|
|
4946
5383
|
t
|
|
4947
5384
|
}),
|
|
4948
5385
|
crossOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneSessionSearch, {
|
|
@@ -5014,7 +5451,7 @@ window.__ModuleLoader__.load({
|
|
|
5014
5451
|
top: rect.top + rect.height / 2
|
|
5015
5452
|
});
|
|
5016
5453
|
},
|
|
5017
|
-
onClick: () => jump(mark.key),
|
|
5454
|
+
onClick: () => jump(mark.key, mark.messageId),
|
|
5018
5455
|
"data-rail-dot": true,
|
|
5019
5456
|
"data-turn-gap": showGroupGap ? "true" : void 0,
|
|
5020
5457
|
"data-turn": showGroupGap && mark.turn !== void 0 ? mark.turn : void 0,
|
|
@@ -5026,7 +5463,7 @@ window.__ModuleLoader__.load({
|
|
|
5026
5463
|
"aria-current": dotState === "active" ? "true" : void 0,
|
|
5027
5464
|
"data-current": dotState === "current" ? "true" : void 0,
|
|
5028
5465
|
"data-dimmed": dotState === "dimmed" ? "true" : void 0,
|
|
5029
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5466
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5030
5467
|
style: {
|
|
5031
5468
|
position: "relative",
|
|
5032
5469
|
width: size,
|
|
@@ -5039,7 +5476,7 @@ window.__ModuleLoader__.load({
|
|
|
5039
5476
|
opacity: isHovered || dotState !== "dimmed" ? 1 : .22
|
|
5040
5477
|
},
|
|
5041
5478
|
"data-bookmarked": bookmarked ? "true" : void 0,
|
|
5042
|
-
children: ringStyle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5479
|
+
children: [ringStyle !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5043
5480
|
"data-badge": badge,
|
|
5044
5481
|
style: {
|
|
5045
5482
|
position: "absolute",
|
|
@@ -5050,7 +5487,31 @@ window.__ModuleLoader__.load({
|
|
|
5050
5487
|
pointerEvents: "none",
|
|
5051
5488
|
animation: ringStyle.pulse ? "milestone-badge-pulse 2s ease-in-out infinite" : void 0
|
|
5052
5489
|
}
|
|
5053
|
-
})
|
|
5490
|
+
}), summaryCount !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5491
|
+
"data-collapsed-badge": true,
|
|
5492
|
+
style: {
|
|
5493
|
+
position: "absolute",
|
|
5494
|
+
top: -5,
|
|
5495
|
+
right: -7,
|
|
5496
|
+
minWidth: 15,
|
|
5497
|
+
height: 15,
|
|
5498
|
+
padding: "0 3px",
|
|
5499
|
+
display: "flex",
|
|
5500
|
+
alignItems: "center",
|
|
5501
|
+
justifyContent: "center",
|
|
5502
|
+
borderRadius: 8,
|
|
5503
|
+
background: "rgba(20, 24, 32, 0.96)",
|
|
5504
|
+
border: `1px solid ${accentSoft}`,
|
|
5505
|
+
boxSizing: "border-box",
|
|
5506
|
+
color: "#e6e8ee",
|
|
5507
|
+
fontSize: 9,
|
|
5508
|
+
lineHeight: 1,
|
|
5509
|
+
fontWeight: 600,
|
|
5510
|
+
whiteSpace: "nowrap",
|
|
5511
|
+
pointerEvents: "none"
|
|
5512
|
+
},
|
|
5513
|
+
children: ["×", summaryCount]
|
|
5514
|
+
})]
|
|
5054
5515
|
})
|
|
5055
5516
|
}) }, mark.key);
|
|
5056
5517
|
})
|
|
@@ -5105,13 +5566,18 @@ window.__ModuleLoader__.load({
|
|
|
5105
5566
|
* resolved by the engine's `create(scopeKey)`). Consumers must call the
|
|
5106
5567
|
* FACTORY (never a module-level handle — module-cache identity is a disguised
|
|
5107
5568
|
* singleton across plugin reloads).
|
|
5569
|
+
*
|
|
5570
|
+
* 0.1.2 compat: the snapshot-store engine moved out of
|
|
5571
|
+
* `@deepseek-ai/dsh-client-runtime/client` into the platform module
|
|
5572
|
+
* `@deepseek-ai/dsh-client-store` (a web module-table seed); the old
|
|
5573
|
+
* specifier is gone from the 0.1.2 module table.
|
|
5108
5574
|
*/
|
|
5109
5575
|
/**
|
|
5110
5576
|
* Declare the bookmarks store handle. Returns a fresh handle per call; the
|
|
5111
5577
|
* framework (or tests) create per-session instances via `create(scopeKey)`.
|
|
5112
5578
|
*/
|
|
5113
5579
|
function createBookmarksStore() {
|
|
5114
|
-
return (0,
|
|
5580
|
+
return (0, _deepseek_ai_dsh_client_store.defineStore)({
|
|
5115
5581
|
init: () => ({ keys: [] }),
|
|
5116
5582
|
persist: "dsh-milestone.bookmarks",
|
|
5117
5583
|
actions: {
|
|
@@ -5150,7 +5616,7 @@ window.__ModuleLoader__.load({
|
|
|
5150
5616
|
return {
|
|
5151
5617
|
items: result.value.items.map((item) => ({
|
|
5152
5618
|
...item,
|
|
5153
|
-
title: byId[item.sessionId]?.
|
|
5619
|
+
title: byId[item.sessionId]?.title
|
|
5154
5620
|
})),
|
|
5155
5621
|
hasMore: result.value.hasMore
|
|
5156
5622
|
};
|
|
@@ -5219,6 +5685,7 @@ window.__ModuleLoader__.load({
|
|
|
5219
5685
|
* @param ctx - client root context.
|
|
5220
5686
|
*/
|
|
5221
5687
|
function apply(ctx) {
|
|
5688
|
+
const sessions = ctx.sessions;
|
|
5222
5689
|
ctx.effect(() => ctx.locale.register("dsh-milestone", {
|
|
5223
5690
|
zh,
|
|
5224
5691
|
en
|
|
@@ -5237,10 +5704,10 @@ window.__ModuleLoader__.load({
|
|
|
5237
5704
|
store: createBookmarksStore,
|
|
5238
5705
|
locale: "dsh-milestone",
|
|
5239
5706
|
inject: (sessionId) => ({
|
|
5240
|
-
loadOlder: createLoadOlder(
|
|
5241
|
-
forkAt: createForkAt(
|
|
5242
|
-
searchSessions: createSessionSearch(
|
|
5243
|
-
openSession: createOpenSession(
|
|
5707
|
+
loadOlder: createLoadOlder(sessions, sessionId),
|
|
5708
|
+
forkAt: createForkAt(sessions, sessionId),
|
|
5709
|
+
searchSessions: createSessionSearch(sessions),
|
|
5710
|
+
openSession: createOpenSession(sessions)
|
|
5244
5711
|
})
|
|
5245
5712
|
}, MilestoneRail));
|
|
5246
5713
|
}
|