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.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +90 -72
  3. package/lib/client.js +220 -91
  4. 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
- # dsh-milestone
2
-
3
- 会话里程碑导航条(DeepSeek Harness 插件)。在会话视图右侧渲染一条垂直 scrubber,每个 **user 发言** 对应一个进度点:鼠标悬停预览该条消息的前 80 字,点击平滑跳转到对应位置。
4
-
5
- > **dsh-plugin** · 适配 DeepSeek Harness (`deepseek-ai/deepseek-harness`) Web UI。
6
-
7
- ## 功能
8
-
9
- - **进度点(tick)**:每条真实 user 消息 = 一个 tick,按**真实渲染偏移**定位(非等距),跟随滚动与内容变化实时更新。
10
- - **悬停预览**:hover 显示该条 user 消息的前 80 字。
11
- - **点击跳转**:点击 tick 平滑滚动到对应消息(`scrollIntoView`)。
12
- - 少于 2 条 user 消息时不渲染(无导航价值)。
13
-
14
- ## 安装
15
-
16
- ```sh
17
- # 本地目录(开发)
18
- dsh plugin --profile demo add ./dsh-milestone
19
-
20
- # npm 包(发布后)
21
- dsh plugin --profile demo add dsh-milestone
22
-
23
- # GitHub 仓库(需 prepare 脚本 + allowBuilds 许可)
24
- dsh plugin --profile demo add github:you/dsh-milestone
25
- ```
26
-
27
- 然后 `npx @deepseek-ai/dsh web` 启动 Web UI 即可看到右侧里程碑条。
28
-
29
- ## 开发
30
-
31
- 要求 **Node.js `>= 24`**(官方 harness 要求 `^22.19 || >=24`,本仓库已验证 `24.19.0`)。依赖从 npm 安装(`0.1.0-rc.6` 系列,完整可用)。
32
-
33
- ```sh
34
- pnpm install
35
- pnpm run build # 产出 lib/index.js (node half) + lib/client.js (browser half)
36
- pnpm exec tsc --noEmit # 类型检查
37
- pnpm run watch # 监听构建
38
- ```
39
-
40
- 构建产物:`lib/index.js`(node half,空 apply)+ `lib/client.js`(browser half,CJS closure,`window.__ModuleLoader__.load` banner)。
41
-
42
- ### 架构
43
-
44
- 双半边插件(Host = Node 进程,Client = 浏览器):
45
-
46
- ```
47
- src/index.ts node half —— apply,让插件进入 host cordis 配置树
48
- src/client/index.ts browser half —— 两个 slot 注册
49
- src/client/MilestoneOverlay.tsx shell.overlay entry(root scope)
50
- └─ 声明 session 子槽 milestone.rail
51
- └─ 通过 SessionProvider 桥接到会话区
52
- src/client/MilestoneRail.tsx milestone.rail entry(session scope)
53
- └─ useSession 读取会话快照
54
- └─ DOM 锚点定位 + tick 渲染 + 跳转
55
- ```
56
-
57
- 关键机制:
58
-
59
- - **注入点**:`shell.overlay`(`kind: 'list'`,`scope: 'root'`)——全框架浮动层,附加式、点击穿透,是"悬浮 rail"的唯一正确位置。
60
- - **会话数据**:通过声明 session-scope 子槽 `milestone.rail`,框架自动注入 `SessionProvider`(`PropsRenderSlots` 从子槽 scope 推导),rail 组件因此拿到 `useSession` / `sessionId`。
61
- - **消息定位**:`useSession(s => s.chat.order)` + `s.chat.nodes.get(key)` 过滤 `kind === 'user'`;DOM 锚点 `data-chat-anchor-key`(节点 key = `13:input-message<messageId>`),scrollport 为 `[data-conversation-scroll]`。
62
- - **性能**:行元素按消息列表版本缓存(O(n) 重算);scroll/resize 通过 `requestAnimationFrame` 节流。
63
-
64
- ## 已知限制
65
-
66
- - 仅覆盖当前已加载的消息窗口(harness 初始加载最近 50 条事件,向上滚动触发 `loadOlder` 分页);rail 会随分页自动更新,但更早的消息需要先滚动加载。
67
- - preview 从 DOM `textContent` 提取,长消息截断为 80 字。
68
- - 尚未实现"当前定位"高亮(P1)。
69
-
70
- ## License
71
-
72
- MIT
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). It reads the
24
- * conversation snapshot through `useSession`, finds every user-message row in
25
- * the chat flow, and renders a fixed right-side vertical scrubber: one tick
26
- * per user message positioned by real rendered offset, a hover preview of the
27
- * message text, and click-to-jump via the row's `data-chat-anchor-key` DOM
28
- * anchor.
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
- * Scroll positioning mirrors the harness chat view's own flowTop math: a row's
31
- * content position is its scrollport-relative top plus the current scrollTop,
32
- * normalized against scrollHeight. Ticks recompute on scroll and on any
33
- * scrollport/row resize, throttled through requestAnimationFrame. Row elements
34
- * are cached once per message-list revision (O(n) per recompute, not O(n^2)).
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
- /** Minimum ticks before the rail adds value. */
38
- const MIN_TICKS = 2;
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 `12:input-message<messageId>`).
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 userKeys = (0, react.useMemo)(() => order.filter((key) => nodes.get(key)?.kind === "user"), [order, nodes]);
55
- const [ticks, setTicks] = (0, react.useState)([]);
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 (userKeys.length < MIN_TICKS) {
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(4, window.innerWidth - sp.right)
142
+ right: Math.max(8, window.innerWidth - sp.right + RAIL_INSET)
95
143
  });
