farming-code 2.2.19 → 2.2.22
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 +5 -5
- package/README.zh_cn.md +5 -5
- package/THIRD_PARTY_NOTICES.md +5 -8
- package/backend/acp/client-services.js +37 -5
- package/backend/acp/patch-decisions.js +19 -3
- package/backend/acp-runtime.js +1201 -198
- package/backend/acp-transcript.js +66 -0
- package/backend/agent-lifecycle-journal.js +133 -0
- package/backend/agent-manager.js +2715 -354
- package/backend/agent-order.js +8 -4
- package/backend/agent-runtime-binding.js +1 -0
- package/backend/bundle-update-helper.js +5 -1
- package/backend/config-manager.js +16 -0
- package/backend/control-api.js +146 -20
- package/backend/farming-app-cli.js +335 -10
- package/backend/farming-session-store.js +283 -36
- package/backend/json-cli-runtime.js +120 -6
- package/backend/npm-update-helper.js +97 -18
- package/backend/provider-adapters.js +18 -6
- package/backend/review-session-service.js +31 -17
- package/backend/server.js +337 -103
- package/backend/storage-layout.js +10 -0
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/usage-monitor.js +11 -0
- package/backend/workspace-file-router.js +9 -3
- package/backend/workspace-file-service.js +292 -62
- package/dist/acp/codex-acp-1.1.4.js +32 -1
- package/dist/assets/App-CmZdNavW.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
- package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
- package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
- package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
- package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
- package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
- package/dist/assets/file-icons-CHWt5nSy.js +1 -0
- package/dist/assets/{index-DQdtPfCk.js → index-RvUiMpGP.js} +2 -2
- package/dist/assets/main-XMXgyJJt.css +1 -0
- package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +365 -107
- package/frontend/skins/crt/index.html +5 -10
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +9 -3
- package/dist/assets/App-CbXYBccJ.js +0 -210
- package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
- package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
- package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
- package/dist/assets/code-dark-qIBiLBWV.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-yzZEfgft.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
|
@@ -404,6 +404,17 @@ function metadataFileVersion(relativePath, stat) {
|
|
|
404
404
|
return sha1(Buffer.from(`${relativePath}:${stat.size}:${stat.mtimeMs}`, 'utf8'));
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
function workspaceEntryVersion(stat) {
|
|
408
|
+
return sha1(Buffer.from([
|
|
409
|
+
stat.dev,
|
|
410
|
+
stat.ino,
|
|
411
|
+
stat.mode,
|
|
412
|
+
stat.size,
|
|
413
|
+
stat.mtimeMs,
|
|
414
|
+
stat.ctimeMs,
|
|
415
|
+
].join(':'), 'utf8'));
|
|
416
|
+
}
|
|
417
|
+
|
|
407
418
|
async function readTextPrefix(target, maxBytes) {
|
|
408
419
|
const handle = await fsp.open(target, 'r');
|
|
409
420
|
try {
|
|
@@ -1122,7 +1133,12 @@ class WorkspaceFileService {
|
|
|
1122
1133
|
this.gitStatusInlineTimeoutMs = options.gitStatusInlineTimeoutMs ?? DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS;
|
|
1123
1134
|
this.gitStatusTimeoutMs = options.gitStatusTimeoutMs ?? DEFAULT_GIT_STATUS_TIMEOUT_MS;
|
|
1124
1135
|
this.gitStatusCache = new Map();
|
|
1136
|
+
this.mutationQueues = new Map();
|
|
1137
|
+
this.flushWorkspaceWrites = options.flushWorkspaceWrites !== false;
|
|
1125
1138
|
this.watchers = new Map();
|
|
1139
|
+
this.watcherSubscriptionGeneration = 0;
|
|
1140
|
+
this.watcherLifecycleGeneration = 0;
|
|
1141
|
+
this.disposed = false;
|
|
1126
1142
|
this.watchOptions = options.watchOptions || {};
|
|
1127
1143
|
this.watchDepth = Number.isFinite(options.watchDepth) ? Math.max(0, options.watchDepth) : DEFAULT_WATCH_DEPTH;
|
|
1128
1144
|
this.commandRunner = options.commandRunner || new CommandRunner(options.commandRunnerOptions);
|
|
@@ -1136,6 +1152,33 @@ class WorkspaceFileService {
|
|
|
1136
1152
|
});
|
|
1137
1153
|
}
|
|
1138
1154
|
|
|
1155
|
+
async runWorkspaceMutation(workspaceRoot, operation) {
|
|
1156
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1157
|
+
const previous = this.mutationQueues.get(root) || Promise.resolve();
|
|
1158
|
+
let release;
|
|
1159
|
+
const current = new Promise(resolve => {
|
|
1160
|
+
release = resolve;
|
|
1161
|
+
});
|
|
1162
|
+
const tail = previous.then(() => current);
|
|
1163
|
+
this.mutationQueues.set(root, tail);
|
|
1164
|
+
await previous;
|
|
1165
|
+
try {
|
|
1166
|
+
return await operation(root);
|
|
1167
|
+
} finally {
|
|
1168
|
+
release();
|
|
1169
|
+
if (this.mutationQueues.get(root) === tail) {
|
|
1170
|
+
this.mutationQueues.delete(root);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
async waitForWorkspaceMutations(workspaceRoot) {
|
|
1176
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1177
|
+
const pending = this.mutationQueues.get(root);
|
|
1178
|
+
if (pending) await pending;
|
|
1179
|
+
return root;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1139
1182
|
async execRipgrep(args, options = {}) {
|
|
1140
1183
|
try {
|
|
1141
1184
|
return await this.execFile(this.rgPath, args, options);
|
|
@@ -1618,7 +1661,8 @@ class WorkspaceFileService {
|
|
|
1618
1661
|
}
|
|
1619
1662
|
|
|
1620
1663
|
async listTree(workspaceRoot, userPath = '', options = {}) {
|
|
1621
|
-
const
|
|
1664
|
+
const root = await this.waitForWorkspaceMutations(workspaceRoot);
|
|
1665
|
+
const { target, relativePath, external: parentExternal = false } = await this.resolvePath(root, userPath, options);
|
|
1622
1666
|
const stat = await fsp.stat(target);
|
|
1623
1667
|
if (!stat.isDirectory()) {
|
|
1624
1668
|
throw new WorkspaceFileError('path must be a directory', 400);
|
|
@@ -1679,6 +1723,7 @@ class WorkspaceFileService {
|
|
|
1679
1723
|
type,
|
|
1680
1724
|
size: targetStat.size,
|
|
1681
1725
|
mtimeMs: targetStat.mtimeMs,
|
|
1726
|
+
version: workspaceEntryVersion(entryStat),
|
|
1682
1727
|
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
1683
1728
|
...(external ? { external: true } : {}),
|
|
1684
1729
|
...(readOnly ? { readOnly: true } : {}),
|
|
@@ -2198,6 +2243,25 @@ class WorkspaceFileService {
|
|
|
2198
2243
|
}
|
|
2199
2244
|
|
|
2200
2245
|
async writeFile(workspaceRoot, userPath, content, options = {}) {
|
|
2246
|
+
return this.runWorkspaceMutation(workspaceRoot, root => (
|
|
2247
|
+
this.writeFileUnlocked(root, userPath, content, options)
|
|
2248
|
+
));
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
async flushWorkspaceFileHandle(handle) {
|
|
2252
|
+
if (!this.flushWorkspaceWrites) return;
|
|
2253
|
+
try {
|
|
2254
|
+
await handle.datasync();
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
if (!['EINVAL', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP'].includes(error?.code)) {
|
|
2257
|
+
throw error;
|
|
2258
|
+
}
|
|
2259
|
+
this.flushWorkspaceWrites = false;
|
|
2260
|
+
console.warn('Workspace file datasync is unsupported; disabling it for this server process:', error);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
async writeFileUnlocked(workspaceRoot, userPath, content, options = {}) {
|
|
2201
2265
|
if (typeof content !== 'string') {
|
|
2202
2266
|
throw new WorkspaceFileError('content must be a string', 400);
|
|
2203
2267
|
}
|
|
@@ -2211,9 +2275,13 @@ class WorkspaceFileService {
|
|
|
2211
2275
|
const overwrite = options.overwrite === true;
|
|
2212
2276
|
let currentMode = 0o666;
|
|
2213
2277
|
let currentSha1 = '';
|
|
2278
|
+
let currentStat = null;
|
|
2214
2279
|
let exists = false;
|
|
2280
|
+
let symbolicLink = false;
|
|
2215
2281
|
|
|
2216
2282
|
try {
|
|
2283
|
+
const requestedStat = await fsp.lstat(target);
|
|
2284
|
+
symbolicLink = requestedStat.isSymbolicLink();
|
|
2217
2285
|
const realTarget = await fsp.realpath(target);
|
|
2218
2286
|
if (!isInside(root, realTarget)) {
|
|
2219
2287
|
throw new WorkspaceFileError('symlinks outside the workspace are not allowed', 403);
|
|
@@ -2225,26 +2293,37 @@ class WorkspaceFileService {
|
|
|
2225
2293
|
}
|
|
2226
2294
|
|
|
2227
2295
|
try {
|
|
2228
|
-
|
|
2229
|
-
if (!
|
|
2296
|
+
currentStat = await fsp.stat(writeTarget);
|
|
2297
|
+
if (!currentStat.isFile()) {
|
|
2230
2298
|
throw new WorkspaceFileError('path must be a file', 400);
|
|
2231
2299
|
}
|
|
2232
|
-
if (
|
|
2233
|
-
throw new WorkspaceFileError('existing file is too large to overwrite safely', 413, { size:
|
|
2300
|
+
if (currentStat.size > this.maxFileSize) {
|
|
2301
|
+
throw new WorkspaceFileError('existing file is too large to overwrite safely', 413, { size: currentStat.size });
|
|
2234
2302
|
}
|
|
2235
2303
|
if (await isProbablyBinaryFile(writeTarget)) {
|
|
2236
2304
|
throw new WorkspaceFileError('binary files cannot be overwritten as text', 415);
|
|
2237
2305
|
}
|
|
2238
2306
|
const currentBuffer = await fsp.readFile(writeTarget);
|
|
2239
2307
|
currentSha1 = sha1(currentBuffer);
|
|
2240
|
-
currentMode =
|
|
2308
|
+
currentMode = currentStat.mode;
|
|
2241
2309
|
exists = true;
|
|
2242
2310
|
} catch (error) {
|
|
2243
2311
|
if (error instanceof WorkspaceFileError) throw error;
|
|
2244
2312
|
if (error.code !== 'ENOENT') throw error;
|
|
2245
2313
|
}
|
|
2246
2314
|
|
|
2315
|
+
const desiredSha1 = sha1(Buffer.from(content, 'utf8'));
|
|
2247
2316
|
if (baseSha1 && currentSha1 && baseSha1 !== currentSha1 && !overwrite) {
|
|
2317
|
+
if (currentSha1 === desiredSha1) {
|
|
2318
|
+
return {
|
|
2319
|
+
path: relativePath,
|
|
2320
|
+
content,
|
|
2321
|
+
size: Buffer.byteLength(content, 'utf8'),
|
|
2322
|
+
mtimeMs: currentStat.mtimeMs,
|
|
2323
|
+
sha1: desiredSha1,
|
|
2324
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2248
2327
|
throw new WorkspaceFileError('file changed on disk', 409, {
|
|
2249
2328
|
path: relativePath,
|
|
2250
2329
|
currentSha1,
|
|
@@ -2256,16 +2335,48 @@ class WorkspaceFileService {
|
|
|
2256
2335
|
|
|
2257
2336
|
const tempPath = path.join(
|
|
2258
2337
|
path.dirname(writeTarget),
|
|
2259
|
-
`.${path.basename(writeTarget)}.farming-${process.pid}-${
|
|
2338
|
+
`.${path.basename(writeTarget)}.farming-${process.pid}-${crypto.randomUUID()}.tmp`
|
|
2260
2339
|
);
|
|
2261
|
-
|
|
2262
|
-
|
|
2340
|
+
let tempHandle = null;
|
|
2341
|
+
let committedStat = null;
|
|
2342
|
+
try {
|
|
2343
|
+
tempHandle = await fsp.open(tempPath, 'wx', currentMode);
|
|
2344
|
+
await tempHandle.writeFile(content);
|
|
2345
|
+
await this.flushWorkspaceFileHandle(tempHandle);
|
|
2346
|
+
committedStat = await tempHandle.stat();
|
|
2347
|
+
await tempHandle.close();
|
|
2348
|
+
tempHandle = null;
|
|
2349
|
+
await fsp.rename(tempPath, writeTarget);
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
if (tempHandle) {
|
|
2352
|
+
await tempHandle.close().catch(() => {});
|
|
2353
|
+
}
|
|
2354
|
+
await fsp.unlink(tempPath).catch(unlinkError => {
|
|
2355
|
+
if (unlinkError.code !== 'ENOENT') {
|
|
2356
|
+
console.error('Failed to clean up workspace file temporary:', unlinkError);
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
throw error;
|
|
2360
|
+
}
|
|
2263
2361
|
this.invalidateGitStatus(root);
|
|
2264
2362
|
|
|
2265
|
-
return
|
|
2363
|
+
return {
|
|
2364
|
+
path: relativePath,
|
|
2365
|
+
content,
|
|
2366
|
+
size: Buffer.byteLength(content, 'utf8'),
|
|
2367
|
+
mtimeMs: committedStat.mtimeMs,
|
|
2368
|
+
sha1: desiredSha1,
|
|
2369
|
+
...(symbolicLink ? { symbolicLink: true } : {}),
|
|
2370
|
+
};
|
|
2266
2371
|
}
|
|
2267
2372
|
|
|
2268
|
-
async moveEntry(workspaceRoot, sourcePath, targetDirectory = '') {
|
|
2373
|
+
async moveEntry(workspaceRoot, sourcePath, targetDirectory = '', options = {}) {
|
|
2374
|
+
return this.runWorkspaceMutation(workspaceRoot, root => (
|
|
2375
|
+
this.moveEntryUnlocked(root, sourcePath, targetDirectory, options)
|
|
2376
|
+
));
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
async moveEntryUnlocked(workspaceRoot, sourcePath, targetDirectory = '', options = {}) {
|
|
2269
2380
|
const root = await this.resolveRoot(workspaceRoot);
|
|
2270
2381
|
const normalizedSource = normalizeUserPath(sourcePath);
|
|
2271
2382
|
const normalizedTargetDirectory = normalizeUserPath(targetDirectory);
|
|
@@ -2310,6 +2421,13 @@ class WorkspaceFileService {
|
|
|
2310
2421
|
}
|
|
2311
2422
|
|
|
2312
2423
|
const sourceStat = await fsp.lstat(source.target);
|
|
2424
|
+
const sourceVersion = workspaceEntryVersion(sourceStat);
|
|
2425
|
+
if (options.expectedVersion && options.expectedVersion !== sourceVersion) {
|
|
2426
|
+
throw new WorkspaceFileError('source changed on disk', 409, {
|
|
2427
|
+
path: source.relativePath,
|
|
2428
|
+
currentVersion: sourceVersion,
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2313
2431
|
if (sourceStat.isDirectory() && isInside(source.target, target)) {
|
|
2314
2432
|
throw new WorkspaceFileError('directory cannot be moved into itself', 400);
|
|
2315
2433
|
}
|
|
@@ -2323,6 +2441,7 @@ class WorkspaceFileService {
|
|
|
2323
2441
|
}
|
|
2324
2442
|
|
|
2325
2443
|
await fsp.rename(source.target, target);
|
|
2444
|
+
const targetStat = await fsp.lstat(target);
|
|
2326
2445
|
this.invalidateGitStatus(root);
|
|
2327
2446
|
|
|
2328
2447
|
return {
|
|
@@ -2330,10 +2449,18 @@ class WorkspaceFileService {
|
|
|
2330
2449
|
targetPath,
|
|
2331
2450
|
sourceDirectory: parentDirectory(source.relativePath),
|
|
2332
2451
|
targetDirectory: normalizedTargetDirectory,
|
|
2452
|
+
sourceVersion,
|
|
2453
|
+
targetVersion: workspaceEntryVersion(targetStat),
|
|
2333
2454
|
};
|
|
2334
2455
|
}
|
|
2335
2456
|
|
|
2336
2457
|
async createEntry(workspaceRoot, parentPath = '', name, type = 'file', content = '') {
|
|
2458
|
+
return this.runWorkspaceMutation(workspaceRoot, root => (
|
|
2459
|
+
this.createEntryUnlocked(root, parentPath, name, type, content)
|
|
2460
|
+
));
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
async createEntryUnlocked(workspaceRoot, parentPath = '', name, type = 'file', content = '') {
|
|
2337
2464
|
const root = await this.resolveRoot(workspaceRoot);
|
|
2338
2465
|
const normalizedParentPath = normalizeUserPath(parentPath);
|
|
2339
2466
|
const entryName = normalizeEntryName(name);
|
|
@@ -2368,43 +2495,59 @@ class WorkspaceFileService {
|
|
|
2368
2495
|
throw new WorkspaceFileError('ignored paths cannot be changed', 403);
|
|
2369
2496
|
}
|
|
2370
2497
|
|
|
2371
|
-
try {
|
|
2372
|
-
await fsp.lstat(target);
|
|
2373
|
-
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
2374
|
-
} catch (error) {
|
|
2375
|
-
if (error instanceof WorkspaceFileError) throw error;
|
|
2376
|
-
if (error.code !== 'ENOENT') throw error;
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
2498
|
if (entryType === 'directory') {
|
|
2380
|
-
|
|
2499
|
+
try {
|
|
2500
|
+
await fsp.mkdir(target);
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
if (error.code === 'EEXIST') {
|
|
2503
|
+
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
2504
|
+
}
|
|
2505
|
+
throw error;
|
|
2506
|
+
}
|
|
2507
|
+
const stat = await fsp.lstat(target);
|
|
2381
2508
|
this.invalidateGitStatus(root);
|
|
2382
2509
|
return {
|
|
2383
2510
|
entry: {
|
|
2384
2511
|
name: entryName,
|
|
2385
2512
|
path: targetPath,
|
|
2386
2513
|
type: 'directory',
|
|
2387
|
-
size:
|
|
2388
|
-
mtimeMs:
|
|
2514
|
+
size: stat.size,
|
|
2515
|
+
mtimeMs: stat.mtimeMs,
|
|
2516
|
+
version: workspaceEntryVersion(stat),
|
|
2389
2517
|
},
|
|
2390
2518
|
};
|
|
2391
2519
|
}
|
|
2392
2520
|
|
|
2393
|
-
|
|
2521
|
+
try {
|
|
2522
|
+
await fsp.writeFile(target, String(content || ''), { flag: 'wx' });
|
|
2523
|
+
} catch (error) {
|
|
2524
|
+
if (error.code === 'EEXIST') {
|
|
2525
|
+
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
2526
|
+
}
|
|
2527
|
+
throw error;
|
|
2528
|
+
}
|
|
2529
|
+
const stat = await fsp.lstat(target);
|
|
2394
2530
|
this.invalidateGitStatus(root);
|
|
2395
2531
|
return {
|
|
2396
2532
|
entry: {
|
|
2397
2533
|
name: entryName,
|
|
2398
2534
|
path: targetPath,
|
|
2399
2535
|
type: 'file',
|
|
2400
|
-
size:
|
|
2401
|
-
mtimeMs:
|
|
2536
|
+
size: stat.size,
|
|
2537
|
+
mtimeMs: stat.mtimeMs,
|
|
2538
|
+
version: workspaceEntryVersion(stat),
|
|
2402
2539
|
},
|
|
2403
2540
|
file: await this.readFile(workspaceRoot, targetPath),
|
|
2404
2541
|
};
|
|
2405
2542
|
}
|
|
2406
2543
|
|
|
2407
|
-
async renameEntry(workspaceRoot, sourcePath, name) {
|
|
2544
|
+
async renameEntry(workspaceRoot, sourcePath, name, options = {}) {
|
|
2545
|
+
return this.runWorkspaceMutation(workspaceRoot, root => (
|
|
2546
|
+
this.renameEntryUnlocked(root, sourcePath, name, options)
|
|
2547
|
+
));
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
async renameEntryUnlocked(workspaceRoot, sourcePath, name, options = {}) {
|
|
2408
2551
|
const root = await this.resolveRoot(workspaceRoot);
|
|
2409
2552
|
const normalizedSource = normalizeUserPath(sourcePath);
|
|
2410
2553
|
const entryName = normalizeEntryName(name);
|
|
@@ -2432,6 +2575,15 @@ class WorkspaceFileService {
|
|
|
2432
2575
|
};
|
|
2433
2576
|
}
|
|
2434
2577
|
|
|
2578
|
+
const sourceStat = await fsp.lstat(source.target);
|
|
2579
|
+
const sourceVersion = workspaceEntryVersion(sourceStat);
|
|
2580
|
+
if (options.expectedVersion && options.expectedVersion !== sourceVersion) {
|
|
2581
|
+
throw new WorkspaceFileError('source changed on disk', 409, {
|
|
2582
|
+
path: source.relativePath,
|
|
2583
|
+
currentVersion: sourceVersion,
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2435
2587
|
try {
|
|
2436
2588
|
await fsp.lstat(target);
|
|
2437
2589
|
throw new WorkspaceFileError('target path already exists', 409, { path: targetPath });
|
|
@@ -2441,6 +2593,7 @@ class WorkspaceFileService {
|
|
|
2441
2593
|
}
|
|
2442
2594
|
|
|
2443
2595
|
await fsp.rename(source.target, target);
|
|
2596
|
+
const targetStat = await fsp.lstat(target);
|
|
2444
2597
|
this.invalidateGitStatus(root);
|
|
2445
2598
|
|
|
2446
2599
|
return {
|
|
@@ -2448,10 +2601,18 @@ class WorkspaceFileService {
|
|
|
2448
2601
|
targetPath,
|
|
2449
2602
|
sourceDirectory: parentDirectory(source.relativePath),
|
|
2450
2603
|
targetDirectory: parentDirectory(targetPath),
|
|
2604
|
+
sourceVersion,
|
|
2605
|
+
targetVersion: workspaceEntryVersion(targetStat),
|
|
2451
2606
|
};
|
|
2452
2607
|
}
|
|
2453
2608
|
|
|
2454
|
-
async deleteEntry(workspaceRoot, userPath) {
|
|
2609
|
+
async deleteEntry(workspaceRoot, userPath, options = {}) {
|
|
2610
|
+
return this.runWorkspaceMutation(workspaceRoot, root => (
|
|
2611
|
+
this.deleteEntryUnlocked(root, userPath, options)
|
|
2612
|
+
));
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
async deleteEntryUnlocked(workspaceRoot, userPath, options = {}) {
|
|
2455
2616
|
const { root, target, relativePath } = await this.resolveEntryPath(workspaceRoot, userPath);
|
|
2456
2617
|
if (!relativePath) {
|
|
2457
2618
|
throw new WorkspaceFileError('workspace root cannot be deleted', 400);
|
|
@@ -2461,6 +2622,13 @@ class WorkspaceFileService {
|
|
|
2461
2622
|
}
|
|
2462
2623
|
|
|
2463
2624
|
const stat = await fsp.lstat(target);
|
|
2625
|
+
const version = workspaceEntryVersion(stat);
|
|
2626
|
+
if (options.expectedVersion && options.expectedVersion !== version) {
|
|
2627
|
+
throw new WorkspaceFileError('entry changed on disk', 409, {
|
|
2628
|
+
path: relativePath,
|
|
2629
|
+
currentVersion: version,
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2464
2632
|
const type = stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : stat.isSymbolicLink() ? 'symlink' : 'other';
|
|
2465
2633
|
if (stat.isDirectory()) {
|
|
2466
2634
|
await fsp.rm(target, { recursive: true, force: false });
|
|
@@ -2473,6 +2641,7 @@ class WorkspaceFileService {
|
|
|
2473
2641
|
path: relativePath,
|
|
2474
2642
|
parentDirectory: parentDirectory(relativePath),
|
|
2475
2643
|
type,
|
|
2644
|
+
version,
|
|
2476
2645
|
};
|
|
2477
2646
|
}
|
|
2478
2647
|
|
|
@@ -2975,12 +3144,17 @@ class WorkspaceFileService {
|
|
|
2975
3144
|
}
|
|
2976
3145
|
|
|
2977
3146
|
async subscribe(workspaceRoot, callback) {
|
|
3147
|
+
if (this.disposed) return async () => {};
|
|
3148
|
+
const lifecycleGeneration = this.watcherLifecycleGeneration;
|
|
2978
3149
|
const root = await this.resolveRoot(workspaceRoot);
|
|
3150
|
+
if (this.disposed || this.watcherLifecycleGeneration !== lifecycleGeneration) return async () => {};
|
|
2979
3151
|
const watchRoot = path.resolve(workspaceRoot);
|
|
2980
3152
|
let record = this.watchers.get(root);
|
|
2981
3153
|
|
|
2982
3154
|
if (!record) {
|
|
2983
3155
|
const subscribers = new Set();
|
|
3156
|
+
const generation = this.watcherSubscriptionGeneration + 1;
|
|
3157
|
+
this.watcherSubscriptionGeneration = generation;
|
|
2984
3158
|
|
|
2985
3159
|
const emit = (eventType, absolutePath) => {
|
|
2986
3160
|
const relative = relativeFromRoot(watchRoot, absolutePath);
|
|
@@ -3003,56 +3177,112 @@ class WorkspaceFileService {
|
|
|
3003
3177
|
});
|
|
3004
3178
|
};
|
|
3005
3179
|
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
pattern instanceof RegExp ? pattern.test(candidatePath) : String(candidatePath).includes(String(pattern))
|
|
3015
|
-
));
|
|
3016
|
-
}
|
|
3017
|
-
return false;
|
|
3180
|
+
record = {
|
|
3181
|
+
watcher: null,
|
|
3182
|
+
subscribers,
|
|
3183
|
+
generation,
|
|
3184
|
+
cancelled: false,
|
|
3185
|
+
cancelInitialization: null,
|
|
3186
|
+
closePromise: null,
|
|
3187
|
+
ready: null,
|
|
3018
3188
|
};
|
|
3019
|
-
const chokidar = await loadChokidar();
|
|
3020
|
-
const watcher = chokidar.watch(watchRoot, {
|
|
3021
|
-
ignoreInitial: true,
|
|
3022
|
-
depth: this.watchDepth,
|
|
3023
|
-
...this.watchOptions,
|
|
3024
|
-
ignored,
|
|
3025
|
-
});
|
|
3026
|
-
|
|
3027
|
-
['add', 'change', 'unlink', 'addDir', 'unlinkDir'].forEach((eventType) => {
|
|
3028
|
-
watcher.on(eventType, filePath => emit(eventType, filePath));
|
|
3029
|
-
});
|
|
3030
|
-
watcher.on('error', emitError);
|
|
3031
|
-
const ready = new Promise(resolve => watcher.once('ready', resolve));
|
|
3032
|
-
|
|
3033
|
-
record = { watcher, subscribers, ready };
|
|
3034
3189
|
this.watchers.set(root, record);
|
|
3190
|
+
record.ready = (async () => {
|
|
3191
|
+
const configuredIgnored = this.watchOptions.ignored;
|
|
3192
|
+
const ignored = (candidatePath) => {
|
|
3193
|
+
const relative = path.relative(watchRoot, candidatePath);
|
|
3194
|
+
if (shouldHidePath(relative)) return true;
|
|
3195
|
+
if (typeof configuredIgnored === 'function') return configuredIgnored(candidatePath);
|
|
3196
|
+
if (configuredIgnored instanceof RegExp) return configuredIgnored.test(candidatePath);
|
|
3197
|
+
if (Array.isArray(configuredIgnored)) {
|
|
3198
|
+
return configuredIgnored.some(pattern => (
|
|
3199
|
+
pattern instanceof RegExp ? pattern.test(candidatePath) : String(candidatePath).includes(String(pattern))
|
|
3200
|
+
));
|
|
3201
|
+
}
|
|
3202
|
+
return false;
|
|
3203
|
+
};
|
|
3204
|
+
const chokidar = await loadChokidar();
|
|
3205
|
+
if (
|
|
3206
|
+
record.cancelled
|
|
3207
|
+
|| this.watchers.get(root)?.generation !== generation
|
|
3208
|
+
) return false;
|
|
3209
|
+
const watcher = chokidar.watch(watchRoot, {
|
|
3210
|
+
ignoreInitial: true,
|
|
3211
|
+
depth: this.watchDepth,
|
|
3212
|
+
...this.watchOptions,
|
|
3213
|
+
ignored,
|
|
3214
|
+
});
|
|
3215
|
+
record.watcher = watcher;
|
|
3216
|
+
|
|
3217
|
+
['add', 'change', 'unlink', 'addDir', 'unlinkDir'].forEach((eventType) => {
|
|
3218
|
+
watcher.on(eventType, filePath => emit(eventType, filePath));
|
|
3219
|
+
});
|
|
3220
|
+
watcher.on('error', emitError);
|
|
3221
|
+
const initialized = await new Promise(resolve => {
|
|
3222
|
+
let settled = false;
|
|
3223
|
+
const finish = value => {
|
|
3224
|
+
if (settled) return;
|
|
3225
|
+
settled = true;
|
|
3226
|
+
resolve(value);
|
|
3227
|
+
};
|
|
3228
|
+
watcher.once('ready', () => finish(true));
|
|
3229
|
+
record.cancelInitialization = () => finish(false);
|
|
3230
|
+
});
|
|
3231
|
+
record.cancelInitialization = null;
|
|
3232
|
+
return initialized
|
|
3233
|
+
&& !record.cancelled
|
|
3234
|
+
&& this.watchers.get(root)?.generation === generation;
|
|
3235
|
+
})();
|
|
3035
3236
|
}
|
|
3036
3237
|
|
|
3037
|
-
await record.ready;
|
|
3038
3238
|
record.subscribers.add(callback);
|
|
3039
|
-
|
|
3239
|
+
let initialized = false;
|
|
3240
|
+
try {
|
|
3241
|
+
initialized = await record.ready;
|
|
3242
|
+
} catch (error) {
|
|
3040
3243
|
record.subscribers.delete(callback);
|
|
3041
|
-
if (record.subscribers.size === 0) {
|
|
3244
|
+
if (record.subscribers.size === 0 && this.watchers.get(root)?.generation === record.generation) {
|
|
3042
3245
|
this.watchers.delete(root);
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3246
|
+
await this.closeWorkspaceWatcherRecord(record);
|
|
3247
|
+
}
|
|
3248
|
+
throw error;
|
|
3249
|
+
}
|
|
3250
|
+
if (!initialized) {
|
|
3251
|
+
record.subscribers.delete(callback);
|
|
3252
|
+
return async () => {};
|
|
3253
|
+
}
|
|
3254
|
+
return async () => {
|
|
3255
|
+
if (!record.subscribers.delete(callback)) return;
|
|
3256
|
+
if (
|
|
3257
|
+
record.subscribers.size === 0
|
|
3258
|
+
&& this.watchers.get(root)?.generation === record.generation
|
|
3259
|
+
) {
|
|
3260
|
+
this.watchers.delete(root);
|
|
3261
|
+
await this.closeWorkspaceWatcherRecord(record);
|
|
3047
3262
|
}
|
|
3048
3263
|
};
|
|
3049
3264
|
}
|
|
3050
3265
|
|
|
3266
|
+
async closeWorkspaceWatcherRecord(record) {
|
|
3267
|
+
if (record.closePromise) return record.closePromise;
|
|
3268
|
+
record.cancelled = true;
|
|
3269
|
+
record.cancelInitialization?.();
|
|
3270
|
+
record.closePromise = (async () => {
|
|
3271
|
+
await record.ready?.catch(() => false);
|
|
3272
|
+
if (!record.watcher) return;
|
|
3273
|
+
const closeResult = record.watcher.close();
|
|
3274
|
+
if (closeResult && typeof closeResult.then === 'function') await closeResult;
|
|
3275
|
+
})();
|
|
3276
|
+
return record.closePromise;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3051
3279
|
async dispose() {
|
|
3052
3280
|
const watchers = Array.from(this.watchers.values());
|
|
3053
3281
|
this.watchers.clear();
|
|
3282
|
+
this.disposed = true;
|
|
3283
|
+
this.watcherLifecycleGeneration += 1;
|
|
3054
3284
|
this.gitStatusCache.clear();
|
|
3055
|
-
await Promise.all(watchers.map(record =>
|
|
3285
|
+
await Promise.all(watchers.map(record => this.closeWorkspaceWatcherRecord(record)));
|
|
3056
3286
|
if (this.ownsCommandRunner) {
|
|
3057
3287
|
this.commandRunner.dispose();
|
|
3058
3288
|
}
|
|
@@ -26066,6 +26066,17 @@ var CodexAcpClient = class {
|
|
|
26066
26066
|
additionalDirectories
|
|
26067
26067
|
};
|
|
26068
26068
|
}
|
|
26069
|
+
async forkSession(request) {
|
|
26070
|
+
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
|
|
26071
|
+
await this.refreshSkills(request.cwd, additionalDirectories);
|
|
26072
|
+
const response = await this.codexClient.threadFork({
|
|
26073
|
+
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
|
|
26074
|
+
cwd: request.cwd,
|
|
26075
|
+
modelProvider: await this.getResumeModelProvider(),
|
|
26076
|
+
threadId: request.sessionId
|
|
26077
|
+
});
|
|
26078
|
+
return { sessionId: response.thread.id };
|
|
26079
|
+
}
|
|
26069
26080
|
async newSession(request) {
|
|
26070
26081
|
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
|
|
26071
26082
|
await this.refreshSkills(request.cwd, additionalDirectories);
|
|
@@ -28211,6 +28222,7 @@ var CodexAcpServer = class _CodexAcpServer {
|
|
|
28211
28222
|
sessionCapabilities: {
|
|
28212
28223
|
resume: {},
|
|
28213
28224
|
list: {},
|
|
28225
|
+
fork: {},
|
|
28214
28226
|
close: {},
|
|
28215
28227
|
delete: {},
|
|
28216
28228
|
additionalDirectories: {}
|
|
@@ -28512,6 +28524,22 @@ You have been logged out. Please try again.`);
|
|
|
28512
28524
|
...this.createSessionConfigOptionsResponse(this.getSessionState(sessionId))
|
|
28513
28525
|
};
|
|
28514
28526
|
}
|
|
28527
|
+
async unstable_forkSession(params) {
|
|
28528
|
+
logger.log("Forking session...", { sessionId: params.sessionId });
|
|
28529
|
+
const sessionState = this.sessions.get(params.sessionId);
|
|
28530
|
+
if (!sessionState) {
|
|
28531
|
+
throw RequestError.invalidParams(void 0, `Unknown session: ${params.sessionId}`);
|
|
28532
|
+
}
|
|
28533
|
+
if (this.activePrompts.has(sessionState.sessionId)) {
|
|
28534
|
+
throw RequestError.invalidRequest({ details: "Cannot fork an active Codex turn" });
|
|
28535
|
+
}
|
|
28536
|
+
const response = await this.runWithProcessCheck(() => this.codexAcpClient.forkSession(params));
|
|
28537
|
+
logger.log("Session forked", {
|
|
28538
|
+
sourceSessionId: params.sessionId,
|
|
28539
|
+
sessionId: response.sessionId
|
|
28540
|
+
});
|
|
28541
|
+
return response;
|
|
28542
|
+
}
|
|
28515
28543
|
async resumeSession(params) {
|
|
28516
28544
|
logger.log("Resuming session...", { sessionId: params.sessionId });
|
|
28517
28545
|
const [sessionId, modelState, modeState] = await this.getOrCreateSession(params);
|
|
@@ -30074,6 +30102,9 @@ var CodexAppServerClient = class {
|
|
|
30074
30102
|
async threadResume(params) {
|
|
30075
30103
|
return await this.sendRequest({ method: "thread/resume", params });
|
|
30076
30104
|
}
|
|
30105
|
+
async threadFork(params) {
|
|
30106
|
+
return await this.sendRequest({ method: "thread/fork", params });
|
|
30107
|
+
}
|
|
30077
30108
|
getThreadSettings(threadId) {
|
|
30078
30109
|
return this.threadSettings.get(threadId);
|
|
30079
30110
|
}
|
|
@@ -30714,5 +30745,5 @@ function startAcpServer() {
|
|
|
30714
30745
|
codexAcpServer = null;
|
|
30715
30746
|
}
|
|
30716
30747
|
});
|
|
30717
|
-
}).onRequest(methods.agent.initialize, (ctx) => getAgent().initialize(ctx.params)).onRequest(methods.agent.session.new, (ctx) => getAgent().newSession(ctx.params)).onRequest(methods.agent.session.load, (ctx) => getAgent().loadSession(ctx.params)).onRequest(methods.agent.session.list, (ctx) => getAgent().listSessions(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => getAgent().deleteSession(ctx.params)).onRequest(methods.agent.session.resume, (ctx) => getAgent().resumeSession(ctx.params)).onRequest(methods.agent.session.close, (ctx) => getAgent().closeSession(ctx.params)).onRequest(methods.agent.session.setMode, (ctx) => getAgent().setSessionMode(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => getAgent().setSessionConfigOption(ctx.params)).onRequest(methods.agent.authenticate, (ctx) => getAgent().authenticate(ctx.params)).onRequest(methods.agent.logout, (ctx) => getAgent().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => getAgent().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => getAgent().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => getAgent().disableProvider(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => getAgent().prompt(ctx.params, ctx.signal)).onNotification(methods.agent.session.cancel, (ctx) => getAgent().cancel(ctx.params)).onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params)).onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params)).onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)).onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent().extMethod(GOAL_CONTROL_METHOD, ctx.params)).onRequest(STEER_METHOD, steerParamsParser, (ctx) => getAgent().extMethod(STEER_METHOD, ctx.params)).connect(acpJsonStream);
|
|
30748
|
+
}).onRequest(methods.agent.initialize, (ctx) => getAgent().initialize(ctx.params)).onRequest(methods.agent.session.new, (ctx) => getAgent().newSession(ctx.params)).onRequest(methods.agent.session.load, (ctx) => getAgent().loadSession(ctx.params)).onRequest(methods.agent.session.fork, (ctx) => getAgent().unstable_forkSession(ctx.params)).onRequest(methods.agent.session.list, (ctx) => getAgent().listSessions(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => getAgent().deleteSession(ctx.params)).onRequest(methods.agent.session.resume, (ctx) => getAgent().resumeSession(ctx.params)).onRequest(methods.agent.session.close, (ctx) => getAgent().closeSession(ctx.params)).onRequest(methods.agent.session.setMode, (ctx) => getAgent().setSessionMode(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => getAgent().setSessionConfigOption(ctx.params)).onRequest(methods.agent.authenticate, (ctx) => getAgent().authenticate(ctx.params)).onRequest(methods.agent.logout, (ctx) => getAgent().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => getAgent().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => getAgent().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => getAgent().disableProvider(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => getAgent().prompt(ctx.params, ctx.signal)).onNotification(methods.agent.session.cancel, (ctx) => getAgent().cancel(ctx.params)).onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params)).onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params)).onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)).onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent().extMethod(GOAL_CONTROL_METHOD, ctx.params)).onRequest(STEER_METHOD, steerParamsParser, (ctx) => getAgent().extMethod(STEER_METHOD, ctx.params)).connect(acpJsonStream);
|
|
30718
30749
|
}
|