dsh-milestone 0.6.6 → 0.7.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/README.md +182 -165
  2. package/lib/client.js +544 -172
  3. package/lib/index.js +203 -6
  4. package/package.json +18 -9
package/lib/index.js CHANGED
@@ -1,10 +1,207 @@
1
+ import { z } from "zod";
2
+ import { SessionSeq } from "@deepseek-ai/dsh-session";
3
+ import { assistantStreamFirstTokenTime } from "@deepseek-ai/dsh-llm";
4
+ const messageEntrySchema = z.object({
5
+ seq: z.number().int().nonnegative().transform(SessionSeq),
6
+ messageId: z.string(),
7
+ time: z.number(),
8
+ turn: z.number().int().nonnegative(),
9
+ preview: z.string().max(80),
10
+ text: z.string()
11
+ });
12
+ const turnMetaSchema = z.object({
13
+ turn: z.number().int().nonnegative(),
14
+ startSeq: z.number().int().nonnegative().transform(SessionSeq),
15
+ startTime: z.number(),
16
+ endTime: z.number().optional(),
17
+ endReason: z.string().optional(),
18
+ firstChunkTime: z.number().optional(),
19
+ usage: z.object({
20
+ input: z.number(),
21
+ output: z.number(),
22
+ total: z.number()
23
+ }).optional()
24
+ });
25
+ z.object({
26
+ messages: z.array(messageEntrySchema),
27
+ turns: z.array(turnMetaSchema)
28
+ });
29
+ /** Space-join text blocks, collapse whitespace, cap at `limit` with an ellipsis. */
30
+ function previewText(content, limit) {
31
+ let text = "";
32
+ let unread = false;
33
+ for (const block of content) {
34
+ if (block.type !== "text" || typeof block.text !== "string") continue;
35
+ if (text.length >= limit * 2) {
36
+ unread = true;
37
+ break;
38
+ }
39
+ const chunk = block.text.length > limit * 2 ? block.text.slice(0, limit * 2) : block.text;
40
+ text += text === "" ? chunk : ` ${chunk}`;
41
+ if (text.length > limit * 2) {
42
+ unread = true;
43
+ break;
44
+ }
45
+ }
46
+ const normalized = text.replace(/\s+/g, " ").trim();
47
+ if (normalized.length > limit - 1) return `${normalized.slice(0, limit - 1).trimEnd()}…`;
48
+ return unread ? `${normalized}…` : normalized;
49
+ }
50
+ /** Extract the full plain text of a user message's content blocks. */
51
+ function fullText(content) {
52
+ const parts = [];
53
+ for (const block of content) if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
54
+ return parts.join("\n");
55
+ }
56
+ /**
57
+ * Finite-number read with a zero fallback. Usage payloads read at this fold
58
+ * boundary may predate the 0.1.5 `TokenUsage` rename (a migrated pre-0.1.5 log
59
+ * carries `input`/`output`/`total` instead), so a missing field must degrade to
60
+ * zero rather than poison the accumulated totals with `NaN`.
61
+ */
62
+ function finiteOrZero(value) {
63
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
64
+ }
65
+ /** Immutable fold state helper: open-turn bookkeeping lives in the state array tail. */
66
+ const EMPTY_MESSAGES = Object.freeze([]);
67
+ const EMPTY_TURNS = Object.freeze([]);
68
+ function initialState() {
69
+ return {
70
+ messages: EMPTY_MESSAGES,
71
+ turns: EMPTY_TURNS
72
+ };
73
+ }
74
+ const milestoneMessagesProjectionDefinition = {
75
+ key: "milestone.messages",
76
+ stateVersion: 3,
77
+ stateSchema: z.custom(),
78
+ init: (_header, _inheritedEventCount) => initialState(),
79
+ apply(state, event) {
80
+ switch (event.type) {
81
+ case "turn/start": {
82
+ const turn = event.data.turn;
83
+ if (state.turns.some((t) => t.turn === turn)) return state;
84
+ return {
85
+ messages: state.messages,
86
+ turns: [...state.turns, {
87
+ turn,
88
+ startSeq: SessionSeq(event.seq),
89
+ startTime: event.time
90
+ }]
91
+ };
92
+ }
93
+ case "user/message": {
94
+ const data = event.data;
95
+ const content = data.content ?? [];
96
+ const text = fullText(content);
97
+ const preview = previewText(content, 80);
98
+ const openTurn = state.turns[state.turns.length - 1];
99
+ return {
100
+ messages: [...state.messages, {
101
+ seq: SessionSeq(event.seq),
102
+ messageId: typeof data.id === "string" ? data.id : "",
103
+ time: event.time,
104
+ turn: openTurn?.turn ?? 0,
105
+ preview,
106
+ text
107
+ }],
108
+ turns: state.turns
109
+ };
110
+ }
111
+ case "assistant/message": {
112
+ const turn = event.data.turn;
113
+ const index = state.turns.findIndex((t) => t.turn === turn);
114
+ if (index < 0) return state;
115
+ const prior = state.turns[index];
116
+ let next = prior;
117
+ const stream = event.data.stream;
118
+ const firstToken = Array.isArray(stream) ? assistantStreamFirstTokenTime(stream) : void 0;
119
+ if (firstToken !== void 0 && prior.firstChunkTime === void 0) next = {
120
+ ...next,
121
+ firstChunkTime: firstToken
122
+ };
123
+ const usage = event.data.usage;
124
+ if (usage !== void 0) {
125
+ const accumulated = next.usage ?? {
126
+ input: 0,
127
+ output: 0,
128
+ total: 0
129
+ };
130
+ const input = finiteOrZero(usage.inputTokens) + finiteOrZero(usage.cacheReadTokens) + finiteOrZero(usage.cacheWriteTokens);
131
+ const output = finiteOrZero(usage.outputTokens);
132
+ const total = usage.totalTokens === void 0 ? input + output : finiteOrZero(usage.totalTokens);
133
+ next = {
134
+ ...next,
135
+ usage: {
136
+ input: accumulated.input + input,
137
+ output: accumulated.output + output,
138
+ total: accumulated.total + total
139
+ }
140
+ };
141
+ }
142
+ if (next === prior) return state;
143
+ const turns = state.turns.slice();
144
+ turns[index] = next;
145
+ return {
146
+ messages: state.messages,
147
+ turns
148
+ };
149
+ }
150
+ case "assistant/attempt": {
151
+ const turn = event.data.turn;
152
+ const index = state.turns.findIndex((t) => t.turn === turn);
153
+ if (index < 0 || state.turns[index].firstChunkTime !== void 0) return state;
154
+ const stream = event.data.stream;
155
+ const firstToken = Array.isArray(stream) ? assistantStreamFirstTokenTime(stream) : void 0;
156
+ if (firstToken === void 0) return state;
157
+ const turns = state.turns.slice();
158
+ turns[index] = {
159
+ ...turns[index],
160
+ firstChunkTime: firstToken
161
+ };
162
+ return {
163
+ messages: state.messages,
164
+ turns
165
+ };
166
+ }
167
+ case "turn/end": {
168
+ const turn = event.data.turn;
169
+ const index = state.turns.findIndex((t) => t.turn === turn);
170
+ if (index < 0 || state.turns[index].endTime !== void 0) return state;
171
+ const turns = state.turns.slice();
172
+ turns[index] = {
173
+ ...turns[index],
174
+ endTime: event.time,
175
+ endReason: event.data.reason.kind
176
+ };
177
+ return {
178
+ messages: state.messages,
179
+ turns
180
+ };
181
+ }
182
+ default: return state;
183
+ }
184
+ },
185
+ wire: {
186
+ viewSchema: z.custom(),
187
+ view: (state) => ({
188
+ messages: state.messages,
189
+ turns: state.turns
190
+ })
191
+ }
192
+ };
193
+ //#endregion
1
194
  //#region src/index.ts
