dsh-milestone 0.6.0 → 0.6.2
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 +211 -120
- package/assets/demo.svg +39 -0
- package/assets/logo.svg +22 -0
- package/lib/client.js +2395 -447
- package/package.json +11 -10
package/lib/client.js
CHANGED
|
@@ -19,8 +19,105 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
//#endregion
|
|
22
|
+
//#region src/client/accent-utils.ts
|
|
23
|
+
/** True for a canonical 6-digit hex color (`#4d7cfd`, `#4D7CFD`). */
|
|
24
|
+
function isHexColor(value) {
|
|
25
|
+
return /^#[0-9a-fA-F]{6}$/.test(value);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a hex color into channels.
|
|
29
|
+
* @param hex - `#rrggbb` (case-insensitive).
|
|
30
|
+
* @returns the RGB channels, or null when `hex` is not a canonical hex color.
|
|
31
|
+
*/
|
|
32
|
+
function hexToRgb(hex) {
|
|
33
|
+
if (!isHexColor(hex)) return null;
|
|
34
|
+
const n = parseInt(hex.slice(1), 16);
|
|
35
|
+
return {
|
|
36
|
+
r: n >> 16 & 255,
|
|
37
|
+
g: n >> 8 & 255,
|
|
38
|
+
b: n & 255
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Format an alpha to the short decimal form the rail's inline styles use (0.55, 0.2). */
|
|
42
|
+
function formatAlpha(alpha) {
|
|
43
|
+
return Math.round(Math.max(0, Math.min(1, alpha)) * 100) / 100;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Color with alpha as a CSS `rgba(r, g, b, a)` string; alpha clamps to [0, 1].
|
|
47
|
+
* @returns the rgba() string, or null when `hex` is invalid.
|
|
48
|
+
*/
|
|
49
|
+
function rgbaString(hex, alpha) {
|
|
50
|
+
const rgb = hexToRgb(hex);
|
|
51
|
+
if (rgb === null) return null;
|
|
52
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${formatAlpha(alpha)})`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Linear mix of two hex colors in RGB space.
|
|
56
|
+
* @param a - start color (t = 0).
|
|
57
|
+
* @param b - end color (t = 1).
|
|
58
|
+
* @param t - mix factor, clamped to [0, 1].
|
|
59
|
+
* @returns the mixed `#rrggbb`, or null when either input is invalid.
|
|
60
|
+
*/
|
|
61
|
+
function mixHex(a, b, t) {
|
|
62
|
+
const ca = hexToRgb(a);
|
|
63
|
+
const cb = hexToRgb(b);
|
|
64
|
+
if (ca === null || cb === null) return null;
|
|
65
|
+
const k = Math.max(0, Math.min(1, t));
|
|
66
|
+
const channel = (from, to) => Math.round(from + (to - from) * k).toString(16).padStart(2, "0");
|
|
67
|
+
return `#${channel(ca.r, cb.r)}${channel(ca.g, cb.g)}${channel(ca.b, cb.b)}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Lighten a hex color toward white — the rail's "accent soft" text shade
|
|
71
|
+
* (t = 0 keeps the color, t = 1 is white).
|
|
72
|
+
*/
|
|
73
|
+
function lighten(hex, t) {
|
|
74
|
+
return mixHex(hex, "#ffffff", t);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convert a hex color to HSL. Double-checked against the standard RGB→HSL
|
|
78
|
+
* formulas; saturation/lightness are percentages, hue is degrees.
|
|
79
|
+
* @returns the HSL channels, or null when `hex` is invalid.
|
|
80
|
+
*/
|
|
81
|
+
function hexToHsl(hex) {
|
|
82
|
+
const rgb = hexToRgb(hex);
|
|
83
|
+
if (rgb === null) return null;
|
|
84
|
+
const r = rgb.r / 255;
|
|
85
|
+
const g = rgb.g / 255;
|
|
86
|
+
const b = rgb.b / 255;
|
|
87
|
+
const max = Math.max(r, g, b);
|
|
88
|
+
const min = Math.min(r, g, b);
|
|
89
|
+
const delta = max - min;
|
|
90
|
+
let h = 0;
|
|
91
|
+
if (delta !== 0) if (max === r) h = 60 * ((g - b) / delta % 6);
|
|
92
|
+
else if (max === g) h = 60 * ((b - r) / delta + 2);
|
|
93
|
+
else h = 60 * ((r - g) / delta + 4);
|
|
94
|
+
if (h < 0) h += 360;
|
|
95
|
+
const l = (max + min) / 2;
|
|
96
|
+
const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
|
|
97
|
+
return {
|
|
98
|
+
h,
|
|
99
|
+
s: s * 100,
|
|
100
|
+
l: l * 100
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
22
104
|
//#region src/client/badge-logic.ts
|
|
23
105
|
/**
|
|
106
|
+
* Pure turn-health badge derivation + styling for the milestone rail: given
|
|
107
|
+
* the harness snapshot signals, decide which (if any) colored glow a mark's
|
|
108
|
+
* dot should wear, plus the style tokens for that badge.
|
|
109
|
+
*
|
|
110
|
+
* Rendering contract (M-design change): a badge is NO LONGER a hard 2px ring
|
|
111
|
+
* (border) — it is a concentric "soft glow" made of three layered box-shadows
|
|
112
|
+
* (a crisp inner ring at 55% alpha, then two blurred blooms), and the
|
|
113
|
+
* running/awaiting pulse breathes opacity + shadow intensity instead of
|
|
114
|
+
* expanding a ring. The `data-badge` value and the semantic color of every
|
|
115
|
+
* kind are unchanged.
|
|
116
|
+
*
|
|
117
|
+
* All functions are side-effect free (no React, no DOM) so the rail component
|
|
118
|
+
* can consume them directly and tests can exercise them in isolation.
|
|
119
|
+
*/
|
|
120
|
+
/**
|
|
24
121
|
* Derive the badge for one mark.
|
|
25
122
|
*
|
|
26
123
|
* Precedence: error > max-tokens > retry > running > awaiting. Node-derived
|
|
@@ -65,13 +162,41 @@ window.__ModuleLoader__.load({
|
|
|
65
162
|
pulse: true
|
|
66
163
|
}
|
|
67
164
|
};
|
|
165
|
+
/** Build the layered soft-glow shadow for a badge color. */
|
|
166
|
+
function glowShadow(color) {
|
|
167
|
+
const layer = (alpha) => rgbaString(color, alpha) ?? "currentColor";
|
|
168
|
+
return [
|
|
169
|
+
`0 0 0 2px ${layer(.55)}`,
|
|
170
|
+
`0 0 8px 2px ${layer(.45)}`,
|
|
171
|
+
`0 0 16px 5px ${layer(.2)}`
|
|
172
|
+
].join(", ");
|
|
173
|
+
}
|
|
68
174
|
/**
|
|
69
175
|
* Style tokens for a badge kind.
|
|
70
176
|
* @param badge - the derived badge kind.
|
|
71
|
-
* @returns the
|
|
177
|
+
* @returns the glow color, whether the dot should pulse (breathing glow), and
|
|
178
|
+
* the static multi-layer box-shadow string for the badge span.
|
|
72
179
|
*/
|
|
73
180
|
function badgeRingStyle(badge) {
|
|
74
|
-
|
|
181
|
+
const base = RING_STYLES[badge];
|
|
182
|
+
return {
|
|
183
|
+
...base,
|
|
184
|
+
shadow: glowShadow(base.color)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Breathing-glow keyframes for one badge color. The rail injects this (via an
|
|
189
|
+
* inline <style>) only while a pulsing badge is on screen — the color is baked
|
|
190
|
+
* into the alphas, so the animation needs no runtime var lookups. Both stops
|
|
191
|
+
* keep the SAME three-layer shape (only alpha/blur breathe), so the glow never
|
|
192
|
+
* looks like the old expanding ring.
|
|
193
|
+
*/
|
|
194
|
+
function badgePulseCss(color) {
|
|
195
|
+
const layer = (alpha) => rgbaString(color, alpha) ?? "currentColor";
|
|
196
|
+
return `@keyframes milestone-badge-pulse {
|
|
197
|
+
0%, 100% { opacity: 0.95; box-shadow: ${`0 0 0 2px ${layer(.55)}, 0 0 8px 2px ${layer(.45)}, 0 0 16px 5px ${layer(.2)}`}; }
|
|
198
|
+
50% { opacity: 0.45; box-shadow: ${`0 0 0 2px ${layer(.35)}, 0 0 4px 1px ${layer(.25)}, 0 0 9px 3px ${layer(.12)}`}; }
|
|
199
|
+
}`;
|
|
75
200
|
}
|
|
76
201
|
//#endregion
|
|
77
202
|
//#region src/client/bookmark-logic.ts
|
|
@@ -349,6 +474,13 @@ window.__ModuleLoader__.load({
|
|
|
349
474
|
//#endregion
|
|
350
475
|
//#region src/client/rail-logic.ts
|
|
351
476
|
/**
|
|
477
|
+
* Pure rail logic for the milestone rail: full-text search matching, current-
|
|
478
|
+
* position highlight, match-cycle navigation, mark visual state, and dot color.
|
|
479
|
+
*
|
|
480
|
+
* All functions are side-effect free (no React, no DOM) so the rail component
|
|
481
|
+
* can consume them directly and tests can exercise them in isolation.
|
|
482
|
+
*/
|
|
483
|
+
/**
|
|
352
484
|
* Extract the FULL plain text of a ContentBlock[] payload: the `text` of every
|
|
353
485
|
* `{ type: 'text', text: string }` block, joined with a single space and
|
|
354
486
|
* trimmed. Unlike the rail's hover preview this is NOT truncated — callers use
|
|
@@ -427,14 +559,312 @@ window.__ModuleLoader__.load({
|
|
|
427
559
|
if (opts.hasQuery) return "dimmed";
|
|
428
560
|
return "normal";
|
|
429
561
|
}
|
|
562
|
+
/** The default accent (the classic milestone blue) — dotColor's fallback. */
|
|
563
|
+
const DEFAULT_DOT_ACCENT = "#4d7cfd";
|
|
430
564
|
/**
|
|
431
|
-
*
|
|
432
|
-
* (
|
|
565
|
+
* Accent-driven gradient dot color: hue/saturation come from the user's
|
|
566
|
+
* accent (settings 强调色), lightness walks 72% → 45% (newest/highest index
|
|
567
|
+
* deepest, oldest lightest — the original gradient shape). Invalid accents
|
|
568
|
+
* degrade to the default blue.
|
|
433
569
|
* @param index - dot position in the rail (0 = oldest).
|
|
434
570
|
* @param total - number of dots.
|
|
571
|
+
* @param accent - the accent hex (`#rrggbb`); defaults to the classic blue.
|
|
572
|
+
*/
|
|
573
|
+
function dotColor(index, total, accent = DEFAULT_DOT_ACCENT) {
|
|
574
|
+
const hsl = hexToHsl(accent) ?? hexToHsl("#4d7cfd");
|
|
575
|
+
const lightness = 72 - (total <= 1 ? 0 : index / (total - 1)) * 27;
|
|
576
|
+
return `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${lightness}%)`;
|
|
577
|
+
}
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/client/locales.ts
|
|
580
|
+
/**
|
|
581
|
+
* UI strings for the milestone rail, keyed flat (single-language-per-key,
|
|
582
|
+
* no nesting) so the later i18n threading stays a mechanical
|
|
583
|
+
* `value.replace('{name}', n)` substitution.
|
|
584
|
+
*
|
|
585
|
+
* `zh` is the source of truth and the key registry: it byte-matches the
|
|
586
|
+
* current hardcoded output of MilestoneRail / MilestoneRailTooltip /
|
|
587
|
+
* MilestoneRailSearch exactly (each `{n}`/`{m}`/`{name}` slot stands in for
|
|
588
|
+
* the interpolated number or label), so swapping in these templates is
|
|
589
|
+
* behavior-preserving. `en` is typed `Record<MilestoneKey, string>` so a
|
|
590
|
+
* missing English translation is a compile error, not a runtime miss.
|
|
591
|
+
*/
|
|
592
|
+
const zh = {
|
|
593
|
+
/** aria-label on each dot: `跳转到第 ${i + 1} 条消息`. */
|
|
594
|
+
"jump.to": "跳转到第 {n} 条消息",
|
|
595
|
+
/** Load-older coverage hint: `已显示 {marks.length} 条 · 还有更早`. */
|
|
596
|
+
"window.hint": "已显示 {n} 条 · 还有更早",
|
|
597
|
+
/** Hover turn badge: `第 ${mark.turn} 轮`. */
|
|
598
|
+
"turn.label": "第 {n} 轮",
|
|
599
|
+
/** Hover position: `第 {hover.index + 1} / {hover.total} 条`. */
|
|
600
|
+
"pos.of": "第 {n} / {m} 条",
|
|
601
|
+
/** Search input placeholder. */
|
|
602
|
+
"search.placeholder": "搜索消息内容",
|
|
603
|
+
/** aria-label on the search toggle button and the search input. */
|
|
604
|
+
"search.label": "搜索消息",
|
|
605
|
+
/** aria-label on the bookmarks-only filter toggle. */
|
|
606
|
+
"bookmark.filter": "只看收藏",
|
|
607
|
+
/** aria-label + title on the focus-mode toggle when focus is OFF (arm it). */
|
|
608
|
+
"focus.on": "聚焦模式",
|
|
609
|
+
/** aria-label + title on the focus-mode toggle when focus is ON (disarm it). */
|
|
610
|
+
"focus.off": "退出聚焦",
|
|
611
|
+
/** aria-label on the hover tooltip star toggle. */
|
|
612
|
+
"bookmark.star": "收藏此消息",
|
|
613
|
+
/** aria-label on the search clear button. */
|
|
614
|
+
"search.clear": "清空搜索",
|
|
615
|
+
/** title + aria-label on the load-older `···` button. */
|
|
616
|
+
"load.older": "加载更早消息",
|
|
617
|
+
/** aria-label on the rail root. */
|
|
618
|
+
"rail.label": "会话里程碑",
|
|
619
|
+
/** aria-label on the dot list. */
|
|
620
|
+
"rail.list": "会话里程碑列表",
|
|
621
|
+
/** Hover preview fallback for empty message text. */
|
|
622
|
+
"no.text": "(无文本)",
|
|
623
|
+
/** Relative time: `< 60s`. */
|
|
624
|
+
"time.justNow": "刚刚",
|
|
625
|
+
/** Relative time: `< 1h`. */
|
|
626
|
+
"time.minutes": "{n} 分钟前",
|
|
627
|
+
/** Relative time: `< 1d`. */
|
|
628
|
+
"time.hours": "{n} 小时前",
|
|
629
|
+
/** Relative time: `>= 1d`. */
|
|
630
|
+
"time.days": "{n} 天前",
|
|
631
|
+
/** Hover duration: `用时 {durationLabel}`. */
|
|
632
|
+
"duration.label": "用时 {name}",
|
|
633
|
+
/** Hover TTFT: `首字 {ttftLabel}`. */
|
|
634
|
+
"ttft.label": "首字 {name}",
|
|
635
|
+
/** TurnEndReason `completed`. */
|
|
636
|
+
"reason.completed": "已完成",
|
|
637
|
+
/** TurnEndReason `aborted`. */
|
|
638
|
+
"reason.aborted": "已中止",
|
|
639
|
+
/** TurnEndReason `error`. */
|
|
640
|
+
"reason.error": "出错",
|
|
641
|
+
/** TurnEndReason `max-tokens`. */
|
|
642
|
+
"reason.maxTokens": "达到上限",
|
|
643
|
+
/** TurnEndReason `interrupted`. */
|
|
644
|
+
"reason.interrupted": "已中断",
|
|
645
|
+
/** TurnEndReason `blocked`. */
|
|
646
|
+
"reason.blocked": "已阻塞",
|
|
647
|
+
/** Copy-message tooltip action. */
|
|
648
|
+
"copy.message": "复制消息",
|
|
649
|
+
/** Fork-from-here tooltip action. */
|
|
650
|
+
"fork.here": "从此处 fork",
|
|
651
|
+
/** Collapse-turn tooltip action. */
|
|
652
|
+
"collapse.turn": "折叠此轮",
|
|
653
|
+
/** Expand-turn tooltip action. */
|
|
654
|
+
"expand.turn": "展开此轮",
|
|
655
|
+
/** aria-label + title on the milestone-list toggle when the panel is CLOSED. */
|
|
656
|
+
"list.open": "打开列表",
|
|
657
|
+
/** aria-label + title on the milestone-list toggle when the panel is OPEN. */
|
|
658
|
+
"list.close": "收起列表",
|
|
659
|
+
/** Header title of the all-prompts list panel. */
|
|
660
|
+
"list.label": "全部提问",
|
|
661
|
+
/** Header title + input placeholder of the cross-session search panel. */
|
|
662
|
+
"search.cross": "跨会话搜索",
|
|
663
|
+
/** aria-label + title on the cross-session search toggle when the panel is CLOSED. */
|
|
664
|
+
"search.cross.open": "打开跨会话搜索",
|
|
665
|
+
/** aria-label + title on the cross-session search toggle when the panel is OPEN. */
|
|
666
|
+
"search.cross.close": "收起跨会话搜索",
|
|
667
|
+
/** Cross-session result row title fallback for sessions with no display title. */
|
|
668
|
+
"search.untitled": "(无标题)",
|
|
669
|
+
/** Cross-session search failure notice. */
|
|
670
|
+
"search.error": "搜索失败,请重试",
|
|
671
|
+
/** Cross-session search footer hint when the harness capped the result list. */
|
|
672
|
+
"search.more": "结果已截断,请细化关键词",
|
|
673
|
+
/** aria-label on the toolbar expand arrow while the toolbar is COLLAPSED (expand it). */
|
|
674
|
+
"toolbar.expand": "展开工具栏",
|
|
675
|
+
/** aria-label on the toolbar expand arrow while the toolbar is EXPANDED (collapse it). */
|
|
676
|
+
"toolbar.collapse": "收起工具栏",
|
|
677
|
+
/** aria-label on the toolbar settings gear while the settings menu is CLOSED. */
|
|
678
|
+
"toolbar.settings.open": "打开设置",
|
|
679
|
+
/** aria-label on the toolbar settings gear while the settings menu is OPEN. */
|
|
680
|
+
"toolbar.settings.close": "关闭设置",
|
|
681
|
+
/** Header title of the toolbar settings menu. */
|
|
682
|
+
"settings.title": "设置",
|
|
683
|
+
/** Per-feature toggle label inside the settings menu: keep visible while collapsed. */
|
|
684
|
+
"settings.pin": "在折叠外显示",
|
|
685
|
+
/** Settings action that clears every pinned feature. */
|
|
686
|
+
"settings.reset": "恢复默认",
|
|
687
|
+
/** Settings footer heading above the project links. */
|
|
688
|
+
"settings.support": "支持我们",
|
|
689
|
+
/** Settings footer link: the GitHub repository. */
|
|
690
|
+
"settings.repo": "GitHub 仓库",
|
|
691
|
+
/** Settings footer link: star the repository. */
|
|
692
|
+
"settings.star": "欢迎 Star ★",
|
|
693
|
+
/** Settings footer link: file an issue. */
|
|
694
|
+
"settings.issues": "提交 Issue",
|
|
695
|
+
/** Settings footer link: the npm install channel. */
|
|
696
|
+
"settings.npm": "npm 安装渠道",
|
|
697
|
+
/** Settings: feature-row name of the settings key itself (registry row). */
|
|
698
|
+
"settings.label": "设置",
|
|
699
|
+
/** aria-label on the settings modal close button. */
|
|
700
|
+
"settings.close": "关闭",
|
|
701
|
+
/** Settings modal: section heading for the feature pin rows. */
|
|
702
|
+
"settings.section.features": "功能与快捷区",
|
|
703
|
+
/** Settings modal: section heading for the personalization controls. */
|
|
704
|
+
"settings.section.personal": "个性化",
|
|
705
|
+
/** Settings: hover description — in-rail search. */
|
|
706
|
+
"settings.desc.search": "按完整消息内容过滤并跳转到对应消息",
|
|
707
|
+
/** Settings: hover description — all-prompts list. */
|
|
708
|
+
"settings.desc.list": "本会话全部提问一览",
|
|
709
|
+
/** Settings: hover description — cross-session search. */
|
|
710
|
+
"settings.desc.sessionSearch": "跨会话搜索所有会话",
|
|
711
|
+
/** Settings: hover description — bookmarks filter. */
|
|
712
|
+
"settings.desc.bookmarks": "只显示已收藏的消息",
|
|
713
|
+
/** Settings: hover description — focus mode. */
|
|
714
|
+
"settings.desc.focus": "淡化 AI 思考块,阅读更清爽",
|
|
715
|
+
/** Settings: hover description — update check. */
|
|
716
|
+
"settings.desc.updateCheck": "检查 npm 是否有新版本",
|
|
717
|
+
/** Settings: hover description — the settings key itself. */
|
|
718
|
+
"settings.desc.settings": "自定义工具栏与外观",
|
|
719
|
+
/** Settings personalization: accent color row label. */
|
|
720
|
+
"settings.accent": "强调色",
|
|
721
|
+
/** Settings personalization: custom color swatch label. */
|
|
722
|
+
"settings.custom": "自定义",
|
|
723
|
+
/** Settings personalization: icon/dot size slider label. */
|
|
724
|
+
"settings.iconSize": "图标 / 圆点大小",
|
|
725
|
+
/** Settings personalization: edge-distance slider label. */
|
|
726
|
+
"settings.inset": "距侧边距离",
|
|
727
|
+
/** Settings personalization: rail side row label. */
|
|
728
|
+
"settings.side": "位置",
|
|
729
|
+
/** Settings personalization: side radio — hug the left edge. */
|
|
730
|
+
"settings.side.left": "左侧",
|
|
731
|
+
/** Settings personalization: side radio — hug the right edge. */
|
|
732
|
+
"settings.side.right": "右侧",
|
|
733
|
+
/** B4 update-check: toolbar button label + title/aria-label. */
|
|
734
|
+
"update.check": "检查更新",
|
|
735
|
+
/** B4 update-check: popover title. */
|
|
736
|
+
"update.title": "更新检测",
|
|
737
|
+
/** B4 update-check: installed-version row label. */
|
|
738
|
+
"update.current": "当前版本",
|
|
739
|
+
/** B4 update-check: newest-published-version row label. */
|
|
740
|
+
"update.latest": "最新版本",
|
|
741
|
+
/** B4 update-check: conclusion when the installed version is current. */
|
|
742
|
+
"update.upToDate": "已是最新版本",
|
|
743
|
+
/** B4 update-check: conclusion when a newer version exists. */
|
|
744
|
+
"update.available": "发现新版本",
|
|
745
|
+
/** B4 update-check: link text for the npm upgrade channel. */
|
|
746
|
+
"update.goNpm": "去 npm 升级",
|
|
747
|
+
/** B4 update-check: supported-host-lines metadata row label. */
|
|
748
|
+
"update.hostLines": "已适配官方版本线",
|
|
749
|
+
/** B4 update-check: in-flight state of the manual check button. */
|
|
750
|
+
"update.checking": "检查中…",
|
|
751
|
+
/** B4 update-check: failed state heading. */
|
|
752
|
+
"update.failed": "检查失败",
|
|
753
|
+
/** B4 update-check: retry action inside the failed state. */
|
|
754
|
+
"update.retry": "重试",
|
|
755
|
+
/** Settings modal section title: language. */
|
|
756
|
+
"settings.language": "语言",
|
|
757
|
+
/** Language option: follow the harness UI language. */
|
|
758
|
+
"settings.lang.system": "跟随系统",
|
|
759
|
+
/** Language option: force Chinese copy. */
|
|
760
|
+
"settings.lang.zh": "中文",
|
|
761
|
+
/** Language option: force English copy. */
|
|
762
|
+
"settings.lang.en": "English"
|
|
763
|
+
};
|
|
764
|
+
const en = {
|
|
765
|
+
"jump.to": "Jump to message {n}",
|
|
766
|
+
"window.hint": "Showing {n} messages · more below",
|
|
767
|
+
"turn.label": "Turn {n}",
|
|
768
|
+
"pos.of": "Message {n} of {m}",
|
|
769
|
+
"search.placeholder": "Search message content",
|
|
770
|
+
"search.label": "Search messages",
|
|
771
|
+
"bookmark.filter": "Bookmarks only",
|
|
772
|
+
"focus.on": "Focus mode",
|
|
773
|
+
"focus.off": "Exit focus",
|
|
774
|
+
"bookmark.star": "Bookmark this message",
|
|
775
|
+
"search.clear": "Clear search",
|
|
776
|
+
"load.older": "Load older messages",
|
|
777
|
+
"rail.label": "Session milestones",
|
|
778
|
+
"rail.list": "Session milestone list",
|
|
779
|
+
"no.text": "(no text)",
|
|
780
|
+
"time.justNow": "Just now",
|
|
781
|
+
"time.minutes": "{n} minutes ago",
|
|
782
|
+
"time.hours": "{n} hours ago",
|
|
783
|
+
"time.days": "{n} days ago",
|
|
784
|
+
"duration.label": "Duration {name}",
|
|
785
|
+
"ttft.label": "First token {name}",
|
|
786
|
+
"reason.completed": "Completed",
|
|
787
|
+
"reason.aborted": "Aborted",
|
|
788
|
+
"reason.error": "Error",
|
|
789
|
+
"reason.maxTokens": "Max tokens reached",
|
|
790
|
+
"reason.interrupted": "Interrupted",
|
|
791
|
+
"reason.blocked": "Blocked",
|
|
792
|
+
"copy.message": "Copy message",
|
|
793
|
+
"fork.here": "Fork from here",
|
|
794
|
+
"collapse.turn": "Collapse turn",
|
|
795
|
+
"expand.turn": "Expand turn",
|
|
796
|
+
"list.open": "Open list",
|
|
797
|
+
"list.close": "Close list",
|
|
798
|
+
"list.label": "All prompts",
|
|
799
|
+
"search.cross": "Cross-session search",
|
|
800
|
+
"search.cross.open": "Open cross-session search",
|
|
801
|
+
"search.cross.close": "Close cross-session search",
|
|
802
|
+
"search.untitled": "(untitled)",
|
|
803
|
+
"search.error": "Search failed, retry",
|
|
804
|
+
"search.more": "Results truncated — refine your query",
|
|
805
|
+
"toolbar.expand": "Expand toolbar",
|
|
806
|
+
"toolbar.collapse": "Collapse toolbar",
|
|
807
|
+
"toolbar.settings.open": "Open settings",
|
|
808
|
+
"toolbar.settings.close": "Close settings",
|
|
809
|
+
"settings.title": "Settings",
|
|
810
|
+
"settings.pin": "Show outside collapse",
|
|
811
|
+
"settings.reset": "Restore defaults",
|
|
812
|
+
"settings.support": "Support us",
|
|
813
|
+
"settings.repo": "GitHub repo",
|
|
814
|
+
"settings.star": "Give us a Star ★",
|
|
815
|
+
"settings.issues": "Report an Issue",
|
|
816
|
+
"settings.npm": "Install via npm",
|
|
817
|
+
"settings.label": "Settings",
|
|
818
|
+
"settings.close": "Close",
|
|
819
|
+
"settings.section.features": "Features & Shortcuts",
|
|
820
|
+
"settings.section.personal": "Personalization",
|
|
821
|
+
"settings.desc.search": "Filter by full message text and jump to the match",
|
|
822
|
+
"settings.desc.list": "Overview of every prompt in this session",
|
|
823
|
+
"settings.desc.sessionSearch": "Search across all sessions",
|
|
824
|
+
"settings.desc.bookmarks": "Show bookmarked messages only",
|
|
825
|
+
"settings.desc.focus": "Dim AI thinking blocks for a cleaner read",
|
|
826
|
+
"settings.desc.updateCheck": "Check npm for a newer release",
|
|
827
|
+
"settings.desc.settings": "Customize the toolbar and appearance",
|
|
828
|
+
"settings.accent": "Accent color",
|
|
829
|
+
"settings.custom": "Custom",
|
|
830
|
+
"settings.iconSize": "Icon / dot size",
|
|
831
|
+
"settings.inset": "Distance from the edge",
|
|
832
|
+
"settings.side": "Position",
|
|
833
|
+
"settings.side.left": "Left",
|
|
834
|
+
"settings.side.right": "Right",
|
|
835
|
+
"update.check": "Check updates",
|
|
836
|
+
"update.title": "Update check",
|
|
837
|
+
"update.current": "Current version",
|
|
838
|
+
"update.latest": "Latest version",
|
|
839
|
+
"update.upToDate": "You are up to date",
|
|
840
|
+
"update.available": "Update available",
|
|
841
|
+
"update.goNpm": "Upgrade on npm",
|
|
842
|
+
"update.hostLines": "Supported official version lines",
|
|
843
|
+
"update.checking": "Checking…",
|
|
844
|
+
"update.failed": "Check failed",
|
|
845
|
+
"update.retry": "Retry",
|
|
846
|
+
"settings.language": "Language",
|
|
847
|
+
"settings.lang.system": "Follow system",
|
|
848
|
+
"settings.lang.zh": "Chinese",
|
|
849
|
+
"settings.lang.en": "English"
|
|
850
|
+
};
|
|
851
|
+
/**
|
|
852
|
+
* Interpolate `{name}` placeholders with params, matching the harness t seat's
|
|
853
|
+
* substitution shape; an unknown parameter leaves the placeholder verbatim.
|
|
854
|
+
*/
|
|
855
|
+
function interpolate(template, params) {
|
|
856
|
+
if (params === void 0) return template;
|
|
857
|
+
return template.replace(/\{([A-Za-z][A-Za-z0-9]*)\}/g, (slot, name) => name in params ? String(params[name]) : slot);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Dictionary-backed translate for the forced-language override (locale prefs
|
|
861
|
+
* 'zh' / 'en'): resolves a key against the plugin's own dictionaries with
|
|
862
|
+
* placeholder interpolation; unknown keys pass through unchanged (same
|
|
863
|
+
* degradation as the harness seat).
|
|
435
864
|
*/
|
|
436
|
-
function
|
|
437
|
-
|
|
865
|
+
function translateDict(dict, key, params) {
|
|
866
|
+
const template = dict[key];
|
|
867
|
+
return template === void 0 ? key : interpolate(template, params);
|
|
438
868
|
}
|
|
439
869
|
//#endregion
|
|
440
870
|
//#region src/client/turn-group-logic.ts
|
|
@@ -503,13 +933,110 @@ window.__ModuleLoader__.load({
|
|
|
503
933
|
};
|
|
504
934
|
}
|
|
505
935
|
//#endregion
|
|
936
|
+
//#region src/client/useOutsideDismiss.ts
|
|
937
|
+
/**
|
|
938
|
+
* useOutsideDismiss: the shared "click outside to dismiss" contract behind the
|
|
939
|
+
* rail's floating panels (in-rail search / all-prompts list / cross-session
|
|
940
|
+
* search). While a panel is open, a pointerdown anywhere OUTSIDE it calls the
|
|
941
|
+
* caller's close handler; a pointerdown inside the panel (or on an excluded
|
|
942
|
+
* element) is left untouched.
|
|
943
|
+
*
|
|
944
|
+
* Event choice — window `pointerdown`:
|
|
945
|
+
* - `pointerdown` unifies mouse / touch / pen, so dismissal works for every
|
|
946
|
+
* input the harness surfaces; a `mousedown`-only listener would miss
|
|
947
|
+
* touch taps entirely.
|
|
948
|
+
* - Closing on the DOWN side of the gesture feels instant — the panel is
|
|
949
|
+
* gone before the pointer is released, which is the expected behaviour
|
|
950
|
+
* for fixed floating layers.
|
|
951
|
+
*
|
|
952
|
+
* Lifecycle: the listener is attached ONLY while `open` and removed on close
|
|
953
|
+
* / unmount, so a closed panel never intercepts events and no listener leaks.
|
|
954
|
+
*
|
|
955
|
+
* Opening-gesture guard: hooks arm their listener in an effect, which React
|
|
956
|
+
* runs AFTER the toggle's pointerdown/click that opened the panel — that
|
|
957
|
+
* gesture can therefore never reach the listener by construction. As
|
|
958
|
+
* defense-in-depth the hook also records its arming time and drops any
|
|
959
|
+
* pointerdown whose `timeStamp` STRICTLY predates it (an event still in
|
|
960
|
+
* flight from the opening gesture). Same-tick and later events pass through,
|
|
961
|
+
* and synthetic test events with `timeStamp === 0` (which carry no real
|
|
962
|
+
* timestamp) are kept — the guard never swallows a legitimate dispatch.
|
|
963
|
+
*
|
|
964
|
+
* `options.exclude` names targets the caller keeps for its own handler — the
|
|
965
|
+
* panel's own toggle button is the canonical case: its click owns the
|
|
966
|
+
* open/close flip, so a pointerdown on it must NOT also dismiss through the
|
|
967
|
+
* hook, or clicking an armed toggle would close on pointerdown and re-open on
|
|
968
|
+
* click.
|
|
969
|
+
*/
|
|
970
|
+
/**
|
|
971
|
+
* True when `target` is (or sits inside) an element matching `selector`.
|
|
972
|
+
* Panels use it to exclude their own rail-top toggle from dismissal (the
|
|
973
|
+
* toggle's data attribute is the established rail DOM contract, so the
|
|
974
|
+
* exclusion is just another access to it).
|
|
975
|
+
*/
|
|
976
|
+
function outsideDismissMatches(target, selector) {
|
|
977
|
+
return target instanceof Element && target.closest(selector) !== null;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* @param panelRef - the floating panel's root element (only mounted while open).
|
|
981
|
+
* @param open - whether the panel is open; the window listener arms only when true.
|
|
982
|
+
* @param onClose - called once per outside pointerdown. `undefined` keeps the
|
|
983
|
+
* hook inert — used while a call site still has no close path wired.
|
|
984
|
+
* @param options - optional exclusion predicate (see {@link OutsideDismissOptions}).
|
|
985
|
+
*/
|
|
986
|
+
function useOutsideDismiss(panelRef, open, onClose, options) {
|
|
987
|
+
const onCloseRef = (0, react.useRef)(onClose);
|
|
988
|
+
const excludeRef = (0, react.useRef)(options?.exclude);
|
|
989
|
+
(0, react.useEffect)(() => {
|
|
990
|
+
onCloseRef.current = onClose;
|
|
991
|
+
excludeRef.current = options?.exclude;
|
|
992
|
+
});
|
|
993
|
+
(0, react.useEffect)(() => {
|
|
994
|
+
if (!open) return;
|
|
995
|
+
const armedAt = performance.now();
|
|
996
|
+
const onPointerDown = (e) => {
|
|
997
|
+
const onClose = onCloseRef.current;
|
|
998
|
+
if (onClose === void 0) return;
|
|
999
|
+
if (e.timeStamp > 0 && e.timeStamp < armedAt) return;
|
|
1000
|
+
if (excludeRef.current?.(e.target)) return;
|
|
1001
|
+
const panel = panelRef.current;
|
|
1002
|
+
if (panel !== null && e.target instanceof Node && panel.contains(e.target)) return;
|
|
1003
|
+
onClose();
|
|
1004
|
+
};
|
|
1005
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
1006
|
+
return () => window.removeEventListener("pointerdown", onPointerDown);
|
|
1007
|
+
}, [open, panelRef]);
|
|
1008
|
+
}
|
|
1009
|
+
//#endregion
|
|
506
1010
|
//#region src/client/MilestoneRailSearch.tsx
|
|
1011
|
+
/**
|
|
1012
|
+
* RailSearchUi: the in-rail search chrome (F1) — the magnifier toggle pinned
|
|
1013
|
+
* to the rail's top and the compact search panel to its left (input, match
|
|
1014
|
+
* counter, clear button).
|
|
1015
|
+
*
|
|
1016
|
+
* Pure presentation: it owns no state. MilestoneRail holds the search state
|
|
1017
|
+
* and handlers and feeds them in as props, so the search lifecycle (query,
|
|
1018
|
+
* match cycle, escape semantics) stays component-local in the rail. Splitting
|
|
1019
|
+
* the chrome into its own file keeps the rail component under the size
|
|
1020
|
+
* ceiling while the two still render one DOM contract
|
|
1021
|
+
* (`data-search-toggle` / `data-rail-search` / `data-match-count` /
|
|
1022
|
+
* `data-search-clear`).
|
|
1023
|
+
*
|
|
1024
|
+
* Outside dismissal: while the panel is open, a pointerdown anywhere outside
|
|
1025
|
+
* it closes it (shared useOutsideDismiss hook). The toggle's own click keeps
|
|
1026
|
+
* its flip semantics — a pointerdown on `[data-search-toggle]` is excluded
|
|
1027
|
+
* from the hook and left to the rail's onToggle. Without a dedicated onClose
|
|
1028
|
+
* prop the fallback is the toggle-off path (query retained, same as clicking
|
|
1029
|
+
* the toggle), matching the rail's current call site; a dedicated onClose
|
|
1030
|
+
* (clearSearch-equivalent) takes precedence once MilestoneRail feeds one.
|
|
1031
|
+
*/
|
|
507
1032
|
/** Dot diameter (px) — matches the rail's DOT_HIT so the toggle aligns. */
|
|
508
1033
|
const DOT_HIT$1 = 28;
|
|
509
1034
|
/**
|
|
510
1035
|
* @param props - the search state slice plus the rail's event handlers.
|
|
511
1036
|
*/
|
|
512
|
-
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear, t }) {
|
|
1037
|
+
function RailSearchUi({ panelTop, panelRight, query, panelOpen, matches, total, onToggle, onQueryChange, onSearchKeyDown, onClear, onClose, t }) {
|
|
1038
|
+
const panelRef = (0, react.useRef)(null);
|
|
1039
|
+
useOutsideDismiss(panelRef, panelOpen, onClose ?? (() => onToggle()), { exclude: (target) => outsideDismissMatches(target, "[data-search-toggle]") });
|
|
513
1040
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
514
1041
|
type: "button",
|
|
515
1042
|
"data-search-toggle": true,
|
|
@@ -545,6 +1072,7 @@ window.__ModuleLoader__.load({
|
|
|
545
1072
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m21 21-4.3-4.3" })]
|
|
546
1073
|
})
|
|
547
1074
|
}), panelOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1075
|
+
ref: panelRef,
|
|
548
1076
|
style: {
|
|
549
1077
|
position: "fixed",
|
|
550
1078
|
top: panelTop,
|
|
@@ -631,10 +1159,33 @@ window.__ModuleLoader__.load({
|
|
|
631
1159
|
//#endregion
|
|
632
1160
|
//#region src/client/MilestoneListPanel.tsx
|
|
633
1161
|
/**
|
|
1162
|
+
* MilestoneListPanel: the expandable all-prompts panel (P3) — the compact
|
|
1163
|
+
* list chrome pinned to the rail's top that enumerates EVERY user-prompt
|
|
1164
|
+
* milestone (序号 + turn + preview), independent of the search/bookmarks
|
|
1165
|
+
* filters. Clicking an entry jumps to that message through the rail's own
|
|
1166
|
+
* `jump` handler (the same path the dots use).
|
|
1167
|
+
*
|
|
1168
|
+
* Pure presentation: it owns no state. MilestoneRail holds the `listOpen`
|
|
1169
|
+
* boolean (the toggle and the Escape/close semantics live there) and feeds
|
|
1170
|
+
* the panel its anchor, the full marks array, and the jump handler as props
|
|
1171
|
+
* — same split as RailSearchUi. Renders one DOM contract
|
|
1172
|
+
* (`data-milestone-list` root / `data-list-item` rows with `data-jump-key`).
|
|
1173
|
+
*
|
|
1174
|
+
* Outside dismissal: while the panel is mounted (it only renders while open),
|
|
1175
|
+
* a pointerdown anywhere outside it calls the rail-fed `onClose` (shared
|
|
1176
|
+
* useOutsideDismiss hook; the toggle's own click keeps its flip semantics
|
|
1177
|
+
* through a `[data-list-toggle]` exclusion). MilestoneRail.tsx does NOT pass
|
|
1178
|
+
* onClose in the current tree (its owner is wiring it separately) — until
|
|
1179
|
+
* then the hook is inert and the panel keeps its existing behaviour.
|
|
1180
|
+
*/
|
|
1181
|
+
/**
|
|
634
1182
|
* @param props - the panel anchor, the full marks array, and the rail's jump handler.
|
|
635
1183
|
*/
|
|
636
|
-
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, t }) {
|
|
1184
|
+
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, onClose, t }) {
|
|
1185
|
+
const panelRef = (0, react.useRef)(null);
|
|
1186
|
+
useOutsideDismiss(panelRef, true, onClose, { exclude: (target) => outsideDismissMatches(target, "[data-list-toggle]") });
|
|
637
1187
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1188
|
+
ref: panelRef,
|
|
638
1189
|
"data-milestone-list": true,
|
|
639
1190
|
style: {
|
|
640
1191
|
position: "fixed",
|
|
@@ -944,6 +1495,11 @@ window.__ModuleLoader__.load({
|
|
|
944
1495
|
* (`data-session-search` root / `data-session-search-input` /
|
|
945
1496
|
* `data-session-search-result` rows / `data-session-search-error` /
|
|
946
1497
|
* `data-session-search-more`).
|
|
1498
|
+
*
|
|
1499
|
+
* Outside dismissal: while the panel is mounted (it only renders while open),
|
|
1500
|
+
* a pointerdown anywhere outside it calls the rail-fed `onClose` (shared
|
|
1501
|
+
* useOutsideDismiss hook; the toggle's own click keeps its flip semantics
|
|
1502
|
+
* through a `[data-session-search-toggle]` exclusion).
|
|
947
1503
|
*/
|
|
948
1504
|
/** Debounce window for the cross-session query (ms). */
|
|
949
1505
|
const SEARCH_DEBOUNCE_MS = 250;
|
|
@@ -955,6 +1511,8 @@ window.__ModuleLoader__.load({
|
|
|
955
1511
|
const [status, setStatus] = (0, react.useState)("idle");
|
|
956
1512
|
const [hits, setHits] = (0, react.useState)([]);
|
|
957
1513
|
const [hasMore, setHasMore] = (0, react.useState)(false);
|
|
1514
|
+
const panelRef = (0, react.useRef)(null);
|
|
1515
|
+
useOutsideDismiss(panelRef, true, onClose, { exclude: (target) => outsideDismissMatches(target, "[data-session-search-toggle]") });
|
|
958
1516
|
(0, react.useEffect)(() => {
|
|
959
1517
|
const trimmed = query.trim();
|
|
960
1518
|
if (trimmed === "") {
|
|
@@ -983,6 +1541,7 @@ window.__ModuleLoader__.load({
|
|
|
983
1541
|
if (e.key === "Escape") onClose();
|
|
984
1542
|
};
|
|
985
1543
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1544
|
+
ref: panelRef,
|
|
986
1545
|
"data-session-search": true,
|
|
987
1546
|
style: {
|
|
988
1547
|
position: "fixed",
|
|
@@ -1169,89 +1728,609 @@ window.__ModuleLoader__.load({
|
|
|
1169
1728
|
return current;
|
|
1170
1729
|
}
|
|
1171
1730
|
//#endregion
|
|
1172
|
-
//#region src/client/
|
|
1731
|
+
//#region src/client/toolbar-prefs.ts
|
|
1173
1732
|
/**
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
* the viewport; hovering a dot shows rich metadata (time, turn, duration, end
|
|
1179
|
-
* reason, TTFT, tokens/sec) and clicking jumps the chat to that message.
|
|
1733
|
+
* toolbar-prefs: the persistence layer for the milestone rail's toolbar
|
|
1734
|
+
* personalization — WHICH function keys stay visible outside the collapse
|
|
1735
|
+
* (pinned) plus the settings-module appearance prefs (accent color, icon/dot
|
|
1736
|
+
* size, distance from the rail's screen edge, and rail side).
|
|
1180
1737
|
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
* - chat.timeline.turns.get(turn) -> turn start/end time, status, reason,
|
|
1184
|
-
* and the ui-conversation 'turn-tail'
|
|
1185
|
-
* location data (ttftMs/tokensPerSecond)
|
|
1738
|
+
* Storage contract: one localStorage key (`dsh-milestone.toolbar`) holding a
|
|
1739
|
+
* JSON object:
|
|
1186
1740
|
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1741
|
+
* { "pinned": string[], "accent": "#rrggbb", "iconSize": number,
|
|
1742
|
+
* "inset": number, "side": "left" | "right", "locale": "system"|"zh"|"en" }
|
|
1189
1743
|
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
* jumps to it, Escape clears and closes. Matching runs over the FULL message
|
|
1194
|
-
* text (`text` from rail-logic.extractText), not the truncated hover preview.
|
|
1744
|
+
* Backward compatibility: the pre-personalization blob `{ "pinned": string[] }`
|
|
1745
|
+
* (and an entirely absent value) parses to the DEFAULT prefs with the new
|
|
1746
|
+
* fields at their defaults — old users keep their pins untouched.
|
|
1195
1747
|
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1748
|
+
* All reads are sanitized per field:
|
|
1749
|
+
* - `pinned`: whitelisted ids only (`TOOLBAR_PIN_IDS`), duplicates dropped,
|
|
1750
|
+
* first-seen (pin) order preserved;
|
|
1751
|
+
* - `accent`: a canonical `#rrggbb` hex, lowercased; anything else falls
|
|
1752
|
+
* back to the default blue;
|
|
1753
|
+
* - `iconSize` / `inset`: finite numbers snapped to the slider step
|
|
1754
|
+
* (even values) and clamped to the slider range;
|
|
1755
|
+
* - `side`: exactly `'left'` or `'right'`.
|
|
1199
1756
|
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
* `loadingOlder`), and a compact hint to the rail's left states how many
|
|
1204
|
-
* messages the current window covers.
|
|
1757
|
+
* The whitelist lives HERE (not in MilestoneRail) so the pure functions stay
|
|
1758
|
+
* dependency-free and unit-testable; MilestoneRail's feature registry keys
|
|
1759
|
+
* itself against the same `ToolbarPinId` type, so id drift is a compile error.
|
|
1205
1760
|
*/
|
|
1206
|
-
/**
|
|
1207
|
-
const
|
|
1208
|
-
const PREVIEW_LENGTH = 80;
|
|
1209
|
-
/** Stable no-bookmarks fallback for render paths without the store seat. */
|
|
1210
|
-
const NO_BOOKMARKS = [];
|
|
1211
|
-
/** Stable no-kinds fallback for marks whose turn carries no badge nodes. */
|
|
1212
|
-
const NO_KINDS = [];
|
|
1761
|
+
/** The single localStorage key holding the toolbar preference blob. */
|
|
1762
|
+
const TOOLBAR_PREFS_KEY = "dsh-milestone.toolbar";
|
|
1213
1763
|
/**
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1764
|
+
* Canonical function-key ids that may be pinned outside the collapse, in
|
|
1765
|
+
* render order. `settings` is a REGULAR feature since the B-design move: the
|
|
1766
|
+
* gear left the always-visible chrome and now sits at the end of the expanded
|
|
1767
|
+
* feature queue (default unpinned). Adding a feature here (plus its registry
|
|
1768
|
+
* entry in MilestoneRail) is the whole "pin it" extension point.
|
|
1218
1769
|
*/
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1770
|
+
const TOOLBAR_PIN_IDS = [
|
|
1771
|
+
"search",
|
|
1772
|
+
"list",
|
|
1773
|
+
"sessionSearch",
|
|
1774
|
+
"bookmarks",
|
|
1775
|
+
"focus",
|
|
1776
|
+
"updateCheck",
|
|
1777
|
+
"settings"
|
|
1778
|
+
];
|
|
1779
|
+
/** The canonical default prefs ("恢复默认" target; also the read fallback). */
|
|
1780
|
+
const DEFAULT_PREFS = {
|
|
1781
|
+
pinned: [],
|
|
1782
|
+
accent: "#4d7cfd",
|
|
1783
|
+
iconSize: 28,
|
|
1784
|
+
inset: 14,
|
|
1785
|
+
side: "right",
|
|
1786
|
+
locale: "system"
|
|
1787
|
+
};
|
|
1788
|
+
/** Type guard for registry ids — unknown strings never survive a parse. */
|
|
1789
|
+
function isToolbarPinId(id) {
|
|
1790
|
+
return TOOLBAR_PIN_IDS.includes(id);
|
|
1791
|
+
}
|
|
1792
|
+
/** Whitelist + dedupe + first-seen-order sanitizer for the pinned list. */
|
|
1793
|
+
function sanitizePinned(raw) {
|
|
1794
|
+
if (!Array.isArray(raw)) return [];
|
|
1795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1796
|
+
const result = [];
|
|
1797
|
+
for (const id of raw) {
|
|
1798
|
+
if (typeof id !== "string" || !isToolbarPinId(id) || seen.has(id)) continue;
|
|
1799
|
+
seen.add(id);
|
|
1800
|
+
result.push(id);
|
|
1801
|
+
}
|
|
1802
|
+
return result;
|
|
1803
|
+
}
|
|
1224
1804
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
* renders it as `data-variant="think"` with `data-state="running|ok"`), so an
|
|
1229
|
-
* overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
|
|
1230
|
-
* opening it, `[data-open]`) restores full opacity. Kept in an inline
|
|
1231
|
-
* <style> so the plugin stays zero-asset — same pattern as BADGE_PULSE_CSS.
|
|
1805
|
+
* Snap a finite number to the nearest `step` inside [min, max]; any non-finite
|
|
1806
|
+
* or non-number input falls back to `fallback`. Used for both sliders so a
|
|
1807
|
+
* hand-edited blob (e.g. `iconSize: 21`) converges on a legal slider value.
|
|
1232
1808
|
*/
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const DOT_HIT = 28;
|
|
1239
|
-
/** Vertical gap between dot hit areas (px) — fixed pitch, never scaled. */
|
|
1240
|
-
const DOT_GAP = 14;
|
|
1241
|
-
/** Inward offset from the scrollport right edge so the rail clears the scrollbar. */
|
|
1242
|
-
const RAIL_INSET = 14;
|
|
1809
|
+
function clampStep(value, min, max, step, fallback) {
|
|
1810
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
1811
|
+
const snapped = Math.round(Math.min(max, Math.max(min, value)) / step) * step;
|
|
1812
|
+
return Math.min(max, Math.max(min, snapped));
|
|
1813
|
+
}
|
|
1243
1814
|
/**
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1815
|
+
* Parse + sanitize the raw persisted blob: `null` (nothing stored), invalid
|
|
1816
|
+
* JSON, or a non-object shape all degrade to the DEFAULT prefs. Each field is
|
|
1817
|
+
* sanitized independently, so a half-corrupt blob keeps its valid parts
|
|
1818
|
+
* (e.g. an old `{pinned}`-only blob gains the default accent/size/inset/side).
|
|
1247
1819
|
*/
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1820
|
+
function parsePrefs(raw) {
|
|
1821
|
+
if (raw === null) return { ...DEFAULT_PREFS };
|
|
1822
|
+
let parsed;
|
|
1823
|
+
try {
|
|
1824
|
+
parsed = JSON.parse(raw);
|
|
1825
|
+
} catch {
|
|
1826
|
+
return { ...DEFAULT_PREFS };
|
|
1827
|
+
}
|
|
1828
|
+
if (typeof parsed !== "object" || parsed === null) return { ...DEFAULT_PREFS };
|
|
1829
|
+
const { pinned, accent, iconSize, inset, side, locale } = parsed;
|
|
1830
|
+
return {
|
|
1831
|
+
pinned: sanitizePinned(pinned),
|
|
1832
|
+
accent: typeof accent === "string" && isHexColor(accent) ? accent.toLowerCase() : DEFAULT_PREFS.accent,
|
|
1833
|
+
iconSize: clampStep(iconSize, 20, 36, 2, DEFAULT_PREFS.iconSize),
|
|
1834
|
+
inset: clampStep(inset, 0, 40, 2, DEFAULT_PREFS.inset),
|
|
1835
|
+
side: side === "left" || side === "right" ? side : DEFAULT_PREFS.side,
|
|
1836
|
+
locale: locale === "zh" || locale === "en" || locale === "system" ? locale : DEFAULT_PREFS.locale
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Read + sanitize the persisted toolbar prefs from localStorage. Degrades to
|
|
1841
|
+
* the DEFAULT prefs when storage is unavailable (SSR, sandboxed iframe) —
|
|
1842
|
+
* personalization is a best-effort enhancement, never a render blocker.
|
|
1843
|
+
*/
|
|
1844
|
+
function loadPrefs() {
|
|
1845
|
+
try {
|
|
1846
|
+
return parsePrefs(localStorage.getItem(TOOLBAR_PREFS_KEY));
|
|
1847
|
+
} catch {
|
|
1848
|
+
return { ...DEFAULT_PREFS };
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Persist the full prefs (sanitized on the way out so a corrupt in-memory
|
|
1853
|
+
* value is never written). Swallows storage failures for the same best-effort
|
|
1854
|
+
* reason as {@link loadPrefs}.
|
|
1855
|
+
*/
|
|
1856
|
+
function savePrefs(prefs) {
|
|
1857
|
+
const cleaned = parsePrefs(JSON.stringify(prefs));
|
|
1858
|
+
try {
|
|
1859
|
+
localStorage.setItem(TOOLBAR_PREFS_KEY, JSON.stringify(cleaned));
|
|
1860
|
+
} catch {}
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Pure toggle: adds `id` to the pinned set when absent, removes it when
|
|
1864
|
+
* present. Unknown ids are ignored (prefs returned unchanged) and the pinned
|
|
1865
|
+
* set is always deduped via the sanitizer, so callers can feed the result
|
|
1866
|
+
* straight back into {@link savePrefs}.
|
|
1867
|
+
*/
|
|
1868
|
+
function togglePin(prefs, id) {
|
|
1869
|
+
if (!isToolbarPinId(id)) return { ...prefs };
|
|
1870
|
+
const next = new Set(prefs.pinned);
|
|
1871
|
+
if (next.has(id)) next.delete(id);
|
|
1872
|
+
else next.add(id);
|
|
1873
|
+
return {
|
|
1874
|
+
...prefs,
|
|
1875
|
+
pinned: sanitizePinned([...next])
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
//#endregion
|
|
1879
|
+
//#region src/client/version-logic.ts
|
|
1880
|
+
/**
|
|
1881
|
+
* Pure update-check logic for dsh-milestone, deliberately free of React, the
|
|
1882
|
+
* harness runtime, and any node-only API so it runs identically in the browser
|
|
1883
|
+
* bundle, vitest (jsdom), and future shells.
|
|
1884
|
+
*
|
|
1885
|
+
* Design intent:
|
|
1886
|
+
* - `compareVersions` / `needsUpdate` are pure, side-effect-free functions that
|
|
1887
|
+
* follow npm semver precedence for the shapes this plugin actually publishes
|
|
1888
|
+
* (`x.y.z` plus optional `-rc.N` / `-beta.N` prereleases). Anything else is
|
|
1889
|
+
* an explicit error rather than a silent mis-comparison.
|
|
1890
|
+
* - `fetchLatestVersion` is the only I/O: it reads the `latest` dist-tag from
|
|
1891
|
+
* npmmirror's cheap dist-tags endpoint first (it CORS-echoes this page's
|
|
1892
|
+
* Origin), then falls back to the full npm packument (ACAO: *). Every
|
|
1893
|
+
* request carries an internal 8s timeout combined with the caller's signal —
|
|
1894
|
+
* whichever aborts first wins — and every failure degrades to a structured
|
|
1895
|
+
* `{ ok: false }` result, never a throw.
|
|
1896
|
+
* - `SUPPORTED_HOST_LINES` is a SELF-DECLARED compatibility list, not a probe:
|
|
1897
|
+
* the harness reports no trustworthy host version in the browser
|
|
1898
|
+
* (`host.describe().version` is a stub), so the plugin declares which npm
|
|
1899
|
+
* `latest` host-version lines it supports and the UI renders that as-is.
|
|
1900
|
+
*/
|
|
1901
|
+
/** Host-version lines this plugin declares support for (npm official latest
|
|
1902
|
+
* line; bump it as the peer/dependency ranges move). */
|
|
1903
|
+
const SUPPORTED_HOST_LINES = ["0.1.1-rc.2"];
|
|
1904
|
+
/** Internal per-request timeout for the network attempts. */
|
|
1905
|
+
const REQUEST_TIMEOUT_MS = 8e3;
|
|
1906
|
+
/** npmmirror dist-tags endpoint: lean JSON (`{"latest":"0.6.0", ...}`),
|
|
1907
|
+
* echoes this page's Origin in `Access-Control-Allow-Origin`. */
|
|
1908
|
+
const NPMIRROR_DIST_TAGS_URL = "https://registry.npmmirror.com/-/package/dsh-milestone/dist-tags";
|
|
1909
|
+
/** npm full packument; `dist-tags.latest` sits at the JSON root, and the
|
|
1910
|
+
* endpoint sends `Access-Control-Allow-Origin: *`. */
|
|
1911
|
+
const NPM_PACKUMENT_URL = "https://registry.npmjs.org/dsh-milestone";
|
|
1912
|
+
/** Numeric identifier test shared by core and prerelease segments. */
|
|
1913
|
+
const NUMERIC_RE = /^\d+$/;
|
|
1914
|
+
/**
|
|
1915
|
+
* Parse a version string into comparable parts.
|
|
1916
|
+
* Accepts `major[.minor[.patch]][-prerelease][+build]`; missing core segments
|
|
1917
|
+
* pad with 0 and `+build` metadata is ignored (per semver precedence).
|
|
1918
|
+
* @throws {Error} with the offending input when the shape is not parseable.
|
|
1919
|
+
*/
|
|
1920
|
+
function parseVersion(input) {
|
|
1921
|
+
if (typeof input !== "string" || input.trim() === "") throw new Error(`Invalid semantic version: ${JSON.stringify(input)} (expected "x.y.z" with optional "-pre" suffix)`);
|
|
1922
|
+
const s = input.trim();
|
|
1923
|
+
const plus = s.indexOf("+");
|
|
1924
|
+
const withoutBuild = plus === -1 ? s : s.slice(0, plus);
|
|
1925
|
+
const dash = withoutBuild.indexOf("-");
|
|
1926
|
+
const corePart = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
|
|
1927
|
+
const prePart = dash === -1 ? void 0 : withoutBuild.slice(dash + 1);
|
|
1928
|
+
const segments = corePart.split(".");
|
|
1929
|
+
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)`);
|
|
1930
|
+
const core = segments.map(Number);
|
|
1931
|
+
let pre = [];
|
|
1932
|
+
if (prePart !== void 0) {
|
|
1933
|
+
if (prePart === "" || !prePart.split(".").every((id) => /^[0-9A-Za-z-]+$/.test(id))) throw new Error(`Invalid semantic version: ${JSON.stringify(input)} (bad prerelease suffix)`);
|
|
1934
|
+
pre = prePart.split(".");
|
|
1935
|
+
}
|
|
1936
|
+
return {
|
|
1937
|
+
core,
|
|
1938
|
+
pre
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Compare two version strings per npm semver precedence.
|
|
1943
|
+
* @param a - first version (`x.y.z` with optional `-pre` suffix; build
|
|
1944
|
+
* metadata ignored; missing core segments pad with 0).
|
|
1945
|
+
* @param b - second version, same grammar.
|
|
1946
|
+
* @returns -1 when `a < b`, 0 when equal, 1 when `a > b`. Prereleases sort
|
|
1947
|
+
* below their same-number release; prerelease identifiers compare numerically
|
|
1948
|
+
* when both are numeric, lexically when both are alphanumeric, and numeric
|
|
1949
|
+
* identifiers always sort before alphanumeric ones.
|
|
1950
|
+
* @throws {Error} for inputs that do not match the grammar.
|
|
1951
|
+
*/
|
|
1952
|
+
function compareVersions(a, b) {
|
|
1953
|
+
const pa = parseVersion(a);
|
|
1954
|
+
const pb = parseVersion(b);
|
|
1955
|
+
for (let i = 0; i < 3; i++) {
|
|
1956
|
+
const x = pa.core[i] ?? 0;
|
|
1957
|
+
const y = pb.core[i] ?? 0;
|
|
1958
|
+
if (x < y) return -1;
|
|
1959
|
+
if (x > y) return 1;
|
|
1960
|
+
}
|
|
1961
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
1962
|
+
if (pa.pre.length === 0) return 1;
|
|
1963
|
+
if (pb.pre.length === 0) return -1;
|
|
1964
|
+
const len = Math.max(pa.pre.length, pb.pre.length);
|
|
1965
|
+
for (let i = 0; i < len; i++) {
|
|
1966
|
+
const x = pa.pre[i];
|
|
1967
|
+
const y = pb.pre[i];
|
|
1968
|
+
if (x === void 0) return -1;
|
|
1969
|
+
if (y === void 0) return 1;
|
|
1970
|
+
const xNum = NUMERIC_RE.test(x);
|
|
1971
|
+
const yNum = NUMERIC_RE.test(y);
|
|
1972
|
+
if (xNum && yNum) {
|
|
1973
|
+
if (x !== y) return Number(x) < Number(y) ? -1 : 1;
|
|
1974
|
+
} else if (xNum) return -1;
|
|
1975
|
+
else if (yNum) return 1;
|
|
1976
|
+
else if (x !== y) return x < y ? -1 : 1;
|
|
1977
|
+
}
|
|
1978
|
+
return 0;
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Whether the installed plugin should offer an update.
|
|
1982
|
+
* @param current - installed version.
|
|
1983
|
+
* @param latest - newest published version.
|
|
1984
|
+
* @returns true only when `latest` is strictly greater than `current`
|
|
1985
|
+
* (identical versions, or a newer installed version, return false).
|
|
1986
|
+
* @throws {Error} when either input is not a parseable version.
|
|
1987
|
+
*/
|
|
1988
|
+
function needsUpdate(current, latest) {
|
|
1989
|
+
return compareVersions(current, latest) < 0;
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Build a request signal: a fresh AbortController aborted by EITHER the
|
|
1993
|
+
* caller's external signal (which wins when it fires first) OR an internal
|
|
1994
|
+
* timeout. Uses `AbortSignal.timeout` when the environment provides it and
|
|
1995
|
+
* falls back to a manual `setTimeout` + abort otherwise (jsdom older
|
|
1996
|
+
* versions expose no `AbortSignal.timeout`).
|
|
1997
|
+
* @param external - caller signal; when already aborted the request fires
|
|
1998
|
+
* immediately with an aborted signal.
|
|
1999
|
+
* @param timeoutMs - internal timeout in ms.
|
|
2000
|
+
* @returns the combined signal plus a cleanup that detaches all listeners
|
|
2001
|
+
* (must be called so no listener outlives the request).
|
|
2002
|
+
*/
|
|
2003
|
+
function createRequestSignal(external, timeoutMs) {
|
|
2004
|
+
const controller = new AbortController();
|
|
2005
|
+
const abort = () => controller.abort();
|
|
2006
|
+
const detach = [];
|
|
2007
|
+
if (external) if (external.aborted) controller.abort();
|
|
2008
|
+
else {
|
|
2009
|
+
external.addEventListener("abort", abort, { once: true });
|
|
2010
|
+
detach.push(() => external.removeEventListener("abort", abort));
|
|
2011
|
+
}
|
|
2012
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
2013
|
+
const t = AbortSignal.timeout(timeoutMs);
|
|
2014
|
+
t.addEventListener("abort", abort, { once: true });
|
|
2015
|
+
detach.push(() => t.removeEventListener("abort", abort));
|
|
2016
|
+
} else {
|
|
2017
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
2018
|
+
detach.push(() => clearTimeout(timer));
|
|
2019
|
+
}
|
|
2020
|
+
return {
|
|
2021
|
+
signal: controller.signal,
|
|
2022
|
+
cleanup: () => {
|
|
2023
|
+
for (const fn of detach) fn();
|
|
2024
|
+
detach.length = 0;
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
/** Extract `latest` from the npmmirror dist-tags JSON root. */
|
|
2029
|
+
function extractFromDistTags(data) {
|
|
2030
|
+
if (data === null || typeof data !== "object") return null;
|
|
2031
|
+
const latest = data.latest;
|
|
2032
|
+
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
|
2033
|
+
}
|
|
2034
|
+
/** Extract `latest` from the npm packument's root `dist-tags` object. */
|
|
2035
|
+
function extractFromPackument(data) {
|
|
2036
|
+
if (data === null || typeof data !== "object") return null;
|
|
2037
|
+
const distTags = data["dist-tags"];
|
|
2038
|
+
if (distTags === null || typeof distTags !== "object") return null;
|
|
2039
|
+
const latest = distTags.latest;
|
|
2040
|
+
return typeof latest === "string" && latest.length > 0 ? latest : null;
|
|
2041
|
+
}
|
|
2042
|
+
/** Human-readable failure detail for one endpoint attempt. */
|
|
2043
|
+
function describeError(source, cause) {
|
|
2044
|
+
const name = cause instanceof Error ? cause.name : "";
|
|
2045
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2046
|
+
if (name === "AbortError") return `${source}: aborted (${message})`;
|
|
2047
|
+
return `${source}: ${message}`;
|
|
2048
|
+
}
|
|
2049
|
+
/**
|
|
2050
|
+
* One attempt at fetching the `latest` dist-tag from an endpoint.
|
|
2051
|
+
* @returns ok with the tag, or ok:false with a per-source error description.
|
|
2052
|
+
*/
|
|
2053
|
+
async function tryFetchLatest(url, extract, source, external) {
|
|
2054
|
+
try {
|
|
2055
|
+
const { signal, cleanup } = createRequestSignal(external, REQUEST_TIMEOUT_MS);
|
|
2056
|
+
try {
|
|
2057
|
+
const res = await fetch(url, { signal });
|
|
2058
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2059
|
+
const latest = extract(await res.json());
|
|
2060
|
+
if (latest === null) throw new Error("unexpected response shape (no \"latest\" dist-tag)");
|
|
2061
|
+
return {
|
|
2062
|
+
ok: true,
|
|
2063
|
+
latest,
|
|
2064
|
+
source
|
|
2065
|
+
};
|
|
2066
|
+
} finally {
|
|
2067
|
+
cleanup();
|
|
2068
|
+
}
|
|
2069
|
+
} catch (cause) {
|
|
2070
|
+
return {
|
|
2071
|
+
ok: false,
|
|
2072
|
+
error: describeError(source, cause)
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Query npm for the newest published version of `dsh-milestone`.
|
|
2078
|
+
*
|
|
2079
|
+
* Strategy: npmmirror's dist-tags endpoint first (lightweight and CORS-open
|
|
2080
|
+
* to this Origin); if that fails or its shape is wrong, the full npm
|
|
2081
|
+
* packument fallback (`dist-tags.latest` at the JSON root). Each request
|
|
2082
|
+
* aborts after 8s via `AbortSignal.timeout` (manual timer fallback when the
|
|
2083
|
+
* environment lacks it) OR immediately when the passed-in signal aborts.
|
|
2084
|
+
* @param signal - optional caller abort signal; aborts the in-flight attempt
|
|
2085
|
+
* with priority over the internal timeout.
|
|
2086
|
+
* @returns the latest version and which registry answered, or a structured
|
|
2087
|
+
* error when both endpoints fail. Never throws.
|
|
2088
|
+
*/
|
|
2089
|
+
async function fetchLatestVersion(signal) {
|
|
2090
|
+
const viaMirror = await tryFetchLatest(NPMIRROR_DIST_TAGS_URL, extractFromDistTags, "npmmirror", signal);
|
|
2091
|
+
if (viaMirror.ok) return viaMirror;
|
|
2092
|
+
const viaNpm = await tryFetchLatest(NPM_PACKUMENT_URL, extractFromPackument, "npm", signal);
|
|
2093
|
+
if (viaNpm.ok) return viaNpm;
|
|
2094
|
+
return {
|
|
2095
|
+
ok: false,
|
|
2096
|
+
error: `${viaMirror.error}; ${viaNpm.error}`
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
/** localStorage key holding the last successful update check. */
|
|
2100
|
+
const UPDATE_CACHE_KEY = "dsh-milestone.update-cache";
|
|
2101
|
+
/**
|
|
2102
|
+
* Pure parse + freshness check of a stored blob: `null` for `null`/invalid
|
|
2103
|
+
* JSON, a wrong shape, or an entry older than {@link UPDATE_CACHE_TTL_MS}.
|
|
2104
|
+
* A `checkedAt` in the future (clock skew) is treated as fresh — it decays
|
|
2105
|
+
* naturally once wall time catches up. Never throws.
|
|
2106
|
+
* @param raw - the raw `localStorage` value (or null when absent).
|
|
2107
|
+
* @param now - wall-clock epoch ms to judge freshness against (injectable for
|
|
2108
|
+
* tests; defaults to `Date.now()`).
|
|
2109
|
+
*/
|
|
2110
|
+
function parseUpdateCache(raw, now = Date.now()) {
|
|
2111
|
+
if (raw === null) return null;
|
|
2112
|
+
let parsed;
|
|
2113
|
+
try {
|
|
2114
|
+
parsed = JSON.parse(raw);
|
|
2115
|
+
} catch {
|
|
2116
|
+
return null;
|
|
2117
|
+
}
|
|
2118
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2119
|
+
const { latest, source, checkedAt } = parsed;
|
|
2120
|
+
if (typeof latest !== "string" || latest === "") return null;
|
|
2121
|
+
if (source !== "npmmirror" && source !== "npm") return null;
|
|
2122
|
+
if (typeof checkedAt !== "number" || !Number.isFinite(checkedAt)) return null;
|
|
2123
|
+
if (now - checkedAt >= 216e5) return null;
|
|
2124
|
+
return {
|
|
2125
|
+
latest,
|
|
2126
|
+
source,
|
|
2127
|
+
checkedAt
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Read + parse the persisted cache entry from localStorage. Any storage
|
|
2132
|
+
* failure degrades to `null` (cache miss) — the update check is best-effort.
|
|
2133
|
+
* @param now - wall-clock epoch ms for freshness (see {@link parseUpdateCache}).
|
|
2134
|
+
*/
|
|
2135
|
+
function readUpdateCache(now = Date.now()) {
|
|
2136
|
+
try {
|
|
2137
|
+
return parseUpdateCache(localStorage.getItem(UPDATE_CACHE_KEY), now);
|
|
2138
|
+
} catch {
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Persist a successful check result (the caller supplies `checkedAt`, usually
|
|
2144
|
+
* `Date.now()`). Silently ignores storage failures — the cache is an
|
|
2145
|
+
* optimization, never a hard dependency.
|
|
2146
|
+
*/
|
|
2147
|
+
function writeUpdateCache(entry) {
|
|
2148
|
+
try {
|
|
2149
|
+
localStorage.setItem(UPDATE_CACHE_KEY, JSON.stringify(entry));
|
|
2150
|
+
} catch {}
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* The cache-aware entry point for the UI: reuse an unexpired cached result
|
|
2154
|
+
* when one exists, otherwise query npm (npmmirror → packument fallback) and
|
|
2155
|
+
* persist a successful result for the next {@link UPDATE_CACHE_TTL_MS}.
|
|
2156
|
+
* Never throws; failures return the same structured `{ ok: false }` result as
|
|
2157
|
+
* {@link fetchLatestVersion}.
|
|
2158
|
+
* @param signal - optional caller abort signal, forwarded to the network
|
|
2159
|
+
* attempt only (a cache hit needs no signal).
|
|
2160
|
+
*/
|
|
2161
|
+
async function loadCachedLatest(signal) {
|
|
2162
|
+
const cached = readUpdateCache();
|
|
2163
|
+
if (cached !== null) return {
|
|
2164
|
+
ok: true,
|
|
2165
|
+
latest: cached.latest,
|
|
2166
|
+
source: cached.source
|
|
2167
|
+
};
|
|
2168
|
+
const fresh = await fetchLatestVersion(signal);
|
|
2169
|
+
if (fresh.ok) writeUpdateCache({
|
|
2170
|
+
latest: fresh.latest,
|
|
2171
|
+
source: fresh.source,
|
|
2172
|
+
checkedAt: Date.now()
|
|
2173
|
+
});
|
|
2174
|
+
return fresh;
|
|
2175
|
+
}
|
|
2176
|
+
//#endregion
|
|
2177
|
+
//#region src/client/version-meta.ts
|
|
2178
|
+
/**
|
|
2179
|
+
* Installed plugin version. Injected at build time as
|
|
2180
|
+
* `__DSH_MILESTONE_VERSION__`; falls back to `0.0.0-dev` when unbuilt.
|
|
2181
|
+
*/
|
|
2182
|
+
const PLUGIN_VERSION = "0.6.2";
|
|
2183
|
+
//#endregion
|
|
2184
|
+
//#region src/client/MilestoneRail.tsx
|
|
2185
|
+
/**
|
|
2186
|
+
* MilestoneRail: the milestone.rail entry (session scope). Renders a fixed
|
|
2187
|
+
* side vertical scrubber as a **fixed-pitch dot list** (like a git commit
|
|
2188
|
+
* graph), NOT a minimap: one dot per user message, equal spacing regardless of
|
|
2189
|
+
* conversation length. The list itself scrolls with the wheel when it outgrows
|
|
2190
|
+
* the viewport; hovering a dot shows rich metadata (time, turn, duration, end
|
|
2191
|
+
* reason, TTFT, tokens/sec) and clicking jumps the chat to that message.
|
|
2192
|
+
*
|
|
2193
|
+
* Data sources (all from the session-scoped `useSession` snapshot):
|
|
2194
|
+
* - chat.order + chat.nodes.get(key) -> user-message nodes (key/id/location)
|
|
2195
|
+
* - chat.timeline.turns.get(turn) -> turn start/end time, status, reason,
|
|
2196
|
+
* and the ui-conversation 'turn-tail'
|
|
2197
|
+
* location data (ttftMs/tokensPerSecond)
|
|
2198
|
+
*
|
|
2199
|
+
* Positioning: the rail hugs the conversation scrollport's chosen screen edge
|
|
2200
|
+
* (settings 位置: left or right), offset a little inward so it clears the
|
|
2201
|
+
* native scrollbar and sits near the prose.
|
|
2202
|
+
*
|
|
2203
|
+
* In-rail search (F1): a magnifier toggle at the rail top opens a compact
|
|
2204
|
+
* panel on the rail's free side with a message-text search input; matches
|
|
2205
|
+
* light up the dots (non-matches dim), Enter cycles the active match
|
|
2206
|
+
* (wrapping) and jumps to it, Escape clears and closes. Matching runs over the
|
|
2207
|
+
* FULL message text (`text` from rail-logic.extractText), not the truncated
|
|
2208
|
+
* hover preview.
|
|
2209
|
+
*
|
|
2210
|
+
* Current-position highlight (F2): the dot for the user message at/just above
|
|
2211
|
+
* the conversation viewport top carries a white ring (`useCurrentAnchor`
|
|
2212
|
+
* observes the scrollport, no polling).
|
|
2213
|
+
*
|
|
2214
|
+
* Load-older + window coverage (F3): when the session still has earlier pages
|
|
2215
|
+
* (`hasMore`) a slim `···` button sits at the rail top and triggers the
|
|
2216
|
+
* injected `loadOlder` action (disabled + `data-loading-older` while
|
|
2217
|
+
* `loadingOlder`), and a compact hint on the rail's free side states how many
|
|
2218
|
+
* messages the current window covers.
|
|
2219
|
+
*
|
|
2220
|
+
* Settings (B-design): the gear is a REGULAR toolbar feature ("settings",
|
|
2221
|
+
* registry last, default unpinned) — the collapsed rail shows only the expand
|
|
2222
|
+
* arrow plus the user's pinned keys, and expanding reveals the gear at the end
|
|
2223
|
+
* of the queue. The gear opens a CENTERED modal dialog (function-key pins /
|
|
2224
|
+
* hover descriptions, the personalization controls, and the support-us card
|
|
2225
|
+
* grid); everything the modal changes persists under `dsh-milestone.toolbar`.
|
|
2226
|
+
*/
|
|
2227
|
+
/** Minimum user messages before the rail adds value. */
|
|
2228
|
+
const MIN_MARKS = 2;
|
|
2229
|
+
const PREVIEW_LENGTH = 80;
|
|
2230
|
+
/** Stable no-bookmarks fallback for render paths without the store seat. */
|
|
2231
|
+
const NO_BOOKMARKS = [];
|
|
2232
|
+
/** Stable no-kinds fallback for marks whose turn carries no badge nodes. */
|
|
2233
|
+
const NO_KINDS = [];
|
|
2234
|
+
/** Visual dot diameter at the default icon size (px). */
|
|
2235
|
+
const DOT_SIZE = 14;
|
|
2236
|
+
/** Hit area per dot at the default icon size (px) — larger than the dot. */
|
|
2237
|
+
const DOT_HIT = 28;
|
|
2238
|
+
/** Vertical gap between dot hit areas at the default icon size (px). */
|
|
2239
|
+
const DOT_GAP = 14;
|
|
2240
|
+
/**
|
|
2241
|
+
* Extra top margin a new turn group's FIRST dot gets (replaces the old
|
|
2242
|
+
* `data-turn-separator` line): same-group pitch stays DOT_GAP, a group
|
|
2243
|
+
* boundary opens another GROUP_GAP_EXTRA px (14 → 18 at default size),
|
|
2244
|
+
* expressed purely as spacing — no line element.
|
|
2245
|
+
*/
|
|
2246
|
+
const GROUP_GAP_EXTRA = 4;
|
|
2247
|
+
/**
|
|
2248
|
+
* Preset accent swatches for the settings 强调色 row (default blue first).
|
|
2249
|
+
* The custom color input accepts any #rrggbb.
|
|
2250
|
+
*/
|
|
2251
|
+
const ACCENT_PRESETS = [
|
|
2252
|
+
"#4d7cfd",
|
|
2253
|
+
"#22c55e",
|
|
2254
|
+
"#f59e0b",
|
|
2255
|
+
"#ef4444",
|
|
2256
|
+
"#a855f7",
|
|
2257
|
+
"#06b6d4",
|
|
2258
|
+
"#ec4899",
|
|
2259
|
+
"#f97316"
|
|
2260
|
+
];
|
|
2261
|
+
/** Known floating-panel widths (px) used to anchor side=left panels to the
|
|
2262
|
+
* rail's free (right) side — the panel components take a viewport `right`
|
|
2263
|
+
* offset, so a left rail must back-calculate it from the panel width. */
|
|
2264
|
+
const PANEL_WIDTH_SEARCH = 220;
|
|
2265
|
+
const PANEL_WIDTH_STANDARD = 280;
|
|
2266
|
+
/** Tooltip anchor width: its maxWidth cap, so a tooltip never overlaps the rail. */
|
|
2267
|
+
const TOOLTIP_ANCHOR_WIDTH = 300;
|
|
2268
|
+
/**
|
|
2269
|
+
* P3 focus mode: dims the harness's AI thinking/scratchpad blocks so the
|
|
2270
|
+
* conversation reads cleaner. The rule targets the stable, un-hashed
|
|
2271
|
+
* `data-variant="think"` attribute on the thinking-block ROOT (the harness
|
|
2272
|
+
* renders it as `data-variant="think"` with `data-state="running|ok"`), so an
|
|
2273
|
+
* overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
|
|
2274
|
+
* opening it, `[data-open]`) restores full opacity. Kept in an inline
|
|
2275
|
+
* <style> so the plugin stays zero-asset.
|
|
2276
|
+
*/
|
|
2277
|
+
const FOCUS_CSS = `[data-variant="think"] { opacity: 0.4; transition: opacity 0.2s; }
|
|
2278
|
+
[data-variant="think"]:hover, [data-variant="think"] [data-open] { opacity: 1; }`;
|
|
2279
|
+
/**
|
|
2280
|
+
* Static settings-modal styling that needs `:hover` (which inline styles
|
|
2281
|
+
* cannot express): the support-us card grid (micro-lift + accent highlight)
|
|
2282
|
+
* and the pin-row hover wash. Accent values come from the rail root's CSS
|
|
2283
|
+
* variables (`--ms-accent`), so one block serves every accent. The
|
|
2284
|
+
* search-toggle recolor rule makes the EXTERNAL RailSearchUi chrome follow
|
|
2285
|
+
* the accent too (that file is owned by an earlier phase and cannot change);
|
|
2286
|
+
* `!important` is required because the toggle's own inline styles win
|
|
2287
|
+
* otherwise.
|
|
2288
|
+
*/
|
|
2289
|
+
const MODAL_CSS = `
|
|
2290
|
+
[data-support-card] {
|
|
2291
|
+
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
2292
|
+
padding: 10px 12px; border-radius: 8px;
|
|
2293
|
+
background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.12);
|
|
2294
|
+
color: #c7cede; text-decoration: none; font-size: 12.5px; line-height: 1.4;
|
|
2295
|
+
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
|
|
2296
|
+
}
|
|
2297
|
+
[data-support-card]:hover { transform: translateY(-2px); border-color: var(--ms-accent); background: rgba(255, 255, 255, 0.09); }
|
|
2298
|
+
[data-toolbar-pin-toggle]:hover, [data-toolbar-pin-toggle]:focus-visible { background: rgba(255, 255, 255, 0.06); }
|
|
2299
|
+
[data-search-toggle] { color: #8b96ab !important; }
|
|
2300
|
+
[data-search-toggle][aria-pressed="true"] { background: var(--ms-accent-bg) !important; color: var(--ms-accent-soft) !important; }
|
|
2301
|
+
`;
|
|
2302
|
+
/**
|
|
2303
|
+
* P3 deep links (`#msg=<anchor-key>`): initial delay before the first
|
|
2304
|
+
* deep-link attempt — the harness scrolls the conversation to the bottom on
|
|
2305
|
+
* load, so the deep link must land AFTER the view mounts.
|
|
2306
|
+
*/
|
|
2307
|
+
const DEEP_LINK_INITIAL_DELAY = 100;
|
|
2308
|
+
/** P3: interval between DOM-row polls while waiting for the target to render. */
|
|
2309
|
+
const DEEP_LINK_POLL_DELAY = 150;
|
|
1251
2310
|
/** P3: polls before falling back to a single `loadOlder` fetch. */
|
|
1252
2311
|
const DEEP_LINK_MAX_POLLS = 5;
|
|
1253
2312
|
/** P3: bounded polls after `loadOlder`, then the deep link gives up silently. */
|
|
1254
2313
|
const DEEP_LINK_MAX_RETRY_POLLS = 5;
|
|
2314
|
+
/** B4 update-check: mount-time silent check delay (ms) — give the harness
|
|
2315
|
+
* time to settle before hitting the registry. */
|
|
2316
|
+
const UPDATE_CHECK_MOUNT_DELAY = 1500;
|
|
2317
|
+
/** Initial state: no check has completed yet, nothing to show. */
|
|
2318
|
+
const NO_UPDATE_CHECK = {
|
|
2319
|
+
phase: "idle",
|
|
2320
|
+
latest: null,
|
|
2321
|
+
source: null,
|
|
2322
|
+
error: null,
|
|
2323
|
+
available: false
|
|
2324
|
+
};
|
|
2325
|
+
/**
|
|
2326
|
+
* B4 display label for one supported host line: strips the fixed
|
|
2327
|
+
* `x.y.z` prefix and appends "line" — `0.1.1-rc.2` → `rc.2 line`,
|
|
2328
|
+
* `0.1.1` → `0.1.1 line`. Pure presentation metadata.
|
|
2329
|
+
*/
|
|
2330
|
+
function hostLineLabel(line) {
|
|
2331
|
+
const suffix = line.replace(/^\d+\.\d+\.\d+-?/, "");
|
|
2332
|
+
return suffix === "" ? `${line} line` : `${suffix} line`;
|
|
2333
|
+
}
|
|
1255
2334
|
/**
|
|
1256
2335
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
1257
2336
|
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
@@ -1287,7 +2366,7 @@ window.__ModuleLoader__.load({
|
|
|
1287
2366
|
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
1288
2367
|
items: [],
|
|
1289
2368
|
hasMore: false
|
|
1290
|
-
}), openSession = () => {}, t = (key) => key }) {
|
|
2369
|
+
}), openSession = () => {}, t: frameworkT = (key) => key }) {
|
|
1291
2370
|
const order = useSession((s) => s.chat.order);
|
|
1292
2371
|
const nodes = useSession((s) => s.chat.nodes);
|
|
1293
2372
|
const locations = useSession((s) => s.chat.locations);
|
|
@@ -1392,43 +2471,141 @@ window.__ModuleLoader__.load({
|
|
|
1392
2471
|
}
|
|
1393
2472
|
return counts;
|
|
1394
2473
|
}, [displayMarks]);
|
|
1395
|
-
(0, react.
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
2474
|
+
const [prefs, setPrefs] = (0, react.useState)(() => loadPrefs());
|
|
2475
|
+
const { pinned, accent, iconSize, inset, side } = prefs;
|
|
2476
|
+
const scale = iconSize / DOT_HIT;
|
|
2477
|
+
const hit = iconSize;
|
|
2478
|
+
const size = DOT_SIZE * scale;
|
|
2479
|
+
const gap = DOT_GAP * scale;
|
|
2480
|
+
const accentSoft = lighten(accent, .42) ?? "#9db8ff";
|
|
2481
|
+
const accentBg = rgbaString(accent, .18) ?? "rgba(77, 124, 254, 0.18)";
|
|
2482
|
+
const accentStrong = rgbaString(accent, .55) ?? "rgba(77, 124, 254, 0.55)";
|
|
2483
|
+
/**
|
|
2484
|
+
* Language override (settings → 语言): `system` delegates to the harness
|
|
2485
|
+
* `t` seat (the framework-synthesized interpreter for the registered
|
|
2486
|
+
* `dsh-milestone` namespace); `zh`/`en` force the plugin's own dictionaries
|
|
2487
|
+
* so the rail copy switches independently of the host UI language. Every
|
|
2488
|
+
* call site below — rail chrome, panels, tooltip and the settings modal —
|
|
2489
|
+
* already resolves through this binding, so the override is global to the
|
|
2490
|
+
* rail without threading a second translate prop anywhere.
|
|
2491
|
+
*/
|
|
2492
|
+
const t = prefs.locale === "system" ? frameworkT : prefs.locale === "en" ? (key, params) => translateDict(en, key, params) : (key, params) => translateDict(zh, key, params);
|
|
2493
|
+
/** Write a patch of prefs through to state + localStorage. */
|
|
2494
|
+
const updatePrefs = (patch) => {
|
|
2495
|
+
setPrefs((prev) => {
|
|
2496
|
+
const next = {
|
|
2497
|
+
...prev,
|
|
2498
|
+
...patch
|
|
2499
|
+
};
|
|
2500
|
+
savePrefs(next);
|
|
2501
|
+
return next;
|
|
2502
|
+
});
|
|
2503
|
+
};
|
|
2504
|
+
/** B1: flip one feature's pin — state and the persisted blob update together. */
|
|
2505
|
+
const onTogglePin = (id) => {
|
|
2506
|
+
setPrefs((prev) => {
|
|
2507
|
+
const next = togglePin(prev, id);
|
|
2508
|
+
savePrefs(next);
|
|
2509
|
+
return next;
|
|
2510
|
+
});
|
|
2511
|
+
};
|
|
2512
|
+
/** B-design: 恢复默认 resets EVERYTHING — pins AND personalization. */
|
|
2513
|
+
const onResetAll = () => {
|
|
2514
|
+
const next = { ...DEFAULT_PREFS };
|
|
2515
|
+
setPrefs(next);
|
|
2516
|
+
savePrefs(next);
|
|
2517
|
+
};
|
|
2518
|
+
const [toolbarExpanded, setToolbarExpanded] = (0, react.useState)(false);
|
|
2519
|
+
const [expandHovered, setExpandHovered] = (0, react.useState)(false);
|
|
2520
|
+
const [settingsHovered, setSettingsHovered] = (0, react.useState)(false);
|
|
2521
|
+
const [settingsOpen, setSettingsOpen] = (0, react.useState)(false);
|
|
2522
|
+
/** The feature whose description the settings modal's right pane shows
|
|
2523
|
+
* (defaults to the first registry feature — 'search'). */
|
|
2524
|
+
const [descFeature, setDescFeature] = (0, react.useState)("search");
|
|
2525
|
+
const settingsRef = (0, react.useRef)(null);
|
|
2526
|
+
const settingsBtnRef = (0, react.useRef)(null);
|
|
2527
|
+
const [updateOpen, setUpdateOpen] = (0, react.useState)(false);
|
|
2528
|
+
const [updateCheck, setUpdateCheck] = (0, react.useState)(NO_UPDATE_CHECK);
|
|
2529
|
+
const updatePanelRef = (0, react.useRef)(null);
|
|
2530
|
+
const updateBtnRef = (0, react.useRef)(null);
|
|
2531
|
+
/**
|
|
2532
|
+
* B1 settings modal: outside-pointerdown dismisses it (shared
|
|
2533
|
+
* useOutsideDismiss contract) with focus returning to the gear afterwards.
|
|
2534
|
+
* The modal's full-screen overlay wraps the dialog, so a pointerdown on the
|
|
2535
|
+
* backdrop (or anywhere outside the dialog) closes it; the gear's own click
|
|
2536
|
+
* keeps its flip semantics through a `[data-toolbar-settings]` exclusion —
|
|
2537
|
+
* pointerdown on an armed gear must not double-close.
|
|
2538
|
+
*/
|
|
2539
|
+
useOutsideDismiss(settingsRef, settingsOpen, () => {
|
|
2540
|
+
setSettingsOpen(false);
|
|
2541
|
+
settingsBtnRef.current?.focus();
|
|
2542
|
+
}, { exclude: (target) => outsideDismissMatches(target, "[data-toolbar-settings]") });
|
|
2543
|
+
(0, react.useEffect)(() => {
|
|
2544
|
+
if (!settingsOpen) return;
|
|
2545
|
+
const onKey = (e) => {
|
|
2546
|
+
if (e.key !== "Escape") return;
|
|
2547
|
+
setSettingsOpen(false);
|
|
2548
|
+
settingsBtnRef.current?.focus();
|
|
1417
2549
|
};
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}, [render.items.length]);
|
|
2550
|
+
window.addEventListener("keydown", onKey);
|
|
2551
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2552
|
+
}, [settingsOpen]);
|
|
1422
2553
|
(0, react.useEffect)(() => {
|
|
1423
|
-
if (!
|
|
2554
|
+
if (!settingsOpen) return;
|
|
2555
|
+
(settingsRef.current?.querySelector("[data-toolbar-settings-close]"))?.focus();
|
|
2556
|
+
}, [settingsOpen]);
|
|
2557
|
+
/**
|
|
2558
|
+
* B4: run one update check. Cache-aware (`loadCachedLatest` reuses an
|
|
2559
|
+
* unexpired cached result without any network traffic) and never throws:
|
|
2560
|
+
* a network failure lands in the `failed` phase with the structured error,
|
|
2561
|
+
* and an unparseable `latest` (registry anomaly) is treated as "not
|
|
2562
|
+
* available" rather than crashing the panel.
|
|
2563
|
+
*/
|
|
2564
|
+
const runUpdateCheck = () => {
|
|
2565
|
+
setUpdateCheck((prev) => ({
|
|
2566
|
+
...prev,
|
|
2567
|
+
phase: "checking"
|
|
2568
|
+
}));
|
|
2569
|
+
loadCachedLatest().then((result) => {
|
|
2570
|
+
if (result.ok) {
|
|
2571
|
+
let available = false;
|
|
2572
|
+
try {
|
|
2573
|
+
available = needsUpdate(PLUGIN_VERSION, result.latest);
|
|
2574
|
+
} catch {
|
|
2575
|
+
available = false;
|
|
2576
|
+
}
|
|
2577
|
+
setUpdateCheck({
|
|
2578
|
+
phase: "ok",
|
|
2579
|
+
latest: result.latest,
|
|
2580
|
+
source: result.source,
|
|
2581
|
+
error: null,
|
|
2582
|
+
available
|
|
2583
|
+
});
|
|
2584
|
+
} else setUpdateCheck((prev) => ({
|
|
2585
|
+
...prev,
|
|
2586
|
+
phase: "failed",
|
|
2587
|
+
error: result.error
|
|
2588
|
+
}));
|
|
2589
|
+
});
|
|
2590
|
+
};
|
|
2591
|
+
(0, react.useEffect)(() => {
|
|
2592
|
+
const timer = window.setTimeout(runUpdateCheck, UPDATE_CHECK_MOUNT_DELAY);
|
|
2593
|
+
return () => window.clearTimeout(timer);
|
|
2594
|
+
}, []);
|
|
2595
|
+
useOutsideDismiss(updatePanelRef, updateOpen, () => {
|
|
2596
|
+
setUpdateOpen(false);
|
|
2597
|
+
updateBtnRef.current?.focus();
|
|
2598
|
+
}, { exclude: (target) => outsideDismissMatches(target, "[data-update-check]") });
|
|
2599
|
+
(0, react.useEffect)(() => {
|
|
2600
|
+
if (!updateOpen) return;
|
|
1424
2601
|
const onKey = (e) => {
|
|
1425
2602
|
if (e.key !== "Escape") return;
|
|
1426
|
-
|
|
1427
|
-
|
|
2603
|
+
setUpdateOpen(false);
|
|
2604
|
+
updateBtnRef.current?.focus();
|
|
1428
2605
|
};
|
|
1429
2606
|
window.addEventListener("keydown", onKey);
|
|
1430
2607
|
return () => window.removeEventListener("keydown", onKey);
|
|
1431
|
-
}, [
|
|
2608
|
+
}, [updateOpen]);
|
|
1432
2609
|
const marksRef = (0, react.useRef)(marks);
|
|
1433
2610
|
(0, react.useEffect)(() => {
|
|
1434
2611
|
marksRef.current = marks;
|
|
@@ -1471,6 +2648,64 @@ window.__ModuleLoader__.load({
|
|
|
1471
2648
|
window.addEventListener("hashchange", onHashChange);
|
|
1472
2649
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
1473
2650
|
}, []);
|
|
2651
|
+
(0, react.useLayoutEffect)(() => {
|
|
2652
|
+
if (marks.length < MIN_MARKS) {
|
|
2653
|
+
setRailBox(null);
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
2657
|
+
if (scrollport === null) return;
|
|
2658
|
+
const compute = () => {
|
|
2659
|
+
const sp = scrollport.getBoundingClientRect();
|
|
2660
|
+
setRailBox({
|
|
2661
|
+
top: sp.top,
|
|
2662
|
+
height: sp.height,
|
|
2663
|
+
right: Math.max(0, window.innerWidth - sp.right + inset),
|
|
2664
|
+
left: Math.max(0, sp.left + inset)
|
|
2665
|
+
});
|
|
2666
|
+
};
|
|
2667
|
+
compute();
|
|
2668
|
+
const observer = new ResizeObserver(compute);
|
|
2669
|
+
observer.observe(scrollport);
|
|
2670
|
+
window.addEventListener("resize", compute);
|
|
2671
|
+
return () => {
|
|
2672
|
+
observer.disconnect();
|
|
2673
|
+
window.removeEventListener("resize", compute);
|
|
2674
|
+
};
|
|
2675
|
+
}, [marks.length, inset]);
|
|
2676
|
+
(0, react.useLayoutEffect)(() => {
|
|
2677
|
+
setFocusIndex((f) => clampIndex(f, render.items.length));
|
|
2678
|
+
}, [render.items.length]);
|
|
2679
|
+
const lastBadge = (0, react.useMemo)(() => {
|
|
2680
|
+
if (displayMarks.length === 0) return null;
|
|
2681
|
+
const last = displayMarks[displayMarks.length - 1];
|
|
2682
|
+
return deriveBadge({
|
|
2683
|
+
nodeKinds: last.turn === void 0 ? NO_KINDS : kindsByTurn.get(last.turn) ?? NO_KINDS,
|
|
2684
|
+
lastMark: true,
|
|
2685
|
+
running,
|
|
2686
|
+
awaitingInput
|
|
2687
|
+
});
|
|
2688
|
+
}, [
|
|
2689
|
+
displayMarks,
|
|
2690
|
+
kindsByTurn,
|
|
2691
|
+
running,
|
|
2692
|
+
awaitingInput
|
|
2693
|
+
]);
|
|
2694
|
+
const pulseCss = (0, react.useMemo)(() => {
|
|
2695
|
+
if (lastBadge === null) return null;
|
|
2696
|
+
const style = badgeRingStyle(lastBadge);
|
|
2697
|
+
return style.pulse ? badgePulseCss(style.color) : null;
|
|
2698
|
+
}, [lastBadge]);
|
|
2699
|
+
(0, react.useEffect)(() => {
|
|
2700
|
+
if (!listOpen && !crossOpen) return;
|
|
2701
|
+
const onKey = (e) => {
|
|
2702
|
+
if (e.key !== "Escape") return;
|
|
2703
|
+
setListOpen(false);
|
|
2704
|
+
setCrossOpen(false);
|
|
2705
|
+
};
|
|
2706
|
+
window.addEventListener("keydown", onKey);
|
|
2707
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2708
|
+
}, [listOpen, crossOpen]);
|
|
1474
2709
|
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
1475
2710
|
const updateQuery = (query) => {
|
|
1476
2711
|
setSearch({
|
|
@@ -1507,6 +2742,279 @@ window.__ModuleLoader__.load({
|
|
|
1507
2742
|
if (e.key === "Enter") advanceMatch();
|
|
1508
2743
|
if (e.key === "Escape") closeSearch();
|
|
1509
2744
|
};
|
|
2745
|
+
/**
|
|
2746
|
+
* B-design: viewport `right` offset for the floating layers. On the classic
|
|
2747
|
+
* right-side rail the panels sit left of the rail (their right edge at
|
|
2748
|
+
* railBox.right + hit + 8); on a LEFT rail every layer flips to the rail's
|
|
2749
|
+
* OTHER side — its left edge at railBox.left + hit + 8, which means its
|
|
2750
|
+
* viewport `right` must be backed out from the (known) panel width.
|
|
2751
|
+
*/
|
|
2752
|
+
const panelRightFor = (panelWidth) => side === "left" ? window.innerWidth - (railBox.left + hit + 8 + panelWidth) : railBox.right + hit + 8;
|
|
2753
|
+
/** Close the settings modal via its backdrop/close button paths. */
|
|
2754
|
+
const closeSettings = () => {
|
|
2755
|
+
setSettingsOpen(false);
|
|
2756
|
+
settingsBtnRef.current?.focus();
|
|
2757
|
+
};
|
|
2758
|
+
/** B1: a feature renders while the toolbar is EXPANDED or while it is pinned. */
|
|
2759
|
+
const featureVisible = (id) => toolbarExpanded || pinned.includes(id);
|
|
2760
|
+
/** Base chrome-button style: accent-defined active tint, scaled hit area. */
|
|
2761
|
+
const chromeButtonStyle = (active) => ({
|
|
2762
|
+
width: hit,
|
|
2763
|
+
height: hit,
|
|
2764
|
+
flexShrink: 0,
|
|
2765
|
+
display: "flex",
|
|
2766
|
+
alignItems: "center",
|
|
2767
|
+
justifyContent: "center",
|
|
2768
|
+
background: active ? accentBg : "transparent",
|
|
2769
|
+
border: "none",
|
|
2770
|
+
padding: 0,
|
|
2771
|
+
cursor: "pointer",
|
|
2772
|
+
color: active ? accentSoft : "#8b96ab",
|
|
2773
|
+
transition: "background 120ms ease, color 120ms ease"
|
|
2774
|
+
});
|
|
2775
|
+
/**
|
|
2776
|
+
* B1: the data-driven feature registry. Each entry's render is the feature's
|
|
2777
|
+
* rail-top chrome (data attributes / aria semantics preserved), moved
|
|
2778
|
+
* verbatim from the previous static button block; `search` is the whole
|
|
2779
|
+
* RailSearchUi (toggle + panel) so its lifecycle stays component-local in
|
|
2780
|
+
* the rail (search state lives in the rail and survives unmount). `settings`
|
|
2781
|
+
* lives LAST in the queue — the gear is a regular, default-unpinned feature;
|
|
2782
|
+
* the modal must stay reachable via 展开→齿轮.
|
|
2783
|
+
* Registry order = settings-menu order (站内搜索/全部提问/跨会话搜索/只看收藏/聚焦模式/检查更新/设置).
|
|
2784
|
+
*
|
|
2785
|
+
* EXTENSION POINT: push a new feature here (+ its id in toolbar-prefs.ts's
|
|
2786
|
+
* TOOLBAR_PIN_IDS and its locale keys) and pinning/settings/expand all
|
|
2787
|
+
* follow automatically — see the ToolbarFeatureDef doc above.
|
|
2788
|
+
*/
|
|
2789
|
+
const toolbarFeatures = [
|
|
2790
|
+
{
|
|
2791
|
+
id: "search",
|
|
2792
|
+
labelKey: "search.label",
|
|
2793
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailSearchUi, {
|
|
2794
|
+
panelTop: railBox.top,
|
|
2795
|
+
panelRight: panelRightFor(PANEL_WIDTH_SEARCH),
|
|
2796
|
+
query: search.query,
|
|
2797
|
+
panelOpen: search.panelOpen,
|
|
2798
|
+
matches: matches.length,
|
|
2799
|
+
total: displayMarks.length,
|
|
2800
|
+
onToggle: () => setSearch((s) => ({
|
|
2801
|
+
...s,
|
|
2802
|
+
panelOpen: !s.panelOpen
|
|
2803
|
+
})),
|
|
2804
|
+
onQueryChange: updateQuery,
|
|
2805
|
+
onSearchKeyDown,
|
|
2806
|
+
onClear: clearSearch,
|
|
2807
|
+
t
|
|
2808
|
+
})
|
|
2809
|
+
},
|
|
2810
|
+
{
|
|
2811
|
+
id: "list",
|
|
2812
|
+
labelKey: "list.label",
|
|
2813
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2814
|
+
type: "button",
|
|
2815
|
+
"data-list-toggle": true,
|
|
2816
|
+
"aria-label": listOpen ? t("list.close") : t("list.open"),
|
|
2817
|
+
title: listOpen ? t("list.close") : t("list.open"),
|
|
2818
|
+
"aria-pressed": listOpen,
|
|
2819
|
+
onClick: () => setListOpen((v) => !v),
|
|
2820
|
+
style: chromeButtonStyle(listOpen),
|
|
2821
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2822
|
+
width: "16",
|
|
2823
|
+
height: "16",
|
|
2824
|
+
viewBox: "0 0 24 24",
|
|
2825
|
+
fill: "none",
|
|
2826
|
+
stroke: "currentColor",
|
|
2827
|
+
strokeWidth: "2.5",
|
|
2828
|
+
strokeLinecap: "round",
|
|
2829
|
+
"aria-hidden": "true",
|
|
2830
|
+
children: [
|
|
2831
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
2832
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h18" }),
|
|
2833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h18" })
|
|
2834
|
+
]
|
|
2835
|
+
})
|
|
2836
|
+
})
|
|
2837
|
+
},
|
|
2838
|
+
{
|
|
2839
|
+
id: "sessionSearch",
|
|
2840
|
+
labelKey: "search.cross",
|
|
2841
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2842
|
+
type: "button",
|
|
2843
|
+
"data-session-search-toggle": true,
|
|
2844
|
+
"aria-label": crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
2845
|
+
title: crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
2846
|
+
"aria-pressed": crossOpen,
|
|
2847
|
+
onClick: () => setCrossOpen((v) => !v),
|
|
2848
|
+
style: chromeButtonStyle(crossOpen),
|
|
2849
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2850
|
+
width: "16",
|
|
2851
|
+
height: "16",
|
|
2852
|
+
viewBox: "0 0 24 24",
|
|
2853
|
+
fill: "none",
|
|
2854
|
+
stroke: "currentColor",
|
|
2855
|
+
strokeWidth: "2",
|
|
2856
|
+
strokeLinecap: "round",
|
|
2857
|
+
strokeLinejoin: "round",
|
|
2858
|
+
"aria-hidden": "true",
|
|
2859
|
+
children: [
|
|
2860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h9" }),
|
|
2861
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h9" }),
|
|
2862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h9" }),
|
|
2863
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
2864
|
+
cx: "17",
|
|
2865
|
+
cy: "7",
|
|
2866
|
+
r: "3.5"
|
|
2867
|
+
}),
|
|
2868
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m19.5 9.5 2.5 2.5" })
|
|
2869
|
+
]
|
|
2870
|
+
})
|
|
2871
|
+
})
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
id: "bookmarks",
|
|
2875
|
+
labelKey: "bookmark.filter",
|
|
2876
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2877
|
+
type: "button",
|
|
2878
|
+
"data-bookmarks-toggle": true,
|
|
2879
|
+
"aria-label": t("bookmark.filter"),
|
|
2880
|
+
"aria-pressed": bookmarksOnly,
|
|
2881
|
+
"data-active": bookmarksOnly ? "true" : void 0,
|
|
2882
|
+
onClick: () => setBookmarksOnly((v) => !v),
|
|
2883
|
+
style: chromeButtonStyle(bookmarksOnly),
|
|
2884
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2885
|
+
width: "16",
|
|
2886
|
+
height: "16",
|
|
2887
|
+
viewBox: "0 0 24 24",
|
|
2888
|
+
fill: bookmarksOnly ? "currentColor" : "none",
|
|
2889
|
+
stroke: "currentColor",
|
|
2890
|
+
strokeWidth: "2",
|
|
2891
|
+
strokeLinejoin: "round",
|
|
2892
|
+
"aria-hidden": "true",
|
|
2893
|
+
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" })
|
|
2894
|
+
})
|
|
2895
|
+
})
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
id: "focus",
|
|
2899
|
+
labelKey: "focus.on",
|
|
2900
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2901
|
+
type: "button",
|
|
2902
|
+
"data-focus-toggle": true,
|
|
2903
|
+
"aria-label": focusActive ? t("focus.off") : t("focus.on"),
|
|
2904
|
+
title: focusActive ? t("focus.off") : t("focus.on"),
|
|
2905
|
+
"aria-pressed": focusActive,
|
|
2906
|
+
onClick: () => setFocusActive((v) => !v),
|
|
2907
|
+
style: chromeButtonStyle(focusActive),
|
|
2908
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2909
|
+
width: "16",
|
|
2910
|
+
height: "16",
|
|
2911
|
+
viewBox: "0 0 24 24",
|
|
2912
|
+
fill: "none",
|
|
2913
|
+
stroke: "currentColor",
|
|
2914
|
+
strokeWidth: "2",
|
|
2915
|
+
strokeLinecap: "round",
|
|
2916
|
+
strokeLinejoin: "round",
|
|
2917
|
+
"aria-hidden": "true",
|
|
2918
|
+
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", {
|
|
2919
|
+
cx: "12",
|
|
2920
|
+
cy: "12",
|
|
2921
|
+
r: "3"
|
|
2922
|
+
})]
|
|
2923
|
+
})
|
|
2924
|
+
})
|
|
2925
|
+
},
|
|
2926
|
+
{
|
|
2927
|
+
id: "updateCheck",
|
|
2928
|
+
labelKey: "update.check",
|
|
2929
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2930
|
+
type: "button",
|
|
2931
|
+
ref: updateBtnRef,
|
|
2932
|
+
"data-update-check": true,
|
|
2933
|
+
"aria-expanded": updateOpen,
|
|
2934
|
+
"aria-label": t("update.check"),
|
|
2935
|
+
title: t("update.check"),
|
|
2936
|
+
onClick: () => setUpdateOpen((v) => !v),
|
|
2937
|
+
style: {
|
|
2938
|
+
position: "relative",
|
|
2939
|
+
width: hit,
|
|
2940
|
+
height: hit,
|
|
2941
|
+
flexShrink: 0,
|
|
2942
|
+
display: "flex",
|
|
2943
|
+
alignItems: "center",
|
|
2944
|
+
justifyContent: "center",
|
|
2945
|
+
background: updateCheck.available ? "rgba(245, 197, 66, 0.14)" : updateOpen ? accentBg : "transparent",
|
|
2946
|
+
border: "none",
|
|
2947
|
+
padding: 0,
|
|
2948
|
+
cursor: "pointer",
|
|
2949
|
+
color: updateCheck.available ? "#f5c542" : updateOpen ? accentSoft : "#8b96ab"
|
|
2950
|
+
},
|
|
2951
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2952
|
+
width: "16",
|
|
2953
|
+
height: "16",
|
|
2954
|
+
viewBox: "0 0 24 24",
|
|
2955
|
+
fill: "none",
|
|
2956
|
+
stroke: "currentColor",
|
|
2957
|
+
strokeWidth: "2",
|
|
2958
|
+
strokeLinecap: "round",
|
|
2959
|
+
strokeLinejoin: "round",
|
|
2960
|
+
"aria-hidden": "true",
|
|
2961
|
+
children: [
|
|
2962
|
+
/* @__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" }),
|
|
2963
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 3v5h-5" }),
|
|
2964
|
+
/* @__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" }),
|
|
2965
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 16H3v5" })
|
|
2966
|
+
]
|
|
2967
|
+
}), updateCheck.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2968
|
+
"data-update-available": true,
|
|
2969
|
+
style: {
|
|
2970
|
+
position: "absolute",
|
|
2971
|
+
top: -2,
|
|
2972
|
+
right: -2,
|
|
2973
|
+
width: 8,
|
|
2974
|
+
height: 8,
|
|
2975
|
+
borderRadius: "50%",
|
|
2976
|
+
background: "#f5c542",
|
|
2977
|
+
border: "2px solid rgba(20, 24, 32, 0.95)",
|
|
2978
|
+
pointerEvents: "none"
|
|
2979
|
+
}
|
|
2980
|
+
})]
|
|
2981
|
+
})
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
id: "settings",
|
|
2985
|
+
labelKey: "settings.label",
|
|
2986
|
+
render: () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2987
|
+
type: "button",
|
|
2988
|
+
ref: settingsBtnRef,
|
|
2989
|
+
"data-toolbar-settings": true,
|
|
2990
|
+
"aria-pressed": settingsOpen,
|
|
2991
|
+
"aria-label": settingsOpen ? t("toolbar.settings.close") : t("toolbar.settings.open"),
|
|
2992
|
+
title: settingsOpen ? t("toolbar.settings.close") : t("toolbar.settings.open"),
|
|
2993
|
+
onClick: () => setSettingsOpen((v) => !v),
|
|
2994
|
+
onMouseEnter: () => setSettingsHovered(true),
|
|
2995
|
+
onMouseLeave: () => setSettingsHovered(false),
|
|
2996
|
+
onFocus: () => setSettingsHovered(true),
|
|
2997
|
+
onBlur: () => setSettingsHovered(false),
|
|
2998
|
+
style: chromeButtonStyle(settingsOpen || settingsHovered),
|
|
2999
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3000
|
+
width: "16",
|
|
3001
|
+
height: "16",
|
|
3002
|
+
viewBox: "0 0 24 24",
|
|
3003
|
+
fill: "none",
|
|
3004
|
+
stroke: "currentColor",
|
|
3005
|
+
strokeWidth: "2",
|
|
3006
|
+
strokeLinecap: "round",
|
|
3007
|
+
strokeLinejoin: "round",
|
|
3008
|
+
"aria-hidden": "true",
|
|
3009
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3010
|
+
cx: "12",
|
|
3011
|
+
cy: "12",
|
|
3012
|
+
r: "3"
|
|
3013
|
+
}), /* @__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" })]
|
|
3014
|
+
})
|
|
3015
|
+
})
|
|
3016
|
+
}
|
|
3017
|
+
];
|
|
1510
3018
|
/** Focus the dot at `index` (no-op while the list is unmounted). */
|
|
1511
3019
|
const focusDotAt = (index) => {
|
|
1512
3020
|
listRef.current?.querySelectorAll("[data-rail-dot]")[index]?.focus();
|
|
@@ -1622,143 +3130,78 @@ window.__ModuleLoader__.load({
|
|
|
1622
3130
|
forkAt(mark.seq).then(() => setForkedKey(mark.key));
|
|
1623
3131
|
};
|
|
1624
3132
|
const showLoadOlder = hasMore && marks.length >= MIN_MARKS;
|
|
3133
|
+
const railStyle = {
|
|
3134
|
+
position: "fixed",
|
|
3135
|
+
top: railBox.top,
|
|
3136
|
+
...side === "left" ? { left: railBox.left } : { right: railBox.right },
|
|
3137
|
+
height: railBox.height,
|
|
3138
|
+
width: hit,
|
|
3139
|
+
pointerEvents: "auto",
|
|
3140
|
+
zIndex: 100,
|
|
3141
|
+
display: "flex",
|
|
3142
|
+
flexDirection: "column",
|
|
3143
|
+
gap: 6,
|
|
3144
|
+
paddingTop: 6,
|
|
3145
|
+
"--ms-accent": accent,
|
|
3146
|
+
"--ms-accent-soft": accentSoft,
|
|
3147
|
+
"--ms-accent-bg": accentBg,
|
|
3148
|
+
"--ms-icon": `${iconSize}px`,
|
|
3149
|
+
"--ms-inset": `${inset}px`
|
|
3150
|
+
};
|
|
3151
|
+
const descKey = `settings.desc.${descFeature}`;
|
|
1625
3152
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1626
|
-
style:
|
|
1627
|
-
position: "fixed",
|
|
1628
|
-
top: railBox.top,
|
|
1629
|
-
right: railBox.right,
|
|
1630
|
-
height: railBox.height,
|
|
1631
|
-
width: DOT_HIT,
|
|
1632
|
-
pointerEvents: "auto",
|
|
1633
|
-
zIndex: 100,
|
|
1634
|
-
display: "flex",
|
|
1635
|
-
flexDirection: "column",
|
|
1636
|
-
gap: 6,
|
|
1637
|
-
paddingTop: 6
|
|
1638
|
-
},
|
|
3153
|
+
style: railStyle,
|
|
1639
3154
|
"aria-label": t("rail.label"),
|
|
1640
3155
|
"data-focus-active": focusActive ? "true" : void 0,
|
|
3156
|
+
"data-accent": accent,
|
|
3157
|
+
"data-side": side,
|
|
3158
|
+
"data-icon-size": String(iconSize),
|
|
3159
|
+
"data-inset": String(inset),
|
|
1641
3160
|
children: [
|
|
1642
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children:
|
|
3161
|
+
pulseCss !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: pulseCss }),
|
|
1643
3162
|
focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: FOCUS_CSS }),
|
|
3163
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: MODAL_CSS }),
|
|
1644
3164
|
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1645
|
-
type: "button",
|
|
1646
|
-
"data-load-older": true,
|
|
1647
|
-
"data-loading-older": loadingOlder ? "true" : void 0,
|
|
1648
|
-
title: t("load.older"),
|
|
1649
|
-
"aria-label": t("load.older"),
|
|
1650
|
-
disabled: loadingOlder,
|
|
1651
|
-
onClick: () => {
|
|
1652
|
-
loadOlder();
|
|
1653
|
-
},
|
|
1654
|
-
style: {
|
|
1655
|
-
width: DOT_HIT,
|
|
1656
|
-
height: DOT_HIT,
|
|
1657
|
-
flexShrink: 0,
|
|
1658
|
-
display: "flex",
|
|
1659
|
-
alignItems: "center",
|
|
1660
|
-
justifyContent: "center",
|
|
1661
|
-
background: "transparent",
|
|
1662
|
-
border: "none",
|
|
1663
|
-
padding: 0,
|
|
1664
|
-
cursor: loadingOlder ? "default" : "pointer",
|
|
1665
|
-
color: loadingOlder ? "#5a6375" : "#8b96ab",
|
|
1666
|
-
fontSize: 13,
|
|
1667
|
-
lineHeight: 1,
|
|
1668
|
-
letterSpacing: 1
|
|
1669
|
-
},
|
|
1670
|
-
children: "···"
|
|
1671
|
-
}),
|
|
1672
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1673
|
-
type: "button",
|
|
1674
|
-
"data-bookmarks-toggle": true,
|
|
1675
|
-
"aria-label": t("bookmark.filter"),
|
|
1676
|
-
"aria-pressed": bookmarksOnly,
|
|
1677
|
-
"data-active": bookmarksOnly ? "true" : void 0,
|
|
1678
|
-
onClick: () => setBookmarksOnly((v) => !v),
|
|
1679
|
-
style: {
|
|
1680
|
-
width: DOT_HIT,
|
|
1681
|
-
height: DOT_HIT,
|
|
1682
|
-
flexShrink: 0,
|
|
1683
|
-
display: "flex",
|
|
1684
|
-
alignItems: "center",
|
|
1685
|
-
justifyContent: "center",
|
|
1686
|
-
background: bookmarksOnly ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
1687
|
-
border: "none",
|
|
1688
|
-
padding: 0,
|
|
1689
|
-
cursor: "pointer",
|
|
1690
|
-
color: bookmarksOnly ? "#9db8ff" : "#8b96ab"
|
|
1691
|
-
},
|
|
1692
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1693
|
-
width: "16",
|
|
1694
|
-
height: "16",
|
|
1695
|
-
viewBox: "0 0 24 24",
|
|
1696
|
-
fill: bookmarksOnly ? "currentColor" : "none",
|
|
1697
|
-
stroke: "currentColor",
|
|
1698
|
-
strokeWidth: "2",
|
|
1699
|
-
strokeLinejoin: "round",
|
|
1700
|
-
"aria-hidden": "true",
|
|
1701
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" })
|
|
1702
|
-
})
|
|
1703
|
-
}),
|
|
1704
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1705
|
-
type: "button",
|
|
1706
|
-
"data-focus-toggle": true,
|
|
1707
|
-
"aria-label": focusActive ? t("focus.off") : t("focus.on"),
|
|
1708
|
-
title: focusActive ? t("focus.off") : t("focus.on"),
|
|
1709
|
-
"aria-pressed": focusActive,
|
|
1710
|
-
onClick: () => setFocusActive((v) => !v),
|
|
1711
|
-
style: {
|
|
1712
|
-
width: DOT_HIT,
|
|
1713
|
-
height: DOT_HIT,
|
|
1714
|
-
flexShrink: 0,
|
|
1715
|
-
display: "flex",
|
|
1716
|
-
alignItems: "center",
|
|
1717
|
-
justifyContent: "center",
|
|
1718
|
-
background: focusActive ? "rgba(126, 226, 168, 0.14)" : "transparent",
|
|
1719
|
-
border: "none",
|
|
1720
|
-
padding: 0,
|
|
1721
|
-
cursor: "pointer",
|
|
1722
|
-
color: focusActive ? "#7ee2a8" : "#8b96ab"
|
|
1723
|
-
},
|
|
1724
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1725
|
-
width: "16",
|
|
1726
|
-
height: "16",
|
|
1727
|
-
viewBox: "0 0 24 24",
|
|
1728
|
-
fill: "none",
|
|
1729
|
-
stroke: "currentColor",
|
|
1730
|
-
strokeWidth: "2",
|
|
1731
|
-
strokeLinecap: "round",
|
|
1732
|
-
strokeLinejoin: "round",
|
|
1733
|
-
"aria-hidden": "true",
|
|
1734
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1735
|
-
cx: "12",
|
|
1736
|
-
cy: "12",
|
|
1737
|
-
r: "3"
|
|
1738
|
-
})]
|
|
1739
|
-
})
|
|
1740
|
-
}),
|
|
1741
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1742
|
-
type: "button",
|
|
1743
|
-
"data-list-toggle": true,
|
|
1744
|
-
"aria-label": listOpen ? t("list.close") : t("list.open"),
|
|
1745
|
-
title: listOpen ? t("list.close") : t("list.open"),
|
|
1746
|
-
"aria-pressed": listOpen,
|
|
1747
|
-
onClick: () => setListOpen((v) => !v),
|
|
3165
|
+
type: "button",
|
|
3166
|
+
"data-load-older": true,
|
|
3167
|
+
"data-loading-older": loadingOlder ? "true" : void 0,
|
|
3168
|
+
title: t("load.older"),
|
|
3169
|
+
"aria-label": t("load.older"),
|
|
3170
|
+
disabled: loadingOlder,
|
|
3171
|
+
onClick: () => {
|
|
3172
|
+
loadOlder();
|
|
3173
|
+
},
|
|
1748
3174
|
style: {
|
|
1749
|
-
width:
|
|
1750
|
-
height:
|
|
3175
|
+
width: hit,
|
|
3176
|
+
height: hit,
|
|
1751
3177
|
flexShrink: 0,
|
|
1752
3178
|
display: "flex",
|
|
1753
3179
|
alignItems: "center",
|
|
1754
3180
|
justifyContent: "center",
|
|
1755
|
-
background:
|
|
3181
|
+
background: "transparent",
|
|
1756
3182
|
border: "none",
|
|
1757
3183
|
padding: 0,
|
|
1758
|
-
cursor: "pointer",
|
|
1759
|
-
color:
|
|
3184
|
+
cursor: loadingOlder ? "default" : "pointer",
|
|
3185
|
+
color: loadingOlder ? "#5a6375" : "#8b96ab",
|
|
3186
|
+
fontSize: 13,
|
|
3187
|
+
lineHeight: 1,
|
|
3188
|
+
letterSpacing: 1
|
|
1760
3189
|
},
|
|
1761
|
-
children:
|
|
3190
|
+
children: "···"
|
|
3191
|
+
}),
|
|
3192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3193
|
+
type: "button",
|
|
3194
|
+
"data-toolbar-expand": true,
|
|
3195
|
+
"aria-expanded": toolbarExpanded,
|
|
3196
|
+
"aria-label": toolbarExpanded ? t("toolbar.collapse") : t("toolbar.expand"),
|
|
3197
|
+
title: toolbarExpanded ? t("toolbar.collapse") : t("toolbar.expand"),
|
|
3198
|
+
onClick: () => setToolbarExpanded((v) => !v),
|
|
3199
|
+
onMouseEnter: () => setExpandHovered(true),
|
|
3200
|
+
onMouseLeave: () => setExpandHovered(false),
|
|
3201
|
+
onFocus: () => setExpandHovered(true),
|
|
3202
|
+
onBlur: () => setExpandHovered(false),
|
|
3203
|
+
style: chromeButtonStyle(expandHovered),
|
|
3204
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1762
3205
|
width: "16",
|
|
1763
3206
|
height: "16",
|
|
1764
3207
|
viewBox: "0 0 24 24",
|
|
@@ -1766,83 +3209,728 @@ window.__ModuleLoader__.load({
|
|
|
1766
3209
|
stroke: "currentColor",
|
|
1767
3210
|
strokeWidth: "2.5",
|
|
1768
3211
|
strokeLinecap: "round",
|
|
3212
|
+
strokeLinejoin: "round",
|
|
1769
3213
|
"aria-hidden": "true",
|
|
1770
|
-
children:
|
|
1771
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
1772
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h18" }),
|
|
1773
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h18" })
|
|
1774
|
-
]
|
|
3214
|
+
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" })
|
|
1775
3215
|
})
|
|
1776
3216
|
}),
|
|
1777
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1778
|
-
|
|
1779
|
-
"data-
|
|
1780
|
-
|
|
1781
|
-
title: crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
1782
|
-
"aria-pressed": crossOpen,
|
|
1783
|
-
onClick: () => setCrossOpen((v) => !v),
|
|
3217
|
+
toolbarFeatures.map((feature) => featureVisible(feature.id) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: feature.render() }, feature.id) : null),
|
|
3218
|
+
settingsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3219
|
+
"data-toolbar-settings-overlay": true,
|
|
3220
|
+
onClick: closeSettings,
|
|
1784
3221
|
style: {
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
3222
|
+
position: "fixed",
|
|
3223
|
+
inset: 0,
|
|
3224
|
+
background: "rgba(8, 10, 15, 0.55)",
|
|
3225
|
+
zIndex: 105,
|
|
1788
3226
|
display: "flex",
|
|
1789
3227
|
alignItems: "center",
|
|
1790
3228
|
justifyContent: "center",
|
|
1791
|
-
|
|
1792
|
-
border: "none",
|
|
1793
|
-
padding: 0,
|
|
1794
|
-
cursor: "pointer",
|
|
1795
|
-
color: crossOpen ? "#9db8ff" : "#8b96ab"
|
|
3229
|
+
padding: 16
|
|
1796
3230
|
},
|
|
1797
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
3231
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3232
|
+
ref: settingsRef,
|
|
3233
|
+
"data-toolbar-settings-panel": true,
|
|
3234
|
+
role: "dialog",
|
|
3235
|
+
"aria-modal": "true",
|
|
3236
|
+
"aria-label": t("settings.title"),
|
|
3237
|
+
onClick: (e) => e.stopPropagation(),
|
|
3238
|
+
style: {
|
|
3239
|
+
width: "min(600px, 92vw)",
|
|
3240
|
+
maxHeight: "78vh",
|
|
3241
|
+
overflowY: "auto",
|
|
3242
|
+
padding: "16px 18px",
|
|
3243
|
+
background: "rgba(20, 24, 32, 0.98)",
|
|
3244
|
+
color: "#e6e8ee",
|
|
3245
|
+
borderRadius: 12,
|
|
3246
|
+
boxShadow: "0 20px 60px rgba(0, 0, 0, 0.5)"
|
|
3247
|
+
},
|
|
1807
3248
|
children: [
|
|
1808
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
3249
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3250
|
+
style: {
|
|
3251
|
+
display: "flex",
|
|
3252
|
+
alignItems: "center",
|
|
3253
|
+
justifyContent: "space-between",
|
|
3254
|
+
gap: 8,
|
|
3255
|
+
marginBottom: 12
|
|
3256
|
+
},
|
|
3257
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3258
|
+
"data-toolbar-settings-title": true,
|
|
3259
|
+
style: {
|
|
3260
|
+
fontSize: 15,
|
|
3261
|
+
fontWeight: 600,
|
|
3262
|
+
color: "#e6e8ee"
|
|
3263
|
+
},
|
|
3264
|
+
children: t("settings.title")
|
|
3265
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3266
|
+
type: "button",
|
|
3267
|
+
"data-toolbar-settings-close": true,
|
|
3268
|
+
"aria-label": t("settings.close"),
|
|
3269
|
+
title: t("settings.close"),
|
|
3270
|
+
onClick: closeSettings,
|
|
3271
|
+
style: {
|
|
3272
|
+
width: 26,
|
|
3273
|
+
height: 26,
|
|
3274
|
+
flexShrink: 0,
|
|
3275
|
+
display: "flex",
|
|
3276
|
+
alignItems: "center",
|
|
3277
|
+
justifyContent: "center",
|
|
3278
|
+
background: "transparent",
|
|
3279
|
+
border: "none",
|
|
3280
|
+
padding: 0,
|
|
3281
|
+
cursor: "pointer",
|
|
3282
|
+
color: "#8b96ab",
|
|
3283
|
+
borderRadius: 6,
|
|
3284
|
+
fontSize: 14,
|
|
3285
|
+
lineHeight: 1
|
|
3286
|
+
},
|
|
3287
|
+
children: "✕"
|
|
3288
|
+
})]
|
|
1815
3289
|
}),
|
|
1816
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3290
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3291
|
+
"data-settings-section": true,
|
|
3292
|
+
style: { marginBottom: 14 },
|
|
3293
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3294
|
+
"data-settings-section-title": true,
|
|
3295
|
+
style: {
|
|
3296
|
+
fontSize: 13,
|
|
3297
|
+
fontWeight: 600,
|
|
3298
|
+
color: "#c7cede",
|
|
3299
|
+
marginBottom: 8
|
|
3300
|
+
},
|
|
3301
|
+
children: t("settings.section.features")
|
|
3302
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3303
|
+
style: {
|
|
3304
|
+
display: "grid",
|
|
3305
|
+
gridTemplateColumns: "minmax(0, 1fr) 190px",
|
|
3306
|
+
gap: 12,
|
|
3307
|
+
alignItems: "stretch"
|
|
3308
|
+
},
|
|
3309
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3310
|
+
style: {
|
|
3311
|
+
display: "flex",
|
|
3312
|
+
flexDirection: "column",
|
|
3313
|
+
gap: 2
|
|
3314
|
+
},
|
|
3315
|
+
children: toolbarFeatures.map((feature) => {
|
|
3316
|
+
const checked = pinned.includes(feature.id);
|
|
3317
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3318
|
+
type: "button",
|
|
3319
|
+
role: "switch",
|
|
3320
|
+
"data-toolbar-pin-toggle": true,
|
|
3321
|
+
"data-pin-id": feature.id,
|
|
3322
|
+
"aria-checked": checked,
|
|
3323
|
+
"aria-label": t(feature.labelKey),
|
|
3324
|
+
onMouseEnter: () => setDescFeature(feature.id),
|
|
3325
|
+
onFocus: () => setDescFeature(feature.id),
|
|
3326
|
+
onClick: () => onTogglePin(feature.id),
|
|
3327
|
+
style: {
|
|
3328
|
+
display: "flex",
|
|
3329
|
+
alignItems: "center",
|
|
3330
|
+
justifyContent: "space-between",
|
|
3331
|
+
gap: 10,
|
|
3332
|
+
width: "100%",
|
|
3333
|
+
padding: "7px 10px",
|
|
3334
|
+
background: "transparent",
|
|
3335
|
+
border: "none",
|
|
3336
|
+
borderRadius: 8,
|
|
3337
|
+
cursor: "pointer",
|
|
3338
|
+
color: "#e6e8ee",
|
|
3339
|
+
fontSize: 13,
|
|
3340
|
+
textAlign: "left"
|
|
3341
|
+
},
|
|
3342
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(feature.labelKey) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3343
|
+
"aria-hidden": "true",
|
|
3344
|
+
style: {
|
|
3345
|
+
position: "relative",
|
|
3346
|
+
width: 32,
|
|
3347
|
+
height: 18,
|
|
3348
|
+
flexShrink: 0,
|
|
3349
|
+
borderRadius: 9,
|
|
3350
|
+
background: checked ? accentStrong : "rgba(255, 255, 255, 0.16)",
|
|
3351
|
+
transition: "background 120ms ease"
|
|
3352
|
+
},
|
|
3353
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
|
|
3354
|
+
position: "absolute",
|
|
3355
|
+
top: 2,
|
|
3356
|
+
left: checked ? 16 : 2,
|
|
3357
|
+
width: 14,
|
|
3358
|
+
height: 14,
|
|
3359
|
+
borderRadius: "50%",
|
|
3360
|
+
background: "#ffffff",
|
|
3361
|
+
transition: "left 120ms ease"
|
|
3362
|
+
} })
|
|
3363
|
+
})]
|
|
3364
|
+
}, feature.id);
|
|
3365
|
+
})
|
|
3366
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3367
|
+
"data-settings-desc": true,
|
|
3368
|
+
"aria-live": "polite",
|
|
3369
|
+
style: {
|
|
3370
|
+
display: "flex",
|
|
3371
|
+
alignItems: "center",
|
|
3372
|
+
padding: "10px 12px",
|
|
3373
|
+
borderRadius: 8,
|
|
3374
|
+
background: "rgba(255, 255, 255, 0.05)",
|
|
3375
|
+
color: "#b9c2d4",
|
|
3376
|
+
fontSize: 12.5,
|
|
3377
|
+
lineHeight: 1.5
|
|
3378
|
+
},
|
|
3379
|
+
children: t(descKey)
|
|
3380
|
+
})]
|
|
3381
|
+
})]
|
|
3382
|
+
}),
|
|
3383
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3384
|
+
"data-settings-section": true,
|
|
3385
|
+
"data-settings-personal": true,
|
|
3386
|
+
style: { marginBottom: 14 },
|
|
3387
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3388
|
+
"data-settings-section-title": true,
|
|
3389
|
+
style: {
|
|
3390
|
+
fontSize: 13,
|
|
3391
|
+
fontWeight: 600,
|
|
3392
|
+
color: "#c7cede",
|
|
3393
|
+
marginBottom: 8
|
|
3394
|
+
},
|
|
3395
|
+
children: t("settings.section.personal")
|
|
3396
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3397
|
+
style: {
|
|
3398
|
+
display: "flex",
|
|
3399
|
+
flexDirection: "column",
|
|
3400
|
+
gap: 10
|
|
3401
|
+
},
|
|
3402
|
+
children: [
|
|
3403
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3404
|
+
style: {
|
|
3405
|
+
display: "flex",
|
|
3406
|
+
alignItems: "center",
|
|
3407
|
+
gap: 10,
|
|
3408
|
+
flexWrap: "wrap"
|
|
3409
|
+
},
|
|
3410
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3411
|
+
style: {
|
|
3412
|
+
fontSize: 12.5,
|
|
3413
|
+
color: "#8b96ab",
|
|
3414
|
+
width: 90,
|
|
3415
|
+
flexShrink: 0
|
|
3416
|
+
},
|
|
3417
|
+
children: t("settings.accent")
|
|
3418
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3419
|
+
"data-accent-swatches": true,
|
|
3420
|
+
style: {
|
|
3421
|
+
display: "flex",
|
|
3422
|
+
alignItems: "center",
|
|
3423
|
+
gap: 6,
|
|
3424
|
+
flexWrap: "wrap"
|
|
3425
|
+
},
|
|
3426
|
+
children: [ACCENT_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3427
|
+
type: "button",
|
|
3428
|
+
"data-accent-swatch": true,
|
|
3429
|
+
"data-accent": preset,
|
|
3430
|
+
"aria-label": preset,
|
|
3431
|
+
"aria-pressed": accent === preset,
|
|
3432
|
+
onClick: () => updatePrefs({ accent: preset }),
|
|
3433
|
+
style: {
|
|
3434
|
+
width: 22,
|
|
3435
|
+
height: 22,
|
|
3436
|
+
borderRadius: "50%",
|
|
3437
|
+
background: preset,
|
|
3438
|
+
border: accent === preset ? "2px solid #ffffff" : "2px solid rgba(255, 255, 255, 0.25)",
|
|
3439
|
+
boxShadow: accent === preset ? `0 0 0 2px ${preset}` : "none",
|
|
3440
|
+
padding: 0,
|
|
3441
|
+
cursor: "pointer",
|
|
3442
|
+
transition: "border-color 120ms ease, box-shadow 120ms ease"
|
|
3443
|
+
}
|
|
3444
|
+
}, preset)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3445
|
+
"data-accent-custom": true,
|
|
3446
|
+
style: {
|
|
3447
|
+
display: "inline-flex",
|
|
3448
|
+
alignItems: "center",
|
|
3449
|
+
gap: 6,
|
|
3450
|
+
fontSize: 12.5,
|
|
3451
|
+
color: "#b9c2d4",
|
|
3452
|
+
cursor: "pointer"
|
|
3453
|
+
},
|
|
3454
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3455
|
+
type: "color",
|
|
3456
|
+
value: accent,
|
|
3457
|
+
onChange: (e) => updatePrefs({ accent: e.target.value }),
|
|
3458
|
+
"aria-label": `${t("settings.custom")} ${t("settings.accent")}`,
|
|
3459
|
+
style: {
|
|
3460
|
+
width: 26,
|
|
3461
|
+
height: 26,
|
|
3462
|
+
padding: 0,
|
|
3463
|
+
border: "none",
|
|
3464
|
+
background: "transparent",
|
|
3465
|
+
cursor: "pointer"
|
|
3466
|
+
}
|
|
3467
|
+
}), t("settings.custom")]
|
|
3468
|
+
})]
|
|
3469
|
+
})]
|
|
3470
|
+
}),
|
|
3471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3472
|
+
style: {
|
|
3473
|
+
display: "flex",
|
|
3474
|
+
alignItems: "center",
|
|
3475
|
+
gap: 10,
|
|
3476
|
+
flexWrap: "wrap"
|
|
3477
|
+
},
|
|
3478
|
+
children: [
|
|
3479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3480
|
+
style: {
|
|
3481
|
+
fontSize: 12.5,
|
|
3482
|
+
color: "#8b96ab",
|
|
3483
|
+
width: 90,
|
|
3484
|
+
flexShrink: 0
|
|
3485
|
+
},
|
|
3486
|
+
children: t("settings.iconSize")
|
|
3487
|
+
}),
|
|
3488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3489
|
+
type: "range",
|
|
3490
|
+
"data-icon-size": true,
|
|
3491
|
+
min: 20,
|
|
3492
|
+
max: 36,
|
|
3493
|
+
step: 2,
|
|
3494
|
+
value: iconSize,
|
|
3495
|
+
onChange: (e) => updatePrefs({ iconSize: Number(e.target.value) }),
|
|
3496
|
+
style: {
|
|
3497
|
+
flex: 1,
|
|
3498
|
+
minWidth: 140,
|
|
3499
|
+
maxWidth: 260
|
|
3500
|
+
}
|
|
3501
|
+
}),
|
|
3502
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3503
|
+
"data-icon-size-value": true,
|
|
3504
|
+
style: {
|
|
3505
|
+
fontSize: 12.5,
|
|
3506
|
+
color: "#b9c2d4",
|
|
3507
|
+
width: 40
|
|
3508
|
+
},
|
|
3509
|
+
children: [iconSize, "px"]
|
|
3510
|
+
})
|
|
3511
|
+
]
|
|
3512
|
+
}),
|
|
3513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3514
|
+
style: {
|
|
3515
|
+
display: "flex",
|
|
3516
|
+
alignItems: "center",
|
|
3517
|
+
gap: 10,
|
|
3518
|
+
flexWrap: "wrap"
|
|
3519
|
+
},
|
|
3520
|
+
children: [
|
|
3521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3522
|
+
style: {
|
|
3523
|
+
fontSize: 12.5,
|
|
3524
|
+
color: "#8b96ab",
|
|
3525
|
+
width: 90,
|
|
3526
|
+
flexShrink: 0
|
|
3527
|
+
},
|
|
3528
|
+
children: t("settings.inset")
|
|
3529
|
+
}),
|
|
3530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3531
|
+
type: "range",
|
|
3532
|
+
"data-inset": true,
|
|
3533
|
+
min: 0,
|
|
3534
|
+
max: 40,
|
|
3535
|
+
step: 2,
|
|
3536
|
+
value: inset,
|
|
3537
|
+
onChange: (e) => updatePrefs({ inset: Number(e.target.value) }),
|
|
3538
|
+
style: {
|
|
3539
|
+
flex: 1,
|
|
3540
|
+
minWidth: 140,
|
|
3541
|
+
maxWidth: 260
|
|
3542
|
+
}
|
|
3543
|
+
}),
|
|
3544
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3545
|
+
"data-inset-value": true,
|
|
3546
|
+
style: {
|
|
3547
|
+
fontSize: 12.5,
|
|
3548
|
+
color: "#b9c2d4",
|
|
3549
|
+
width: 40
|
|
3550
|
+
},
|
|
3551
|
+
children: [inset, "px"]
|
|
3552
|
+
})
|
|
3553
|
+
]
|
|
3554
|
+
}),
|
|
3555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3556
|
+
role: "radiogroup",
|
|
3557
|
+
"aria-label": t("settings.side"),
|
|
3558
|
+
style: {
|
|
3559
|
+
display: "flex",
|
|
3560
|
+
alignItems: "center",
|
|
3561
|
+
gap: 10,
|
|
3562
|
+
flexWrap: "wrap"
|
|
3563
|
+
},
|
|
3564
|
+
children: [
|
|
3565
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3566
|
+
style: {
|
|
3567
|
+
fontSize: 12.5,
|
|
3568
|
+
color: "#8b96ab",
|
|
3569
|
+
width: 90,
|
|
3570
|
+
flexShrink: 0
|
|
3571
|
+
},
|
|
3572
|
+
children: t("settings.side")
|
|
3573
|
+
}),
|
|
3574
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3575
|
+
style: {
|
|
3576
|
+
display: "inline-flex",
|
|
3577
|
+
alignItems: "center",
|
|
3578
|
+
gap: 5,
|
|
3579
|
+
fontSize: 13,
|
|
3580
|
+
color: "#e6e8ee",
|
|
3581
|
+
cursor: "pointer"
|
|
3582
|
+
},
|
|
3583
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3584
|
+
type: "radio",
|
|
3585
|
+
name: "ms-rail-side",
|
|
3586
|
+
"data-side-radio": true,
|
|
3587
|
+
value: "left",
|
|
3588
|
+
checked: side === "left",
|
|
3589
|
+
onChange: () => updatePrefs({ side: "left" })
|
|
3590
|
+
}), t("settings.side.left")]
|
|
3591
|
+
}),
|
|
3592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3593
|
+
style: {
|
|
3594
|
+
display: "inline-flex",
|
|
3595
|
+
alignItems: "center",
|
|
3596
|
+
gap: 5,
|
|
3597
|
+
fontSize: 13,
|
|
3598
|
+
color: "#e6e8ee",
|
|
3599
|
+
cursor: "pointer"
|
|
3600
|
+
},
|
|
3601
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3602
|
+
type: "radio",
|
|
3603
|
+
name: "ms-rail-side",
|
|
3604
|
+
"data-side-radio": true,
|
|
3605
|
+
value: "right",
|
|
3606
|
+
checked: side === "right",
|
|
3607
|
+
onChange: () => updatePrefs({ side: "right" })
|
|
3608
|
+
}), t("settings.side.right")]
|
|
3609
|
+
})
|
|
3610
|
+
]
|
|
3611
|
+
})
|
|
3612
|
+
]
|
|
3613
|
+
})]
|
|
3614
|
+
}),
|
|
3615
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3616
|
+
"data-settings-section": true,
|
|
3617
|
+
"data-settings-lang": true,
|
|
3618
|
+
style: { marginBottom: 14 },
|
|
3619
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3620
|
+
"data-settings-section-title": true,
|
|
3621
|
+
style: {
|
|
3622
|
+
fontSize: 13,
|
|
3623
|
+
fontWeight: 600,
|
|
3624
|
+
color: "#c7cede",
|
|
3625
|
+
marginBottom: 8
|
|
3626
|
+
},
|
|
3627
|
+
children: t("settings.language")
|
|
3628
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3629
|
+
role: "radiogroup",
|
|
3630
|
+
"aria-label": t("settings.language"),
|
|
3631
|
+
style: {
|
|
3632
|
+
display: "flex",
|
|
3633
|
+
alignItems: "center",
|
|
3634
|
+
gap: 18,
|
|
3635
|
+
flexWrap: "wrap"
|
|
3636
|
+
},
|
|
3637
|
+
children: [
|
|
3638
|
+
"system",
|
|
3639
|
+
"zh",
|
|
3640
|
+
"en"
|
|
3641
|
+
].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
3642
|
+
style: {
|
|
3643
|
+
display: "inline-flex",
|
|
3644
|
+
alignItems: "center",
|
|
3645
|
+
gap: 5,
|
|
3646
|
+
fontSize: 13,
|
|
3647
|
+
color: "#e6e8ee",
|
|
3648
|
+
cursor: "pointer"
|
|
3649
|
+
},
|
|
3650
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3651
|
+
type: "radio",
|
|
3652
|
+
name: "ms-rail-locale",
|
|
3653
|
+
"data-locale-pref": true,
|
|
3654
|
+
value,
|
|
3655
|
+
checked: prefs.locale === value,
|
|
3656
|
+
onChange: () => updatePrefs({ locale: value })
|
|
3657
|
+
}), t(`settings.lang.${value}`)]
|
|
3658
|
+
}, value))
|
|
3659
|
+
})]
|
|
3660
|
+
}),
|
|
3661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3662
|
+
"data-toolbar-settings-footer": true,
|
|
3663
|
+
style: { textAlign: "center" },
|
|
3664
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3665
|
+
style: {
|
|
3666
|
+
fontSize: 12,
|
|
3667
|
+
color: "#8b96ab",
|
|
3668
|
+
marginBottom: 10
|
|
3669
|
+
},
|
|
3670
|
+
children: t("settings.support")
|
|
3671
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3672
|
+
"data-support-grid": true,
|
|
3673
|
+
style: {
|
|
3674
|
+
display: "grid",
|
|
3675
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
3676
|
+
gap: 10,
|
|
3677
|
+
maxWidth: 460,
|
|
3678
|
+
margin: "0 auto"
|
|
3679
|
+
},
|
|
3680
|
+
children: [
|
|
3681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
3682
|
+
href: "https://github.com/SnowCrescenter-tech/dsh-milestone",
|
|
3683
|
+
target: "_blank",
|
|
3684
|
+
rel: "noreferrer",
|
|
3685
|
+
"data-support-card": true,
|
|
3686
|
+
"data-card": "repo",
|
|
3687
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3688
|
+
width: "14",
|
|
3689
|
+
height: "14",
|
|
3690
|
+
viewBox: "0 0 24 24",
|
|
3691
|
+
fill: "none",
|
|
3692
|
+
stroke: "currentColor",
|
|
3693
|
+
strokeWidth: "2",
|
|
3694
|
+
strokeLinecap: "round",
|
|
3695
|
+
strokeLinejoin: "round",
|
|
3696
|
+
"aria-hidden": "true",
|
|
3697
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" })
|
|
3698
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.repo") })]
|
|
3699
|
+
}),
|
|
3700
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
3701
|
+
href: "https://github.com/SnowCrescenter-tech/dsh-milestone",
|
|
3702
|
+
target: "_blank",
|
|
3703
|
+
rel: "noreferrer",
|
|
3704
|
+
"data-support-card": true,
|
|
3705
|
+
"data-card": "star",
|
|
3706
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3707
|
+
width: "14",
|
|
3708
|
+
height: "14",
|
|
3709
|
+
viewBox: "0 0 24 24",
|
|
3710
|
+
fill: "currentColor",
|
|
3711
|
+
stroke: "none",
|
|
3712
|
+
"aria-hidden": "true",
|
|
3713
|
+
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" })
|
|
3714
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.star") })]
|
|
3715
|
+
}),
|
|
3716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
3717
|
+
href: `https://github.com/SnowCrescenter-tech/dsh-milestone/issues`,
|
|
3718
|
+
target: "_blank",
|
|
3719
|
+
rel: "noreferrer",
|
|
3720
|
+
"data-support-card": true,
|
|
3721
|
+
"data-card": "issues",
|
|
3722
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3723
|
+
width: "14",
|
|
3724
|
+
height: "14",
|
|
3725
|
+
viewBox: "0 0 24 24",
|
|
3726
|
+
fill: "none",
|
|
3727
|
+
stroke: "currentColor",
|
|
3728
|
+
strokeWidth: "2",
|
|
3729
|
+
"aria-hidden": "true",
|
|
3730
|
+
children: [
|
|
3731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3732
|
+
cx: "12",
|
|
3733
|
+
cy: "12",
|
|
3734
|
+
r: "9"
|
|
3735
|
+
}),
|
|
3736
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
3737
|
+
d: "M12 8v4",
|
|
3738
|
+
strokeLinecap: "round"
|
|
3739
|
+
}),
|
|
3740
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3741
|
+
cx: "12",
|
|
3742
|
+
cy: "16",
|
|
3743
|
+
r: "0.5",
|
|
3744
|
+
fill: "currentColor"
|
|
3745
|
+
})
|
|
3746
|
+
]
|
|
3747
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.issues") })]
|
|
3748
|
+
}),
|
|
3749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
3750
|
+
href: "https://www.npmjs.com/package/dsh-milestone",
|
|
3751
|
+
target: "_blank",
|
|
3752
|
+
rel: "noreferrer",
|
|
3753
|
+
"data-support-card": true,
|
|
3754
|
+
"data-card": "npm",
|
|
3755
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3756
|
+
width: "14",
|
|
3757
|
+
height: "14",
|
|
3758
|
+
viewBox: "0 0 24 24",
|
|
3759
|
+
fill: "currentColor",
|
|
3760
|
+
stroke: "none",
|
|
3761
|
+
"aria-hidden": "true",
|
|
3762
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 8.5h20V15h-6v2.5h-3V15H2V8.5zm1.5 1.5v3.5H6V11.5h1.5v3.5h1.5V10h-4.5zm6 0v5h3V13h2v2h1.5v-5h-6.5z" })
|
|
3763
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settings.npm") })]
|
|
3764
|
+
})
|
|
3765
|
+
]
|
|
3766
|
+
})]
|
|
3767
|
+
}),
|
|
3768
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3769
|
+
type: "button",
|
|
3770
|
+
"data-toolbar-settings-reset": true,
|
|
3771
|
+
onClick: onResetAll,
|
|
3772
|
+
style: {
|
|
3773
|
+
display: "block",
|
|
3774
|
+
margin: "14px auto 0",
|
|
3775
|
+
padding: "7px 16px",
|
|
3776
|
+
background: "rgba(255, 255, 255, 0.06)",
|
|
3777
|
+
border: "1px solid rgba(255, 255, 255, 0.14)",
|
|
3778
|
+
borderRadius: 8,
|
|
3779
|
+
cursor: "pointer",
|
|
3780
|
+
color: "#b9c2d4",
|
|
3781
|
+
fontSize: 12.5
|
|
3782
|
+
},
|
|
3783
|
+
children: t("settings.reset")
|
|
3784
|
+
})
|
|
1817
3785
|
]
|
|
1818
3786
|
})
|
|
1819
3787
|
}),
|
|
1820
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
3788
|
+
updateOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3789
|
+
ref: updatePanelRef,
|
|
3790
|
+
"data-update-panel": true,
|
|
3791
|
+
style: {
|
|
3792
|
+
position: "fixed",
|
|
3793
|
+
top: railBox.top,
|
|
3794
|
+
right: panelRightFor(PANEL_WIDTH_STANDARD),
|
|
3795
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
3796
|
+
padding: "10px 12px",
|
|
3797
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
3798
|
+
color: "#e6e8ee",
|
|
3799
|
+
borderRadius: 8,
|
|
3800
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
3801
|
+
zIndex: 104
|
|
3802
|
+
},
|
|
3803
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3804
|
+
"data-update-title": true,
|
|
3805
|
+
style: {
|
|
3806
|
+
fontSize: 13,
|
|
3807
|
+
fontWeight: 600,
|
|
3808
|
+
color: "#e6e8ee",
|
|
3809
|
+
marginBottom: 8
|
|
3810
|
+
},
|
|
3811
|
+
children: t("update.title")
|
|
3812
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3813
|
+
style: {
|
|
3814
|
+
display: "flex",
|
|
3815
|
+
flexDirection: "column",
|
|
3816
|
+
gap: 6,
|
|
3817
|
+
fontSize: 12,
|
|
3818
|
+
lineHeight: 1.5
|
|
3819
|
+
},
|
|
3820
|
+
children: [
|
|
3821
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3822
|
+
style: { color: "#8b96ab" },
|
|
3823
|
+
children: [t("update.current"), ": "]
|
|
3824
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "0.6.2" })] }),
|
|
3825
|
+
updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3826
|
+
"data-update-latest": true,
|
|
3827
|
+
children: [
|
|
3828
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3829
|
+
style: { color: "#8b96ab" },
|
|
3830
|
+
children: [t("update.latest"), ": "]
|
|
3831
|
+
}),
|
|
3832
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: updateCheck.latest }),
|
|
3833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3834
|
+
style: { color: "#8b96ab" },
|
|
3835
|
+
children: [
|
|
3836
|
+
" (",
|
|
3837
|
+
updateCheck.source,
|
|
3838
|
+
")"
|
|
3839
|
+
]
|
|
3840
|
+
})
|
|
3841
|
+
]
|
|
3842
|
+
}),
|
|
3843
|
+
updateCheck.phase === "checking" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3844
|
+
"data-update-status": true,
|
|
3845
|
+
style: { color: "#8b96ab" },
|
|
3846
|
+
children: t("update.checking")
|
|
3847
|
+
}),
|
|
3848
|
+
updateCheck.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3849
|
+
"data-update-failed": true,
|
|
3850
|
+
children: [
|
|
3851
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("update.failed"), ":"] }),
|
|
3852
|
+
" ",
|
|
3853
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3854
|
+
style: { color: "#8b96ab" },
|
|
3855
|
+
children: updateCheck.error
|
|
3856
|
+
}),
|
|
3857
|
+
" ",
|
|
3858
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3859
|
+
type: "button",
|
|
3860
|
+
"data-update-retry": true,
|
|
3861
|
+
onClick: runUpdateCheck,
|
|
3862
|
+
style: {
|
|
3863
|
+
background: "transparent",
|
|
3864
|
+
border: "none",
|
|
3865
|
+
padding: 0,
|
|
3866
|
+
cursor: "pointer",
|
|
3867
|
+
color: accentSoft,
|
|
3868
|
+
fontSize: 12,
|
|
3869
|
+
textDecoration: "underline"
|
|
3870
|
+
},
|
|
3871
|
+
children: t("update.retry")
|
|
3872
|
+
})
|
|
3873
|
+
]
|
|
3874
|
+
}),
|
|
3875
|
+
updateCheck.phase === "ok" && updateCheck.latest !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3876
|
+
"data-update-conclusion": true,
|
|
3877
|
+
children: updateCheck.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
3878
|
+
t("update.available"),
|
|
3879
|
+
" v",
|
|
3880
|
+
updateCheck.latest,
|
|
3881
|
+
" → "
|
|
3882
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
3883
|
+
href: "https://www.npmjs.com/package/dsh-milestone",
|
|
3884
|
+
target: "_blank",
|
|
3885
|
+
rel: "noreferrer",
|
|
3886
|
+
style: {
|
|
3887
|
+
color: accentSoft,
|
|
3888
|
+
textDecoration: "none"
|
|
3889
|
+
},
|
|
3890
|
+
children: t("update.goNpm")
|
|
3891
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3892
|
+
style: { color: "#7ee2a8" },
|
|
3893
|
+
children: t("update.upToDate")
|
|
3894
|
+
})
|
|
3895
|
+
}),
|
|
3896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3897
|
+
"data-update-host-lines": true,
|
|
3898
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3899
|
+
style: { color: "#8b96ab" },
|
|
3900
|
+
children: [t("update.hostLines"), ": "]
|
|
3901
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: SUPPORTED_HOST_LINES.map(hostLineLabel).join("、") })]
|
|
3902
|
+
}),
|
|
3903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3904
|
+
type: "button",
|
|
3905
|
+
"data-update-manual": true,
|
|
3906
|
+
disabled: updateCheck.phase === "checking",
|
|
3907
|
+
onClick: runUpdateCheck,
|
|
3908
|
+
style: {
|
|
3909
|
+
marginTop: 4,
|
|
3910
|
+
padding: "6px 10px",
|
|
3911
|
+
background: updateCheck.phase === "checking" ? "transparent" : accentBg,
|
|
3912
|
+
border: "none",
|
|
3913
|
+
borderRadius: 6,
|
|
3914
|
+
cursor: updateCheck.phase === "checking" ? "default" : "pointer",
|
|
3915
|
+
color: updateCheck.phase === "checking" ? "#5a6375" : accentSoft,
|
|
3916
|
+
fontSize: 12,
|
|
3917
|
+
alignSelf: "flex-start"
|
|
3918
|
+
},
|
|
3919
|
+
children: updateCheck.phase === "checking" ? t("update.checking") : t("update.check")
|
|
3920
|
+
})
|
|
3921
|
+
]
|
|
3922
|
+
})]
|
|
1835
3923
|
}),
|
|
1836
3924
|
listOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneListPanel, {
|
|
1837
3925
|
panelTop: railBox.top,
|
|
1838
|
-
panelRight:
|
|
3926
|
+
panelRight: panelRightFor(PANEL_WIDTH_STANDARD),
|
|
1839
3927
|
marks,
|
|
1840
3928
|
onJump: jump,
|
|
1841
3929
|
t
|
|
1842
3930
|
}),
|
|
1843
3931
|
crossOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneSessionSearch, {
|
|
1844
3932
|
panelTop: railBox.top,
|
|
1845
|
-
panelRight:
|
|
3933
|
+
panelRight: panelRightFor(PANEL_WIDTH_STANDARD),
|
|
1846
3934
|
onClose: () => setCrossOpen(false),
|
|
1847
3935
|
searchSessions,
|
|
1848
3936
|
openSession,
|
|
@@ -1862,12 +3950,12 @@ window.__ModuleLoader__.load({
|
|
|
1862
3950
|
display: "flex",
|
|
1863
3951
|
flexDirection: "column",
|
|
1864
3952
|
alignItems: "center",
|
|
1865
|
-
gap
|
|
3953
|
+
gap,
|
|
1866
3954
|
padding: "6px 0",
|
|
1867
3955
|
scrollbarWidth: "none"
|
|
1868
3956
|
},
|
|
1869
3957
|
children: render.items.map((item, i) => {
|
|
1870
|
-
const
|
|
3958
|
+
const showGroupGap = separatorIndices.has(i) && i > 0;
|
|
1871
3959
|
const mark = displayMarks[item.displayIndex];
|
|
1872
3960
|
const summaryCount = collapsedSummaries.get(mark.key);
|
|
1873
3961
|
const bookmarked = isBookmarked(bookmarkedKeys, mark.key);
|
|
@@ -1879,7 +3967,7 @@ window.__ModuleLoader__.load({
|
|
|
1879
3967
|
isCurrent: !hasQuery && mark.key === currentKey
|
|
1880
3968
|
});
|
|
1881
3969
|
const isHovered = hover?.mark.key === mark.key;
|
|
1882
|
-
const boxShadow = isHovered ?
|
|
3970
|
+
const boxShadow = isHovered ? `0 0 0 3px ${rgbaString(accent, .35) ?? "rgba(77, 124, 254, 0.35)"}` : dotState === "active" ? `0 0 0 3px rgba(255, 255, 255, 0.9), 0 0 10px 2px ${rgbaString(accent, .55) ?? "rgba(77, 124, 254, 0.55)"}` : dotState === "current" ? "0 0 0 3px rgba(255, 255, 255, 0.75)" : dotState === "match" ? `0 0 0 2px ${rgbaString(accent, .45) ?? "rgba(77, 124, 254, 0.45)"}` : "none";
|
|
1883
3971
|
const badge = deriveBadge({
|
|
1884
3972
|
nodeKinds: mark.turn === void 0 ? NO_KINDS : kindsByTurn.get(mark.turn) ?? NO_KINDS,
|
|
1885
3973
|
lastMark: item.displayIndex === displayMarks.length - 1,
|
|
@@ -1887,21 +3975,11 @@ window.__ModuleLoader__.load({
|
|
|
1887
3975
|
awaitingInput
|
|
1888
3976
|
});
|
|
1889
3977
|
const ringStyle = badge === null ? null : badgeRingStyle(badge);
|
|
1890
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1891
|
-
"data-turn-separator": true,
|
|
1892
|
-
"data-turn": mark.turn === void 0 ? void 0 : mark.turn,
|
|
1893
|
-
style: {
|
|
1894
|
-
width: DOT_HIT - 8,
|
|
1895
|
-
height: 1,
|
|
1896
|
-
flexShrink: 0,
|
|
1897
|
-
background: "rgba(139, 150, 171, 0.35)",
|
|
1898
|
-
borderRadius: 1
|
|
1899
|
-
}
|
|
1900
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3978
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1901
3979
|
type: "button",
|
|
1902
3980
|
style: {
|
|
1903
|
-
width:
|
|
1904
|
-
height:
|
|
3981
|
+
width: hit,
|
|
3982
|
+
height: hit,
|
|
1905
3983
|
flexShrink: 0,
|
|
1906
3984
|
display: "flex",
|
|
1907
3985
|
alignItems: "center",
|
|
@@ -1909,7 +3987,8 @@ window.__ModuleLoader__.load({
|
|
|
1909
3987
|
background: "transparent",
|
|
1910
3988
|
border: "none",
|
|
1911
3989
|
padding: 0,
|
|
1912
|
-
cursor: "pointer"
|
|
3990
|
+
cursor: "pointer",
|
|
3991
|
+
marginTop: showGroupGap ? GROUP_GAP_EXTRA : 0
|
|
1913
3992
|
},
|
|
1914
3993
|
onMouseEnter: (e) => {
|
|
1915
3994
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
@@ -1920,6 +3999,8 @@ window.__ModuleLoader__.load({
|
|
|
1920
3999
|
},
|
|
1921
4000
|
onClick: () => jump(mark.key),
|
|
1922
4001
|
"data-rail-dot": true,
|
|
4002
|
+
"data-turn-gap": showGroupGap ? "true" : void 0,
|
|
4003
|
+
"data-turn": showGroupGap && mark.turn !== void 0 ? mark.turn : void 0,
|
|
1923
4004
|
"data-collapsed-summary": summaryCount !== void 0 ? "true" : void 0,
|
|
1924
4005
|
"data-collapsed-count": summaryCount,
|
|
1925
4006
|
tabIndex: focusIndex === i ? 0 : -1,
|
|
@@ -1931,10 +4012,10 @@ window.__ModuleLoader__.load({
|
|
|
1931
4012
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1932
4013
|
style: {
|
|
1933
4014
|
position: "relative",
|
|
1934
|
-
width:
|
|
1935
|
-
height:
|
|
4015
|
+
width: size,
|
|
4016
|
+
height: size,
|
|
1936
4017
|
borderRadius: "50%",
|
|
1937
|
-
background: dotColor(item.displayIndex, marks.length),
|
|
4018
|
+
background: dotColor(item.displayIndex, marks.length, accent),
|
|
1938
4019
|
boxShadow,
|
|
1939
4020
|
transition: "transform 120ms ease, opacity 120ms ease",
|
|
1940
4021
|
transform: `scale(${isHovered ? 1.35 : dotState === "active" || dotState === "current" ? 1.25 : 1})`,
|
|
@@ -1947,18 +4028,18 @@ window.__ModuleLoader__.load({
|
|
|
1947
4028
|
position: "absolute",
|
|
1948
4029
|
inset: -3,
|
|
1949
4030
|
borderRadius: "50%",
|
|
1950
|
-
|
|
4031
|
+
boxShadow: ringStyle.shadow,
|
|
1951
4032
|
color: ringStyle.color,
|
|
1952
4033
|
pointerEvents: "none",
|
|
1953
|
-
animation: ringStyle.pulse ? "milestone-badge-pulse
|
|
4034
|
+
animation: ringStyle.pulse ? "milestone-badge-pulse 2s ease-in-out infinite" : void 0
|
|
1954
4035
|
}
|
|
1955
4036
|
})
|
|
1956
4037
|
})
|
|
1957
|
-
})
|
|
4038
|
+
}) }, mark.key);
|
|
1958
4039
|
})
|
|
1959
4040
|
}),
|
|
1960
4041
|
hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneRailTooltip, {
|
|
1961
|
-
panelRight:
|
|
4042
|
+
panelRight: panelRightFor(TOOLTIP_ANCHOR_WIDTH),
|
|
1962
4043
|
hover,
|
|
1963
4044
|
bookmarked: isBookmarked(bookmarkedKeys, hover.mark.key),
|
|
1964
4045
|
onToggleBookmark: () => onToggleBookmark(hover.mark.key),
|
|
@@ -1977,8 +4058,13 @@ window.__ModuleLoader__.load({
|
|
|
1977
4058
|
style: {
|
|
1978
4059
|
position: "absolute",
|
|
1979
4060
|
bottom: 6,
|
|
1980
|
-
|
|
1981
|
-
|
|
4061
|
+
...side === "left" ? {
|
|
4062
|
+
left: "100%",
|
|
4063
|
+
marginLeft: 8
|
|
4064
|
+
} : {
|
|
4065
|
+
right: "100%",
|
|
4066
|
+
marginRight: 8
|
|
4067
|
+
},
|
|
1982
4068
|
whiteSpace: "nowrap",
|
|
1983
4069
|
fontSize: 12,
|
|
1984
4070
|
lineHeight: 1,
|
|
@@ -2101,144 +4187,6 @@ window.__ModuleLoader__.load({
|
|
|
2101
4187
|
});
|
|
2102
4188
|
}
|
|
2103
4189
|
//#endregion
|
|
2104
|
-
//#region src/client/locales.ts
|
|
2105
|
-
/**
|
|
2106
|
-
* UI strings for the milestone rail, keyed flat (single-language-per-key,
|
|
2107
|
-
* no nesting) so the later i18n threading stays a mechanical
|
|
2108
|
-
* `value.replace('{name}', n)` substitution.
|
|
2109
|
-
*
|
|
2110
|
-
* `zh` is the source of truth and the key registry: it byte-matches the
|
|
2111
|
-
* current hardcoded output of MilestoneRail / MilestoneRailTooltip /
|
|
2112
|
-
* MilestoneRailSearch exactly (each `{n}`/`{m}`/`{name}` slot stands in for
|
|
2113
|
-
* the interpolated number or label), so swapping in these templates is
|
|
2114
|
-
* behavior-preserving. `en` is typed `Record<MilestoneKey, string>` so a
|
|
2115
|
-
* missing English translation is a compile error, not a runtime miss.
|
|
2116
|
-
*/
|
|
2117
|
-
const zh = {
|
|
2118
|
-
/** aria-label on each dot: `跳转到第 ${i + 1} 条消息`. */
|
|
2119
|
-
"jump.to": "跳转到第 {n} 条消息",
|
|
2120
|
-
/** Load-older coverage hint: `已显示 {marks.length} 条 · 还有更早`. */
|
|
2121
|
-
"window.hint": "已显示 {n} 条 · 还有更早",
|
|
2122
|
-
/** Hover turn badge: `第 ${mark.turn} 轮`. */
|
|
2123
|
-
"turn.label": "第 {n} 轮",
|
|
2124
|
-
/** Hover position: `第 {hover.index + 1} / {hover.total} 条`. */
|
|
2125
|
-
"pos.of": "第 {n} / {m} 条",
|
|
2126
|
-
/** Search input placeholder. */
|
|
2127
|
-
"search.placeholder": "搜索消息内容",
|
|
2128
|
-
/** aria-label on the search toggle button and the search input. */
|
|
2129
|
-
"search.label": "搜索消息",
|
|
2130
|
-
/** aria-label on the bookmarks-only filter toggle. */
|
|
2131
|
-
"bookmark.filter": "只看收藏",
|
|
2132
|
-
/** aria-label + title on the focus-mode toggle when focus is OFF (arm it). */
|
|
2133
|
-
"focus.on": "聚焦模式",
|
|
2134
|
-
/** aria-label + title on the focus-mode toggle when focus is ON (disarm it). */
|
|
2135
|
-
"focus.off": "退出聚焦",
|
|
2136
|
-
/** aria-label on the hover tooltip star toggle. */
|
|
2137
|
-
"bookmark.star": "收藏此消息",
|
|
2138
|
-
/** aria-label on the search clear button. */
|
|
2139
|
-
"search.clear": "清空搜索",
|
|
2140
|
-
/** title + aria-label on the load-older `···` button. */
|
|
2141
|
-
"load.older": "加载更早消息",
|
|
2142
|
-
/** aria-label on the rail root. */
|
|
2143
|
-
"rail.label": "会话里程碑",
|
|
2144
|
-
/** aria-label on the dot list. */
|
|
2145
|
-
"rail.list": "会话里程碑列表",
|
|
2146
|
-
/** Hover preview fallback for empty message text. */
|
|
2147
|
-
"no.text": "(无文本)",
|
|
2148
|
-
/** Relative time: `< 60s`. */
|
|
2149
|
-
"time.justNow": "刚刚",
|
|
2150
|
-
/** Relative time: `< 1h`. */
|
|
2151
|
-
"time.minutes": "{n} 分钟前",
|
|
2152
|
-
/** Relative time: `< 1d`. */
|
|
2153
|
-
"time.hours": "{n} 小时前",
|
|
2154
|
-
/** Relative time: `>= 1d`. */
|
|
2155
|
-
"time.days": "{n} 天前",
|
|
2156
|
-
/** Hover duration: `用时 {durationLabel}`. */
|
|
2157
|
-
"duration.label": "用时 {name}",
|
|
2158
|
-
/** Hover TTFT: `首字 {ttftLabel}`. */
|
|
2159
|
-
"ttft.label": "首字 {name}",
|
|
2160
|
-
/** TurnEndReason `completed`. */
|
|
2161
|
-
"reason.completed": "已完成",
|
|
2162
|
-
/** TurnEndReason `aborted`. */
|
|
2163
|
-
"reason.aborted": "已中止",
|
|
2164
|
-
/** TurnEndReason `error`. */
|
|
2165
|
-
"reason.error": "出错",
|
|
2166
|
-
/** TurnEndReason `max-tokens`. */
|
|
2167
|
-
"reason.maxTokens": "达到上限",
|
|
2168
|
-
/** TurnEndReason `interrupted`. */
|
|
2169
|
-
"reason.interrupted": "已中断",
|
|
2170
|
-
/** TurnEndReason `blocked`. */
|
|
2171
|
-
"reason.blocked": "已阻塞",
|
|
2172
|
-
/** Copy-message tooltip action. */
|
|
2173
|
-
"copy.message": "复制消息",
|
|
2174
|
-
/** Fork-from-here tooltip action. */
|
|
2175
|
-
"fork.here": "从此处 fork",
|
|
2176
|
-
/** Collapse-turn tooltip action. */
|
|
2177
|
-
"collapse.turn": "折叠此轮",
|
|
2178
|
-
/** Expand-turn tooltip action. */
|
|
2179
|
-
"expand.turn": "展开此轮",
|
|
2180
|
-
/** aria-label + title on the milestone-list toggle when the panel is CLOSED. */
|
|
2181
|
-
"list.open": "打开列表",
|
|
2182
|
-
/** aria-label + title on the milestone-list toggle when the panel is OPEN. */
|
|
2183
|
-
"list.close": "收起列表",
|
|
2184
|
-
/** Header title of the all-prompts list panel. */
|
|
2185
|
-
"list.label": "全部提问",
|
|
2186
|
-
/** Header title + input placeholder of the cross-session search panel. */
|
|
2187
|
-
"search.cross": "跨会话搜索",
|
|
2188
|
-
/** aria-label + title on the cross-session search toggle when the panel is CLOSED. */
|
|
2189
|
-
"search.cross.open": "打开跨会话搜索",
|
|
2190
|
-
/** aria-label + title on the cross-session search toggle when the panel is OPEN. */
|
|
2191
|
-
"search.cross.close": "收起跨会话搜索",
|
|
2192
|
-
/** Cross-session result row title fallback for sessions with no display title. */
|
|
2193
|
-
"search.untitled": "(无标题)",
|
|
2194
|
-
/** Cross-session search failure notice. */
|
|
2195
|
-
"search.error": "搜索失败,请重试",
|
|
2196
|
-
/** Cross-session search footer hint when the harness capped the result list. */
|
|
2197
|
-
"search.more": "结果已截断,请细化关键词"
|
|
2198
|
-
};
|
|
2199
|
-
const en = {
|
|
2200
|
-
"jump.to": "Jump to message {n}",
|
|
2201
|
-
"window.hint": "Showing {n} messages · more below",
|
|
2202
|
-
"turn.label": "Turn {n}",
|
|
2203
|
-
"pos.of": "Message {n} of {m}",
|
|
2204
|
-
"search.placeholder": "Search message content",
|
|
2205
|
-
"search.label": "Search messages",
|
|
2206
|
-
"bookmark.filter": "Bookmarks only",
|
|
2207
|
-
"focus.on": "Focus mode",
|
|
2208
|
-
"focus.off": "Exit focus",
|
|
2209
|
-
"bookmark.star": "Bookmark this message",
|
|
2210
|
-
"search.clear": "Clear search",
|
|
2211
|
-
"load.older": "Load older messages",
|
|
2212
|
-
"rail.label": "Session milestones",
|
|
2213
|
-
"rail.list": "Session milestone list",
|
|
2214
|
-
"no.text": "(no text)",
|
|
2215
|
-
"time.justNow": "Just now",
|
|
2216
|
-
"time.minutes": "{n} minutes ago",
|
|
2217
|
-
"time.hours": "{n} hours ago",
|
|
2218
|
-
"time.days": "{n} days ago",
|
|
2219
|
-
"duration.label": "Duration {name}",
|
|
2220
|
-
"ttft.label": "First token {name}",
|
|
2221
|
-
"reason.completed": "Completed",
|
|
2222
|
-
"reason.aborted": "Aborted",
|
|
2223
|
-
"reason.error": "Error",
|
|
2224
|
-
"reason.maxTokens": "Max tokens reached",
|
|
2225
|
-
"reason.interrupted": "Interrupted",
|
|
2226
|
-
"reason.blocked": "Blocked",
|
|
2227
|
-
"copy.message": "Copy message",
|
|
2228
|
-
"fork.here": "Fork from here",
|
|
2229
|
-
"collapse.turn": "Collapse turn",
|
|
2230
|
-
"expand.turn": "Expand turn",
|
|
2231
|
-
"list.open": "Open list",
|
|
2232
|
-
"list.close": "Close list",
|
|
2233
|
-
"list.label": "All prompts",
|
|
2234
|
-
"search.cross": "Cross-session search",
|
|
2235
|
-
"search.cross.open": "Open cross-session search",
|
|
2236
|
-
"search.cross.close": "Close cross-session search",
|
|
2237
|
-
"search.untitled": "(untitled)",
|
|
2238
|
-
"search.error": "Search failed, retry",
|
|
2239
|
-
"search.more": "Results truncated — refine your query"
|
|
2240
|
-
};
|
|
2241
|
-
//#endregion
|
|
2242
4190
|
//#region src/client/index.ts
|
|
2243
4191
|
/** Required services (cordis fiber inject). */
|
|
2244
4192
|
const inject = [
|