dsh-quick-toc 0.5.0 → 0.5.1
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.en.md +18 -0
- package/CHANGELOG.md +18 -0
- package/README.en.md +19 -3
- package/README.md +19 -3
- package/lib/client.js +351 -75
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-quick-toc** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-09-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **"Back to the newest row" button**: a round button in the outline list's lower-right corner. It fades in whenever the list is not at its bottom and fades out once it is; clicking scrolls the outline back to the newest entry (smooth for a short distance, instant when the list is far up).
|
|
9
|
+
- **Turn stamps carry the day**: the time shown in group headers, search result rows and hover cards is no longer a bare `HH:MM` — yesterday reads `昨天 15:04`, the day before `前天 15:04`, anything older `25-09-11 15:04` (two-digit year; decided by calendar day, not a 24-hour difference).
|
|
10
|
+
- **Failures are reported**: a turn that produced no reply at all (request timeout, upstream error) now shows a `请求失败` row under its group header with the host's own error text; clicking it jumps to that error in the conversation, and the error text is searchable in full-text scope (its result row is tagged 请求失败).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **The header now holds two buttons per side**: the heading-level filter and the dock toggle on the left, search and collapse on the right, with a spacer pinning the pairs to the two ends; the three-bar identity mark on the left is gone.
|
|
14
|
+
- **The level filter popup is left-aligned with its button** (it used to be right-aligned).
|
|
15
|
+
- **The mount diagnostic is silent by default**: the console no longer prints `[dsh-quick-toc] panel mounted …`. To diagnose, run `localStorage.setItem("dsh-quick-toc.debug", "1")` and reload — the line then reports `turnOutline` and the jump loader's stage-by-stage state, while normal use stays quiet.
|
|
16
|
+
- **Long jumps land instantly**: a jump across a long history no longer "stops halfway" (short jumps still glide smoothly); after landing it re-checks for ~0.6s, which corrects a target that moved while content was mounting.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Failed or aborted turns were mislabelled 未加载**: those turns have no model reply, so they carried neither a time nor headings and were re-listed as unloaded turns. A loaded user message now counts as "this turn is loaded", failures additionally show their error row, and a user-aborted turn simply shows as a heading-less turn.
|
|
20
|
+
- **Scrolling the outline up to load older messages bounced the transcript back to the newest turn**, so paging up never got anywhere: the host force-scrolls to the bottom whenever a scroll is not attributed to the reader while it still believes the reader sits at the bottom, and a page-in triggered from the outline matches exactly that condition. The plugin now lifts the transcript just off the bottom stick zone before clicking the host's "load earlier", so the page-in keeps its place.
|
|
21
|
+
- **A heading-less turn's group header was formatted unlike every other header**: it used to be enlarged (12px / 600 / primary colour), which made it read as a different kind of entry. All group headers now share one style (11px, secondary colour, one row box); a heading-less turn is simply a group with no rows under its header.
|
|
22
|
+
|
|
5
23
|
## [0.5.0] - 2026-09-12
|
|
6
24
|
|
|
7
25
|
### Added
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-quick-toc** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-09-13
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **「回到底部」浮动按钮**:大纲列表右下角的圆形按钮。列表不在底部时渐显,滚到底后渐隐;点击把大纲滚回最新一条(近距离平滑滚动,长距离即时到位)。
|
|
9
|
+
- **回合时间带日期**:组头、搜索结果行与悬停卡片里的时间不再只有 `HH:MM`——昨天显示 `昨天 15:04`,前天显示 `前天 15:04`,更早显示 `25-09-11 15:04`(年份取后两位;按自然日判定,不是按 24 小时差)。
|
|
10
|
+
- **失败回合的报错**:请求超时、上游报错这类没有产出任何回复的回合,现在在组头下方显示一行 `请求失败` 与宿主提供的报错原文;点击跳到对话中该报错的位置,报错原文同样能被全文搜索命中(结果行带「请求失败」标记)。
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
- **标题栏改为一边两个按钮**:左侧「层级筛选 + 停靠切换」,右侧「搜索 + 收起」,中间留白把两组顶到两端;并去掉左侧的三条杠装饰标识。
|
|
14
|
+
- **层级筛选弹层改为左对齐按钮**(此前在按钮右侧对齐)。
|
|
15
|
+
- **挂载诊断日志默认静默**:控制台不再打印 `[dsh-quick-toc] panel mounted …`。需要排查时先执行 `localStorage.setItem("dsh-quick-toc.debug", "1")` 再刷新即可看到该行(`turnOutline` 与跳转加载器的分阶段状态),正常使用不再有噪音。
|
|
16
|
+
- **长距离跳转改为即时定位**:跨越很长历史的跳转不再"滚到一半停下"(近距离仍是平滑滚动),落地后还有约 0.6 秒的校验,用于纠正内容高度在飞行途中变化的情况。
|
|
17
|
+
|
|
18
|
+
### 修复
|
|
19
|
+
- **失败或中止的回合被误标成「未加载」**:这类回合没有模型回复,既没有时间也没有标题,被当成尚未加载的回合重新列了一遍。现在"该回合的用户消息已加载"本身就是已加载的依据,失败回合另加报错行;被用户中止的回合按普通无标题回合显示。
|
|
20
|
+
- **从大纲往上翻加载更早消息时,对话区会被弹回最新一条**,越翻越回到底部:宿主在"这次滚动不是读者发起、且它仍认为读者停在底部"时会强制滚回底部,而从大纲触发的翻页恰好命中该条件。现在点击宿主的「加载更早」之前会先把对话区抬离贴底区,翻页保持原位。
|
|
21
|
+
- **无标题回合的组头与其它组头格式不一致**:它此前被整体放大(12px / 600 / 正文色),与常规组头并列时像另一种条目。现在所有组头共用同一套样式(11px、次要色、同一行盒),无标题回合只是"组头下面没有行"。
|
|
22
|
+
|
|
5
23
|
## [0.5.0] - 2026-09-12
|
|
6
24
|
|
|
7
25
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -8,6 +8,8 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
8
8
|
|
|
9
9
|
- **Turn-grouped outline** — each user message plus its following AI replies form one group; the group header shows the turn's time and a first-line preview, and clicking it jumps to the start of that turn's model reply
|
|
10
10
|
- **Whole-session coverage** — turns the conversation window has not loaded are listed too (tagged 未加载, with previews); clicking one loads that turn and jumps to it
|
|
11
|
+
- **Failures are reported** — a turn with no reply at all (request timeout, upstream error) shows `请求失败` plus the host's error text, and clicking it jumps to that error in the conversation
|
|
12
|
+
- **Turn stamps carry the day** — yesterday reads `昨天 15:04`, the day before `前天 15:04`, anything older `25-09-11 15:04`, so times never blur together in a session spanning days
|
|
11
13
|
- **Row subtitles** — under each heading, the first sentence of that section, so identically-titled headings can be told apart
|
|
12
14
|
- **Hover previews** — hovering a heading row shows the section's opening, the turn time and the heading path
|
|
13
15
|
- **Search** — title / full-text scopes; click any result row to locate and highlight that hit, `n/N` Enter stepping, Esc to close
|
|
@@ -17,6 +19,7 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
17
19
|
- **Heading level filter** — the round levels button in the header pops down H1–H6 switches for any combination
|
|
18
20
|
- **Auto-follow** — the turn being read lights up in a closed blue box while you scroll the conversation; the outline follows on its own
|
|
19
21
|
- **Jumping** — click a heading to jump to its position in the conversation
|
|
22
|
+
- **Back to the newest row** — after paging far up, a floating button in the list's lower-right corner returns to the newest entry in one click (it appears when the list is not at its bottom and disappears once it is)
|
|
20
23
|
- **Dockable and resizable** — drag the top bar to move the panel, ◀ / ▶ to dock left or right, drag an edge to resize, and collapse it into an edge handle; position and size are remembered
|
|
21
24
|
- **Paging** — the most recent groups show first; scrolling up both expands the index and loads older conversation
|
|
22
25
|
- **Edge hints** — a brief hint at the bottom of the panel when you keep scrolling past the first or the last entry
|
|
@@ -28,13 +31,14 @@ A conversation TOC plugin for [DeepSeek Harness](https://github.com/deepseek-ai/
|
|
|
28
31
|
|
|
29
32
|
| Plugin | Supported DSH |
|
|
30
33
|
| --- | --- |
|
|
31
|
-
| **0.5.
|
|
34
|
+
| **0.5.1** (latest) | 0.1.5-rc.1, 0.1.5-rc.2 |
|
|
35
|
+
| 0.5.0 | 0.1.5-rc.1, 0.1.5-rc.2 |
|
|
32
36
|
| 0.4.1 | 0.1.5-rc.1, 0.1.5-rc.2 |
|
|
33
37
|
| 0.4.0 | 0.1.5-rc.1, 0.1.5-rc.2 |
|
|
34
38
|
| 0.3.3 | 0.1.5-rc.1 |
|
|
35
39
|
| 0.3.0 – 0.3.2 | ≥ 0.1.2-rc.1 |
|
|
36
40
|
|
|
37
|
-
`engines.dsh` has a floor of **0.1.5-rc.1** (`>=`): from that release on, the plugin uses the host's session-scoped slot injection. 0.5.
|
|
41
|
+
`engines.dsh` has a floor of **0.1.5-rc.1** (`>=`): from that release on, the plugin uses the host's session-scoped slot injection. 0.5.1 was tested on **0.1.5-rc.2**, and the host interfaces it relies on — the `turnOutline` projection, the session-scoped `useProjection`, the client `sessions` service and its `loadThrough` jump loader (since 0.5.0), and the `turn-error` node used for failed turns (since 0.5.1) — were checked one by one against the installed **0.1.5-rc.1** bundles, so both are declared. Older or newer DSH versions are unverified and therefore not claimed; on an older DSH the newest usable plugin version is **0.3.2**. On install or update, the DSH market pre-flights host compatibility from `engines.dsh`, `dsh.compatibility.dshReleases` and `peerDependencies` in `package.json`.
|
|
38
42
|
|
|
39
43
|
## Install
|
|
40
44
|
|
|
@@ -60,15 +64,26 @@ After installing, restart DSH and open the Web UI. The panel starts collapsed; c
|
|
|
60
64
|
|
|
61
65
|
## Usage
|
|
62
66
|
|
|
63
|
-
- **Jumping**: click an outline heading or a group header to jump to it; entries tagged 未加载 load that turn first
|
|
67
|
+
- **Jumping**: click an outline heading or a group header to jump to it; entries tagged 未加载 load that turn first, and a failed turn's error row jumps to that error in the conversation
|
|
64
68
|
- **Search**: open the box with the magnifier, click a result to locate and highlight it, Enter to step through matches, Esc to close
|
|
65
69
|
- **Search tolerance**: full-width/half-width, case and whitespace differences match automatically; for looser matching turn on the "fuzzy" switch next to the search box
|
|
66
70
|
- **Level filter**: click the round levels button in the header to pop down the H1–H6 switches
|
|
67
71
|
- **Moving and docking**: drag the top bar to move, ◀ / ▶ to switch sides
|
|
68
72
|
- **Resizing**: drag the right edge, bottom edge or bottom-right corner
|
|
69
73
|
- **Loading older turns**: scroll up inside the outline (it both expands the index and loads older conversation)
|
|
74
|
+
- **Back to the newest row**: after paging far up, click the floating button in the list's lower-right corner
|
|
70
75
|
- **Reading position**: scroll the conversation and the turn you are reading is boxed in blue; the outline follows
|
|
71
76
|
|
|
77
|
+
## Diagnostics
|
|
78
|
+
|
|
79
|
+
Mounting the panel prints **nothing** by default. To check the host facilities, run this once in the browser console:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
localStorage.setItem("dsh-quick-toc.debug", "1")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
then reload: the console shows one line, `[dsh-quick-toc] panel mounted · turnOutline=… · jumpLoader=…`. A missing `turnOutline` means the host has no such projection, and the staged `jumpLoader` wording (`no-sessions-service` / `no-binding-api` / `no-binding-for-session` / `no-loadThrough` / `binding-threw`) pinpoints which link of the jump bridge is broken. Turn it off again with `localStorage.removeItem("dsh-quick-toc.debug")`.
|
|
86
|
+
|
|
72
87
|
## Development
|
|
73
88
|
|
|
74
89
|
- `lib/client.js` — all UI logic (browser side)
|
|
@@ -76,6 +91,7 @@ After installing, restart DSH and open the Web UI. The panel starts collapsed; c
|
|
|
76
91
|
- `cordis.patch.yml` — loader patch (official DSH bundle format)
|
|
77
92
|
- The panel registers into the session-scoped `conversation.input.overlay` slot so it receives session-scoped hooks (`useChat`, `useSession`, `sessionId`, …), and renders itself through `createPortal` into `document.body` as a fixed floating dock; conversation data comes from `props.useChat` (`ChatSnapshot.order` and `nodes`; node shape: `kind: user/assistant-step`, `location.turn`, `data.blocks`)
|
|
78
93
|
- The "unloaded turn" capability needs two host facilities: the `turnOutline` projection (the whole-session turn index, each entry carrying its `turn/start` seq) and the turn-jump loader (the client `sessions` service's `binding(sessionId).session.loadThrough(seq)`, reached through the client ctx's declaration-free `ctx.get("sessions")` lookup). Each degrades on its own: without the projection the outline lists loaded turns only, without the loader unloaded entries are shown but not jumped to, and nothing else in the panel is affected.
|
|
94
|
+
- The failure row reads the host's `turn-error` conversation node (published when `turn/end` carries an `error` reason, with `message` and an optional `code`); on a host without that node the row is simply absent and nothing else changes.
|
|
79
95
|
- Changes to `lib/client.js` show up after a page refresh (client modules are served under a content hash and DSH's client HMR pushes reloads); restart DSH only if that does not take
|
|
80
96
|
|
|
81
97
|
## License
|
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
- **按回合分组** —— 每条用户消息 + 其后续 AI 回复为一组,组头显示回合时间与首行预览,点击跳到该回合模型回答的开头
|
|
10
10
|
- **覆盖整段会话** —— 未加载进对话窗口的回合也在列表里(带「未加载」标记与预览);点击即加载该回合并跳过去
|
|
11
|
+
- **失败回合的报错** —— 请求超时 / 上游报错这类没有回复的回合显示 `请求失败` 与报错原文,点击跳到对话中的报错位置
|
|
12
|
+
- **回合时间带日期** —— 昨天 `昨天 15:04`、前天 `前天 15:04`、更早 `25-09-11 15:04`,跨天的会话里时间不再重影
|
|
11
13
|
- **标题行副标题** —— 每行标题下方显示该节正文的第一句,同名标题一眼可辨
|
|
12
14
|
- **悬停预览** —— 悬停标题行显示该节开头、回合时间与层级路径
|
|
13
15
|
- **搜索** —— 标题 / 全文两种范围,结果列表点击定位并高亮,`n/N` 回车逐处跳,Esc 关闭
|
|
@@ -17,6 +19,7 @@
|
|
|
17
19
|
- **层级筛选** —— 标题栏的圆形层级按钮弹出 H1–H6 开关,任意组合显示
|
|
18
20
|
- **自动跟随** —— 滚动对话时正在阅读的回合自动点亮(封闭蓝框),大纲自动跟随
|
|
19
21
|
- **跳转** —— 点击标题跳到对话中该标题的位置;面板与对话双向定位
|
|
22
|
+
- **回到底部** —— 往上翻远之后,列表右下角的浮动按钮一步回到最新一条(不在底部时出现、到底后消失)
|
|
20
23
|
- **可停靠、可缩放** —— 拖顶部横条移动,◀ / ▶ 切换左右停靠,拖边缘调宽高,收起后成为边缘把手;位置与尺寸会被记住
|
|
21
24
|
- **分页** —— 默认显示最近的若干组,向上滚动既展开索引也加载更早的对话
|
|
22
25
|
- **边界提示** —— 滚到最早或最新再继续滚动时,面板底部短暂提示
|
|
@@ -28,13 +31,14 @@
|
|
|
28
31
|
|
|
29
32
|
| 插件版本 | 支持的 DSH 版本 |
|
|
30
33
|
| --- | --- |
|
|
31
|
-
| **0.5.
|
|
34
|
+
| **0.5.1**(最新) | 0.1.5-rc.1、0.1.5-rc.2 |
|
|
35
|
+
| 0.5.0 | 0.1.5-rc.1、0.1.5-rc.2 |
|
|
32
36
|
| 0.4.1 | 0.1.5-rc.1、0.1.5-rc.2 |
|
|
33
37
|
| 0.4.0 | 0.1.5-rc.1、0.1.5-rc.2 |
|
|
34
38
|
| 0.3.3 | 0.1.5-rc.1 |
|
|
35
39
|
| 0.3.0 – 0.3.2 | ≥ 0.1.2-rc.1 |
|
|
36
40
|
|
|
37
|
-
`engines.dsh` 下限为 **0.1.5-rc.1**(`>=`,该版本起插件改用会话级槽位注入的宿主接口)。0.5.
|
|
41
|
+
`engines.dsh` 下限为 **0.1.5-rc.1**(`>=`,该版本起插件改用会话级槽位注入的宿主接口)。0.5.1 在 **0.1.5-rc.2** 上实测通过;它依赖的宿主接口——`turnOutline` 投影、会话级槽位的 `useProjection`、客户端 `sessions` 服务与 `loadThrough` 跳转加载器(0.5.0 起)、以及失败回合所用的 `turn-error` 节点(0.5.1 起)——已在 **0.1.5-rc.1** 的安装包中逐个核对存在,因此两版都声明兼容。更早或更新的 DSH 版本未经验证,不作声明;在更早的 DSH 上可安装的最新插件版本是 **0.3.2**。安装或更新时,DSH 市场会依据 `package.json` 中的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies` 做宿主兼容预检。
|
|
38
42
|
|
|
39
43
|
## 安装
|
|
40
44
|
|
|
@@ -60,15 +64,26 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
60
64
|
|
|
61
65
|
## 使用
|
|
62
66
|
|
|
63
|
-
-
|
|
67
|
+
- **跳转**:点击大纲标题或组头跳到对应位置;带「未加载」标记的条目会先把该回合加载进来再跳;失败回合点报错行跳到对话中的报错位置
|
|
64
68
|
- **搜索**:放大镜打开搜索框,输入后点结果定位并高亮,回车逐处跳,Esc 关闭
|
|
65
69
|
- **搜索容错**:全角/半角、大小写、空格差异会自动匹配;需要更宽松时点搜索框右侧的「模糊」开关
|
|
66
70
|
- **层级筛选**:点标题栏的圆形层级按钮弹出 H1–H6 开关,选择显示的层级
|
|
67
71
|
- **移动与停靠**:拖顶部横条移动,◀ / ▶ 切换左右停靠
|
|
68
72
|
- **调整大小**:拖右边缘、下边缘或右下角
|
|
69
73
|
- **加载更早**:在大纲中向上滚动(既展开索引,也加载更早的对话)
|
|
74
|
+
- **回到底部**:翻远了之后点列表右下角的浮动按钮回到最新一条
|
|
70
75
|
- **阅读位置**:滚动对话,正在阅读的回合会以蓝框标出;大纲会自动跟随
|
|
71
76
|
|
|
77
|
+
## 诊断
|
|
78
|
+
|
|
79
|
+
面板挂载时默认**不打印**任何日志。需要排查宿主能力时,在浏览器控制台执行一次:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
localStorage.setItem("dsh-quick-toc.debug", "1")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
再刷新页面,控制台会出现一行 `[dsh-quick-toc] panel mounted · turnOutline=… · jumpLoader=…`:`turnOutline` 缺失说明宿主没有该投影,`jumpLoader` 的分阶段措辞(`no-sessions-service` / `no-binding-api` / `no-binding-for-session` / `no-loadThrough` / `binding-threw`)可直接定位跳转桥断在哪一环。用完执行 `localStorage.removeItem("dsh-quick-toc.debug")` 关掉。
|
|
86
|
+
|
|
72
87
|
## 开发
|
|
73
88
|
|
|
74
89
|
- `lib/client.js` —— 全部 UI 逻辑(浏览器端)
|
|
@@ -76,6 +91,7 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
76
91
|
- `cordis.patch.yml` —— loader patch(符合官方 bundle 规范)
|
|
77
92
|
- 面板注册进会话级 `conversation.input.overlay` 槽以取得会话级 hook(`useChat`、`useSession`、`sessionId` 等),面板本体通过 `createPortal` 渲染到 `document.body` 成为固定浮层;对话数据来自 `props.useChat`(`ChatSnapshot.order` 与 `nodes`,节点形状:`kind: user/assistant-step`、`location.turn`、`data.blocks`)
|
|
78
93
|
- 「未加载回合」能力依赖宿主的两样东西:`turnOutline` 投影(整段会话的回合索引,每条含 `turn/start` 的 seq)与会话跳转加载器(客户端 `sessions` 服务的 `binding(sessionId).session.loadThrough(seq)`,通过客户端 ctx 的 `ctx.get("sessions")` 免声明查找取得)。两者各自独立降级:没有投影时只列已加载回合,没有加载器时未加载条目只展示、不跳转,面板其余功能不受影响。
|
|
94
|
+
- 失败回合的报错行读的是宿主的 `turn-error` 会话节点(宿主在 `turn/end` 的原因为 `error` 时发布,含 `message` 与可选的 `code`);宿主不提供该节点时只是不显示这一行,其余功能不受影响。
|
|
79
95
|
- 修改 `lib/client.js` 后刷新页面即可看到变化(客户端模块按内容哈希发版,DSH 的客户端 HMR 也会推送重载);若未生效再重启 DSH
|
|
80
96
|
|
|
81
97
|
## License
|
package/lib/client.js
CHANGED
|
@@ -52,11 +52,16 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
"search.hintMore": "向上滚动可加载更早的消息",
|
|
53
53
|
"search.hintOldest": "已经是最早的消息",
|
|
54
54
|
"hint.bottom": "已经到底了",
|
|
55
|
+
"outline.bottom": "回到底部",
|
|
55
56
|
"turn.jumpReply": "跳转到该回合的模型回答开头",
|
|
56
57
|
"turn.jumpTurn": "跳转到该回合开头",
|
|
57
58
|
"turn.loading": "正在加载这个回合…",
|
|
58
59
|
"turn.unloaded": "未加载",
|
|
59
60
|
"turn.loadTip": "点击加载这个回合并跳转过去",
|
|
61
|
+
"turn.failed": "请求失败",
|
|
62
|
+
"turn.jumpFailed": "跳转到该回合的报错位置",
|
|
63
|
+
"time.yesterday": "昨天",
|
|
64
|
+
"time.beforeYesterday": "前天",
|
|
60
65
|
"preview.truncated": "预览由宿主提供,可能被截断",
|
|
61
66
|
"resize.w": "拖拽调整宽度",
|
|
62
67
|
"resize.h": "拖拽调整高度",
|
|
@@ -592,6 +597,9 @@ window.__ModuleLoader__.load({
|
|
|
592
597
|
text: "var(--dsw-alias-label-primary, #e8eaee)",
|
|
593
598
|
muted: "var(--dsw-alias-label-secondary, #9aa0ab)",
|
|
594
599
|
accent: "var(--dsw-alias-brand-primary, #4f8cff)",
|
|
600
|
+
// terminal turn failures (a reply that never arrived) get the host's own
|
|
601
|
+
// error colour, so the outline speaks the same language as the transcript
|
|
602
|
+
error: "var(--dsw-alias-state-error-primary, #e5534b)",
|
|
595
603
|
hover: "var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))",
|
|
596
604
|
// ONE source for every "this control is on / this button is open / this match
|
|
597
605
|
// is current" tint, so the panel cannot drift into five near-identical blues.
|
|
@@ -952,6 +960,41 @@ window.__ModuleLoader__.load({
|
|
|
952
960
|
var lastScrollTopRef = react.useRef(0);
|
|
953
961
|
var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
|
|
954
962
|
|
|
963
|
+
// ---- "back to the newest row" button ------------------------------------
|
|
964
|
+
// The list can be scrolled far up into history (that is the whole point of
|
|
965
|
+
// paging), and the newest turns are then a long scroll away. The button
|
|
966
|
+
// appears whenever the list is NOT parked at its bottom and disappears once it
|
|
967
|
+
// is. `atBottomRef` mirrors the state so the scroll handler can tell "changed"
|
|
968
|
+
// from "same value" without depending on a stale render closure.
|
|
969
|
+
var _sAtBottom = react.useState(true);
|
|
970
|
+
var atBottom = _sAtBottom[0];
|
|
971
|
+
var setAtBottom = _sAtBottom[1];
|
|
972
|
+
var atBottomRef = react.useRef(true);
|
|
973
|
+
// a few px of slack: scrollTop is fractional and sub-pixel remainders are
|
|
974
|
+
// normal at the end of a smooth scroll
|
|
975
|
+
var syncAtBottom = function (el) {
|
|
976
|
+
if (!el) return;
|
|
977
|
+
var max = el.scrollHeight - el.clientHeight;
|
|
978
|
+
var next = max <= 4 || el.scrollTop >= max - 4;
|
|
979
|
+
if (next !== atBottomRef.current) {
|
|
980
|
+
atBottomRef.current = next;
|
|
981
|
+
setAtBottom(next);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
var scrollToBottom = function (e) {
|
|
985
|
+
if (e && e.stopPropagation) e.stopPropagation();
|
|
986
|
+
outlineTouchRef.current = Date.now();
|
|
987
|
+
hoverEnd();
|
|
988
|
+
var el = listRef.current;
|
|
989
|
+
if (!el) return;
|
|
990
|
+
// optimistic: the list is on its way to the newest row, so the button starts
|
|
991
|
+
// fading at once; if the user interrupts the scroll the handler brings it back
|
|
992
|
+
atBottomRef.current = true;
|
|
993
|
+
setAtBottom(true);
|
|
994
|
+
if (typeof el.scrollTo === "function") el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
995
|
+
else el.scrollTop = el.scrollHeight;
|
|
996
|
+
};
|
|
997
|
+
|
|
955
998
|
// ---- "scroll up to load earlier messages" hint (search mode) -----------
|
|
956
999
|
// Armed by a search when older history is still reachable; dismissed the
|
|
957
1000
|
// moment the reader scrolls up (which is when the older page starts loading,
|
|
@@ -1163,9 +1206,26 @@ window.__ModuleLoader__.load({
|
|
|
1163
1206
|
}
|
|
1164
1207
|
return false;
|
|
1165
1208
|
};
|
|
1209
|
+
var liftOffBottom = function () {
|
|
1210
|
+
// DSH re-pins the transcript to the newest message whenever it sees a
|
|
1211
|
+
// scroll it does not attribute to the reader while it still believes the
|
|
1212
|
+
// reader is at the bottom (`toBottom`). Paging history in from the OUTLINE
|
|
1213
|
+
// is exactly that case: the prepend compensation DSH applies is a
|
|
1214
|
+
// programmatic scroll, `atBottomRef` was never cleared (the reader never
|
|
1215
|
+
// touched the transcript), so the page-in is immediately undone and the
|
|
1216
|
+
// whole view — and with it the outline's follow — snaps back to the newest
|
|
1217
|
+
// turn. Nudging the transcript just past DSH's 25px stick zone first makes
|
|
1218
|
+
// that scroll read as a reader movement, so the page-in keeps its place.
|
|
1219
|
+
// (Same trick `glideTo` uses so a jump is not yanked back.)
|
|
1220
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
1221
|
+
if (!sp) return;
|
|
1222
|
+
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
1223
|
+
if (floor - sp.scrollTop <= 25) sp.scrollTop = Math.max(0, floor - 26);
|
|
1224
|
+
};
|
|
1166
1225
|
var viaHost = function () {
|
|
1167
1226
|
var btn = findLoadOlderButton();
|
|
1168
1227
|
if (!btn || btn.disabled) return false;
|
|
1228
|
+
liftOffBottom();
|
|
1169
1229
|
btn.click();
|
|
1170
1230
|
// once the conversation loads more, expand the outline window too
|
|
1171
1231
|
setTimeout(function () {
|
|
@@ -1240,6 +1300,7 @@ window.__ModuleLoader__.load({
|
|
|
1240
1300
|
var onListScroll = function (e) {
|
|
1241
1301
|
outlineTouchRef.current = Date.now();
|
|
1242
1302
|
var el = e.currentTarget;
|
|
1303
|
+
syncAtBottom(el);
|
|
1243
1304
|
var upward = el.scrollTop < lastScrollTopRef.current - 2;
|
|
1244
1305
|
if (upward) {
|
|
1245
1306
|
if (hint === "more") setHint("");
|
|
@@ -1332,24 +1393,39 @@ window.__ModuleLoader__.load({
|
|
|
1332
1393
|
window.addEventListener("pointerup", up);
|
|
1333
1394
|
};
|
|
1334
1395
|
|
|
1335
|
-
// ---- node timestamp ->
|
|
1336
|
-
var
|
|
1337
|
-
if (t === undefined || t === null) return
|
|
1396
|
+
// ---- node timestamp -> Date (handles ms / s epochs / ISO strings) ----
|
|
1397
|
+
var toDate = function (t) {
|
|
1398
|
+
if (t === undefined || t === null) return null;
|
|
1338
1399
|
if (typeof t === "string") {
|
|
1339
1400
|
var d0 = new Date(t);
|
|
1340
|
-
|
|
1341
|
-
var hh0 = ("0" + d0.getHours()).slice(-2);
|
|
1342
|
-
var mm0 = ("0" + d0.getMinutes()).slice(-2);
|
|
1343
|
-
return hh0 + ":" + mm0;
|
|
1401
|
+
return isNaN(d0.getTime()) ? null : d0;
|
|
1344
1402
|
}
|
|
1345
1403
|
var n = Number(t);
|
|
1346
|
-
if (!isFinite(n) || n <= 0) return
|
|
1404
|
+
if (!isFinite(n) || n <= 0) return null;
|
|
1347
1405
|
if (n < 1e12) n = n * 1000; // epoch seconds -> ms
|
|
1348
1406
|
var d = new Date(n);
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1407
|
+
return isNaN(d.getTime()) ? null : d;
|
|
1408
|
+
};
|
|
1409
|
+
var pad2 = function (n) { return ("0" + n).slice(-2); };
|
|
1410
|
+
// ---- node timestamp -> HH:MM ----
|
|
1411
|
+
var fmtTime = function (t) {
|
|
1412
|
+
var d = toDate(t);
|
|
1413
|
+
return d ? pad2(d.getHours()) + ":" + pad2(d.getMinutes()) : "";
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
// ---- turn stamp: the clock alone is ambiguous in a session that spans days,
|
|
1417
|
+
// so today stays "HH:MM", yesterday and the day before get a word, and anything
|
|
1418
|
+
// older gets its date as YY-MM-DD (weekday-free, sortable, short).
|
|
1419
|
+
var startOfDay = function (d) { return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); };
|
|
1420
|
+
var fmtStamp = function (t) {
|
|
1421
|
+
var d = toDate(t);
|
|
1422
|
+
if (!d) return "";
|
|
1423
|
+
var clock = pad2(d.getHours()) + ":" + pad2(d.getMinutes());
|
|
1424
|
+
var days = Math.round((startOfDay(new Date()) - startOfDay(d)) / 86400000);
|
|
1425
|
+
if (days <= 0) return clock; // today (a future skew also reads as today)
|
|
1426
|
+
if (days === 1) return T("time.yesterday") + " " + clock;
|
|
1427
|
+
if (days === 2) return T("time.beforeYesterday") + " " + clock;
|
|
1428
|
+
return pad2(d.getFullYear() % 100) + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " " + clock;
|
|
1353
1429
|
};
|
|
1354
1430
|
|
|
1355
1431
|
// ---- best-effort node time across common field names ----
|
|
@@ -1357,7 +1433,7 @@ window.__ModuleLoader__.load({
|
|
|
1357
1433
|
var d = node && node.data;
|
|
1358
1434
|
if (!d) return "";
|
|
1359
1435
|
var t = d.time !== undefined ? d.time : (d.createdAt !== undefined ? d.createdAt : d.timestamp);
|
|
1360
|
-
return
|
|
1436
|
+
return fmtStamp(t);
|
|
1361
1437
|
};
|
|
1362
1438
|
|
|
1363
1439
|
// ---- group headings by conversation turn (node.location.turn) ----
|
|
@@ -1370,12 +1446,18 @@ window.__ModuleLoader__.load({
|
|
|
1370
1446
|
var turnUserFull = {};
|
|
1371
1447
|
if (!order || !nodes) return result;
|
|
1372
1448
|
var seen = {};
|
|
1449
|
+
// A turn's group is anchored by EVERY node that belongs to it — the user
|
|
1450
|
+
// message, its assistant steps, and the terminal `turn-error` the host
|
|
1451
|
+
// publishes when a request never produced a reply. Without the error node a
|
|
1452
|
+
// timed-out turn had no time and no headings, so it was dropped here and the
|
|
1453
|
+
// host outline re-listed it as "未加载" even though it was fully loaded.
|
|
1454
|
+
var anchorKind = function (kind) { return kind === "user" || kind === "assistant-step" || kind === "turn-error"; };
|
|
1373
1455
|
// first pass: per-turn time — the LAST message of the turn wins (end time);
|
|
1374
1456
|
// also remember each turn's user message key + first-line preview + full text
|
|
1375
1457
|
for (var i = 0; i < order.length; i++) {
|
|
1376
1458
|
var k0 = order[i];
|
|
1377
1459
|
var n0 = nodes.get(k0);
|
|
1378
|
-
if (!n0 || (n0.kind
|
|
1460
|
+
if (!n0 || !anchorKind(n0.kind)) continue;
|
|
1379
1461
|
var l0 = n0.location;
|
|
1380
1462
|
var tid0 = l0 && (l0.kind === "turn" || l0.kind === "step") && l0.turn ? l0.turn.turn : null;
|
|
1381
1463
|
if (tid0 === null) continue;
|
|
@@ -1389,11 +1471,12 @@ window.__ModuleLoader__.load({
|
|
|
1389
1471
|
var t0 = getNodeTime(n0);
|
|
1390
1472
|
if (t0) turnTimes[tid0] = t0;
|
|
1391
1473
|
}
|
|
1392
|
-
// second pass: group assistant headings + full reply texts by turn
|
|
1474
|
+
// second pass: group assistant headings + full reply texts by turn, and
|
|
1475
|
+
// record a turn's terminal failure so the outline can say WHY it is empty
|
|
1393
1476
|
for (var j = 0; j < order.length; j++) {
|
|
1394
1477
|
var key = order[j];
|
|
1395
1478
|
var node = nodes.get(key);
|
|
1396
|
-
if (!node || node.kind
|
|
1479
|
+
if (!node || !anchorKind(node.kind)) continue;
|
|
1397
1480
|
var loc = node.location;
|
|
1398
1481
|
var turnId = loc && (loc.kind === "turn" || loc.kind === "step") && loc.turn ? loc.turn.turn : null;
|
|
1399
1482
|
var sameGroup = current !== null && current.turn === turnId;
|
|
@@ -1404,12 +1487,26 @@ window.__ModuleLoader__.load({
|
|
|
1404
1487
|
userKey: turnId !== null ? (turnUserKey[turnId] || "") : "",
|
|
1405
1488
|
userText: turnId !== null ? (turnUserText[turnId] || "") : "",
|
|
1406
1489
|
userFull: turnId !== null ? (turnUserFull[turnId] || "") : "",
|
|
1490
|
+
failure: null,
|
|
1407
1491
|
msgs: [],
|
|
1408
1492
|
headings: []
|
|
1409
1493
|
};
|
|
1410
1494
|
result.push(current);
|
|
1411
1495
|
}
|
|
1412
1496
|
seen[key] = true;
|
|
1497
|
+
if (node.kind === "turn-error") {
|
|
1498
|
+
// the host's terminal failure row: the turn ended without a reply, so
|
|
1499
|
+
// the outline shows the reason instead of pretending it is unloaded
|
|
1500
|
+
if (current.failure === null) {
|
|
1501
|
+
current.failure = {
|
|
1502
|
+
key: key,
|
|
1503
|
+
message: (node.data && node.data.message) || "",
|
|
1504
|
+
code: (node.data && node.data.code) || ""
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
if (node.kind !== "assistant-step") continue;
|
|
1413
1510
|
var info = nodeInfo(key, node);
|
|
1414
1511
|
current.msgs.push({ key: key, text: info.text });
|
|
1415
1512
|
for (var k = 0; k < info.headings.length; k++) {
|
|
@@ -1424,9 +1521,11 @@ window.__ModuleLoader__.load({
|
|
|
1424
1521
|
}
|
|
1425
1522
|
}
|
|
1426
1523
|
pruneNodeInfo(seen);
|
|
1427
|
-
// keep a turn when it has headings
|
|
1428
|
-
// still get a standalone time entry
|
|
1429
|
-
var ready = result.filter(function (g) {
|
|
1524
|
+
// keep a turn when it has headings, a time, a loaded prompt or a failure —
|
|
1525
|
+
// turns without headings still get a standalone time entry (click to jump)
|
|
1526
|
+
var ready = result.filter(function (g) {
|
|
1527
|
+
return g.headings.length > 0 || g.time !== "" || g.userKey !== "" || g.failure !== null;
|
|
1528
|
+
});
|
|
1430
1529
|
if (outlineTurns === null) return ready;
|
|
1431
1530
|
// ---- merge the whole-log turn index ---------------------------------
|
|
1432
1531
|
// Loaded groups keep their order (a group whose node carries no turn
|
|
@@ -1452,6 +1551,7 @@ window.__ModuleLoader__.load({
|
|
|
1452
1551
|
userKey: "",
|
|
1453
1552
|
userText: entry.prompt,
|
|
1454
1553
|
userFull: "",
|
|
1554
|
+
failure: null,
|
|
1455
1555
|
msgs: [],
|
|
1456
1556
|
headings: [],
|
|
1457
1557
|
preview: entry.response
|
|
@@ -1469,12 +1569,26 @@ window.__ModuleLoader__.load({
|
|
|
1469
1569
|
didInitScroll.current = true;
|
|
1470
1570
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
1471
1571
|
}, [groups.length]);
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1572
|
+
// The "back to the newest row" button follows the list's real geometry, and a
|
|
1573
|
+
// bottom can be reached WITHOUT a scroll event: older turns page in, a search
|
|
1574
|
+
// replaces the rows, the panel is resized, the panel is expanded/collapsed.
|
|
1575
|
+
// Also must stay below `groups` (deps arrays read the vars eagerly).
|
|
1576
|
+
react.useEffect(function () {
|
|
1577
|
+
syncAtBottom(listRef.current);
|
|
1578
|
+
}, [groups.length, query, visibleCount, panelW, panelH, open]);
|
|
1579
|
+
// Mount diagnostic: the host capabilities this panel actually reached
|
|
1580
|
+
// (whole-log turn index + jump loader). SILENT by default — a normal console
|
|
1581
|
+
// should stay clean — and switched on with
|
|
1582
|
+
// localStorage.setItem("dsh-quick-toc.debug", "1")
|
|
1583
|
+
// plus a reload. Kept because it is the fastest way to tell "the host has no
|
|
1584
|
+
// turnOutline projection" apart from "the jump-loader bridge is broken".
|
|
1474
1585
|
var mountLogRef = react.useRef(false);
|
|
1475
1586
|
react.useEffect(function () {
|
|
1476
1587
|
if (mountLogRef.current) return;
|
|
1477
1588
|
mountLogRef.current = true;
|
|
1589
|
+
var debug = false;
|
|
1590
|
+
try { debug = localStorage.getItem("dsh-quick-toc.debug") === "1"; } catch (e) { debug = false; }
|
|
1591
|
+
if (!debug) return;
|
|
1478
1592
|
console.log("[dsh-quick-toc] panel mounted · turnOutline=" +
|
|
1479
1593
|
(outlineTurns ? outlineTurns.length + " turns" : "unavailable") +
|
|
1480
1594
|
" · jumpLoader=" + jumpLoaderState());
|
|
@@ -1506,6 +1620,7 @@ window.__ModuleLoader__.load({
|
|
|
1506
1620
|
var g = groups[i];
|
|
1507
1621
|
if (g.ghost) continue; // no loaded rows to match
|
|
1508
1622
|
if (g.userKey) m[g.userKey] = i;
|
|
1623
|
+
if (g.failure) m[g.failure.key] = i;
|
|
1509
1624
|
for (var j = 0; j < g.headings.length; j++) {
|
|
1510
1625
|
m[g.headings[j].key] = i;
|
|
1511
1626
|
}
|
|
@@ -1604,6 +1719,18 @@ window.__ModuleLoader__.load({
|
|
|
1604
1719
|
level: 0, path: "", time: g.time, snippet: snippetAround(msg.text, fq, 36, fuzzy)
|
|
1605
1720
|
}, nm);
|
|
1606
1721
|
}
|
|
1722
|
+
// a failed turn's provider message is searchable too — "timed out" must
|
|
1723
|
+
// find the turns it killed
|
|
1724
|
+
if (g.failure && g.failure.message) {
|
|
1725
|
+
var nf = countOccurrences(g.failure.message, needle, fuzzy);
|
|
1726
|
+
if (nf > 0) {
|
|
1727
|
+
push({
|
|
1728
|
+
gi: gi, key: g.failure.key, idx: undefined, title: previewText(g.failure.message, 40),
|
|
1729
|
+
level: 0, path: "", time: g.time, failed: true,
|
|
1730
|
+
snippet: snippetAround(g.failure.message, fq, 36, fuzzy)
|
|
1731
|
+
}, nf);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1607
1734
|
}
|
|
1608
1735
|
}
|
|
1609
1736
|
return rows;
|
|
@@ -1831,21 +1958,45 @@ window.__ModuleLoader__.load({
|
|
|
1831
1958
|
if (handleRef.current) handleRef.current.style.opacity = on ? "1" : "0.35";
|
|
1832
1959
|
};
|
|
1833
1960
|
|
|
1834
|
-
// ---- jump:
|
|
1835
|
-
//
|
|
1836
|
-
// the
|
|
1961
|
+
// ---- jump: glide to the exact heading element ---------------------------
|
|
1962
|
+
// A SMOOTH scroll is only trustworthy while the paged conversation keeps its
|
|
1963
|
+
// geometry. A LONG jump travels through the whole window, and the host mounts
|
|
1964
|
+
// and drops turns along the way, so the target's offset moves mid-flight and
|
|
1965
|
+
// the animation lands short ("it scrolled halfway and stopped"). Long jumps
|
|
1966
|
+
// therefore position INSTANTLY and then re-check for a short while (the same
|
|
1967
|
+
// "instant, never smooth" rule the outline's own follow uses), while nearby
|
|
1968
|
+
// headings keep the glide. When glued to the bottom, lift just past DSH's 25px
|
|
1969
|
+
// stick-to-bottom threshold first so the landing is not yanked back.
|
|
1970
|
+
var scrollTargetOf = function (el, sp) {
|
|
1971
|
+
var t = el.getBoundingClientRect().top - sp.getBoundingClientRect().top + sp.scrollTop - 20;
|
|
1972
|
+
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
1973
|
+
return Math.max(0, Math.min(t, floor));
|
|
1974
|
+
};
|
|
1837
1975
|
var glideTo = function (el) {
|
|
1838
1976
|
var sp = el.closest ? el.closest("[data-conversation-scroll]") : null;
|
|
1839
1977
|
if (!sp) {
|
|
1840
1978
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1841
1979
|
return;
|
|
1842
1980
|
}
|
|
1843
|
-
var t = el
|
|
1981
|
+
var t = scrollTargetOf(el, sp);
|
|
1844
1982
|
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
1845
1983
|
if (floor - sp.scrollTop <= 25 && Math.abs(t - sp.scrollTop) > 60) {
|
|
1846
1984
|
sp.scrollTop = Math.max(0, floor - 26);
|
|
1847
1985
|
}
|
|
1848
|
-
|
|
1986
|
+
if (Math.abs(t - sp.scrollTop) <= sp.clientHeight * 1.5) {
|
|
1987
|
+
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
// long jump: land now, then keep the landing honest for ~0.6s while the host
|
|
1991
|
+
// finishes measuring the window it just re-paged
|
|
1992
|
+
sp.scrollTop = t;
|
|
1993
|
+
var ticks = 0;
|
|
1994
|
+
var settle = setInterval(function () {
|
|
1995
|
+
ticks++;
|
|
1996
|
+
var next = scrollTargetOf(el, sp);
|
|
1997
|
+
if (Math.abs(next - sp.scrollTop) > 2) sp.scrollTop = next;
|
|
1998
|
+
if (ticks >= 5) clearInterval(settle);
|
|
1999
|
+
}, 120);
|
|
1849
2000
|
};
|
|
1850
2001
|
|
|
1851
2002
|
var jump = function (key, idx) {
|
|
@@ -2192,30 +2343,16 @@ window.__ModuleLoader__.load({
|
|
|
2192
2343
|
gap: 6
|
|
2193
2344
|
},
|
|
2194
2345
|
children: [
|
|
2195
|
-
//
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
title: T("panel.title"),
|
|
2199
|
-
children: react_jsx_runtime.jsx("svg", {
|
|
2200
|
-
width: 18,
|
|
2201
|
-
height: 14,
|
|
2202
|
-
viewBox: "0 0 18 14",
|
|
2203
|
-
style: { display: "block" },
|
|
2204
|
-
children: [
|
|
2205
|
-
react_jsx_runtime.jsx("line", { x1: 0, y1: 2, x2: 18, y2: 2, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" }),
|
|
2206
|
-
react_jsx_runtime.jsx("line", { x1: 0, y1: 7, x2: 12, y2: 7, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" }),
|
|
2207
|
-
react_jsx_runtime.jsx("line", { x1: 0, y1: 12, x2: 6, y2: 12, stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" })
|
|
2208
|
-
]
|
|
2209
|
-
})
|
|
2210
|
-
}),
|
|
2346
|
+
// LEFT pair: the heading-level filter and the dock toggle take the
|
|
2347
|
+
// left end — so the header reads two controls per side (search +
|
|
2348
|
+
// collapse sit at the other end).
|
|
2211
2349
|
react_jsx_runtime.jsx("div", {
|
|
2212
2350
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
2213
2351
|
children: [
|
|
2214
|
-
// heading-level filter: a round icon button
|
|
2215
|
-
//
|
|
2216
|
-
//
|
|
2217
|
-
//
|
|
2218
|
-
// tint change; the resting look never shifts when open.
|
|
2352
|
+
// heading-level filter: a round icon button that pops down the
|
|
2353
|
+
// H1–H6 picker. Icon: three lines of decreasing width = outline
|
|
2354
|
+
// levels. Hover behaviour matches the magnifier: background +
|
|
2355
|
+
// icon tint change; the resting look never shifts when open.
|
|
2219
2356
|
react_jsx_runtime.jsx("button", {
|
|
2220
2357
|
className: "dqt-levels-btn",
|
|
2221
2358
|
onClick: function () { levelsOpen ? closeLevels() : setLevelsOpen(true); },
|
|
@@ -2262,6 +2399,16 @@ window.__ModuleLoader__.load({
|
|
|
2262
2399
|
]
|
|
2263
2400
|
})
|
|
2264
2401
|
}),
|
|
2402
|
+
// dock toggle: the triangle tips toward the side it will move TO
|
|
2403
|
+
iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 })
|
|
2404
|
+
]
|
|
2405
|
+
}),
|
|
2406
|
+
// spacer: nothing here, it just pins the two pairs to the two ends
|
|
2407
|
+
react_jsx_runtime.jsx("div", { style: { flex: "1 1 auto" } }),
|
|
2408
|
+
// RIGHT pair: search + collapse
|
|
2409
|
+
react_jsx_runtime.jsx("div", {
|
|
2410
|
+
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
2411
|
+
children: [
|
|
2265
2412
|
// magnifier button (SVG, matches the other buttons' style)
|
|
2266
2413
|
react_jsx_runtime.jsx("button", {
|
|
2267
2414
|
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
@@ -2304,8 +2451,6 @@ window.__ModuleLoader__.load({
|
|
|
2304
2451
|
]
|
|
2305
2452
|
})
|
|
2306
2453
|
}),
|
|
2307
|
-
// triangle tips toward the side it will move TO: shift slightly up + toward the tip
|
|
2308
|
-
iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 }),
|
|
2309
2454
|
// close: thick SVG cross, nudged slightly down
|
|
2310
2455
|
react_jsx_runtime.jsx("button", {
|
|
2311
2456
|
onClick: function () { setOpen(false); },
|
|
@@ -2575,6 +2720,71 @@ window.__ModuleLoader__.load({
|
|
|
2575
2720
|
: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup, renderExtra)
|
|
2576
2721
|
})
|
|
2577
2722
|
}),
|
|
2723
|
+
// "back to the newest row": floats over the list's lower-right corner and
|
|
2724
|
+
// fades in the moment the newest turns fall below the fold. Kept MOUNTED
|
|
2725
|
+
// and toggled by opacity/pointer-events — an unmounted button has nothing
|
|
2726
|
+
// to animate, and (like the level picker) its animation must not depend on
|
|
2727
|
+
// inline keyframes that every re-render rewrites.
|
|
2728
|
+
react_jsx_runtime.jsx("button", {
|
|
2729
|
+
onClick: scrollToBottom,
|
|
2730
|
+
className: "dqt-bottom-btn",
|
|
2731
|
+
"data-at-bottom": atBottom ? "on" : "off",
|
|
2732
|
+
title: T("outline.bottom"),
|
|
2733
|
+
tabIndex: atBottom ? -1 : 0,
|
|
2734
|
+
"aria-hidden": atBottom ? "true" : undefined,
|
|
2735
|
+
onMouseEnter: function (e) {
|
|
2736
|
+
e.currentTarget.style.color = C.accent;
|
|
2737
|
+
e.currentTarget.style.borderColor = C.groupEdgeSoft;
|
|
2738
|
+
},
|
|
2739
|
+
onMouseLeave: function (e) {
|
|
2740
|
+
e.currentTarget.style.color = C.text;
|
|
2741
|
+
e.currentTarget.style.borderColor = C.panelBorder;
|
|
2742
|
+
},
|
|
2743
|
+
style: {
|
|
2744
|
+
position: "absolute",
|
|
2745
|
+
// clear of the resize handle in the very corner (16x16)
|
|
2746
|
+
right: 18,
|
|
2747
|
+
bottom: 12,
|
|
2748
|
+
width: 26,
|
|
2749
|
+
height: 26,
|
|
2750
|
+
display: "flex",
|
|
2751
|
+
alignItems: "center",
|
|
2752
|
+
justifyContent: "center",
|
|
2753
|
+
padding: 0,
|
|
2754
|
+
// OPAQUE surface + outline + lift, the same vocabulary as the hover card:
|
|
2755
|
+
// a translucent chip disappeared into the rows behind it (and the panel's
|
|
2756
|
+
// own idle transparency multiplied it away). The surface is the host's
|
|
2757
|
+
// RAISED menu colour, not the panel background — a fill identical to the
|
|
2758
|
+
// panel it sits on reads as "no fill" and the button looks see-through.
|
|
2759
|
+
background: "var(--dsw-specific-menu, rgba(44, 49, 60, 0.99))",
|
|
2760
|
+
border: "1px solid " + C.panelBorder,
|
|
2761
|
+
borderRadius: "50%",
|
|
2762
|
+
cornerShape: "round",
|
|
2763
|
+
color: C.text,
|
|
2764
|
+
cursor: "pointer",
|
|
2765
|
+
zIndex: 6,
|
|
2766
|
+
boxShadow: "0 3px 10px rgba(0, 0, 0, 0.30)",
|
|
2767
|
+
opacity: atBottom ? 0 : 1,
|
|
2768
|
+
transform: atBottom ? "translateY(6px)" : "translateY(0)",
|
|
2769
|
+
pointerEvents: atBottom ? "none" : "auto",
|
|
2770
|
+
transition: "opacity 0.22s ease, transform 0.22s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease"
|
|
2771
|
+
},
|
|
2772
|
+
children: react_jsx_runtime.jsx("svg", {
|
|
2773
|
+
width: 14,
|
|
2774
|
+
height: 14,
|
|
2775
|
+
viewBox: "0 0 14 14",
|
|
2776
|
+
fill: "none",
|
|
2777
|
+
stroke: "currentColor",
|
|
2778
|
+
strokeWidth: 1.8,
|
|
2779
|
+
strokeLinecap: "round",
|
|
2780
|
+
strokeLinejoin: "round",
|
|
2781
|
+
style: { display: "block" },
|
|
2782
|
+
children: [
|
|
2783
|
+
react_jsx_runtime.jsx("polyline", { points: "3.5,4.5 7,8 10.5,4.5" }),
|
|
2784
|
+
react_jsx_runtime.jsx("line", { x1: 3.5, y1: 11, x2: 10.5, y2: 11 })
|
|
2785
|
+
]
|
|
2786
|
+
})
|
|
2787
|
+
}),
|
|
2578
2788
|
toastEl,
|
|
2579
2789
|
// resize handles (right edge: width, bottom edge: height)
|
|
2580
2790
|
react_jsx_runtime.jsx("div", {
|
|
@@ -2620,8 +2830,9 @@ window.__ModuleLoader__.load({
|
|
|
2620
2830
|
onPointerDown: onResizeCornerDown,
|
|
2621
2831
|
title: T("resize.wh")
|
|
2622
2832
|
}),
|
|
2623
|
-
// heading-level picker: pops down from the round header button
|
|
2624
|
-
//
|
|
2833
|
+
// heading-level picker: pops down from the round header button and its
|
|
2834
|
+
// LEFT edge lines up with that button (the button now sits at the left end
|
|
2835
|
+
// of the header, so 10px is exactly the header's own left padding).
|
|
2625
2836
|
// Open/close animate from the button's center: the animation lives in
|
|
2626
2837
|
// the injected stylesheet (classes, not inline animation) so React
|
|
2627
2838
|
// re-renders never restart or cancel it — an inline `animation` set on
|
|
@@ -2631,7 +2842,7 @@ window.__ModuleLoader__.load({
|
|
|
2631
2842
|
style: {
|
|
2632
2843
|
position: "absolute",
|
|
2633
2844
|
top: 40,
|
|
2634
|
-
|
|
2845
|
+
left: 10,
|
|
2635
2846
|
zIndex: 30,
|
|
2636
2847
|
display: "flex",
|
|
2637
2848
|
alignItems: "center",
|
|
@@ -2643,8 +2854,9 @@ window.__ModuleLoader__.load({
|
|
|
2643
2854
|
borderRadius: 10,
|
|
2644
2855
|
cornerShape: "round",
|
|
2645
2856
|
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.28), 0 2px 6px rgba(0, 0, 0, 0.18)",
|
|
2646
|
-
maxWidth: "calc(100% -
|
|
2647
|
-
|
|
2857
|
+
maxWidth: "calc(100% - 20px)",
|
|
2858
|
+
// the button's center sits 12px inside the popup's left edge
|
|
2859
|
+
transformOrigin: "12px top"
|
|
2648
2860
|
},
|
|
2649
2861
|
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
2650
2862
|
var active = !!levelSet[lv];
|
|
@@ -2893,6 +3105,7 @@ window.__ModuleLoader__.load({
|
|
|
2893
3105
|
var fuzzy = !!(extra && extra.fuzzy);
|
|
2894
3106
|
var meta = [];
|
|
2895
3107
|
if (r.ghost) meta.push(T("turn.unloaded"));
|
|
3108
|
+
if (r.failed) meta.push(T("turn.failed"));
|
|
2896
3109
|
if (r.time) meta.push(r.time);
|
|
2897
3110
|
if (r.path) meta.push(r.path);
|
|
2898
3111
|
if (r.count > 1) meta.push("×" + r.count);
|
|
@@ -2984,8 +3197,10 @@ window.__ModuleLoader__.load({
|
|
|
2984
3197
|
// Implemented as a function (not an inline closure in a loop) so each
|
|
2985
3198
|
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
2986
3199
|
// otherwise make every header jump to the last group.
|
|
2987
|
-
function renderGroupHeader(g, gi, jump, C, isActive
|
|
2988
|
-
|
|
3200
|
+
function renderGroupHeader(g, gi, jump, C, isActive) {
|
|
3201
|
+
// a turn that ended in a failure has no reply to land on: its terminal row IS
|
|
3202
|
+
// the interesting position, so the header lands there (and says so in the tip)
|
|
3203
|
+
var replyKey = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : (g.failure ? g.failure.key : "");
|
|
2989
3204
|
var jumpToTurn = function (e) {
|
|
2990
3205
|
e.stopPropagation();
|
|
2991
3206
|
// prefer the model's reply; fall back to the user message, then to the
|
|
@@ -2996,8 +3211,12 @@ window.__ModuleLoader__.load({
|
|
|
2996
3211
|
};
|
|
2997
3212
|
return react_jsx_runtime.jsx("div", {
|
|
2998
3213
|
style: {
|
|
2999
|
-
|
|
3000
|
-
|
|
3214
|
+
// ONE row geometry for every group header, whether or not the turn has
|
|
3215
|
+
// headings: same box, same 18px rhythm, same left inset, so the time
|
|
3216
|
+
// column of a heading-less turn lines up with all the others. Only the
|
|
3217
|
+
// type emphasis differs (weight + colour, see the label below).
|
|
3218
|
+
padding: "1px 4px 2px",
|
|
3219
|
+
height: 18,
|
|
3001
3220
|
display: "flex",
|
|
3002
3221
|
alignItems: "center",
|
|
3003
3222
|
minWidth: 0,
|
|
@@ -3027,19 +3246,21 @@ window.__ModuleLoader__.load({
|
|
|
3027
3246
|
}, "g-h-tint-" + gi),
|
|
3028
3247
|
react_jsx_runtime.jsx("span", {
|
|
3029
3248
|
onClick: jumpToTurn,
|
|
3030
|
-
title:
|
|
3249
|
+
title: (g.msgs && g.msgs.length > 0) ? T("turn.jumpReply") : (g.failure ? T("turn.jumpFailed") : T("turn.jumpTurn")),
|
|
3031
3250
|
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
3032
3251
|
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
3033
3252
|
style: {
|
|
3034
|
-
//
|
|
3035
|
-
//
|
|
3036
|
-
//
|
|
3037
|
-
//
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3253
|
+
// ONE style for every group header, heading-less turns included: a turn
|
|
3254
|
+
// whose reply has no markdown headings is not a different kind of entry,
|
|
3255
|
+
// it is simply a group with no rows under its header, and it must line up
|
|
3256
|
+
// with the rest of the list (same size, weight, colour, opacity). The only
|
|
3257
|
+
// thing that marks a group is the shared "current group" tint, which lives
|
|
3258
|
+
// on the group box — never on the label.
|
|
3259
|
+
fontSize: 11,
|
|
3260
|
+
fontWeight: 400,
|
|
3261
|
+
color: C.muted,
|
|
3041
3262
|
cursor: "pointer",
|
|
3042
|
-
padding:
|
|
3263
|
+
padding: "1px 5px",
|
|
3043
3264
|
borderRadius: 4,
|
|
3044
3265
|
background: "transparent",
|
|
3045
3266
|
transition: "background 0.15s ease",
|
|
@@ -3054,8 +3275,8 @@ window.__ModuleLoader__.load({
|
|
|
3054
3275
|
react_jsx_runtime.jsx("span", { style: { fontWeight: 600, flex: "none" }, children: g.time || " " }),
|
|
3055
3276
|
g.userText ? react_jsx_runtime.jsx("span", {
|
|
3056
3277
|
style: {
|
|
3057
|
-
fontWeight:
|
|
3058
|
-
opacity:
|
|
3278
|
+
fontWeight: 400,
|
|
3279
|
+
opacity: 0.75,
|
|
3059
3280
|
overflow: "hidden",
|
|
3060
3281
|
textOverflow: "ellipsis",
|
|
3061
3282
|
whiteSpace: "nowrap",
|
|
@@ -3123,6 +3344,55 @@ window.__ModuleLoader__.load({
|
|
|
3123
3344
|
}, "g-ghost-" + gi);
|
|
3124
3345
|
}
|
|
3125
3346
|
|
|
3347
|
+
// A turn that ended in a terminal failure (the host's `turn-error` row): the
|
|
3348
|
+
// outline lists it as a normal turn and adds this row, so a turn whose request
|
|
3349
|
+
// never came back reads as "请求失败 · <provider message>" instead of "未加载"
|
|
3350
|
+
// and never silently disappears from the outline.
|
|
3351
|
+
function renderFailureRow(g, gi, jump, C) {
|
|
3352
|
+
var f = g.failure;
|
|
3353
|
+
var message = f.message || f.code || "";
|
|
3354
|
+
var open = function (e) {
|
|
3355
|
+
e.stopPropagation();
|
|
3356
|
+
if (findRow(f.key)) jump(f.key);
|
|
3357
|
+
else if (g.userKey && findRow(g.userKey)) jump(g.userKey);
|
|
3358
|
+
};
|
|
3359
|
+
return react_jsx_runtime.jsx("div", {
|
|
3360
|
+
onClick: open,
|
|
3361
|
+
title: message || T("turn.failed"),
|
|
3362
|
+
"data-turn-failure": g.turn === null || g.turn === undefined ? String(gi) : String(g.turn),
|
|
3363
|
+
style: {
|
|
3364
|
+
display: "flex",
|
|
3365
|
+
alignItems: "center",
|
|
3366
|
+
gap: 5,
|
|
3367
|
+
minWidth: 0,
|
|
3368
|
+
padding: "1px 6px",
|
|
3369
|
+
margin: "1px 0",
|
|
3370
|
+
borderRadius: 6,
|
|
3371
|
+
cursor: "pointer",
|
|
3372
|
+
transition: "background 0.15s ease"
|
|
3373
|
+
},
|
|
3374
|
+
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
3375
|
+
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
3376
|
+
children: [
|
|
3377
|
+
react_jsx_runtime.jsx("span", {
|
|
3378
|
+
style: { fontSize: 10, fontWeight: 600, flex: "none", color: C.error, border: "1px solid " + C.error, borderRadius: 4, padding: "0 4px" },
|
|
3379
|
+
children: T("turn.failed")
|
|
3380
|
+
}),
|
|
3381
|
+
message ? react_jsx_runtime.jsx("span", {
|
|
3382
|
+
style: {
|
|
3383
|
+
fontSize: 11,
|
|
3384
|
+
color: C.muted,
|
|
3385
|
+
overflow: "hidden",
|
|
3386
|
+
textOverflow: "ellipsis",
|
|
3387
|
+
whiteSpace: "nowrap",
|
|
3388
|
+
minWidth: 0
|
|
3389
|
+
},
|
|
3390
|
+
children: message
|
|
3391
|
+
}) : null
|
|
3392
|
+
]
|
|
3393
|
+
}, "g-fail-" + gi);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3126
3396
|
// render each conversation turn as its own block: solid divider + time header
|
|
3127
3397
|
// offset = global group index of the first rendered group (stable React keys)
|
|
3128
3398
|
// activeIdx = array of groups being read (full opacity); others are dimmed
|
|
@@ -3148,14 +3418,20 @@ window.__ModuleLoader__.load({
|
|
|
3148
3418
|
if (g.ghost) {
|
|
3149
3419
|
items.push(renderGhostGroup(g, gi, C, extra));
|
|
3150
3420
|
} else {
|
|
3151
|
-
|
|
3421
|
+
// every group gets the SAME header, whether or not this turn produced any
|
|
3422
|
+
// markdown headings — a heading-less turn is just a group with no rows, not
|
|
3423
|
+
// a second kind of entry (`rows` only decides what gets listed below it)
|
|
3424
|
+
items.push(renderGroupHeader(g, gi, jump, C, isActive));
|
|
3152
3425
|
items.push(renderNodes(rows, 0, jump, C, "g" + gi, extra));
|
|
3426
|
+
// the failure is the turn's LAST event, so its row sits under the rows
|
|
3427
|
+
if (g.failure) items.push(renderFailureRow(g, gi, jump, C));
|
|
3153
3428
|
}
|
|
3154
|
-
//
|
|
3155
|
-
//
|
|
3156
|
-
//
|
|
3157
|
-
//
|
|
3158
|
-
//
|
|
3429
|
+
// The group being read stays at full opacity and every other group is dimmed
|
|
3430
|
+
// to 0.85 — enough to mark the reading position without turning the list grey
|
|
3431
|
+
// (0.6 did, and multiplied by the panel's own idle transparency it made the
|
|
3432
|
+
// outline barely readable). The current group ALSO gets the closed tinted box
|
|
3433
|
+
// below (fill + outline + accent bar), which stays visible on top of any
|
|
3434
|
+
// panel transparency and never looks cut off on the right.
|
|
3159
3435
|
var dim = !isActive && !g.ghost;
|
|
3160
3436
|
out.push(react_jsx_runtime.jsx("div", {
|
|
3161
3437
|
"data-group-idx": gi,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Conversation TOC for DeepSeek Harness (DSH): turn-grouped Markdown outline covering the whole session (unloaded turns included), title/full-text search with an optional fuzzy switch, hover previews, in-chat match highlighting, heading-level filter and reading-position auto-follow",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|