dsh-conversation-navigator 0.1.25 → 0.2.1

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 (4) hide show
  1. package/README.en.md +106 -0
  2. package/README.md +102 -98
  3. package/lib/client.js +271 -90
  4. package/package.json +47 -47
package/README.en.md ADDED
@@ -0,0 +1,106 @@
1
+ # DSH Conversation Navigator (dsh-conversation-navigator)
2
+
3
+ [English](README.en.md) | [中文](README.md)
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-conversation-navigator?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-conversation-navigator)
6
+ [![downloads](https://img.shields.io/npm/dm/dsh-conversation-navigator?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-conversation-navigator)
7
+ [![stars](https://img.shields.io/github/stars/gjj-star/dsh-conversation-navigator?style=flat-square&color=green)](https://github.com/gjj-star/dsh-conversation-navigator)
8
+ [![license](https://img.shields.io/github/license/gjj-star/dsh-conversation-navigator?style=flat-square&color=teal)](https://github.com/gjj-star/dsh-conversation-navigator)
9
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](https://www.npmjs.com/package/dsh-conversation-navigator)
10
+ [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
11
+ [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek_Harness-Plugin-blue?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
12
+ [![dsh.so risk](https://www.dsh.so/badge/dsh-conversation-navigator-2.svg)](https://www.dsh.so/artifact/dsh-conversation-navigator-2/)
13
+ [![dsh.so install](https://www.dsh.so/badge/install/dsh-conversation-navigator-2.svg)](https://www.dsh.so/artifact/dsh-conversation-navigator-2/)
14
+
15
+ A **DeepSeek Harness (DSH) Web conversation navigator panel**: a turn-folded outline floating on the **right** side of the conversation page. Click any node to smooth-jump, watch the current reading position highlight as you scroll, with step badges colored to match the built-in "Trajectory" view.
16
+
17
+ Browser-only (no host behavior), plain JavaScript, zero build step, zero npm dependencies (buttons/tooltips reuse the official primitives from the DSH kernel seed).
18
+
19
+ ![Full mode](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-main.png)
20
+ ![Hidden mode](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-no-round.png)
21
+ ![Minimal mode · collapsed](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-minimal-hide.png)
22
+ ![Minimal mode · expanded](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-minimal-expand.png)
23
+
24
+ > Four forms: full, hidden, minimal-right, and minimal-left (the minimal strips collapse to the in-row indicator bars; hover expands the positioning panel). More screenshots in [assets/screenshots](./assets/screenshots). The two "community skins" shots are taken under third-party skins (whale-girl maid theme and the "Your Name" theme), not bundled with the plugin.
25
+
26
+ ## Features
27
+
28
+ - **Turn-folded outline**: shows "Turn N + your question" rows by default, long conversations at a glance
29
+ - **Keyword filter**: click the top-left search icon to reveal the inline input; matches only your questions + the assistant's actual reply text (context, tool calls, commands, compaction and reasoning never match); hits are highlighted and the list text is windowed to the keyword
30
+ - **Expand / collapse steps**: the arrow button on the right of each turn row (`▸ N`, rotates to `▾` when expanded) smoothly expands or collapses the turn's step details (assistant replies, tool calls, commands, compaction points, etc.)
31
+ - **Hover full text**: rest the mouse on a turn row and a bubble shows the user's complete question, no longer truncated to one line
32
+ - **Full / Hidden / Minimal modes**: the header toggle cycles four states —
33
+ - **Full**: the classic grouped view (system events like compaction are shown bold, at the same level as turns)
34
+ - **Hidden**: every row is a trajectory badge + text (user = business blue, assistant = violet, compaction = neutral gray)
35
+ - **Minimal-right**: the indicator-bar strip docked to the right edge of the viewport
36
+ - **Minimal-left**: the same strip pinned to the left edge of the conversation area (at the right border of the DSH sidebar), leaving the right side for dsh-better-sidebar
37
+ - The minimal strip collapses to the in-row indicator bars (current = solid brand color, others = 40% foreground-color mix); hovering expands a fixed 7-row positioning panel (click to jump, hover bubble for the full question, thumb-only scrollbar beyond 7 rows; right-aligned expands from right to left, left-aligned from left to right); a floating button above the bars keeps cycling forward (minimal-right → minimal-left → Full); the toggle yields while the search input is open
38
+ - **Draggable panel**: in Full / Hidden mode, grab the header to drag the panel anywhere in the viewport (auto-clamped to the edges; the expanded height is capped at half the viewport with the top edge fixed and the panel growing downward — expanding a huge turn scrolls the list internally while the "Back to latest / Collapse all" footer stays reachable); the pin button in the header toggles between the default right-dock and the last dragged position; the dragged position and the minimal left/right alignment survive page reloads via localStorage
39
+ - **Silky motion**: panel fade, step expand/collapse height transitions, staggered fade-in of filter results, rotating collapse arrow — all pure CSS, zero dependencies
40
+ - **Click to locate**: click a turn or a step to smooth-scroll the conversation to that exact position (fold state is preserved)
41
+ - **Load earlier / Load all**: two buttons on top — "Load earlier" pages one batch back, "Load all" loads every historical turn into the navigator for arbitrary jumps (the page itself stays lazily loaded until you click)
42
+ - **Position tracking**: scrolling the conversation highlights and follows the turn you are currently reading
43
+ - **Right-side docking (default)**: the panel is anchored to the right edge of the viewport and stays put when the left sidebar collapses or expands; unpin (drag it or hit the pin) to place it anywhere
44
+ - **Back to latest / Collapse all**: two shortcut buttons at the bottom; "Collapse all" is disabled when no turn is expanded, so it never looks broken for doing nothing
45
+ - **Trajectory colors**: user/steering = business blue, context = success green, assistant = violet, tool = amber, compaction = neutral gray (`--dsw` theme tokens matching the built-in trajectory view, light/dark adaptive)
46
+ - **Native DSH look**: action buttons reuse the official `Button`/`Tooltip` components and official icons (search, close); the remaining icons (navigate, load earlier, load all, back to latest, collapse all, switch modes, …) are outline icons from **Lucide** (ISC License, [lucide.dev](https://lucide.dev)), `currentColor` adaptive to light/dark themes
47
+ - Follows workspace/session switches automatically and rebuilds the outline
48
+
49
+ ## Install
50
+
51
+ This plugin is a spec-compliant **bundle** (`dsh.bundle` manifest + `dsh.client` declaration), plain JavaScript with no build step. Install with the official CLI:
52
+
53
+ ```sh
54
+ # Option 1: npm (no build authorization needed once published)
55
+ dsh plugin --profile web add dsh-conversation-navigator
56
+
57
+ # Option 2: GitHub (pure JS package, no prepare/allowBuilds required)
58
+ dsh plugin --profile web add github:gjj-star/dsh-conversation-navigator
59
+
60
+ # Option 3: local tarball
61
+ pnpm pack
62
+ dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
63
+ ```
64
+
65
+ `dsh plugin` forwards to pnpm inside the profile directory, so **pnpm must be on your PATH**; installation appends this package to the profile's `dsh.profile.bundles`, and its bundled `cordis.patch.yml` layer inserts the plugin row. Restart `dsh web` and the panel appears (expanded by default).
66
+
67
+ > Manual install (no pnpm): place the repo at `<DSH_HOME>\profiles\<profile>\node_modules\dsh-conversation-navigator` and append the content of [`example.patch.yml`](./example.patch.yml) to the top-level array of the profile's `cordis.patch.yml`.
68
+
69
+ ## Updates
70
+
71
+ Edit `lib/client.js` and restart `dsh web`. Only the panel position, docked state and minimal alignment persist (localStorage `dsh-cnvnav:ui:v1`); expand/collapse, mode selection and search keywords still live only within the page session.
72
+
73
+ ## How it works
74
+
75
+ - Slots: `conversation.session.header.utilities` (the "导航" toggle in the title bar) + `shell.overlay` (the floating panel)
76
+ - Data: the session-level standard props `useSession` (selects the stable render order `order` of `ConversationSnapshot.chat` + the `ChatNodeStore`), grouped by `node.location` turn
77
+ - History backfill: pages back through `sessions`'s `binding(sessionId).session.loadOlder()`, "Load all" loops until `hasMore=false`
78
+ - Jumping: reuses the DSH chat view's own stable DOM anchor `[data-chat-anchor-key]` (the same anchor the product uses internally for paging/scroll positioning) with `scrollIntoView` smooth scrolling
79
+ - Position tracking: captures scroll events on the `[data-conversation-scroll]` container (throttled 120ms) and computes the first visible node at the viewport top
80
+ - Keyword filter: extracts searchable text only for `user` and `assistant-step` nodes (`dialogueText`), case-insensitive matching, hits wrapped in `<mark>` and the display windowed around the first hit
81
+ - Hover full text: the turn-row bubble reads `fullDialogueText` (all text blocks of the user node joined), shown via `Tooltip` with a 340px width cap
82
+ - Modes: the header button cycles `viewMode` (full/hidden/minimal/minimal-left); hidden replaces the turn head title with a trajectory badge (`titleNode` strategy); minimal-right is docked by CSS `right`, minimal-left is pinned to `scrollport.left + 12` (the placement key includes `r.left` and a ResizeObserver on the chat area re-anchors it whenever the left sidebar expands or collapses); the toggle yields while the search input is open
83
+ - Drag & persistence: the full-panel header is draggable (pointer events, clamped to the viewport); dragging or the pin toggles `docked`; `place()` only clamps a freely-placed panel into the viewport instead of re-docking it; the position, docked state and alignment choice persist in localStorage (`dsh-cnvnav:ui:v1`)
84
+ - Styling: `Button`/`Tooltip`/search & close icons reuse `@deepseek-ai/dsh-client-ui-primitives`, the rest are inline Lucide SVGs (ISC License); the panel injects its own `<style>` element, colors use `--dsw-*` theme tokens; everything is cleaned up with the fiber on unload
85
+
86
+ ## Compatibility
87
+
88
+ - Target platform: DSH Web (`dsh.client.platform: web`), depends on the kernel seed's `react`, `slots`, `sessions` services and `@deepseek-ai/dsh-client-ui-primitives`, plus the standard capabilities provided by `dsh-client-runtime` and `dsh-client-ui-conversation` (`dsh.client.inject` declares runtime and conversation)
89
+ - **Version-sensitive points**: `[data-chat-anchor-key]` / `[data-conversation-scroll]` are the current DOM anchor conventions of the DSH chat view; if they change after a DSH upgrade, only `findAnchor` / `computeActiveKey` in `lib/client.js` need adjusting
90
+ - No hard `timer` dependency: the client timer service is used for throttling when present, and degrades to unthrottled otherwise
91
+
92
+ ## Structure
93
+
94
+ ```
95
+ lib/
96
+ index.js # empty host entry (browser-only plugin)
97
+ client.js # full browser implementation (window.__ModuleLoader__ module format)
98
+ assets/
99
+ screenshots/ # screenshots (README hero + market gallery)
100
+ cordis.patch.yml # bundle patch layer (inserts the plugin row)
101
+ example.patch.yml # patch example for manual installs
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
package/README.md CHANGED
@@ -1,98 +1,102 @@
1
- # DSH 会话导航插件(dsh-conversation-navigator)
2
-
3
- [![npm](https://img.shields.io/npm/v/dsh-conversation-navigator?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-conversation-navigator)
4
- [![downloads](https://img.shields.io/npm/dm/dsh-conversation-navigator?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-conversation-navigator)
5
- [![stars](https://img.shields.io/github/stars/gjj-star/dsh-conversation-navigator?style=flat-square&color=green)](https://github.com/gjj-star/dsh-conversation-navigator)
6
- [![license](https://img.shields.io/github/license/gjj-star/dsh-conversation-navigator?style=flat-square&color=teal)](https://github.com/gjj-star/dsh-conversation-navigator)
7
- [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](https://www.npmjs.com/package/dsh-conversation-navigator)
8
- [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
9
- [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek_Harness-Plugin-blue?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
10
-
11
- **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页**右侧**悬浮展示按轮折叠的对话大纲,点击任意节点平滑跳转,滚动对话时实时高亮当前阅读位置,步次徽标配色与内置"轨迹"视图统一。
12
-
13
- 纯浏览器插件(无宿主行为)、纯 JavaScript、零构建、零 npm 依赖(按钮/Tooltip 复用 DSH 内核 seed 的官方 primitives)
14
-
15
- ![显示轮次模式](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-main.png)
16
- ![隐藏轮次模式](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-no-round.png)
17
- ![极简模式·收起](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-minimal-hide.png)
18
- ![极简模式·展开](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-minimal-expand.png)
19
-
20
- > 三种模式:显示轮次、隐藏轮次、极简(收起仅露行内指示条,悬停展开定位面板)。完整截图见 [assets/screenshots](./assets/screenshots)。其中「社区皮肤适配」两张为第三方皮肤下的效果(鲸鱼娘女仆主题与君の名は主题),非插件自带。
21
-
22
- ## 功能
23
-
24
- - **按轮折叠大纲**:默认只显示"第 N 轮 + 你的问题"列表,长对话一目了然
25
- - **关键词过滤**:点击左上角搜索图标呼出输入框,只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
26
- - **展开/折叠步次**:点击轮次行右侧的箭头按钮(`▸ N`,展开时旋转为 `▾`)平滑展开/折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
27
- - **悬停查看全文**:鼠标在轮次行停留片刻,气泡显示该轮用户提问的完整原文,不再被单行截断
28
- - **显示 / 隐藏 / 极简三种模式**:头部切换按钮三态循环——
29
- - 「显示轮次」为经典分组视图(压缩等系统事件与轮次同级加粗展示);
30
- - 「隐藏轮次」每行统一为轨迹徽标 + 文本(用户 = 业务蓝、助手 = 紫罗兰、压缩 = 中性灰);
31
- - 「极简模式」收起时只露行内指示条(当前 = 品牌色实色、非当前 = 主题前景色掺 40%),悬停从右向左展开 7 行画幅的定位面板(点击跳转、悬停全文气泡、超 7 条仅滑块滚动),外置悬浮按钮返回「显示轮次」,搜索框展开时按钮自动让位
32
- - **丝滑动效**:面板开合淡入淡出、步次展开/折叠高度过渡、过滤结果逐条级联淡入、折叠箭头旋转——全部纯 CSS 实现,零依赖
33
- - **点击定位**:点击轮次主体或步次条目,平滑滚动跳转到对话中对应位置(不改变折叠状态)
34
- - **加载更早 / 加载全部**:面板顶部两个按钮——「加载更早」向后翻一页、「加载全部」一键把所有历史轮次载入导航,之后可任意跳转(页面默认仍懒加载,只有点按钮才补载)
35
- - **位置跟踪**:手动滚动对话时,面板自动高亮并跟随当前正在阅读的轮次
36
- - **右侧定位**:面板锚定视口右侧,收起/展开左侧边栏时纹丝不动
37
- - **回到最新 / 全部折叠**:面板底部两个快捷按钮
38
- - **轨迹配色**:用户/插话 = 业务蓝、上下文 = 成功绿、助手 = 紫罗兰、工具 = 琥珀、压缩 = 中性灰(与内置轨迹视图一致的 `--dsw` 主题 token,自动适配明暗主题)
39
- - **DSH 原生风格**:操作按钮复用官方 `Button`/`Tooltip` 组件与官方图标(搜索、关闭);其余操作图标(导航、加载更早、加载全部、回到最新、全部折叠、切换轮次等)为按 DSH 描边风格手绘的内联 SVG,`currentColor` 自动适配明暗主题
40
- - 切换工作区/会话自动跟随并重建大纲
41
-
42
- ## 安装
43
-
44
- 本插件是官方规范的**组合包**(`dsh.bundle` manifest + `dsh.client` 声明),纯 JavaScript、无构建步骤,推荐用官方 CLI 安装:
45
-
46
- ```sh
47
- # 方式一:npm(发布后,用户无需任何构建授权)
48
- dsh plugin --profile web add dsh-conversation-navigator
49
-
50
- # 方式二:GitHub(纯 JS 包无需 prepare/allowBuilds,直接可用)
51
- dsh plugin --profile web add github:gjj-star/dsh-conversation-navigator
52
-
53
- # 方式三:本地 tarball
54
- pnpm pack
55
- dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
56
- ```
57
-
58
- `dsh plugin` profile 目录内转发给 pnpm,因此需要 **pnpm 在 PATH 上**;安装会自动把本包追加进 profile 的 `dsh.profile.bundles`,其自带的 `cordis.patch.yml` 层负责插入插件行。重启 `dsh web` 后生效,面板默认展开。
59
-
60
- > 手动方式(不依赖 pnpm):把仓库放进 `<DSH_HOME>\profiles\<profile>\node_modules\dsh-conversation-navigator`,并在 profile 的 `cordis.patch.yml` 顶层数组追加 [`example.patch.yml`](./example.patch.yml) 的内容。
61
-
62
- ## 更新
63
-
64
- 修改 `lib/client.js` 后重启 `dsh web` 即可;插件没有持久状态,展开/折叠、显示/隐藏/极简模式、搜索关键词等界面状态仅存于页面会话内。
65
-
66
- ## 工作原理
67
-
68
- - 注册槽位:`conversation.session.header.utilities`(标题栏「导航」开关)+ `shell.overlay`(浮动面板)
69
- - 数据来源:会话级标准 props 的 `useSession`(选择 `ConversationSnapshot.chat` 的稳定渲染顺序 `order` + `ChatNodeStore`),按 `node.location` 的 turn 分组
70
- - 历史补载:通过 `sessions` 服务的 `binding(sessionId).session.loadOlder()` 分页向后加载,「加载全部」循环至 `hasMore=false`
71
- - 跳转定位:复用 DSH 聊天视图自身的稳定 DOM 锚点 `[data-chat-anchor-key]`(与产品内部 paging/scroll 定位同源),`scrollIntoView` 平滑滚动
72
- - 位置跟踪:捕获 `[data-conversation-scroll]` 滚动容器的 scroll 事件(节流 120ms),计算视口顶部首个可见节点
73
- - 关键词过滤:仅为 `user` `assistant-step` 节点提取检索文本(`dialogueText`),大小写不敏感匹配,命中片段以 `<mark>` 高亮并按首个命中位置截取显示窗口
74
- - 悬停全文:轮次头气泡读取 `fullDialogueText`(用户节点全部文本块拼接),用 `Tooltip` 展示并限宽 340px
75
- - 显示/隐藏/极简模式:头部按钮循环切换 `viewMode`(full/hidden/minimal),隐藏时仅把轮次头标题替换为轨迹徽标(`titleNode` 策略);极简时渲染独立的定位面板(收起仅露行内指示条,悬停展开),搜索框展开时按钮让位
76
- - 样式:`Button`/`Tooltip`/搜索与关闭图标复用 `@deepseek-ai/dsh-client-ui-primitives`,其余图标为内联 SVG;面板容器自建 `<style>` 注入,颜色使用 `--dsw-*` 主题 token;插件卸载时随 fiber 清理
77
-
78
- ## 兼容性说明
79
-
80
- - 目标平台:DSH Web 端(`dsh.client.platform: web`),依赖内核 seed 的 `react`、`slots`、`sessions` 服务与 `@deepseek-ai/dsh-client-ui-primitives`,以及 `dsh-client-runtime`、`dsh-client-ui-conversation` 提供的标准能力(`dsh.client.inject` 已声明 runtime conversation)
81
- - **版本敏感点**:`[data-chat-anchor-key]` / `[data-conversation-scroll]` 是当前 DSH 聊天视图的 DOM 锚点约定,DSH 升级后若锚点变化,只需调整 `lib/client.js` 中 `findAnchor` / `computeActiveKey` 两个函数
82
- - 未声明 `timer` 硬依赖:客户端的 timer 服务存在则用于节流,不存在时自动退化为未节流模式
83
-
84
- ## 目录结构
85
-
86
- ```
87
- lib/
88
- index.js # 宿主侧空入口(纯浏览器插件)
89
- client.js # 浏览器端完整实现(window.__ModuleLoader__ 模块格式)
90
- assets/
91
- screenshots/ # 截图(README 主图 + 市场截图墙)
92
- cordis.patch.yml # 组合包补丁层(插入插件行)
93
- example.patch.yml # 手动安装时的补丁示例
94
- ```
95
-
96
- ## License
97
-
98
- MIT
1
+ # DSH 会话导航插件(dsh-conversation-navigator)
2
+
3
+ [English](README.en.md) | 中文
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-conversation-navigator?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-conversation-navigator)
6
+ [![downloads](https://img.shields.io/npm/dm/dsh-conversation-navigator?style=flat-square&color=blue)](https://www.npmjs.com/package/dsh-conversation-navigator)
7
+ [![stars](https://img.shields.io/github/stars/gjj-star/dsh-conversation-navigator?style=flat-square&color=green)](https://github.com/gjj-star/dsh-conversation-navigator)
8
+ [![license](https://img.shields.io/github/license/gjj-star/dsh-conversation-navigator?style=flat-square&color=teal)](https://github.com/gjj-star/dsh-conversation-navigator)
9
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](https://www.npmjs.com/package/dsh-conversation-navigator)
10
+ [![awesome · DSH plugin](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
11
+ [![DeepSeek Harness](https://img.shields.io/badge/DeepSeek_Harness-Plugin-blue?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
12
+ [![dsh.so risk](https://www.dsh.so/badge/dsh-conversation-navigator-2.svg)](https://www.dsh.so/artifact/dsh-conversation-navigator-2/)
13
+ [![dsh.so install](https://www.dsh.so/badge/install/dsh-conversation-navigator-2.svg)](https://www.dsh.so/artifact/dsh-conversation-navigator-2/)
14
+
15
+ **DeepSeek Harness(DSH)Web 端会话导航面板**:在对话页**右侧**悬浮展示按轮折叠的对话大纲,点击任意节点平滑跳转,滚动对话时实时高亮当前阅读位置,步次徽标配色与内置"轨迹"视图统一。
16
+
17
+ 纯浏览器插件(无宿主行为)、纯 JavaScript、零构建、零 npm 依赖(按钮/Tooltip 复用 DSH 内核 seed 的官方 primitives)。
18
+
19
+ ![显示轮次模式](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-main.png)
20
+ ![隐藏轮次模式](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-no-round.png)
21
+ ![极简模式·收起](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-minimal-hide.png)
22
+ ![极简模式·展开](https://raw.githubusercontent.com/gjj-star/dsh-conversation-navigator/main/assets/modes/mode-minimal-expand.png)
23
+
24
+ > 四种形态:显示轮次、隐藏轮次、极简·右、极简·左(极简收起仅露行内指示条,悬停展开定位面板)。完整截图见 [assets/screenshots](./assets/screenshots)。其中「社区皮肤适配」两张为第三方皮肤下的效果(鲸鱼娘女仆主题与君の名は主题),非插件自带。
25
+
26
+ ## 功能
27
+
28
+ - **按轮折叠大纲**:默认只显示"第 N + 你的问题"列表,长对话一目了然
29
+ - **关键词过滤**:点击左上角搜索图标呼出输入框,只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
30
+ - **展开/折叠步次**:点击轮次行右侧的箭头按钮(`▸ N`,展开时旋转为 `▾`)平滑展开/折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
31
+ - **悬停查看全文**:鼠标在轮次行停留片刻,气泡显示该轮用户提问的完整原文,不再被单行截断
32
+ - **显示 / 隐藏 / 极简四种形态**:头部切换按钮四态循环——「显示轮次」为经典分组视图(压缩等系统事件与轮次同级加粗展示)→「隐藏轮次」每行统一为轨迹徽标 + 文本(用户 = 业务蓝、助手 = 紫罗兰、压缩 = 中性灰)→「极简·右」视口右缘指示条→「极简·左」锚定对话区左缘(为 dsh-better-sidebar 让出右侧),再回到「显示轮次」;
33
+ - 「极简」形态收起时只露行内指示条(当前 = 品牌色实色、非当前 = 主题前景色掺 40%),悬停展开 7 行画幅的定位面板(点击跳转、悬停全文气泡、超 7 条仅滑块滚动;右对齐从右向左展开、左对齐向右展开),外置悬浮按钮继续循环(极简·右→极简·左→显示轮次),搜索框展开时按钮自动让位
34
+ - **面板可拖动**:显示轮次 / 隐藏轮次面板按住头部即可拖到视口任意位置(自动夹边;展开高度至多半屏,上边缘固定、下边缘展开,超长轮次时列表内部滚动,底部「回到最新/全部折叠」始终可达),头部图钉按钮在「默认右侧停靠」与「上次拖动位置」间切换;拖动位置与极简左/右对齐选择持久化于 localStorage,刷新页面原样恢复
35
+ - **丝滑动效**:面板开合淡入淡出、步次展开/折叠高度过渡、过滤结果逐条级联淡入、折叠箭头旋转——全部纯 CSS 实现,零依赖
36
+ - **点击定位**:点击轮次主体或步次条目,平滑滚动跳转到对话中对应位置(不改变折叠状态)
37
+ - **加载更早 / 加载全部**:面板顶部两个按钮——「加载更早」向后翻一页、「加载全部」一键把所有历史轮次载入导航,之后可任意跳转(页面默认仍懒加载,只有点按钮才补载)
38
+ - **位置跟踪**:手动滚动对话时,面板自动高亮并跟随当前正在阅读的轮次
39
+ - **右侧定位**:默认锚定视口右侧,收起/展开左侧边栏时纹丝不动;解除停靠(拖动/图钉)后可自由放置
40
+ - **回到最新 / 全部折叠**:面板底部两个快捷按钮;「全部折叠」在没有展开的轮次时置灰,避免误以为失效
41
+ - **轨迹配色**:用户/插话 = 业务蓝、上下文 = 成功绿、助手 = 紫罗兰、工具 = 琥珀、压缩 = 中性灰(与内置轨迹视图一致的 `--dsw` 主题 token,自动适配明暗主题)
42
+ - **DSH 原生风格**:操作按钮复用官方 `Button`/`Tooltip` 组件与官方图标(搜索、关闭);其余操作图标(导航、加载更早、加载全部、回到最新、全部折叠、切换轮次等)取自 **Lucide** 描边图标集(ISC License,[lucide.dev](https://lucide.dev)),`currentColor` 自动适配明暗主题
43
+ - 切换工作区/会话自动跟随并重建大纲
44
+
45
+ ## 安装
46
+
47
+ 本插件是官方规范的**组合包**(`dsh.bundle` manifest + `dsh.client` 声明),纯 JavaScript、无构建步骤,推荐用官方 CLI 安装:
48
+
49
+ ```sh
50
+ # 方式一:npm(发布后,用户无需任何构建授权)
51
+ dsh plugin --profile web add dsh-conversation-navigator
52
+
53
+ # 方式二:GitHub(纯 JS 包无需 prepare/allowBuilds,直接可用)
54
+ dsh plugin --profile web add github:gjj-star/dsh-conversation-navigator
55
+
56
+ # 方式三:本地 tarball
57
+ pnpm pack
58
+ dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
59
+ ```
60
+
61
+ `dsh plugin` 在 profile 目录内转发给 pnpm,因此需要 **pnpm 在 PATH 上**;安装会自动把本包追加进 profile 的 `dsh.profile.bundles`,其自带的 `cordis.patch.yml` 层负责插入插件行。重启 `dsh web` 后生效,面板默认展开。
62
+
63
+ > 手动方式(不依赖 pnpm):把仓库放进 `<DSH_HOME>\profiles\<profile>\node_modules\dsh-conversation-navigator`,并在 profile 的 `cordis.patch.yml` 顶层数组追加 [`example.patch.yml`](./example.patch.yml) 的内容。
64
+
65
+ ## 更新
66
+
67
+ 修改 `lib/client.js` 后重启 `dsh web` 即可;仅面板位置、停靠状态与极简左/右对齐持久化于 localStorage(`dsh-cnvnav:ui:v1`),展开/折叠、模式、搜索关键词等界面状态仍只存于页面会话内。
68
+
69
+ ## 工作原理
70
+
71
+ - 注册槽位:`conversation.session.header.utilities`(标题栏「导航」开关)+ `shell.overlay`(浮动面板)
72
+ - 数据来源:会话级标准 props 的 `useSession`(选择 `ConversationSnapshot.chat` 的稳定渲染顺序 `order` + `ChatNodeStore`),按 `node.location` 的 turn 分组
73
+ - 历史补载:通过 `sessions` 服务的 `binding(sessionId).session.loadOlder()` 分页向后加载,「加载全部」循环至 `hasMore=false`
74
+ - 跳转定位:复用 DSH 聊天视图自身的稳定 DOM 锚点 `[data-chat-anchor-key]`(与产品内部 paging/scroll 定位同源),`scrollIntoView` 平滑滚动
75
+ - 位置跟踪:捕获 `[data-conversation-scroll]` 滚动容器的 scroll 事件(节流 120ms),计算视口顶部首个可见节点
76
+ - 关键词过滤:仅为 `user` 与 `assistant-step` 节点提取检索文本(`dialogueText`),大小写不敏感匹配,命中片段以 `<mark>` 高亮并按首个命中位置截取显示窗口
77
+ - 悬停全文:轮次头气泡读取 `fullDialogueText`(用户节点全部文本块拼接),用 `Tooltip` 展示并限宽 340px
78
+ - 显示/隐藏/极简模式:`viewMode` 四态循环(full/hidden/minimal/minimal-left),隐藏时仅把轮次头标题替换为轨迹徽标(`titleNode` 策略);极简右对齐由 CSS `right` 锚定视口右缘、左对齐以 `scrollport.left + 12` 钉在对话区左缘(定位 key 含 `r.left`,并用 ResizeObserver 监听对话区宽度变化,左侧栏展开/折叠时紧贴跟随);搜索框展开时按钮让位
79
+ - 拖动定位与持久化:面板头部拖拽(pointer events,视口夹边),拖动或图钉切换 `docked` 状态;`place()` 对自由放置的面板只做视口夹边、不再自动停靠;位置、停靠状态与对齐选择存于 localStorage(`dsh-cnvnav:ui:v1`)
80
+ - 样式:`Button`/`Tooltip`/搜索与关闭图标复用 `@deepseek-ai/dsh-client-ui-primitives`,其余图标为 Lucide 内联 SVG(ISC License);面板容器自建 `<style>` 注入,颜色使用 `--dsw-*` 主题 token;插件卸载时随 fiber 清理
81
+
82
+ ## 兼容性说明
83
+
84
+ - 目标平台:DSH Web 端(`dsh.client.platform: web`),依赖内核 seed 的 `react`、`slots`、`sessions` 服务与 `@deepseek-ai/dsh-client-ui-primitives`,以及 `dsh-client-runtime`、`dsh-client-ui-conversation` 提供的标准能力(`dsh.client.inject` 已声明 runtime 与 conversation)
85
+ - **版本敏感点**:`[data-chat-anchor-key]` / `[data-conversation-scroll]` 是当前 DSH 聊天视图的 DOM 锚点约定,DSH 升级后若锚点变化,只需调整 `lib/client.js` 中 `findAnchor` / `computeActiveKey` 两个函数
86
+ - 未声明 `timer` 硬依赖:客户端的 timer 服务存在则用于节流,不存在时自动退化为未节流模式
87
+
88
+ ## 目录结构
89
+
90
+ ```
91
+ lib/
92
+ index.js # 宿主侧空入口(纯浏览器插件)
93
+ client.js # 浏览器端完整实现(window.__ModuleLoader__ 模块格式)
94
+ assets/
95
+ screenshots/ # 截图(README 主图 + 市场截图墙)
96
+ cordis.patch.yml # 组合包补丁层(插入插件行)
97
+ example.patch.yml # 手动安装时的补丁示例
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
package/lib/client.js CHANGED
@@ -7,73 +7,90 @@ window.__ModuleLoader__.load({
7
7
  const React = require("react");
8
8
  const { Button, Tooltip, IconCloseOutline16, IconSearchOutline16 } = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
 
10
- /* ---------- custom icons (DSH outline style, unique to this panel) ---------- */
11
- function ToggleIcon() {
12
- return React.createElement("svg", {
13
- viewBox: "0 0 14 14", width: 14, height: 14, fill: "none",
14
- stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round",
15
- "aria-hidden": true,
16
- },
17
- React.createElement("circle", { cx: 3.5, cy: 3, r: 1, fill: "currentColor", stroke: "none" }),
18
- React.createElement("path", { d: "M6.5 3H12" }),
19
- React.createElement("circle", { cx: 3.5, cy: 7, r: 1, fill: "currentColor", stroke: "none" }),
20
- React.createElement("path", { d: "M6.5 7H12" }),
21
- React.createElement("circle", { cx: 3.5, cy: 11, r: 1, fill: "currentColor", stroke: "none" }),
22
- React.createElement("path", { d: "M6.5 11H12" }),
23
- );
24
- }
25
- function LoadEarlierIcon() {
26
- return React.createElement("svg", {
27
- viewBox: "0 0 14 14", width: 14, height: 14, fill: "none",
28
- stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round",
29
- "aria-hidden": true,
30
- },
31
- React.createElement("path", { d: "M7 11V5M4 7.5l3-3 3 3" }),
32
- React.createElement("line", { x1: 2, y1: 3, x2: 12, y2: 3 }),
33
- );
34
- }
35
- function LoadAllIcon() {
36
- return React.createElement("svg", {
37
- viewBox: "0 0 14 14", width: 14, height: 14, fill: "none",
38
- stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round",
39
- "aria-hidden": true,
40
- },
41
- React.createElement("path", { d: "M7 2l5 3-5 3-5-3 5-3z" }),
42
- React.createElement("path", { d: "M2 5v3l5 3 5-3V5" }),
43
- );
44
- }
45
- function JumpLatestIcon() {
46
- return React.createElement("svg", {
47
- viewBox: "0 0 14 14", width: 14, height: 14, fill: "none",
48
- stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round",
49
- "aria-hidden": true,
50
- },
51
- React.createElement("path", { d: "M7 3V9M4 7l3 3 3-3" }),
52
- React.createElement("line", { x1: 2, y1: 11, x2: 12, y2: 11 }),
53
- );
54
- }
55
- function CollapseAllIcon() {
56
- return React.createElement("svg", {
57
- viewBox: "0 0 14 14", width: 14, height: 14, fill: "none",
58
- stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round",
59
- "aria-hidden": true,
60
- },
61
- React.createElement("line", { x1: 2.5, y1: 2, x2: 11.5, y2: 2 }),
62
- React.createElement("line", { x1: 2.5, y1: 5, x2: 11.5, y2: 5 }),
63
- React.createElement("line", { x1: 2.5, y1: 8, x2: 11.5, y2: 8 }),
64
- React.createElement("line", { x1: 2.5, y1: 11, x2: 11.5, y2: 11 }),
65
- );
66
- }
10
+ /* ---------- 自绘图标: Lucide (ISC License, https://lucide.dev)
11
+ 24px 网格 / 2px 描边 / 圆角线帽, 与 DSH 官方描边图标同一视觉语言 ---------- */
12
+ function ToggleIcon() {
13
+ return React.createElement("svg", {
14
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
15
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
16
+ "aria-hidden": true,
17
+ },
18
+ React.createElement("path", { d: "M3 5h.01" }),
19
+ React.createElement("path", { d: "M3 12h.01" }),
20
+ React.createElement("path", { d: "M3 19h.01" }),
21
+ React.createElement("path", { d: "M8 5h13" }),
22
+ React.createElement("path", { d: "M8 12h13" }),
23
+ React.createElement("path", { d: "M8 19h13" }),
24
+ );
25
+ }
26
+ function LoadEarlierIcon() {
27
+ return React.createElement("svg", {
28
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
29
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
30
+ "aria-hidden": true,
31
+ },
32
+ React.createElement("path", { d: "M5 3h14" }),
33
+ React.createElement("path", { d: "m18 13-6-6-6 6" }),
34
+ React.createElement("path", { d: "M12 7v14" }),
35
+ );
36
+ }
37
+ function LoadAllIcon() {
38
+ return React.createElement("svg", {
39
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
40
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
41
+ "aria-hidden": true,
42
+ },
43
+ React.createElement("path", { d: "m17 11-5-5-5 5" }),
44
+ React.createElement("path", { d: "m17 18-5-5-5 5" }),
45
+ );
46
+ }
47
+ function JumpLatestIcon() {
48
+ return React.createElement("svg", {
49
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
50
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
51
+ "aria-hidden": true,
52
+ },
53
+ React.createElement("path", { d: "M12 17V3" }),
54
+ React.createElement("path", { d: "m6 11 6 6 6-6" }),
55
+ React.createElement("path", { d: "M19 21H5" }),
56
+ );
57
+ }
58
+ function CollapseAllIcon() {
59
+ return React.createElement("svg", {
60
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
61
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
62
+ "aria-hidden": true,
63
+ },
64
+ React.createElement("path", { d: "m14 10 7-7" }),
65
+ React.createElement("path", { d: "M20 10h-6V4" }),
66
+ React.createElement("path", { d: "m3 21 7-7" }),
67
+ React.createElement("path", { d: "M4 14h6v6" }),
68
+ );
69
+ }
67
70
 
68
- function SwitchTurnsIcon() {
69
- return React.createElement("svg", {
70
- viewBox: "0 0 1024 1024", width: 14, height: 14, fill: "currentColor",
71
- "aria-hidden": true,
72
- },
73
- React.createElement("path", { d: "M168 454c-8.837 0-16-7.163-16-16v-48c0-8.837 7.163-16 16-16h531.572L519.63 191.225c-9.858-10.014-2.934-26.883 10.978-27.22l0.424-0.005h70a16 16 0 0 1 11.402 4.775l254 258C876.392 436.89 869.226 454 855.032 454H168z m642-61.741v0.032L855 438h0.032L810 392.259zM601.032 180H601l190.992 194h0.032L601.032 180z" }),
74
- React.createElement("path", { d: "M855.064 570c8.837 0 16 7.163 16 16l0 48c0 8.837-7.163 16-16 16l-531.572 0L503.434 832.775c9.858 10.014 2.934 26.884-10.978 27.22l-0.424 0.005-70 0a16 16 0 0 1-11.402-4.775l-254-258C146.672 587.11 153.838 570 168.032 570L855.064 570z m-642 61.741l0-0.032L168.064 586l-0.032 0L213.064 631.741zM422.032 844H422.064l-190.992-194-0.032 0L422.032 844z" }),
75
- );
76
- }
71
+ function SwitchTurnsIcon() {
72
+ return React.createElement("svg", {
73
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
74
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
75
+ "aria-hidden": true,
76
+ },
77
+ React.createElement("path", { d: "M8 3 4 7l4 4" }),
78
+ React.createElement("path", { d: "M4 7h16" }),
79
+ React.createElement("path", { d: "m16 21 4-4-4-4" }),
80
+ React.createElement("path", { d: "M20 17H4" }),
81
+ );
82
+ }
83
+
84
+ function PinIcon() {
85
+ return React.createElement("svg", {
86
+ viewBox: "0 0 24 24", width: 14, height: 14, fill: "none",
87
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
88
+ "aria-hidden": true,
89
+ },
90
+ React.createElement("path", { d: "M12 17v5" }),
91
+ React.createElement("path", { d: "M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" }),
92
+ );
93
+ }
77
94
 
78
95
  /* ---------- own stylesheet (static packages have no styles builtin) ---------- */
79
96
  const CSS = `
@@ -105,7 +122,6 @@ window.__ModuleLoader__.load({
105
122
  .cnvnav-list {
106
123
  overflow-y: auto; padding: 6px; min-height: 0;
107
124
  display: flex; flex-direction: column; gap: 2px;
108
- scrollbar-width: thin;
109
125
  }
110
126
  .cnvnav-group { display: flex; flex-direction: column; margin-bottom: 2px; }
111
127
  .cnvnav-group-row {
@@ -228,7 +244,7 @@ window.__ModuleLoader__.load({
228
244
  box-shadow: 0 16px 48px rgba(0,0,0,.22);
229
245
  }
230
246
  .cnvnav-mini-fab {
231
- position: absolute; bottom: 100%; right: -12.5px;
247
+ position: absolute; bottom: 100%; right: -8px;
232
248
  margin-bottom: 6px;
233
249
  display: flex;
234
250
  }
@@ -238,16 +254,24 @@ window.__ModuleLoader__.load({
238
254
  display: flex; flex-direction: column; gap: 2px;
239
255
  }
240
256
  /* dsweb 式滚动条: 只显示滑块, 无滑轨无上下按钮.
241
- Chromium 用 webkit 伪元素(标准属性会令其失效), Firefox 用 @supports 标准属性 */
257
+ 面板列表与极简条统一为常显细白条(极简条收起时列表被裁剪,
258
+ 悬停展开后滑块自然可见). Chromium 用 webkit 伪元素
259
+ (标准属性会令其失效), Firefox 用 @supports 标准属性 */
260
+ .cnvnav-list::-webkit-scrollbar,
242
261
  .cnvnav-mini-list::-webkit-scrollbar { width: 4px; }
262
+ .cnvnav-list::-webkit-scrollbar-track,
243
263
  .cnvnav-mini-list::-webkit-scrollbar-track { background: transparent; }
264
+ .cnvnav-list::-webkit-scrollbar-thumb,
244
265
  .cnvnav-mini-list::-webkit-scrollbar-thumb {
245
266
  background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent);
246
267
  border-radius: 2px;
247
268
  }
269
+ .cnvnav-list::-webkit-scrollbar-button,
248
270
  .cnvnav-mini-list::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
271
+ .cnvnav-list::-webkit-scrollbar-corner,
249
272
  .cnvnav-mini-list::-webkit-scrollbar-corner { background: transparent; }
250
273
  @supports (-moz-appearance: none) {
274
+ .cnvnav-list,
251
275
  .cnvnav-mini-list {
252
276
  scrollbar-width: thin;
253
277
  scrollbar-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 40%, transparent) transparent;
@@ -296,6 +320,21 @@ window.__ModuleLoader__.load({
296
320
  .cnvnav-footer { display: flex; gap: 6px; margin: 0 6px 6px; }
297
321
  .cnvnav-bar-btn { flex: 1; display: block; }
298
322
  .cnvnav-bar-fill { width: 100%; }
323
+ /* ---------- 拖动定位与左对齐极简 ---------- */
324
+ .cnvnav-head { cursor: grab; touch-action: none; }
325
+ .cnvnav-dragging { cursor: grabbing; user-select: none; }
326
+ .cnvnav-dragging .cnvnav-head { cursor: grabbing; }
327
+ /* 模式切换按钮激活态: 与整行描边图标按钮同一视觉层级
328
+ (实心 primary 填充夹在幽灵图标中间会显得"错位") */
329
+ .cnvnav-head button.cnvnav-toggle-on {
330
+ color: var(--dsw-alias-brand-primary, #4f46e5);
331
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 13%, transparent);
332
+ }
333
+ .cnvnav-head button.cnvnav-toggle-on:hover {
334
+ background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f46e5) 20%, transparent);
335
+ }
336
+ .cnvnav-mini-wrap-left { right: auto; }
337
+ .cnvnav-mini-wrap-left .cnvnav-mini-fab { right: auto; left: -8px; }
299
338
  `;
300
339
 
301
340
  function apply(ctx) {
@@ -312,7 +351,38 @@ window.__ModuleLoader__.load({
312
351
  ctx.effect(() => () => styleEl.remove());
313
352
 
314
353
  /* ---------- shared store (toggle button + overlay panel) ---------- */
315
- let state = { open: true, sessionId: null, outline: null, activeKey: null, expanded: {}, hasMore: false, loadingOlder: false, loadingAll: false };
354
+ const UI_KEY = "dsh-cnvnav:ui:v1";
355
+ const VIEW_MODES = ["full", "hidden", "minimal", "minimal-left"];
356
+ function readUi() {
357
+ try {
358
+ const raw = window.localStorage.getItem(UI_KEY);
359
+ if (!raw) return {};
360
+ const o = JSON.parse(raw);
361
+ const out = {};
362
+ if (VIEW_MODES.indexOf(o.viewMode) !== -1) out.viewMode = o.viewMode;
363
+ if (o.docked === true || o.docked === false) out.docked = o.docked;
364
+ if (o.userPos && typeof o.userPos.left === "number" && typeof o.userPos.top === "number"
365
+ && Number.isFinite(o.userPos.left) && Number.isFinite(o.userPos.top)) {
366
+ out.userPos = { left: o.userPos.left, top: o.userPos.top };
367
+ }
368
+ return out;
369
+ } catch (err) {
370
+ return {};
371
+ }
372
+ }
373
+ function persistUi() {
374
+ try {
375
+ window.localStorage.setItem(UI_KEY, JSON.stringify({
376
+ viewMode: state.viewMode, docked: state.docked, userPos: state.userPos,
377
+ }));
378
+ } catch (err) { /* 隐私模式等场景忽略 */ }
379
+ }
380
+ const savedUi = readUi();
381
+ let state = Object.assign({
382
+ open: true, sessionId: null, outline: null, activeKey: null, expanded: {},
383
+ hasMore: false, loadingOlder: false, loadingAll: false,
384
+ viewMode: "full", docked: true, userPos: null,
385
+ }, savedUi);
316
386
  const listeners = new Set();
317
387
  function setState(patch) {
318
388
  state = Object.assign({}, state, patch);
@@ -594,6 +664,7 @@ window.__ModuleLoader__.load({
594
664
 
595
665
  let panelEl = null;
596
666
  let listEl = null;
667
+ let dragMove = null;
597
668
  const itemEls = {};
598
669
  const groupEls = {};
599
670
 
@@ -634,18 +705,60 @@ window.__ModuleLoader__.load({
634
705
  const snap = useStore();
635
706
  const [filter, setFilter] = React.useState("");
636
707
  const [searchOpen, setSearchOpen] = React.useState(false);
637
- const [viewMode, setViewMode] = React.useState("full"); /* full | hidden | minimal */
708
+ const viewMode = snap.viewMode; /* full | hidden | minimal | minimal-left */
638
709
  const turnsHidden = viewMode !== "full";
639
710
  function nextModeLabel() {
640
- return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "极简模式" : "显示轮次";
711
+ return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "极简·右" : viewMode === "minimal" ? "极简·左" : "显示轮次";
641
712
  }
642
713
  function cycleMode() {
643
- setViewMode(viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal" : "full");
714
+ const next = viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal" : viewMode === "minimal" ? "minimal-left" : "full";
715
+ setState({ viewMode: next });
716
+ persistUi();
644
717
  }
645
718
  function toggleSearch() {
646
719
  if (searchOpen) setFilter("");
647
720
  setSearchOpen(!searchOpen);
648
721
  }
722
+ /* ---------- 拖动定位(仅面板形式, 极简条不参与) ---------- */
723
+ function startDrag(e) {
724
+ if (e.button !== 0) return;
725
+ if (typeof e.target.closest === "function" && e.target.closest("button") !== null) return;
726
+ const el = panelEl;
727
+ if (el === null) return;
728
+ dragMove = { startX: e.clientX, startY: e.clientY, baseLeft: el.offsetLeft, baseTop: el.offsetTop, el: el, dragging: false };
729
+ try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) { /* 忽略捕获失败 */ }
730
+ }
731
+ function moveDrag(e) {
732
+ const d = dragMove;
733
+ if (d === null) return;
734
+ const dx = e.clientX - d.startX;
735
+ const dy = e.clientY - d.startY;
736
+ if (!d.dragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
737
+ d.dragging = true;
738
+ d.el.classList.add("cnvnav-dragging");
739
+ }
740
+ if (!d.dragging) return;
741
+ const vw = document.documentElement.clientWidth || 1000;
742
+ const vh = document.documentElement.clientHeight || 800;
743
+ const w = d.el.offsetWidth || 288;
744
+ const h = d.el.offsetHeight || 200;
745
+ const left = Math.min(Math.max(8, d.baseLeft + dx), Math.max(8, vw - w - 8));
746
+ const top = Math.min(Math.max(8, d.baseTop + dy), Math.max(8, vh - h - 8));
747
+ d.el.style.left = Math.round(left) + "px";
748
+ d.el.style.top = Math.round(top) + "px";
749
+ }
750
+ function endDrag() {
751
+ const d = dragMove;
752
+ dragMove = null;
753
+ if (d === null || !d.dragging) return;
754
+ d.el.classList.remove("cnvnav-dragging");
755
+ const r = d.el.getBoundingClientRect();
756
+ /* 拖完同步封顶(与 place() 浮动分支一致): 上边缘固定, 高度至多半屏 */
757
+ const vh = document.documentElement.clientHeight || 800;
758
+ d.el.style.maxHeight = Math.max(160, Math.round(Math.min(vh / 2, vh - Math.max(8, Math.round(r.top)) - 8))) + "px";
759
+ setState({ docked: false, userPos: { left: Math.round(r.left), top: Math.round(r.top) } });
760
+ persistUi();
761
+ }
649
762
 
650
763
  React.useEffect(() => {
651
764
  if (!snap.open) return;
@@ -655,11 +768,25 @@ window.__ModuleLoader__.load({
655
768
  function place() {
656
769
  const panel = panelEl;
657
770
  if (panel === null) return false;
771
+ if (dragMove !== null) return true;
658
772
  const vw = document.documentElement.clientWidth || 1000;
659
773
  const vh = document.documentElement.clientHeight || 800;
774
+ const mini = viewMode === "minimal" || viewMode === "minimal-left";
775
+ const alignLeft = viewMode === "minimal-left";
776
+ /* 面板被拖走后不再自动停靠: 夹进视口、上边缘固定, 展开高度至多半屏以便下方留白 */
777
+ if (!mini && !state.docked && state.userPos !== null) {
778
+ const w = panel.offsetWidth || 288;
779
+ const h = panel.offsetHeight || 200;
780
+ const left = Math.min(Math.max(8, Math.round(state.userPos.left)), Math.max(8, vw - w - 8));
781
+ const top = Math.min(Math.max(8, Math.round(state.userPos.top)), Math.max(8, vh - h - 8));
782
+ panel.style.maxHeight = Math.max(160, Math.round(Math.min(vh / 2, vh - top - 8))) + "px";
783
+ panel.style.left = left + "px";
784
+ panel.style.top = top + "px";
785
+ panel.classList.remove("cnvnav-panel-unplaced");
786
+ return true;
787
+ }
660
788
  const sp = document.querySelector("[data-conversation-scroll]");
661
789
  const width = 288;
662
- const mini = viewMode === "minimal";
663
790
  const MINI_H = 220;
664
791
  if (sp === null) {
665
792
  /* 会话视图可能尚未挂载(刷新/重启/切会话时面板先就位);
@@ -668,8 +795,14 @@ window.__ModuleLoader__.load({
668
795
  misses += 1;
669
796
  if (misses >= 15) {
670
797
  if (mini) {
671
- /* 极简: 水平由 CSS right 锚定, 清掉普通面板残留的 left */
672
- panel.style.left = "";
798
+ /* 极简: 水平锚定, 右对齐由 CSS right 负责, 左对齐钉在视口左侧 */
799
+ if (alignLeft) {
800
+ panel.style.left = "12px";
801
+ panel.style.right = "auto";
802
+ } else {
803
+ panel.style.left = "";
804
+ panel.style.right = "";
805
+ }
673
806
  panel.style.top = Math.max(8, Math.round((vh - MINI_H) / 2)) + "px";
674
807
  panel.style.maxHeight = MINI_H + "px";
675
808
  } else {
@@ -684,13 +817,19 @@ window.__ModuleLoader__.load({
684
817
  }
685
818
  const r = sp.getBoundingClientRect();
686
819
  const key = mini
687
- ? [Math.round(r.top), Math.round(r.height), Math.round(vh)].join("|")
820
+ ? [Math.round(r.left), Math.round(r.top), Math.round(r.height), Math.round(vh)].join("|")
688
821
  : [Math.round(r.top), Math.round(r.height), Math.round(vw), Math.round(vh)].join("|");
689
822
  if (key !== lastKey) {
690
823
  lastKey = key;
691
824
  if (mini) {
692
- /* 极简: 右侧锚定由 CSS(right)负责, 这里只定垂直中点 */
693
- panel.style.left = "";
825
+ /* 极简: 右对齐由 CSS(right)锚定视口右缘, 左对齐钉在对话区左缘 */
826
+ if (alignLeft) {
827
+ panel.style.left = Math.max(8, Math.round(r.left + 12)) + "px";
828
+ panel.style.right = "auto";
829
+ } else {
830
+ panel.style.left = "";
831
+ panel.style.right = "";
832
+ }
694
833
  panel.style.top = Math.max(8, Math.round(r.top + (r.height - MINI_H) / 2)) + "px";
695
834
  panel.style.maxHeight = MINI_H + "px";
696
835
  } else {
@@ -715,11 +854,19 @@ window.__ModuleLoader__.load({
715
854
  }
716
855
  }, 400);
717
856
  window.addEventListener("resize", place);
857
+ /* 左对齐极简条需跟随左侧栏展开/折叠: 对话区宽度变化时重新校准 */
858
+ let ro = null;
859
+ const roTarget = document.querySelector("[data-conversation-scroll]");
860
+ if (typeof ResizeObserver === "function" && roTarget !== null) {
861
+ ro = new ResizeObserver(() => { place(); });
862
+ ro.observe(roTarget);
863
+ }
718
864
  return () => {
719
865
  clearInterval(id);
720
866
  window.removeEventListener("resize", place);
867
+ if (ro !== null) ro.disconnect();
721
868
  };
722
- }, [snap.open, snap.sessionId, viewMode]);
869
+ }, [snap.open, snap.sessionId, viewMode, state.docked, state.userPos]);
723
870
 
724
871
  React.useEffect(() => {
725
872
  if (!snap.open) return;
@@ -761,9 +908,10 @@ window.__ModuleLoader__.load({
761
908
 
762
909
  const groups = snap.outline !== null ? snap.outline.groups : [];
763
910
  const count = snap.outline !== null ? snap.outline.count : 0;
911
+ const hasExpanded = Object.keys(snap.expanded).some((k) => snap.expanded[k] === true);
764
912
 
765
- /* ---------- 极简模式: 只保留定位 ---------- */
766
- if (viewMode === "minimal") {
913
+ /* ---------- 极简模式(右/左对齐): 只保留定位 ---------- */
914
+ if (viewMode === "minimal" || viewMode === "minimal-left") {
767
915
  const miniRows = [];
768
916
  for (const g of groups) {
769
917
  if (g.turn === null) continue;
@@ -776,13 +924,13 @@ window.__ModuleLoader__.load({
776
924
  }
777
925
  return React.createElement("div", {
778
926
  ref: (el) => { panelEl = el; },
779
- className: "cnvnav-mini-wrap cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
927
+ className: "cnvnav-mini-wrap" + (viewMode === "minimal-left" ? " cnvnav-mini-wrap-left" : "") + " cnvnav-panel-unplaced" + (snap.open ? "" : " cnvnav-panel-hidden"),
780
928
  role: "complementary",
781
929
  "aria-label": "会话导航",
782
930
  "aria-hidden": !snap.open,
783
931
  },
784
932
  React.createElement(Tooltip, {
785
- label: "显示轮次",
933
+ label: nextModeLabel(),
786
934
  side: "top",
787
935
  delayMs: 500,
788
936
  },
@@ -984,11 +1132,11 @@ window.__ModuleLoader__.load({
984
1132
  },
985
1133
  React.createElement("svg", {
986
1134
  className: "cnvnav-chevron-icon",
987
- viewBox: "0 0 12 12", width: 12, height: 12, fill: "none",
988
- stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round",
1135
+ viewBox: "0 0 24 24", width: 12, height: 12, fill: "none",
1136
+ stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round",
989
1137
  "aria-hidden": true,
990
1138
  },
991
- React.createElement("path", { d: "M4.5 2.5 8 6l-3.5 3.5" }),
1139
+ React.createElement("path", { d: "m9 18 6-6-6-6" }),
992
1140
  ),
993
1141
  expanded ? null : React.createElement("span", { className: "cnvnav-chevron-count" }, String(stepItems.length)),
994
1142
  ),
@@ -1018,7 +1166,13 @@ window.__ModuleLoader__.load({
1018
1166
  "aria-label": "会话导航",
1019
1167
  "aria-hidden": !snap.open,
1020
1168
  },
1021
- React.createElement("div", { className: "cnvnav-head" },
1169
+ React.createElement("div", {
1170
+ className: "cnvnav-head",
1171
+ onPointerDown: startDrag,
1172
+ onPointerMove: moveDrag,
1173
+ onPointerUp: endDrag,
1174
+ onPointerCancel: endDrag,
1175
+ },
1022
1176
  React.createElement(Tooltip, {
1023
1177
  label: searchOpen ? "收起搜索" : "搜索关键词",
1024
1178
  side: "bottom",
@@ -1042,8 +1196,9 @@ window.__ModuleLoader__.load({
1042
1196
  },
1043
1197
  React.createElement("span", { className: "cnvnav-turns-toggle" },
1044
1198
  React.createElement(Button, {
1045
- variant: viewMode !== "full" ? "primary" : "ghost",
1199
+ variant: "ghost",
1046
1200
  size: "sm",
1201
+ className: viewMode !== "full" ? "cnvnav-toggle-on" : "",
1047
1202
  icon: React.createElement(SwitchTurnsIcon),
1048
1203
  "aria-pressed": viewMode !== "full",
1049
1204
  onClick: () => cycleMode(),
@@ -1060,6 +1215,30 @@ window.__ModuleLoader__.load({
1060
1215
  onChange: (e) => setFilter(e.target.value),
1061
1216
  })
1062
1217
  : null,
1218
+ React.createElement(Tooltip, {
1219
+ label: snap.docked ? "解除停靠(拖动头部定位)" : "停靠回右侧",
1220
+ side: "bottom",
1221
+ delayMs: 500,
1222
+ },
1223
+ React.createElement("span", null,
1224
+ React.createElement(Button, {
1225
+ variant: snap.docked ? "primary" : "ghost",
1226
+ size: "sm",
1227
+ icon: React.createElement(PinIcon),
1228
+ "aria-pressed": snap.docked,
1229
+ onClick: () => {
1230
+ if (snap.docked) {
1231
+ const el = panelEl;
1232
+ const r = el !== null ? el.getBoundingClientRect() : null;
1233
+ setState({ docked: false, userPos: r !== null ? { left: Math.round(r.left), top: Math.round(r.top) } : null });
1234
+ } else {
1235
+ setState({ docked: true });
1236
+ }
1237
+ persistUi();
1238
+ },
1239
+ }),
1240
+ ),
1241
+ ),
1063
1242
  React.createElement("span", { className: "cnvnav-count" }, (filtering ? groupNodes.length + " / " : "") + String(count) + " 条"),
1064
1243
  React.createElement(Tooltip, {
1065
1244
  label: "收起面板",
@@ -1145,9 +1324,10 @@ window.__ModuleLoader__.load({
1145
1324
  ),
1146
1325
  ),
1147
1326
  React.createElement(Tooltip, {
1148
- label: "折叠所有已展开的轮次",
1327
+ label: hasExpanded ? "折叠所有已展开的轮次" : "当前无展开的轮次",
1149
1328
  side: "top",
1150
1329
  delayMs: 500,
1330
+ disabled: !hasExpanded,
1151
1331
  },
1152
1332
  React.createElement("span", { className: "cnvnav-bar-btn" },
1153
1333
  React.createElement(Button, {
@@ -1155,6 +1335,7 @@ window.__ModuleLoader__.load({
1155
1335
  size: "sm",
1156
1336
  className: "cnvnav-bar-fill",
1157
1337
  icon: React.createElement(CollapseAllIcon),
1338
+ disabled: !hasExpanded,
1158
1339
  onClick: () => setState({ expanded: {} }),
1159
1340
  }, "全部折叠"),
1160
1341
  ),
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "dsh-conversation-navigator",
3
- "version": "0.1.25",
4
- "description": "DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
5
- "keywords": [
6
- "dsh",
7
- "deepseek-harness",
8
- "cordis",
9
- "cordis-plugin",
10
- "conversation",
11
- "navigator",
12
- "outline",
13
- "session",
14
- "会话导航"
15
- ],
16
- "license": "MIT",
17
- "repository": {
18
- "type": "git",
19
- "url": "git+https://github.com/gjj-star/dsh-conversation-navigator.git"
20
- },
21
- "type": "module",
22
- "main": "lib/index.js",
23
- "exports": {
24
- ".": "./lib/index.js",
25
- "./client": "./lib/client.js",
26
- "./package.json": "./package.json"
27
- },
28
- "files": [
29
- "lib",
30
- "cordis.patch.yml"
31
- ],
32
- "engines": {
33
- "node": ">=18"
34
- },
35
- "dsh": {
36
- "bundle": {
37
- "patch": "./cordis.patch.yml"
38
- },
39
- "client": {
40
- "platform": "web",
41
- "inject": [
42
- "@deepseek-ai/dsh-client-runtime",
43
- "@deepseek-ai/dsh-client-ui-conversation"
44
- ]
45
- }
46
- }
47
- }
1
+ {
2
+ "name": "dsh-conversation-navigator",
3
+ "version": "0.2.1",
4
+ "description": "DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "cordis",
9
+ "cordis-plugin",
10
+ "conversation",
11
+ "navigator",
12
+ "outline",
13
+ "session",
14
+ "会话导航"
15
+ ],
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/gjj-star/dsh-conversation-navigator.git"
20
+ },
21
+ "type": "module",
22
+ "main": "lib/index.js",
23
+ "exports": {
24
+ ".": "./lib/index.js",
25
+ "./client": "./lib/client.js",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib",
30
+ "cordis.patch.yml"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dsh": {
36
+ "bundle": {
37
+ "patch": "./cordis.patch.yml"
38
+ },
39
+ "client": {
40
+ "platform": "web",
41
+ "inject": [
42
+ "@deepseek-ai/dsh-client-runtime",
43
+ "@deepseek-ai/dsh-client-ui-conversation"
44
+ ]
45
+ }
46
+ }
47
+ }