dsh-conversation-navigator 0.1.17 → 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 +3 -2
  2. package/lib/client.js +90 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,8 +7,9 @@ Codex 风格的 **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页*
7
7
  ## 功能
8
8
 
9
9
  - **按轮折叠大纲**:默认只显示"第 N 轮 + 你的问题"列表,长对话一目了然
10
- - **关键词过滤**:顶部搜索框只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
11
- - **展开/折叠步次**:点击轮次行右侧的 `▸ N` / `▾` 展开或折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
10
+ - **关键词过滤**:点击左上角搜索图标呼出输入框,只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
11
+ - **展开/折叠步次**:点击轮次行右侧的箭头按钮(`▸ N`,展开时旋转为 `▾`)平滑展开/折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
12
+ - **丝滑动效**:面板开合淡入淡出、步次展开/折叠高度过渡、过滤结果逐条级联淡入、折叠箭头旋转——全部纯 CSS 实现,零依赖
12
13
  - **点击定位**:点击轮次主体或步次条目,平滑滚动跳转到对话中对应位置(不改变折叠状态)
13
14
  - **加载更早 / 加载全部**:面板顶部两个按钮——「加载更早」向后翻一页、「加载全部」一键把所有历史轮次载入导航,之后可任意跳转(页面默认仍懒加载,只有点按钮才补载)
