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.
- package/README.md +182 -165
- package/lib/client.js +544 -172
- package/lib/index.js +203 -6
- 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
|
-
*
|
|
4
|
-
* plugin
|
|
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.
|
|
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-
|
|
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": "
|
|
52
|
-
"@deepseek-ai/dsh-client-
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
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-
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-
|
|
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
|
}
|