@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 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 — visible at a glance, one click to jump back.
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 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.
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="640" alt="the expanded message timeline on the chat's left edge: one row per user message, click a row to jump to it">
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
- ## Features
11
+ ## What it does
12
12
 
13
- - **One row per user message, the whole session listed from the first one** — tick plus ellipsized one-line preview; the host `turnOutline` projection names every started turn directly, no paging required. Clicking a not-yet-loaded older message pages history back to that turn before landing (its tick pulses while paging). 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
- - **Textless rows stay listed and jumpable** — automated turns (restart recovery, scheduled jobs) show their trigger card's text once loaded, and fall back to a "Turn N" label while unloaded; image/attachment-only inputs are labelled "Image message" / "Attachment message". After jumping to an automated turn, the selection stays on that turn instead of moving to the next user message.
15
- - **Ambient rest state** — only dimmed grey ticks show until you hover the strip or focus the list.
16
- - **Reading position tracking** — the current position's tick stays blue and lengthened with bold text (defaulting to the latest message while nothing is selected); every other row rests grey. A long answer anchors the lit tick to the user message it replies to.
17
- - **Click to jump** — a loaded row scrolls the transcript straight to it; an outline row goes through the session's jump loader and lands on the freshly loaded message.
18
- - **Long-history friendly** — a short list centers vertically; a long one scrolls, its bottom-most row flush with the chat input box; goal/todo dock cards never push the timeline up. Assemblies without the turn-outline unit fall back to window paging: a few pages prefetch on open, and older history loads when the panel is scrolled to its top.
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="640" alt="the timeline at rest: a strip of dimmed ticks that stays out of the way, expanding on hover">
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
- ```sh
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
- ```sh
32
- dsh plugin --profile web remove @khorsheed/dsh-message-timeline
26
+ ```
27
+ @khorsheed/dsh-basic
33
28
  ```
34
29
 
35
- ## Config
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
- **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.
32
+ ## Known limits
82
33
 
83
- </details>
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
- ## Development
38
+ ## Compatibility
86
39
 
87
- Part of the [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo (`packages/message-timeline`). Issues and contributions welcome there.
40
+ Needs DSH 0.1.2-rc.1 or later; verified on 0.2.1-rc.1.
88
41
 
89
- ## Changelog
42
+ ## For developers
90
43
 
91
- See [CHANGELOG.md](CHANGELOG.md).
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: 812b2b960137a976e0d91267308f0854ed96e20d
6
- packages/message-timeline/README.md: ba8eaffbfabed8162a8ff26256143de743759019
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="640" alt="对话左缘展开的消息时间轴:每行一条用户消息,点行即跳转到对应消息">
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
- - **每行一条用户消息,从第一句起全量列出**——竖刻度加单行省略预览;宿主 `turnOutline` 投影直接给出整段会话的每一轮(含第一条消息),无需翻页。点击尚未加载的更早消息会先把历史回翻到那一轮再落位(回翻期间该行的刻度脉动提示)。steering 消息也计入(可配置关闭),message-tools 的编辑/恢复气泡也保留一行。被撤回的原消息不展示(其会话行被隐藏,点击也到不了)。
14
- - **无文本预览的消息也有行、也能跳**——自动轮次(重启恢复、定时任务等)已加载时直接显示触发卡片正文,未加载时按「第 N 轮」标记;图片/附件消息分别标记为「图片消息」「附件消息」。点击自动轮次落到触发卡片后,选中保持在该轮,不会跳到下一条用户消息。
15
- - **环境化静止态**——只显示压淡的灰色刻度,悬停刻度条或聚焦列表才展开文字。
16
- - **阅读位置跟踪**——当前位置的刻度保持蓝色、加长且文字加粗(未选中时默认最新一条),其余行一律灰色;停在超长回复中时锚定在所回答的用户消息上。
17
- - **点击跳转**——点击已加载的行直接滚动到位;点击未加载的行经会话的 jump loader 回翻历史后落位。
18
- - **长历史友好**——列表短时垂直居中;长时隐形滚动,最底部贴着聊天输入框;目标/任务等 dock 卡片不会把时间轴顶上去。投影缺席的装配(未挂载 turn-outline 单元)退回窗口分页:打开时预取数页,更早历史在面板滚动到顶部时按需加载。
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="640" alt="时间轴的静止态:收成一排压淡的刻度条,不挡视线,悬停才展开">
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
- ```sh
26
- dsh plugin --profile web add @khorsheed/dsh-message-timeline
27
- ```
28
-
29
- 然后重启 web 实例。卸载:
24
+ 在 DSH 的「插件 → 添加插件」里输入下面的包名,点安装:
30
25
 
31
- ```sh
32
- dsh plugin --profile web remove @khorsheed/dsh-message-timeline
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
- - **依赖官方 DOM 探针** —— 面板针对官方行属性与 `[data-conversation-scroll]` 滚动区;官方结构变化时面板自行隐藏(console.warn 一次)直到探针更新。
55
- - **best-effort 宽度兜底** —— 消息流探针无应答时,面板以滚动区受限比例渲染而非隐藏,可能压到消息流直到探针恢复;探针健康时宽度绝不越过消息流左缘。
56
- - **单会话渲染** —— 行与跳转只作用于当前渲染的会话。
57
- - **大纲按轮次归组** —— 投影每轮只留首条用户消息的预览:steering 行在其所在轮次加载进窗口后才出现;被撤回消息的轮次仍按大纲列出(与官方轮次轨道一致),点击后落位到该轮最近可见行。
58
- - **暂无整页导览** —— 可搜索的消息索引标签页在计划中。
59
-
60
- ## 实现原理
34
+ - 它认的是官方界面的结构;DSH 改版后如果认不出来,时间轴会先自己隐藏,等插件更新。
35
+ - 只对当前打开的那个会话起作用。
36
+ - 回合中途插进去的话,要等那一轮加载出来才会出现在列表里。
61
37
 
62
- <details>
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
- 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/message-timeline`)。问题与贡献请移步该仓库。
40
+ 需要 DSH 0.1.2-rc.1 或更新版本,已在 0.2.1-rc.1 上验证。
88
41
 
89
- ## 变更记录
42
+ ## 给开发者
90
43
 
91
- 见 [CHANGELOG.md](CHANGELOG.md)。
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 false;
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 true;
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) => scrollport !== null && jumpRow(scrollport, 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
- "tickCurrent": "_8u17xW_tickCurrent",
579
+ "railPending": "_8u17xW_railPending",
569
580
  "item": "_8u17xW_item",
570
- "itemCurrent": "_8u17xW_itemCurrent",
571
- "itemFocused": "_8u17xW_itemFocused",
581
+ "panelNarrow": "_8u17xW_panelNarrow",
572
582
  "tick": "_8u17xW_tick",
573
- "railPending": "_8u17xW_railPending"
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 the transcript to the message addressed by `key`.
83
- * @returns whether the row was found and scrolled (false while unbound).
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
- const row = scrollport.querySelector(`[data-chat-anchor-key=${JSON.stringify(key)}]`);
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) => scrollport !== null && jumpRow(scrollport, 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 the transcript to the user message row addressed by `key`.
62
- * @returns whether the row was found and scrolled.
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.6",
4
+ "version": "0.2.8",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",