dskcode 0.1.28 → 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
@@ -772,7 +772,7 @@ async function getAllSkills(cwd) {
772
772
 
773
773
  // src/cli/index.tsx
774
774
  import { readFile as readFile11 } from "fs/promises";
775
- import { join as join10 } from "path";
775
+ import { join as join12 } from "path";
776
776
 
777
777
  // src/ui/RenderScope.tsx
778
778
  import { render } from "ink";
@@ -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";
@@ -2277,21 +2744,271 @@ function isENOENT(err) {
2277
2744
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
2278
2745
  }
2279
2746
 
2747
+ // src/logger/logger.ts
2748
+ import { mkdir as mkdir5, appendFile } from "fs/promises";
2749
+ import { fileURLToPath } from "url";
2750
+ import { join as join5, basename, sep } from "path";
2751
+ var LOGGER_DIR = "logger";
2752
+ var MAX_DATA_LEN = 2e3;
2753
+ var MAX_QUEUE = 500;
2754
+ function defaultLogsDir() {
2755
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
2756
+ return join5(home, ".dskcode", "logs");
2757
+ }
2758
+ function projectNameFromCwd(cwd) {
2759
+ const name = basename(cwd);
2760
+ return name || "_root";
2761
+ }
2762
+ var ConversationLogger = class _ConversationLogger {
2763
+ /** 所有活跃的 logger 实例(用于退出时统一 flush) */
2764
+ static #instances = /* @__PURE__ */ new Set();
2765
+ /**
2766
+ * 刷新所有活跃 logger 实例的缓冲,确保事件落盘。
2767
+ * 进程退出前调用。
2768
+ */
2769
+ static async flushAll() {
2770
+ const promises = [];
2771
+ for (const logger of _ConversationLogger.#instances) {
2772
+ promises.push(logger.flush());
2773
+ }
2774
+ await Promise.allSettled(promises);
2775
+ }
2776
+ #logPath;
2777
+ #enabled;
2778
+ #queue = Promise.resolve();
2779
+ #queueLen = 0;
2780
+ #closed = false;
2781
+ constructor(sessionId, cwd, options) {
2782
+ this.#enabled = options?.enabled ?? true;
2783
+ if (!this.#enabled) {
2784
+ this.#logPath = "";
2785
+ return;
2786
+ }
2787
+ const baseDir = options?.logsDir ?? defaultLogsDir();
2788
+ const projectName = projectNameFromCwd(cwd);
2789
+ this.#logPath = join5(baseDir, projectName, `${sessionId}.jsonl`);
2790
+ _ConversationLogger.#instances.add(this);
2791
+ }
2792
+ /** 日志文件完整路径(禁用时返回空字符串) */
2793
+ get logPath() {
2794
+ return this.#logPath;
2795
+ }
2796
+ /** 是否启用日志记录 */
2797
+ get enabled() {
2798
+ return this.#enabled;
2799
+ }
2800
+ /**
2801
+ * 记录一个事件。
2802
+ *
2803
+ * 内部将事件序列化为 pretty JSON 并以多行块的形式追加到日志文件。
2804
+ * 写入操作串行排队,保证事件顺序。调用方无需 await。
2805
+ */
2806
+ log(event) {
2807
+ if (!this.#enabled || this.#closed) return;
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
+ };
2814
+ this.#queueLen++;
2815
+ this.#queue = this.#queue.then(() => this.#writeBlock(enriched)).catch(() => {
2816
+ }).finally(() => this.#queueLen--);
2817
+ }
2818
+ /**
2819
+ * 关闭日志记录器。
2820
+ *
2821
+ * 标记关闭,不再接受新事件,但会等待队列中已有事件写入完成。
2822
+ * 返回的 Promise resolve 后保证所有已 log 的事件都已落盘。
2823
+ */
2824
+ async flush() {
2825
+ this.#closed = true;
2826
+ await this.#queue.catch(() => {
2827
+ });
2828
+ _ConversationLogger.#instances.delete(this);
2829
+ }
2830
+ /** 将一条事件以多行块的形式写入日志文件 */
2831
+ async #writeBlock(event) {
2832
+ const dir = join5(this.#logPath, "..");
2833
+ await mkdir5(dir, { recursive: true });
2834
+ const block = formatBlock(event);
2835
+ await appendFile(this.#logPath, block, "utf-8");
2836
+ }
2837
+ // -----------------------------------------------------------------------
2838
+ // 便捷方法 — 封装常见事件类型,减少调用方样板代码
2839
+ // -----------------------------------------------------------------------
2840
+ /** 记录会话开始 */
2841
+ logSessionStart(sessionId, cwd, model, mode) {
2842
+ this.log({ ts: Date.now(), type: "session_start", sessionId, cwd, model, mode });
2843
+ }
2844
+ /** 记录用户消息 */
2845
+ logUserMessage(content) {
2846
+ this.log({ ts: Date.now(), type: "user_message", content });
2847
+ }
2848
+ /** 记录助手文本(一轮中模型输出的完整文本) */
2849
+ logAssistantText(content, round) {
2850
+ this.log({ ts: Date.now(), type: "assistant_text", content, round });
2851
+ }
2852
+ /** 记录工具调用 */
2853
+ logToolCall(name, callId, args, round) {
2854
+ this.log({ ts: Date.now(), type: "tool_call", name, callId, arguments: args, round });
2855
+ }
2856
+ /** 记录工具结果 */
2857
+ logToolResult(name, callId, success, data, error, elapsed, round) {
2858
+ this.log({
2859
+ ts: Date.now(),
2860
+ type: "tool_result",
2861
+ name,
2862
+ callId,
2863
+ success,
2864
+ data: truncate(data),
2865
+ ...error ? { error } : {},
2866
+ ...elapsed !== void 0 ? { elapsed } : {},
2867
+ round
2868
+ });
2869
+ }
2870
+ /** 记录 Token 用量与费用 */
2871
+ logUsage(model, promptTokens, completionTokens, cachedPromptTokens, cost, round) {
2872
+ this.log({
2873
+ ts: Date.now(),
2874
+ type: "usage",
2875
+ model,
2876
+ promptTokens,
2877
+ completionTokens,
2878
+ ...cachedPromptTokens !== void 0 ? { cachedPromptTokens } : {},
2879
+ cost,
2880
+ round
2881
+ });
2882
+ }
2883
+ /** 记录错误 */
2884
+ logError(message, stack) {
2885
+ this.log({ ts: Date.now(), type: "error", message, ...stack ? { stack } : {} });
2886
+ }
2887
+ /** 记录一轮对话完成 */
2888
+ logTurnDone(elapsed, toolRounds) {
2889
+ this.log({ ts: Date.now(), type: "turn_done", elapsed, toolRounds });
2890
+ }
2891
+ /** 记录会话结束 */
2892
+ logSessionEnd(elapsed) {
2893
+ this.log({ ts: Date.now(), type: "session_end", elapsed });
2894
+ }
2895
+ /** 记录反射(工具失败归因注入到下一轮 prompt 时) */
2896
+ logReflections(items) {
2897
+ this.log({ ts: Date.now(), type: "reflection", items });
2898
+ }
2899
+ };
2900
+ function truncate(text) {
2901
+ if (text.length <= MAX_DATA_LEN) return text;
2902
+ return text.slice(0, MAX_DATA_LEN) + `...[\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u957F\u5EA6 ${text.length}]`;
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
+ }
2965
+
2280
2966
  // src/agent/index.ts
2281
2967
  var Session = class _Session {
2968
+ /** 当前会话的消息历史(含 system/user/assistant/tool),可外部只读 */
2282
2969
  #messages = [];
2970
+ /** LLM Provider(DeepSeek 等) */
2283
2971
  #provider;
2972
+ /** 工具注册表(所有可用工具) */
2284
2973
  #toolRegistry;
2974
+ /** 成本追踪器(今日 + 本会话) */
2285
2975
  #costTracker;
2976
+ /** 归一化后的构造选项(带默认值) */
2286
2977
  #options;
2978
+ /** 中止信号控制器(abort() 时触发,传递给 LLM 和工具) */
2287
2979
  #abortController = new AbortController();
2980
+ /** 会话唯一 ID(UUID) */
2288
2981
  #sessionId;
2982
+ /** 持久化存储;传 false 时为 null */
2289
2983
  #store;
2984
+ /** 会话创建时间(毫秒) */
2290
2985
  #createdAt;
2986
+ /** 节流持久化定时器(500ms debounce) */
2291
2987
  #persistTimer = null;
2988
+ /** user 消息 → Checkpoint 映射(仅给 user 消息建点) */
2292
2989
  #checkpoints = /* @__PURE__ */ new Map();
2990
+ /** 最近的失败记录(仅失败的,用于风暴检测) */
2293
2991
  #stormRecords = [];
2992
+ /** 当前会话模式:code(默认)/ plan(只读) */
2294
2993
  #mode = "code";
2994
+ /** 对话日志记录器(写入 .dskcode/logs/) */
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
+ */
2295
3012
  constructor(provider, tools = [], costTracker, options) {
2296
3013
  this.#provider = provider;
2297
3014
  if (tools instanceof ToolRegistry) {
@@ -2309,45 +3026,87 @@ var Session = class _Session {
2309
3026
  projectContext: options?.projectContext,
2310
3027
  gate: options?.gate ?? new AlwaysAllowGate(),
2311
3028
  writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()],
2312
- enableCheckpoint: options?.enableCheckpoint ?? true
3029
+ enableCheckpoint: options?.enableCheckpoint ?? true,
3030
+ enableLog: options?.enableLog ?? true,
3031
+ enableReflection: options?.enableReflection !== false
2313
3032
  };
2314
3033
  this.#sessionId = options?.sessionId ?? SessionStore.newId();
2315
3034
  this.#store = options?.store === false ? null : options?.store ?? new SessionStore();
2316
3035
  this.#createdAt = Date.now();
3036
+ this.#logger = new ConversationLogger(this.#sessionId, this.#options.cwd, {
3037
+ enabled: this.#options.enableLog
3038
+ });
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;
2317
3042
  }
