farming-code 2.2.20 → 2.2.23
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/native-pty-host.js +44 -8
- 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 +332 -102
- package/backend/storage-layout.js +10 -0
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- 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-BbNBDA__.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-DuMjesL2.js → index-DhTUO8DA.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 +199 -81
- package/package.json +9 -3
- package/dist/assets/App-BupehIbc.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-BAt0Q_Op.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-y2aK_56c.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
|
@@ -77,6 +77,15 @@ function sanitizeAgentEnv(env) {
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function socketIdentity(socketPath) {
|
|
81
|
+
const stat = fs.statSync(socketPath, { bigint: true });
|
|
82
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sameSocketIdentity(left, right) {
|
|
86
|
+
return Boolean(left && right && left.dev === right.dev && left.ino === right.ino);
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
class NativePtyHost {
|
|
81
90
|
constructor(options = {}) {
|
|
82
91
|
this.configDir = options.configDir || process.env.FARMING_CONFIG_DIR || path.join(os.homedir(), '.farming');
|
|
@@ -84,6 +93,13 @@ class NativePtyHost {
|
|
|
84
93
|
this.ownerPid = Number(options.ownerPid || process.env.FARMING_NATIVE_PTY_HOST_OWNER_PID || 0);
|
|
85
94
|
this.runtimeIdentity = options.runtimeIdentity || HOST_RUNTIME_IDENTITY;
|
|
86
95
|
this.exitOnShutdown = options.exitOnShutdown !== false;
|
|
96
|
+
this.boundSocketPath = process.platform === 'win32'
|
|
97
|
+
? this.socketPath
|
|
98
|
+
: path.join(
|
|
99
|
+
path.dirname(this.socketPath),
|
|
100
|
+
`.fpty-${process.pid}-${crypto.createHash('sha256').update(this.socketPath).digest('hex').slice(0, 8)}-${crypto.randomBytes(4).toString('hex')}.sock`,
|
|
101
|
+
);
|
|
102
|
+
this.boundSocketIdentity = null;
|
|
87
103
|
this.sessions = new Map();
|
|
88
104
|
this.clients = new Set();
|
|
89
105
|
this.ownerCheckTimer = null;
|
|
@@ -130,13 +146,22 @@ class NativePtyHost {
|
|
|
130
146
|
this.server = net.createServer(socket => this.handleConnection(socket));
|
|
131
147
|
await new Promise((resolve, reject) => {
|
|
132
148
|
this.server.once('error', reject);
|
|
133
|
-
this.server.listen(this.
|
|
149
|
+
this.server.listen(this.boundSocketPath, () => {
|
|
134
150
|
this.server.off('error', reject);
|
|
135
151
|
resolve();
|
|
136
152
|
});
|
|
137
153
|
});
|
|
138
154
|
if (process.platform !== 'win32') {
|
|
139
|
-
|
|
155
|
+
this.boundSocketIdentity = socketIdentity(this.boundSocketPath);
|
|
156
|
+
fs.chmodSync(this.boundSocketPath, 0o600);
|
|
157
|
+
try {
|
|
158
|
+
fs.linkSync(this.boundSocketPath, this.socketPath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
await new Promise(resolve => this.server.close(() => resolve()));
|
|
161
|
+
this.server = null;
|
|
162
|
+
this.boundSocketIdentity = null;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
140
165
|
}
|
|
141
166
|
console.error(`[${new Date().toISOString()}] Native PTY host listening on ${this.socketPath} ownerPid=${this.ownerPid || ''}`);
|
|
142
167
|
this.startOwnerWatch();
|
|
@@ -173,6 +198,11 @@ class NativePtyHost {
|
|
|
173
198
|
} catch (error) {
|
|
174
199
|
if (!error || error.code !== 'ENOENT') throw error;
|
|
175
200
|
}
|
|
201
|
+
try {
|
|
202
|
+
fs.unlinkSync(this.boundSocketPath);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
205
|
+
}
|
|
176
206
|
}
|
|
177
207
|
|
|
178
208
|
handleConnection(socket) {
|
|
@@ -1476,16 +1506,22 @@ class NativePtyHost {
|
|
|
1476
1506
|
}
|
|
1477
1507
|
}
|
|
1478
1508
|
await this.screenWorkerPool.dispose();
|
|
1479
|
-
if (this.server) {
|
|
1480
|
-
await new Promise(resolve => this.server.close(() => resolve()));
|
|
1481
|
-
this.server = null;
|
|
1482
|
-
}
|
|
1483
|
-
if (process.platform !== 'win32') {
|
|
1509
|
+
if (this.server && process.platform !== 'win32') {
|
|
1484
1510
|
try {
|
|
1485
|
-
|
|
1511
|
+
const currentSocketIdentity = socketIdentity(this.socketPath);
|
|
1512
|
+
if (sameSocketIdentity(currentSocketIdentity, this.boundSocketIdentity)) {
|
|
1513
|
+
// Unlink while this listener is still active. A competing host cannot
|
|
1514
|
+
// replace an active socket between the ownership check and unlink.
|
|
1515
|
+
fs.unlinkSync(this.socketPath);
|
|
1516
|
+
}
|
|
1486
1517
|
} catch (error) {
|
|
1487
1518
|
if (!error || error.code !== 'ENOENT') throw error;
|
|
1488
1519
|
}
|
|
1520
|
+
this.boundSocketIdentity = null;
|
|
1521
|
+
}
|
|
1522
|
+
if (this.server) {
|
|
1523
|
+
await new Promise(resolve => this.server.close(() => resolve()));
|
|
1524
|
+
this.server = null;
|
|
1489
1525
|
}
|
|
1490
1526
|
}
|
|
1491
1527
|
}
|
|
@@ -61,10 +61,11 @@ async function stopProcess(pid, timeoutMs = 15_000) {
|
|
|
61
61
|
|
|
62
62
|
function validatePayload(payload) {
|
|
63
63
|
if (!payload || typeof payload !== 'object') throw new Error('Missing npm update payload');
|
|
64
|
+
if (!['prepare', 'apply'].includes(payload.action)) throw new Error('Invalid npm update action');
|
|
64
65
|
if (!/^[A-Za-z0-9@/._-]+$/.test(String(payload.packageName || ''))) throw new Error('Invalid npm package name');
|
|
65
66
|
if (!/^[0-9A-Za-z.+-]+$/.test(String(payload.targetVersion || ''))) throw new Error('Invalid npm target version');
|
|
66
67
|
if (!/^[0-9A-Za-z.+-]+$/.test(String(payload.previousVersion || ''))) throw new Error('Invalid npm previous version');
|
|
67
|
-
for (const key of ['stateFile', 'logPath', 'cliPath', 'packageRoot', 'configDir']) {
|
|
68
|
+
for (const key of ['stateFile', 'logPath', 'cliPath', 'packageRoot', 'configDir', 'stagingPrefix', 'stagingPackageRoot']) {
|
|
68
69
|
if (!path.isAbsolute(String(payload[key] || ''))) throw new Error(`Invalid npm update ${key}`);
|
|
69
70
|
}
|
|
70
71
|
if (payload.npmPrefix && !path.isAbsolute(String(payload.npmPrefix))) {
|
|
@@ -81,6 +82,13 @@ function validatePayload(payload) {
|
|
|
81
82
|
throw new Error('Invalid npm update registry');
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
const expectedStagingRoot = path.join(payload.stagingPrefix, 'lib', 'node_modules', payload.packageName);
|
|
86
|
+
if (path.resolve(payload.stagingPackageRoot) !== path.resolve(expectedStagingRoot)) {
|
|
87
|
+
throw new Error('Invalid npm update stagingPackageRoot');
|
|
88
|
+
}
|
|
89
|
+
if (path.resolve(payload.stagingPackageRoot) === path.resolve(payload.packageRoot)) {
|
|
90
|
+
throw new Error('npm update staging root must differ from the running package root');
|
|
91
|
+
}
|
|
84
92
|
return payload;
|
|
85
93
|
}
|
|
86
94
|
|
|
@@ -93,6 +101,8 @@ function stateFor(payload, phase, extra = {}) {
|
|
|
93
101
|
packageName: payload.packageName,
|
|
94
102
|
startedAt: payload.startedAt,
|
|
95
103
|
logPath: payload.logPath,
|
|
104
|
+
stagingPrefix: payload.stagingPrefix,
|
|
105
|
+
stagingPackageRoot: payload.stagingPackageRoot,
|
|
96
106
|
...extra,
|
|
97
107
|
};
|
|
98
108
|
}
|
|
@@ -118,13 +128,13 @@ function commandEnvironment() {
|
|
|
118
128
|
return env;
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
async function installPackage(payload, version) {
|
|
131
|
+
async function installPackage(payload, version, npmPrefix = payload.npmPrefix) {
|
|
122
132
|
const packageSpec = `${payload.packageName}@${version}`;
|
|
123
|
-
return installPackageFromRegistry(payload, packageSpec);
|
|
133
|
+
return installPackageFromRegistry(payload, packageSpec, '', npmPrefix);
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
function verifyInstalledVersion(payload, expectedVersion) {
|
|
127
|
-
const packageJsonPath = path.join(
|
|
136
|
+
function verifyInstalledVersion(payload, expectedVersion, packageRoot = payload.packageRoot) {
|
|
137
|
+
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
128
138
|
let metadata;
|
|
129
139
|
try {
|
|
130
140
|
metadata = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
@@ -153,10 +163,10 @@ function logSince(filePath, offset) {
|
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
|
|
156
|
-
async function installPackageFromRegistry(payload, packageSpec, registryUrl = '') {
|
|
166
|
+
async function installPackageFromRegistry(payload, packageSpec, registryUrl = '', npmPrefix = payload.npmPrefix) {
|
|
157
167
|
appendLog(payload.logPath, `Installing ${packageSpec}${registryUrl ? ' from the update-status registry' : ''}`);
|
|
158
168
|
const args = ['install', '--global'];
|
|
159
|
-
if (
|
|
169
|
+
if (npmPrefix) args.push('--prefix', npmPrefix);
|
|
160
170
|
if (registryUrl) args.push('--registry', registryUrl);
|
|
161
171
|
args.push(packageSpec, '--no-audit', '--no-fund');
|
|
162
172
|
const offset = logSize(payload.logPath);
|
|
@@ -169,7 +179,7 @@ async function installPackageFromRegistry(payload, packageSpec, registryUrl = ''
|
|
|
169
179
|
} catch (error) {
|
|
170
180
|
if (!registryUrl && payload.npmFallbackRegistryUrl && /(?:ETARGET|No matching version found)/.test(logSince(payload.logPath, offset))) {
|
|
171
181
|
appendLog(payload.logPath, `Configured npm registry has no ${packageSpec}; retrying from the update-status registry`);
|
|
172
|
-
return installPackageFromRegistry(payload, packageSpec, payload.npmFallbackRegistryUrl);
|
|
182
|
+
return installPackageFromRegistry(payload, packageSpec, payload.npmFallbackRegistryUrl, npmPrefix);
|
|
173
183
|
}
|
|
174
184
|
throw error;
|
|
175
185
|
}
|
|
@@ -184,38 +194,100 @@ async function startServer(payload, version = payload.targetVersion) {
|
|
|
184
194
|
});
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
async function
|
|
188
|
-
const payload = validatePayload(rawPayload);
|
|
197
|
+
async function prepareNpmUpdate(payload) {
|
|
189
198
|
try {
|
|
190
199
|
writeJsonAtomic(payload.stateFile, stateFor(payload, 'installing'));
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
fs.mkdirSync(payload.stagingPrefix, { recursive: true });
|
|
201
|
+
await installPackage(payload, payload.targetVersion, payload.stagingPrefix);
|
|
202
|
+
verifyInstalledVersion(payload, payload.targetVersion, payload.stagingPackageRoot);
|
|
203
|
+
const preparedAt = new Date().toISOString();
|
|
204
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'ready-to-restart', {
|
|
205
|
+
preparedAt,
|
|
206
|
+
}));
|
|
207
|
+
appendLog(payload.logPath, `Farming ${payload.targetVersion} is ready to restart`);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const message = error && error.message ? error.message : String(error);
|
|
210
|
+
appendLog(payload.logPath, `Update preparation failed: ${message}`);
|
|
211
|
+
try {
|
|
212
|
+
fs.rmSync(payload.stagingPrefix, { recursive: true, force: true });
|
|
213
|
+
} catch (cleanupError) {
|
|
214
|
+
appendLog(payload.logPath, `Update preparation cleanup failed: ${cleanupError.message || cleanupError}`);
|
|
215
|
+
}
|
|
216
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
|
|
217
|
+
error: message,
|
|
218
|
+
completedAt: new Date().toISOString(),
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function applyNpmUpdate(payload) {
|
|
224
|
+
const backupRoot = path.join(
|
|
225
|
+
path.dirname(payload.packageRoot),
|
|
226
|
+
`.${path.basename(payload.packageRoot)}.backup-${process.pid}`,
|
|
227
|
+
);
|
|
228
|
+
let stoppedOldServer = false;
|
|
229
|
+
let movedOldPackage = false;
|
|
230
|
+
let movedNewPackage = false;
|
|
231
|
+
try {
|
|
232
|
+
verifyInstalledVersion(payload, payload.previousVersion, payload.packageRoot);
|
|
233
|
+
verifyInstalledVersion(payload, payload.targetVersion, payload.stagingPackageRoot);
|
|
234
|
+
if (fs.existsSync(backupRoot)) throw new Error(`npm update backup already exists: ${backupRoot}`);
|
|
193
235
|
|
|
194
|
-
writeJsonAtomic(payload.stateFile, stateFor(payload, 'restarting'
|
|
236
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'restarting', {
|
|
237
|
+
preparedAt: payload.preparedAt,
|
|
238
|
+
}));
|
|
239
|
+
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
195
240
|
await stopProcess(Number(payload.serverPid));
|
|
241
|
+
stoppedOldServer = true;
|
|
242
|
+
fs.renameSync(payload.packageRoot, backupRoot);
|
|
243
|
+
movedOldPackage = true;
|
|
244
|
+
fs.renameSync(payload.stagingPackageRoot, payload.packageRoot);
|
|
245
|
+
movedNewPackage = true;
|
|
196
246
|
await startServer(payload);
|
|
197
247
|
|
|
198
248
|
writeJsonAtomic(payload.stateFile, stateFor(payload, 'succeeded', {
|
|
249
|
+
preparedAt: payload.preparedAt,
|
|
199
250
|
completedAt: new Date().toISOString(),
|
|
200
251
|
}));
|
|
201
252
|
appendLog(payload.logPath, `Farming updated to ${payload.targetVersion}`);
|
|
253
|
+
try {
|
|
254
|
+
fs.rmSync(backupRoot, { recursive: true, force: true });
|
|
255
|
+
fs.rmSync(payload.stagingPrefix, { recursive: true, force: true });
|
|
256
|
+
} catch (cleanupError) {
|
|
257
|
+
appendLog(payload.logPath, `Update cleanup failed: ${cleanupError.message || cleanupError}`);
|
|
258
|
+
}
|
|
202
259
|
} catch (error) {
|
|
203
260
|
const message = error && error.message ? error.message : String(error);
|
|
204
|
-
appendLog(payload.logPath, `Update failed: ${message}`);
|
|
261
|
+
appendLog(payload.logPath, `Update apply failed: ${message}`);
|
|
205
262
|
|
|
206
|
-
if (!isProcessRunning(Number(payload.serverPid))) {
|
|
263
|
+
if (stoppedOldServer || !isProcessRunning(Number(payload.serverPid))) {
|
|
207
264
|
try {
|
|
208
|
-
writeJsonAtomic(payload.stateFile, stateFor(payload, 'rolling-back', {
|
|
209
|
-
|
|
210
|
-
|
|
265
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'rolling-back', {
|
|
266
|
+
preparedAt: payload.preparedAt,
|
|
267
|
+
error: message,
|
|
268
|
+
}));
|
|
269
|
+
if (movedNewPackage && fs.existsSync(payload.packageRoot)) {
|
|
270
|
+
fs.mkdirSync(path.dirname(payload.stagingPackageRoot), { recursive: true });
|
|
271
|
+
fs.renameSync(payload.packageRoot, payload.stagingPackageRoot);
|
|
272
|
+
}
|
|
273
|
+
if (movedOldPackage && fs.existsSync(backupRoot)) {
|
|
274
|
+
fs.renameSync(backupRoot, payload.packageRoot);
|
|
275
|
+
}
|
|
276
|
+
verifyInstalledVersion(payload, payload.previousVersion, payload.packageRoot);
|
|
211
277
|
await startServer(payload, payload.previousVersion);
|
|
212
278
|
writeJsonAtomic(payload.stateFile, stateFor(payload, 'rolled-back', {
|
|
213
279
|
version: payload.previousVersion,
|
|
214
280
|
attemptedVersion: payload.targetVersion,
|
|
281
|
+
preparedAt: payload.preparedAt,
|
|
215
282
|
error: message,
|
|
216
283
|
completedAt: new Date().toISOString(),
|
|
217
284
|
}));
|
|
218
285
|
appendLog(payload.logPath, `Rolled back to ${payload.previousVersion}`);
|
|
286
|
+
try {
|
|
287
|
+
fs.rmSync(payload.stagingPrefix, { recursive: true, force: true });
|
|
288
|
+
} catch (cleanupError) {
|
|
289
|
+
appendLog(payload.logPath, `Rollback cleanup failed: ${cleanupError.message || cleanupError}`);
|
|
290
|
+
}
|
|
219
291
|
return;
|
|
220
292
|
} catch (rollbackError) {
|
|
221
293
|
const rollbackMessage = rollbackError && rollbackError.message ? rollbackError.message : String(rollbackError);
|
|
@@ -228,12 +300,19 @@ async function runNpmUpdate(rawPayload) {
|
|
|
228
300
|
}
|
|
229
301
|
|
|
230
302
|
writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
|
|
303
|
+
preparedAt: payload.preparedAt,
|
|
231
304
|
error: message,
|
|
232
305
|
completedAt: new Date().toISOString(),
|
|
233
306
|
}));
|
|
234
307
|
}
|
|
235
308
|
}
|
|
236
309
|
|
|
310
|
+
async function runNpmUpdate(rawPayload) {
|
|
311
|
+
const payload = validatePayload(rawPayload);
|
|
312
|
+
if (payload.action === 'prepare') return prepareNpmUpdate(payload);
|
|
313
|
+
return applyNpmUpdate(payload);
|
|
314
|
+
}
|
|
315
|
+
|
|
237
316
|
if (require.main === module) {
|
|
238
317
|
let payload;
|
|
239
318
|
try {
|
|
@@ -221,7 +221,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
|
|
|
221
221
|
terminalResumeArgs: (args, sessionId) => ['resume', sessionId, ...args],
|
|
222
222
|
acp: { packageName: '@agentclientprotocol/codex-acp', version: '1.1.4' },
|
|
223
223
|
prepareAcpEnvironment: codexAcpEnvironment,
|
|
224
|
-
capabilities: { runtimeSwitch: true, terminalProfile: true, goals: false },
|
|
224
|
+
capabilities: { runtimeSwitch: true, terminalProfile: true, goals: false, sessionFork: true },
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
id: 'claude',
|
|
@@ -233,8 +233,12 @@ const PROVIDER_ADAPTERS = Object.freeze([
|
|
|
233
233
|
commands: ['claude'],
|
|
234
234
|
supportedRuntimes: ['terminal', 'acp'],
|
|
235
235
|
planSession: (rawArgs, launchArgs) => explicitSessionPlan('claude', rawArgs, launchArgs),
|
|
236
|
-
acp: {
|
|
237
|
-
|
|
236
|
+
acp: {
|
|
237
|
+
packageName: '@agentclientprotocol/claude-agent-acp',
|
|
238
|
+
version: '0.59.0',
|
|
239
|
+
forkMode: 'target-process',
|
|
240
|
+
},
|
|
241
|
+
capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false, sessionFork: true },
|
|
238
242
|
},
|
|
239
243
|
{
|
|
240
244
|
id: 'opencode',
|
|
@@ -263,7 +267,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
|
|
|
263
267
|
args: ['acp', '--cwd', path.resolve(options.cwd || process.cwd())],
|
|
264
268
|
}),
|
|
265
269
|
},
|
|
266
|
-
capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
|
|
270
|
+
capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false, sessionFork: true },
|
|
267
271
|
},
|
|
268
272
|
{
|
|
269
273
|
id: 'qoder',
|
|
@@ -279,7 +283,7 @@ const PROVIDER_ADAPTERS = Object.freeze([
|
|
|
279
283
|
version: 'native',
|
|
280
284
|
launch: options => ({ command: options.executable || 'qodercli', args: ['--acp'] }),
|
|
281
285
|
},
|
|
282
|
-
capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false },
|
|
286
|
+
capabilities: { runtimeSwitch: true, terminalProfile: false, goals: false, sessionFork: true },
|
|
283
287
|
},
|
|
284
288
|
]);
|
|
285
289
|
|
|
@@ -303,10 +307,13 @@ function listProviderAdapters() {
|
|
|
303
307
|
function providerCapabilities(provider) {
|
|
304
308
|
const adapter = getProviderAdapter(provider);
|
|
305
309
|
return {
|
|
306
|
-
supportedRuntimes: adapter
|
|
310
|
+
supportedRuntimes: adapter
|
|
311
|
+
? adapter.supportedRuntimes.filter(runtime => runtime !== 'json')
|
|
312
|
+
: ['terminal'],
|
|
307
313
|
runtimeSwitch: adapter?.capabilities?.runtimeSwitch === true,
|
|
308
314
|
terminalProfile: adapter?.capabilities?.terminalProfile === true,
|
|
309
315
|
goals: adapter?.capabilities?.goals === true,
|
|
316
|
+
sessionFork: adapter?.capabilities?.sessionFork === true,
|
|
310
317
|
...(adapter
|
|
311
318
|
? chatCapabilitiesForProvider(provider)
|
|
312
319
|
: { chatRuntime: '', supportsChat: false, supportsSteer: false }),
|
|
@@ -317,6 +324,10 @@ function providerSupportsRuntime(provider, runtime) {
|
|
|
317
324
|
return getProviderAdapter(provider)?.supportedRuntimes.includes(runtime) === true;
|
|
318
325
|
}
|
|
319
326
|
|
|
327
|
+
function providerAcpForkMode(provider) {
|
|
328
|
+
return getProviderAdapter(provider)?.acp?.forkMode || 'source-then-load';
|
|
329
|
+
}
|
|
330
|
+
|
|
320
331
|
function applyProviderHomeEnvironment(env, provider, homePath) {
|
|
321
332
|
const key = getProviderAdapter(provider)?.homeEnvKey;
|
|
322
333
|
if (key && homePath) env[key] = homePath;
|
|
@@ -332,6 +343,7 @@ module.exports = {
|
|
|
332
343
|
applyProviderHomeEnvironment,
|
|
333
344
|
isFreshAcpSessionSource,
|
|
334
345
|
listProviderAdapters,
|
|
346
|
+
providerAcpForkMode,
|
|
335
347
|
providerCapabilities,
|
|
336
348
|
providerForProgram,
|
|
337
349
|
providerSupportsRuntime,
|
|
@@ -178,6 +178,18 @@ class ReviewSessionService {
|
|
|
178
178
|
this.reviewStateStore = reviewStateStore;
|
|
179
179
|
this.resolveAgentRoot = options.resolveAgentRoot;
|
|
180
180
|
this.resolveAcpReviewChanges = options.resolveAcpReviewChanges;
|
|
181
|
+
this.refreshQueues = new Map();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
enqueueRefresh(reviewId, operation) {
|
|
185
|
+
const previous = this.refreshQueues.get(reviewId) || Promise.resolve();
|
|
186
|
+
const next = previous.catch(() => {}).then(operation);
|
|
187
|
+
this.refreshQueues.set(reviewId, next);
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
if (this.refreshQueues.get(reviewId) === next) this.refreshQueues.delete(reviewId);
|
|
190
|
+
};
|
|
191
|
+
next.then(cleanup, cleanup);
|
|
192
|
+
return next;
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
async git(root, args, options = {}) {
|
|
@@ -373,24 +385,26 @@ class ReviewSessionService {
|
|
|
373
385
|
|
|
374
386
|
async refresh(reviewId) {
|
|
375
387
|
if (!REVIEW_ID_PATTERN.test(reviewId)) throw new ReviewSessionError('review session id is invalid');
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
388
|
+
return this.enqueueRefresh(reviewId, async () => {
|
|
389
|
+
const current = this.sessionStore.get(reviewId);
|
|
390
|
+
if (!current) throw new ReviewSessionError('review session not found', 404);
|
|
391
|
+
const paths = await this.capturePaths(current.root, current);
|
|
392
|
+
const tree = await this.captureStableTree(current.root, paths);
|
|
393
|
+
const previous = current.revisions[current.revisions.length - 1];
|
|
394
|
+
if (previous.tree === tree) return { ...publicRevision(current, previous), changedPaths: [], unchanged: true };
|
|
395
|
+
const nextNumber = previous.number + 1;
|
|
396
|
+
const changedPaths = await this.changedPaths(current.root, previous.tree, tree);
|
|
397
|
+
await this.keepRevision(current.root, reviewId, nextNumber, tree);
|
|
398
|
+
const result = this.sessionStore.appendRevision(reviewId, tree);
|
|
399
|
+
const next = result.session.revisions[result.session.revisions.length - 1];
|
|
400
|
+
this.reviewStateStore?.inheritPatchset?.({
|
|
401
|
+
changedPaths,
|
|
402
|
+
nextPatchset: tree,
|
|
403
|
+
previousPatchset: previous.tree,
|
|
404
|
+
reviewId,
|
|
405
|
+
});
|
|
406
|
+
return { ...publicRevision(result.session, next), changedPaths, unchanged: false };
|
|
392
407
|
});
|
|
393
|
-
return { ...publicRevision(result.session, next), changedPaths, unchanged: false };
|
|
394
408
|
}
|
|
395
409
|
|
|
396
410
|
get(reviewId) {
|