195
+ /** Cordis plugin name. */
196
+ const name = "milestone";
197
+ /** The projection registry is the node half's only dependency. */
198
+ const inject = ["sessionProjections"];
2
199
  /**
3
- * dsh-milestone node half. Pure UI plugin: the empty apply exists so the
4
- * plugin appears in the host cordis.yml / Loader (load and lifecycle follow
5
- * the host); the browser half ships via exports["./client"], discovered
6
- * through the package.json dsh.client declaration.
200
+ * Register the milestone projection unit; the registration is an effect on
201
+ * this plugin's fiber, so unloading removes the key.
7
202
  */
8
- function apply() {}
203
+ function apply(ctx) {
204
+ ctx.sessionProjections.register(milestoneMessagesProjectionDefinition);
205
+ }
9
206
  //#endregion
10
- export { apply };
207
+ export { apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-milestone",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "Git-style milestone timeline for DeepSeek Harness: hover for metadata, click to jump to any message. 会话里程碑导航条:圆点时间线,定位并跳转到每条提问。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "client": {
17
17
  "inject": [
18
- "@deepseek-ai/dsh-client-runtime",
18
+ "@deepseek-ai/dsh-client-store",
19
19
  "@deepseek-ai/dsh-client-ui-conversation",
20
20
  "@deepseek-ai/dsh-client-ui-layout"
21
21
  ],
@@ -48,17 +48,20 @@
48
48
  ],
49
49
  "peerDependencies": {
50
50
  "@deepseek-ai/cordis": "^4.0.1",
51
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
52
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
53
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
51
+ "@deepseek-ai/dsh-client-locale": ">=0.1.5-rc.1 <0.3.0-0",
52
+ "@deepseek-ai/dsh-client-store": ">=0.1.5-rc.1 <0.3.0-0",
53
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-rc.1 <0.3.0-0",
54
54
  "react": "^18.2.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@deepseek-ai/cordis": "^4.0.1",
58
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
58
+ "@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.1",
59
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
60
+ "@deepseek-ai/dsh-client-store": "0.1.5-rc.1",
61
+ "@deepseek-ai/dsh-client-ui-layout": "0.1.5-rc.1",
62
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.1",
63
+ "@deepseek-ai/dsh-client-ui-session": "0.1.5-rc.1",
64
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
62
65
  "@playwright/test": "^1.62.1",
63
66
  "@testing-library/jest-dom": "^7.0.1",
64
67
  "@testing-library/react": "^16.3.2",
@@ -70,5 +73,11 @@
70
73
  "tsdown": "0.22.2",
71
74
  "typescript": "^6.0.3",
72
75
  "vitest": "^4.1.10"
76
+ },
77
+ "dependencies": {
78
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
79
+ "@deepseek-ai/dsh-session": "0.1.5-rc.1",
80
+ "@deepseek-ai/dsh-session-projection": "0.1.5-rc.1",
81
+ "zod": "^4.5.4"
73
82
  }
74
83
  }