3043
+ /** 当前会话的完整消息历史(只读视图) */
2318
3044
  get messages() {
2319
3045
  return this.#messages;
2320
3046
  }
3047
+ /** 本次会话累计成本(人民币元) */
2321
3048
  get accumulatedCost() {
2322
3049
  return this.#costTracker.sessionTotalCost;
2323
3050
  }
3051
+ /** 成本追踪器(含今日总成本、会话成本、模型维度统计) */
2324
3052
  get costTracker() {
2325
3053
  return this.#costTracker;
2326
3054
  }
3055
+ /** 当前模型标识(如 "deepseek-v4-flash") */
2327
3056
  get model() {
2328
3057
  return this.#provider.model();
2329
3058
  }
3059
+ /** 工具注册表(可外部读 / 运行时 register) */
2330
3060
  get toolRegistry() {
2331
3061
  return this.#toolRegistry;
2332
3062
  }
3063
+ /** 当前模式:"code" | "plan" */
2333
3064
  get mode() {
2334
3065
  return this.#mode;
2335
3066
  }
3067
+ /** 会话 ID(UUID) */
2336
3068
  get id() {
2337
3069
  return this.#sessionId;
2338
3070
  }
3071
+ /** 持久化存储实例(禁用持久化时为 null) */
2339
3072
  get store() {
2340
3073
  return this.#store;
2341
3074
  }
3075
+ /** 会话创建时间戳(毫秒) */
2342
3076
  get createdAt() {
2343
3077
  return this.#createdAt;
2344
3078
  }
3079
+ /**
3080
+ * 切换会话模式。
3081
+ *
3082
+ * @param mode — "code":全部工具;"plan":只暴露只读工具
3083
+ * @returns 设置后的模式(便于链式调用)
3084
+ */
2345
3085
  setMode(mode) {
2346
3086
  this.#mode = mode;
2347
3087
  return this.#mode;
2348
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
+ */
2349
3107
  async *chat(userInput, opts) {
2350
3108
  this.#messages.push({ role: "user", content: userInput });
3109
+ this.#logger.logUserMessage(userInput);
2351
3110
  const userMsgIndex = this.#messages.length - 1;
2352
3111
  if (this.#options.enableCheckpoint) {
2353
3112
  try {
@@ -2358,9 +3117,23 @@ var Session = class _Session {
2358
3117
  }
2359
3118
  const startTime = Date.now();
2360
3119
  let toolRounds = 0;
3120
+ const toolExecutor = this.#buildToolExecutor();
2361
3121
  try {
2362
3122
  while (toolRounds < this.#options.maxToolRounds) {
2363
- 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
+ }
2364
3137
  const [trimmed] = trimMessages(
2365
3138
  [...this.#messages],
2366
3139
  {
@@ -2371,7 +3144,7 @@ var Session = class _Session {
2371
3144
  }
2372
3145
  );
2373
3146
  const apiMessages = buildApiMessages(systemPrompt, trimmed);
2374
- const toolDefs = this.#buildToolDefinitions();
3147
+ const toolDefs = buildToolDefinitions(this.#toolRegistry, this.#mode);
2375
3148
  const stream = this.#provider.chat(apiMessages, {
2376
3149
  signal: this.#abortController.signal,
2377
3150
  tools: toolDefs.length > 0 ? toolDefs : void 0,
@@ -2380,30 +3153,91 @@ var Session = class _Session {
2380
3153
  responseFormat: opts?.responseFormat,
2381
3154
  toolChoice: opts?.toolChoice
2382
3155
  });
3156
+ const modelId = this.#provider.model();
2383
3157
  let accumulatedText = "";
2384
3158
  let lastUsage;
2385
3159
  let lastToolCalls;
2386
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;
2387
3167
  for await (const chunk of stream) {
2388
3168
  if (chunk.content) {
2389
3169
  accumulatedText += chunk.content;
2390
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
+ }
2391
3188
  }
2392
3189
  if (chunk.toolCalls && chunk.toolCalls.length > 0) lastToolCalls = chunk.toolCalls;
2393
- 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
+ }
2394
3201
  if (chunk.finishReason) _lastFinishReason = chunk.finishReason;
2395
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
+ }
2396
3218
  if (lastUsage) {
2397
- const modelId = this.#provider.model();
2398
3219
  this.#costTracker.record(lastUsage, modelId);
3220
+ const cost = calculateCost(lastUsage, modelId);
3221
+ this.#logger.logUsage(
3222
+ modelId,
3223
+ lastUsage.promptTokens,
3224
+ lastUsage.completionTokens,
3225
+ lastUsage.cachedPromptTokens,
3226
+ cost.totalCost,
3227
+ toolRounds
3228
+ );
2399
3229
  yield { type: "usage", usage: lastUsage, model: modelId };
2400
3230
  }
2401
3231
  const assistantMsg = { role: "assistant", content: accumulatedText };
2402
3232
  if (lastToolCalls && lastToolCalls.length > 0) assistantMsg.toolCalls = lastToolCalls;
2403
3233
  this.#messages.push(assistantMsg);
3234
+ this.#logger.logAssistantText(accumulatedText, toolRounds);
2404
3235
  if (lastToolCalls && lastToolCalls.length > 0) {
2405
3236
  yield { type: "tool_calls", calls: lastToolCalls };
2406
- const stormBroken = this.#checkStormBreak(lastToolCalls);
3237
+ for (const tc of lastToolCalls) {
3238
+ this.#logger.logToolCall(tc.name, tc.id, tc.arguments, toolRounds);
3239
+ }
3240
+ const stormBroken = this.#stormDetector.shouldBreak(this.#stormRecords, lastToolCalls);
2407
3241
  if (stormBroken) {
2408
3242
  const stormMsg = "\n\u26A0\uFE0F \u540C\u4E00\u5DE5\u5177\u91CD\u590D\u51FA\u9519\uFF0C\u5DF2\u5F3A\u5236\u5207\u6362\u7B56\u7565\n";
2409
3243
  yield { type: "text_delta", content: stormMsg };
@@ -2413,10 +3247,29 @@ var Session = class _Session {
2413
3247
  toolRounds++;
2414
3248
  continue;
2415
3249
  }
2416
- const results = await this.#executeBatch(lastToolCalls);
3250
+ const results = await toolExecutor.executeBatch(lastToolCalls);
2417
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
+ });
2418
3262
  for (const item of results.items) {
2419
3263
  yield { type: "tool_result", name: item.name, result: item.result };
3264
+ this.#logger.logToolResult(
3265
+ item.name,
3266
+ item.callId,
3267
+ item.result.success,
3268
+ item.result.data,
3269
+ item.result.error,
3270
+ void 0,
3271
+ toolRounds
3272
+ );
2420
3273
  let toolContent = item.result.data;
2421
3274
  if (item.result.diff && item.result.diff.patch) toolContent += `
2422
3275
 
@@ -2436,107 +3289,29 @@ ${item.result.diff.patch}`;
2436
3289
  } catch (err) {
2437
3290
  if (err instanceof DOMException && err.name === "AbortError") return;
2438
3291
  if (err instanceof Error && err.name === "AbortError") return;
3292
+ this.#logger.logError(
3293
+ err instanceof Error ? err.message : String(err),
3294
+ err instanceof Error ? err.stack : void 0
3295
+ );
2439
3296
  yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
2440
3297
  return;
2441
3298
  }
2442
3299
  const elapsed = Date.now() - startTime;
3300
+ this.#logger.logTurnDone(elapsed, toolRounds);
2443
3301
  void this.#persist();
2444
3302
  await this.#costTracker.flush().catch(() => {
2445
3303
  });
2446
3304
  yield { type: "done", elapsed };
2447
3305
  }
