@max-null/dsh-chat-rail 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -1,87 +1,88 @@
1
- # @max-null/dsh-chat-rail
2
-
3
- 本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-capture` · `dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-header-unify` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui`。
4
-
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
-
7
- 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover 展开画卷查看消息列表,点击跳转任意历史消息。
8
-
9
- > 不同于市面上"面板弹出式"的导航插件,dsh-chat-rail 采用**画卷式展开**:收起时是一条干净的竖条,hover 时从右往左平滑展开,像打开画卷一样。
10
-
11
- ## 截图
12
-
13
- | 画卷展开(收藏过滤 + 收藏导航条) | 消息操作按钮(收藏 ★ / 填充 ➕ / 复制) |
14
- |---|---|
15
- | ![rail-fav-1](docs/shots/rail-fav-1.png) | ![rail-fav-2](docs/shots/rail-fav-2.png) |
16
-
17
- ## 为什么比市面上的导航栏优秀
18
-
19
- 市面上的会话导航插件(chat-timeline / milestone / msg-nav / turn-rail…)大多只做"指示条 + 点击跳转",在真实使用中有一堆体验问题。dsh-chat-rail 针对每一个问题给出了答案:
20
-
21
- | 能力 | 市面同类 | dsh-chat-rail |
22
- |---|---|---|
23
- | **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅ **跟随 CSS 变量动画同步避让**:`--dsh-sidebar-width` / `--dsh-sidebar-height`,与面板共享同一 transition,右栏/底栏展开收起时导航无缝跟随,互不遮挡 |
24
- | **展开形态** | ❌ 面板突兀弹出,高度错位 | ✅ **画卷式展开**:单容器从右往左平滑展开,高度天然一致 |
25
- | **阅读位置跟随** | 高亮点会跑出显示区 | ✅ **导航条自动滚动,高亮点持续居中**:无论会话滚到哪里,当前消息的指示点始终在导航可视区中央 |
26
- | **切换会话定位** | 回到顶部 | ✅ 自动定位到新会话当前阅读位置 |
27
- | **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title 干扰 |
28
- | **消息信息** | 只有预览文本 | ✅ **编号 + 相对时间 + 文本三列对齐**:编号固定宽、时间靠右,文本区域最大化且整齐 |
29
- | **长会话跳转** | 干等卡死 | ✅ **加载反馈**:跳转远距离消息时显示 spinner + "加载中",收起态只留图标 |
30
- | **消息收藏** | ❌ 大多数只能看不能存 | ✅ **收藏 + 快速填充**:消息行星标收藏(黄色导航条联动)、只显示收藏过滤、加号一键全文+附件回填输入框 |
31
- | **主题适配** | 部分 | ✅ 全部使用 DSH 主题变量(`--dsw-alias-*`),浅深色无缝适配 |
32
- | **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH 环境开箱即用,固定右缘 + 垂直居中 |
33
-
34
- ## 兼容性(重点)
35
-
36
- - **与 dsh-better-sidebar 深度兼容**:导航栏的右缘和垂直位置跟随 better-sidebar 的 `--dsh-sidebar-width` / `--dsh-sidebar-height` CSS 变量,右栏和底栏展开/收起时导航与面板**共享同一段 transition 动画同步移动**,永不错位、永不遮挡(包括底栏右上角的收起按钮)。
37
- - **无 better-sidebar 也能用**:变量缺失时走 CSS fallback(右缘 12px + 垂直居中),纯 DSH 环境开箱即用。
38
- - 需要 DSH ≥ `0.1.0-rc.6`。
39
-
40
- ## 安装
41
-
42
- ```bash
43
- dsh plugin --profile <name> add @max-null/dsh-chat-rail
44
- ```
45
-
46
- 或手动加入 profile 的 `package.json` dependencies + `dsh.profile.bundles`:
47
-
48
- ```yaml
49
- - id: chat-rail
50
- name: '@max-null/dsh-chat-rail'
51
- ```
52
-
53
- ## 交互
54
-
55
- - **hover 导航条**:画卷展开,显示编号 / 相对时间 / 消息预览
56
- - **hover 消息项**:左侧弹出完整内容 tip 面板(含图消息在文本上方显示图片缩略图)
57
- - **点击消息项**:跳转到会话中对应消息(未加载的历史自动翻页补载)
58
- - **滚动会话**:高亮点跟随,导航条自动滚动保持居中
59
- - **含图消息**:展开态显示柔和图片角标(收起态不显示,保持竖条干净)
60
-
61
- ### 收藏与快速填充(v0.4.0)
62
-
63
- - **消息操作按钮**:每条用户消息的复制按钮左侧新增两个按钮
64
- - **星 ★**:收藏/取消收藏该消息(收藏后变成黄色实心星);收藏状态按会话持久化(localStorage),刷新与重启后保留
65
- - **加号 +**:一键把该消息**全文 + 附件**填入输入框(草稿设为消息全文,历史图片经官方附件通道回填入输入框)
66
- - **导航条联动**:收藏的消息指示条显示为**黄色**;展开态在标题前显示小星标
67
- - **只显示收藏**:导航条顶部新增星形过滤开关,点击点亮(黄色实心)后导航栏只显示已收藏的消息;再点恢复全部
68
-
69
- ## 架构
70
-
71
- - **host 半端**(`lib/index.mjs`):注册 `chatRail` 会话投影——从 session log 折叠用户消息锚点 `{ seq, time, text, id }`(排除插件/工具注入的上下文行)
72
- - **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body
73
- - 数据源优先序:`chatRail` 投影 → 已加载聊天节点 → 后台 loadOlder 循环
74
-
75
- ## SSID 系列
76
-
77
- ## 开发
78
-
79
- ```bash
80
- pnpm install
81
- pnpm typecheck # tsc 严格类型检查
82
- pnpm build # 产出 lib/
83
- ```
84
-
85
- ## License
86
-
87
- MIT
1
+ # @max-null/dsh-chat-rail
2
+
3
+ 本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-capture` · `dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-quick-toolbar` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui` · `dsh-achievements`。
4
+
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
+
7
+ 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover 展开画卷查看消息列表,点击跳转任意历史消息。
8
+
9
+ > 不同于市面上"面板弹出式"的导航插件,dsh-chat-rail 采用**画卷式展开**:收起时是一条干净的竖条,hover 时从右往左平滑展开,像打开画卷一样。
10
+
11
+ ## 截图
12
+
13
+ | 画卷展开(收藏过滤 + 收藏导航条) | 消息操作按钮(收藏 ★ / 填充 ➕ / 复制) |
14
+ |---|---|
15
+ | ![rail-fav-1](docs/shots/rail-fav-1.png) | ![rail-fav-2](docs/shots/rail-fav-2.png) |
16
+
17
+ ## 为什么比市面上的导航栏优秀
18
+
19
+ 市面上的会话导航插件(chat-timeline / milestone / msg-nav / turn-rail…)大多只做"指示条 + 点击跳转",在真实使用中有一堆体验问题。dsh-chat-rail 针对每一个问题给出了答案:
20
+
21
+ | 能力 | 市面同类 | dsh-chat-rail |
22
+ |---|---|---|
23
+ | **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅ **跟随 CSS 变量动画同步避让**:`--dsh-sidebar-width` / `--dsh-sidebar-height`,与面板共享同一 transition,右栏/底栏展开收起时导航无缝跟随,互不遮挡 |
24
+ | **展开形态** | ❌ 面板突兀弹出,高度错位 | ✅ **画卷式展开**:单容器从右往左平滑展开,高度天然一致 |
25
+ | **阅读位置跟随** | 高亮点会跑出显示区 | ✅ **导航条自动滚动,高亮点持续居中**:无论会话滚到哪里,当前消息的指示点始终在导航可视区中央 |
26
+ | **切换会话定位** | 回到顶部 | ✅ 自动定位到新会话当前阅读位置 |
27
+ | **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title 干扰 |
28
+ | **消息信息** | 只有预览文本 | ✅ **编号 + 相对时间 + 文本三列对齐**:编号固定宽、时间靠右,文本区域最大化且整齐 |
29
+ | **长会话跳转** | 干等卡死 | ✅ **加载反馈**:跳转远距离消息时显示 spinner + "加载中",收起态只留图标 |
30
+ | **消息收藏** | ❌ 大多数只能看不能存 | ✅ **收藏 + 快速填充**:消息行星标收藏(黄色导航条联动)、只显示收藏过滤、加号一键全文+附件回填输入框 |
31
+ | **主题适配** | 部分 | ✅ 全部使用 DSH 主题变量(`--dsw-alias-*`),浅深色无缝适配 |
32
+ | **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH 环境开箱即用,固定右缘 + 垂直居中 |
33
+
34
+ ## 兼容性(重点)
35
+
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),由本插件统一承担会话导航;未安装本插件时官方导航照常显示。
39
+ - 需要 DSH ≥ `0.1.0-rc.6`。
40
+
41
+ ## 安装
42
+
43
+ ```bash
44
+ dsh plugin --profile <name> add @max-null/dsh-chat-rail
45
+ ```
46
+
47
+ 或手动加入 profile 的 `package.json` dependencies + `dsh.profile.bundles`:
48
+
49
+ ```yaml
50
+ - id: chat-rail
51
+ name: '@max-null/dsh-chat-rail'
52
+ ```
53
+
54
+ ## 交互
55
+
56
+ - **hover 导航条**:画卷展开,显示编号 / 相对时间 / 消息预览
57
+ - **hover 消息项**:左侧弹出完整内容 tip 面板(含图消息在文本上方显示图片缩略图)
58
+ - **点击消息项**:跳转到会话中对应消息(未加载的历史自动翻页补载)
59
+ - **滚动会话**:高亮点跟随,导航条自动滚动保持居中
60
+ - **含图消息**:展开态显示柔和图片角标(收起态不显示,保持竖条干净)
61
+
62
+ ### 收藏与快速填充(v0.4.0)
63
+
64
+ - **消息操作按钮**:每条用户消息的复制按钮左侧新增两个按钮
65
+ - **星 ★**:收藏/取消收藏该消息(收藏后变成黄色实心星);收藏状态按会话持久化(localStorage),刷新与重启后保留
66
+ - **加号 +**:一键把该消息**全文 + 附件**填入输入框(草稿设为消息全文,历史图片经官方附件通道回填入输入框)
67
+ - **导航条联动**:收藏的消息指示条显示为**黄色**;展开态在标题前显示小星标
68
+ - **只显示收藏**:导航条顶部新增星形过滤开关,点击点亮(黄色实心)后导航栏只显示已收藏的消息;再点恢复全部
69
+
70
+ ## 架构
71
+
72
+ - **host 半端**(`lib/index.mjs`):注册 `chatRail` 会话投影——从 session log 折叠用户消息锚点 `{ seq, time, text, id }`(排除插件/工具注入的上下文行)
73
+ - **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body
74
+ - 数据源优先序:`chatRail` 投影 → 已加载聊天节点 → 后台 loadOlder 循环
75
+
76
+ ## SSID 系列
77
+
78
+ ## 开发
79
+
80
+ ```bash
81
+ pnpm install
82
+ pnpm typecheck # tsc 严格类型检查
83
+ pnpm build # 产出 lib/
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT
package/lib/client.js CHANGED
@@ -145,7 +145,7 @@ window.__ModuleLoader__.load({
145
145
  ".crl_item.crl_favItem .crl_line{background-color:#ffd166}",
146
146
  "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}",
147
147
  ".crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}",
148
- ".crl_favToggle{position:sticky;top:0;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}",
148
+ ".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}",
149
149
  ".crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}",
150
150
  ".crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}",
151
151
  ".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}",
@@ -154,7 +154,8 @@ window.__ModuleLoader__.load({
154
154
  "body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}",
155
155
  "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)}",
156
156
  "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)}",
157
- "@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}"
157
+ "@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}",
158
+ "nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}"
158
159
  ].join("");
159
160
  const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
160
161
  if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) === null) {
@@ -494,30 +495,57 @@ window.__ModuleLoader__.load({
494
495
  if (!Array.isArray(content)) return false;
495
496
  return content.some((block) => block !== null && typeof block === "object" && block.type === "image");
496
497
  }
497
- /** Fallback collector: enumerate user messages from the loaded chat nodes. */
498
+ /**
499
+ * Resolve one chat node from either data plane: the official Chat view
500
+ * snapshot (`nodes` keyed store) on current DSH, or the legacy Session
501
+ * snapshot's `chat.nodes` Map on older DSH.
502
+ * @param snapshot - the active node snapshot.
503
+ * @param key - the Conversation Context key.
504
+ * @returns the raw node, when its window covers the key.
505
+ */
506
+ function chatNodeOf(snapshot, key) {
507
+ if (key === void 0 || snapshot === void 0 || snapshot === null) return void 0;
508
+ const store = snapshot?.nodes;
509
+ if (store !== void 0 && typeof store.get === "function") return store.get(key);
510
+ return (snapshot?.chat?.nodes)?.get(key);
511
+ }
512
+ /** Map one chat node (either plane) to a rail message entry. */
513
+ function railMessageOfNode(node) {
514
+ if (node === null || typeof node !== "object") return null;
515
+ const n = node;
516
+ if (n.kind !== "user" && n.kind !== "steering") return null;
517
+ const key = typeof n.key === "string" ? n.key : void 0;
518
+ if (key === void 0) return null;
519
+ const data = n.data;
520
+ if (data === null || typeof data !== "object") return null;
521
+ if (typeof data.time !== "number" || !Array.isArray(data.content)) return null;
522
+ const images = nodeImagesOf(data.content);
523
+ return {
524
+ seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
525
+ time: data.time,
526
+ text: userTextOf(data.content),
527
+ hasImage: userHasImage(data.content),
528
+ ...images.length > 0 ? { images } : {},
529
+ key
530
+ };
531
+ }
532
+ /** Fallback collector: enumerate user messages from the loaded chat nodes.
533
+ * Accepts either data plane — the official Chat view snapshot (`nodes`
534
+ * values) or the legacy Session snapshot's `chat.nodes` Map. */
498
535
  function collectFromNodes(snapshot) {
499
536
  const out = [];
500
- if (snapshot === void 0 || snapshot.chat === void 0) return out;
501
- const chat = snapshot.chat;
502
- if (!chat.nodes) return out;
503
- for (const node of chat.nodes.values()) {
504
- if (node === null || typeof node !== "object") continue;
505
- const n = node;
506
- if (n.kind !== "user") continue;
507
- const data = n.data;
508
- if (data === null || typeof data !== "object") continue;
509
- if (typeof data.time !== "number" || !Array.isArray(data.content)) continue;
510
- const key = typeof n.key === "string" ? n.key : void 0;
511
- if (key === void 0) continue;
512
- const images = nodeImagesOf(data.content);
513
- out.push({
514
- seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
515
- time: data.time,
516
- text: userTextOf(data.content),
517
- hasImage: userHasImage(data.content),
518
- ...images.length > 0 ? { images } : {},
519
- key
520
- });
537
+ const store = snapshot?.nodes;
538
+ if (store !== void 0 && typeof store.values === "function") for (const node of store.values()) {
539
+ const m = railMessageOfNode(node);
540
+ if (m !== null) out.push(m);
541
+ }
542
+ else {
543
+ const chat = snapshot?.chat;
544
+ if (chat?.nodes === void 0) return out;
545
+ for (const node of chat.nodes.values()) {
546
+ const m = railMessageOfNode(node);
547
+ if (m !== null) out.push(m);
548
+ }
521
549
  }
522
550
  out.sort((a, b) => a.seq - b.seq);
523
551
  return out;
@@ -530,9 +558,7 @@ window.__ModuleLoader__.load({
530
558
  /** Full text of a rail message from the loaded chat nodes (uncapped),
531
559
  * falling back to the projection preview when the node is not mounted. */
532
560
  function fullTextOf(m, nodeSnapshot) {
533
- const key = anchorKeyOf(m);
534
- const nodes = nodeSnapshot?.chat?.nodes;
535
- const content = (key === void 0 ? void 0 : nodes?.get(key))?.data?.content;
561
+ const content = chatNodeOf(nodeSnapshot, anchorKeyOf(m))?.data?.content;
536
562
  if (Array.isArray(content)) {
537
563
  let out = "";
538
564
  for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
@@ -550,9 +576,7 @@ window.__ModuleLoader__.load({
550
576
  attachmentId: img.attachmentId,
551
577
  mediaType: img.mediaType
552
578
  }));
553
- const key = anchorKeyOf(m);
554
- const nodes = nodeSnapshot?.chat?.nodes;
555
- return imageSpecsOfContent((key === void 0 ? void 0 : nodes?.get(key))?.data?.content);
579
+ return imageSpecsOfContent(chatNodeOf(nodeSnapshot, anchorKeyOf(m))?.data?.content);
556
580
  }
557
581
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
558
582
  /**
@@ -561,22 +585,28 @@ window.__ModuleLoader__.load({
561
585
  * in a long session needs many pages. Report page count through onProgress so
562
586
  * the UI can show a loading state instead of appearing frozen.
563
587
  *
588
+ * "Loaded" is decided through `nodeOf`, the live chat-node reader (the
589
+ * uiConversation `'chat'` target on current DSH, where the Session snapshot no
590
+ * longer carries chat.nodes); without it the legacy Session snapshot's
591
+ * `chat.nodes` Map is used.
592
+ *
564
593
  * The paged-in data lands in the session snapshot synchronously with
565
594
  * loadOlder's resolution, but the chat rows render through React — the DOM row
566
595
  * for the target key appears a beat later. We therefore poll for the row (with
567
596
  * a timeout) instead of querying once and giving up; otherwise a multi-page
568
597
  * jump would "finish loading" without scrolling and need a second click.
569
598
  */
570
- async function jumpToMessage(sessionsService, sessionId, key, onProgress, signal) {
599
+ async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal) {
571
600
  const session = sessionsService.binding(sessionId)?.session;
572
601
  if (session === void 0) return false;
602
+ const isLoaded = nodeOf === void 0 ? (k) => session.getSnapshot().chat?.nodes?.get(k) !== void 0 : nodeOf;
573
603
  let pages = 0;
574
604
  let guard = 0;
575
605
  let loaded = false;
576
606
  while (guard++ < 120) {
577
607
  if (signal?.aborted) return false;
578
608
  const snapshot = session.getSnapshot();
579
- if (snapshot?.chat?.nodes?.get(key) !== void 0) {
609
+ if (isLoaded(key)) {
580
610
  loaded = true;
581
611
  break;
582
612
  }
@@ -688,11 +718,11 @@ window.__ModuleLoader__.load({
688
718
  const lang = typeof document !== "undefined" ? (document.documentElement.lang || "zh").toLowerCase() : "zh";
689
719
  return STRINGS[lang.startsWith("zh") ? "zh" : "en"];
690
720
  }
691
- function TimelineRail({ useProjection, sessionId, sessionsService, inputActions, conversation }) {
721
+ function TimelineRail({ useProjection, sessionId, sessionsService, chatOf, inputActions, conversation }) {
692
722
  const t = langStrings();
693
723
  const projected = useProjection("chatRail");
694
724
  const session = sessionId === void 0 ? void 0 : sessionsService.binding(sessionId)?.session;
695
- const fallbackStore = session === void 0 ? NOOP_STORE : session;
725
+ const fallbackStore = (sessionId === void 0 ? void 0 : chatOf?.(sessionId)) ?? (session === void 0 ? NOOP_STORE : session);
696
726
  const nodeSnapshot = (0, react.useSyncExternalStore)((cb) => fallbackStore.subscribe(cb), () => fallbackStore.getSnapshot());
697
727
  let messages = [];
698
728
  if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
@@ -944,7 +974,7 @@ window.__ModuleLoader__.load({
944
974
  const controller = new AbortController();
945
975
  jumpAbortRef.current = controller;
946
976
  setJumping(true);
947
- jumpToMessage(sessionsService, sessionId, key, void 0, controller.signal).finally(() => setJumping(false));
977
+ jumpToMessage(sessionsService, sessionId, key, (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal).finally(() => setJumping(false));
948
978
  },
949
979
  onMouseEnter: () => handleItemEnter(i),
950
980
  onMouseLeave: () => handleItemLeave(i),
@@ -1070,12 +1100,24 @@ window.__ModuleLoader__.load({
1070
1100
  })()) : null], document.body);
1071
1101
  }
1072
1102
  function apply(ctx) {
1103
+ let chatOf;
1104
+ ctx.inject(["uiConversation"], (scope) => {
1105
+ const ui = scope.uiConversation;
1106
+ chatOf = (sessionId) => {
1107
+ try {
1108
+ return ui.binding(sessionId).target("chat");
1109
+ } catch {
1110
+ return;
1111
+ }
1112
+ };
1113
+ });
1073
1114
  ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
1074
1115
  name: "conversation.input.dock",
1075
1116
  id: "chat-rail",
1076
1117
  order: 40,
1077
1118
  inject: () => ({
1078
1119
  sessionsService: ctx.sessions,
1120
+ chatOf,
1079
1121
  conversation: ctx.conversation
1080
1122
  })
1081
1123
  }, TimelineRail));
@@ -1083,12 +1125,16 @@ window.__ModuleLoader__.load({
1083
1125
  //#endregion
1084
1126
  exports.TimelineRail = TimelineRail;
1085
1127
  exports.apply = apply;
1128
+ exports.chatNodeOf = chatNodeOf;
1129
+ exports.collectFromNodes = collectFromNodes;
1086
1130
  exports.favoriteIdsOf = favoriteIdsOf;
1087
1131
  exports.imageSpecsOfContent = imageSpecsOfContent;
1088
1132
  exports.inject = inject;
1089
1133
  exports.isFavorite = isFavorite;
1134
+ exports.jumpToMessage = jumpToMessage;
1090
1135
  exports.messageIdOfAnchorKey = messageIdOfAnchorKey;
1091
1136
  exports.normalize = normalize;
1137
+ exports.railMessageOfNode = railMessageOfNode;
1092
1138
  exports.readFavorites = readFavorites;
1093
1139
  exports.toggleFavoriteId = toggleFavoriteId;
1094
1140
  return module.exports;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","createElement","useSyncExternalStore","useRef","useMemo","useCallback","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation']\r\n\r\n// ---- i18n (DSH locale-aware, zh/en) ----\r\ntype LocaleId = 'zh' | 'en'\r\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\r\n zh: {\r\n railLabel: '消息导航',\r\n roleUser: '用户',\r\n noText: '(无文本内容)',\r\n ariaJump: '跳转到消息',\r\n hasImage: '含图片',\r\n loading: '加载中…',\r\n timeJustNow: '刚刚',\r\n timeMinutes: '{n}分钟前',\r\n timeHours: '{n}小时前',\r\n timeDays: '{n}天前',\r\n fav: '收藏消息',\r\n unfav: '取消收藏',\r\n fill: '填充到输入框',\r\n filled: '已填入输入框',\r\n favOnly: '只显示收藏',\r\n },\r\n en: {\r\n railLabel: 'Message rail',\r\n roleUser: 'User',\r\n noText: '(no text)',\r\n ariaJump: 'Jump to message',\r\n hasImage: 'Has image',\r\n loading: 'Loading…',\r\n timeJustNow: 'just now',\r\n timeMinutes: '{n}m ago',\r\n timeHours: '{n}h ago',\r\n timeDays: '{n}d ago',\r\n fav: 'Bookmark message',\r\n unfav: 'Remove bookmark',\r\n fill: 'Fill into input',\r\n filled: 'Filled into input',\r\n favOnly: 'Bookmarks only',\r\n },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.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:thin;scrollbar-color:rgba(0,0,0,.18) transparent;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}',\r\n '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);scrollbar-color:rgba(255,255,255,.25) transparent}',\r\n '.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)}',\r\n '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)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.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}',\r\n '.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}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.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}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n // Image badge: hidden in the collapsed state (the rail is a clean 36px\r\n // capsule of indicator lines; an icon there would crowd and distract) and\r\n // revealed with the other labels on expand. A quiet stroke glyph in a muted\r\n // tone that ignores the active-blue text color.\r\n '.crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}',\r\n '.crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_img,[data-theme=\\'dark\\'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n '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)}',\r\n '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)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.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}',\r\n '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)))}',\r\n // Tip thumbnail gallery: stacked above the text, one row per image, capped\r\n // height so a tall screenshot cannot swallow the preview panel. The\r\n // placeholder pulses while session.readAttachment resolves the bytes; a\r\n // failed load removes the row entirely rather than showing a broken image.\r\n '.crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}',\r\n '.crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}',\r\n '.crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}',\r\n '.crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}',\r\n '.crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}',\r\n 'body[data-ds-dark-theme] .crl_tipImg,[data-theme=\\'dark\\'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '.crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}',\r\n '@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}',\r\n // Message action buttons injected next to the copy button (DOM row: the\r\n // user-message IconActions row). The host rows use 28px round icon-action\r\n // buttons (padding 6, 16px glyphs, DSW token colors, hover fill) — the\r\n // injected buttons mirror the exact geometry and token surface so the star\r\n // and plus read as native siblings of copy/branch. The star lights\r\n // yellowish (#ffd166, milestone-compatible) once favorited.\r\n // Tooltip: same dark pill as the host Tooltip (black rounded label, no\r\n // native title balloon). The delay mirrors the host tooltip's settle; the\r\n // fade is short so the pill appears right beside the hovered button.\r\n '.crl_msgAct{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:6px;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,rgba(0,0,0,.42));cursor:pointer;transition:background .15s ease,color .15s ease}',\r\n '.crl_msgAct:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,rgba(0,0,0,.72))}',\r\n '.crl_msgAct:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}',\r\n '.crl_msgAct[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);padding:5px 10px;border-radius:8px;background:rgba(24,28,36,.94);color:rgba(255,255,255,.95);font-size:12px;line-height:1.4;white-space:nowrap;pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '.crl_msgAct[data-tip]:hover::before{content:\"\";position:absolute;left:50%;bottom:calc(100% + 3px);transform:translateX(-50%);border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid rgba(24,28,36,.94);pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '@keyframes crl-tip-in{to{opacity:1}}',\r\n '.crl_msgAct.crl_fav.crl_on{color:#ffd166}',\r\n '.crl_msgAct.crl_fav.crl_on svg{fill:currentColor}',\r\n '.crl_msgAct.crl_fav.crl_on:hover{color:#ffd166}',\r\n // Rail row favorite badge + yellow indicator line for favorited messages.\r\n '.crl_favStar{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-right:6px;color:#ffd166}',\r\n '.crl_show .crl_favStar{display:inline-flex;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_favItem .crl_line{background-color:#ffd166}',\r\n '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}',\r\n // A favorited line that is also the current reading position keeps its\r\n // yellow identity — the generic active glow is blue, which pairs miserably\r\n // with the gold line, so the favorite-active state recasts the glow in the\r\n // same warm tone.\r\n '.crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}',\r\n // Rail-top favorites-only toggle: a round icon pill in the collapsed state;\r\n // on expand it becomes a full-width label row (star + \"bookmarks only\") so\r\n // the control reads as a real rail header action instead of a stray dot.\r\n // STICKY: the expanded rail's rows scroll inside the capsule (overflow-y),\r\n // and the pill must stay pinned at the top instead of scrolling away with\r\n // them — sticky + an opaque inherited background keeps the labels reading\r\n // cleanly over passing rows.\r\n '.crl_favToggle{position:sticky;top:0;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}',\r\n '.crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}',\r\n '.crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}',\r\n '.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}',\r\n '.crl_favToggleLabel{display:none}',\r\n '.crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}',\r\n 'body[data-ds-dark-theme] .crl_favToggle,[data-theme=\\'dark\\'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}',\r\n '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)}',\r\n '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)}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}',\r\n].join('')\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n tipImgs: 'crl_tipImgs',\r\n tipImgWrap: 'crl_tipImgWrap',\r\n tipImg: 'crl_tipImg',\r\n tipImgCount: 'crl_tipImgCount',\r\n tipImgPh: 'crl_tipImgPh',\r\n tipBadge: 'crl_tipBadge',\r\n favStar: 'crl_favStar',\r\n favItem: 'crl_favItem',\r\n favToggle: 'crl_favToggle',\r\n favToggleLabel: 'crl_favToggleLabel',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\n// ---- favorites (persisted per-session bookmark store) ----\r\n// Shape: Record<sessionId, messageId[]>. The durable message id is the same\r\n// identity the host projection emits (`id`) and the DOM anchor encodes\r\n// (`13:input-message<id>`), so DOM-injected buttons and the rail agree on\r\n// the key with no projection hop.\r\n// 持久化(2026-08-27):host 文件(~/.dsh/chat-rail-favorites.json,经\r\n// /chat-rail/api/favorites GET/PUT)——localStorage 按 origin 隔离,思灵 DSH\r\n// web 端口每次启动随机 → 跨重启丢弃。localStorage 仅作旧数据一次性迁移。\r\nconst FAVORITES_KEY = '@max-null/dsh-chat-rail:favorites'\r\ntype FavoritesMap = Record<string, string[]>\r\n\r\nconst FAVORITES_API = '/chat-rail/api/favorites'\r\n/** 模块级快照缓存(host 加载后充当源 truth)+ 订阅者集合。 */\r\nlet favoritesCache: FavoritesMap | null = null\r\nconst favoritesListeners = new Set<() => void>()\r\n\r\n/** 从 localStorage 读旧数据(迁移用/离线兜底)。 */\r\nfunction readFavoritesLocal(): FavoritesMap {\r\n if (typeof localStorage === 'undefined') return {}\r\n try {\r\n const raw = localStorage.getItem(FAVORITES_KEY)\r\n if (raw === null) return {}\r\n const value: unknown = JSON.parse(raw)\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nasync function fetchFavoritesMap(): Promise<FavoritesMap> {\r\n try {\r\n const res = await fetch(FAVORITES_API, { headers: { Accept: 'application/json' } })\r\n if (!res.ok) return {}\r\n const data = await res.json() as { ok?: boolean, value?: unknown }\r\n const value = data?.value\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\n/** 模块加载时同步 host;旧 localStorage 数据迁移一次(host 为空且本地有值)。 */\r\nvoid (async () => {\r\n const host = await fetchFavoritesMap()\r\n if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {\r\n const legacy = readFavoritesLocal()\r\n if (Object.keys(host).length === 0 && Object.keys(legacy).length > 0) {\r\n await fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: legacy }),\r\n }).catch(() => {})\r\n }\r\n try { localStorage.removeItem(FAVORITES_KEY) } catch { /* 忽略 */ }\r\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\r\n favoritesCache = Object.keys(host).length === 0 ? legacy : host\r\n // host 就位后通知快照订阅(rail/收藏按钮重渲染)\r\n for (const listener of favoritesListeners) listener()\r\n } else {\r\n favoritesCache = host\r\n }\r\n})()\r\n\r\n/** Read the favorites map(内存镜像;未加载完成时回退 localStorage)。 */\r\nexport function readFavorites(): FavoritesMap {\r\n if (favoritesCache !== null) return favoritesCache\r\n return readFavoritesLocal()\r\n}\r\n\r\n/** Write the favorites map;更新内存并 fire-and-forget 持久化到 host(host-only)。 */\r\nfunction writeFavorites(map: FavoritesMap): void {\r\n favoritesCache = map\r\n void fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: map }),\r\n }).catch(() => { /* 网络失败:内存仍在用(下次写会重试) */ })\r\n}\r\n\r\n/** Normalized id list for one session (drops non-string entries). */\r\nexport function favoriteIdsOf(map: FavoritesMap, sessionId: string): string[] {\r\n const list = map[sessionId]\r\n return Array.isArray(list) ? list.filter((id): id is string => typeof id === 'string') : []\r\n}\r\n\r\n/** Toggle one id in a list (stable new array; no in-place mutation). */\r\nexport function toggleFavoriteId(list: string[], id: string): string[] {\r\n return list.includes(id) ? list.filter((candidate) => candidate !== id) : [...list, id]\r\n}\r\n\r\n/** Whether one message is favorited in one session. */\r\nexport function isFavorite(map: FavoritesMap, sessionId: string, messageId: string): boolean {\r\n return favoriteIdsOf(map, sessionId).includes(messageId)\r\n}\r\n\r\n/** Stable snapshot for useSyncExternalStore (loaded once, cached until toggle). */\r\nfunction favoritesSnapshot(): FavoritesMap {\r\n if (favoritesCache === null) favoritesCache = readFavorites()\r\n return favoritesCache\r\n}\r\n\r\n/** Subscribe to favorites changes; returns the unsubscribe. */\r\nfunction subscribeFavorites(listener: () => void): () => void {\r\n favoritesListeners.add(listener)\r\n return () => { favoritesListeners.delete(listener) }\r\n}\r\n\r\n/** Toggle one message's favorite state: snapshot → new map → persist → notify. */\r\nfunction toggleFavorite(sessionId: string, messageId: string): void {\r\n const next = { ...favoritesSnapshot() }\r\n next[sessionId] = toggleFavoriteId(favoriteIdsOf(favoritesSnapshot(), sessionId), messageId)\r\n favoritesCache = next\r\n writeFavorites(next)\r\n for (const listener of [...favoritesListeners]) listener()\r\n}\r\n\r\n/** Remember the messageId read off a DOM anchor key (`13:input-message<id>`). */\r\nexport function messageIdOfAnchorKey(key: string): string {\r\n return key.startsWith('13:input-message') ? key.slice('13:input-message'.length) : key\r\n}\r\n\r\n/** The favorite key of one rail message: the durable id when present (host\r\n * projection), otherwise the id reconstructed from the node anchor (the\r\n * loaded-node fallback path). */\r\nfunction favoriteIdOfMessage(m: Pick<RailMessage, 'id' | 'key'>): string {\r\n if (typeof m.id === 'string' && m.id !== '') return m.id\r\n if (typeof m.key === 'string') return messageIdOfAnchorKey(m.key)\r\n return ''\r\n}\r\n\r\n// ---- message-action injector (DOM) ----\r\n// User-message rows have no official action slot (assistant-actions serves\r\n// assistant turns only), so the star (favorite) and plus (fill-to-input)\r\n// buttons are injected next to the copy button of each user row. The rows\r\n// are located by their anchor key so history paging and re-renders both\r\n// land; the injector is a MutationObserver that re-scans after any DOM\r\n// change and never double-injects (row marker).\r\n//\r\n// Runtime wiring lives in a module-level context the rail component updates\r\n// every render (session id + handlers), so the pure DOM buttons stay tiny:\r\n// they only read data attributes and call context handlers.\r\nconst actionCtx: {\r\n sessionId: string | undefined\r\n onToggleFavorite: (messageId: string) => void\r\n onFill: (messageId: string) => void\r\n} = {\r\n sessionId: undefined,\r\n onToggleFavorite: () => {},\r\n onFill: () => {},\r\n}\r\n\r\n/** aria-label values of the copy button across shipped locales (button sits\r\n * in the user row's IconActions; its parent is the row's action host). */\r\nconst COPY_ARIA_LABELS = new Set(['复制', 'Copy', '已复制', 'Copied'])\r\n\r\n/** The copy button inside one user message row (null until rendered). */\r\nfunction copyButtonOf(row: HTMLElement): HTMLButtonElement | null {\r\n for (const button of row.querySelectorAll<HTMLButtonElement>('button')) {\r\n const label = button.getAttribute('aria-label')\r\n if (label !== null && COPY_ARIA_LABELS.has(label)) return button\r\n }\r\n return null\r\n}\r\n\r\n/** Star SVG path (milestone-compatible glyph, 24-unit viewBox). */\r\nconst STAR_PATH = 'm12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'\r\n\r\n/** Build the favorite (star) button for one user message row. */\r\nfunction favoriteButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fav'\r\n button.dataset.crlStar = messageId\r\n const initialLabel = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? lang.unfav : lang.fav\r\n button.setAttribute('aria-pressed', String(isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId)))\r\n button.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n button.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n // The CSS tooltip reads data-tip (a short dark pill like the host Tooltip);\r\n // the native title balloon is intentionally absent — double tips would\r\n // stack a browser bubble over the styled one.\r\n button.setAttribute('data-tip', initialLabel)\r\n button.setAttribute('aria-label', initialLabel)\r\n button.innerHTML = `<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"${STAR_PATH}\"/></svg>`\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n // Read the id off the dataset at click time: the injector may refresh the\r\n // button for a new session and the closure must stay current.\r\n const id = button.dataset.crlStar\r\n if (id !== undefined) {\r\n actionCtx.onToggleFavorite(id)\r\n refreshFavoriteButton(button, lang)\r\n }\r\n })\r\n return button\r\n}\r\n\r\n/** Build the fill-to-input (plus) button for one user message row. */\r\nfunction fillButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fill'\r\n button.dataset.crlFill = messageId\r\n button.setAttribute('data-tip', lang.fill)\r\n button.setAttribute('aria-label', lang.fill)\r\n button.innerHTML = '<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>'\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n const id = button.dataset.crlFill\r\n if (id !== undefined) actionCtx.onFill(id)\r\n })\r\n return button\r\n}\r\n\r\n/** Sync one star button to its message's current favorite state. */\r\nfunction refreshFavoriteButton(button: HTMLButtonElement, lang: Record<string, string>): void {\r\n const id = button.dataset.crlStar\r\n if (id === undefined) return\r\n const starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', id)\r\n button.setAttribute('aria-pressed', String(starred))\r\n button.dataset.starred = starred ? 'true' : undefined\r\n button.classList.toggle('crl_on', starred)\r\n const label = starred ? lang.unfav : lang.fav\r\n // Both the a11y label and the styled tooltip follow the new state.\r\n button.setAttribute('data-tip', label)\r\n button.setAttribute('aria-label', label)\r\n}\r\n\r\n/** Refresh every injected star button (session switch / external toggle). */\r\nfunction refreshAllFavoriteButtons(lang: Record<string, string>): void {\r\n for (const button of document.querySelectorAll<HTMLButtonElement>('[data-crl-star]')) {\r\n refreshFavoriteButton(button, lang)\r\n }\r\n}\r\n\r\n/** Inject star + plus buttons into one user message row (idempotent). */\r\nfunction injectMessageActions(row: HTMLElement, lang: Record<string, string>): void {\r\n // Already injected and both buttons still present → leave it alone.\r\n if (row.dataset.crlActions === '1') {\r\n if (row.querySelector('[data-crl-star]') !== null && row.querySelector('[data-crl-fill]') !== null) return\r\n // React re-rendered the row and replaced the icons: fall through to re-inject.\r\n }\r\n const copy = copyButtonOf(row)\r\n const messageId = messageIdOfAnchorKey(row.getAttribute('data-chat-anchor-key') ?? '')\r\n if (copy === null || messageId === '') return\r\n const star = favoriteButtonOf(messageId, lang)\r\n star.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n star.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n const fill = fillButtonOf(messageId, lang)\r\n copy.before(star, fill)\r\n row.dataset.crlActions = '1'\r\n}\r\n\r\n/** Start observing new user rows so injected actions follow history paging. */\r\nfunction startActionInjector(lang: Record<string, string>): () => void {\r\n const scan = () => {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-chat-anchor-key^=\"13:input-message\"]')) {\r\n injectMessageActions(row, lang)\r\n }\r\n }\r\n const observer = new MutationObserver(scan)\r\n if (typeof document !== 'undefined' && document.body !== null) {\r\n observer.observe(document.body, { childList: true, subtree: true })\r\n }\r\n scan()\r\n return () => observer.disconnect()\r\n}\r\n\r\ninterface RailImage {\r\n attachmentId: string\r\n mediaType: string\r\n width: number\r\n height: number\r\n}\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n /** Stored-image references from the host projection (empty when inline-only). */\r\n images?: RailImage[]\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** One displayable tip image: either a durable reference (resolved lazily) or\r\n * an inline base64 payload already usable as an <img> src. */\r\ntype ImageSpec =\r\n | { kind: 'ref'; attachmentId: string; mediaType: string }\r\n | { kind: 'data'; src: string }\r\n\r\n/** Extract stored-image references from a ContentBlock list (reference form). */\r\nfunction nodeImagesOf(content: unknown): RailImage[] {\r\n if (!Array.isArray(content)) return []\r\n const out: RailImage[] = []\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a === null || typeof a !== 'object') continue\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown; width?: unknown; height?: unknown }\r\n if (typeof ref.attachmentId !== 'string' || ref.attachmentId === '') continue\r\n out.push({\r\n attachmentId: ref.attachmentId,\r\n mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png',\r\n width: typeof ref.width === 'number' ? ref.width : 0,\r\n height: typeof ref.height === 'number' ? ref.height : 0,\r\n })\r\n }\r\n return out\r\n}\r\n\r\n/** Convert a ContentBlock list to displayable image specs; inline base64\r\n * blocks (rare in replayed history) become data URLs on the spot. */\r\nfunction imageSpecsOfContent(content: unknown): ImageSpec[] {\r\n const specs: ImageSpec[] = []\r\n if (!Array.isArray(content)) return specs\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown; data?: unknown; mediaType?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a !== null && typeof a === 'object') {\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown }\r\n if (typeof ref.attachmentId === 'string' && ref.attachmentId !== '') {\r\n specs.push({ kind: 'ref', attachmentId: ref.attachmentId, mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png' })\r\n continue\r\n }\r\n }\r\n if (typeof b.data === 'string' && b.data !== '') {\r\n specs.push({ kind: 'data', src: `data:${typeof b.mediaType === 'string' ? b.mediaType : 'image/png'};base64,${b.data}` })\r\n }\r\n }\r\n return specs\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n const images = Array.isArray(o.images)\r\n ? o.images.map(nodeImagesOfEntry).filter((i): i is RailImage => i !== null)\r\n : undefined\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(images !== undefined && images.length > 0 ? { images } : {}),\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Normalize one projection entry of the `images` array (wire form). */\r\nfunction nodeImagesOfEntry(v: unknown): RailImage | null {\r\n if (v === null || typeof v !== 'object') return null\r\n const o = v as Record<string, unknown>\r\n if (typeof o.attachmentId !== 'string' || o.attachmentId === '') return null\r\n return {\r\n attachmentId: o.attachmentId,\r\n mediaType: typeof o.mediaType === 'string' ? o.mediaType : 'image/png',\r\n width: typeof o.width === 'number' ? o.width : 0,\r\n height: typeof o.height === 'number' ? o.height : 0,\r\n }\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\r\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\r\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\r\n if (!chat.nodes) return out\r\n for (const node of chat.nodes.values()) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\r\n if (n.kind !== 'user') continue\r\n const data = n.data as { time?: unknown; content?: unknown } | null\r\n if (data === null || typeof data !== 'object') continue\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) continue\r\n const images = nodeImagesOf(data.content)\r\n out.push({\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: data.time,\r\n text: userTextOf(data.content),\r\n hasImage: userHasImage(data.content),\r\n ...(images.length > 0 ? { images } : {}),\r\n key,\r\n })\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\n/** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\nfunction fullTextOf(m: RailMessage, nodeSnapshot: unknown): string {\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\r\n const content = node?.data?.content\r\n if (Array.isArray(content)) {\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n const full = out.trim()\r\n if (full !== '') return full\r\n }\r\n return m.text\r\n}\r\n\r\n/** Displayable tip images for one rail message: host projection references\r\n * first (works even before the node window covers the message), then the\r\n * loaded chat node's content blocks. */\r\nfunction tipImagesOf(m: RailMessage, nodeSnapshot: unknown): ImageSpec[] {\r\n if (Array.isArray(m.images) && m.images.length > 0) {\r\n return m.images.map(img => ({ kind: 'ref', attachmentId: img.attachmentId, mediaType: img.mediaType }))\r\n }\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\r\n return imageSpecsOfContent(node?.data?.content)\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\r\n * in a long session needs many pages. Report page count through onProgress so\r\n * the UI can show a loading state instead of appearing frozen.\r\n *\r\n * The paged-in data lands in the session snapshot synchronously with\r\n * loadOlder's resolution, but the chat rows render through React — the DOM row\r\n * for the target key appears a beat later. We therefore poll for the row (with\r\n * a timeout) instead of querying once and giving up; otherwise a multi-page\r\n * jump would \"finish loading\" without scrolling and need a second click.\r\n */\r\nasync function jumpToMessage(\r\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\r\n sessionId: string,\r\n key: string,\r\n onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n let pages = 0\r\n let guard = 0\r\n let loaded = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (snapshot?.chat?.nodes?.get(key) !== undefined) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\r\n return true\r\n}\r\n\r\n// ---- tip image thumbnails ----\r\n\r\n/** Session-scoped browser URL cache for resolved attachment bytes (one entry\r\n * per session:attachment; a failed load evicts itself so the next hover\r\n * retries instead of showing a permanently broken thumbnail). */\r\nconst thumbUrls = new Map<string, Promise<string>>()\r\n\r\n/** Minimal RpcResult shape of session.readAttachment (contract level). */\r\ninterface ReadAttachmentResult {\r\n ok: boolean\r\n value?: { attachment?: { mediaType?: string }; data?: Uint8Array }\r\n error?: { code?: string; message?: string }\r\n}\r\n\r\ntype ReadAttachmentFn = (attachmentId: string) => Promise<ReadAttachmentResult>\r\n\r\n/** Fallback data URL when the browser cannot mint object URLs. */\r\nfunction bytesToDataUrl(data: Uint8Array, mediaType: string): string {\r\n let binary = ''\r\n const chunk = 0x8000\r\n for (let offset = 0; offset < data.length; offset += chunk) {\r\n binary += String.fromCharCode(...data.subarray(offset, offset + chunk))\r\n }\r\n return `data:${mediaType};base64,${btoa(binary)}`\r\n}\r\n\r\n/** Resolve (and cache) one stored image to a displayable URL. */\r\nfunction resolveThumb(sessionId: string, read: ReadAttachmentFn, image: Pick<RailImage, 'attachmentId' | 'mediaType'>): Promise<string> {\r\n const key = `${sessionId}:${image.attachmentId}`\r\n let pending = thumbUrls.get(key)\r\n if (pending === undefined) {\r\n pending = read(image.attachmentId).then((result): string => {\r\n if (!result.ok || result.value === undefined) {\r\n throw new Error(result.error?.message ?? result.error?.code ?? 'readAttachment failed')\r\n }\r\n const data = result.value.data\r\n if (data === undefined) throw new Error('readAttachment resolved no bytes')\r\n const mediaType = result.value.attachment?.mediaType ?? image.mediaType\r\n if (typeof URL.createObjectURL === 'function') {\r\n return URL.createObjectURL(new Blob([data as BlobPart], { type: mediaType }))\r\n }\r\n return bytesToDataUrl(data, mediaType)\r\n })\r\n void pending.catch(() => { thumbUrls.delete(key) })\r\n thumbUrls.set(key, pending)\r\n }\r\n return pending\r\n}\r\n\r\n/** One tip thumbnail: resolves its attachment lazily, shows a pulsing\r\n * placeholder while loading, and removes itself on failure. */\r\nfunction TipThumb({ spec, sessionId, read }: {\r\n spec: ImageSpec\r\n sessionId: string\r\n read: ReadAttachmentFn\r\n}): ReactNode {\r\n const [src, setSrc] = useState<string | null>(null)\r\n const [failed, setFailed] = useState(false)\r\n const specKey = spec.kind === 'ref' ? `ref:${spec.attachmentId}` : 'data'\r\n useEffect(() => {\r\n let alive = true\r\n setSrc(null)\r\n setFailed(false)\r\n if (spec.kind === 'data') {\r\n setSrc(spec.src)\r\n return () => { alive = false }\r\n }\r\n resolveThumb(sessionId, read, spec)\r\n .then((url) => { if (alive) setSrc(url) })\r\n .catch(() => { if (alive) setFailed(true) })\r\n return () => { alive = false }\r\n // spec is a fresh object each render; the string key is the stable identity.\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [specKey, sessionId, read])\r\n if (failed) return null\r\n if (src === null) return createElement('div', { className: S.tipImgPh, 'aria-hidden': true })\r\n return createElement('img', { className: S.tipImg, src, alt: '', draggable: false })\r\n}\r\n\r\n// ---- component ----\r\ninterface TimelineRailProps {\r\n useProjection: (key: string) => { messages?: unknown[] } | undefined\r\n sessionId?: SessionId\r\n sessionsService: ISessions\r\n /** Draft write + attachment-add face (session-scope framework injection). */\r\n inputActions?: {\r\n setDraft(text: string): void\r\n addImages(ids: readonly string[]): boolean\r\n } | undefined\r\n /** Runtime draft-image registry (hosted by ui-conversation). */\r\n conversation?: {\r\n createDraftImages(files: readonly File[]): readonly { id: string }[]\r\n } | undefined\r\n}\r\n\r\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\r\nfunction langStrings(): Record<string, string> {\r\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\r\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\r\n}\r\n\r\nfunction TimelineRail({ useProjection, sessionId, sessionsService, inputActions, conversation }: TimelineRailProps): ReactNode {\r\n const t = langStrings()\r\n const projected = useProjection('chatRail')\r\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\r\n const fallbackStore = session === undefined ? NOOP_STORE : session\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n // Favorites: per-session persisted set + a \"bookmarks only\" filter that\r\n // narrows the rail to favorited messages (mirrors dsh-milestone's\r\n // bookmarksOnly toggle; the DOM-injected buttons share the same store).\r\n const favorites = useSyncExternalStore(subscribeFavorites, favoritesSnapshot)\r\n const [favOnly, setFavOnly] = useState(false)\r\n // Count of favorited messages in THIS session. The filter toggle is only\r\n // worth showing when a favorite exists — with zero favorites there is\r\n // nothing to filter to, so the pill disappears entirely.\r\n const favCount = sessionId === undefined\r\n ? 0\r\n : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length\r\n const showFavToggle = favCount > 0\r\n const effectiveMessages = favOnly\r\n ? messages.filter((m) => sessionId !== undefined && isFavorite(favorites, sessionId, favoriteIdOfMessage(m)))\r\n : messages\r\n\r\n // If the filter is armed and the last favorite is un-starred (or the\r\n // session switched), the filtered list would be empty AND the toggle would\r\n // vanish — leaving a rail with nothing to un-filter. Auto-release the\r\n // filter instead so the full list returns.\r\n useEffect(() => {\r\n if (favOnly && !showFavToggle) setFavOnly(false)\r\n }, [favOnly, showFavToggle])\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null) // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Last observed pointer position over the rail. A mouseenter that arrives\r\n // before the width transition cannot show the tip (rects unstable), yet the\r\n // enter event is already gone by the time the animation ends — the settle\r\n // handler re-hit-tests the cursor against this position to show the tip for\r\n // the node still under the pointer.\r\n const lastPointerRef = useRef<{ x: number; y: number } | null>(null)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n // Track the cursor over the rail for the expand-settled tip re-hit-test\r\n // (the `show` effect's settle handler reads lastPointerRef). Native\r\n // listener: plain DOM events keep the callback typed without React\r\n // synthetic-event generics in the hand-written createElement props.\r\n useEffect(() => {\r\n const el = navRef.current\r\n if (el === null) return\r\n const onMove = (e: PointerEvent) => { lastPointerRef.current = { x: e.clientX, y: e.clientY } }\r\n el.addEventListener('pointermove', onMove, { passive: true })\r\n return () => el.removeEventListener('pointermove', onMove)\r\n }, [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n let settled = false\r\n const settle = () => {\r\n if (settled) return\r\n settled = true\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n // Re-hit-test the cursor: a mouseenter that fired before the width\r\n // transition could not show the tip (rects were still unstable), and\r\n // the enter event is long gone once the animation ends. If the pointer\r\n // still sits on a rail item, position that item's tip now.\r\n const p = lastPointerRef.current\r\n if (p !== null) {\r\n const hit = document.elementFromPoint(p.x, p.y)\r\n const item = hit === null ? null : hit.closest('[data-crl-index]')\r\n const idx = item === null ? -1 : Number(item.getAttribute('data-crl-index'))\r\n if (Number.isInteger(idx) && idx >= 0) positionTip(idx)\r\n }\r\n }\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n settle()\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n // Fallback for `prefers-reduced-motion: reduce` (the stylesheet disables\r\n // the width transition, so `transitionend` never fires) and for any lost\r\n // event: settle after the transition duration regardless.\r\n const timer = setTimeout(settle, 300)\r\n return () => {\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n }\r\n }, [show])\r\n\r\n // Session-scoped attachment reader for tip thumbnails (stable identity so\r\n // TipThumb effects do not re-run every render; resolveThumb caches anyway).\r\n const readThumb = useMemo<ReadAttachmentFn | undefined>(() => {\r\n if (sessionId === undefined) return undefined\r\n const s = (sessionsService.binding(sessionId) as unknown as\r\n { session?: { readAttachment?: ReadAttachmentFn } } | undefined)?.session\r\n return s?.readAttachment === undefined\r\n ? undefined\r\n : (id: string) => s.readAttachment!(id)\r\n }, [sessionId, sessionsService])\r\n\r\n // Fill-to-input (plus button): put the message's full text into the draft\r\n // and re-attach its stored images through the official draft-image path\r\n // (readAttachment → File → createDraftImages → addImages). Degrades to\r\n // text-only when the conversation service or storage read is unavailable.\r\n const fillToInput = useCallback((messageId: string) => {\r\n const m = messages.find((candidate) => favoriteIdOfMessage(candidate) === messageId)\r\n if (m === undefined || inputActions === undefined || sessionId === undefined) return\r\n const text = fullTextOf(m, nodeSnapshot) || m.text\r\n if (inputActions.setDraft !== undefined) inputActions.setDraft(text)\r\n const specs = tipImagesOf(m, nodeSnapshot).filter((spec): spec is ImageSpec & { kind: 'ref' } => spec.kind === 'ref')\r\n if (specs.length === 0 || conversation === undefined || readThumb === undefined) return\r\n void (async () => {\r\n const files: File[] = []\r\n for (const spec of specs) {\r\n try {\r\n const result = await readThumb(spec.attachmentId) as unknown as { ok?: boolean; value?: { attachment?: { mediaType?: string }; data?: Uint8Array } }\r\n if (result?.ok !== true || result.value?.data === undefined) continue\r\n const mediaType = result.value.attachment?.mediaType ?? spec.mediaType\r\n files.push(new File([result.value.data as BlobPart], `attachment-${spec.attachmentId}${mediaType.includes('jpeg') ? '.jpg' : '.png'}`, { type: mediaType }))\r\n } catch {\r\n // One failed attachment must not block the others.\r\n }\r\n }\r\n if (files.length === 0) return\r\n const drafts = conversation.createDraftImages(files)\r\n const ids = drafts.map((draft) => draft.id).filter((id): id is string => id !== undefined)\r\n if (ids.length > 0) {\r\n try { inputActions.addImages?.(ids) } catch { /* draft-image registry may reject; text fill already landed */ }\r\n }\r\n })()\r\n }, [conversation, inputActions, messages, nodeSnapshot, readThumb, sessionId])\r\n\r\n // Bridge the DOM-injected buttons (module-level actionCtx) to this\r\n // component's live closures + session id; refresh star states on every\r\n // favorites change and on session switch.\r\n useEffect(() => {\r\n actionCtx.sessionId = sessionId\r\n actionCtx.onToggleFavorite = (messageId) => {\r\n if (sessionId !== undefined) toggleFavorite(sessionId, messageId)\r\n }\r\n actionCtx.onFill = fillToInput\r\n refreshAllFavoriteButtons(t)\r\n return () => {\r\n // Only clear what this render set: a newer session may already own the ctx.\r\n if (actionCtx.sessionId === sessionId) actionCtx.sessionId = undefined\r\n }\r\n }, [sessionId, fillToInput, t])\r\n\r\n // Keep the injected DOM buttons in sync with external favorite toggles\r\n // (rail star / another session's store write).\r\n useEffect(() => {\r\n refreshAllFavoriteButtons(t)\r\n }, [favorites, t])\r\n\r\n // Inject star + plus buttons into every user message row; observe DOM\r\n // changes so history paging and re-renders keep them present.\r\n useEffect(() => {\r\n if (typeof document === 'undefined' || document.body === null) return\r\n return startActionInjector(t)\r\n }, [t])\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (effectiveMessages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < effectiveMessages.length; i++) {\r\n const key = anchorKeyOf(effectiveMessages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, effectiveMessages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n // The rail hides on tiny sessions (fewer than 2 user messages) — but the\r\n // count is the UNFILTERED list: the favorites-only filter may legitimately\r\n // narrow to a single row, and hiding the rail then would strand the user\r\n // with no way to switch the filter back off.\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n // Fav-only filter remaps the tip index into the FULL list (tip content and\r\n // hover data always come from the unfiltered messages array).\r\n const tipIndex = tip === null\r\n ? -1\r\n : favOnly\r\n ? messages.findIndex((m) => effectiveMessages[tip.index] !== undefined\r\n && favoriteIdOfMessage(m) === favoriteIdOfMessage(effectiveMessages[tip.index]))\r\n : tip.index\r\n\r\n const items = effectiveMessages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n const starred = favoriteIdOfMessage(m) !== '' && isFavorite(favorites, sessionId, favoriteIdOfMessage(m))\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : '') + (starred ? ` ${S.favItem}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(sessionsService as never, sessionId as string, key, undefined, controller.signal)\r\n .finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n starred\r\n ? createElement('span', { className: S.favStar, role: 'img', 'aria-label': t.fav, 'aria-hidden': undefined },\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 13,\r\n height: 13,\r\n fill: 'currentColor',\r\n stroke: 'none',\r\n 'aria-hidden': true,\r\n },\r\n createElement('path', { d: STAR_PATH, 'aria-hidden': true })))\r\n : null,\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage\r\n ? createElement('span', { className: S.img, role: 'img', 'aria-label': t.hasImage },\r\n createElement('svg', {\r\n viewBox: '0 0 16 16',\r\n width: 12,\r\n height: 12,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.4,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('rect', { x: 1.7, y: 2.7, width: 12.6, height: 10.6, rx: 2.2 }),\r\n createElement('circle', { cx: 5.6, cy: 6.4, r: 1.15, fill: 'currentColor', stroke: 'none' }),\r\n createElement('path', { d: 'M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8' })))\r\n : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n // Favorites-only filter: a star pill pinned at the top of the rail\r\n // (sticky; it never scrolls away with the rows). On state fills the\r\n // star (yellow) and narrows the rail to favorited messages; the label\r\n // (\"bookmarks only\") only shows once the rail expands — the collapsed\r\n // 36px capsule keeps a clean round icon. Hidden entirely when this\r\n // session has no favorites (nothing to filter to).\r\n showFavToggle\r\n ? createElement('button', {\r\n type: 'button',\r\n key: 'favToggle',\r\n className: S.favToggle + (favOnly ? ' crl_on' : ''),\r\n 'aria-pressed': favOnly,\r\n 'aria-label': t.favOnly,\r\n title: t.favOnly,\r\n onMouseEnter: (e: MouseEvent) => e.stopPropagation(),\r\n onClick: () => setFavOnly((v) => !v),\r\n children: [\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 14,\r\n height: 14,\r\n fill: favOnly ? 'currentColor' : 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 2,\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n }, createElement('path', { d: STAR_PATH })),\r\n createElement('span', { className: S.favToggleLabel }, t.favOnly),\r\n ],\r\n })\r\n : null,\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item. Images\r\n // stack above the text; a message whose image cannot be resolved (no\r\n // projection refs and unloaded node) shows a quiet \"has image\" badge.\r\n tip !== null && tipIndex >= 0 && tipIndex < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, (() => {\r\n const m = messages[tipIndex]\r\n const specs = tipImagesOf(m, nodeSnapshot)\r\n const children: ReactNode[] = []\r\n if (specs.length > 0 && readThumb !== undefined) {\r\n // Only the first image renders; the rest collapse into a \"+N\"\r\n // badge so a many-image message cannot push the tip past its\r\n // viewport cap (the stacked thumbnails did).\r\n children.push(createElement('div', { className: S.tipImgs, key: 'imgs' },\r\n createElement('div', { className: S.tipImgWrap, key: 'img' },\r\n createElement(TipThumb, { spec: specs[0], sessionId: sessionId as string, read: readThumb }),\r\n specs.length > 1\r\n ? createElement('span', { className: S.tipImgCount, 'aria-hidden': true }, `+${specs.length - 1}`)\r\n : null)))\r\n } else if (m.hasImage) {\r\n children.push(createElement('span', { className: S.tipBadge, key: 'badge' }, t.hasImage))\r\n }\r\n children.push(createElement('span', { key: 'text' }, fullTextOf(m, nodeSnapshot) || t.noText))\r\n return children\r\n })())\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\r\n name: 'conversation.input.dock',\r\n id: 'chat-rail',\r\n order: 40,\r\n inject: () => ({\r\n sessionsService: ctx.sessions,\r\n // createDraftImages lives on the concrete ConversationController, not\r\n // the outward IConversation face; the runtime service is always the\r\n // controller, so the cast is structural, never a feature guess.\r\n conversation: ctx.conversation as unknown as { createDraftImages(files: readonly File[]): readonly { id: string }[] },\r\n }),\r\n }, TimelineRail))\r\n}\r\n\r\nexport { apply, TimelineRail, imageSpecsOfContent, normalize }\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","createElement","useSyncExternalStore","useRef","useMemo","useCallback","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions', 'conversation']\r\n\r\n// ---- i18n (DSH locale-aware, zh/en) ----\r\ntype LocaleId = 'zh' | 'en'\r\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\r\n zh: {\r\n railLabel: '消息导航',\r\n roleUser: '用户',\r\n noText: '(无文本内容)',\r\n ariaJump: '跳转到消息',\r\n hasImage: '含图片',\r\n loading: '加载中…',\r\n timeJustNow: '刚刚',\r\n timeMinutes: '{n}分钟前',\r\n timeHours: '{n}小时前',\r\n timeDays: '{n}天前',\r\n fav: '收藏消息',\r\n unfav: '取消收藏',\r\n fill: '填充到输入框',\r\n filled: '已填入输入框',\r\n favOnly: '只显示收藏',\r\n },\r\n en: {\r\n railLabel: 'Message rail',\r\n roleUser: 'User',\r\n noText: '(no text)',\r\n ariaJump: 'Jump to message',\r\n hasImage: 'Has image',\r\n loading: 'Loading…',\r\n timeJustNow: 'just now',\r\n timeMinutes: '{n}m ago',\r\n timeHours: '{n}h ago',\r\n timeDays: '{n}d ago',\r\n fav: 'Bookmark message',\r\n unfav: 'Remove bookmark',\r\n fill: 'Fill into input',\r\n filled: 'Filled into input',\r\n favOnly: 'Bookmarks only',\r\n },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.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:thin;scrollbar-color:rgba(0,0,0,.18) transparent;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}',\r\n '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);scrollbar-color:rgba(255,255,255,.25) transparent}',\r\n '.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)}',\r\n '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)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.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}',\r\n '.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}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.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}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n // Image badge: hidden in the collapsed state (the rail is a clean 36px\r\n // capsule of indicator lines; an icon there would crowd and distract) and\r\n // revealed with the other labels on expand. A quiet stroke glyph in a muted\r\n // tone that ignores the active-blue text color.\r\n '.crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}',\r\n '.crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_img,[data-theme=\\'dark\\'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n '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)}',\r\n '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)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.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}',\r\n '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)))}',\r\n // Tip thumbnail gallery: stacked above the text, one row per image, capped\r\n // height so a tall screenshot cannot swallow the preview panel. The\r\n // placeholder pulses while session.readAttachment resolves the bytes; a\r\n // failed load removes the row entirely rather than showing a broken image.\r\n '.crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}',\r\n '.crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}',\r\n '.crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}',\r\n '.crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}',\r\n '.crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}',\r\n 'body[data-ds-dark-theme] .crl_tipImg,[data-theme=\\'dark\\'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '.crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}',\r\n '@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}',\r\n // Message action buttons injected next to the copy button (DOM row: the\r\n // user-message IconActions row). The host rows use 28px round icon-action\r\n // buttons (padding 6, 16px glyphs, DSW token colors, hover fill) — the\r\n // injected buttons mirror the exact geometry and token surface so the star\r\n // and plus read as native siblings of copy/branch. The star lights\r\n // yellowish (#ffd166, milestone-compatible) once favorited.\r\n // Tooltip: same dark pill as the host Tooltip (black rounded label, no\r\n // native title balloon). The delay mirrors the host tooltip's settle; the\r\n // fade is short so the pill appears right beside the hovered button.\r\n '.crl_msgAct{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:6px;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,rgba(0,0,0,.42));cursor:pointer;transition:background .15s ease,color .15s ease}',\r\n '.crl_msgAct:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,rgba(0,0,0,.72))}',\r\n '.crl_msgAct:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}',\r\n '.crl_msgAct[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);padding:5px 10px;border-radius:8px;background:rgba(24,28,36,.94);color:rgba(255,255,255,.95);font-size:12px;line-height:1.4;white-space:nowrap;pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '.crl_msgAct[data-tip]:hover::before{content:\"\";position:absolute;left:50%;bottom:calc(100% + 3px);transform:translateX(-50%);border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid rgba(24,28,36,.94);pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}',\r\n '@keyframes crl-tip-in{to{opacity:1}}',\r\n '.crl_msgAct.crl_fav.crl_on{color:#ffd166}',\r\n '.crl_msgAct.crl_fav.crl_on svg{fill:currentColor}',\r\n '.crl_msgAct.crl_fav.crl_on:hover{color:#ffd166}',\r\n // Rail row favorite badge + yellow indicator line for favorited messages.\r\n '.crl_favStar{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-right:6px;color:#ffd166}',\r\n '.crl_show .crl_favStar{display:inline-flex;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_favItem .crl_line{background-color:#ffd166}',\r\n '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}',\r\n // A favorited line that is also the current reading position keeps its\r\n // yellow identity — the generic active glow is blue, which pairs miserably\r\n // with the gold line, so the favorite-active state recasts the glow in the\r\n // same warm tone.\r\n '.crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}',\r\n // Rail-top favorites-only toggle: a round icon pill in the collapsed state;\r\n // on expand it becomes a full-width label row (star + \"bookmarks only\") so\r\n // the control reads as a real rail header action instead of a stray dot.\r\n // STICKY: the expanded rail's rows scroll inside the capsule (overflow-y),\r\n // and the pill must stay pinned at the top instead of scrolling away with\r\n // them. The capsule has padding:10px 0 — sticky `top:0` would leave the\r\n // last 10px of the scrollport uncovered, letting a passing row peek through\r\n // above the pill. top:-10px shifts the pin edge to the capsule's true top\r\n // (padding included), so rows scroll fully underneath.\r\n '.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}',\r\n '.crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}',\r\n '.crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}',\r\n '.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}',\r\n '.crl_favToggleLabel{display:none}',\r\n '.crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}',\r\n 'body[data-ds-dark-theme] .crl_favToggle,[data-theme=\\'dark\\'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}',\r\n '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)}',\r\n '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)}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}',\r\n // 屏蔽官方 TurnNavigator(轮次导航竖轨,DSH 0.1.2-alpha.1+):官方 rail 与本\r\n // 插件 rail 同处会话面板右缘且只支持「轮次」维度,安装本插件后隐藏官方的,\r\n // 由本插件统一承担消息导航。锚点用 aria-label(zh/en 双文案)而非 CSS module\r\n // 类名(如 hkplfa_slot,hash 随构建内容漂移)。\r\n 'nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}',\r\n].join('')\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n tipImgs: 'crl_tipImgs',\r\n tipImgWrap: 'crl_tipImgWrap',\r\n tipImg: 'crl_tipImg',\r\n tipImgCount: 'crl_tipImgCount',\r\n tipImgPh: 'crl_tipImgPh',\r\n tipBadge: 'crl_tipBadge',\r\n favStar: 'crl_favStar',\r\n favItem: 'crl_favItem',\r\n favToggle: 'crl_favToggle',\r\n favToggleLabel: 'crl_favToggleLabel',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\n// ---- favorites (persisted per-session bookmark store) ----\r\n// Shape: Record<sessionId, messageId[]>. The durable message id is the same\r\n// identity the host projection emits (`id`) and the DOM anchor encodes\r\n// (`13:input-message<id>`), so DOM-injected buttons and the rail agree on\r\n// the key with no projection hop.\r\n// 持久化(2026-08-27):host 文件(~/.dsh/chat-rail-favorites.json,经\r\n// /chat-rail/api/favorites GET/PUT)——localStorage 按 origin 隔离,思灵 DSH\r\n// web 端口每次启动随机 → 跨重启丢弃。localStorage 仅作旧数据一次性迁移。\r\nconst FAVORITES_KEY = '@max-null/dsh-chat-rail:favorites'\r\ntype FavoritesMap = Record<string, string[]>\r\n\r\nconst FAVORITES_API = '/chat-rail/api/favorites'\r\n/** 模块级快照缓存(host 加载后充当源 truth)+ 订阅者集合。 */\r\nlet favoritesCache: FavoritesMap | null = null\r\nconst favoritesListeners = new Set<() => void>()\r\n\r\n/** 从 localStorage 读旧数据(迁移用/离线兜底)。 */\r\nfunction readFavoritesLocal(): FavoritesMap {\r\n if (typeof localStorage === 'undefined') return {}\r\n try {\r\n const raw = localStorage.getItem(FAVORITES_KEY)\r\n if (raw === null) return {}\r\n const value: unknown = JSON.parse(raw)\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nasync function fetchFavoritesMap(): Promise<FavoritesMap> {\r\n try {\r\n const res = await fetch(FAVORITES_API, { headers: { Accept: 'application/json' } })\r\n if (!res.ok) return {}\r\n const data = await res.json() as { ok?: boolean, value?: unknown }\r\n const value = data?.value\r\n return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as FavoritesMap : {}\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\n/** 模块加载时同步 host;旧 localStorage 数据迁移一次(host 为空且本地有值)。 */\r\nvoid (async () => {\r\n const host = await fetchFavoritesMap()\r\n if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {\r\n const legacy = readFavoritesLocal()\r\n if (Object.keys(host).length === 0 && Object.keys(legacy).length > 0) {\r\n await fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: legacy }),\r\n }).catch(() => {})\r\n }\r\n try { localStorage.removeItem(FAVORITES_KEY) } catch { /* 忽略 */ }\r\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\r\n favoritesCache = Object.keys(host).length === 0 ? legacy : host\r\n // host 就位后通知快照订阅(rail/收藏按钮重渲染)\r\n for (const listener of favoritesListeners) listener()\r\n } else {\r\n favoritesCache = host\r\n }\r\n})()\r\n\r\n/** Read the favorites map(内存镜像;未加载完成时回退 localStorage)。 */\r\nexport function readFavorites(): FavoritesMap {\r\n if (favoritesCache !== null) return favoritesCache\r\n return readFavoritesLocal()\r\n}\r\n\r\n/** Write the favorites map;更新内存并 fire-and-forget 持久化到 host(host-only)。 */\r\nfunction writeFavorites(map: FavoritesMap): void {\r\n favoritesCache = map\r\n void fetch(FAVORITES_API, {\r\n method: 'PUT', headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({ favorites: map }),\r\n }).catch(() => { /* 网络失败:内存仍在用(下次写会重试) */ })\r\n}\r\n\r\n/** Normalized id list for one session (drops non-string entries). */\r\nexport function favoriteIdsOf(map: FavoritesMap, sessionId: string): string[] {\r\n const list = map[sessionId]\r\n return Array.isArray(list) ? list.filter((id): id is string => typeof id === 'string') : []\r\n}\r\n\r\n/** Toggle one id in a list (stable new array; no in-place mutation). */\r\nexport function toggleFavoriteId(list: string[], id: string): string[] {\r\n return list.includes(id) ? list.filter((candidate) => candidate !== id) : [...list, id]\r\n}\r\n\r\n/** Whether one message is favorited in one session. */\r\nexport function isFavorite(map: FavoritesMap, sessionId: string, messageId: string): boolean {\r\n return favoriteIdsOf(map, sessionId).includes(messageId)\r\n}\r\n\r\n/** Stable snapshot for useSyncExternalStore (loaded once, cached until toggle). */\r\nfunction favoritesSnapshot(): FavoritesMap {\r\n if (favoritesCache === null) favoritesCache = readFavorites()\r\n return favoritesCache\r\n}\r\n\r\n/** Subscribe to favorites changes; returns the unsubscribe. */\r\nfunction subscribeFavorites(listener: () => void): () => void {\r\n favoritesListeners.add(listener)\r\n return () => { favoritesListeners.delete(listener) }\r\n}\r\n\r\n/** Toggle one message's favorite state: snapshot → new map → persist → notify. */\r\nfunction toggleFavorite(sessionId: string, messageId: string): void {\r\n const next = { ...favoritesSnapshot() }\r\n next[sessionId] = toggleFavoriteId(favoriteIdsOf(favoritesSnapshot(), sessionId), messageId)\r\n favoritesCache = next\r\n writeFavorites(next)\r\n for (const listener of [...favoritesListeners]) listener()\r\n}\r\n\r\n/** Remember the messageId read off a DOM anchor key (`13:input-message<id>`). */\r\nexport function messageIdOfAnchorKey(key: string): string {\r\n return key.startsWith('13:input-message') ? key.slice('13:input-message'.length) : key\r\n}\r\n\r\n/** The favorite key of one rail message: the durable id when present (host\r\n * projection), otherwise the id reconstructed from the node anchor (the\r\n * loaded-node fallback path). */\r\nfunction favoriteIdOfMessage(m: Pick<RailMessage, 'id' | 'key'>): string {\r\n if (typeof m.id === 'string' && m.id !== '') return m.id\r\n if (typeof m.key === 'string') return messageIdOfAnchorKey(m.key)\r\n return ''\r\n}\r\n\r\n// ---- message-action injector (DOM) ----\r\n// User-message rows have no official action slot (assistant-actions serves\r\n// assistant turns only), so the star (favorite) and plus (fill-to-input)\r\n// buttons are injected next to the copy button of each user row. The rows\r\n// are located by their anchor key so history paging and re-renders both\r\n// land; the injector is a MutationObserver that re-scans after any DOM\r\n// change and never double-injects (row marker).\r\n//\r\n// Runtime wiring lives in a module-level context the rail component updates\r\n// every render (session id + handlers), so the pure DOM buttons stay tiny:\r\n// they only read data attributes and call context handlers.\r\nconst actionCtx: {\r\n sessionId: string | undefined\r\n onToggleFavorite: (messageId: string) => void\r\n onFill: (messageId: string) => void\r\n} = {\r\n sessionId: undefined,\r\n onToggleFavorite: () => {},\r\n onFill: () => {},\r\n}\r\n\r\n/** aria-label values of the copy button across shipped locales (button sits\r\n * in the user row's IconActions; its parent is the row's action host). */\r\nconst COPY_ARIA_LABELS = new Set(['复制', 'Copy', '已复制', 'Copied'])\r\n\r\n/** The copy button inside one user message row (null until rendered). */\r\nfunction copyButtonOf(row: HTMLElement): HTMLButtonElement | null {\r\n for (const button of row.querySelectorAll<HTMLButtonElement>('button')) {\r\n const label = button.getAttribute('aria-label')\r\n if (label !== null && COPY_ARIA_LABELS.has(label)) return button\r\n }\r\n return null\r\n}\r\n\r\n/** Star SVG path (milestone-compatible glyph, 24-unit viewBox). */\r\nconst STAR_PATH = 'm12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'\r\n\r\n/** Build the favorite (star) button for one user message row. */\r\nfunction favoriteButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fav'\r\n button.dataset.crlStar = messageId\r\n const initialLabel = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? lang.unfav : lang.fav\r\n button.setAttribute('aria-pressed', String(isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId)))\r\n button.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n button.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n // The CSS tooltip reads data-tip (a short dark pill like the host Tooltip);\r\n // the native title balloon is intentionally absent — double tips would\r\n // stack a browser bubble over the styled one.\r\n button.setAttribute('data-tip', initialLabel)\r\n button.setAttribute('aria-label', initialLabel)\r\n button.innerHTML = `<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"${STAR_PATH}\"/></svg>`\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n // Read the id off the dataset at click time: the injector may refresh the\r\n // button for a new session and the closure must stay current.\r\n const id = button.dataset.crlStar\r\n if (id !== undefined) {\r\n actionCtx.onToggleFavorite(id)\r\n refreshFavoriteButton(button, lang)\r\n }\r\n })\r\n return button\r\n}\r\n\r\n/** Build the fill-to-input (plus) button for one user message row. */\r\nfunction fillButtonOf(messageId: string, lang: Record<string, string>): HTMLButtonElement {\r\n const button = document.createElement('button')\r\n button.type = 'button'\r\n button.className = 'crl_msgAct crl_fill'\r\n button.dataset.crlFill = messageId\r\n button.setAttribute('data-tip', lang.fill)\r\n button.setAttribute('aria-label', lang.fill)\r\n button.innerHTML = '<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>'\r\n button.addEventListener('click', (event) => {\r\n event.preventDefault()\r\n event.stopPropagation()\r\n const id = button.dataset.crlFill\r\n if (id !== undefined) actionCtx.onFill(id)\r\n })\r\n return button\r\n}\r\n\r\n/** Sync one star button to its message's current favorite state. */\r\nfunction refreshFavoriteButton(button: HTMLButtonElement, lang: Record<string, string>): void {\r\n const id = button.dataset.crlStar\r\n if (id === undefined) return\r\n const starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', id)\r\n button.setAttribute('aria-pressed', String(starred))\r\n button.dataset.starred = starred ? 'true' : undefined\r\n button.classList.toggle('crl_on', starred)\r\n const label = starred ? lang.unfav : lang.fav\r\n // Both the a11y label and the styled tooltip follow the new state.\r\n button.setAttribute('data-tip', label)\r\n button.setAttribute('aria-label', label)\r\n}\r\n\r\n/** Refresh every injected star button (session switch / external toggle). */\r\nfunction refreshAllFavoriteButtons(lang: Record<string, string>): void {\r\n for (const button of document.querySelectorAll<HTMLButtonElement>('[data-crl-star]')) {\r\n refreshFavoriteButton(button, lang)\r\n }\r\n}\r\n\r\n/** Inject star + plus buttons into one user message row (idempotent). */\r\nfunction injectMessageActions(row: HTMLElement, lang: Record<string, string>): void {\r\n // Already injected and both buttons still present → leave it alone.\r\n if (row.dataset.crlActions === '1') {\r\n if (row.querySelector('[data-crl-star]') !== null && row.querySelector('[data-crl-fill]') !== null) return\r\n // React re-rendered the row and replaced the icons: fall through to re-inject.\r\n }\r\n const copy = copyButtonOf(row)\r\n const messageId = messageIdOfAnchorKey(row.getAttribute('data-chat-anchor-key') ?? '')\r\n if (copy === null || messageId === '') return\r\n const star = favoriteButtonOf(messageId, lang)\r\n star.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId) ? 'true' : undefined\r\n star.classList.toggle('crl_on', isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? '', messageId))\r\n const fill = fillButtonOf(messageId, lang)\r\n copy.before(star, fill)\r\n row.dataset.crlActions = '1'\r\n}\r\n\r\n/** Start observing new user rows so injected actions follow history paging. */\r\nfunction startActionInjector(lang: Record<string, string>): () => void {\r\n const scan = () => {\r\n for (const row of document.querySelectorAll<HTMLElement>('[data-chat-anchor-key^=\"13:input-message\"]')) {\r\n injectMessageActions(row, lang)\r\n }\r\n }\r\n const observer = new MutationObserver(scan)\r\n if (typeof document !== 'undefined' && document.body !== null) {\r\n observer.observe(document.body, { childList: true, subtree: true })\r\n }\r\n scan()\r\n return () => observer.disconnect()\r\n}\r\n\r\ninterface RailImage {\r\n attachmentId: string\r\n mediaType: string\r\n width: number\r\n height: number\r\n}\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n /** Stored-image references from the host projection (empty when inline-only). */\r\n images?: RailImage[]\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** One displayable tip image: either a durable reference (resolved lazily) or\r\n * an inline base64 payload already usable as an <img> src. */\r\ntype ImageSpec =\r\n | { kind: 'ref'; attachmentId: string; mediaType: string }\r\n | { kind: 'data'; src: string }\r\n\r\n/** Extract stored-image references from a ContentBlock list (reference form). */\r\nfunction nodeImagesOf(content: unknown): RailImage[] {\r\n if (!Array.isArray(content)) return []\r\n const out: RailImage[] = []\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a === null || typeof a !== 'object') continue\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown; width?: unknown; height?: unknown }\r\n if (typeof ref.attachmentId !== 'string' || ref.attachmentId === '') continue\r\n out.push({\r\n attachmentId: ref.attachmentId,\r\n mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png',\r\n width: typeof ref.width === 'number' ? ref.width : 0,\r\n height: typeof ref.height === 'number' ? ref.height : 0,\r\n })\r\n }\r\n return out\r\n}\r\n\r\n/** Convert a ContentBlock list to displayable image specs; inline base64\r\n * blocks (rare in replayed history) become data URLs on the spot. */\r\nfunction imageSpecsOfContent(content: unknown): ImageSpec[] {\r\n const specs: ImageSpec[] = []\r\n if (!Array.isArray(content)) return specs\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown; data?: unknown; mediaType?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a !== null && typeof a === 'object') {\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown }\r\n if (typeof ref.attachmentId === 'string' && ref.attachmentId !== '') {\r\n specs.push({ kind: 'ref', attachmentId: ref.attachmentId, mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png' })\r\n continue\r\n }\r\n }\r\n if (typeof b.data === 'string' && b.data !== '') {\r\n specs.push({ kind: 'data', src: `data:${typeof b.mediaType === 'string' ? b.mediaType : 'image/png'};base64,${b.data}` })\r\n }\r\n }\r\n return specs\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n const images = Array.isArray(o.images)\r\n ? o.images.map(nodeImagesOfEntry).filter((i): i is RailImage => i !== null)\r\n : undefined\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(images !== undefined && images.length > 0 ? { images } : {}),\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Normalize one projection entry of the `images` array (wire form). */\r\nfunction nodeImagesOfEntry(v: unknown): RailImage | null {\r\n if (v === null || typeof v !== 'object') return null\r\n const o = v as Record<string, unknown>\r\n if (typeof o.attachmentId !== 'string' || o.attachmentId === '') return null\r\n return {\r\n attachmentId: o.attachmentId,\r\n mediaType: typeof o.mediaType === 'string' ? o.mediaType : 'image/png',\r\n width: typeof o.width === 'number' ? o.width : 0,\r\n height: typeof o.height === 'number' ? o.height : 0,\r\n }\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n// ---- Chat-node data plane (DSH 0.1.2-alpha.2+) ----\r\n// DSH 0.1.2-alpha.2 moved Conversation target data out of the Session\r\n// snapshot: `session.getSnapshot()` no longer carries `chat.nodes`, and the\r\n// per-Session Chat nodes live in the uiConversation `'chat'` view target\r\n// (ChatSnapshot with a keyed `nodes` store). The rail resolves nodes from\r\n// either plane — current DSH first, legacy Session snapshot as fallback.\r\n\r\n/** Observable snapshot surface of one Session's `'chat'` view target. */\r\nexport interface ChatViewSource {\r\n subscribe(listener: () => void): () => void\r\n getSnapshot(): unknown\r\n}\r\n\r\n/** Keyed Chat node store published by the official ChatSnapshot (`nodes`). */\r\ninterface ChatNodeStoreLike {\r\n get(key: string): unknown\r\n values(): Iterable<unknown> | readonly unknown[]\r\n}\r\n\r\n/** Structural shape of the uiConversation `'chat'` target snapshot. */\r\ninterface ChatSnapshotLike {\r\n readonly nodes: ChatNodeStoreLike\r\n}\r\n\r\n/** Minimal observable shape of a chat node (either data plane). */\r\ninterface ChatNodeLike {\r\n readonly key?: unknown\r\n readonly kind?: unknown\r\n readonly anchorSeq?: unknown\r\n readonly data?: { time?: unknown; content?: unknown } | null\r\n}\r\n/** Runtime face of the official Conversation assembly service\r\n * (DSH 0.1.2-alpha.2+). Kept as a local structural type because the npm\r\n * client-runtime types (0.1.1-rc.1) predate the service rename and still\r\n * expose the legacy registry names. */\r\ninterface UiConversationRuntime {\r\n binding(sessionId: string): {\r\n target(name: 'chat'): ChatViewSource | undefined\r\n }\r\n}\r\n\r\n/**\r\n * Resolve one chat node from either data plane: the official Chat view\r\n * snapshot (`nodes` keyed store) on current DSH, or the legacy Session\r\n * snapshot's `chat.nodes` Map on older DSH.\r\n * @param snapshot - the active node snapshot.\r\n * @param key - the Conversation Context key.\r\n * @returns the raw node, when its window covers the key.\r\n */\r\nexport function chatNodeOf(snapshot: unknown, key: string | undefined): unknown {\r\n if (key === undefined || snapshot === undefined || snapshot === null) return undefined\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.get === 'function') {\r\n return store.get(key)\r\n }\r\n const map = (snapshot as { chat?: { nodes?: Map<string, unknown> } } | undefined)?.chat?.nodes\r\n return map?.get(key)\r\n}\r\n\r\n/** Map one chat node (either plane) to a rail message entry. */\r\nexport function railMessageOfNode(node: unknown): RailMessage | null {\r\n if (node === null || typeof node !== 'object') return null\r\n const n = node as ChatNodeLike\r\n if (n.kind !== 'user' && n.kind !== 'steering') return null\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) return null\r\n const data = n.data\r\n if (data === null || typeof data !== 'object') return null\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) return null\r\n const images = nodeImagesOf(data.content)\r\n return {\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: data.time,\r\n text: userTextOf(data.content),\r\n hasImage: userHasImage(data.content),\r\n ...(images.length > 0 ? { images } : {}),\r\n key,\r\n }\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes.\r\n * Accepts either data plane — the official Chat view snapshot (`nodes`\r\n * values) or the legacy Session snapshot's `chat.nodes` Map. */\r\nexport function collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n const store = (snapshot as ChatSnapshotLike | undefined)?.nodes as ChatNodeStoreLike | undefined\r\n if (store !== undefined && typeof store.values === 'function') {\r\n for (const node of store.values()) {\r\n const m = railMessageOfNode(node)\r\n if (m !== null) out.push(m)\r\n }\r\n } else {\r\n const chat = (snapshot as { chat?: { nodes?: Map<unknown, unknown> } } | undefined)?.chat\r\n if (chat?.nodes === undefined) return out\r\n for (const node of chat.nodes.values()) {\r\n const m = railMessageOfNode(node)\r\n if (m !== null) out.push(m)\r\n }\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\n/** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\nfunction fullTextOf(m: RailMessage, nodeSnapshot: unknown): string {\r\n const key = anchorKeyOf(m)\r\n const node = chatNodeOf(nodeSnapshot, key) as { data?: { content?: unknown } } | undefined\r\n const content = node?.data?.content\r\n if (Array.isArray(content)) {\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n const full = out.trim()\r\n if (full !== '') return full\r\n }\r\n return m.text\r\n}\r\n\r\n/** Displayable tip images for one rail message: host projection references\r\n * first (works even before the node window covers the message), then the\r\n * loaded chat node's content blocks. */\r\nfunction tipImagesOf(m: RailMessage, nodeSnapshot: unknown): ImageSpec[] {\r\n if (Array.isArray(m.images) && m.images.length > 0) {\r\n return m.images.map(img => ({ kind: 'ref', attachmentId: img.attachmentId, mediaType: img.mediaType }))\r\n }\r\n const key = anchorKeyOf(m)\r\n const node = chatNodeOf(nodeSnapshot, key) as { data?: { content?: unknown } } | undefined\r\n return imageSpecsOfContent(node?.data?.content)\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\r\n * in a long session needs many pages. Report page count through onProgress so\r\n * the UI can show a loading state instead of appearing frozen.\r\n *\r\n * \"Loaded\" is decided through `nodeOf`, the live chat-node reader (the\r\n * uiConversation `'chat'` target on current DSH, where the Session snapshot no\r\n * longer carries chat.nodes); without it the legacy Session snapshot's\r\n * `chat.nodes` Map is used.\r\n *\r\n * The paged-in data lands in the session snapshot synchronously with\r\n * loadOlder's resolution, but the chat rows render through React — the DOM row\r\n * for the target key appears a beat later. We therefore poll for the row (with\r\n * a timeout) instead of querying once and giving up; otherwise a multi-page\r\n * jump would \"finish loading\" without scrolling and need a second click.\r\n */\r\nasync function jumpToMessage(\r\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\r\n sessionId: string,\r\n key: string,\r\n /** Live chat-node reader: called with the current snapshot on every probe. */\r\n nodeOf?: (key: string) => unknown,\r\n onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n const isLoaded = nodeOf === undefined\r\n ? (k: string) => (session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> } }).chat?.nodes?.get(k) !== undefined\r\n : nodeOf\r\n let pages = 0\r\n let guard = 0\r\n let loaded = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (isLoaded(key)) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\r\n return true\r\n}\r\n\r\n// ---- tip image thumbnails ----\r\n\r\n/** Session-scoped browser URL cache for resolved attachment bytes (one entry\r\n * per session:attachment; a failed load evicts itself so the next hover\r\n * retries instead of showing a permanently broken thumbnail). */\r\nconst thumbUrls = new Map<string, Promise<string>>()\r\n\r\n/** Minimal RpcResult shape of session.readAttachment (contract level). */\r\ninterface ReadAttachmentResult {\r\n ok: boolean\r\n value?: { attachment?: { mediaType?: string }; data?: Uint8Array }\r\n error?: { code?: string; message?: string }\r\n}\r\n\r\ntype ReadAttachmentFn = (attachmentId: string) => Promise<ReadAttachmentResult>\r\n\r\n/** Fallback data URL when the browser cannot mint object URLs. */\r\nfunction bytesToDataUrl(data: Uint8Array, mediaType: string): string {\r\n let binary = ''\r\n const chunk = 0x8000\r\n for (let offset = 0; offset < data.length; offset += chunk) {\r\n binary += String.fromCharCode(...data.subarray(offset, offset + chunk))\r\n }\r\n return `data:${mediaType};base64,${btoa(binary)}`\r\n}\r\n\r\n/** Resolve (and cache) one stored image to a displayable URL. */\r\nfunction resolveThumb(sessionId: string, read: ReadAttachmentFn, image: Pick<RailImage, 'attachmentId' | 'mediaType'>): Promise<string> {\r\n const key = `${sessionId}:${image.attachmentId}`\r\n let pending = thumbUrls.get(key)\r\n if (pending === undefined) {\r\n pending = read(image.attachmentId).then((result): string => {\r\n if (!result.ok || result.value === undefined) {\r\n throw new Error(result.error?.message ?? result.error?.code ?? 'readAttachment failed')\r\n }\r\n const data = result.value.data\r\n if (data === undefined) throw new Error('readAttachment resolved no bytes')\r\n const mediaType = result.value.attachment?.mediaType ?? image.mediaType\r\n if (typeof URL.createObjectURL === 'function') {\r\n return URL.createObjectURL(new Blob([data as BlobPart], { type: mediaType }))\r\n }\r\n return bytesToDataUrl(data, mediaType)\r\n })\r\n void pending.catch(() => { thumbUrls.delete(key) })\r\n thumbUrls.set(key, pending)\r\n }\r\n return pending\r\n}\r\n\r\n/** One tip thumbnail: resolves its attachment lazily, shows a pulsing\r\n * placeholder while loading, and removes itself on failure. */\r\nfunction TipThumb({ spec, sessionId, read }: {\r\n spec: ImageSpec\r\n sessionId: string\r\n read: ReadAttachmentFn\r\n}): ReactNode {\r\n const [src, setSrc] = useState<string | null>(null)\r\n const [failed, setFailed] = useState(false)\r\n const specKey = spec.kind === 'ref' ? `ref:${spec.attachmentId}` : 'data'\r\n useEffect(() => {\r\n let alive = true\r\n setSrc(null)\r\n setFailed(false)\r\n if (spec.kind === 'data') {\r\n setSrc(spec.src)\r\n return () => { alive = false }\r\n }\r\n resolveThumb(sessionId, read, spec)\r\n .then((url) => { if (alive) setSrc(url) })\r\n .catch(() => { if (alive) setFailed(true) })\r\n return () => { alive = false }\r\n // spec is a fresh object each render; the string key is the stable identity.\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [specKey, sessionId, read])\r\n if (failed) return null\r\n if (src === null) return createElement('div', { className: S.tipImgPh, 'aria-hidden': true })\r\n return createElement('img', { className: S.tipImg, src, alt: '', draggable: false })\r\n}\r\n\r\n// ---- component ----\r\ninterface TimelineRailProps {\r\n useProjection: (key: string) => { messages?: unknown[] } | undefined\r\n sessionId?: SessionId\r\n sessionsService: ISessions\r\n /** Live Chat view source for the session (current DSH `'chat'` target). */\r\n chatOf?: (sessionId: string) => ChatViewSource | undefined\r\n /** Draft write + attachment-add face (session-scope framework injection). */\r\n inputActions?: {\r\n setDraft(text: string): void\r\n addImages(ids: readonly string[]): boolean\r\n } | undefined\r\n /** Runtime draft-image registry (hosted by ui-conversation). */\r\n conversation?: {\r\n createDraftImages(files: readonly File[]): readonly { id: string }[]\r\n } | undefined\r\n}\r\n\r\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\r\nfunction langStrings(): Record<string, string> {\r\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\r\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\r\n}\r\n\r\nfunction TimelineRail({ useProjection, sessionId, sessionsService, chatOf, inputActions, conversation }: TimelineRailProps): ReactNode {\r\n const t = langStrings()\r\n const projected = useProjection('chatRail')\r\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\r\n // Chat-node source: the official `'chat'` view target on current DSH (the\r\n // Session snapshot no longer carries chat.nodes); the Session snapshot stays\r\n // as the legacy fallback on older DSH.\r\n const chatSource = sessionId === undefined ? undefined : chatOf?.(sessionId)\r\n const fallbackStore = chatSource ?? (session === undefined ? NOOP_STORE : session)\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n // Favorites: per-session persisted set + a \"bookmarks only\" filter that\r\n // narrows the rail to favorited messages (mirrors dsh-milestone's\r\n // bookmarksOnly toggle; the DOM-injected buttons share the same store).\r\n const favorites = useSyncExternalStore(subscribeFavorites, favoritesSnapshot)\r\n const [favOnly, setFavOnly] = useState(false)\r\n // Count of favorited messages in THIS session. The filter toggle is only\r\n // worth showing when a favorite exists — with zero favorites there is\r\n // nothing to filter to, so the pill disappears entirely.\r\n const favCount = sessionId === undefined\r\n ? 0\r\n : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length\r\n const showFavToggle = favCount > 0\r\n const effectiveMessages = favOnly\r\n ? messages.filter((m) => sessionId !== undefined && isFavorite(favorites, sessionId, favoriteIdOfMessage(m)))\r\n : messages\r\n\r\n // If the filter is armed and the last favorite is un-starred (or the\r\n // session switched), the filtered list would be empty AND the toggle would\r\n // vanish — leaving a rail with nothing to un-filter. Auto-release the\r\n // filter instead so the full list returns.\r\n useEffect(() => {\r\n if (favOnly && !showFavToggle) setFavOnly(false)\r\n }, [favOnly, showFavToggle])\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null) // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Last observed pointer position over the rail. A mouseenter that arrives\r\n // before the width transition cannot show the tip (rects unstable), yet the\r\n // enter event is already gone by the time the animation ends — the settle\r\n // handler re-hit-tests the cursor against this position to show the tip for\r\n // the node still under the pointer.\r\n const lastPointerRef = useRef<{ x: number; y: number } | null>(null)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n // Track the cursor over the rail for the expand-settled tip re-hit-test\r\n // (the `show` effect's settle handler reads lastPointerRef). Native\r\n // listener: plain DOM events keep the callback typed without React\r\n // synthetic-event generics in the hand-written createElement props.\r\n useEffect(() => {\r\n const el = navRef.current\r\n if (el === null) return\r\n const onMove = (e: PointerEvent) => { lastPointerRef.current = { x: e.clientX, y: e.clientY } }\r\n el.addEventListener('pointermove', onMove, { passive: true })\r\n return () => el.removeEventListener('pointermove', onMove)\r\n }, [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n let settled = false\r\n const settle = () => {\r\n if (settled) return\r\n settled = true\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n // Re-hit-test the cursor: a mouseenter that fired before the width\r\n // transition could not show the tip (rects were still unstable), and\r\n // the enter event is long gone once the animation ends. If the pointer\r\n // still sits on a rail item, position that item's tip now.\r\n const p = lastPointerRef.current\r\n if (p !== null) {\r\n const hit = document.elementFromPoint(p.x, p.y)\r\n const item = hit === null ? null : hit.closest('[data-crl-index]')\r\n const idx = item === null ? -1 : Number(item.getAttribute('data-crl-index'))\r\n if (Number.isInteger(idx) && idx >= 0) positionTip(idx)\r\n }\r\n }\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n settle()\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n // Fallback for `prefers-reduced-motion: reduce` (the stylesheet disables\r\n // the width transition, so `transitionend` never fires) and for any lost\r\n // event: settle after the transition duration regardless.\r\n const timer = setTimeout(settle, 300)\r\n return () => {\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n }\r\n }, [show])\r\n\r\n // Session-scoped attachment reader for tip thumbnails (stable identity so\r\n // TipThumb effects do not re-run every render; resolveThumb caches anyway).\r\n const readThumb = useMemo<ReadAttachmentFn | undefined>(() => {\r\n if (sessionId === undefined) return undefined\r\n const s = (sessionsService.binding(sessionId) as unknown as\r\n { session?: { readAttachment?: ReadAttachmentFn } } | undefined)?.session\r\n return s?.readAttachment === undefined\r\n ? undefined\r\n : (id: string) => s.readAttachment!(id)\r\n }, [sessionId, sessionsService])\r\n\r\n // Fill-to-input (plus button): put the message's full text into the draft\r\n // and re-attach its stored images through the official draft-image path\r\n // (readAttachment → File → createDraftImages → addImages). Degrades to\r\n // text-only when the conversation service or storage read is unavailable.\r\n const fillToInput = useCallback((messageId: string) => {\r\n const m = messages.find((candidate) => favoriteIdOfMessage(candidate) === messageId)\r\n if (m === undefined || inputActions === undefined || sessionId === undefined) return\r\n const text = fullTextOf(m, nodeSnapshot) || m.text\r\n if (inputActions.setDraft !== undefined) inputActions.setDraft(text)\r\n const specs = tipImagesOf(m, nodeSnapshot).filter((spec): spec is ImageSpec & { kind: 'ref' } => spec.kind === 'ref')\r\n if (specs.length === 0 || conversation === undefined || readThumb === undefined) return\r\n void (async () => {\r\n const files: File[] = []\r\n for (const spec of specs) {\r\n try {\r\n const result = await readThumb(spec.attachmentId) as unknown as { ok?: boolean; value?: { attachment?: { mediaType?: string }; data?: Uint8Array } }\r\n if (result?.ok !== true || result.value?.data === undefined) continue\r\n const mediaType = result.value.attachment?.mediaType ?? spec.mediaType\r\n files.push(new File([result.value.data as BlobPart], `attachment-${spec.attachmentId}${mediaType.includes('jpeg') ? '.jpg' : '.png'}`, { type: mediaType }))\r\n } catch {\r\n // One failed attachment must not block the others.\r\n }\r\n }\r\n if (files.length === 0) return\r\n const drafts = conversation.createDraftImages(files)\r\n const ids = drafts.map((draft) => draft.id).filter((id): id is string => id !== undefined)\r\n if (ids.length > 0) {\r\n try { inputActions.addImages?.(ids) } catch { /* draft-image registry may reject; text fill already landed */ }\r\n }\r\n })()\r\n }, [conversation, inputActions, messages, nodeSnapshot, readThumb, sessionId])\r\n\r\n // Bridge the DOM-injected buttons (module-level actionCtx) to this\r\n // component's live closures + session id; refresh star states on every\r\n // favorites change and on session switch.\r\n useEffect(() => {\r\n actionCtx.sessionId = sessionId\r\n actionCtx.onToggleFavorite = (messageId) => {\r\n if (sessionId !== undefined) toggleFavorite(sessionId, messageId)\r\n }\r\n actionCtx.onFill = fillToInput\r\n refreshAllFavoriteButtons(t)\r\n return () => {\r\n // Only clear what this render set: a newer session may already own the ctx.\r\n if (actionCtx.sessionId === sessionId) actionCtx.sessionId = undefined\r\n }\r\n }, [sessionId, fillToInput, t])\r\n\r\n // Keep the injected DOM buttons in sync with external favorite toggles\r\n // (rail star / another session's store write).\r\n useEffect(() => {\r\n refreshAllFavoriteButtons(t)\r\n }, [favorites, t])\r\n\r\n // Inject star + plus buttons into every user message row; observe DOM\r\n // changes so history paging and re-renders keep them present.\r\n useEffect(() => {\r\n if (typeof document === 'undefined' || document.body === null) return\r\n return startActionInjector(t)\r\n }, [t])\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (effectiveMessages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < effectiveMessages.length; i++) {\r\n const key = anchorKeyOf(effectiveMessages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, effectiveMessages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n // The rail hides on tiny sessions (fewer than 2 user messages) — but the\r\n // count is the UNFILTERED list: the favorites-only filter may legitimately\r\n // narrow to a single row, and hiding the rail then would strand the user\r\n // with no way to switch the filter back off.\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n // Fav-only filter remaps the tip index into the FULL list (tip content and\r\n // hover data always come from the unfiltered messages array).\r\n const tipIndex = tip === null\r\n ? -1\r\n : favOnly\r\n ? messages.findIndex((m) => effectiveMessages[tip.index] !== undefined\r\n && favoriteIdOfMessage(m) === favoriteIdOfMessage(effectiveMessages[tip.index]))\r\n : tip.index\r\n\r\n const items = effectiveMessages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n const starred = favoriteIdOfMessage(m) !== '' && isFavorite(favorites, sessionId, favoriteIdOfMessage(m))\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : '') + (starred ? ` ${S.favItem}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(\r\n sessionsService as never,\r\n sessionId as string,\r\n key,\r\n (k) => chatNodeOf(fallbackStore.getSnapshot(), k),\r\n undefined,\r\n controller.signal,\r\n ).finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n starred\r\n ? createElement('span', { className: S.favStar, role: 'img', 'aria-label': t.fav, 'aria-hidden': undefined },\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 13,\r\n height: 13,\r\n fill: 'currentColor',\r\n stroke: 'none',\r\n 'aria-hidden': true,\r\n },\r\n createElement('path', { d: STAR_PATH, 'aria-hidden': true })))\r\n : null,\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage\r\n ? createElement('span', { className: S.img, role: 'img', 'aria-label': t.hasImage },\r\n createElement('svg', {\r\n viewBox: '0 0 16 16',\r\n width: 12,\r\n height: 12,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.4,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('rect', { x: 1.7, y: 2.7, width: 12.6, height: 10.6, rx: 2.2 }),\r\n createElement('circle', { cx: 5.6, cy: 6.4, r: 1.15, fill: 'currentColor', stroke: 'none' }),\r\n createElement('path', { d: 'M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8' })))\r\n : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n // Favorites-only filter: a star pill pinned at the top of the rail\r\n // (sticky; it never scrolls away with the rows). On state fills the\r\n // star (yellow) and narrows the rail to favorited messages; the label\r\n // (\"bookmarks only\") only shows once the rail expands — the collapsed\r\n // 36px capsule keeps a clean round icon. Hidden entirely when this\r\n // session has no favorites (nothing to filter to).\r\n showFavToggle\r\n ? createElement('button', {\r\n type: 'button',\r\n key: 'favToggle',\r\n className: S.favToggle + (favOnly ? ' crl_on' : ''),\r\n 'aria-pressed': favOnly,\r\n 'aria-label': t.favOnly,\r\n title: t.favOnly,\r\n onMouseEnter: (e: MouseEvent) => e.stopPropagation(),\r\n onClick: () => setFavOnly((v) => !v),\r\n children: [\r\n createElement('svg', {\r\n viewBox: '0 0 24 24',\r\n width: 14,\r\n height: 14,\r\n fill: favOnly ? 'currentColor' : 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 2,\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n }, createElement('path', { d: STAR_PATH })),\r\n createElement('span', { className: S.favToggleLabel }, t.favOnly),\r\n ],\r\n })\r\n : null,\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item. Images\r\n // stack above the text; a message whose image cannot be resolved (no\r\n // projection refs and unloaded node) shows a quiet \"has image\" badge.\r\n tip !== null && tipIndex >= 0 && tipIndex < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, (() => {\r\n const m = messages[tipIndex]\r\n const specs = tipImagesOf(m, nodeSnapshot)\r\n const children: ReactNode[] = []\r\n if (specs.length > 0 && readThumb !== undefined) {\r\n // Only the first image renders; the rest collapse into a \"+N\"\r\n // badge so a many-image message cannot push the tip past its\r\n // viewport cap (the stacked thumbnails did).\r\n children.push(createElement('div', { className: S.tipImgs, key: 'imgs' },\r\n createElement('div', { className: S.tipImgWrap, key: 'img' },\r\n createElement(TipThumb, { spec: specs[0], sessionId: sessionId as string, read: readThumb }),\r\n specs.length > 1\r\n ? createElement('span', { className: S.tipImgCount, 'aria-hidden': true }, `+${specs.length - 1}`)\r\n : null)))\r\n } else if (m.hasImage) {\r\n children.push(createElement('span', { className: S.tipBadge, key: 'badge' }, t.hasImage))\r\n }\r\n children.push(createElement('span', { key: 'text' }, fullTextOf(m, nodeSnapshot) || t.noText))\r\n return children\r\n })())\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n // Current DSH (0.1.2-alpha.2+) moved Conversation target data out of the\r\n // Session snapshot: chat nodes live in the uiConversation `'chat'` view\r\n // target. Resolved through optional ctx.inject (own fiber, never blocking\r\n // this plugin) so older DSH without the service keeps the legacy Session\r\n // snapshot fallback untouched.\r\n let chatOf: ((sessionId: string) => ChatViewSource | undefined) | undefined\r\n ctx.inject(['uiConversation'], (scope: ClientContext) => {\r\n const ui = (scope as unknown as { uiConversation: UiConversationRuntime }).uiConversation\r\n chatOf = (sessionId: string): ChatViewSource | undefined => {\r\n try {\r\n const binding = ui.binding(sessionId)\r\n return binding.target('chat') as unknown as ChatViewSource | undefined\r\n } catch {\r\n // Unknown session: the rail already gates on a live session id.\r\n return undefined\r\n }\r\n }\r\n })\r\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\r\n name: 'conversation.input.dock',\r\n id: 'chat-rail',\r\n order: 40,\r\n inject: () => ({\r\n sessionsService: ctx.sessions,\r\n chatOf,\r\n // createDraftImages lives on the concrete ConversationController, not\r\n // the outward IConversation face; the runtime service is always the\r\n // controller, so the cast is structural, never a feature guess.\r\n conversation: ctx.conversation as unknown as { createDraftImages(files: readonly File[]): readonly { id: string }[] },\r\n }),\r\n }, TimelineRail))\r\n}\r\n\r\nexport { apply, TimelineRail, imageSpecsOfContent, normalize, jumpToMessage }\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-chat-rail",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
5
5
  "type": "module",
6
6
  "main": "lib/index.mjs",