@khorsheed/dsh-message-timeline 0.1.0 → 0.2.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/CHANGELOG.md +8 -0
- package/README.en.md +18 -10
- package/README.i18n.yaml +2 -2
- package/README.md +34 -26
- package/lib/client.js +175 -39
- package/lib/client.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/client/TimelineRail.d.ts +1 -1
- package/lib/types/client/TimelineRail.d.ts.map +1 -1
- package/lib/types/client/TimelineRail.js +69 -5
- package/lib/types/client/chat-hook.d.ts +35 -0
- package/lib/types/client/chat-hook.d.ts.map +1 -0
- package/lib/types/client/chat-hook.js +15 -0
- package/lib/types/client/hidden-spans.d.ts +40 -0
- package/lib/types/client/hidden-spans.d.ts.map +1 -0
- package/lib/types/client/hidden-spans.js +40 -0
- package/lib/types/client/index.d.ts +2 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/rail-tracker.d.ts +23 -11
- package/lib/types/client/rail-tracker.d.ts.map +1 -1
- package/lib/types/client/rail-tracker.js +98 -32
- package/lib/types/client/slots.d.ts +6 -2
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/client/timeline-kinds.d.ts +32 -0
- package/lib/types/client/timeline-kinds.d.ts.map +1 -0
- package/lib/types/client/timeline-kinds.js +36 -0
- package/package.json +17 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.2.0(2026-09-10)
|
|
4
|
+
|
|
5
|
+
适配宿主 0.1.2 线。
|
|
6
|
+
|
|
7
|
+
- **BREAKING**:minHost 前移至 `0.1.2-rc.1`;宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 的用户请停留在 0.1.x 线(末版 `0.1.0`)
|
|
8
|
+
- 聊天快照改读 0.1.2 的 `useChat` 单座席(rc 线的 `useSession(...).chat` 读取臂退役);座席缺席时面板静默隐藏,绝不报错
|
|
9
|
+
- 导入面迁移:client bundle 不再引用宿主已删除的 `dsh-client-runtime`
|
|
10
|
+
|
|
3
11
|
## 0.1.0(2026-08-22)
|
|
4
12
|
|
|
5
13
|
首个公开发布。
|
package/README.en.md
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every message you sent in a long conversation — visible at a glance, one click to jump back.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Once a session gets long, finding that requirement you mentioned three messages ago means scrolling up forever. This plugin parks a timeline on the chat's left edge: one row per message you sent, with a one-line preview. At rest it's just a strip of dimmed ticks that stays out of the way; hover to expand it, and clicking a row scrolls the transcript straight to that message. It only reads the session and never sends anything — the model doesn't notice it at all.
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/message-
|
|
9
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/message-timeline1.png" width="640" alt="the expanded message timeline on the chat's left edge: one row per user message, click a row to jump to it">
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **One row per user message** — tick plus ellipsized one-line preview; steering messages count too (configurable off).
|
|
13
|
+
- **One row per user message** — tick plus ellipsized one-line preview; steering messages count too (configurable off), and message-tools edited/restored bubbles keep a row. Withdrawn originals are not listed (their transcript row is hidden, so a click could never reach them).
|
|
14
14
|
- **Ambient rest state** — only dimmed ticks show until you hover the strip or focus the list.
|
|
15
15
|
- **Reading position tracking** — the current position's tick stays lit blue, anchoring to the user message a long answer is replying to.
|
|
16
16
|
- **Click to jump** — a row scrolls the transcript to that message; the list follows the reading position.
|
|
17
|
-
- **Long-history friendly** — a short list centers vertically; a long one scrolls and pages older history at its top.
|
|
17
|
+
- **Long-history friendly** — a short list centers vertically; a long one scrolls and pages older history at its top, its bottom-most row flush with the chat input box; goal/todo dock cards never push the timeline up.
|
|
18
|
+
- **Never covers the message flow** — the panel's width is capped by the scrollport's left gutter; a gutter too small for the minimum width hides it rather than overlapping the transcript.
|
|
19
|
+
|
|
20
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/message-timeline2.png" width="640" alt="the timeline at rest: a strip of dimmed ticks that stays out of the way, expanding on hover">
|
|
18
21
|
|
|
19
22
|
## Install
|
|
20
23
|
|
|
@@ -39,12 +42,15 @@ dsh plugin --profile web remove @khorsheed/dsh-message-timeline
|
|
|
39
42
|
|
|
40
43
|
## Compatibility
|
|
41
44
|
|
|
42
|
-
- npm release line (`@deepseek-ai/dsh@0.1.
|
|
43
|
-
- source line (deepseek-harness master): ✅
|
|
45
|
+
- npm release line (`@deepseek-ai/dsh@0.1.2-rc.1`): ✅ full — baseline moved to the 0.1.2-rc.1 API surface (single-arm 0.1.2 API consumption; the 0.1.1-rc.2 runtime arm is retired), full build+test green; minHost moves up to 0.1.2-rc.1 — older hosts stay on the previous release line.
|
|
46
|
+
- source line (deepseek-harness master): ✅ (verifiedHost: 0.1.2-rc.1)
|
|
47
|
+
|
|
48
|
+
**Version line mapping**: 0.2.0 and up support host `0.1.2-rc.1` and later; hosts on `0.1.0-rc.6` ~ `0.1.1-rc.2` stay on the 0.1.x release line (last release `0.1.0`).
|
|
44
49
|
|
|
45
50
|
## Known Limitations
|
|
46
51
|
|
|
47
52
|
- **DOM probe coupling** — the panel targets official row attributes and the `[data-conversation-scroll]` scrollport; if the official DOM changes, it hides itself (one `console.warn`) until the probe is updated.
|
|
53
|
+
- **Best-effort width fallback** — while the message-flow probe is unanswered, the panel renders at a capped fraction of the scrollport instead of hiding, so it may overlap the flow until the probe recovers; with a healthy probe the width never crosses the flow's left edge.
|
|
48
54
|
- **Single rendered session** — rows and jumps address only the currently rendered conversation.
|
|
49
55
|
- **Loaded history only** — rows cover materialized nodes; older messages arrive one page at a time when the panel is scrolled to its top.
|
|
50
56
|
- **No full index view yet** — a searchable message-index tab is planned.
|
|
@@ -57,14 +63,16 @@ dsh plugin --profile web remove @khorsheed/dsh-message-timeline
|
|
|
57
63
|
The plugin is purely additive and modifies no official code.
|
|
58
64
|
|
|
59
65
|
- `src/client/index.ts` — plugin body (`apply`/`inject`)
|
|
60
|
-
- `src/client/rail-tracker.ts` — the only DOM the plugin touches: read-only probes plus the jump's scroll write
|
|
66
|
+
- `src/client/rail-tracker.ts` — the only DOM the plugin touches: read-only probes, the reading-position resolution, plus the jump's scroll write
|
|
61
67
|
- `src/client/TimelineRail.tsx` — the panel component
|
|
68
|
+
- `src/client/timeline-kinds.ts` — the single source of truth for which node kinds are rows and which carry a hidden span
|
|
69
|
+
- `src/client/hidden-spans.ts` — folds the message-tools withdraw/edit spans so covered originals are dropped as dead rows
|
|
62
70
|
- `src/client/preview.ts` — message content to one-line preview text
|
|
63
71
|
- `src/index.ts` — empty host `apply`, anchors the plugin into the host Loader
|
|
64
72
|
|
|
65
|
-
**Mount & data** — one entry in the official `conversation.session.header.utilities` slot anchors the plugin into the session scope; the panel renders through a body portal with fixed geometry measured from the official scrollport. Rows derive from the framework `useSession` chat snapshot (`s.chat.order` / `s.chat.nodes`)
|
|
73
|
+
**Mount & data** — one entry in the official `conversation.session.header.utilities` slot anchors the plugin into the session scope; the panel renders through a body portal with fixed geometry measured from the official scrollport. Rows derive from the framework `useSession` chat snapshot (`s.chat.order` / `s.chat.nodes`): the ordinary `user` (and optional `steering`) rows come from the host order, and message-tools `message-tools-edited`/`message-tools-restored` bubbles that the host order does not always surface are appended from the node store — no store outside the session, no event registration. A bubble for a withdrawn original is dropped (its row is hidden by the DOM hider, so a click could never reach it), and appended bubbles are sorted by their anchor seq so the newest message is always last.
|
|
66
74
|
|
|
67
|
-
**Jump & degradation** — clicking a row finds the transcript row by the official `data-chat-anchor-key` attribute and writes `scrollTop`; the official ChatView treats that as a normal reader move (bottom-follow and scroll memory keep working). The probed attributes are official render output, not a declared API: when they change, the panel hides itself with one `console.warn`; nothing throws and the boot never fails.
|
|
75
|
+
**Jump & degradation** — clicking a row finds the transcript row by the official `data-chat-anchor-key` attribute and writes `scrollTop`; the official ChatView treats that as a normal reader move (bottom-follow and scroll memory keep working). The probed attributes are official render output, not a declared API: when they change, the panel hides itself with one `console.warn`; nothing throws and the boot never fails. The panel's width is capped by the scrollport's left gutter so it never covers the message flow; while the flow probe is unanswered (an official structure change) the width degrades to a capped fraction of the scrollport as a best-effort fallback — the panel may then overlap the flow until the probe recovers.
|
|
68
76
|
|
|
69
77
|
**Model experience: none.** The panel reads the session snapshot and scrolls the transcript; it never sends prompts, appends session events, or enters the session log. KV cache effect: none. The `enabled` config turns the plugin off entirely; removing it from cordis.yml removes every surface it adds.
|
|
70
78
|
|
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/message-timeline/README.en.md
|
|
5
|
-
packages/message-timeline/README.en.md:
|
|
6
|
-
packages/message-timeline/README.md:
|
|
5
|
+
packages/message-timeline/README.en.md: 5c8d6f4b24c2852e1d64db8aeb0a4817e0832c64
|
|
6
|
+
packages/message-timeline/README.md: 1718fba1afa90e0cdd1e23956cea3aa45fde0502
|
package/README.md
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
# dsh-message-timeline
|
|
1
|
+
# @khorsheed/dsh-message-timeline
|
|
2
2
|
|
|
3
3
|
[English](README.en.md) | 中文
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
长对话里你说过的每句话,一眼看到、一点就跳回去。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
会话一长,想找回三条消息前提过的那个要求,就只能一路往上滚。这个插件在对话左缘放了一条时间轴:你说过的每句话占一行,带单行预览;平时它收成一排压淡的刻度,不占视线,鼠标一悬停才展开,点哪一行就把会话滚到哪一句。它只读会话、不发任何消息,模型完全无感。
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/message-
|
|
9
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/message-timeline1.png" width="640" alt="对话左缘展开的消息时间轴:每行一条用户消息,点行即跳转到对应消息">
|
|
10
10
|
|
|
11
11
|
## 特性
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
13
|
+
- **每行一条用户消息**——竖刻度加单行省略预览;steering 消息也计入(可配置关闭),message-tools 的编辑/恢复气泡也保留一行。被撤回的原消息不展示(其会话行被隐藏,点击也到不了)。
|
|
14
|
+
- **环境化静止态**——只显示压淡的刻度,悬停刻度条或聚焦列表才展开文字。
|
|
15
|
+
- **阅读位置跟踪**——当前位置的刻度保持蓝色高亮,停在超长回复中时锚定在所回答的用户消息上。
|
|
16
|
+
- **点击跳转**——点击行把会话滚动到对应消息;列表跟随阅读位置。
|
|
17
|
+
- **长历史友好**——列表短时垂直居中;长时隐形滚动并在顶部翻页加载更早历史,最底部贴着聊天输入框;目标/任务等 dock 卡片不会把时间轴顶上去。
|
|
18
|
+
- **绝不遮住消息流**——面板宽度受滚动区左缘沟槽约束;沟槽放不下最小宽度时隐藏而不是盖住会话。
|
|
19
|
+
|
|
20
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/message-timeline2.png" width="640" alt="时间轴的静止态:收成一排压淡的刻度条,不挡视线,悬停才展开">
|
|
18
21
|
|
|
19
22
|
## 安装
|
|
20
23
|
|
|
@@ -22,7 +25,7 @@ dsh web GUI 的历史消息导览:会话左缘的一条悬浮时间轴,每行一
|
|
|
22
25
|
dsh plugin --profile web add @khorsheed/dsh-message-timeline
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
然后重启 web
|
|
28
|
+
然后重启 web 实例。卸载:
|
|
26
29
|
|
|
27
30
|
```sh
|
|
28
31
|
dsh plugin --profile web remove @khorsheed/dsh-message-timeline
|
|
@@ -32,47 +35,52 @@ dsh plugin --profile web remove @khorsheed/dsh-message-timeline
|
|
|
32
35
|
|
|
33
36
|
| 字段 | 默认 | 含义 |
|
|
34
37
|
| --- | --- | --- |
|
|
35
|
-
| `enabled` | `true` |
|
|
36
|
-
| `includeSteering` | `true` |
|
|
37
|
-
| `panelWidth` | `360` |
|
|
38
|
-
| `initialPages` | `5` |
|
|
38
|
+
| `enabled` | `true` | 总开关;false 时面板完全不渲染。 |
|
|
39
|
+
| `includeSteering` | `true` | 回合中插入的用户消息(steering)是否也算行。 |
|
|
40
|
+
| `panelWidth` | `360` | 面板宽度(px,限 120–640);窄列自动收缩,左缘沟槽放不下时整条隐藏。 |
|
|
41
|
+
| `initialPages` | `5` | 面板打开时预取的历史页数(每页 50 条事件);更早历史在面板滚动到顶部时按需加载(限 1–20)。 |
|
|
39
42
|
|
|
40
43
|
## Compatibility
|
|
41
44
|
|
|
42
|
-
- npm 发布线(`@deepseek-ai/dsh@0.1.
|
|
43
|
-
-
|
|
45
|
+
- npm 发布线(`@deepseek-ai/dsh@0.1.2-rc.1`):✅ 完整——基线迁移至 0.1.2-rc.1 API 面(单臂消费 0.1.2 API,0.1.1-rc.2 运行臂已退役),全量构建测试通过;minHost 前移至 0.1.2-rc.1,旧宿主请停留在旧发布线。
|
|
46
|
+
- 源码线(deepseek-harness master):✅(verifiedHost: 0.1.2-rc.1)
|
|
47
|
+
|
|
48
|
+
**版本线对照**:0.2.0 起支持宿主 `0.1.2-rc.1` 及以后;宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 的用户请停留在 0.1.x 发布线(末版 `0.1.0`)。
|
|
44
49
|
|
|
45
50
|
## 已知限制
|
|
46
51
|
|
|
47
|
-
- **依赖官方 DOM 探针** —— 面板针对官方行属性与 `[data-conversation-scroll]`
|
|
52
|
+
- **依赖官方 DOM 探针** —— 面板针对官方行属性与 `[data-conversation-scroll]` 滚动区;官方结构变化时面板自行隐藏(console.warn 一次)直到探针更新。
|
|
53
|
+
- **best-effort 宽度兜底** —— 消息流探针无应答时,面板以滚动区受限比例渲染而非隐藏,可能压到消息流直到探针恢复;探针健康时宽度绝不越过消息流左缘。
|
|
48
54
|
- **单会话渲染** —— 行与跳转只作用于当前渲染的会话。
|
|
49
|
-
- **仅已加载历史** ——
|
|
55
|
+
- **仅已加载历史** —— 行覆盖已物化的节点;更早消息在面板滚动到顶部时逐页加载。
|
|
50
56
|
- **暂无整页导览** —— 可搜索的消息索引标签页在计划中。
|
|
51
57
|
|
|
52
58
|
## 实现原理
|
|
53
59
|
|
|
54
60
|
<details>
|
|
55
|
-
<summary
|
|
61
|
+
<summary>内部结构(点击展开)</summary>
|
|
56
62
|
|
|
57
63
|
插件纯增量、不改任何官方代码。
|
|
58
64
|
|
|
59
|
-
- `src/client/index.ts` ——
|
|
60
|
-
- `src/client/rail-tracker.ts` —— 插件唯一接触的 DOM
|
|
65
|
+
- `src/client/index.ts` —— 插件主体(`apply`/`inject`)
|
|
66
|
+
- `src/client/rail-tracker.ts` —— 插件唯一接触的 DOM:只读探针、阅读位置解析,加跳转时的滚动写入
|
|
61
67
|
- `src/client/TimelineRail.tsx` —— 面板组件
|
|
68
|
+
- `src/client/timeline-kinds.ts` —— 哪些 kind 算行、哪些 kind 携带隐藏范围,的唯一权威
|
|
69
|
+
- `src/client/hidden-spans.ts` —— 折叠 message-tools 撤回/编辑范围,把被覆盖的原消息作为死行丢弃
|
|
62
70
|
- `src/client/preview.ts` —— 消息内容转单行预览文本
|
|
63
|
-
- `src/index.ts` —— 空的宿主 `apply
|
|
71
|
+
- `src/index.ts` —— 空的宿主 `apply`,只负责把插件锚定进宿主 Loader
|
|
64
72
|
|
|
65
|
-
**挂载与数据** —— 一个条目注册进官方 `conversation.session.header.utilities`
|
|
73
|
+
**挂载与数据** —— 一个条目注册进官方 `conversation.session.header.utilities` 槽位,把插件锚定进会话作用域;面板通过 body portal 以固定几何渲染,数据从官方滚动区实测。行取自框架 `useSession` 会话快照(`s.chat.order` / `s.chat.nodes`):普通 `user`(及可选 `steering`)行来自宿主 order,而宿主 order 未必补上的 message-tools `message-tools-edited`/`message-tools-restored` 气泡从节点库追加——不持有会话外状态,不注册事件。被撤回原消息的气泡会被丢弃(其会话行已被 DOM hider 隐藏,点击到不了),追加的气泡按锚定 seq 排序,保证最新消息恒为最后一行。
|
|
66
74
|
|
|
67
|
-
**跳转与降级** —— 点击行按官方 `data-chat-anchor-key` 属性找到会话行并写 `scrollTop
|
|
75
|
+
**跳转与降级** —— 点击行按官方 `data-chat-anchor-key` 属性找到会话行并写 `scrollTop`;官方 ChatView 把这种程序化滚动当正常读者移动处理(底部跟随与滚动记忆照常工作)。探测的属性是官方渲染产物而非契约 API:官方改结构时面板自行隐藏并 console.warn 一次,不抛错、boot 永不失败。面板宽度受左缘沟槽约束、不越过消息流左缘;流探针无应答(官方结构变化)时宽度退化为滚动区受限比例以 best-effort 兜底——此时面板可能压到消息流,直到探针恢复。
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
**模型体验:无。** 面板只读会话快照并滚动会话,不发送提示词、不追加会话事件、不进会话日志。KV 缓存影响:无。`enabled` 配置可整体关闭插件;从 cordis.yml 移除本插件即移除它添加的所有界面。
|
|
70
78
|
|
|
71
79
|
</details>
|
|
72
80
|
|
|
73
81
|
## 开发
|
|
74
82
|
|
|
75
|
-
隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo
|
|
83
|
+
隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/message-timeline`)。问题与贡献请移步该仓库。
|
|
76
84
|
|
|
77
85
|
## 变更记录
|
|
78
86
|
|
package/lib/client.js
CHANGED
|
@@ -40,6 +40,27 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
"rail.panel": "Message timeline",
|
|
41
41
|
"rail.empty": "No user messages"
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Whether a node kind renders as a timeline row. The rail lists user messages
|
|
45
|
+
* (and optionally steering), plus the message-tools edited/restored bubbles
|
|
46
|
+
* that the host `order` does not always surface.
|
|
47
|
+
* @param kind - the node's kind string.
|
|
48
|
+
* @param includeSteering - whether steering rows count as user messages.
|
|
49
|
+
* @returns true when the kind belongs on the timeline.
|
|
50
|
+
*/
|
|
51
|
+
function isTimelineRowKind(kind, includeSteering) {
|
|
52
|
+
return kind === "user" || includeSteering && kind === "steering" || kind === "message-tools-edited" || kind === "message-tools-restored";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether a node kind carries a hidden span (`{ hiddenStartSeq, seq }`) that
|
|
56
|
+
* the rail must fold to drop the covered originals. The withdrawal divider and
|
|
57
|
+
* the edited bubble both declare the span they shadow.
|
|
58
|
+
* @param kind - the node's kind string.
|
|
59
|
+
* @returns true when the kind declares a hidden span.
|
|
60
|
+
*/
|
|
61
|
+
function isHiddenSpanCarrierKind(kind) {
|
|
62
|
+
return kind === "message-tools-withdrawn" || kind === "message-tools-edited";
|
|
63
|
+
}
|
|
43
64
|
//#endregion
|
|
44
65
|
//#region src/client/rail-tracker.ts
|
|
45
66
|
/** Horizontal inset of the rail from the scrollport's left edge (px). */
|
|
@@ -62,10 +83,15 @@ window.__ModuleLoader__.load({
|
|
|
62
83
|
};
|
|
63
84
|
/**
|
|
64
85
|
* Measure the panel's viewport box from one scrollport: its rect inset by the
|
|
65
|
-
* panel padding, minus the
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
86
|
+
* panel padding, minus the chat input card at the bottom and the conversation
|
|
87
|
+
* tab strip at the top. The tabs render just above the scrollport, but
|
|
88
|
+
* centering reads against the whole window, so the strip height leaves the
|
|
89
|
+
* box either way — otherwise the list sits visibly high.
|
|
90
|
+
* The bottom ends at the `[data-composer-card]` top — the chat box — NOT the
|
|
91
|
+
* whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above
|
|
92
|
+
* the input inside the seat, and counting them would push the timeline up off
|
|
93
|
+
* the conversation. Falls back to the seat top, then the column bottom, when
|
|
94
|
+
* the markers are absent.
|
|
69
95
|
* The scrollport's own width rides along for the width-cap fallback when the
|
|
70
96
|
* message-flow probe is unanswered.
|
|
71
97
|
* @param scrollport - the official conversation scrollport element.
|
|
@@ -75,7 +101,6 @@ window.__ModuleLoader__.load({
|
|
|
75
101
|
function measureGeometry(scrollport) {
|
|
76
102
|
const rect = scrollport.getBoundingClientRect();
|
|
77
103
|
if (rect.width === 0 && rect.height === 0) return null;
|
|
78
|
-
const composerHeight = scrollport.querySelector("[data-composer-seat]")?.getBoundingClientRect().height ?? 0;
|
|
79
104
|
let topInset = RAIL_VERTICAL_PADDING;
|
|
80
105
|
for (const tabs of scrollport.ownerDocument.querySelectorAll("[role=\"tablist\"]")) {
|
|
81
106
|
const tabsRect = tabs.getBoundingClientRect();
|
|
@@ -84,25 +109,40 @@ window.__ModuleLoader__.load({
|
|
|
84
109
|
break;
|
|
85
110
|
}
|
|
86
111
|
}
|
|
112
|
+
const card = scrollport.querySelector("[data-composer-card]");
|
|
113
|
+
const seat = scrollport.querySelector("[data-composer-seat]");
|
|
114
|
+
const bottom = card !== null ? card.getBoundingClientRect().top - RAIL_VERTICAL_PADDING : seat !== null ? seat.getBoundingClientRect().top - RAIL_VERTICAL_PADDING : rect.bottom - RAIL_VERTICAL_PADDING;
|
|
87
115
|
return {
|
|
88
116
|
left: rect.left + RAIL_LEFT_INSET,
|
|
89
117
|
top: rect.top + topInset,
|
|
90
|
-
height: Math.max(0,
|
|
118
|
+
height: Math.max(0, bottom - rect.top - topInset),
|
|
91
119
|
width: rect.width
|
|
92
120
|
};
|
|
93
121
|
}
|
|
94
122
|
/**
|
|
95
123
|
* The viewport x of the message flow's left edge: the left of the first
|
|
96
|
-
*
|
|
97
|
-
* centered content column (max 748px, `margin: 0 auto`).
|
|
98
|
-
*
|
|
99
|
-
* stays left of it — the panel may only occupy the scrollport's left
|
|
124
|
+
* laid-out `[data-chat-flow-kind]` row, which sits flush inside the official
|
|
125
|
+
* centered content column (max 748px, `margin: 0 auto`). Laid-out flow rows
|
|
126
|
+
* share that edge, so the first meaningful one suffices. The panel's right
|
|
127
|
+
* edge stays left of it — the panel may only occupy the scrollport's left
|
|
128
|
+
* gutter.
|
|
129
|
+
*
|
|
130
|
+
* Rows that are not laid out in the flow are skipped: a message-tools edit
|
|
131
|
+
* leaves the withdrawn originals in the DOM (hidden, zero-size, or off the
|
|
132
|
+
* column at x=0), and probing their left edge yields 0 — which would make the
|
|
133
|
+
* width gate compute a negative left gutter and hide the entire rail. Any
|
|
134
|
+
* real flow row sits inside the conversation column at a positive x.
|
|
100
135
|
* @param scrollport - the official conversation scrollport element.
|
|
101
|
-
* @returns the flow's left edge, or null while no flow row is rendered.
|
|
136
|
+
* @returns the flow's left edge, or null while no laid-out flow row is rendered.
|
|
102
137
|
*/
|
|
103
138
|
function flowLeftX(scrollport) {
|
|
104
|
-
const row
|
|
105
|
-
|
|
139
|
+
for (const row of scrollport.querySelectorAll("[data-chat-flow-kind]")) {
|
|
140
|
+
const rect = row.getBoundingClientRect();
|
|
141
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
142
|
+
if (rect.left <= 0) continue;
|
|
143
|
+
return rect.left;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
106
146
|
}
|
|
107
147
|
/**
|
|
108
148
|
* Resolve the key of the user-message row the reading position belongs to:
|
|
@@ -120,7 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
120
160
|
let lastAbove = null;
|
|
121
161
|
for (const row of scrollport.querySelectorAll("[data-chat-flow-kind]")) {
|
|
122
162
|
const kind = row.dataset.chatFlowKind;
|
|
123
|
-
if (kind
|
|
163
|
+
if (kind === void 0 || !isTimelineRowKind(kind, includeSteering)) continue;
|
|
124
164
|
if (row.getBoundingClientRect().bottom <= viewTop) {
|
|
125
165
|
lastAbove = row.dataset.chatAnchorKey ?? lastAbove;
|
|
126
166
|
continue;
|
|
@@ -159,6 +199,7 @@ window.__ModuleLoader__.load({
|
|
|
159
199
|
let resizeObserver;
|
|
160
200
|
let rafPending = false;
|
|
161
201
|
let warned = false;
|
|
202
|
+
let bindToken = 0;
|
|
162
203
|
const nextFrame = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (callback) => {
|
|
163
204
|
setTimeout(callback, 16);
|
|
164
205
|
};
|
|
@@ -168,9 +209,28 @@ window.__ModuleLoader__.load({
|
|
|
168
209
|
state = next;
|
|
169
210
|
for (const fn of [...listeners]) fn();
|
|
170
211
|
};
|
|
212
|
+
const attach = (el) => {
|
|
213
|
+
if (scrollport !== null) scrollport.removeEventListener("scroll", onScroll);
|
|
214
|
+
resizeObserver?.disconnect();
|
|
215
|
+
scrollport = el;
|
|
216
|
+
scrollport.addEventListener("scroll", onScroll, { passive: true });
|
|
217
|
+
if (typeof ResizeObserver === "function") {
|
|
218
|
+
resizeObserver = new ResizeObserver(scheduleUpdate);
|
|
219
|
+
resizeObserver.observe(el);
|
|
220
|
+
const composer = el.querySelector("[data-composer-seat]");
|
|
221
|
+
if (composer !== null) resizeObserver.observe(composer);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
171
224
|
const update = () => {
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
let el = scrollport;
|
|
226
|
+
if (el === null || !el.isConnected) {
|
|
227
|
+
if (activeSession === void 0) return;
|
|
228
|
+
const found = document.querySelector("[data-conversation-scroll]");
|
|
229
|
+
if (found === null) return;
|
|
230
|
+
attach(found);
|
|
231
|
+
el = found;
|
|
232
|
+
}
|
|
233
|
+
const geometry = measureGeometry(el);
|
|
174
234
|
if (geometry === null) return;
|
|
175
235
|
publish({
|
|
176
236
|
sessionId: activeSession,
|
|
@@ -179,9 +239,9 @@ window.__ModuleLoader__.load({
|
|
|
179
239
|
top: geometry.top,
|
|
180
240
|
height: geometry.height,
|
|
181
241
|
scrollportWidth: geometry.width,
|
|
182
|
-
flowLeft: flowLeftX(
|
|
183
|
-
activeKey: activeRowKey(
|
|
184
|
-
chatView:
|
|
242
|
+
flowLeft: flowLeftX(el),
|
|
243
|
+
activeKey: activeRowKey(el, includeSteering),
|
|
244
|
+
chatView: el.querySelector("[data-chat-flow]") !== null
|
|
185
245
|
});
|
|
186
246
|
};
|
|
187
247
|
const scheduleUpdate = () => {
|
|
@@ -196,6 +256,7 @@ window.__ModuleLoader__.load({
|
|
|
196
256
|
scheduleUpdate();
|
|
197
257
|
};
|
|
198
258
|
const teardownBind = () => {
|
|
259
|
+
bindToken++;
|
|
199
260
|
if (scrollport !== null) scrollport.removeEventListener("scroll", onScroll);
|
|
200
261
|
scrollport = null;
|
|
201
262
|
resizeObserver?.disconnect();
|
|
@@ -210,7 +271,9 @@ window.__ModuleLoader__.load({
|
|
|
210
271
|
publish(IDLE);
|
|
211
272
|
return;
|
|
212
273
|
}
|
|
274
|
+
const token = bindToken;
|
|
213
275
|
nextFrame(() => {
|
|
276
|
+
if (token !== bindToken) return;
|
|
214
277
|
const found = document.querySelector("[data-conversation-scroll]");
|
|
215
278
|
if (found === null) {
|
|
216
279
|
if (!warned) {
|
|
@@ -220,14 +283,7 @@ window.__ModuleLoader__.load({
|
|
|
220
283
|
publish(IDLE);
|
|
221
284
|
return;
|
|
222
285
|
}
|
|
223
|
-
|
|
224
|
-
scrollport.addEventListener("scroll", onScroll, { passive: true });
|
|
225
|
-
if (typeof ResizeObserver === "function") {
|
|
226
|
-
resizeObserver = new ResizeObserver(scheduleUpdate);
|
|
227
|
-
resizeObserver.observe(scrollport);
|
|
228
|
-
const composer = scrollport.querySelector("[data-composer-seat]");
|
|
229
|
-
if (composer !== null) resizeObserver.observe(composer);
|
|
230
|
-
}
|
|
286
|
+
attach(found);
|
|
231
287
|
update();
|
|
232
288
|
});
|
|
233
289
|
};
|
|
@@ -235,7 +291,7 @@ window.__ModuleLoader__.load({
|
|
|
235
291
|
bind(ctx.sessions.list.getSnapshot().current);
|
|
236
292
|
};
|
|
237
293
|
const stopList = ctx.sessions.list.subscribe(bindCurrent);
|
|
238
|
-
const stopProvide = ctx.sessions.currentProvideInfo
|
|
294
|
+
const stopProvide = ctx.sessions.currentProvideInfo?.subscribe(bindCurrent);
|
|
239
295
|
bindCurrent();
|
|
240
296
|
const onWindowResize = () => {
|
|
241
297
|
scheduleUpdate();
|
|
@@ -264,7 +320,7 @@ window.__ModuleLoader__.load({
|
|
|
264
320
|
},
|
|
265
321
|
dispose: () => {
|
|
266
322
|
stopList();
|
|
267
|
-
stopProvide();
|
|
323
|
+
stopProvide?.();
|
|
268
324
|
if (typeof window !== "undefined") window.removeEventListener("resize", onWindowResize);
|
|
269
325
|
mutationObserver?.disconnect();
|
|
270
326
|
teardownBind();
|
|
@@ -272,6 +328,26 @@ window.__ModuleLoader__.load({
|
|
|
272
328
|
};
|
|
273
329
|
}
|
|
274
330
|
//#endregion
|
|
331
|
+
//#region src/client/chat-hook.ts
|
|
332
|
+
/** Empty slice behind the absent-prop degrade: no rows, so the panel hides. */
|
|
333
|
+
const EMPTY_SLICE = {
|
|
334
|
+
order: [],
|
|
335
|
+
nodes: {
|
|
336
|
+
get: () => void 0,
|
|
337
|
+
values: () => []
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
/** Constant hook answering every selector from the empty slice. */
|
|
341
|
+
const EMPTY_HOOK = (selector) => selector(EMPTY_SLICE);
|
|
342
|
+
/**
|
|
343
|
+
* Pick the live chat hook for this entry.
|
|
344
|
+
* @param props - the entry's composed props (useChat present on 0.1.2 hosts).
|
|
345
|
+
* @returns the `useChat` prop, or the empty-slice hook when the seat is absent.
|
|
346
|
+
*/
|
|
347
|
+
function chatHookOf(props) {
|
|
348
|
+
return props.useChat ?? EMPTY_HOOK;
|
|
349
|
+
}
|
|
350
|
+
//#endregion
|
|
275
351
|
//#region src/client/preview.ts
|
|
276
352
|
/**
|
|
277
353
|
* Join the text blocks of one message into a single preview string.
|
|
@@ -284,7 +360,44 @@ window.__ModuleLoader__.load({
|
|
|
284
360
|
return parts.length === 0 ? null : parts.join("\n");
|
|
285
361
|
}
|
|
286
362
|
//#endregion
|
|
287
|
-
//#region
|
|
363
|
+
//#region src/client/hidden-spans.ts
|
|
364
|
+
/**
|
|
365
|
+
* Fold the hidden seq spans out of the store into the flat
|
|
366
|
+
* `[start, endExclusive, ...]` pair list ordered by start — the same shape
|
|
367
|
+
* message-tools' renderers and DOM hider consume (`{ hiddenStartSeq, seq }`,
|
|
368
|
+
* inclusive start, exclusive end). A malformed carrier (missing or inverted
|
|
369
|
+
* bounds) is skipped rather than poisoning the fold.
|
|
370
|
+
* @param nodes - every materialized chat node (any order).
|
|
371
|
+
* @returns the flattened hidden spans; empty when no withdraw/edit landed.
|
|
372
|
+
*/
|
|
373
|
+
function foldHiddenSpans(nodes) {
|
|
374
|
+
const pairs = [];
|
|
375
|
+
for (const node of nodes) {
|
|
376
|
+
if (!isHiddenSpanCarrierKind(node.kind)) continue;
|
|
377
|
+
const data = node.data;
|
|
378
|
+
if (data.hiddenStartSeq === void 0 || data.seq === void 0) continue;
|
|
379
|
+
if (data.hiddenStartSeq >= data.seq) continue;
|
|
380
|
+
pairs.push([data.hiddenStartSeq, data.seq]);
|
|
381
|
+
}
|
|
382
|
+
pairs.sort((left, right) => left[0] - right[0]);
|
|
383
|
+
return pairs.flat();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Whether a node seq falls inside one of the folded hidden spans.
|
|
387
|
+
* @param spans - the flattened spans from {@link foldHiddenSpans}.
|
|
388
|
+
* @param seq - the node seq to test.
|
|
389
|
+
* @returns true when the seq is hidden (a withdrawn original).
|
|
390
|
+
*/
|
|
391
|
+
function isSeqHidden(spans, seq) {
|
|
392
|
+
for (let index = 0; index + 1 < spans.length; index += 2) {
|
|
393
|
+
const start = spans[index];
|
|
394
|
+
const end = spans[index + 1];
|
|
395
|
+
if (start !== void 0 && end !== void 0 && seq >= start && seq < end) return true;
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region \0dsh-css:src/client/TimelineRail.module.css.mjs
|
|
288
401
|
const css = "._8u17xW_panel{scrollbar-width:none;z-index:100;pointer-events:none;flex-direction:column;padding:4px 8px;display:flex;position:fixed;overflow-y:auto}._8u17xW_panel:before,._8u17xW_panel:after{content:\"\";margin:auto}._8u17xW_panel::-webkit-scrollbar{display:none}._8u17xW_item{width:100%;max-width:20px;color:var(--dsw-alias-label-tertiary);text-align:left;cursor:pointer;-webkit-user-select:none;user-select:none;pointer-events:auto;opacity:.55;background:0 0;border:none;flex:none;align-items:center;gap:8px;padding:5px 0;font-size:13px;line-height:1.5;transition:opacity .12s;display:flex}._8u17xW_panel:hover ._8u17xW_item,._8u17xW_panel:focus-within ._8u17xW_item{opacity:1;max-width:100%}._8u17xW_tick{background:var(--dsw-static-deepseek-400);border-radius:2px;flex:none;width:3px;height:14px}._8u17xW_itemCurrent{color:var(--dsw-static-deepseek-500);opacity:.75}._8u17xW_tickCurrent{background:var(--dsw-static-deepseek-500)}._8u17xW_itemText{white-space:nowrap;text-overflow:ellipsis;opacity:0;transition:opacity .12s;overflow:hidden}._8u17xW_panel:hover ._8u17xW_itemText,._8u17xW_panel:focus-within ._8u17xW_itemText{opacity:1;transition-delay:90ms}._8u17xW_itemFocused{color:var(--dsw-alias-label-primary);font-weight:600}._8u17xW_item:focus:not(:focus-visible){outline:none}._8u17xW_item:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:1px}";
|
|
289
402
|
const tagId = "@khorsheed/dsh-message-timeline/TimelineRail.module.css";
|
|
290
403
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
@@ -295,13 +408,13 @@ window.__ModuleLoader__.load({
|
|
|
295
408
|
document.head.appendChild(tag);
|
|
296
409
|
}
|
|
297
410
|
var TimelineRail_module_css_default = {
|
|
298
|
-
"
|
|
299
|
-
"itemText": "_8u17xW_itemText",
|
|
411
|
+
"item": "_8u17xW_item",
|
|
300
412
|
"tick": "_8u17xW_tick",
|
|
301
413
|
"itemCurrent": "_8u17xW_itemCurrent",
|
|
302
|
-
"
|
|
414
|
+
"panel": "_8u17xW_panel",
|
|
303
415
|
"tickCurrent": "_8u17xW_tickCurrent",
|
|
304
|
-
"
|
|
416
|
+
"itemText": "_8u17xW_itemText",
|
|
417
|
+
"itemFocused": "_8u17xW_itemFocused"
|
|
305
418
|
};
|
|
306
419
|
//#endregion
|
|
307
420
|
//#region src/client/TimelineRail.tsx
|
|
@@ -337,24 +450,47 @@ window.__ModuleLoader__.load({
|
|
|
337
450
|
* @param props - composed props (see {@link TimelineRailProps}).
|
|
338
451
|
* @returns nothing visible in the seat; the floating panel while the chat view shows.
|
|
339
452
|
*/
|
|
340
|
-
function TimelineRail({
|
|
453
|
+
function TimelineRail({ sessionId, includeSteering, panelWidth, initialPages, loadOlder, jumpTo, useRail, t, ...standard }) {
|
|
341
454
|
const rail = useRail((s) => s);
|
|
342
|
-
const
|
|
343
|
-
const
|
|
455
|
+
const useChatSlice = chatHookOf(standard);
|
|
456
|
+
const useSession = standard.useSession;
|
|
457
|
+
const order = useChatSlice((c) => c.order);
|
|
458
|
+
const nodes = useChatSlice((c) => c.nodes);
|
|
344
459
|
const hasMore = useSession((s) => s.hasMore);
|
|
345
460
|
const loadingOlder = useSession((s) => s.loadingOlder);
|
|
346
461
|
const items = (0, react.useMemo)(() => {
|
|
462
|
+
let store = [];
|
|
463
|
+
let spans = [];
|
|
464
|
+
try {
|
|
465
|
+
store = nodes.values();
|
|
466
|
+
spans = foldHiddenSpans(store);
|
|
467
|
+
} catch {}
|
|
468
|
+
const hidden = (node) => isSeqHidden(spans, node.anchorSeq);
|
|
347
469
|
const result = [];
|
|
470
|
+
const seen = /* @__PURE__ */ new Set();
|
|
348
471
|
for (const key of order) {
|
|
349
472
|
const node = nodes.get(key);
|
|
350
473
|
if (node === void 0) continue;
|
|
351
|
-
|
|
352
|
-
if (
|
|
474
|
+
if (!isTimelineRowKind(node.kind, includeSteering)) continue;
|
|
475
|
+
if (hidden(node)) continue;
|
|
353
476
|
result.push({
|
|
354
477
|
key,
|
|
355
478
|
node
|
|
356
479
|
});
|
|
480
|
+
seen.add(key);
|
|
481
|
+
}
|
|
482
|
+
for (const node of store) {
|
|
483
|
+
if (node.kind !== "message-tools-edited" && node.kind !== "message-tools-restored") continue;
|
|
484
|
+
if (node.visibility === "hidden") continue;
|
|
485
|
+
if (seen.has(node.key)) continue;
|
|
486
|
+
if (hidden(node)) continue;
|
|
487
|
+
result.push({
|
|
488
|
+
key: node.key,
|
|
489
|
+
node
|
|
490
|
+
});
|
|
491
|
+
seen.add(node.key);
|
|
357
492
|
}
|
|
493
|
+
result.sort((left, right) => left.node.anchorSeq - right.node.anchorSeq);
|
|
358
494
|
return result;
|
|
359
495
|
}, [
|
|
360
496
|
order,
|