14
15
  - **位置跟踪**:手动滚动对话时,面板自动高亮并跟随当前正在阅读的轮次
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;
@@ -118,18 +121,28 @@ window.__ModuleLoader__.load({
118
121
  .cnvnav-chevron-wrap { flex: none; display: flex; }
119
122
  .cnvnav-chevron {
120
123
  min-width: 30px; align-self: stretch;
121
- display: flex; align-items: center; justify-content: center;
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)); }
@@ -167,9 +180,8 @@ window.__ModuleLoader__.load({
167
180
  color: inherit; border-radius: 2px; padding: 0 1px; font-weight: 600;
168
181
  }
169
182
  .cnvnav-empty { padding: 18px 10px; text-align: center; color: var(--dsw-alias-label-secondary, #888); }
170
- .cnvnav-filter-row { padding: 6px 6px 0; }
171
183
  .cnvnav-filter {
172
- width: 100%; box-sizing: border-box;
184
+ flex: 1; min-width: 0; box-sizing: border-box;
173
185
  height: 28px; padding: 0 8px;
174
186
  border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.35));
175
187
  border-radius: 8px;
@@ -177,9 +189,14 @@ window.__ModuleLoader__.load({
177
189
  color: var(--dsw-alias-label-primary, #222);
178
190
  font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif);
179
191
  font-size: 12px; outline: none;
192
+ animation: cnvnav-search-in .16s ease-out;
180
193
  }
181
194
  .cnvnav-filter::placeholder { color: var(--dsw-alias-label-secondary, #888); }
182
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
+ }
183
200
  .cnvnav-topbar { display: flex; gap: 6px; margin: 6px 6px 0; }
184
201
  .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
185
202
  .cnvnav-bar-btn { flex: 1; display: block; }
@@ -503,6 +520,11 @@ window.__ModuleLoader__.load({
503
520
  function Panel() {
504
521
  const snap = useStore();
505
522
  const [filter, setFilter] = React.useState("");
523
+ const [searchOpen, setSearchOpen] = React.useState(false);
524
+ function toggleSearch() {
525
+ if (searchOpen) setFilter("");
526
+ setSearchOpen(!searchOpen);
527
+ }
506
528
 
507
529
  React.useEffect(() => {
508
530
  if (!snap.open) return;
@@ -547,7 +569,7 @@ window.__ModuleLoader__.load({
547
569
  React.useEffect(() => {
548
570
  if (!snap.open || snap.activeKey === null) return;
549
571
  let target = itemEls[snap.activeKey];
550
- if (target === undefined || target === null) {
572
+ if (target === undefined || target === null || (typeof target.closest === "function" && target.closest(".cnvnav-steps-collapsed") !== null)) {
551
573
  const gid = snap.outline !== null ? snap.outline.byKey[snap.activeKey] : undefined;
552
574
  if (gid !== undefined) target = groupEls[gid];
553
575
  }
@@ -557,13 +579,11 @@ window.__ModuleLoader__.load({
557
579
  const er = target.getBoundingClientRect();
558
580
  const rel = er.top - lr.top + list.scrollTop;
559
581
  if (rel < list.scrollTop || rel + er.height > list.scrollTop + list.clientHeight) {
560
- list.scrollTop = Math.max(0, rel - list.clientHeight / 2);
582
+ list.scrollTo({ top: Math.max(0, rel - list.clientHeight / 2), behavior: "smooth" });
561
583
  }
562
584
  }
563
585
  }, [snap.open, snap.activeKey]);
564
586
 
565
- if (!snap.open) return null;
566
-
567
587
  const groups = snap.outline !== null ? snap.outline.groups : [];
568
588
  const count = snap.outline !== null ? snap.outline.count : 0;
569
589
 
@@ -603,7 +623,7 @@ window.__ModuleLoader__.load({
603
623
  if (end < text.length) parts.push("…");
604
624
  return parts;
605
625
  }
606
- function renderItem(item) {
626
+ function renderItem(item, staggerMs) {
607
627
  const useSnippet = filtering && hit(item.search);
608
628
  const preview = useSnippet ? snippetNodes(item.search, kw, 20, 50) : item.preview;
609
629
  const hasPreview = preview !== null && preview !== "" && (!Array.isArray(preview) || preview.length > 0);
@@ -611,7 +631,8 @@ window.__ModuleLoader__.load({
611
631
  key: item.key,
612
632
  type: "button",
613
633
  ref: (el) => { itemEls[item.key] = el; },
614
- 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,
615
636
  onClick: () => jumpTo(item.key),
616
637
  },
617
638
  React.createElement("span", { className: "cnvnav-badge cnvnav-badge-" + item.cls }, item.label),
@@ -666,9 +687,19 @@ window.__ModuleLoader__.load({
666
687
  );
667
688
  if (!headHit && hits.length === 0) continue;
668
689
  const groupActive = g.items.some((it) => it.key === snap.activeKey);
669
- groupNodes.push(React.createElement("div", { key: g.gid, className: "cnvnav-group" },
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
+ },
670
695
  renderGroupHead(g, groupActive, null),
671
- hits.map(renderItem),
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,
672
703
  ));
673
704
  }
674
705
  } else {
@@ -694,15 +725,30 @@ window.__ModuleLoader__.load({
694
725
  setState({ expanded: Object.assign({}, state.expanded, { [g.gid]: !expanded }) });
695
726
  },
696
727
  },
697
- expanded ? "▾" : "▸ " + stepItems.length,
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)),
698
737
  ),
699
738
  ),
700
739
  )
701
740
  : null;
702
741
  return React.createElement("div", { key: g.gid, className: "cnvnav-group" },
703
742
  renderGroupHead(g, groupActive, chevron),
704
- expanded && hasSteps
705
- ? stepItems.map(renderItem)
743
+ hasSteps
744
+ ? React.createElement("div", {
745
+ className: "cnvnav-steps" + (expanded ? "" : " cnvnav-steps-collapsed"),
746
+ "aria-hidden": !expanded,
747
+ },
748
+ React.createElement("div", { className: "cnvnav-steps-inner" },
749
+ stepItems.map(renderItem),
750
+ ),
751
+ )
706
752
  : null,
707
753
  );
708
754
  });
@@ -710,12 +756,36 @@ window.__ModuleLoader__.load({
710
756
 
711
757
  return React.createElement("div", {
712
758
  ref: (el) => { panelEl = el; },
713
- className: "cnvnav-panel",
759
+ className: "cnvnav-panel" + (snap.open ? "" : " cnvnav-panel-hidden"),
714
760
  role: "complementary",
715
761
  "aria-label": "会话导航",
762
+ "aria-hidden": !snap.open,
716
763
  },
717
764
  React.createElement("div", { className: "cnvnav-head" },
718
- 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,
719
789
  React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
720
790
  React.createElement(Tooltip, {
721
791
  label: "收起面板",
@@ -732,15 +802,6 @@ window.__ModuleLoader__.load({
732
802
  ),
733
803
  ),
734
804
  ),
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
- ),
744
805
  React.createElement("div", { className: "cnvnav-topbar" },
745
806
  React.createElement(Tooltip, {
746
807
  label: "加载更早的一批轮次",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-conversation-navigator",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Codex 风格的 DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
5
5
  "keywords": [
6
6
  "dsh",