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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/lib/client.js +97 -33
  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,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
- item.preview !== null && item.preview !== ""
563
- ? React.createElement("span", { className: "cnvnav-item-preview" }, 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
- g.subtitle !== ""
588
- ? React.createElement("span", { className: "cnvnav-group-sub" }, g.subtitle)
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 subtitleHit = hit(g.subtitle);
601
- const hitItems = g.items.filter((it) => hit(it.preview) || hit(it.label));
602
- if (!subtitleHit && hitItems.length === 0) continue;
603
- const rows = subtitleHit ? g.items : hitItems;
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
- rows.map(renderItem),
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
- return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
618
- renderGroupHead(g, groupActive),
619
- hasSteps
620
- ? React.createElement(Tooltip, {
621
- label: expanded ? "折叠步次" : "展开步次",
622
- side: "top",
623
- delayMs: 500,
624
- },
625
- React.createElement("span", { className: "cnvnav-chevron-wrap" },
626
- React.createElement("button", {
627
- type: "button",
628
- className: "cnvnav-chevron",
629
- "aria-expanded": expanded,
630
- onClick: (e) => {
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
- expanded ? "▾" : "▸ " + stepItems.length,
636
- ),
696
+ },
697
+ expanded ? "▾" : "▸ " + stepItems.length,
637
698
  ),
638
- )
639
- : null,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-conversation-navigator",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Codex 风格的 DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
5
5
  "keywords": [
6
6
  "dsh",