@max-null/dsh-chat-rail 0.6.1 → 0.6.3

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.md CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
5
  This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
6
6
 
7
- 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover 展开画卷查看消息列表,点击跳转任意历史消息。
7
+ 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover 展开画卷查看消息列表,点击跳转任意历史消息;消息 hover tip 内嵌「提问&回答」直达区,收藏 + 一键回填输入框。
8
8
 
9
9
  > 不同于市面上"面板弹出式"的导航插件,dsh-chat-rail 采用**画卷式展开**:收起时是一条干净的竖条,hover 时从右往左平滑展开,像打开画卷一样。
10
10
 
11
11
  ## 截图
12
12
 
13
- | 画卷展开(收藏过滤 + 收藏导航条) | 消息操作按钮(收藏 ★ / 填充 ➕ / 复制) |
13
+ | 画卷展开:只显示收藏(通栏 header + 黄色收藏条) | 消息操作按钮(收藏 ★ / 填充 ➕ / 复制)+ 折叠态导航条 |
14
14
  |---|---|
15
15
  | ![rail-fav-1](docs/shots/rail-fav-1.png) | ![rail-fav-2](docs/shots/rail-fav-2.png) |
16
16
 
@@ -20,24 +20,28 @@ This plugin belongs to the **`@max-null/*` family** — a set of plugins that to
20
20
 
21
21
  | 能力 | 市面同类 | dsh-chat-rail |
22
22
  |---|---|---|
23
- | **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅ **跟随 CSS 变量动画同步避让**:`--dsh-sidebar-width` / `--dsh-sidebar-height`,与面板共享同一 transition,右栏/底栏展开收起时导航无缝跟随,互不遮挡 |
23
+ | **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅ **锚定会话区右列 + 底栏变量避让**:导航条实时跟随会话列右缘,官方右侧边栏 push 展开时会话列左移、导航**同步左移**(共享同一段过渡动画),不再被压在面板下面;底栏展开收起按 `--dsh-sidebar-height` 垂直避让,互不遮挡 |
24
+ | **空间不足** | ❌ 依然悬浮,遮挡会话 | ✅ **自动退场**:会话区剩余宽度不足(<260px,如窄屏右侧边栏完全遮住会话)时导航条整体淡出,空间恢复即回来 |
24
25
  | **展开形态** | ❌ 面板突兀弹出,高度错位 | ✅ **画卷式展开**:单容器从右往左平滑展开,高度天然一致 |
25
26
  | **阅读位置跟随** | 高亮点会跑出显示区 | ✅ **导航条自动滚动,高亮点持续居中**:无论会话滚到哪里,当前消息的指示点始终在导航可视区中央 |
26
27
  | **切换会话定位** | 回到顶部 | ✅ 自动定位到新会话当前阅读位置 |
27
- | **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title 干扰 |
28
+ | **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title 干扰;含图消息在文本上方显示缩略图,tip 底部还有**「提问&回答」直达区** |
28
29
  | **消息信息** | 只有预览文本 | ✅ **编号 + 相对时间 + 文本三列对齐**:编号固定宽、时间靠右,文本区域最大化且整齐 |
29
30
  | **长会话跳转** | 干等卡死 | ✅ **加载反馈**:跳转远距离消息时显示 spinner + "加载中",收起态只留图标 |
30
- | **消息收藏** | ❌ 大多数只能看不能存 | ✅ **收藏 + 快速填充**:消息行星标收藏(黄色导航条联动)、只显示收藏过滤、加号一键全文+附件回填输入框 |
31
+ | **消息收藏** | ❌ 大多数只能看不能存 | ✅ **收藏 + 快速填充**:消息行星标收藏(黄色导航条联动)、「只显示收藏」为面板**通栏 header**(折叠/展开同构、贴顶不遮条目)、加号一键全文+附件回填输入框 |
31
32
  | **主题适配** | 部分 | ✅ 全部使用 DSH 主题变量(`--dsw-alias-*`),浅深色无缝适配 |
32
- | **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH 环境开箱即用,固定右缘 + 垂直居中 |
33
+ | **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH 环境开箱即用,锚定会话区右缘 + 垂直居中 |
33
34
 
34
35
  ## 兼容性(重点)
35
36
 
