dskcode 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -16
- package/dist/index.js +646 -148
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
775
|
-
import { join as
|
|
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";
|
|
@@ -846,7 +846,15 @@ import { Box as Box5, Text as Text6 } from "ink";
|
|
|
846
846
|
// src/provider/cost-tracker.ts
|
|
847
847
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
848
848
|
import { join as join3 } from "path";
|
|
849
|
-
var CostTracker = class {
|
|
849
|
+
var CostTracker = class _CostTracker {
|
|
850
|
+
/** 全局活跃实例表,用于进程退出时统一 flush。 */
|
|
851
|
+
static #instances = /* @__PURE__ */ new Set();
|
|
852
|
+
/** 进程退出兜底:统一 flush 所有活跃 CostTracker。 */
|
|
853
|
+
static async flushAll() {
|
|
854
|
+
await Promise.allSettled(
|
|
855
|
+
[..._CostTracker.#instances].map((t) => t.flush())
|
|
856
|
+
);
|
|
857
|
+
}
|
|
850
858
|
#costDir;
|
|
851
859
|
#budgetLimit;
|
|
852
860
|
#tokenBudgetLimit;
|
|
@@ -872,6 +880,7 @@ var CostTracker = class {
|
|
|
872
880
|
const today = getTodayStr();
|
|
873
881
|
this.#todayDate = today;
|
|
874
882
|
this.#todaySummary = createEmptyDailySummary(today);
|
|
883
|
+
_CostTracker.#instances.add(this);
|
|
875
884
|
}
|
|
876
885
|
// -----------------------------------------------------------------------
|
|
877
886
|
// 核心方法
|
|
@@ -879,6 +888,7 @@ var CostTracker = class {
|
|
|
879
888
|
/**
|
|
880
889
|
* 记录一次 API 调用的 Token 使用量和成本。
|
|
881
890
|
* 自动计算费用,累加到会话级和日级统计。
|
|
891
|
+
* 标记脏数据(dirty),由上层在合适的时机调用 flush() 持久化到磁盘。
|
|
882
892
|
*/
|
|
883
893
|
record(usage, model) {
|
|
884
894
|
const cost = calculateCost(usage, model);
|
|
@@ -987,8 +997,10 @@ var CostTracker = class {
|
|
|
987
997
|
/**
|
|
988
998
|
* 从磁盘加载历史成本数据。
|
|
989
999
|
* 启动时调用,用于恢复今日和历史数据。
|
|
1000
|
+
* 副作用:确保成本目录存在(首次运行时创建 ~/.dskcode/costs)。
|
|
990
1001
|
*/
|
|
991
1002
|
async load() {
|
|
1003
|
+
await this.#ensureCostDir();
|
|
992
1004
|
const store = await this.#loadStore();
|
|
993
1005
|
const today = getTodayStr();
|
|
994
1006
|
if (store.daily[today]) {
|
|
@@ -998,14 +1010,19 @@ var CostTracker = class {
|
|
|
998
1010
|
}
|
|
999
1011
|
/**
|
|
1000
1012
|
* 将脏数据持久化到磁盘。
|
|
1001
|
-
*
|
|
1013
|
+
* 会话结束/进程退出时主动调用一次即可。需要每日数据精确时也可在每次
|
|
1014
|
+
* record() 后调用。并发安全:靠 #flushInProgress 互斥。
|
|
1002
1015
|
*/
|
|
1003
1016
|
async flush() {
|
|
1004
1017
|
if (!this.#dirty || this.#flushInProgress) return;
|
|
1005
1018
|
this.#flushInProgress = true;
|
|
1006
1019
|
this.#dirty = false;
|
|
1007
1020
|
try {
|
|
1021
|
+
await this.#ensureCostDir();
|
|
1008
1022
|
await this.#saveStore();
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
this.#dirty = true;
|
|
1025
|
+
console.error("[CostTracker] flush \u5931\u8D25:", err);
|
|
1009
1026
|
} finally {
|
|
1010
1027
|
this.#flushInProgress = false;
|
|
1011
1028
|
}
|
|
@@ -1033,6 +1050,14 @@ var CostTracker = class {
|
|
|
1033
1050
|
this.#sessionStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1034
1051
|
this.#sessionRecords.length = 0;
|
|
1035
1052
|
}
|
|
1053
|
+
/**
|
|
1054
|
+
* 销毁实例:从全局实例表移除并 flush。
|
|
1055
|
+
* 一般用不到,仅在测试或显式生命周期管理时使用。
|
|
1056
|
+
*/
|
|
1057
|
+
async dispose() {
|
|
1058
|
+
_CostTracker.#instances.delete(this);
|
|
1059
|
+
await this.flush();
|
|
1060
|
+
}
|
|
1036
1061
|
// -----------------------------------------------------------------------
|
|
1037
1062
|
// 内部方法
|
|
1038
1063
|
// -----------------------------------------------------------------------
|
|
@@ -1085,6 +1110,10 @@ var CostTracker = class {
|
|
|
1085
1110
|
return { version: 1, daily: {} };
|
|
1086
1111
|
}
|
|
1087
1112
|
}
|
|
1113
|
+
/** 确保成本目录存在(首次运行时创建 ~/.dskcode/costs) */
|
|
1114
|
+
async #ensureCostDir() {
|
|
1115
|
+
await mkdir3(this.#costDir, { recursive: true });
|
|
1116
|
+
}
|
|
1088
1117
|
/** 保存成本存储到磁盘 */
|
|
1089
1118
|
async #saveStore() {
|
|
1090
1119
|
const store = await this.#loadStore();
|
|
@@ -1096,7 +1125,6 @@ var CostTracker = class {
|
|
|
1096
1125
|
for (const date of datesToRemove) {
|
|
1097
1126
|
delete store.daily[date];
|
|
1098
1127
|
}
|
|
1099
|
-
await mkdir3(this.#costDir, { recursive: true });
|
|
1100
1128
|
const filePath = join3(this.#costDir, "history.json");
|
|
1101
1129
|
await writeFile2(filePath, JSON.stringify(store, null, 2), "utf-8");
|
|
1102
1130
|
}
|
|
@@ -2086,17 +2114,183 @@ var ToolRegistry = class {
|
|
|
2086
2114
|
}
|
|
2087
2115
|
};
|
|
2088
2116
|
|
|
2117
|
+
// src/checkpoint/git-checkpoint.ts
|
|
2118
|
+
import { execFile } from "child_process";
|
|
2119
|
+
import { promisify } from "util";
|
|
2120
|
+
var execFileAsync = promisify(execFile);
|
|
2121
|
+
var EXEC_OPTIONS = { windowsHide: true };
|
|
2122
|
+
async function isGitRepo(cwd) {
|
|
2123
|
+
try {
|
|
2124
|
+
await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, ...EXEC_OPTIONS });
|
|
2125
|
+
return true;
|
|
2126
|
+
} catch {
|
|
2127
|
+
return false;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
async function hasCommits(cwd) {
|
|
2131
|
+
try {
|
|
2132
|
+
await execFileAsync("git", ["rev-parse", "--verify", "HEAD"], { cwd, ...EXEC_OPTIONS });
|
|
2133
|
+
return true;
|
|
2134
|
+
} catch {
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
async function hasWorkingChanges(cwd) {
|
|
2139
|
+
const out = await execFileAsync("git", ["status", "--porcelain"], { cwd, ...EXEC_OPTIONS });
|
|
2140
|
+
return out.stdout.trim().length > 0;
|
|
2141
|
+
}
|
|
2142
|
+
async function git(args, cwd) {
|
|
2143
|
+
try {
|
|
2144
|
+
const { stdout } = await execFileAsync("git", args, { cwd, ...EXEC_OPTIONS });
|
|
2145
|
+
return stdout;
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2148
|
+
throw new Error(`git ${args.join(" ")} \u5931\u8D25: ${msg}`);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async function listStashShas(cwd) {
|
|
2152
|
+
const out = await git(["stash", "list", "--format=%H"], cwd);
|
|
2153
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2154
|
+
}
|
|
2155
|
+
async function createCheckpoint(cwd) {
|
|
2156
|
+
const timestamp = Date.now();
|
|
2157
|
+
const inRepo = await isGitRepo(cwd);
|
|
2158
|
+
if (!inRepo) return { stashSha: "", timestamp, cwd, isGitRepo: false };
|
|
2159
|
+
if (!await hasCommits(cwd)) return { stashSha: "", timestamp, cwd, isGitRepo: true };
|
|
2160
|
+
if (!await hasWorkingChanges(cwd)) return { stashSha: "", timestamp, cwd, isGitRepo: true };
|
|
2161
|
+
const beforeShas = await listStashShas(cwd);
|
|
2162
|
+
await git(["stash", "push", "-m", `dskcode-cp-${timestamp}`, "-u"], cwd);
|
|
2163
|
+
const newShas = await listStashShas(cwd);
|
|
2164
|
+
if (newShas.length === 0) throw new Error("git stash push \u672A\u80FD\u521B\u5EFA stash entry");
|
|
2165
|
+
const newSha = newShas.find((s) => !beforeShas.includes(s)) ?? newShas[0];
|
|
2166
|
+
await git(["stash", "apply", newSha], cwd);
|
|
2167
|
+
return { stashSha: newSha, timestamp, cwd, isGitRepo: true };
|
|
2168
|
+
}
|
|
2169
|
+
async function restoreCheckpointForce(checkpoint) {
|
|
2170
|
+
if (!checkpoint.isGitRepo) throw new Error("\u975E git \u4ED3\u5E93\uFF0C\u65E0\u6CD5\u6062\u590D\u6587\u4EF6\u72B6\u6001");
|
|
2171
|
+
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");
|
|
2172
|
+
const { cwd, stashSha } = checkpoint;
|
|
2173
|
+
const currentShas = await listStashShas(cwd);
|
|
2174
|
+
if (!currentShas.includes(stashSha)) {
|
|
2175
|
+
throw new Error("\u68C0\u67E5\u70B9\u5DF2\u5931\u6548\uFF08stash entry \u5DF2\u88AB\u6D88\u8D39\u6216 GC\uFF09\uFF0C\u65E0\u6CD5\u6062\u590D");
|
|
2176
|
+
}
|
|
2177
|
+
await git(["checkout", "--", "."], cwd);
|
|
2178
|
+
await git(["clean", "-fd"], cwd);
|
|
2179
|
+
await git(["stash", "apply", stashSha], cwd);
|
|
2180
|
+
const refIndex = currentShas.indexOf(stashSha);
|
|
2181
|
+
if (refIndex >= 0) await git(["stash", "drop", `stash@{${refIndex}}`], cwd);
|
|
2182
|
+
}
|
|
2183
|
+
async function restoreToClean(cwd) {
|
|
2184
|
+
const inRepo = await isGitRepo(cwd);
|
|
2185
|
+
if (!inRepo) throw new Error("\u975E git \u4ED3\u5E93\uFF0C\u65E0\u6CD5\u6062\u590D\u6587\u4EF6\u72B6\u6001");
|
|
2186
|
+
if (!await hasCommits(cwd)) throw new Error("\u4ED3\u5E93\u65E0 commit\uFF0C\u65E0\u6CD5\u6062\u590D");
|
|
2187
|
+
await git(["checkout", "--", "."], cwd);
|
|
2188
|
+
await git(["clean", "-fd"], cwd);
|
|
2189
|
+
}
|
|
2190
|
+
async function discardCheckpoint(checkpoint) {
|
|
2191
|
+
if (!checkpoint.isGitRepo || !checkpoint.stashSha) return;
|
|
2192
|
+
const { cwd, stashSha } = checkpoint;
|
|
2193
|
+
const shas = await listStashShas(cwd);
|
|
2194
|
+
const idx = shas.indexOf(stashSha);
|
|
2195
|
+
if (idx < 0) return;
|
|
2196
|
+
try {
|
|
2197
|
+
await git(["stash", "drop", `stash@{${idx}}`], cwd);
|
|
2198
|
+
} catch {
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// src/session-store/session-store.ts
|
|
2203
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3, readdir as readdir2, unlink, rename } from "fs/promises";
|
|
2204
|
+
import { join as join4 } from "path";
|
|
2205
|
+
import { randomUUID } from "crypto";
|
|
2206
|
+
function defaultSessionsDir() {
|
|
2207
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
2208
|
+
return join4(home, ".dskcode", "sessions");
|
|
2209
|
+
}
|
|
2210
|
+
var SessionStore = class {
|
|
2211
|
+
#dir;
|
|
2212
|
+
constructor(dir) {
|
|
2213
|
+
this.#dir = dir ?? defaultSessionsDir();
|
|
2214
|
+
}
|
|
2215
|
+
get dir() {
|
|
2216
|
+
return this.#dir;
|
|
2217
|
+
}
|
|
2218
|
+
async save(session) {
|
|
2219
|
+
await mkdir4(this.#dir, { recursive: true });
|
|
2220
|
+
const finalPath = join4(this.#dir, `${session.id}.json`);
|
|
2221
|
+
const tmpPath = join4(this.#dir, `.${session.id}.json.tmp`);
|
|
2222
|
+
await writeFile3(tmpPath, JSON.stringify(session, null, 2), "utf-8");
|
|
2223
|
+
await rename(tmpPath, finalPath);
|
|
2224
|
+
}
|
|
2225
|
+
async load(id) {
|
|
2226
|
+
const path = join4(this.#dir, `${id}.json`);
|
|
2227
|
+
try {
|
|
2228
|
+
const content = await readFile4(path, "utf-8");
|
|
2229
|
+
return JSON.parse(content);
|
|
2230
|
+
} catch (err) {
|
|
2231
|
+
if (isENOENT(err)) return null;
|
|
2232
|
+
throw err;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
async list() {
|
|
2236
|
+
let files;
|
|
2237
|
+
try {
|
|
2238
|
+
files = await readdir2(this.#dir);
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
if (isENOENT(err)) return [];
|
|
2241
|
+
throw err;
|
|
2242
|
+
}
|
|
2243
|
+
const results = [];
|
|
2244
|
+
for (const file of files) {
|
|
2245
|
+
if (!file.endsWith(".json") || file.startsWith(".")) continue;
|
|
2246
|
+
try {
|
|
2247
|
+
const content = await readFile4(join4(this.#dir, file), "utf-8");
|
|
2248
|
+
const s = JSON.parse(content);
|
|
2249
|
+
results.push({
|
|
2250
|
+
id: s.id,
|
|
2251
|
+
title: s.title || "\uFF08\u65E0\u6807\u9898\uFF09",
|
|
2252
|
+
updatedAt: s.updatedAt,
|
|
2253
|
+
cwd: s.cwd,
|
|
2254
|
+
messageCount: s.messages?.length ?? 0
|
|
2255
|
+
});
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return results.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
2260
|
+
}
|
|
2261
|
+
async delete(id) {
|
|
2262
|
+
const path = join4(this.#dir, `${id}.json`);
|
|
2263
|
+
try {
|
|
2264
|
+
await unlink(path);
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
if (!isENOENT(err)) throw err;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async exists(id) {
|
|
2270
|
+
return await this.load(id) !== null;
|
|
2271
|
+
}
|
|
2272
|
+
static newId() {
|
|
2273
|
+
return randomUUID();
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
function isENOENT(err) {
|
|
2277
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2089
2280
|
// src/agent/index.ts
|
|
2090
|
-
var Session = class {
|
|
2281
|
+
var Session = class _Session {
|
|
2091
2282
|
#messages = [];
|
|
2092
2283
|
#provider;
|
|
2093
2284
|
#toolRegistry;
|
|
2094
2285
|
#costTracker;
|
|
2095
2286
|
#options;
|
|
2096
2287
|
#abortController = new AbortController();
|
|
2097
|
-
|
|
2288
|
+
#sessionId;
|
|
2289
|
+
#store;
|
|
2290
|
+
#createdAt;
|
|
2291
|
+
#persistTimer = null;
|
|
2292
|
+
#checkpoints = /* @__PURE__ */ new Map();
|
|
2098
2293
|
#stormRecords = [];
|
|
2099
|
-
/** 当前会话模式:code(代码模式)或 plan(计划模式) */
|
|
2100
2294
|
#mode = "code";
|
|
2101
2295
|
constructor(provider, tools = [], costTracker, options) {
|
|
2102
2296
|
this.#provider = provider;
|
|
@@ -2114,12 +2308,13 @@ var Session = class {
|
|
|
2114
2308
|
preserveRecentRounds: options?.preserveRecentRounds ?? 10,
|
|
2115
2309
|
projectContext: options?.projectContext,
|
|
2116
2310
|
gate: options?.gate ?? new AlwaysAllowGate(),
|
|
2117
|
-
writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()]
|
|
2311
|
+
writeRoots: options?.writeRoots ?? [options?.cwd ?? process.cwd()],
|
|
2312
|
+
enableCheckpoint: options?.enableCheckpoint ?? true
|
|
2118
2313
|
};
|
|
2314
|
+
this.#sessionId = options?.sessionId ?? SessionStore.newId();
|
|
2315
|
+
this.#store = options?.store === false ? null : options?.store ?? new SessionStore();
|
|
2316
|
+
this.#createdAt = Date.now();
|
|
2119
2317
|
}
|
|
2120
|
-
// -------------------------------------------------------------------------
|
|
2121
|
-
// 公共只读属性
|
|
2122
|
-
// -------------------------------------------------------------------------
|
|
2123
2318
|
get messages() {
|
|
2124
2319
|
return this.#messages;
|
|
2125
2320
|
}
|
|
@@ -2132,36 +2327,35 @@ var Session = class {
|
|
|
2132
2327
|
get model() {
|
|
2133
2328
|
return this.#provider.model();
|
|
2134
2329
|
}
|
|
2135
|
-
/** 获取工具注册表(只读视图) */
|
|
2136
2330
|
get toolRegistry() {
|
|
2137
2331
|
return this.#toolRegistry;
|
|
2138
2332
|
}
|
|
2139
|
-
/** 获取当前会话模式 */
|
|
2140
2333
|
get mode() {
|
|
2141
2334
|
return this.#mode;
|
|
2142
2335
|
}
|
|
2143
|
-
|
|
2336
|
+
get id() {
|
|
2337
|
+
return this.#sessionId;
|
|
2338
|
+
}
|
|
2339
|
+
get store() {
|
|
2340
|
+
return this.#store;
|
|
2341
|
+
}
|
|
2342
|
+
get createdAt() {
|
|
2343
|
+
return this.#createdAt;
|
|
2344
|
+
}
|
|
2144
2345
|
setMode(mode) {
|
|
2145
2346
|
this.#mode = mode;
|
|
2146
2347
|
return this.#mode;
|
|
2147
2348
|
}
|
|
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
2349
|
async *chat(userInput, opts) {
|
|
2164
2350
|
this.#messages.push({ role: "user", content: userInput });
|
|
2351
|
+
const userMsgIndex = this.#messages.length - 1;
|
|
2352
|
+
if (this.#options.enableCheckpoint) {
|
|
2353
|
+
try {
|
|
2354
|
+
const checkpoint = await createCheckpoint(this.#options.cwd);
|
|
2355
|
+
this.#checkpoints.set(userMsgIndex, checkpoint);
|
|
2356
|
+
} catch {
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2165
2359
|
const startTime = Date.now();
|
|
2166
2360
|
let toolRounds = 0;
|
|
2167
2361
|
try {
|
|
@@ -2170,7 +2364,6 @@ var Session = class {
|
|
|
2170
2364
|
const [trimmed] = trimMessages(
|
|
2171
2365
|
[...this.#messages],
|
|
2172
2366
|
{
|
|
2173
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2174
2367
|
model: this.#provider.model(),
|
|
2175
2368
|
reservedForOutput: this.#options.reservedForOutput,
|
|
2176
2369
|
systemPrompt,
|
|
@@ -2196,28 +2389,17 @@ var Session = class {
|
|
|
2196
2389
|
accumulatedText += chunk.content;
|
|
2197
2390
|
yield { type: "text_delta", content: chunk.content };
|
|
2198
2391
|
}
|
|
2199
|
-
if (chunk.toolCalls && chunk.toolCalls.length > 0)
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if (chunk.usage) {
|
|
2203
|
-
lastUsage = chunk.usage;
|
|
2204
|
-
}
|
|
2205
|
-
if (chunk.finishReason) {
|
|
2206
|
-
_lastFinishReason = chunk.finishReason;
|
|
2207
|
-
}
|
|
2392
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) lastToolCalls = chunk.toolCalls;
|
|
2393
|
+
if (chunk.usage) lastUsage = chunk.usage;
|
|
2394
|
+
if (chunk.finishReason) _lastFinishReason = chunk.finishReason;
|
|
2208
2395
|
}
|
|
2209
2396
|
if (lastUsage) {
|
|
2210
2397
|
const modelId = this.#provider.model();
|
|
2211
2398
|
this.#costTracker.record(lastUsage, modelId);
|
|
2212
2399
|
yield { type: "usage", usage: lastUsage, model: modelId };
|
|
2213
2400
|
}
|
|
2214
|
-
const assistantMsg = {
|
|
2215
|
-
|
|
2216
|
-
content: accumulatedText
|
|
2217
|
-
};
|
|
2218
|
-
if (lastToolCalls && lastToolCalls.length > 0) {
|
|
2219
|
-
assistantMsg.toolCalls = lastToolCalls;
|
|
2220
|
-
}
|
|
2401
|
+
const assistantMsg = { role: "assistant", content: accumulatedText };
|
|
2402
|
+
if (lastToolCalls && lastToolCalls.length > 0) assistantMsg.toolCalls = lastToolCalls;
|
|
2221
2403
|
this.#messages.push(assistantMsg);
|
|
2222
2404
|
if (lastToolCalls && lastToolCalls.length > 0) {
|
|
2223
2405
|
yield { type: "tool_calls", calls: lastToolCalls };
|
|
@@ -2236,11 +2418,9 @@ var Session = class {
|
|
|
2236
2418
|
for (const item of results.items) {
|
|
2237
2419
|
yield { type: "tool_result", name: item.name, result: item.result };
|
|
2238
2420
|
let toolContent = item.result.data;
|
|
2239
|
-
if (item.result.diff && item.result.diff.patch)
|
|
2240
|
-
toolContent += `
|
|
2421
|
+
if (item.result.diff && item.result.diff.patch) toolContent += `
|
|
2241
2422
|
|
|
2242
2423
|
${item.result.diff.patch}`;
|
|
2243
|
-
}
|
|
2244
2424
|
this.#messages.push({
|
|
2245
2425
|
role: "tool",
|
|
2246
2426
|
content: toolContent,
|
|
@@ -2254,31 +2434,17 @@ ${item.result.diff.patch}`;
|
|
|
2254
2434
|
break;
|
|
2255
2435
|
}
|
|
2256
2436
|
} catch (err) {
|
|
2257
|
-
if (err instanceof DOMException && err.name === "AbortError")
|
|
2258
|
-
|
|
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
|
-
};
|
|
2437
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
2438
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
2439
|
+
yield { type: "error", error: err instanceof Error ? err : new Error(String(err)) };
|
|
2267
2440
|
return;
|
|
2268
2441
|
}
|
|
2269
2442
|
const elapsed = Date.now() - startTime;
|
|
2443
|
+
void this.#persist();
|
|
2444
|
+
await this.#costTracker.flush().catch(() => {
|
|
2445
|
+
});
|
|
2270
2446
|
yield { type: "done", elapsed };
|
|
2271
2447
|
}
|
|
2272
|
-
// -------------------------------------------------------------------------
|
|
2273
|
-
// 工具执行 — 批量、并行/串行、Gate、风暴检测
|
|
2274
|
-
// -------------------------------------------------------------------------
|
|
2275
|
-
/**
|
|
2276
|
-
* 执行一批工具调用。
|
|
2277
|
-
*
|
|
2278
|
-
* 并行策略:
|
|
2279
|
-
* - 如果这批工具全部是 ReadOnly 的,并行执行(最多 8 并发)
|
|
2280
|
-
* - 否则按顺序串行执行,保证写/读顺序
|
|
2281
|
-
*/
|
|
2282
2448
|
async #executeBatch(calls) {
|
|
2283
2449
|
const toolCtx = {
|
|
2284
2450
|
cwd: this.#options.cwd,
|
|
@@ -2313,9 +2479,6 @@ ${item.result.diff.patch}`;
|
|
|
2313
2479
|
}
|
|
2314
2480
|
return { items, records };
|
|
2315
2481
|
}
|
|
2316
|
-
/**
|
|
2317
|
-
* 执行单个工具调用,包含 Gate 检查和预览。
|
|
2318
|
-
*/
|
|
2319
2482
|
async #executeOne(tc, ctx) {
|
|
2320
2483
|
const toolName = tc.name;
|
|
2321
2484
|
const timestamp = Date.now();
|
|
@@ -2354,12 +2517,7 @@ ${item.result.diff.patch}`;
|
|
|
2354
2517
|
const result = await tool.execute(toolArgs, ctx);
|
|
2355
2518
|
return {
|
|
2356
2519
|
item: { name: toolName, callId: tc.id, result },
|
|
2357
|
-
record: {
|
|
2358
|
-
name: toolName,
|
|
2359
|
-
success: result.success,
|
|
2360
|
-
error: result.error,
|
|
2361
|
-
timestamp
|
|
2362
|
-
}
|
|
2520
|
+
record: { name: toolName, success: result.success, error: result.error, timestamp }
|
|
2363
2521
|
};
|
|
2364
2522
|
} catch (err) {
|
|
2365
2523
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2370,46 +2528,172 @@ ${item.result.diff.patch}`;
|
|
|
2370
2528
|
};
|
|
2371
2529
|
}
|
|
2372
2530
|
}
|
|
2373
|
-
// -------------------------------------------------------------------------
|
|
2374
|
-
// 风暴检测 — 同一工具同一错误连续 3 次 → 强制换策略
|
|
2375
|
-
// -------------------------------------------------------------------------
|
|
2376
|
-
/**
|
|
2377
|
-
* 连续 3 次同一工具同一错误 → 触发风暴中断。
|
|
2378
|
-
*/
|
|
2379
2531
|
#checkStormBreak(currentCalls) {
|
|
2380
2532
|
if (this.#stormRecords.length < 3) return false;
|
|
2381
2533
|
const recentErrors = this.#stormRecords.slice(-3);
|
|
2382
2534
|
if (recentErrors.length < 3) return false;
|
|
2383
2535
|
const first = recentErrors[0];
|
|
2384
|
-
const allSame = recentErrors.every(
|
|
2385
|
-
(r) => r.name === first.name && r.error === first.error && !r.success
|
|
2386
|
-
);
|
|
2536
|
+
const allSame = recentErrors.every((r) => r.name === first.name && r.error === first.error && !r.success);
|
|
2387
2537
|
if (!allSame) return false;
|
|
2388
2538
|
return currentCalls.some((tc) => tc.name === first.name);
|
|
2389
2539
|
}
|
|
2390
|
-
// -------------------------------------------------------------------------
|
|
2391
|
-
// 会话管理
|
|
2392
|
-
// -------------------------------------------------------------------------
|
|
2393
|
-
/** 取消正在进行的流式请求 */
|
|
2394
2540
|
abort() {
|
|
2395
2541
|
this.#abortController.abort();
|
|
2542
|
+
if (this.#persistTimer) {
|
|
2543
|
+
clearTimeout(this.#persistTimer);
|
|
2544
|
+
this.#persistTimer = null;
|
|
2545
|
+
}
|
|
2396
2546
|
}
|
|
2397
|
-
/** 重置会话历史(保留 provider/tools 配置,重置成本追踪) */
|
|
2398
2547
|
reset() {
|
|
2399
2548
|
this.#messages.length = 0;
|
|
2400
2549
|
this.#costTracker.resetSession();
|
|
2401
2550
|
this.#stormRecords = [];
|
|
2551
|
+
this.#checkpoints.clear();
|
|
2552
|
+
}
|
|
2553
|
+
// -------------------------------------------------------------------------
|
|
2554
|
+
// 持久化与恢复
|
|
2555
|
+
// -------------------------------------------------------------------------
|
|
2556
|
+
async persistNow() {
|
|
2557
|
+
if (this.#persistTimer) {
|
|
2558
|
+
clearTimeout(this.#persistTimer);
|
|
2559
|
+
this.#persistTimer = null;
|
|
2560
|
+
}
|
|
2561
|
+
await this.#doPersist();
|
|
2562
|
+
}
|
|
2563
|
+
#persist() {
|
|
2564
|
+
if (!this.#store) return;
|
|
2565
|
+
if (this.#persistTimer) {
|
|
2566
|
+
this.#persistTimer.refresh();
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
this.#persistTimer = setTimeout(() => {
|
|
2570
|
+
this.#persistTimer = null;
|
|
2571
|
+
void this.#doPersist();
|
|
2572
|
+
}, 500);
|
|
2573
|
+
this.#persistTimer.unref();
|
|
2574
|
+
}
|
|
2575
|
+
async #doPersist() {
|
|
2576
|
+
if (!this.#store) return;
|
|
2577
|
+
const stored = {
|
|
2578
|
+
id: this.#sessionId,
|
|
2579
|
+
title: this.#deriveTitle(),
|
|
2580
|
+
createdAt: this.#createdAt,
|
|
2581
|
+
updatedAt: Date.now(),
|
|
2582
|
+
cwd: this.#options.cwd,
|
|
2583
|
+
model: this.#provider.model(),
|
|
2584
|
+
messages: this.#serializeMessages(),
|
|
2585
|
+
totalCost: this.#costTracker.sessionTotalCost
|
|
2586
|
+
};
|
|
2587
|
+
try {
|
|
2588
|
+
await this.#store.save(stored);
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
console.error("[Session] \u6301\u4E45\u5316\u5931\u8D25:", err);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
#deriveTitle() {
|
|
2594
|
+
for (const m of this.#messages) {
|
|
2595
|
+
if (m.role === "user" && m.content.trim()) return m.content.trim().slice(0, 40);
|
|
2596
|
+
}
|
|
2597
|
+
return "\u65B0\u4F1A\u8BDD";
|
|
2598
|
+
}
|
|
2599
|
+
#serializeMessages() {
|
|
2600
|
+
return this.#messages.map((msg, idx) => {
|
|
2601
|
+
const checkpoint = this.#checkpoints.get(idx);
|
|
2602
|
+
if (msg.role === "user" && checkpoint) return { ...msg, checkpoint };
|
|
2603
|
+
return { ...msg };
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
static async resume(id, provider, tools = [], costTracker, options) {
|
|
2607
|
+
const store = options?.store === false ? null : options?.store ?? new SessionStore();
|
|
2608
|
+
if (!store) throw new Error("resume \u9700\u8981\u542F\u7528\u6301\u4E45\u5316\uFF08options.store \u4E0D\u80FD\u4E3A false\uFF09");
|
|
2609
|
+
const stored = await store.load(id);
|
|
2610
|
+
if (!stored) throw new Error(`\u4F1A\u8BDD ${id} \u4E0D\u5B58\u5728`);
|
|
2611
|
+
const session = new _Session(provider, tools, costTracker, { ...options, sessionId: id, store });
|
|
2612
|
+
for (const m of stored.messages) {
|
|
2613
|
+
session.#messages.push({
|
|
2614
|
+
role: m.role,
|
|
2615
|
+
content: m.content,
|
|
2616
|
+
toolCallId: m.toolCallId,
|
|
2617
|
+
name: m.name,
|
|
2618
|
+
toolCalls: m.toolCalls
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
for (let i = 0; i < stored.messages.length; i++) {
|
|
2622
|
+
const cp2 = stored.messages[i]?.checkpoint;
|
|
2623
|
+
if (cp2) session.#checkpoints.set(i, cp2);
|
|
2624
|
+
}
|
|
2625
|
+
session.#createdAt = stored.createdAt;
|
|
2626
|
+
return session;
|
|
2627
|
+
}
|
|
2628
|
+
// -------------------------------------------------------------------------
|
|
2629
|
+
// 检查点与 Rewind
|
|
2630
|
+
// -------------------------------------------------------------------------
|
|
2631
|
+
listCheckpoints() {
|
|
2632
|
+
const result = [];
|
|
2633
|
+
for (const [index, checkpoint] of this.#checkpoints) {
|
|
2634
|
+
const msg = this.#messages[index];
|
|
2635
|
+
if (!msg || msg.role !== "user") continue;
|
|
2636
|
+
result.push({ index, preview: msg.content.slice(0, 80), timestamp: checkpoint.timestamp, isGitRepo: checkpoint.isGitRepo });
|
|
2637
|
+
}
|
|
2638
|
+
return result.sort((a, b) => a.index - b.index);
|
|
2639
|
+
}
|
|
2640
|
+
async rewind(targetIndex) {
|
|
2641
|
+
if (targetIndex < 0 || targetIndex >= this.#messages.length) {
|
|
2642
|
+
return { ok: false, error: `\u65E0\u6548\u7684\u6D88\u606F\u7D22\u5F15 ${targetIndex}` };
|
|
2643
|
+
}
|
|
2644
|
+
const target = this.#messages[targetIndex];
|
|
2645
|
+
if (!target || target.role !== "user") {
|
|
2646
|
+
return { ok: false, error: `\u7D22\u5F15 ${targetIndex} \u4E0D\u662F user \u6D88\u606F` };
|
|
2647
|
+
}
|
|
2648
|
+
const checkpoint = this.#checkpoints.get(targetIndex);
|
|
2649
|
+
if (!checkpoint) return { ok: false, error: "\u8BE5\u6D88\u606F\u6CA1\u6709\u68C0\u67E5\u70B9" };
|
|
2650
|
+
this.#messages.length = targetIndex + 1;
|
|
2651
|
+
const toDiscard = [];
|
|
2652
|
+
for (const [idx, cp2] of this.#checkpoints) {
|
|
2653
|
+
if (idx > targetIndex) {
|
|
2654
|
+
toDiscard.push(cp2);
|
|
2655
|
+
this.#checkpoints.delete(idx);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
let fileRestored = false;
|
|
2659
|
+
if (checkpoint.isGitRepo) {
|
|
2660
|
+
try {
|
|
2661
|
+
if (checkpoint.stashSha) {
|
|
2662
|
+
await restoreCheckpointForce(checkpoint);
|
|
2663
|
+
} else {
|
|
2664
|
+
await restoreToClean(this.#options.cwd);
|
|
2665
|
+
}
|
|
2666
|
+
fileRestored = true;
|
|
2667
|
+
} catch (err) {
|
|
2668
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2669
|
+
return { ok: false, error: `\u5BF9\u8BDD\u5DF2\u622A\u65AD\u4F46\u6587\u4EF6\u6062\u590D\u5931\u8D25\uFF1A${msg}` };
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
this.#checkpoints.delete(targetIndex);
|
|
2673
|
+
for (const cp2 of toDiscard) {
|
|
2674
|
+
void discardCheckpoint(cp2);
|
|
2675
|
+
}
|
|
2676
|
+
this.#persist();
|
|
2677
|
+
return { ok: true, fileRestored };
|
|
2678
|
+
}
|
|
2679
|
+
hasCheckpoints() {
|
|
2680
|
+
return this.listCheckpoints().length > 0;
|
|
2681
|
+
}
|
|
2682
|
+
async delete() {
|
|
2683
|
+
if (this.#store) await this.#store.delete(this.#sessionId);
|
|
2684
|
+
for (const cp2 of this.#checkpoints.values()) {
|
|
2685
|
+
void discardCheckpoint(cp2);
|
|
2686
|
+
}
|
|
2687
|
+
this.#checkpoints.clear();
|
|
2402
2688
|
}
|
|
2403
2689
|
// -------------------------------------------------------------------------
|
|
2404
2690
|
// 内部方法
|
|
2405
2691
|
// -------------------------------------------------------------------------
|
|
2406
|
-
/** 构建系统提示词 */
|
|
2407
2692
|
#buildSystemPrompt() {
|
|
2408
2693
|
const enabledTools = this.#toolRegistry.list();
|
|
2409
2694
|
const toolDescs = enabledTools.map((t) => ({
|
|
2410
2695
|
name: t.name,
|
|
2411
2696
|
description: t.description,
|
|
2412
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
2413
2697
|
parameters: t.parameters
|
|
2414
2698
|
}));
|
|
2415
2699
|
const opts = {
|
|
@@ -2419,25 +2703,14 @@ ${item.result.diff.patch}`;
|
|
|
2419
2703
|
projectContext: this.#options.projectContext ?? void 0,
|
|
2420
2704
|
cwd: this.#options.cwd
|
|
2421
2705
|
};
|
|
2422
|
-
if (this.#mode === "plan")
|
|
2423
|
-
return buildPlanSystemPrompt(opts);
|
|
2424
|
-
}
|
|
2706
|
+
if (this.#mode === "plan") return buildPlanSystemPrompt(opts);
|
|
2425
2707
|
return buildSystemPrompt(opts);
|
|
2426
2708
|
}
|
|
2427
|
-
/**
|
|
2428
|
-
* 将注册的工具转为 ToolDefinition 格式(预留给 function calling)。
|
|
2429
|
-
* 计划模式下只返回读工具,禁止非读工具暴露给 LLM。
|
|
2430
|
-
*/
|
|
2431
2709
|
#buildToolDefinitions() {
|
|
2432
2710
|
const tools = this.#mode === "plan" ? this.#toolRegistry.listReadTools() : this.#toolRegistry.list();
|
|
2433
2711
|
return tools.map((t) => ({
|
|
2434
2712
|
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
|
-
}
|
|
2713
|
+
function: { name: t.name, description: t.description, parameters: t.parameters }
|
|
2441
2714
|
}));
|
|
2442
2715
|
}
|
|
2443
2716
|
};
|
|
@@ -2831,7 +3104,7 @@ function computeFileDiff(oldContent, newContent, filePath) {
|
|
|
2831
3104
|
}
|
|
2832
3105
|
|
|
2833
3106
|
// src/tool/eol.ts
|
|
2834
|
-
import { writeFile as
|
|
3107
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
2835
3108
|
function detectEol(text) {
|
|
2836
3109
|
if (text.length === 0) return "\n";
|
|
2837
3110
|
const crlfIdx = text.indexOf("\r\n");
|
|
@@ -2862,11 +3135,11 @@ function normalizeEol(originalContent, newContent) {
|
|
|
2862
3135
|
}
|
|
2863
3136
|
async function writeFileWithEol(filePath, originalContent, newContent) {
|
|
2864
3137
|
const content = originalContent.length > 0 ? normalizeEol(originalContent, newContent) : newContent;
|
|
2865
|
-
await
|
|
3138
|
+
await writeFile4(filePath, content, "utf-8");
|
|
2866
3139
|
}
|
|
2867
3140
|
|
|
2868
3141
|
// src/tool/builtins/read-file.ts
|
|
2869
|
-
import { readFile as
|
|
3142
|
+
import { readFile as readFile5, stat } from "fs/promises";
|
|
2870
3143
|
import { open } from "fs/promises";
|
|
2871
3144
|
import { relative as relative2 } from "path";
|
|
2872
3145
|
async function checkBinary(filePath) {
|
|
@@ -2934,7 +3207,7 @@ var readFileTool = {
|
|
|
2934
3207
|
};
|
|
2935
3208
|
}
|
|
2936
3209
|
}
|
|
2937
|
-
const content = await
|
|
3210
|
+
const content = await readFile5(filePath, "utf-8");
|
|
2938
3211
|
const lines = content.split("\n");
|
|
2939
3212
|
if (lines.length > 0 && lines[lines.length - 1] === "" && content.endsWith("\n")) {
|
|
2940
3213
|
lines.pop();
|
|
@@ -2970,7 +3243,7 @@ var readFileTool = {
|
|
|
2970
3243
|
};
|
|
2971
3244
|
|
|
2972
3245
|
// src/tool/builtins/write-file.ts
|
|
2973
|
-
import { mkdir as
|
|
3246
|
+
import { mkdir as mkdir5, readFile as readFile6 } from "fs/promises";
|
|
2974
3247
|
import { dirname, relative as relative3, basename } from "path";
|
|
2975
3248
|
var writeFileTool = {
|
|
2976
3249
|
name: "write_file",
|
|
@@ -3009,11 +3282,11 @@ var writeFileTool = {
|
|
|
3009
3282
|
let oldContent = "";
|
|
3010
3283
|
let existedBefore = false;
|
|
3011
3284
|
try {
|
|
3012
|
-
oldContent = await
|
|
3285
|
+
oldContent = await readFile6(filePath, "utf-8");
|
|
3013
3286
|
existedBefore = true;
|
|
3014
3287
|
} catch {
|
|
3015
3288
|
}
|
|
3016
|
-
await
|
|
3289
|
+
await mkdir5(dirname(filePath), { recursive: true });
|
|
3017
3290
|
const content = args.content;
|
|
3018
3291
|
await writeFileWithEol(filePath, oldContent, content);
|
|
3019
3292
|
const diff = computeFileDiff(oldContent, content, filePath);
|
|
@@ -3042,7 +3315,7 @@ var writeFileTool = {
|
|
|
3042
3315
|
};
|
|
3043
3316
|
|
|
3044
3317
|
// src/tool/builtins/edit-file.ts
|
|
3045
|
-
import { readFile as
|
|
3318
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
3046
3319
|
import { basename as basename2 } from "path";
|
|
3047
3320
|
var editFileTool = {
|
|
3048
3321
|
name: "edit_file",
|
|
@@ -3085,7 +3358,7 @@ var editFileTool = {
|
|
|
3085
3358
|
}
|
|
3086
3359
|
}
|
|
3087
3360
|
try {
|
|
3088
|
-
const content = await
|
|
3361
|
+
const content = await readFile7(filePath, "utf-8");
|
|
3089
3362
|
const firstIndex = content.indexOf(args.old_text);
|
|
3090
3363
|
if (firstIndex === -1) {
|
|
3091
3364
|
return {
|
|
@@ -3133,7 +3406,7 @@ ${oldLines} \u884C \u2192 ${newLines} \u884C
|
|
|
3133
3406
|
};
|
|
3134
3407
|
|
|
3135
3408
|
// src/tool/builtins/multi-edit.ts
|
|
3136
|
-
import { readFile as
|
|
3409
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3137
3410
|
import { basename as basename3 } from "path";
|
|
3138
3411
|
var multiEditTool = {
|
|
3139
3412
|
name: "multi_edit",
|
|
@@ -3179,7 +3452,7 @@ var multiEditTool = {
|
|
|
3179
3452
|
}
|
|
3180
3453
|
}
|
|
3181
3454
|
try {
|
|
3182
|
-
const originalContent = await
|
|
3455
|
+
const originalContent = await readFile8(filePath, "utf-8");
|
|
3183
3456
|
let currentContent = originalContent;
|
|
3184
3457
|
for (let idx = 0; idx < args.edits.length; idx++) {
|
|
3185
3458
|
const step = args.edits[idx];
|
|
@@ -3247,7 +3520,7 @@ var multiEditTool = {
|
|
|
3247
3520
|
};
|
|
3248
3521
|
|
|
3249
3522
|
// src/tool/builtins/delete-range.ts
|
|
3250
|
-
import { readFile as
|
|
3523
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3251
3524
|
import { basename as basename4 } from "path";
|
|
3252
3525
|
function findUniqueLine(lines, anchor, label) {
|
|
3253
3526
|
const matches = [];
|
|
@@ -3310,7 +3583,7 @@ var deleteRangeTool = {
|
|
|
3310
3583
|
}
|
|
3311
3584
|
}
|
|
3312
3585
|
try {
|
|
3313
|
-
const content = await
|
|
3586
|
+
const content = await readFile9(filePath, "utf-8");
|
|
3314
3587
|
const lines = content.split("\n");
|
|
3315
3588
|
const startResult = findUniqueLine(lines, args.startAnchor, "start_anchor");
|
|
3316
3589
|
if ("error" in startResult) {
|
|
@@ -3433,8 +3706,8 @@ ${truncateOutput(result.stderr)}`);
|
|
|
3433
3706
|
};
|
|
3434
3707
|
|
|
3435
3708
|
// src/tool/builtins/glob.ts
|
|
3436
|
-
import { readdir as
|
|
3437
|
-
import { join as
|
|
3709
|
+
import { readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
3710
|
+
import { join as join5, relative as relative4, isAbsolute as isAbsolute2 } from "path";
|
|
3438
3711
|
function globToRegex(pattern) {
|
|
3439
3712
|
let regexStr = pattern;
|
|
3440
3713
|
regexStr = regexStr.replace(/\*\*\//g, "<<GLOBSTAR_SLASH>>");
|
|
@@ -3451,7 +3724,7 @@ async function walkDir(dir, baseDir) {
|
|
|
3451
3724
|
const results = [];
|
|
3452
3725
|
let entries;
|
|
3453
3726
|
try {
|
|
3454
|
-
entries = await
|
|
3727
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
3455
3728
|
} catch {
|
|
3456
3729
|
return results;
|
|
3457
3730
|
}
|
|
@@ -3459,7 +3732,7 @@ async function walkDir(dir, baseDir) {
|
|
|
3459
3732
|
if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git")) {
|
|
3460
3733
|
continue;
|
|
3461
3734
|
}
|
|
3462
|
-
const fullPath =
|
|
3735
|
+
const fullPath = join5(dir, entry.name);
|
|
3463
3736
|
const relPath = relative4(baseDir, fullPath);
|
|
3464
3737
|
if (entry.isDirectory()) {
|
|
3465
3738
|
results.push(...await walkDir(fullPath, baseDir));
|
|
@@ -3492,7 +3765,7 @@ var globTool = {
|
|
|
3492
3765
|
if (!args?.pattern || typeof args.pattern !== "string") {
|
|
3493
3766
|
return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
|
|
3494
3767
|
}
|
|
3495
|
-
const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory :
|
|
3768
|
+
const searchDir = args.directory ? isAbsolute2(args.directory) ? args.directory : join5(ctx.cwd, args.directory) : ctx.cwd;
|
|
3496
3769
|
const regex = globToRegex(args.pattern);
|
|
3497
3770
|
try {
|
|
3498
3771
|
const dirStat = await stat2(searchDir);
|
|
@@ -3531,13 +3804,13 @@ var globTool = {
|
|
|
3531
3804
|
};
|
|
3532
3805
|
|
|
3533
3806
|
// src/tool/builtins/grep.ts
|
|
3534
|
-
import { readdir as
|
|
3535
|
-
import { join as
|
|
3807
|
+
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";
|
|
3536
3809
|
async function collectFiles(dir, extension, maxFiles = 200) {
|
|
3537
3810
|
const results = [];
|
|
3538
3811
|
let entries;
|
|
3539
3812
|
try {
|
|
3540
|
-
entries = await
|
|
3813
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
3541
3814
|
} catch {
|
|
3542
3815
|
return results;
|
|
3543
3816
|
}
|
|
@@ -3546,7 +3819,7 @@ async function collectFiles(dir, extension, maxFiles = 200) {
|
|
|
3546
3819
|
if (entry.isDirectory() && (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist")) {
|
|
3547
3820
|
continue;
|
|
3548
3821
|
}
|
|
3549
|
-
const fullPath =
|
|
3822
|
+
const fullPath = join6(dir, entry.name);
|
|
3550
3823
|
if (entry.isDirectory()) {
|
|
3551
3824
|
results.push(...await collectFiles(fullPath, extension, maxFiles - results.length));
|
|
3552
3825
|
} else {
|
|
@@ -3593,7 +3866,7 @@ var grepTool = {
|
|
|
3593
3866
|
if (!args?.pattern || typeof args.pattern !== "string") {
|
|
3594
3867
|
return { success: false, data: "\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570 pattern", error: "INVALID_ARGS" };
|
|
3595
3868
|
}
|
|
3596
|
-
const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory :
|
|
3869
|
+
const searchDir = args.directory ? isAbsolute3(args.directory) ? args.directory : join6(ctx.cwd, args.directory) : ctx.cwd;
|
|
3597
3870
|
const maxFiles = args.max_files ?? 200;
|
|
3598
3871
|
try {
|
|
3599
3872
|
const flags = args.case_sensitive ? "g" : "gi";
|
|
@@ -3606,7 +3879,7 @@ var grepTool = {
|
|
|
3606
3879
|
const matches = [];
|
|
3607
3880
|
for (const filePath of files) {
|
|
3608
3881
|
try {
|
|
3609
|
-
const content = await
|
|
3882
|
+
const content = await readFile10(filePath, "utf-8");
|
|
3610
3883
|
const lines = content.split("\n");
|
|
3611
3884
|
const relPath = relative5(searchDir, filePath);
|
|
3612
3885
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3652,8 +3925,8 @@ var grepTool = {
|
|
|
3652
3925
|
};
|
|
3653
3926
|
|
|
3654
3927
|
// src/tool/builtins/ls.ts
|
|
3655
|
-
import { readdir as
|
|
3656
|
-
import { join as
|
|
3928
|
+
import { readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
3929
|
+
import { join as join7, relative as relative6 } from "path";
|
|
3657
3930
|
var lsTool = {
|
|
3658
3931
|
name: "ls",
|
|
3659
3932
|
kind: "read" /* Read */,
|
|
@@ -3677,7 +3950,7 @@ var lsTool = {
|
|
|
3677
3950
|
const dirPath = args.path ? resolvePath(args.path, ctx.cwd) : ctx.cwd;
|
|
3678
3951
|
const showAll = args.all ?? false;
|
|
3679
3952
|
try {
|
|
3680
|
-
const entries = await
|
|
3953
|
+
const entries = await readdir5(dirPath, { withFileTypes: true });
|
|
3681
3954
|
const lines = [];
|
|
3682
3955
|
const sorted = [...entries].toSorted((a, b) => {
|
|
3683
3956
|
if (a.isDirectory() !== b.isDirectory()) {
|
|
@@ -3691,7 +3964,7 @@ var lsTool = {
|
|
|
3691
3964
|
let sizeStr = "";
|
|
3692
3965
|
if (typeMark === "FILE") {
|
|
3693
3966
|
try {
|
|
3694
|
-
const fileStat = await stat4(
|
|
3967
|
+
const fileStat = await stat4(join7(dirPath, entry.name));
|
|
3695
3968
|
if (fileStat.size < 1024) {
|
|
3696
3969
|
sizeStr = `${fileStat.size}B`;
|
|
3697
3970
|
} else if (fileStat.size < 1024 * 1024) {
|
|
@@ -3900,6 +4173,9 @@ var PHASE_CONFIG = {
|
|
|
3900
4173
|
calling_tools: { icon: "\u{1F6E0}", label: "\u8C03\u7528\u5DE5\u5177", color: "#f59e0b" },
|
|
3901
4174
|
executing_tools: { icon: "\u26A1", label: "\u6267\u884C\u5DE5\u5177", color: "#00ffff" }
|
|
3902
4175
|
};
|
|
4176
|
+
function isFileMutatingTool(name) {
|
|
4177
|
+
return name === "edit_file" || name === "write_file" || name === "multi_edit" || name === "delete_range";
|
|
4178
|
+
}
|
|
3903
4179
|
var commandRegistry = /* @__PURE__ */ new Map();
|
|
3904
4180
|
function registerCommand(name, cmd) {
|
|
3905
4181
|
commandRegistry.set(name, cmd);
|
|
@@ -3929,6 +4205,7 @@ registerCommand("/game", { desc: "\u542F\u52A8\u6E38\u620F", handler: () => ({ k
|
|
|
3929
4205
|
registerCommand("/stock", { desc: "\u67E5\u770B\u80A1\u7968\u884C\u60C5", handler: () => ({ kind: "navigate", target: "stock" }) });
|
|
3930
4206
|
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
4207
|
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" }) });
|
|
4208
|
+
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
4209
|
var STREAMING_PLACEHOLDERS = [
|
|
3933
4210
|
"\u8BA9\u5B50\u5F39\u98DE\u4E00\u4F1A\u513F...",
|
|
3934
4211
|
"\u9A6C\u4E0A\u5C31\u597D...",
|
|
@@ -4007,6 +4284,13 @@ function ChatSession({
|
|
|
4007
4284
|
const [skillSelectIndex, setSkillSelectIndex] = useState3(0);
|
|
4008
4285
|
const [fileSelectIndex, setFileSelectIndex] = useState3(0);
|
|
4009
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);
|
|
4010
4294
|
const [selectingModel, setSelectingModel] = useState3(false);
|
|
4011
4295
|
const [modelSelectIndex, setModelSelectIndex] = useState3(0);
|
|
4012
4296
|
const modelOptions = ["deepseek-v4-flash", "deepseek-v4-pro"];
|
|
@@ -4023,6 +4307,14 @@ function ChatSession({
|
|
|
4023
4307
|
setSkillSelectIndex(0);
|
|
4024
4308
|
setFileSelectIndex(0);
|
|
4025
4309
|
}, [input]);
|
|
4310
|
+
useEffect3(() => {
|
|
4311
|
+
return () => {
|
|
4312
|
+
if (rewindHintTimerRef.current) {
|
|
4313
|
+
clearTimeout(rewindHintTimerRef.current);
|
|
4314
|
+
rewindHintTimerRef.current = null;
|
|
4315
|
+
}
|
|
4316
|
+
};
|
|
4317
|
+
}, []);
|
|
4026
4318
|
const getFilteredSkills = useCallback2(
|
|
4027
4319
|
(value) => {
|
|
4028
4320
|
const match = value.match(/(?:^|\s)\/([^/]*)$/);
|
|
@@ -4053,12 +4345,89 @@ function ChatSession({
|
|
|
4053
4345
|
},
|
|
4054
4346
|
[files]
|
|
4055
4347
|
);
|
|
4348
|
+
function rebuildDisplayFromSession(sess) {
|
|
4349
|
+
const next = [];
|
|
4350
|
+
for (const m of sess.messages) {
|
|
4351
|
+
if (m.role === "user") {
|
|
4352
|
+
next.push({ role: "user", content: m.content });
|
|
4353
|
+
} else if (m.role === "assistant") {
|
|
4354
|
+
next.push({
|
|
4355
|
+
role: "assistant",
|
|
4356
|
+
content: m.content,
|
|
4357
|
+
assistantDetail: {
|
|
4358
|
+
content: m.content,
|
|
4359
|
+
toolCalls: m.toolCalls
|
|
4360
|
+
}
|
|
4361
|
+
});
|
|
4362
|
+
} else if (m.role === "tool") {
|
|
4363
|
+
next.push({ role: "tool", content: m.content });
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
setDisplayMessages(next);
|
|
4367
|
+
setStaticKey((prev) => prev + 1);
|
|
4368
|
+
}
|
|
4369
|
+
const doRewind = useCallback2(async (target, displayNumber) => {
|
|
4370
|
+
const session = sessionRef.current;
|
|
4371
|
+
if (!session) return;
|
|
4372
|
+
setRewinding(true);
|
|
4373
|
+
setIsStreaming(true);
|
|
4374
|
+
setStreamingPhase("thinking");
|
|
4375
|
+
setStreamingPlaceholder("\u23EA \u6B63\u5728\u56DE\u9000\u5230\u68C0\u67E5\u70B9\u2026");
|
|
4376
|
+
setInput("");
|
|
4377
|
+
try {
|
|
4378
|
+
const r = await session.rewind(target.index);
|
|
4379
|
+
if (r.ok) {
|
|
4380
|
+
rebuildDisplayFromSession(session);
|
|
4381
|
+
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";
|
|
4382
|
+
setDisplayMessages((prev) => [
|
|
4383
|
+
...prev,
|
|
4384
|
+
{ role: "assistant", content: `\u23EA \u5DF2\u56DE\u9000\u5230\u68C0\u67E5\u70B9 #${displayNumber}${tail}\u3002` }
|
|
4385
|
+
]);
|
|
4386
|
+
} else {
|
|
4387
|
+
setDisplayMessages((prev) => [
|
|
4388
|
+
...prev,
|
|
4389
|
+
{ role: "assistant", content: `\u26A0 \u56DE\u9000\u5931\u8D25\uFF1A${r.error}` }
|
|
4390
|
+
]);
|
|
4391
|
+
}
|
|
4392
|
+
} catch (err) {
|
|
4393
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4394
|
+
setDisplayMessages((prev) => [
|
|
4395
|
+
...prev,
|
|
4396
|
+
{ role: "assistant", content: `\u26A0 \u56DE\u9000\u5F02\u5E38\uFF1A${msg}` }
|
|
4397
|
+
]);
|
|
4398
|
+
} finally {
|
|
4399
|
+
setRewinding(false);
|
|
4400
|
+
setIsStreaming(false);
|
|
4401
|
+
setStreamingPhase(null);
|
|
4402
|
+
setStreamingPlaceholder("");
|
|
4403
|
+
}
|
|
4404
|
+
}, []);
|
|
4056
4405
|
const { doubleCtrlC, handleCtrlC } = useDoubleCtrlC(() => {
|
|
4057
4406
|
process.exit(0);
|
|
4058
4407
|
});
|
|
4059
4408
|
useInput(
|
|
4060
4409
|
useCallback2(
|
|
4061
4410
|
(_input, key) => {
|
|
4411
|
+
if (rewindSelecting) {
|
|
4412
|
+
if (key.upArrow) {
|
|
4413
|
+
setRewindSelectIndex((prev) => (prev - 1 + rewindList.length) % rewindList.length);
|
|
4414
|
+
} else if (key.downArrow) {
|
|
4415
|
+
setRewindSelectIndex((prev) => (prev + 1) % rewindList.length);
|
|
4416
|
+
} else if (key.return) {
|
|
4417
|
+
const target = rewindList[rewindSelectIndex];
|
|
4418
|
+
setRewindSelecting(false);
|
|
4419
|
+
if (target) {
|
|
4420
|
+
void doRewind(target, rewindSelectIndex + 1);
|
|
4421
|
+
}
|
|
4422
|
+
} else if (key.escape) {
|
|
4423
|
+
setRewindSelecting(false);
|
|
4424
|
+
setDisplayMessages((prev) => [
|
|
4425
|
+
...prev,
|
|
4426
|
+
{ role: "assistant", content: "\u5DF2\u53D6\u6D88\u56DE\u9000\u3002" }
|
|
4427
|
+
]);
|
|
4428
|
+
}
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4062
4431
|
if (selectingModel) {
|
|
4063
4432
|
if (key.upArrow) {
|
|
4064
4433
|
setModelSelectIndex((prev) => (prev - 1 + modelOptions.length) % modelOptions.length);
|
|
@@ -4165,7 +4534,7 @@ function ChatSession({
|
|
|
4165
4534
|
setInput(_input);
|
|
4166
4535
|
}
|
|
4167
4536
|
},
|
|
4168
|
-
[selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode]
|
|
4537
|
+
[selectingModel, modelSelectIndex, modelOptions, activeModel, isStreaming, handleCtrlC, input, skills, skillSelectIndex, fileSelectIndex, getFilteredSkills, getFilteredFiles, sessionMode, setSessionMode, rewindSelecting, rewindSelectIndex, rewindList, doRewind]
|
|
4169
4538
|
)
|
|
4170
4539
|
);
|
|
4171
4540
|
useEffect3(() => {
|
|
@@ -4289,6 +4658,64 @@ function ChatSession({
|
|
|
4289
4658
|
if (!trimmed) return;
|
|
4290
4659
|
if (trimmed.startsWith("/") && trimmed.length > 1) {
|
|
4291
4660
|
const cmdLower = trimmed.toLowerCase();
|
|
4661
|
+
if (cmdLower === "/rewind" || cmdLower.startsWith("/rewind ")) {
|
|
4662
|
+
if (isStreaming || rewinding) {
|
|
4663
|
+
const reason = rewinding ? "\u56DE\u9000\u4E2D" : "\u751F\u6210\u4E2D";
|
|
4664
|
+
setDisplayMessages((prev) => [
|
|
4665
|
+
...prev,
|
|
4666
|
+
{ role: "user", content: trimmed },
|
|
4667
|
+
{ role: "assistant", content: `\u26A0 \u6B63\u5728${reason}\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5 /rewind\u3002` }
|
|
4668
|
+
]);
|
|
4669
|
+
setInput("");
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
if (!sessionRef.current) {
|
|
4673
|
+
setDisplayMessages((prev) => [
|
|
4674
|
+
...prev,
|
|
4675
|
+
{ role: "user", content: trimmed },
|
|
4676
|
+
{ role: "assistant", content: "\u26A0 Session \u672A\u5C31\u7EEA\uFF0C\u65E0\u6CD5\u56DE\u9000\u3002" }
|
|
4677
|
+
]);
|
|
4678
|
+
setInput("");
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
const cps = sessionRef.current.listCheckpoints();
|
|
4682
|
+
if (cps.length === 0) {
|
|
4683
|
+
setDisplayMessages((prev) => [
|
|
4684
|
+
...prev,
|
|
4685
|
+
{ role: "user", content: trimmed },
|
|
4686
|
+
{ 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" }
|
|
4687
|
+
]);
|
|
4688
|
+
setInput("");
|
|
4689
|
+
return;
|
|
4690
|
+
}
|
|
4691
|
+
const parts = trimmed.split(/\s+/);
|
|
4692
|
+
if (parts.length >= 2) {
|
|
4693
|
+
const n = Number(parts[1]);
|
|
4694
|
+
if (!Number.isInteger(n) || n < 1 || n > cps.length) {
|
|
4695
|
+
setDisplayMessages((prev) => [
|
|
4696
|
+
...prev,
|
|
4697
|
+
{ role: "user", content: trimmed },
|
|
4698
|
+
{ 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` }
|
|
4699
|
+
]);
|
|
4700
|
+
setInput("");
|
|
4701
|
+
return;
|
|
4702
|
+
}
|
|
4703
|
+
const target = cps[cps.length - n];
|
|
4704
|
+
setInput("");
|
|
4705
|
+
await doRewind(target, n);
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
setRewindList([...cps].reverse());
|
|
4709
|
+
setRewindSelectIndex(0);
|
|
4710
|
+
setRewindSelecting(true);
|
|
4711
|
+
setDisplayMessages((prev) => [
|
|
4712
|
+
...prev,
|
|
4713
|
+
{ role: "user", content: trimmed },
|
|
4714
|
+
{ 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` }
|
|
4715
|
+
]);
|
|
4716
|
+
setInput("");
|
|
4717
|
+
return;
|
|
4718
|
+
}
|
|
4292
4719
|
if (cmdLower === "/model") {
|
|
4293
4720
|
const curIdx = modelOptions.indexOf(activeModel);
|
|
4294
4721
|
setModelSelectIndex(curIdx >= 0 ? curIdx : 0);
|
|
@@ -4443,6 +4870,12 @@ function ChatSession({
|
|
|
4443
4870
|
currentCostRef.current = void 0;
|
|
4444
4871
|
currentModelRef.current = void 0;
|
|
4445
4872
|
streamErrorRef.current = void 0;
|
|
4873
|
+
currentRoundModifiedRef.current = false;
|
|
4874
|
+
setRewindHintPhase("idle");
|
|
4875
|
+
if (rewindHintTimerRef.current) {
|
|
4876
|
+
clearTimeout(rewindHintTimerRef.current);
|
|
4877
|
+
rewindHintTimerRef.current = null;
|
|
4878
|
+
}
|
|
4446
4879
|
const session = sessionRef.current;
|
|
4447
4880
|
const abortController = new AbortController();
|
|
4448
4881
|
abortRef.current = abortController;
|
|
@@ -4470,6 +4903,12 @@ function ChatSession({
|
|
|
4470
4903
|
currentToolCallsRef.current = next;
|
|
4471
4904
|
return next;
|
|
4472
4905
|
});
|
|
4906
|
+
for (const call of event.calls) {
|
|
4907
|
+
if (isFileMutatingTool(call.name)) {
|
|
4908
|
+
currentRoundModifiedRef.current = true;
|
|
4909
|
+
break;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4473
4912
|
break;
|
|
4474
4913
|
case "tool_result":
|
|
4475
4914
|
setStreamingPhase("executing_tools");
|
|
@@ -4540,6 +4979,14 @@ function ChatSession({
|
|
|
4540
4979
|
}
|
|
4541
4980
|
]);
|
|
4542
4981
|
}
|
|
4982
|
+
if (currentRoundModifiedRef.current && !finStreamError) {
|
|
4983
|
+
setRewindHintPhase("pending");
|
|
4984
|
+
if (rewindHintTimerRef.current) clearTimeout(rewindHintTimerRef.current);
|
|
4985
|
+
rewindHintTimerRef.current = setTimeout(() => {
|
|
4986
|
+
setRewindHintPhase((prev) => prev === "pending" ? "visible" : prev);
|
|
4987
|
+
rewindHintTimerRef.current = null;
|
|
4988
|
+
}, 2e3);
|
|
4989
|
+
}
|
|
4543
4990
|
}
|
|
4544
4991
|
}, [onLaunchGame, onLaunchStock, currentContent, currentToolCalls, skills, skillSelectIndex, getFilteredSkills, thinkingEnabled, thinkingEffort, responseFormat, toolChoice, activeModel, sessionMode]);
|
|
4545
4992
|
useEffect3(() => {
|
|
@@ -4685,6 +5132,38 @@ function ChatSession({
|
|
|
4685
5132
|
/* @__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
5133
|
] }),
|
|
4687
5134
|
/* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
|
|
5135
|
+
] }) : rewindSelecting ? /* @__PURE__ */ jsxs9(Box9, { marginTop: 1, flexDirection: "column", children: [
|
|
5136
|
+
/* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) }),
|
|
5137
|
+
/* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginTop: 1, children: [
|
|
5138
|
+
/* @__PURE__ */ jsx9(Text10, { bold: true, color: "#ff9800", children: "\u9009\u62E9\u8981\u56DE\u9000\u7684\u68C0\u67E5\u70B9\uFF1A" }),
|
|
5139
|
+
rewindList.map((cp2, i) => {
|
|
5140
|
+
const isSelected = i === rewindSelectIndex;
|
|
5141
|
+
const marker = isSelected ? " > " : " ";
|
|
5142
|
+
const time = new Date(cp2.timestamp).toLocaleTimeString();
|
|
5143
|
+
const preview = cp2.preview || "(\u7A7A)";
|
|
5144
|
+
const tag = cp2.isGitRepo ? "" : " [\u975E git\uFF0C\u4EC5\u5BF9\u8BDD]";
|
|
5145
|
+
return /* @__PURE__ */ jsx9(Box9, { children: /* @__PURE__ */ jsxs9(
|
|
5146
|
+
Text10,
|
|
5147
|
+
{
|
|
5148
|
+
color: isSelected ? "#ff9800" : void 0,
|
|
5149
|
+
bold: isSelected,
|
|
5150
|
+
children: [
|
|
5151
|
+
marker,
|
|
5152
|
+
"#",
|
|
5153
|
+
i + 1,
|
|
5154
|
+
" ",
|
|
5155
|
+
time,
|
|
5156
|
+
" `",
|
|
5157
|
+
preview,
|
|
5158
|
+
tag,
|
|
5159
|
+
"`"
|
|
5160
|
+
]
|
|
5161
|
+
}
|
|
5162
|
+
) }, cp2.index);
|
|
5163
|
+
}),
|
|
5164
|
+
/* @__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
|
+
] }),
|
|
5166
|
+
/* @__PURE__ */ jsx9(Text10, { color: "#00ffff", dimColor: true, children: "\u2500".repeat(dividerWidth) })
|
|
4688
5167
|
] }) : /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
4689
5168
|
(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
5169
|
PHASE_CONFIG[streamingPhase].icon,
|
|
@@ -4692,7 +5171,11 @@ function ChatSession({
|
|
|
4692
5171
|
PHASE_CONFIG[streamingPhase].label,
|
|
4693
5172
|
" ",
|
|
4694
5173
|
/* @__PURE__ */ jsx9(InkSpinner2, { type: "dots" })
|
|
4695
|
-
] }) }) :
|
|
5174
|
+
] }) }) : rewindHintPhase === "visible" ? (
|
|
5175
|
+
// 本轮修改了文件时,流式结束后 2s 在原位置展示 /rewind 提示
|
|
5176
|
+
// 一直保留到下次对话开始,与 /rewind 1(最新检查点)配套
|
|
5177
|
+
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsx9(Text10, { color: "#808080", children: "\u21A9 /rewind 1 \u53EF\u64A4\u56DE\u672C\u6B21\u4FEE\u6539" }) })
|
|
5178
|
+
) : null,
|
|
4696
5179
|
/* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: hasConversationStarted || sessionMode === "plan" ? /* @__PURE__ */ jsxs9(Text10, { color: sessionMode === "plan" ? "#ff69b4" : "#00ffff", dimColor: sessionMode !== "plan", children: [
|
|
4697
5180
|
/* @__PURE__ */ jsx9(Text10, { children: sessionMode === "plan" ? "\u2500".repeat(Math.max(dividerWidth - 48, 1)) : "\u2500".repeat(Math.max(dividerWidth - 35, 10)) }),
|
|
4698
5181
|
balance !== null && /* @__PURE__ */ jsxs9(Text10, { color: "yellow", children: [
|
|
@@ -5966,9 +6449,9 @@ import { Box as Box13, Text as Text14, useInput as useInput5 } from "ink";
|
|
|
5966
6449
|
import { useState as useState7, useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo2 } from "react";
|
|
5967
6450
|
import asciichart from "asciichart";
|
|
5968
6451
|
import os from "os";
|
|
5969
|
-
import { join as
|
|
6452
|
+
import { join as join8 } from "path";
|
|
5970
6453
|
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
5971
|
-
var SETTINGS_PATH =
|
|
6454
|
+
var SETTINGS_PATH = join8(os.homedir(), ".dskcode", "settings.json");
|
|
5972
6455
|
function toFileUrl(p) {
|
|
5973
6456
|
const norm = p.replace(/\\/g, "/");
|
|
5974
6457
|
if (process.platform === "win32") {
|
|
@@ -6302,8 +6785,8 @@ function renderDetail(stock, _onBack, prices, countdown = 10, currentTime) {
|
|
|
6302
6785
|
}
|
|
6303
6786
|
|
|
6304
6787
|
// src/utils/scan-files.ts
|
|
6305
|
-
import { readdir as
|
|
6306
|
-
import { join as
|
|
6788
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
6789
|
+
import { join as join9, relative as relative7 } from "path";
|
|
6307
6790
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
6308
6791
|
"node_modules",
|
|
6309
6792
|
".git",
|
|
@@ -6354,7 +6837,7 @@ async function scanProjectFiles(baseDir, dir) {
|
|
|
6354
6837
|
const currentDir = dir ?? baseDir;
|
|
6355
6838
|
let entries;
|
|
6356
6839
|
try {
|
|
6357
|
-
entries = await
|
|
6840
|
+
entries = await readdir6(currentDir, { withFileTypes: true });
|
|
6358
6841
|
} catch {
|
|
6359
6842
|
return [];
|
|
6360
6843
|
}
|
|
@@ -6368,12 +6851,12 @@ async function scanProjectFiles(baseDir, dir) {
|
|
|
6368
6851
|
} else if (entry.isFile()) {
|
|
6369
6852
|
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
6370
6853
|
if (SOURCE_EXTS.has(ext)) {
|
|
6371
|
-
files.push(relative7(baseDir,
|
|
6854
|
+
files.push(relative7(baseDir, join9(currentDir, name)));
|
|
6372
6855
|
}
|
|
6373
6856
|
}
|
|
6374
6857
|
}
|
|
6375
6858
|
const nested = await Promise.all(
|
|
6376
|
-
dirs.map((d) => scanProjectFiles(baseDir,
|
|
6859
|
+
dirs.map((d) => scanProjectFiles(baseDir, join9(currentDir, d)))
|
|
6377
6860
|
);
|
|
6378
6861
|
for (const n of nested) {
|
|
6379
6862
|
files.push(...n);
|
|
@@ -6461,10 +6944,10 @@ compdef _dskcode_completion dskcode`);
|
|
|
6461
6944
|
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
6945
|
const _ctx = this.dskcodeCtx;
|
|
6463
6946
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
6464
|
-
const globalConfigPath =
|
|
6947
|
+
const globalConfigPath = join10(home, ".dskcode", "settings.json");
|
|
6465
6948
|
let globalConfigHasStock = false;
|
|
6466
6949
|
try {
|
|
6467
|
-
const raw = await
|
|
6950
|
+
const raw = await readFile11(globalConfigPath, "utf-8");
|
|
6468
6951
|
const parsed = JSON.parse(raw);
|
|
6469
6952
|
const stock = parsed.stock;
|
|
6470
6953
|
globalConfigHasStock = Array.isArray(stock?.symbols) && stock.symbols.length > 0;
|
|
@@ -6629,10 +7112,18 @@ var ExitCode = {
|
|
|
6629
7112
|
// src/index.ts
|
|
6630
7113
|
var sigintCount = 0;
|
|
6631
7114
|
var sigintTimer = null;
|
|
7115
|
+
async function gracefulExit(code) {
|
|
7116
|
+
try {
|
|
7117
|
+
await CostTracker.flushAll();
|
|
7118
|
+
} catch {
|
|
7119
|
+
}
|
|
7120
|
+
process.exit(code);
|
|
7121
|
+
}
|
|
6632
7122
|
process.on("SIGINT", () => {
|
|
6633
7123
|
sigintCount++;
|
|
6634
7124
|
if (sigintCount >= 2) {
|
|
6635
|
-
|
|
7125
|
+
void gracefulExit(ExitCode.SIGINT);
|
|
7126
|
+
return;
|
|
6636
7127
|
}
|
|
6637
7128
|
process.stdout.write("\n \u26A0 \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA dskcode\n");
|
|
6638
7129
|
if (sigintTimer) clearTimeout(sigintTimer);
|
|
@@ -6640,18 +7131,25 @@ process.on("SIGINT", () => {
|
|
|
6640
7131
|
sigintCount = 0;
|
|
6641
7132
|
}, 1500);
|
|
6642
7133
|
});
|
|
7134
|
+
process.on("SIGTERM", () => {
|
|
7135
|
+
void gracefulExit(ExitCode.SIGINT);
|
|
7136
|
+
});
|
|
6643
7137
|
var program = createCli();
|
|
6644
7138
|
try {
|
|
6645
7139
|
await program.parseAsync(process.argv);
|
|
7140
|
+
await CostTracker.flushAll();
|
|
6646
7141
|
} catch (err) {
|
|
6647
7142
|
const error = err;
|
|
6648
7143
|
if (error.code === "commander.helpDisplayed" || error.code === "commander.version") {
|
|
7144
|
+
await CostTracker.flushAll();
|
|
6649
7145
|
process.exit(error.exitCode ?? ExitCode.SUCCESS);
|
|
6650
7146
|
}
|
|
6651
7147
|
if (typeof error.exitCode === "number") {
|
|
7148
|
+
await CostTracker.flushAll();
|
|
6652
7149
|
process.exit(error.exitCode);
|
|
6653
7150
|
}
|
|
6654
7151
|
console.error(String(err));
|
|
7152
|
+
await CostTracker.flushAll();
|
|
6655
7153
|
process.exit(ExitCode.GENERAL_ERROR);
|
|
6656
7154
|
}
|
|
6657
7155
|
//# sourceMappingURL=index.js.map
|