dsh-conversation-navigator 0.2.5 → 0.2.7
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.
- package/README.en.md +13 -11
- package/README.md +12 -9
- package/lib/client.js +367 -21
- package/package.json +4 -2
package/README.en.md
CHANGED
|
@@ -22,7 +22,7 @@ Browser-only (no host behavior), plain JavaScript, zero build step, zero npm dep
|
|
|
22
22
|

|
|
23
23
|

|
|
24
24
|
|
|
25
|
-
>
|
|
25
|
+
> Three forms: full, hidden, and minimal-left (minimal-right is shelved as of 0.2.6, code retained). 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.
|
|
26
26
|
|
|
27
27
|
## Features
|
|
28
28
|
|
|
@@ -30,12 +30,11 @@ Browser-only (no host behavior), plain JavaScript, zero build step, zero npm dep
|
|
|
30
30
|
- **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
|
|
31
31
|
- **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.)
|
|
32
32
|
- **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
|
|
33
|
-
- **Full / Hidden / Minimal modes**: the header toggle cycles
|
|
33
|
+
- **Full / Hidden / Minimal-left modes**: the header toggle cycles three states —
|
|
34
34
|
- **Full**: the classic grouped view (system events like compaction are shown bold, at the same level as turns)
|
|
35
35
|
- **Hidden**: every row is a trajectory badge + text (user = business blue, assistant = violet, compaction = neutral gray)
|
|
36
|
-
- **Minimal-
|
|
37
|
-
-
|
|
38
|
-
- 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
|
|
36
|
+
- **Minimal-left**: the indicator-bar 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 and zero overlap with the official rail
|
|
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; left-aligned expands from left to right); a floating button above the bars keeps cycling forward (minimal-left → Full); the toggle yields while the search input is open
|
|
39
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
|
|
40
39
|
- **Resizable panel**: drag from the bottom-left rounded corner (Full / Hidden modes only) — width 240–560 px, height vertical. Height follows a content-adaptive + cap model: actual height = min(turn content, the cap you drag to), tracking the number of turns with no blank space under the footer; floor ~3 turns, ceiling within the conversation viewport. The drag tracks the pointer 1:1 and is fully reversible; width/height persist in localStorage
|
|
41
40
|
- **Silky motion**: panel fade, step expand/collapse height transitions, staggered fade-in of filter results, rotating collapse arrow — all pure CSS, zero dependencies
|
|
@@ -64,34 +63,37 @@ pnpm pack
|
|
|
64
63
|
dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
|
|
65
64
|
```
|
|
66
65
|
|
|
67
|
-
`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
|
+
`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. **Dual-form defaults**: on official DSH < 0.1.2-rc.1 (no built-in navigator) the panel is expanded by default; on official DSH ≥ 0.1.2-rc.1 (the conversation view ships the built-in TurnNavigator rail) the panel starts closed in "minimal-left" form with the search box collapsed, summoned by the "导航" title-bar toggle, with zero overlap against the official right-edge rail.
|
|
68
67
|
|
|
69
68
|
> 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`.
|
|
70
69
|
|
|
71
70
|
## Updates
|
|
72
71
|
|
|
73
|
-
Edit `lib/client.js` and restart `dsh web`. Docked state, dragged position, minimal alignment and the panel width/height persist (localStorage `dsh-cnvnav:ui:
|
|
72
|
+
Edit `lib/client.js` and restart `dsh web`. Docked state, dragged position, minimal alignment and the panel width/height persist (localStorage `dsh-cnvnav:ui:v2`); the open/closed state is deliberately NOT persisted — on the new host the panel always starts closed — and search keywords still live only within the page session.
|
|
74
73
|
|
|
75
74
|
> Published to npm as `dsh-conversation-navigator` (the version badge above always shows the latest release); to upgrade an installed copy, update from the marketplace or run `dsh plugin --profile web add dsh-conversation-navigator` again, then restart.
|
|
76
75
|
|
|
77
76
|
## How it works
|
|
78
77
|
|
|
79
78
|
- Slots: `conversation.session.header.utilities` (the "导航" toggle in the title bar) + `shell.overlay` (the floating panel)
|
|
80
|
-
- Data: the session-level standard props `useSession
|
|
79
|
+
- Data (dual-form): legacy hosts read the session-level standard props `useSession`; new hosts (official ≥ 0.1.2-rc.1) read the `uiConversation.binding(binding).target("chat")` snapshot — the primary channel groups by the official `timeline.turnOrder` + `locations` index, backs off to `ChatNodeStore.values()` (mapping via the node's own `location` or its `anchorSeq` against the official turn boundaries), and finally merges the official `turnOutline` projection (`sessions.projectionStore(sid).get("turnOutline")` / the slot standard prop `useProjection`, the same source the official rail's `mergeTurnRailItems` uses) to restore user messages the assembly index drops and to add out-of-window turns; re-derived automatically on `loadOlder`
|
|
81
80
|
- History backfill: pages back through `sessions`'s `binding(sessionId).session.loadOlder()`, "Load all" loops until `hasMore=false`
|
|
82
81
|
- 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
|
|
83
82
|
- Position tracking: captures scroll events on the `[data-conversation-scroll]` container (throttled 120ms) and computes the first visible node at the viewport top
|
|
84
83
|
- 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
|
|
85
84
|
- 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
|
|
86
|
-
- Modes: the header button cycles `viewMode` (full/hidden/minimal
|
|
85
|
+
- Modes: the header button cycles `viewMode` (full/hidden/minimal-left; `minimal`/minimal-right is commented out and retained); hidden replaces the turn head title with a trajectory badge (`titleNode` strategy); 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
|
|
87
86
|
- 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`)
|
|
88
87
|
- Resize: a transparent bottom-left corner grip captures pointer events; width writes `style.width` directly (240–560), height only adjusts `maxHeight` while `height` stays `auto`, so the actual height is always min(content, cap) with no blank space; dragging follows the start point linearly (`base + delta`) and shares the same container/viewport clamps as `place()` so nothing snaps on release; width/height persist as `panelWidth`/`panelHeight`
|
|
89
88
|
- 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
|
|
90
89
|
|
|
91
90
|
## Compatibility
|
|
92
91
|
|
|
93
|
-
- 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
|
|
94
|
-
- **
|
|
92
|
+
- 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`; `dsh.client.inject` declares `@deepseek-ai/dsh-client-ui-conversation` (`dsh-client-runtime` was removed in official v0.1.2-rc.1 and is no longer injected)
|
|
93
|
+
- **Dual form (0.2.6)**: the host form is detected at runtime via feature detection — the presence of the `uiConversation` service marks the new host (official ≥ 0.1.2-rc.1); no hardcoded version numbers. `uiConversation` is now declared in the client `exports.inject`, so the runtime guarantees it is ready and host detection no longer flips with load timing. Legacy hosts keep the full navigator (expanded by default); new hosts coexist with the official TurnNavigator: closed by default, "minimal-left" form, search box collapsed by default — the official rail handles jumping, this plugin focuses on keyword search, bulk "Load all" and step-level badges
|
|
94
|
+
- **Host version note**: fully verified on official **DSH v0.1.2-rc.1** (Web) — the new `uiConversation` service, the `conversation.session.header.utilities` empty-props contract, the chat target snapshot (`timeline`/`locations`/`nodes`/`navigation`/`legacy`) and the `turnOutline` projection. Legacy hosts (no `uiConversation`) are covered by the `useSession` path; older official versions are not individually regression-tested
|
|
95
|
+
- **Host dependency declaration**: `@deepseek-ai/dsh-client-ui-conversation` is declared as a `peerDependencies` semver range (the awesome-dsh-plugin convention; dshmarket's dependency check surfaces host compatibility from it)
|
|
96
|
+
- **Version-sensitive points**: `[data-chat-anchor-key]` / `[data-conversation-scroll]` are the current DOM anchor conventions of the DSH chat view (verified still present in official v0.1.2-rc.1); if they change after a DSH upgrade, only `findAnchor` / `computeActiveKey` in `lib/client.js` need adjusting
|
|
95
97
|
- No hard `timer` dependency: the client timer service is used for throttling when present, and degrades to unthrottled otherwise
|
|
96
98
|
|
|
97
99
|
## Structure
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|

|
|
23
23
|

|
|
24
24
|
|
|
25
|
-
>
|
|
25
|
+
> 三种形态:显示轮次、隐藏轮次、极简·左(极简·右已暂缓,代码保留在仓库中;极简收起仅露行内指示条,悬停展开定位面板)。完整截图见 [assets/screenshots](./assets/screenshots)。其中「社区皮肤适配」两张为第三方皮肤下的效果(鲸鱼娘女仆主题与君の名は主题),非插件自带。
|
|
26
26
|
|
|
27
27
|
## 功能
|
|
28
28
|
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
- **关键词过滤**:点击左上角搜索图标呼出输入框,只按「你的提问 + 助手实际回复文本」过滤(上下文、工具、命令、压缩、推理等不参与匹配),命中词高亮,列表文本自动定位到关键词处
|
|
31
31
|
- **展开/折叠步次**:点击轮次行右侧的箭头按钮(`▸ N`,展开时旋转为 `▾`)平滑展开/折叠该轮的步次明细(助手回复、工具调用、命令、压缩点等)
|
|
32
32
|
- **悬停查看全文**:鼠标在轮次行停留片刻,气泡显示该轮用户提问的完整原文,不再被单行截断
|
|
33
|
-
- **显示 / 隐藏 /
|
|
34
|
-
- 「极简」形态收起时只露行内指示条(当前 = 品牌色实色、非当前 = 主题前景色掺 40%),悬停展开 7 行画幅的定位面板(点击跳转、悬停全文气泡、超 7
|
|
33
|
+
- **显示 / 隐藏 / 极简·左三种形态**:头部切换按钮三态循环——「显示轮次」为经典分组视图(压缩等系统事件与轮次同级加粗展示)→「隐藏轮次」每行统一为轨迹徽标 + 文本(用户 = 业务蓝、助手 = 紫罗兰、压缩 = 中性灰)→「极简·左」锚定对话区左缘(为 dsh-better-sidebar 让出右侧,并与官方右侧轨道零重叠),再回到「显示轮次」;「极简·右」形态自 0.2.6 起暂缓(与官方 TurnNavigator 轨道视觉重叠),代码保留;
|
|
34
|
+
- 「极简」形态收起时只露行内指示条(当前 = 品牌色实色、非当前 = 主题前景色掺 40%),悬停展开 7 行画幅的定位面板(点击跳转、悬停全文气泡、超 7 条仅滑块滚动;左对齐向右展开),外置悬浮按钮继续循环(极简·左→显示轮次),搜索框展开时按钮自动让位
|
|
35
35
|
- **面板可拖动**:显示轮次 / 隐藏轮次面板按住头部即可拖到视口任意位置(自动夹边;展开高度至多半屏,上边缘固定、下边缘展开,超长轮次时列表内部滚动,底部「回到最新/全部折叠」始终可达),头部图钉按钮在「默认右侧停靠」与「上次拖动位置」间切换;拖动位置与极简左/右对齐选择持久化于 localStorage,刷新页面原样恢复
|
|
36
36
|
- **面板尺寸可调**:左下角圆角处拖拽缩放(仅显示轮次 / 隐藏轮次)——横向拖宽(240–560px),纵向拖高/拖矮;高度采用「内容自适应 + 上限」模型:实际高度 = min(轮次内容, 你拖到的上限),随轮数变化、不会在底部产生空白,下限约 3 轮,上限不超过会话可视区;拖拽 1:1 线性跟随、可逆,尺寸持久化于 localStorage
|
|
37
37
|
- **丝滑动效**:面板开合淡入淡出、步次展开/折叠高度过渡、过滤结果逐条级联淡入、折叠箭头旋转——全部纯 CSS 实现,零依赖
|
|
@@ -60,34 +60,37 @@ pnpm pack
|
|
|
60
60
|
dsh plugin --profile web add ./dsh-conversation-navigator-<version>.tgz
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`dsh plugin` 在 profile 目录内转发给 pnpm,因此需要 **pnpm 在 PATH 上**;安装会自动把本包追加进 profile 的 `dsh.profile.bundles`,其自带的 `cordis.patch.yml` 层负责插入插件行。重启 `dsh web`
|
|
63
|
+
`dsh plugin` 在 profile 目录内转发给 pnpm,因此需要 **pnpm 在 PATH 上**;安装会自动把本包追加进 profile 的 `dsh.profile.bundles`,其自带的 `cordis.patch.yml` 层负责插入插件行。重启 `dsh web` 后生效。**双形态默认值**:官方 < 0.1.2-rc.1(无内置导航)时面板默认展开;官方 ≥ 0.1.2-rc.1(会话视图内置 TurnNavigator 轮次轨道)时面板默认关闭、默认形态为「极简·左」、搜索框默认收起,由标题栏「导航」按钮主动呼出,与官方右侧轨道零重叠。
|
|
64
64
|
|
|
65
65
|
> 手动方式(不依赖 pnpm):把仓库放进 `<DSH_HOME>\profiles\<profile>\node_modules\dsh-conversation-navigator`,并在 profile 的 `cordis.patch.yml` 顶层数组追加 [`example.patch.yml`](./example.patch.yml) 的内容。
|
|
66
66
|
|
|
67
67
|
## 更新
|
|
68
68
|
|
|
69
|
-
修改 `lib/client.js` 后重启 `dsh web`
|
|
69
|
+
修改 `lib/client.js` 后重启 `dsh web` 即可;停靠状态、拖动位置、极简左对齐、面板宽高持久化于 localStorage(`dsh-cnvnav:ui:v2`;面板的展开/收起 `open` 不再持久化——新宿主每次启动都从默认关闭开始,搜索关键词等界面状态仍只存于页面会话内。
|
|
70
70
|
|
|
71
71
|
> 已发布到 npm(`dsh-conversation-navigator`,徽章实时显示最新版本);升级已安装副本:市场更新或 `dsh plugin --profile web add dsh-conversation-navigator` 后重启。
|
|
72
72
|
|
|
73
73
|
## 工作原理
|
|
74
74
|
|
|
75
75
|
- 注册槽位:`conversation.session.header.utilities`(标题栏「导航」开关)+ `shell.overlay`(浮动面板)
|
|
76
|
-
-
|
|
76
|
+
- 数据来源(双形态):旧宿主走会话级标准 props 的 `useSession`;新宿主(官方 ≥ 0.1.2-rc.1)走 `uiConversation.binding(binding).target("chat")` 快照——主通道为官方 `timeline.turnOrder` + `locations` 索引分组,`ChatNodeStore.values()` 兜底(依节点自带 `location` 或 `anchorSeq` 对照轮次边界归组),再用官方 `turnOutline` 投影(`sessions.projectionStore(sid).get("turnOutline")` / slot 标准 props 的 `useProjection`,与官方 rail 的 `mergeTurnRailItems` 同源)补上被组装器漏挂的用户消息与加载窗口外的轮次;随 `loadOlder` 自动重派生
|
|
77
77
|
- 历史补载:通过 `sessions` 服务的 `binding(sessionId).session.loadOlder()` 分页向后加载,「加载全部」循环至 `hasMore=false`
|
|
78
78
|
- 跳转定位:复用 DSH 聊天视图自身的稳定 DOM 锚点 `[data-chat-anchor-key]`(与产品内部 paging/scroll 定位同源),`scrollIntoView` 平滑滚动
|
|
79
79
|
- 位置跟踪:捕获 `[data-conversation-scroll]` 滚动容器的 scroll 事件(节流 120ms),计算视口顶部首个可见节点
|
|
80
80
|
- 关键词过滤:仅为 `user` 与 `assistant-step` 节点提取检索文本(`dialogueText`),大小写不敏感匹配,命中片段以 `<mark>` 高亮并按首个命中位置截取显示窗口
|
|
81
81
|
- 悬停全文:轮次头气泡读取 `fullDialogueText`(用户节点全部文本块拼接),用 `Tooltip` 展示并限宽 340px
|
|
82
|
-
- 显示/隐藏/极简模式:`viewMode`
|
|
82
|
+
- 显示/隐藏/极简模式:`viewMode` 三态循环(full/hidden/minimal-left,「极简·右」minimal 已注释保留),隐藏时仅把轮次头标题替换为轨迹徽标(`titleNode` 策略);极简左对齐以 `scrollport.left + 12` 钉在对话区左缘(定位 key 含 `r.left`,并用 ResizeObserver 监听对话区宽度变化,左侧栏展开/折叠时紧贴跟随);搜索框展开时按钮让位
|
|
83
83
|
- 拖动定位与持久化:面板头部拖拽(pointer events,视口夹边),拖动或图钉切换 `docked` 状态;`place()` 对自由放置的面板只做视口夹边、不再自动停靠;位置、停靠状态与对齐选择存于 localStorage(`dsh-cnvnav:ui:v1`)
|
|
84
84
|
- 拖拽缩放:左下角透明热区捕获 pointer 事件;宽度直接写 `style.width`(240–560),高度只调 `maxHeight` 并保持 `height:auto`,使实际高度恒为 min(内容, 上限) 而不产生空白;拖动以起点为基准线性跟随(`base + 位移`),上限与 `place()` 的容器/视口钳制同源,松手零回弹;宽高随 `panelWidth`/`panelHeight` 持久化
|
|
85
85
|
- 样式:`Button`/`Tooltip`/搜索与关闭图标复用 `@deepseek-ai/dsh-client-ui-primitives`,其余图标为 Lucide 内联 SVG(ISC License);面板容器自建 `<style>` 注入,颜色使用 `--dsw-*` 主题 token;插件卸载时随 fiber 清理
|
|
86
86
|
|
|
87
87
|
## 兼容性说明
|
|
88
88
|
|
|
89
|
-
- 目标平台:DSH Web 端(`dsh.client.platform: web`),依赖内核 seed 的 `react`、`slots`、`sessions` 服务与 `@deepseek-ai/dsh-client-ui-primitives
|
|
90
|
-
-
|
|
89
|
+
- 目标平台:DSH Web 端(`dsh.client.platform: web`),依赖内核 seed 的 `react`、`slots`、`sessions` 服务与 `@deepseek-ai/dsh-client-ui-primitives`;`dsh.client.inject` 声明 `@deepseek-ai/dsh-client-ui-conversation`(`dsh-client-runtime` 已于官方 v0.1.2-rc.1 移除,不再注入)
|
|
90
|
+
- **双形态(0.2.6)**:运行时用 feature detection 区分宿主——存在 `uiConversation` 服务即新宿主(官方 ≥ 0.1.2-rc.1),不硬编码版本号。`uiConversation` 已加入客户端 `exports.inject` 声明,运行时保证其先就绪,宿主检测稳定不再随加载时序翻转。旧宿主保持全功能导航器(默认展开);新宿主与官方内置 TurnNavigator 共存:默认关闭 + 「极简·左」 + 搜索框默认收起,官方轨道负责跳转,本插件专注关键词搜索、批量「加载全部」与步次级徽标
|
|
91
|
+
- **宿主版本说明**:已在官方 **DSH v0.1.2-rc.1**(Web 端)完整验证——含新的 `uiConversation` 服务、`conversation.session.header.utilities` 空 props 契约、chat target 快照(`timeline`/`locations`/`nodes`/`navigation`/`legacy`)与 `turnOutline` 投影;旧宿主(无 `uiConversation`)经 `useSession` 路径兼容保留,未在 0.1.2-rc.1 之前的官方版本上逐一回归
|
|
92
|
+
- **宿主依赖声明**:`@deepseek-ai/dsh-client-ui-conversation` 以 peerDependencies + semver 范围声明(awesome-dsh-plugin 约定,dshmarket 的依赖检查据此展示宿主兼容性)
|
|
93
|
+
- **版本敏感点**:`[data-chat-anchor-key]` / `[data-conversation-scroll]` 是当前 DSH 聊天视图的 DOM 锚点约定(官方 v0.1.2-rc.1 中已验证仍在),DSH 升级后若锚点变化,只需调整 `lib/client.js` 中 `findAnchor` / `computeActiveKey` 两个函数
|
|
91
94
|
- 未声明 `timer` 硬依赖:客户端的 timer 服务存在则用于节流,不存在时自动退化为未节流模式
|
|
92
95
|
|
|
93
96
|
## 目录结构
|
package/lib/client.js
CHANGED
|
@@ -351,17 +351,33 @@ window.__ModuleLoader__.load({
|
|
|
351
351
|
if (slots === undefined) return;
|
|
352
352
|
const timer = ctx.get("timer");
|
|
353
353
|
const sessions = ctx.get("sessions");
|
|
354
|
+
/* 宿主形态检测:官方 v0.1.2-rc.1 起引入 uiConversation 服务并把会话节点
|
|
355
|
+
数据从 session 快照挪到 chat target 快照。用 feature detection,
|
|
356
|
+
不硬编码版本号。 */
|
|
357
|
+
const uiConversation = ctx.get("uiConversation");
|
|
358
|
+
const NEW_HOST = uiConversation !== undefined;
|
|
359
|
+
/* 运行时只保证 exports.inject 声明的服务先就绪; uiConversation 未声明时
|
|
360
|
+
会在"已物化/未物化"之间随机翻转(apply 竞态), 导致宿主检测时好时坏。
|
|
361
|
+
这里提供实时重查, 供效果/布局在运行时使用, 不缓存负结果。 */
|
|
362
|
+
const getUiConv = () => ctx.get("uiConversation");
|
|
363
|
+
const isNewHostNow = () => getUiConv() !== undefined;
|
|
354
364
|
|
|
355
365
|
/* own stylesheet with fiber-scoped cleanup */
|
|
356
366
|
const styleEl = document.createElement("style");
|
|
357
367
|
styleEl.setAttribute("data-dsh-cnvnav", "");
|
|
358
368
|
styleEl.textContent = CSS;
|
|
359
369
|
document.head.appendChild(styleEl);
|
|
360
|
-
ctx.effect(() => () =>
|
|
370
|
+
ctx.effect(() => () => {
|
|
371
|
+
styleEl.remove();
|
|
372
|
+
if (chatUnsub !== null) { chatUnsub(); chatUnsub = null; }
|
|
373
|
+
});
|
|
361
374
|
|
|
362
375
|
/* ---------- shared store (toggle button + overlay panel) ---------- */
|
|
363
|
-
|
|
364
|
-
|
|
376
|
+
/* 0.2.6: 键名升级 v2 —— 旧 v1 记录(0.2.5 时代)在新宿主下会以 viewMode:"full" 等旧值
|
|
377
|
+
覆盖新的 minimal-left 默认,导致"验收不过";升级后新宿主从干净默认开始 */
|
|
378
|
+
const UI_KEY = "dsh-cnvnav:ui:v2";
|
|
379
|
+
/* 极简·右(minimal)与新宿主官方 rail 视觉重叠,先注释掉保留代码,待有更好的共存方案再恢复 */
|
|
380
|
+
const VIEW_MODES = ["full", "hidden", /* "minimal", */ "minimal-left"];
|
|
365
381
|
function readUi() {
|
|
366
382
|
try {
|
|
367
383
|
const raw = window.localStorage.getItem(UI_KEY);
|
|
@@ -370,6 +386,8 @@ window.__ModuleLoader__.load({
|
|
|
370
386
|
const out = {};
|
|
371
387
|
if (VIEW_MODES.indexOf(o.viewMode) !== -1) out.viewMode = o.viewMode;
|
|
372
388
|
if (o.docked === true || o.docked === false) out.docked = o.docked;
|
|
389
|
+
/* open 不持久化: 新宿主每次启动都从默认(关闭)开始,
|
|
390
|
+
避免重启/正常启动时面板被旧状态默认展开 */
|
|
373
391
|
if (typeof o.panelWidth === "number" && o.panelWidth >= 240 && o.panelWidth <= 560) out.panelWidth = o.panelWidth;
|
|
374
392
|
if (typeof o.panelHeight === "number" && o.panelHeight >= 90 && o.panelHeight <= 2000) out.panelHeight = o.panelHeight;
|
|
375
393
|
if (o.userPos && typeof o.userPos.left === "number" && typeof o.userPos.top === "number"
|
|
@@ -384,15 +402,18 @@ window.__ModuleLoader__.load({
|
|
|
384
402
|
function persistUi() {
|
|
385
403
|
try {
|
|
386
404
|
window.localStorage.setItem(UI_KEY, JSON.stringify({
|
|
387
|
-
viewMode: state.viewMode, docked: state.docked, userPos: state.userPos, panelWidth: state.panelWidth, panelHeight: state.panelHeight,
|
|
405
|
+
viewMode: state.viewMode, docked: state.docked, userPos: state.userPos, panelWidth: state.panelWidth, panelHeight: state.panelHeight, open: state.open,
|
|
388
406
|
}));
|
|
389
407
|
} catch (err) { /* 隐私模式等场景忽略 */ }
|
|
390
408
|
}
|
|
391
409
|
const savedUi = readUi();
|
|
392
410
|
let state = Object.assign({
|
|
393
|
-
|
|
411
|
+
/* 双形态默认值:旧宿主保持全功能导航器(默认展开);
|
|
412
|
+
新宿主(官方内置 TurnNavigator 常显)默认关闭 + 极简·左,
|
|
413
|
+
与官方右侧 rail 零重叠,由用户主动呼出。 */
|
|
414
|
+
open: NEW_HOST ? false : true, sessionId: null, outline: null, activeKey: null, expanded: {},
|
|
394
415
|
hasMore: false, loadingOlder: false, loadingAll: false,
|
|
395
|
-
viewMode: "full", docked: true, userPos: null, panelWidth: 288, panelHeight: null,
|
|
416
|
+
viewMode: NEW_HOST ? "minimal-left" : "full", docked: true, userPos: null, panelWidth: 288, panelHeight: null,
|
|
396
417
|
}, savedUi);
|
|
397
418
|
const listeners = new Set();
|
|
398
419
|
function setState(patch) {
|
|
@@ -457,6 +478,12 @@ window.__ModuleLoader__.load({
|
|
|
457
478
|
function firstText(blocks) {
|
|
458
479
|
if (!Array.isArray(blocks)) return null;
|
|
459
480
|
for (const b of blocks) {
|
|
481
|
+
/* 官方 user.content 为嵌套数组(多 part),递归展平提取 */
|
|
482
|
+
if (Array.isArray(b)) {
|
|
483
|
+
const inner = firstText(b);
|
|
484
|
+
if (inner !== null) return inner;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
460
487
|
if (!b || typeof b !== "object") continue;
|
|
461
488
|
if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") return b.text;
|
|
462
489
|
}
|
|
@@ -467,11 +494,13 @@ window.__ModuleLoader__.load({
|
|
|
467
494
|
function dialogueText(node) {
|
|
468
495
|
const d = node && node.data;
|
|
469
496
|
if (!d || typeof d !== "object") return null;
|
|
470
|
-
if (node.kind === "user") return firstText(d.content);
|
|
497
|
+
if (node.kind === "user" || node.kind === "input-message") return firstText(d.content);
|
|
471
498
|
if (node.kind === "assistant-step") {
|
|
472
499
|
if (!Array.isArray(d.blocks)) return null;
|
|
473
500
|
const parts = [];
|
|
474
|
-
|
|
501
|
+
/* 官方 assistant.blocks 同样可能嵌套,先展平 */
|
|
502
|
+
const flat = d.blocks.flatMap((x) => Array.isArray(x) ? x : [x]);
|
|
503
|
+
for (const b of flat) {
|
|
475
504
|
if (!b || typeof b !== "object") continue;
|
|
476
505
|
if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
|
|
477
506
|
}
|
|
@@ -483,14 +512,16 @@ window.__ModuleLoader__.load({
|
|
|
483
512
|
function fullDialogueText(node) {
|
|
484
513
|
const d = node && node.data;
|
|
485
514
|
if (!d || typeof d !== "object") return null;
|
|
486
|
-
if (node.kind !== "user") return null;
|
|
515
|
+
if (node.kind !== "user" && node.kind !== "input-message") return null;
|
|
487
516
|
if (typeof d.content === "string") {
|
|
488
517
|
const t = d.content.trim();
|
|
489
518
|
return t === "" ? null : t;
|
|
490
519
|
}
|
|
491
520
|
if (!Array.isArray(d.content)) return null;
|
|
492
521
|
const parts = [];
|
|
493
|
-
|
|
522
|
+
/* 官方 user.content 为嵌套数组(多 part),先展平 */
|
|
523
|
+
const flat = d.content.flatMap((x) => Array.isArray(x) ? x : [x]);
|
|
524
|
+
for (const b of flat) {
|
|
494
525
|
if (!b || typeof b !== "object") continue;
|
|
495
526
|
if ((b.type === "text" || b.kind === "text") && typeof b.text === "string" && b.text.trim() !== "") parts.push(b.text.trim());
|
|
496
527
|
}
|
|
@@ -632,6 +663,220 @@ window.__ModuleLoader__.load({
|
|
|
632
663
|
return cacheValue;
|
|
633
664
|
}
|
|
634
665
|
|
|
666
|
+
/* ---------- 新宿主大纲:官方 v0.1.2-rc.1 起会话节点从 session 快照
|
|
667
|
+
挪到 chat target 快照(uiConversation.binding(...).target("chat"))。
|
|
668
|
+
走其 legacy 兼容投影(有序 nodes + turnEnds 轮次边界)派生大纲。 ---------- */
|
|
669
|
+
const KIND_META_NEW = {
|
|
670
|
+
"user": { label: "你", cls: "traj-user" },
|
|
671
|
+
"steering": { label: "插话", cls: "traj-user" },
|
|
672
|
+
"context": { label: "上下文", cls: "traj-context" },
|
|
673
|
+
"assistant-step": { label: "助手", cls: "traj-assistant" },
|
|
674
|
+
"tool-call": { label: "工具", cls: "traj-tool" },
|
|
675
|
+
"command": { label: "命令", cls: "other-command" },
|
|
676
|
+
"compaction": { label: "压缩", cls: "traj-compacted" },
|
|
677
|
+
"manual-compaction": { label: "压缩", cls: "traj-compacted" },
|
|
678
|
+
"model-retry": { label: "重试", cls: "other-retry" },
|
|
679
|
+
"turn-error": { label: "出错", cls: "other-error" },
|
|
680
|
+
"turn-max-tokens": { label: "截断", cls: "other-error" },
|
|
681
|
+
/* 官方 Chat 节点额外 renderer kind:
|
|
682
|
+
turn-process 回合过程折叠、turn-tail 轮尾、system-prompt 系统提示 */
|
|
683
|
+
"turn-process": { label: "过程", cls: "other-neutral" },
|
|
684
|
+
"turn-tail": { label: "轮尾", cls: "other-neutral" },
|
|
685
|
+
"system-prompt": { label: "系统提示", cls: "other-neutral" },
|
|
686
|
+
"unknown": { label: "其他", cls: "other-neutral" },
|
|
687
|
+
};
|
|
688
|
+
function previewForNew(node) {
|
|
689
|
+
const d = node && node.data;
|
|
690
|
+
if (!d || typeof d !== "object") return null;
|
|
691
|
+
switch (node.kind) {
|
|
692
|
+
case "user":
|
|
693
|
+
case "steering":
|
|
694
|
+
case "context":
|
|
695
|
+
return firstText(d.content);
|
|
696
|
+
case "assistant-step": {
|
|
697
|
+
const text = firstText(d.blocks);
|
|
698
|
+
if (text !== null) return text;
|
|
699
|
+
if (Array.isArray(d.blocks) && d.blocks.some((b) => b && b.kind === "tool-call")) return "调用工具";
|
|
700
|
+
return d.status === "running" ? "进行中…" : null;
|
|
701
|
+
}
|
|
702
|
+
case "tool-call": {
|
|
703
|
+
/* data = ToolChatData { root: RunningToolCall | ToolResultNode } */
|
|
704
|
+
const root = d.root;
|
|
705
|
+
if (!root || typeof root !== "object") return null;
|
|
706
|
+
if (root.isError === true) return "执行失败";
|
|
707
|
+
const name = typeof root.name === "string" && root.name !== "" ? root.name
|
|
708
|
+
: (root.call && typeof root.call.name === "string" ? root.call.name : null);
|
|
709
|
+
return name !== null ? name : "工具";
|
|
710
|
+
}
|
|
711
|
+
case "command":
|
|
712
|
+
return typeof d.name === "string" && d.name !== "" ? d.name + (typeof d.args === "string" && d.args !== "" ? " " + d.args : "") : "命令";
|
|
713
|
+
case "manual-compaction": {
|
|
714
|
+
const c = d.compaction;
|
|
715
|
+
if (c && typeof c.summary === "string" && c.summary !== "") return c.summary;
|
|
716
|
+
return "手动压缩";
|
|
717
|
+
}
|
|
718
|
+
case "compaction":
|
|
719
|
+
return typeof d.summary === "string" && d.summary !== "" ? d.summary : "上下文已压缩";
|
|
720
|
+
case "model-retry": {
|
|
721
|
+
const n = Array.isArray(d.attempts) ? d.attempts.length : 0;
|
|
722
|
+
return "自动重试" + (n > 0 ? " ×" + n : "…");
|
|
723
|
+
}
|
|
724
|
+
case "turn-error":
|
|
725
|
+
return typeof d.message === "string" && d.message !== "" ? d.message : "本轮出错";
|
|
726
|
+
case "turn-max-tokens":
|
|
727
|
+
return "达到输出上限";
|
|
728
|
+
case "turn-process": {
|
|
729
|
+
const n = typeof d.toolCallCount === "number" ? d.toolCallCount : 0;
|
|
730
|
+
const s = typeof d.subagentCount === "number" ? d.subagentCount : 0;
|
|
731
|
+
return "过程" + (n > 0 ? " · " + n + " 工具" : "") + (s > 0 ? " · " + s + " 子代理" : "");
|
|
732
|
+
}
|
|
733
|
+
case "turn-tail":
|
|
734
|
+
return null;
|
|
735
|
+
case "system-prompt":
|
|
736
|
+
return "系统提示";
|
|
737
|
+
case "unknown":
|
|
738
|
+
return typeof d.type === "string" ? "未知事件 " + d.type : "未知事件";
|
|
739
|
+
default:
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function deriveOutlineNew(snap) {
|
|
744
|
+
const empty = { groups: [], count: 0, byKey: {} };
|
|
745
|
+
if (snap === null || snap === undefined) return empty;
|
|
746
|
+
/* 官方 Chat 快照: order = 可见节点(按展示序), nodes = 键→节点存储。
|
|
747
|
+
只遍历 order 可见节点, 保证每个 item.key 都有对应 DOM 锚点
|
|
748
|
+
(data-chat-anchor-key = node.key)。被 turn-process 折叠隐藏的
|
|
749
|
+
"助手:调用工具" 等隐藏节点(无锚点)不再产生不可跳转的死行。 */
|
|
750
|
+
const order = snap.order;
|
|
751
|
+
const nodesMap = snap.nodes;
|
|
752
|
+
if (!Array.isArray(order) || nodesMap === undefined || nodesMap === null || typeof nodesMap.get !== "function") return empty;
|
|
753
|
+
function turnOfNode(node) {
|
|
754
|
+
const loc = node !== null && node !== undefined ? node.location : undefined;
|
|
755
|
+
if (loc === undefined || loc === null || typeof loc !== "object") return null;
|
|
756
|
+
if (loc.kind === "turn" || loc.kind === "step") {
|
|
757
|
+
const t = loc.turn;
|
|
758
|
+
return t !== undefined && t !== null && typeof t.turn === "number" ? t.turn : null;
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const groups = [];
|
|
763
|
+
const byKey = {};
|
|
764
|
+
let current = null;
|
|
765
|
+
let count = 0;
|
|
766
|
+
for (const key of order) {
|
|
767
|
+
const node = nodesMap.get(key);
|
|
768
|
+
if (node === undefined || node === null) continue;
|
|
769
|
+
const meta = KIND_META_NEW[node.kind] || KIND_META_NEW.unknown;
|
|
770
|
+
const item = {
|
|
771
|
+
key: String(node.key !== undefined && node.key !== null ? node.key : node.anchorSeq),
|
|
772
|
+
kind: node.kind,
|
|
773
|
+
label: meta.label,
|
|
774
|
+
cls: meta.cls,
|
|
775
|
+
preview: previewForNew(node),
|
|
776
|
+
search: dialogueText(node),
|
|
777
|
+
full: fullDialogueText(node),
|
|
778
|
+
time: fmtTime(timeOf(node)),
|
|
779
|
+
};
|
|
780
|
+
const turn = turnOfNode(node);
|
|
781
|
+
if (turn === null) {
|
|
782
|
+
groups.push({ turn: null, items: [item], gid: "x" + groups.length });
|
|
783
|
+
count += 1;
|
|
784
|
+
current = null;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (current === null || current.turn !== turn) {
|
|
788
|
+
current = { turn: turn, items: [], gid: "t" + turn };
|
|
789
|
+
groups.push(current);
|
|
790
|
+
}
|
|
791
|
+
current.items.push(item);
|
|
792
|
+
count += 1;
|
|
793
|
+
}
|
|
794
|
+
for (const g of groups) {
|
|
795
|
+
const first = g.items[0];
|
|
796
|
+
if (g.turn === null) {
|
|
797
|
+
g.title = first.label;
|
|
798
|
+
g.subtitle = truncate(first.preview || "", 60);
|
|
799
|
+
g.headKey = first.key;
|
|
800
|
+
g.headIsUser = false;
|
|
801
|
+
} else {
|
|
802
|
+
g.title = "第 " + g.turn + " 轮";
|
|
803
|
+
const head = first.kind === "user" ? first : null;
|
|
804
|
+
g.headKey = head ? head.key : first.key;
|
|
805
|
+
g.headIsUser = head !== null;
|
|
806
|
+
g.subtitle = truncate((head ? head.preview : first.preview) || "", 60);
|
|
807
|
+
}
|
|
808
|
+
for (const it of g.items) byKey[it.key] = g.gid;
|
|
809
|
+
}
|
|
810
|
+
return { groups: groups, count: count, byKey: byKey };
|
|
811
|
+
}
|
|
812
|
+
/* 投影合并:官方 turnOutline 投影(全会话 {turn, seq, prompt, response} 摘要),
|
|
813
|
+
与官方 rail 的 mergeTurnRailItems 同源。用它为缺失用户消息的轮次补一个
|
|
814
|
+
合成"你"条目, 并为加载窗口外的轮次生成纯投影分组。 */
|
|
815
|
+
function mergeOutlineWithProjection(outline, proj) {
|
|
816
|
+
if (outline === null || outline === undefined) return null;
|
|
817
|
+
if (!Array.isArray(proj) || proj.length === 0) return null;
|
|
818
|
+
const byTurn = {};
|
|
819
|
+
for (const e of proj) {
|
|
820
|
+
if (e === null || typeof e !== "object") continue;
|
|
821
|
+
if (typeof e.turn !== "number" || !Number.isSafeInteger(e.turn)) continue;
|
|
822
|
+
byTurn[e.turn] = e;
|
|
823
|
+
}
|
|
824
|
+
const groups = [];
|
|
825
|
+
const byKey = {};
|
|
826
|
+
let count = 0;
|
|
827
|
+
let changed = false;
|
|
828
|
+
for (const g of outline.groups) {
|
|
829
|
+
const items = g.items.slice();
|
|
830
|
+
const projE = byTurn[g.turn];
|
|
831
|
+
const headIsUser = items.length > 0 && (items[0].kind === "user" || items[0].kind === "input-message");
|
|
832
|
+
if (projE !== undefined && typeof projE.prompt === "string" && projE.prompt !== "" && !headIsUser) {
|
|
833
|
+
items.unshift({
|
|
834
|
+
key: "proj-" + g.turn + "-user",
|
|
835
|
+
kind: "user", label: "你", cls: "traj-user",
|
|
836
|
+
preview: truncate(projE.prompt, 120),
|
|
837
|
+
search: projE.prompt,
|
|
838
|
+
full: projE.prompt,
|
|
839
|
+
time: null, seq: null, synthetic: true,
|
|
840
|
+
});
|
|
841
|
+
changed = true;
|
|
842
|
+
}
|
|
843
|
+
const first = items[0];
|
|
844
|
+
const ng = {
|
|
845
|
+
turn: g.turn, items: items, gid: g.gid,
|
|
846
|
+
title: "第 " + g.turn + " 轮",
|
|
847
|
+
headKey: first !== undefined ? first.key : g.headKey,
|
|
848
|
+
headIsUser: first !== undefined && (first.kind === "user" || first.kind === "input-message"),
|
|
849
|
+
subtitle: truncate((first ? first.preview : null) || "", 60),
|
|
850
|
+
};
|
|
851
|
+
groups.push(ng);
|
|
852
|
+
count += ng.items.length;
|
|
853
|
+
}
|
|
854
|
+
for (const numStr of Object.keys(byTurn)) {
|
|
855
|
+
const num = Number(numStr);
|
|
856
|
+
if (groups.some((g) => g.turn === num)) continue;
|
|
857
|
+
const projE = byTurn[num];
|
|
858
|
+
const items = [];
|
|
859
|
+
if (typeof projE.prompt === "string" && projE.prompt !== "") {
|
|
860
|
+
items.push({ key: "proj-" + num + "-user", kind: "user", label: "你", cls: "traj-user", preview: truncate(projE.prompt, 120), search: projE.prompt, full: projE.prompt, time: null, seq: null, synthetic: true });
|
|
861
|
+
}
|
|
862
|
+
if (typeof projE.response === "string" && projE.response !== "") {
|
|
863
|
+
items.push({ key: "proj-" + num + "-resp", kind: "assistant", label: "助手", cls: "traj-assistant", preview: truncate(projE.response, 120), search: projE.response, full: projE.response, time: null, seq: null, synthetic: true });
|
|
864
|
+
}
|
|
865
|
+
if (items.length === 0) continue;
|
|
866
|
+
const first = items[0];
|
|
867
|
+
groups.push({ turn: num, items: items, gid: "t" + num, title: "第 " + num + " 轮", headKey: first.key, headIsUser: true, subtitle: truncate(first.preview || "", 60) });
|
|
868
|
+
count += items.length;
|
|
869
|
+
changed = true;
|
|
870
|
+
}
|
|
871
|
+
if (!changed) return null;
|
|
872
|
+
groups.sort((a, b) => a.turn - b.turn);
|
|
873
|
+
for (const g of groups) for (const it of g.items) byKey[it.key] = g.gid;
|
|
874
|
+
return { groups: groups, count: count, byKey: byKey };
|
|
875
|
+
}
|
|
876
|
+
/* 新宿主 chat target 订阅:随 loadOlder 等更新自动重派生大纲 */
|
|
877
|
+
let chatUnsub = null;
|
|
878
|
+
let projUnsub = null;
|
|
879
|
+
|
|
635
880
|
/* ---------- DOM helpers: jump + position tracking ---------- */
|
|
636
881
|
function findAnchor(key) {
|
|
637
882
|
const rows = document.querySelectorAll("[data-chat-anchor-key]");
|
|
@@ -686,9 +931,104 @@ window.__ModuleLoader__.load({
|
|
|
686
931
|
const outline = props.useSession(selectOutline);
|
|
687
932
|
const hasMore = props.useSession((s) => s.hasMore);
|
|
688
933
|
const loadingOlder = props.useSession((s) => s.loadingOlder);
|
|
934
|
+
/* 投影通道 B:slot 标准 props 的 useProjection(官方 rail 同款) */
|
|
935
|
+
const turnOutlineProj = typeof props.useProjection === "function" ? props.useProjection("turnOutline") : undefined;
|
|
689
936
|
React.useEffect(() => {
|
|
690
|
-
|
|
937
|
+
if (!NEW_HOST || turnOutlineProj === undefined || !Array.isArray(turnOutlineProj) || turnOutlineProj.length === 0) return;
|
|
938
|
+
try {
|
|
939
|
+
const merged = mergeOutlineWithProjection(state.outline, turnOutlineProj);
|
|
940
|
+
if (merged !== null) setState({ outline: merged });
|
|
941
|
+
} catch (err) {
|
|
942
|
+
console.error("[cnvnav] projection(use) merge failed", err);
|
|
943
|
+
}
|
|
944
|
+
}, [turnOutlineProj]);
|
|
945
|
+
React.useEffect(() => {
|
|
946
|
+
/* 旧宿主:大纲来自 useSession 的 chat 快照;新宿主:大纲由下方
|
|
947
|
+
chat target 订阅独立驱动,此处只同步会话级字段。 */
|
|
948
|
+
setState(Object.assign({
|
|
949
|
+
sessionId: props.sessionId, hasMore: hasMore, loadingOlder: loadingOlder,
|
|
950
|
+
}, NEW_HOST ? {} : { outline: outline }));
|
|
691
951
|
}, [outline, props.sessionId, hasMore, loadingOlder]);
|
|
952
|
+
React.useEffect(() => {
|
|
953
|
+
/* 实时重查宿主(apply 时 uiConversation 可能尚未物化, 不依赖 NEW_HOST 常量) */
|
|
954
|
+
if (getUiConv() === undefined) {
|
|
955
|
+
/* 旧宿主:大纲由上方 effect 的 props.useSession 派生, 无需此处订阅 */
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (chatUnsub !== null) { chatUnsub(); chatUnsub = null; }
|
|
959
|
+
if (projUnsub !== null) { projUnsub(); projUnsub = null; }
|
|
960
|
+
/* 会话恢复的时序竞争修复:binding/target 未物化时不再静默放弃,
|
|
961
|
+
订阅 sessions.list + 定时兜底自动重试(重启后导航空的根因) */
|
|
962
|
+
const trySubscribe = () => {
|
|
963
|
+
if (props.sessionId == null || sessions === undefined) {
|
|
964
|
+
setState({ outline: null });
|
|
965
|
+
return true; /* 无会话:状态已处理,无需重试 */
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const binding = sessions.binding(props.sessionId);
|
|
969
|
+
if (binding === undefined) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
const uiConv = getUiConv(); /* 兜底重查, 不依赖 apply 时刻的快照 */
|
|
973
|
+
if (uiConv === undefined) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
const target = uiConv.binding(binding).target("chat");
|
|
977
|
+
if (target === undefined) {
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
/* 投影兜底:官方 turnOutline 投影(全会话轮次摘要, 含加载窗口外轮次)
|
|
981
|
+
与官方 rail 的 mergeTurnRailItems 同源 —— 用它补上被组装器
|
|
982
|
+
漏掉的用户消息与窗口外轮次。 */
|
|
983
|
+
const projStore = sessions.projectionStore !== undefined && typeof sessions.projectionStore === "function"
|
|
984
|
+
? sessions.projectionStore(props.sessionId) : undefined;
|
|
985
|
+
const mergeProj = () => {
|
|
986
|
+
try {
|
|
987
|
+
const proj = projStore !== undefined ? projStore.get("turnOutline") : undefined;
|
|
988
|
+
const merged = mergeOutlineWithProjection(state.outline, proj);
|
|
989
|
+
if (merged !== null) setState({ outline: merged });
|
|
990
|
+
} catch (err) {
|
|
991
|
+
console.error("[cnvnav] projection merge failed", err);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const update = () => {
|
|
995
|
+
try {
|
|
996
|
+
const snap = target.getSnapshot();
|
|
997
|
+
const outline = deriveOutlineNew(snap);
|
|
998
|
+
setState({ outline: outline });
|
|
999
|
+
mergeProj();
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
console.error("[cnvnav] derive failed", err);
|
|
1002
|
+
setState({ outline: null });
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
chatUnsub = target.subscribe(update);
|
|
1006
|
+
update();
|
|
1007
|
+
if (projStore !== undefined) projUnsub = projStore.subscribeAny(mergeProj);
|
|
1008
|
+
mergeProj();
|
|
1009
|
+
return true;
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
console.error("[cnvnav] subscribe failed", err);
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
if (trySubscribe()) return;
|
|
1016
|
+
let dispose = null;
|
|
1017
|
+
let timer = null;
|
|
1018
|
+
let stopped = false;
|
|
1019
|
+
const retry = () => {
|
|
1020
|
+
if (stopped || chatUnsub !== null) return;
|
|
1021
|
+
if (trySubscribe()) cleanup();
|
|
1022
|
+
};
|
|
1023
|
+
const cleanup = () => {
|
|
1024
|
+
stopped = true;
|
|
1025
|
+
if (dispose !== null) { dispose(); dispose = null; }
|
|
1026
|
+
if (timer !== null) { window.clearInterval(timer); timer = null; }
|
|
1027
|
+
};
|
|
1028
|
+
dispose = sessions.list.subscribe(retry);
|
|
1029
|
+
timer = window.setInterval(retry, 400);
|
|
1030
|
+
return cleanup;
|
|
1031
|
+
}, [props.sessionId]);
|
|
692
1032
|
React.useEffect(() => {
|
|
693
1033
|
if (state.sessionId !== null && state.sessionId !== props.sessionId) {
|
|
694
1034
|
lastActive = null;
|
|
@@ -716,14 +1056,15 @@ window.__ModuleLoader__.load({
|
|
|
716
1056
|
function Panel() {
|
|
717
1057
|
const snap = useStore();
|
|
718
1058
|
const [filter, setFilter] = React.useState("");
|
|
1059
|
+
/* 新宿主搜索优先:面板打开时搜索框常驻;旧宿主保持隐藏搜索图标 */
|
|
719
1060
|
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
720
|
-
const viewMode = snap.viewMode; /* full | hidden | minimal
|
|
1061
|
+
const viewMode = snap.viewMode; /* full | hidden | minimal-left (极简·右已注释) */
|
|
721
1062
|
const turnsHidden = viewMode !== "full";
|
|
722
1063
|
function nextModeLabel() {
|
|
723
|
-
return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "
|
|
1064
|
+
return viewMode === "full" ? "隐藏轮次" : viewMode === "hidden" ? "极简·左" : "显示轮次";
|
|
724
1065
|
}
|
|
725
1066
|
function cycleMode() {
|
|
726
|
-
const next = viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal
|
|
1067
|
+
const next = viewMode === "full" ? "hidden" : viewMode === "hidden" ? "minimal-left" : "full";
|
|
727
1068
|
setState({ viewMode: next });
|
|
728
1069
|
persistUi();
|
|
729
1070
|
}
|
|
@@ -859,7 +1200,10 @@ window.__ModuleLoader__.load({
|
|
|
859
1200
|
if (dragMove !== null) return true;
|
|
860
1201
|
const vw = document.documentElement.clientWidth || 1000;
|
|
861
1202
|
const vh = document.documentElement.clientHeight || 800;
|
|
862
|
-
|
|
1203
|
+
/* 新宿主 dock 右侧需让出官方 rail 的 28px 竖带(right: calc(12px - clearance) + width:28px),
|
|
1204
|
+
留 40px 总避让;旧宿主维持 12px。运行时实时重查, 不依赖 apply 时刻的检测结果。 */
|
|
1205
|
+
const railClearance = isNewHostNow() ? 40 : 12;
|
|
1206
|
+
const mini = /* viewMode === "minimal" || */ viewMode === "minimal-left";
|
|
863
1207
|
const alignLeft = viewMode === "minimal-left";
|
|
864
1208
|
const width = (state.panelWidth || 288);
|
|
865
1209
|
/* 面板宽度对非极简模式统一生效, 确保拖动(未停靠)状态下也能实时刷新 */
|
|
@@ -904,7 +1248,7 @@ window.__ModuleLoader__.load({
|
|
|
904
1248
|
panel.style.top = Math.max(8, Math.round((vh - MINI_H) / 2)) + "px";
|
|
905
1249
|
panel.style.maxHeight = MINI_H + "px";
|
|
906
1250
|
} else {
|
|
907
|
-
panel.style.left = Math.max(8, vw - width -
|
|
1251
|
+
panel.style.left = Math.max(8, vw - width - railClearance) + "px";
|
|
908
1252
|
panel.style.top = Math.max(8, 60) + "px";
|
|
909
1253
|
const fh = state.panelHeight;
|
|
910
1254
|
const cap = Math.round(vh - 90);
|
|
@@ -939,7 +1283,7 @@ window.__ModuleLoader__.load({
|
|
|
939
1283
|
panel.style.top = Math.max(8, Math.round(r.top + (r.height - MINI_H) / 2)) + "px";
|
|
940
1284
|
panel.style.maxHeight = MINI_H + "px";
|
|
941
1285
|
} else {
|
|
942
|
-
panel.style.left = Math.max(8, vw - width -
|
|
1286
|
+
panel.style.left = Math.max(8, vw - width - railClearance) + "px";
|
|
943
1287
|
panel.style.top = Math.max(8, Math.round(r.top + 8)) + "px";
|
|
944
1288
|
const fh = state.panelHeight;
|
|
945
1289
|
const cap = Math.max(minPanelH(), Math.round(Math.min(r.height - 24, vh - 40)));
|
|
@@ -1020,8 +1364,8 @@ window.__ModuleLoader__.load({
|
|
|
1020
1364
|
const count = snap.outline !== null ? snap.outline.count : 0;
|
|
1021
1365
|
const hasExpanded = Object.keys(snap.expanded).some((k) => snap.expanded[k] === true);
|
|
1022
1366
|
|
|
1023
|
-
/* ---------- 极简模式(
|
|
1024
|
-
if (viewMode === "minimal" || viewMode === "minimal-left") {
|
|
1367
|
+
/* ---------- 极简模式(左对齐): 只保留定位 (极简·右已注释,待共存方案) ---------- */
|
|
1368
|
+
if (/* viewMode === "minimal" || */ viewMode === "minimal-left") {
|
|
1025
1369
|
const miniRows = [];
|
|
1026
1370
|
for (const g of groups) {
|
|
1027
1371
|
if (g.turn === null) continue;
|
|
@@ -1159,7 +1503,9 @@ window.__ModuleLoader__.load({
|
|
|
1159
1503
|
} else if (!turnsHidden) {
|
|
1160
1504
|
titleNode = React.createElement("span", { className: "cnvnav-group-title" }, g.title);
|
|
1161
1505
|
} else if (g.items.length > 0) {
|
|
1162
|
-
|
|
1506
|
+
/* 组头徽标优先取用户节点(官方节点顺序里 turn-process 常在 user 之前,
|
|
1507
|
+
直接取 items[0] 会显示「过程」而非「用户」) */
|
|
1508
|
+
const headItem = g.items.find((it) => it.kind === "user" || it.kind === "input-message") ?? g.items[0];
|
|
1163
1509
|
titleNode = React.createElement("span", {
|
|
1164
1510
|
className: "cnvnav-badge cnvnav-badge-" + headItem.cls,
|
|
1165
1511
|
}, headItem.kind === "user" ? "用户" : headItem.label);
|
|
@@ -1475,7 +1821,7 @@ window.__ModuleLoader__.load({
|
|
|
1475
1821
|
));
|
|
1476
1822
|
}
|
|
1477
1823
|
|
|
1478
|
-
const inject = ["slots", "sessions"];
|
|
1824
|
+
const inject = ["slots", "sessions", "uiConversation"];
|
|
1479
1825
|
exports.apply = apply;
|
|
1480
1826
|
exports.inject = inject;
|
|
1481
1827
|
return module.exports;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-conversation-navigator",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "DSH(DeepSeek Harness)会话导航插件:按轮折叠的对话大纲面板,支持节点点击跳转、滚动位置高亮与轨迹视图同款配色。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"engines": {
|
|
33
33
|
"node": ">=18"
|
|
34
34
|
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.1-rc.2 <0.2.0"
|
|
37
|
+
},
|
|
35
38
|
"dsh": {
|
|
36
39
|
"bundle": {
|
|
37
40
|
"patch": "./cordis.patch.yml"
|
|
@@ -39,7 +42,6 @@
|
|
|
39
42
|
"client": {
|
|
40
43
|
"platform": "web",
|
|
41
44
|
"inject": [
|
|
42
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
43
45
|
"@deepseek-ai/dsh-client-ui-conversation"
|
|
44
46
|
]
|
|
45
47
|
}
|