dsh-conversation-navigator 0.1.16 → 0.1.18

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 +5 -2
  2. package/lib/client.js +182 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,9 @@ Codex 风格的 **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页*
7
7
  ## 功能
8
8
 
9
9
  - **按轮折叠大纲**:默认只显示"第 N 轮 + 你的问题"列表,长对话一目了然
10
- - **展开/折叠步次**:点击轮次行右侧的 `▸ N` / `▾` 展开或折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
10
+ - **关键词过滤**:点击左上角搜索图标呼出输入框,只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
11
+ - **展开/折叠步次**:点击轮次行右侧的箭头按钮(`▸ N`,展开时旋转为 `▾`)平滑展开/折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
12
+ - **丝滑动效**:面板开合淡入淡出、步次展开/折叠高度过渡、过滤结果逐条级联淡入、折叠箭头旋转——全部纯 CSS 实现,零依赖
11
13
  - **点击定位**:点击轮次主体或步次条目,平滑滚动跳转到对话中对应位置(不改变折叠状态)
12
14
  - **加载更早 / 加载全部**:面板顶部两个按钮——「加载更早」向后翻一页、「加载全部」一键把所有历史轮次载入导航,之后可任意跳转(页面默认仍懒加载,只有点按钮才补载)
13
15
  - **位置跟踪**:手动滚动对话时,面板自动高亮并跟随当前正在阅读的轮次
@@ -26,7 +28,7 @@ Codex 风格的 **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页*
26
28
  dsh plugin --profile web add dsh-conversation-navigator
27
29
 
28
30
  # 方式二:GitHub(纯 JS 包无需 prepare/allowBuilds,直接可用)
29
- dsh plugin --profile web add github:gjj-star/dsh-conversation-navigato
31
+ dsh plugin --profile web add github:gjj-star/dsh-conversation-navigator
30
32
 
31
33
  # 方式三:本地 tarball
32
34
  pnpm pack
@@ -48,6 +50,7 @@ dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
48
50
  - 历史补载:通过 `sessions` 服务的 `binding(sessionId).session.loadOlder()` 分页向后加载,「加载全部」循环至 `hasMore=false`
49
51
  - 跳转定位:复用 DSH 聊天视图自身的稳定 DOM 锚点 `[data-chat-anchor-key]`(与产品内部 paging/scroll 定位同源),`scrollIntoView` 平滑滚动
50
52
  - 位置跟踪:捕获 `[data-conversation-scroll]` 滚动容器的 scroll 事件(节流 120ms),计算视口顶部首个可见节点
53
+ - 关键词过滤:仅为 `user` 与 `assistant-step` 节点提取检索文本(`dialogueText`),大小写不敏感匹配,命中片段以 `<mark>` 高亮并按首个命中位置截取显示窗口
51
54
  - 样式:按钮/图标复用 `@deepseek-ai/dsh-client-ui-primitives`;面板容器自建 `<style>` 注入,颜色使用 `--dsw-*` 主题 token;插件卸载时随 fiber 清理
52
55
 
53
56
  ## 兼容性说明
package/lib/client.js CHANGED
@@ -5,7 +5,7 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  const React = require("react");
8
- const { Button, Tooltip, IconCloseOutline16 } = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ const { Button, Tooltip, IconCloseOutline16, IconSearchOutline16 } = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
 
10
10
  /* ---------- custom icons (DSH outline style, unique to this panel) ---------- */
