dskcode 0.1.29 → 0.1.30

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/dist/index.js CHANGED
@@ -807,7 +807,7 @@ var LOGO_LINES = [
807
807
  // src/ui/ChatSession.tsx
808
808
  import { Box as Box9, Text as Text10, useInput, Static } from "ink";
809
809
  import TextInput from "ink-text-input";
810
- import { useEffect as useEffect3, useState as useState3, useCallback as useCallback2, useMemo, useRef as useRef2 } from "react";
810
+ import { useEffect as useEffect4, useState as useState4, useCallback as useCallback2, useMemo, useRef as useRef3 } from "react";
811
811
 
812
812
  // src/ui/useDoubleCtrlC.ts
813
813
  import { useState as useState2, useCallback, useEffect as useEffect2, useRef } from "react";
@@ -842,6 +842,7 @@ import InkSpinner2 from "ink-spinner";
842
842
 
843
843
  // src/ui/AssistantMessage.tsx
844
844
  import { Box as Box5, Text as Text6 } from "ink";
845
+ import { useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
845
846
 
846
847
  // src/provider/cost-tracker.ts
847
848
  import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
@@ -1674,12 +1675,90 @@ function HighlightedText({ children: text }) {
1674
1675
  }
1675
1676
 
1676
1677
  // src/ui/AssistantMessage.tsx
1677
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1678
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1678
1679
  function formatElapsed(ms) {
1679
1680
  if (ms < 1e3) return `${ms}ms`;
1680
1681
  const seconds = (ms / 1e3).toFixed(1);
1681
1682
  return `${seconds}s`;
1682
1683
  }
1684
+ function AnimatedUsage({ usage, cost }) {
1685
+ const targetTokens = usage ? usage.promptTokens + usage.completionTokens : 0;
1686
+ const targetCost = cost ?? 0;
1687
+ const [displayedTokens, setDisplayedTokens] = useState3(targetTokens);
1688
+ const [displayedCost, setDisplayedCost] = useState3(targetCost);
1689
+ const startRef = useRef2(null);
1690
+ const rafRef = useRef2(null);
1691
+ useEffect3(() => {
1692
+ if (startRef.current && startRef.current.toTokens === targetTokens && startRef.current.toCost === targetCost) {
1693
+ return;
1694
+ }
1695
+ if (targetTokens <= displayedTokens && targetCost <= displayedCost) {
1696
+ setDisplayedTokens(targetTokens);
1697
+ setDisplayedCost(targetCost);
1698
+ startRef.current = {
1699
+ fromTokens: targetTokens,
1700
+ fromCost: targetCost,
1701
+ toTokens: targetTokens,
1702
+ toCost: targetCost,
1703
+ startMs: Date.now(),
1704
+ durationMs: 0
1705
+ };
1706
+ return;
1707
+ }
1708
+ const tokensDelta = targetTokens - displayedTokens;
1709
+ const costDelta = targetCost - displayedCost;
1710
+ const durationMs = Math.min(
1711
+ 600,
1712
+ Math.max(220, Math.max(tokensDelta, 0) * 1.2 + 220)
1713
+ );
1714
+ startRef.current = {
1715
+ fromTokens: displayedTokens,
1716
+ fromCost: displayedCost,
1717
+ toTokens: targetTokens,
1718
+ toCost: targetCost,
1719
+ startMs: Date.now(),
1720
+ durationMs
1721
+ };
1722
+ if (rafRef.current) clearInterval(rafRef.current);
1723
+ rafRef.current = setInterval(() => {
1724
+ const s = startRef.current;
1725
+ if (!s) return;
1726
+ const elapsed = Date.now() - s.startMs;
1727
+ if (elapsed >= s.durationMs) {
1728
+ setDisplayedTokens(s.toTokens);
1729
+ setDisplayedCost(s.toCost);
1730
+ if (rafRef.current) {
1731
+ clearInterval(rafRef.current);
1732
+ rafRef.current = null;
1733
+ }
1734
+ return;
1735
+ }
1736
+ const t = elapsed / s.durationMs;
1737
+ const eased = 1 - Math.pow(1 - t, 3);
1738
+ const tokens = s.fromTokens + (s.toTokens - s.fromTokens) * eased;
1739
+ const cost2 = s.fromCost + (s.toCost - s.fromCost) * eased;
1740
+ setDisplayedTokens(tokens);
1741
+ setDisplayedCost(cost2);
1742
+ }, 33);
1743
+ return () => {
1744
+ if (rafRef.current) {
1745
+ clearInterval(rafRef.current);
1746
+ rafRef.current = null;
1747
+ }
1748
+ };
1749
+ }, [targetTokens, targetCost]);
1750
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
1751
+ usage && /* @__PURE__ */ jsxs5(Text6, { color: "#888888", children: [
1752
+ Math.round(displayedTokens).toLocaleString(),
1753
+ " tokens"
1754
+ ] }),
1755
+ cost !== void 0 && cost > 0 && /* @__PURE__ */ jsxs5(Text6, { color: "#888888", children: [
1756
+ " \xB7 ",
1757
+ "\xA5",
1758
+ displayedCost.toFixed(4)
1759
+ ] })
1760
+ ] });
1761
+ }
1683
1762
  function AssistantMessage({
1684
1763
  content,
1685
1764
  toolCalls,
@@ -1701,6 +1780,11 @@ function AssistantMessage({
1701
1780
  ] })
1702
1781
  ] }),
1703
1782
  toolCalls && toolCalls.length > 0 && /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", children: toolCalls.map((tc, i) => /* @__PURE__ */ jsx5(ToolCallBlock, { call: tc }, tc.id ?? i)) }),
1783
+ isStreaming && (usage || cost !== void 0) && /* @__PURE__ */ jsx5(Box5, { flexDirection: "row", marginTop: 1, marginLeft: 3, children: /* @__PURE__ */ jsxs5(Text6, { color: "#666666", dimColor: true, children: [
1784
+ "\u23F3 \u5DF2\u6D88\u8017",
1785
+ " ",
1786
+ /* @__PURE__ */ jsx5(AnimatedUsage, { usage, cost })
1787
+ ] }) }),
1704
1788
  !isStreaming && (usage || elapsed !== void 0) && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, marginLeft: 3, children: [
1705
1789
  /* @__PURE__ */ jsx5(Text6, { color: "#555555", children: "\u2500".repeat(36) }),
1706
1790
  /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", gap: 2, children: [
@@ -2114,6 +2198,389 @@ var ToolRegistry = class {
2114
2198
  }
2115
2199
  };
2116
2200
 