96
144
  };
97
145
  compute();
98
- let raf = 0;
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
- for (const row of rowCache.values()) observer.observe(row);
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
- }, [userKeys]);
113
- if (railBox === null || ticks.length < MIN_TICKS) return 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 onEnter = (tick) => {
121
- const preview = findRow(tick.key)?.textContent?.trim().slice(0, PREVIEW_LENGTH) ?? "";
122
- setHover({
123
- key: tick.key,
124
- preview,
125
- top: tick.top
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: 20,
196
+ width: DOT_HIT,
135
197
  pointerEvents: "auto",
136
198
  zIndex: 100
137
199
  },
138
- "aria-label": "Conversation milestones",
139
- children: [ticks.map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
140
- type: "button",
200
+ "aria-label": "会话里程碑",
201
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
141
202
  style: {
142
- position: "absolute",
143
- top: `${tick.top}%`,
144
- right: 6,
145
- width: 8,
146
- height: 3,
147
- borderRadius: 2,
148
- border: "none",
149
- padding: 0,
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
- onMouseEnter: () => onEnter(tick),
154
- onMouseLeave: () => setHover(null),
155
- onClick: () => jump(tick.key),
156
- "aria-label": "Jump to user message"
157
- }, tick.key)), hover !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
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 + 22,
161
- top: railBox.top + railBox.height * hover.top / 100,
249
+ right: railBox.right + DOT_HIT + 8,
250
+ top: hover.top,
162
251
  transform: "translateY(-50%)",
163
- maxWidth: 280,
164
- padding: "8px 10px",
165
- background: "rgba(20, 24, 32, 0.95)",
252
+ maxWidth: 300,
253
+ minWidth: 180,
254
+ padding: "8px 12px",
255
+ background: "rgba(20, 24, 32, 0.96)",
166
256
  color: "#e6e8ee",
167
- borderRadius: 6,
257
+ borderRadius: 8,
168
258
  fontSize: 12,
169
- lineHeight: 1.5,
259
+ lineHeight: 1.6,
170
260
  whiteSpace: "pre-wrap",
171
261
  wordBreak: "break-word",
172
- boxShadow: "0 4px 16px rgba(0, 0, 0, 0.3)",
173
- zIndex: 101
262
+ boxShadow: "0 6px 20px rgba(0, 0, 0, 0.4)",
263
+ zIndex: 101,
264
+ pointerEvents: "none"
174
265
  },
175
- children: hover.preview
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.1.0",
4
- "description": "Conversation milestone rail for DeepSeek Harness: a right-side vertical scrubber showing one tick per user message — hover to preview, click to jump.",
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
+ }