@max-null/dsh-chat-rail 0.1.0
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/LICENSE +21 -0
- package/README.md +77 -0
- package/cordis.patch.yml +6 -0
- package/docs/shots/rail-1.png +0 -0
- package/docs/shots/rail-2.png +0 -0
- package/lib/client.js +445 -0
- package/lib/client.js.map +1 -0
- package/lib/index.mjs +61 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MaxNull
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @max-null/dsh-chat-rail
|
|
2
|
+
|
|
3
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover 展开画卷查看消息列表,点击跳转任意历史消息。
|
|
4
|
+
|
|
5
|
+
> 不同于市面上"面板弹出式"的导航插件,dsh-chat-rail 采用**画卷式展开**:收起时是一条干净的竖条,hover 时从右往左平滑展开,像打开画卷一样。
|
|
6
|
+
|
|
7
|
+
## 截图
|
|
8
|
+
|
|
9
|
+
| 展开态(画卷 + 消息预览 tip) | 收起态(与 better-sidebar 共存) |
|
|
10
|
+
|---|---|
|
|
11
|
+
|  |  |
|
|
12
|
+
|
|
13
|
+
## 为什么比市面上的导航栏优秀
|
|
14
|
+
|
|
15
|
+
市面上的会话导航插件(chat-timeline / milestone / msg-nav / turn-rail…)大多只做"指示条 + 点击跳转",在真实使用中有一堆体验问题。dsh-chat-rail 针对每一个问题给出了答案:
|
|
16
|
+
|
|
17
|
+
| 能力 | 市面同类 | dsh-chat-rail |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅ **跟随 CSS 变量动画同步避让**:`--dsh-sidebar-width` / `--dsh-sidebar-height`,与面板共享同一 transition,右栏/底栏展开收起时导航无缝跟随,互不遮挡 |
|
|
20
|
+
| **展开形态** | ❌ 面板突兀弹出,高度错位 | ✅ **画卷式展开**:单容器从右往左平滑展开,高度天然一致 |
|
|
21
|
+
| **阅读位置跟随** | 高亮点会跑出显示区 | ✅ **导航条自动滚动,高亮点持续居中**:无论会话滚到哪里,当前消息的指示点始终在导航可视区中央 |
|
|
22
|
+
| **切换会话定位** | 回到顶部 | ✅ 自动定位到新会话当前阅读位置 |
|
|
23
|
+
| **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title 干扰 |
|
|
24
|
+
| **消息信息** | 只有预览文本 | ✅ **编号 + 相对时间 + 文本三列对齐**:编号固定宽、时间靠右,文本区域最大化且整齐 |
|
|
25
|
+
| **长会话跳转** | 干等卡死 | ✅ **加载反馈**:跳转远距离消息时显示 spinner + "加载中",收起态只留图标 |
|
|
26
|
+
| **主题适配** | 部分 | ✅ 全部使用 DSH 主题变量(`--dsw-alias-*`),浅深色无缝适配 |
|
|
27
|
+
| **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH 环境开箱即用,固定右缘 + 垂直居中 |
|
|
28
|
+
|
|
29
|
+
## 兼容性(重点)
|
|
30
|
+
|
|
31
|
+
- **与 dsh-better-sidebar 深度兼容**:导航栏的右缘和垂直位置跟随 better-sidebar 的 `--dsh-sidebar-width` / `--dsh-sidebar-height` CSS 变量,右栏和底栏展开/收起时导航与面板**共享同一段 transition 动画同步移动**,永不错位、永不遮挡(包括底栏右上角的收起按钮)。
|
|
32
|
+
- **无 better-sidebar 也能用**:变量缺失时走 CSS fallback(右缘 12px + 垂直居中),纯 DSH 环境开箱即用。
|
|
33
|
+
- 需要 DSH ≥ `0.1.0-rc.6`。
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dsh plugin --profile <name> add @max-null/dsh-chat-rail
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
或手动加入 profile 的 `package.json` dependencies + `dsh.profile.bundles`:
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
- id: chat-rail
|
|
45
|
+
name: '@max-null/dsh-chat-rail'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 交互
|
|
49
|
+
|
|
50
|
+
- **hover 导航条**:画卷展开,显示编号 / 相对时间 / 消息预览
|
|
51
|
+
- **hover 消息项**:左侧弹出完整内容 tip 面板
|
|
52
|
+
- **点击消息项**:跳转到会话中对应消息(未加载的历史自动翻页补载)
|
|
53
|
+
- **滚动会话**:高亮点跟随,导航条自动滚动保持居中
|
|
54
|
+
|
|
55
|
+
## 架构
|
|
56
|
+
|
|
57
|
+
- **host 半端**(`lib/index.mjs`):注册 `chatRail` 会话投影——从 session log 折叠用户消息锚点 `{ seq, time, text, id }`(排除插件/工具注入的上下文行)
|
|
58
|
+
- **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body
|
|
59
|
+
- 数据源优先序:`chatRail` 投影 → 已加载聊天节点 → 后台 loadOlder 循环
|
|
60
|
+
|
|
61
|
+
## SSID 系列
|
|
62
|
+
|
|
63
|
+
本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-chat-rail` · `dsh-plugin-center` · `dsh-memory` · `dsh-chinese-thinking` · `dsh-guardian` · `dsh-habit`。
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
## 开发
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm install
|
|
71
|
+
pnpm typecheck # tsc 严格类型检查
|
|
72
|
+
pnpm build # 产出 lib/
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# @max-null/dsh-chat-rail bundle patch: one insert over the profile root.
|
|
2
|
+
# Host half registers the user-message projection; client half renders the
|
|
3
|
+
# right-edge navigation rail in the conversation view.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: chat-rail
|
|
6
|
+
name: '@max-null/dsh-chat-rail'
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@max-null/dsh-chat-rail",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_dom = require("react-dom");
|
|
9
|
+
//#region src/client/index.tsx
|
|
10
|
+
/**
|
|
11
|
+
* @max-null/dsh-chat-rail — web client half.
|
|
12
|
+
*
|
|
13
|
+
* Right-edge conversation navigation rail: one indicator per user message,
|
|
14
|
+
* scroll-spy highlight of the reading position, hover preview, click to jump.
|
|
15
|
+
*
|
|
16
|
+
* Two deliberate fixes over the dsh-chat-timeline reference:
|
|
17
|
+
*
|
|
18
|
+
* 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout
|
|
19
|
+
* push through the `--dsh-sidebar-width` CSS variable on `:root` (the
|
|
20
|
+
* `#root` margin-right transition reads the same variable). The rail's
|
|
21
|
+
* `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same
|
|
22
|
+
* transition timing, so rail and scrollport move together while the panel
|
|
23
|
+
* expands — no "panel first, scrollbar later" lag.
|
|
24
|
+
*
|
|
25
|
+
* 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)
|
|
26
|
+
* lines — nearly invisible on a light background. Here the rail keeps a
|
|
27
|
+
* constant translucent capsule, thicker/darker indicator lines, and an
|
|
28
|
+
* always-visible collapsed state (no "reveal on pointer proximity").
|
|
29
|
+
*
|
|
30
|
+
* Data: host `chatRail` projection first, loaded chat nodes fallback, then a
|
|
31
|
+
* background loadOlder loop (stopped as soon as the projection delivers).
|
|
32
|
+
* Mounted in conversation.input.dock, portal-rendered to body.
|
|
33
|
+
*/
|
|
34
|
+
const inject = ["slots", "sessions"];
|
|
35
|
+
const STRINGS = {
|
|
36
|
+
zh: {
|
|
37
|
+
railLabel: "消息导航",
|
|
38
|
+
roleUser: "用户",
|
|
39
|
+
noText: "(无文本内容)",
|
|
40
|
+
ariaJump: "跳转到消息",
|
|
41
|
+
loading: "加载中…",
|
|
42
|
+
timeJustNow: "刚刚",
|
|
43
|
+
timeMinutes: "{n}分钟前",
|
|
44
|
+
timeHours: "{n}小时前",
|
|
45
|
+
timeDays: "{n}天前"
|
|
46
|
+
},
|
|
47
|
+
en: {
|
|
48
|
+
railLabel: "Message rail",
|
|
49
|
+
roleUser: "User",
|
|
50
|
+
noText: "(no text)",
|
|
51
|
+
ariaJump: "Jump to message",
|
|
52
|
+
loading: "Loading…",
|
|
53
|
+
timeJustNow: "just now",
|
|
54
|
+
timeMinutes: "{n}m ago",
|
|
55
|
+
timeHours: "{n}h ago",
|
|
56
|
+
timeDays: "{n}d ago"
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/** Compact relative time for a message timestamp (zh/en via template). */
|
|
60
|
+
function relativeTime(ts, s) {
|
|
61
|
+
const diff = Date.now() - ts;
|
|
62
|
+
if (diff < 6e4) return s.timeJustNow;
|
|
63
|
+
if (diff < 36e5) return s.timeMinutes.replace("{n}", String(Math.floor(diff / 6e4)));
|
|
64
|
+
if (diff < 864e5) return s.timeHours.replace("{n}", String(Math.floor(diff / 36e5)));
|
|
65
|
+
if (diff < 6048e5) return s.timeDays.replace("{n}", String(Math.floor(diff / 864e5)));
|
|
66
|
+
const d = new Date(ts);
|
|
67
|
+
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
68
|
+
}
|
|
69
|
+
const css = [
|
|
70
|
+
".crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 12px);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}",
|
|
71
|
+
"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}",
|
|
72
|
+
".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)}",
|
|
73
|
+
"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)}",
|
|
74
|
+
".crl_nav::-webkit-scrollbar{width:4px}",
|
|
75
|
+
".crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}",
|
|
76
|
+
".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}",
|
|
77
|
+
".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}",
|
|
78
|
+
".crl_loading .crl_loadingLabel{display:none}",
|
|
79
|
+
".crl_show .crl_loading .crl_loadingLabel{display:inline}",
|
|
80
|
+
"@keyframes crl-spin{to{transform:rotate(360deg)}}",
|
|
81
|
+
".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}",
|
|
82
|
+
".crl_show .crl_item{padding:0 14px}",
|
|
83
|
+
".crl_item:hover{color:rgba(0,0,0,.95)}",
|
|
84
|
+
".crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
85
|
+
"body[data-ds-dark-theme] .crl_item,[data-theme='dark'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}",
|
|
86
|
+
"body[data-ds-dark-theme] .crl_item:hover,[data-theme='dark'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}",
|
|
87
|
+
".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}",
|
|
88
|
+
".crl_show .crl_title{display:block;animation:crl-fade .18s ease}",
|
|
89
|
+
".crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}",
|
|
90
|
+
".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}",
|
|
91
|
+
".crl_show .crl_num{display:block;animation:crl-fade .18s ease}",
|
|
92
|
+
"body[data-ds-dark-theme] .crl_num,[data-theme='dark'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}",
|
|
93
|
+
".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}",
|
|
94
|
+
".crl_show .crl_time{display:block;animation:crl-fade .18s ease}",
|
|
95
|
+
"body[data-ds-dark-theme] .crl_time,[data-theme='dark'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}",
|
|
96
|
+
"@keyframes crl-fade{from{opacity:0}to{opacity:1}}",
|
|
97
|
+
".crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}",
|
|
98
|
+
".crl_show .crl_ind{margin-left:8px}",
|
|
99
|
+
".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}",
|
|
100
|
+
".crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}",
|
|
101
|
+
".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)}",
|
|
102
|
+
"body[data-ds-dark-theme] .crl_line,[data-theme='dark'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}",
|
|
103
|
+
"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)}",
|
|
104
|
+
"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)}",
|
|
105
|
+
".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}",
|
|
106
|
+
"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)))}",
|
|
107
|
+
"@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}}"
|
|
108
|
+
].join("");
|
|
109
|
+
const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
|
|
110
|
+
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) === null) {
|
|
111
|
+
const tag = document.createElement("style");
|
|
112
|
+
tag.dataset.plugin = "dsh-chat-rail";
|
|
113
|
+
tag.dataset.pluginCss = STYLE_ID;
|
|
114
|
+
tag.textContent = css;
|
|
115
|
+
document.head.appendChild(tag);
|
|
116
|
+
}
|
|
117
|
+
const S = {
|
|
118
|
+
nav: "crl_nav",
|
|
119
|
+
navShow: "crl_show",
|
|
120
|
+
item: "crl_item",
|
|
121
|
+
itemActive: "crl_active",
|
|
122
|
+
title: "crl_title",
|
|
123
|
+
num: "crl_num",
|
|
124
|
+
time: "crl_time",
|
|
125
|
+
ind: "crl_ind",
|
|
126
|
+
line: "crl_line",
|
|
127
|
+
loading: "crl_loading",
|
|
128
|
+
loadingLabel: "crl_loadingLabel",
|
|
129
|
+
tip: "crl_tip"
|
|
130
|
+
};
|
|
131
|
+
const NOOP_STORE = {
|
|
132
|
+
getSnapshot: () => void 0,
|
|
133
|
+
subscribe: () => () => {}
|
|
134
|
+
};
|
|
135
|
+
/** Extract preview text from a user message's ContentBlock list. */
|
|
136
|
+
function userTextOf(content) {
|
|
137
|
+
if (!Array.isArray(content)) return "";
|
|
138
|
+
let out = "";
|
|
139
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
140
|
+
return out.trim().slice(0, 80);
|
|
141
|
+
}
|
|
142
|
+
/** Normalize one projection entry to a rail message. */
|
|
143
|
+
function normalize(m) {
|
|
144
|
+
if (m === null || typeof m !== "object") return null;
|
|
145
|
+
const o = m;
|
|
146
|
+
if (typeof o.seq !== "number") return null;
|
|
147
|
+
const text = typeof o.text === "string" ? o.text : typeof o.preview === "string" ? o.preview : "";
|
|
148
|
+
return {
|
|
149
|
+
seq: o.seq,
|
|
150
|
+
time: typeof o.time === "number" ? o.time : 0,
|
|
151
|
+
text,
|
|
152
|
+
...typeof o.key === "string" ? { key: o.key } : {},
|
|
153
|
+
...typeof o.id === "string" ? { id: o.id } : {}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Fallback collector: enumerate user messages from the loaded chat nodes. */
|
|
157
|
+
function collectFromNodes(snapshot) {
|
|
158
|
+
const out = [];
|
|
159
|
+
if (snapshot === void 0 || snapshot.chat === void 0) return out;
|
|
160
|
+
const chat = snapshot.chat;
|
|
161
|
+
if (!chat.nodes) return out;
|
|
162
|
+
for (const node of chat.nodes.values()) {
|
|
163
|
+
if (node === null || typeof node !== "object") continue;
|
|
164
|
+
const n = node;
|
|
165
|
+
if (n.kind !== "user") continue;
|
|
166
|
+
const data = n.data;
|
|
167
|
+
if (data === null || typeof data !== "object") continue;
|
|
168
|
+
if (typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
169
|
+
const key = typeof n.key === "string" ? n.key : void 0;
|
|
170
|
+
if (key === void 0) continue;
|
|
171
|
+
out.push({
|
|
172
|
+
seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
|
|
173
|
+
time: data.time,
|
|
174
|
+
text: userTextOf(data.content),
|
|
175
|
+
key
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */
|
|
182
|
+
function anchorKeyOf(m) {
|
|
183
|
+
if (typeof m.key === "string" && m.key !== "") return m.key;
|
|
184
|
+
if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
|
|
185
|
+
}
|
|
186
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
187
|
+
/**
|
|
188
|
+
* Ensure the message node is loaded into the visible window, then scroll to it.
|
|
189
|
+
* loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back
|
|
190
|
+
* in a long session needs many pages. Report page count through onProgress so
|
|
191
|
+
* the UI can show a loading state instead of appearing frozen.
|
|
192
|
+
*/
|
|
193
|
+
async function jumpToMessage(sessionsService, sessionId, key, onProgress) {
|
|
194
|
+
const session = sessionsService.binding(sessionId)?.session;
|
|
195
|
+
if (session === void 0) return false;
|
|
196
|
+
let pages = 0;
|
|
197
|
+
let guard = 0;
|
|
198
|
+
while (guard++ < 120) {
|
|
199
|
+
const snapshot = session.getSnapshot();
|
|
200
|
+
if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
|
|
201
|
+
if (snapshot?.hasMore !== true) return false;
|
|
202
|
+
if (snapshot.loadingOlder === true) {
|
|
203
|
+
await delay(50);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
await session.loadOlder();
|
|
207
|
+
pages++;
|
|
208
|
+
onProgress?.(pages);
|
|
209
|
+
}
|
|
210
|
+
const scrollport = typeof document !== "undefined" ? document.querySelector("[data-conversation-scroll]") : null;
|
|
211
|
+
const row = scrollport === null ? null : scrollport.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`);
|
|
212
|
+
if (row === null) return false;
|
|
213
|
+
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
214
|
+
row.scrollIntoView({
|
|
215
|
+
behavior: reducedMotion ? "auto" : "smooth",
|
|
216
|
+
block: "center"
|
|
217
|
+
});
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */
|
|
221
|
+
function langStrings() {
|
|
222
|
+
const lang = typeof document !== "undefined" ? (document.documentElement.lang || "zh").toLowerCase() : "zh";
|
|
223
|
+
return STRINGS[lang.startsWith("zh") ? "zh" : "en"];
|
|
224
|
+
}
|
|
225
|
+
function TimelineRail({ useProjection, sessionId, sessionsService }) {
|
|
226
|
+
const t = langStrings();
|
|
227
|
+
const projected = useProjection("chatRail");
|
|
228
|
+
const session = sessionId === void 0 ? void 0 : sessionsService.binding(sessionId)?.session;
|
|
229
|
+
const fallbackStore = session === void 0 ? NOOP_STORE : session;
|
|
230
|
+
const nodeSnapshot = (0, react.useSyncExternalStore)((cb) => fallbackStore.subscribe(cb), () => fallbackStore.getSnapshot());
|
|
231
|
+
let messages = [];
|
|
232
|
+
if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
|
|
233
|
+
if (messages.length === 0) messages = collectFromNodes(nodeSnapshot);
|
|
234
|
+
const [activeIndex, setActiveIndex] = (0, react.useState)(-1);
|
|
235
|
+
const [show, setShow] = (0, react.useState)(false);
|
|
236
|
+
const [jumping, setJumping] = (0, react.useState)(false);
|
|
237
|
+
const [tip, setTip] = (0, react.useState)(null);
|
|
238
|
+
const navRef = (0, react.useRef)(null);
|
|
239
|
+
const expandedRef = (0, react.useRef)(false);
|
|
240
|
+
/** Position the tip against the item's CURRENT (post-expand) rect. */
|
|
241
|
+
const positionTip = (index) => {
|
|
242
|
+
if (index < 0) return;
|
|
243
|
+
const el = navRef.current?.querySelector(`[data-crl-index="${index}"]`);
|
|
244
|
+
if (el === null || el === void 0) return;
|
|
245
|
+
const rect = el.getBoundingClientRect();
|
|
246
|
+
setTip({
|
|
247
|
+
index,
|
|
248
|
+
x: rect.left - 12,
|
|
249
|
+
y: rect.top
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
/** Handle item hover: the collapsed state only expands the rail; the tip
|
|
253
|
+
* only triggers once the expand animation has settled (item rects are then
|
|
254
|
+
* stable, so the tip is positioned correctly). */
|
|
255
|
+
const handleItemEnter = (index) => {
|
|
256
|
+
if (!expandedRef.current) {
|
|
257
|
+
setShow(true);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
positionTip(index);
|
|
261
|
+
};
|
|
262
|
+
const handleItemLeave = (index) => {
|
|
263
|
+
setTip((prev) => prev?.index === index ? null : prev);
|
|
264
|
+
};
|
|
265
|
+
(0, react.useEffect)(() => {
|
|
266
|
+
if (!show) {
|
|
267
|
+
expandedRef.current = false;
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const el = navRef.current;
|
|
271
|
+
if (el === null) return;
|
|
272
|
+
const onTransitionEnd = (e) => {
|
|
273
|
+
if (e.propertyName !== "width") return;
|
|
274
|
+
expandedRef.current = true;
|
|
275
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
276
|
+
};
|
|
277
|
+
el.addEventListener("transitionend", onTransitionEnd);
|
|
278
|
+
return () => el.removeEventListener("transitionend", onTransitionEnd);
|
|
279
|
+
}, [show]);
|
|
280
|
+
/** Full text of a rail message from the loaded chat nodes (uncapped),
|
|
281
|
+
* falling back to the projection preview when the node is not mounted. */
|
|
282
|
+
const fullTextOf = (m) => {
|
|
283
|
+
const key = anchorKeyOf(m);
|
|
284
|
+
const nodes = nodeSnapshot?.chat?.nodes;
|
|
285
|
+
const content = (key === void 0 ? void 0 : nodes?.get(key))?.data?.content;
|
|
286
|
+
if (Array.isArray(content)) {
|
|
287
|
+
let out = "";
|
|
288
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
289
|
+
const full = out.trim();
|
|
290
|
+
if (full !== "") return full;
|
|
291
|
+
}
|
|
292
|
+
return m.text;
|
|
293
|
+
};
|
|
294
|
+
(0, react.useEffect)(() => {
|
|
295
|
+
if (session === void 0) return;
|
|
296
|
+
if (Array.isArray(projected?.messages) && projected.messages.length > 0) return;
|
|
297
|
+
let cancelled = false;
|
|
298
|
+
const run = async () => {
|
|
299
|
+
let guard = 0;
|
|
300
|
+
while (!cancelled && guard++ < 120) {
|
|
301
|
+
if (Array.isArray(projected?.messages) && projected.messages.length > 0) return;
|
|
302
|
+
const snap = session.getSnapshot();
|
|
303
|
+
if (snap?.hasMore !== true) return;
|
|
304
|
+
if (snap.loadingOlder === true) {
|
|
305
|
+
await delay(50);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
await session.loadOlder();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
run().catch(() => {});
|
|
312
|
+
return () => {
|
|
313
|
+
cancelled = true;
|
|
314
|
+
};
|
|
315
|
+
}, [
|
|
316
|
+
sessionId,
|
|
317
|
+
session === void 0 ? "none" : "ready",
|
|
318
|
+
Array.isArray(projected?.messages) && projected.messages.length > 0 ? "have" : "none"
|
|
319
|
+
]);
|
|
320
|
+
(0, react.useEffect)(() => {
|
|
321
|
+
if (messages.length === 0) return;
|
|
322
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
323
|
+
for (let i = 0; i < messages.length; i++) {
|
|
324
|
+
const key = anchorKeyOf(messages[i]);
|
|
325
|
+
if (key !== void 0) indexByKey.set(key, i);
|
|
326
|
+
}
|
|
327
|
+
const updateActive = () => {
|
|
328
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
329
|
+
if (sp === null) return;
|
|
330
|
+
const rect = sp.getBoundingClientRect();
|
|
331
|
+
if (rect.height === 0) return;
|
|
332
|
+
const line = rect.top + rect.height * .4;
|
|
333
|
+
const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"]");
|
|
334
|
+
let best = -1;
|
|
335
|
+
let bestDist = Infinity;
|
|
336
|
+
for (const row of rows) {
|
|
337
|
+
const key = row.getAttribute("data-chat-anchor-key");
|
|
338
|
+
if (key === null) continue;
|
|
339
|
+
const idx = indexByKey.get(key) ?? -1;
|
|
340
|
+
if (idx === -1) continue;
|
|
341
|
+
const r = row.getBoundingClientRect();
|
|
342
|
+
const dist = Math.abs(r.top + r.height / 2 - line);
|
|
343
|
+
if (dist < bestDist) {
|
|
344
|
+
bestDist = dist;
|
|
345
|
+
best = idx;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
setActiveIndex(best);
|
|
349
|
+
};
|
|
350
|
+
updateActive();
|
|
351
|
+
const el = document.querySelector("[data-conversation-scroll]");
|
|
352
|
+
let scrollTimer = null;
|
|
353
|
+
const onScroll = () => {
|
|
354
|
+
if (scrollTimer !== null) return;
|
|
355
|
+
scrollTimer = setTimeout(() => {
|
|
356
|
+
scrollTimer = null;
|
|
357
|
+
updateActive();
|
|
358
|
+
}, 60);
|
|
359
|
+
};
|
|
360
|
+
el?.addEventListener("scroll", onScroll, { passive: true });
|
|
361
|
+
const timer = window.setInterval(updateActive, 2e3);
|
|
362
|
+
return () => {
|
|
363
|
+
el?.removeEventListener("scroll", onScroll);
|
|
364
|
+
window.clearInterval(timer);
|
|
365
|
+
if (scrollTimer !== null) clearTimeout(scrollTimer);
|
|
366
|
+
};
|
|
367
|
+
}, [sessionId, messages.length]);
|
|
368
|
+
(0, react.useEffect)(() => {
|
|
369
|
+
if (activeIndex < 0) return;
|
|
370
|
+
const el = navRef.current;
|
|
371
|
+
if (el === null) return;
|
|
372
|
+
const item = el.querySelector(`[data-crl-index="${activeIndex}"]`);
|
|
373
|
+
if (item === null) return;
|
|
374
|
+
const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2;
|
|
375
|
+
el.scrollTo({
|
|
376
|
+
top: Math.max(0, target),
|
|
377
|
+
behavior: "smooth"
|
|
378
|
+
});
|
|
379
|
+
}, [activeIndex]);
|
|
380
|
+
if (sessionId === void 0 || messages.length < 2) return null;
|
|
381
|
+
const items = messages.map((m, i) => {
|
|
382
|
+
const key = anchorKeyOf(m);
|
|
383
|
+
return (0, react.createElement)("button", {
|
|
384
|
+
type: "button",
|
|
385
|
+
key: m.seq,
|
|
386
|
+
"data-crl-index": String(i),
|
|
387
|
+
className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ""),
|
|
388
|
+
"aria-label": `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,
|
|
389
|
+
"aria-current": activeIndex === i ? "location" : void 0,
|
|
390
|
+
disabled: jumping,
|
|
391
|
+
onClick: () => {
|
|
392
|
+
if (key === void 0 || jumping) return;
|
|
393
|
+
setJumping(true);
|
|
394
|
+
jumpToMessage(sessionsService, sessionId, key).finally(() => setJumping(false));
|
|
395
|
+
},
|
|
396
|
+
onMouseEnter: () => handleItemEnter(i),
|
|
397
|
+
onMouseLeave: () => handleItemLeave(i),
|
|
398
|
+
children: [
|
|
399
|
+
(0, react.createElement)("span", { className: S.num }, `#${i + 1}`),
|
|
400
|
+
(0, react.createElement)("span", { className: S.title }, m.text === "" ? t.noText : m.text),
|
|
401
|
+
(0, react.createElement)("span", { className: S.time }, relativeTime(m.time, t)),
|
|
402
|
+
(0, react.createElement)("span", {
|
|
403
|
+
className: S.ind,
|
|
404
|
+
"aria-hidden": true
|
|
405
|
+
}, (0, react.createElement)("span", { className: S.line }))
|
|
406
|
+
]
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
return (0, react_dom.createPortal)([(0, react.createElement)("div", {
|
|
410
|
+
ref: navRef,
|
|
411
|
+
className: S.nav + (show ? ` ${S.navShow}` : ""),
|
|
412
|
+
role: "navigation",
|
|
413
|
+
"aria-label": t.railLabel,
|
|
414
|
+
onMouseEnter: () => setShow(true),
|
|
415
|
+
onMouseLeave: () => setShow(false),
|
|
416
|
+
children: [jumping ? (0, react.createElement)("div", {
|
|
417
|
+
className: S.loading,
|
|
418
|
+
key: "loading"
|
|
419
|
+
}, (0, react.createElement)("span", { className: S.loadingLabel }, t.loading)) : null, ...items]
|
|
420
|
+
}), tip !== null && tip.index >= 0 && tip.index < messages.length ? (0, react.createElement)("div", {
|
|
421
|
+
className: S.tip,
|
|
422
|
+
style: {
|
|
423
|
+
left: `${tip.x}px`,
|
|
424
|
+
top: `${tip.y}px`,
|
|
425
|
+
transform: "translateX(-100%)"
|
|
426
|
+
}
|
|
427
|
+
}, fullTextOf(messages[tip.index]) || t.noText) : null], document.body);
|
|
428
|
+
}
|
|
429
|
+
function apply(ctx) {
|
|
430
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
431
|
+
name: "conversation.input.dock",
|
|
432
|
+
id: "chat-rail",
|
|
433
|
+
order: 40,
|
|
434
|
+
inject: () => ({ sessionsService: ctx.sessions })
|
|
435
|
+
}, TimelineRail));
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
exports.TimelineRail = TimelineRail;
|
|
439
|
+
exports.apply = apply;
|
|
440
|
+
exports.inject = inject;
|
|
441
|
+
return module.exports;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["useSyncExternalStore","useState","useRef","createElement","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\n * @max-null/dsh-chat-rail — web client half.\n *\n * Right-edge conversation navigation rail: one indicator per user message,\n * scroll-spy highlight of the reading position, hover preview, click to jump.\n *\n * Two deliberate fixes over the dsh-chat-timeline reference:\n *\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\n * `#root` margin-right transition reads the same variable). The rail's\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\n * transition timing, so rail and scrollport move together while the panel\n * expands — no \"panel first, scrollbar later\" lag.\n *\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\n * lines — nearly invisible on a light background. Here the rail keeps a\n * constant translucent capsule, thicker/darker indicator lines, and an\n * always-visible collapsed state (no \"reveal on pointer proximity\").\n *\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\n * background loadOlder loop (stopped as soon as the projection delivers).\n * Mounted in conversation.input.dock, portal-rendered to body.\n */\n\nimport { createElement, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n\nexport const inject = ['slots', 'sessions']\n\n// ---- i18n (DSH locale-aware, zh/en) ----\ntype LocaleId = 'zh' | 'en'\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\n zh: {\n railLabel: '消息导航',\n roleUser: '用户',\n noText: '(无文本内容)',\n ariaJump: '跳转到消息',\n loading: '加载中…',\n timeJustNow: '刚刚',\n timeMinutes: '{n}分钟前',\n timeHours: '{n}小时前',\n timeDays: '{n}天前',\n },\n en: {\n railLabel: 'Message rail',\n roleUser: 'User',\n noText: '(no text)',\n ariaJump: 'Jump to message',\n loading: 'Loading…',\n timeJustNow: 'just now',\n timeMinutes: '{n}m ago',\n timeHours: '{n}h ago',\n timeDays: '{n}d ago',\n },\n}\n\n/** Compact relative time for a message timestamp (zh/en via template). */\nfunction relativeTime(ts: number, s: Record<string, string>): string {\n const diff = Date.now() - ts\n if (diff < 60_000) return s.timeJustNow\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\n const d = new Date(ts)\n return `${d.getMonth() + 1}/${d.getDate()}`\n}\n\n// ---- styles ----\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\n// 面板共享同一 CSS 变量 + transition,动画同步。\nconst css = [\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 12px);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}',\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}',\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)}',\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)}',\n '.crl_nav::-webkit-scrollbar{width:4px}',\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\n // icon is always visible; the \"Loading…\" label only appears once expanded\n // (in the collapsed 36px rail the text would overflow the capsule).\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}',\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}',\n '.crl_loading .crl_loadingLabel{display:none}',\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\n // Item row: always flex-end (the indicator rides the right edge through the\n // width animation, never jumping). The collapsed padding (0 9px) centers the\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\n // the text. Padding transitions with the same timing as the width, so the\n // indicator glides smoothly instead of snapping when text hides/shows.\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}',\n '.crl_show .crl_item{padding:0 14px}',\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\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)}',\n // Title / ordinal / time: display:none in collapsed state (so they take no\n // flex space and the indicator line stays centered), revealed on expand.\n // The ordinal has a fixed width (right-aligned) so message text starts at\n // the same x across items; time sits AFTER the title (no fixed width, so it\n // never shrinks the text area).\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}',\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\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}',\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\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}',\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\n // Indicator line: 10×3px, clearly visible in both themes.\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\n '.crl_show .crl_ind{margin-left:8px}',\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}',\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\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)}',\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\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)}',\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)}',\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\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}',\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)))}',\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}}',\n].join('')\n\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-chat-rail'\n tag.dataset.pluginCss = STYLE_ID\n tag.textContent = css\n document.head.appendChild(tag)\n}\n\nconst S = {\n nav: 'crl_nav',\n navShow: 'crl_show',\n item: 'crl_item',\n itemActive: 'crl_active',\n title: 'crl_title',\n num: 'crl_num',\n time: 'crl_time',\n ind: 'crl_ind',\n line: 'crl_line',\n loading: 'crl_loading',\n loadingLabel: 'crl_loadingLabel',\n tip: 'crl_tip',\n}\n\n// ---- data helpers ----\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\n\ninterface RailMessage {\n seq: number\n time: number\n text: string\n key?: string\n id?: string\n}\n\n/** Extract preview text from a user message's ContentBlock list. */\nfunction userTextOf(content: unknown): string {\n if (!Array.isArray(content)) return ''\n let out = ''\n for (const block of content) {\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\n && typeof (block as { text?: unknown }).text === 'string') {\n out += (block as { text: string }).text\n }\n }\n return out.trim().slice(0, 80)\n}\n\n/** Normalize one projection entry to a rail message. */\nfunction normalize(m: unknown): RailMessage | null {\n if (m === null || typeof m !== 'object') return null\n const o = m as Record<string, unknown>\n if (typeof o.seq !== 'number') return null\n // Host projection emits `text`; keep `preview` as a defensive fallback in\n // case an older projection payload is still cached in the browser.\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\n return {\n seq: o.seq,\n time: typeof o.time === 'number' ? o.time : 0,\n text,\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\n }\n}\n\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\n const out: RailMessage[] = []\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\n if (!chat.nodes) return out\n for (const node of chat.nodes.values()) {\n if (node === null || typeof node !== 'object') continue\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\n if (n.kind !== 'user') continue\n const data = n.data as { time?: unknown; content?: unknown } | null\n if (data === null || typeof data !== 'object') continue\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\n const key = typeof n.key === 'string' ? n.key : undefined\n if (key === undefined) continue\n out.push({ seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0, time: data.time, text: userTextOf(data.content), key })\n }\n out.sort((a, b) => a.seq - b.seq)\n return out\n}\n\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\nfunction anchorKeyOf(m: RailMessage): string | undefined {\n if (typeof m.key === 'string' && m.key !== '') return m.key\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\n return undefined\n}\n\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\n\n/**\n * Ensure the message node is loaded into the visible window, then scroll to it.\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\n * in a long session needs many pages. Report page count through onProgress so\n * the UI can show a loading state instead of appearing frozen.\n */\nasync function jumpToMessage(\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\n sessionId: string,\n key: string,\n onProgress?: (pages: number) => void,\n): Promise<boolean> {\n const session = sessionsService.binding(sessionId)?.session\n if (session === undefined) return false\n let pages = 0\n let guard = 0\n while (guard++ < 120) {\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\n if (snapshot?.chat?.nodes?.get(key) !== undefined) break\n if (snapshot?.hasMore !== true) return false\n if (snapshot.loadingOlder === true) {\n // Another loader owns the current page; wait for it without busy-spinning.\n await delay(50)\n continue\n }\n await session.loadOlder()\n pages++\n // Report progress only on page boundaries, not every loop iteration.\n onProgress?.(pages)\n }\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\n const row = scrollport === null ? null : scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\n if (row === null) return false\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\n return true\n}\n\n// ---- component ----\ninterface TimelineRailProps {\n useProjection: (key: string) => { messages?: unknown[] } | undefined\n sessionId?: SessionId\n sessionsService: ISessions\n}\n\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\nfunction langStrings(): Record<string, string> {\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\n}\n\nfunction TimelineRail({ useProjection, sessionId, sessionsService }: TimelineRailProps): ReactNode {\n const t = langStrings()\n const projected = useProjection('chatRail')\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\n const fallbackStore = session === undefined ? NOOP_STORE : session\n const nodeSnapshot = useSyncExternalStore(\n (cb) => fallbackStore.subscribe(cb),\n () => fallbackStore.getSnapshot(),\n )\n\n let messages: RailMessage[] = []\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\n }\n if (messages.length === 0) {\n messages = collectFromNodes(nodeSnapshot)\n }\n\n const [activeIndex, setActiveIndex] = useState(-1)\n const [show, setShow] = useState(false)\n const [jumping, setJumping] = useState(false)\n // Hover full-content panel: index of the hovered item + fixed position.\n // The tip only triggers after the expand animation settles — in the\n // collapsed state the item rect is only 36px wide, so a tip positioned\n // there would be wrong once the rail expands.\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\n const navRef = useRef<HTMLDivElement | null>(null)\n // True only after the expand animation has fully settled; the width\n // transition takes ~250ms after `show` flips, and item rects are only\n // stable once it finishes. Tip positioning must wait for this.\n const expandedRef = useRef(false)\n\n /** Position the tip against the item's CURRENT (post-expand) rect. */\n const positionTip = (index: number) => {\n if (index < 0) return\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\n if (el === null || el === undefined) return\n const rect = el.getBoundingClientRect()\n setTip({ index, x: rect.left - 12, y: rect.top })\n }\n\n /** Handle item hover: the collapsed state only expands the rail; the tip\n * only triggers once the expand animation has settled (item rects are then\n * stable, so the tip is positioned correctly). */\n const handleItemEnter = (index: number) => {\n if (!expandedRef.current) {\n setShow(true)\n return\n }\n positionTip(index)\n }\n\n const handleItemLeave = (index: number) => {\n setTip((prev) => (prev?.index === index ? null : prev))\n }\n\n // Track the settled expand state: when `show` flips true, wait for the width\n // transition to end before allowing tip triggers; when it flips false, the\n // rail is collapsing — treat as unexpanded immediately.\n useEffect(() => {\n if (!show) {\n expandedRef.current = false\n return\n }\n const el = navRef.current\n if (el === null) return\n const onTransitionEnd = (e: TransitionEvent) => {\n if (e.propertyName !== 'width') return\n expandedRef.current = true\n el.removeEventListener('transitionend', onTransitionEnd)\n }\n el.addEventListener('transitionend', onTransitionEnd)\n return () => el.removeEventListener('transitionend', onTransitionEnd)\n }, [show])\n\n /** Full text of a rail message from the loaded chat nodes (uncapped),\n * falling back to the projection preview when the node is not mounted. */\n const fullTextOf = (m: RailMessage): string => {\n const key = anchorKeyOf(m)\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\n const node = key === undefined ? undefined : nodes?.get(key)\n const content = node?.data?.content\n if (Array.isArray(content)) {\n let out = ''\n for (const block of content) {\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\n && typeof (block as { text?: unknown }).text === 'string') {\n out += (block as { text: string }).text\n }\n }\n const full = out.trim()\n if (full !== '') return full\n }\n return m.text\n }\n\n // Background full-history load: follow the runtime's authoritative hasMore\n // flag, but STOP as soon as the projection delivers.\n useEffect(() => {\n if (session === undefined) return\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\n let cancelled = false\n const run = async () => {\n let guard = 0\n while (!cancelled && guard++ < 120) {\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\n if (snap?.hasMore !== true) return\n if (snap.loadingOlder === true) { await delay(50); continue }\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\n }\n }\n run().catch(() => {})\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\n\n // Track the reading position (active item): nearest row to the 40% viewport line.\n useEffect(() => {\n if (messages.length === 0) return\n const indexByKey = new Map<string, number>()\n for (let i = 0; i < messages.length; i++) {\n const key = anchorKeyOf(messages[i])\n if (key !== undefined) indexByKey.set(key, i)\n }\n const updateActive = () => {\n const sp = document.querySelector('[data-conversation-scroll]')\n if (sp === null) return\n const rect = sp.getBoundingClientRect()\n if (rect.height === 0) return\n const line = rect.top + rect.height * 0.4\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\n let best = -1\n let bestDist = Infinity\n for (const row of rows) {\n const key = row.getAttribute('data-chat-anchor-key')\n if (key === null) continue\n const idx = indexByKey.get(key) ?? -1\n if (idx === -1) continue\n const r = row.getBoundingClientRect()\n const dist = Math.abs(r.top + r.height / 2 - line)\n if (dist < bestDist) { bestDist = dist; best = idx }\n }\n setActiveIndex(best)\n }\n updateActive()\n const el = document.querySelector('[data-conversation-scroll]')\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\n const onScroll = () => {\n if (scrollTimer !== null) return\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\n }\n el?.addEventListener('scroll', onScroll, { passive: true })\n const timer = window.setInterval(updateActive, 2000)\n return () => {\n el?.removeEventListener('scroll', onScroll)\n window.clearInterval(timer)\n if (scrollTimer !== null) clearTimeout(scrollTimer)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [sessionId, messages.length])\n\n // Scroll the rail so the active item stays centered in its visible area:\n // follows the conversation's reading position (and re-centers after a\n // session switch, where activeIndex is recomputed for the new session).\n useEffect(() => {\n if (activeIndex < 0) return\n const el = navRef.current\n if (el === null) return\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\n if (item === null) return\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\n }, [activeIndex])\n\n if (sessionId === undefined || messages.length < 2) return null\n\n const items = messages.map((m, i) => {\n const key = anchorKeyOf(m)\n return createElement('button', {\n type: 'button',\n key: m.seq,\n 'data-crl-index': String(i),\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ''),\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\n 'aria-current': activeIndex === i ? 'location' : undefined,\n disabled: jumping,\n onClick: () => {\n if (key === undefined || jumping) return\n setJumping(true)\n void jumpToMessage(sessionsService as never, sessionId as string, key).finally(() => setJumping(false))\n },\n onMouseEnter: () => handleItemEnter(i),\n onMouseLeave: () => handleItemLeave(i),\n children: [\n createElement('span', { className: S.num }, `#${i + 1}`),\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\n createElement('span', { className: S.ind, 'aria-hidden': true },\n createElement('span', { className: S.line })),\n ],\n })\n })\n\n return createPortal(\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\n // child inside a transformed ancestor is positioned relative to that\n // ancestor, breaking the viewport coordinates the tip uses.\n [createElement('div', {\n ref: navRef,\n className: S.nav + (show ? ` ${S.navShow}` : ''),\n role: 'navigation',\n 'aria-label': t.railLabel,\n onMouseEnter: () => setShow(true),\n onMouseLeave: () => setShow(false),\n children: [\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\n ...items,\n ],\n }),\n // Full-content panel: anchored to the left of the hovered item.\n tip !== null && tip.index >= 0 && tip.index < messages.length\n ? createElement('div', {\n className: S.tip,\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\n }, fullTextOf(messages[tip.index]) || t.noText)\n : null],\n document.body,\n )\n}\n\nfunction apply(ctx: ClientContext): void {\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\n name: 'conversation.input.dock',\n id: 'chat-rail',\n order: 40,\n inject: () => ({ sessionsService: ctx.sessions }),\n }, TimelineRail))\n}\n\nexport { apply, TimelineRail }\n"],"mappings":"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"}
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* @max-null/dsh-chat-rail — host half.
|
|
4
|
+
*
|
|
5
|
+
* Registers the `chatRail` session projection unit: a complete, durable
|
|
6
|
+
* enumeration of the session's USER-sent messages (seq / time / preview /
|
|
7
|
+
* durable message id). The client rail only needs user turns; assistant
|
|
8
|
+
* replies are excluded so the rail stays compact.
|
|
9
|
+
*
|
|
10
|
+
* Compaction deliberately does not drop user messages: dsh renders a
|
|
11
|
+
* compaction marker row at the checkpoint position but keeps the transcript
|
|
12
|
+
* above it intact, so every user-sent message stays visible on the rail.
|
|
13
|
+
*
|
|
14
|
+
* Architecture reference: dsh-chat-timeline (MIT) — same projection shape.
|
|
15
|
+
*/
|
|
16
|
+
const name = "chat-rail";
|
|
17
|
+
const PROJECTION_KEY = "chatRail";
|
|
18
|
+
/** Cap preview text so projection payloads stay small (80 chars ≈ 1-2 lines). */
|
|
19
|
+
const MAX_TEXT_CHARS = 80;
|
|
20
|
+
/** Join the text blocks of a host-side ContentBlock list. */
|
|
21
|
+
function textOf(content) {
|
|
22
|
+
if (!Array.isArray(content)) return "";
|
|
23
|
+
let out = "";
|
|
24
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
25
|
+
return out.trim().slice(0, MAX_TEXT_CHARS);
|
|
26
|
+
}
|
|
27
|
+
const messageIndexProjectionDefinition = {
|
|
28
|
+
key: PROJECTION_KEY,
|
|
29
|
+
schema: { parse: (val) => val },
|
|
30
|
+
init: () => ({ messages: [] }),
|
|
31
|
+
apply: (state, event) => {
|
|
32
|
+
if (event.type === "user/message") {
|
|
33
|
+
const data = event.data;
|
|
34
|
+
if (data === null || typeof data !== "object" || data.source === null || typeof data.source !== "object" || data.source.kind !== "user") return state;
|
|
35
|
+
const text = textOf(data.content);
|
|
36
|
+
const id = typeof data.id === "string" ? data.id : "";
|
|
37
|
+
if (!id) return state;
|
|
38
|
+
return { messages: [...state.messages, {
|
|
39
|
+
seq: event.seq,
|
|
40
|
+
time: event.time,
|
|
41
|
+
text,
|
|
42
|
+
id
|
|
43
|
+
}] };
|
|
44
|
+
}
|
|
45
|
+
return state;
|
|
46
|
+
},
|
|
47
|
+
view: (state) => state,
|
|
48
|
+
stateVersion: 4
|
|
49
|
+
};
|
|
50
|
+
const Config = { "~standard": {
|
|
51
|
+
version: 1,
|
|
52
|
+
vendor: "chat-rail",
|
|
53
|
+
validate: (value) => ({ value: value ?? {} })
|
|
54
|
+
} };
|
|
55
|
+
function apply(ctx) {
|
|
56
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
57
|
+
projectionCtx.sessionProjections.register(messageIndexProjectionDefinition);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { Config, apply, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@max-null/dsh-chat-rail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Max-Null/dsh-chat-rail.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Max-Null/dsh-chat-rail#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Max-Null/dsh-chat-rail/issues"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./lib/index.mjs",
|
|
17
|
+
"./client": "./lib/client.js",
|
|
18
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"docs/shots"
|
|
27
|
+
],
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
},
|
|
32
|
+
"client": {
|
|
33
|
+
"inject": [
|
|
34
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
35
|
+
"@deepseek-ai/dsh-client-locale",
|
|
36
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
38
|
+
],
|
|
39
|
+
"platform": "web"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"watch": "tsdown --watch",
|
|
46
|
+
"test": "node --import tsx/esm --test tests/**/*.spec.ts"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"keywords": [
|
|
50
|
+
"dsh",
|
|
51
|
+
"dsh-plugin",
|
|
52
|
+
"deepseek-harness",
|
|
53
|
+
"ssid",
|
|
54
|
+
"conversation",
|
|
55
|
+
"navigation",
|
|
56
|
+
"timeline",
|
|
57
|
+
"rail"
|
|
58
|
+
],
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.0-rc.6",
|
|
62
|
+
"dsh-better-sidebar": ">=0.12.0",
|
|
63
|
+
"react": "^18.2.0",
|
|
64
|
+
"react-dom": "^18.2.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"dsh-better-sidebar": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
73
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
76
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.0-rc.6",
|
|
77
|
+
"@types/node": "^22.0.0",
|
|
78
|
+
"@types/react": "~18.3.1",
|
|
79
|
+
"@types/react-dom": "^18.3.7",
|
|
80
|
+
"cordis": "^4.0.0-rc.7",
|
|
81
|
+
"dsh-better-sidebar": "^0.13.0",
|
|
82
|
+
"react": "^18.2.0",
|
|
83
|
+
"react-dom": "^18.2.0",
|
|
84
|
+
"tsdown": "^0.22.2",
|
|
85
|
+
"tsx": "^4.0.0",
|
|
86
|
+
"typescript": "^5.6.0"
|
|
87
|
+
}
|
|
88
|
+
}
|