@khorsheed/dsh-message-timeline 0.2.5 → 0.2.7
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 +11 -0
- package/README.en.md +26 -71
- package/README.i18n.yaml +2 -2
- package/README.md +23 -68
- package/lib/client.js +42 -15
- package/lib/types/client/TimelineRail.js +29 -13
- package/lib/types/client/rail-tracker.d.ts +1 -1
- package/lib/types/client/rail-tracker.js +23 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.2.7(2026-10-26)
|
|
4
|
+
|
|
5
|
+
- **README 重写**:改为面向用户的简短说明。无功能变更。
|
|
6
|
+
|
|
7
|
+
## 0.2.6(2026-10-24)
|
|
8
|
+
|
|
9
|
+
- 窄布局下退化为刻度条而不是整个隐藏;测量轨道高度时跳过隐藏的输入卡片;面板高度贴合行数,不再撑满整条轨道。
|
|
10
|
+
|
|
3
11
|
## 0.2.5(2026-10-22)
|
|
4
12
|
|
|
5
13
|
- **整会话一次列出,首条消息即刻可达**:列表改读宿主 `turnOutline` 投影(每轮一条:轮次号、`turn/start` 序号、50 字首条预览),与已加载窗口合并——不再靠逐页翻历史才能看到更早消息。未加载轮次的行点击后经会话 jump loader(`ISession.loadThrough`)回翻历史再落位;行物化即落位,翻页停滞或轮次不可达时落到其后最近可见行。
|
|
6
14
|
- **静止态对比度调整**:所有行统一压淡灰刻度(不再区分已加载/未加载),只有当前阅读位置为蓝色加长刻度加粗行文字(无选中时默认最新一条);回翻中的行以刻度脉动提示。
|
|
7
15
|
- **无文本预览的消息保留展示**(标记「无文本预览」),不再过滤;**修复自动轮次点击无反应**:重启恢复、定时任务等轮次在聊天视图渲染为 turn-trigger 卡片而非 user 行,原落位只找 user 行导致永远落不到。落位改为官方同款 `user ?? loaded[0]`:优先该轮首个 user 行,没有则落该轮第一个可见节点(触发卡片、图片气泡均可达)。
|
|
8
16
|
- **自动轮次成为一等行,选中不再跑偏**:turn-trigger 纳入时间轴行 kind——已加载时直接显示触发正文,未加载时标记「第 N 轮」;阅读位置锚点也认触发卡片,跳转后选中保持在该轮,不再跳到下一条用户消息。图片/附件消息分别标记「图片消息」「附件消息」。
|
|
17
|
+
- **窄布局不再隐藏时间轴**:右侧栏打开或窗口窄导致左沟槽不足最小宽度时,不再整条隐藏,而是退为左缘刻度条(24px,落在消息流留白内),悬停/聚焦时展开带底色的浮层;宽布局仍按沟槽封顶宽度。修复 Room 会话右侧栏展开时时间轴消失的问题。
|
|
18
|
+
- **修复 Room 会话时间轴高度为零**:Room 输入框以 overlay 接管输入区时,官方输入栏仍以 `display:none` 留在 DOM 里,测高误取它的零尺寸卡片,面板被压成几像素(只露一截蓝色刻度、打开时一闪而过)。现在跳过未布局的卡片,退到输入区顶边。
|
|
19
|
+
- **悬停浮层高度随消息条数**:窄布局悬停展开时,底色不再铺满从标签条到输入框的整段高度;面板高度随行数、在原位置垂直居中,条数多时才铺满并滚动。
|
|
9
20
|
- 装配未挂载 turn-outline 单元时退回原窗口分页行为(打开预取 + 面板滚顶翻页),`initialPages` 只作用于该兜底路径。
|
|
10
21
|
- 注入面变化:`jumpTo` 现在返回是否命中(供落位重试),新增 `loadThrough`。
|
|
11
22
|
- **适配宿主 0.2.1 线**:`@deepseek-ai/dsh-*` peer 区间追加 `^0.2.1-rc.1`(`^0.2.0-rc.1` 在 npm/pnpm 严格解析下不覆盖 0.2.1 预发布三元组);宿主 0.2.1 移除 invariants 体系,随包 invariant 伴生退役、兼容判定改由 package.json `engines.dsh` 声明;`dsh.compat.verifiedHost` 前移至 `0.2.1-rc.1`(全量构建+测试在该树通过,无其它表面变化)。
|
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
|
-
- **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.
|
|
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
|
-
- **绝不遮住消息流**——面板宽度受滚动区左缘沟槽约束;沟槽放不下最小宽度时隐藏而不是盖住会话。
|
|
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
|
@@ -95,6 +95,25 @@ window.__ModuleLoader__.load({
|
|
|
95
95
|
chatView: false
|
|
96
96
|
};
|
|
97
97
|
/**
|
|
98
|
+
* The rect of the first laid-out chat input card in the scrollport. A
|
|
99
|
+
* business composer (the Room composer) takes the `conversation.composer`
|
|
100
|
+
* chain as an overlay: the official input bar stays mounted under a
|
|
101
|
+
* `display: none` fallback wrapper, so its card is still the first marker in
|
|
102
|
+
* the DOM but measures as a zero rect at y=0 — ending the box there collapses
|
|
103
|
+
* the panel to zero height. Unlaid-out cards are skipped, so such a layout
|
|
104
|
+
* falls through to the seat top.
|
|
105
|
+
* @param scrollport - the official conversation scrollport element.
|
|
106
|
+
* @returns the card's rect, or null when no laid-out card is rendered.
|
|
107
|
+
*/
|
|
108
|
+
function laidOutCard(scrollport) {
|
|
109
|
+
for (const card of scrollport.querySelectorAll("[data-composer-card]")) {
|
|
110
|
+
const rect = card.getBoundingClientRect();
|
|
111
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
112
|
+
return rect;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
98
117
|
* Measure the panel's viewport box from one scrollport: its rect inset by the
|
|
99
118
|
* panel padding, minus the chat input card at the bottom and the conversation
|
|
100
119
|
* tab strip at the top. The tabs render just above the scrollport, but
|
|
@@ -104,7 +123,7 @@ window.__ModuleLoader__.load({
|
|
|
104
123
|
* whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above
|
|
105
124
|
* the input inside the seat, and counting them would push the timeline up off
|
|
106
125
|
* the conversation. Falls back to the seat top, then the column bottom, when
|
|
107
|
-
*
|
|
126
|
+
* no laid-out card is rendered (see {@link laidOutCard}).
|
|
108
127
|
* The scrollport's own width rides along for the width-cap fallback when the
|
|
109
128
|
* message-flow probe is unanswered.
|
|
110
129
|
* @param scrollport - the official conversation scrollport element.
|
|
@@ -122,9 +141,9 @@ window.__ModuleLoader__.load({
|
|
|
122
141
|
break;
|
|
123
142
|
}
|
|
124
143
|
}
|
|
125
|
-
const card = scrollport
|
|
144
|
+
const card = laidOutCard(scrollport);
|
|
126
145
|
const seat = scrollport.querySelector("[data-composer-seat]");
|
|
127
|
-
const bottom = card !== null ? card.
|
|
146
|
+
const bottom = card !== null ? card.top - RAIL_VERTICAL_PADDING : seat !== null ? seat.getBoundingClientRect().top - RAIL_VERTICAL_PADDING : rect.bottom - RAIL_VERTICAL_PADDING;
|
|
128
147
|
return {
|
|
129
148
|
left: rect.left + RAIL_LEFT_INSET,
|
|
130
149
|
top: rect.top + topInset,
|
|
@@ -532,7 +551,7 @@ window.__ModuleLoader__.load({
|
|
|
532
551
|
}
|
|
533
552
|
//#endregion
|
|
534
553
|
//#region \0dsh-css:src/client/TimelineRail.module.css.mjs
|
|
535
|
-
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
|
|
554
|
+
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::-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-alias-label-tertiary);border-radius:2px;flex:none;width:3px;height:14px}._8u17xW_itemCurrent{color:var(--dsw-static-deepseek-500);opacity:1;font-weight:600}._8u17xW_tickCurrent{background:var(--dsw-static-deepseek-500);height:18px}._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}._8u17xW_itemPending ._8u17xW_tick{background:var(--dsw-static-deepseek-400);animation:1s ease-in-out infinite _8u17xW_railPending}@keyframes _8u17xW_railPending{50%{opacity:.2}}@media (prefers-reduced-motion:reduce){._8u17xW_itemPending ._8u17xW_tick{animation:none}}._8u17xW_panelNarrow{z-index:101}._8u17xW_panelNarrow:hover,._8u17xW_panelNarrow:focus-within{background:var(--dsw-alias-bg-layer-1);border-radius:8px;box-shadow:0 4px 16px #0000002e;width:var(--panel-wide)!important}";
|
|
536
555
|
const tagId = "@khorsheed/dsh-message-timeline/TimelineRail.module.css";
|
|
537
556
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
538
557
|
const tag = document.createElement("style");
|
|
@@ -542,15 +561,16 @@ window.__ModuleLoader__.load({
|
|
|
542
561
|
document.head.appendChild(tag);
|
|
543
562
|
}
|
|
544
563
|
var TimelineRail_module_css_default = {
|
|
564
|
+
"itemFocused": "_8u17xW_itemFocused",
|
|
545
565
|
"itemText": "_8u17xW_itemText",
|
|
546
566
|
"railPending": "_8u17xW_railPending",
|
|
567
|
+
"tick": "_8u17xW_tick",
|
|
547
568
|
"panel": "_8u17xW_panel",
|
|
548
|
-
"tickCurrent": "_8u17xW_tickCurrent",
|
|
549
|
-
"item": "_8u17xW_item",
|
|
550
569
|
"itemCurrent": "_8u17xW_itemCurrent",
|
|
551
570
|
"itemPending": "_8u17xW_itemPending",
|
|
552
|
-
"
|
|
553
|
-
"
|
|
571
|
+
"panelNarrow": "_8u17xW_panelNarrow",
|
|
572
|
+
"tickCurrent": "_8u17xW_tickCurrent",
|
|
573
|
+
"item": "_8u17xW_item"
|
|
554
574
|
};
|
|
555
575
|
//#endregion
|
|
556
576
|
//#region src/client/TimelineRail.tsx
|
|
@@ -613,6 +633,10 @@ window.__ModuleLoader__.load({
|
|
|
613
633
|
const PANEL_GAP = 16;
|
|
614
634
|
/** Degraded width cap (fraction of the scrollport) while the flow probe is unanswered. */
|
|
615
635
|
const DEGRADED_WIDTH_RATIO = .4;
|
|
636
|
+
/** Rest width of the narrow-layout tick strip (px) — fits the flow's left padding. */
|
|
637
|
+
const PANEL_NARROW_REST_WIDTH = 24;
|
|
638
|
+
/** Expanded width of the narrow-layout hover overlay (fraction of the scrollport). */
|
|
639
|
+
const NARROW_WIDE_RATIO = .6;
|
|
616
640
|
/**
|
|
617
641
|
* Frames an outline jump retries the DOM scroll after its row materializes in
|
|
618
642
|
* the chat store: the transcript renders the new page in the same commit or a
|
|
@@ -746,8 +770,9 @@ window.__ModuleLoader__.load({
|
|
|
746
770
|
const active = rail.chatView && rail.ready && rail.sessionId === sessionId;
|
|
747
771
|
const visible = active && items.length > 0;
|
|
748
772
|
const gutter = rail.flowLeft === null ? null : rail.flowLeft - rail.left - PANEL_PADDING_X - PANEL_GAP;
|
|
749
|
-
const
|
|
750
|
-
const
|
|
773
|
+
const narrow = gutter !== null && gutter < 120;
|
|
774
|
+
const width = gutter === null ? Math.min(panelWidth, Math.max(120, rail.scrollportWidth * DEGRADED_WIDTH_RATIO)) : narrow ? PANEL_NARROW_REST_WIDTH : Math.min(panelWidth, gutter);
|
|
775
|
+
const wideWidth = Math.min(panelWidth, Math.max(120, rail.scrollportWidth * NARROW_WIDE_RATIO));
|
|
751
776
|
(0, react.useEffect)(() => {
|
|
752
777
|
panelRef.current?.querySelector(`[data-item-key=${JSON.stringify(current)}]`)?.scrollIntoView({ block: "nearest" });
|
|
753
778
|
}, [current]);
|
|
@@ -792,19 +817,21 @@ window.__ModuleLoader__.load({
|
|
|
792
817
|
fromHead: null
|
|
793
818
|
});
|
|
794
819
|
};
|
|
795
|
-
if (!visible
|
|
820
|
+
if (!visible) return null;
|
|
796
821
|
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
797
822
|
ref: panelRef,
|
|
798
|
-
className: TimelineRail_module_css_default.panel
|
|
823
|
+
className: `${TimelineRail_module_css_default.panel} ${narrow ? TimelineRail_module_css_default.panelNarrow : ""}`,
|
|
799
824
|
role: "navigation",
|
|
800
825
|
"aria-label": t("rail.panel"),
|
|
801
826
|
tabIndex: 0,
|
|
802
827
|
"data-timeline-panel": "",
|
|
803
828
|
style: {
|
|
804
829
|
left: rail.left,
|
|
805
|
-
top: rail.top,
|
|
806
|
-
|
|
807
|
-
|
|
830
|
+
top: rail.top + rail.height / 2,
|
|
831
|
+
maxHeight: rail.height,
|
|
832
|
+
transform: "translateY(-50%)",
|
|
833
|
+
width,
|
|
834
|
+
...narrow ? { "--panel-wide": `${wideWidth}px` } : {}
|
|
808
835
|
},
|
|
809
836
|
onScroll: onPanelScroll,
|
|
810
837
|
onKeyDown: (event) => {
|
|
@@ -74,6 +74,10 @@ const PANEL_PADDING_X = 8;
|
|
|
74
74
|
const PANEL_GAP = 16;
|
|
75
75
|
/** Degraded width cap (fraction of the scrollport) while the flow probe is unanswered. */
|
|
76
76
|
const DEGRADED_WIDTH_RATIO = 0.4;
|
|
77
|
+
/** Rest width of the narrow-layout tick strip (px) — fits the flow's left padding. */
|
|
78
|
+
const PANEL_NARROW_REST_WIDTH = 24;
|
|
79
|
+
/** Expanded width of the narrow-layout hover overlay (fraction of the scrollport). */
|
|
80
|
+
const NARROW_WIDE_RATIO = 0.6;
|
|
77
81
|
/**
|
|
78
82
|
* Frames an outline jump retries the DOM scroll after its row materializes in
|
|
79
83
|
* the chat store: the transcript renders the new page in the same commit or a
|
|
@@ -260,20 +264,21 @@ export function TimelineRail({ sessionId, includeSteering, panelWidth, initialPa
|
|
|
260
264
|
// pages until one materializes — hence `active` splits from `visible`.
|
|
261
265
|
const active = rail.chatView && rail.ready && rail.sessionId === sessionId;
|
|
262
266
|
const visible = active && items.length > 0;
|
|
263
|
-
// The panel must never cover the message flow: its width is the
|
|
264
|
-
// preferred width capped by the scrollport's left gutter — the
|
|
265
|
-
// flow's left edge minus the panel's left edge, less the panel's
|
|
266
|
-
// padding and the visible breathing gap. A gutter too small for the
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
267
|
+
// The panel must never cover the message flow AT REST: its width is the
|
|
268
|
+
// configured preferred width capped by the scrollport's left gutter — the
|
|
269
|
+
// message flow's left edge minus the panel's left edge, less the panel's
|
|
270
|
+
// right padding and the visible breathing gap. A gutter too small for the
|
|
271
|
+
// minimum width no longer hides the panel: it degrades to a tick strip in
|
|
272
|
+
// the flow's left padding and expands over the flow on hover/focus with a
|
|
273
|
+
// solid background (right sidebars and narrow windows keep the rail
|
|
274
|
+
// usable). When the flow probe is unanswered (official structure change),
|
|
275
|
+
// the width degrades to a fraction of the scrollport instead.
|
|
272
276
|
const gutter = rail.flowLeft === null ? null : rail.flowLeft - rail.left - PANEL_PADDING_X - PANEL_GAP;
|
|
277
|
+
const narrow = gutter !== null && gutter < PANEL_WIDTH_MIN;
|
|
273
278
|
const width = gutter === null
|
|
274
279
|
? Math.min(panelWidth, Math.max(PANEL_WIDTH_MIN, rail.scrollportWidth * DEGRADED_WIDTH_RATIO))
|
|
275
|
-
: Math.min(panelWidth,
|
|
276
|
-
const
|
|
280
|
+
: narrow ? PANEL_NARROW_REST_WIDTH : Math.min(panelWidth, gutter);
|
|
281
|
+
const wideWidth = Math.min(panelWidth, Math.max(PANEL_WIDTH_MIN, rail.scrollportWidth * NARROW_WIDE_RATIO));
|
|
277
282
|
// Keep the lit row in view: the panel follows the reading position (a new
|
|
278
283
|
// message scrolls its row in), and mouse browsing is never yanked because
|
|
279
284
|
// the hovered row is the current one and always visible under the pointer.
|
|
@@ -326,9 +331,20 @@ export function TimelineRail({ sessionId, includeSteering, panelWidth, initialPa
|
|
|
326
331
|
// landing scroll. Re-clicking another outline row retargets the jump.
|
|
327
332
|
setPendingJump({ sessionId, turn: row.turn, seq: row.seq, requested: false, awaiting: false, fromHead: null });
|
|
328
333
|
};
|
|
329
|
-
if (!visible
|
|
334
|
+
if (!visible)
|
|
330
335
|
return null;
|
|
331
|
-
return createPortal(_jsx("div", { ref: panelRef, className: css.panel
|
|
336
|
+
return createPortal(_jsx("div", { ref: panelRef, className: `${css.panel} ${narrow ? css.panelNarrow : ''}`, role: "navigation", "aria-label": t('rail.panel'), tabIndex: 0, "data-timeline-panel": "", style: {
|
|
337
|
+
// The box fits its rows (up to the rail height) and centers on the
|
|
338
|
+
// rail's midline, so the narrow-mode hover background wraps the list
|
|
339
|
+
// instead of painting the whole column; a longer list fills the rail
|
|
340
|
+
// and scrolls.
|
|
341
|
+
left: rail.left,
|
|
342
|
+
top: rail.top + rail.height / 2,
|
|
343
|
+
maxHeight: rail.height,
|
|
344
|
+
transform: 'translateY(-50%)',
|
|
345
|
+
width,
|
|
346
|
+
...(narrow ? { '--panel-wide': `${wideWidth}px` } : {}),
|
|
347
|
+
}, onScroll: onPanelScroll, onKeyDown: (event) => {
|
|
332
348
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
333
349
|
event.preventDefault();
|
|
334
350
|
const anchor = items.findIndex(item => item.id === current);
|
|
@@ -25,7 +25,7 @@ import type { TimelineRailState } from './slots.ts';
|
|
|
25
25
|
* whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above
|
|
26
26
|
* the input inside the seat, and counting them would push the timeline up off
|
|
27
27
|
* the conversation. Falls back to the seat top, then the column bottom, when
|
|
28
|
-
*
|
|
28
|
+
* no laid-out card is rendered (see {@link laidOutCard}).
|
|
29
29
|
* The scrollport's own width rides along for the width-cap fallback when the
|
|
30
30
|
* message-flow probe is unanswered.
|
|
31
31
|
* @param scrollport - the official conversation scrollport element.
|
|
@@ -14,6 +14,26 @@ const IDLE = {
|
|
|
14
14
|
sessionId: undefined, ready: false, left: 0, top: 0, height: 0, scrollportWidth: 0, flowLeft: null,
|
|
15
15
|
activeKey: null, chatView: false,
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* The rect of the first laid-out chat input card in the scrollport. A
|
|
19
|
+
* business composer (the Room composer) takes the `conversation.composer`
|
|
20
|
+
* chain as an overlay: the official input bar stays mounted under a
|
|
21
|
+
* `display: none` fallback wrapper, so its card is still the first marker in
|
|
22
|
+
* the DOM but measures as a zero rect at y=0 — ending the box there collapses
|
|
23
|
+
* the panel to zero height. Unlaid-out cards are skipped, so such a layout
|
|
24
|
+
* falls through to the seat top.
|
|
25
|
+
* @param scrollport - the official conversation scrollport element.
|
|
26
|
+
* @returns the card's rect, or null when no laid-out card is rendered.
|
|
27
|
+
*/
|
|
28
|
+
function laidOutCard(scrollport) {
|
|
29
|
+
for (const card of scrollport.querySelectorAll('[data-composer-card]')) {
|
|
30
|
+
const rect = card.getBoundingClientRect();
|
|
31
|
+
if (rect.width === 0 && rect.height === 0)
|
|
32
|
+
continue;
|
|
33
|
+
return rect;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
17
37
|
/**
|
|
18
38
|
* Measure the panel's viewport box from one scrollport: its rect inset by the
|
|
19
39
|
* panel padding, minus the chat input card at the bottom and the conversation
|
|
@@ -24,7 +44,7 @@ const IDLE = {
|
|
|
24
44
|
* whole `[data-composer-seat]` top: dock cards (goal/todo/queue) sit above
|
|
25
45
|
* the input inside the seat, and counting them would push the timeline up off
|
|
26
46
|
* the conversation. Falls back to the seat top, then the column bottom, when
|
|
27
|
-
*
|
|
47
|
+
* no laid-out card is rendered (see {@link laidOutCard}).
|
|
28
48
|
* The scrollport's own width rides along for the width-cap fallback when the
|
|
29
49
|
* message-flow probe is unanswered.
|
|
30
50
|
* @param scrollport - the official conversation scrollport element.
|
|
@@ -44,10 +64,10 @@ export function measureGeometry(scrollport) {
|
|
|
44
64
|
break;
|
|
45
65
|
}
|
|
46
66
|
}
|
|
47
|
-
const card = scrollport
|
|
67
|
+
const card = laidOutCard(scrollport);
|
|
48
68
|
const seat = scrollport.querySelector('[data-composer-seat]');
|
|
49
69
|
const bottom = card !== null
|
|
50
|
-
? card.
|
|
70
|
+
? card.top - RAIL_VERTICAL_PADDING
|
|
51
71
|
: seat !== null
|
|
52
72
|
? seat.getBoundingClientRect().top - RAIL_VERTICAL_PADDING
|
|
53
73
|
: rect.bottom - RAIL_VERTICAL_PADDING;
|
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.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|