36
- - **与 dsh-better-sidebar 深度兼容**:导航栏的右缘和垂直位置跟随 better-sidebar 的 `--dsh-sidebar-width` / `--dsh-sidebar-height` CSS 变量,右栏和底栏展开/收起时导航与面板**共享同一段 transition 动画同步移动**,永不错位、永不遮挡(包括底栏右上角的收起按钮)。
37
- - **无 better-sidebar 也能用**:变量缺失时走 CSS fallback(右缘 12px + 垂直居中),纯 DSH 环境开箱即用。
38
- - **与 DSH 自带轮次导航协同**:DSH `0.1.2-alpha.1+` 自带 TurnNavigator(轮次导航竖轨)。安装本插件后自动隐藏官方竖轨(aria-label 双文案锚点,不依赖构建 hash),由本插件统一承担会话导航;未安装本插件时官方导航照常显示。
37
+ - **与 dsh-better-sidebar 深度兼容**:底栏展开/收起时导航按 `--dsh-sidebar-height` 垂直避让,与面板**共享同一段 transition 动画同步移动**,永不错位(包括底栏右上角的收起按钮)。
38
+ - **右缘锚定会话区右列**:导航条实时测量会话列右缘定位——官方右侧边栏默认 push 展开时会话列左移,导航**同步左移**(同段过渡动画),不再被压在文件面板下面;不再依赖恒为 0 的 `--dsh-sidebar-width`。
39
+ - **空间不足自动退场**:会话区剩余宽度 < 260px 时(如窄屏右侧边栏完全遮住会话),导航条整体淡出,空间恢复即回来。
40
+ - **会话区滚动条隐藏**:会话区(`[data-conversation-scroll]`)自带的 Y 轴滚动条被本插件画成透明——它与导航条的位置指示功能重复,官方内核 `0.1.5` 起又把 bar 从「紧贴内容边缘」改为「离边缘 2px」,观感上更显眼。只覆盖 thumb 颜色、**保留官方 8px 槽位**(`scrollbar-gutter: stable` 是官方为输入卡跨视图不位移而保留的),滚轮/触控板滚动不受影响。
41
+ - **无 better-sidebar 也能用**:变量缺失时走 CSS fallback(垂直居中 + 视口右缘),纯 DSH 环境开箱即用。
42
+ - **与 DSH 自带轮次导航协同**:DSH `0.1.2-alpha.1+` 自带 TurnNavigator(轮次导航竖轨)。安装本插件后自动隐藏官方竖轨(aria-label 双文案锚点,不依赖构建 hash),由本插件统一承担会话导航;未安装本插件时官方导航照常显示。设置卡片另提供**「使用官方轮次导航条(对比模式)」**开关,可随时切回官方竖轨对比(免重启生效)。
39
43
  - **深历史跳转(DSH `0.1.2-alpha.3+` 内核)**:会话历史按窗口分页加载(`hasMore`)时,点击跳转走官方 `loadThrough(seq)` 精确跳转装载器(200 条/页一次到位),替代旧版逐页 `loadOlder`(50 条/页)循环;旧内核无 `loadThrough` 时自动回退逐页循环。
40
- - 需要 DSH ≥ `0.1.0-rc.6`;完整适配 DSH `0.1.2-alpha.4`。
44
+ - 需要 DSH ≥ `0.1.0-rc.6`;随 SSiD v0.3.0(内核 `0.1.5-rc.2`)完整适配验证。
41
45
 
42
46
  ## 安装
43
47
 
@@ -55,27 +59,29 @@ dsh plugin --profile <name> add @max-null/dsh-chat-rail
55
59
  ## 交互
56
60
 
57
61
  - **hover 导航条**:画卷展开,显示编号 / 相对时间 / 消息预览
58
- - **hover 消息项**:左侧弹出完整内容 tip 面板(含图消息在文本上方显示图片缩略图)
62
+ - **hover 消息项**:左侧弹出完整内容 tip 面板(含图消息在文本上方显示图片缩略图;tip 底部**「提问&回答」区**列出该消息内的提问——SVG 图标 + 单行问题,点击直达提问/回答位置)
59
63
  - **点击消息项**:跳转到会话中对应消息(未加载的历史自动补载;DSH `0.1.2-alpha.3+` 走官方 `loadThrough` 精确跳转,旧内核回退逐页循环)
60
64
  - **滚动会话**:高亮点跟随,导航条自动滚动保持居中
61
65
  - **含图消息**:展开态显示柔和图片角标(收起态不显示,保持竖条干净)
62
66
 
63
- ### 收藏与快速填充(v0.4.0)
67
+ ### 收藏与快速填充
64
68
 
65
69
  - **消息操作按钮**:每条用户消息的复制按钮左侧新增两个按钮
66
70
  - **星 ★**:收藏/取消收藏该消息(收藏后变成黄色实心星);收藏状态按会话持久化(localStorage),刷新与重启后保留
67
71
  - **加号 +**:一键把该消息**全文 + 附件**填入输入框(草稿设为消息全文,历史图片经官方附件通道回填入输入框)
68
72
  - **导航条联动**:收藏的消息指示条显示为**黄色**;展开态在标题前显示小星标
69
- - **只显示收藏**:导航条顶部新增星形过滤开关,点击点亮(黄色实心)后导航栏只显示已收藏的消息;再点恢复全部
73
+ - **只显示收藏**:过滤开关是导航面板的**通栏 header**(星标 + 文案 + 底部分隔线,折叠/展开同构、贴顶),点击点亮(黄色实心)后导航栏只显示已收藏的消息;再点恢复全部。列表在 header 之下独立滚动,开关不压住条目;无收藏时不显示该开关,过滤中取消最后一条收藏会自动退出过滤
70
74
 
71
75
  ## 架构
72
76
 
73
77
  - **host 半端**(`lib/index.mjs`):注册 `chatRail` 会话投影——从 session log 折叠用户消息锚点 `{ seq, time, text, id }`(排除插件/工具注入的上下文行)
74
- - **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body
78
+ - **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body;右缘实时测量会话列定位(ResizeObserver + 兜底采样),侧边栏 push 展开时跟随移动,空间不足时淡出
75
79
  - 数据源优先序:`chatRail` 投影 → 已加载聊天节点 → 后台 loadOlder 循环
76
80
 
77
81
  ## SSID 系列
78
82
 
83
+ 本插件是 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 全家桶的一员;也可以单独安装到任意 DSH profile——除设置卡片外不依赖其它同系列插件(右侧边栏避让、导航条定位都走官方 DOM 契约,缺失时自动降级)。
84
+
79
85
  ## 开发
