dsh-milestone 0.6.6 → 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 -165
- package/lib/client.js +544 -172
- 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
|
-
};
|
|
404
|
-
return {
|
|
405
|
-
inputTokens: typeof usage.inputTokens === "number" ? usage.inputTokens : null,
|
|
406
|
-
outputTokens: typeof usage.outputTokens === "number" ? usage.outputTokens : null
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
/** Resolve model/purpose from a request config, falling back to provenance. */
|
|
410
|
-
function metaFromRecord(record) {
|
|
471
|
+
function deriveTurnMetaFromProjection(turn) {
|
|
472
|
+
if (turn?.usage === void 0) return EMPTY_META;
|
|
411
473
|
return {
|
|
412
|
-
model:
|
|
413
|
-
purpose:
|
|
414
|
-
|
|
474
|
+
model: null,
|
|
475
|
+
purpose: null,
|
|
476
|
+
inputTokens: turn.usage.input,
|
|
477
|
+
outputTokens: turn.usage.output
|
|
415
478
|
};
|
|
416
479
|
}
|
|
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.
|
|
@@ -621,6 +637,10 @@ window.__ModuleLoader__.load({
|
|
|
621
637
|
"rail.label": "会话里程碑",
|
|
622
638
|
/** aria-label on the dot list. */
|
|
623
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": "展开里程碑条",
|
|
624
644
|
/** Hover preview fallback for empty message text. */
|
|
625
645
|
"no.text": "(无文本)",
|
|
626
646
|
/** Relative time: `< 60s`. */
|
|
@@ -711,6 +731,8 @@ window.__ModuleLoader__.load({
|
|
|
711
731
|
"settings.section.personal": "个性化",
|
|
712
732
|
/** Settings modal: section heading for the focus-mode controls (0.6.3). */
|
|
713
733
|
"settings.section.focus": "聚焦",
|
|
734
|
+
/** Settings modal: section heading for the floating-ball controls (issue #4). */
|
|
735
|
+
"settings.section.ball": "悬浮球",
|
|
714
736
|
/** Settings: personalization-section hint shown inside the expanded block. */
|
|
715
737
|
"settings.personal.hint": "圆点、强调色与位置,即调即存",
|
|
716
738
|
/** Settings: aria-label on the personalization block toggle while COLLAPSED. */
|
|
@@ -747,6 +769,16 @@ window.__ModuleLoader__.load({
|
|
|
747
769
|
"settings.side.left": "左侧",
|
|
748
770
|
/** Settings personalization: side radio — hug the right edge. */
|
|
749
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": "重置位置",
|
|
750
782
|
/** Settings: focus block — hint shown inside the expanded block. */
|
|
751
783
|
"settings.focus.hint": "这些选项自由组合成你的「聚焦搭配」;总开关仍是工具栏的眼睛按钮",
|
|
752
784
|
/** Settings: aria-label on the focus block toggle while COLLAPSED. */
|
|
@@ -853,6 +885,8 @@ window.__ModuleLoader__.load({
|
|
|
853
885
|
"load.older": "Load older messages",
|
|
854
886
|
"rail.label": "Session milestones",
|
|
855
887
|
"rail.list": "Session milestone list",
|
|
888
|
+
"rail.collapse": "Collapse to floating ball",
|
|
889
|
+
"ball.expand": "Expand milestone rail",
|
|
856
890
|
"no.text": "(no text)",
|
|
857
891
|
"time.justNow": "Just now",
|
|
858
892
|
"time.minutes": "{n} minutes ago",
|
|
@@ -899,6 +933,7 @@ window.__ModuleLoader__.load({
|
|
|
899
933
|
"settings.section.features": "Features & Shortcuts",
|
|
900
934
|
"settings.section.personal": "Personalization",
|
|
901
935
|
"settings.section.focus": "Focus",
|
|
936
|
+
"settings.section.ball": "Floating ball",
|
|
902
937
|
"settings.personal.hint": "Dot size, accent color, and position — saved as you adjust",
|
|
903
938
|
"settings.personal.expand": "Expand personalization",
|
|
904
939
|
"settings.personal.collapse": "Collapse personalization",
|
|
@@ -917,6 +952,11 @@ window.__ModuleLoader__.load({
|
|
|
917
952
|
"settings.side": "Position",
|
|
918
953
|
"settings.side.left": "Left",
|
|
919
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",
|
|
920
960
|
"settings.focus.hint": "Combine these options into your own focus recipe; the eye button on the toolbar stays the master switch",
|
|
921
961
|
"settings.focus.expand": "Expand focus settings",
|
|
922
962
|
"settings.focus.collapse": "Collapse focus settings",
|
|
@@ -2287,7 +2327,8 @@ window.__ModuleLoader__.load({
|
|
|
2287
2327
|
* toolbar-prefs: the persistence layer for the milestone rail's toolbar
|
|
2288
2328
|
* personalization — WHICH function keys stay visible outside the collapse
|
|
2289
2329
|
* (pinned) plus the settings-module appearance prefs (accent color, icon/dot
|
|
2290
|
-
* 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).
|
|
2291
2332
|
*
|
|
2292
2333
|
* Storage contract: one localStorage key (`dsh-milestone.toolbar`) holding a
|
|
2293
2334
|
* JSON object:
|
|
@@ -2295,13 +2336,17 @@ window.__ModuleLoader__.load({
|
|
|
2295
2336
|
* { "pinned": string[], "accent": "#rrggbb", "iconSize": number,
|
|
2296
2337
|
* "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en",
|
|
2297
2338
|
* "focus": { "dimThink": boolean, "dimTools": boolean,
|
|
2298
|
-
* "collapseThink": boolean, "opacity": number }
|
|
2339
|
+
* "collapseThink": boolean, "opacity": number },
|
|
2340
|
+
* "ballMode": "fixed" | "draggable",
|
|
2341
|
+
* "ball": { "x": number, "y": number } | null }
|
|
2299
2342
|
*
|
|
2300
2343
|
* Backward compatibility: the pre-personalization blob `{ "pinned": string[] }`
|
|
2301
2344
|
* (and an entirely absent value) parses to the DEFAULT prefs with the new
|
|
2302
2345
|
* fields at their defaults — old users keep their pins untouched. The same
|
|
2303
2346
|
* rule covers the `focus` object: a blob stored before 0.6.3 (no `focus`
|
|
2304
|
-
* 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).
|
|
2305
2350
|
*
|
|
2306
2351
|
* All reads are sanitized per field:
|
|
2307
2352
|
* - `pinned`: whitelisted ids only (`TOOLBAR_PIN_IDS`), duplicates dropped,
|
|
@@ -2313,7 +2358,11 @@ window.__ModuleLoader__.load({
|
|
|
2313
2358
|
* - `side`: exactly `'left'` or `'right'`;
|
|
2314
2359
|
* - `focus`: three booleans (`dimThink` / `dimTools` / `collapseThink`)
|
|
2315
2360
|
* defaulting to `true` / `false` / `false`, plus the dim `opacity`
|
|
2316
|
-
* 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.
|
|
2317
2366
|
*
|
|
2318
2367
|
* The whitelist lives HERE (not in MilestoneRail) so the pure functions stay
|
|
2319
2368
|
* dependency-free and unit-testable; MilestoneRail's feature registry keys
|
|
@@ -2357,7 +2406,9 @@ window.__ModuleLoader__.load({
|
|
|
2357
2406
|
inset: 14,
|
|
2358
2407
|
side: "right",
|
|
2359
2408
|
locale: "system",
|
|
2360
|
-
focus: { ...DEFAULT_FOCUS_PREFS }
|
|
2409
|
+
focus: { ...DEFAULT_FOCUS_PREFS },
|
|
2410
|
+
ballMode: "draggable",
|
|
2411
|
+
ball: null
|
|
2361
2412
|
};
|
|
2362
2413
|
/** Type guard for registry ids — unknown strings never survive a parse. */
|
|
2363
2414
|
function isToolbarPinId(id) {
|
|
@@ -2425,7 +2476,7 @@ window.__ModuleLoader__.load({
|
|
|
2425
2476
|
return { ...DEFAULT_PREFS };
|
|
2426
2477
|
}
|
|
2427
2478
|
if (typeof parsed !== "object" || parsed === null) return { ...DEFAULT_PREFS };
|
|
2428
|
-
const { pinned, accent, iconSize, inset, side, locale, focus } = parsed;
|
|
2479
|
+
const { pinned, accent, iconSize, inset, side, locale, focus, ballMode, ball } = parsed;
|
|
2429
2480
|
return {
|
|
2430
2481
|
pinned: sanitizePinned(pinned),
|
|
2431
2482
|
accent: typeof accent === "string" && isHexColor(accent) ? accent.toLowerCase() : DEFAULT_PREFS.accent,
|
|
@@ -2433,7 +2484,9 @@ window.__ModuleLoader__.load({
|
|
|
2433
2484
|
inset: clampStep(inset, 0, 40, 2, DEFAULT_PREFS.inset),
|
|
2434
2485
|
side: side === "left" || side === "right" ? side : DEFAULT_PREFS.side,
|
|
2435
2486
|
locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale,
|
|
2436
|
-
focus: sanitizeFocus(focus)
|
|
2487
|
+
focus: sanitizeFocus(focus),
|
|
2488
|
+
ballMode: ballMode === "fixed" ? "fixed" : "draggable",
|
|
2489
|
+
ball: sanitizeBallPosition(ball)
|
|
2437
2490
|
};
|
|
2438
2491
|
}
|
|
2439
2492
|
/**
|
|
@@ -2779,7 +2832,7 @@ window.__ModuleLoader__.load({
|
|
|
2779
2832
|
* Installed plugin version. Injected at build time as
|
|
2780
2833
|
* `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
|
|
2781
2834
|
*/
|
|
2782
|
-
const PLUGIN_VERSION = "0.
|
|
2835
|
+
const PLUGIN_VERSION = "0.7.0";
|
|
2783
2836
|
//#endregion
|
|
2784
2837
|
//#region src/client/MilestoneRail.tsx
|
|
2785
2838
|
/**
|
|
@@ -2833,9 +2886,12 @@ window.__ModuleLoader__.load({
|
|
|
2833
2886
|
*/
|
|
2834
2887
|
/** Minimum user messages before the rail adds value. */
|
|
2835
2888
|
const MIN_MARKS = 2;
|
|
2836
|
-
const PREVIEW_LENGTH = 80;
|
|
2837
2889
|
/** Stable no-bookmarks fallback for render paths without the store seat. */
|
|
2838
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();
|
|
2839
2895
|
/** Stable no-kinds fallback for marks whose turn carries no badge nodes. */
|
|
2840
2896
|
const NO_KINDS = [];
|
|
2841
2897
|
/** Visual dot diameter at the default icon size (px). */
|
|
@@ -2958,24 +3014,24 @@ window.__ModuleLoader__.load({
|
|
|
2958
3014
|
/* Row and header washes (the inline styles deliberately leave backgrounds
|
|
2959
3015
|
unset so these rules win over the default padding-box background). */
|
|
2960
3016
|
[data-toolbar-pin-toggle]:hover, [data-toolbar-pin-toggle]:focus-visible { background: rgba(255, 255, 255, 0.06); }
|
|
2961
|
-
[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); }
|
|
2962
3018
|
[data-focus-option]:hover { background: rgba(255, 255, 255, 0.04); }
|
|
2963
3019
|
[data-toolbar-settings-close]:hover { background: rgba(255, 255, 255, 0.08); }
|
|
2964
|
-
[data-toolbar-settings-reset], [data-onboarding-reopen] { background: rgba(255, 255, 255, 0.06); }
|
|
2965
|
-
[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); }
|
|
2966
3022
|
/* BASE state reset: every modal surface must sit transparent on the dark
|
|
2967
3023
|
panel — without it the UA default button face (light gray) floods through
|
|
2968
3024
|
and rows become unreadable light-on-light. Hover washes above take over
|
|
2969
3025
|
on interaction. */
|
|
2970
|
-
[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],
|
|
2971
3027
|
[data-toolbar-settings-close], [data-focus-option] {
|
|
2972
3028
|
background: transparent;
|
|
2973
3029
|
}
|
|
2974
3030
|
/* ONE accent ring for keyboard focus on every modal control. */
|
|
2975
3031
|
[data-toolbar-pin-toggle]:focus-visible, [data-personal-toggle]:focus-visible,
|
|
2976
|
-
[data-focus-toggle-settings]:focus-visible,
|
|
3032
|
+
[data-focus-toggle-settings]:focus-visible, [data-ball-toggle]:focus-visible,
|
|
2977
3033
|
[data-toolbar-settings-close]:focus-visible, [data-toolbar-settings-reset]:focus-visible,
|
|
2978
|
-
[data-onboarding-reopen]:focus-visible {
|
|
3034
|
+
[data-onboarding-reopen]:focus-visible, [data-ball-reset]:focus-visible {
|
|
2979
3035
|
box-shadow: 0 0 0 2px var(--ms-accent-soft);
|
|
2980
3036
|
}
|
|
2981
3037
|
/* Near-row description tip: a rotated square peeks out of the LEFT edge so
|
|
@@ -2989,10 +3045,10 @@ window.__ModuleLoader__.load({
|
|
|
2989
3045
|
}
|
|
2990
3046
|
/* Tip + chevron motion lives here (not inline) so reduced-motion can kill it. */
|
|
2991
3047
|
[data-settings-tip] { transition: opacity 140ms ease, transform 140ms ease, visibility 140ms; }
|
|
2992
|
-
[data-personal-toggle] svg, [data-focus-toggle-settings] svg { transition: transform 150ms ease; }
|
|
2993
|
-
/* 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. */
|
|
2994
3050
|
@keyframes ms-settings-fade { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } }
|
|
2995
|
-
[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; }
|
|
2996
3052
|
/* Thin themed scrollbar for the scrollable modal panel. */
|
|
2997
3053
|
[data-toolbar-settings-panel]::-webkit-scrollbar { width: 10px; }
|
|
2998
3054
|
[data-toolbar-settings-panel]::-webkit-scrollbar-thumb {
|
|
@@ -3000,8 +3056,8 @@ window.__ModuleLoader__.load({
|
|
|
3000
3056
|
}
|
|
3001
3057
|
[data-toolbar-settings-panel]::-webkit-scrollbar-track { background: transparent; }
|
|
3002
3058
|
@media (prefers-reduced-motion: reduce) {
|
|
3003
|
-
[data-settings-tip], [data-personal-toggle] svg, [data-focus-toggle-settings] svg, [data-support-card] { transition: none; }
|
|
3004
|
-
[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; }
|
|
3005
3061
|
}
|
|
3006
3062
|
[data-search-toggle] { color: #8b96ab !important; }
|
|
3007
3063
|
[data-search-toggle][aria-pressed="true"] { background: var(--ms-accent-bg) !important; color: var(--ms-accent-soft) !important; }
|
|
@@ -3075,26 +3131,21 @@ window.__ModuleLoader__.load({
|
|
|
3075
3131
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
3076
3132
|
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
3077
3133
|
*/
|
|
3078
|
-
function findRow(key) {
|
|
3079
|
-
|
|
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
|
+
}
|
|
3080
3141
|
return null;
|
|
3081
3142
|
}
|
|
3082
|
-
/** Extract a plain-text hover preview (first 80 chars) from a ContentBlock[]. */
|
|
3083
|
-
function extractPreview(content) {
|
|
3084
|
-
return extractText(content).slice(0, PREVIEW_LENGTH);
|
|
3085
|
-
}
|
|
3086
3143
|
/** Compact duration label (ms). */
|
|
3087
3144
|
function formatDuration(ms) {
|
|
3088
3145
|
if (ms < 1e3) return `${ms}ms`;
|
|
3089
3146
|
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
3090
3147
|
return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
3091
3148
|
}
|
|
3092
|
-
/** Read the ui-conversation 'turn-tail' location data (ttftMs/tokensPerSecond). */
|
|
3093
|
-
function turnTailOf(turn) {
|
|
3094
|
-
const data = turn.data;
|
|
3095
|
-
if (data?.get === void 0) return void 0;
|
|
3096
|
-
return data.get("turn-tail");
|
|
3097
|
-
}
|
|
3098
3149
|
/**
|
|
3099
3150
|
* @param props - session standard kit (useSession, sessionId, useProjection),
|
|
3100
3151
|
* the injected loadOlder/forkAt actions, the bookmarks store pair (useStore +
|
|
@@ -3103,52 +3154,28 @@ window.__ModuleLoader__.load({
|
|
|
3103
3154
|
* `locale: 'dsh-milestone'`; defaults to a key-pass fallback for renders
|
|
3104
3155
|
* outside the slot machinery).
|
|
3105
3156
|
*/
|
|
3106
|
-
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
3157
|
+
function MilestoneRail({ useSession, useProjection, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
3107
3158
|
items: [],
|
|
3108
3159
|
hasMore: false
|
|
3109
3160
|
}), openSession = () => {}, t: frameworkT = (key) => key }) {
|
|
3110
|
-
const
|
|
3111
|
-
const nodes = useSession((s) => s.chat.nodes);
|
|
3112
|
-
const locations = useSession((s) => s.chat.locations);
|
|
3113
|
-
const timeline = useSession((s) => s.chat.timeline);
|
|
3114
|
-
const trajectoryRequests = useSession((s) => s.views.get("trajectory")?.requests);
|
|
3161
|
+
const projection = useProjection?.("milestone.messages");
|
|
3115
3162
|
const hasMore = useSession((s) => s.hasMore);
|
|
3116
3163
|
const loadingOlder = useSession((s) => s.loadingOlder);
|
|
3117
3164
|
const bookmarkedKeys = useStore?.((s) => s.keys) ?? NO_BOOKMARKS;
|
|
3118
3165
|
const marks = (0, react.useMemo)(() => {
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
text: extractText(data.content),
|
|
3131
|
-
preview: extractPreview(data.content)
|
|
3132
|
-
});
|
|
3133
|
-
}
|
|
3134
|
-
return result;
|
|
3135
|
-
}, [order, nodes]);
|
|
3136
|
-
const kindsByTurn = (0, react.useMemo)(() => {
|
|
3137
|
-
const result = /* @__PURE__ */ new Map();
|
|
3138
|
-
for (const node of nodes.values()) {
|
|
3139
|
-
if (node.kind !== "turn-error" && node.kind !== "turn-max-tokens" && node.kind !== "model-retry") continue;
|
|
3140
|
-
if (node.kind === "model-retry") {
|
|
3141
|
-
if (node.data?.retryState === "cancelled") continue;
|
|
3142
|
-
}
|
|
3143
|
-
if (node.location.kind !== "turn" && node.location.kind !== "step") continue;
|
|
3144
|
-
const kinds = result.get(node.location.turn.turn) ?? [];
|
|
3145
|
-
kinds.push(node.kind);
|
|
3146
|
-
result.set(node.location.turn.turn, kinds);
|
|
3147
|
-
}
|
|
3148
|
-
return result;
|
|
3149
|
-
}, [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;
|
|
3150
3177
|
const running = useSession((s) => s.running);
|
|
3151
|
-
const awaitingInput = useSession((s) => s.
|
|
3178
|
+
const awaitingInput = useSession((s) => s.queue).length > 0;
|
|
3152
3179
|
const [railBox, setRailBox] = (0, react.useState)(null);
|
|
3153
3180
|
const [hover, setHover] = (0, react.useState)(null);
|
|
3154
3181
|
const [search, setSearch] = (0, react.useState)({
|
|
@@ -3167,7 +3194,7 @@ window.__ModuleLoader__.load({
|
|
|
3167
3194
|
const [collapsedTurns, setCollapsedTurns] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
3168
3195
|
const [focusIndex, setFocusIndex] = (0, react.useState)(0);
|
|
3169
3196
|
const listRef = (0, react.useRef)(null);
|
|
3170
|
-
const currentKey = useCurrentAnchor(
|
|
3197
|
+
const currentKey = useCurrentAnchor(marks.map((m) => m.key));
|
|
3171
3198
|
/**
|
|
3172
3199
|
* P3: jump to the chat row with the given node key — smooth-scroll it into
|
|
3173
3200
|
* view and write the position back into the URL hash (`#msg=<key>`) so
|
|
@@ -3177,8 +3204,8 @@ window.__ModuleLoader__.load({
|
|
|
3177
3204
|
* rail's own updates. No-op when the row is not (yet) rendered — the
|
|
3178
3205
|
* deep-link mount retry and the load-older flow cover that case.
|
|
3179
3206
|
*/
|
|
3180
|
-
const jump = (key) => {
|
|
3181
|
-
const row = findRow(key);
|
|
3207
|
+
const jump = (key, messageId) => {
|
|
3208
|
+
const row = findRow(key, messageId);
|
|
3182
3209
|
if (row === null) return;
|
|
3183
3210
|
row.scrollIntoView({
|
|
3184
3211
|
behavior: "smooth",
|
|
@@ -3215,7 +3242,7 @@ window.__ModuleLoader__.load({
|
|
|
3215
3242
|
}, [displayMarks]);
|
|
3216
3243
|
const displayTurns = (0, react.useMemo)(() => buildDisplayTurns(marks), [marks]);
|
|
3217
3244
|
const [prefs, setPrefs] = (0, react.useState)(() => loadPrefs());
|
|
3218
|
-
const { pinned, accent, iconSize, inset, side } = prefs;
|
|
3245
|
+
const { pinned, accent, iconSize, inset, side, ballMode, ball } = prefs;
|
|
3219
3246
|
const scale = iconSize / DOT_HIT;
|
|
3220
3247
|
const hit = iconSize;
|
|
3221
3248
|
const size = DOT_SIZE * scale;
|
|
@@ -3295,6 +3322,27 @@ window.__ModuleLoader__.load({
|
|
|
3295
3322
|
/** B-design (0.6.3): the focus block mirrors the personalization block —
|
|
3296
3323
|
* collapsed by default, the header leads with a live option summary. */
|
|
3297
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]);
|
|
3298
3346
|
const settingsRef = (0, react.useRef)(null);
|
|
3299
3347
|
const settingsBtnRef = (0, react.useRef)(null);
|
|
3300
3348
|
const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
|
|
@@ -3396,11 +3444,12 @@ window.__ModuleLoader__.load({
|
|
|
3396
3444
|
let timer;
|
|
3397
3445
|
const attempt = (pollsLeft, canLoadOlder) => {
|
|
3398
3446
|
if (cancelled) return;
|
|
3399
|
-
|
|
3400
|
-
|
|
3447
|
+
const mark = marksRef.current.find((m) => m.key === key);
|
|
3448
|
+
if (findRow(key, mark?.messageId) !== null) {
|
|
3449
|
+
jump(key, mark?.messageId);
|
|
3401
3450
|
return;
|
|
3402
3451
|
}
|
|
3403
|
-
if (marksRef.current.length > 0 &&
|
|
3452
|
+
if (marksRef.current.length > 0 && mark === void 0) return;
|
|
3404
3453
|
if (pollsLeft > 0) {
|
|
3405
3454
|
timer = window.setTimeout(() => attempt(pollsLeft - 1, canLoadOlder), DEEP_LINK_POLL_DELAY);
|
|
3406
3455
|
return;
|
|
@@ -3422,7 +3471,8 @@ window.__ModuleLoader__.load({
|
|
|
3422
3471
|
const onHashChange = () => {
|
|
3423
3472
|
const key = parseDeepLinkHash(window.location.hash);
|
|
3424
3473
|
if (key === null) return;
|
|
3425
|
-
|
|
3474
|
+
const mark = marksRef.current.find((m) => m.key === key);
|
|
3475
|
+
if (mark !== void 0) jump(key, mark.messageId);
|
|
3426
3476
|
};
|
|
3427
3477
|
window.addEventListener("hashchange", onHashChange);
|
|
3428
3478
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
@@ -3531,7 +3581,8 @@ window.__ModuleLoader__.load({
|
|
|
3531
3581
|
...s,
|
|
3532
3582
|
activePos: next
|
|
3533
3583
|
}));
|
|
3534
|
-
|
|
3584
|
+
const mark = displayMarks[matches[next]];
|
|
3585
|
+
jump(mark.key, mark.messageId);
|
|
3535
3586
|
};
|
|
3536
3587
|
const onSearchKeyDown = (e) => {
|
|
3537
3588
|
if (e.key === "Enter") advanceMatch();
|
|
@@ -3861,24 +3912,21 @@ window.__ModuleLoader__.load({
|
|
|
3861
3912
|
const buildHover = (mark, index) => {
|
|
3862
3913
|
if (copiedKey !== null && mark.key !== copiedKey) setCopiedKey(null);
|
|
3863
3914
|
if (forkedKey !== null && mark.key !== forkedKey) setForkedKey(null);
|
|
3864
|
-
const
|
|
3915
|
+
const turnMeta = mark.turn !== void 0 ? projection?.turns.find((turn) => turn.turn === mark.turn) : void 0;
|
|
3865
3916
|
let durationLabel = null;
|
|
3866
3917
|
let reasonLabel = null;
|
|
3867
3918
|
let ttftLabel = null;
|
|
3868
3919
|
let tpsLabel = null;
|
|
3869
|
-
if (
|
|
3870
|
-
if (
|
|
3871
|
-
if (
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
if (tail !== void 0) {
|
|
3877
|
-
if (tail.ttftMs !== void 0) ttftLabel = formatDuration(tail.ttftMs);
|
|
3878
|
-
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`;
|
|
3879
3927
|
}
|
|
3880
3928
|
}
|
|
3881
|
-
const meta =
|
|
3929
|
+
const meta = deriveTurnMetaFromProjection(turnMeta);
|
|
3882
3930
|
const summaryCount = collapsedSummaries.get(mark.key);
|
|
3883
3931
|
return {
|
|
3884
3932
|
mark,
|
|
@@ -3943,6 +3991,157 @@ window.__ModuleLoader__.load({
|
|
|
3943
3991
|
forkAt(mark.seq).then(() => setForkedKey(mark.key));
|
|
3944
3992
|
};
|
|
3945
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
|
+
}
|
|
3946
4145
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3947
4146
|
style: {
|
|
3948
4147
|
position: "fixed",
|
|
@@ -4000,6 +4199,40 @@ window.__ModuleLoader__.load({
|
|
|
4000
4199
|
},
|
|
4001
4200
|
children: "···"
|
|
4002
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
|
+
}),
|
|
4003
4236
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4004
4237
|
type: "button",
|
|
4005
4238
|
"data-toolbar-expand": true,
|
|
@@ -4509,6 +4742,139 @@ window.__ModuleLoader__.load({
|
|
|
4509
4742
|
]
|
|
4510
4743
|
})]
|
|
4511
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
|
+
}),
|
|
4512
4878
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4513
4879
|
"data-settings-section": true,
|
|
4514
4880
|
"data-focus-settings": true,
|
|
@@ -4907,7 +5273,7 @@ window.__ModuleLoader__.load({
|
|
|
4907
5273
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4908
5274
|
style: { color: "#8b96ab" },
|
|
4909
5275
|
children: [t("update.current"), ": "]
|
|
4910
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.
|
|
5276
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.7.0" })] }),
|
|
4911
5277
|
updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4912
5278
|
"data-update-latest": true,
|
|
4913
5279
|
children: [
|
|
@@ -5085,7 +5451,7 @@ window.__ModuleLoader__.load({
|
|
|
5085
5451
|
top: rect.top + rect.height / 2
|
|
5086
5452
|
});
|
|
5087
5453
|
},
|
|
5088
|
-
onClick: () => jump(mark.key),
|
|
5454
|
+
onClick: () => jump(mark.key, mark.messageId),
|
|
5089
5455
|
"data-rail-dot": true,
|
|
5090
5456
|
"data-turn-gap": showGroupGap ? "true" : void 0,
|
|
5091
5457
|
"data-turn": showGroupGap && mark.turn !== void 0 ? mark.turn : void 0,
|
|
@@ -5200,13 +5566,18 @@ window.__ModuleLoader__.load({
|
|
|
5200
5566
|
* resolved by the engine's `create(scopeKey)`). Consumers must call the
|
|
5201
5567
|
* FACTORY (never a module-level handle — module-cache identity is a disguised
|
|
5202
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.
|
|
5203
5574
|
*/
|
|
5204
5575
|
/**
|
|
5205
5576
|
* Declare the bookmarks store handle. Returns a fresh handle per call; the
|
|
5206
5577
|
* framework (or tests) create per-session instances via `create(scopeKey)`.
|
|
5207
5578
|
*/
|
|
5208
5579
|
function createBookmarksStore() {
|
|
5209
|
-
return (0,
|
|
5580
|
+
return (0, _deepseek_ai_dsh_client_store.defineStore)({
|
|
5210
5581
|
init: () => ({ keys: [] }),
|
|
5211
5582
|
persist: "dsh-milestone.bookmarks",
|
|
5212
5583
|
actions: {
|
|
@@ -5245,7 +5616,7 @@ window.__ModuleLoader__.load({
|
|
|
5245
5616
|
return {
|
|
5246
5617
|
items: result.value.items.map((item) => ({
|
|
5247
5618
|
...item,
|
|
5248
|
-
title: byId[item.sessionId]?.
|
|
5619
|
+
title: byId[item.sessionId]?.title
|
|
5249
5620
|
})),
|
|
5250
5621
|
hasMore: result.value.hasMore
|
|
5251
5622
|
};
|
|
@@ -5314,6 +5685,7 @@ window.__ModuleLoader__.load({
|
|
|
5314
5685
|
* @param ctx - client root context.
|
|
5315
5686
|
*/
|
|
5316
5687
|
function apply(ctx) {
|
|
5688
|
+
const sessions = ctx.sessions;
|
|
5317
5689
|
ctx.effect(() => ctx.locale.register("dsh-milestone", {
|
|
5318
5690
|
zh,
|
|
5319
5691
|
en
|
|
@@ -5332,10 +5704,10 @@ window.__ModuleLoader__.load({
|
|
|
5332
5704
|
store: createBookmarksStore,
|
|
5333
5705
|
locale: "dsh-milestone",
|
|
5334
5706
|
inject: (sessionId) => ({
|
|
5335
|
-
loadOlder: createLoadOlder(
|
|
5336
|
-
forkAt: createForkAt(
|
|
5337
|
-
searchSessions: createSessionSearch(
|
|
5338
|
-
openSession: createOpenSession(
|
|
5707
|
+
loadOlder: createLoadOlder(sessions, sessionId),
|
|
5708
|
+
forkAt: createForkAt(sessions, sessionId),
|
|
5709
|
+
searchSessions: createSessionSearch(sessions),
|
|
5710
|
+
openSession: createOpenSession(sessions)
|
|
5339
5711
|
})
|
|
5340
5712
|
}, MilestoneRail));
|
|
5341
5713
|
}
|