dsh-milestone 0.5.0 → 0.6.1
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 +21 -0
- package/lib/client.js +1894 -59
- package/package.json +9 -9
package/lib/client.js
CHANGED
|
@@ -149,6 +149,59 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
//#endregion
|
|
152
|
+
//#region src/client/deep-link-logic.ts
|
|
153
|
+
/**
|
|
154
|
+
* Pure deep-link helpers for the milestone rail: parse the `#msg=<key>` URL
|
|
155
|
+
* hash and rebuild it.
|
|
156
|
+
*
|
|
157
|
+
* The conversation anchor key is treated as an OPAQUE string — it is a
|
|
158
|
+
* length-prefixed node key like `13:input-message<messageId>` or
|
|
159
|
+
* `14:assistant-step3:2` (the length prefix makes naive splitting ambiguous),
|
|
160
|
+
* so the parser never inspects or splits the key itself. Callers match the
|
|
161
|
+
* returned key against the session's marks, which is the real validity check.
|
|
162
|
+
*
|
|
163
|
+
* Percent-encoding: the URL fragment parser percent-encodes `"` `<` `>` `` ` ``
|
|
164
|
+
* and lone `%`, so a hash built with a RAW `<`-containing key would read back
|
|
165
|
+
* from `location.hash` percent-encoded (`13:user%3Cdl-2%3E`) and never match
|
|
166
|
+
* a mark after a refresh. `buildMessageHash` therefore percent-encodes those
|
|
167
|
+
* characters itself (a byte-exact URL), and `parseDeepLinkHash` decodes them
|
|
168
|
+
* back — the key round-trips through the URL untouched.
|
|
169
|
+
*/
|
|
170
|
+
/** The URL hash fragment prefix that carries a message anchor key. */
|
|
171
|
+
const MSG_HASH_PREFIX = "msg=";
|
|
172
|
+
/**
|
|
173
|
+
* Characters the WHATWG URL fragment parser cannot round-trip raw (they are
|
|
174
|
+
* percent-encoded on parse): `"` (0x22), `<` (0x3C), `>` (0x3E), backtick
|
|
175
|
+
* (0x60), and `%` (0x25, so a literal `%` never reads as an escape start).
|
|
176
|
+
*/
|
|
177
|
+
const FRAGMENT_UNSAFE = /["<>\u0060%]/g;
|
|
178
|
+
/**
|
|
179
|
+
* Parse a `location.hash` fragment into the message anchor key it references.
|
|
180
|
+
* @param hash - the raw `location.hash` value: `''` or a fragment starting
|
|
181
|
+
* with `#` (e.g. `#msg=13:input-messageabc`).
|
|
182
|
+
* @returns the anchor key (percent-escapes decoded), or null when the
|
|
183
|
+
* fragment is not a message deep link: empty hash, `#msg=` with an empty
|
|
184
|
+
* value, a `#msg` prefix without the `=`, any other hash shape, a hash
|
|
185
|
+
* missing the leading `#`, or a malformed percent escape.
|
|
186
|
+
*/
|
|
187
|
+
function parseDeepLinkHash(hash) {
|
|
188
|
+
if (!hash.startsWith(`#${MSG_HASH_PREFIX}`)) return null;
|
|
189
|
+
const encoded = hash.slice(5);
|
|
190
|
+
if (encoded === "") return null;
|
|
191
|
+
try {
|
|
192
|
+
return decodeURIComponent(encoded);
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build the URL hash fragment that deep-links to a message anchor key.
|
|
199
|
+
* @param key - the conversation node key (opaque, never parsed here).
|
|
200
|
+
*/
|
|
201
|
+
function buildMessageHash(key) {
|
|
202
|
+
return `#${MSG_HASH_PREFIX}${key.replace(FRAGMENT_UNSAFE, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
152
205
|
//#region src/client/label-logic.ts
|
|
153
206
|
const MINUTE_MS = 6e4;
|
|
154
207
|
const HOUR_MS = 36e5;
|
|
@@ -450,13 +503,110 @@ window.__ModuleLoader__.load({
|
|
|
450
503
|
};
|
|
451
504
|
}
|
|
452
505
|
//#endregion
|
|
506
|
+
//#region src/client/useOutsideDismiss.ts
|
|
507
|
+
/**
|
|
508
|
+
* useOutsideDismiss: the shared "click outside to dismiss" contract behind the
|
|
509
|
+
* rail's floating panels (in-rail search / all-prompts list / cross-session
|
|
510
|
+
* search). While a panel is open, a pointerdown anywhere OUTSIDE it calls the
|
|
511
|
+
* caller's close handler; a pointerdown inside the panel (or on an excluded
|
|
512
|
+
* element) is left untouched.
|
|
513
|
+
*
|
|
514
|
+
* Event choice — window `pointerdown`:
|
|
515
|
+
* - `pointerdown` unifies mouse / touch / pen, so dismissal works for every
|
|
516
|
+
* input the harness surfaces; a `mousedown`-only listener would miss
|
|
517
|
+
* touch taps entirely.
|
|
518
|
+
* - Closing on the DOWN side of the gesture feels instant — the panel is
|
|
519
|
+
* gone before the pointer is released, which is the expected behaviour
|
|
520
|
+
* for fixed floating layers.
|
|
521
|
+
*
|
|
522
|
+
* Lifecycle: the listener is attached ONLY while `open` and removed on close
|
|
523
|
+
* / unmount, so a closed panel never intercepts events and no listener leaks.
|
|
524
|
+
*
|
|
525
|
+
* Opening-gesture guard: hooks arm their listener in an effect, which React
|
|
526
|
+
* runs AFTER the toggle's pointerdown/click that opened the panel — that
|
|
527
|
+
* gesture can therefore never reach the listener by construction. As
|
|
528
|
+
* defense-in-depth the hook also records its arming time and drops any
|
|
529
|
+
* pointerdown whose `timeStamp` STRICTLY predates it (an event still in
|
|
530
|
+
* flight from the opening gesture). Same-tick and later events pass through,
|
|
531
|
+
* and synthetic test events with `timeStamp === 0` (which carry no real
|
|
532
|
+
* timestamp) are kept — the guard never swallows a legitimate dispatch.
|
|
533
|
+
*
|
|
534
|
+
* `options.exclude` names targets the caller keeps for its own handler — the
|
|
535
|
+
* panel's own toggle button is the canonical case: its click owns the
|
|
536
|
+
* open/close flip, so a pointerdown on it must NOT also dismiss through the
|
|
537
|
+
* hook, or clicking an armed toggle would close on pointerdown and re-open on
|
|
538
|
+
* click.
|
|
539
|
+
*/
|
|
540
|
+
/**
|
|
541
|
+
* True when `target` is (or sits inside) an element matching `selector`.
|
|
542
|
+
* Panels use it to exclude their own rail-top toggle from dismissal (the
|
|
543
|
+
* toggle's data attribute is the established rail DOM contract, so the
|
|
544
|
+
* exclusion is just another access to it).
|
|
545
|
+
*/
|
|
546
|
+
function outsideDismissMatches(target, selector) {
|
|
547
|
+
return target instanceof Element && target.closest(selector) !== null;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* @param panelRef - the floating panel's root element (only mounted while open).
|
|
551
|
+
* @param open - whether the panel is open; the window listener arms only when true.
|
|
552
|
+
* @param onClose - called once per outside pointerdown. `undefined` keeps the
|
|
553
|
+
* hook inert — used while a call site still has no close path wired.
|
|
554
|
+
* @param options - optional exclusion predicate (see {@link OutsideDismissOptions}).
|
|
555
|
+
*/
|
|
556
|
+
function useOutsideDismiss(panelRef, open, onClose, options) {
|
|
557
|
+
const onCloseRef = (0, react.useRef)(onClose);
|
|
558
|
+
const excludeRef = (0, react.useRef)(options?.exclude);
|
|
559
|
+
(0, react.useEffect)(() => {
|
|
560
|
+
onCloseRef.current = onClose;
|
|
561
|
+
excludeRef.current = options?.exclude;
|
|
562
|
+
});
|
|
563
|
+
(0, react.useEffect)(() => {
|
|
564
|
+
if (!open) return;
|
|
565
|
+
const armedAt = performance.now();
|
|
566
|
+
const onPointerDown = (e) => {
|
|
567
|
+
const onClose = onCloseRef.current;
|
|
568
|
+
if (onClose === void 0) return;
|
|
569
|
+
if (e.timeStamp > 0 && e.timeStamp < armedAt) return;
|
|
570
|
+
if (excludeRef.current?.(e.target)) return;
|
|
571
|
+
const panel = panelRef.current;
|
|
572
|
+
if (panel !== null && e.target instanceof Node && panel.contains(e.target)) return;
|
|
573
|
+
onClose();
|
|
574
|
+
};
|
|
575
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
576
|
+
return () => window.removeEventListener("pointerdown", onPointerDown);
|
|
577
|
+
}, [open, panelRef]);
|
|
578
|
+
}
|
|
579
|
+
//#endregion
|
|
453
580
|
//#region src/client/MilestoneRailSearch.tsx
|
|
581
|
+
/**
|
|
582
|
+
* RailSearchUi: the in-rail search chrome (F1) — the magnifier toggle pinned
|
|
583
|
+
* to the rail's top and the compact search panel to its left (input, match
|
|
584
|
+
* counter, clear button).
|
|
585
|
+
*
|
|
586
|
+
* Pure presentation: it owns no state. MilestoneRail holds the search state
|
|
587
|
+
* and handlers and feeds them in as props, so the search lifecycle (query,
|
|
588
|
+
* match cycle, escape semantics) stays component-local in the rail. Splitting
|
|
589
|
+
* the chrome into its own file keeps the rail component under the size
|
|
590
|
+
* ceiling while the two still render one DOM contract
|
|
591
|
+
* (`data-search-toggle` / `data-rail-search` / `data-match-count` /
|
|
592
|
+
* `data-search-clear`).
|
|
593
|
+
*
|
|
594
|
+
* Outside dismissal: while the panel is open, a pointerdown anywhere outside
|
|
595
|
+
* it closes it (shared useOutsideDismiss hook). The toggle's own click keeps
|
|
596
|
+
* its flip semantics — a pointerdown on `[data-search-toggle]` is excluded
|
|
597
|
+
* from the hook and left to the rail's onToggle. Without a dedicated onClose
|
|
598
|
+
* prop the fallback is the toggle-off path (query retained, same as clicking
|
|
599
|
+
* the toggle), matching the rail's current call site; a dedicated onClose
|
|
600
|
+
* (clearSearch-equivalent) takes precedence once MilestoneRail feeds one.
|
|
601
|
+
*/
|
|
454
602
|
/** Dot diameter (px) — matches the rail's DOT_HIT so the toggle aligns. */
|
|
455
|
-
const DOT_HIT$1 =
|
|
603
|
+
const DOT_HIT$1 = 28;
|
|
456
604
|
/**
|
|
457
605
|
* @param props - the search state slice plus the rail's event handlers.
|
|
458
606
|
*/
|
|
459
|
-
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear, t }) {
|
|
607
|
+
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear, onClose, t }) {
|
|
608
|
+
const panelRef = (0, react.useRef)(null);
|
|
609
|
+
useOutsideDismiss(panelRef, panelOpen, onClose ?? (() => onToggle()), { exclude: (target) => outsideDismissMatches(target, "[data-search-toggle]") });
|
|
460
610
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
461
611
|
type: "button",
|
|
462
612
|
"data-search-toggle": true,
|
|
@@ -477,8 +627,8 @@ window.__ModuleLoader__.load({
|
|
|
477
627
|
color: panelOpen ? "#9db8ff" : "#8b96ab"
|
|
478
628
|
},
|
|
479
629
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
480
|
-
width: "
|
|
481
|
-
height: "
|
|
630
|
+
width: "16",
|
|
631
|
+
height: "16",
|
|
482
632
|
viewBox: "0 0 24 24",
|
|
483
633
|
fill: "none",
|
|
484
634
|
stroke: "currentColor",
|
|
@@ -492,11 +642,12 @@ window.__ModuleLoader__.load({
|
|
|
492
642
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m21 21-4.3-4.3" })]
|
|
493
643
|
})
|
|
494
644
|
}), panelOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
645
|
+
ref: panelRef,
|
|
495
646
|
style: {
|
|
496
647
|
position: "fixed",
|
|
497
648
|
top: panelTop,
|
|
498
649
|
right: panelRight,
|
|
499
|
-
width:
|
|
650
|
+
width: "min(220px, calc(100vw - 48px))",
|
|
500
651
|
padding: "10px 12px",
|
|
501
652
|
background: "rgba(20, 24, 32, 0.97)",
|
|
502
653
|
color: "#e6e8ee",
|
|
@@ -521,8 +672,9 @@ window.__ModuleLoader__.load({
|
|
|
521
672
|
style: {
|
|
522
673
|
flex: 1,
|
|
523
674
|
minWidth: 0,
|
|
524
|
-
padding: "
|
|
525
|
-
fontSize:
|
|
675
|
+
padding: "6px 10px",
|
|
676
|
+
fontSize: 14,
|
|
677
|
+
lineHeight: 1.4,
|
|
526
678
|
color: "#e6e8ee",
|
|
527
679
|
background: "rgba(255, 255, 255, 0.08)",
|
|
528
680
|
border: "1px solid rgba(255, 255, 255, 0.16)",
|
|
@@ -548,8 +700,8 @@ window.__ModuleLoader__.load({
|
|
|
548
700
|
color: "#8b96ab"
|
|
549
701
|
},
|
|
550
702
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
551
|
-
width: "
|
|
552
|
-
height: "
|
|
703
|
+
width: "12",
|
|
704
|
+
height: "12",
|
|
553
705
|
viewBox: "0 0 24 24",
|
|
554
706
|
fill: "none",
|
|
555
707
|
stroke: "currentColor",
|
|
@@ -563,7 +715,7 @@ window.__ModuleLoader__.load({
|
|
|
563
715
|
"data-match-count": true,
|
|
564
716
|
style: {
|
|
565
717
|
marginTop: 6,
|
|
566
|
-
fontSize:
|
|
718
|
+
fontSize: 13,
|
|
567
719
|
color: "#8b96ab"
|
|
568
720
|
},
|
|
569
721
|
children: [
|
|
@@ -575,6 +727,124 @@ window.__ModuleLoader__.load({
|
|
|
575
727
|
})] });
|
|
576
728
|
}
|
|
577
729
|
//#endregion
|
|
730
|
+
//#region src/client/MilestoneListPanel.tsx
|
|
731
|
+
/**
|
|
732
|
+
* MilestoneListPanel: the expandable all-prompts panel (P3) — the compact
|
|
733
|
+
* list chrome pinned to the rail's top that enumerates EVERY user-prompt
|
|
734
|
+
* milestone (序号 + turn + preview), independent of the search/bookmarks
|
|
735
|
+
* filters. Clicking an entry jumps to that message through the rail's own
|
|
736
|
+
* `jump` handler (the same path the dots use).
|
|
737
|
+
*
|
|
738
|
+
* Pure presentation: it owns no state. MilestoneRail holds the `listOpen`
|
|
739
|
+
* boolean (the toggle and the Escape/close semantics live there) and feeds
|
|
740
|
+
* the panel its anchor, the full marks array, and the jump handler as props
|
|
741
|
+
* — same split as RailSearchUi. Renders one DOM contract
|
|
742
|
+
* (`data-milestone-list` root / `data-list-item` rows with `data-jump-key`).
|
|
743
|
+
*
|
|
744
|
+
* Outside dismissal: while the panel is mounted (it only renders while open),
|
|
745
|
+
* a pointerdown anywhere outside it calls the rail-fed `onClose` (shared
|
|
746
|
+
* useOutsideDismiss hook; the toggle's own click keeps its flip semantics
|
|
747
|
+
* through a `[data-list-toggle]` exclusion). MilestoneRail.tsx does NOT pass
|
|
748
|
+
* onClose in the current tree (its owner is wiring it separately) — until
|
|
749
|
+
* then the hook is inert and the panel keeps its existing behaviour.
|
|
750
|
+
*/
|
|
751
|
+
/**
|
|
752
|
+
* @param props - the panel anchor, the full marks array, and the rail's jump handler.
|
|
753
|
+
*/
|
|
754
|
+
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, onClose, t }) {
|
|
755
|
+
const panelRef = (0, react.useRef)(null);
|
|
756
|
+
useOutsideDismiss(panelRef, true, onClose, { exclude: (target) => outsideDismissMatches(target, "[data-list-toggle]") });
|
|
757
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
758
|
+
ref: panelRef,
|
|
759
|
+
"data-milestone-list": true,
|
|
760
|
+
style: {
|
|
761
|
+
position: "fixed",
|
|
762
|
+
top: panelTop,
|
|
763
|
+
right: panelRight,
|
|
764
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
765
|
+
padding: "10px 12px",
|
|
766
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
767
|
+
color: "#e6e8ee",
|
|
768
|
+
borderRadius: 8,
|
|
769
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
770
|
+
zIndex: 103
|
|
771
|
+
},
|
|
772
|
+
children: [
|
|
773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-list-item]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
774
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
775
|
+
style: {
|
|
776
|
+
display: "flex",
|
|
777
|
+
alignItems: "center",
|
|
778
|
+
gap: 6,
|
|
779
|
+
marginBottom: 6
|
|
780
|
+
},
|
|
781
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
782
|
+
style: {
|
|
783
|
+
fontSize: 13,
|
|
784
|
+
fontWeight: 600,
|
|
785
|
+
color: "#e6e8ee"
|
|
786
|
+
},
|
|
787
|
+
children: t("list.label")
|
|
788
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
789
|
+
style: {
|
|
790
|
+
fontSize: 12,
|
|
791
|
+
color: "#8b96ab"
|
|
792
|
+
},
|
|
793
|
+
children: marks.length
|
|
794
|
+
})]
|
|
795
|
+
}),
|
|
796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
797
|
+
style: {
|
|
798
|
+
maxHeight: 300,
|
|
799
|
+
overflowY: "auto",
|
|
800
|
+
display: "flex",
|
|
801
|
+
flexDirection: "column",
|
|
802
|
+
gap: 2
|
|
803
|
+
},
|
|
804
|
+
children: marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
805
|
+
type: "button",
|
|
806
|
+
"data-list-item": true,
|
|
807
|
+
"data-jump-key": mark.key,
|
|
808
|
+
onClick: () => onJump(mark.key),
|
|
809
|
+
title: mark.preview,
|
|
810
|
+
style: {
|
|
811
|
+
display: "block",
|
|
812
|
+
width: "100%",
|
|
813
|
+
minWidth: 0,
|
|
814
|
+
padding: "6px 8px",
|
|
815
|
+
background: "transparent",
|
|
816
|
+
border: "none",
|
|
817
|
+
borderRadius: 6,
|
|
818
|
+
cursor: "pointer",
|
|
819
|
+
color: "#e6e8ee",
|
|
820
|
+
textAlign: "left"
|
|
821
|
+
},
|
|
822
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
823
|
+
style: {
|
|
824
|
+
fontSize: 12,
|
|
825
|
+
color: "#8b96ab",
|
|
826
|
+
whiteSpace: "nowrap"
|
|
827
|
+
},
|
|
828
|
+
children: [t("pos.of", {
|
|
829
|
+
n: i + 1,
|
|
830
|
+
m: marks.length
|
|
831
|
+
}), mark.turn !== void 0 ? ` · ${t("turn.label", { n: mark.turn })}` : null]
|
|
832
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
833
|
+
style: {
|
|
834
|
+
fontSize: 13,
|
|
835
|
+
lineHeight: 1.4,
|
|
836
|
+
overflow: "hidden",
|
|
837
|
+
textOverflow: "ellipsis",
|
|
838
|
+
whiteSpace: "nowrap"
|
|
839
|
+
},
|
|
840
|
+
children: mark.preview || t("no.text")
|
|
841
|
+
})]
|
|
842
|
+
}, mark.key))
|
|
843
|
+
})
|
|
844
|
+
]
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
578
848
|
//#region src/client/MilestoneRailTooltip.tsx
|
|
579
849
|
/**
|
|
580
850
|
* @param props - the hovered mark + bookmark wiring (see {@link MilestoneRailTooltipProps}).
|
|
@@ -591,14 +861,14 @@ window.__ModuleLoader__.load({
|
|
|
591
861
|
right: panelRight,
|
|
592
862
|
top: hover.top,
|
|
593
863
|
transform: "translateY(-50%)",
|
|
594
|
-
maxWidth:
|
|
864
|
+
maxWidth: "min(300px, calc(100vw - 120px))",
|
|
595
865
|
minWidth: 180,
|
|
596
866
|
padding: "8px 12px",
|
|
597
867
|
background: "rgba(20, 24, 32, 0.96)",
|
|
598
868
|
color: "#e6e8ee",
|
|
599
869
|
borderRadius: 8,
|
|
600
|
-
fontSize:
|
|
601
|
-
lineHeight: 1.
|
|
870
|
+
fontSize: "var(--dsw-font-s-14, 14px)",
|
|
871
|
+
lineHeight: 1.5,
|
|
602
872
|
whiteSpace: "pre-wrap",
|
|
603
873
|
wordBreak: "break-word",
|
|
604
874
|
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
@@ -613,7 +883,8 @@ window.__ModuleLoader__.load({
|
|
|
613
883
|
flexWrap: "wrap",
|
|
614
884
|
gap: 8,
|
|
615
885
|
color: "#9aa4b8",
|
|
616
|
-
fontSize:
|
|
886
|
+
fontSize: 13,
|
|
887
|
+
lineHeight: 1.4,
|
|
617
888
|
marginBottom: 4
|
|
618
889
|
},
|
|
619
890
|
children: [
|
|
@@ -647,8 +918,8 @@ window.__ModuleLoader__.load({
|
|
|
647
918
|
color: bookmarked ? "#ffd166" : "#8b96ab"
|
|
648
919
|
},
|
|
649
920
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
650
|
-
width: "
|
|
651
|
-
height: "
|
|
921
|
+
width: "14",
|
|
922
|
+
height: "14",
|
|
652
923
|
viewBox: "0 0 24 24",
|
|
653
924
|
fill: bookmarked ? "currentColor" : "none",
|
|
654
925
|
stroke: "currentColor",
|
|
@@ -673,7 +944,7 @@ window.__ModuleLoader__.load({
|
|
|
673
944
|
justifyContent: "center",
|
|
674
945
|
background: "transparent",
|
|
675
946
|
border: "none",
|
|
676
|
-
padding: "
|
|
947
|
+
padding: "3px 8px",
|
|
677
948
|
cursor: "pointer",
|
|
678
949
|
whiteSpace: "nowrap",
|
|
679
950
|
color: copied ? "#7ee2a8" : "#8b96ab"
|
|
@@ -695,7 +966,7 @@ window.__ModuleLoader__.load({
|
|
|
695
966
|
justifyContent: "center",
|
|
696
967
|
background: "transparent",
|
|
697
968
|
border: "none",
|
|
698
|
-
padding: "
|
|
969
|
+
padding: "3px 8px",
|
|
699
970
|
cursor: "pointer",
|
|
700
971
|
whiteSpace: "nowrap",
|
|
701
972
|
color: forked ? "#7ee2a8" : "#8b96ab"
|
|
@@ -718,7 +989,7 @@ window.__ModuleLoader__.load({
|
|
|
718
989
|
justifyContent: "center",
|
|
719
990
|
background: "transparent",
|
|
720
991
|
border: "none",
|
|
721
|
-
padding: "
|
|
992
|
+
padding: "3px 8px",
|
|
722
993
|
cursor: "pointer",
|
|
723
994
|
whiteSpace: "nowrap",
|
|
724
995
|
color: turnCollapsed ? "#7ee2a8" : "#8b96ab"
|
|
@@ -737,7 +1008,8 @@ window.__ModuleLoader__.load({
|
|
|
737
1008
|
flexWrap: "wrap",
|
|
738
1009
|
gap: 8,
|
|
739
1010
|
color: "#8b96ab",
|
|
740
|
-
fontSize:
|
|
1011
|
+
fontSize: 13,
|
|
1012
|
+
lineHeight: 1.4,
|
|
741
1013
|
marginTop: 4
|
|
742
1014
|
},
|
|
743
1015
|
children: [
|
|
@@ -754,7 +1026,8 @@ window.__ModuleLoader__.load({
|
|
|
754
1026
|
flexWrap: "wrap",
|
|
755
1027
|
gap: 8,
|
|
756
1028
|
color: "#8b96ab",
|
|
757
|
-
fontSize:
|
|
1029
|
+
fontSize: 13,
|
|
1030
|
+
lineHeight: 1.4,
|
|
758
1031
|
marginTop: 4
|
|
759
1032
|
},
|
|
760
1033
|
children: [
|
|
@@ -776,6 +1049,193 @@ window.__ModuleLoader__.load({
|
|
|
776
1049
|
});
|
|
777
1050
|
}
|
|
778
1051
|
//#endregion
|
|
1052
|
+
//#region src/client/MilestoneSessionSearch.tsx
|
|
1053
|
+
/**
|
|
1054
|
+
* MilestoneSessionSearch: the cross-session search panel (P3) — the fixed
|
|
1055
|
+
* chrome pinned to the rail's top that searches EVERY session's message
|
|
1056
|
+
* content through the injected `searchSessions` action (the harness
|
|
1057
|
+
* `session.search` RPC), lists ranked hits (display title + snippet), and
|
|
1058
|
+
* opens the clicked session via `openSession` (the same selection path the
|
|
1059
|
+
* sidebar uses).
|
|
1060
|
+
*
|
|
1061
|
+
* Owns its search lifecycle — debounce + AbortController + status — unlike
|
|
1062
|
+
* the rail's in-session search (RailSearchUi), which is a pure presentation
|
|
1063
|
+
* slice of MilestoneRail's own state. Mirrors MilestoneListPanel's
|
|
1064
|
+
* fixed-panel styling. Renders one DOM contract
|
|
1065
|
+
* (`data-session-search` root / `data-session-search-input` /
|
|
1066
|
+
* `data-session-search-result` rows / `data-session-search-error` /
|
|
1067
|
+
* `data-session-search-more`).
|
|
1068
|
+
*
|
|
1069
|
+
* Outside dismissal: while the panel is mounted (it only renders while open),
|
|
1070
|
+
* a pointerdown anywhere outside it calls the rail-fed `onClose` (shared
|
|
1071
|
+
* useOutsideDismiss hook; the toggle's own click keeps its flip semantics
|
|
1072
|
+
* through a `[data-session-search-toggle]` exclusion).
|
|
1073
|
+
*/
|
|
1074
|
+
/** Debounce window for the cross-session query (ms). */
|
|
1075
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
1076
|
+
/**
|
|
1077
|
+
* @param props - the panel anchor, the close/open/search actions, and the locale interpreter.
|
|
1078
|
+
*/
|
|
1079
|
+
function MilestoneSessionSearch({ panelTop, panelRight, onClose, searchSessions, openSession, t }) {
|
|
1080
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
1081
|
+
const [status, setStatus] = (0, react.useState)("idle");
|
|
1082
|
+
const [hits, setHits] = (0, react.useState)([]);
|
|
1083
|
+
const [hasMore, setHasMore] = (0, react.useState)(false);
|
|
1084
|
+
const panelRef = (0, react.useRef)(null);
|
|
1085
|
+
useOutsideDismiss(panelRef, true, onClose, { exclude: (target) => outsideDismissMatches(target, "[data-session-search-toggle]") });
|
|
1086
|
+
(0, react.useEffect)(() => {
|
|
1087
|
+
const trimmed = query.trim();
|
|
1088
|
+
if (trimmed === "") {
|
|
1089
|
+
setStatus("idle");
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const controller = new AbortController();
|
|
1093
|
+
setStatus("loading");
|
|
1094
|
+
const timer = window.setTimeout(() => {
|
|
1095
|
+
searchSessions(trimmed, controller.signal).then((result) => {
|
|
1096
|
+
if (controller.signal.aborted) return;
|
|
1097
|
+
setHits(result.items);
|
|
1098
|
+
setHasMore(result.hasMore);
|
|
1099
|
+
setStatus(result.items.length > 0 ? "results" : "empty");
|
|
1100
|
+
}, () => {
|
|
1101
|
+
if (controller.signal.aborted) return;
|
|
1102
|
+
setStatus("error");
|
|
1103
|
+
});
|
|
1104
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
1105
|
+
return () => {
|
|
1106
|
+
window.clearTimeout(timer);
|
|
1107
|
+
controller.abort();
|
|
1108
|
+
};
|
|
1109
|
+
}, [query, searchSessions]);
|
|
1110
|
+
const onInputKeyDown = (e) => {
|
|
1111
|
+
if (e.key === "Escape") onClose();
|
|
1112
|
+
};
|
|
1113
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1114
|
+
ref: panelRef,
|
|
1115
|
+
"data-session-search": true,
|
|
1116
|
+
style: {
|
|
1117
|
+
position: "fixed",
|
|
1118
|
+
top: panelTop,
|
|
1119
|
+
right: panelRight,
|
|
1120
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
1121
|
+
padding: "10px 12px",
|
|
1122
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
1123
|
+
color: "#e6e8ee",
|
|
1124
|
+
borderRadius: 8,
|
|
1125
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
1126
|
+
zIndex: 103
|
|
1127
|
+
},
|
|
1128
|
+
children: [
|
|
1129
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-session-search-result]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
1130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1131
|
+
style: {
|
|
1132
|
+
display: "flex",
|
|
1133
|
+
alignItems: "center",
|
|
1134
|
+
gap: 6,
|
|
1135
|
+
marginBottom: 6
|
|
1136
|
+
},
|
|
1137
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1138
|
+
style: {
|
|
1139
|
+
fontSize: 13,
|
|
1140
|
+
fontWeight: 600,
|
|
1141
|
+
color: "#e6e8ee"
|
|
1142
|
+
},
|
|
1143
|
+
children: t("search.cross")
|
|
1144
|
+
})
|
|
1145
|
+
}),
|
|
1146
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1147
|
+
"data-session-search-input": true,
|
|
1148
|
+
"aria-label": t("search.cross"),
|
|
1149
|
+
placeholder: t("search.cross"),
|
|
1150
|
+
value: query,
|
|
1151
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1152
|
+
onKeyDown: onInputKeyDown,
|
|
1153
|
+
autoFocus: true,
|
|
1154
|
+
style: {
|
|
1155
|
+
boxSizing: "border-box",
|
|
1156
|
+
width: "100%",
|
|
1157
|
+
padding: "6px 10px",
|
|
1158
|
+
fontSize: 14,
|
|
1159
|
+
lineHeight: 1.4,
|
|
1160
|
+
color: "#e6e8ee",
|
|
1161
|
+
background: "rgba(255, 255, 255, 0.08)",
|
|
1162
|
+
border: "1px solid rgba(255, 255, 255, 0.16)",
|
|
1163
|
+
borderRadius: 6,
|
|
1164
|
+
outline: "none"
|
|
1165
|
+
}
|
|
1166
|
+
}),
|
|
1167
|
+
status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1168
|
+
"data-session-search-error": true,
|
|
1169
|
+
style: {
|
|
1170
|
+
marginTop: 8,
|
|
1171
|
+
fontSize: 13,
|
|
1172
|
+
color: "#f07c7c"
|
|
1173
|
+
},
|
|
1174
|
+
children: t("search.error")
|
|
1175
|
+
}),
|
|
1176
|
+
status === "results" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1177
|
+
style: {
|
|
1178
|
+
maxHeight: 300,
|
|
1179
|
+
overflowY: "auto",
|
|
1180
|
+
display: "flex",
|
|
1181
|
+
flexDirection: "column",
|
|
1182
|
+
gap: 2,
|
|
1183
|
+
marginTop: 8
|
|
1184
|
+
},
|
|
1185
|
+
children: hits.map((hit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1186
|
+
type: "button",
|
|
1187
|
+
"data-session-search-result": true,
|
|
1188
|
+
onClick: () => {
|
|
1189
|
+
openSession(hit.sessionId);
|
|
1190
|
+
onClose();
|
|
1191
|
+
},
|
|
1192
|
+
title: hit.snippet,
|
|
1193
|
+
style: {
|
|
1194
|
+
display: "block",
|
|
1195
|
+
width: "100%",
|
|
1196
|
+
minWidth: 0,
|
|
1197
|
+
padding: "6px 8px",
|
|
1198
|
+
background: "transparent",
|
|
1199
|
+
border: "none",
|
|
1200
|
+
borderRadius: 6,
|
|
1201
|
+
cursor: "pointer",
|
|
1202
|
+
color: "#e6e8ee",
|
|
1203
|
+
textAlign: "left"
|
|
1204
|
+
},
|
|
1205
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1206
|
+
style: {
|
|
1207
|
+
fontSize: 12,
|
|
1208
|
+
color: "#9db8ff",
|
|
1209
|
+
whiteSpace: "nowrap",
|
|
1210
|
+
overflow: "hidden",
|
|
1211
|
+
textOverflow: "ellipsis"
|
|
1212
|
+
},
|
|
1213
|
+
children: hit.title ?? t("search.untitled")
|
|
1214
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1215
|
+
style: {
|
|
1216
|
+
fontSize: 13,
|
|
1217
|
+
lineHeight: 1.4,
|
|
1218
|
+
color: "#c6ccd8",
|
|
1219
|
+
overflow: "hidden",
|
|
1220
|
+
textOverflow: "ellipsis",
|
|
1221
|
+
whiteSpace: "nowrap"
|
|
1222
|
+
},
|
|
1223
|
+
children: hit.snippet
|
|
1224
|
+
})]
|
|
1225
|
+
}, hit.sessionId))
|
|
1226
|
+
}), hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1227
|
+
"data-session-search-more": true,
|
|
1228
|
+
style: {
|
|
1229
|
+
marginTop: 6,
|
|
1230
|
+
fontSize: 12,
|
|
1231
|
+
color: "#8b96ab"
|
|
1232
|
+
},
|
|
1233
|
+
children: t("search.more")
|
|
1234
|
+
})] })
|
|
1235
|
+
]
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
//#endregion
|
|
779
1239
|
//#region src/client/useCurrentAnchor.ts
|
|
780
1240
|
/**
|
|
781
1241
|
* useCurrentAnchor: tracks which user-message row sits at/just above the
|
|
@@ -838,6 +1298,416 @@ window.__ModuleLoader__.load({
|
|
|
838
1298
|
return current;
|
|
839
1299
|
}
|
|
840
1300
|
//#endregion
|
|
1301
|
+
//#region src/client/toolbar-prefs.ts
|
|
1302
|
+
/**
|
|
1303
|
+
* toolbar-prefs: the persistence layer for the milestone rail's collapsible
|
|
1304
|
+
* toolbar "pinned outside the collapse" preferences.
|
|
1305
|
+
*
|
|
1306
|
+
* Storage contract: one localStorage key (`dsh-milestone.toolbar`) holding
|
|
1307
|
+
* `{ "pinned": string[] }` — the ids of the function-key features the user
|
|
1308
|
+
* wants to keep visible while the toolbar is COLLAPSED. An absent or corrupt
|
|
1309
|
+
* value degrades to `[]` (everything folded away).
|
|
1310
|
+
*
|
|
1311
|
+
* All reads are sanitized: ids are checked against the canonical whitelist
|
|
1312
|
+
* (`TOOLBAR_PIN_IDS`), duplicates are dropped, unknown ids are discarded, and
|
|
1313
|
+
* the pinned order is the caller's first-seen order (the order ids were
|
|
1314
|
+
* pinned) — a future feature removal therefore never leaves a stale id
|
|
1315
|
+
* behind, and a hand-edited value can never smuggle an out-of-registry id
|
|
1316
|
+
* into render state.
|
|
1317
|
+
*
|
|
1318
|
+
* The whitelist lives HERE (not in MilestoneRail) so the pure functions stay
|
|
1319
|
+
* dependency-free and unit-testable; MilestoneRail's feature registry keys
|
|
1320
|
+
* itself against the same `ToolbarPinId` type, so id drift is a compile error.
|
|
1321
|
+
*/
|
|
1322
|
+
/** The single localStorage key holding the toolbar preference blob. */
|
|
1323
|
+
const TOOLBAR_PREFS_KEY = "dsh-milestone.toolbar";
|
|
1324
|
+
/**
|
|
1325
|
+
* Canonical function-key ids that may be pinned outside the collapse, in
|
|
1326
|
+
* toolbar render order. Adding a feature here (plus its registry entry in
|
|
1327
|
+
* MilestoneRail) is the whole "pin it" extension point.
|
|
1328
|
+
*/
|
|
1329
|
+
const TOOLBAR_PIN_IDS = [
|
|
1330
|
+
"search",
|
|
1331
|
+
"list",
|
|
1332
|
+
"sessionSearch",
|
|
1333
|
+
"bookmarks",
|
|
1334
|
+
"focus",
|
|
1335
|
+
"updateCheck"
|
|
1336
|
+
];
|
|
1337
|
+
/** Type guard for registry ids — unknown strings never survive a parse. */
|
|
1338
|
+
function isToolbarPinId(id) {
|
|
1339
|
+
return TOOLBAR_PIN_IDS.includes(id);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Parse + sanitize the raw persisted blob: `null` (nothing stored), invalid
|
|
1343
|
+
* JSON, or a non-`{pinned: string[]}` shape all degrade to `[]`; unknown ids
|
|
1344
|
+
* and duplicates are dropped; the result keeps the caller's first-seen
|
|
1345
|
+
* (pin) order.
|
|
1346
|
+
*/
|
|
1347
|
+
function parsePrefs(raw) {
|
|
1348
|
+
if (raw === null) return [];
|
|
1349
|
+
let parsed;
|
|
1350
|
+
try {
|
|
1351
|
+
parsed = JSON.parse(raw);
|
|
1352
|
+
} catch {
|
|
1353
|
+
return [];
|
|
1354
|
+
}
|
|
1355
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
1356
|
+
const { pinned } = parsed;
|
|
1357
|
+
if (!Array.isArray(pinned)) return [];
|
|
1358
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1359
|
+
const result = [];
|
|
1360
|
+
for (const id of pinned) {
|
|
1361
|
+
if (typeof id !== "string" || !isToolbarPinId(id) || seen.has(id)) continue;
|
|
1362
|
+
seen.add(id);
|
|
1363
|
+
result.push(id);
|
|
1364
|
+
}
|
|
1365
|
+
return result;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Read + sanitize the persisted toolbar prefs from localStorage. Degrades to
|
|
1369
|
+
* `[]` when storage is unavailable (SSR, sandboxed iframe) — pinning is a
|
|
1370
|
+
* best-effort enhancement, never a render blocker.
|
|
1371
|
+
*/
|
|
1372
|
+
function loadPrefs() {
|
|
1373
|
+
try {
|
|
1374
|
+
return parsePrefs(localStorage.getItem(TOOLBAR_PREFS_KEY));
|
|
1375
|
+
} catch {
|
|
1376
|
+
return [];
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Persist the pinned ids (sanitized on the way out so a corrupt in-memory
|
|
1381
|
+
* value is never written). Takes plain strings — it cannot trust its input
|
|
1382
|
+
* any more than it trusts localStorage, so unknown ids are dropped, not
|
|
1383
|
+
* rejected. Swallows storage failures for the same best-effort reason as
|
|
1384
|
+
* {@link loadPrefs}.
|
|
1385
|
+
*/
|
|
1386
|
+
function savePrefs(pinned) {
|
|
1387
|
+
const cleaned = parsePrefs(JSON.stringify({ pinned: [...pinned] }));
|
|
1388
|
+
try {
|
|
1389
|
+
localStorage.setItem(TOOLBAR_PREFS_KEY, JSON.stringify({ pinned: cleaned }));
|
|
1390
|
+
} catch {}
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Pure toggle: adds `id` to the pinned set when absent, removes it when
|
|
1394
|
+
* present. Unknown ids are ignored (the set is returned unchanged) and the
|
|
1395
|
+
* result is always deduped via the parse path, so callers can feed state
|
|
1396
|
+
* straight back into {@link savePrefs}.
|
|
1397
|
+
*/
|
|
1398
|
+
function togglePin(pinned, id) {
|
|
1399
|
+
if (!isToolbarPinId(id)) return [...pinned];
|
|
1400
|
+
const next = new Set(pinned);
|
|
1401
|
+
if (next.has(id)) next.delete(id);
|
|
1402
|
+
else next.add(id);
|
|
1403
|
+
return parsePrefs(JSON.stringify({ pinned: [...next] }));
|
|
1404
|
+
}
|
|
1405
|
+
//#endregion
|
|
1406
|
+
//#region src/client/version-logic.ts
|
|
1407
|
+
/**
|
|
1408
|
+
* Pure update-check logic for dsh-milestone, deliberately free of React, the
|
|
1409
|
+
* harness runtime, and any node-only API so it runs identically in the browser
|
|
1410
|
+
* bundle, vitest (jsdom), and future shells.
|
|
1411
|
+
*
|
|
1412
|
+
* Design intent:
|
|
1413
|
+
* - `compareVersions` / `needsUpdate` are pure, side-effect-free functions that
|
|
1414
|
+
* follow npm semver precedence for the shapes this plugin actually publishes
|
|
1415
|
+
* (`x.y.z` plus optional `-rc.N` / `-beta.N` prereleases). Anything else is
|
|
1416
|
+
* an explicit error rather than a silent mis-comparison.
|
|
1417
|
+
* - `fetchLatestVersion` is the only I/O: it reads the `latest` dist-tag from
|
|
1418
|
+
* npmmirror's cheap dist-tags endpoint first (it CORS-echoes this page's
|
|
1419
|
+
* Origin), then falls back to the full npm packument (ACAO: *). Every
|
|
1420
|
+
* request carries an internal 8s timeout combined with the caller's signal —
|
|
1421
|
+
* whichever aborts first wins — and every failure degrades to a structured
|
|
1422
|
+
* `{ ok: false }` result, never a throw.
|
|
1423
|
+
* - `SUPPORTED_HOST_LINES` is a SELF-DECLARED compatibility list, not a probe:
|
|
1424
|
+
* the harness reports no trustworthy host version in the browser
|
|
1425
|
+
* (`host.describe().version` is a stub), so the plugin declares which npm
|
|
1426
|
+
* `latest` host-version lines it supports and the UI renders that as-is.
|
|
1427
|
+
*/
|
|
1428
|
+
/** Host-version lines this plugin declares support for (npm official latest
|
|
1429
|
+
* line; bump it as the peer/dependency ranges move). */
|
|
1430
|
+
const SUPPORTED_HOST_LINES = ["0.1.1-rc.2"];
|
|
1431
|
+
/** Internal per-request timeout for the network attempts. */
|
|
1432
|
+
const REQUEST_TIMEOUT_MS = 8e3;
|
|
1433
|
+
/** npmmirror dist-tags endpoint: lean JSON (`{"latest":"0.6.0", ...}`),
|
|
1434
|
+
* echoes this page's Origin in `Access-Control-Allow-Origin`. */
|
|
1435
|
+
const NPMIRROR_DIST_TAGS_URL = "https://registry.npmmirror.com/-/package/dsh-milestone/dist-tags";
|
|
1436
|
+
/** npm full packument; `dist-tags.latest` sits at the JSON root, and the
|
|
1437
|
+
* endpoint sends `Access-Control-Allow-Origin: *`. */
|
|
1438
|
+
const NPM_PACKUMENT_URL = "https://registry.npmjs.org/dsh-milestone";
|
|
1439
|
+
/** Numeric identifier test shared by core and prerelease segments. */
|
|
1440
|
+
const NUMERIC_RE = /^\d+$/;
|
|
1441
|
+
/**
|
|
1442
|
+
* Parse a version string into comparable parts.
|
|
1443
|
+
* Accepts `major[.minor[.patch]][-prerelease][+build]`; missing core segments
|
|
1444
|
+
* pad with 0 and `+build` metadata is ignored (per semver precedence).
|
|
1445
|
+
* @throws {Error} with the offending input when the shape is not parseable.
|
|
1446
|
+
*/
|
|
1447
|
+
function parseVersion(input) {
|
|
1448
|
+
if (typeof input !== "string" || input.trim() === "") throw new Error(`Invalid semantic version: ${JSON.stringify(input)} (expected "x.y.z" with optional "-pre" suffix)`);
|
|
1449
|
+
const s = input.trim();
|
|
1450
|
+
const plus = s.indexOf("+");
|
|
1451
|
+
const withoutBuild = plus === -1 ? s : s.slice(0, plus);
|
|
1452
|
+
const dash = withoutBuild.indexOf("-");
|
|
1453
|
+
const corePart = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
1454
|
+
const prePart = dash === -1 ? void 0 : withoutBuild.slice(dash + 1);
|
|
1455
|
+
const segments = corePart.split(".");
|
|
1456
|
+
if (segments.length < 1 || segments.length > 3 || !segments.every((seg) => NUMERIC_RE.test(seg))) throw new Error(`Invalid semantic version: ${JSON.stringify(input)} (expected "x.y.z" with optional "-pre" suffix)`);
|
|
1457
|
+
const core = segments.map(Number);
|
|
1458
|
+
let pre = [];
|
|
1459
|
+
if (prePart !== void 0) {
|
|
1460
|
+
if (prePart === "" || !prePart.split(".").every((id) => /^[0-9A-Za-z-]+$/.test(id))) throw new Error(`Invalid semantic version: ${JSON.stringify(input)} (bad prerelease suffix)`);
|
|
1461
|
+
pre = prePart.split(".");
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
core,
|
|
1465
|
+
pre
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Compare two version strings per npm semver precedence.
|
|
1470
|
+
* @param a - first version (`x.y.z` with optional `-pre` suffix; build
|
|
1471
|
+
* metadata ignored; missing core segments pad with 0).
|
|
1472
|
+
* @param b - second version, same grammar.
|
|
1473
|
+
* @returns -1 when `a < b`, 0 when equal, 1 when `a > b`. Prereleases sort
|
|
1474
|
+
* below their same-number release; prerelease identifiers compare numerically
|
|
1475
|
+
* when both are numeric, lexically when both are alphanumeric, and numeric
|
|
1476
|
+
* identifiers always sort before alphanumeric ones.
|
|
1477
|
+
* @throws {Error} for inputs that do not match the grammar.
|
|
1478
|
+
*/
|
|
1479
|
+
function compareVersions(a, b) {
|
|
1480
|
+
const pa = parseVersion(a);
|
|
1481
|
+
const pb = parseVersion(b);
|
|
1482
|
+
for (let i = 0; i < 3; i++) {
|
|
1483
|
+
const x = pa.core[i] ?? 0;
|
|
1484
|
+
const y = pb.core[i] ?? 0;
|
|
1485
|
+
if (x < y) return -1;
|
|
1486
|
+
if (x > y) return 1;
|
|
1487
|
+
}
|
|
1488
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
1489
|
+
if (pa.pre.length === 0) return 1;
|
|
1490
|
+
if (pb.pre.length === 0) return -1;
|
|
1491
|
+
const len = Math.max(pa.pre.length, pb.pre.length);
|
|
1492
|
+
for (let i = 0; i < len; i++) {
|
|
1493
|
+
const x = pa.pre[i];
|
|
1494
|
+
const y = pb.pre[i];
|
|
1495
|
+
if (x === void 0) return -1;
|
|
1496
|
+
if (y === void 0) return 1;
|
|
1497
|
+
const xNum = NUMERIC_RE.test(x);
|
|
1498
|
+
const yNum = NUMERIC_RE.test(y);
|
|
1499
|
+
if (xNum && yNum) {
|
|
1500
|
+
if (x !== y) return Number(x) < Number(y) ? -1 : 1;
|
|
1501
|
+
} else if (xNum) return -1;
|
|
1502
|
+
else if (yNum) return 1;
|
|
1503
|
+
else if (x !== y) return x < y ? -1 : 1;
|
|
1504
|
+
}
|
|
1505
|
+
return 0;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Whether the installed plugin should offer an update.
|
|
1509
|
+
* @param current - installed version.
|
|
1510
|
+
* @param latest - newest published version.
|
|
1511
|
+
* @returns true only when `latest` is strictly greater than `current`
|
|
1512
|
+
* (identical versions, or a newer installed version, return false).
|
|
1513
|
+
* @throws {Error} when either input is not a parseable version.
|
|
1514
|
+
*/
|
|
1515
|
+
function needsUpdate(current, latest) {
|
|
1516
|
+
return compareVersions(current, latest) < 0;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Build a request signal: a fresh AbortController aborted by EITHER the
|
|
1520
|
+
* caller's external signal (which wins when it fires first) OR an internal
|
|
1521
|
+
* timeout. Uses `AbortSignal.timeout` when the environment provides it and
|
|
1522
|
+
* falls back to a manual `setTimeout` + abort otherwise (jsdom older
|
|
1523
|
+
* versions expose no `AbortSignal.timeout`).
|
|
1524
|
+
* @param external - caller signal; when already aborted the request fires
|
|
1525
|
+
* immediately with an aborted signal.
|
|
1526
|
+
* @param timeoutMs - internal timeout in ms.
|
|
1527
|
+
* @returns the combined signal plus a cleanup that detaches all listeners
|
|
1528
|
+
* (must be called so no listener outlives the request).
|
|
1529
|
+
*/
|
|
1530
|
+
function createRequestSignal(external, timeoutMs) {
|
|
1531
|
+
const controller = new AbortController();
|
|
1532
|
+
const abort = () => controller.abort();
|
|
1533
|
+
const detach = [];
|
|
1534
|
+
if (external) if (external.aborted) controller.abort();
|
|
1535
|
+
else {
|
|
1536
|
+
external.addEventListener("abort", abort, { once: true });
|
|
1537
|
+
detach.push(() => external.removeEventListener("abort", abort));
|
|
1538
|
+
}
|
|
1539
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
1540
|
+
const t = AbortSignal.timeout(timeoutMs);
|
|
1541
|
+
t.addEventListener("abort", abort, { once: true });
|
|
1542
|
+
detach.push(() => t.removeEventListener("abort", abort));
|
|
1543
|
+
} else {
|
|
1544
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
1545
|
+
detach.push(() => clearTimeout(timer));
|
|
1546
|
+
}
|
|
1547
|
+
return {
|
|
1548
|
+
signal: controller.signal,
|
|
1549
|
+
cleanup: () => {
|
|
1550
|
+
for (const fn of detach) fn();
|
|
1551
|
+
detach.length = 0;
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
/** Extract `latest` from the npmmirror dist-tags JSON root. */
|
|
1556
|
+
function extractFromDistTags(data) {
|
|
1557
|
+
if (data === null || typeof data !== "object") return null;
|
|
1558
|
+
const latest = data.latest;
|
|
1559
|
+
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
|
1560
|
+
}
|
|
1561
|
+
/** Extract `latest` from the npm packument's root `dist-tags` object. */
|
|
1562
|
+
function extractFromPackument(data) {
|
|
1563
|
+
if (data === null || typeof data !== "object") return null;
|
|
1564
|
+
const distTags = data["dist-tags"];
|
|
1565
|
+
if (distTags === null || typeof distTags !== "object") return null;
|
|
1566
|
+
const latest = distTags.latest;
|
|
1567
|
+
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
|
1568
|
+
}
|
|
1569
|
+
/** Human-readable failure detail for one endpoint attempt. */
|
|
1570
|
+
function describeError(source, cause) {
|
|
1571
|
+
const name = cause instanceof Error ? cause.name : "";
|
|
1572
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1573
|
+
if (name === "AbortError") return `${source}: aborted (${message})`;
|
|
1574
|
+
return `${source}: ${message}`;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* One attempt at fetching the `latest` dist-tag from an endpoint.
|
|
1578
|
+
* @returns ok with the tag, or ok:false with a per-source error description.
|
|
1579
|
+
*/
|
|
1580
|
+
async function tryFetchLatest(url, extract, source, external) {
|
|
1581
|
+
try {
|
|
1582
|
+
const { signal, cleanup } = createRequestSignal(external, REQUEST_TIMEOUT_MS);
|
|
1583
|
+
try {
|
|
1584
|
+
const res = await fetch(url, { signal });
|
|
1585
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1586
|
+
const latest = extract(await res.json());
|
|
1587
|
+
if (latest === null) throw new Error("unexpected response shape (no \"latest\" dist-tag)");
|
|
1588
|
+
return {
|
|
1589
|
+
ok: true,
|
|
1590
|
+
latest,
|
|
1591
|
+
source
|
|
1592
|
+
};
|
|
1593
|
+
} finally {
|
|
1594
|
+
cleanup();
|
|
1595
|
+
}
|
|
1596
|
+
} catch (cause) {
|
|
1597
|
+
return {
|
|
1598
|
+
ok: false,
|
|
1599
|
+
error: describeError(source, cause)
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Query npm for the newest published version of `dsh-milestone`.
|
|
1605
|
+
*
|
|
1606
|
+
* Strategy: npmmirror's dist-tags endpoint first (lightweight and CORS-open
|
|
1607
|
+
* to this Origin); if that fails or its shape is wrong, the full npm
|
|
1608
|
+
* packument fallback (`dist-tags.latest` at the JSON root). Each request
|
|
1609
|
+
* aborts after 8s via `AbortSignal.timeout` (manual timer fallback when the
|
|
1610
|
+
* environment lacks it) OR immediately when the passed-in signal aborts.
|
|
1611
|
+
* @param signal - optional caller abort signal; aborts the in-flight attempt
|
|
1612
|
+
* with priority over the internal timeout.
|
|
1613
|
+
* @returns the latest version and which registry answered, or a structured
|
|
1614
|
+
* error when both endpoints fail. Never throws.
|
|
1615
|
+
*/
|
|
1616
|
+
async function fetchLatestVersion(signal) {
|
|
1617
|
+
const viaMirror = await tryFetchLatest(NPMIRROR_DIST_TAGS_URL, extractFromDistTags, "npmmirror", signal);
|
|
1618
|
+
if (viaMirror.ok) return viaMirror;
|
|
1619
|
+
const viaNpm = await tryFetchLatest(NPM_PACKUMENT_URL, extractFromPackument, "npm", signal);
|
|
1620
|
+
if (viaNpm.ok) return viaNpm;
|
|
1621
|
+
return {
|
|
1622
|
+
ok: false,
|
|
1623
|
+
error: `${viaMirror.error}; ${viaNpm.error}`
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
/** localStorage key holding the last successful update check. */
|
|
1627
|
+
const UPDATE_CACHE_KEY = "dsh-milestone.update-cache";
|
|
1628
|
+
/**
|
|
1629
|
+
* Pure parse + freshness check of a stored blob: `null` for `null`/invalid
|
|
1630
|
+
* JSON, a wrong shape, or an entry older than {@link UPDATE_CACHE_TTL_MS}.
|
|
1631
|
+
* A `checkedAt` in the future (clock skew) is treated as fresh — it decays
|
|
1632
|
+
* naturally once wall time catches up. Never throws.
|
|
1633
|
+
* @param raw - the raw `localStorage` value (or null when absent).
|
|
1634
|
+
* @param now - wall-clock epoch ms to judge freshness against (injectable for
|
|
1635
|
+
* tests; defaults to `Date.now()`).
|
|
1636
|
+
*/
|
|
1637
|
+
function parseUpdateCache(raw, now = Date.now()) {
|
|
1638
|
+
if (raw === null) return null;
|
|
1639
|
+
let parsed;
|
|
1640
|
+
try {
|
|
1641
|
+
parsed = JSON.parse(raw);
|
|
1642
|
+
} catch {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1646
|
+
const { latest, source, checkedAt } = parsed;
|
|
1647
|
+
if (typeof latest !== "string" || latest === "") return null;
|
|
1648
|
+
if (source !== "npmmirror" && source !== "npm") return null;
|
|
1649
|
+
if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt)) return null;
|
|
1650
|
+
if (now - checkedAt >= 216e5) return null;
|
|
1651
|
+
return {
|
|
1652
|
+
latest,
|
|
1653
|
+
source,
|
|
1654
|
+
checkedAt
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Read + parse the persisted cache entry from localStorage. Any storage
|
|
1659
|
+
* failure degrades to `null` (cache miss) — the update check is best-effort.
|
|
1660
|
+
* @param now - wall-clock epoch ms for freshness (see {@link parseUpdateCache}).
|
|
1661
|
+
*/
|
|
1662
|
+
function readUpdateCache(now = Date.now()) {
|
|
1663
|
+
try {
|
|
1664
|
+
return parseUpdateCache(localStorage.getItem(UPDATE_CACHE_KEY), now);
|
|
1665
|
+
} catch {
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Persist a successful check result (the caller supplies `checkedAt`, usually
|
|
1671
|
+
* `Date.now()`). Silently ignores storage failures — the cache is an
|
|
1672
|
+
* optimization, never a hard dependency.
|
|
1673
|
+
*/
|
|
1674
|
+
function writeUpdateCache(entry) {
|
|
1675
|
+
try {
|
|
1676
|
+
localStorage.setItem(UPDATE_CACHE_KEY, JSON.stringify(entry));
|
|
1677
|
+
} catch {}
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* The cache-aware entry point for the UI: reuse an unexpired cached result
|
|
1681
|
+
* when one exists, otherwise query npm (npmmirror → packument fallback) and
|
|
1682
|
+
* persist a successful result for the next {@link UPDATE_CACHE_TTL_MS}.
|
|
1683
|
+
* Never throws; failures return the same structured `{ ok: false }` result as
|
|
1684
|
+
* {@link fetchLatestVersion}.
|
|
1685
|
+
* @param signal - optional caller abort signal, forwarded to the network
|
|
1686
|
+
* attempt only (a cache hit needs no signal).
|
|
1687
|
+
*/
|
|
1688
|
+
async function loadCachedLatest(signal) {
|
|
1689
|
+
const cached = readUpdateCache();
|
|
1690
|
+
if (cached !== null) return {
|
|
1691
|
+
ok: true,
|
|
1692
|
+
latest: cached.latest,
|
|
1693
|
+
source: cached.source
|
|
1694
|
+
};
|
|
1695
|
+
const fresh = await fetchLatestVersion(signal);
|
|
1696
|
+
if (fresh.ok) writeUpdateCache({
|
|
1697
|
+
latest: fresh.latest,
|
|
1698
|
+
source: fresh.source,
|
|
1699
|
+
checkedAt: Date.now()
|
|
1700
|
+
});
|
|
1701
|
+
return fresh;
|
|
1702
|
+
}
|
|
1703
|
+
//#endregion
|
|
1704
|
+
//#region src/client/version-meta.ts
|
|
1705
|
+
/**
|
|
1706
|
+
* Installed plugin version. Injected at build time as
|
|
1707
|
+
* `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
|
|
1708
|
+
*/
|
|
1709
|
+
const PLUGIN_VERSION = "0.6.1";
|
|
1710
|
+
//#endregion
|
|
841
1711
|
//#region src/client/MilestoneRail.tsx
|
|
842
1712
|
/**
|
|
843
1713
|
* MilestoneRail: the milestone.rail entry (session scope). Renders a fixed
|
|
@@ -890,15 +1760,58 @@ window.__ModuleLoader__.load({
|
|
|
890
1760
|
70% { box-shadow: 0 0 0 5px transparent; opacity: 0.35 }
|
|
891
1761
|
100% { box-shadow: 0 0 0 0 transparent; opacity: 0.85 }
|
|
892
1762
|
}`;
|
|
1763
|
+
/**
|
|
1764
|
+
* P3 focus mode: dims the harness's AI thinking/scratchpad blocks so the
|
|
1765
|
+
* conversation reads cleaner. The rule targets the stable, un-hashed
|
|
1766
|
+
* `data-variant="think"` attribute on the thinking-block ROOT (the harness
|
|
1767
|
+
* renders it as `data-variant="think"` with `data-state="running|ok"`), so an
|
|
1768
|
+
* overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
|
|
1769
|
+
* opening it, `[data-open]`) restores full opacity. Kept in an inline
|
|
1770
|
+
* <style> so the plugin stays zero-asset — same pattern as BADGE_PULSE_CSS.
|
|
1771
|
+
*/
|
|
1772
|
+
const FOCUS_CSS = `[data-variant="think"] { opacity: 0.4; transition: opacity 0.2s; }
|
|
1773
|
+
[data-variant="think"]:hover, [data-variant="think"] [data-open] { opacity: 1; }`;
|
|
893
1774
|
/** Visual dot diameter (px). */
|
|
894
|
-
const DOT_SIZE =
|
|
1775
|
+
const DOT_SIZE = 14;
|
|
895
1776
|
/** Hit area per dot (px) — larger than the dot for comfortable clicking. */
|
|
896
|
-
const DOT_HIT =
|
|
1777
|
+
const DOT_HIT = 28;
|
|
897
1778
|
/** Vertical gap between dot hit areas (px) — fixed pitch, never scaled. */
|
|
898
|
-
const DOT_GAP =
|
|
1779
|
+
const DOT_GAP = 14;
|
|
899
1780
|
/** Inward offset from the scrollport right edge so the rail clears the scrollbar. */
|
|
900
1781
|
const RAIL_INSET = 14;
|
|
901
1782
|
/**
|
|
1783
|
+
* P3 deep links (`#msg=<anchor-key>`): initial delay before the first
|
|
1784
|
+
* deep-link attempt — the harness scrolls the conversation to the bottom on
|
|
1785
|
+
* load, so the deep link must land AFTER the view mounts.
|
|
1786
|
+
*/
|
|
1787
|
+
const DEEP_LINK_INITIAL_DELAY = 100;
|
|
1788
|
+
/** P3: interval between DOM-row polls while waiting for the target to render. */
|
|
1789
|
+
const DEEP_LINK_POLL_DELAY = 150;
|
|
1790
|
+
/** P3: polls before falling back to a single `loadOlder` fetch. */
|
|
1791
|
+
const DEEP_LINK_MAX_POLLS = 5;
|
|
1792
|
+
/** P3: bounded polls after `loadOlder`, then the deep link gives up silently. */
|
|
1793
|
+
const DEEP_LINK_MAX_RETRY_POLLS = 5;
|
|
1794
|
+
/** B4 update-check: mount-time silent check delay (ms) — give the harness
|
|
1795
|
+
* time to settle before hitting the registry. */
|
|
1796
|
+
const UPDATE_CHECK_MOUNT_DELAY = 1500;
|
|
1797
|
+
/** Initial state: no check has completed yet, nothing to show. */
|
|
1798
|
+
const NO_UPDATE_CHECK = {
|
|
1799
|
+
phase: "idle",
|
|
1800
|
+
latest: null,
|
|
1801
|
+
source: null,
|
|
1802
|
+
error: null,
|
|
1803
|
+
available: false
|
|
1804
|
+
};
|
|
1805
|
+
/**
|
|
1806
|
+
* B4 display label for one supported host line: strips the fixed
|
|
1807
|
+
* `x.y.z` prefix and appends "line" — `0.1.1-rc.2` → `rc.2 line`,
|
|
1808
|
+
* `0.1.1` → `0.1.1 line`. Pure presentation metadata.
|
|
1809
|
+
*/
|
|
1810
|
+
function hostLineLabel(line) {
|
|
1811
|
+
const suffix = line.replace(/^\d+\.\d+\.\d+-?/, "");
|
|
1812
|
+
return suffix === "" ? `${line} line` : `${suffix} line`;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
902
1815
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
903
1816
|
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
904
1817
|
*/
|
|
@@ -930,7 +1843,10 @@ window.__ModuleLoader__.load({
|
|
|
930
1843
|
* `locale: 'dsh-milestone'`; defaults to a key-pass fallback for renders
|
|
931
1844
|
* outside the slot machinery).
|
|
932
1845
|
*/
|
|
933
|
-
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions,
|
|
1846
|
+
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
1847
|
+
items: [],
|
|
1848
|
+
hasMore: false
|
|
1849
|
+
}), openSession = () => {}, t = (key) => key }) {
|
|
934
1850
|
const order = useSession((s) => s.chat.order);
|
|
935
1851
|
const nodes = useSession((s) => s.chat.nodes);
|
|
936
1852
|
const locations = useSession((s) => s.chat.locations);
|
|
@@ -981,12 +1897,33 @@ window.__ModuleLoader__.load({
|
|
|
981
1897
|
panelOpen: false
|
|
982
1898
|
});
|
|
983
1899
|
const [bookmarksOnly, setBookmarksOnly] = (0, react.useState)(false);
|
|
1900
|
+
const [focusActive, setFocusActive] = (0, react.useState)(false);
|
|
1901
|
+
const [listOpen, setListOpen] = (0, react.useState)(false);
|
|
1902
|
+
const [crossOpen, setCrossOpen] = (0, react.useState)(false);
|
|
984
1903
|
const [copiedKey, setCopiedKey] = (0, react.useState)(null);
|
|
985
1904
|
const [forkedKey, setForkedKey] = (0, react.useState)(null);
|
|
986
1905
|
const [collapsedTurns, setCollapsedTurns] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
987
1906
|
const [focusIndex, setFocusIndex] = (0, react.useState)(0);
|
|
988
1907
|
const listRef = (0, react.useRef)(null);
|
|
989
1908
|
const currentKey = useCurrentAnchor(order);
|
|
1909
|
+
/**
|
|
1910
|
+
* P3: jump to the chat row with the given node key — smooth-scroll it into
|
|
1911
|
+
* view and write the position back into the URL hash (`#msg=<key>`) so
|
|
1912
|
+
* refresh and share preserve it. `history.replaceState` (not a
|
|
1913
|
+
* `location.hash` assignment) keeps the history stack clean, and it never
|
|
1914
|
+
* fires `hashchange`, so the deep-link listeners below never echo the
|
|
1915
|
+
* rail's own updates. No-op when the row is not (yet) rendered — the
|
|
1916
|
+
* deep-link mount retry and the load-older flow cover that case.
|
|
1917
|
+
*/
|
|
1918
|
+
const jump = (key) => {
|
|
1919
|
+
const row = findRow(key);
|
|
1920
|
+
if (row === null) return;
|
|
1921
|
+
row.scrollIntoView({
|
|
1922
|
+
behavior: "smooth",
|
|
1923
|
+
block: "start"
|
|
1924
|
+
});
|
|
1925
|
+
history.replaceState(null, "", buildMessageHash(key));
|
|
1926
|
+
};
|
|
990
1927
|
const displayMarks = (0, react.useMemo)(() => {
|
|
991
1928
|
if (!bookmarksOnly) return marks;
|
|
992
1929
|
return filterByBookmarks(marks, bookmarkedKeys).visible.map((i) => marks[i]);
|
|
@@ -1041,13 +1978,141 @@ window.__ModuleLoader__.load({
|
|
|
1041
1978
|
(0, react.useLayoutEffect)(() => {
|
|
1042
1979
|
setFocusIndex((f) => clampIndex(f, render.items.length));
|
|
1043
1980
|
}, [render.items.length]);
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1981
|
+
(0, react.useEffect)(() => {
|
|
1982
|
+
if (!listOpen && !crossOpen) return;
|
|
1983
|
+
const onKey = (e) => {
|
|
1984
|
+
if (e.key !== "Escape") return;
|
|
1985
|
+
setListOpen(false);
|
|
1986
|
+
setCrossOpen(false);
|
|
1987
|
+
};
|
|
1988
|
+
window.addEventListener("keydown", onKey);
|
|
1989
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1990
|
+
}, [listOpen, crossOpen]);
|
|
1991
|
+
const [toolbarExpanded, setToolbarExpanded] = (0, react.useState)(false);
|
|
1992
|
+
const [settingsOpen, setSettingsOpen] = (0, react.useState)(false);
|
|
1993
|
+
const [pinned, setPinned] = (0, react.useState)(() => loadPrefs());
|
|
1994
|
+
const settingsRef = (0, react.useRef)(null);
|
|
1995
|
+
const settingsBtnRef = (0, react.useRef)(null);
|
|
1996
|
+
const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
|
|
1997
|
+
const [updateCheck, setUpdateCheck] = (0, react.useState)(NO_UPDATE_CHECK);
|
|
1998
|
+
const updatePanelRef = (0, react.useRef)(null);
|
|
1999
|
+
const updateBtnRef = (0, react.useRef)(null);
|
|
2000
|
+
/**
|
|
2001
|
+
* B1 settings menu: outside-pointerdown dismisses the menu (shared
|
|
2002
|
+
* useOutsideDismiss contract) with focus returning to the gear afterwards.
|
|
2003
|
+
* The gear's own click keeps its flip semantics through a
|
|
2004
|
+
* `[data-toolbar-settings]` exclusion — pointerdown on an armed gear must
|
|
2005
|
+
* not double-close.
|
|
2006
|
+
*/
|
|
2007
|
+
useOutsideDismiss(settingsRef, settingsOpen, () => {
|
|
2008
|
+
setSettingsOpen(false);
|
|
2009
|
+
settingsBtnRef.current?.focus();
|
|
2010
|
+
}, { exclude: (target) => outsideDismissMatches(target, "[data-toolbar-settings]") });
|
|
2011
|
+
(0, react.useEffect)(() => {
|
|
2012
|
+
if (!settingsOpen) return;
|
|
2013
|
+
const onKey = (e) => {
|
|
2014
|
+
if (e.key !== "Escape") return;
|
|
2015
|
+
setSettingsOpen(false);
|
|
2016
|
+
settingsBtnRef.current?.focus();
|
|
2017
|
+
};
|
|
2018
|
+
window.addEventListener("keydown", onKey);
|
|
2019
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2020
|
+
}, [settingsOpen]);
|
|
2021
|
+
/**
|
|
2022
|
+
* B4: run one update check. Cache-aware (`loadCachedLatest` reuses an
|
|
2023
|
+
* unexpired cached result without any network traffic) and never throws:
|
|
2024
|
+
* a network failure lands in the `failed` phase with the structured error,
|
|
2025
|
+
* and an unparseable `latest` (registry anomaly) is treated as "not
|
|
2026
|
+
* available" rather than crashing the panel.
|
|
2027
|
+
*/
|
|
2028
|
+
const runUpdateCheck = () => {
|
|
2029
|
+
setUpdateCheck((prev) => ({
|
|
2030
|
+
...prev,
|
|
2031
|
+
phase: "checking"
|
|
2032
|
+
}));
|
|
2033
|
+
loadCachedLatest().then((result) => {
|
|
2034
|
+
if (result.ok) {
|
|
2035
|
+
let available = false;
|
|
2036
|
+
try {
|
|
2037
|
+
available = needsUpdate(PLUGIN_VERSION, result.latest);
|
|
2038
|
+
} catch {
|
|
2039
|
+
available = false;
|
|
2040
|
+
}
|
|
2041
|
+
setUpdateCheck({
|
|
2042
|
+
phase: "ok",
|
|
2043
|
+
latest: result.latest,
|
|
2044
|
+
source: result.source,
|
|
2045
|
+
error: null,
|
|
2046
|
+
available
|
|
2047
|
+
});
|
|
2048
|
+
} else setUpdateCheck((prev) => ({
|
|
2049
|
+
...prev,
|
|
2050
|
+
phase: "failed",
|
|
2051
|
+
error: result.error
|
|
2052
|
+
}));
|
|
1049
2053
|
});
|
|
1050
2054
|
};
|
|
2055
|
+
(0, react.useEffect)(() => {
|
|
2056
|
+
const timer = window.setTimeout(runUpdateCheck, UPDATE_CHECK_MOUNT_DELAY);
|
|
2057
|
+
return () => window.clearTimeout(timer);
|
|
2058
|
+
}, []);
|
|
2059
|
+
useOutsideDismiss(updatePanelRef, updateOpen, () => {
|
|
2060
|
+
setUpdateOpen(false);
|
|
2061
|
+
updateBtnRef.current?.focus();
|
|
2062
|
+
}, { exclude: (target) => outsideDismissMatches(target, "[data-update-check]") });
|
|
2063
|
+
(0, react.useEffect)(() => {
|
|
2064
|
+
if (!updateOpen) return;
|
|
2065
|
+
const onKey = (e) => {
|
|
2066
|
+
if (e.key !== "Escape") return;
|
|
2067
|
+
setUpdateOpen(false);
|
|
2068
|
+
updateBtnRef.current?.focus();
|
|
2069
|
+
};
|
|
2070
|
+
window.addEventListener("keydown", onKey);
|
|
2071
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2072
|
+
}, [updateOpen]);
|
|
2073
|
+
const marksRef = (0, react.useRef)(marks);
|
|
2074
|
+
(0, react.useEffect)(() => {
|
|
2075
|
+
marksRef.current = marks;
|
|
2076
|
+
});
|
|
2077
|
+
(0, react.useEffect)(() => {
|
|
2078
|
+
const key = parseDeepLinkHash(window.location.hash);
|
|
2079
|
+
if (key === null) return;
|
|
2080
|
+
let cancelled = false;
|
|
2081
|
+
let timer;
|
|
2082
|
+
const attempt = (pollsLeft, canLoadOlder) => {
|
|
2083
|
+
if (cancelled) return;
|
|
2084
|
+
if (findRow(key) !== null) {
|
|
2085
|
+
jump(key);
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
if (marksRef.current.length > 0 && !marksRef.current.some((m) => m.key === key)) return;
|
|
2089
|
+
if (pollsLeft > 0) {
|
|
2090
|
+
timer = window.setTimeout(() => attempt(pollsLeft - 1, canLoadOlder), DEEP_LINK_POLL_DELAY);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
if (canLoadOlder) {
|
|
2094
|
+
loadOlder().then(() => {
|
|
2095
|
+
timer = window.setTimeout(() => attempt(DEEP_LINK_MAX_RETRY_POLLS, false), DEEP_LINK_POLL_DELAY);
|
|
2096
|
+
}, () => {});
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
timer = window.setTimeout(() => attempt(DEEP_LINK_MAX_POLLS, true), DEEP_LINK_INITIAL_DELAY);
|
|
2101
|
+
return () => {
|
|
2102
|
+
cancelled = true;
|
|
2103
|
+
if (timer !== void 0) window.clearTimeout(timer);
|
|
2104
|
+
};
|
|
2105
|
+
}, []);
|
|
2106
|
+
(0, react.useEffect)(() => {
|
|
2107
|
+
const onHashChange = () => {
|
|
2108
|
+
const key = parseDeepLinkHash(window.location.hash);
|
|
2109
|
+
if (key === null) return;
|
|
2110
|
+
if (marksRef.current.some((m) => m.key === key)) jump(key);
|
|
2111
|
+
};
|
|
2112
|
+
window.addEventListener("hashchange", onHashChange);
|
|
2113
|
+
return () => window.removeEventListener("hashchange", onHashChange);
|
|
2114
|
+
}, []);
|
|
2115
|
+
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
1051
2116
|
const updateQuery = (query) => {
|
|
1052
2117
|
setSearch({
|
|
1053
2118
|
query,
|
|
@@ -1083,6 +2148,276 @@ window.__ModuleLoader__.load({
|
|
|
1083
2148
|
if (e.key === "Enter") advanceMatch();
|
|
1084
2149
|
if (e.key === "Escape") closeSearch();
|
|
1085
2150
|
};
|
|
2151
|
+
/** B1: flip one feature's pin — state and the persisted blob update together. */
|
|
2152
|
+
const onTogglePin = (id) => {
|
|
2153
|
+
setPinned((prev) => {
|
|
2154
|
+
const next = togglePin(prev, id);
|
|
2155
|
+
savePrefs(next);
|
|
2156
|
+
return next;
|
|
2157
|
+
});
|
|
2158
|
+
};
|
|
2159
|
+
/** B1: reset every pin (everything folds away) and persist the empty set. */
|
|
2160
|
+
const onResetPins = () => {
|
|
2161
|
+
setPinned([]);
|
|
2162
|
+
savePrefs([]);
|
|
2163
|
+
};
|
|
2164
|
+
/** B1: a feature renders while the toolbar is EXPANDED or while it is pinned. */
|
|
2165
|
+
const featureVisible = (id) => toolbarExpanded || pinned.includes(id);
|
|
2166
|
+
/**
|
|
2167
|
+
* B1: the data-driven feature registry. Each entry's render is the feature's
|
|
2168
|
+
* UNCHANGED rail-top chrome (data attributes / aria semantics preserved),
|
|
2169
|
+
* moved verbatim from the previous static button block; `search` is the
|
|
2170
|
+
* whole RailSearchUi (toggle + panel) so its lifecycle stays component-local
|
|
2171
|
+
* in the rail (search state lives in the rail and survives unmount).
|
|
2172
|
+
* Registry order = settings-menu order (站内搜索/全部提问/跨会话搜索/只看收藏/聚焦模式).
|
|
2173
|
+
*
|
|
2174
|
+
* EXTENSION POINT: push a new feature here (+ its id in toolbar-prefs.ts's
|
|
2175
|
+
* TOOLBAR_PIN_IDS and its locale keys) and pinning/settings/expand all
|
|
2176
|
+
* follow automatically — see the ToolbarFeatureDef doc above.
|
|
2177
|
+
*/
|
|
2178
|
+
const toolbarFeatures = [
|
|
2179
|
+
{
|
|
2180
|
+
id: "search",
|
|
2181
|
+
labelKey: "search.label",
|
|
2182
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailSearchUi, {
|
|
2183
|
+
panelTop: railBox.top,
|
|
2184
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
2185
|
+
query: search.query,
|
|
2186
|
+
panelOpen: search.panelOpen,
|
|
2187
|
+
matches: matches.length,
|
|
2188
|
+
total: displayMarks.length,
|
|
2189
|
+
onToggle: () => setSearch((s) => ({
|
|
2190
|
+
...s,
|
|
2191
|
+
panelOpen: !s.panelOpen
|
|
2192
|
+
})),
|
|
2193
|
+
onQueryChange: updateQuery,
|
|
2194
|
+
onSearchKeyDown,
|
|
2195
|
+
onClear: clearSearch,
|
|
2196
|
+
t
|
|
2197
|
+
})
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
id: "list",
|
|
2201
|
+
labelKey: "list.label",
|
|
2202
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2203
|
+
type: "button",
|
|
2204
|
+
"data-list-toggle": true,
|
|
2205
|
+
"aria-label": listOpen ? t("list.close") : t("list.open"),
|
|
2206
|
+
title: listOpen ? t("list.close") : t("list.open"),
|
|
2207
|
+
"aria-pressed": listOpen,
|
|
2208
|
+
onClick: () => setListOpen((v) => !v),
|
|
2209
|
+
style: {
|
|
2210
|
+
width: DOT_HIT,
|
|
2211
|
+
height: DOT_HIT,
|
|
2212
|
+
flexShrink: 0,
|
|
2213
|
+
display: "flex",
|
|
2214
|
+
alignItems: "center",
|
|
2215
|
+
justifyContent: "center",
|
|
2216
|
+
background: listOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
2217
|
+
border: "none",
|
|
2218
|
+
padding: 0,
|
|
2219
|
+
cursor: "pointer",
|
|
2220
|
+
color: listOpen ? "#9db8ff" : "#8b96ab"
|
|
2221
|
+
},
|
|
2222
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2223
|
+
width: "16",
|
|
2224
|
+
height: "16",
|
|
2225
|
+
viewBox: "0 0 24 24",
|
|
2226
|
+
fill: "none",
|
|
2227
|
+
stroke: "currentColor",
|
|
2228
|
+
strokeWidth: "2.5",
|
|
2229
|
+
strokeLinecap: "round",
|
|
2230
|
+
"aria-hidden": "true",
|
|
2231
|
+
children: [
|
|
2232
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
2233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h18" }),
|
|
2234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h18" })
|
|
2235
|
+
]
|
|
2236
|
+
})
|
|
2237
|
+
})
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
id: "sessionSearch",
|
|
2241
|
+
labelKey: "search.cross",
|
|
2242
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2243
|
+
type: "button",
|
|
2244
|
+
"data-session-search-toggle": true,
|
|
2245
|
+
"aria-label": crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
2246
|
+
title: crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
2247
|
+
"aria-pressed": crossOpen,
|
|
2248
|
+
onClick: () => setCrossOpen((v) => !v),
|
|
2249
|
+
style: {
|
|
2250
|
+
width: DOT_HIT,
|
|
2251
|
+
height: DOT_HIT,
|
|
2252
|
+
flexShrink: 0,
|
|
2253
|
+
display: "flex",
|
|
2254
|
+
alignItems: "center",
|
|
2255
|
+
justifyContent: "center",
|
|
2256
|
+
background: crossOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
2257
|
+
border: "none",
|
|
2258
|
+
padding: 0,
|
|
2259
|
+
cursor: "pointer",
|
|
2260
|
+
color: crossOpen ? "#9db8ff" : "#8b96ab"
|
|
2261
|
+
},
|
|
2262
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2263
|
+
width: "16",
|
|
2264
|
+
height: "16",
|
|
2265
|
+
viewBox: "0 0 24 24",
|
|
2266
|
+
fill: "none",
|
|
2267
|
+
stroke: "currentColor",
|
|
2268
|
+
strokeWidth: "2",
|
|
2269
|
+
strokeLinecap: "round",
|
|
2270
|
+
strokeLinejoin: "round",
|
|
2271
|
+
"aria-hidden": "true",
|
|
2272
|
+
children: [
|
|
2273
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h9" }),
|
|
2274
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h9" }),
|
|
2275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h9" }),
|
|
2276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
2277
|
+
cx: "17",
|
|
2278
|
+
cy: "7",
|
|
2279
|
+
r: "3.5"
|
|
2280
|
+
}),
|
|
2281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m19.5 9.5 2.5 2.5" })
|
|
2282
|
+
]
|
|
2283
|
+
})
|
|
2284
|
+
})
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
id: "bookmarks",
|
|
2288
|
+
labelKey: "bookmark.filter",
|
|
2289
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2290
|
+
type: "button",
|
|
2291
|
+
"data-bookmarks-toggle": true,
|
|
2292
|
+
"aria-label": t("bookmark.filter"),
|
|
2293
|
+
"aria-pressed": bookmarksOnly,
|
|
2294
|
+
"data-active": bookmarksOnly ? "true" : void 0,
|
|
2295
|
+
onClick: () => setBookmarksOnly((v) => !v),
|
|
2296
|
+
style: {
|
|
2297
|
+
width: DOT_HIT,
|
|
2298
|
+
height: DOT_HIT,
|
|
2299
|
+
flexShrink: 0,
|
|
2300
|
+
display: "flex",
|
|
2301
|
+
alignItems: "center",
|
|
2302
|
+
justifyContent: "center",
|
|
2303
|
+
background: bookmarksOnly ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
2304
|
+
border: "none",
|
|
2305
|
+
padding: 0,
|
|
2306
|
+
cursor: "pointer",
|
|
2307
|
+
color: bookmarksOnly ? "#9db8ff" : "#8b96ab"
|
|
2308
|
+
},
|
|
2309
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2310
|
+
width: "16",
|
|
2311
|
+
height: "16",
|
|
2312
|
+
viewBox: "0 0 24 24",
|
|
2313
|
+
fill: bookmarksOnly ? "currentColor" : "none",
|
|
2314
|
+
stroke: "currentColor",
|
|
2315
|
+
strokeWidth: "2",
|
|
2316
|
+
strokeLinejoin: "round",
|
|
2317
|
+
"aria-hidden": "true",
|
|
2318
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
|
|
2319
|
+
})
|
|
2320
|
+
})
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
id: "focus",
|
|
2324
|
+
labelKey: "focus.on",
|
|
2325
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2326
|
+
type: "button",
|
|
2327
|
+
"data-focus-toggle": true,
|
|
2328
|
+
"aria-label": focusActive ? t("focus.off") : t("focus.on"),
|
|
2329
|
+
title: focusActive ? t("focus.off") : t("focus.on"),
|
|
2330
|
+
"aria-pressed": focusActive,
|
|
2331
|
+
onClick: () => setFocusActive((v) => !v),
|
|
2332
|
+
style: {
|
|
2333
|
+
width: DOT_HIT,
|
|
2334
|
+
height: DOT_HIT,
|
|
2335
|
+
flexShrink: 0,
|
|
2336
|
+
display: "flex",
|
|
2337
|
+
alignItems: "center",
|
|
2338
|
+
justifyContent: "center",
|
|
2339
|
+
background: focusActive ? "rgba(126, 226, 168, 0.14)" : "transparent",
|
|
2340
|
+
border: "none",
|
|
2341
|
+
padding: 0,
|
|
2342
|
+
cursor: "pointer",
|
|
2343
|
+
color: focusActive ? "#7ee2a8" : "#8b96ab"
|
|
2344
|
+
},
|
|
2345
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2346
|
+
width: "16",
|
|
2347
|
+
height: "16",
|
|
2348
|
+
viewBox: "0 0 24 24",
|
|
2349
|
+
fill: "none",
|
|
2350
|
+
stroke: "currentColor",
|
|
2351
|
+
strokeWidth: "2",
|
|
2352
|
+
strokeLinecap: "round",
|
|
2353
|
+
strokeLinejoin: "round",
|
|
2354
|
+
"aria-hidden": "true",
|
|
2355
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
2356
|
+
cx: "12",
|
|
2357
|
+
cy: "12",
|
|
2358
|
+
r: "3"
|
|
2359
|
+
})]
|
|
2360
|
+
})
|
|
2361
|
+
})
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
id: "updateCheck",
|
|
2365
|
+
labelKey: "update.check",
|
|
2366
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2367
|
+
type: "button",
|
|
2368
|
+
ref: updateBtnRef,
|
|
2369
|
+
"data-update-check": true,
|
|
2370
|
+
"aria-expanded": updateOpen,
|
|
2371
|
+
"aria-label": t("update.check"),
|
|
2372
|
+
title: t("update.check"),
|
|
2373
|
+
onClick: () => setUpdateOpen((v) => !v),
|
|
2374
|
+
style: {
|
|
2375
|
+
position: "relative",
|
|
2376
|
+
width: DOT_HIT,
|
|
2377
|
+
height: DOT_HIT,
|
|
2378
|
+
flexShrink: 0,
|
|
2379
|
+
display: "flex",
|
|
2380
|
+
alignItems: "center",
|
|
2381
|
+
justifyContent: "center",
|
|
2382
|
+
background: updateOpen || updateCheck.available ? "rgba(245, 197, 66, 0.14)" : "transparent",
|
|
2383
|
+
border: "none",
|
|
2384
|
+
padding: 0,
|
|
2385
|
+
cursor: "pointer",
|
|
2386
|
+
color: updateCheck.available ? "#f5c542" : "#8b96ab"
|
|
2387
|
+
},
|
|
2388
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2389
|
+
width: "16",
|
|
2390
|
+
height: "16",
|
|
2391
|
+
viewBox: "0 0 24 24",
|
|
2392
|
+
fill: "none",
|
|
2393
|
+
stroke: "currentColor",
|
|
2394
|
+
strokeWidth: "2",
|
|
2395
|
+
strokeLinecap: "round",
|
|
2396
|
+
strokeLinejoin: "round",
|
|
2397
|
+
"aria-hidden": "true",
|
|
2398
|
+
children: [
|
|
2399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
|
|
2400
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 3v5h-5" }),
|
|
2401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
|
|
2402
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 16H3v5" })
|
|
2403
|
+
]
|
|
2404
|
+
}), updateCheck.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2405
|
+
"data-update-available": true,
|
|
2406
|
+
style: {
|
|
2407
|
+
position: "absolute",
|
|
2408
|
+
top: -2,
|
|
2409
|
+
right: -2,
|
|
2410
|
+
width: 8,
|
|
2411
|
+
height: 8,
|
|
2412
|
+
borderRadius: "50%",
|
|
2413
|
+
background: "#f5c542",
|
|
2414
|
+
border: "2px solid rgba(20, 24, 32, 0.95)",
|
|
2415
|
+
pointerEvents: "none"
|
|
2416
|
+
}
|
|
2417
|
+
})]
|
|
2418
|
+
})
|
|
2419
|
+
}
|
|
2420
|
+
];
|
|
1086
2421
|
/** Focus the dot at `index` (no-op while the list is unmounted). */
|
|
1087
2422
|
const focusDotAt = (index) => {
|
|
1088
2423
|
listRef.current?.querySelectorAll("[data-rail-dot]")[index]?.focus();
|
|
@@ -1208,11 +2543,15 @@ window.__ModuleLoader__.load({
|
|
|
1208
2543
|
pointerEvents: "auto",
|
|
1209
2544
|
zIndex: 100,
|
|
1210
2545
|
display: "flex",
|
|
1211
|
-
flexDirection: "column"
|
|
2546
|
+
flexDirection: "column",
|
|
2547
|
+
gap: 6,
|
|
2548
|
+
paddingTop: 6
|
|
1212
2549
|
},
|
|
1213
2550
|
"aria-label": t("rail.label"),
|
|
2551
|
+
"data-focus-active": focusActive ? "true" : void 0,
|
|
1214
2552
|
children: [
|
|
1215
2553
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BADGE_PULSE_CSS }),
|
|
2554
|
+
focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: FOCUS_CSS }),
|
|
1216
2555
|
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1217
2556
|
type: "button",
|
|
1218
2557
|
"data-load-older": true,
|
|
@@ -1235,7 +2574,7 @@ window.__ModuleLoader__.load({
|
|
|
1235
2574
|
padding: 0,
|
|
1236
2575
|
cursor: loadingOlder ? "default" : "pointer",
|
|
1237
2576
|
color: loadingOlder ? "#5a6375" : "#8b96ab",
|
|
1238
|
-
fontSize:
|
|
2577
|
+
fontSize: 13,
|
|
1239
2578
|
lineHeight: 1,
|
|
1240
2579
|
letterSpacing: 1
|
|
1241
2580
|
},
|
|
@@ -1243,11 +2582,11 @@ window.__ModuleLoader__.load({
|
|
|
1243
2582
|
}),
|
|
1244
2583
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1245
2584
|
type: "button",
|
|
1246
|
-
"data-
|
|
1247
|
-
"aria-
|
|
1248
|
-
"aria-
|
|
1249
|
-
|
|
1250
|
-
onClick: () =>
|
|
2585
|
+
"data-toolbar-expand": true,
|
|
2586
|
+
"aria-expanded": toolbarExpanded,
|
|
2587
|
+
"aria-label": toolbarExpanded ? t("toolbar.collapse") : t("toolbar.expand"),
|
|
2588
|
+
title: toolbarExpanded ? t("toolbar.collapse") : t("toolbar.expand"),
|
|
2589
|
+
onClick: () => setToolbarExpanded((v) => !v),
|
|
1251
2590
|
style: {
|
|
1252
2591
|
width: DOT_HIT,
|
|
1253
2592
|
height: DOT_HIT,
|
|
@@ -1255,38 +2594,390 @@ window.__ModuleLoader__.load({
|
|
|
1255
2594
|
display: "flex",
|
|
1256
2595
|
alignItems: "center",
|
|
1257
2596
|
justifyContent: "center",
|
|
1258
|
-
background:
|
|
2597
|
+
background: "transparent",
|
|
1259
2598
|
border: "none",
|
|
1260
2599
|
padding: 0,
|
|
1261
2600
|
cursor: "pointer",
|
|
1262
|
-
color:
|
|
2601
|
+
color: "#8b96ab"
|
|
1263
2602
|
},
|
|
1264
2603
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1265
|
-
width: "
|
|
1266
|
-
height: "
|
|
2604
|
+
width: "16",
|
|
2605
|
+
height: "16",
|
|
1267
2606
|
viewBox: "0 0 24 24",
|
|
1268
|
-
fill:
|
|
2607
|
+
fill: "none",
|
|
2608
|
+
stroke: "currentColor",
|
|
2609
|
+
strokeWidth: "2.5",
|
|
2610
|
+
strokeLinecap: "round",
|
|
2611
|
+
strokeLinejoin: "round",
|
|
2612
|
+
"aria-hidden": "true",
|
|
2613
|
+
children: toolbarExpanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m18 15-6-6-6 6" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m6 9 6 6 6-6" })
|
|
2614
|
+
})
|
|
2615
|
+
}),
|
|
2616
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2617
|
+
type: "button",
|
|
2618
|
+
ref: settingsBtnRef,
|
|
2619
|
+
"data-toolbar-settings": true,
|
|
2620
|
+
"aria-expanded": settingsOpen,
|
|
2621
|
+
"aria-label": settingsOpen ? t("toolbar.settings.close") : t("toolbar.settings.open"),
|
|
2622
|
+
title: settingsOpen ? t("toolbar.settings.close") : t("toolbar.settings.open"),
|
|
2623
|
+
onClick: () => setSettingsOpen((v) => !v),
|
|
2624
|
+
style: {
|
|
2625
|
+
width: DOT_HIT,
|
|
2626
|
+
height: DOT_HIT,
|
|
2627
|
+
flexShrink: 0,
|
|
2628
|
+
display: "flex",
|
|
2629
|
+
alignItems: "center",
|
|
2630
|
+
justifyContent: "center",
|
|
2631
|
+
background: settingsOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
2632
|
+
border: "none",
|
|
2633
|
+
padding: 0,
|
|
2634
|
+
cursor: "pointer",
|
|
2635
|
+
color: settingsOpen ? "#9db8ff" : "#8b96ab"
|
|
2636
|
+
},
|
|
2637
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2638
|
+
width: "16",
|
|
2639
|
+
height: "16",
|
|
2640
|
+
viewBox: "0 0 24 24",
|
|
2641
|
+
fill: "none",
|
|
1269
2642
|
stroke: "currentColor",
|
|
1270
2643
|
strokeWidth: "2",
|
|
2644
|
+
strokeLinecap: "round",
|
|
1271
2645
|
strokeLinejoin: "round",
|
|
1272
2646
|
"aria-hidden": "true",
|
|
1273
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
2647
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
2648
|
+
cx: "12",
|
|
2649
|
+
cy: "12",
|
|
2650
|
+
r: "3"
|
|
2651
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })]
|
|
1274
2652
|
})
|
|
1275
2653
|
}),
|
|
1276
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
2654
|
+
toolbarFeatures.map((feature) => featureVisible(feature.id) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: feature.render() }, feature.id) : null),
|
|
2655
|
+
settingsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2656
|
+
ref: settingsRef,
|
|
2657
|
+
"data-toolbar-settings-panel": true,
|
|
2658
|
+
style: {
|
|
2659
|
+
position: "fixed",
|
|
2660
|
+
top: railBox.top,
|
|
2661
|
+
right: railBox.right + DOT_HIT + 8,
|
|
2662
|
+
width: "min(240px, calc(100vw - 48px))",
|
|
2663
|
+
padding: "10px 12px",
|
|
2664
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
2665
|
+
color: "#e6e8ee",
|
|
2666
|
+
borderRadius: 8,
|
|
2667
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
2668
|
+
zIndex: 104
|
|
2669
|
+
},
|
|
2670
|
+
children: [
|
|
2671
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-toolbar-pin-toggle]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
2672
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2673
|
+
"data-toolbar-settings-title": true,
|
|
2674
|
+
style: {
|
|
2675
|
+
fontSize: 13,
|
|
2676
|
+
fontWeight: 600,
|
|
2677
|
+
color: "#e6e8ee",
|
|
2678
|
+
marginBottom: 2
|
|
2679
|
+
},
|
|
2680
|
+
children: t("settings.title")
|
|
2681
|
+
}),
|
|
2682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2683
|
+
style: {
|
|
2684
|
+
fontSize: 12,
|
|
2685
|
+
color: "#8b96ab",
|
|
2686
|
+
marginBottom: 4,
|
|
2687
|
+
textAlign: "right"
|
|
2688
|
+
},
|
|
2689
|
+
children: t("settings.pin")
|
|
2690
|
+
}),
|
|
2691
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2692
|
+
role: "menu",
|
|
2693
|
+
"aria-label": t("settings.title"),
|
|
2694
|
+
style: {
|
|
2695
|
+
display: "flex",
|
|
2696
|
+
flexDirection: "column",
|
|
2697
|
+
gap: 2
|
|
2698
|
+
},
|
|
2699
|
+
children: toolbarFeatures.map((feature) => {
|
|
2700
|
+
const checked = pinned.includes(feature.id);
|
|
2701
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2702
|
+
type: "button",
|
|
2703
|
+
role: "menuitemcheckbox",
|
|
2704
|
+
"data-toolbar-pin-toggle": true,
|
|
2705
|
+
"data-pin-id": feature.id,
|
|
2706
|
+
"aria-checked": checked,
|
|
2707
|
+
onClick: () => onTogglePin(feature.id),
|
|
2708
|
+
style: {
|
|
2709
|
+
display: "flex",
|
|
2710
|
+
alignItems: "center",
|
|
2711
|
+
justifyContent: "space-between",
|
|
2712
|
+
gap: 8,
|
|
2713
|
+
width: "100%",
|
|
2714
|
+
padding: "6px 8px",
|
|
2715
|
+
background: "transparent",
|
|
2716
|
+
border: "none",
|
|
2717
|
+
borderRadius: 6,
|
|
2718
|
+
cursor: "pointer",
|
|
2719
|
+
color: "#e6e8ee",
|
|
2720
|
+
fontSize: 13,
|
|
2721
|
+
textAlign: "left"
|
|
2722
|
+
},
|
|
2723
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(feature.labelKey) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2724
|
+
"aria-hidden": "true",
|
|
2725
|
+
style: {
|
|
2726
|
+
width: 14,
|
|
2727
|
+
height: 14,
|
|
2728
|
+
flexShrink: 0,
|
|
2729
|
+
borderRadius: 3,
|
|
2730
|
+
border: "1px solid rgba(255, 255, 255, 0.35)",
|
|
2731
|
+
background: checked ? "rgba(77, 124, 254, 0.9)" : "transparent",
|
|
2732
|
+
display: "flex",
|
|
2733
|
+
alignItems: "center",
|
|
2734
|
+
justifyContent: "center",
|
|
2735
|
+
color: "#ffffff",
|
|
2736
|
+
fontSize: 10,
|
|
2737
|
+
lineHeight: 1
|
|
2738
|
+
},
|
|
2739
|
+
children: checked ? "✓" : ""
|
|
2740
|
+
})]
|
|
2741
|
+
}, feature.id);
|
|
2742
|
+
})
|
|
2743
|
+
}),
|
|
2744
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2745
|
+
type: "button",
|
|
2746
|
+
"data-toolbar-settings-reset": true,
|
|
2747
|
+
onClick: onResetPins,
|
|
2748
|
+
style: {
|
|
2749
|
+
width: "100%",
|
|
2750
|
+
marginTop: 6,
|
|
2751
|
+
padding: "6px 8px",
|
|
2752
|
+
background: "transparent",
|
|
2753
|
+
border: "none",
|
|
2754
|
+
borderRadius: 6,
|
|
2755
|
+
cursor: "pointer",
|
|
2756
|
+
color: "#8b96ab",
|
|
2757
|
+
fontSize: 13,
|
|
2758
|
+
textAlign: "left"
|
|
2759
|
+
},
|
|
2760
|
+
children: t("settings.reset")
|
|
2761
|
+
}),
|
|
2762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2763
|
+
"data-toolbar-settings-footer": true,
|
|
2764
|
+
style: {
|
|
2765
|
+
marginTop: 8,
|
|
2766
|
+
paddingTop: 8,
|
|
2767
|
+
borderTop: "1px solid rgba(255, 255, 255, 0.12)"
|
|
2768
|
+
},
|
|
2769
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2770
|
+
style: {
|
|
2771
|
+
fontSize: 12,
|
|
2772
|
+
color: "#8b96ab",
|
|
2773
|
+
marginBottom: 6
|
|
2774
|
+
},
|
|
2775
|
+
children: t("settings.support")
|
|
2776
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2777
|
+
style: {
|
|
2778
|
+
display: "flex",
|
|
2779
|
+
flexDirection: "column",
|
|
2780
|
+
gap: 4
|
|
2781
|
+
},
|
|
2782
|
+
children: [
|
|
2783
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2784
|
+
href: "https://github.com/SnowCrescenter-tech/dsh-milestone",
|
|
2785
|
+
target: "_blank",
|
|
2786
|
+
rel: "noreferrer",
|
|
2787
|
+
style: {
|
|
2788
|
+
fontSize: 12,
|
|
2789
|
+
color: "#9db8ff",
|
|
2790
|
+
textDecoration: "none"
|
|
2791
|
+
},
|
|
2792
|
+
children: t("settings.repo")
|
|
2793
|
+
}),
|
|
2794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2795
|
+
href: "https://github.com/SnowCrescenter-tech/dsh-milestone",
|
|
2796
|
+
target: "_blank",
|
|
2797
|
+
rel: "noreferrer",
|
|
2798
|
+
style: {
|
|
2799
|
+
fontSize: 12,
|
|
2800
|
+
color: "#9db8ff",
|
|
2801
|
+
textDecoration: "none"
|
|
2802
|
+
},
|
|
2803
|
+
children: t("settings.star")
|
|
2804
|
+
}),
|
|
2805
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2806
|
+
href: `https://github.com/SnowCrescenter-tech/dsh-milestone/issues`,
|
|
2807
|
+
target: "_blank",
|
|
2808
|
+
rel: "noreferrer",
|
|
2809
|
+
style: {
|
|
2810
|
+
fontSize: 12,
|
|
2811
|
+
color: "#9db8ff",
|
|
2812
|
+
textDecoration: "none"
|
|
2813
|
+
},
|
|
2814
|
+
children: t("settings.issues")
|
|
2815
|
+
}),
|
|
2816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2817
|
+
href: "https://www.npmjs.com/package/dsh-milestone",
|
|
2818
|
+
target: "_blank",
|
|
2819
|
+
rel: "noreferrer",
|
|
2820
|
+
style: {
|
|
2821
|
+
fontSize: 12,
|
|
2822
|
+
color: "#9db8ff",
|
|
2823
|
+
textDecoration: "none"
|
|
2824
|
+
},
|
|
2825
|
+
children: t("settings.npm")
|
|
2826
|
+
})
|
|
2827
|
+
]
|
|
2828
|
+
})]
|
|
2829
|
+
})
|
|
2830
|
+
]
|
|
2831
|
+
}),
|
|
2832
|
+
updateOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2833
|
+
ref: updatePanelRef,
|
|
2834
|
+
"data-update-panel": true,
|
|
2835
|
+
style: {
|
|
2836
|
+
position: "fixed",
|
|
2837
|
+
top: railBox.top,
|
|
2838
|
+
right: railBox.right + DOT_HIT + 8,
|
|
2839
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
2840
|
+
padding: "10px 12px",
|
|
2841
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
2842
|
+
color: "#e6e8ee",
|
|
2843
|
+
borderRadius: 8,
|
|
2844
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
2845
|
+
zIndex: 104
|
|
2846
|
+
},
|
|
2847
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2848
|
+
"data-update-title": true,
|
|
2849
|
+
style: {
|
|
2850
|
+
fontSize: 13,
|
|
2851
|
+
fontWeight: 600,
|
|
2852
|
+
color: "#e6e8ee",
|
|
2853
|
+
marginBottom: 8
|
|
2854
|
+
},
|
|
2855
|
+
children: t("update.title")
|
|
2856
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2857
|
+
style: {
|
|
2858
|
+
display: "flex",
|
|
2859
|
+
flexDirection: "column",
|
|
2860
|
+
gap: 6,
|
|
2861
|
+
fontSize: 12,
|
|
2862
|
+
lineHeight: 1.5
|
|
2863
|
+
},
|
|
2864
|
+
children: [
|
|
2865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2866
|
+
style: { color: "#8b96ab" },
|
|
2867
|
+
children: [t("update.current"), ": "]
|
|
2868
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.1" })] }),
|
|
2869
|
+
updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2870
|
+
"data-update-latest": true,
|
|
2871
|
+
children: [
|
|
2872
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2873
|
+
style: { color: "#8b96ab" },
|
|
2874
|
+
children: [t("update.latest"), ": "]
|
|
2875
|
+
}),
|
|
2876
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: updateCheck.latest }),
|
|
2877
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2878
|
+
style: { color: "#8b96ab" },
|
|
2879
|
+
children: [
|
|
2880
|
+
" (",
|
|
2881
|
+
updateCheck.source,
|
|
2882
|
+
")"
|
|
2883
|
+
]
|
|
2884
|
+
})
|
|
2885
|
+
]
|
|
2886
|
+
}),
|
|
2887
|
+
updateCheck.phase === "checking" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2888
|
+
"data-update-status": true,
|
|
2889
|
+
style: { color: "#8b96ab" },
|
|
2890
|
+
children: t("update.checking")
|
|
2891
|
+
}),
|
|
2892
|
+
updateCheck.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2893
|
+
"data-update-failed": true,
|
|
2894
|
+
children: [
|
|
2895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("update.failed"), ":"] }),
|
|
2896
|
+
" ",
|
|
2897
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2898
|
+
style: { color: "#8b96ab" },
|
|
2899
|
+
children: updateCheck.error
|
|
2900
|
+
}),
|
|
2901
|
+
" ",
|
|
2902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2903
|
+
type: "button",
|
|
2904
|
+
"data-update-retry": true,
|
|
2905
|
+
onClick: runUpdateCheck,
|
|
2906
|
+
style: {
|
|
2907
|
+
background: "transparent",
|
|
2908
|
+
border: "none",
|
|
2909
|
+
padding: 0,
|
|
2910
|
+
cursor: "pointer",
|
|
2911
|
+
color: "#9db8ff",
|
|
2912
|
+
fontSize: 12,
|
|
2913
|
+
textDecoration: "underline"
|
|
2914
|
+
},
|
|
2915
|
+
children: t("update.retry")
|
|
2916
|
+
})
|
|
2917
|
+
]
|
|
2918
|
+
}),
|
|
2919
|
+
updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2920
|
+
"data-update-conclusion": true,
|
|
2921
|
+
children: updateCheck.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2922
|
+
t("update.available"),
|
|
2923
|
+
" v",
|
|
2924
|
+
updateCheck.latest,
|
|
2925
|
+
" → "
|
|
2926
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2927
|
+
href: "https://www.npmjs.com/package/dsh-milestone",
|
|
2928
|
+
target: "_blank",
|
|
2929
|
+
rel: "noreferrer",
|
|
2930
|
+
style: {
|
|
2931
|
+
color: "#9db8ff",
|
|
2932
|
+
textDecoration: "none"
|
|
2933
|
+
},
|
|
2934
|
+
children: t("update.goNpm")
|
|
2935
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2936
|
+
style: { color: "#7ee2a8" },
|
|
2937
|
+
children: t("update.upToDate")
|
|
2938
|
+
})
|
|
2939
|
+
}),
|
|
2940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2941
|
+
"data-update-host-lines": true,
|
|
2942
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2943
|
+
style: { color: "#8b96ab" },
|
|
2944
|
+
children: [t("update.hostLines"), ": "]
|
|
2945
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: SUPPORTED_HOST_LINES.map(hostLineLabel).join("、") })]
|
|
2946
|
+
}),
|
|
2947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2948
|
+
type: "button",
|
|
2949
|
+
"data-update-manual": true,
|
|
2950
|
+
disabled: updateCheck.phase === "checking",
|
|
2951
|
+
onClick: runUpdateCheck,
|
|
2952
|
+
style: {
|
|
2953
|
+
marginTop: 4,
|
|
2954
|
+
padding: "6px 10px",
|
|
2955
|
+
background: updateCheck.phase === "checking" ? "transparent" : "rgba(77, 124, 254, 0.18)",
|
|
2956
|
+
border: "none",
|
|
2957
|
+
borderRadius: 6,
|
|
2958
|
+
cursor: updateCheck.phase === "checking" ? "default" : "pointer",
|
|
2959
|
+
color: updateCheck.phase === "checking" ? "#5a6375" : "#9db8ff",
|
|
2960
|
+
fontSize: 12,
|
|
2961
|
+
alignSelf: "flex-start"
|
|
2962
|
+
},
|
|
2963
|
+
children: updateCheck.phase === "checking" ? t("update.checking") : t("update.check")
|
|
2964
|
+
})
|
|
2965
|
+
]
|
|
2966
|
+
})]
|
|
2967
|
+
}),
|
|
2968
|
+
listOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneListPanel, {
|
|
1277
2969
|
panelTop: railBox.top,
|
|
1278
2970
|
panelRight: railBox.right + DOT_HIT + 8,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
onClear: clearSearch,
|
|
2971
|
+
marks,
|
|
2972
|
+
onJump: jump,
|
|
2973
|
+
t
|
|
2974
|
+
}),
|
|
2975
|
+
crossOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneSessionSearch, {
|
|
2976
|
+
panelTop: railBox.top,
|
|
2977
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
2978
|
+
onClose: () => setCrossOpen(false),
|
|
2979
|
+
searchSessions,
|
|
2980
|
+
openSession,
|
|
1290
2981
|
t
|
|
1291
2982
|
}),
|
|
1292
2983
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -1421,7 +3112,7 @@ window.__ModuleLoader__.load({
|
|
|
1421
3112
|
right: "100%",
|
|
1422
3113
|
marginRight: 8,
|
|
1423
3114
|
whiteSpace: "nowrap",
|
|
1424
|
-
fontSize:
|
|
3115
|
+
fontSize: 12,
|
|
1425
3116
|
lineHeight: 1,
|
|
1426
3117
|
color: "rgba(139, 150, 171, 0.9)",
|
|
1427
3118
|
pointerEvents: "none",
|
|
@@ -1465,6 +3156,46 @@ window.__ModuleLoader__.load({
|
|
|
1465
3156
|
//#endregion
|
|
1466
3157
|
//#region src/client/railInject.ts
|
|
1467
3158
|
/**
|
|
3159
|
+
* Wrap the `session.search` RPC into a safe cross-session search action.
|
|
3160
|
+
*
|
|
3161
|
+
* - `ok: true` unwraps the value and joins each hit's human display title
|
|
3162
|
+
* from the session list snapshot (a session outside the list keeps no
|
|
3163
|
+
* title — the caller falls back to `search.untitled`).
|
|
3164
|
+
* - `ok: false` throws `new Error(error.message)` so the caller can surface
|
|
3165
|
+
* the business/transport error as the `search.error` state.
|
|
3166
|
+
* - A rejected RPC propagates unchanged.
|
|
3167
|
+
*
|
|
3168
|
+
* The list read happens inside the returned closure (never at factory time),
|
|
3169
|
+
* so the title join always reflects the current list snapshot.
|
|
3170
|
+
*
|
|
3171
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
3172
|
+
* @returns an action that searches all sessions' message content.
|
|
3173
|
+
*/
|
|
3174
|
+
function createSessionSearch(sessions) {
|
|
3175
|
+
return async (query, signal) => {
|
|
3176
|
+
const result = await sessions.search(query, signal);
|
|
3177
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
3178
|
+
const byId = sessions.list.getSnapshot().byId;
|
|
3179
|
+
return {
|
|
3180
|
+
items: result.value.items.map((item) => ({
|
|
3181
|
+
...item,
|
|
3182
|
+
title: byId[item.sessionId]?.displayTitle
|
|
3183
|
+
})),
|
|
3184
|
+
hasMore: result.value.hasMore
|
|
3185
|
+
};
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Wrap a session `open` call into a safe action that selects a listed session
|
|
3190
|
+
* as current — the exact selection path the sidebar uses on click.
|
|
3191
|
+
*
|
|
3192
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
3193
|
+
* @returns an action that opens the given session.
|
|
3194
|
+
*/
|
|
3195
|
+
function createOpenSession(sessions) {
|
|
3196
|
+
return (id) => sessions.open(id);
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
1468
3199
|
* Wrap a session-bound `loadOlder` call into a safe action closure.
|
|
1469
3200
|
*
|
|
1470
3201
|
* - Missing binding: resolves (never throws on an unlisted/unscoped session).
|
|
@@ -1530,6 +3261,10 @@ window.__ModuleLoader__.load({
|
|
|
1530
3261
|
"search.label": "搜索消息",
|
|
1531
3262
|
/** aria-label on the bookmarks-only filter toggle. */
|
|
1532
3263
|
"bookmark.filter": "只看收藏",
|
|
3264
|
+
/** aria-label + title on the focus-mode toggle when focus is OFF (arm it). */
|
|
3265
|
+
"focus.on": "聚焦模式",
|
|
3266
|
+
/** aria-label + title on the focus-mode toggle when focus is ON (disarm it). */
|
|
3267
|
+
"focus.off": "退出聚焦",
|
|
1533
3268
|
/** aria-label on the hover tooltip star toggle. */
|
|
1534
3269
|
"bookmark.star": "收藏此消息",
|
|
1535
3270
|
/** aria-label on the search clear button. */
|
|
@@ -1573,7 +3308,71 @@ window.__ModuleLoader__.load({
|
|
|
1573
3308
|
/** Collapse-turn tooltip action. */
|
|
1574
3309
|
"collapse.turn": "折叠此轮",
|
|
1575
3310
|
/** Expand-turn tooltip action. */
|
|
1576
|
-
"expand.turn": "展开此轮"
|
|
3311
|
+
"expand.turn": "展开此轮",
|
|
3312
|
+
/** aria-label + title on the milestone-list toggle when the panel is CLOSED. */
|
|
3313
|
+
"list.open": "打开列表",
|
|
3314
|
+
/** aria-label + title on the milestone-list toggle when the panel is OPEN. */
|
|
3315
|
+
"list.close": "收起列表",
|
|
3316
|
+
/** Header title of the all-prompts list panel. */
|
|
3317
|
+
"list.label": "全部提问",
|
|
3318
|
+
/** Header title + input placeholder of the cross-session search panel. */
|
|
3319
|
+
"search.cross": "跨会话搜索",
|
|
3320
|
+
/** aria-label + title on the cross-session search toggle when the panel is CLOSED. */
|
|
3321
|
+
"search.cross.open": "打开跨会话搜索",
|
|
3322
|
+
/** aria-label + title on the cross-session search toggle when the panel is OPEN. */
|
|
3323
|
+
"search.cross.close": "收起跨会话搜索",
|
|
3324
|
+
/** Cross-session result row title fallback for sessions with no display title. */
|
|
3325
|
+
"search.untitled": "(无标题)",
|
|
3326
|
+
/** Cross-session search failure notice. */
|
|
3327
|
+
"search.error": "搜索失败,请重试",
|
|
3328
|
+
/** Cross-session search footer hint when the harness capped the result list. */
|
|
3329
|
+
"search.more": "结果已截断,请细化关键词",
|
|
3330
|
+
/** aria-label on the toolbar expand arrow while the toolbar is COLLAPSED (expand it). */
|
|
3331
|
+
"toolbar.expand": "展开工具栏",
|
|
3332
|
+
/** aria-label on the toolbar expand arrow while the toolbar is EXPANDED (collapse it). */
|
|
3333
|
+
"toolbar.collapse": "收起工具栏",
|
|
3334
|
+
/** aria-label on the toolbar settings gear while the settings menu is CLOSED. */
|
|
3335
|
+
"toolbar.settings.open": "打开设置",
|
|
3336
|
+
/** aria-label on the toolbar settings gear while the settings menu is OPEN. */
|
|
3337
|
+
"toolbar.settings.close": "关闭设置",
|
|
3338
|
+
/** Header title of the toolbar settings menu. */
|
|
3339
|
+
"settings.title": "设置",
|
|
3340
|
+
/** Per-feature toggle label inside the settings menu: keep visible while collapsed. */
|
|
3341
|
+
"settings.pin": "在折叠外显示",
|
|
3342
|
+
/** Settings action that clears every pinned feature. */
|
|
3343
|
+
"settings.reset": "恢复默认",
|
|
3344
|
+
/** Settings footer heading above the project links. */
|
|
3345
|
+
"settings.support": "支持我们",
|
|
3346
|
+
/** Settings footer link: the GitHub repository. */
|
|
3347
|
+
"settings.repo": "GitHub 仓库",
|
|
3348
|
+
/** Settings footer link: star the repository. */
|
|
3349
|
+
"settings.star": "欢迎 Star ★",
|
|
3350
|
+
/** Settings footer link: file an issue. */
|
|
3351
|
+
"settings.issues": "提交 Issue",
|
|
3352
|
+
/** Settings footer link: the npm install channel. */
|
|
3353
|
+
"settings.npm": "npm 安装渠道",
|
|
3354
|
+
/** B4 update-check: toolbar button label + title/aria-label. */
|
|
3355
|
+
"update.check": "检查更新",
|
|
3356
|
+
/** B4 update-check: popover title. */
|
|
3357
|
+
"update.title": "更新检测",
|
|
3358
|
+
/** B4 update-check: installed-version row label. */
|
|
3359
|
+
"update.current": "当前版本",
|
|
3360
|
+
/** B4 update-check: newest-published-version row label. */
|
|
3361
|
+
"update.latest": "最新版本",
|
|
3362
|
+
/** B4 update-check: conclusion when the installed version is current. */
|
|
3363
|
+
"update.upToDate": "已是最新版本",
|
|
3364
|
+
/** B4 update-check: conclusion when a newer version exists. */
|
|
3365
|
+
"update.available": "发现新版本",
|
|
3366
|
+
/** B4 update-check: link text for the npm upgrade channel. */
|
|
3367
|
+
"update.goNpm": "去 npm 升级",
|
|
3368
|
+
/** B4 update-check: supported-host-lines metadata row label. */
|
|
3369
|
+
"update.hostLines": "已适配官方版本线",
|
|
3370
|
+
/** B4 update-check: in-flight state of the manual check button. */
|
|
3371
|
+
"update.checking": "检查中…",
|
|
3372
|
+
/** B4 update-check: failed state heading. */
|
|
3373
|
+
"update.failed": "检查失败",
|
|
3374
|
+
/** B4 update-check: retry action inside the failed state. */
|
|
3375
|
+
"update.retry": "重试"
|
|
1577
3376
|
};
|
|
1578
3377
|
const en = {
|
|
1579
3378
|
"jump.to": "Jump to message {n}",
|
|
@@ -1583,6 +3382,8 @@ window.__ModuleLoader__.load({
|
|
|
1583
3382
|
"search.placeholder": "Search message content",
|
|
1584
3383
|
"search.label": "Search messages",
|
|
1585
3384
|
"bookmark.filter": "Bookmarks only",
|
|
3385
|
+
"focus.on": "Focus mode",
|
|
3386
|
+
"focus.off": "Exit focus",
|
|
1586
3387
|
"bookmark.star": "Bookmark this message",
|
|
1587
3388
|
"search.clear": "Clear search",
|
|
1588
3389
|
"load.older": "Load older messages",
|
|
@@ -1604,7 +3405,39 @@ window.__ModuleLoader__.load({
|
|
|
1604
3405
|
"copy.message": "Copy message",
|
|
1605
3406
|
"fork.here": "Fork from here",
|
|
1606
3407
|
"collapse.turn": "Collapse turn",
|
|
1607
|
-
"expand.turn": "Expand turn"
|
|
3408
|
+
"expand.turn": "Expand turn",
|
|
3409
|
+
"list.open": "Open list",
|
|
3410
|
+
"list.close": "Close list",
|
|
3411
|
+
"list.label": "All prompts",
|
|
3412
|
+
"search.cross": "Cross-session search",
|
|
3413
|
+
"search.cross.open": "Open cross-session search",
|
|
3414
|
+
"search.cross.close": "Close cross-session search",
|
|
3415
|
+
"search.untitled": "(untitled)",
|
|
3416
|
+
"search.error": "Search failed, retry",
|
|
3417
|
+
"search.more": "Results truncated — refine your query",
|
|
3418
|
+
"toolbar.expand": "Expand toolbar",
|
|
3419
|
+
"toolbar.collapse": "Collapse toolbar",
|
|
3420
|
+
"toolbar.settings.open": "Open settings",
|
|
3421
|
+
"toolbar.settings.close": "Close settings",
|
|
3422
|
+
"settings.title": "Settings",
|
|
3423
|
+
"settings.pin": "Show outside collapse",
|
|
3424
|
+
"settings.reset": "Restore defaults",
|
|
3425
|
+
"settings.support": "Support us",
|
|
3426
|
+
"settings.repo": "GitHub repo",
|
|
3427
|
+
"settings.star": "Give us a Star ★",
|
|
3428
|
+
"settings.issues": "Report an Issue",
|
|
3429
|
+
"settings.npm": "Install via npm",
|
|
3430
|
+
"update.check": "Check updates",
|
|
3431
|
+
"update.title": "Update check",
|
|
3432
|
+
"update.current": "Current version",
|
|
3433
|
+
"update.latest": "Latest version",
|
|
3434
|
+
"update.upToDate": "You are up to date",
|
|
3435
|
+
"update.available": "Update available",
|
|
3436
|
+
"update.goNpm": "Upgrade on npm",
|
|
3437
|
+
"update.hostLines": "Supported official version lines",
|
|
3438
|
+
"update.checking": "Checking…",
|
|
3439
|
+
"update.failed": "Check failed",
|
|
3440
|
+
"update.retry": "Retry"
|
|
1608
3441
|
};
|
|
1609
3442
|
//#endregion
|
|
1610
3443
|
//#region src/client/index.ts
|
|
@@ -1641,7 +3474,9 @@ window.__ModuleLoader__.load({
|
|
|
1641
3474
|
locale: "dsh-milestone",
|
|
1642
3475
|
inject: (sessionId) => ({
|
|
1643
3476
|
loadOlder: createLoadOlder(ctx.sessions, sessionId),
|
|
1644
|
-
forkAt: createForkAt(ctx.sessions, sessionId)
|
|
3477
|
+
forkAt: createForkAt(ctx.sessions, sessionId),
|
|
3478
|
+
searchSessions: createSessionSearch(ctx.sessions),
|
|
3479
|
+
openSession: createOpenSession(ctx.sessions)
|
|
1645
3480
|
})
|
|
1646
3481
|
}, MilestoneRail));
|
|
1647
3482
|
}
|