dsh-conversation-navigator 0.1.15 → 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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/lib/client.js +180 -65
  3. 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-navigato
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; width: 100%;
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,7 +162,24 @@ 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); }
170
+ .cnvnav-filter-row { padding: 6px 6px 0; }
171
+ .cnvnav-filter {
172
+ width: 100%; box-sizing: border-box;
173
+ height: 28px; padding: 0 8px;
174
+ border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
175
+ border-radius: 8px;
176
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06));
177
+ color: var(--dsw-alias-label-primary, #222);
178
+ font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif);
179
+ font-size: 12px; outline: none;
180
+ }
181
+ .cnvnav-filter::placeholder { color: var(--dsw-alias-label-secondary, #888); }
182
+ .cnvnav-filter:focus { border-color: var(--dsw-alias-brand-primary, #4f46e5); }
166
183
  .cnvnav-topbar { display: flex; gap: 6px; margin: 6px 6px 0; }
167
184
  .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
168
185
  .cnvnav-bar-btn { flex: 1; display: block; }
@@ -252,6 +269,23 @@ window.__ModuleLoader__.load({
252
269
  }
253
270
  return null;
254
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
+ }
255
289
  function previewFor(node) {
256
290
  const d = node && node.data;
257
291
  if (!d || typeof d !== "object") return null;
@@ -342,6 +376,7 @@ window.__ModuleLoader__.load({
342
376
  label: meta.label,
343
377
  cls: meta.cls,
344
378
  preview: previewFor(node),
379
+ search: dialogueText(node),
345
380
  time: fmtTime(timeOf(node)),
346
381
  };
347
382
  const turn = turnOf(node);
@@ -467,6 +502,7 @@ window.__ModuleLoader__.load({
467
502
  /* ---------- floating navigator panel ---------- */
468
503
  function Panel() {
469
504
  const snap = useStore();
505
+ const [filter, setFilter] = React.useState("");
470
506
 
471
507
  React.useEffect(() => {
472
508
  if (!snap.open) return;
@@ -531,78 +567,146 @@ window.__ModuleLoader__.load({
531
567
  const groups = snap.outline !== null ? snap.outline.groups : [];
532
568
  const count = snap.outline !== null ? snap.outline.count : 0;
533
569
 
534
- const groupNodes = groups.map((g) => {
535
- const expanded = snap.expanded[g.gid] === true;
536
- const isTurnGroup = g.turn !== null;
537
- const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
538
- const hasSteps = stepItems.length > 0;
539
- const groupActive = g.items.some((it) => it.key === snap.activeKey);
540
- return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
541
- React.createElement("div", {
542
- ref: (el) => { groupEls[g.gid] = el; },
543
- className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
570
+ const kw = filter.trim().toLowerCase();
571
+ const filtering = kw !== "";
572
+ function hit(text) {
573
+ return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
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
+ }
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);
610
+ return React.createElement("button", {
611
+ key: item.key,
612
+ type: "button",
613
+ ref: (el) => { itemEls[item.key] = el; },
614
+ className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : ""),
615
+ onClick: () => jumpTo(item.key),
616
+ },
617
+ React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
618
+ hasPreview
619
+ ? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
620
+ : null,
621
+ item.time !== ""
622
+ ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
623
+ : null,
624
+ );
625
+ }
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);
631
+ return React.createElement("div", {
632
+ ref: (el) => { groupEls[g.gid] = el; },
633
+ className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
634
+ },
635
+ React.createElement(Tooltip, {
636
+ label: "跳转到此位置",
637
+ side: "top",
638
+ delayMs: 500,
544
639
  },
545
- React.createElement(Tooltip, {
546
- label: "跳转到此位置",
640
+ React.createElement("span", { className: "cnvnav-group-head-wrap" },
641
+ React.createElement("button", {
642
+ type: "button",
643
+ className: "cnvnav-group-head",
644
+ onClick: () => jumpTo(g.headKey),
645
+ },
646
+ React.createElement("span", { className: "cnvnav-group-title" }, g.title),
647
+ hasSub
648
+ ? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
649
+ : null,
650
+ ),
651
+ ),
652
+ ),
653
+ chevron !== undefined ? chevron : null,
654
+ );
655
+ }
656
+
657
+ let groupNodes;
658
+ if (filtering) {
659
+ groupNodes = [];
660
+ for (const g of groups) {
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;
668
+ const groupActive = g.items.some((it) => it.key === snap.activeKey);
669
+ groupNodes.push(React.createElement("div", { key: g.gid, className: "cnvnav-group" },
670
+ renderGroupHead(g, groupActive, null),
671
+ hits.map(renderItem),
672
+ ));
673
+ }
674
+ } else {
675
+ groupNodes = groups.map((g) => {
676
+ const expanded = snap.expanded[g.gid] === true;
677
+ const isTurnGroup = g.turn !== null;
678
+ const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
679
+ const hasSteps = stepItems.length > 0;
680
+ const groupActive = g.items.some((it) => it.key === snap.activeKey);
681
+ const chevron = hasSteps
682
+ ? React.createElement(Tooltip, {
683
+ label: expanded ? "折叠步次" : "展开步次",
547
684
  side: "top",
548
685
  delayMs: 500,
549
686
  },
550
- React.createElement("span", { className: "cnvnav-group-head-wrap" },
687
+ React.createElement("span", { className: "cnvnav-chevron-wrap" },
551
688
  React.createElement("button", {
552
689
  type: "button",
553
- className: "cnvnav-group-head",
554
- onClick: () => jumpTo(g.headKey),
555
- },
556
- React.createElement("span", { className: "cnvnav-group-title" }, g.title),
557
- g.subtitle !== ""
558
- ? React.createElement("span", { className: "cnvnav-group-sub" }, g.subtitle)
559
- : null,
560
- ),
561
- ),
562
- ),
563
- hasSteps
564
- ? React.createElement(Tooltip, {
565
- label: expanded ? "折叠步次" : "展开步次",
566
- side: "top",
567
- delayMs: 500,
568
- },
569
- React.createElement("span", { className: "cnvnav-chevron-wrap" },
570
- React.createElement("button", {
571
- type: "button",
572
- className: "cnvnav-chevron",
573
- "aria-expanded": expanded,
574
- onClick: (e) => {
575
- e.stopPropagation();
576
- setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
577
- },
690
+ className: "cnvnav-chevron",
691
+ "aria-expanded": expanded,
692
+ onClick: (e) => {
693
+ e.stopPropagation();
694
+ setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
578
695
  },
579
- expanded ? "▾" : "▸ " + stepItems.length,
580
- ),
696
+ },
697
+ expanded ? "▾" : "▸ " + stepItems.length,
581
698
  ),
582
- )
583
- : null,
584
- ),
585
- expanded && hasSteps
586
- ? stepItems.map((item) =>
587
- React.createElement("button", {
588
- key: item.key,
589
- type: "button",
590
- ref: (el) => { itemEls[item.key] = el; },
591
- className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : ""),
592
- onClick: () => jumpTo(item.key),
593
- },
594
- React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
595
- item.preview !== null && item.preview !== ""
596
- ? React.createElement("span", { className: "cnvnav-item-preview" }, item.preview)
597
- : null,
598
- item.time !== ""
599
- ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
600
- : null,
601
699
  ),
602
700
  )
603
- : null,
604
- );
605
- });
701
+ : null;
702
+ return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
703
+ renderGroupHead(g, groupActive, chevron),
704
+ expanded && hasSteps
705
+ ? stepItems.map(renderItem)
706
+ : null,
707
+ );
708
+ });
709
+ }
606
710
 
607
711
  return React.createElement("div", {
608
712
  ref: (el) => { panelEl = el; },
@@ -612,7 +716,7 @@ window.__ModuleLoader__.load({
612
716
  },
613
717
  React.createElement("div", { className: "cnvnav-head" },
614
718
  React.createElement("span", { className: "cnvnav-title" }, "会话导航"),
615
- React.createElement("span", { className: "cnvnav-count" }, String(count) + " 条"),
719
+ React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
616
720
  React.createElement(Tooltip, {
617
721
  label: "收起面板",
618
722
  side: "bottom",
@@ -628,6 +732,15 @@ window.__ModuleLoader__.load({
628
732
  ),
629
733
  ),
630
734
  ),
735
+ React.createElement("div", { className: "cnvnav-filter-row" },
736
+ React.createElement("input", {
737
+ type: "text",
738
+ className: "cnvnav-filter",
739
+ placeholder: "过滤提问 / 回复…",
740
+ value: filter,
741
+ onChange: (e) => setFilter(e.target.value),
742
+ }),
743
+ ),
631
744
  React.createElement("div", { className: "cnvnav-topbar" },
632
745
  React.createElement(Tooltip, {
633
746
  label: "加载更早的一批轮次",
@@ -670,7 +783,9 @@ window.__ModuleLoader__.load({
670
783
  },
671
784
  groups.length === 0
672
785
  ? React.createElement("div", { className: "cnvnav-empty" }, "当前会话暂无对话节点")
673
- : groupNodes,
786
+ : (filtering && groupNodes.length === 0
787
+ ? React.createElement("div", { className: "cnvnav-empty" }, "无匹配结果")
788
+ : groupNodes),
674
789
  ),
675
790
  React.createElement("div", { className: "cnvnav-footer" },
676
791
  React.createElement(Tooltip, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-conversation-navigator",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Codex 风格的 DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
5
5
  "keywords": [
6
6
  "dsh",