2448
- async #executeBatch(calls) {
2449
- const toolCtx = {
2450
- cwd: this.#options.cwd,
2451
- signal: this.#abortController.signal,
2452
- writeRoots: this.#options.writeRoots
2453
- };
2454
- const allReadOnly = calls.every((tc) => {
2455
- const tool = this.#toolRegistry.get(tc.name);
2456
- return tool ? isReadOnly(tool.kind) : true;
2457
- });
2458
- if (allReadOnly && calls.length > 1) {
2459
- const MAX_PARALLEL = 8;
2460
- const items2 = [];
2461
- const records2 = [];
2462
- for (let i = 0; i < calls.length; i += MAX_PARALLEL) {
2463
- const batch = calls.slice(i, i + MAX_PARALLEL);
2464
- const promises = batch.map((tc) => this.#executeOne(tc, toolCtx));
2465
- const batchResults = await Promise.all(promises);
2466
- for (const r of batchResults) {
2467
- items2.push(r.item);
2468
- records2.push(r.record);
2469
- }
2470
- }
2471
- return { items: items2, records: records2 };
2472
- }
2473
- const items = [];
2474
- const records = [];
2475
- for (const tc of calls) {
2476
- const r = await this.#executeOne(tc, toolCtx);
2477
- items.push(r.item);
2478
- records.push(r.record);
2479
- }
2480
- return { items, records };
2481
- }
2482
- async #executeOne(tc, ctx) {
2483
- const toolName = tc.name;
2484
- const timestamp = Date.now();
2485
- const tool = this.#toolRegistry.get(toolName);
2486
- if (!tool) {
2487
- const errMsg = `\u5DE5\u5177 "${toolName}" \u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u7981\u7528`;
2488
- return {
2489
- item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "TOOL_NOT_FOUND" } },
2490
- record: { name: toolName, success: false, error: "TOOL_NOT_FOUND", timestamp }
2491
- };
2492
- }
2493
- let toolArgs;
2494
- try {
2495
- toolArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
2496
- } catch {
2497
- toolArgs = {};
2498
- }
2499
- const gateResult = await this.#options.gate.check(toolName, toolArgs);
2500
- if (!gateResult) {
2501
- const errMsg = `\u5DE5\u5177 "${toolName}" \u88AB\u6743\u9650\u95E8\u62D2\u7EDD`;
2502
- return {
2503
- item: { name: toolName, callId: tc.id, result: { success: false, data: errMsg, error: "GATE_DENIED" } },
2504
- record: { name: toolName, success: false, error: "GATE_DENIED", timestamp }
2505
- };
2506
- }
2507
- if (!isReadOnly(tool.kind)) {
2508
- const maybePreview = tool.preview;
2509
- if (typeof maybePreview === "function") {
2510
- try {
2511
- await maybePreview(toolArgs, ctx);
2512
- } catch {
2513
- }
2514
- }
2515
- }
2516
- try {
2517
- const result = await tool.execute(toolArgs, ctx);
2518
- return {
2519
- item: { name: toolName, callId: tc.id, result },
2520
- record: { name: toolName, success: result.success, error: result.error, timestamp }
2521
- };
2522
- } catch (err) {
2523
- const message = err instanceof Error ? err.message : String(err);
2524
- const errorResult = { success: false, data: `\u5DE5\u5177 "${toolName}" \u6267\u884C\u5F02\u5E38\uFF1A${message}`, error: "EXECUTION_ERROR" };
2525
- return {
2526
- item: { name: toolName, callId: tc.id, result: errorResult },
2527
- record: { name: toolName, success: false, error: "EXECUTION_ERROR", timestamp }
2528
- };
2529
- }
2530
- }
2531
- #checkStormBreak(currentCalls) {
2532
- if (this.#stormRecords.length < 3) return false;
2533
- const recentErrors = this.#stormRecords.slice(-3);
2534
- if (recentErrors.length < 3) return false;
2535
- const first = recentErrors[0];
2536
- const allSame = recentErrors.every((r) => r.name === first.name && r.error === first.error && !r.success);
2537
- if (!allSame) return false;
2538
- return currentCalls.some((tc) => tc.name === first.name);
2539
- }
3306
+ // -------------------------------------------------------------------------
3307
+ // 持久化与恢复
3308
+ // -------------------------------------------------------------------------
3309
+ /**
3310
+ * 中止当前会话:触发 AbortController,停止流式 LLM 和正在执行的工具。
3311
+ * 同时清掉节流持久化定时器。
3312
+ *
3313
+ * @sideEffect abort 当前 chat 循环、取消所有 #abortController 监听者
3314
+ */
2540
3315
  abort() {
2541
3316
  this.#abortController.abort();
2542
3317
  if (this.#persistTimer) {
@@ -2544,15 +3319,34 @@ ${item.result.diff.patch}`;
2544
3319
  this.#persistTimer = null;
2545
3320
  }
2546
3321
  }
3322
+ /**
3323
+ * 刷新日志缓冲,确保所有已记录的事件落盘。
3324
+ * 通常在进程退出前调用,避免日志丢失。
3325
+ *
3326
+ * @sideEffect 写入 .dskcode/logs/ 下的日志文件
3327
+ */
3328
+ async flushLog() {
3329
+ await this.#logger.flush();
3330
+ }
3331
+ /**
3332
+ * 清空消息历史、会话成本、风暴记录、checkpoints。
3333
+ * 注意:不会清掉磁盘上的会话记录(用 delete() 删除),仅重置内存状态。
3334
+ *
3335
+ * @sideEffect 抹掉 messages / costTracker / stormRecords / checkpoints
3336
+ */
2547
3337
  reset() {
2548
3338
  this.#messages.length = 0;
2549
3339
  this.#costTracker.resetSession();
2550
3340
  this.#stormRecords = [];
2551
3341
  this.#checkpoints.clear();
3342
+ this.#lastRoundResults = null;
2552
3343
  }
2553
- // -------------------------------------------------------------------------
2554
- // 持久化与恢复
2555
- // -------------------------------------------------------------------------
3344
+ /**
3345
+ * 立即把当前状态写入持久化(不等 500ms debounce)。
3346
+ * 用于 UI 上"立即保存"按钮或异常退出前的最后兜底。
3347
+ *
3348
+ * @sideEffect 调用 #doPersist(),写入磁盘
3349
+ */
2556
3350
  async persistNow() {
2557
3351
  if (this.#persistTimer) {
2558
3352
  clearTimeout(this.#persistTimer);
@@ -2560,6 +3354,12 @@ ${item.result.diff.patch}`;
2560
3354
  }
2561
3355
  await this.#doPersist();
2562
3356
  }
3357
+ /**
3358
+ * 节流持久化:500ms 内多次调用只会真正落盘一次。
3359
+ * 通过 setTimeout + timer.refresh() 实现 debounce。
3360
+ *
3361
+ * @sideEffect 调度一个 500ms 后的 #doPersist() 任务
3362
+ */
2563
3363
  #persist() {
2564
3364
  if (!this.#store) return;
2565
3365
  if (this.#persistTimer) {
@@ -2572,6 +3372,12 @@ ${item.result.diff.patch}`;
2572
3372
  }, 500);
2573
3373
  this.#persistTimer.unref();
2574
3374
  }
3375
+ /**
3376
+ * 真正执行持久化:构造 StoredSession 并写入 store。
3377
+ * 失败仅 console.error,不抛给调用方(持久化失败不应阻塞对话)。
3378
+ *
3379
+ * @sideEffect 写磁盘 ~/.dskcode/sessions/<id>.json
3380
+ */
2575
3381
  async #doPersist() {
2576
3382
  if (!this.#store) return;
2577
3383
  const stored = {
@@ -2590,12 +3396,24 @@ ${item.result.diff.patch}`;
2590
3396
  console.error("[Session] \u6301\u4E45\u5316\u5931\u8D25:", err);
2591
3397
  }
2592
3398
  }
3399
+ /**
3400
+ * 从消息历史推导会话标题:取第一条非空 user 消息的前 40 字。
3401
+ * 无任何 user 消息时返回 "新会话"。
3402
+ *
3403
+ * @pure 不修改任何状态
3404
+ */
2593
3405
  #deriveTitle() {
2594
3406
  for (const m of this.#messages) {
2595
3407
  if (m.role === "user" && m.content.trim()) return m.content.trim().slice(0, 40);
2596
3408
  }
2597
3409
  return "\u65B0\u4F1A\u8BDD";
2598
3410
  }
3411
+ /**
3412
+ * 把内存 messages 转成可持久化的 StoredSession["messages"]。
3413
+ * 给 user 消息挂上对应 checkpoint(若存在),用于 rewind 时文件回退。
3414
+ *
3415
+ * @pure 不修改任何状态
3416
+ */
2599
3417
  #serializeMessages() {
2600
3418
  return this.#messages.map((msg, idx) => {
2601
3419
  const checkpoint = this.#checkpoints.get(idx);
@@ -2603,6 +3421,19 @@ ${item.result.diff.patch}`;
2603
3421
  return { ...msg };
2604
3422
  });
2605
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
+ */
2606
3437
  static async resume(id, provider, tools = [], costTracker, options) {
2607
3438
  const store = options?.store === false ? null : options?.store ?? new SessionStore();
2608
3439
  if (!store) throw new Error("resume \u9700\u8981\u542F\u7528\u6301\u4E45\u5316\uFF08options.store \u4E0D\u80FD\u4E3A false\uFF09");
@@ -2628,6 +3459,13 @@ ${item.result.diff.patch}`;
2628
3459
  // -------------------------------------------------------------------------
2629
3460
  // 检查点与 Rewind
2630
3461
  // -------------------------------------------------------------------------