80
86
 
81
87
  ```bash
Binary file
Binary file
package/lib/client.js CHANGED
@@ -15,12 +15,11 @@ window.__ModuleLoader__.load({
15
15
  *
16
16
  * Two deliberate fixes over the dsh-chat-timeline reference:
17
17
  *
18
- * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout
19
- * push through the `--dsh-sidebar-width` CSS variable on `:root` (the
20
- * `#root` margin-right transition reads the same variable). The rail's
21
- * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same
22
- * transition timing, so rail and scrollport move together while the panel
23
- * expands — no "panel first, scrollbar later" lag.
18
+ * 1. POSITION ANCHOR. The rail's right edge follows the conversation column
19
+ * (`[data-conversation-scroll]`), not the viewport: the official right
20
+ * sidebar pushes the conversation narrower, and the rail must travel with
21
+ * it. `--dsh-sidebar-width` reads 0px on the current kernel, so a CSS-only
22
+ * offset would pin the rail underneath that sidebar.
24
23
  *
25
24
  * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)
26
25
  * lines — nearly invisible on a light background. Here the rail keeps a
@@ -94,18 +93,21 @@ window.__ModuleLoader__.load({
94
93
  return `${d.getMonth() + 1}/${d.getDate()}`;
95
94
  }
96
95
  const css = [
97
- ".crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 3px);top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:10px 0;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),top var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),background .2s ease,border-color .2s ease,box-shadow .2s ease}",
96
+ ".crl_nav{user-select:none;z-index:100;position:fixed;right:3px;top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:0 0 10px;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right .25s cubic-bezier(.4,0,.2,1),top .25s cubic-bezier(.4,0,.2,1),background .2s ease,border-color .2s ease,box-shadow .2s ease}",
97
+ ".crl_list{display:flex;flex-direction:column;align-items:center;width:100%;min-height:0;flex:1 1 auto;position:relative;overflow-y:hidden;overflow-x:hidden;scrollbar-width:none}",
98
98
  "body[data-ds-dark-theme] .crl_nav,[data-theme='dark'] .crl_nav,.dark .crl_nav{background:rgba(28,28,32,.6);border-color:rgba(255,255,255,.09)}",
99
- ".crl_nav.crl_show{width:280px;overflow-y:auto;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}",
99
+ ".crl_nav.crl_show{width:280px;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}",
100
100
  "body[data-ds-dark-theme] .crl_nav.crl_show,[data-theme='dark'] .crl_nav.crl_show,.dark .crl_nav.crl_show{background:rgba(28,28,32,.96);border-color:rgba(255,255,255,.09);box-shadow:0 10px 30px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.28)}",
101
- ".crl_nav::-webkit-scrollbar{width:0;height:0}",
101
+ ".crl_nav::-webkit-scrollbar,.crl_list::-webkit-scrollbar{width:0;height:0}",
102
+ "[data-conversation-scroll]::-webkit-scrollbar-thumb,[data-conversation-scroll]::-webkit-scrollbar-thumb:hover{background:transparent}",
103
+ ".crl_show .crl_list{overflow-y:auto;align-items:stretch}",
102
104
  ".crl_loading{position:sticky;top:0;z-index:2;flex-shrink:0;width:100%;padding:4px 0;font-size:10px;line-height:1;text-align:center;color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.5)));background:inherit;pointer-events:none}",
103
105
  ".crl_loading::before{content:\"\";display:inline-block;width:8px;height:8px;margin-right:4px;border:1.5px solid var(--dsw-alias-state-business-primary,#4d6bfe);border-top-color:transparent;border-radius:50%;vertical-align:-1px;animation:crl-spin .8s linear infinite}",
104
106
  ".crl_loading .crl_loadingLabel{display:none}",
105
107
  ".crl_show .crl_loading .crl_loadingLabel{display:inline}",
106
108
  "@keyframes crl-spin{to{transform:rotate(360deg)}}",
107
- ".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:flex-end;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}",
108
- ".crl_show .crl_item{padding:0 14px}",
109
+ ".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:center;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}",
110
+ ".crl_show .crl_item{justify-content:flex-end;padding:0 14px}",
109
111
  ".crl_item:hover{color:rgba(0,0,0,.95)}",
110
112
  ".crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
111
113
  "body[data-ds-dark-theme] .crl_item,[data-theme='dark'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}",
@@ -131,7 +133,12 @@ window.__ModuleLoader__.load({
131
133
  "body[data-ds-dark-theme] .crl_line,[data-theme='dark'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}",
132
134
  "body[data-ds-dark-theme] .crl_item:hover .crl_line,[data-theme='dark'] .crl_item:hover .crl_line,.dark .crl_item:hover .crl_line{background-color:rgba(255,255,255,.95)}",
133
135
  "body[data-ds-dark-theme] .crl_item.crl_active .crl_line,[data-theme='dark'] .crl_item.crl_active .crl_line,.dark .crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
134
- ".crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:none}",
136
+ ".crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:auto}",
137
+ ".crl_tipQaWrap{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));display:flex;flex-direction:column;gap:4px;pointer-events:auto}",
138
+ ".crl_tipQa{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;border:none;border-radius:7px;background:rgba(77,107,254,.08);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}",
139
+ ".crl_tipQa:hover{background:rgba(77,107,254,.16)}",
140
+ ".crl_tipQaIcon{flex:none;display:block;color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
141
+ ".crl_tipQaText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
135
142
  "body[data-ds-dark-theme] .crl_tip,[data-theme='dark'] .crl_tip,.dark .crl_tip{background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(28,28,32,.97)));border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}",
136
143
  ".crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}",
137
144
  ".crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}",
@@ -155,14 +162,16 @@ window.__ModuleLoader__.load({
155
162
  ".crl_item.crl_favItem .crl_line{background-color:#ffd166}",
156
163
  "body[data-ds-dark-theme] .crl_item.crl_favItem .crl_line,[data-theme='dark'] .crl_item.crl_favItem .crl_line,.dark .crl_item.crl_favItem .crl_line{background-color:#ffd166}",
157
164
  ".crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}",
158
- ".crl_favToggle{position:sticky;top:-10px;z-index:3;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:26px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:13px;background:inherit;color:rgba(0,0,0,.4);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease}",
165
+ ".crl_favToggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:34px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:0;background:transparent;border-bottom:1px solid rgba(0,0,0,.07);color:rgba(0,0,0,.42);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease,margin .25s cubic-bezier(.4,0,.2,1),left .25s cubic-bezier(.4,0,.2,1)}",
159
166
  ".crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}",
160
167
  ".crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}",
161
- ".crl_show .crl_favToggle{width:auto;height:26px;margin:0 6px 8px;padding:0 11px;border-radius:13px;align-self:flex-start;text-align:left}",
168
+ ".crl_show .crl_favToggle{width:100%;height:28px;margin:0 0 6px;padding:0 12px;border-radius:0;justify-content:flex-start;text-align:left;border-bottom:1px solid rgba(0,0,0,.07)}",
162
169
  ".crl_favToggleLabel{display:none}",
163
170
  ".crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}",
164
- "body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}",
171
+ "body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4);border-bottom-color:rgba(255,255,255,.1)}",
165
172
  "body[data-ds-dark-theme] .crl_favToggle:hover,[data-theme='dark'] .crl_favToggle:hover,.dark .crl_favToggle:hover{background:rgba(255,255,255,.1);color:rgba(255,255,255,.85)}",
173
+ "body[data-ds-dark-theme] .crl_show .crl_favToggle,[data-theme='dark'] .crl_show .crl_favToggle,.dark .crl_show .crl_favToggle{border-bottom-color:rgba(255,255,255,.1)}",
174
+ ".crl_nav.crl_navHidden{opacity:0 !important;pointer-events:none !important}",
166
175
  "body[data-ds-dark-theme] .crl_favToggle.crl_on,[data-theme='dark'] .crl_favToggle.crl_on,.dark .crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.18)}",
167
176
  "@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}",
168
177
  ".crlSetCard{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s;cursor:pointer}",
@@ -189,6 +198,14 @@ window.__ModuleLoader__.load({
189
198
  * CSS module 类名(如 hkplfa_slot,hash 随构建内容漂移)。 */
190
199
  const HIDE_OFFICIAL_CSS = "nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}";
191
200
  const HIDE_OFFICIAL_STYLE_ID = "data-crl-hide-official";
201
+ /**
202
+ * `data-plugin` 归属标记:DSH 的 client 模块系统只把**没有** `data-plugin` 的 `<style>`
203
+ * 认领给正在 materialize 的模块(`claimStyles`),而 HMR 重载时按
204
+ * `data-plugin === 模块 id` 逐字匹配删除。裸注入的 style 会被任意模块认领走,
205
+ * 随后随那个模块的一次重载被物理删除——元素还在、样式消失(悬浮球样式事故的机制)。
206
+ * 这里用一个独立于模块 id 的稳定值:既不进别人的账,也不随本模块的进出被清掉。
207
+ */
208
+ const HIDE_OFFICIAL_PLUGIN_TAG = "dsh-chat-rail-hide-official";
192
209
  /** 按设置同步「屏蔽官方 TurnNavigator」样式(showOfficial=true=官方模式:不屏蔽)。 */
193
210
  function syncOfficialHide(showOfficial) {
194
211
  const el = document.head.querySelector(`style[${HIDE_OFFICIAL_STYLE_ID}]`);
@@ -199,10 +216,19 @@ window.__ModuleLoader__.load({
199
216
  if (el === null) {
200
217
  const tag = document.createElement("style");
201
218
  tag.setAttribute(HIDE_OFFICIAL_STYLE_ID, "");
219
+ tag.setAttribute("data-plugin", HIDE_OFFICIAL_PLUGIN_TAG);
202
220
  tag.textContent = HIDE_OFFICIAL_CSS;
203
221
  document.head.appendChild(tag);
204
- }
222
+ } else if (el.textContent !== HIDE_OFFICIAL_CSS) el.textContent = HIDE_OFFICIAL_CSS;
205
223
  }
224
+ /**
225
+ * 会话区剩余宽度低于此值时隐藏导航条(px)。
226
+ *
227
+ * 36px 是 rail 自身宽度,留 ~220px 给消息区——再窄时指示点已无参考意义。
228
+ */
229
+ const RAIL_MIN_SPACE = 260;
230
+ /** 导航条隐藏类(空间不足时由 React 加上;用类而非内联 display,避免与官方模式冲突)。 */
231
+ const NAV_HIDDEN_CLASS = "crl_navHidden";
206
232
  /** 移除所有已注入的行内收藏/填充按钮(官方模式下的回溯清理)。 */
207
233
  function clearRowActions() {
208
234
  for (const row of document.querySelectorAll("[data-crl-actions=\"1\"]")) {
@@ -219,16 +245,20 @@ window.__ModuleLoader__.load({
219
245
  /** No-op subscribe for useSyncExternalStore when the scope is not yet bound. */
220
246
  const NOOP_SUB = (() => () => {});
221
247
  const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
222
- if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) === null) {
223
- const tag = document.createElement("style");
224
- tag.dataset.plugin = "dsh-chat-rail";
225
- tag.dataset.pluginCss = STYLE_ID;
226
- tag.textContent = css;
227
- document.head.appendChild(tag);
248
+ if (typeof document !== "undefined") {
249
+ const existing = document.head.querySelector(`style[data-plugin-css="${STYLE_ID}"]`);
250
+ if (existing === null) {
251
+ const tag = document.createElement("style");
252
+ tag.dataset.plugin = "dsh-chat-rail";
253
+ tag.dataset.pluginCss = STYLE_ID;
254
+ tag.textContent = css;
255
+ document.head.appendChild(tag);
256
+ } else if (existing.textContent !== css) existing.textContent = css;
228
257
  }
229
258
  const S = {
230
259
  nav: "crl_nav",
231
260
  navShow: "crl_show",
261
+ list: "crl_list",
232
262
  item: "crl_item",
233
263
  itemActive: "crl_active",
234
264
  title: "crl_title",
@@ -240,6 +270,10 @@ window.__ModuleLoader__.load({
240
270
  loading: "crl_loading",
241
271
  loadingLabel: "crl_loadingLabel",
242
272
  tip: "crl_tip",
273
+ tipQaWrap: "crl_tipQaWrap",
274
+ tipQa: "crl_tipQa",
275
+ tipQaIcon: "crl_tipQaIcon",
276
+ tipQaText: "crl_tipQaText",
243
277
  tipImgs: "crl_tipImgs",
244
278
  tipImgWrap: "crl_tipImgWrap",
245
279
  tipImg: "crl_tipImg",
@@ -611,11 +645,109 @@ window.__ModuleLoader__.load({
611
645
  out.sort((a, b) => a.seq - b.seq);
612
646
  return out;
613
647
  }
614
- /** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */
648
+ /** 任一数据平面的节点集合(与 collectFromNodes 同一套取值规则)。 */
649
+ function nodeValuesOf(snapshot) {
650
+ const store = snapshot?.nodes;
651
+ if (store !== void 0 && typeof store.values === "function") return [...store.values()];
652
+ const chat = snapshot?.chat;
653
+ if (chat?.nodes === void 0) return [];
654
+ return [...chat.nodes.values()];
655
+ }
656
+ /**
657
+ * 「提问&回答」也要能被导航。
658
+ *
659
+ * 它**不是** chat node:`ask_user_question` 是助手消息里的一个 tool-call 块,
660
+ * 用户的答案落在同一个 callId 的 `tool/result`(实测会话日志:有 `tool/call
661
+ * name=ask_user_question` 与对应结果,**没有任何带问答痕迹的 user/message**)。
662
+ * 所以 `railMessageOfNode` 的 `kind==='user'` 判定永远覆盖不到它。
663
+ *
664
+ * 但官方给工具调用也设了 DOM 锚点(`ui-tool/ToolCallTree.tsx:36` →
665
+ * `data-chat-anchor-key={`call:${callId}`}`),所以**可跳转**——只要把
666
+ * 助手节点里的问答块收集成 rail 条目即可。
667
+ *
668
+ * 范围:只覆盖已加载的 chat node(不做 loadThrough 拉取未加载历史)——
669
+ * 用户 2026-09-14 拍板。
670
+ *
671
+ * @param snapshot - 当前 chat node 快照(任一平面)。
672
+ * @returns 按 seq 升序的问答条目。
673
+ */
674
+ function collectQaFromNodes(snapshot) {
675
+ const out = [];
676
+ for (const node of nodeValuesOf(snapshot)) {
677
+ if (node === null || typeof node !== "object") continue;
678
+ const n = node;
679
+ if (n.kind !== "tool-call") continue;
680
+ const root = n.data?.root;
681
+ if (root === null || typeof root !== "object") continue;
682
+ const r = root;
683
+ const callId = typeof r.callId === "string" ? r.callId : "";
684
+ if (callId === "") continue;
685
+ if ((typeof r.name === "string" ? r.name : typeof r.call?.name === "string" ? r.call.name : "") !== "ask_user_question") continue;
686
+ const asked = questionsTextOf(typeof r.argsRaw === "string" ? r.argsRaw : typeof r.call?.argsRaw === "string" ? r.call.argsRaw : "");
687
+ if (asked === "") continue;
688
+ out.push({
689
+ seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
690
+ time: typeof r.time === "number" ? r.time : 0,
691
+ text: asked,
692
+ hasImage: false,
693
+ key: typeof n.key === "string" ? n.key : void 0,
694
+ anchor: `call:${callId}`,
695
+ qa: true
696
+ });
697
+ }
698
+ out.sort((a, b) => a.seq - b.seq);
699
+ return out;
700
+ }
701
+ /**
702
+ * 属于第 `index` 条消息区间的问答——**只归给它前面最近的那条用户消息**。
703
+ *
704
+ * 区间取 `[本条 seq, 下一条消息的 seq)`,最后一条消息无上界。
705
+ * 判据不能只写 `qa.seq >= m.seq`:那会让同一条提问落进**所有更早消息**的 tip
706
+ * (2026-09-14 用户报告的归属 bug)。依据是「提问一定在某条消息的回答中」——
707
+ * 它必然夹在两条用户消息之间,因此上界就是下一条消息。
708
+ *
709
+ * @param qaMessages - 已按 seq 升序的问答条目(collectQaFromNodes 的产出)。
710
+ * @param messages - 完整的用户消息列表(**不是**收藏过滤后的列表:区间按消息序列划分)。
711
+ * @param index - 目标消息在 `messages` 中的下标。
712
+ * @returns 落在该区间内的问答条目(保持输入顺序)。
713
+ */
714
+ function qaWithinMessage(qaMessages, messages, index) {
715
+ const self = messages[index];
716
+ if (self === void 0) return [];
717
+ const next = messages[index + 1];
718
+ const upper = next === void 0 ? void 0 : next.seq;
719
+ return qaMessages.filter((q) => q.seq >= self.seq && (upper === void 0 || q.seq < upper));
720
+ }
721
+ /** 问答条目的预览文本:取 `arguments` JSON 里的问题标题(多问用 ` / ` 连)。 */
722
+ function questionsTextOf(argsRaw) {
723
+ if (typeof argsRaw !== "string" || argsRaw.trim() === "") return "";
724
+ let parsed;
725
+ try {
726
+ parsed = JSON.parse(argsRaw);
727
+ } catch {
728
+ return "";
729
+ }
730
+ const list = parsed?.questions;
731
+ if (!Array.isArray(list)) return "";
732
+ const parts = [];
733
+ for (const q of list) {
734
+ if (q === null || typeof q !== "object") continue;
735
+ const o = q;
736
+ const text = typeof o.question === "string" && o.question !== "" ? o.question : typeof o.header === "string" ? o.header : "";
737
+ if (text !== "") parts.push(text);
738
+ }
739
+ return parts.join(" / ").trim().slice(0, 80);
740
+ }
741
+ /** Resolve the DOM anchor key (`data-chat-anchor-key`) for one rail message. */
615
742
  function anchorKeyOf(m) {
743
+ if (typeof m.anchor === "string" && m.anchor !== "") return m.anchor;
616
744
  if (typeof m.key === "string" && m.key !== "") return m.key;
617
745
  if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
618
746
  }
747
+ /** Resolve the **chat node key** for one rail message (used for load checks). */
748
+ function nodeKeyOf(m) {
749
+ return typeof m.key === "string" && m.key !== "" ? m.key : anchorKeyOf(m);
750
+ }
619
751
  /** Full text of a rail message from the loaded chat nodes (uncapped),
620
752
  * falling back to the projection preview when the node is not mounted. */
621
753
  function fullTextOf(m, nodeSnapshot) {
@@ -736,13 +868,13 @@ window.__ModuleLoader__.load({
736
868
  if (row === null) return false;
737
869
  onJump?.("landed");
738
870
  const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
739
- const scrollToRow = () => {
871
+ const scrollToRow = (instant = false) => {
740
872
  const viewRect = scrollport.getBoundingClientRect();
741
873
  const rowRect = row.getBoundingClientRect();
742
874
  const target = Math.max(0, scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2);
743
875
  scrollport.scrollTo({
744
876
  top: target,
745
- behavior: reducedMotion ? "auto" : "smooth"
877
+ behavior: instant || reducedMotion ? "auto" : "smooth"
746
878
  });
747
879
  };
748
880
  const rowDelta = () => {
@@ -773,7 +905,7 @@ window.__ModuleLoader__.load({
773
905
  await delay(600);
774
906
  if (signal?.aborted) return false;
775
907
  if (rowDelta() <= Math.max(120, scrollport.getBoundingClientRect().height * .2)) break;
776
- scrollToRow();
908
+ scrollToRow(true);
777
909
  }
778
910
  return true;
779
911
  }
@@ -863,6 +995,7 @@ window.__ModuleLoader__.load({
863
995
  let messages = [];
864
996
  if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
865
997
  if (messages.length === 0) messages = collectFromNodes(nodeSnapshot);
998
+ const qaMessages = collectQaFromNodes(nodeSnapshot);
866
999
  const showOfficial = (0, react.useSyncExternalStore)(railSettingsScope !== null ? (cb) => railSettingsScope.subscribe(cb) : NOOP_SUB, () => railSettingsScope?.getSnapshot() ?? { value: void 0 }).value?.showOfficialNavigator === true;
867
1000
  const applyOfficialMode = (on) => {
868
1001
  syncOfficialHide(on);
@@ -874,6 +1007,39 @@ window.__ModuleLoader__.load({
874
1007
  (0, react.useEffect)(() => {
875
1008
  applyOfficialMode(showOfficial);
876
1009
  }, [showOfficial]);
1010
+ const [anchor, setAnchor] = (0, react.useState)(null);
1011
+ (0, react.useEffect)(() => {
1012
+ const measure = () => {
1013
+ if (typeof document === "undefined") return;
1014
+ const scrollport = document.querySelector("[data-conversation-scroll]");
1015
+ if (scrollport === null) {
1016
+ setAnchor(null);
1017
+ return;
1018
+ }
1019
+ const rect = scrollport.getBoundingClientRect();
1020
+ if (rect.width === 0) {
1021
+ setAnchor(null);
1022
+ return;
1023
+ }
1024
+ const next = {
1025
+ right: Math.max(0, Math.round(window.innerWidth - rect.right)),
1026
+ width: Math.round(rect.width)
1027
+ };
1028
+ setAnchor((prev) => prev !== null && prev.right === next.right && prev.width === next.width ? prev : next);
1029
+ };
1030
+ measure();
1031
+ const scrollport = document.querySelector("[data-conversation-scroll]");
1032
+ const observer = typeof ResizeObserver === "undefined" || scrollport === null ? null : new ResizeObserver(measure);
1033
+ if (observer !== null && scrollport !== null) observer.observe(scrollport);
1034
+ window.addEventListener("resize", measure);
1035
+ const timer = window.setInterval(measure, 400);
1036
+ return () => {
1037
+ if (observer !== null) observer.disconnect();
1038
+ window.removeEventListener("resize", measure);
1039
+ window.clearInterval(timer);
1040
+ };
1041
+ }, [sessionId]);
1042
+ const spaceTight = anchor !== null && anchor.width < RAIL_MIN_SPACE;
877
1043
  const favorites = (0, react.useSyncExternalStore)(subscribeFavorites, favoritesSnapshot);
878
1044
  const [favOnly, setFavOnly] = (0, react.useState)(false);
879
1045
  const showFavToggle = (sessionId === void 0 ? 0 : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length) > 0;
@@ -886,6 +1052,7 @@ window.__ModuleLoader__.load({
886
1052
  const [jumping, setJumping] = (0, react.useState)(false);
887
1053
  const [tip, setTip] = (0, react.useState)(null);
888
1054
  const navRef = (0, react.useRef)(null);
1055
+ const listRef = (0, react.useRef)(null);
889
1056
  const expandedRef = (0, react.useRef)(false);
890
1057
  const lastPointerRef = (0, react.useRef)(null);
891
1058
  const jumpAbortRef = (0, react.useRef)(null);
@@ -924,9 +1091,21 @@ window.__ModuleLoader__.load({
924
1091
  }
925
1092
  positionTip(index);
926
1093
  };
1094
+ const tipCloseTimerRef = (0, react.useRef)(null);
1095
+ const cancelTipClose = () => {
1096
+ if (tipCloseTimerRef.current !== null) {
1097
+ clearTimeout(tipCloseTimerRef.current);
1098
+ tipCloseTimerRef.current = null;
1099
+ }
1100
+ };
927
1101
  const handleItemLeave = (index) => {
928
- setTip((prev) => prev?.index === index ? null : prev);
1102
+ cancelTipClose();
1103
+ tipCloseTimerRef.current = setTimeout(() => {
1104
+ tipCloseTimerRef.current = null;
1105
+ setTip((prev) => prev?.index === index ? null : prev);
1106
+ }, 180);
929
1107
  };
1108
+ (0, react.useEffect)(() => cancelTipClose, []);
930
1109
  (0, react.useEffect)(() => {
931
1110
  if (!show) {
932
1111
  expandedRef.current = false;
@@ -1056,7 +1235,7 @@ window.__ModuleLoader__.load({
1056
1235
  const rect = sp.getBoundingClientRect();
1057
1236
  if (rect.height === 0) return;
1058
1237
  const line = rect.top + rect.height * .4;
1059
- const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"]");
1238
+ const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"],[data-chat-anchor-key^=\"call:\"]");
1060
1239
  let best = -1;
1061
1240
  let bestDist = Infinity;
1062
1241
  for (const row of rows) {
@@ -1093,7 +1272,7 @@ window.__ModuleLoader__.load({
1093
1272
  }, [sessionId, effectiveMessages.length]);
1094
1273
  (0, react.useEffect)(() => {
1095
1274
  if (activeIndex < 0) return;
1096
- const el = navRef.current;
1275
+ const el = listRef.current;
1097
1276
  if (el === null) return;
1098
1277
  const item = el.querySelector(`[data-crl-index="${activeIndex}"]`);
1099
1278
  if (item === null) return;
@@ -1112,6 +1291,8 @@ window.__ModuleLoader__.load({
1112
1291
  type: "button",
1113
1292
  key: m.seq,
1114
1293
  "data-crl-index": String(i),
1294
+ ...key !== void 0 ? { "data-crl-anchor": key } : {},
1295
+ ...m.qa === true ? { "data-crl-qa": "true" } : {},
1115
1296
  className: S.item + (activeIndex === i ? ` ${S.itemActive}` : "") + (starred ? ` ${S.favItem}` : ""),
1116
1297
  "aria-label": `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,
1117
1298
  "aria-current": activeIndex === i ? "location" : void 0,
@@ -1121,7 +1302,7 @@ window.__ModuleLoader__.load({
1121
1302
  jumpAbortRef.current?.abort();
1122
1303
  const controller = new AbortController();
1123
1304
  jumpAbortRef.current = controller;
1124
- jumpToMessage(sessionsService, sessionId, key, (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
1305
+ jumpToMessage(sessionsService, sessionId, nodeKeyOf(m), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
1125
1306
  },
1126
1307
  onMouseEnter: () => handleItemEnter(i),
1127
1308
  onMouseLeave: () => handleItemLeave(i),
@@ -1181,7 +1362,8 @@ window.__ModuleLoader__.load({
1181
1362
  });
1182
1363
  return (0, react_dom.createPortal)([(0, react.createElement)("div", {
1183
1364
  ref: navRef,
1184
- className: S.nav + (show ? ` ${S.navShow}` : ""),
1365
+ className: S.nav + (show ? ` ${S.navShow}` : "") + (spaceTight ? ` ${NAV_HIDDEN_CLASS}` : ""),
1366
+ ...anchor !== null ? { style: { right: `${anchor.right + 3}px` } } : {},
1185
1367
  role: "navigation",
1186
1368
  "aria-label": t.railLabel,
1187
1369
  onMouseEnter: () => setShow(true),
@@ -1211,7 +1393,11 @@ window.__ModuleLoader__.load({
1211
1393
  className: S.loading,
1212
1394
  key: "loading"
1213
1395
  }, (0, react.createElement)("span", { className: S.loadingLabel }, t.loading)) : null,
1214
- ...items
1396
+ (0, react.createElement)("div", {
1397
+ ref: listRef,
1398
+ key: "list",
1399
+ className: S.list
1400
+ }, ...items)
1215
1401
  ]
1216
1402
  }), tip !== null && tipIndex >= 0 && tipIndex < messages.length ? (0, react.createElement)("div", {
1217
1403
  className: S.tip,
@@ -1219,7 +1405,9 @@ window.__ModuleLoader__.load({
1219
1405
  left: `${tip.x}px`,
1220
1406
  top: `${tip.y}px`,
1221
1407
  transform: "translateX(-100%)"
1222
- }
1408
+ },
1409
+ onMouseEnter: () => cancelTipClose(),
1410
+ onMouseLeave: () => setTip(null)
1223
1411
  }, (() => {
1224
1412
  const m = messages[tipIndex];
1225
1413
  const specs = tipImagesOf(m, nodeSnapshot);
@@ -1243,6 +1431,51 @@ window.__ModuleLoader__.load({
1243
1431
  key: "badge"
1244
1432
  }, t.hasImage));
1245
1433
  children.push((0, react.createElement)("span", { key: "text" }, fullTextOf(m, nodeSnapshot) || t.noText));
1434
+ const qaForTip = qaWithinMessage(qaMessages, messages, tipIndex);
1435
+ if (qaForTip.length > 0) children.push((0, react.createElement)("div", {
1436
+ className: S.tipQaWrap,
1437
+ key: "qa"
1438
+ }, qaForTip.map((q) => (0, react.createElement)("button", {
1439
+ type: "button",
1440
+ key: q.key ?? q.anchor,
1441
+ className: S.tipQa,
1442
+ title: q.text,
1443
+ onClick: (e) => {
1444
+ e.stopPropagation();
1445
+ if (jumping) return;
1446
+ jumpAbortRef.current?.abort();
1447
+ const controller = new AbortController();
1448
+ jumpAbortRef.current = controller;
1449
+ jumpToMessage(sessionsService, sessionId, nodeKeyOf(q), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, q.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
1450
+ },
1451
+ children: [(0, react.createElement)("svg", {
1452
+ key: "icon",
1453
+ className: S.tipQaIcon,
1454
+ viewBox: "0 0 16 16",
1455
+ width: 13,
1456
+ height: 13,
1457
+ fill: "none",
1458
+ stroke: "currentColor",
1459
+ strokeWidth: 1.6,
1460
+ strokeLinecap: "round",
1461
+ strokeLinejoin: "round",
1462
+ "aria-hidden": true
1463
+ }, (0, react.createElement)("circle", {
1464
+ cx: 8,
1465
+ cy: 8,
1466
+ r: 6.5,
1467
+ "aria-hidden": true
1468
+ }), (0, react.createElement)("path", {
1469
+ d: "M6.06 6a2 2 0 0 1 3.89.67c0 1.33-2 2-2 2",
1470
+ "aria-hidden": true
1471
+ }), (0, react.createElement)("path", {
1472
+ d: "M8 11.33h.01",
1473
+ "aria-hidden": true
1474
+ })), (0, react.createElement)("span", {
1475
+ className: S.tipQaText,
1476
+ key: "txt"
1477
+ }, q.text)]
1478
+ }))));
1246
1479
  return children;
1247
1480
  })()) : null], document.body);
1248
1481
  }
@@ -1319,6 +1552,7 @@ window.__ModuleLoader__.load({
1319
1552
  exports.chatNodeOf = chatNodeOf;
1320
1553
  exports.clearRowActions = clearRowActions;
1321
1554
  exports.collectFromNodes = collectFromNodes;
1555
+ exports.collectQaFromNodes = collectQaFromNodes;
1322
1556
  exports.favoriteIdsOf = favoriteIdsOf;
1323
1557
  exports.imageSpecsOfContent = imageSpecsOfContent;
1324
1558
  exports.inject = inject;
@@ -1326,6 +1560,7 @@ window.__ModuleLoader__.load({
1326
1560
  exports.jumpToMessage = jumpToMessage;
1327
1561
  exports.messageIdOfAnchorKey = messageIdOfAnchorKey;
1328
1562
  exports.normalize = normalize;
1563
+ exports.qaWithinMessage = qaWithinMessage;
1329
1564
  exports.railMessageOfNode = railMessageOfNode;
1330
1565
  exports.readFavorites = readFavorites;
1331
1566
  exports.syncOfficialHide = syncOfficialHide;