@khorsheed/dsh-message-timeline 0.2.6 → 0.2.8
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 +26 -71
- package/README.i18n.yaml +2 -2
- package/README.md +23 -68
- package/lib/client.js +30 -17
- package/lib/types/client/rail-tracker.d.ts +3 -2
- package/lib/types/client/rail-tracker.js +27 -9
- package/lib/types/client/slots.d.ts +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.2.8(2026-10-27)
|
|
4
|
+
|
|
5
|
+
- **修复会话底部的消息点击后无法选中**:点击最新一条或底部可视区内的后续消息时,浏览器可能已把滚动位置钳制在最大值,原先高亮因此回退到第一条可见消息。现在跳转会保留实际点击落点;真正滚动、目标行移除或会话重绑后再恢复普通阅读位置跟踪。
|
|
6
|
+
|
|
7
|
+
## 0.2.7(2026-10-26)
|
|
8
|
+
|
|
9
|
+
- **README 重写**:改为面向用户的简短说明。无功能变更。
|
|
10
|
+
|
|
3
11
|
## 0.2.6(2026-10-24)
|
|
4
12
|
|
|
5
13
|
- 窄布局下退化为刻度条而不是整个隐藏;测量轨道高度时跳过隐藏的输入卡片;面板高度贴合行数,不再撑满整条轨道。
|
package/README.en.md
CHANGED
|
@@ -1,91 +1,46 @@
|
|
|
1
|
-
# dsh-message-timeline
|
|
1
|
+
# @khorsheed/dsh-message-timeline
|
|
2
2
|
|
|
3
3
|
English | [中文](README.md)
|
|
4
4
|
|
|
5
|
-
Every message you sent in a long conversation
|
|
5
|
+
Every message you sent in a long conversation, visible at a glance and one click away.
|
|
6
6
|
|
|
7
|
-
Once a session gets long, finding that
|
|
7
|
+
Once a session gets long, finding that request you made earlier means scrolling all the way up. With Message Timeline, a thin rail appears on the left of the conversation: one tick per message you sent, faded so it stays out of the way. Hover it and it opens into one-line previews; click a line and the conversation scrolls right to it.
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline1.png" width="
|
|
9
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline1.png" width="560" alt="the message timeline expanded on the conversation's left edge: one row per user message; click a row to jump to it">
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## What it does
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **Width adapts to the layout** — on wide layouts the panel's width is capped by the scrollport's left gutter; on narrow ones (a right sidebar or a small window, e.g. a Room's member panel open) it degrades to a tick strip in the flow's left padding and expands into a solid-background overlay on hover/focus instead of hiding.
|
|
13
|
+
- **Everything from the first message**: every message you sent in the session is listed with a one-line preview; click an early one that isn't loaded yet and it pages history back first, then lands there.
|
|
14
|
+
- **One click to jump back**: click a line and the conversation scrolls to that message.
|
|
15
|
+
- **Know where you are**: the tick for your current spot is blue, longer and bold, the rest stay gray; inside a very long reply, it marks the message that reply answers.
|
|
16
|
+
- **Messages without text still get a line**: image messages, attachment messages, and automatic turns (restart recovery, scheduled tasks and so on) are labelled and clickable too.
|
|
17
|
+
- **Quiet until you need it**: at rest it is a row of faint gray ticks; text appears only on hover or keyboard focus.
|
|
18
|
+
- **Works in narrow windows**: with the right sidebar open or the window narrow, it shrinks to a rail on the left and floats out on hover.
|
|
20
19
|
|
|
21
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline2.png" width="
|
|
20
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline2.png" width="560" alt="the timeline at rest: a faint strip of ticks that stays out of the way and expands on hover">
|
|
22
21
|
|
|
23
22
|
## Install
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
dsh plugin --profile web add @khorsheed/dsh-message-timeline
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Then restart the web instance. Uninstall:
|
|
24
|
+
In DSH, open Plugins → Add plugin, enter the package name below, and click install:
|
|
30
25
|
|
|
31
|
-
```
|
|
32
|
-
|
|
26
|
+
```
|
|
27
|
+
@khorsheed/dsh-basic
|
|
33
28
|
```
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| Field | Default | Meaning |
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| `enabled` | `true` | Master switch; false hides the panel entirely. |
|
|
40
|
-
| `includeSteering` | `true` | Count steering messages (user text admitted mid-turn) as rows. |
|
|
41
|
-
| `panelWidth` | `360` | Panel width in px (clamped 120–640); shrinks to fit a narrow column, hides entirely when the gutter is too small. |
|
|
42
|
-
| `initialPages` | `5` | Window-paging fallback only: history pages (50 events each) prefetched on open (clamped 1–20). When the host serves the turnOutline projection the list starts whole and this does nothing. |
|
|
43
|
-
|
|
44
|
-
## Compatibility
|
|
45
|
-
|
|
46
|
-
- 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.
|
|
47
|
-
- source line (deepseek-harness master): ✅ (verifiedHost: 0.2.1-rc.1) — host-021 wave: full build+test green against the 0.2.1-rc.1 tree, no other surface changes.
|
|
48
|
-
- Degraded item: the whole-session list relies on the host `turnOutline` projection (`@deepseek-ai/dsh-session-turn-outline`, mounted by the official web bundle since 0.1.2-alpha.3 — below the minHost floor). An assembly without the unit falls back to window paging: older history loads page by page, fully functional, but the first message appears only once paged in.
|
|
49
|
-
|
|
50
|
-
**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`).
|
|
51
|
-
|
|
52
|
-
## Known Limitations
|
|
53
|
-
|
|
54
|
-
- **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.
|
|
55
|
-
- **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.
|
|
56
|
-
- **Single rendered session** — rows and jumps address only the currently rendered conversation.
|
|
57
|
-
- **The outline is turn-grained** — the projection keeps only each turn's first-prompt preview: steering rows appear once their turn pages into the window; a turn whose first message was withdrawn stays listed per the outline (matching the official turn rail), and confirming it lands on the turn's nearest visible row.
|
|
58
|
-
- **No full index view yet** — a searchable message-index tab is planned.
|
|
59
|
-
|
|
60
|
-
## How it works
|
|
61
|
-
|
|
62
|
-
<details>
|
|
63
|
-
<summary>Internals (click to expand)</summary>
|
|
64
|
-
|
|
65
|
-
The plugin is purely additive and modifies no official code.
|
|
66
|
-
|
|
67
|
-
- `src/client/index.ts` — plugin body (`apply`/`inject`)
|
|
68
|
-
- `src/client/rail-tracker.ts` — the only DOM the plugin touches: read-only probes, the reading-position resolution, plus the jump's scroll write
|
|
69
|
-
- `src/client/TimelineRail.tsx` — the panel component
|
|
70
|
-
- `src/client/outline.ts` — the `turnOutline` wire narrow (absent or damaged values both read as "capability absent")
|
|
71
|
-
- `src/client/rows.ts` — the merge of loaded-window rows with outline rows (single interleaving pass, transcript-ordered)
|
|
72
|
-
- `src/client/timeline-kinds.ts` — the single source of truth for which node kinds are rows and which carry a hidden span
|
|
73
|
-
- `src/client/hidden-spans.ts` — folds the message-tools withdraw/edit spans so covered originals are dropped as dead rows
|
|
74
|
-
- `src/client/preview.ts` — message content to one-line preview text
|
|
75
|
-
- `src/index.ts` — empty host `apply`, anchors the plugin into the host Loader
|
|
76
|
-
|
|
77
|
-
**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 merge two sources: the host `turnOutline` projection (the `useProjection` standard seat — one entry per started turn: its number, its `turn/start` seq, and a 50-char first-prompt preview) lists every turn of the session, and the loaded window (`useChat`'s `order` / `nodes`) overlays richer previews, steering rows, and message-tools bubbles for the turns it covers (`message-tools-edited`/`message-tools-restored` bubbles the host order does not always surface are appended from the node store). 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. Without the projection, the window rows alone carry the list.
|
|
78
|
-
|
|
79
|
-
**Jump & degradation** — clicking a loaded 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). Clicking an outline row goes through the session's jump loader (`ISession.loadThrough`, since 0.1.2-alpha.3): history pages back through the turn's `turn/start` seq, and the rail lands the moment the row materializes; when paging stalls or the turn is unreachable (e.g. its first message was withdrawn), it lands on the nearest visible row after the turn instead. 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.
|
|
30
|
+
Message Timeline comes with the Basic Essentials suite. Once installed, open any session with a few turns and you'll see it on the left. It only reads the conversation and never sends anything, so the agent doesn't know it exists.
|
|
80
31
|
|
|
81
|
-
|
|
32
|
+
## Known limits
|
|
82
33
|
|
|
83
|
-
|
|
34
|
+
- It recognizes the official page layout; if a DSH update changes it beyond recognition, the timeline hides itself until the plugin is updated.
|
|
35
|
+
- It only works on the session that is currently open.
|
|
36
|
+
- A message you sent mid-turn shows up in the list once that turn is loaded.
|
|
84
37
|
|
|
85
|
-
##
|
|
38
|
+
## Compatibility
|
|
86
39
|
|
|
87
|
-
|
|
40
|
+
Needs DSH 0.1.2-rc.1 or later; verified on 0.2.1-rc.1.
|
|
88
41
|
|
|
89
|
-
##
|
|
42
|
+
## For developers
|
|
90
43
|
|
|
91
|
-
|
|
44
|
+
- Where rows come from, how jumping works, the config fields (`enabled`, `includeSteering`, `panelWidth`, `initialPages`), page probes and degradation, and the internals are in the [full reference](https://github.com/Khorsheed/dsh-plugins/blob/main/packages/message-timeline/docs/reference.en.md).
|
|
45
|
+
- Entry points: `src/client/index.ts` (the plugin body), `src/client/rail-tracker.ts` (the only place that touches the page structure). Standalone install: `dsh plugin --profile web add @khorsheed/dsh-message-timeline`.
|
|
46
|
+
- Part of [dsh-plugins](https://github.com/Khorsheed/dsh-plugins); open issues and contributions there. Changes: [CHANGELOG.md](CHANGELOG.md).
|
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: 57f086cd2a8d2e8866ad4363ea93f144fbdded20
|
|
6
|
+
packages/message-timeline/README.md: b4d28b2b38e3b4dc75ab545801aca79b6f3e4f4a
|
package/README.md
CHANGED
|
@@ -4,88 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
长对话里你说过的每句话,一眼看到、一点就跳回去。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
会话一长,想找回前面提过的那个要求,只能一路往上滚。装上消息时间轴,对话左边多一条细细的刻度:你说过的每句话占一格,平时压得很淡、不挡视线;鼠标移上去就展开成一行行预览,点哪一行,会话就滚到哪一句。
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline1.png" width="
|
|
9
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline1.png" width="560" alt="对话左缘展开的消息时间轴:每行一条用户消息,点行即跳转到对应消息">
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 能做什么
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- **宽度随布局自适应**——宽布局下面板宽度受滚动区左缘沟槽约束;窄布局(右侧栏打开、窗口窄,如 Room 的成员面板展开时)退为左缘刻度条,悬停或聚焦时展开带底色的浮层,不再直接隐藏。
|
|
13
|
+
- **从第一句起全都在**:整个会话里你说过的每句话都列出来,带一行预览;还没加载的早期消息,点一下它会先把历史翻回去再落位。
|
|
14
|
+
- **一点就跳回去**:点哪一行,会话就滚到那一句。
|
|
15
|
+
- **知道自己读到哪了**:当前位置那一格是蓝色、加长加粗,其余都是灰的;停在一段很长的回复里时,标的是你问的那一句。
|
|
16
|
+
- **没字的消息也有一格**:图片消息、附件消息、自动触发的轮次(重启恢复、定时任务等)都会标出来,也能点。
|
|
17
|
+
- **平时不打扰**:静止时只是一排淡灰刻度,悬停或用键盘聚焦才展开文字。
|
|
18
|
+
- **窄窗口也能用**:右侧栏打开、窗口变窄时,它收成左边一条刻度,悬停时浮出来。
|
|
20
19
|
|
|
21
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline2.png" width="
|
|
20
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/message-timeline2.png" width="560" alt="时间轴的静止态:收成一排压淡的刻度条,不挡视线,悬停才展开">
|
|
22
21
|
|
|
23
22
|
## 安装
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
dsh plugin --profile web add @khorsheed/dsh-message-timeline
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
然后重启 web 实例。卸载:
|
|
24
|
+
在 DSH 的「插件 → 添加插件」里输入下面的包名,点安装:
|
|
30
25
|
|
|
31
|
-
```
|
|
32
|
-
|
|
26
|
+
```
|
|
27
|
+
@khorsheed/dsh-basic
|
|
33
28
|
```
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| 字段 | 默认 | 含义 |
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| `enabled` | `true` | 总开关;false 时面板完全不渲染。 |
|
|
40
|
-
| `includeSteering` | `true` | 回合中插入的用户消息(steering)是否也算行。 |
|
|
41
|
-
| `panelWidth` | `360` | 面板宽度(px,限 120–640);窄列自动收缩,左缘沟槽放不下时整条隐藏。 |
|
|
42
|
-
| `initialPages` | `5` | 窗口分页兜底路径下,面板打开时预取的历史页数(每页 50 条事件;限 1–20)。宿主提供 turnOutline 投影时列表一次到位,此配置不生效。 |
|
|
43
|
-
|
|
44
|
-
## Compatibility
|
|
45
|
-
|
|
46
|
-
- 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,旧宿主请停留在旧发布线。
|
|
47
|
-
- 源码线(deepseek-harness master):✅(verifiedHost: 0.2.1-rc.1)——host-021 波对 0.2.1-rc.1 树全量 build+test 绿,无其它表面变化。
|
|
48
|
-
- 降级项:整会话列表依赖宿主 `turnOutline` 投影(`@deepseek-ai/dsh-session-turn-outline`,官方 web bundle 自 0.1.2-alpha.3 起默认挂载,早于 minHost)。装配未挂载该单元时面板退回窗口分页——逐页加载更早历史,功能完整但首条消息要翻到才出现。
|
|
49
|
-
|
|
50
|
-
**版本线对照**:0.2.0 起支持宿主 `0.1.2-rc.1` 及以后;宿主 `0.1.0-rc.6` ~ `0.1.1-rc.2` 的用户请停留在 0.1.x 发布线(末版 `0.1.0`)。
|
|
30
|
+
消息时间轴会跟着「基础体验」一起装好。装完打开任何一个有几轮对话的会话,左边就能看到它。它只读会话、不发任何消息,agent 完全不知道它的存在。
|
|
51
31
|
|
|
52
32
|
## 已知限制
|
|
53
33
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
- **大纲按轮次归组** —— 投影每轮只留首条用户消息的预览:steering 行在其所在轮次加载进窗口后才出现;被撤回消息的轮次仍按大纲列出(与官方轮次轨道一致),点击后落位到该轮最近可见行。
|
|
58
|
-
- **暂无整页导览** —— 可搜索的消息索引标签页在计划中。
|
|
59
|
-
|
|
60
|
-
## 实现原理
|
|
34
|
+
- 它认的是官方界面的结构;DSH 改版后如果认不出来,时间轴会先自己隐藏,等插件更新。
|
|
35
|
+
- 只对当前打开的那个会话起作用。
|
|
36
|
+
- 回合中途插进去的话,要等那一轮加载出来才会出现在列表里。
|
|
61
37
|
|
|
62
|
-
|
|
63
|
-
<summary>内部结构(点击展开)</summary>
|
|
64
|
-
|
|
65
|
-
插件纯增量、不改任何官方代码。
|
|
66
|
-
|
|
67
|
-
- `src/client/index.ts` —— 插件主体(`apply`/`inject`)
|
|
68
|
-
- `src/client/rail-tracker.ts` —— 插件唯一接触的 DOM:只读探针、阅读位置解析,加跳转时的滚动写入
|
|
69
|
-
- `src/client/TimelineRail.tsx` —— 面板组件
|
|
70
|
-
- `src/client/outline.ts` —— `turnOutline` 投影值的线路收窄(缺席/损坏一律读作"能力不在")
|
|
71
|
-
- `src/client/rows.ts` —— 已加载窗口行与大纲行的合并(单趟交错,保持会话序)
|
|
72
|
-
- `src/client/timeline-kinds.ts` —— 哪些 kind 算行、哪些 kind 携带隐藏范围,的唯一权威
|
|
73
|
-
- `src/client/hidden-spans.ts` —— 折叠 message-tools 撤回/编辑范围,把被覆盖的原消息作为死行丢弃
|
|
74
|
-
- `src/client/preview.ts` —— 消息内容转单行预览文本
|
|
75
|
-
- `src/index.ts` —— 空的宿主 `apply`,只负责把插件锚定进宿主 Loader
|
|
76
|
-
|
|
77
|
-
**挂载与数据** —— 一个条目注册进官方 `conversation.session.header.utilities` 槽位,把插件锚定进会话作用域;面板通过 body portal 以固定几何渲染,数据从官方滚动区实测。行来自两路合并:宿主 `turnOutline` 投影(`useProjection` 标准座席,整会话每轮一条:轮次号、`turn/start` 序号、首条用户消息的 50 字预览)列出每一轮;已加载窗口(`useChat` 的 `order` / `nodes`)为其覆盖的轮次提供更丰富的预览、steering 行和 message-tools 气泡(宿主 order 未必补上的 `message-tools-edited`/`message-tools-restored` 气泡从节点库追加)。被撤回原消息的气泡会被丢弃(其会话行已被 DOM hider 隐藏,点击到不了),追加的气泡按锚定 seq 排序,保证最新消息恒为最后一行。投影缺席时退回纯窗口行。
|
|
78
|
-
|
|
79
|
-
**跳转与降级** —— 点击已加载行按官方 `data-chat-anchor-key` 属性找到会话行并写 `scrollTop`;官方 ChatView 把这种程序化滚动当正常读者移动处理(底部跟随与滚动记忆照常工作)。点击大纲行则经会话的 jump loader(`ISession.loadThrough`,0.1.2-alpha.3 起)把历史回翻到该轮的 `turn/start` 序号,行一物化即落位;翻页停滞或该轮不可达(如首条消息已被撤回)时落位到其后最近的可行。探测的属性是官方渲染产物而非契约 API:官方改结构时面板自行隐藏并 console.warn 一次,不抛错、boot 永不失败。面板宽度受左缘沟槽约束、不越过消息流左缘;流探针无应答(官方结构变化)时宽度退化为滚动区受限比例以 best-effort 兜底——此时面板可能压到消息流,直到探针恢复。
|
|
80
|
-
|
|
81
|
-
**模型体验:无。** 面板只读会话快照并滚动会话,不发送提示词、不追加会话事件、不进会话日志。KV 缓存影响:无。`enabled` 配置可整体关闭插件;从 cordis.yml 移除本插件即移除它添加的所有界面。
|
|
82
|
-
|
|
83
|
-
</details>
|
|
84
|
-
|
|
85
|
-
## 开发
|
|
38
|
+
## Compatibility
|
|
86
39
|
|
|
87
|
-
|
|
40
|
+
需要 DSH 0.1.2-rc.1 或更新版本,已在 0.2.1-rc.1 上验证。
|
|
88
41
|
|
|
89
|
-
##
|
|
42
|
+
## 给开发者
|
|
90
43
|
|
|
91
|
-
|
|
44
|
+
- 行从哪来、怎么跳转、配置项(`enabled`、`includeSteering`、`panelWidth`、`initialPages`)、界面探针与降级、实现原理,都在[完整参考](https://github.com/Khorsheed/dsh-plugins/blob/main/packages/message-timeline/docs/reference.md)里。
|
|
45
|
+
- 入口:`src/client/index.ts`(插件主体)、`src/client/rail-tracker.ts`(唯一接触页面结构的地方)。也可以单独安装:`dsh plugin --profile web add @khorsheed/dsh-message-timeline`。
|
|
46
|
+
- 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins),问题与贡献请到该仓库;变更见 [CHANGELOG.md](CHANGELOG.md)。
|
package/lib/client.js
CHANGED
|
@@ -201,18 +201,12 @@ window.__ModuleLoader__.load({
|
|
|
201
201
|
}
|
|
202
202
|
return lastAbove;
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
* Scroll one user-message row to the top of the transcript scrollport.
|
|
206
|
-
* @param scrollport - the official conversation scrollport element.
|
|
207
|
-
* @param key - the target node's anchor key.
|
|
208
|
-
* @returns whether the row was found and scrolled.
|
|
209
|
-
*/
|
|
210
|
-
function jumpRow(scrollport, key) {
|
|
204
|
+
function jumpTarget(scrollport, key) {
|
|
211
205
|
const row = scrollport.querySelector(`[data-chat-anchor-key=${JSON.stringify(key)}]`);
|
|
212
|
-
if (row === null) return
|
|
206
|
+
if (row === null) return null;
|
|
213
207
|
const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top + scrollport.scrollTop - JUMP_OFFSET;
|
|
214
208
|
scrollport.scrollTop = Math.max(0, target);
|
|
215
|
-
return
|
|
209
|
+
return row;
|
|
216
210
|
}
|
|
217
211
|
/**
|
|
218
212
|
* Install the rail tracker: bind the current session's scrollport, publish
|
|
@@ -228,6 +222,7 @@ window.__ModuleLoader__.load({
|
|
|
228
222
|
let state = IDLE;
|
|
229
223
|
let activeSession;
|
|
230
224
|
let scrollport = null;
|
|
225
|
+
let landing = null;
|
|
231
226
|
let resizeObserver;
|
|
232
227
|
let rafPending = false;
|
|
233
228
|
let warned = false;
|
|
@@ -244,6 +239,7 @@ window.__ModuleLoader__.load({
|
|
|
244
239
|
const attach = (el) => {
|
|
245
240
|
if (scrollport !== null) scrollport.removeEventListener("scroll", onScroll);
|
|
246
241
|
resizeObserver?.disconnect();
|
|
242
|
+
landing = null;
|
|
247
243
|
scrollport = el;
|
|
248
244
|
scrollport.addEventListener("scroll", onScroll, { passive: true });
|
|
249
245
|
if (typeof ResizeObserver === "function") {
|
|
@@ -264,6 +260,7 @@ window.__ModuleLoader__.load({
|
|
|
264
260
|
}
|
|
265
261
|
const geometry = measureGeometry(el);
|
|
266
262
|
if (geometry === null) return;
|
|
263
|
+
if (landing !== null && (landing.top !== el.scrollTop || !el.contains(landing.row))) landing = null;
|
|
267
264
|
publish({
|
|
268
265
|
sessionId: activeSession,
|
|
269
266
|
ready: true,
|
|
@@ -272,7 +269,7 @@ window.__ModuleLoader__.load({
|
|
|
272
269
|
height: geometry.height,
|
|
273
270
|
scrollportWidth: geometry.width,
|
|
274
271
|
flowLeft: flowLeftX(el),
|
|
275
|
-
activeKey: activeRowKey(el, includeSteering),
|
|
272
|
+
activeKey: landing?.key ?? activeRowKey(el, includeSteering),
|
|
276
273
|
chatView: el.querySelector("[data-chat-flow]") !== null
|
|
277
274
|
});
|
|
278
275
|
};
|
|
@@ -291,6 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
291
288
|
bindToken++;
|
|
292
289
|
if (scrollport !== null) scrollport.removeEventListener("scroll", onScroll);
|
|
293
290
|
scrollport = null;
|
|
291
|
+
landing = null;
|
|
294
292
|
resizeObserver?.disconnect();
|
|
295
293
|
resizeObserver = void 0;
|
|
296
294
|
rafPending = false;
|
|
@@ -347,7 +345,22 @@ window.__ModuleLoader__.load({
|
|
|
347
345
|
};
|
|
348
346
|
}
|
|
349
347
|
},
|
|
350
|
-
jumpTo: (key) =>
|
|
348
|
+
jumpTo: (key) => {
|
|
349
|
+
const el = scrollport;
|
|
350
|
+
if (el === null) return false;
|
|
351
|
+
const row = jumpTarget(el, key);
|
|
352
|
+
if (row === null) return false;
|
|
353
|
+
landing = {
|
|
354
|
+
key,
|
|
355
|
+
row,
|
|
356
|
+
top: el.scrollTop
|
|
357
|
+
};
|
|
358
|
+
publish({
|
|
359
|
+
...state,
|
|
360
|
+
activeKey: key
|
|
361
|
+
});
|
|
362
|
+
return true;
|
|
363
|
+
},
|
|
351
364
|
dispose: () => {
|
|
352
365
|
stopList();
|
|
353
366
|
stopProvide?.();
|
|
@@ -561,16 +574,16 @@ window.__ModuleLoader__.load({
|
|
|
561
574
|
document.head.appendChild(tag);
|
|
562
575
|
}
|
|
563
576
|
var TimelineRail_module_css_default = {
|
|
564
|
-
"panelNarrow": "_8u17xW_panelNarrow",
|
|
565
|
-
"itemText": "_8u17xW_itemText",
|
|
566
577
|
"panel": "_8u17xW_panel",
|
|
567
578
|
"itemPending": "_8u17xW_itemPending",
|
|
568
|
-
"
|
|
579
|
+
"railPending": "_8u17xW_railPending",
|
|
569
580
|
"item": "_8u17xW_item",
|
|
570
|
-
"
|
|
571
|
-
"itemFocused": "_8u17xW_itemFocused",
|
|
581
|
+
"panelNarrow": "_8u17xW_panelNarrow",
|
|
572
582
|
"tick": "_8u17xW_tick",
|
|
573
|
-
"
|
|
583
|
+
"itemCurrent": "_8u17xW_itemCurrent",
|
|
584
|
+
"tickCurrent": "_8u17xW_tickCurrent",
|
|
585
|
+
"itemText": "_8u17xW_itemText",
|
|
586
|
+
"itemFocused": "_8u17xW_itemFocused"
|
|
574
587
|
};
|
|
575
588
|
//#endregion
|
|
576
589
|
//#region src/client/TimelineRail.tsx
|
|
@@ -79,8 +79,9 @@ export interface RailTracker {
|
|
|
79
79
|
/** Live rail geometry and active marker for the current session. */
|
|
80
80
|
readonly state: HostObservable<TimelineRailState>;
|
|
81
81
|
/**
|
|
82
|
-
* Scroll
|
|
83
|
-
*
|
|
82
|
+
* Scroll to the message addressed by `key` and keep that landing active
|
|
83
|
+
* until the reading position moves or the row disappears.
|
|
84
|
+
* @returns whether the target row was found (false while unbound or missing).
|
|
84
85
|
*/
|
|
85
86
|
jumpTo(key: string): boolean;
|
|
86
87
|
/** Unbind every listener and observer. */
|
|
@@ -131,6 +131,15 @@ export function activeRowKey(scrollport, includeSteering) {
|
|
|
131
131
|
}
|
|
132
132
|
return lastAbove;
|
|
133
133
|
}
|
|
134
|
+
function jumpTarget(scrollport, key) {
|
|
135
|
+
const row = scrollport.querySelector(`[data-chat-anchor-key=${JSON.stringify(key)}]`);
|
|
136
|
+
if (row === null)
|
|
137
|
+
return null;
|
|
138
|
+
const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top
|
|
139
|
+
+ scrollport.scrollTop - JUMP_OFFSET;
|
|
140
|
+
scrollport.scrollTop = Math.max(0, target);
|
|
141
|
+
return row;
|
|
142
|
+
}
|
|
134
143
|
/**
|
|
135
144
|
* Scroll one user-message row to the top of the transcript scrollport.
|
|
136
145
|
* @param scrollport - the official conversation scrollport element.
|
|
@@ -138,13 +147,7 @@ export function activeRowKey(scrollport, includeSteering) {
|
|
|
138
147
|
* @returns whether the row was found and scrolled.
|
|
139
148
|
*/
|
|
140
149
|
export function jumpRow(scrollport, key) {
|
|
141
|
-
|
|
142
|
-
if (row === null)
|
|
143
|
-
return false;
|
|
144
|
-
const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top
|
|
145
|
-
+ scrollport.scrollTop - JUMP_OFFSET;
|
|
146
|
-
scrollport.scrollTop = Math.max(0, target);
|
|
147
|
-
return true;
|
|
150
|
+
return jumpTarget(scrollport, key) !== null;
|
|
148
151
|
}
|
|
149
152
|
/**
|
|
150
153
|
* Install the rail tracker: bind the current session's scrollport, publish
|
|
@@ -160,6 +163,7 @@ export function installRailTracker(ctx, includeSteering) {
|
|
|
160
163
|
let state = IDLE;
|
|
161
164
|
let activeSession;
|
|
162
165
|
let scrollport = null;
|
|
166
|
+
let landing = null;
|
|
163
167
|
let resizeObserver;
|
|
164
168
|
let rafPending = false;
|
|
165
169
|
let warned = false;
|
|
@@ -189,6 +193,7 @@ export function installRailTracker(ctx, includeSteering) {
|
|
|
189
193
|
if (scrollport !== null)
|
|
190
194
|
scrollport.removeEventListener('scroll', onScroll);
|
|
191
195
|
resizeObserver?.disconnect();
|
|
196
|
+
landing = null;
|
|
192
197
|
scrollport = el;
|
|
193
198
|
scrollport.addEventListener('scroll', onScroll, { passive: true });
|
|
194
199
|
if (typeof ResizeObserver === 'function') {
|
|
@@ -218,6 +223,8 @@ export function installRailTracker(ctx, includeSteering) {
|
|
|
218
223
|
const geometry = measureGeometry(el);
|
|
219
224
|
if (geometry === null)
|
|
220
225
|
return;
|
|
226
|
+
if (landing !== null && (landing.top !== el.scrollTop || !el.contains(landing.row)))
|
|
227
|
+
landing = null;
|
|
221
228
|
publish({
|
|
222
229
|
sessionId: activeSession,
|
|
223
230
|
ready: true,
|
|
@@ -226,7 +233,7 @@ export function installRailTracker(ctx, includeSteering) {
|
|
|
226
233
|
height: geometry.height,
|
|
227
234
|
scrollportWidth: geometry.width,
|
|
228
235
|
flowLeft: flowLeftX(el),
|
|
229
|
-
activeKey: activeRowKey(el, includeSteering),
|
|
236
|
+
activeKey: landing?.key ?? activeRowKey(el, includeSteering),
|
|
230
237
|
chatView: el.querySelector('[data-chat-flow]') !== null,
|
|
231
238
|
});
|
|
232
239
|
};
|
|
@@ -245,6 +252,7 @@ export function installRailTracker(ctx, includeSteering) {
|
|
|
245
252
|
if (scrollport !== null)
|
|
246
253
|
scrollport.removeEventListener('scroll', onScroll);
|
|
247
254
|
scrollport = null;
|
|
255
|
+
landing = null;
|
|
248
256
|
resizeObserver?.disconnect();
|
|
249
257
|
resizeObserver = undefined;
|
|
250
258
|
rafPending = false;
|
|
@@ -314,7 +322,17 @@ export function installRailTracker(ctx, includeSteering) {
|
|
|
314
322
|
return () => { listeners.delete(fn); };
|
|
315
323
|
},
|
|
316
324
|
},
|
|
317
|
-
jumpTo: (key) =>
|
|
325
|
+
jumpTo: (key) => {
|
|
326
|
+
const el = scrollport;
|
|
327
|
+
if (el === null)
|
|
328
|
+
return false;
|
|
329
|
+
const row = jumpTarget(el, key);
|
|
330
|
+
if (row === null)
|
|
331
|
+
return false;
|
|
332
|
+
landing = { key, row, top: el.scrollTop };
|
|
333
|
+
publish({ ...state, activeKey: key });
|
|
334
|
+
return true;
|
|
335
|
+
},
|
|
318
336
|
dispose: () => {
|
|
319
337
|
stopList();
|
|
320
338
|
stopProvide?.();
|
|
@@ -58,8 +58,9 @@ export interface TimelineRailInjected {
|
|
|
58
58
|
*/
|
|
59
59
|
loadThrough: ISession['loadThrough'];
|
|
60
60
|
/**
|
|
61
|
-
* Scroll
|
|
62
|
-
*
|
|
61
|
+
* Scroll to the user message addressed by `key` and keep that landing active
|
|
62
|
+
* until the reading position moves or the row disappears.
|
|
63
|
+
* @returns whether the target row was found (false while unbound or missing).
|
|
63
64
|
*/
|
|
64
65
|
jumpTo: (key: string) => boolean;
|
|
65
66
|
hooks: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khorsheed/dsh-message-timeline",
|
|
3
3
|
"description": "Message timeline: a flat floating list over the conversation scrollport that jumps to any user message, with a tick-only rest state",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|