3462
+ /**
3463
+ * 列出所有 user 消息对应的 checkpoint(按 index 升序)。
3464
+ * 用于 UI 展示 /rewind 列表。
3465
+ *
3466
+ * @returns 每个元素的 index = messages 数组索引,可直接传给 rewind()
3467
+ * @pure 不修改任何状态
3468
+ */
2631
3469
  listCheckpoints() {
2632
3470
  const result = [];
2633
3471
  for (const [index, checkpoint] of this.#checkpoints) {
@@ -2637,6 +3475,25 @@ ${item.result.diff.patch}`;
2637
3475
  }
2638
3476
  return result.sort((a, b) => a.index - b.index);
2639
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
+ */
2640
3497
  async rewind(targetIndex) {
2641
3498
  if (targetIndex < 0 || targetIndex >= this.#messages.length) {
2642
3499
  return { ok: false, error: `\u65E0\u6548\u7684\u6D88\u606F\u7D22\u5F15 ${targetIndex}` };
@@ -2676,19 +3533,58 @@ ${item.result.diff.patch}`;
2676
3533
  this.#persist();
2677
3534
  return { ok: true, fileRestored };
2678
3535
  }
3536
+ /**
3537
+ * 是否存在可用的 checkpoint(决定 UI 是否显示 /rewind 入口)。
3538
+ *
3539
+ * @pure 不修改任何状态
3540
+ */
2679
3541
  hasCheckpoints() {
2680
3542
  return this.listCheckpoints().length > 0;
2681
3543
  }
3544
+ /**
3545
+ * 彻底删除会话:从磁盘移除 SessionStore 条目、丢弃所有 checkpoint、关闭日志。
3546
+ * 删除后该 Session 实例不应再被使用。
3547
+ *
3548
+ * @sideEffect 删磁盘文件、清 checkpoints、flush 并关闭 logger
3549
+ */
2682
3550
  async delete() {
2683
3551
  if (this.#store) await this.#store.delete(this.#sessionId);
2684
3552
  for (const cp2 of this.#checkpoints.values()) {
2685
3553
  void discardCheckpoint(cp2);
2686
3554
  }
2687
3555
  this.#checkpoints.clear();
3556
+ this.#logger.logSessionEnd(Date.now() - this.#createdAt);
3557
+ await this.#logger.flush();
2688
3558
  }
2689
3559
  // -------------------------------------------------------------------------
2690
3560
  // 内部方法
2691
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
+ */
2692
3588
  #buildSystemPrompt() {
2693
3589
  const enabledTools = this.#toolRegistry.list();
2694
3590
  const toolDescs = enabledTools.map((t) => ({
@@ -2706,17 +3602,10 @@ ${item.result.diff.patch}`;
2706
3602
  if (this.#mode === "plan") return buildPlanSystemPrompt(opts);
2707
3603
  return buildSystemPrompt(opts);
2708
3604
  }
2709
- #buildToolDefinitions() {
2710
- const tools = this.#mode === "plan" ? this.#toolRegistry.listReadTools() : this.#toolRegistry.list();
2711
- return tools.map((t) => ({
2712
- type: "function",
2713
- function: { name: t.name, description: t.description, parameters: t.parameters }
2714
- }));
2715
- }
2716
3605
  };
2717
3606
 
2718
3607
  // src/tool/sandbox.ts
2719
- import { resolve, relative, isAbsolute } from "path";
3608
+ import { resolve, relative as relative2, isAbsolute } from "path";
2720
3609
  import { realpath as realpath2 } from "fs/promises";
2721
3610
  import { spawn } from "child_process";
2722
3611
  import process2 from "process";
@@ -2735,7 +3624,7 @@ async function realPath(target) {
2735
3624
  const parent = resolve(target, "..");
2736
3625
  try {
2737
3626
  const realParent = await realpath2(parent);
2738
- return resolve(realParent, relative(parent, target));
3627
+ return resolve(realParent, relative2(parent, target));
2739
3628
  } catch {
2740
3629
  return resolve(target);
2741
3630
  }
@@ -2748,7 +3637,7 @@ async function confine(allowedRoots, target) {
2748
3637
  const realTarget = await realPath(target);
2749
3638
  for (const root of allowedRoots) {
2750
3639
  const realRoot = await realPath(root);
2751
- const rel = relative(realRoot, realTarget);
3640
+ const rel = relative2(realRoot, realTarget);
2752
3641
  if (!rel.startsWith("..") && rel !== "" && !rel.startsWith("/") && !rel.startsWith("\\")) {
2753
3642
  return { ok: true };
2754
3643
  }
@@ -2780,6 +3669,10 @@ async function execCommand(command, args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, s
2780
3669
  spawnCmd = command;
2781
3670
  spawnArgs = args;
2782
3671
  useShell = false;
3672
+ if (isWindows && spawnCmd === "cmd" && spawnArgs[0] === "/c") {
3673
+ const prefixed = `chcp 65001 >nul && ${spawnArgs[1] ?? ""}`;
3674
+ spawnArgs = ["/c", prefixed];
3675
+ }
2783
3676
  } else {
2784
3677
  useShell = !isWindows;
2785
3678
  spawnCmd = isWindows ? "cmd" : command;
@@ -2794,10 +3687,10 @@ async function execCommand(command, args, cwd, timeoutMs = DEFAULT_TIMEOUT_MS, s
2794
3687
  let stdout = "";
2795
3688
  let stderr = "";
2796
3689
  child.stdout.on("data", (data) => {
2797
- stdout += data.toString();
3690
+ stdout += data.toString("utf8");
2798
3691
  });
2799
3692
  child.stderr.on("data", (data) => {
2800
- stderr += data.toString();
3693
+ stderr += data.toString("utf8");
2801
3694
  });
2802
3695
  const timeout = setTimeout(() => {
2803
3696
  child.kill("SIGTERM");
@@ -3141,7 +4034,7 @@ async function writeFileWithEol(filePath, originalContent, newContent) {
3141
4034
  // src/tool/builtins/read-file.ts
3142
4035
  import { readFile as readFile5, stat } from "fs/promises";
3143
4036
  import { open } from "fs/promises";
3144
- import { relative as relative2 } from "path";
4037
+ import { relative as relative3 } from "path";
3145
4038
  async function checkBinary(filePath) {
3146
4039
  const fileHandle = await open(filePath, "r");
3147
4040
  try {
@@ -3224,7 +4117,7 @@ var readFileTool = {
3224
4117
  const tailHint = remaining > 0 ? `
3225
4118
 
3226
4119
  [\u8FD8\u6709 ${remaining} \u884C\uFF1B\u4F7F\u7528 startLine=${endLine + 1} \u7EE7\u7EED\u67E5\u770B]` : "";
3227
- const relPath = relative2(ctx.cwd, filePath).replace(/\\/g, "/");
4120
+ const relPath = relative3(ctx.cwd, filePath).replace(/\\/g, "/");
3228
4121
  const rangeLabel = startLine > 0 || endLine < lines.length ? `\u7B2C ${startLine + 1}-${endLine} \u884C` : `${lines.length} \u884C`;
3229
4122
  return {
3230
4123
  success: true,
@@ -3243,8 +4136,8 @@ var readFileTool = {
3243
4136
  };
3244
4137
 
3245
4138
  // src/tool/builtins/write-file.ts
3246
- import { mkdir as mkdir5, readFile as readFile6 } from "fs/promises";
3247
- import { dirname, relative as relative3, basename } from "path";
4139
+ import { mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
4140
+ import { dirname, relative as relative4, basename as basename2 } from "path";
3248
4141
  var writeFileTool = {
3249
4142
  name: "write_file",
3250
4143
  kind: "edit" /* Edit */,
@@ -3286,7 +4179,7 @@ var writeFileTool = {
3286
4179
  existedBefore = true;
3287
4180
  } catch {
3288
4181
  }
3289
- await mkdir5(dirname(filePath), { recursive: true });
4182
+ await mkdir6(dirname(filePath), { recursive: true });
3290
4183
  const content = args.content;
3291
4184
  await writeFileWithEol(filePath, oldContent, content);
3292
4185
  const diff = computeFileDiff(oldContent, content, filePath);
@@ -3295,11 +4188,11 @@ var writeFileTool = {
3295
4188
  const byteSize = Buffer.byteLength(content, "utf-8");
3296
4189
  const action = existedBefore ? "\u5DF2\u4FEE\u6539" : "\u5DF2\u521B\u5EFA";
3297
4190
  const diffSummary = existedBefore ? `\uFF0C+${diff.additions} -${diff.deletions}` : `\uFF0C+${diff.additions} \u884C\uFF08\u65B0\u5EFA\uFF09`;
3298
- const fileName = basename(filePath);
4191
+ const fileName = basename2(filePath);
3299
4192
  const summary = existedBefore ? `\u{1F4DD} \u4FEE\u6539: ${fileName} (+${diff.additions} -${diff.deletions})` : `\u{1F4DD} \u65B0\u5EFA: ${fileName} (+${diff.additions} \u884C)`;
3300
4193
  return {
3301
4194
  success: true,
3302
- 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`,
3303
4196
  summary,
3304
4197
  diff
3305
4198
  };
@@ -3316,7 +4209,7 @@ var writeFileTool = {
3316
4209
 
3317
4210
  // src/tool/builtins/edit-file.ts
3318
4211
  import { readFile as readFile7 } from "fs/promises";
3319
- import { basename as basename2 } from "path";
4212
+ import { basename as basename3 } from "path";
3320
4213
  var editFileTool = {
3321
4214
  name: "edit_file",
3322
4215
  kind: "edit" /* Edit */,
@@ -3384,7 +4277,7 @@ var editFileTool = {
3384
4277
  const oldLines = args.old_text.split("\n").length;
3385
4278
  const newLines = args.new_text.split("\n").length;
3386
4279
  const diffSummary = `+${diff.additions} -${diff.deletions}`;
3387
- const summary = `\u{1F4DD} \u4FEE\u6539: ${basename2(filePath)} (+${diff.additions} -${diff.deletions})`;
4280
+ const summary = `\u{1F4DD} \u4FEE\u6539: ${basename3(filePath)} (+${diff.additions} -${diff.deletions})`;
3388
4281
  return {
3389
4282
  success: true,
3390
4283
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
@@ -3407,7 +4300,7 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
3407
4300
 
3408
4301
  // src/tool/builtins/multi-edit.ts
3409
4302
  import { readFile as readFile8 } from "fs/promises";
3410
- import { basename as basename3 } from "path";
4303
+ import { basename as basename4 } from "path";
3411
4304
  var multiEditTool = {
3412
4305
  name: "multi_edit",
3413
4306
  kind: "edit" /* Edit */,
@@ -3505,7 +4398,7 @@ var multiEditTool = {
3505
4398
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
3506
4399
  \u5171\u6267\u884C ${args.edits.length} \u6B65\u66FF\u6362
3507
4400
  \u53D8\u66F4\uFF1A+${diff.additions} -${diff.deletions}`,
3508
- summary: `\u{1F4DD} \u4FEE\u6539: ${basename3(filePath)} (${args.edits.length} \u6B65, +${diff.additions} -${diff.deletions})`,
4401
+ summary: `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (${args.edits.length} \u6B65, +${diff.additions} -${diff.deletions})`,
3509
4402
  diff
3510
4403
  };
3511
4404
  } catch (err) {
@@ -3521,7 +4414,7 @@ var multiEditTool = {
3521
4414
 
3522
4415
  // src/tool/builtins/delete-range.ts
3523
4416
  import { readFile as readFile9 } from "fs/promises";
3524
- import { basename as basename4 } from "path";
4417
+ import { basename as basename5 } from "path";
3525
4418
  function findUniqueLine(lines, anchor, label) {
3526
4419
  const matches = [];
3527
4420
  for (let i = 0; i < lines.length; i++) {
@@ -3617,7 +4510,7 @@ var deleteRangeTool = {
3617
4510
  const diff = computeFileDiff(content, newContent, filePath);
3618
4511
  diff.existedBefore = true;
3619
4512
  const deletedLines = rangeEnd - rangeStart + 1;
3620
- const summary = `\u{1F4DD} \u4FEE\u6539: ${basename4(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
4513
+ const summary = `\u{1F4DD} \u4FEE\u6539: ${basename5(filePath)} (\u5220 ${deletedLines} \u884C, +${diff.additions} -${diff.deletions})`;
3621
4514
  return {
3622
4515
  success: true,
3623
4516
  data: `\u6587\u4EF6\u5DF2\u7F16\u8F91\uFF1A${filePath}
@@ -3639,17 +4532,62 @@ var deleteRangeTool = {
3639
4532
 
3640
4533
  // src/tool/builtins/bash.ts
3641
4534
  import process3 from "process";
4535
+ import { existsSync as existsSync3 } from "fs";
4536
+ import { join as join6 } from "path";
3642
4537
  var isWindows2 = process3.platform === "win32";
4538
+ function detectShell() {
4539
+ if (!isWindows2) {
4540
+ return {
4541
+ bin: "sh",
4542
+ args: ["-c"],
4543
+ label: "sh",
4544
+ hint: "\u5F53\u524D shell \u662F bash \u517C\u5BB9\u8BED\u6CD5\uFF0C\u652F\u6301 pwd/ls/&&/\u7BA1\u9053\u7B49\u6807\u51C6 Unix \u5DE5\u5177\u3002"
4545
+ };
4546
+ }
4547
+ const envBash = process3.env.DSKCODE_BASH;
4548
+ if (envBash && existsSync3(envBash)) {
4549
+ return { bin: envBash, args: ["-c"], label: "Git Bash", hint: SHELL_HINT_BASH };
4550
+ }
4551
+ const roots = [
4552
+ process3.env.ProgramFiles,
4553
+ process3.env["ProgramFiles(x86)"],
4554
+ process3.env.LOCALAPPDATA ? join6(process3.env.LOCALAPPDATA, "Programs") : null,
4555
+ // 非 C 盘安装(你机器就是 D 盘),枚举几个常见盘符根
4556
+ "D:\\Program Files",
4557
+ "E:\\Program Files"
4558
+ ].filter((v) => Boolean(v));
4559
+ for (const root of roots) {
4560
+ for (const sub of ["Git\\bin\\bash.exe", "Git\\usr\\bin\\bash.exe"]) {
4561
+ const candidate = join6(root, sub);
4562
+ if (existsSync3(candidate)) {
4563
+ return { bin: candidate, args: ["-c"], label: "Git Bash", hint: SHELL_HINT_BASH };
4564
+ }
4565
+ }
4566
+ }
4567
+ return {
4568
+ bin: "cmd",
4569
+ args: ["/c"],
4570
+ label: "cmd.exe",
4571
+ hint: "\u5F53\u524D shell \u662F Windows cmd.exe\u3002\u8BF7\u4F7F\u7528 cmd \u8BED\u6CD5\uFF1A\u8DEF\u5F84\u7528\u53CD\u659C\u6760\u6216\u6B63\u659C\u6760\u5747\u53EF\u3001\u547D\u4EE4\u94FE\u7528 & \u6216\u6362\u884C\uFF0C\u907F\u514D\u4F7F\u7528 pwd/ls/&& \u7B49 Unix \u547D\u4EE4\u3002"
4572
+ };
4573
+ }
4574
+ var SHELL_HINT_BASH = "\u5F53\u524D shell \u662F bash \u517C\u5BB9\u8BED\u6CD5\uFF0C\u652F\u6301 pwd/ls/&&/\u7BA1\u9053\u7B49\u6807\u51C6 Unix \u5DE5\u5177\u3002";
4575
+ var SHELL_INFO = detectShell();
3643
4576
  var bashTool = {
3644
4577
  name: "bash",
3645
4578
  kind: "other" /* Other */,
3646
- description: "\u5728 shell \u4E2D\u6267\u884C\u547D\u4EE4\u3002\u8FD4\u56DE\u6807\u51C6\u8F93\u51FA\u3001\u6807\u51C6\u9519\u8BEF\u548C\u9000\u51FA\u7801\u3002\u652F\u6301\u8D85\u65F6\u63A7\u5236\u548C\u4FE1\u53F7\u4E2D\u6B62\u3002\u9002\u7528\u4E8E\u8FD0\u884C\u6784\u5EFA\u3001\u6D4B\u8BD5\u3001Git \u64CD\u4F5C\u7B49\u547D\u4EE4\u3002",
4579
+ description: [
4580
+ "\u5728 shell \u4E2D\u6267\u884C\u547D\u4EE4\uFF0C\u8FD4\u56DE stdout / stderr / \u9000\u51FA\u7801\u3002\u652F\u6301\u8D85\u65F6\u63A7\u5236\u548C\u4FE1\u53F7\u4E2D\u6B62\u3002",
4581
+ `\u5F53\u524D shell\uFF1A${SHELL_INFO.label}\u3002${SHELL_INFO.hint}`,
4582
+ "\u8BFB\u53D6\u6587\u4EF6\u8BF7\u4F18\u5148\u4F7F\u7528 read_file \u5DE5\u5177\uFF0C\u4E0D\u8981\u7528 cat/type/Get-Content\uFF1B",
4583
+ "\u641C\u7D22\u4EE3\u7801\u7528 grep / glob\uFF0C\u5217\u76EE\u5F55\u7528 ls\u2014\u2014\u8FD9\u4E9B\u4E13\u7528\u5DE5\u5177\u5728\u6240\u6709\u5E73\u53F0\u90FD\u53EF\u7528\u4E14\u65E0\u9700\u5173\u5FC3 shell \u8BED\u6CD5\u3002"
4584
+ ].join(""),
3647
4585
  parameters: {
3648
4586
  type: "object",
3649
4587
  properties: {
3650
4588
  command: {
3651
4589
  type: "string",
3652
- description: "\u8981\u6267\u884C\u7684 shell \u547D\u4EE4"
4590
+ description: `\u8981\u6267\u884C\u7684 shell \u547D\u4EE4\uFF08\u5F53\u524D shell\uFF1A${SHELL_INFO.label}\uFF09`
3653
4591
  },
3654
4592
  timeout: {
3655
4593
  type: "number",
@@ -3665,8 +4603,8 @@ var bashTool = {
3665
4603
  }
3666
4604
  const timeout = args.timeout ?? ctx.timeout ?? getDefaultTimeout();
3667
4605
  try {
3668
- const shellCommand = isWindows2 ? "cmd" : "sh";
3669
- const shellArgs = isWindows2 ? ["/c", args.command] : ["-c", args.command];
4606
+ const shellCommand = SHELL_INFO.bin ?? "cmd";
4607
+ const shellArgs = [...SHELL_INFO.args, args.command];
3670
4608
  const result = await execCommand(
3671
4609
  shellCommand,
3672
4610
  shellArgs,
@@ -3707,7 +4645,7 @@ ${truncateOutput(result.stderr)}`);
3707
4645
 
3708
4646
  // src/tool/builtins/glob.ts
3709
4647
  import { readdir as readdir3, stat as stat2 } from "fs/promises";
3710
- import { join as join5, relative as relative4, isAbsolute as isAbsolute2 } from "path";
4648
+ import { join as join7, relative as relative5, isAbsolute as isAbsolute2 } from "path";
3711
4649
  function globToRegex(pattern) {
3712
4650
  let regexStr = pattern;
3713
4651
  regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
@@ -3732,8 +4670,8 @@ async function walkDir(dir, baseDir) {
3732
4670
  if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git")) {
3733
4671
  continue;
3734
4672
  }
3735
- const fullPath = join5(dir, entry.name);
3736
- const relPath = relative4(baseDir, fullPath);
4673
+ const fullPath = join7(dir, entry.name);
4674
+ const relPath = relative5(baseDir, fullPath);
3737
4675
  if (entry.isDirectory()) {
3738
4676
  results.push(...await walkDir(fullPath, baseDir));
3739
4677
  } else {
@@ -3765,7 +4703,7 @@ var globTool = {
3765
4703
  if (!args?.pattern || typeof args.pattern !== "string") {
3766
4704
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3767
4705
  }
3768
- const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join5(ctx.cwd, args.directory) : ctx.cwd;
4706
+ const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join7(ctx.cwd, args.directory) : ctx.cwd;
3769
4707
  const regex = globToRegex(args.pattern);
3770
4708
  try {
3771
4709
  const dirStat = await stat2(searchDir);
@@ -3805,7 +4743,7 @@ var globTool = {
3805
4743
 
3806
4744
  // src/tool/builtins/grep.ts
3807
4745
  import { readdir as readdir4, readFile as readFile10, stat as stat3 } from "fs/promises";
3808
- import { join as join6, relative as relative5, isAbsolute as isAbsolute3 } from "path";
4746
+ import { join as join8, relative as relative6, isAbsolute as isAbsolute3 } from "path";
3809
4747
  async function collectFiles(dir, extension, maxFiles = 200) {
3810
4748
  const results = [];
3811
4749
  let entries;
@@ -3819,7 +4757,7 @@ async function collectFiles(dir, extension, maxFiles = 200) {
3819
4757
  if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist")) {
3820
4758
  continue;
3821
4759
  }
3822
- const fullPath = join6(dir, entry.name);
4760
+ const fullPath = join8(dir, entry.name);
3823
4761
  if (entry.isDirectory()) {
3824
4762
  results.push(...await collectFiles(fullPath, extension, maxFiles - results.length));
3825
4763
  } else {
@@ -3866,7 +4804,7 @@ var grepTool = {
3866
4804
  if (!args?.pattern || typeof args.pattern !== "string") {
3867
4805
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3868
4806
  }
3869
- const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join6(ctx.cwd, args.directory) : ctx.cwd;
4807
+ const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join8(ctx.cwd, args.directory) : ctx.cwd;
3870
4808
  const maxFiles = args.max_files ?? 200;
3871
4809
  try {
3872
4810
  const flags = args.case_sensitive ? "g" : "gi";
@@ -3881,7 +4819,7 @@ var grepTool = {
3881
4819
  try {
3882
4820
  const content = await readFile10(filePath, "utf-8");
3883
4821
  const lines = content.split("\n");
3884
- const relPath = relative5(searchDir, filePath);
4822
+ const relPath = relative6(searchDir, filePath);
3885
4823
  for (let i = 0; i < lines.length; i++) {
3886
4824
  if (regex.test(lines[i] ?? "")) {
3887
4825
  regex.lastIndex = 0;
@@ -3926,7 +4864,7 @@ var grepTool = {
3926
4864
 
3927
4865
  // src/tool/builtins/ls.ts
3928
4866
  import { readdir as readdir5, stat as stat4 } from "fs/promises";
3929
- import { join as join7, relative as relative6 } from "path";
4867
+ import { join as join9, relative as relative7 } from "path";
3930
4868
  var lsTool = {
3931
4869
  name: "ls",
3932
4870
  kind: "read" /* Read */,
@@ -3964,7 +4902,7 @@ var lsTool = {
3964
4902
  let sizeStr = "";
3965
4903
  if (typeMark === "FILE") {
3966
4904
  try {
3967
- const fileStat = await stat4(join7(dirPath, entry.name));
4905
+ const fileStat = await stat4(join9(dirPath, entry.name));
3968
4906
  if (fileStat.size < 1024) {
3969
4907
  sizeStr = `${fileStat.size}B`;
3970
4908
  } else if (fileStat.size < 1024 * 1024) {
@@ -3980,9 +4918,9 @@ var lsTool = {
3980
4918
  lines.push(`${typeLabel} ${entry.name}${sizeStr ? ` (${sizeStr})` : ""}`);
3981
4919
  }
3982
4920
  if (lines.length === 0) {
3983
- 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` };
3984
4922
  }
3985
- const relPath = relative6(ctx.cwd, dirPath).replace(/\\/g, "/");
4923
+ const relPath = relative7(ctx.cwd, dirPath).replace(/\\/g, "/");
3986
4924
  return {
3987
4925
  success: true,
3988
4926
  data: truncateOutput(`\u76EE\u5F55\uFF1A${dirPath}
@@ -4166,9 +5104,9 @@ function getGradientColors(text, phase, stops) {
4166
5104
  }
4167
5105
 
4168
5106
  // src/ui/ChatSession.tsx
4169
- 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";
4170
5108
  var PHASE_CONFIG = {
4171
- thinking: { icon: "\u{1F9E0}", label: "\u601D\u8003\u4E2D", color: "#ff9800" },
5109
+ thinking: { icon: "\u{1F9E0}", label: "\u6DF1\u5EA6\u601D\u8003\u4E2D", color: "#ff9800" },
4172
5110
  generating: { icon: "\u2728", label: "\u751F\u6210\u4E2D", color: "#00ff41" },
4173
5111
  calling_tools: { icon: "\u{1F6E0}", label: "\u8C03\u7528\u5DE5\u5177", color: "#f59e0b" },
4174
5112
  executing_tools: { icon: "\u26A1", label: "\u6267\u884C\u5DE5\u5177", color: "#00ffff" }
@@ -4240,34 +5178,34 @@ function ChatSession({
4240
5178
  }) {
4241
5179
  const termWidth = typeof process.stdout.columns === "number" ? process.stdout.columns : 80;
4242
5180
  const dividerWidth = Math.max(termWidth - 2, 1);
4243
- const [offset, setOffset] = useState3(0);
4244
- const [displayMessages, setDisplayMessages] = useState3([]);
4245
- const [staticKey, setStaticKey] = useState3(0);
4246
- const [input, setInput] = useState3("");
4247
- const [balance, setBalance] = useState3(null);
4248
- const [balanceLoading, setBalanceLoading] = useState3(false);
4249
- const [todayCost, setTodayCost] = useState3(null);
4250
- const [isStreaming, setIsStreaming] = useState3(false);
4251
- const [streamingPhase, setStreamingPhase] = useState3(null);
4252
- const [streamingPlaceholder, setStreamingPlaceholder] = useState3("");
4253
- const [idlePlaceholder, setIdlePlaceholder] = useState3(() => pickRandom(IDLE_PLACEHOLDERS));
4254
- const [gradientColors, setGradientColors] = useState3([]);
4255
- const gradientPhaseRef = useRef2(0);
4256
- const [streamingGradientColors, setStreamingGradientColors] = useState3([]);
4257
- const streamingPhaseRef = useRef2(0);
4258
- const [currentContent, setCurrentContent] = useState3("");
4259
- const [currentToolCalls, setCurrentToolCalls] = useState3([]);
4260
- const [_currentUsage, setCurrentUsage] = useState3(void 0);
4261
- const [_currentElapsed, setCurrentElapsed] = useState3(void 0);
4262
- const [_currentCost, setCurrentCost] = useState3(void 0);
4263
- const [activeModel, setActiveModel] = useState3(model);
4264
- const [_streamingModel, setStreamingModel] = useState3(void 0);
4265
- const [streamError, setStreamError] = useState3(void 0);
4266
- const [sessionMode, setSessionMode] = useState3("code");
4267
- const [thinkingEnabled, setThinkingEnabled] = useState3(true);
4268
- const [thinkingEffort, setThinkingEffort] = useState3("high");
4269
- const [responseFormat] = useState3("text");
4270
- 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);
4271
5209
  const sessionCost = useMemo(() => {
4272
5210
  return displayMessages.reduce((sum, msg) => {
4273
5211
  if (msg.assistantDetail?.cost) {
@@ -4278,36 +5216,36 @@ function ChatSession({
4278
5216
  }, [displayMessages]);
4279
5217
  const hasConversationStarted = displayMessages.length > 0;
4280
5218
  const cmdTips = Array.from(getRegisteredCommands()).filter(([name]) => name !== "/exit" && name !== "/quit").map(([name, cmd]) => ({ name, desc: cmd.desc }));
4281
- const [cmdTipIndex, setCmdTipIndex] = useState3(0);
4282
- const [cmdTipGradientColors, setCmdTipGradientColors] = useState3([]);
4283
- const cmdTipPhaseRef = useRef2(0);
4284
- const [skillSelectIndex, setSkillSelectIndex] = useState3(0);
4285
- const [fileSelectIndex, setFileSelectIndex] = useState3(0);
4286
- const [inputKey, setInputKey] = useState3(0);
4287
- const [rewindSelecting, setRewindSelecting] = useState3(false);
4288
- const [rewindSelectIndex, setRewindSelectIndex] = useState3(0);
4289
- const [rewindList, setRewindList] = useState3([]);
4290
- const [rewinding, setRewinding] = useState3(false);
4291
- const [rewindHintPhase, setRewindHintPhase] = useState3("idle");
4292
- const currentRoundModifiedRef = useRef2(false);
4293
- const rewindHintTimerRef = useRef2(null);
4294
- const [selectingModel, setSelectingModel] = useState3(false);
4295
- 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);
4296
5234
  const modelOptions = ["deepseek-v4-flash", "deepseek-v4-pro"];
4297
- const sessionRef = useRef2(null);
4298
- const abortRef = useRef2(null);
4299
- const currentContentRef = useRef2("");
4300
- const currentToolCallsRef = useRef2([]);
4301
- const currentUsageRef = useRef2(void 0);
4302
- const currentElapsedRef = useRef2(void 0);
4303
- const currentCostRef = useRef2(void 0);
4304
- const currentModelRef = useRef2(void 0);
4305
- const streamErrorRef = useRef2(void 0);
4306
- 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(() => {
4307
5245
  setSkillSelectIndex(0);
4308
5246
  setFileSelectIndex(0);
4309
5247
  }, [input]);
4310
- useEffect3(() => {
5248
+ useEffect4(() => {
4311
5249
  return () => {
4312
5250
  if (rewindHintTimerRef.current) {
4313
5251
  clearTimeout(rewindHintTimerRef.current);
@@ -4403,7 +5341,12 @@ function ChatSession({
4403
5341
  }
4404
5342
  }, []);
4405
5343
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(() => {
4406
- process.exit(0);
5344
+ const s = sessionRef.current;
5345
+ if (s) {
5346
+ void s.flushLog().finally(() => process.exit(0));
5347
+ } else {
5348
+ process.exit(0);
5349
+ }
4407
5350
  });
4408
5351
  useInput(
4409
5352
  useCallback2(
@@ -4537,14 +5480,14 @@ function ChatSession({
4537
5480
  [selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode, rewindSelecting, rewindSelectIndex, rewindList, doRewind]
4538
5481
  )
4539
5482
  );
4540
- useEffect3(() => {
5483
+ useEffect4(() => {
4541
5484
  if (cmdTips.length <= 1) return;
4542
5485
  const timer = setInterval(() => {
4543
5486
  setCmdTipIndex((prev) => (prev + 1) % cmdTips.length);
4544
5487
  }, 2e3);
4545
5488
  return () => clearInterval(timer);
4546
5489
  }, [cmdTips.length]);
4547
- useEffect3(() => {
5490
+ useEffect4(() => {
4548
5491
  const tip = cmdTips[cmdTipIndex % cmdTips.length];
4549
5492
  if (!tip) {
4550
5493
  setCmdTipGradientColors([]);
@@ -4559,13 +5502,13 @@ function ChatSession({
4559
5502
  }, GRADIENT_ANIMATION.cmdTipInterval);
4560
5503
  return () => clearInterval(interval);
4561
5504
  }, [cmdTipIndex, cmdTips.length]);
4562
- useEffect3(() => {
5505
+ useEffect4(() => {
4563
5506
  const timer = setInterval(() => {
4564
5507
  setOffset((prev) => (prev + 1) % CYBER_PALETTE.length);
4565
5508
  }, 500);
4566
5509
  return () => clearInterval(timer);
4567
5510
  }, []);
4568
- useEffect3(() => {
5511
+ useEffect4(() => {
4569
5512
  if (!apiKey || !baseUrl) return;
4570
5513
  const provider = createProvider({
4571
5514
  name: "deepseek",
@@ -4584,7 +5527,7 @@ function ChatSession({
4584
5527
  sessionRef.current = null;
4585
5528
  };
4586
5529
  }, [apiKey, baseUrl, activeModel, externalCostTracker]);
4587
- useEffect3(() => {
5530
+ useEffect4(() => {
4588
5531
  if (!apiKey || !baseUrl) return;
4589
5532
  let cancelled = false;
4590
5533
  setBalanceLoading(true);
@@ -4609,7 +5552,7 @@ function ChatSession({
4609
5552
  cancelled = true;
4610
5553
  };
4611
5554
  }, [apiKey, baseUrl]);
4612
- useEffect3(() => {
5555
+ useEffect4(() => {
4613
5556
  if (!externalCostTracker) return;
4614
5557
  let cancelled = false;
4615
5558
  let timer;
@@ -4627,7 +5570,7 @@ function ChatSession({
4627
5570
  if (timer) clearInterval(timer);
4628
5571
  };
4629
5572
  }, [externalCostTracker]);
4630
- useEffect3(() => {
5573
+ useEffect4(() => {
4631
5574
  if (isStreaming || !idlePlaceholder) {
4632
5575
  setGradientColors([]);
4633
5576
  return;
@@ -4640,7 +5583,7 @@ function ChatSession({
4640
5583
  }, GRADIENT_ANIMATION.idleInterval);
4641
5584
  return () => clearInterval(interval);
4642
5585
  }, [isStreaming, idlePlaceholder]);
4643
- useEffect3(() => {
5586
+ useEffect4(() => {
4644
5587
  if (!isStreaming || !streamingPlaceholder) {
4645
5588
  setStreamingGradientColors([]);
4646
5589
  return;
@@ -4803,6 +5746,7 @@ function ChatSession({
4803
5746
  const result = cmd.handler();
4804
5747
  switch (result.kind) {
4805
5748
  case "exit":
5749
+ await sessionRef.current?.flushLog();
4806
5750
  process.exit(0);
4807
5751
  return;
4808
5752
  case "clear":
@@ -4988,8 +5932,8 @@ function ChatSession({
4988
5932
  }, 2e3);
4989
5933
  }
4990
5934
  }
4991
- }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode]);
4992
- useEffect3(() => {
5935
+ }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode, isStreaming, rewinding]);
5936
+ useEffect4(() => {
4993
5937
  if (!isStreaming && externalCostTracker) {
4994
5938
  setTodayCost(externalCostTracker.todayTotalCost);
4995
5939
  }
@@ -5092,7 +6036,10 @@ function ChatSession({
5092
6036
  {
5093
6037
  content: currentContent,
5094
6038
  toolCalls: currentToolCalls.length > 0 ? currentToolCalls : void 0,
5095
- isStreaming: true
6039
+ isStreaming: true,
6040
+ usage: _currentUsage,
6041
+ cost: _currentCost,
6042
+ model: _streamingModel
5096
6043
  }
5097
6044
  ),
5098
6045
  !isStreaming && streamError && /* @__PURE__ */ jsx9(Box9, { marginTop: 1, marginLeft: 3, children: /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
@@ -5164,7 +6111,7 @@ function ChatSession({
5164
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" }) })
5165
6112
  ] }),
5166
6113
  /* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
5167
- ] }) : /* @__PURE__ */ jsxs9(Fragment, { children: [
6114
+ ] }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
5168
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: [
5169
6116
  PHASE_CONFIG[streamingPhase].icon,
5170
6117
  " ",
@@ -5216,10 +6163,10 @@ function ChatSession({
5216
6163
 
5217
6164
  // src/ui/GamePicker.tsx
5218
6165
  import { Box as Box10, Text as Text11, useInput as useInput2 } from "ink";
5219
- import { useState as useState4, useCallback as useCallback3 } from "react";
6166
+ import { useState as useState5, useCallback as useCallback3 } from "react";
5220
6167
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
5221
6168
  function GamePicker({ games, onSelect, onExit, onBackToChat }) {
5222
- const [selectedIndex, setSelectedIndex] = useState4(0);
6169
+ const [selectedIndex, setSelectedIndex] = useState5(0);
5223
6170
  const exitAction = onBackToChat ?? onExit ?? (() => process.exit(0));
5224
6171
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(exitAction);
5225
6172
  useInput2(
@@ -5274,7 +6221,7 @@ function listGames() {
5274
6221
 
5275
6222
  // src/game/brick-breaker/index.tsx
5276
6223
  import { Box as Box11, Text as Text12, useInput as useInput3, render as render2 } from "ink";
5277
- 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";
5278
6225
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
5279
6226
  var GAME_WIDTH = 40;
5280
6227
  var GAME_HEIGHT = 18;
@@ -5406,13 +6353,13 @@ function buildBoard(state) {
5406
6353
  return lines.map((l) => `\u2502${l}\u2502`).join("\n");
5407
6354
  }
5408
6355
  function BrickBreakerGame({ onExit: _onExit }) {
5409
- const [initialLevel, setInitialLevel] = useState5(1);
5410
- const [selectingLevel, setSelectingLevel] = useState5(true);
5411
- const stateRef = useRef3(createInitialState(initialLevel));
5412
- const [tick2, setTick] = useState5(0);
5413
- 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);
5414
6361
  onExitRef.current = _onExit;
5415
- useEffect4(() => {
6362
+ useEffect5(() => {
5416
6363
  if (selectingLevel) return;
5417
6364
  const interval = setInterval(() => {
5418
6365
  update(stateRef.current);
@@ -5546,7 +6493,7 @@ var brick_breaker_default = {
5546
6493
 
5547
6494
  // src/game/coder-check/index.tsx
5548
6495
  import { Box as Box12, Text as Text13, useInput as useInput4, render as render3 } from "ink";
5549
- 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";
5550
6497
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
5551
6498
  var GAME_W = 66;
5552
6499
  var GAME_H = 20;
@@ -5938,18 +6885,18 @@ function buildGameView(s, scoreLines, scoreColor, message) {
5938
6885
  return rows;
5939
6886
  }
5940
6887
  function CoderCheck({ onExit: _onExit }) {
5941
- const stateRef = useRef4(createInitialState2());
5942
- const [tick2, setTick] = useState6(0);
5943
- const [colorOffset, setColorOffset] = useState6(0);
5944
- 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);
5945
6892
  onExitRef.current = _onExit;
5946
- useEffect5(() => {
6893
+ useEffect6(() => {
5947
6894
  const timer = setInterval(() => {
5948
6895
  setColorOffset((prev) => (prev + 1) % CYBER_PALETTE2.length);
5949
6896
  }, 400);
5950
6897
  return () => clearInterval(timer);
5951
6898
  }, []);
5952
- useEffect5(() => {
6899
+ useEffect6(() => {
5953
6900
  const interval = setInterval(() => {
5954
6901
  update2(stateRef.current);
5955
6902
  setTick((t) => t + 1);
@@ -6446,12 +7393,12 @@ import chalk5 from "chalk";
6446
7393
 
6447
7394
  // src/stock/StockList.tsx
6448
7395
  import { Box as Box13, Text as Text14, useInput as useInput5 } from "ink";
6449
- 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";
6450
7397
  import asciichart from "asciichart";
6451
7398
  import os from "os";
6452
- import { join as join8 } from "path";
7399
+ import { join as join10 } from "path";
6453
7400
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
6454
- var SETTINGS_PATH = join8(os.homedir(), ".dskcode", "settings.json");
7401
+ var SETTINGS_PATH = join10(os.homedir(), ".dskcode", "settings.json");
6455
7402
  function toFileUrl(p) {
6456
7403
  const norm = p.replace(/\\/g, "/");
6457
7404
  if (process.platform === "win32") {
@@ -6553,20 +7500,20 @@ function latestPoints(data, maxPoints = 60) {
6553
7500
  return data.slice(data.length - maxPoints);
6554
7501
  }
6555
7502
  function StockList({ codes, onExit, onBackToChat }) {
6556
- const [stocks, setStocks] = useState7([]);
6557
- const [selectedIndex, setSelectedIndex] = useState7(0);
6558
- const [loading, setLoading] = useState7(true);
6559
- const [lastUpdate, setLastUpdate] = useState7("");
6560
- const [detailView, setDetailView] = useState7(null);
6561
- const [detailPrices, setDetailPrices] = useState7(null);
6562
- const [detailLoading, setDetailLoading] = useState7(false);
6563
- const [detailCountdown, setDetailCountdown] = useState7(10);
6564
- const [countdown, setCountdown] = useState7(5);
6565
- 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(
6566
7513
  () => (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })
6567
7514
  );
6568
- const [dimMode, setDimMode] = useState7(false);
6569
- const [sortOrder, setSortOrder] = useState7("default");
7515
+ const [dimMode, setDimMode] = useState8(false);
7516
+ const [sortOrder, setSortOrder] = useState8("default");
6570
7517
  const sortedStocks = useMemo2(() => {
6571
7518
  if (sortOrder === "default") return stocks;
6572
7519
  return [...stocks].toSorted(
@@ -6574,7 +7521,7 @@ function StockList({ codes, onExit, onBackToChat }) {
6574
7521
  );
6575
7522
  }, [stocks, sortOrder]);
6576
7523
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(onExit);
6577
- useEffect6(() => {
7524
+ useEffect7(() => {
6578
7525
  const timer = setInterval(() => {
6579
7526
  setCurrentTime((/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false }));
6580
7527
  }, 1e3);
@@ -6590,10 +7537,10 @@ function StockList({ codes, onExit, onBackToChat }) {
6590
7537
  }
6591
7538
  setLoading(false);
6592
7539
  }, [codes]);
6593
- useEffect6(() => {
7540
+ useEffect7(() => {
6594
7541
  void loadData();
6595
7542
  }, [loadData]);
6596
- useEffect6(() => {
7543
+ useEffect7(() => {
6597
7544
  const interval = setInterval(() => {
6598
7545
  setCountdown((prev) => {
6599
7546
  if (prev <= 1) {
@@ -6605,7 +7552,7 @@ function StockList({ codes, onExit, onBackToChat }) {
6605
7552
  }, 1e3);
6606
7553
  return () => clearInterval(interval);
6607
7554
  }, [loadData]);
6608
- useEffect6(() => {
7555
+ useEffect7(() => {
6609
7556
  if (detailView) {
6610
7557
  const loadDetail = () => {
6611
7558
  minuteCache.delete(detailView.code);
@@ -6624,7 +7571,7 @@ function StockList({ codes, onExit, onBackToChat }) {
6624
7571
  setDetailPrices(null);
6625
7572
  setDetailLoading(false);
6626
7573
  }, [detailView]);
6627
- useEffect6(() => {
7574
+ useEffect7(() => {
6628
7575
  if (!detailView) return;
6629
7576
  const timer = setInterval(() => {
6630
7577
  setDetailCountdown((prev) => prev > 0 ? prev - 1 : 10);
@@ -6786,7 +7733,7 @@ function renderDetail(stock, _onBack, prices, countdown = 10, currentTime) {
6786
7733
 
6787
7734
  // src/utils/scan-files.ts
6788
7735
  import { readdir as readdir6 } from "fs/promises";
6789
- import { join as join9, relative as relative7 } from "path";
7736
+ import { join as join11, relative as relative8 } from "path";
6790
7737
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
6791
7738
  "node_modules",
6792
7739
  ".git",
@@ -6851,12 +7798,12 @@ async function scanProjectFiles(baseDir, dir) {
6851
7798
  } else if (entry.isFile()) {
6852
7799
  const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
6853
7800
  if (SOURCE_EXTS.has(ext)) {
6854
- files.push(relative7(baseDir, join9(currentDir, name)));
7801
+ files.push(relative8(baseDir, join11(currentDir, name)));
6855
7802
  }
6856
7803
  }
6857
7804
  }
6858
7805
  const nested = await Promise.all(
6859
- dirs.map((d) => scanProjectFiles(baseDir, join9(currentDir, d)))
7806
+ dirs.map((d) => scanProjectFiles(baseDir, join11(currentDir, d)))
6860
7807
  );
6861
7808
  for (const n of nested) {
6862
7809
  files.push(...n);
@@ -6944,7 +7891,7 @@ compdef _dskcode_completion dskcode`);
6944
7891
  program2.command("stock").description("\u67E5\u770B\u81EA\u9009\u80A1\u5B9E\u65F6\u884C\u60C5").argument("[codes...]", "\u80A1\u7968\u4EE3\u7801\uFF08\u7A7A\u683C\u5206\u9694\uFF09\uFF0C\u5982 513090 600519").action(async function(codes) {
6945
7892
  const _ctx = this.dskcodeCtx;
6946
7893
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
6947
- const globalConfigPath = join10(home, ".dskcode", "settings.json");
7894
+ const globalConfigPath = join12(home, ".dskcode", "settings.json");
6948
7895
  let globalConfigHasStock = false;
6949
7896
  try {
6950
7897
  const raw = await readFile11(globalConfigPath, "utf-8");
@@ -7114,7 +8061,10 @@ var sigintCount = 0;
7114
8061
  var sigintTimer = null;
7115
8062
  async function gracefulExit(code) {
7116
8063
  try {
7117
- await CostTracker.flushAll();
8064
+ await Promise.all([
8065
+ CostTracker.flushAll(),
8066
+ ConversationLogger.flushAll()
8067
+ ]);
7118
8068
  } catch {
7119
8069
  }
7120
8070
  process.exit(code);
@@ -7137,19 +8087,19 @@ process.on("SIGTERM", () => {
7137
8087
  var program = createCli();
7138
8088
  try {
7139
8089
  await program.parseAsync(process.argv);
7140
- await CostTracker.flushAll();
8090
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7141
8091
  } catch (err) {
7142
8092
  const error = err;
7143
8093
  if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
7144
- await CostTracker.flushAll();
8094
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7145
8095
  process.exit(error.exitCode ?? ExitCode.SUCCESS);
7146
8096
  }
7147
8097
  if (typeof error.exitCode === "number") {
7148
- await CostTracker.flushAll();
8098
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7149
8099
  process.exit(error.exitCode);
7150
8100
  }
7151
8101
  console.error(String(err));
7152
- await CostTracker.flushAll();
8102
+ await Promise.all([CostTracker.flushAll(), ConversationLogger.flushAll()]);
7153
8103
  process.exit(ExitCode.GENERAL_ERROR);
7154
8104
  }
7155
8105
  //# sourceMappingURL=index.js.map