dsh-turn-outline 0.2.6 → 0.3.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/README.en.md +22 -16
- package/README.md +23 -17
- package/lib/client.js +172 -51
- package/lib/client.js.map +2 -2
- package/lib/index.js +15 -4
- package/lib/index.js.map +2 -2
- package/package.json +4 -3
package/README.en.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/Andor-Z/dsh-turn-outline/releases)
|
|
7
7
|
[](https://github.com/topics/dsh-plugin)
|
|
8
|
-
[](https://github.com/omdsh-dev/DSH-better-sidebar)
|
|
9
|
+
[](https://github.com/omdsh-dev/DSH-better-sidebar)
|
|
9
10
|
|
|
10
11
|
[中文](README.md)
|
|
11
12
|
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
**What it solves**:
|
|
25
26
|
|
|
26
27
|
- **Fold the session into user turns** — the conversation is collapsed into a structured "input → tool steps → output" trail, so long sessions become scannable at a glance; multi-level folding inside a turn plus tool-type filter chips let you focus on what matters.
|
|
27
|
-
- **Structured jump-back** — click any turn / tool step to auto-load history and scroll to the matching row in the chat view / trajectory view.
|
|
28
|
+
- **Structured jump-back** — click any turn / tool step to auto-load history and scroll to the matching row in the chat view / trajectory view. On 0.2.0 navigation goes through the client `uiConversation` conversation view registry (`anchorSeq` node matching + `loadOlder` paging); on 0.1.x it falls back to the legacy session snapshot — no dependence on DOM text or hashed class names.
|
|
28
29
|
- **Zero AI dependency** — pure event folding, no LLM calls: zero tokens, instant open, no failure states.
|
|
29
30
|
- **Read-only & non-invasive** — never touches the DSH core: one read-only host route + one client tab; registers cleanly and unloads without residue.
|
|
30
31
|
- **Incremental performance** — refreshes are driven by session subscriptions (no polling); `TrailFolder` only re-folds open turns (O(delta)); seq-keyed rows reconcile in place instead of remounting.
|
|
@@ -37,16 +38,16 @@
|
|
|
37
38
|
|
|
38
39
|
### Option 1: one-liner from npm (recommended)
|
|
39
40
|
|
|
40
|
-
Install better-sidebar first
|
|
41
|
+
Install better-sidebar first, then this plugin:
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
|
-
npx @deepseek-ai/dsh plugin --profile desktop add dsh-better-sidebar
|
|
44
|
+
npx @deepseek-ai/dsh plugin --profile desktop add dsh-better-sidebar
|
|
44
45
|
npx @deepseek-ai/dsh plugin --profile desktop add dsh-turn-outline
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
> Using the Web profile? Swap `--profile desktop` for `--profile web`.
|
|
48
49
|
>
|
|
49
|
-
>
|
|
50
|
+
> ℹ️ **Version-line note**: **since 0.3.0 the primary line is DSH 0.2.0-rc.x** (tested against the DSH Desktop 2.0.17 bundled runtime + better-sidebar 0.24.1; jump-back rides the `uiConversation` conversation view registry and the host live path uses `snapshotEvents()`). The 0.1.x lines stay fallback-compatible — **better-sidebar 0.18.0 + DSH 0.1.2-rc.1** and **better-sidebar 0.17.1 + DSH 0.1.1-rc.x (stable)**: on older hosts the plugin automatically uses the legacy snapshot jump and the `.events` live path, with no configuration. Note that better-sidebar 0.18.0 supports only DSH 0.1.2-rc.1+: on an older stable host (0.1.0-rc.8 ~ 0.1.1-rc.2), pin `dsh-better-sidebar@0.17.1`.
|
|
50
51
|
|
|
51
52
|
### Option 2: install from source (development / offline)
|
|
52
53
|
|
|
@@ -78,28 +79,32 @@ npx @deepseek-ai/dsh plugin --profile desktop remove dsh-turn-outline
|
|
|
78
79
|
|
|
79
80
|
```
|
|
80
81
|
host (Node) inject: ['webServer','sessionQuery','sessions']
|
|
81
|
-
POST /
|
|
82
|
-
→ live sessions: borrow the frozen snapshot of the live store (
|
|
82
|
+
POST /turn-outline/api/events { sessionId, afterSeq }
|
|
83
|
+
→ live sessions: borrow the frozen snapshot of the live store (snapshotEvents() on DSH
|
|
84
|
+
0.2.0, the .events property on 0.1.x; O(1) probe + O(delta) projection, zero deep clones)
|
|
83
85
|
→ cold sessions: sessionQuery persisted log + lastSeq cache (LRU + byte cap)
|
|
84
86
|
→ paged responses (PAGE_SIZE events per page, hasMore semantics) + field trimming (no full tool-result payloads)
|
|
85
87
|
→ 200 { events, hasMore } / 400 / 403 / 404 / 405 / 413
|
|
86
88
|
|
|
87
|
-
client (browser) inject: ['sessions','betterSidebar'] (
|
|
88
|
-
→ registerTab({ id:'turn-outline', single:true })
|
|
89
|
-
→
|
|
89
|
+
client (browser) inject: ['sessions','uiConversation','betterSidebar'] (the latter two are optional peers)
|
|
90
|
+
→ registerTab({ id:'turn-outline', single:true, description })
|
|
91
|
+
→ jump-back: on 0.2.0 the ctx.uiConversation conversation view registry (chat/trajectory
|
|
92
|
+
targets); on 0.1.x the legacy session snapshot's chat.nodes / views fields
|
|
93
|
+
→ subscription-driven refresh (session.subscribe + the 0.2.0 eventSource; no polling)
|
|
94
|
+
+ incremental folding (TrailFolder: only re-folds open turns)
|
|
90
95
|
+ seq-keyed rows (append without remount) + per-turn React.memo + three-level folding + tool filter chips
|
|
91
96
|
```
|
|
92
97
|
|
|
93
98
|
## Performance design (introduced in v0.2.1)
|
|
94
99
|
|
|
95
|
-
- **No more deep-cloning the whole log on the host**: the old `listEvents` probe and `readSession` read each `structuredClone` every event (~3 full deep clones per request for live sessions). Live sessions now borrow the store's frozen
|
|
100
|
+
- **No more deep-cloning the whole log on the host**: the old `listEvents` probe and `readSession` read each `structuredClone` every event (~3 full deep clones per request for live sessions). Live sessions now borrow the store's frozen event snapshot directly (`snapshotEvents()` on 0.2.0 / the `events` property on 0.1.x — the cached, immutable array is reused): O(1) probe, O(delta) incremental reads, fully synchronous, zero copies.
|
|
96
101
|
- **Paged + trimmed responses**: at most `PAGE_SIZE` events per page (`hasMore` pulls the rest; first paint renders progressively); events project only the fields folding needs (text capped at `TEXT_CAP`, arguments at `ARGS_CAP`, `tool/result` keeps only the error field — the largest transfer/memory cost is gone).
|
|
97
102
|
- **Incremental client folding**: `TrailFolder.push()` consumes each event exactly once; `snapshot()` reuses the TrailTurn objects of already-sealed turns (stable identity → `React.memo` re-renders only open turns), turning each refresh from O(n) full refolds into O(delta); duplicate/re-sent pages are dropped by the seq watermark.
|
|
98
103
|
- **Rows keyed by seq**: appended steps reconcile in place in the DOM instead of shifting indexes and remounting whole chunks.
|
|
99
104
|
|
|
100
105
|
## Permissions & data
|
|
101
106
|
|
|
102
|
-
- **Session logs**: one read-only host route `POST /
|
|
107
|
+
- **Session logs**: one read-only host route `POST /turn-outline/api/events`, browser same-origin + Host-header trust fence; read-only, never writes or mutates the session.
|
|
103
108
|
- **Zero LLM calls**: nothing is sent to any model; no token consumption.
|
|
104
109
|
- **No credential access**: no other network/file access besides session events; no user config, no persistent storage (tab state follows better-sidebar's layout persistence).
|
|
105
110
|
|
|
@@ -107,17 +112,18 @@ client (browser) inject: ['sessions','betterSidebar'] (betterSidebar is an optio
|
|
|
107
112
|
|
|
108
113
|
| Item | Value |
|
|
109
114
|
|---|---|
|
|
110
|
-
| Platform | DSH 0.
|
|
111
|
-
|
|
|
115
|
+
| Platform (primary) | DSH 0.2.0-rc.x (tested against the DSH Desktop 2.0.17 bundled runtime; web/desktop profile) |
|
|
116
|
+
| Platform (fallback-compatible) | DSH 0.1.2-rc.1 / 0.1.1-rc.x (stable line) |
|
|
117
|
+
| Dependencies | dsh-better-sidebar (optional peer, `>=0.17.0`; inert when absent), react (injected by the DSH client seed) |
|
|
112
118
|
| Build | TypeScript + esbuild, no runtime npm dependencies |
|
|
113
|
-
| Tests | vitest,
|
|
119
|
+
| Tests | vitest, 100 cases (fold / folder / jump / tab / host) |
|
|
114
120
|
|
|
115
121
|
## Development
|
|
116
122
|
|
|
117
123
|
```sh
|
|
118
124
|
pnpm install # if pnpm 10+ blocks build scripts, allow esbuild as prompted (see pnpm-workspace.yaml)
|
|
119
125
|
pnpm typecheck # tsc --noEmit
|
|
120
|
-
pnpm test # vitest run (
|
|
126
|
+
pnpm test # vitest run (100 cases)
|
|
121
127
|
pnpm build # esbuild → lib/index.js + lib/client.js (includes bundle-contract self check)
|
|
122
128
|
pnpm pack # produces dsh-turn-outline-<version>.tgz (prepack rebuilds automatically)
|
|
123
129
|
```
|
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://github.com/Andor-Z/dsh-turn-outline/releases)
|
|
7
7
|
[](https://github.com/topics/dsh-plugin)
|
|
8
|
-
[](https://github.com/omdsh-dev/DSH-better-sidebar)
|
|
9
|
+
[](https://github.com/omdsh-dev/DSH-better-sidebar)
|
|
9
10
|
|
|
10
11
|
[English](README.en.md)
|
|
11
12
|
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
**解决什么问题**:
|
|
25
26
|
|
|
26
27
|
- **按用户轮次折叠轨迹** —— 会话被折叠成「输入 → 工具步骤 → 输出」的结构化轨迹,长会话一目了然;轮次内支持多级折叠与工具类型筛选 chips,只看你想看的部分。
|
|
27
|
-
- **一键跳回原位** —— 点击任意轮次 / 工具步骤,自动加载历史并滚动到聊天视图 /
|
|
28
|
+
- **一键跳回原位** —— 点击任意轮次 / 工具步骤,自动加载历史并滚动到聊天视图 / 轨迹视图的对应行;0.2.0 走 client `uiConversation` 会话视图注册表(`anchorSeq` 节点匹配 + `loadOlder` 翻页),0.1.x 回退旧 session 快照,不依赖 DOM 文本与哈希类名。
|
|
28
29
|
- **零 AI 依赖** —— 纯事件折叠,不调用 LLM:零 token、秒开、无失败态。
|
|
29
30
|
- **只读零侵入** —— 不改动 DSH 核心:host 一个只读路由 + client 一个 tab,注册即用、卸载干净。
|
|
30
31
|
- **增量性能** —— 会话订阅驱动刷新(无定时轮询);`TrailFolder` 只重折打开中的轮次(O(delta));seq-keyed 行追加不重挂。
|
|
@@ -37,16 +38,16 @@
|
|
|
37
38
|
|
|
38
39
|
### 方式一:npm 一行命令(推荐)
|
|
39
40
|
|
|
40
|
-
先装 better-sidebar
|
|
41
|
+
先装 better-sidebar,再装本插件:
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
|
-
npx @deepseek-ai/dsh plugin --profile desktop add dsh-better-sidebar
|
|
44
|
+
npx @deepseek-ai/dsh plugin --profile desktop add dsh-better-sidebar
|
|
44
45
|
npx @deepseek-ai/dsh plugin --profile desktop add dsh-turn-outline
|
|
45
46
|
```
|
|
46
47
|
|
|
47
48
|
> 使用 Web profile 时把 `--profile desktop` 换成 `--profile web`。
|
|
48
49
|
>
|
|
49
|
-
>
|
|
50
|
+
> ℹ️ **版本线说明**:**0.3.0 起以 DSH 0.2.0-rc.x 为主线**(DSH Desktop 2.0.17 内置运行时 + better-sidebar 0.24.1 实测联调;跳回走 `uiConversation` 会话视图注册表,host live 路径用 `snapshotEvents()`)。0.1.x 线继续回退兼容——**better-sidebar 0.18.0 + DSH 0.1.2-rc.1** 与 **better-sidebar 0.17.1 + DSH 0.1.1-rc.x(stable)**:旧宿主上插件自动走旧快照跳转与 `.events` live 路径,无需任何配置。注意 better-sidebar 0.18.0 仅支持 DSH 0.1.2-rc.1+:更旧的 stable 宿主(0.1.0-rc.8 ~ 0.1.1-rc.2)请固定安装 `dsh-better-sidebar@0.17.1`。
|
|
50
51
|
|
|
51
52
|
### 方式二:源码本地安装(开发 / 离线)
|
|
52
53
|
|
|
@@ -78,28 +79,32 @@ npx @deepseek-ai/dsh plugin --profile desktop remove dsh-turn-outline
|
|
|
78
79
|
|
|
79
80
|
```
|
|
80
81
|
host(Node) inject: ['webServer','sessionQuery','sessions']
|
|
81
|
-
POST /
|
|
82
|
-
→
|
|
82
|
+
POST /turn-outline/api/events { sessionId, afterSeq }
|
|
83
|
+
→ 活跃会话:借用 live store 冻结快照(0.2.0 经 snapshotEvents(),0.1.x 读 .events;
|
|
84
|
+
O(1) 探测 + O(delta) 投影,零深拷贝)
|
|
83
85
|
→ 冷会话:sessionQuery 持久化日志 + lastSeq 缓存(LRU + 字节上限)
|
|
84
86
|
→ 响应逐页(每页 PAGE_SIZE 事件,hasMore 语义)+ 字段裁剪(不发 tool 结果全文)
|
|
85
87
|
→ 200 { events, hasMore } / 400 / 403 / 404 / 405 / 413
|
|
86
88
|
|
|
87
|
-
client(浏览器) inject: ['sessions','betterSidebar']
|
|
88
|
-
→ registerTab({ id:'turn-outline', single:true })
|
|
89
|
-
→
|
|
90
|
-
|
|
89
|
+
client(浏览器) inject: ['sessions','uiConversation','betterSidebar'](后两者可选 peer)
|
|
90
|
+
→ registerTab({ id:'turn-outline', single:true, description })
|
|
91
|
+
→ 跳回:0.2.0 走 ctx.uiConversation 会话视图注册表(chat/trajectory target);
|
|
92
|
+
0.1.x 回退 session snapshot 的 chat.nodes / views 字段
|
|
93
|
+
→ 会话订阅驱动刷新(session.subscribe + 0.2.0 eventSource,无定时轮询)
|
|
94
|
+
→ 增量折叠(TrailFolder:只重折打开中的轮次)+ seq-keyed 行(追加不重挂)
|
|
95
|
+
+ 按轮次 React.memo + 三级折叠 + 工具筛选 chips
|
|
91
96
|
```
|
|
92
97
|
|
|
93
98
|
## 性能设计(v0.2.1 引入)
|
|
94
99
|
|
|
95
|
-
- **host 不再对整份日志做深拷贝**:旧的 `listEvents` 探测与 `readSession` 读取会对每个事件 `structuredClone`(活跃会话约 3 次全量深拷贝/请求)。现在活跃会话直接借用 store
|
|
100
|
+
- **host 不再对整份日志做深拷贝**:旧的 `listEvents` 探测与 `readSession` 读取会对每个事件 `structuredClone`(活跃会话约 3 次全量深拷贝/请求)。现在活跃会话直接借用 store 的冻结事件快照(0.2.0 的 `snapshotEvents()` / 0.1.x 的 `events` 属性,复用不变数组),探测 O(1)、取增量 O(delta),全程同步零拷贝。
|
|
96
101
|
- **响应逐页 + 裁剪**:每页最多 `PAGE_SIZE` 事件(`hasMore` 续拉,首载渐进渲染);事件只投影折叠所需字段(文本截断到 `TEXT_CAP`、arguments 到 `ARGS_CAP`、`tool/result` 只留 error 字段——最大的传输与内存占用被去掉)。
|
|
97
102
|
- **client 增量折叠**:`TrailFolder.push()` 每事件只消费一次,`snapshot()` 复用已封口轮次的 TrailTurn 对象(身份稳定 → `React.memo` 只重渲染打开中的轮次),每轮刷新从 O(n) 全量重折降为 O(delta);重复/重发的分页被 seq 水位线丢弃。
|
|
98
103
|
- **行 key 用 seq**:新步骤追加时行在其 DOM 原位 reconcile,不再索引平移导致整段卸载重挂。
|
|
99
104
|
|
|
100
105
|
## 权限与数据
|
|
101
106
|
|
|
102
|
-
- **会话日志**:host 只读路由 `POST /
|
|
107
|
+
- **会话日志**:host 只读路由 `POST /turn-outline/api/events`,浏览器同源 + Host 头信任围栏;只读不回写、不改动会话。
|
|
103
108
|
- **零 LLM 调用**:不向任何模型发送内容,无 token 消耗。
|
|
104
109
|
- **不读取凭据**:除会话事件外无其他网络 / 文件访问;无用户配置、无持久化存储(tab 状态随 better-sidebar 布局持久化)。
|
|
105
110
|
|
|
@@ -107,17 +112,18 @@ client(浏览器) inject: ['sessions','betterSidebar'](betterSidebar 可
|
|
|
107
112
|
|
|
108
113
|
| 项 | 值 |
|
|
109
114
|
|---|---|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
115
|
+
| 平台(主线) | DSH 0.2.0-rc.x(DSH Desktop 2.0.17 内置运行时实测;web/desktop profile) |
|
|
116
|
+
| 平台(回退兼容) | DSH 0.1.2-rc.1 / 0.1.1-rc.x(stable 线) |
|
|
117
|
+
| 依赖 | dsh-better-sidebar(可选 peer,`>=0.17.0`;未装则 inert)、react(DSH client 种子注入) |
|
|
112
118
|
| 构建 | TypeScript + esbuild,无运行时 npm 依赖 |
|
|
113
|
-
| 测试 | vitest
|
|
119
|
+
| 测试 | vitest 100 用例(fold / folder / jump / tab / host) |
|
|
114
120
|
|
|
115
121
|
## 开发
|
|
116
122
|
|
|
117
123
|
```sh
|
|
118
124
|
pnpm install # pnpm 10+ 拦截构建脚本时按提示允许 esbuild(见 pnpm-workspace.yaml)
|
|
119
125
|
pnpm typecheck # tsc --noEmit
|
|
120
|
-
pnpm test # vitest run(
|
|
126
|
+
pnpm test # vitest run(100 用例)
|
|
121
127
|
pnpm build # esbuild → lib/index.js + lib/client.js(含 bundle 契约自检)
|
|
122
128
|
pnpm pack # 产出 dsh-turn-outline-<version>.tgz(prepack 自动重新构建)
|
|
123
129
|
```
|
package/lib/client.js
CHANGED
|
@@ -1107,7 +1107,7 @@ function t(lang, key) {
|
|
|
1107
1107
|
var React = __toESM(require("react"), 1);
|
|
1108
1108
|
var MAX_PULL_PAGES = 64;
|
|
1109
1109
|
function fetchTrailEvents(sessionId, afterSeq, base) {
|
|
1110
|
-
const url = new URL("/
|
|
1110
|
+
const url = new URL("/turn-outline/api/events", base ?? window.location.origin);
|
|
1111
1111
|
return fetch(url.toString(), {
|
|
1112
1112
|
method: "POST",
|
|
1113
1113
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
@@ -1223,18 +1223,60 @@ function useTrail(sessionId, visible, fetchEvents, sessionsService) {
|
|
|
1223
1223
|
if (sessionId.length === 0 || !visible) return void 0;
|
|
1224
1224
|
void pull();
|
|
1225
1225
|
const binding = sessionsService?.binding(sessionId);
|
|
1226
|
+
const disposers = [];
|
|
1226
1227
|
const session = binding !== void 0 && binding.session !== void 0 ? binding.session : void 0;
|
|
1227
|
-
if (session
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1228
|
+
if (session !== void 0 && typeof session.subscribe === "function") {
|
|
1229
|
+
disposers.push(session.subscribe(() => {
|
|
1230
|
+
schedulePull();
|
|
1231
|
+
}));
|
|
1232
|
+
}
|
|
1233
|
+
const eventSource = binding !== void 0 ? binding.eventSource : void 0;
|
|
1234
|
+
if (eventSource !== void 0 && typeof eventSource.subscribe === "function") {
|
|
1235
|
+
try {
|
|
1236
|
+
disposers.push(eventSource.subscribe(() => {
|
|
1237
|
+
schedulePull();
|
|
1238
|
+
}));
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (disposers.length === 0) return void 0;
|
|
1243
|
+
return () => {
|
|
1244
|
+
for (const dispose of disposers) dispose();
|
|
1245
|
+
};
|
|
1231
1246
|
}, [sessionId, visible, pull, schedulePull, sessionsService]);
|
|
1232
1247
|
return { turns, stats, status, error, refresh };
|
|
1233
1248
|
}
|
|
1234
1249
|
|
|
1235
1250
|
// src/client/trail-jump.ts
|
|
1236
|
-
function
|
|
1237
|
-
|
|
1251
|
+
function targetSnapshot(conversation, name) {
|
|
1252
|
+
try {
|
|
1253
|
+
const target = conversation.target(name);
|
|
1254
|
+
return target !== void 0 ? target.getSnapshot() : void 0;
|
|
1255
|
+
} catch {
|
|
1256
|
+
return void 0;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function viewStateForJump(session, conversation) {
|
|
1260
|
+
const snapshot = session.getSnapshot();
|
|
1261
|
+
let chatNodes;
|
|
1262
|
+
let trajectoryView;
|
|
1263
|
+
if (conversation !== void 0) {
|
|
1264
|
+
const chat = targetSnapshot(conversation, "chat");
|
|
1265
|
+
if (chat !== null && typeof chat === "object" && chat.nodes !== void 0 && chat.nodes !== null) {
|
|
1266
|
+
chatNodes = chat.nodes;
|
|
1267
|
+
}
|
|
1268
|
+
const trajectory = targetSnapshot(conversation, "trajectory");
|
|
1269
|
+
if (trajectory !== null && typeof trajectory === "object") trajectoryView = trajectory;
|
|
1270
|
+
}
|
|
1271
|
+
if (chatNodes === void 0) chatNodes = snapshot.chat?.nodes;
|
|
1272
|
+
if (trajectoryView === void 0 && snapshot.views !== void 0 && typeof snapshot.views.get === "function") {
|
|
1273
|
+
const legacy = snapshot.views.get("trajectory");
|
|
1274
|
+
if (legacy !== null && typeof legacy === "object") trajectoryView = legacy;
|
|
1275
|
+
}
|
|
1276
|
+
return { hasMore: snapshot.hasMore === true, chatNodes, trajectoryView };
|
|
1277
|
+
}
|
|
1278
|
+
function hasSeqInWindow(view, seq) {
|
|
1279
|
+
const store = view.chatNodes;
|
|
1238
1280
|
if (store === void 0 || typeof store.values !== "function") return false;
|
|
1239
1281
|
let min = Number.POSITIVE_INFINITY;
|
|
1240
1282
|
let max = Number.NEGATIVE_INFINITY;
|
|
@@ -1245,8 +1287,8 @@ function hasSeqInWindow(snapshot, seq) {
|
|
|
1245
1287
|
}
|
|
1246
1288
|
return min <= seq && seq <= max;
|
|
1247
1289
|
}
|
|
1248
|
-
function findNodeForSeq(
|
|
1249
|
-
const store =
|
|
1290
|
+
function findNodeForSeq(view, seq) {
|
|
1291
|
+
const store = view.chatNodes;
|
|
1250
1292
|
if (store === void 0 || typeof store.values !== "function") return null;
|
|
1251
1293
|
let exact = null;
|
|
1252
1294
|
let next = null;
|
|
@@ -1267,12 +1309,9 @@ function findNodeForSeq(snapshot, seq) {
|
|
|
1267
1309
|
function isTrajectoryViewActive(document2) {
|
|
1268
1310
|
return document2.querySelector("[data-trajectory-scroll]") !== null;
|
|
1269
1311
|
}
|
|
1270
|
-
function
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
const trajectory = views.get("trajectory");
|
|
1274
|
-
if (trajectory === null || typeof trajectory !== "object") return null;
|
|
1275
|
-
const view = trajectory;
|
|
1312
|
+
function trajectoryRecordIdsFor(item, trajectoryView) {
|
|
1313
|
+
if (trajectoryView === null || typeof trajectoryView !== "object") return [];
|
|
1314
|
+
const view = trajectoryView;
|
|
1276
1315
|
const nodes = Array.isArray(view.eventNodes) ? view.eventNodes : [];
|
|
1277
1316
|
const requests = Array.isArray(view.requests) ? view.requests : [];
|
|
1278
1317
|
const nodeWithKind = (kinds, seq) => {
|
|
@@ -1281,20 +1320,25 @@ function trajectoryRecordIdFor(item, snapshot) {
|
|
|
1281
1320
|
};
|
|
1282
1321
|
if (item.kind === "user" || item.kind === "context" || item.kind === "input") {
|
|
1283
1322
|
const target = nodeWithKind(["user", "steering", "context"], item.seq);
|
|
1284
|
-
if (target === void 0 || typeof target.seq !== "number") return
|
|
1323
|
+
if (target === void 0 || typeof target.seq !== "number") return [];
|
|
1285
1324
|
const rowKind = target.kind === "steering" ? "user" : target.kind;
|
|
1286
|
-
return `${rowKind}\0seq\0${target.seq}
|
|
1325
|
+
return [`${rowKind}\0seq\0${target.seq}`];
|
|
1287
1326
|
}
|
|
1288
1327
|
if (item.kind === "assistant" || item.kind === "output") {
|
|
1289
1328
|
const target = nodeWithKind(["assistant"], item.seq);
|
|
1290
|
-
if (target === void 0 || typeof target.
|
|
1291
|
-
|
|
1329
|
+
if (target === void 0 || typeof target.seq !== "number") return [];
|
|
1330
|
+
const ids = [];
|
|
1331
|
+
if (typeof target.turn === "number" && typeof target.step === "number") {
|
|
1332
|
+
ids.push(`assistant\0${target.turn}\0${target.step}`);
|
|
1333
|
+
}
|
|
1334
|
+
ids.push(`assistant\0seq\0${target.seq}`);
|
|
1335
|
+
return ids;
|
|
1292
1336
|
}
|
|
1293
1337
|
if (item.kind === "tool" || item.kind === "error") {
|
|
1294
|
-
if (typeof item.callId === "string" && item.callId.length > 0) return `tool\0call\0${item.callId}
|
|
1338
|
+
if (typeof item.callId === "string" && item.callId.length > 0) return [`tool\0call\0${item.callId}`];
|
|
1295
1339
|
const target = nodeWithKind(["tool-result"], item.seq);
|
|
1296
|
-
if (target === void 0 || typeof target.callId !== "string") return
|
|
1297
|
-
return `tool\0call\0${target.callId}
|
|
1340
|
+
if (target === void 0 || typeof target.callId !== "string") return [];
|
|
1341
|
+
return [`tool\0call\0${target.callId}`];
|
|
1298
1342
|
}
|
|
1299
1343
|
if (item.kind === "compaction") {
|
|
1300
1344
|
const compactions = [];
|
|
@@ -1306,18 +1350,21 @@ function trajectoryRecordIdFor(item, snapshot) {
|
|
|
1306
1350
|
}
|
|
1307
1351
|
}
|
|
1308
1352
|
const target = compactions.find((request) => request.resultSeq === item.seq) ?? compactions.filter((request) => (request.resultSeq ?? -1) >= item.seq).sort((a, b) => (a.resultSeq ?? -1) - (b.resultSeq ?? -1))[0];
|
|
1309
|
-
if (target === void 0) return
|
|
1310
|
-
return `compacted\0seq\0${target.startSeq}
|
|
1353
|
+
if (target === void 0) return [];
|
|
1354
|
+
return [`compacted\0seq\0${target.startSeq}`];
|
|
1311
1355
|
}
|
|
1312
|
-
return
|
|
1313
|
-
}
|
|
1314
|
-
function
|
|
1315
|
-
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1356
|
+
return [];
|
|
1357
|
+
}
|
|
1358
|
+
function findTrajectoryRowByKeys(document2, keys) {
|
|
1359
|
+
if (keys.length === 0) return null;
|
|
1360
|
+
for (const key of keys) {
|
|
1361
|
+
const encoded = encodeURIComponent(key);
|
|
1362
|
+
const rows = document2.querySelectorAll("tr[data-trajectory-row-key]");
|
|
1363
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
1364
|
+
const row = rows[index];
|
|
1365
|
+
const value = row.dataset !== void 0 ? row.dataset.trajectoryRowKey : void 0;
|
|
1366
|
+
if (value === encoded || value === key) return row;
|
|
1367
|
+
}
|
|
1321
1368
|
}
|
|
1322
1369
|
return null;
|
|
1323
1370
|
}
|
|
@@ -1353,30 +1400,70 @@ function waitForNextFrame() {
|
|
|
1353
1400
|
}
|
|
1354
1401
|
});
|
|
1355
1402
|
}
|
|
1403
|
+
function findChatRow(document2, key) {
|
|
1404
|
+
const exact = document2.querySelector(`[data-chat-anchor-key="${cssEscape(key)}"]`);
|
|
1405
|
+
if (exact !== null) return exact;
|
|
1406
|
+
const rows = document2.querySelectorAll("[data-chat-node-key],[data-chat-anchor-key]");
|
|
1407
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
1408
|
+
const row = rows[index];
|
|
1409
|
+
if (row.dataset !== void 0 && (row.dataset.chatNodeKey === key || row.dataset.chatAnchorKey === key)) return row;
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
function isHiddenInTree(row) {
|
|
1414
|
+
let el = row;
|
|
1415
|
+
while (el !== null && el !== void 0) {
|
|
1416
|
+
if (typeof el.hasAttribute === "function" && el.hasAttribute("hidden")) return true;
|
|
1417
|
+
el = el.parentElement;
|
|
1418
|
+
}
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
function dispatchBeforematchUp(row) {
|
|
1422
|
+
let el = row;
|
|
1423
|
+
while (el !== null && el !== void 0) {
|
|
1424
|
+
if (typeof el.hasAttribute === "function" && el.hasAttribute("hidden") && typeof el.dispatchEvent === "function" && typeof Event === "function") {
|
|
1425
|
+
try {
|
|
1426
|
+
el.dispatchEvent(new Event("beforematch"));
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
el = el.parentElement;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
async function revealRowUntilVisible(row, frames = 10) {
|
|
1434
|
+
if (!isHiddenInTree(row)) return true;
|
|
1435
|
+
for (let index = 0; index < frames; index += 1) {
|
|
1436
|
+
dispatchBeforematchUp(row);
|
|
1437
|
+
await waitForNextFrame();
|
|
1438
|
+
if (!isHiddenInTree(row)) return true;
|
|
1439
|
+
}
|
|
1440
|
+
return !isHiddenInTree(row);
|
|
1441
|
+
}
|
|
1356
1442
|
async function jumpToItem(session, item, options) {
|
|
1357
1443
|
const guard = Number.isFinite(Number(options.guard)) && (options.guard ?? 0) > 0 ? Math.min(500, Math.max(1, Math.floor(options.guard ?? 200))) : 200;
|
|
1358
1444
|
const document2 = options.document;
|
|
1359
1445
|
try {
|
|
1360
|
-
let
|
|
1446
|
+
let view = viewStateForJump(session, options.conversation);
|
|
1361
1447
|
const trajectoryActive = isTrajectoryViewActive(document2);
|
|
1362
1448
|
if (trajectoryActive) {
|
|
1363
|
-
let
|
|
1364
|
-
let row2 =
|
|
1449
|
+
let keys = trajectoryRecordIdsFor(item, view.trajectoryView);
|
|
1450
|
+
let row2 = findTrajectoryRowByKeys(document2, keys);
|
|
1365
1451
|
let pages2 = 0;
|
|
1366
|
-
if (row2 === null &&
|
|
1367
|
-
while (
|
|
1452
|
+
if (row2 === null && view.hasMore) {
|
|
1453
|
+
while (view.hasMore && pages2 < guard) {
|
|
1368
1454
|
await session.loadOlder();
|
|
1369
|
-
|
|
1455
|
+
view = viewStateForJump(session, options.conversation);
|
|
1370
1456
|
pages2 += 1;
|
|
1371
1457
|
options.onProgress?.(pages2);
|
|
1372
1458
|
}
|
|
1373
|
-
|
|
1374
|
-
row2 =
|
|
1459
|
+
keys = trajectoryRecordIdsFor(item, view.trajectoryView);
|
|
1460
|
+
row2 = findTrajectoryRowByKeys(document2, keys);
|
|
1375
1461
|
}
|
|
1376
1462
|
if (row2 === null) {
|
|
1377
1463
|
expandCollapsedSummaries(document2);
|
|
1378
1464
|
await waitForNextFrame();
|
|
1379
|
-
|
|
1465
|
+
keys = trajectoryRecordIdsFor(item, view.trajectoryView);
|
|
1466
|
+
row2 = findTrajectoryRowByKeys(document2, keys);
|
|
1380
1467
|
}
|
|
1381
1468
|
if (row2 === null) {
|
|
1382
1469
|
row2 = findTrajectoryRowBySeq(document2, item);
|
|
@@ -1386,23 +1473,36 @@ async function jumpToItem(session, item, options) {
|
|
|
1386
1473
|
return { ok: true, mode: "trajectory" };
|
|
1387
1474
|
}
|
|
1388
1475
|
let pages = 0;
|
|
1389
|
-
|
|
1476
|
+
if (!hasSeqInWindow(view, item.seq) && typeof session.loadThrough === "function") {
|
|
1477
|
+
await session.loadThrough(item.seq);
|
|
1478
|
+
await waitForNextFrame();
|
|
1479
|
+
await waitForNextFrame();
|
|
1480
|
+
view = viewStateForJump(session, options.conversation);
|
|
1481
|
+
}
|
|
1482
|
+
while (view.hasMore && !hasSeqInWindow(view, item.seq) && pages < guard) {
|
|
1390
1483
|
await session.loadOlder();
|
|
1391
|
-
|
|
1484
|
+
view = viewStateForJump(session, options.conversation);
|
|
1392
1485
|
pages += 1;
|
|
1393
1486
|
options.onProgress?.(pages);
|
|
1394
1487
|
}
|
|
1395
|
-
|
|
1488
|
+
let node = findNodeForSeq(view, item.seq);
|
|
1489
|
+
let nodeRetries = 0;
|
|
1490
|
+
while (node === null && nodeRetries < 6) {
|
|
1491
|
+
await waitForNextFrame();
|
|
1492
|
+
view = viewStateForJump(session, options.conversation);
|
|
1493
|
+
node = findNodeForSeq(view, item.seq);
|
|
1494
|
+
nodeRetries += 1;
|
|
1495
|
+
}
|
|
1396
1496
|
if (node === null) return { ok: false, error: "seq-not-rendered" };
|
|
1397
|
-
|
|
1398
|
-
let row = document2.querySelector(selector);
|
|
1497
|
+
let row = findChatRow(document2, node.key);
|
|
1399
1498
|
let retries = 0;
|
|
1400
1499
|
while (row === null && retries < 6) {
|
|
1401
1500
|
await waitForNextFrame();
|
|
1402
|
-
row = document2.
|
|
1501
|
+
row = findChatRow(document2, node.key);
|
|
1403
1502
|
retries += 1;
|
|
1404
1503
|
}
|
|
1405
1504
|
if (row === null) return { ok: false, error: "row-not-in-chat-view" };
|
|
1505
|
+
if (!await revealRowUntilVisible(row)) return { ok: false, error: "row-hidden" };
|
|
1406
1506
|
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1407
1507
|
if (row.classList !== void 0 && typeof row.classList.add === "function") {
|
|
1408
1508
|
row.classList.add("ttl-flash");
|
|
@@ -1423,6 +1523,17 @@ function sessionsOf(ctx) {
|
|
|
1423
1523
|
const service = ctx.get("sessions");
|
|
1424
1524
|
return service !== void 0 && service !== null && typeof service.binding === "function" ? service : void 0;
|
|
1425
1525
|
}
|
|
1526
|
+
function conversationOf(ctx, sessionId) {
|
|
1527
|
+
if (ctx === void 0 || typeof ctx.get !== "function") return void 0;
|
|
1528
|
+
const service = ctx.get("uiConversation");
|
|
1529
|
+
if (service === void 0 || service === null || typeof service.binding !== "function") return void 0;
|
|
1530
|
+
try {
|
|
1531
|
+
const targets = service.binding(sessionId);
|
|
1532
|
+
return targets !== void 0 && targets !== null && typeof targets.target === "function" ? targets : void 0;
|
|
1533
|
+
} catch {
|
|
1534
|
+
return void 0;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1426
1537
|
function turnByIdentity(turn) {
|
|
1427
1538
|
return turn.turn !== null ? `turn:${turn.turn}` : `input:${turn.anchorSeq}`;
|
|
1428
1539
|
}
|
|
@@ -1701,6 +1812,10 @@ function TrailTab(props) {
|
|
|
1701
1812
|
if (typeof face.loadOlder !== "function" || typeof face.getSnapshot !== "function") return null;
|
|
1702
1813
|
return session;
|
|
1703
1814
|
}, [sessionsService, sessionId]);
|
|
1815
|
+
const conversationForJump = React2.useMemo(
|
|
1816
|
+
() => conversationOf(props.ctx, sessionId),
|
|
1817
|
+
[props.ctx, sessionId]
|
|
1818
|
+
);
|
|
1704
1819
|
const doJump = React2.useCallback(async (target, turn) => {
|
|
1705
1820
|
if (jumpingRef.current) return;
|
|
1706
1821
|
setActiveTurnId(turnByIdentity(turn));
|
|
@@ -1714,7 +1829,8 @@ function TrailTab(props) {
|
|
|
1714
1829
|
setJumpMessage(null);
|
|
1715
1830
|
const outcome = await jumpToItem(sessionForJump, target, {
|
|
1716
1831
|
document: window.document,
|
|
1717
|
-
onProgress: (pages) => setJumpPages(pages)
|
|
1832
|
+
onProgress: (pages) => setJumpPages(pages),
|
|
1833
|
+
...conversationForJump !== void 0 ? { conversation: conversationForJump } : {}
|
|
1718
1834
|
});
|
|
1719
1835
|
jumpingRef.current = false;
|
|
1720
1836
|
setJumping(null);
|
|
@@ -1723,7 +1839,7 @@ function TrailTab(props) {
|
|
|
1723
1839
|
} else {
|
|
1724
1840
|
setJumpMessage(t(lang, "jumpFailed"));
|
|
1725
1841
|
}
|
|
1726
|
-
}, [sessionForJump, lang]);
|
|
1842
|
+
}, [sessionForJump, conversationForJump, lang]);
|
|
1727
1843
|
const stepMatches = (step) => {
|
|
1728
1844
|
if (filter === "all") return true;
|
|
1729
1845
|
if (filter === "failed") return step.kind === "error";
|
|
@@ -1849,7 +1965,11 @@ function routeIcon(size) {
|
|
|
1849
1965
|
children
|
|
1850
1966
|
);
|
|
1851
1967
|
}
|
|
1852
|
-
var inject = ["sessions", "betterSidebar"];
|
|
1968
|
+
var inject = ["sessions", "uiConversation", "betterSidebar"];
|
|
1969
|
+
function tabDescription() {
|
|
1970
|
+
if (typeof navigator === "undefined") return "Turn outline";
|
|
1971
|
+
return navigator.language.toLowerCase().startsWith("zh") ? "\u6309\u7528\u6237\u8F6E\u6B21\u6298\u53E0\u4F1A\u8BDD\u8F68\u8FF9\uFF0C\u4E00\u952E\u5B9A\u4F4D\u56DE\u5BF9\u8BDD\u539F\u4F4D" : "Fold the session into per-turn steps and jump back to any of them";
|
|
1972
|
+
}
|
|
1853
1973
|
function apply(ctx) {
|
|
1854
1974
|
const style = document.createElement("style");
|
|
1855
1975
|
style.dataset.plugin = "dsh-turn-outline";
|
|
@@ -1863,6 +1983,7 @@ function apply(ctx) {
|
|
|
1863
1983
|
ctx.effect(() => betterSidebar.registerTab({
|
|
1864
1984
|
id: "turn-outline",
|
|
1865
1985
|
title: () => typeof navigator !== "undefined" && navigator.language.toLowerCase().startsWith("zh") ? "\u8F6E\u6B21\u8F68\u8FF9" : "Turn Outline",
|
|
1986
|
+
description: tabDescription,
|
|
1866
1987
|
icon: (size) => routeIcon(size),
|
|
1867
1988
|
order: 46,
|
|
1868
1989
|
single: true,
|