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/README.md +54 -16
- package/dist/index.js +596 -144
- 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";
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3108
|
+
await writeFile4(filePath, content, "utf-8");
|
|
2866
3109
|
}
|
|
2867
3110
|
|
|
2868
3111
|
// src/tool/builtins/read-file.ts
|
|
2869
|
-
import { readFile as
|
|
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
|
|
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
|
|
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
|
|
3255
|
+
oldContent = await readFile6(filePath, "utf-8");
|
|
3013
3256
|
existedBefore = true;
|
|
3014
3257
|
} catch {
|
|
3015
3258
|
}
|
|
3016
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3437
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 :
|
|
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
|
|
3535
|
-
import { join as
|
|
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
|
|
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 =
|
|
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 :
|
|
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
|
|
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
|
|
3656
|
-
import { join as
|
|
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
|
|
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(
|
|
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
|
-
] }) }) :
|
|
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
|
|
6421
|
+
import { join as join8 } from "path";
|
|
5970
6422
|
import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
5971
|
-
var SETTINGS_PATH =
|
|
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
|
|
6306
|
-
import { join as
|
|
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
|
|
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,
|
|
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,
|
|
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 =
|
|
6916
|
+
const globalConfigPath = join10(home, ".dskcode", "settings.json");
|
|
6465
6917
|
let globalConfigHasStock = false;
|
|
6466
6918
|
try {
|
|
6467
|
-
const raw = await
|
|
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;
|