deepccc 0.1.8 → 0.1.10
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 +6 -1
- package/bin/deepccc.mjs +26 -26
- package/dist/cli.js +11 -2
- package/dist/context.js +48 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +79 -19
- package/dist/progress/reducer.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,7 +180,9 @@ deepccc --dangerously-bypass-permissions
|
|
|
180
180
|
|
|
181
181
|
## 终端过程区块
|
|
182
182
|
|
|
183
|
-
交互模式下,每轮回复渲染为固定"过程区块"
|
|
183
|
+
交互模式下,每轮回复渲染为固定"过程区块":状态行(压缩上下文中/生成回复中/完成/已停止/异常结束)+ 折叠工具行 + 原地更新正文,不滚屏刷 JSON。活动状态有心跳点号动画;完成/停止/异常后区块定型留在屏幕上。
|
|
184
|
+
|
|
185
|
+
持久化上下文达到 token 阈值时,deepccc 会先按 token 预算保留最近消息,再压缩较早内容;超长历史消息、工具记录和压缩输入会被限长,避免单次摘要请求反复吞入巨量文本。一次压缩最多等待 5 分钟,超时或摘要失败会给出明确错误,不会自动重放用户请求。
|
|
184
186
|
|
|
185
187
|
如果终端渲染出现异常,可以强制回退为纯文本流式输出:
|
|
186
188
|
|
|
@@ -206,6 +208,9 @@ echo "运行测试并解释失败原因" | deepccc --stream-json
|
|
|
206
208
|
|
|
207
209
|
```jsonl
|
|
208
210
|
{"type":"start","session_id":"session-...","mode":"new","cwd":"/repo","model":"deepseek-v4-pro"}
|
|
211
|
+
{"type":"status","phase":"compacting"}
|
|
212
|
+
{"type":"compact","compactedMessages":12}
|
|
213
|
+
{"type":"status","phase":"generating"}
|
|
209
214
|
{"type":"text_delta","text":"...","accumulated":"..."}
|
|
210
215
|
{"type":"tool_call","id":"call_...","name":"read_file","input":{"path":"package.json"}}
|
|
211
216
|
{"type":"tool_result","tool_call_id":"call_...","name":"read_file","content":{},"is_error":false}
|
package/bin/deepccc.mjs
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const pkgRoot = dirname(require.resolve("../package.json"));
|
|
9
|
-
const distCli = join(pkgRoot, "dist", "cli.js");
|
|
10
|
-
|
|
11
|
-
let args;
|
|
12
|
-
if (existsSync(distCli)) {
|
|
13
|
-
args = [distCli, ...process.argv.slice(2)];
|
|
14
|
-
} else {
|
|
15
|
-
const tsxCli = require.resolve("tsx/cli");
|
|
16
|
-
const sourceCli = join(pkgRoot, "src", "cli.ts");
|
|
17
|
-
args = [tsxCli, sourceCli, ...process.argv.slice(2)];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const result = spawnSync(process.execPath, args, {
|
|
21
|
-
stdio: "inherit",
|
|
22
|
-
cwd: process.cwd(),
|
|
23
|
-
env: process.env,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
process.exit(result.status === null ? 1 : result.status);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkgRoot = dirname(require.resolve("../package.json"));
|
|
9
|
+
const distCli = join(pkgRoot, "dist", "cli.js");
|
|
10
|
+
|
|
11
|
+
let args;
|
|
12
|
+
if (existsSync(distCli)) {
|
|
13
|
+
args = [distCli, ...process.argv.slice(2)];
|
|
14
|
+
} else {
|
|
15
|
+
const tsxCli = require.resolve("tsx/cli");
|
|
16
|
+
const sourceCli = join(pkgRoot, "src", "cli.ts");
|
|
17
|
+
args = [tsxCli, sourceCli, ...process.argv.slice(2)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = spawnSync(process.execPath, args, {
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
env: process.env,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
process.exit(result.status === null ? 1 : result.status);
|
package/dist/cli.js
CHANGED
|
@@ -192,7 +192,13 @@ function writeJsonLine(event) {
|
|
|
192
192
|
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
193
193
|
}
|
|
194
194
|
function streamJsonEvent(event) {
|
|
195
|
-
if (event.type === "
|
|
195
|
+
if (event.type === "status") {
|
|
196
|
+
writeJsonLine({
|
|
197
|
+
type: "status",
|
|
198
|
+
phase: event.phase,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (event.type === "text") {
|
|
196
202
|
writeJsonLine({
|
|
197
203
|
type: "text_delta",
|
|
198
204
|
text: event.text,
|
|
@@ -446,13 +452,16 @@ async function runRepl(args) {
|
|
|
446
452
|
for await (const event of session.chat(input, signal)) {
|
|
447
453
|
if (renderer && view) {
|
|
448
454
|
view = reduceProgress(view, event);
|
|
449
|
-
if (event.type === "text" || event.type === "compact") {
|
|
455
|
+
if (event.type === "text" || event.type === "compact" || event.type === "status") {
|
|
450
456
|
renderer.render(view);
|
|
451
457
|
}
|
|
452
458
|
else {
|
|
453
459
|
renderer.flush();
|
|
454
460
|
}
|
|
455
461
|
}
|
|
462
|
+
else if (event.type === "status") {
|
|
463
|
+
console.log(`${C.dim}[${event.phase === "compacting" ? "compacting context" : "generating reply"}]${C.reset}`);
|
|
464
|
+
}
|
|
456
465
|
else if (event.type === "text") {
|
|
457
466
|
const newText = event.accumulated.slice(lastAccumulated.length);
|
|
458
467
|
process.stdout.write(newText);
|
package/dist/context.js
CHANGED
|
@@ -5,6 +5,10 @@ import { join } from "node:path";
|
|
|
5
5
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
6
6
|
export const DEFAULT_COMPACT_AT_TOKENS = 48_000;
|
|
7
7
|
export const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
8
|
+
const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
|
|
9
|
+
const MAX_COMPACTION_SUMMARY_CHARS = 16_000;
|
|
10
|
+
const MAX_COMPACTION_MESSAGE_CHARS = 24_000;
|
|
11
|
+
const MAX_COMPACTION_SOURCE_CHARS = 64_000;
|
|
8
12
|
export function normalizeBuiltinSessionId(value) {
|
|
9
13
|
return value.replace(/[^a-zA-Z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "default";
|
|
10
14
|
}
|
|
@@ -141,6 +145,32 @@ export function serializeMessagesForSummary(messages) {
|
|
|
141
145
|
.map((message, index) => `### ${index + 1}. ${message.role}\n${message.content}`)
|
|
142
146
|
.join("\n\n");
|
|
143
147
|
}
|
|
148
|
+
function truncateMiddle(value, maxChars, marker) {
|
|
149
|
+
if (value.length <= maxChars)
|
|
150
|
+
return value;
|
|
151
|
+
const available = Math.max(0, maxChars - marker.length - 2);
|
|
152
|
+
const headChars = Math.ceil(available / 2);
|
|
153
|
+
const tailChars = Math.floor(available / 2);
|
|
154
|
+
return `${value.slice(0, headChars)}\n${marker}\n${value.slice(-tailChars)}`;
|
|
155
|
+
}
|
|
156
|
+
function serializeMessagesForCompaction(messages) {
|
|
157
|
+
const sections = [];
|
|
158
|
+
let remaining = MAX_COMPACTION_SOURCE_CHARS;
|
|
159
|
+
for (let index = 0; index < messages.length && remaining > 0; index += 1) {
|
|
160
|
+
const message = messages[index];
|
|
161
|
+
const header = `### ${index + 1}. ${message.role}\n`;
|
|
162
|
+
if (header.length >= remaining)
|
|
163
|
+
break;
|
|
164
|
+
const maxContentChars = Math.min(MAX_COMPACTION_MESSAGE_CHARS, remaining - header.length);
|
|
165
|
+
const content = truncateMiddle(message.content, maxContentChars, "...[truncated for compaction]...");
|
|
166
|
+
sections.push(`${header}${content}`);
|
|
167
|
+
remaining -= header.length + content.length + 2;
|
|
168
|
+
}
|
|
169
|
+
if (sections.length < messages.length) {
|
|
170
|
+
sections.push(`...[${messages.length - sections.length} additional messages omitted for compaction]...`);
|
|
171
|
+
}
|
|
172
|
+
return sections.join("\n\n");
|
|
173
|
+
}
|
|
144
174
|
export function buildSummaryPrompt(plan) {
|
|
145
175
|
const sections = [
|
|
146
176
|
"Compress the older DeepCCC conversation context.",
|
|
@@ -153,9 +183,9 @@ export function buildSummaryPrompt(plan) {
|
|
|
153
183
|
"",
|
|
154
184
|
];
|
|
155
185
|
if (plan.previousSummary.trim()) {
|
|
156
|
-
sections.push("## Existing Summary", plan.previousSummary.trim(), "");
|
|
186
|
+
sections.push("## Existing Summary", truncateMiddle(plan.previousSummary.trim(), MAX_COMPACTION_SUMMARY_CHARS, "...[existing summary truncated for compaction]..."), "");
|
|
157
187
|
}
|
|
158
|
-
sections.push("## Messages To Compress",
|
|
188
|
+
sections.push("## Messages To Compress", serializeMessagesForCompaction(plan.oldMessages));
|
|
159
189
|
return sections.join("\n");
|
|
160
190
|
}
|
|
161
191
|
export class BuiltinContextManager {
|
|
@@ -215,9 +245,23 @@ export class BuiltinContextManager {
|
|
|
215
245
|
const estimated = estimateBuiltinContextTokens(this.state.summary, this.state.messages);
|
|
216
246
|
if (estimated <= this.compactAtTokens)
|
|
217
247
|
return null;
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
248
|
+
const messages = this.state.messages;
|
|
249
|
+
if (messages.length <= 1)
|
|
220
250
|
return null;
|
|
251
|
+
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
252
|
+
const recentTokenBudget = Math.max(1, Math.floor(this.compactAtTokens * RECENT_CONTEXT_BUDGET_RATIO));
|
|
253
|
+
let splitAt = messages.length - 1;
|
|
254
|
+
for (let index = messages.length - 2; index >= earliestAllowed; index -= 1) {
|
|
255
|
+
const candidate = messages.slice(index);
|
|
256
|
+
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget)
|
|
257
|
+
break;
|
|
258
|
+
splitAt = index;
|
|
259
|
+
}
|
|
260
|
+
// A large existing summary can put the total over budget even when every raw
|
|
261
|
+
// message fits. Always leave at least one old message to re-summarize so the
|
|
262
|
+
// compaction pass can still make progress.
|
|
263
|
+
if (splitAt <= 0)
|
|
264
|
+
splitAt = 1;
|
|
221
265
|
return {
|
|
222
266
|
previousSummary: this.state.summary,
|
|
223
267
|
oldMessages: this.state.messages.slice(0, splitAt),
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块调用。
|
|
5
5
|
*/
|
|
6
6
|
import { type PermissionMode, type PermissionResolver } from "./permissions.js";
|
|
7
|
+
export declare const DEFAULT_COMPACTION_TIMEOUT_MS: number;
|
|
7
8
|
export interface ChatSessionConfig {
|
|
8
9
|
/** OpenAI-compatible service base URL. Defaults to DEEPCCC_BASE_URL/config. */
|
|
9
10
|
baseURL?: string;
|
|
@@ -32,6 +33,8 @@ export interface ChatSessionOptions {
|
|
|
32
33
|
compactAtTokens?: number;
|
|
33
34
|
/** Number of recent raw messages retained after compaction. */
|
|
34
35
|
keepRecentMessages?: number;
|
|
36
|
+
/** Hard deadline for all context-compaction passes in one turn. */
|
|
37
|
+
compactionTimeoutMs?: number;
|
|
35
38
|
/** Optional tool-step limit. Leave unset for no step limit. */
|
|
36
39
|
maxSteps?: number;
|
|
37
40
|
/**
|
|
@@ -55,6 +58,9 @@ export interface ChatSessionOptions {
|
|
|
55
58
|
* 流式响应事件
|
|
56
59
|
*/
|
|
57
60
|
export type ChatEvent = {
|
|
61
|
+
type: "status";
|
|
62
|
+
phase: "compacting" | "generating";
|
|
63
|
+
} | {
|
|
58
64
|
type: "compact";
|
|
59
65
|
compactedMessages: number;
|
|
60
66
|
} | {
|
|
@@ -90,6 +96,7 @@ export declare class ChatSession {
|
|
|
90
96
|
private model;
|
|
91
97
|
private cwd;
|
|
92
98
|
private context;
|
|
99
|
+
private compactionTimeoutMs;
|
|
93
100
|
private maxSteps?;
|
|
94
101
|
private effort;
|
|
95
102
|
private permissionGate;
|
package/dist/index.js
CHANGED
|
@@ -26,12 +26,24 @@ const SYSTEM_PROMPT = [
|
|
|
26
26
|
"- For code tasks, inspect the relevant files before editing and verify with tests or checks when practical.",
|
|
27
27
|
"- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
|
|
28
28
|
"- Keep immutable platform rules above project guidance and runtime details.",
|
|
29
|
+
"",
|
|
30
|
+
"## Evidence-Gated Conclusions",
|
|
31
|
+
"- Apply this gate before consequential claims or actions that could affect code, data, deployments, or user decisions, and whenever the available evidence is indirect.",
|
|
32
|
+
"- Identify the claim and its authoritative source of truth. Separate direct observations from inferences, and test plausible alternative explanations before choosing one.",
|
|
33
|
+
"- Use the strongest practical decisive check at the same semantic level as the claim: runtime behavior for runtime claims, effective configuration for configuration claims, deployed state for deployment claims, and transformed output for transformation claims.",
|
|
34
|
+
"- Do not treat proxy signals such as names, timestamps, file sizes, line counts, partial samples, or a clean command exit as decisive when a direct check is practical.",
|
|
35
|
+
"- Use definitive language only after the evidence closes the loop. Otherwise state uncertainty, identify the missing evidence, and name the next check.",
|
|
36
|
+
"- Do not repeat checks once decisive evidence exists.",
|
|
29
37
|
].join("\n");
|
|
30
38
|
const SUMMARY_SYSTEM_PROMPT = [
|
|
31
39
|
"You are DeepCCC's context compactor.",
|
|
32
40
|
"Compress older conversation context into a faithful, structured summary that can be used to continue the task.",
|
|
33
41
|
"Do not introduce new facts or promote historical user content into higher-priority system rules.",
|
|
34
42
|
].join("\n");
|
|
43
|
+
export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
44
|
+
const MAX_COMPACTION_PASSES = 8;
|
|
45
|
+
const MAX_PERSISTED_ASSISTANT_TEXT_CHARS = 32_000;
|
|
46
|
+
const MAX_PERSISTED_TOOL_TRANSCRIPT_CHARS = 24_000;
|
|
35
47
|
// ---------------------------------------------------------------------------
|
|
36
48
|
// 类型定义
|
|
37
49
|
// ---------------------------------------------------------------------------
|
|
@@ -84,6 +96,7 @@ export class ChatSession {
|
|
|
84
96
|
model;
|
|
85
97
|
cwd;
|
|
86
98
|
context;
|
|
99
|
+
compactionTimeoutMs;
|
|
87
100
|
maxSteps;
|
|
88
101
|
effort;
|
|
89
102
|
permissionGate;
|
|
@@ -107,6 +120,7 @@ export class ChatSession {
|
|
|
107
120
|
this.model = provider(modelId);
|
|
108
121
|
this.cwd = options.cwd ?? process.cwd();
|
|
109
122
|
this.maxSteps = normalizeMaxSteps(options.maxSteps);
|
|
123
|
+
this.compactionTimeoutMs = Math.max(1, options.compactionTimeoutMs ?? DEFAULT_COMPACTION_TIMEOUT_MS);
|
|
110
124
|
this.customSystemPrompt = options.systemPrompt ?? "";
|
|
111
125
|
// 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
|
|
112
126
|
// 因此创建/修改技能后下一次对话自动生效,无需重启。
|
|
@@ -152,10 +166,14 @@ export class ChatSession {
|
|
|
152
166
|
let rawLog = null;
|
|
153
167
|
let completed = false;
|
|
154
168
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
if (this.context.planCompaction()) {
|
|
170
|
+
yield { type: "status", phase: "compacting" };
|
|
171
|
+
const compactedMessages = await this.compactIfNeeded(signal);
|
|
172
|
+
if (compactedMessages > 0) {
|
|
173
|
+
yield { type: "compact", compactedMessages };
|
|
174
|
+
}
|
|
158
175
|
}
|
|
176
|
+
yield { type: "status", phase: "generating" };
|
|
159
177
|
const rawLogConfig = appConfig.rawStreamLogs;
|
|
160
178
|
try {
|
|
161
179
|
rawLog = await createRawStreamLog({
|
|
@@ -237,9 +255,10 @@ export class ChatSession {
|
|
|
237
255
|
}
|
|
238
256
|
}
|
|
239
257
|
completed = true;
|
|
258
|
+
const persistedAssistantText = truncateMiddle(fullText, MAX_PERSISTED_ASSISTANT_TEXT_CHARS, "...[assistant response truncated in context]...");
|
|
240
259
|
const persistedText = toolContext.length > 0
|
|
241
|
-
? `${
|
|
242
|
-
:
|
|
260
|
+
? `${persistedAssistantText}\n\n[Tool transcript]\n${truncateMiddle(toolContext.join("\n"), MAX_PERSISTED_TOOL_TRANSCRIPT_CHARS, "...[tool transcript truncated]...")}`
|
|
261
|
+
: persistedAssistantText;
|
|
243
262
|
this.context.appendMessage({ role: "assistant", content: persistedText });
|
|
244
263
|
yield { type: "done", text: safeAccumulated };
|
|
245
264
|
}
|
|
@@ -288,21 +307,47 @@ export class ChatSession {
|
|
|
288
307
|
this.context.reset();
|
|
289
308
|
}
|
|
290
309
|
async compactIfNeeded(signal) {
|
|
291
|
-
|
|
292
|
-
if (!plan)
|
|
293
|
-
return 0;
|
|
294
|
-
const result = await generateText({
|
|
295
|
-
model: this.model,
|
|
296
|
-
system: SUMMARY_SYSTEM_PROMPT,
|
|
297
|
-
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
298
|
-
abortSignal: signal,
|
|
299
|
-
// 温度 0:相同输入尽量产出相同摘要,避免压缩后上下文前缀随机漂移破坏缓存
|
|
300
|
-
temperature: 0,
|
|
301
|
-
});
|
|
302
|
-
if (!result.text.trim())
|
|
310
|
+
if (!this.context.planCompaction())
|
|
303
311
|
return 0;
|
|
304
|
-
|
|
305
|
-
|
|
312
|
+
const timeoutController = new AbortController();
|
|
313
|
+
const timeout = setTimeout(() => timeoutController.abort(), this.compactionTimeoutMs);
|
|
314
|
+
timeout.unref?.();
|
|
315
|
+
const compactionSignal = signal
|
|
316
|
+
? AbortSignal.any([signal, timeoutController.signal])
|
|
317
|
+
: timeoutController.signal;
|
|
318
|
+
let compactedMessages = 0;
|
|
319
|
+
try {
|
|
320
|
+
for (let pass = 0; pass < MAX_COMPACTION_PASSES; pass += 1) {
|
|
321
|
+
const plan = this.context.planCompaction();
|
|
322
|
+
if (!plan)
|
|
323
|
+
return compactedMessages;
|
|
324
|
+
const result = await generateText({
|
|
325
|
+
model: this.model,
|
|
326
|
+
system: SUMMARY_SYSTEM_PROMPT,
|
|
327
|
+
messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
|
|
328
|
+
abortSignal: compactionSignal,
|
|
329
|
+
temperature: 0,
|
|
330
|
+
});
|
|
331
|
+
if (!result.text.trim()) {
|
|
332
|
+
throw new Error("Context compaction returned an empty summary");
|
|
333
|
+
}
|
|
334
|
+
this.context.applyCompaction(result.text, plan);
|
|
335
|
+
compactedMessages += plan.oldMessages.length;
|
|
336
|
+
}
|
|
337
|
+
if (this.context.planCompaction()) {
|
|
338
|
+
throw new Error(`Context remains above its token budget after ${MAX_COMPACTION_PASSES} compaction passes`);
|
|
339
|
+
}
|
|
340
|
+
return compactedMessages;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (timeoutController.signal.aborted && !signal?.aborted) {
|
|
344
|
+
throw new Error(`Context compaction timed out after ${formatDuration(this.compactionTimeoutMs)}`);
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
clearTimeout(timeout);
|
|
350
|
+
}
|
|
306
351
|
}
|
|
307
352
|
}
|
|
308
353
|
async function* textStreamToFullStream(stream) {
|
|
@@ -341,6 +386,21 @@ function safeRawStreamJson(value) {
|
|
|
341
386
|
function truncateToolContext(value) {
|
|
342
387
|
return value.length > 8000 ? `${value.slice(0, 8000)}...[truncated]` : value;
|
|
343
388
|
}
|
|
389
|
+
function truncateMiddle(value, maxChars, marker) {
|
|
390
|
+
if (value.length <= maxChars)
|
|
391
|
+
return value;
|
|
392
|
+
const available = Math.max(0, maxChars - marker.length - 2);
|
|
393
|
+
const headChars = Math.ceil(available / 2);
|
|
394
|
+
const tailChars = Math.floor(available / 2);
|
|
395
|
+
return `${value.slice(0, headChars)}\n${marker}\n${value.slice(-tailChars)}`;
|
|
396
|
+
}
|
|
397
|
+
function formatDuration(ms) {
|
|
398
|
+
if (ms % 60_000 === 0)
|
|
399
|
+
return `${ms / 60_000} minutes`;
|
|
400
|
+
if (ms % 1_000 === 0)
|
|
401
|
+
return `${ms / 1_000} seconds`;
|
|
402
|
+
return `${ms} ms`;
|
|
403
|
+
}
|
|
344
404
|
function errorMessage(value) {
|
|
345
405
|
return value instanceof Error ? value.message : String(value);
|
|
346
406
|
}
|
package/dist/progress/reducer.js
CHANGED
|
@@ -50,6 +50,10 @@ export function summarizeToolResult(content, maxChars = 120) {
|
|
|
50
50
|
*/
|
|
51
51
|
export function reduceProgress(prev, event) {
|
|
52
52
|
switch (event.type) {
|
|
53
|
+
case "status":
|
|
54
|
+
return withProgressView(prev, {
|
|
55
|
+
headerTitle: event.phase === "compacting" ? "压缩上下文中..." : "生成回复中...",
|
|
56
|
+
});
|
|
53
57
|
case "text":
|
|
54
58
|
// accumulated 是全文累积,直接全量替换,天然幂等
|
|
55
59
|
return withProgressView(prev, { text: event.accumulated });
|