dsh-milestone 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/lib/client.js +672 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
- **turn 分组折叠** —— 圆点按轮次分组,一眼看清对话章节;长轮次可折叠成一条,减少干扰。
|
|
45
45
|
- **复制与 fork** —— 悬停一键复制该条提问全文,或「从此处 fork」开一个分支会话。
|
|
46
46
|
- **中英双语** —— 跟随 harness 界面语言自动切换中英文。
|
|
47
|
+
- **聚焦模式** —— 一键淡化 AI 的思考(thinking)区块,对话正文更清爽;悬停或展开时自动恢复。
|
|
48
|
+
- **全部提问列表** —— 顶部展开面板,一次看全所有提问(序号 + 轮次 + 预览),点击即跳,长对话不再靠圆点一个个数。
|
|
49
|
+
- **深链接** —— 跳转时 URL 带上 `#msg=` 锚点,刷新或分享链接后直接回到同一条消息。
|
|
50
|
+
- **跨会话搜索** —— 一键搜索**所有会话**的消息内容(harness 原生索引),点击结果直接打开对应会话。
|
|
47
51
|
- **零侵入** —— 官方 slot 机制挂载,不修改 harness 源码,装完即用。
|
|
48
52
|
|
|
49
53
|
## 悬停能看到什么
|
|
@@ -100,6 +104,9 @@ shell.overlay (root scope)
|
|
|
100
104
|
- 书签按**会话**隔离(不跨会话共享)。
|
|
101
105
|
- 模型 / token 用量依赖该轮 assistant 节点的元数据,部分场景下缺失则自动隐藏该行。
|
|
102
106
|
- fork 从选中消息所在轮次开始分支,不会自动打开子会话(需在会话列表手动打开)。
|
|
107
|
+
- 深链接的目标消息若早于已加载窗口,会先自动加载更早历史再定位(受加载上限约束,极端深的历史可能定位失败)。
|
|
108
|
+
- 跨会话搜索依赖 harness 的消息内容索引(`session.search`),仅返回片段(≤240 字符)、最多 20 条结果;命中过多时请细化关键词。
|
|
109
|
+
- 聚焦模式作用于当前会话视图的思考区块,不会影响其他插件或工具的展示。
|
|
103
110
|
- 尚无全局快捷键聚焦里程碑条(需 Tab 键切换到)。
|
|
104
111
|
|
|
105
112
|
## License
|
package/lib/client.js
CHANGED
|
@@ -149,6 +149,59 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
//#endregion
|
|
152
|
+
//#region src/client/deep-link-logic.ts
|
|
153
|
+
/**
|
|
154
|
+
* Pure deep-link helpers for the milestone rail: parse the `#msg=<key>` URL
|
|
155
|
+
* hash and rebuild it.
|
|
156
|
+
*
|
|
157
|
+
* The conversation anchor key is treated as an OPAQUE string — it is a
|
|
158
|
+
* length-prefixed node key like `13:input-message<messageId>` or
|
|
159
|
+
* `14:assistant-step3:2` (the length prefix makes naive splitting ambiguous),
|
|
160
|
+
* so the parser never inspects or splits the key itself. Callers match the
|
|
161
|
+
* returned key against the session's marks, which is the real validity check.
|
|
162
|
+
*
|
|
163
|
+
* Percent-encoding: the URL fragment parser percent-encodes `"` `<` `>` `` ` ``
|
|
164
|
+
* and lone `%`, so a hash built with a RAW `<`-containing key would read back
|
|
165
|
+
* from `location.hash` percent-encoded (`13:user%3Cdl-2%3E`) and never match
|
|
166
|
+
* a mark after a refresh. `buildMessageHash` therefore percent-encodes those
|
|
167
|
+
* characters itself (a byte-exact URL), and `parseDeepLinkHash` decodes them
|
|
168
|
+
* back — the key round-trips through the URL untouched.
|
|
169
|
+
*/
|
|
170
|
+
/** The URL hash fragment prefix that carries a message anchor key. */
|
|
171
|
+
const MSG_HASH_PREFIX = "msg=";
|
|
172
|
+
/**
|
|
173
|
+
* Characters the WHATWG URL fragment parser cannot round-trip raw (they are
|
|
174
|
+
* percent-encoded on parse): `"` (0x22), `<` (0x3C), `>` (0x3E), backtick
|
|
175
|
+
* (0x60), and `%` (0x25, so a literal `%` never reads as an escape start).
|
|
176
|
+
*/
|
|
177
|
+
const FRAGMENT_UNSAFE = /["<>\u0060%]/g;
|
|
178
|
+
/**
|
|
179
|
+
* Parse a `location.hash` fragment into the message anchor key it references.
|
|
180
|
+
* @param hash - the raw `location.hash` value: `''` or a fragment starting
|
|
181
|
+
* with `#` (e.g. `#msg=13:input-messageabc`).
|
|
182
|
+
* @returns the anchor key (percent-escapes decoded), or null when the
|
|
183
|
+
* fragment is not a message deep link: empty hash, `#msg=` with an empty
|
|
184
|
+
* value, a `#msg` prefix without the `=`, any other hash shape, a hash
|
|
185
|
+
* missing the leading `#`, or a malformed percent escape.
|
|
186
|
+
*/
|
|
187
|
+
function parseDeepLinkHash(hash) {
|
|
188
|
+
if (!hash.startsWith(`#${MSG_HASH_PREFIX}`)) return null;
|
|
189
|
+
const encoded = hash.slice(5);
|
|
190
|
+
if (encoded === "") return null;
|
|
191
|
+
try {
|
|
192
|
+
return decodeURIComponent(encoded);
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Build the URL hash fragment that deep-links to a message anchor key.
|
|
199
|
+
* @param key - the conversation node key (opaque, never parsed here).
|
|
200
|
+
*/
|
|
201
|
+
function buildMessageHash(key) {
|
|
202
|
+
return `#${MSG_HASH_PREFIX}${key.replace(FRAGMENT_UNSAFE, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
152
205
|
//#region src/client/label-logic.ts
|
|
153
206
|
const MINUTE_MS = 6e4;
|
|
154
207
|
const HOUR_MS = 36e5;
|
|
@@ -452,7 +505,7 @@ window.__ModuleLoader__.load({
|
|
|
452
505
|
//#endregion
|
|
453
506
|
//#region src/client/MilestoneRailSearch.tsx
|
|
454
507
|
/** Dot diameter (px) — matches the rail's DOT_HIT so the toggle aligns. */
|
|
455
|
-
const DOT_HIT$1 =
|
|
508
|
+
const DOT_HIT$1 = 28;
|
|
456
509
|
/**
|
|
457
510
|
* @param props - the search state slice plus the rail's event handlers.
|
|
458
511
|
*/
|
|
@@ -477,8 +530,8 @@ window.__ModuleLoader__.load({
|
|
|
477
530
|
color: panelOpen ? "#9db8ff" : "#8b96ab"
|
|
478
531
|
},
|
|
479
532
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
480
|
-
width: "
|
|
481
|
-
height: "
|
|
533
|
+
width: "16",
|
|
534
|
+
height: "16",
|
|
482
535
|
viewBox: "0 0 24 24",
|
|
483
536
|
fill: "none",
|
|
484
537
|
stroke: "currentColor",
|
|
@@ -496,7 +549,7 @@ window.__ModuleLoader__.load({
|
|
|
496
549
|
position: "fixed",
|
|
497
550
|
top: panelTop,
|
|
498
551
|
right: panelRight,
|
|
499
|
-
width:
|
|
552
|
+
width: "min(220px, calc(100vw - 48px))",
|
|
500
553
|
padding: "10px 12px",
|
|
501
554
|
background: "rgba(20, 24, 32, 0.97)",
|
|
502
555
|
color: "#e6e8ee",
|
|
@@ -521,8 +574,9 @@ window.__ModuleLoader__.load({
|
|
|
521
574
|
style: {
|
|
522
575
|
flex: 1,
|
|
523
576
|
minWidth: 0,
|
|
524
|
-
padding: "
|
|
525
|
-
fontSize:
|
|
577
|
+
padding: "6px 10px",
|
|
578
|
+
fontSize: 14,
|
|
579
|
+
lineHeight: 1.4,
|
|
526
580
|
color: "#e6e8ee",
|
|
527
581
|
background: "rgba(255, 255, 255, 0.08)",
|
|
528
582
|
border: "1px solid rgba(255, 255, 255, 0.16)",
|
|
@@ -548,8 +602,8 @@ window.__ModuleLoader__.load({
|
|
|
548
602
|
color: "#8b96ab"
|
|
549
603
|
},
|
|
550
604
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
551
|
-
width: "
|
|
552
|
-
height: "
|
|
605
|
+
width: "12",
|
|
606
|
+
height: "12",
|
|
553
607
|
viewBox: "0 0 24 24",
|
|
554
608
|
fill: "none",
|
|
555
609
|
stroke: "currentColor",
|
|
@@ -563,7 +617,7 @@ window.__ModuleLoader__.load({
|
|
|
563
617
|
"data-match-count": true,
|
|
564
618
|
style: {
|
|
565
619
|
marginTop: 6,
|
|
566
|
-
fontSize:
|
|
620
|
+
fontSize: 13,
|
|
567
621
|
color: "#8b96ab"
|
|
568
622
|
},
|
|
569
623
|
children: [
|
|
@@ -575,6 +629,101 @@ window.__ModuleLoader__.load({
|
|
|
575
629
|
})] });
|
|
576
630
|
}
|
|
577
631
|
//#endregion
|
|
632
|
+
//#region src/client/MilestoneListPanel.tsx
|
|
633
|
+
/**
|
|
634
|
+
* @param props - the panel anchor, the full marks array, and the rail's jump handler.
|
|
635
|
+
*/
|
|
636
|
+
function MilestoneListPanel({ panelTop, panelRight, marks, onJump, t }) {
|
|
637
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
638
|
+
"data-milestone-list": true,
|
|
639
|
+
style: {
|
|
640
|
+
position: "fixed",
|
|
641
|
+
top: panelTop,
|
|
642
|
+
right: panelRight,
|
|
643
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
644
|
+
padding: "10px 12px",
|
|
645
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
646
|
+
color: "#e6e8ee",
|
|
647
|
+
borderRadius: 8,
|
|
648
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
649
|
+
zIndex: 103
|
|
650
|
+
},
|
|
651
|
+
children: [
|
|
652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-list-item]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
653
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
654
|
+
style: {
|
|
655
|
+
display: "flex",
|
|
656
|
+
alignItems: "center",
|
|
657
|
+
gap: 6,
|
|
658
|
+
marginBottom: 6
|
|
659
|
+
},
|
|
660
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
661
|
+
style: {
|
|
662
|
+
fontSize: 13,
|
|
663
|
+
fontWeight: 600,
|
|
664
|
+
color: "#e6e8ee"
|
|
665
|
+
},
|
|
666
|
+
children: t("list.label")
|
|
667
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
668
|
+
style: {
|
|
669
|
+
fontSize: 12,
|
|
670
|
+
color: "#8b96ab"
|
|
671
|
+
},
|
|
672
|
+
children: marks.length
|
|
673
|
+
})]
|
|
674
|
+
}),
|
|
675
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
676
|
+
style: {
|
|
677
|
+
maxHeight: 300,
|
|
678
|
+
overflowY: "auto",
|
|
679
|
+
display: "flex",
|
|
680
|
+
flexDirection: "column",
|
|
681
|
+
gap: 2
|
|
682
|
+
},
|
|
683
|
+
children: marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
684
|
+
type: "button",
|
|
685
|
+
"data-list-item": true,
|
|
686
|
+
"data-jump-key": mark.key,
|
|
687
|
+
onClick: () => onJump(mark.key),
|
|
688
|
+
title: mark.preview,
|
|
689
|
+
style: {
|
|
690
|
+
display: "block",
|
|
691
|
+
width: "100%",
|
|
692
|
+
minWidth: 0,
|
|
693
|
+
padding: "6px 8px",
|
|
694
|
+
background: "transparent",
|
|
695
|
+
border: "none",
|
|
696
|
+
borderRadius: 6,
|
|
697
|
+
cursor: "pointer",
|
|
698
|
+
color: "#e6e8ee",
|
|
699
|
+
textAlign: "left"
|
|
700
|
+
},
|
|
701
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
702
|
+
style: {
|
|
703
|
+
fontSize: 12,
|
|
704
|
+
color: "#8b96ab",
|
|
705
|
+
whiteSpace: "nowrap"
|
|
706
|
+
},
|
|
707
|
+
children: [t("pos.of", {
|
|
708
|
+
n: i + 1,
|
|
709
|
+
m: marks.length
|
|
710
|
+
}), mark.turn !== void 0 ? ` · ${t("turn.label", { n: mark.turn })}` : null]
|
|
711
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
712
|
+
style: {
|
|
713
|
+
fontSize: 13,
|
|
714
|
+
lineHeight: 1.4,
|
|
715
|
+
overflow: "hidden",
|
|
716
|
+
textOverflow: "ellipsis",
|
|
717
|
+
whiteSpace: "nowrap"
|
|
718
|
+
},
|
|
719
|
+
children: mark.preview || t("no.text")
|
|
720
|
+
})]
|
|
721
|
+
}, mark.key))
|
|
722
|
+
})
|
|
723
|
+
]
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
//#endregion
|
|
578
727
|
//#region src/client/MilestoneRailTooltip.tsx
|
|
579
728
|
/**
|
|
580
729
|
* @param props - the hovered mark + bookmark wiring (see {@link MilestoneRailTooltipProps}).
|
|
@@ -591,14 +740,14 @@ window.__ModuleLoader__.load({
|
|
|
591
740
|
right: panelRight,
|
|
592
741
|
top: hover.top,
|
|
593
742
|
transform: "translateY(-50%)",
|
|
594
|
-
maxWidth:
|
|
743
|
+
maxWidth: "min(300px, calc(100vw - 120px))",
|
|
595
744
|
minWidth: 180,
|
|
596
745
|
padding: "8px 12px",
|
|
597
746
|
background: "rgba(20, 24, 32, 0.96)",
|
|
598
747
|
color: "#e6e8ee",
|
|
599
748
|
borderRadius: 8,
|
|
600
|
-
fontSize:
|
|
601
|
-
lineHeight: 1.
|
|
749
|
+
fontSize: "var(--dsw-font-s-14, 14px)",
|
|
750
|
+
lineHeight: 1.5,
|
|
602
751
|
whiteSpace: "pre-wrap",
|
|
603
752
|
wordBreak: "break-word",
|
|
604
753
|
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
@@ -613,7 +762,8 @@ window.__ModuleLoader__.load({
|
|
|
613
762
|
flexWrap: "wrap",
|
|
614
763
|
gap: 8,
|
|
615
764
|
color: "#9aa4b8",
|
|
616
|
-
fontSize:
|
|
765
|
+
fontSize: 13,
|
|
766
|
+
lineHeight: 1.4,
|
|
617
767
|
marginBottom: 4
|
|
618
768
|
},
|
|
619
769
|
children: [
|
|
@@ -647,8 +797,8 @@ window.__ModuleLoader__.load({
|
|
|
647
797
|
color: bookmarked ? "#ffd166" : "#8b96ab"
|
|
648
798
|
},
|
|
649
799
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
650
|
-
width: "
|
|
651
|
-
height: "
|
|
800
|
+
width: "14",
|
|
801
|
+
height: "14",
|
|
652
802
|
viewBox: "0 0 24 24",
|
|
653
803
|
fill: bookmarked ? "currentColor" : "none",
|
|
654
804
|
stroke: "currentColor",
|
|
@@ -673,7 +823,7 @@ window.__ModuleLoader__.load({
|
|
|
673
823
|
justifyContent: "center",
|
|
674
824
|
background: "transparent",
|
|
675
825
|
border: "none",
|
|
676
|
-
padding: "
|
|
826
|
+
padding: "3px 8px",
|
|
677
827
|
cursor: "pointer",
|
|
678
828
|
whiteSpace: "nowrap",
|
|
679
829
|
color: copied ? "#7ee2a8" : "#8b96ab"
|
|
@@ -695,7 +845,7 @@ window.__ModuleLoader__.load({
|
|
|
695
845
|
justifyContent: "center",
|
|
696
846
|
background: "transparent",
|
|
697
847
|
border: "none",
|
|
698
|
-
padding: "
|
|
848
|
+
padding: "3px 8px",
|
|
699
849
|
cursor: "pointer",
|
|
700
850
|
whiteSpace: "nowrap",
|
|
701
851
|
color: forked ? "#7ee2a8" : "#8b96ab"
|
|
@@ -718,7 +868,7 @@ window.__ModuleLoader__.load({
|
|
|
718
868
|
justifyContent: "center",
|
|
719
869
|
background: "transparent",
|
|
720
870
|
border: "none",
|
|
721
|
-
padding: "
|
|
871
|
+
padding: "3px 8px",
|
|
722
872
|
cursor: "pointer",
|
|
723
873
|
whiteSpace: "nowrap",
|
|
724
874
|
color: turnCollapsed ? "#7ee2a8" : "#8b96ab"
|
|
@@ -737,7 +887,8 @@ window.__ModuleLoader__.load({
|
|
|
737
887
|
flexWrap: "wrap",
|
|
738
888
|
gap: 8,
|
|
739
889
|
color: "#8b96ab",
|
|
740
|
-
fontSize:
|
|
890
|
+
fontSize: 13,
|
|
891
|
+
lineHeight: 1.4,
|
|
741
892
|
marginTop: 4
|
|
742
893
|
},
|
|
743
894
|
children: [
|
|
@@ -754,7 +905,8 @@ window.__ModuleLoader__.load({
|
|
|
754
905
|
flexWrap: "wrap",
|
|
755
906
|
gap: 8,
|
|
756
907
|
color: "#8b96ab",
|
|
757
|
-
fontSize:
|
|
908
|
+
fontSize: 13,
|
|
909
|
+
lineHeight: 1.4,
|
|
758
910
|
marginTop: 4
|
|
759
911
|
},
|
|
760
912
|
children: [
|
|
@@ -776,6 +928,185 @@ window.__ModuleLoader__.load({
|
|
|
776
928
|
});
|
|
777
929
|
}
|
|
778
930
|
//#endregion
|
|
931
|
+
//#region src/client/MilestoneSessionSearch.tsx
|
|
932
|
+
/**
|
|
933
|
+
* MilestoneSessionSearch: the cross-session search panel (P3) — the fixed
|
|
934
|
+
* chrome pinned to the rail's top that searches EVERY session's message
|
|
935
|
+
* content through the injected `searchSessions` action (the harness
|
|
936
|
+
* `session.search` RPC), lists ranked hits (display title + snippet), and
|
|
937
|
+
* opens the clicked session via `openSession` (the same selection path the
|
|
938
|
+
* sidebar uses).
|
|
939
|
+
*
|
|
940
|
+
* Owns its search lifecycle — debounce + AbortController + status — unlike
|
|
941
|
+
* the rail's in-session search (RailSearchUi), which is a pure presentation
|
|
942
|
+
* slice of MilestoneRail's own state. Mirrors MilestoneListPanel's
|
|
943
|
+
* fixed-panel styling. Renders one DOM contract
|
|
944
|
+
* (`data-session-search` root / `data-session-search-input` /
|
|
945
|
+
* `data-session-search-result` rows / `data-session-search-error` /
|
|
946
|
+
* `data-session-search-more`).
|
|
947
|
+
*/
|
|
948
|
+
/** Debounce window for the cross-session query (ms). */
|
|
949
|
+
const SEARCH_DEBOUNCE_MS = 250;
|
|
950
|
+
/**
|
|
951
|
+
* @param props - the panel anchor, the close/open/search actions, and the locale interpreter.
|
|
952
|
+
*/
|
|
953
|
+
function MilestoneSessionSearch({ panelTop, panelRight, onClose, searchSessions, openSession, t }) {
|
|
954
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
955
|
+
const [status, setStatus] = (0, react.useState)("idle");
|
|
956
|
+
const [hits, setHits] = (0, react.useState)([]);
|
|
957
|
+
const [hasMore, setHasMore] = (0, react.useState)(false);
|
|
958
|
+
(0, react.useEffect)(() => {
|
|
959
|
+
const trimmed = query.trim();
|
|
960
|
+
if (trimmed === "") {
|
|
961
|
+
setStatus("idle");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const controller = new AbortController();
|
|
965
|
+
setStatus("loading");
|
|
966
|
+
const timer = window.setTimeout(() => {
|
|
967
|
+
searchSessions(trimmed, controller.signal).then((result) => {
|
|
968
|
+
if (controller.signal.aborted) return;
|
|
969
|
+
setHits(result.items);
|
|
970
|
+
setHasMore(result.hasMore);
|
|
971
|
+
setStatus(result.items.length > 0 ? "results" : "empty");
|
|
972
|
+
}, () => {
|
|
973
|
+
if (controller.signal.aborted) return;
|
|
974
|
+
setStatus("error");
|
|
975
|
+
});
|
|
976
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
977
|
+
return () => {
|
|
978
|
+
window.clearTimeout(timer);
|
|
979
|
+
controller.abort();
|
|
980
|
+
};
|
|
981
|
+
}, [query, searchSessions]);
|
|
982
|
+
const onInputKeyDown = (e) => {
|
|
983
|
+
if (e.key === "Escape") onClose();
|
|
984
|
+
};
|
|
985
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
986
|
+
"data-session-search": true,
|
|
987
|
+
style: {
|
|
988
|
+
position: "fixed",
|
|
989
|
+
top: panelTop,
|
|
990
|
+
right: panelRight,
|
|
991
|
+
width: "min(280px, calc(100vw - 48px))",
|
|
992
|
+
padding: "10px 12px",
|
|
993
|
+
background: "rgba(20, 24, 32, 0.97)",
|
|
994
|
+
color: "#e6e8ee",
|
|
995
|
+
borderRadius: 8,
|
|
996
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
997
|
+
zIndex: 103
|
|
998
|
+
},
|
|
999
|
+
children: [
|
|
1000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `[data-session-search-result]:hover { background: rgba(77, 124, 254, 0.18); }` }),
|
|
1001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1002
|
+
style: {
|
|
1003
|
+
display: "flex",
|
|
1004
|
+
alignItems: "center",
|
|
1005
|
+
gap: 6,
|
|
1006
|
+
marginBottom: 6
|
|
1007
|
+
},
|
|
1008
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1009
|
+
style: {
|
|
1010
|
+
fontSize: 13,
|
|
1011
|
+
fontWeight: 600,
|
|
1012
|
+
color: "#e6e8ee"
|
|
1013
|
+
},
|
|
1014
|
+
children: t("search.cross")
|
|
1015
|
+
})
|
|
1016
|
+
}),
|
|
1017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1018
|
+
"data-session-search-input": true,
|
|
1019
|
+
"aria-label": t("search.cross"),
|
|
1020
|
+
placeholder: t("search.cross"),
|
|
1021
|
+
value: query,
|
|
1022
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1023
|
+
onKeyDown: onInputKeyDown,
|
|
1024
|
+
autoFocus: true,
|
|
1025
|
+
style: {
|
|
1026
|
+
boxSizing: "border-box",
|
|
1027
|
+
width: "100%",
|
|
1028
|
+
padding: "6px 10px",
|
|
1029
|
+
fontSize: 14,
|
|
1030
|
+
lineHeight: 1.4,
|
|
1031
|
+
color: "#e6e8ee",
|
|
1032
|
+
background: "rgba(255, 255, 255, 0.08)",
|
|
1033
|
+
border: "1px solid rgba(255, 255, 255, 0.16)",
|
|
1034
|
+
borderRadius: 6,
|
|
1035
|
+
outline: "none"
|
|
1036
|
+
}
|
|
1037
|
+
}),
|
|
1038
|
+
status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1039
|
+
"data-session-search-error": true,
|
|
1040
|
+
style: {
|
|
1041
|
+
marginTop: 8,
|
|
1042
|
+
fontSize: 13,
|
|
1043
|
+
color: "#f07c7c"
|
|
1044
|
+
},
|
|
1045
|
+
children: t("search.error")
|
|
1046
|
+
}),
|
|
1047
|
+
status === "results" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1048
|
+
style: {
|
|
1049
|
+
maxHeight: 300,
|
|
1050
|
+
overflowY: "auto",
|
|
1051
|
+
display: "flex",
|
|
1052
|
+
flexDirection: "column",
|
|
1053
|
+
gap: 2,
|
|
1054
|
+
marginTop: 8
|
|
1055
|
+
},
|
|
1056
|
+
children: hits.map((hit) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1057
|
+
type: "button",
|
|
1058
|
+
"data-session-search-result": true,
|
|
1059
|
+
onClick: () => {
|
|
1060
|
+
openSession(hit.sessionId);
|
|
1061
|
+
onClose();
|
|
1062
|
+
},
|
|
1063
|
+
title: hit.snippet,
|
|
1064
|
+
style: {
|
|
1065
|
+
display: "block",
|
|
1066
|
+
width: "100%",
|
|
1067
|
+
minWidth: 0,
|
|
1068
|
+
padding: "6px 8px",
|
|
1069
|
+
background: "transparent",
|
|
1070
|
+
border: "none",
|
|
1071
|
+
borderRadius: 6,
|
|
1072
|
+
cursor: "pointer",
|
|
1073
|
+
color: "#e6e8ee",
|
|
1074
|
+
textAlign: "left"
|
|
1075
|
+
},
|
|
1076
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1077
|
+
style: {
|
|
1078
|
+
fontSize: 12,
|
|
1079
|
+
color: "#9db8ff",
|
|
1080
|
+
whiteSpace: "nowrap",
|
|
1081
|
+
overflow: "hidden",
|
|
1082
|
+
textOverflow: "ellipsis"
|
|
1083
|
+
},
|
|
1084
|
+
children: hit.title ?? t("search.untitled")
|
|
1085
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1086
|
+
style: {
|
|
1087
|
+
fontSize: 13,
|
|
1088
|
+
lineHeight: 1.4,
|
|
1089
|
+
color: "#c6ccd8",
|
|
1090
|
+
overflow: "hidden",
|
|
1091
|
+
textOverflow: "ellipsis",
|
|
1092
|
+
whiteSpace: "nowrap"
|
|
1093
|
+
},
|
|
1094
|
+
children: hit.snippet
|
|
1095
|
+
})]
|
|
1096
|
+
}, hit.sessionId))
|
|
1097
|
+
}), hasMore && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1098
|
+
"data-session-search-more": true,
|
|
1099
|
+
style: {
|
|
1100
|
+
marginTop: 6,
|
|
1101
|
+
fontSize: 12,
|
|
1102
|
+
color: "#8b96ab"
|
|
1103
|
+
},
|
|
1104
|
+
children: t("search.more")
|
|
1105
|
+
})] })
|
|
1106
|
+
]
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
//#endregion
|
|
779
1110
|
//#region src/client/useCurrentAnchor.ts
|
|
780
1111
|
/**
|
|
781
1112
|
* useCurrentAnchor: tracks which user-message row sits at/just above the
|
|
@@ -890,15 +1221,38 @@ window.__ModuleLoader__.load({
|
|
|
890
1221
|
70% { box-shadow: 0 0 0 5px transparent; opacity: 0.35 }
|
|
891
1222
|
100% { box-shadow: 0 0 0 0 transparent; opacity: 0.85 }
|
|
892
1223
|
}`;
|
|
1224
|
+
/**
|
|
1225
|
+
* P3 focus mode: dims the harness's AI thinking/scratchpad blocks so the
|
|
1226
|
+
* conversation reads cleaner. The rule targets the stable, un-hashed
|
|
1227
|
+
* `data-variant="think"` attribute on the thinking-block ROOT (the harness
|
|
1228
|
+
* renders it as `data-variant="think"` with `data-state="running|ok"`), so an
|
|
1229
|
+
* overlay plugin can dim it with plain CSS. Hovering a dimmed block (or
|
|
1230
|
+
* opening it, `[data-open]`) restores full opacity. Kept in an inline
|
|
1231
|
+
* <style> so the plugin stays zero-asset — same pattern as BADGE_PULSE_CSS.
|
|
1232
|
+
*/
|
|
1233
|
+
const FOCUS_CSS = `[data-variant="think"] { opacity: 0.4; transition: opacity 0.2s; }
|
|
1234
|
+
[data-variant="think"]:hover, [data-variant="think"] [data-open] { opacity: 1; }`;
|
|
893
1235
|
/** Visual dot diameter (px). */
|
|
894
|
-
const DOT_SIZE =
|
|
1236
|
+
const DOT_SIZE = 14;
|
|
895
1237
|
/** Hit area per dot (px) — larger than the dot for comfortable clicking. */
|
|
896
|
-
const DOT_HIT =
|
|
1238
|
+
const DOT_HIT = 28;
|
|
897
1239
|
/** Vertical gap between dot hit areas (px) — fixed pitch, never scaled. */
|
|
898
|
-
const DOT_GAP =
|
|
1240
|
+
const DOT_GAP = 14;
|
|
899
1241
|
/** Inward offset from the scrollport right edge so the rail clears the scrollbar. */
|
|
900
1242
|
const RAIL_INSET = 14;
|
|
901
1243
|
/**
|
|
1244
|
+
* P3 deep links (`#msg=<anchor-key>`): initial delay before the first
|
|
1245
|
+
* deep-link attempt — the harness scrolls the conversation to the bottom on
|
|
1246
|
+
* load, so the deep link must land AFTER the view mounts.
|
|
1247
|
+
*/
|
|
1248
|
+
const DEEP_LINK_INITIAL_DELAY = 100;
|
|
1249
|
+
/** P3: interval between DOM-row polls while waiting for the target to render. */
|
|
1250
|
+
const DEEP_LINK_POLL_DELAY = 150;
|
|
1251
|
+
/** P3: polls before falling back to a single `loadOlder` fetch. */
|
|
1252
|
+
const DEEP_LINK_MAX_POLLS = 5;
|
|
1253
|
+
/** P3: bounded polls after `loadOlder`, then the deep link gives up silently. */
|
|
1254
|
+
const DEEP_LINK_MAX_RETRY_POLLS = 5;
|
|
1255
|
+
/**
|
|
902
1256
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
903
1257
|
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
904
1258
|
*/
|
|
@@ -930,7 +1284,10 @@ window.__ModuleLoader__.load({
|
|
|
930
1284
|
* `locale: 'dsh-milestone'`; defaults to a key-pass fallback for renders
|
|
931
1285
|
* outside the slot machinery).
|
|
932
1286
|
*/
|
|
933
|
-
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions,
|
|
1287
|
+
function MilestoneRail({ useSession, loadOlder, forkAt, useStore, actions, searchSessions = async () => ({
|
|
1288
|
+
items: [],
|
|
1289
|
+
hasMore: false
|
|
1290
|
+
}), openSession = () => {}, t = (key) => key }) {
|
|
934
1291
|
const order = useSession((s) => s.chat.order);
|
|
935
1292
|
const nodes = useSession((s) => s.chat.nodes);
|
|
936
1293
|
const locations = useSession((s) => s.chat.locations);
|
|
@@ -981,12 +1338,33 @@ window.__ModuleLoader__.load({
|
|
|
981
1338
|
panelOpen: false
|
|
982
1339
|
});
|
|
983
1340
|
const [bookmarksOnly, setBookmarksOnly] = (0, react.useState)(false);
|
|
1341
|
+
const [focusActive, setFocusActive] = (0, react.useState)(false);
|
|
1342
|
+
const [listOpen, setListOpen] = (0, react.useState)(false);
|
|
1343
|
+
const [crossOpen, setCrossOpen] = (0, react.useState)(false);
|
|
984
1344
|
const [copiedKey, setCopiedKey] = (0, react.useState)(null);
|
|
985
1345
|
const [forkedKey, setForkedKey] = (0, react.useState)(null);
|
|
986
1346
|
const [collapsedTurns, setCollapsedTurns] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
987
1347
|
const [focusIndex, setFocusIndex] = (0, react.useState)(0);
|
|
988
1348
|
const listRef = (0, react.useRef)(null);
|
|
989
1349
|
const currentKey = useCurrentAnchor(order);
|
|
1350
|
+
/**
|
|
1351
|
+
* P3: jump to the chat row with the given node key — smooth-scroll it into
|
|
1352
|
+
* view and write the position back into the URL hash (`#msg=<key>`) so
|
|
1353
|
+
* refresh and share preserve it. `history.replaceState` (not a
|
|
1354
|
+
* `location.hash` assignment) keeps the history stack clean, and it never
|
|
1355
|
+
* fires `hashchange`, so the deep-link listeners below never echo the
|
|
1356
|
+
* rail's own updates. No-op when the row is not (yet) rendered — the
|
|
1357
|
+
* deep-link mount retry and the load-older flow cover that case.
|
|
1358
|
+
*/
|
|
1359
|
+
const jump = (key) => {
|
|
1360
|
+
const row = findRow(key);
|
|
1361
|
+
if (row === null) return;
|
|
1362
|
+
row.scrollIntoView({
|
|
1363
|
+
behavior: "smooth",
|
|
1364
|
+
block: "start"
|
|
1365
|
+
});
|
|
1366
|
+
history.replaceState(null, "", buildMessageHash(key));
|
|
1367
|
+
};
|
|
990
1368
|
const displayMarks = (0, react.useMemo)(() => {
|
|
991
1369
|
if (!bookmarksOnly) return marks;
|
|
992
1370
|
return filterByBookmarks(marks, bookmarkedKeys).visible.map((i) => marks[i]);
|
|
@@ -1041,13 +1419,59 @@ window.__ModuleLoader__.load({
|
|
|
1041
1419
|
(0, react.useLayoutEffect)(() => {
|
|
1042
1420
|
setFocusIndex((f) => clampIndex(f, render.items.length));
|
|
1043
1421
|
}, [render.items.length]);
|
|
1422
|
+
(0, react.useEffect)(() => {
|
|
1423
|
+
if (!listOpen && !crossOpen) return;
|
|
1424
|
+
const onKey = (e) => {
|
|
1425
|
+
if (e.key !== "Escape") return;
|
|
1426
|
+
setListOpen(false);
|
|
1427
|
+
setCrossOpen(false);
|
|
1428
|
+
};
|
|
1429
|
+
window.addEventListener("keydown", onKey);
|
|
1430
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1431
|
+
}, [listOpen, crossOpen]);
|
|
1432
|
+
const marksRef = (0, react.useRef)(marks);
|
|
1433
|
+
(0, react.useEffect)(() => {
|
|
1434
|
+
marksRef.current = marks;
|
|
1435
|
+
});
|
|
1436
|
+
(0, react.useEffect)(() => {
|
|
1437
|
+
const key = parseDeepLinkHash(window.location.hash);
|
|
1438
|
+
if (key === null) return;
|
|
1439
|
+
let cancelled = false;
|
|
1440
|
+
let timer;
|
|
1441
|
+
const attempt = (pollsLeft, canLoadOlder) => {
|
|
1442
|
+
if (cancelled) return;
|
|
1443
|
+
if (findRow(key) !== null) {
|
|
1444
|
+
jump(key);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (marksRef.current.length > 0 && !marksRef.current.some((m) => m.key === key)) return;
|
|
1448
|
+
if (pollsLeft > 0) {
|
|
1449
|
+
timer = window.setTimeout(() => attempt(pollsLeft - 1, canLoadOlder), DEEP_LINK_POLL_DELAY);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (canLoadOlder) {
|
|
1453
|
+
loadOlder().then(() => {
|
|
1454
|
+
timer = window.setTimeout(() => attempt(DEEP_LINK_MAX_RETRY_POLLS, false), DEEP_LINK_POLL_DELAY);
|
|
1455
|
+
}, () => {});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
timer = window.setTimeout(() => attempt(DEEP_LINK_MAX_POLLS, true), DEEP_LINK_INITIAL_DELAY);
|
|
1460
|
+
return () => {
|
|
1461
|
+
cancelled = true;
|
|
1462
|
+
if (timer !== void 0) window.clearTimeout(timer);
|
|
1463
|
+
};
|
|
1464
|
+
}, []);
|
|
1465
|
+
(0, react.useEffect)(() => {
|
|
1466
|
+
const onHashChange = () => {
|
|
1467
|
+
const key = parseDeepLinkHash(window.location.hash);
|
|
1468
|
+
if (key === null) return;
|
|
1469
|
+
if (marksRef.current.some((m) => m.key === key)) jump(key);
|
|
1470
|
+
};
|
|
1471
|
+
window.addEventListener("hashchange", onHashChange);
|
|
1472
|
+
return () => window.removeEventListener("hashchange", onHashChange);
|
|
1473
|
+
}, []);
|
|
1044
1474
|
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
1045
|
-
const jump = (key) => {
|
|
1046
|
-
findRow(key)?.scrollIntoView({
|
|
1047
|
-
behavior: "smooth",
|
|
1048
|
-
block: "start"
|
|
1049
|
-
});
|
|
1050
|
-
};
|
|
1051
1475
|
const updateQuery = (query) => {
|
|
1052
1476
|
setSearch({
|
|
1053
1477
|
query,
|
|
@@ -1208,11 +1632,15 @@ window.__ModuleLoader__.load({
|
|
|
1208
1632
|
pointerEvents: "auto",
|
|
1209
1633
|
zIndex: 100,
|
|
1210
1634
|
display: "flex",
|
|
1211
|
-
flexDirection: "column"
|
|
1635
|
+
flexDirection: "column",
|
|
1636
|
+
gap: 6,
|
|
1637
|
+
paddingTop: 6
|
|
1212
1638
|
},
|
|
1213
1639
|
"aria-label": t("rail.label"),
|
|
1640
|
+
"data-focus-active": focusActive ? "true" : void 0,
|
|
1214
1641
|
children: [
|
|
1215
1642
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BADGE_PULSE_CSS }),
|
|
1643
|
+
focusActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: FOCUS_CSS }),
|
|
1216
1644
|
showLoadOlder && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1217
1645
|
type: "button",
|
|
1218
1646
|
"data-load-older": true,
|
|
@@ -1235,7 +1663,7 @@ window.__ModuleLoader__.load({
|
|
|
1235
1663
|
padding: 0,
|
|
1236
1664
|
cursor: loadingOlder ? "default" : "pointer",
|
|
1237
1665
|
color: loadingOlder ? "#5a6375" : "#8b96ab",
|
|
1238
|
-
fontSize:
|
|
1666
|
+
fontSize: 13,
|
|
1239
1667
|
lineHeight: 1,
|
|
1240
1668
|
letterSpacing: 1
|
|
1241
1669
|
},
|
|
@@ -1262,8 +1690,8 @@ window.__ModuleLoader__.load({
|
|
|
1262
1690
|
color: bookmarksOnly ? "#9db8ff" : "#8b96ab"
|
|
1263
1691
|
},
|
|
1264
1692
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1265
|
-
width: "
|
|
1266
|
-
height: "
|
|
1693
|
+
width: "16",
|
|
1694
|
+
height: "16",
|
|
1267
1695
|
viewBox: "0 0 24 24",
|
|
1268
1696
|
fill: bookmarksOnly ? "currentColor" : "none",
|
|
1269
1697
|
stroke: "currentColor",
|
|
@@ -1273,6 +1701,122 @@ window.__ModuleLoader__.load({
|
|
|
1273
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" })
|
|
1274
1702
|
})
|
|
1275
1703
|
}),
|
|
1704
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1705
|
+
type: "button",
|
|
1706
|
+
"data-focus-toggle": true,
|
|
1707
|
+
"aria-label": focusActive ? t("focus.off") : t("focus.on"),
|
|
1708
|
+
title: focusActive ? t("focus.off") : t("focus.on"),
|
|
1709
|
+
"aria-pressed": focusActive,
|
|
1710
|
+
onClick: () => setFocusActive((v) => !v),
|
|
1711
|
+
style: {
|
|
1712
|
+
width: DOT_HIT,
|
|
1713
|
+
height: DOT_HIT,
|
|
1714
|
+
flexShrink: 0,
|
|
1715
|
+
display: "flex",
|
|
1716
|
+
alignItems: "center",
|
|
1717
|
+
justifyContent: "center",
|
|
1718
|
+
background: focusActive ? "rgba(126, 226, 168, 0.14)" : "transparent",
|
|
1719
|
+
border: "none",
|
|
1720
|
+
padding: 0,
|
|
1721
|
+
cursor: "pointer",
|
|
1722
|
+
color: focusActive ? "#7ee2a8" : "#8b96ab"
|
|
1723
|
+
},
|
|
1724
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1725
|
+
width: "16",
|
|
1726
|
+
height: "16",
|
|
1727
|
+
viewBox: "0 0 24 24",
|
|
1728
|
+
fill: "none",
|
|
1729
|
+
stroke: "currentColor",
|
|
1730
|
+
strokeWidth: "2",
|
|
1731
|
+
strokeLinecap: "round",
|
|
1732
|
+
strokeLinejoin: "round",
|
|
1733
|
+
"aria-hidden": "true",
|
|
1734
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1735
|
+
cx: "12",
|
|
1736
|
+
cy: "12",
|
|
1737
|
+
r: "3"
|
|
1738
|
+
})]
|
|
1739
|
+
})
|
|
1740
|
+
}),
|
|
1741
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1742
|
+
type: "button",
|
|
1743
|
+
"data-list-toggle": true,
|
|
1744
|
+
"aria-label": listOpen ? t("list.close") : t("list.open"),
|
|
1745
|
+
title: listOpen ? t("list.close") : t("list.open"),
|
|
1746
|
+
"aria-pressed": listOpen,
|
|
1747
|
+
onClick: () => setListOpen((v) => !v),
|
|
1748
|
+
style: {
|
|
1749
|
+
width: DOT_HIT,
|
|
1750
|
+
height: DOT_HIT,
|
|
1751
|
+
flexShrink: 0,
|
|
1752
|
+
display: "flex",
|
|
1753
|
+
alignItems: "center",
|
|
1754
|
+
justifyContent: "center",
|
|
1755
|
+
background: listOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
1756
|
+
border: "none",
|
|
1757
|
+
padding: 0,
|
|
1758
|
+
cursor: "pointer",
|
|
1759
|
+
color: listOpen ? "#9db8ff" : "#8b96ab"
|
|
1760
|
+
},
|
|
1761
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1762
|
+
width: "16",
|
|
1763
|
+
height: "16",
|
|
1764
|
+
viewBox: "0 0 24 24",
|
|
1765
|
+
fill: "none",
|
|
1766
|
+
stroke: "currentColor",
|
|
1767
|
+
strokeWidth: "2.5",
|
|
1768
|
+
strokeLinecap: "round",
|
|
1769
|
+
"aria-hidden": "true",
|
|
1770
|
+
children: [
|
|
1771
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h18" }),
|
|
1772
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h18" }),
|
|
1773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h18" })
|
|
1774
|
+
]
|
|
1775
|
+
})
|
|
1776
|
+
}),
|
|
1777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1778
|
+
type: "button",
|
|
1779
|
+
"data-session-search-toggle": true,
|
|
1780
|
+
"aria-label": crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
1781
|
+
title: crossOpen ? t("search.cross.close") : t("search.cross.open"),
|
|
1782
|
+
"aria-pressed": crossOpen,
|
|
1783
|
+
onClick: () => setCrossOpen((v) => !v),
|
|
1784
|
+
style: {
|
|
1785
|
+
width: DOT_HIT,
|
|
1786
|
+
height: DOT_HIT,
|
|
1787
|
+
flexShrink: 0,
|
|
1788
|
+
display: "flex",
|
|
1789
|
+
alignItems: "center",
|
|
1790
|
+
justifyContent: "center",
|
|
1791
|
+
background: crossOpen ? "rgba(77, 124, 254, 0.18)" : "transparent",
|
|
1792
|
+
border: "none",
|
|
1793
|
+
padding: 0,
|
|
1794
|
+
cursor: "pointer",
|
|
1795
|
+
color: crossOpen ? "#9db8ff" : "#8b96ab"
|
|
1796
|
+
},
|
|
1797
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1798
|
+
width: "16",
|
|
1799
|
+
height: "16",
|
|
1800
|
+
viewBox: "0 0 24 24",
|
|
1801
|
+
fill: "none",
|
|
1802
|
+
stroke: "currentColor",
|
|
1803
|
+
strokeWidth: "2",
|
|
1804
|
+
strokeLinecap: "round",
|
|
1805
|
+
strokeLinejoin: "round",
|
|
1806
|
+
"aria-hidden": "true",
|
|
1807
|
+
children: [
|
|
1808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 6h9" }),
|
|
1809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 12h9" }),
|
|
1810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 18h9" }),
|
|
1811
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1812
|
+
cx: "17",
|
|
1813
|
+
cy: "7",
|
|
1814
|
+
r: "3.5"
|
|
1815
|
+
}),
|
|
1816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "m19.5 9.5 2.5 2.5" })
|
|
1817
|
+
]
|
|
1818
|
+
})
|
|
1819
|
+
}),
|
|
1276
1820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RailSearchUi, {
|
|
1277
1821
|
panelTop: railBox.top,
|
|
1278
1822
|
panelRight: railBox.right + DOT_HIT + 8,
|
|
@@ -1289,6 +1833,21 @@ window.__ModuleLoader__.load({
|
|
|
1289
1833
|
onClear: clearSearch,
|
|
1290
1834
|
t
|
|
1291
1835
|
}),
|
|
1836
|
+
listOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneListPanel, {
|
|
1837
|
+
panelTop: railBox.top,
|
|
1838
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
1839
|
+
marks,
|
|
1840
|
+
onJump: jump,
|
|
1841
|
+
t
|
|
1842
|
+
}),
|
|
1843
|
+
crossOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MilestoneSessionSearch, {
|
|
1844
|
+
panelTop: railBox.top,
|
|
1845
|
+
panelRight: railBox.right + DOT_HIT + 8,
|
|
1846
|
+
onClose: () => setCrossOpen(false),
|
|
1847
|
+
searchSessions,
|
|
1848
|
+
openSession,
|
|
1849
|
+
t
|
|
1850
|
+
}),
|
|
1292
1851
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1293
1852
|
ref: listRef,
|
|
1294
1853
|
"data-rail-list": true,
|
|
@@ -1421,7 +1980,7 @@ window.__ModuleLoader__.load({
|
|
|
1421
1980
|
right: "100%",
|
|
1422
1981
|
marginRight: 8,
|
|
1423
1982
|
whiteSpace: "nowrap",
|
|
1424
|
-
fontSize:
|
|
1983
|
+
fontSize: 12,
|
|
1425
1984
|
lineHeight: 1,
|
|
1426
1985
|
color: "rgba(139, 150, 171, 0.9)",
|
|
1427
1986
|
pointerEvents: "none",
|
|
@@ -1465,6 +2024,46 @@ window.__ModuleLoader__.load({
|
|
|
1465
2024
|
//#endregion
|
|
1466
2025
|
//#region src/client/railInject.ts
|
|
1467
2026
|
/**
|
|
2027
|
+
* Wrap the `session.search` RPC into a safe cross-session search action.
|
|
2028
|
+
*
|
|
2029
|
+
* - `ok: true` unwraps the value and joins each hit's human display title
|
|
2030
|
+
* from the session list snapshot (a session outside the list keeps no
|
|
2031
|
+
* title — the caller falls back to `search.untitled`).
|
|
2032
|
+
* - `ok: false` throws `new Error(error.message)` so the caller can surface
|
|
2033
|
+
* the business/transport error as the `search.error` state.
|
|
2034
|
+
* - A rejected RPC propagates unchanged.
|
|
2035
|
+
*
|
|
2036
|
+
* The list read happens inside the returned closure (never at factory time),
|
|
2037
|
+
* so the title join always reflects the current list snapshot.
|
|
2038
|
+
*
|
|
2039
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
2040
|
+
* @returns an action that searches all sessions' message content.
|
|
2041
|
+
*/
|
|
2042
|
+
function createSessionSearch(sessions) {
|
|
2043
|
+
return async (query, signal) => {
|
|
2044
|
+
const result = await sessions.search(query, signal);
|
|
2045
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
2046
|
+
const byId = sessions.list.getSnapshot().byId;
|
|
2047
|
+
return {
|
|
2048
|
+
items: result.value.items.map((item) => ({
|
|
2049
|
+
...item,
|
|
2050
|
+
title: byId[item.sessionId]?.displayTitle
|
|
2051
|
+
})),
|
|
2052
|
+
hasMore: result.value.hasMore
|
|
2053
|
+
};
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Wrap a session `open` call into a safe action that selects a listed session
|
|
2058
|
+
* as current — the exact selection path the sidebar uses on click.
|
|
2059
|
+
*
|
|
2060
|
+
* @param sessions - the injected sessions service (`ctx.sessions`).
|
|
2061
|
+
* @returns an action that opens the given session.
|
|
2062
|
+
*/
|
|
2063
|
+
function createOpenSession(sessions) {
|
|
2064
|
+
return (id) => sessions.open(id);
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
1468
2067
|
* Wrap a session-bound `loadOlder` call into a safe action closure.
|
|
1469
2068
|
*
|
|
1470
2069
|
* - Missing binding: resolves (never throws on an unlisted/unscoped session).
|
|
@@ -1530,6 +2129,10 @@ window.__ModuleLoader__.load({
|
|
|
1530
2129
|
"search.label": "搜索消息",
|
|
1531
2130
|
/** aria-label on the bookmarks-only filter toggle. */
|
|
1532
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": "退出聚焦",
|
|
1533
2136
|
/** aria-label on the hover tooltip star toggle. */
|
|
1534
2137
|
"bookmark.star": "收藏此消息",
|
|
1535
2138
|
/** aria-label on the search clear button. */
|
|
@@ -1573,7 +2176,25 @@ window.__ModuleLoader__.load({
|
|
|
1573
2176
|
/** Collapse-turn tooltip action. */
|
|
1574
2177
|
"collapse.turn": "折叠此轮",
|
|
1575
2178
|
/** Expand-turn tooltip action. */
|
|
1576
|
-
"expand.turn": "展开此轮"
|
|
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": "结果已截断,请细化关键词"
|
|
1577
2198
|
};
|
|
1578
2199
|
const en = {
|
|
1579
2200
|
"jump.to": "Jump to message {n}",
|
|
@@ -1583,6 +2204,8 @@ window.__ModuleLoader__.load({
|
|
|
1583
2204
|
"search.placeholder": "Search message content",
|
|
1584
2205
|
"search.label": "Search messages",
|
|
1585
2206
|
"bookmark.filter": "Bookmarks only",
|
|
2207
|
+
"focus.on": "Focus mode",
|
|
2208
|
+
"focus.off": "Exit focus",
|
|
1586
2209
|
"bookmark.star": "Bookmark this message",
|
|
1587
2210
|
"search.clear": "Clear search",
|
|
1588
2211
|
"load.older": "Load older messages",
|
|
@@ -1604,7 +2227,16 @@ window.__ModuleLoader__.load({
|
|
|
1604
2227
|
"copy.message": "Copy message",
|
|
1605
2228
|
"fork.here": "Fork from here",
|
|
1606
2229
|
"collapse.turn": "Collapse turn",
|
|
1607
|
-
"expand.turn": "Expand 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"
|
|
1608
2240
|
};
|
|
1609
2241
|
//#endregion
|
|
1610
2242
|
//#region src/client/index.ts
|
|
@@ -1641,7 +2273,9 @@ window.__ModuleLoader__.load({
|
|
|
1641
2273
|
locale: "dsh-milestone",
|
|
1642
2274
|
inject: (sessionId) => ({
|
|
1643
2275
|
loadOlder: createLoadOlder(ctx.sessions, sessionId),
|
|
1644
|
-
forkAt: createForkAt(ctx.sessions, sessionId)
|
|
2276
|
+
forkAt: createForkAt(ctx.sessions, sessionId),
|
|
2277
|
+
searchSessions: createSessionSearch(ctx.sessions),
|
|
2278
|
+
openSession: createOpenSession(ctx.sessions)
|
|
1645
2279
|
})
|
|
1646
2280
|
}, MilestoneRail));
|
|
1647
2281
|
}
|
package/package.json
CHANGED