dskcode 0.1.26 → 0.1.27

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
@@ -771,8 +771,8 @@ async function getAllSkills(cwd) {
771
771
  }
772
772
 
773
773
  // src/cli/index.tsx
774
- import { readFile as readFile10 } from "fs/promises";
775
- import { join as join9 } from "path";
774
+ import { readFile as readFile11 } from "fs/promises";
775
+ import { join as join10 } from "path";
776
776
 
777
777
  // src/ui/RenderScope.tsx
778
778
  import { render } from "ink";
@@ -2086,17 +2086,183 @@ var ToolRegistry = class {
2086
2086
  }
2087
2087
  };
2088
2088
 
2089
+ // src/checkpoint/git-checkpoint.ts
2090
+ import { execFile } from "child_process";
2091
+ import { promisify } from "util";
2092
+ var execFileAsync = promisify(execFile);
2093
+ var EXEC_OPTIONS = { windowsHide: true };
2094
+ async function isGitRepo(cwd) {
2095
+ try {
2096
+ await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, ...EXEC_OPTIONS });
2097
+ return true;
2098
+ } catch {
2099
+ return false;
2100
+ }
2101
+ }
2102
+ async function hasCommits(cwd) {
2103
+ try {
2104
+ await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { cwd, ...EXEC_OPTIONS });
2105
+ return true;
2106
+ } catch {
2107
+ return false;
2108
+ }
2109
+ }
2110
+ async function hasWorkingChanges(cwd) {
2111
+ const out = await execFileAsync("git", ["status", "--porcelain"], { cwd, ...EXEC_OPTIONS });
2112
+ return out.stdout.trim().length > 0;
2113
+ }
2114
+ async function git(args, cwd) {
2115
+ try {
2116
+ const { stdout } = await execFileAsync("git", args, { cwd, ...EXEC_OPTIONS });
2117
+ return stdout;
2118
+ } catch (err) {
2119
+ const msg = err instanceof Error ? err.message : String(err);
2120
+ throw new Error(`git ${args.join(" ")} \u5931\u8D25: ${msg}`);
2121
+ }
2122
+ }
2123
+ async function listStashShas(cwd) {
2124
+ const out = await git(["stash", "list", "--format=%H"], cwd);
2125
+ return out.split("\n").map((l) => l.trim()).filter(Boolean);
2126
+ }
2127
+ async function createCheckpoint(cwd) {
2128
+ const timestamp = Date.now();
2129
+ const inRepo = await isGitRepo(cwd);
2130
+ if (!inRepo) return { stashSha: "", timestamp, cwd, isGitRepo: false };
2131
+ if (!await hasCommits(cwd)) return { stashSha: "", timestamp, cwd, isGitRepo: true };
2132
+ if (!await hasWorkingChanges(cwd)) return { stashSha: "", timestamp, cwd, isGitRepo: true };
2133
+ const beforeShas = await listStashShas(cwd);
2134
+ await git(["stash", "push", "-m", `dskcode-cp-${timestamp}`, "-u"], cwd);
2135
+ const newShas = await listStashShas(cwd);
2136
+ if (newShas.length === 0) throw new Error("git stash push \u672A\u80FD\u521B\u5EFA stash entry");
2137
+ const newSha = newShas.find((s) => !beforeShas.includes(s)) ?? newShas[0];
2138
+ await git(["stash", "apply", newSha], cwd);
2139
+ return { stashSha: newSha, timestamp, cwd, isGitRepo: true };
2140
+ }
2141
+ async function restoreCheckpointForce(checkpoint) {
2142
+ if (!checkpoint.isGitRepo) throw new Error("\u975E git \u4ED3\u5E93\uFF0C\u65E0\u6CD5\u6062\u590D\u6587\u4EF6\u72B6\u6001");
2143
+ if (!checkpoint.stashSha) throw new Error("\u68C0\u67E5\u70B9\u4E3A\u7A7A\uFF08\u5DE5\u4F5C\u533A\u539F\u672C\u5C31\u5E72\u51C0\uFF09\uFF0C\u65E0\u9700\u6062\u590D");
2144
+ const { cwd, stashSha } = checkpoint;
2145
+ const currentShas = await listStashShas(cwd);
2146
+ if (!currentShas.includes(stashSha)) {
2147
+ throw new Error("\u68C0\u67E5\u70B9\u5DF2\u5931\u6548\uFF08stash entry \u5DF2\u88AB\u6D88\u8D39\u6216 GC\uFF09\uFF0C\u65E0\u6CD5\u6062\u590D");
2148
+ }
2149
+ await git(["checkout", "--", "."], cwd);
2150
+ await git(["clean", "-fd"], cwd);
2151
+ await git(["stash", "apply", stashSha], cwd);
2152
+ const refIndex = currentShas.indexOf(stashSha);
2153
+ if (refIndex >= 0) await git(["stash", "drop", `stash@{${refIndex}}`], cwd);
2154
+ }
2155
+ async function restoreToClean(cwd) {
2156
+ const inRepo = await isGitRepo(cwd);
2157
+ if (!inRepo) throw new Error("\u975E git \u4ED3\u5E93\uFF0C\u65E0\u6CD5\u6062\u590D\u6587\u4EF6\u72B6\u6001");
2158
+ if (!await hasCommits(cwd)) throw new Error("\u4ED3\u5E93\u65E0 commit\uFF0C\u65E0\u6CD5\u6062\u590D");
2159
+ await git(["checkout", "--", "."], cwd);
2160
+ await git(["clean", "-fd"], cwd);
2161
+ }
2162
+ async function discardCheckpoint(checkpoint) {
2163
+ if (!checkpoint.isGitRepo || !checkpoint.stashSha) return;
2164
+ const { cwd, stashSha } = checkpoint;
2165
+ const shas = await listStashShas(cwd);
2166
+ const idx = shas.indexOf(stashSha);
2167
+ if (idx < 0) return;
2168
+ try {
2169
+ await git(["stash", "drop", `stash@{${idx}}`], cwd);
2170
+ } catch {
2171
+ }
2172
+ }
2173
+
2174
+ // src/session-store/session-store.ts
2175
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3, readdir as readdir2, unlink, rename } from "fs/promises";
2176
+ import { join as join4 } from "path";
2177
+ import { randomUUID } from "crypto";
2178
+ function defaultSessionsDir() {
2179
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
2180
+ return join4(home, ".dskcode", "sessions");
2181
+ }
2182
+ var SessionStore = class {
2183
+ #dir;
2184
+ constructor(dir) {
2185
+ this.#dir = dir ?? defaultSessionsDir();
2186
+ }
2187
+ get dir() {
2188
+ return this.#dir;
2189
+ }
2190
+ async save(session) {
2191
+ await mkdir4(this.#dir, { recursive: true });
2192
+ const finalPath = join4(this.#dir, `${session.id}.json`);
2193
+ const tmpPath = join4(this.#dir, `.${session.id}.json.tmp`);
2194
+ await writeFile3(tmpPath, JSON.stringify(session, null, 2), "utf-8");
2195
+ await rename(tmpPath, finalPath);
2196
+ }
2197
+ async load(id) {
2198
+ const path = join4(this.#dir, `${id}.json`);
2199
+ try {
2200
+ const content = await readFile4(path, "utf-8");
2201
+ return JSON.parse(content);
2202
+ } catch (err) {
2203
+ if (isENOENT(err)) return null;
2204
+ throw err;
2205
+ }
2206
+ }
2207
+ async list() {
2208
+ let files;
2209
+ try {
2210
+ files = await readdir2(this.#dir);
2211
+ } catch (err) {
2212
+ if (isENOENT(err)) return [];
2213
+ throw err;
2214
+ }
2215
+ const results = [];
2216
+ for (const file of files) {
2217
+ if (!file.endsWith(".json") || file.startsWith(".")) continue;
2218
+ try {
2219
+ const content = await readFile4(join4(this.#dir, file), "utf-8");
2220
+ const s = JSON.parse(content);
2221
+ results.push({
2222
+ id: s.id,
2223
+ title: s.title || "\uFF08\u65E0\u6807\u9898\uFF09",
2224
+ updatedAt: s.updatedAt,
2225
+ cwd: s.cwd,
2226
+ messageCount: s.messages?.length ?? 0
2227
+ });
2228
+ } catch {
2229
+ }
2230
+ }
2231
+ return results.sort((a, b) => b.updatedAt - a.updatedAt);
2232
+ }
2233
+ async delete(id) {
2234
+ const path = join4(this.#dir, `${id}.json`);
2235
+ try {
2236
+ await unlink(path);
2237
+ } catch (err) {
2238
+ if (!isENOENT(err)) throw err;
2239
+ }
2240
+ }
2241
+ async exists(id) {
2242
+ return await this.load(id) !== null;
2243
+ }
2244
+ static newId() {
2245
+ return randomUUID();
2246
+ }
2247
+ };
2248
+ function isENOENT(err) {
2249
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
2250
+ }
2251
+
2089
2252
  // src/agent/index.ts
2090
- var Session = class {
2253
+ var Session = class _Session {
2091
2254
  #messages = [];
2092
2255
  #provider;
2093
2256
  #toolRegistry;
2094
2257
  #costTracker;
2095
2258
  #options;
2096
2259
  #abortController = new AbortController();
2097
- // 风暴检测:记录每轮的工具调用错误
2260
+ #sessionId;
2261
+ #store;
2262
+ #createdAt;
2263
+ #persistTimer = null;
2264
+ #checkpoints = /* @__PURE__ */ new Map();
2098
2265
  #stormRecords = [];
2099
- /** 当前会话模式:code(代码模式)或 plan(计划模式) */
2100
2266
  #mode = "code";
2101
2267
  constructor(provider, tools = [], costTracker, options) {
2102
2268
  this.#provider = provider;
@@ -2114,12 +2280,13 @@ var Session = class {
2114
2280
  preserveRecentRounds: options?.preserveRecentRounds ?? 10,
2115
2281
  projectContext: options?.projectContext,
2116
2282
  gate: options?.gate ?? new AlwaysAllowGate(),
2117
- writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()]
2283
+ writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()],
2284
+ enableCheckpoint: options?.enableCheckpoint ?? true
2118
2285
  };
2286
+ this.#sessionId = options?.sessionId ?? SessionStore.newId();
2287
+ this.#store = options?.store === false ? null : options?.store ?? new SessionStore();
2288
+ this.#createdAt = Date.now();
2119
2289
  }
2120
- // -------------------------------------------------------------------------
2121
- // 公共只读属性
2122
- // -------------------------------------------------------------------------
2123
2290
  get messages() {
2124
2291
  return this.#messages;
2125
2292
  }
@@ -2132,36 +2299,35 @@ var Session = class {
2132
2299
  get model() {
2133
2300
  return this.#provider.model();
2134
2301
  }
2135
- /** 获取工具注册表(只读视图) */
2136
2302
  get toolRegistry() {
2137
2303
  return this.#toolRegistry;
2138
2304
  }
2139
- /** 获取当前会话模式 */
2140
2305
  get mode() {
2141
2306
  return this.#mode;
2142
2307
  }
2143
- /** 切换会话模式,返回新模式 */
2308
+ get id() {
2309
+ return this.#sessionId;
2310
+ }
2311
+ get store() {
2312
+ return this.#store;
2313
+ }
2314
+ get createdAt() {
2315
+ return this.#createdAt;
2316
+ }
2144
2317
  setMode(mode) {
2145
2318
  this.#mode = mode;
2146
2319
  return this.#mode;
2147
2320
  }
2148
- // -------------------------------------------------------------------------
2149
- // 流式对话 — Agent 主循环
2150
- // -------------------------------------------------------------------------
2151
- /**
2152
- * 执行一轮用户对话,以 AsyncGenerator 形式逐步 yield 事件。
2153
- *
2154
- * 主循环流程:
2155
- * 1. 追加用户消息
2156
- * 2. 进入 Agent 循环(最多 maxToolRounds 轮)
2157
- * a. 构建消息 → 裁剪 → 调用 Provider 流式接口
2158
- * b. 解析响应:文本增量、工具调用、使用量
2159
- * c. 如果有工具调用 → 执行工具 → 追加结果 → 继续循环
2160
- * d. 如果没有工具调用 → 退出循环
2161
- * 3. yield done 事件
2162
- */
2163
2321
  async *chat(userInput, opts) {
2164
2322
  this.#messages.push({ role: "user", content: userInput });
2323
+ const userMsgIndex = this.#messages.length - 1;
2324
+ if (this.#options.enableCheckpoint) {
2325
+ try {
2326
+ const checkpoint = await createCheckpoint(this.#options.cwd);
2327
+ this.#checkpoints.set(userMsgIndex, checkpoint);
2328
+ } catch {
2329
+ }
2330
+ }
2165
2331
  const startTime = Date.now();
2166
2332
  let toolRounds = 0;
2167
2333
  try {
@@ -2170,7 +2336,6 @@ var Session = class {
2170
2336
  const [trimmed] = trimMessages(
2171
2337
  [...this.#messages],
2172
2338
  {
2173
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2174
2339
  model: this.#provider.model(),
2175
2340
  reservedForOutput: this.#options.reservedForOutput,
2176
2341
  systemPrompt,
@@ -2196,28 +2361,17 @@ var Session = class {
2196
2361
  accumulatedText += chunk.content;
2197
2362
  yield { type: "text_delta", content: chunk.content };
2198
2363
  }
2199
- if (chunk.toolCalls && chunk.toolCalls.length > 0) {
2200
- lastToolCalls = chunk.toolCalls;
2201
- }
2202
- if (chunk.usage) {
2203
- lastUsage = chunk.usage;
2204
- }
2205
- if (chunk.finishReason) {
2206
- _lastFinishReason = chunk.finishReason;
2207
- }
2364
+ if (chunk.toolCalls && chunk.toolCalls.length > 0) lastToolCalls = chunk.toolCalls;
2365
+ if (chunk.usage) lastUsage = chunk.usage;
2366
+ if (chunk.finishReason) _lastFinishReason = chunk.finishReason;
2208
2367
  }
2209
2368
  if (lastUsage) {
2210
2369
  const modelId = this.#provider.model();
2211
2370
  this.#costTracker.record(lastUsage, modelId);
2212
2371
  yield { type: "usage", usage: lastUsage, model: modelId };
2213
2372
  }
2214
- const assistantMsg = {
2215
- role: "assistant",
2216
- content: accumulatedText
2217
- };
2218
- if (lastToolCalls && lastToolCalls.length > 0) {
2219
- assistantMsg.toolCalls = lastToolCalls;
2220
- }
2373
+ const assistantMsg = { role: "assistant", content: accumulatedText };
2374
+ if (lastToolCalls && lastToolCalls.length > 0) assistantMsg.toolCalls = lastToolCalls;
2221
2375
  this.#messages.push(assistantMsg);
2222
2376
  if (lastToolCalls && lastToolCalls.length > 0) {
2223
2377
  yield { type: "tool_calls", calls: lastToolCalls };
@@ -2236,11 +2390,9 @@ var Session = class {
2236
2390
  for (const item of results.items) {
2237
2391
  yield { type: "tool_result", name: item.name, result: item.result };
2238
2392
  let toolContent = item.result.data;
2239
- if (item.result.diff && item.result.diff.patch) {
2240
- toolContent += `
2393
+ if (item.result.diff && item.result.diff.patch) toolContent += `
2241
2394
 
2242
2395
  ${item.result.diff.patch}`;
2243
- }
2244
2396
  this.#messages.push({
2245
2397
  role: "tool",
2246
2398
  content: toolContent,
@@ -2254,31 +2406,15 @@ ${item.result.diff.patch}`;
2254
2406
  break;
2255
2407
  }
2256
2408
  } catch (err) {
2257
- if (err instanceof DOMException && err.name === "AbortError") {
2258
- return;
2259
- }
2260
- if (err instanceof Error && err.name === "AbortError") {
2261
- return;
2262
- }
2263
- yield {
2264
- type: "error",
2265
- error: err instanceof Error ? err : new Error(String(err))
2266
- };
2409
+ if (err instanceof DOMException && err.name === "AbortError") return;
2410
+ if (err instanceof Error && err.name === "AbortError") return;
2411
+ yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
2267
2412
  return;
2268
2413
  }
2269
2414
  const elapsed = Date.now() - startTime;
2415
+ void this.#persist();
2270
2416
  yield { type: "done", elapsed };
2271
2417
  }
2272
- // -------------------------------------------------------------------------
2273
- // 工具执行 — 批量、并行/串行、Gate、风暴检测
2274
- // -------------------------------------------------------------------------
2275
- /**
2276
- * 执行一批工具调用。
2277
- *
2278
- * 并行策略:
2279
- * - 如果这批工具全部是 ReadOnly 的,并行执行(最多 8 并发)
2280
- * - 否则按顺序串行执行,保证写/读顺序
2281
- */
2282
2418
  async #executeBatch(calls) {
2283
2419
  const toolCtx = {
2284
2420
  cwd: this.#options.cwd,
@@ -2313,9 +2449,6 @@ ${item.result.diff.patch}`;
2313
2449
  }
2314
2450
  return { items, records };
2315
2451
  }
2316
- /**
2317
- * 执行单个工具调用,包含 Gate 检查和预览。
2318
- */
2319
2452
  async #executeOne(tc, ctx) {
2320
2453
  const toolName = tc.name;
2321
2454
  const timestamp = Date.now();
@@ -2354,12 +2487,7 @@ ${item.result.diff.patch}`;
2354
2487
  const result = await tool.execute(toolArgs, ctx);
2355
2488
  return {
2356
2489
  item: { name: toolName, callId: tc.id, result },
2357
- record: {
2358
- name: toolName,
2359
- success: result.success,
2360
- error: result.error,
2361
- timestamp
2362
- }
2490
+ record: { name: toolName, success: result.success, error: result.error, timestamp }
2363
2491
  };
2364
2492
  } catch (err) {
2365
2493
  const message = err instanceof Error ? err.message : String(err);
@@ -2370,46 +2498,172 @@ ${item.result.diff.patch}`;
2370
2498
  };
2371
2499
  }
2372
2500
  }
2373
- // -------------------------------------------------------------------------
2374
- // 风暴检测 — 同一工具同一错误连续 3 次 → 强制换策略
2375
- // -------------------------------------------------------------------------
2376
- /**
2377
- * 连续 3 次同一工具同一错误 → 触发风暴中断。
2378
- */
2379
2501
  #checkStormBreak(currentCalls) {
2380
2502
  if (this.#stormRecords.length < 3) return false;
2381
2503
  const recentErrors = this.#stormRecords.slice(-3);
2382
2504
  if (recentErrors.length < 3) return false;
2383
2505
  const first = recentErrors[0];
2384
- const allSame = recentErrors.every(
2385
- (r) => r.name === first.name && r.error === first.error && !r.success
2386
- );
2506
+ const allSame = recentErrors.every((r) => r.name === first.name && r.error === first.error && !r.success);
2387
2507
  if (!allSame) return false;
2388
2508
  return currentCalls.some((tc) => tc.name === first.name);
2389
2509
  }
2390
- // -------------------------------------------------------------------------
2391
- // 会话管理
2392
- // -------------------------------------------------------------------------
2393
- /** 取消正在进行的流式请求 */
2394
2510
  abort() {
2395
2511
  this.#abortController.abort();
2512
+ if (this.#persistTimer) {
2513
+ clearTimeout(this.#persistTimer);
2514
+ this.#persistTimer = null;
2515
+ }
2396
2516
  }
2397
- /** 重置会话历史(保留 provider/tools 配置,重置成本追踪) */
2398
2517
  reset() {
2399
2518
  this.#messages.length = 0;
2400
2519
  this.#costTracker.resetSession();
2401
2520
  this.#stormRecords = [];
2521
+ this.#checkpoints.clear();
2522
+ }
2523
+ // -------------------------------------------------------------------------
2524
+ // 持久化与恢复
2525
+ // -------------------------------------------------------------------------
2526
+ async persistNow() {
2527
+ if (this.#persistTimer) {
2528
+ clearTimeout(this.#persistTimer);
2529
+ this.#persistTimer = null;
2530
+ }
2531
+ await this.#doPersist();
2532
+ }
2533
+ #persist() {
2534
+ if (!this.#store) return;
2535
+ if (this.#persistTimer) {
2536
+ this.#persistTimer.refresh();
2537
+ return;
2538
+ }
2539
+ this.#persistTimer = setTimeout(() => {
2540
+ this.#persistTimer = null;
2541
+ void this.#doPersist();
2542
+ }, 500);
2543
+ this.#persistTimer.unref();
2544
+ }
2545
+ async #doPersist() {
2546
+ if (!this.#store) return;
2547
+ const stored = {
2548
+ id: this.#sessionId,
2549
+ title: this.#deriveTitle(),
2550
+ createdAt: this.#createdAt,
2551
+ updatedAt: Date.now(),
2552
+ cwd: this.#options.cwd,
2553
+ model: this.#provider.model(),
2554
+ messages: this.#serializeMessages(),
2555
+ totalCost: this.#costTracker.sessionTotalCost
2556
+ };
2557
+ try {
2558
+ await this.#store.save(stored);
2559
+ } catch (err) {
2560
+ console.error("[Session] \u6301\u4E45\u5316\u5931\u8D25:", err);
2561
+ }
2562
+ }
2563
+ #deriveTitle() {
2564
+ for (const m of this.#messages) {
2565
+ if (m.role === "user" && m.content.trim()) return m.content.trim().slice(0, 40);
2566
+ }
2567
+ return "\u65B0\u4F1A\u8BDD";
2568
+ }
2569
+ #serializeMessages() {
2570
+ return this.#messages.map((msg, idx) => {
2571
+ const checkpoint = this.#checkpoints.get(idx);
2572
+ if (msg.role === "user" && checkpoint) return { ...msg, checkpoint };
2573
+ return { ...msg };
2574
+ });
2575
+ }
2576
+ static async resume(id, provider, tools = [], costTracker, options) {
2577
+ const store = options?.store === false ? null : options?.store ?? new SessionStore();
2578
+ if (!store) throw new Error("resume \u9700\u8981\u542F\u7528\u6301\u4E45\u5316\uFF08options.store \u4E0D\u80FD\u4E3A false\uFF09");
2579
+ const stored = await store.load(id);
2580
+ if (!stored) throw new Error(`\u4F1A\u8BDD ${id} \u4E0D\u5B58\u5728`);
2581
+ const session = new _Session(provider, tools, costTracker, { ...options, sessionId: id, store });
2582
+ for (const m of stored.messages) {
2583
+ session.#messages.push({
2584
+ role: m.role,
2585
+ content: m.content,
2586
+ toolCallId: m.toolCallId,
2587
+ name: m.name,
2588
+ toolCalls: m.toolCalls
2589
+ });
2590
+ }
2591
+ for (let i = 0; i < stored.messages.length; i++) {
2592
+ const cp2 = stored.messages[i]?.checkpoint;
2593
+ if (cp2) session.#checkpoints.set(i, cp2);
2594
+ }
2595
+ session.#createdAt = stored.createdAt;
2596
+ return session;
2597
+ }
2598
+ // -------------------------------------------------------------------------
2599
+ // 检查点与 Rewind
2600
+ // -------------------------------------------------------------------------
2601
+ listCheckpoints() {
2602
+ const result = [];
2603
+ for (const [index, checkpoint] of this.#checkpoints) {
2604
+ const msg = this.#messages[index];
2605
+ if (!msg || msg.role !== "user") continue;
2606
+ result.push({ index, preview: msg.content.slice(0, 80), timestamp: checkpoint.timestamp, isGitRepo: checkpoint.isGitRepo });
2607
+ }
2608
+ return result.sort((a, b) => a.index - b.index);
2609
+ }
2610
+ async rewind(targetIndex) {
2611
+ if (targetIndex < 0 || targetIndex >= this.#messages.length) {
2612
+ return { ok: false, error: `\u65E0\u6548\u7684\u6D88\u606F\u7D22\u5F15 ${targetIndex}` };
2613
+ }
2614
+ const target = this.#messages[targetIndex];
2615
+ if (!target || target.role !== "user") {
2616
+ return { ok: false, error: `\u7D22\u5F15 ${targetIndex} \u4E0D\u662F user \u6D88\u606F` };
2617
+ }
2618
+ const checkpoint = this.#checkpoints.get(targetIndex);
2619
+ if (!checkpoint) return { ok: false, error: "\u8BE5\u6D88\u606F\u6CA1\u6709\u68C0\u67E5\u70B9" };
2620
+ this.#messages.length = targetIndex + 1;
2621
+ const toDiscard = [];
2622
+ for (const [idx, cp2] of this.#checkpoints) {
2623
+ if (idx > targetIndex) {
2624
+ toDiscard.push(cp2);
2625
+ this.#checkpoints.delete(idx);
2626
+ }
2627
+ }
2628
+ let fileRestored = false;
2629
+ if (checkpoint.isGitRepo) {
2630
+ try {
2631
+ if (checkpoint.stashSha) {
2632
+ await restoreCheckpointForce(checkpoint);
2633
+ } else {
2634
+ await restoreToClean(this.#options.cwd);
2635
+ }
2636
+ fileRestored = true;
2637
+ } catch (err) {
2638
+ const msg = err instanceof Error ? err.message : String(err);
2639
+ return { ok: false, error: `\u5BF9\u8BDD\u5DF2\u622A\u65AD\u4F46\u6587\u4EF6\u6062\u590D\u5931\u8D25\uFF1A${msg}` };
2640
+ }
2641
+ }
2642
+ this.#checkpoints.delete(targetIndex);
2643
+ for (const cp2 of toDiscard) {
2644
+ void discardCheckpoint(cp2);
2645
+ }
2646
+ this.#persist();
2647
+ return { ok: true, fileRestored };
2648
+ }
2649
+ hasCheckpoints() {
2650
+ return this.listCheckpoints().length > 0;
2651
+ }
2652
+ async delete() {
2653
+ if (this.#store) await this.#store.delete(this.#sessionId);
2654
+ for (const cp2 of this.#checkpoints.values()) {
2655
+ void discardCheckpoint(cp2);
2656
+ }
2657
+ this.#checkpoints.clear();
2402
2658
  }
2403
2659
  // -------------------------------------------------------------------------
2404
2660
  // 内部方法
2405
2661
  // -------------------------------------------------------------------------
2406
- /** 构建系统提示词 */
2407
2662
  #buildSystemPrompt() {
2408
2663
  const enabledTools = this.#toolRegistry.list();
2409
2664
  const toolDescs = enabledTools.map((t) => ({
2410
2665
  name: t.name,
2411
2666
  description: t.description,
2412
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2413
2667
  parameters: t.parameters
2414
2668
  }));
2415
2669
  const opts = {
@@ -2419,25 +2673,14 @@ ${item.result.diff.patch}`;
2419
2673
  projectContext: this.#options.projectContext ?? void 0,
2420
2674
  cwd: this.#options.cwd
2421
2675
  };
2422
- if (this.#mode === "plan") {
2423
- return buildPlanSystemPrompt(opts);
2424
- }
2676
+ if (this.#mode === "plan") return buildPlanSystemPrompt(opts);
2425
2677
  return buildSystemPrompt(opts);
2426
2678
  }
2427
- /**
2428
- * 将注册的工具转为 ToolDefinition 格式(预留给 function calling)。
2429
- * 计划模式下只返回读工具,禁止非读工具暴露给 LLM。
2430
- */
2431
2679
  #buildToolDefinitions() {
2432
2680
  const tools = this.#mode === "plan" ? this.#toolRegistry.listReadTools() : this.#toolRegistry.list();
2433
2681
  return tools.map((t) => ({
2434
2682
  type: "function",
2435
- function: {
2436
- name: t.name,
2437
- description: t.description,
2438
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2439
- parameters: t.parameters
2440
- }
2683
+ function: { name: t.name, description: t.description, parameters: t.parameters }
2441
2684
  }));
2442
2685
  }
2443
2686
  };
@@ -2831,7 +3074,7 @@ function computeFileDiff(oldContent, newContent, filePath) {
2831
3074
  }
2832
3075
 
2833
3076
  // src/tool/eol.ts
2834
- import { writeFile as writeFile3 } from "fs/promises";
3077
+ import { writeFile as writeFile4 } from "fs/promises";
2835
3078
  function detectEol(text) {
2836
3079
  if (text.length === 0) return "\n";
2837
3080
  const crlfIdx = text.indexOf("\r\n");
@@ -2862,11 +3105,11 @@ function normalizeEol(originalContent, newContent) {
2862
3105
  }
2863
3106
  async function writeFileWithEol(filePath, originalContent, newContent) {
2864
3107
  const content = originalContent.length > 0 ? normalizeEol(originalContent, newContent) : newContent;
2865
- await writeFile3(filePath, content, "utf-8");
3108
+ await writeFile4(filePath, content, "utf-8");
2866
3109
  }
2867
3110
 
2868
3111
  // src/tool/builtins/read-file.ts
2869
- import { readFile as readFile4, stat } from "fs/promises";
3112
+ import { readFile as readFile5, stat } from "fs/promises";
2870
3113
  import { open } from "fs/promises";
2871
3114
  import { relative as relative2 } from "path";
2872
3115
  async function checkBinary(filePath) {
@@ -2934,7 +3177,7 @@ var readFileTool = {
2934
3177
  };
2935
3178
  }
2936
3179
  }
2937
- const content = await readFile4(filePath, "utf-8");
3180
+ const content = await readFile5(filePath, "utf-8");
2938
3181
  const lines = content.split("\n");
2939
3182
  if (lines.length > 0 && lines[lines.length - 1] === "" && content.endsWith("\n")) {
2940
3183
  lines.pop();
@@ -2970,7 +3213,7 @@ var readFileTool = {
2970
3213
  };
2971
3214
 
2972
3215
  // src/tool/builtins/write-file.ts
2973
- import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
3216
+ import { mkdir as mkdir5, readFile as readFile6 } from "fs/promises";
2974
3217
  import { dirname, relative as relative3, basename } from "path";
2975
3218
  var writeFileTool = {
2976
3219
  name: "write_file",
@@ -3009,11 +3252,11 @@ var writeFileTool = {
3009
3252
  let oldContent = "";
3010
3253
  let existedBefore = false;
3011
3254
  try {
3012
- oldContent = await readFile5(filePath, "utf-8");
3255
+ oldContent = await readFile6(filePath, "utf-8");
3013
3256
  existedBefore = true;
3014
3257
  } catch {
3015
3258
  }
3016
- await mkdir4(dirname(filePath), { recursive: true });
3259
+ await mkdir5(dirname(filePath), { recursive: true });
3017
3260
  const content = args.content;
3018
3261
  await writeFileWithEol(filePath, oldContent, content);
3019
3262
  const diff = computeFileDiff(oldContent, content, filePath);
@@ -3042,7 +3285,7 @@ var writeFileTool = {
3042
3285
  };
3043
3286
 
3044
3287
  // src/tool/builtins/edit-file.ts
3045
- import { readFile as readFile6 } from "fs/promises";
3288
+ import { readFile as readFile7 } from "fs/promises";
3046
3289
  import { basename as basename2 } from "path";
3047
3290
  var editFileTool = {
3048
3291
  name: "edit_file",
@@ -3085,7 +3328,7 @@ var editFileTool = {
3085
3328
  }
3086
3329
  }
3087
3330
  try {
3088
- const content = await readFile6(filePath, "utf-8");
3331
+ const content = await readFile7(filePath, "utf-8");
3089
3332
  const firstIndex = content.indexOf(args.old_text);
3090
3333
  if (firstIndex === -1) {
3091
3334
  return {
@@ -3133,7 +3376,7 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
3133
3376
  };
3134
3377
 
3135
3378
  // src/tool/builtins/multi-edit.ts
3136
- import { readFile as readFile7 } from "fs/promises";
3379
+ import { readFile as readFile8 } from "fs/promises";
3137
3380
  import { basename as basename3 } from "path";
3138
3381
  var multiEditTool = {
3139
3382
  name: "multi_edit",
@@ -3179,7 +3422,7 @@ var multiEditTool = {
3179
3422
  }
3180
3423
  }
3181
3424
  try {
3182
- const originalContent = await readFile7(filePath, "utf-8");
3425
+ const originalContent = await readFile8(filePath, "utf-8");
3183
3426
  let currentContent = originalContent;
3184
3427
  for (let idx = 0; idx < args.edits.length; idx++) {
3185
3428
  const step = args.edits[idx];
@@ -3247,7 +3490,7 @@ var multiEditTool = {
3247
3490
  };
3248
3491
 
3249
3492
  // src/tool/builtins/delete-range.ts
3250
- import { readFile as readFile8 } from "fs/promises";
3493
+ import { readFile as readFile9 } from "fs/promises";
3251
3494
  import { basename as basename4 } from "path";
3252
3495
  function findUniqueLine(lines, anchor, label) {
3253
3496
  const matches = [];
@@ -3310,7 +3553,7 @@ var deleteRangeTool = {
3310
3553
  }
3311
3554
  }
3312
3555
  try {
3313
- const content = await readFile8(filePath, "utf-8");
3556
+ const content = await readFile9(filePath, "utf-8");
3314
3557
  const lines = content.split("\n");
3315
3558
  const startResult = findUniqueLine(lines, args.startAnchor, "start_anchor");
3316
3559
  if ("error" in startResult) {
@@ -3433,8 +3676,8 @@ ${truncateOutput(result.stderr)}`);
3433
3676
  };
3434
3677
 
3435
3678
  // src/tool/builtins/glob.ts
3436
- import { readdir as readdir2, stat as stat2 } from "fs/promises";
3437
- import { join as join4, relative as relative4, isAbsolute as isAbsolute2 } from "path";
3679
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
3680
+ import { join as join5, relative as relative4, isAbsolute as isAbsolute2 } from "path";
3438
3681
  function globToRegex(pattern) {
3439
3682
  let regexStr = pattern;
3440
3683
  regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
@@ -3451,7 +3694,7 @@ async function walkDir(dir, baseDir) {
3451
3694
  const results = [];
3452
3695
  let entries;
3453
3696
  try {
3454
- entries = await readdir2(dir, { withFileTypes: true });
3697
+ entries = await readdir3(dir, { withFileTypes: true });
3455
3698
  } catch {
3456
3699
  return results;
3457
3700
  }
@@ -3459,7 +3702,7 @@ async function walkDir(dir, baseDir) {
3459
3702
  if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git")) {
3460
3703
  continue;
3461
3704
  }
3462
- const fullPath = join4(dir, entry.name);
3705
+ const fullPath = join5(dir, entry.name);
3463
3706
  const relPath = relative4(baseDir, fullPath);
3464
3707
  if (entry.isDirectory()) {
3465
3708
  results.push(...await walkDir(fullPath, baseDir));
@@ -3492,7 +3735,7 @@ var globTool = {
3492
3735
  if (!args?.pattern || typeof args.pattern !== "string") {
3493
3736
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3494
3737
  }
3495
- const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join4(ctx.cwd, args.directory) : ctx.cwd;
3738
+ const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join5(ctx.cwd, args.directory) : ctx.cwd;
3496
3739
  const regex = globToRegex(args.pattern);
3497
3740
  try {
3498
3741
  const dirStat = await stat2(searchDir);
@@ -3531,13 +3774,13 @@ var globTool = {
3531
3774
  };
3532
3775
 
3533
3776
  // src/tool/builtins/grep.ts
3534
- import { readdir as readdir3, readFile as readFile9, stat as stat3 } from "fs/promises";
3535
- import { join as join5, relative as relative5, isAbsolute as isAbsolute3 } from "path";
3777
+ import { readdir as readdir4, readFile as readFile10, stat as stat3 } from "fs/promises";
3778
+ import { join as join6, relative as relative5, isAbsolute as isAbsolute3 } from "path";
3536
3779
  async function collectFiles(dir, extension, maxFiles = 200) {
3537
3780
  const results = [];
3538
3781
  let entries;
3539
3782
  try {
3540
- entries = await readdir3(dir, { withFileTypes: true });
3783
+ entries = await readdir4(dir, { withFileTypes: true });
3541
3784
  } catch {
3542
3785
  return results;
3543
3786
  }
@@ -3546,7 +3789,7 @@ async function collectFiles(dir, extension, maxFiles = 200) {
3546
3789
  if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist")) {
3547
3790
  continue;
3548
3791
  }
3549
- const fullPath = join5(dir, entry.name);
3792
+ const fullPath = join6(dir, entry.name);
3550
3793
  if (entry.isDirectory()) {
3551
3794
  results.push(...await collectFiles(fullPath, extension, maxFiles - results.length));
3552
3795
  } else {
@@ -3593,7 +3836,7 @@ var grepTool = {
3593
3836
  if (!args?.pattern || typeof args.pattern !== "string") {
3594
3837
  return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
3595
3838
  }
3596
- const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join5(ctx.cwd, args.directory) : ctx.cwd;
3839
+ const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join6(ctx.cwd, args.directory) : ctx.cwd;
3597
3840
  const maxFiles = args.max_files ?? 200;
3598
3841
  try {
3599
3842
  const flags = args.case_sensitive ? "g" : "gi";
@@ -3606,7 +3849,7 @@ var grepTool = {
3606
3849
  const matches = [];
3607
3850
  for (const filePath of files) {
3608
3851
  try {
3609
- const content = await readFile9(filePath, "utf-8");
3852
+ const content = await readFile10(filePath, "utf-8");
3610
3853
  const lines = content.split("\n");
3611
3854
  const relPath = relative5(searchDir, filePath);
3612
3855
  for (let i = 0; i < lines.length; i++) {
@@ -3652,8 +3895,8 @@ var grepTool = {
3652
3895
  };
3653
3896
 
3654
3897
  // src/tool/builtins/ls.ts
3655
- import { readdir as readdir4, stat as stat4 } from "fs/promises";
3656
- import { join as join6, relative as relative6 } from "path";
3898
+ import { readdir as readdir5, stat as stat4 } from "fs/promises";
3899
+ import { join as join7, relative as relative6 } from "path";
3657
3900
  var lsTool = {
3658
3901
  name: "ls",
3659
3902
  kind: "read" /* Read */,
@@ -3677,7 +3920,7 @@ var lsTool = {
3677
3920
  const dirPath = args.path ? resolvePath(args.path, ctx.cwd) : ctx.cwd;
3678
3921
  const showAll = args.all ?? false;
3679
3922
  try {
3680
- const entries = await readdir4(dirPath, { withFileTypes: true });
3923
+ const entries = await readdir5(dirPath, { withFileTypes: true });
3681
3924
  const lines = [];
3682
3925
  const sorted = [...entries].toSorted((a, b) => {
3683
3926
  if (a.isDirectory() !== b.isDirectory()) {
@@ -3691,7 +3934,7 @@ var lsTool = {
3691
3934
  let sizeStr = "";
3692
3935
  if (typeMark === "FILE") {
3693
3936
  try {
3694
- const fileStat = await stat4(join6(dirPath, entry.name));
3937
+ const fileStat = await stat4(join7(dirPath, entry.name));
3695
3938
  if (fileStat.size < 1024) {
3696
3939
  sizeStr = `${fileStat.size}B`;
3697
3940
  } else if (fileStat.size < 1024 * 1024) {
@@ -3900,6 +4143,9 @@ var PHASE_CONFIG = {
3900
4143
  calling_tools: { icon: "\u{1F6E0}", label: "\u8C03\u7528\u5DE5\u5177", color: "#f59e0b" },
3901
4144
  executing_tools: { icon: "\u26A1", label: "\u6267\u884C\u5DE5\u5177", color: "#00ffff" }
3902
4145
  };
4146
+ function isFileMutatingTool(name) {
4147
+ return name === "edit_file" || name === "write_file" || name === "multi_edit" || name === "delete_range";
4148
+ }
3903
4149
  var commandRegistry = /* @__PURE__ */ new Map();
3904
4150
  function registerCommand(name, cmd) {
3905
4151
  commandRegistry.set(name, cmd);
@@ -3929,6 +4175,7 @@ registerCommand("/game", { desc: "\u542F\u52A8\u6E38\u620F", handler: () => ({ k
3929
4175
  registerCommand("/stock", { desc: "\u67E5\u770B\u80A1\u7968\u884C\u60C5", handler: () => ({ kind: "navigate", target: "stock" }) });
3930
4176
  registerCommand("/plan", { desc: "\u5207\u6362\u4E3A\u8BA1\u5212\u6A21\u5F0F\uFF08Shift+Tab\uFF09", handler: () => ({ kind: "text", content: "\u8F93\u5165 /plan \u6216\u6309 Shift+Tab \u5207\u6362\u4E3A\u8BA1\u5212\u6A21\u5F0F" }) });
3931
4177
  registerCommand("/code", { desc: "\u5207\u6362\u56DE\u4EE3\u7801\u6A21\u5F0F\uFF08Shift+Tab\uFF09", handler: () => ({ kind: "text", content: "\u8F93\u5165 /code \u6216\u6309 Shift+Tab \u5207\u6362\u56DE\u4EE3\u7801\u6A21\u5F0F" }) });
4178
+ registerCommand("/rewind", { desc: "\u56DE\u9000\u5230\u5386\u53F2\u68C0\u67E5\u70B9\uFF081 = \u6700\u65B0\uFF09", handler: () => ({ kind: "text", content: "\u8BF7\u76F4\u63A5\u8F93\u5165 /rewind \u67E5\u770B\u53EF\u56DE\u9000\u7684\u68C0\u67E5\u70B9\u5217\u8868\uFF0C\u6216 /rewind <\u5E8F\u53F7> \u76F4\u63A5\u56DE\u9000\uFF081 = \u6700\u65B0\uFF0C2 = \u4E0A\u4E00\u6B21\uFF0C\u4F9D\u6B64\u7C7B\u63A8\uFF09" }) });
3932
4179
  var STREAMING_PLACEHOLDERS = [
3933
4180
  "\u8BA9\u5B50\u5F39\u98DE\u4E00\u4F1A\u513F...",
3934
4181
  "\u9A6C\u4E0A\u5C31\u597D...",
@@ -4007,6 +4254,13 @@ function ChatSession({
4007
4254
  const [skillSelectIndex, setSkillSelectIndex] = useState3(0);
4008
4255
  const [fileSelectIndex, setFileSelectIndex] = useState3(0);
4009
4256
  const [inputKey, setInputKey] = useState3(0);
4257
+ const [rewindSelecting, setRewindSelecting] = useState3(false);
4258
+ const [rewindSelectIndex, setRewindSelectIndex] = useState3(0);
4259
+ const [rewindList, setRewindList] = useState3([]);
4260
+ const [rewinding, setRewinding] = useState3(false);
4261
+ const [rewindHintPhase, setRewindHintPhase] = useState3("idle");
4262
+ const currentRoundModifiedRef = useRef2(false);
4263
+ const rewindHintTimerRef = useRef2(null);
4010
4264
  const [selectingModel, setSelectingModel] = useState3(false);
4011
4265
  const [modelSelectIndex, setModelSelectIndex] = useState3(0);
4012
4266
  const modelOptions = ["deepseek-v4-flash", "deepseek-v4-pro"];
@@ -4023,6 +4277,14 @@ function ChatSession({
4023
4277
  setSkillSelectIndex(0);
4024
4278
  setFileSelectIndex(0);
4025
4279
  }, [input]);
4280
+ useEffect3(() => {
4281
+ return () => {
4282
+ if (rewindHintTimerRef.current) {
4283
+ clearTimeout(rewindHintTimerRef.current);
4284
+ rewindHintTimerRef.current = null;
4285
+ }
4286
+ };
4287
+ }, []);
4026
4288
  const getFilteredSkills = useCallback2(
4027
4289
  (value) => {
4028
4290
  const match = value.match(/(?:^|\s)\/([^/]*)$/);
@@ -4053,12 +4315,89 @@ function ChatSession({
4053
4315
  },
4054
4316
  [files]
4055
4317
  );
4318
+ function rebuildDisplayFromSession(sess) {
4319
+ const next = [];
4320
+ for (const m of sess.messages) {
4321
+ if (m.role === "user") {
4322
+ next.push({ role: "user", content: m.content });
4323
+ } else if (m.role === "assistant") {
4324
+ next.push({
4325
+ role: "assistant",
4326
+ content: m.content,
4327
+ assistantDetail: {
4328
+ content: m.content,
4329
+ toolCalls: m.toolCalls
4330
+ }
4331
+ });
4332
+ } else if (m.role === "tool") {
4333
+ next.push({ role: "tool", content: m.content });
4334
+ }
4335
+ }
4336
+ setDisplayMessages(next);
4337
+ setStaticKey((prev) => prev + 1);
4338
+ }
4339
+ const doRewind = useCallback2(async (target, displayNumber) => {
4340
+ const session = sessionRef.current;
4341
+ if (!session) return;
4342
+ setRewinding(true);
4343
+ setIsStreaming(true);
4344
+ setStreamingPhase("thinking");
4345
+ setStreamingPlaceholder("\u23EA \u6B63\u5728\u56DE\u9000\u5230\u68C0\u67E5\u70B9\u2026");
4346
+ setInput("");
4347
+ try {
4348
+ const r = await session.rewind(target.index);
4349
+ if (r.ok) {
4350
+ rebuildDisplayFromSession(session);
4351
+ const tail = r.fileRestored ? "\uFF0C\u5DE5\u4F5C\u533A\u6587\u4EF6\u5DF2\u6062\u590D" : "\uFF08\u4EC5\u5BF9\u8BDD\u56DE\u9000\uFF0C\u672A\u6062\u590D\u6587\u4EF6\uFF09";
4352
+ setDisplayMessages((prev) => [
4353
+ ...prev,
4354
+ { role: "assistant", content: `\u23EA \u5DF2\u56DE\u9000\u5230\u68C0\u67E5\u70B9 #${displayNumber}${tail}\u3002` }
4355
+ ]);
4356
+ } else {
4357
+ setDisplayMessages((prev) => [
4358
+ ...prev,
4359
+ { role: "assistant", content: `\u26A0 \u56DE\u9000\u5931\u8D25\uFF1A${r.error}` }
4360
+ ]);
4361
+ }
4362
+ } catch (err) {
4363
+ const msg = err instanceof Error ? err.message : String(err);
4364
+ setDisplayMessages((prev) => [
4365
+ ...prev,
4366
+ { role: "assistant", content: `\u26A0 \u56DE\u9000\u5F02\u5E38\uFF1A${msg}` }
4367
+ ]);
4368
+ } finally {
4369
+ setRewinding(false);
4370
+ setIsStreaming(false);
4371
+ setStreamingPhase(null);
4372
+ setStreamingPlaceholder("");
4373
+ }
4374
+ }, []);
4056
4375
  const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(() => {
4057
4376
  process.exit(0);
4058
4377
  });
4059
4378
  useInput(
4060
4379
  useCallback2(
4061
4380
  (_input, key) => {
4381
+ if (rewindSelecting) {
4382
+ if (key.upArrow) {
4383
+ setRewindSelectIndex((prev) => (prev - 1 + rewindList.length) % rewindList.length);
4384
+ } else if (key.downArrow) {
4385
+ setRewindSelectIndex((prev) => (prev + 1) % rewindList.length);
4386
+ } else if (key.return) {
4387
+ const target = rewindList[rewindSelectIndex];
4388
+ setRewindSelecting(false);
4389
+ if (target) {
4390
+ void doRewind(target, rewindSelectIndex + 1);
4391
+ }
4392
+ } else if (key.escape) {
4393
+ setRewindSelecting(false);
4394
+ setDisplayMessages((prev) => [
4395
+ ...prev,
4396
+ { role: "assistant", content: "\u5DF2\u53D6\u6D88\u56DE\u9000\u3002" }
4397
+ ]);
4398
+ }
4399
+ return;
4400
+ }
4062
4401
  if (selectingModel) {
4063
4402
  if (key.upArrow) {
4064
4403
  setModelSelectIndex((prev) => (prev - 1 + modelOptions.length) % modelOptions.length);
@@ -4165,7 +4504,7 @@ function ChatSession({
4165
4504
  setInput(_input);
4166
4505
  }
4167
4506
  },
4168
- [selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode]
4507
+ [selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode, rewindSelecting, rewindSelectIndex, rewindList, doRewind]
4169
4508
  )
4170
4509
  );
4171
4510
  useEffect3(() => {
@@ -4289,6 +4628,63 @@ function ChatSession({
4289
4628
  if (!trimmed) return;
4290
4629
  if (trimmed.startsWith("/") && trimmed.length > 1) {
4291
4630
  const cmdLower = trimmed.toLowerCase();
4631
+ if (cmdLower === "/rewind" || cmdLower.startsWith("/rewind ")) {
4632
+ if (isStreaming || rewinding) {
4633
+ setDisplayMessages((prev) => [
4634
+ ...prev,
4635
+ { role: "user", content: trimmed },
4636
+ { role: "assistant", content: "\u26A0 \u6B63\u5728\u751F\u6210\u6216\u56DE\u9000\u4E2D\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5 /rewind\u3002" }
4637
+ ]);
4638
+ setInput("");
4639
+ return;
4640
+ }
4641
+ if (!sessionRef.current) {
4642
+ setDisplayMessages((prev) => [
4643
+ ...prev,
4644
+ { role: "user", content: trimmed },
4645
+ { role: "assistant", content: "\u26A0 Session \u672A\u5C31\u7EEA\uFF0C\u65E0\u6CD5\u56DE\u9000\u3002" }
4646
+ ]);
4647
+ setInput("");
4648
+ return;
4649
+ }
4650
+ const cps = sessionRef.current.listCheckpoints();
4651
+ if (cps.length === 0) {
4652
+ setDisplayMessages((prev) => [
4653
+ ...prev,
4654
+ { role: "user", content: trimmed },
4655
+ { role: "assistant", content: "\u26A0 \u6CA1\u6709\u53EF\u56DE\u9000\u7684\u68C0\u67E5\u70B9\u3002\n\u53EA\u6709\u5728 git \u4ED3\u5E93\u5185\u4E14\u53D1\u751F\u8FC7\u5BF9\u8BDD\u540E\u624D\u4F1A\u751F\u6210\u68C0\u67E5\u70B9\u3002" }
4656
+ ]);
4657
+ setInput("");
4658
+ return;
4659
+ }
4660
+ const parts = trimmed.split(/\s+/);
4661
+ if (parts.length >= 2) {
4662
+ const n = Number(parts[1]);
4663
+ if (!Number.isInteger(n) || n < 1 || n > cps.length) {
4664
+ setDisplayMessages((prev) => [
4665
+ ...prev,
4666
+ { role: "user", content: trimmed },
4667
+ { role: "assistant", content: `\u26A0 \u65E0\u6548\u7684\u5E8F\u53F7\u300C${parts[1]}\u300D\u3002\u53EF\u7528\u8303\u56F4 1~${cps.length}\uFF081 \u8868\u793A\u6700\u65B0\uFF09\u3002` }
4668
+ ]);
4669
+ setInput("");
4670
+ return;
4671
+ }
4672
+ const target = cps[cps.length - n];
4673
+ setInput("");
4674
+ await doRewind(target, n);
4675
+ return;
4676
+ }
4677
+ setRewindList([...cps].reverse());
4678
+ setRewindSelectIndex(0);
4679
+ setRewindSelecting(true);
4680
+ setDisplayMessages((prev) => [
4681
+ ...prev,
4682
+ { role: "user", content: trimmed },
4683
+ { role: "assistant", content: `\u23F7\u2191\u2193 \u9009\u62E9\u68C0\u67E5\u70B9\uFF0CEnter \u786E\u8BA4\uFF0CEsc \u53D6\u6D88\uFF08\u5171 ${cps.length} \u4E2A\u53EF\u56DE\u9000\u4F4D\u7F6E\uFF09` }
4684
+ ]);
4685
+ setInput("");
4686
+ return;
4687
+ }
4292
4688
  if (cmdLower === "/model") {
4293
4689
  const curIdx = modelOptions.indexOf(activeModel);
4294
4690
  setModelSelectIndex(curIdx >= 0 ? curIdx : 0);
@@ -4443,6 +4839,12 @@ function ChatSession({
4443
4839
  currentCostRef.current = void 0;
4444
4840
  currentModelRef.current = void 0;
4445
4841
  streamErrorRef.current = void 0;
4842
+ currentRoundModifiedRef.current = false;
4843
+ setRewindHintPhase("idle");
4844
+ if (rewindHintTimerRef.current) {
4845
+ clearTimeout(rewindHintTimerRef.current);
4846
+ rewindHintTimerRef.current = null;
4847
+ }
4446
4848
  const session = sessionRef.current;
4447
4849
  const abortController = new AbortController();
4448
4850
  abortRef.current = abortController;
@@ -4470,6 +4872,12 @@ function ChatSession({
4470
4872
  currentToolCallsRef.current = next;
4471
4873
  return next;
4472
4874
  });
4875
+ for (const call of event.calls) {
4876
+ if (isFileMutatingTool(call.name)) {
4877
+ currentRoundModifiedRef.current = true;
4878
+ break;
4879
+ }
4880
+ }
4473
4881
  break;
4474
4882
  case "tool_result":
4475
4883
  setStreamingPhase("executing_tools");
@@ -4540,6 +4948,14 @@ function ChatSession({
4540
4948
  }
4541
4949
  ]);
4542
4950
  }
4951
+ if (currentRoundModifiedRef.current && !finStreamError) {
4952
+ setRewindHintPhase("pending");
4953
+ if (rewindHintTimerRef.current) clearTimeout(rewindHintTimerRef.current);
4954
+ rewindHintTimerRef.current = setTimeout(() => {
4955
+ setRewindHintPhase((prev) => prev === "pending" ? "visible" : prev);
4956
+ rewindHintTimerRef.current = null;
4957
+ }, 2e3);
4958
+ }
4543
4959
  }
4544
4960
  }, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode]);
4545
4961
  useEffect3(() => {
@@ -4685,6 +5101,38 @@ function ChatSession({
4685
5101
  /* @__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" }) })
4686
5102
  ] }),
4687
5103
  /* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
5104
+ ] }) : rewindSelecting ? /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
5105
+ /* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) }),
5106
+ /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, children: [
5107
+ /* @__PURE__ */ jsx9(Text10, { bold: true, color: "#ff9800", children: "\u9009\u62E9\u8981\u56DE\u9000\u7684\u68C0\u67E5\u70B9\uFF1A" }),
5108
+ rewindList.map((cp2, i) => {
5109
+ const isSelected = i === rewindSelectIndex;
5110
+ const marker = isSelected ? " > " : " ";
5111
+ const time = new Date(cp2.timestamp).toLocaleTimeString();
5112
+ const preview = cp2.preview || "(\u7A7A)";
5113
+ const tag = cp2.isGitRepo ? "" : " [\u975E git\uFF0C\u4EC5\u5BF9\u8BDD]";
5114
+ return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(
5115
+ Text10,
5116
+ {
5117
+ color: isSelected ? "#ff9800" : void 0,
5118
+ bold: isSelected,
5119
+ children: [
5120
+ marker,
5121
+ "#",
5122
+ i + 1,
5123
+ " ",
5124
+ time,
5125
+ " `",
5126
+ preview,
5127
+ tag,
5128
+ "`"
5129
+ ]
5130
+ }
5131
+ ) }, cp2.index);
5132
+ }),
5133
+ /* @__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" }) })
5134
+ ] }),
5135
+ /* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
4688
5136
  ] }) : /* @__PURE__ */ jsxs9(Fragment, { children: [
4689
5137
  (hasConversationStarted || sessionMode === "plan") && isStreaming && streamingPhase ? /* @__PURE__ */ jsx9(Box9, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs9(Text10, { bold: true, color: PHASE_CONFIG[streamingPhase].color, children: [
4690
5138
  PHASE_CONFIG[streamingPhase].icon,
@@ -4692,7 +5140,11 @@ function ChatSession({
4692
5140
  PHASE_CONFIG[streamingPhase].label,
4693
5141
  " ",
4694
5142
  /* @__PURE__ */ jsx9(InkSpinner2, { type: "dots" })
4695
- ] }) }) : null,
5143
+ ] }) }) : rewindHintPhase === "visible" ? (
5144
+ // 本轮修改了文件时,流式结束后 2s 在原位置展示 /rewind 提示
5145
+ // 一直保留到下次对话开始,与 /rewind 1(最新检查点)配套
5146
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsx9(Text10, { color: "#808080", children: "\u21A9 /rewind 1 \u53EF\u64A4\u56DE\u672C\u6B21\u4FEE\u6539" }) })
5147
+ ) : null,
4696
5148
  /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: hasConversationStarted || sessionMode === "plan" ? /* @__PURE__ */ jsxs9(Text10, { color: sessionMode === "plan" ? "#ff69b4" : "#00ffff", dimColor: sessionMode !== "plan", children: [
4697
5149
  /* @__PURE__ */ jsx9(Text10, { children: sessionMode === "plan" ? "\u2500".repeat(Math.max(dividerWidth - 48, 1)) : "\u2500".repeat(Math.max(dividerWidth - 35, 10)) }),
4698
5150
  balance !== null && /* @__PURE__ */ jsxs9(Text10, { color: "yellow", children: [
@@ -5966,9 +6418,9 @@ import { Box as Box13, Text as Text14, useInput as useInput5 } from "ink";
5966
6418
  import { useState as useState7, useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo2 } from "react";
5967
6419
  import asciichart from "asciichart";
5968
6420
  import os from "os";
5969
- import { join as join7 } from "path";
6421
+ import { join as join8 } from "path";
5970
6422
  import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
5971
- var SETTINGS_PATH = join7(os.homedir(), ".dskcode", "settings.json");
6423
+ var SETTINGS_PATH = join8(os.homedir(), ".dskcode", "settings.json");
5972
6424
  function toFileUrl(p) {
5973
6425
  const norm = p.replace(/\\/g, "/");
5974
6426
  if (process.platform === "win32") {
@@ -6302,8 +6754,8 @@ function renderDetail(stock, _onBack, prices, countdown = 10, currentTime) {
6302
6754
  }
6303
6755
 
6304
6756
  // src/utils/scan-files.ts
6305
- import { readdir as readdir5 } from "fs/promises";
6306
- import { join as join8, relative as relative7 } from "path";
6757
+ import { readdir as readdir6 } from "fs/promises";
6758
+ import { join as join9, relative as relative7 } from "path";
6307
6759
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
6308
6760
  "node_modules",
6309
6761
  ".git",
@@ -6354,7 +6806,7 @@ async function scanProjectFiles(baseDir, dir) {
6354
6806
  const currentDir = dir ?? baseDir;
6355
6807
  let entries;
6356
6808
  try {
6357
- entries = await readdir5(currentDir, { withFileTypes: true });
6809
+ entries = await readdir6(currentDir, { withFileTypes: true });
6358
6810
  } catch {
6359
6811
  return [];
6360
6812
  }
@@ -6368,12 +6820,12 @@ async function scanProjectFiles(baseDir, dir) {
6368
6820
  } else if (entry.isFile()) {
6369
6821
  const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
6370
6822
  if (SOURCE_EXTS.has(ext)) {
6371
- files.push(relative7(baseDir, join8(currentDir, name)));
6823
+ files.push(relative7(baseDir, join9(currentDir, name)));
6372
6824
  }
6373
6825
  }
6374
6826
  }
6375
6827
  const nested = await Promise.all(
6376
- dirs.map((d) => scanProjectFiles(baseDir, join8(currentDir, d)))
6828
+ dirs.map((d) => scanProjectFiles(baseDir, join9(currentDir, d)))
6377
6829
  );
6378
6830
  for (const n of nested) {
6379
6831
  files.push(...n);
@@ -6461,10 +6913,10 @@ compdef _dskcode_completion dskcode`);
6461
6913
  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) {
6462
6914
  const _ctx = this.dskcodeCtx;
6463
6915
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
6464
- const globalConfigPath = join9(home, ".dskcode", "settings.json");
6916
+ const globalConfigPath = join10(home, ".dskcode", "settings.json");
6465
6917
  let globalConfigHasStock = false;
6466
6918
  try {
6467
- const raw = await readFile10(globalConfigPath, "utf-8");
6919
+ const raw = await readFile11(globalConfigPath, "utf-8");
6468
6920
  const parsed = JSON.parse(raw);
6469
6921
  const stock = parsed.stock;
6470
6922
  globalConfigHasStock = Array.isArray(stock?.symbols) && stock.symbols.length > 0;