2201
+ // src/agent/tool-executor.ts
2202
+ var ToolExecutor = class {
2203
+ /** 工具注册表(只读引用) */
2204
+ #registry;
2205
+ /** 权限门(只读引用) */
2206
+ #gate;
2207
+ /** 工具执行上下文(每次执行都共用) */
2208
+ #baseCtx;
2209
+ /** 并行执行的最大同时执行数(默认 8) */
2210
+ #maxParallel;
2211
+ /**
2212
+ * 构造一个 ToolExecutor。
2213
+ *
2214
+ * @param deps — 注入依赖(registry / gate / baseCtx / maxParallel)
2215
+ * @pure 仅保存引用,不调用任何外部 IO
2216
+ */
2217
+ constructor(deps) {
2218
+ this.#registry = deps.registry;
2219
+ this.#gate = deps.gate;
2220
+ this.#baseCtx = {
2221
+ cwd: deps.baseCtx.cwd,
2222
+ signal: deps.baseCtx.signal,
2223
+ writeRoots: deps.baseCtx.writeRoots,
2224
+ ...deps.baseCtx.timeout !== void 0 ? { timeout: deps.baseCtx.timeout } : {}
2225
+ };
2226
+ this.#maxParallel = deps.maxParallel ?? 8;
2227
+ }
2228
+ /**
2229
+ * 执行一批工具调用。
2230
+ *
2231
+ * 自动选择并行 / 串行:
2232
+ * - 全部为只读工具 且 calls.length > 1:分批并行(每批最多 #maxParallel 个)
2233
+ * - 其他:严格串行
2234
+ *
2235
+ * @param calls — LLM 决定的本轮工具调用列表
2236
+ * @returns items(全部结果)+ records(仅失败项,用于风暴检测)
2237
+ *
2238
+ * @sideEffect 调用 registry / gate / tool.execute;不修改 Session.messages
2239
+ */
2240
+ async executeBatch(calls) {
2241
+ if (calls.length === 0) return { items: [], records: [] };
2242
+ if (this.#allReadOnly(calls) && calls.length > 1) {
2243
+ return this.#executeParallel(calls);
2244
+ }
2245
+ return this.#executeSequential(calls);
2246
+ }
2247
+ // -------------------------------------------------------------------------
2248
+ // 内部
2249
+ // -------------------------------------------------------------------------
2250
+ /**
2251
+ * 判定这一批调用是否全部为只读工具。
2252
+ * 工具未找到时按"只读"处理(不会因为没注册就拒绝并行)。
2253
+ *
2254
+ * @pure 不修改任何状态
2255
+ */
2256
+ #allReadOnly(calls) {
2257
+ return calls.every((tc) => {
2258
+ const tool = this.#registry.get(tc.name);
2259
+ return tool ? isReadOnly(tool.kind) : true;
2260
+ });
2261
+ }
2262
+ /**
2263
+ * 并行执行:按 #maxParallel 分批,每批内 Promise.all。
2264
+ * 用于"全部只读 + 多于 1 个"的场景。
2265
+ *
2266
+ * @param calls — 全部只读的工具调用
2267
+ * @returns items + records
2268
+ *
2269
+ * @sideEffect 调用 tool.execute
2270
+ */
2271
+ async #executeParallel(calls) {
2272
+ const items = [];
2273
+ const records = [];
2274
+ for (let i = 0; i < calls.length; i += this.#maxParallel) {
2275
+ const batch = calls.slice(i, i + this.#maxParallel);
2276
+ const results = await Promise.all(batch.map((tc) => this.#executeOne(tc)));
2277
+ for (const r of results) {
2278
+ items.push(r.item);
2279
+ if (!r.record.success) records.push(r.record);
2280
+ }
2281
+ }
2282
+ return { items, records };
2283
+ }
2284
+ /**
2285
+ * 串行执行:按 calls 顺序逐个执行。
2286
+ * 用于含写工具的场景(保证副作用顺序与 LLM 给出顺序一致)。
2287
+ *
2288
+ * @param calls — 工具调用列表
2289
+ * @returns items + records
2290
+ *
2291
+ * @sideEffect 按序调用 tool.execute
2292
+ */
2293
+ async #executeSequential(calls) {
2294
+ const items = [];
2295
+ const records = [];
2296
+ for (const tc of calls) {
2297
+ const r = await this.#executeOne(tc);
2298
+ items.push(r.item);
2299
+ if (!r.record.success) records.push(r.record);
2300
+ }
2301
+ return { items, records };
2302
+ }
2303
+ /**
2304
+ * 执行单个工具调用,包含查找 / 参数解析 / 权限门 / 写工具 preview / 异常捕获。
2305
+ *
2306
+ * 失败映射:
2307
+ * - 工具不存在或被禁用 → `TOOL_NOT_FOUND`
2308
+ * - 权限门拒绝 → `GATE_DENIED`
2309
+ * - 工具 execute 抛错 → `EXECUTION_ERROR`
2310
+ *
2311
+ * @param tc — 单个工具调用(含 id / name / arguments 字符串)
2312
+ * @returns { item, record } — item 喂回 Session;record 用于风暴检测
2313
+ *
2314
+ * @sideEffect 调 tool.execute、可能调 tool.preview;不抛错给调用方
2315
+ */
2316
+ async #executeOne(tc) {
2317
+ const toolName = tc.name;
2318
+ const timestamp = Date.now();
2319
+ const tool = this.#registry.get(toolName);
2320
+ if (!tool) {
2321
+ const errMsg = `\u5DE5\u5177 "${toolName}" \u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u7981\u7528`;
2322
+ return {
2323
+ item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "TOOL_NOT_FOUND" } },
2324
+ record: { name: toolName, success: false, error: "TOOL_NOT_FOUND", timestamp }
2325
+ };
2326
+ }
2327
+ let toolArgs;
2328
+ try {
2329
+ toolArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
2330
+ } catch {
2331
+ toolArgs = {};
2332
+ }
2333
+ const gateResult = await this.#gate.check(toolName, toolArgs);
2334
+ if (!gateResult) {
2335
+ const errMsg = `\u5DE5\u5177 "${toolName}" \u88AB\u6743\u9650\u95E8\u62D2\u7EDD`;
2336
+ return {
2337
+ item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "GATE_DENIED" } },
2338
+ record: { name: toolName, success: false, error: "GATE_DENIED", timestamp }
2339
+ };
2340
+ }
2341
+ if (!isReadOnly(tool.kind)) {
2342
+ const maybePreview = tool.preview;
2343
+ if (typeof maybePreview === "function") {
2344
+ try {
2345
+ await maybePreview(toolArgs, this.#baseCtx);
2346
+ } catch {
2347
+ }
2348
+ }
2349
+ }
2350
+ try {
2351
+ const result = await this.#invokeTool(tool, toolArgs);
2352
+ return {
2353
+ item: { name: toolName, callId: tc.id, result },
2354
+ record: { name: toolName, success: result.success, error: result.error, timestamp }
2355
+ };
2356
+ } catch (err) {
2357
+ const message = err instanceof Error ? err.message : String(err);
2358
+ const errorResult = {
2359
+ success: false,
2360
+ data: `\u5DE5\u5177 "${toolName}" \u6267\u884C\u5F02\u5E38\uFF1A${message}`,
2361
+ error: "EXECUTION_ERROR"
2362
+ };
2363
+ return {
2364
+ item: { name: toolName, callId: tc.id, result: errorResult },
2365
+ record: { name: toolName, success: false, error: "EXECUTION_ERROR", timestamp }
2366
+ };
2367
+ }
2368
+ }
2369
+ /**
2370
+ * 真正调 tool.execute 的薄包装(拆出来便于以后插入横切关注点,如重试 / 计时)。
2371
+ *
2372
+ * @param tool — 类型擦除后的工具
2373
+ * @param args — 解析后的参数(unknown)
2374
+ * @returns 工具的 ToolResult
2375
+ *
2376
+ * @sideEffect 调 tool.execute
2377
+ */
2378
+ async #invokeTool(tool, args) {
2379
+ return tool.execute(args, this.#baseCtx);
2380
+ }
2381
+ };
2382
+
2383
+ // src/agent/storm-detector.ts
2384
+ var StormDetector = class {
2385
+ /** 触发风暴需要的最少连续失败次数(默认 3) */
2386
+ #threshold;
2387
+ /**
2388
+ * 构造一个 StormDetector。
2389
+ *
2390
+ * @param opts — 判定选项(threshold)
2391
+ * @pure 仅保存配置
2392
+ */
2393
+ constructor(opts = {}) {
2394
+ this.#threshold = opts.threshold ?? 3;
2395
+ }
2396
+ /**
2397
+ * 判定本轮是否应该中断风暴。
2398
+ *
2399
+ * @param recentRecords — 历史的失败记录(不含成功的;通常来自 ToolExecutor 的 records)
2400
+ * @param currentCalls — 本轮 LLM 决定调用的工具
2401
+ * @returns true 表示触发风暴,调用方应放弃本轮工具执行并提示切换策略
2402
+ *
2403
+ * @pure 不修改任何状态,仅做比较判断
2404
+ */
2405
+ shouldBreak(recentRecords, currentCalls) {
2406
+ if (recentRecords.length < this.#threshold) return false;
2407
+ const lastN = recentRecords.slice(-this.#threshold);
2408
+ if (lastN.length < this.#threshold) return false;
2409
+ const first = lastN[0];
2410
+ const allSame = lastN.every((r) => r.name === first.name && r.error === first.error && !r.success);
2411
+ if (!allSame) return false;
2412
+ return currentCalls.some((tc) => tc.name === first.name);
2413
+ }
2414
+ };
2415
+
2416
+ // src/agent/reflector.ts
2417
+ var Reflector = class {
2418
+ /** 触发 R1 需要的最小连续失败次数 */
2419
+ #repeatThreshold;
2420
+ /** 注入到 prompt 的最大反射数 */
2421
+ #maxReflections;
2422
+ /** 单条 hint 的最大字符数 */
2423
+ #maxHintChars;
2424
+ /**
2425
+ * 构造一个 Reflector。
2426
+ *
2427
+ * @param options — 限流与阈值配置
2428
+ * @pure 仅保存配置
2429
+ */
2430
+ constructor(options = {}) {
2431
+ this.#repeatThreshold = options.repeatThreshold ?? 2;
2432
+ this.#maxReflections = options.maxReflections ?? 5;
2433
+ this.#maxHintChars = options.maxHintChars ?? 800;
2434
+ }
2435
+ /**
2436
+ * 分析一批工具执行结果,返回要注入的反射列表。
2437
+ *
2438
+ * 行为:
2439
+ * - 成功项直接跳过
2440
+ * - 失败项按 R1 → R2 → R3 → R4 顺序检查,先命中先用,单条目最多产生 1 条
2441
+ * - 总数超过 maxReflections 时截断到前 N 条
2442
+ *
2443
+ * @param items — 本轮工具执行结果列表
2444
+ * @param ctx — 上下文(writeRoots / cwd,用于生成 hint)
2445
+ * @returns 反射列表(可能为空;最多 maxReflections 条)
2446
+ *
2447
+ * @pure 不修改任何状态
2448
+ */
2449
+ analyze(items, ctx) {
2450
+ const reflections = [];
2451
+ for (const item of items) {
2452
+ if (item.result.success) continue;
2453
+ const r = this.#ruleRepeatedFailure(item) ?? this.#ruleFileNotFound(item) ?? this.#rulePermissionDenied(item) ?? this.#ruleOutOfWriteRoot(item, ctx.writeRoots);
2454
+ if (r) reflections.push(r);
2455
+ if (reflections.length >= this.#maxReflections) break;
2456
+ }
2457
+ return reflections;
2458
+ }
2459
+ /**
2460
+ * 把反射列表拼成"反思"section,追加到 system prompt 末尾。
2461
+ *
2462
+ * 输出格式:
2463
+ * ```
2464
+ * {原 prompt}
2465
+ *
2466
+ * ## ⚠️ 上一轮工具调用的反思
2467
+ * - 工具 `<tool>`:<hint>
2468
+ * - 工具 `<tool>`:<hint>
2469
+ * ```
2470
+ *
2471
+ * @param systemPrompt — 原始 system prompt
2472
+ * @param reflections — 反射列表(空数组时原样返回)
2473
+ * @returns 拼装后的新 prompt
2474
+ *
2475
+ * @pure 不修改入参
2476
+ */
2477
+ injectIntoPrompt(systemPrompt, reflections) {
2478
+ if (reflections.length === 0) return systemPrompt;
2479
+ const lines = reflections.map((r) => {
2480
+ const hint = this.#truncate(r.hint);
2481
+ return `- \u5DE5\u5177 \`${r.toolName}\` \u5931\u8D25\uFF1A${hint}`;
2482
+ });
2483
+ const section = [
2484
+ "## \u26A0\uFE0F \u4E0A\u4E00\u8F6E\u5DE5\u5177\u8C03\u7528\u7684\u53CD\u601D",
2485
+ "\u4EE5\u4E0B\u662F\u672C\u8F6E\u5DE5\u5177\u6267\u884C\u5931\u8D25\u7684\u539F\u56E0\u5206\u6790\u3002\u8BF7\u5728\u4E0B\u4E00\u6B21\u8C03\u7528\u524D\u5148\u89E3\u51B3\u8FD9\u4E9B\u95EE\u9898\uFF0C\u4E0D\u8981\u91CD\u590D\u76F8\u540C\u7684\u9519\u8BEF\u8C03\u7528\u3002",
2486
+ ...lines
2487
+ ].join("\n");
2488
+ return systemPrompt + "\n\n" + section;
2489
+ }
2490
+ // -------------------------------------------------------------------------
2491
+ // 规则实现(按 R1 → R2 → R3 → R4 顺序)
2492
+ // -------------------------------------------------------------------------
2493
+ /**
2494
+ * R1 连续失败:同一工具连续 ≥#repeatThreshold 次相同错误码失败。
2495
+ *
2496
+ * @pure 仅读入参
2497
+ */
2498
+ #ruleRepeatedFailure(item) {
2499
+ const recent = item.recentSameTool;
2500
+ if (recent.length < this.#repeatThreshold) return null;
2501
+ const lastN = recent.slice(-this.#repeatThreshold);
2502
+ const first = lastN[0];
2503
+ if (!lastN.every((r) => !r.success && r.error === first.error)) {
2504
+ return null;
2505
+ }
2506
+ const errText = first.error ?? "\u672A\u77E5\u9519\u8BEF";
2507
+ return {
2508
+ category: "repeated_failure",
2509
+ toolName: item.name,
2510
+ hint: `\u4F60\u5DF2\u8FDE\u7EED ${this.#repeatThreshold} \u6B21\u8C03\u7528 \`${item.name}\` \u5931\u8D25\uFF08\u9519\u8BEF\uFF1A${errText}\uFF09\u3002\u8BF7\u5148 \`read_file\` \u770B\u6E05\u73B0\u573A\u6216\u6362\u5176\u4ED6\u5DE5\u5177\uFF0C\u4E0D\u8981\u7EE7\u7EED\u540C\u6837\u7684\u9519\u8BEF\u8C03\u7528\u3002`
2511
+ };
2512
+ }
2513
+ /**
2514
+ * R2 文件不存在:TOOL_NOT_FOUND 错误码 或 data 含 ENOENT / not found / No such file。
2515
+ *
2516
+ * @pure 仅读入参
2517
+ */
2518
+ #ruleFileNotFound(item) {
2519
+ const isToolNotFound = item.result.error === "TOOL_NOT_FOUND";
2520
+ const data = item.result.data ?? "";
2521
+ const hasKeyword = data.includes("ENOENT") || data.includes("No such file") || /\bnot found\b/i.test(data);
2522
+ if (!isToolNotFound && !hasKeyword) return null;
2523
+ return {
2524
+ category: "file_not_found",
2525
+ toolName: item.name,
2526
+ hint: `\`${item.name}\` \u5931\u8D25\uFF1A\u6587\u4EF6\u4E0D\u5B58\u5728\u3002\u8BF7\u5148\u7528 \`ls\` / \`glob\` \u770B\u4E00\u4E0B\u76EE\u5F55\u7ED3\u6784\uFF0C\u6216\u786E\u8BA4\u8DEF\u5F84\u62FC\u5199\uFF08\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u89C1 system prompt\uFF09\u3002`
2527
+ };
2528
+ }
2529
+ /**
2530
+ * R3 权限拒绝:GATE_DENIED 错误码 或 data 含 EACCES / permission / denied(不区分大小写)。
2531
+ *
2532
+ * @pure 仅读入参
2533
+ */
2534
+ #rulePermissionDenied(item) {
2535
+ const isGateDenied = item.result.error === "GATE_DENIED";
2536
+ const data = item.result.data ?? "";
2537
+ const hasKeyword = data.includes("EACCES") || /\bpermission\b/i.test(data) || /\bdenied\b/i.test(data);
2538
+ if (!isGateDenied && !hasKeyword) return null;
2539
+ return {
2540
+ category: "permission_denied",
2541
+ toolName: item.name,
2542
+ hint: `\`${item.name}\` \u88AB\u6743\u9650\u7CFB\u7EDF\u62D2\u7EDD\u3002\u8BF7\u68C0\u67E5\uFF1A1) \u8DEF\u5F84\u662F\u5426\u5728\u5141\u8BB8\u6839\u76EE\u5F55\u5185\uFF1B2) \u662F\u5426\u9700\u8981\u5207\u6362\u6388\u6743\u6A21\u5F0F\uFF1B3) \u6539\u7528\u5176\u4ED6\u4E0D\u9700\u8981\u6743\u9650\u7684\u5DE5\u5177\u3002`
2543
+ };
2544
+ }
2545
+ /**
2546
+ * R4 写根外:kind ∈ Edit/Delete/Move 且失败。
2547
+ *
2548
+ * @pure 仅读入参
2549
+ */
2550
+ #ruleOutOfWriteRoot(item, writeRoots) {
2551
+ const isWriteKind = item.kind === "edit" /* Edit */ || item.kind === "delete" /* Delete */ || item.kind === "move" /* Move */;
2552
+ if (!isWriteKind) return null;
2553
+ const primary = writeRoots[0] ?? "<\u672A\u914D\u7F6E>";
2554
+ return {
2555
+ category: "out_of_write_root",
2556
+ toolName: item.name,
2557
+ hint: `\`${item.name}\` \u662F\u5199\u64CD\u4F5C\u4F46\u76EE\u6807\u8DEF\u5F84\u4E0D\u5728\u5141\u8BB8\u7684\u6839\u76EE\u5F55\u5185\u3002\u8BF7\u6539\u7528 \`${primary}\` \u4E0B\u7684\u8DEF\u5F84\uFF0C\u6216\u8C03\u6574\u9879\u76EE\u7684 \`writeRoots\` \u914D\u7F6E\u3002`
2558
+ };
2559
+ }
2560
+ /**
2561
+ * 截断超长 hint,末尾加省略号。
2562
+ *
2563
+ * @pure 字符串处理
2564
+ */
2565
+ #truncate(text) {
2566
+ if (text.length <= this.#maxHintChars) return text;
2567
+ return text.slice(0, this.#maxHintChars - 3) + "...";
2568
+ }
2569
+ };
2570
+
2571
+ // src/agent/tool-definitions.ts
2572
+ function buildToolDefinitions(registry2, mode) {
2573
+ const tools = mode === "plan" ? registry2.listReadTools() : registry2.list();
2574
+ return tools.map((t) => ({
2575
+ type: "function",
2576
+ function: {
2577
+ name: t.name,
2578
+ description: t.description,
2579
+ parameters: t.parameters
2580
+ }
2581
+ }));
2582
+ }
2583
+
2117
2584
  // src/checkpoint/git-checkpoint.ts
2118
2585
  import { execFile } from "child_process";
2119
2586
  import { promisify } from "util";
@@ -2279,7 +2746,9 @@ function isENOENT(err) {
2279
2746
 
2280
2747
  // src/logger/logger.ts
2281
2748
  import { mkdir as mkdir5, appendFile } from "fs/promises";
2282
- import { join as join5, basename } from "path";
2749
+ import { fileURLToPath } from "url";
2750
+ import { join as join5, basename, sep } from "path";
2751
+ var LOGGER_DIR = "logger";
2283
2752
  var MAX_DATA_LEN = 2e3;
2284
2753
  var MAX_QUEUE = 500;
2285
2754
  function defaultLogsDir() {
@@ -2331,14 +2800,19 @@ var ConversationLogger = class _ConversationLogger {
2331
2800
  /**
2332
2801
  * 记录一个事件。
2333
2802
  *
2334
- * 内部将事件序列化为 JSON 并追加到日志文件。
2803
+ * 内部将事件序列化为 pretty JSON 并以多行块的形式追加到日志文件。
2335
2804
  * 写入操作串行排队,保证事件顺序。调用方无需 await。
2336
2805
  */
2337
2806
  log(event) {
2338
2807
  if (!this.#enabled || this.#closed) return;
2339
2808
  if (this.#queueLen >= MAX_QUEUE) return;
2809
+ const enriched = {
2810
+ ...event,
2811
+ time: event.time ?? formatTime(event.ts),
2812
+ loc: event.loc ?? captureCallerLocation()
2813
+ };
2340
2814
  this.#queueLen++;
2341
- this.#queue = this.#queue.then(() => this.#writeLine(event)).catch(() => {
2815
+ this.#queue = this.#queue.then(() => this.#writeBlock(enriched)).catch(() => {
2342
2816
  }).finally(() => this.#queueLen--);
2343
2817
  }
2344
2818
  /**
@@ -2353,12 +2827,12 @@ var ConversationLogger = class _ConversationLogger {
2353
2827
  });
2354
2828
  _ConversationLogger.#instances.delete(this);
2355
2829
  }
2356
- /** 将一行 JSON 写入日志文件 */
2357
- async #writeLine(event) {
2830
+ /** 将一条事件以多行块的形式写入日志文件 */
2831
+ async #writeBlock(event) {
2358
2832
  const dir = join5(this.#logPath, "..");
2359
2833
  await mkdir5(dir, { recursive: true });
2360
- const line = JSON.stringify(event) + "\n";
2361
- await appendFile(this.#logPath, line, "utf-8");
2834
+ const block = formatBlock(event);
2835
+ await appendFile(this.#logPath, block, "utf-8");
2362
2836
  }
2363
2837
  // -----------------------------------------------------------------------
2364
2838
  // 便捷方法 — 封装常见事件类型,减少调用方样板代码
@@ -2418,28 +2892,123 @@ var ConversationLogger = class _ConversationLogger {
2418
2892
  logSessionEnd(elapsed) {
2419
2893
  this.log({ ts: Date.now(), type: "session_end", elapsed });
2420
2894
  }
2895
+ /** 记录反射(工具失败归因注入到下一轮 prompt 时) */
2896
+ logReflections(items) {
2897
+ this.log({ ts: Date.now(), type: "reflection", items });
2898
+ }
2421
2899
  };
2422
2900
  function truncate(text) {
2423
2901
  if (text.length <= MAX_DATA_LEN) return text;
2424
2902
  return text.slice(0, MAX_DATA_LEN) + `...[\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${text.length}]`;
2425
2903
  }
2904
+ function formatTime(ts) {
2905
+ const d = new Date(ts);
2906
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
2907
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
2908
+ }
2909
+ var projectRootCache = null;
2910
+ function getProjectRoot() {
2911
+ if (projectRootCache !== null) return projectRootCache;
2912
+ try {
2913
+ const here = fileURLToPath(import.meta.url);
2914
+ const segments = here.split(sep);
2915
+ const srcIdx = segments.lastIndexOf("src");
2916
+ projectRootCache = srcIdx > 0 ? segments.slice(0, srcIdx).join(sep) : here;
2917
+ } catch {
2918
+ projectRootCache = "";
2919
+ }
2920
+ return projectRootCache;
2921
+ }
2922
+ var STACK_FRAME_RE = /\s+at\s+.+?\((.+):(\d+):\d+\)|\s+at\s+(.+):(\d+):\d+/;
2923
+ function captureCallerLocation() {
2924
+ let stack;
2925
+ try {
2926
+ const err = new Error();
2927
+ Error.captureStackTrace?.(err, captureCallerLocation);
2928
+ stack = err.stack;
2929
+ } catch {
2930
+ return { file: "<unknown>", line: 0 };
2931
+ }
2932
+ if (!stack) return { file: "<unknown>", line: 0 };
2933
+ const root = getProjectRoot();
2934
+ const lines = stack.split("\n");
2935
+ for (const raw of lines) {
2936
+ const m = STACK_FRAME_RE.exec(raw);
2937
+ if (!m) continue;
2938
+ const filePath = m[1] ?? m[3] ?? "";
2939
+ const lineNo = Number(m[2] ?? m[4] ?? "0");
2940
+ if (!filePath) continue;
2941
+ const normalized = filePath.replace(/^file:\/\/\//, "").replace(/^file:\/\//, "");
2942
+ if (normalized.includes(`${sep}${LOGGER_DIR}${sep}`) || normalized.endsWith(`${sep}${LOGGER_DIR}`)) {
2943
+ continue;
2944
+ }
2945
+ if (normalized.includes(`${sep}node_modules${sep}`)) continue;
2946
+ let rel = normalized;
2947
+ if (root && normalized.startsWith(root)) {
2948
+ rel = normalized.slice(root.length).replace(/^[/\\]/, "") || basename(normalized);
2949
+ }
2950
+ return { file: rel || basename(normalized), line: lineNo };
2951
+ }
2952
+ return { file: "<unknown>", line: 0 };
2953
+ }
2954
+ var SEPARATOR_WIDTH = 80;
2955
+ var SEPARATOR = "\u2500".repeat(SEPARATOR_WIDTH);
2956
+ function formatBlock(event) {
2957
+ const header = `[${event.time}] [${event.type}] @ ${event.loc.file}:${event.loc.line}`;
2958
+ const body = JSON.stringify(event, null, 2);
2959
+ return `${SEPARATOR}
2960
+ ${header}
2961
+ ${body}
2962
+
2963
+ `;
2964
+ }
2426
2965
 
2427
2966
  // src/agent/index.ts
2428
2967
  var Session = class _Session {
2968
+ /** 当前会话的消息历史(含 system/user/assistant/tool),可外部只读 */
2429
2969
  #messages = [];
2970
+ /** LLM Provider(DeepSeek 等) */
2430
2971
  #provider;
2972
+ /** 工具注册表(所有可用工具) */
2431
2973
  #toolRegistry;
2974
+ /** 成本追踪器(今日 + 本会话) */
2432
2975
  #costTracker;
2976
+ /** 归一化后的构造选项(带默认值) */
2433
2977
  #options;
2978
+ /** 中止信号控制器(abort() 时触发,传递给 LLM 和工具) */
2434
2979
  #abortController = new AbortController();
2980
+ /** 会话唯一 ID(UUID) */
2435
2981
  #sessionId;
2982
+ /** 持久化存储;传 false 时为 null */
2436
2983
  #store;
2984
+ /** 会话创建时间(毫秒) */
2437
2985
  #createdAt;
2986
+ /** 节流持久化定时器(500ms debounce) */
2438
2987
  #persistTimer = null;
2988
+ /** user 消息 → Checkpoint 映射(仅给 user 消息建点) */
2439
2989
  #checkpoints = /* @__PURE__ */ new Map();
2990
+ /** 最近的失败记录(仅失败的,用于风暴检测) */
2440
2991
  #stormRecords = [];
2992
+ /** 当前会话模式:code(默认)/ plan(只读) */
2441
2993
  #mode = "code";
2994
+ /** 对话日志记录器(写入 .dskcode/logs/) */
2442
2995
  #logger;
2996
+ /** 风暴检测器(连续 3 次同工具同错码失败时中断本轮) */
2997
+ #stormDetector;
2998
+ /** 失败归因 Reflector(将本轮失败原因拼到下一轮 prompt;null 表示已关闭) */
2999
+ #reflector;
3000
+ /** 本轮工具执行结果(仅在 chat() 循环内使用;用于下一轮注入 reflection) */
3001
+ #lastRoundResults = null;
3002
+ /**
3003
+ * 构造一个 Session。
3004
+ *
3005
+ * @param provider — LLM Provider 实例(必须)
3006
+ * @param tools — 工具列表或已初始化的 ToolRegistry(默认空)
3007
+ * @param costTracker — 成本追踪器(默认新建)
3008
+ * @param options — 会话选项(详见 SessionOptions)
3009
+ *
3010
+ * @sideEffect 创建 .dskcode/logs/ 下的日志文件并写入 session_start 事件
3011
+ */
2443
3012
  constructor(provider, tools = [], costTracker, options) {
2444
3013
  this.#provider = provider;
2445
3014
  if (tools instanceof ToolRegistry) {
@@ -2458,7 +3027,8 @@ var Session = class _Session {
2458
3027
  gate: options?.gate ?? new AlwaysAllowGate(),
2459
3028
  writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()],
2460
3029
  enableCheckpoint: options?.enableCheckpoint ?? true,
2461
- enableLog: options?.enableLog ?? true
3030
+ enableLog: options?.enableLog ?? true,
3031
+ enableReflection: options?.enableReflection !== false
2462
3032
  };
2463
3033
  this.#sessionId = options?.sessionId ?? SessionStore.newId();
2464
3034
  this.#store = options?.store === false ? null : options?.store ?? new SessionStore();
@@ -2467,38 +3037,73 @@ var Session = class _Session {
2467
3037
  enabled: this.#options.enableLog
2468
3038
  });
2469
3039
  this.#logger.logSessionStart(this.#sessionId, this.#options.cwd, this.#provider.model(), this.#mode);
3040
+ this.#stormDetector = new StormDetector({ threshold: 3 });
3041
+ this.#reflector = this.#options.enableReflection ? new Reflector() : null;
2470
3042
  }
3043
+ /** 当前会话的完整消息历史(只读视图) */
2471
3044
  get messages() {
2472
3045
  return this.#messages;
2473
3046
  }
3047
+ /** 本次会话累计成本(人民币元) */
2474
3048
  get accumulatedCost() {
2475
3049
  return this.#costTracker.sessionTotalCost;
2476
3050
  }
3051
+ /** 成本追踪器(含今日总成本、会话成本、模型维度统计) */
2477
3052
  get costTracker() {
2478
3053
  return this.#costTracker;
2479
3054
  }
3055
+ /** 当前模型标识(如 "deepseek-v4-flash") */
2480
3056
  get model() {
2481
3057
  return this.#provider.model();
2482
3058
  }
3059
+ /** 工具注册表(可外部读 / 运行时 register) */
2483
3060
  get toolRegistry() {
2484
3061
  return this.#toolRegistry;
2485
3062
  }
3063
+ /** 当前模式:"code" | "plan" */
2486
3064
  get mode() {
2487
3065
  return this.#mode;
2488
3066
  }
3067
+ /** 会话 ID(UUID) */
2489
3068
  get id() {
2490
3069
  return this.#sessionId;
2491
3070
  }
3071
+ /** 持久化存储实例(禁用持久化时为 null) */
2492
3072
  get store() {
2493
3073
  return this.#store;
2494
3074
  }
3075
+ /** 会话创建时间戳(毫秒) */
2495
3076
  get createdAt() {
2496
3077
  return this.#createdAt;
2497
3078
  }
3079
+ /**
3080
+ * 切换会话模式。
3081
+ *
3082
+ * @param mode — "code":全部工具;"plan":只暴露只读工具
3083
+ * @returns 设置后的模式(便于链式调用)
3084
+ */
2498
3085
  setMode(mode) {
2499
3086
  this.#mode = mode;
2500
3087
  return this.#mode;
2501
3088
  }
3089
+ /**
3090
+ * chat() — 接收一轮用户输入,串起 LLM 调用 / 工具执行 / 风暴中断 / 持久化。
3091
+ *
3092
+ * 主循环每轮做:
3093
+ * 1. 构建 system prompt + 裁剪消息 + 拼装 apiMessages
3094
+ * 2. 调 provider.chat() 拿流式 chunks,向外 yield text_delta / usage 事件
3095
+ * 3. 若本轮有 tool_calls:
3096
+ * a. 先调 StormDetector.shouldBreak 判定是否中断风暴
3097
+ * b. 调 ToolExecutor.executeBatch 执行工具
3098
+ * c. 把工具结果 yield 出去并写入 messages
3099
+ * 4. 持续到 LLM 不再调工具 或 达到 maxToolRounds
3100
+ *
3101
+ * @param userInput — 用户本轮输入
3102
+ * @param opts — 透传给 provider.chat() 的 ChatOptions(thinking / tool_choice 等)
3103
+ * @yields AgentEvent — text_delta / tool_calls / tool_result / usage / done / error
3104
+ *
3105
+ * @sideEffect 写入 messages、logger、costTracker、可能触发 checkpoint / persist
3106
+ */
2502
3107
  async *chat(userInput, opts) {
2503
3108
  this.#messages.push({ role: "user", content: userInput });
2504
3109
  this.#logger.logUserMessage(userInput);
@@ -2512,9 +3117,23 @@ var Session = class _Session {
2512
3117
  }
2513
3118
  const startTime = Date.now();
2514
3119
  let toolRounds = 0;
3120
+ const toolExecutor = this.#buildToolExecutor();
2515
3121
  try {
2516
3122
  while (toolRounds < this.#options.maxToolRounds) {
2517
- const systemPrompt = this.#buildSystemPrompt();
3123
+ let systemPrompt = this.#buildSystemPrompt();
3124
+ if (this.#reflector && this.#lastRoundResults) {
3125
+ const reflections = this.#reflector.analyze(this.#lastRoundResults, {
3126
+ writeRoots: this.#options.writeRoots,
3127
+ cwd: this.#options.cwd
3128
+ });
3129
+ if (reflections.length > 0) {
3130
+ systemPrompt = this.#reflector.injectIntoPrompt(systemPrompt, reflections);
3131
+ this.#logger.logReflections(
3132
+ reflections.map((r) => ({ category: r.category, toolName: r.toolName, hint: r.hint }))
3133
+ );
3134
+ }
3135
+ this.#lastRoundResults = null;
3136
+ }
2518
3137
  const [trimmed] = trimMessages(
2519
3138
  [...this.#messages],
2520
3139
  {
@@ -2525,7 +3144,7 @@ var Session = class _Session {
2525
3144
  }
2526
3145
  );
2527
3146
  const apiMessages = buildApiMessages(systemPrompt, trimmed);
2528
- const toolDefs = this.#buildToolDefinitions();
3147
+ const toolDefs = buildToolDefinitions(this.#toolRegistry, this.#mode);
2529
3148
  const stream = this.#provider.chat(apiMessages, {
2530
3149
  signal: this.#abortController.signal,
2531
3150
  tools: toolDefs.length > 0 ? toolDefs : void 0,
@@ -2534,21 +3153,69 @@ var Session = class _Session {
2534
3153
  responseFormat: opts?.responseFormat,
2535
3154
  toolChoice: opts?.toolChoice
2536
3155
  });
3156
+ const modelId = this.#provider.model();
2537
3157
  let accumulatedText = "";
2538
3158
  let lastUsage;
2539
3159
  let lastToolCalls;
2540
3160
  let _lastFinishReason = null;
3161
+ let estimatedInputTokens = 0;
3162
+ for (const m of apiMessages) {
3163
+ estimatedInputTokens += this.#provider.countTokens(m.content) + 10;
3164
+ }
3165
+ let lastEmitMs = 0;
3166
+ const EST_EMIT_INTERVAL_MS = 300;
2541
3167
  for await (const chunk of stream) {
2542
3168
  if (chunk.content) {
2543
3169
  accumulatedText += chunk.content;
2544
3170
  yield { type: "text_delta", content: chunk.content };
3171
+ const now = Date.now();
3172
+ if (now - lastEmitMs >= EST_EMIT_INTERVAL_MS) {
3173
+ lastEmitMs = now;
3174
+ const estOutput = this.#provider.countTokens(accumulatedText);
3175
+ const estUsage = {
3176
+ promptTokens: estimatedInputTokens,
3177
+ completionTokens: estOutput
3178
+ };
3179
+ const estCost = calculateCost(estUsage, modelId);
3180
+ yield {
3181
+ type: "usage",
3182
+ usage: estUsage,
3183
+ model: modelId,
3184
+ cost: estCost.totalCost,
3185
+ estimated: true
3186
+ };
3187
+ }
2545
3188
  }
2546
3189
  if (chunk.toolCalls && chunk.toolCalls.length > 0) lastToolCalls = chunk.toolCalls;
2547
- if (chunk.usage) lastUsage = chunk.usage;
3190
+ if (chunk.usage) {
3191
+ lastUsage = chunk.usage;
3192
+ const realCost = calculateCost(chunk.usage, modelId);
3193
+ yield {
3194
+ type: "usage",
3195
+ usage: chunk.usage,
3196
+ model: modelId,
3197
+ cost: realCost.totalCost,
3198
+ estimated: false
3199
+ };
3200
+ }
2548
3201
  if (chunk.finishReason) _lastFinishReason = chunk.finishReason;
2549
3202
  }
3203
+ if (accumulatedText && !lastUsage) {
3204
+ const estOutput = this.#provider.countTokens(accumulatedText);
3205
+ const estUsage = {
3206
+ promptTokens: estimatedInputTokens,
3207
+ completionTokens: estOutput
3208
+ };
3209
+ const estCost = calculateCost(estUsage, modelId);
3210
+ yield {
3211
+ type: "usage",
3212
+ usage: estUsage,
3213
+ model: modelId,
3214
+ cost: estCost.totalCost,
3215
+ estimated: true
3216
+ };
3217
+ }
2550
3218
  if (lastUsage) {
2551
- const modelId = this.#provider.model();
2552
3219
  this.#costTracker.record(lastUsage, modelId);
2553
3220
  const cost = calculateCost(lastUsage, modelId);
2554
3221
  this.#logger.logUsage(
@@ -2570,7 +3237,7 @@ var Session = class _Session {
2570
3237
  for (const tc of lastToolCalls) {
2571
3238
  this.#logger.logToolCall(tc.name, tc.id, tc.arguments, toolRounds);
2572
3239
  }
2573
- const stormBroken = this.#checkStormBreak(lastToolCalls);
3240
+ const stormBroken = this.#stormDetector.shouldBreak(this.#stormRecords, lastToolCalls);
2574
3241
  if (stormBroken) {
2575
3242
  const stormMsg = "\n\u26A0\uFE0F \u540C\u4E00\u5DE5\u5177\u91CD\u590D\u51FA\u9519\uFF0C\u5DF2\u5F3A\u5236\u5207\u6362\u7B56\u7565\n";
2576
3243
  yield { type: "text_delta", content: stormMsg };
@@ -2580,8 +3247,18 @@ var Session = class _Session {
2580
3247
  toolRounds++;
2581
3248
  continue;
2582
3249
  }
2583
- const results = await this.#executeBatch(lastToolCalls);
3250
+ const results = await toolExecutor.executeBatch(lastToolCalls);
2584
3251
  this.#stormRecords = results.records;
3252
+ console.log("results", results);
3253
+ this.#lastRoundResults = results.items.map((it) => {
3254
+ const tool = this.#toolRegistry.get(it.name);
3255
+ return {
3256
+ name: it.name,
3257
+ result: it.result,
3258
+ kind: tool?.kind ?? "other" /* Other */,
3259
+ recentSameTool: results.records.filter((r) => r.name === it.name).map((r) => ({ success: r.success, error: r.error }))
3260
+ };
3261
+ });
2585
3262
  for (const item of results.items) {
2586
3263
  yield { type: "tool_result", name: item.name, result: item.result };
2587
3264
  this.#logger.logToolResult(
@@ -2626,98 +3303,15 @@ ${item.result.diff.patch}`;
2626
3303
  });
2627
3304
  yield { type: "done", elapsed };
2628
3305
  }
2629
- async #executeBatch(calls) {
2630
- const toolCtx = {
2631
- cwd: this.#options.cwd,
2632
- signal: this.#abortController.signal,
2633
- writeRoots: this.#options.writeRoots
2634
- };
2635
- const allReadOnly = calls.every((tc) => {
2636
- const tool = this.#toolRegistry.get(tc.name);
2637
- return tool ? isReadOnly(tool.kind) : true;
2638
- });
2639
- if (allReadOnly && calls.length > 1) {
2640
- const MAX_PARALLEL = 8;
2641
- const items2 = [];
2642
- const records2 = [];
2643
- for (let i = 0; i < calls.length; i += MAX_PARALLEL) {
2644
- const batch = calls.slice(i, i + MAX_PARALLEL);
2645
- const promises = batch.map((tc) => this.#executeOne(tc, toolCtx));
2646
- const batchResults = await Promise.all(promises);
2647
- for (const r of batchResults) {
2648
- items2.push(r.item);
2649
- records2.push(r.record);
2650
- }
2651
- }
2652
- return { items: items2, records: records2 };
2653
- }
2654
- const items = [];
2655
- const records = [];
2656
- for (const tc of calls) {
2657
- const r = await this.#executeOne(tc, toolCtx);
2658
- items.push(r.item);
2659
- records.push(r.record);
2660
- }
2661
- return { items, records };
2662
- }
2663
- async #executeOne(tc, ctx) {
2664
- const toolName = tc.name;
2665
- const timestamp = Date.now();
2666
- const tool = this.#toolRegistry.get(toolName);
2667
- if (!tool) {
2668
- const errMsg = `\u5DE5\u5177 "${toolName}" \u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u7981\u7528`;
2669
- return {
2670
- item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "TOOL_NOT_FOUND" } },
2671
- record: { name: toolName, success: false, error: "TOOL_NOT_FOUND", timestamp }
2672
- };
2673
- }
2674
- let toolArgs;
2675
- try {
2676
- toolArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
2677
- } catch {
2678
- toolArgs = {};
2679
- }
2680
- const gateResult = await this.#options.gate.check(toolName, toolArgs);
2681
- if (!gateResult) {
2682
- const errMsg = `\u5DE5\u5177 "${toolName}" \u88AB\u6743\u9650\u95E8\u62D2\u7EDD`;
2683
- return {
2684
- item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "GATE_DENIED" } },
2685
- record: { name: toolName, success: false, error: "GATE_DENIED", timestamp }
2686
- };
2687
- }
2688
- if (!isReadOnly(tool.kind)) {
2689
- const maybePreview = tool.preview;
2690
- if (typeof maybePreview === "function") {
2691
- try {
2692
- await maybePreview(toolArgs, ctx);
2693
- } catch {
2694
- }
2695
- }
2696
- }
2697
- try {
2698
- const result = await tool.execute(toolArgs, ctx);
2699
- return {
2700
- item: { name: toolName, callId: tc.id, result },
2701
- record: { name: toolName, success: result.success, error: result.error, timestamp }
2702
- };
2703
- } catch (err) {
2704
- const message = err instanceof Error ? err.message : String(err);
2705
- const errorResult = { success: false, data: `\u5DE5\u5177 "${toolName}" \u6267\u884C\u5F02\u5E38\uFF1A${message}`, error: "EXECUTION_ERROR" };
2706
- return {
2707
- item: { name: toolName, callId: tc.id, result: errorResult },
2708
- record: { name: toolName, success: false, error: "EXECUTION_ERROR", timestamp }
2709
- };
2710
- }
2711
- }
2712
- #checkStormBreak(currentCalls) {
2713
- if (this.#stormRecords.length < 3) return false;
2714
- const recentErrors = this.#stormRecords.slice(-3);
2715
- if (recentErrors.length < 3) return false;
2716
- const first = recentErrors[0];
2717
- const allSame = recentErrors.every((r) => r.name === first.name && r.error === first.error && !r.success);
2718
- if (!allSame) return false;
2719
- return currentCalls.some((tc) => tc.name === first.name);
2720
- }
3306
+ // -------------------------------------------------------------------------
3307
+ // 持久化与恢复
3308
+ // -------------------------------------------------------------------------
3309
+ /**
3310
+ * 中止当前会话:触发 AbortController,停止流式 LLM 和正在执行的工具。
3311
+ * 同时清掉节流持久化定时器。
3312
+ *
3313
+ * @sideEffect abort 当前 chat 循环、取消所有 #abortController 监听者
3314
+ */
2721
3315
  abort() {
2722
3316
  this.#abortController.abort();
2723
3317
  if (this.#persistTimer) {
@@ -2725,19 +3319,34 @@ ${item.result.diff.patch}`;
2725
3319
  this.#persistTimer = null;
2726
3320
  }
2727
3321
  }
2728
- /** 刷新日志缓冲,确保所有已记录的事件落盘 */
3322
+ /**
3323
+ * 刷新日志缓冲,确保所有已记录的事件落盘。
3324
+ * 通常在进程退出前调用,避免日志丢失。
3325
+ *
3326
+ * @sideEffect 写入 .dskcode/logs/ 下的日志文件
3327
+ */
2729
3328
  async flushLog() {
2730
3329
  await this.#logger.flush();
2731
3330
  }
3331
+ /**
3332
+ * 清空消息历史、会话成本、风暴记录、checkpoints。
3333
+ * 注意:不会清掉磁盘上的会话记录(用 delete() 删除),仅重置内存状态。
3334
+ *
3335
+ * @sideEffect 抹掉 messages / costTracker / stormRecords / checkpoints
3336
+ */
2732
3337
  reset() {
2733
3338
  this.#messages.length = 0;
2734
3339
  this.#costTracker.resetSession();
2735
3340
  this.#stormRecords = [];
2736
3341
  this.#checkpoints.clear();
3342
+ this.#lastRoundResults = null;
2737
3343
  }
2738
- // -------------------------------------------------------------------------
2739
- // 持久化与恢复
2740
- // -------------------------------------------------------------------------
3344
+ /**
3345
+ * 立即把当前状态写入持久化(不等 500ms debounce)。
3346
+ * 用于 UI 上"立即保存"按钮或异常退出前的最后兜底。
3347
+ *
3348
+ * @sideEffect 调用 #doPersist(),写入磁盘
3349
+ */
2741
3350
  async persistNow() {
2742
3351
  if (this.#persistTimer) {
2743
3352
  clearTimeout(this.#persistTimer);
@@ -2745,6 +3354,12 @@ ${item.result.diff.patch}`;
2745
3354
  }
2746
3355
  await this.#doPersist();
2747
3356
  }
3357
+ /**
3358
+ * 节流持久化:500ms 内多次调用只会真正落盘一次。
3359
+ * 通过 setTimeout + timer.refresh() 实现 debounce。
3360
+ *
3361
+ * @sideEffect 调度一个 500ms 后的 #doPersist() 任务
3362
+ */
2748
3363
  #persist() {
2749
3364
  if (!this.#store) return;
2750
3365
  if (this.#persistTimer) {
@@ -2757,6 +3372,12 @@ ${item.result.diff.patch}`;
2757
3372
  }, 500);
2758
3373
  this.#persistTimer.unref();
2759
3374
  }
3375
+ /**
3376
+ * 真正执行持久化:构造 StoredSession 并写入 store。
3377
+ * 失败仅 console.error,不抛给调用方(持久化失败不应阻塞对话)。
3378
+ *
3379
+ * @sideEffect 写磁盘 ~/.dskcode/sessions/<id>.json
3380
+ */
2760
3381
  async #doPersist() {
2761
3382
  if (!this.#store) return;
2762
3383
  const stored = {
@@ -2775,12 +3396,24 @@ ${item.result.diff.patch}`;
2775
3396
  console.error("[Session] \u6301\u4E45\u5316\u5931\u8D25:", err);
2776
3397
  }
2777
3398
  }
3399
+ /**
3400
+ * 从消息历史推导会话标题:取第一条非空 user 消息的前 40 字。
3401
+ * 无任何 user 消息时返回 "新会话"。
3402
+ *
3403
+ * @pure 不修改任何状态
3404
+ */
2778
3405
  #deriveTitle() {
2779
3406
  for (const m of this.#messages) {
2780
3407
  if (m.role === "user" && m.content.trim()) return m.content.trim().slice(0, 40);
2781
3408
  }
2782
3409
  return "\u65B0\u4F1A\u8BDD";
2783
3410
  }
3411
+ /**
3412
+ * 把内存 messages 转成可持久化的 StoredSession["messages"]。
3413
+ * 给 user 消息挂上对应 checkpoint(若存在),用于 rewind 时文件回退。
3414
+ *
3415
+ * @pure 不修改任何状态
3416
+ */
2784
3417
  #serializeMessages() {
2785
3418
  return this.#messages.map((msg, idx) => {
2786
3419
  const checkpoint = this.#checkpoints.get(idx);
@@ -2788,6 +3421,19 @@ ${item.result.diff.patch}`;
2788
3421
  return { ...msg };
2789
3422
  });
2790
3423
  }
3424
+ /**
3425
+ * 静态方法:从磁盘恢复一个之前保存的 Session。
3426
+ *
3427
+ * @param id — 之前 SessionStore 保存的会话 ID
3428
+ * @param provider — LLM Provider(必须)
3429
+ * @param tools — 工具列表/Registry(必须重新提供,因为工具不持久化)
3430
+ * @param costTracker — 成本追踪器(可选)
3431
+ * @param options — 会话选项(sessionId / store 必须与持久化 ID 对应)
3432
+ * @returns 恢复后的 Session 实例
3433
+ * @throws 持久化被禁用时、ID 不存在时抛错
3434
+ *
3435
+ * @sideEffect 从磁盘读 messages + checkpoints
3436
+ */
2791
3437
  static async resume(id, provider, tools = [], costTracker, options) {
2792
3438
  const store = options?.store === false ? null : options?.store ?? new SessionStore();
2793
3439
  if (!store) throw new Error("resume \u9700\u8981\u542F\u7528\u6301\u4E45\u5316\uFF08options.store \u4E0D\u80FD\u4E3A false\uFF09");
@@ -2813,6 +3459,13 @@ ${item.result.diff.patch}`;
2813
3459
  // -------------------------------------------------------------------------
2814
3460
  // 检查点与 Rewind
2815
3461
  // -------------------------------------------------------------------------
3462
+ /**
3463
+ * 列出所有 user 消息对应的 checkpoint(按 index 升序)。
3464
+ * 用于 UI 展示 /rewind 列表。
3465
+ *
3466
+ * @returns 每个元素的 index = messages 数组索引,可直接传给 rewind()
3467
+ * @pure 不修改任何状态
3468
+ */
2816
3469
  listCheckpoints() {
2817
3470
  const result = [];
2818
3471
  for (const [index, checkpoint] of this.#checkpoints) {
@@ -2822,6 +3475,25 @@ ${item.result.diff.patch}`;
2822
3475
  }
2823
3476
  return result.sort((a, b) => a.index - b.index);
2824
3477
  }
3478
+ /**
3479
+ * rewind — 把消息历史截断到 targetIndex 对应的 user 消息,
3480
+ * 并尝试把文件工作区也回退到那一刻。
3481
+ *
3482
+ * @param targetIndex — 要回退到的 user 消息索引(来自 listCheckpoints())
3483
+ * @returns
3484
+ * - `{ ok: true, fileRestored }`:成功;fileRestored 表示工作区是否也被还原
3485
+ * - `{ ok: false, error }`:失败(无效索引 / 非 user / 无 checkpoint / 文件恢复失败)
3486
+ *
3487
+ * 行为:
3488
+ * 1. 截断 messages 数组到 targetIndex + 1
3489
+ * 2. 移除并丢弃所有 > targetIndex 的 checkpoints
3490
+ * 3. 若目标 checkpoint 是 git 仓库:
3491
+ * - 有 stashSha:恢复该 stash(force)
3492
+ * - 无 stashSha:把工作区 restore 到 HEAD 干净状态
3493
+ * 4. 触发 #persist() 把截断后的状态写盘
3494
+ *
3495
+ * @sideEffect 改写 messages、checkpoints、磁盘
3496
+ */
2825
3497
  async rewind(targetIndex) {
2826
3498
  if (targetIndex < 0 || targetIndex >= this.#messages.length) {
2827
3499
  return { ok: false, error: `\u65E0\u6548\u7684\u6D88\u606F\u7D22\u5F15 ${targetIndex}` };
@@ -2861,9 +3533,20 @@ ${item.result.diff.patch}`;
2861
3533
  this.#persist();
2862
3534
  return { ok: true, fileRestored };
2863
3535
  }
3536
+ /**
3537
+ * 是否存在可用的 checkpoint(决定 UI 是否显示 /rewind 入口)。
3538
+ *
3539
+ * @pure 不修改任何状态
3540
+ */
2864
3541
  hasCheckpoints() {
2865
3542
  return this.listCheckpoints().length > 0;
2866
3543
  }
3544
+ /**
3545
+ * 彻底删除会话:从磁盘移除 SessionStore 条目、丢弃所有 checkpoint、关闭日志。
3546
+ * 删除后该 Session 实例不应再被使用。
3547
+ *
3548
+ * @sideEffect 删磁盘文件、清 checkpoints、flush 并关闭 logger
3549
+ */
2867
3550
  async delete() {
2868
3551
  if (this.#store) await this.#store.delete(this.#sessionId);
2869
3552
  for (const cp2 of this.#checkpoints.values()) {
@@ -2876,6 +3559,32 @@ ${item.result.diff.patch}`;
2876
3559
  // -------------------------------------------------------------------------
2877
3560
  // 内部方法
2878
3561
  // -------------------------------------------------------------------------
3562
+ /**
3563
+ * 构建本轮使用的工具执行器(每次 chat() 调用一次,绑定当前 signal)。
3564
+ * 每次新建确保 abort signal 是当下有效的,且 #baseCtx 不会被多次调用共享篡改。
3565
+ *
3566
+ * @returns 新的 ToolExecutor 实例
3567
+ * @pure 仅组装参数,不修改任何状态
3568
+ */
3569
+ #buildToolExecutor() {
3570
+ return new ToolExecutor({
3571
+ registry: this.#toolRegistry,
3572
+ gate: this.#options.gate,
3573
+ baseCtx: {
3574
+ cwd: this.#options.cwd,
3575
+ signal: this.#abortController.signal,
3576
+ writeRoots: this.#options.writeRoots
3577
+ }
3578
+ });
3579
+ }
3580
+ /**
3581
+ * 构建 system prompt:注入当前模型、cwd、工具清单、项目上下文。
3582
+ * plan 模式使用 buildPlanSystemPrompt(只读工具 + 计划输出格式),
3583
+ * code 模式使用 buildSystemPrompt。
3584
+ *
3585
+ * @returns 渲染好的 prompt 字符串
3586
+ * @pure 不修改任何状态
3587
+ */
2879
3588
  #buildSystemPrompt() {
2880
3589
  const enabledTools = this.#toolRegistry.list();
2881
3590
  const toolDescs = enabledTools.map((t) => ({
@@ -2893,17 +3602,10 @@ ${item.result.diff.patch}`;
2893
3602
  if (this.#mode === "plan") return buildPlanSystemPrompt(opts);
2894
3603
  return buildSystemPrompt(opts);
2895
3604
  }
2896
- #buildToolDefinitions() {
2897
- const tools = this.#mode === "plan" ? this.#toolRegistry.listReadTools() : this.#toolRegistry.list();
2898
- return tools.map((t) => ({
2899
- type: "function",
2900
- function: { name: t.name, description: t.description, parameters: t.parameters }
2901
- }));
2902
- }
2903
3605
  };
2904
3606
 
2905
3607
  // src/tool/sandbox.ts
2906
- import { resolve, relative, isAbsolute } from "path";
3608
+ import { resolve, relative as relative2, isAbsolute } from "path";
2907
3609
  import { realpath as realpath2 } from "fs/promises";
2908
3610
  import { spawn } from "child_process";
2909
3611
  import process2 from "process";
@@ -2922,7 +3624,7 @@ async function realPath(target) {
2922
3624
  const parent = resolve(target, "..");
2923
3625
  try {
2924
3626
  const realParent = await realpath2(parent);
2925
- return resolve(realParent, relative(parent, target));
3627
+ return resolve(realParent, relative2(parent, target));
2926
3628
  } catch {
2927
3629
  return resolve(target);
2928
3630
  }
@@ -2935,7 +3637,7 @@ async function confine(allowedRoots, target) {
2935
3637
  const realTarget = await realPath(target);
2936
3638
  for (const root of allowedRoots) {
2937
3639
  const realRoot = await realPath(root);
2938
- const rel = relative(realRoot, realTarget);
3640
+ const rel = relative2(realRoot, realTarget);
2939
3641
  if (!rel.startsWith("..") && rel !== "" && !rel.startsWith("/") && !rel.startsWith("\\")) {
2940
3642
  return { ok: true };
2941
3643
  }
@@ -3332,7 +4034,7 @@ async function writeFileWithEol(filePath, originalContent, newContent) {
3332
4034
  // src/tool/builtins/read-file.ts
3333
4035
  import { readFile as readFile5, stat } from "fs/promises";
3334
4036
  import { open } from "fs/promises";
3335
- import { relative as relative2 } from "path";
4037
+ import { relative as relative3 } from "path";
3336
4038
  async function checkBinary(filePath) {
3337
4039
  const fileHandle = await open(filePath, "r");
3338
4040
  try {
@@ -3415,7 +4117,7 @@ var readFileTool = {
3415
4117
  const tailHint = remaining > 0 ? `
3416
4118
 
3417
4119
  [\u8FD8\u6709 ${remaining} \u884C\uFF1B\u4F7F\u7528 startLine=${endLine + 1} \u7EE7\u7EED\u67E5\u770B]` : "";
3418
- const relPath = relative2(ctx.cwd, filePath).replace(/\\/g, "/");
4120
+ const relPath = relative3(ctx.cwd, filePath).replace(/\\/g, "/");
3419
4121
  const rangeLabel = startLine > 0 || endLine < lines.length ? `\u7B2C ${startLine + 1}-${endLine} \u884C` : `${lines.length} \u884C`;
3420
4122
  return {
3421
4123
  success: true,
@@ -3435,7 +4137,7 @@ var readFileTool = {
3435
4137
 
3436
4138
  // src/tool/builtins/write-file.ts
3437
4139
  import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
3438
- import { dirname, relative as relative3, basename as basename2 } from "path";
4140
+ import { dirname, relative as relative4, basename as basename2 } from "path";
3439
4141
  var writeFileTool = {
3440
4142
  name: "write_file",
3441
4143
  kind: "edit" /* Edit */,
@@ -3490,7 +4192,7 @@ var writeFileTool = {
3490
4192
  const summary = existedBefore ? `\u{1F4DD} \u4FEE\u6539: ${fileName} (+${diff.additions} -${diff.deletions})` : `\u{1F4DD} \u65B0\u5EFA: ${fileName} (+${diff.additions} \u884C)`;
3491
4193
  return {
3492
4194
  success: true,
3493
- data: `\u6587\u4EF6${action}\uFF1A${relative3(ctx.cwd, filePath).replace(/\\/g, "/")}\uFF08${lineCount} \u884C\uFF0C${byteSize} \u5B57\u8282${diffSummary}\uFF09`,
4195
+ data: `\u6587\u4EF6${action}\uFF1A${relative4(ctx.cwd, filePath).replace(/\\/g, "/")}\uFF08${lineCount} \u884C\uFF0C${byteSize} \u5B57\u8282${diffSummary}\uFF09`,
3494
4196
  summary,
3495
4197
  diff
3496
4198
  };
@@ -3943,7 +4645,7 @@ ${truncateOutput(result.stderr)}`);
3943
4645
 
3944
4646
  // src/tool/builtins/glob.ts
3945
4647
  import { readdir as readdir3, stat as stat2 } from "fs/promises";
3946
- import { join as join7, relative as relative4, isAbsolute as isAbsolute2 } from "path";
4648
+ import { join as join7, relative as relative5, isAbsolute as isAbsolute2 } from "path";
3947
4649
  function globToRegex(pattern) {
3948
4650
  let regexStr = pattern;
3949
4651
  regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
@@ -3969,7 +4671,7 @@ async function walkDir(dir, baseDir) {
3969
4671
  continue;
3970
4672
  }
3971
4673
  const fullPath = join7(dir, entry.name);
3972
- const relPath = relative4(baseDir, fullPath);
4674
+ const relPath = relative5(baseDir, fullPath);
3973
4675
  if (entry.isDirectory()) {
3974
4676
  results.push(...await walkDir(fullPath, baseDir));
3975
4677
  } else {
@@ -4041,7 +4743,7 @@ var globTool = {
4041
4743
 
4042
4744
  // src/tool/builtins/grep.ts
4043
4745
  import { readdir as readdir4, readFile as readFile10, stat as stat3 } from "fs/promises";
4044
- import { join as join8, relative as relative5, isAbsolute as isAbsolute3 } from "path";
4746
+ import { join as join8, relative as relative6, isAbsolute as isAbsolute3 } from "path";
4045
4747
  async function collectFiles(dir, extension, maxFiles = 200) {
4046
4748
  const results = [];
4047
4749
  let entries;
@@ -4117,7 +4819,7 @@ var grepTool = {
4117
4819
  try {
4118
4820
  const content = await readFile10(filePath, "utf-8");
4119
4821
  const lines = content.split("\n");
4120
- const relPath = relative5(searchDir, filePath);
4822
+ const relPath = relative6(searchDir, filePath);
4121
4823
  for (let i = 0; i < lines.length; i++) {
4122
4824
  if (regex.test(lines[i] ?? "")) {
4123
4825
  regex.lastIndex = 0;
@@ -4162,7 +4864,7 @@ var grepTool = {
4162
4864
 
4163
4865
  // src/tool/builtins/ls.ts
4164
4866
  import { readdir as readdir5, stat as stat4 } from "fs/promises";
4165
- import { join as join9, relative as relative6 } from "path";
4867
+ import { join as join9, relative as relative7 } from "path";
4166
4868
  var lsTool = {
4167
4869
  name: "ls",
4168
4870
  kind: "read" /* Read */,
@@ -4216,9 +4918,9 @@ var lsTool = {
4216
4918
  lines.push(`${typeLabel} ${entry.name}${sizeStr ? ` (${sizeStr})` : ""}`);
4217
4919
  }
4218
4920
  if (lines.length === 0) {
4219
- return { success: true, data: "\u76EE\u5F55\u4E3A\u7A7A", summary: `\u{1F4C2} ${relative6(ctx.cwd, dirPath).replace(/\\/g, "/")}\uFF08\u7A7A\uFF09` };
4921
+ return { success: true, data: "\u76EE\u5F55\u4E3A\u7A7A", summary: `\u{1F4C2} ${relative7(ctx.cwd, dirPath).replace(/\\/g, "/")}\uFF08\u7A7A\uFF09` };
4220
4922
  }
4221
- const relPath = relative6(ctx.cwd, dirPath).replace(/\\/g, "/");
4923
+ const relPath = relative7(ctx.cwd, dirPath).replace(/\\/g, "/");
4222
4924
  return {
4223
4925
  success: true,
4224
4926
  data: truncateOutput(`\u76EE\u5F55\uFF1A${dirPath}
@@ -4402,9 +5104,9 @@ function getGradientColors(text, phase, stops) {
4402
5104
  }
4403
5105
 
4404
5106
  // src/ui/ChatSession.tsx
4405
- import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
5107
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
4406
5108
  var PHASE_CONFIG = {
4407
- thinking: { icon: "\u{1F9E0}", label: "\u601D\u8003\u4E2D", color: "#ff9800" },
5109
+ thinking: { icon: "\u{1F9E0}", label: "\u6DF1\u5EA6\u601D\u8003\u4E2D", color: "#ff9800" },
4408
5110
  generating: { icon: "\u2728", label: "\u751F\u6210\u4E2D", color: "#00ff41" },
4409
5111
  calling_tools: { icon: "\u{1F6E0}", label: "\u8C03\u7528\u5DE5\u5177", color: "#f59e0b" },
4410
5112
  executing_tools: { icon: "\u26A1", label: "\u6267\u884C\u5DE5\u5177", color: "#00ffff" }
@@ -4476,34 +5178,34 @@ function ChatSession({
4476
5178
  }) {
4477
5179
  const termWidth = typeof process.stdout.columns === "number" ? process.stdout.columns : 80;
4478
5180
  const dividerWidth = Math.max(termWidth - 2, 1);
4479
- const [offset, setOffset] = useState3(0);
4480
- const [displayMessages, setDisplayMessages] = useState3([]);
4481
- const [staticKey, setStaticKey] = useState3(0);
4482
- const [input, setInput] = useState3("");
4483
- const [balance, setBalance] = useState3(null);
4484
- const [balanceLoading, setBalanceLoading] = useState3(false);
4485
- const [todayCost, setTodayCost] = useState3(null);
4486
- const [isStreaming, setIsStreaming] = useState3(false);
4487
- const [streamingPhase, setStreamingPhase] = useState3(null);
4488
- const [streamingPlaceholder, setStreamingPlaceholder] = useState3("");
4489
- const [idlePlaceholder, setIdlePlaceholder] = useState3(() => pickRandom(IDLE_PLACEHOLDERS));
4490
- const [gradientColors, setGradientColors] = useState3([]);
4491
- const gradientPhaseRef = useRef2(0);
4492
- const [streamingGradientColors, setStreamingGradientColors] = useState3([]);
4493
- const streamingPhaseRef = useRef2(0);
4494
- const [currentContent, setCurrentContent] = useState3("");
4495
- const [currentToolCalls, setCurrentToolCalls] = useState3([]);
4496
- const [_currentUsage, setCurrentUsage] = useState3(void 0);
4497
- const [_currentElapsed, setCurrentElapsed] = useState3(void 0);
4498
- const [_currentCost, setCurrentCost] = useState3(void 0);
4499
- const [activeModel, setActiveModel] = useState3(model);
4500
- const [_streamingModel, setStreamingModel] = useState3(void 0);
4501
- const [streamError, setStreamError] = useState3(void 0);
4502
- const [sessionMode, setSessionMode] = useState3("code");
4503
- const [thinkingEnabled, setThinkingEnabled] = useState3(true);
4504
- const [thinkingEffort, setThinkingEffort] = useState3("high");
4505
- const [responseFormat] = useState3("text");
4506
- const [toolChoice, setToolChoice] = useState3(void 0);
5181
+ const [offset, setOffset] = useState4(0);
5182
+ const [displayMessages, setDisplayMessages] = useState4([]);
5183
+ const [staticKey, setStaticKey] = useState4(0);
5184
+ const [input, setInput] = useState4("");
5185
+ const [balance, setBalance] = useState4(null);
5186
+ const [balanceLoading, setBalanceLoading] = useState4(false);
5187
+ const [todayCost, setTodayCost] = useState4(null);
5188
+ const [isStreaming, setIsStreaming] = useState4(false);
5189
+ const [streamingPhase, setStreamingPhase] = useState4(null);
5190
+ const [streamingPlaceholder, setStreamingPlaceholder] = useState4("");
5191
+ const [idlePlaceholder, setIdlePlaceholder] = useState4(() => pickRandom(IDLE_PLACEHOLDERS));
5192
+ const [gradientColors, setGradientColors] = useState4([]);
5193
+ const gradientPhaseRef = useRef3(0);
5194
+ const [streamingGradientColors, setStreamingGradientColors] = useState4([]);
5195
+ const streamingPhaseRef = useRef3(0);
5196
+ const [currentContent, setCurrentContent] = useState4("");
5197
+ const [currentToolCalls, setCurrentToolCalls] = useState4([]);
5198
+ const [_currentUsage, setCurrentUsage] = useState4(void 0);
5199
+ const [_currentElapsed, setCurrentElapsed] = useState4(void 0);
5200
+ const [_currentCost, setCurrentCost] = useState4(void 0);
5201
+ const [activeModel, setActiveModel] = useState4(model);
5202
+ const [_streamingModel, setStreamingModel] = useState4(void 0);
5203
+ const [streamError, setStreamError] = useState4(void 0);
5204
+ const [sessionMode, setSessionMode] = useState4("code");
5205
+ const [thinkingEnabled, setThinkingEnabled] = useState4(true);
5206
+ const [thinkingEffort, setThinkingEffort] = useState4("high");
5207
+ const [responseFormat] = useState4("text");
5208
+ const [toolChoice, setToolChoice] = useState4(void 0);
4507
5209
  const sessionCost = useMemo(() => {
4508
5210
  return displayMessages.reduce((sum, msg) => {
4509
5211
  if (msg.assistantDetail?.cost) {
@@ -4514,36 +5216,36 @@ function ChatSession({
4514
5216
  }, [displayMessages]);
4515
5217
  const hasConversationStarted = displayMessages.length > 0;
4516
5218
  const cmdTips = Array.from(getRegisteredCommands()).filter(([name]) => name !== "/exit" && name !== "/quit").map(([name, cmd]) => ({ name, desc: cmd.desc }));
4517
- const [cmdTipIndex, setCmdTipIndex] = useState3(0);
4518
- const [cmdTipGradientColors, setCmdTipGradientColors] = useState3([]);
4519
- const cmdTipPhaseRef = useRef2(0);
4520
- const [skillSelectIndex, setSkillSelectIndex] = useState3(0);
4521
- const [fileSelectIndex, setFileSelectIndex] = useState3(0);
4522
- const [inputKey, setInputKey] = useState3(0);
4523
- const [rewindSelecting, setRewindSelecting] = useState3(false);
4524
- const [rewindSelectIndex, setRewindSelectIndex] = useState3(0);
4525
- const [rewindList, setRewindList] = useState3([]);
4526
- const [rewinding, setRewinding] = useState3(false);
4527
- const [rewindHintPhase, setRewindHintPhase] = useState3("idle");
4528
- const currentRoundModifiedRef = useRef2(false);
4529
- const rewindHintTimerRef = useRef2(null);
4530
- const [selectingModel, setSelectingModel] = useState3(false);
4531
- const [modelSelectIndex, setModelSelectIndex] = useState3(0);
5219
+ const [cmdTipIndex, setCmdTipIndex] = useState4(0);
5220
+ const [cmdTipGradientColors, setCmdTipGradientColors] = useState4([]);
5221
+ const cmdTipPhaseRef = useRef3(0);
5222
+ const [skillSelectIndex, setSkillSelectIndex] = useState4(0);
5223
+ const [fileSelectIndex, setFileSelectIndex] = useState4(0);
5224
+ const [inputKey, setInputKey] = useState4(0);
5225
+ const [rewindSelecting, setRewindSelecting] = useState4(false);
5226
+ const [rewindSelectIndex, setRewindSelectIndex] = useState4(0);
5227
+ const [rewindList, setRewindList] = useState4([]);
5228
+ const [rewinding, setRewinding] = useState4(false);
5229
+ const [rewindHintPhase, setRewindHintPhase] = useState4("idle");
5230
+ const currentRoundModifiedRef = useRef3(false);
5231
+ const rewindHintTimerRef = useRef3(null);
5232
+ const [selectingModel, setSelectingModel] = useState4(false);
5233
+ const [modelSelectIndex, setModelSelectIndex] = useState4(0);
4532
5234
  const modelOptions = ["deepseek-v4-flash", "deepseek-v4-pro"];
4533
- const sessionRef = useRef2(null);
4534
- const abortRef = useRef2(null);
4535
- const currentContentRef = useRef2("");
4536
- const currentToolCallsRef = useRef2([]);
4537
- const currentUsageRef = useRef2(void 0);
4538
- const currentElapsedRef = useRef2(void 0);
4539
- const currentCostRef = useRef2(void 0);
4540
- const currentModelRef = useRef2(void 0);
4541
- const streamErrorRef = useRef2(void 0);
4542
- useEffect3(() => {
5235
+ const sessionRef = useRef3(null);
5236
+ const abortRef = useRef3(null);
5237
+ const currentContentRef = useRef3("");
5238
+ const currentToolCallsRef = useRef3([]);
5239
+ const currentUsageRef = useRef3(void 0);
5240
+ const currentElapsedRef = useRef3(void 0);
5241
+ const currentCostRef = useRef3(void 0);
5242
+ const currentModelRef = useRef3(void 0);
5243
+ const streamErrorRef = useRef3(void 0);
5244
+ useEffect4(() => {
4543
5245
  setSkillSelectIndex(0);
4544
5246
  setFileSelectIndex(0);
4545
5247
  }, [input]);
4546
- useEffect3(() => {
5248
+ useEffect4(() => {
4547
5249
  return () => {
4548
5250
  if (rewindHintTimerRef.current) {
4549
5251
  clearTimeout(rewindHintTimerRef.current);
@@ -4778,14 +5480,14 @@ function ChatSession({
4778
5480
  [selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode, rewindSelecting, rewindSelectIndex, rewindList, doRewind]
4779
5481
  )
4780
5482
  );
4781
- useEffect3(() => {
5483
+ useEffect4(() => {
4782
5484
  if (cmdTips.length <= 1) return;
4783
5485
  const timer = setInterval(() => {
4784
5486
  setCmdTipIndex((prev) => (prev + 1) % cmdTips.length);
4785
5487
  }, 2e3);
4786
5488
  return () => clearInterval(timer);
4787
5489
  }, [cmdTips.length]);
4788
- useEffect3(() => {
5490
+ useEffect4(() => {
4789
5491
  const tip = cmdTips[cmdTipIndex % cmdTips.length];
4790
5492
  if (!tip) {
4791
5493
  setCmdTipGradientColors([]);
@@ -4800,13 +5502,13 @@ function ChatSession({
4800
5502
  }, GRADIENT_ANIMATION.cmdTipInterval);
4801
5503
  return () => clearInterval(interval);
4802
5504
  }, [cmdTipIndex, cmdTips.length]);
4803
- useEffect3(() => {
5505
+ useEffect4(() => {
4804
5506
  const timer = setInterval(() => {
4805
5507
  setOffset((prev) => (prev + 1) % CYBER_PALETTE.length);
4806
5508
  }, 500);
4807
5509
  return () => clearInterval(timer);
4808
5510
  }, []);
4809
- useEffect3(() => {
5511
+ useEffect4(() => {
4810
5512
  if (!apiKey || !baseUrl) return;
4811
5513
  const provider = createProvider({
4812
5514
  name: "deepseek",
@@ -4825,7 +5527,7 @@ function ChatSession({
4825
5527
  sessionRef.current = null;
4826
5528
  };
4827
5529
  }, [apiKey, baseUrl, activeModel, externalCostTracker]);
4828
- useEffect3(() => {
5530
+ useEffect4(() => {
4829
5531
  if (!apiKey || !baseUrl) return;
4830
5532
  let cancelled = false;
4831
5533
  setBalanceLoading(true);
@@ -4850,7 +5552,7 @@ function ChatSession({
4850
5552
  cancelled = true;
4851
5553
  };
4852
5554
  }, [apiKey, baseUrl]);
4853
- useEffect3(() => {
5555
+ useEffect4(() => {
4854
5556
  if (!externalCostTracker) return;
4855
5557
  let cancelled = false;
4856
5558
  let timer;
@@ -4868,7 +5570,7 @@ function ChatSession({
4868
5570
  if (timer) clearInterval(timer);
4869
5571
  };
4870
5572
  }, [externalCostTracker]);
4871
- useEffect3(() => {
5573
+ useEffect4(() => {
4872
5574
  if (isStreaming || !idlePlaceholder) {
4873
5575
  setGradientColors([]);
4874
5576
  return;
@@ -4881,7 +5583,7 @@ function ChatSession({
4881
5583
  }, GRADIENT_ANIMATION.idleInterval);
4882
5584
  return () => clearInterval(interval);
4883
5585
  }, [isStreaming, idlePlaceholder]);
4884
- useEffect3(() => {
5586
+ useEffect4(() => {
4885
5587
  if (!isStreaming || !streamingPlaceholder) {
4886
5588
  setStreamingGradientColors([]);
4887
5589
  return;
@@ -5231,7 +5933,7 @@ function ChatSession({
5231
5933
  }
5232
5934
  }
5233
5935
  }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode, isStreaming, rewinding]);
5234
- useEffect3(() => {
5936
+ useEffect4(() => {
5235
5937
  if (!isStreaming && externalCostTracker) {
5236
5938
  setTodayCost(externalCostTracker.todayTotalCost);
5237
5939
  }
@@ -5334,7 +6036,10 @@ function ChatSession({
5334
6036
  {
5335
6037
  content: currentContent,
5336
6038
  toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0,
5337
- isStreaming: true
6039
+ isStreaming: true,
6040
+ usage: _currentUsage,
6041
+ cost: _currentCost,
6042
+ model: _streamingModel
5338
6043
  }
5339
6044
  ),
5340
6045
  !isStreaming && streamError && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, marginLeft: 3, children: /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
@@ -5406,7 +6111,7 @@ function ChatSession({
5406
6111
  /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text10, { color: "#808080", dimColor: true, children: "\u2191\u2193 \u9009\u62E9 \xB7 Enter \u786E\u8BA4 \xB7 Esc \u53D6\u6D88" }) })
5407
6112
  ] }),
5408
6113
  /* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
5409
- ] }) : /* @__PURE__ */ jsxs9(Fragment, { children: [
6114
+ ] }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
5410
6115
  (hasConversationStarted || sessionMode === "plan") && isStreaming && streamingPhase ? /* @__PURE__ */ jsx9(Box9, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs9(Text10, { bold: true, color: PHASE_CONFIG[streamingPhase].color, children: [
5411
6116
  PHASE_CONFIG[streamingPhase].icon,
5412
6117
  " ",
@@ -5458,10 +6163,10 @@ function ChatSession({
5458
6163
 
5459
6164
  // src/ui/GamePicker.tsx
5460
6165
  import { Box as Box10, Text as Text11, useInput as useInput2 } from "ink";
5461
- import { useState as useState4, useCallback as useCallback3 } from "react";
6166
+ import { useState as useState5, useCallback as useCallback3 } from "react";
5462
6167
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
5463
6168
  function GamePicker({ games, onSelect, onExit, onBackToChat }) {
5464
- const [selectedIndex, setSelectedIndex] = useState4(0);
6169
+ const [selectedIndex, setSelectedIndex] = useState5(0);
5465
6170
  const exitAction = onBackToChat ?? onExit ?? (() => process.exit(0));
5466
6171
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(exitAction);
5467
6172
  useInput2(
@@ -5516,7 +6221,7 @@ function listGames() {
5516
6221
 
5517
6222
  // src/game/brick-breaker/index.tsx
5518
6223
  import { Box as Box11, Text as Text12, useInput as useInput3, render as render2 } from "ink";
5519
- import { useState as useState5, useEffect as useEffect4, useRef as useRef3, useCallback as useCallback4 } from "react";
6224
+ import { useState as useState6, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback4 } from "react";
5520
6225
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
5521
6226
  var GAME_WIDTH = 40;
5522
6227
  var GAME_HEIGHT = 18;
@@ -5648,13 +6353,13 @@ function buildBoard(state) {
5648
6353
  return lines.map((l) => `\u2502${l}\u2502`).join("\n");
5649
6354
  }
5650
6355
  function BrickBreakerGame({ onExit: _onExit }) {
5651
- const [initialLevel, setInitialLevel] = useState5(1);
5652
- const [selectingLevel, setSelectingLevel] = useState5(true);
5653
- const stateRef = useRef3(createInitialState(initialLevel));
5654
- const [tick2, setTick] = useState5(0);
5655
- const onExitRef = useRef3(_onExit);
6356
+ const [initialLevel, setInitialLevel] = useState6(1);
6357
+ const [selectingLevel, setSelectingLevel] = useState6(true);
6358
+ const stateRef = useRef4(createInitialState(initialLevel));
6359
+ const [tick2, setTick] = useState6(0);
6360
+ const onExitRef = useRef4(_onExit);
5656
6361
  onExitRef.current = _onExit;
5657
- useEffect4(() => {
6362
+ useEffect5(() => {
5658
6363
  if (selectingLevel) return;
5659
6364
  const interval = setInterval(() => {
5660
6365
  update(stateRef.current);
@@ -5788,7 +6493,7 @@ var brick_breaker_default = {
5788
6493
 
5789
6494
  // src/game/coder-check/index.tsx
5790
6495
  import { Box as Box12, Text as Text13, useInput as useInput4, render as render3 } from "ink";
5791
- import { useState as useState6, useEffect as useEffect5, useRef as useRef4, useCallback as useCallback5 } from "react";
6496
+ import { useState as useState7, useEffect as useEffect6, useRef as useRef5, useCallback as useCallback5 } from "react";
5792
6497
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
5793
6498
  var GAME_W = 66;
5794
6499
  var GAME_H = 20;
@@ -6180,18 +6885,18 @@ function buildGameView(s, scoreLines, scoreColor, message) {
6180
6885
  return rows;
6181
6886
  }
6182
6887
  function CoderCheck({ onExit: _onExit }) {
6183
- const stateRef = useRef4(createInitialState2());
6184
- const [tick2, setTick] = useState6(0);
6185
- const [colorOffset, setColorOffset] = useState6(0);
6186
- const onExitRef = useRef4(_onExit);
6888
+ const stateRef = useRef5(createInitialState2());
6889
+ const [tick2, setTick] = useState7(0);
6890
+ const [colorOffset, setColorOffset] = useState7(0);
6891
+ const onExitRef = useRef5(_onExit);
6187
6892
  onExitRef.current = _onExit;
6188
- useEffect5(() => {
6893
+ useEffect6(() => {
6189
6894
  const timer = setInterval(() => {
6190
6895
  setColorOffset((prev) => (prev + 1) % CYBER_PALETTE2.length);
6191
6896
  }, 400);
6192
6897
  return () => clearInterval(timer);
6193
6898
  }, []);
6194
- useEffect5(() => {
6899
+ useEffect6(() => {
6195
6900
  const interval = setInterval(() => {
6196
6901
  update2(stateRef.current);
6197
6902
  setTick((t) => t + 1);
@@ -6688,7 +7393,7 @@ import chalk5 from "chalk";
6688
7393
 
6689
7394
  // src/stock/StockList.tsx
6690
7395
  import { Box as Box13, Text as Text14, useInput as useInput5 } from "ink";
6691
- import { useState as useState7, useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo2 } from "react";
7396
+ import { useState as useState8, useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo2 } from "react";
6692
7397
  import asciichart from "asciichart";
6693
7398
  import os from "os";
6694
7399
  import { join as join10 } from "path";
@@ -6795,20 +7500,20 @@ function latestPoints(data, maxPoints = 60) {
6795
7500
  return data.slice(data.length - maxPoints);
6796
7501
  }
6797
7502
  function StockList({ codes, onExit, onBackToChat }) {
6798
- const [stocks, setStocks] = useState7([]);
6799
- const [selectedIndex, setSelectedIndex] = useState7(0);
6800
- const [loading, setLoading] = useState7(true);
6801
- const [lastUpdate, setLastUpdate] = useState7("");
6802
- const [detailView, setDetailView] = useState7(null);
6803
- const [detailPrices, setDetailPrices] = useState7(null);
6804
- const [detailLoading, setDetailLoading] = useState7(false);
6805
- const [detailCountdown, setDetailCountdown] = useState7(10);
6806
- const [countdown, setCountdown] = useState7(5);
6807
- const [currentTime, setCurrentTime] = useState7(
7503
+ const [stocks, setStocks] = useState8([]);
7504
+ const [selectedIndex, setSelectedIndex] = useState8(0);
7505
+ const [loading, setLoading] = useState8(true);
7506
+ const [lastUpdate, setLastUpdate] = useState8("");
7507
+ const [detailView, setDetailView] = useState8(null);
7508
+ const [detailPrices, setDetailPrices] = useState8(null);
7509
+ const [detailLoading, setDetailLoading] = useState8(false);
7510
+ const [detailCountdown, setDetailCountdown] = useState8(10);
7511
+ const [countdown, setCountdown] = useState8(5);
7512
+ const [currentTime, setCurrentTime] = useState8(
6808
7513
  () => (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })
6809
7514
  );
6810
- const [dimMode, setDimMode] = useState7(false);
6811
- const [sortOrder, setSortOrder] = useState7("default");
7515
+ const [dimMode, setDimMode] = useState8(false);
7516
+ const [sortOrder, setSortOrder] = useState8("default");
6812
7517
  const sortedStocks = useMemo2(() => {
6813
7518
  if (sortOrder === "default") return stocks;
6814
7519
  return [...stocks].toSorted(
@@ -6816,7 +7521,7 @@ function StockList({ codes, onExit, onBackToChat }) {
6816
7521
  );
6817
7522
  }, [stocks, sortOrder]);
6818
7523
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(onExit);
6819
- useEffect6(() => {
7524
+ useEffect7(() => {
6820
7525
  const timer = setInterval(() => {
6821
7526
  setCurrentTime((/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }));
6822
7527
  }, 1e3);
@@ -6832,10 +7537,10 @@ function StockList({ codes, onExit, onBackToChat }) {
6832
7537
  }
6833
7538
  setLoading(false);
6834
7539
  }, [codes]);
6835
- useEffect6(() => {
7540
+ useEffect7(() => {
6836
7541
  void loadData();
6837
7542
  }, [loadData]);
6838
- useEffect6(() => {
7543
+ useEffect7(() => {
6839
7544
  const interval = setInterval(() => {
6840
7545
  setCountdown((prev) => {
6841
7546
  if (prev <= 1) {
@@ -6847,7 +7552,7 @@ function StockList({ codes, onExit, onBackToChat }) {
6847
7552
  }, 1e3);
6848
7553
  return () => clearInterval(interval);
6849
7554
  }, [loadData]);
6850
- useEffect6(() => {
7555
+ useEffect7(() => {
6851
7556
  if (detailView) {
6852
7557
  const loadDetail = () => {
6853
7558
  minuteCache.delete(detailView.code);
@@ -6866,7 +7571,7 @@ function StockList({ codes, onExit, onBackToChat }) {
6866
7571
  setDetailPrices(null);
6867
7572
  setDetailLoading(false);
6868
7573
  }, [detailView]);
6869
- useEffect6(() => {
7574
+ useEffect7(() => {
6870
7575
  if (!detailView) return;
6871
7576
  const timer = setInterval(() => {
6872
7577
  setDetailCountdown((prev) => prev > 0 ? prev - 1 : 10);
@@ -7028,7 +7733,7 @@ function renderDetail(stock, _onBack, prices, countdown = 10, currentTime) {
7028
7733
 
7029
7734
  // src/utils/scan-files.ts
7030
7735
  import { readdir as readdir6 } from "fs/promises";
7031
- import { join as join11, relative as relative7 } from "path";
7736
+ import { join as join11, relative as relative8 } from "path";
7032
7737
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
7033
7738
  "node_modules",
7034
7739
  ".git",
@@ -7093,7 +7798,7 @@ async function scanProjectFiles(baseDir, dir) {
7093
7798
  } else if (entry.isFile()) {
7094
7799
  const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
7095
7800
  if (SOURCE_EXTS.has(ext)) {
7096
- files.push(relative7(baseDir, join11(currentDir, name)));
7801
+ files.push(relative8(baseDir, join11(currentDir, name)));
7097
7802
  }
7098
7803
  }
7099
7804
  }