11
11
  function ToggleIcon() {
@@ -79,24 +79,27 @@ window.__ModuleLoader__.load({
79
79
  font-size: 12px;
80
80
  color: var(--dsw-alias-label-primary, #222);
81
81
  overflow: hidden;
82
+ transition: opacity .18s ease, transform .18s ease;
82
83
  }
84
+ .cnvnav-panel-hidden { opacity: 0; transform: translateY(-6px) scale(.985); pointer-events: none; }
83
85
  .cnvnav-head {
84
86
  display: flex; align-items: center; gap: 8px;
85
87
  padding: 10px 12px;
86
88
  border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
87
89
  }
88
- .cnvnav-title { font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif); font-size: 16px; font-weight: 700; line-height: 22px; }
89
90
  .cnvnav-count { color: var(--dsw-alias-label-secondary, #888); font-size: 11px; }
91
+ .cnvnav-search { flex: none; display: flex; }
90
92
  .cnvnav-close { margin-left: auto; }
91
93
  .cnvnav-list {
92
94
  overflow-y: auto; padding: 6px; min-height: 0;
93
95
  display: flex; flex-direction: column; gap: 2px;
94
96
  scrollbar-width: thin;
95
97
  }
96
- .cnvnav-group { display: flex; flex-direction: column; gap: 2px; margin-bottom: 2px; }
98
+ .cnvnav-group { display: flex; flex-direction: column; margin-bottom: 2px; }
97
99
  .cnvnav-group-row {
98
100
  display: flex; align-items: stretch;
99
101
  border-radius: 8px; overflow: hidden;
102
+ transition: background .16s ease;
100
103
  }
101
104
  .cnvnav-group-row:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
102
105
  .cnvnav-group-row-active { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
@@ -109,7 +112,7 @@ window.__ModuleLoader__.load({
109
112
  padding: 5px 0 5px 8px;
110
113
  color: var(--dsw-alias-label-primary, #222);
111
114
  }
112
- .cnvnav-group-title { font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif); font-size: 14px; font-weight: 700; line-height: 20px; white-space: nowrap; }
115
+ .cnvnav-group-title { font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif); font-size: 14px; font-weight: 700; line-height: 20px; white-space: nowrap; transition: color .16s ease; }
113
116
  .cnvnav-group-sub {
114
117
  color: var(--dsw-alias-label-secondary, #888);
115
118
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -117,19 +120,29 @@ window.__ModuleLoader__.load({
117
120
  }
118
121
  .cnvnav-chevron-wrap { flex: none; display: flex; }
119
122
  .cnvnav-chevron {
120
- min-width: 30px; width: 100%;
121
- display: flex; align-items: center; justify-content: center;
123
+ min-width: 30px; align-self: stretch;
124
+ display: flex; align-items: center; justify-content: center; gap: 3px;
122
125
  border: none; background: transparent; cursor: pointer;
123
126
  color: var(--dsw-alias-label-secondary, #888);
124
127
  font-size: 11px; line-height: 1; padding: 0 8px;
125
128
  white-space: nowrap;
129
+ transition: color .16s ease, background .16s ease;
126
130
  }
127
131
  .cnvnav-chevron:hover { color: var(--dsw-alias-brand-primary, #4f46e5); background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
132
+ .cnvnav-chevron-icon { flex: none; display: block; transition: transform .2s cubic-bezier(.4, 0, .2, 1); }
133
+ .cnvnav-chevron[aria-expanded="true"] .cnvnav-chevron-icon { transform: rotate(90deg); }
134
+ .cnvnav-chevron-count { font-size: 11px; line-height: 1; }
135
+ .cnvnav-steps { display: grid; grid-template-rows: 1fr; margin-top: 2px; transition: grid-template-rows .24s cubic-bezier(.4, 0, .2, 1), margin-top .24s cubic-bezier(.4, 0, .2, 1); }
136
+ .cnvnav-steps-collapsed { grid-template-rows: 0fr; margin-top: 0; }
137
+ .cnvnav-steps-inner { overflow: hidden; min-height: 0; display: flex; flex-direction: column; gap: 2px; }
138
+ .cnvnav-fade-item { animation: cnvnav-fade-up .2s ease-out backwards; }
139
+ @keyframes cnvnav-fade-up { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
128
140
  .cnvnav-item {
129
141
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
130
142
  border: none; background: transparent; cursor: pointer;
131
143
  padding: 5px 8px 5px 22px; border-radius: 8px; position: relative;
132
144
  color: var(--dsw-alias-label-primary, #222);
145
+ transition: background .16s ease;
133
146
  }
134
147
  .cnvnav-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
135
148
  .cnvnav-item-active { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.12)); }
@@ -162,10 +175,13 @@ window.__ModuleLoader__.load({
162
175
  .cnvnav-item-time {
163
176
  flex: none; color: var(--dsw-alias-label-secondary, #888); font-size: 10px; opacity: .8;
164
177
  }
178
+ .cnvnav-hl {
179
+ background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 30%, transparent);
180
+ color: inherit; border-radius: 2px; padding: 0 1px; font-weight: 600;
181
+ }
165
182
  .cnvnav-empty { padding: 18px 10px; text-align: center; color: var(--dsw-alias-label-secondary, #888); }
166
- .cnvnav-filter-row { padding: 6px 6px 0; }
167
183
  .cnvnav-filter {
168
- width: 100%; box-sizing: border-box;
184
+ flex: 1; min-width: 0; box-sizing: border-box;
169
185
  height: 28px; padding: 0 8px;
170
186
  border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
171
187
  border-radius: 8px;
@@ -173,9 +189,14 @@ window.__ModuleLoader__.load({
173
189
  color: var(--dsw-alias-label-primary, #222);
174
190
  font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif);
175
191
  font-size: 12px; outline: none;
192
+ animation: cnvnav-search-in .16s ease-out;
176
193
  }
177
194
  .cnvnav-filter::placeholder { color: var(--dsw-alias-label-secondary, #888); }
178
195
  .cnvnav-filter:focus { border-color: var(--dsw-alias-brand-primary, #4f46e5); }
196
+ @keyframes cnvnav-search-in {
197
+ from { opacity: 0; transform: translateX(4px); }
198
+ to { opacity: 1; transform: translateX(0); }
199
+ }
179
200
  .cnvnav-topbar { display: flex; gap: 6px; margin: 6px 6px 0; }
180
201
  .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
181
202
  .cnvnav-bar-btn { flex: 1; display: block; }
@@ -265,6 +286,23 @@ window.__ModuleLoader__.load({
265
286
  }
266
287
  return null;
267
288
  }
289
+ /* Searchable dialogue text: user questions + agent actual dialogue only
290
+ (excludes context, tool calls, commands, compaction, reasoning, etc.) */
291
+ function dialogueText(node) {
292
+ const d = node && node.data;
293
+ if (!d || typeof d !== "object") return null;
294
+ if (node.kind === "user") return firstText(d.content);
295
+ if (node.kind === "assistant-step") {
296
+ if (!Array.isArray(d.blocks)) return null;
297
+ const parts = [];
298
+ for (const b of d.blocks) {
299
+ if (!b || typeof b !== "object") continue;
300
+ if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
301
+ }
302
+ return parts.length > 0 ? parts.join(" ") : null;
303
+ }
304
+ return null;
305
+ }
268
306
  function previewFor(node) {
269
307
  const d = node && node.data;
270
308
  if (!d || typeof d !== "object") return null;
@@ -355,6 +393,7 @@ window.__ModuleLoader__.load({
355
393
  label: meta.label,
356
394
  cls: meta.cls,
357
395
  preview: previewFor(node),
396
+ search: dialogueText(node),
358
397
  time: fmtTime(timeOf(node)),
359
398
  };
360
399
  const turn = turnOf(node);
@@ -481,6 +520,11 @@ window.__ModuleLoader__.load({
481
520
  function Panel() {
482
521
  const snap = useStore();
483
522
  const [filter, setFilter] = React.useState("");
523
+ const [searchOpen, setSearchOpen] = React.useState(false);
524
+ function toggleSearch() {
525
+ if (searchOpen) setFilter("");
526
+ setSearchOpen(!searchOpen);
527
+ }
484
528
 
485
529
  React.useEffect(() => {
486
530
  if (!snap.open) return;
@@ -525,7 +569,7 @@ window.__ModuleLoader__.load({
525
569
  React.useEffect(() => {
526
570
  if (!snap.open || snap.activeKey === null) return;
527
571
  let target = itemEls[snap.activeKey];
528
- if (target === undefined || target === null) {
572
+ if (target === undefined || target === null || (typeof target.closest === "function" && target.closest(".cnvnav-steps-collapsed") !== null)) {
529
573
  const gid = snap.outline !== null ? snap.outline.byKey[snap.activeKey] : undefined;
530
574
  if (gid !== undefined) target = groupEls[gid];
531
575
  }
@@ -535,13 +579,11 @@ window.__ModuleLoader__.load({
535
579
  const er = target.getBoundingClientRect();
536
580
  const rel = er.top - lr.top + list.scrollTop;
537
581
  if (rel < list.scrollTop || rel + er.height > list.scrollTop + list.clientHeight) {
538
- list.scrollTop = Math.max(0, rel - list.clientHeight / 2);
582
+ list.scrollTo({ top: Math.max(0, rel - list.clientHeight / 2), behavior: "smooth" });
539
583
  }
540
584
  }
541
585
  }, [snap.open, snap.activeKey]);
542
586
 
543
- if (!snap.open) return null;
544
-
545
587
  const groups = snap.outline !== null ? snap.outline.groups : [];
546
588
  const count = snap.outline !== null ? snap.outline.count : 0;
547
589
 
@@ -550,24 +592,63 @@ window.__ModuleLoader__.load({
550
592
  function hit(text) {
551
593
  return typeof text === "string" && text !== "" && text.toLowerCase().indexOf(kw) !== -1;
552
594
  }
553
- function renderItem(item) {
595
+ /* Split text into nodes, wrapping every case-insensitive keyword match in a <mark>. */
596
+ function highlightedNodes(text, kw) {
597
+ if (typeof text !== "string" || kw === "") return [text];
598
+ const lower = text.toLowerCase();
599
+ const out = [];
600
+ let i = 0;
601
+ let m = 0;
602
+ for (;;) {
603
+ const idx = lower.indexOf(kw, i);
604
+ if (idx === -1) { if (i < text.length) out.push(text.slice(i)); break; }
605
+ if (idx > i) out.push(text.slice(i, idx));
606
+ out.push(React.createElement("mark", { key: "m" + (m++), className: "cnvnav-hl" }, text.slice(idx, idx + kw.length)));
607
+ i = idx + kw.length;
608
+ }
609
+ return out;
610
+ }
611
+ /* Window the text around the first keyword match (so the keyword is visible,
612
+ not hidden behind head-truncation), then highlight every occurrence. */
613
+ function snippetNodes(text, kw, before, after) {
614
+ if (typeof text !== "string") return [text];
615
+ const idx = text.toLowerCase().indexOf(kw);
616
+ if (idx === -1) return highlightedNodes(text, kw);
617
+ const start = Math.max(0, idx - before);
618
+ const end = Math.min(text.length, idx + kw.length + after);
619
+ const s = text.slice(start, end);
620
+ const parts = [];
621
+ if (start > 0) parts.push("…");
622
+ parts.push(...highlightedNodes(s, kw));
623
+ if (end < text.length) parts.push("…");
624
+ return parts;
625
+ }
626
+ function renderItem(item, staggerMs) {
627
+ const useSnippet = filtering && hit(item.search);
628
+ const preview = useSnippet ? snippetNodes(item.search, kw, 20, 50) : item.preview;
629
+ const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
554
630
  return React.createElement("button", {
555
631
  key: item.key,
556
632
  type: "button",
557
633
  ref: (el) => { itemEls[item.key] = el; },
558
- className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : ""),
634
+ className: "cnvnav-item" + (item.key === snap.activeKey ? " cnvnav-item-active" : "") + (staggerMs !== undefined ? " cnvnav-fade-item" : ""),
635
+ style: staggerMs !== undefined ? { animationDelay: staggerMs + "ms" } : undefined,
559
636
  onClick: () => jumpTo(item.key),
560
637
  },
561
638
  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)
639
+ hasPreview
640
+ ? React.createElement("span", { className: "cnvnav-item-preview" }, preview)
564
641
  : null,
565
642
  item.time !== ""
566
643
  ? React.createElement("span", { className: "cnvnav-item-time" }, item.time)
567
644
  : null,
568
645
  );
569
646
  }
570
- function renderGroupHead(g, groupActive) {
647
+ function renderGroupHead(g, groupActive, chevron) {
648
+ const headSearch = g.items.length > 0 ? g.items[0].search : null;
649
+ const useSnippet = filtering && hit(headSearch);
650
+ const sub = useSnippet ? snippetNodes(headSearch, kw, 15, 40) : g.subtitle;
651
+ const hasSub = sub !== "" && (!Array.isArray(sub) || sub.length > 0);
571
652
  return React.createElement("div", {
572
653
  ref: (el) => { groupEls[g.gid] = el; },
573
654
  className: "cnvnav-group-row" + (groupActive ? " cnvnav-group-row-active" : ""),
@@ -584,12 +665,13 @@ window.__ModuleLoader__.load({
584
665
  onClick: () => jumpTo(g.headKey),
585
666
  },
586
667
  React.createElement("span", { className: "cnvnav-group-title" }, g.title),
587
- g.subtitle !== ""
588
- ? React.createElement("span", { className: "cnvnav-group-sub" }, g.subtitle)
668
+ hasSub
669
+ ? React.createElement("span", { className: "cnvnav-group-sub" }, sub)
589
670
  : null,
590
671
  ),
591
672
  ),
592
673
  ),
674
+ chevron !== undefined ? chevron : null,
593
675
  );
594
676
  }
595
677
 
@@ -597,14 +679,27 @@ window.__ModuleLoader__.load({
597
679
  if (filtering) {
598
680
  groupNodes = [];
599
681
  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;
682
+ const headHit = g.headIsUser && g.items.length > 0 && hit(g.items[0].search);
683
+ const hits = g.items.filter((it, i) =>
684
+ (it.kind === "user" || it.kind === "assistant-step") &&
685
+ hit(it.search) &&
686
+ !(i === 0 && g.headIsUser)
687
+ );
688
+ if (!headHit && hits.length === 0) continue;
604
689
  const groupActive = g.items.some((it) => it.key === snap.activeKey);
605
- groupNodes.push(React.createElement("div", { key: g.gid, className: "cnvnav-group" },
606
- renderGroupHead(g, groupActive),
607
- rows.map(renderItem),
690
+ groupNodes.push(React.createElement("div", {
691
+ key: g.gid,
692
+ className: "cnvnav-group cnvnav-fade-item",
693
+ style: { animationDelay: (groupNodes.length * 18) + "ms" },
694
+ },
695
+ renderGroupHead(g, groupActive, null),
696
+ hits.length > 0
697
+ ? React.createElement("div", { className: "cnvnav-steps" },
698
+ React.createElement("div", { className: "cnvnav-steps-inner" },
699
+ hits.map((it, i) => renderItem(it, i * 20)),
700
+ ),
701
+ )
702
+ : null,
608
703
  ));
609
704
  }
610
705
  } else {
@@ -614,44 +709,83 @@ window.__ModuleLoader__.load({
614
709
  const stepItems = isTurnGroup ? (g.headIsUser ? g.items.slice(1) : g.items) : [];
615
710
  const hasSteps = stepItems.length > 0;
616
711
  const groupActive = g.items.some((it) => it.key === snap.activeKey);
712
+ const chevron = hasSteps
713
+ ? React.createElement(Tooltip, {
714
+ label: expanded ? "折叠步次" : "展开步次",
715
+ side: "top",
716
+ delayMs: 500,
717
+ },
718
+ React.createElement("span", { className: "cnvnav-chevron-wrap" },
719
+ React.createElement("button", {
720
+ type: "button",
721
+ className: "cnvnav-chevron",
722
+ "aria-expanded": expanded,
723
+ onClick: (e) => {
724
+ e.stopPropagation();
725
+ setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
726
+ },
727
+ },
728
+ React.createElement("svg", {
729
+ className: "cnvnav-chevron-icon",
730
+ viewBox: "0 0 12 12", width: 12, height: 12, fill: "none",
731
+ stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round",
732
+ "aria-hidden": true,
733
+ },
734
+ React.createElement("path", { d: "M4.5 2.5 8 6l-3.5 3.5" }),
735
+ ),
736
+ expanded ? null : React.createElement("span", { className: "cnvnav-chevron-count" }, String(stepItems.length)),
737
+ ),
738
+ ),
739
+ )
740
+ : null;
617
741
  return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
618
- renderGroupHead(g, groupActive),
742
+ renderGroupHead(g, groupActive, chevron),
619
743
  hasSteps
620
- ? React.createElement(Tooltip, {
621
- label: expanded ? "折叠步次" : "展开步次",
622
- side: "top",
623
- delayMs: 500,
744
+ ? React.createElement("div", {
745
+ className: "cnvnav-steps" + (expanded ? "" : " cnvnav-steps-collapsed"),
746
+ "aria-hidden": !expanded,
624
747
  },
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
- },
634
- },
635
- expanded ? "▾" : "▸ " + stepItems.length,
636
- ),
748
+ React.createElement("div", { className: "cnvnav-steps-inner" },
749
+ stepItems.map(renderItem),
637
750
  ),
638
751
  )
639
752
  : null,
640
- expanded && hasSteps
641
- ? stepItems.map(renderItem)
642
- : null,
643
753
  );
644
754
  });
645
755
  }
646
756
 
647
757
  return React.createElement("div", {
648
758
  ref: (el) => { panelEl = el; },
649
- className: "cnvnav-panel",
759
+ className: "cnvnav-panel" + (snap.open ? "" : " cnvnav-panel-hidden"),
650
760
  role: "complementary",
651
761
  "aria-label": "会话导航",
762
+ "aria-hidden": !snap.open,
652
763
  },
653
764
  React.createElement("div", { className: "cnvnav-head" },
654
- React.createElement("span", { className: "cnvnav-title" }, "会话导航"),
765
+ React.createElement(Tooltip, {
766
+ label: searchOpen ? "收起搜索" : "搜索关键词",
767
+ side: "bottom",
768
+ delayMs: 500,
769
+ },
770
+ React.createElement("span", { className: "cnvnav-search" },
771
+ React.createElement(Button, {
772
+ variant: "ghost",
773
+ size: "sm",
774
+ icon: React.createElement(IconSearchOutline16, { size: 14 }),
775
+ onClick: () => toggleSearch(),
776
+ }),
777
+ ),
778
+ ),
779
+ searchOpen
780
+ ? React.createElement("input", {
781
+ type: "text",
782
+ className: "cnvnav-filter",
783
+ placeholder: "请输入关键词",
784
+ value: filter,
785
+ autoFocus: true,
786
+ onChange: (e) => setFilter(e.target.value),
787
+ })
788
+ : null,
655
789
  React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
656
790
  React.createElement(Tooltip, {
657
791
  label: "收起面板",
@@ -668,15 +802,6 @@ window.__ModuleLoader__.load({
668
802
  ),
669
803
  ),
670
804
  ),
671
- React.createElement("div", { className: "cnvnav-filter-row" },
672
- React.createElement("input", {
673
- type: "text",
674
- className: "cnvnav-filter",
675
- placeholder: "过滤提问 / 回复…",
676
- value: filter,
677
- onChange: (e) => setFilter(e.target.value),
678
- }),
679
- ),
680
805
  React.createElement("div", { className: "cnvnav-topbar" },
681
806
  React.createElement(Tooltip, {
682
807
  label: "加载更早的一批轮次",
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.18",
4
4
  "description": "Codex 风格的 DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
5
5
  "keywords": [
6
6
  "dsh",