dsh-conversation-navigator 0.1.16 → 0.1.17
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 +3 -1
- package/lib/client.js +97 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Codex 风格的 **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页*
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
9
|
- **按轮折叠大纲**:默认只显示"第 N 轮 + 你的问题"列表,长对话一目了然
|
|
10
|
+
- **关键词过滤**:顶部搜索框只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
|
|
10
11
|
- **展开/折叠步次**:点击轮次行右侧的 `▸ N` / `▾` 展开或折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
|
|
11
12
|
- **点击定位**:点击轮次主体或步次条目,平滑滚动跳转到对话中对应位置(不改变折叠状态)
|
|
12
13
|
- **加载更早 / 加载全部**:面板顶部两个按钮——「加载更早」向后翻一页、「加载全部」一键把所有历史轮次载入导航,之后可任意跳转(页面默认仍懒加载,只有点按钮才补载)
|
|
@@ -26,7 +27,7 @@ Codex 风格的 **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页*
|
|
|
26
27
|
dsh plugin --profile web add dsh-conversation-navigator
|
|
27
28
|
|
|
28
29
|
# 方式二:GitHub(纯 JS 包无需 prepare/allowBuilds,直接可用)
|
|
29
|
-
dsh plugin --profile web add github:gjj-star/dsh-conversation-
|
|
30
|
+
dsh plugin --profile web add github:gjj-star/dsh-conversation-navigator
|
|
30
31
|
|
|
31
32
|
# 方式三:本地 tarball
|
|
32
33
|
pnpm pack
|
|
@@ -48,6 +49,7 @@ dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
|
|
|
48
49
|
- 历史补载:通过 `sessions` 服务的 `binding(sessionId).session.loadOlder()` 分页向后加载,「加载全部」循环至 `hasMore=false`
|
|
49
50
|
- 跳转定位:复用 DSH 聊天视图自身的稳定 DOM 锚点 `[data-chat-anchor-key]`(与产品内部 paging/scroll 定位同源),`scrollIntoView` 平滑滚动
|
|
50
51
|
- 位置跟踪:捕获 `[data-conversation-scroll]` 滚动容器的 scroll 事件(节流 120ms),计算视口顶部首个可见节点
|
|
52
|
+
- 关键词过滤:仅为 `user` 与 `assistant-step` 节点提取检索文本(`dialogueText`),大小写不敏感匹配,命中片段以 `<mark>` 高亮并按首个命中位置截取显示窗口
|
|
51
53
|
- 样式:按钮/图标复用 `@deepseek-ai/dsh-client-ui-primitives`;面板容器自建 `<style>` 注入,颜色使用 `--dsw-*` 主题 token;插件卸载时随 fiber 清理
|
|
52
54
|
|
|
53
55
|
## 兼容性说明
|
package/lib/client.js
CHANGED
|
@@ -117,7 +117,7 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
}
|
|
118
118
|
.cnvnav-chevron-wrap { flex: none; display: flex; }
|
|
119
119
|
.cnvnav-chevron {
|
|
120
|
-
min-width: 30px;
|
|
120
|
+
min-width: 30px; align-self: stretch;
|
|
121
121
|
display: flex; align-items: center; justify-content: center;
|
|
122
122
|
border: none; background: transparent; cursor: pointer;
|
|
123
123
|
color: var(--dsw-alias-label-secondary, #888);
|
|
@@ -162,6 +162,10 @@ window.__ModuleLoader__.load({
|
|
|
162
162
|
.cnvnav-item-time {
|
|
163
163
|
flex: none; color: var(--dsw-alias-label-secondary, #888); font-size: 10px; opacity: .8;
|
|
164
164
|
}
|
|
165
|
+
.cnvnav-hl {
|
|
166
|
+
background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 30%, transparent);
|
|
167
|
+
color: inherit; border-radius: 2px; padding: 0 1px; font-weight: 600;
|
|
168
|
+
}
|
|
165
169
|
.cnvnav-empty { padding: 18px 10px; text-align: center; color: var(--dsw-alias-label-secondary, #888); }
|
|
166
170
|
.cnvnav-filter-row { padding: 6px 6px 0; }
|
|
167
171
|
.cnvnav-filter {
|
|
@@ -265,6 +269,23 @@ window.__ModuleLoader__.load({
|
|
|
265
269
|
}
|
|
266
270
|
return null;
|
|
267
271
|
}
|
|
272
|
+
/* Searchable dialogue text: user questions + agent actual dialogue only
|
|
273
|
+
(excludes context, tool calls, commands, compaction, reasoning, etc.) */
|
|
274
|
+
function dialogueText(node) {
|
|
275
|
+
const d = node && node.data;
|
|
276
|
+
if (!d || typeof d !== "object") return null;
|
|
277
|
+
if (node.kind === "user") return firstText(d.content);
|
|
278
|
+
if (node.kind === "assistant-step") {
|
|
279
|
+
if (!Array.isArray(d.blocks)) return null;
|
|
280
|
+
const parts = [];
|
|
281
|
+
for (const b of d.blocks) {
|
|
282
|
+
if (!b || typeof b !== "object") continue;
|
|
283
|
+
if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
|
|
284
|
+
}
|
|
285
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
268
289
|
function previewFor(node) {
|
|
269
290
|
const d = node && node.data;
|
|
270
291
|
if (!d || typeof d !== "object") return null;
|
|
@@ -355,6 +376,7 @@ window.__ModuleLoader__.load({
|
|
|
355
376
|
label: meta.label,
|
|
356
377
|
cls: meta.cls,
|
|
357
378
|
preview: previewFor(node),
|
|
379
|
+
search: dialogueText(node),
|
|
358
380
|
time: fmtTime(timeOf(node)),
|
|
359
381
|
};
|
|
360
382
|
const turn = turnOf(node);
|
|
@@ -550,7 +572,41 @@ window.__ModuleLoader__.load({
|
|
|
550
572
|
function hit(text) {
|
|
551
573
|
return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
|
|
552
574
|
}
|
|
575
|
+
/* Split text into nodes, wrapping every case-insensitive keyword match in a <mark>. */
|
|
576
|
+
function highlightedNodes(text, kw) {
|
|
577
|
+
if (typeof text !== "string" || kw === "") return [text];
|
|
578
|
+
const lower = text.toLowerCase();
|
|
579
|
+
const out = [];
|
|
580
|
+
let i = 0;
|
|
581
|
+
let m = 0;
|
|
582
|
+
for (;;) {
|
|
583
|
+
const idx = lower.indexOf(kw, i);
|
|
584
|
+
if (idx === -1) { if (i < text.length) out.push(text.slice(i)); break; }
|
|
585
|
+
if (idx > i) out.push(text.slice(i, idx));
|
|
586
|
+
out.push(React.createElement("mark", { key: "m" + (m++), className: "cnvnav-hl" }, text.slice(idx, idx + kw.length)));
|
|
587
|
+
i = idx + kw.length;
|
|
588
|
+
}
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
/* Window the text around the first keyword match (so the keyword is visible,
|
|
592
|
+
not hidden behind head-truncation), then highlight every occurrence. */
|
|
593
|
+
function snippetNodes(text, kw, before, after) {
|
|
594
|
+
if (typeof text !== "string") return [text];
|
|
595
|
+
const idx = text.toLowerCase().indexOf(kw);
|
|
596
|
+
if (idx === -1) return highlightedNodes(text, kw);
|
|
597
|
+
const start = Math.max(0, idx - before);
|
|
598
|
+
const end = Math.min(text.length, idx + kw.length + after);
|
|
599
|
+
const s = text.slice(start, end);
|
|
600
|
+
const parts = [];
|
|
601
|
+
if (start > 0) parts.push("…");
|
|
602
|
+
parts.push(...highlightedNodes(s, kw));
|
|
603
|
+
if (end < text.length) parts.push("…");
|
|
604
|
+
return parts;
|
|
605
|
+
}
|
|
553
606
|
function renderItem(item) {
|
|
607
|
+
const useSnippet = filtering && hit(item.search);
|
|
608
|
+
const preview = useSnippet ? snippetNodes(item.search, kw, 20, 50) : item.preview;
|
|
609
|
+
const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
|
|
554
610
|
return React.createElement("button", {
|
|
555
611
|
key: item.key,
|
|
556
612
|
type: "button",
|
|
@@ -559,15 +615,19 @@ window.__ModuleLoader__.load({
|
|
|
559
615
|
onClick: () => jumpTo(item.key),
|
|
560
616
|
},
|
|
561
617
|
React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
|
|
562
|
-
|
|
563
|
-
? React.createElement("span", { className: "cnvnav-item-preview" },
|
|
618
|
+
hasPreview
|
|
619
|
+
? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
|
|
564
620
|
: null,
|
|
565
621
|
item.time !== ""
|
|
566
622
|
? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
|
|
567
623
|
: null,
|
|
568
624
|
);
|
|
569
625
|
}
|
|
570
|
-
function renderGroupHead(g, groupActive) {
|
|
626
|
+
function renderGroupHead(g, groupActive, chevron) {
|
|
627
|
+
const headSearch = g.items.length > 0 ? g.items[0].search : null;
|
|
628
|
+
const useSnippet = filtering && hit(headSearch);
|
|
629
|
+
const sub = useSnippet ? snippetNodes(headSearch, kw, 15, 40) : g.subtitle;
|
|
630
|
+
const hasSub = sub !== "" && (!Array.isArray(sub) || sub.length > 0);
|
|
571
631
|
return React.createElement("div", {
|
|
572
632
|
ref: (el) => { groupEls[g.gid] = el; },
|
|
573
633
|
className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
|
|
@@ -584,12 +644,13 @@ window.__ModuleLoader__.load({
|
|
|
584
644
|
onClick: () => jumpTo(g.headKey),
|
|
585
645
|
},
|
|
586
646
|
React.createElement("span", { className: "cnvnav-group-title" }, g.title),
|
|
587
|
-
|
|
588
|
-
? React.createElement("span", { className: "cnvnav-group-sub" },
|
|
647
|
+
hasSub
|
|
648
|
+
? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
|
|
589
649
|
: null,
|
|
590
650
|
),
|
|
591
651
|
),
|
|
592
652
|
),
|
|
653
|
+
chevron !== undefined ? chevron : null,
|
|
593
654
|
);
|
|
594
655
|
}
|
|
595
656
|
|
|
@@ -597,14 +658,17 @@ window.__ModuleLoader__.load({
|
|
|
597
658
|
if (filtering) {
|
|
598
659
|
groupNodes = [];
|
|
599
660
|
for (const g of groups) {
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
661
|
+
const headHit = g.headIsUser && g.items.length > 0 && hit(g.items[0].search);
|
|
662
|
+
const hits = g.items.filter((it, i) =>
|
|
663
|
+
(it.kind === "user" || it.kind === "assistant-step") &&
|
|
664
|
+
hit(it.search) &&
|
|
665
|
+
!(i === 0 && g.headIsUser)
|
|
666
|
+
);
|
|
667
|
+
if (!headHit && hits.length === 0) continue;
|
|
604
668
|
const groupActive = g.items.some((it) => it.key === snap.activeKey);
|
|
605
669
|
groupNodes.push(React.createElement("div", { key: g.gid, className: "cnvnav-group" },
|
|
606
|
-
renderGroupHead(g, groupActive),
|
|
607
|
-
|
|
670
|
+
renderGroupHead(g, groupActive, null),
|
|
671
|
+
hits.map(renderItem),
|
|
608
672
|
));
|
|
609
673
|
}
|
|
610
674
|
} else {
|
|
@@ -614,29 +678,29 @@ window.__ModuleLoader__.load({
|
|
|
614
678
|
const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
|
|
615
679
|
const hasSteps = stepItems.length > 0;
|
|
616
680
|
const groupActive = g.items.some((it) => it.key === snap.activeKey);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
e.stopPropagation();
|
|
632
|
-
setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
|
|
633
|
-
},
|
|
681
|
+
const chevron = hasSteps
|
|
682
|
+
? React.createElement(Tooltip, {
|
|
683
|
+
label: expanded ? "折叠步次" : "展开步次",
|
|
684
|
+
side: "top",
|
|
685
|
+
delayMs: 500,
|
|
686
|
+
},
|
|
687
|
+
React.createElement("span", { className: "cnvnav-chevron-wrap" },
|
|
688
|
+
React.createElement("button", {
|
|
689
|
+
type: "button",
|
|
690
|
+
className: "cnvnav-chevron",
|
|
691
|
+
"aria-expanded": expanded,
|
|
692
|
+
onClick: (e) => {
|
|
693
|
+
e.stopPropagation();
|
|
694
|
+
setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
|
|
634
695
|
},
|
|
635
|
-
|
|
636
|
-
|
|
696
|
+
},
|
|
697
|
+
expanded ? "▾" : "▸ " + stepItems.length,
|
|
637
698
|
),
|
|
638
|
-
)
|
|
639
|
-
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
: null;
|
|
702
|
+
return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
|
|
703
|
+
renderGroupHead(g, groupActive, chevron),
|
|
640
704
|
expanded && hasSteps
|
|
641
705
|
? stepItems.map(renderItem)
|
|
642
706
|
: null,
|