dsh-milestone 0.1.0 → 0.2.0
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/LICENSE +21 -21
- package/README.md +90 -72
- package/lib/client.js +220 -91
- package/package.json +61 -61
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 dsh-milestone contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-milestone contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,72 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
dsh
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🧭 dsh-milestone
|
|
4
|
+
|
|
5
|
+
**DeepSeek Harness 的会话里程碑导航条**
|
|
6
|
+
|
|
7
|
+
像 Git 提交图一样,一眼定位每一次提问,一键跳转到任何位置。
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://www.npmjs.com/package/dsh-milestone"><img src="https://img.shields.io/npm/v/dsh-milestone?color=2563eb" alt="npm version"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/dsh-milestone"><img src="https://img.shields.io/npm/dm/dsh-milestone" alt="npm downloads"></a>
|
|
12
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/dsh-milestone" alt="license"></a>
|
|
13
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/topic-dsh--plugin-2563eb" alt="dsh-plugin"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 为什么需要它?
|
|
21
|
+
|
|
22
|
+
和 AI 聊了上百轮之后,想找回**第 17 轮那个提问**?你只能不停滚轮往上翻,眼睛在一堆代码块、工具调用和思考过程里大海捞针。
|
|
23
|
+
|
|
24
|
+
**dsh-milestone** 在会话右侧挂一条**圆点时间线**——每一条提问对应一个圆点,鼠标悬停看内容,点击瞬间跳转。长对话的"导航地图"。
|
|
25
|
+
|
|
26
|
+
<img src="./assets/demo.svg" alt="dsh-milestone 效果示意图" width="100%">
|
|
27
|
+
|
|
28
|
+
## 好用在哪?
|
|
29
|
+
|
|
30
|
+
- ⚡ **一键定位** —— 点击任意圆点,平滑滚动到那条消息,不用再手动翻几百行。
|
|
31
|
+
- 📏 **固定间距** —— 圆点**等距排列**,不随对话长度挤压变形,永远点得准。
|
|
32
|
+
- 🎨 **蓝色渐变** —— 最新最深、最早最浅,一眼看清提问的先后顺序,像 Git 提交图。
|
|
33
|
+
- 🖱️ **滚轮滑动** —— 长会话圆点超出可视区时,鼠标在里程碑条上滚轮即可滑动选点。
|
|
34
|
+
- 💬 **丰富悬停** —— 悬停展示消息预览、相对时间、第 N 轮、用时、结束原因、首字延迟(TTFT)、tokens/秒。
|
|
35
|
+
- 🪶 **零侵入** —— 官方 slot 机制挂载,不修改 harness 源码,装完即用。
|
|
36
|
+
|
|
37
|
+
## 悬停能看到什么
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
┌─────────────────────────────────────────┐
|
|
41
|
+
│ 第 3 / 5 条 · 第 2 轮 │ ← 序号 + 轮次
|
|
42
|
+
│ 帮我优化这段代码的性能 │ ← 消息预览(前 80 字)
|
|
43
|
+
│ 5 分钟前 · 用时 1m30s · 首字 1.2s · 12.4 tok/s │ ← 时间 · 耗时 · TTFT · 吞吐
|
|
44
|
+
└─────────────────────────────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
元信息全部来自 harness 原生的 session 快照(`turnTimings` / `timeline.turns` / `turn-tail`),无额外依赖。
|
|
48
|
+
|
|
49
|
+
## 快速开始
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
# 安装插件到某个 profile
|
|
53
|
+
dsh plugin --profile demo add dsh-milestone
|
|
54
|
+
|
|
55
|
+
# 启动 Web UI
|
|
56
|
+
npx @deepseek-ai/dsh web # → http://127.0.0.1:3080
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
打开一个**多轮对话**(至少 2 条提问),会话视图右侧就会出现里程碑条。
|
|
60
|
+
|
|
61
|
+
> 要求 Node.js `>= 24`(harness 官方要求)。
|
|
62
|
+
|
|
63
|
+
## 它是什么做的?
|
|
64
|
+
|
|
65
|
+
双半边浏览器插件(空 node half + `shell.overlay` slot 挂载的 client half):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
shell.overlay (root scope)
|
|
69
|
+
└─ milestone.rail (session scope, 自声明子槽)
|
|
70
|
+
└─ useSession 读取会话快照 → 圆点列表 + 悬停 + 跳转
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **注入点**:`shell.overlay` —— 全框架浮动层,附加式、点击穿透,不影响任何现有 UI。
|
|
74
|
+
- **数据源**:`chat.order` + `chat.nodes`(user 消息)+ `chat.timeline`(turn 元数据)。
|
|
75
|
+
- **跳转**:DOM 锚点 `data-chat-anchor-key`,`scrollIntoView` 平滑定位。
|
|
76
|
+
|
|
77
|
+
## 已知限制
|
|
78
|
+
|
|
79
|
+
- 仅覆盖当前已加载的消息窗口(初始 50 条,向上滚动分页会自动补圆点)。
|
|
80
|
+
- TTFT / tokens/秒 依赖 turn 位置数据,窗口外或未完成的 turn 不显示(自动隐藏)。
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
[MIT](./LICENSE)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
<p align="center">
|
|
89
|
+
觉得好用?给个 ⭐ 支持一下,或把它推荐给正在 DeepSeek Harness 里挣扎的开发者吧。
|
|
90
|
+
</p>
|
package/lib/client.js
CHANGED
|
@@ -20,110 +20,172 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/client/MilestoneRail.tsx
|
|
22
22
|
/**
|
|
23
|
-
* MilestoneRail: the milestone.rail entry (session scope).
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* MilestoneRail: the milestone.rail entry (session scope). Renders a fixed
|
|
24
|
+
* right-side vertical scrubber as a **fixed-pitch dot list** (like a git commit
|
|
25
|
+
* graph), NOT a minimap: one dot per user message, equal spacing regardless of
|
|
26
|
+
* conversation length. The list itself scrolls with the wheel when it outgrows
|
|
27
|
+
* the viewport; hovering a dot shows rich metadata (time, turn, duration, end
|
|
28
|
+
* reason, TTFT, tokens/sec) and clicking jumps the chat to that message.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
30
|
+
* Data sources (all from the session-scoped `useSession` snapshot):
|
|
31
|
+
* - chat.order + chat.nodes.get(key) -> user-message nodes (key/id/location)
|
|
32
|
+
* - chat.timeline.turns.get(turn) -> turn start/end time, status, reason,
|
|
33
|
+
* and the ui-conversation 'turn-tail'
|
|
34
|
+
* location data (ttftMs/tokensPerSecond)
|
|
35
|
+
*
|
|
36
|
+
* Positioning: the rail hugs the conversation scrollport's right edge (offset a
|
|
37
|
+
* little inward so it clears the native scrollbar and sits near the prose).
|
|
35
38
|
*/
|
|
39
|
+
/** Minimum user messages before the rail adds value. */
|
|
40
|
+
const MIN_MARKS = 2;
|
|
36
41
|
const PREVIEW_LENGTH = 80;
|
|
37
|
-
/**
|
|
38
|
-
const
|
|
42
|
+
/** Visual dot diameter (px). */
|
|
43
|
+
const DOT_SIZE = 12;
|
|
44
|
+
/** Hit area per dot (px) — larger than the dot for comfortable clicking. */
|
|
45
|
+
const DOT_HIT = 22;
|
|
46
|
+
/** Vertical gap between dot hit areas (px) — fixed pitch, never scaled. */
|
|
47
|
+
const DOT_GAP = 12;
|
|
48
|
+
/** Inward offset from the scrollport right edge so the rail clears the scrollbar. */
|
|
49
|
+
const RAIL_INSET = 14;
|
|
39
50
|
/**
|
|
40
51
|
* Find a chat row by its node key, avoiding CSS.escape pitfalls on keys that
|
|
41
|
-
* contain `<`/`>`/`:` (the node key is `
|
|
42
|
-
* Mirrors the harness ChatView's `anchorElement` scan.
|
|
52
|
+
* contain `<`/`>`/`:` (the node key is `13:input-message<messageId>`).
|
|
43
53
|
*/
|
|
44
54
|
function findRow(key) {
|
|
45
55
|
for (const row of document.querySelectorAll("[data-chat-anchor-key]")) if (row.dataset.chatAnchorKey === key) return row;
|
|
46
56
|
return null;
|
|
47
57
|
}
|
|
58
|
+
/** Extract a plain-text preview from a user message's ContentBlock[] payload. */
|
|
59
|
+
function extractPreview(content) {
|
|
60
|
+
if (!Array.isArray(content)) return "";
|
|
61
|
+
let text = "";
|
|
62
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
|
|
63
|
+
const t = block.text;
|
|
64
|
+
if (typeof t === "string") text += (text === "" ? "" : " ") + t;
|
|
65
|
+
}
|
|
66
|
+
return text.trim().slice(0, PREVIEW_LENGTH);
|
|
67
|
+
}
|
|
68
|
+
/** Blue gradient: newest (last) dots are deepest, oldest are lightest. */
|
|
69
|
+
function dotColor(index, total) {
|
|
70
|
+
return `hsl(218, 88%, ${72 - (total <= 1 ? 0 : index / (total - 1)) * 27}%)`;
|
|
71
|
+
}
|
|
72
|
+
/** Relative wall-clock label for a Unix-epoch-ms timestamp. */
|
|
73
|
+
function formatRelativeTime(time) {
|
|
74
|
+
const diff = Date.now() - time;
|
|
75
|
+
if (diff < 6e4) return "刚刚";
|
|
76
|
+
if (diff < 36e5) return `${Math.floor(diff / 6e4)} 分钟前`;
|
|
77
|
+
if (diff < 864e5) return `${Math.floor(diff / 36e5)} 小时前`;
|
|
78
|
+
return `${Math.floor(diff / 864e5)} 天前`;
|
|
79
|
+
}
|
|
80
|
+
/** Compact duration label (ms). */
|
|
81
|
+
function formatDuration(ms) {
|
|
82
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
83
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
84
|
+
return `${Math.floor(ms / 6e4)}m${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
85
|
+
}
|
|
86
|
+
/** Human label for a TurnEndReason kind. */
|
|
87
|
+
function reasonLabelOf(kind) {
|
|
88
|
+
switch (kind) {
|
|
89
|
+
case "completed": return "已完成";
|
|
90
|
+
case "aborted": return "已中止";
|
|
91
|
+
case "error": return "出错";
|
|
92
|
+
case "max-tokens": return "达到上限";
|
|
93
|
+
case "interrupted": return "已中断";
|
|
94
|
+
case "blocked": return "已阻塞";
|
|
95
|
+
default: return kind;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Read the ui-conversation 'turn-tail' location data (ttftMs/tokensPerSecond). */
|
|
99
|
+
function turnTailOf(turn) {
|
|
100
|
+
const data = turn.data;
|
|
101
|
+
if (data?.get === void 0) return void 0;
|
|
102
|
+
return data.get("turn-tail");
|
|
103
|
+
}
|
|
48
104
|
/**
|
|
49
105
|
* @param props - session standard kit (useSession, sessionId, useProjection).
|
|
50
106
|
*/
|
|
51
107
|
function MilestoneRail({ useSession }) {
|
|
52
108
|
const order = useSession((s) => s.chat.order);
|
|
53
109
|
const nodes = useSession((s) => s.chat.nodes);
|
|
54
|
-
const
|
|
55
|
-
const
|
|
110
|
+
const timeline = useSession((s) => s.chat.timeline);
|
|
111
|
+
const marks = (0, react.useMemo)(() => {
|
|
112
|
+
const result = [];
|
|
113
|
+
for (const key of order) {
|
|
114
|
+
const node = nodes.get(key);
|
|
115
|
+
if (node === void 0 || node.kind !== "user") continue;
|
|
116
|
+
const data = node.data;
|
|
117
|
+
const turn = node.location.kind === "turn" || node.location.kind === "step" ? node.location.turn.turn : void 0;
|
|
118
|
+
result.push({
|
|
119
|
+
key,
|
|
120
|
+
turn,
|
|
121
|
+
seq: data.seq ?? 0,
|
|
122
|
+
time: data.time ?? 0,
|
|
123
|
+
preview: extractPreview(data.content)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}, [order, nodes]);
|
|
56
128
|
const [railBox, setRailBox] = (0, react.useState)(null);
|
|
57
129
|
const [hover, setHover] = (0, react.useState)(null);
|
|
58
130
|
(0, react.useLayoutEffect)(() => {
|
|
59
|
-
if (
|
|
60
|
-
setTicks([]);
|
|
131
|
+
if (marks.length < MIN_MARKS) {
|
|
61
132
|
setRailBox(null);
|
|
62
133
|
return;
|
|
63
134
|
}
|
|
64
135
|
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
65
136
|
if (scrollport === null) return;
|
|
66
|
-
const rowCache = /* @__PURE__ */ new Map();
|
|
67
|
-
for (const key of userKeys) {
|
|
68
|
-
const row = findRow(key);
|
|
69
|
-
if (row !== null) rowCache.set(key, row);
|
|
70
|
-
}
|
|
71
137
|
const compute = () => {
|
|
72
138
|
const sp = scrollport.getBoundingClientRect();
|
|
73
|
-
const total = scrollport.scrollHeight;
|
|
74
|
-
const next = [];
|
|
75
|
-
for (const key of userKeys) {
|
|
76
|
-
let row = rowCache.get(key);
|
|
77
|
-
if (row === void 0 || !row.isConnected) {
|
|
78
|
-
const fresh = findRow(key);
|
|
79
|
-
if (fresh === null) continue;
|
|
80
|
-
row = fresh;
|
|
81
|
-
rowCache.set(key, fresh);
|
|
82
|
-
}
|
|
83
|
-
const contentTop = row.getBoundingClientRect().top - sp.top + scrollport.scrollTop;
|
|
84
|
-
const top = total > 0 ? contentTop / total * 100 : 0;
|
|
85
|
-
next.push({
|
|
86
|
-
key,
|
|
87
|
-
top: Math.min(99, Math.max(0, top))
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
setTicks(next);
|
|
91
139
|
setRailBox({
|
|
92
140
|
top: sp.top,
|
|
93
141
|
height: sp.height,
|
|
94
|
-
right: Math.max(
|
|
142
|
+
right: Math.max(8, window.innerWidth - sp.right + RAIL_INSET)
|
|
95
143
|
});
|
|
96
144
|
};
|
|
97
145
|
compute();
|
|
98
|
-
|
|
99
|
-
const schedule = () => {
|
|
100
|
-
cancelAnimationFrame(raf);
|
|
101
|
-
raf = requestAnimationFrame(compute);
|
|
102
|
-
};
|
|
103
|
-
scrollport.addEventListener("scroll", schedule, { passive: true });
|
|
104
|
-
const observer = new ResizeObserver(schedule);
|
|
146
|
+
const observer = new ResizeObserver(compute);
|
|
105
147
|
observer.observe(scrollport);
|
|
106
|
-
|
|
148
|
+
window.addEventListener("resize", compute);
|
|
107
149
|
return () => {
|
|
108
|
-
cancelAnimationFrame(raf);
|
|
109
|
-
scrollport.removeEventListener("scroll", schedule);
|
|
110
150
|
observer.disconnect();
|
|
151
|
+
window.removeEventListener("resize", compute);
|
|
111
152
|
};
|
|
112
|
-
}, [
|
|
113
|
-
if (railBox === null ||
|
|
153
|
+
}, [marks.length]);
|
|
154
|
+
if (railBox === null || marks.length < MIN_MARKS) return null;
|
|
114
155
|
const jump = (key) => {
|
|
115
156
|
findRow(key)?.scrollIntoView({
|
|
116
157
|
behavior: "smooth",
|
|
117
158
|
block: "start"
|
|
118
159
|
});
|
|
119
160
|
};
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
const buildHover = (mark, index) => {
|
|
162
|
+
const turn = mark.turn !== void 0 ? timeline.turns.get(mark.turn) : void 0;
|
|
163
|
+
let durationLabel = null;
|
|
164
|
+
let reasonLabel = null;
|
|
165
|
+
let ttftLabel = null;
|
|
166
|
+
let tpsLabel = null;
|
|
167
|
+
if (turn !== void 0) {
|
|
168
|
+
if (turn.start !== void 0 && turn.end !== void 0) durationLabel = formatDuration(turn.end.time - turn.start.time);
|
|
169
|
+
if (turn.end !== void 0) {
|
|
170
|
+
const reason = turn.end.data.reason;
|
|
171
|
+
if (reason?.kind !== void 0) reasonLabel = reasonLabelOf(reason.kind);
|
|
172
|
+
}
|
|
173
|
+
const tail = turnTailOf(turn);
|
|
174
|
+
if (tail !== void 0) {
|
|
175
|
+
if (tail.ttftMs !== void 0) ttftLabel = formatDuration(tail.ttftMs);
|
|
176
|
+
if (tail.tokensPerSecond !== void 0) tpsLabel = `${tail.tokensPerSecond.toFixed(1)} tok/s`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
mark,
|
|
181
|
+
index,
|
|
182
|
+
total: marks.length,
|
|
183
|
+
turnLabel: mark.turn !== void 0 ? `第 ${mark.turn} 轮` : null,
|
|
184
|
+
durationLabel,
|
|
185
|
+
reasonLabel,
|
|
186
|
+
ttftLabel,
|
|
187
|
+
tpsLabel
|
|
188
|
+
};
|
|
127
189
|
};
|
|
128
190
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
129
191
|
style: {
|
|
@@ -131,48 +193,115 @@ window.__ModuleLoader__.load({
|
|
|
131
193
|
top: railBox.top,
|
|
132
194
|
right: railBox.right,
|
|
133
195
|
height: railBox.height,
|
|
134
|
-
width:
|
|
196
|
+
width: DOT_HIT,
|
|
135
197
|
pointerEvents: "auto",
|
|
136
198
|
zIndex: 100
|
|
137
199
|
},
|
|
138
|
-
"aria-label": "
|
|
139
|
-
children: [
|
|
140
|
-
type: "button",
|
|
200
|
+
"aria-label": "会话里程碑",
|
|
201
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
141
202
|
style: {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
background: hover?.key === tick.key ? "#4d7cfe" : "rgba(120, 130, 150, 0.5)",
|
|
151
|
-
cursor: "pointer"
|
|
203
|
+
height: "100%",
|
|
204
|
+
overflowY: "auto",
|
|
205
|
+
display: "flex",
|
|
206
|
+
flexDirection: "column",
|
|
207
|
+
alignItems: "center",
|
|
208
|
+
gap: DOT_GAP,
|
|
209
|
+
padding: "6px 0",
|
|
210
|
+
scrollbarWidth: "none"
|
|
152
211
|
},
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
212
|
+
children: marks.map((mark, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
213
|
+
type: "button",
|
|
214
|
+
style: {
|
|
215
|
+
width: DOT_HIT,
|
|
216
|
+
height: DOT_HIT,
|
|
217
|
+
flexShrink: 0,
|
|
218
|
+
display: "flex",
|
|
219
|
+
alignItems: "center",
|
|
220
|
+
justifyContent: "center",
|
|
221
|
+
background: "transparent",
|
|
222
|
+
border: "none",
|
|
223
|
+
padding: 0,
|
|
224
|
+
cursor: "pointer"
|
|
225
|
+
},
|
|
226
|
+
onMouseEnter: (e) => {
|
|
227
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
228
|
+
setHover({
|
|
229
|
+
...buildHover(mark, i),
|
|
230
|
+
top: rect.top + rect.height / 2
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
onMouseLeave: () => setHover(null),
|
|
234
|
+
onClick: () => jump(mark.key),
|
|
235
|
+
"aria-label": `跳转到第 ${i + 1} 条消息`,
|
|
236
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
|
|
237
|
+
width: DOT_SIZE,
|
|
238
|
+
height: DOT_SIZE,
|
|
239
|
+
borderRadius: "50%",
|
|
240
|
+
background: dotColor(i, marks.length),
|
|
241
|
+
boxShadow: hover?.mark.key === mark.key ? "0 0 0 3px rgba(77, 124, 254, 0.35)" : "none",
|
|
242
|
+
transition: "transform 120ms ease",
|
|
243
|
+
transform: hover?.mark.key === mark.key ? "scale(1.35)" : "none"
|
|
244
|
+
} })
|
|
245
|
+
}, mark.key))
|
|
246
|
+
}), hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
158
247
|
style: {
|
|
159
248
|
position: "fixed",
|
|
160
|
-
right: railBox.right +
|
|
161
|
-
top:
|
|
249
|
+
right: railBox.right + DOT_HIT + 8,
|
|
250
|
+
top: hover.top,
|
|
162
251
|
transform: "translateY(-50%)",
|
|
163
|
-
maxWidth:
|
|
164
|
-
|
|
165
|
-
|
|
252
|
+
maxWidth: 300,
|
|
253
|
+
minWidth: 180,
|
|
254
|
+
padding: "8px 12px",
|
|
255
|
+
background: "rgba(20, 24, 32, 0.96)",
|
|
166
256
|
color: "#e6e8ee",
|
|
167
|
-
borderRadius:
|
|
257
|
+
borderRadius: 8,
|
|
168
258
|
fontSize: 12,
|
|
169
|
-
lineHeight: 1.
|
|
259
|
+
lineHeight: 1.6,
|
|
170
260
|
whiteSpace: "pre-wrap",
|
|
171
261
|
wordBreak: "break-word",
|
|
172
|
-
boxShadow: "0
|
|
173
|
-
zIndex: 101
|
|
262
|
+
boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
|
|
263
|
+
zIndex: 101,
|
|
264
|
+
pointerEvents: "none"
|
|
174
265
|
},
|
|
175
|
-
children:
|
|
266
|
+
children: [
|
|
267
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
268
|
+
style: {
|
|
269
|
+
display: "flex",
|
|
270
|
+
gap: 8,
|
|
271
|
+
color: "#9aa4b8",
|
|
272
|
+
fontSize: 11,
|
|
273
|
+
marginBottom: 4
|
|
274
|
+
},
|
|
275
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
276
|
+
"第 ",
|
|
277
|
+
hover.index + 1,
|
|
278
|
+
" / ",
|
|
279
|
+
hover.total,
|
|
280
|
+
" 条"
|
|
281
|
+
] }), hover.turnLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.turnLabel })]
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
284
|
+
style: { color: "#c7cede" },
|
|
285
|
+
children: hover.mark.preview !== "" ? hover.mark.preview : "(无文本)"
|
|
286
|
+
}),
|
|
287
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
288
|
+
style: {
|
|
289
|
+
display: "flex",
|
|
290
|
+
flexWrap: "wrap",
|
|
291
|
+
gap: 8,
|
|
292
|
+
color: "#8b96ab",
|
|
293
|
+
fontSize: 11,
|
|
294
|
+
marginTop: 4
|
|
295
|
+
},
|
|
296
|
+
children: [
|
|
297
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime(hover.mark.time) }),
|
|
298
|
+
hover.durationLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["用时 ", hover.durationLabel] }),
|
|
299
|
+
hover.reasonLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.reasonLabel }),
|
|
300
|
+
hover.ttftLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["首字 ", hover.ttftLabel] }),
|
|
301
|
+
hover.tpsLabel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: hover.tpsLabel })
|
|
302
|
+
]
|
|
303
|
+
})
|
|
304
|
+
]
|
|
176
305
|
})]
|
|
177
306
|
});
|
|
178
307
|
}
|
package/package.json
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-milestone",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./client": "./lib/client.js",
|
|
10
|
-
"./package.json": "./package.json"
|
|
11
|
-
},
|
|
12
|
-
"dsh": {
|
|
13
|
-
"bundle": {
|
|
14
|
-
"patch": "./cordis.patch.yml"
|
|
15
|
-
},
|
|
16
|
-
"client": {
|
|
17
|
-
"inject": [
|
|
18
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
19
|
-
"@deepseek-ai/dsh-client-ui-conversation",
|
|
20
|
-
"@deepseek-ai/dsh-client-ui-layout"
|
|
21
|
-
],
|
|
22
|
-
"platform": "web"
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"scripts": {
|
|
26
|
-
"build": "tsdown",
|
|
27
|
-
"watch": "tsdown --watch",
|
|
28
|
-
"typecheck": "tsc --noEmit",
|
|
29
|
-
"prepublishOnly": "pnpm run build"
|
|
30
|
-
},
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"keywords": [
|
|
33
|
-
"deepseek",
|
|
34
|
-
"harness",
|
|
35
|
-
"dsh",
|
|
36
|
-
"dsh-plugin",
|
|
37
|
-
"conversation",
|
|
38
|
-
"navigation"
|
|
39
|
-
],
|
|
40
|
-
"files": [
|
|
41
|
-
"lib/index.js",
|
|
42
|
-
"lib/client.js",
|
|
43
|
-
"cordis.patch.yml"
|
|
44
|
-
],
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
47
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
49
|
-
"react": "^18.2.0"
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.6",
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
56
|
-
"@types/react": "~18.3.1",
|
|
57
|
-
"react": "^18.2.0",
|
|
58
|
-
"tsdown": "0.22.2",
|
|
59
|
-
"typescript": "^6.0.3"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-milestone",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Git-style milestone timeline for DeepSeek Harness: a fixed-pitch dot rail beside the conversation — hover for rich metadata, click to jump to any message.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
},
|
|
16
|
+
"client": {
|
|
17
|
+
"inject": [
|
|
18
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
19
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
20
|
+
"@deepseek-ai/dsh-client-ui-layout"
|
|
21
|
+
],
|
|
22
|
+
"platform": "web"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"watch": "tsdown --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "pnpm run build"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"deepseek",
|
|
34
|
+
"harness",
|
|
35
|
+
"dsh",
|
|
36
|
+
"dsh-plugin",
|
|
37
|
+
"conversation",
|
|
38
|
+
"navigation"
|
|
39
|
+
],
|
|
40
|
+
"files": [
|
|
41
|
+
"lib/index.js",
|
|
42
|
+
"lib/client.js",
|
|
43
|
+
"cordis.patch.yml"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
47
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
49
|
+
"react": "^18.2.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
56
|
+
"@types/react": "~18.3.1",
|
|
57
|
+
"react": "^18.2.0",
|
|
58
|
+
"tsdown": "0.22.2",
|
|
59
|
+
"typescript": "^6.0.3"
|
|
60
|
+
}
|
|
61
|
+
}
|