@lasso-ai/cli 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agent.js +18 -1
- package/dist/cli/bridge.d.ts +22 -0
- package/dist/cli/bridge.js +448 -74
- package/dist/cli/oneshot.js +59 -43
- package/dist/cli/snapshot.d.ts +33 -0
- package/dist/cli/snapshot.js +225 -0
- package/dist/overlay.js +444 -3
- package/package.json +2 -1
package/dist/cli/bridge.js
CHANGED
|
@@ -17,6 +17,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
17
17
|
const agent_1 = require("./agent");
|
|
18
18
|
const project_1 = require("./project");
|
|
19
19
|
const auth_1 = require("./auth");
|
|
20
|
+
const snapshot_1 = require("./snapshot");
|
|
20
21
|
exports.DEFAULT_BRIDGE_PORT = 3056;
|
|
21
22
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
22
23
|
async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
|
|
@@ -147,6 +148,164 @@ function proposalMatchesCurrentSource(cwd, changes) {
|
|
|
147
148
|
return false;
|
|
148
149
|
}
|
|
149
150
|
}
|
|
151
|
+
async function triggerBuildFix(cwd, taskId, buildErrors, buildCommand, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext) {
|
|
152
|
+
const MAX_RETRIES = 3;
|
|
153
|
+
const context = buildErrorContext.get(taskId);
|
|
154
|
+
if (!context)
|
|
155
|
+
return;
|
|
156
|
+
context.retryCount += 1;
|
|
157
|
+
if (context.retryCount > MAX_RETRIES) {
|
|
158
|
+
// Max retries reached - revert changes and show error
|
|
159
|
+
for (const snapshot of taskSnapshots.get(taskId) || []) {
|
|
160
|
+
if (snapshot.content === null) {
|
|
161
|
+
if (node_fs_1.default.existsSync(snapshot.filePath))
|
|
162
|
+
node_fs_1.default.unlinkSync(snapshot.filePath);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
taskSnapshots.delete(taskId);
|
|
169
|
+
buildErrorContext.delete(taskId);
|
|
170
|
+
socket.send(JSON.stringify({
|
|
171
|
+
type: "agent_status",
|
|
172
|
+
taskId,
|
|
173
|
+
status: "error",
|
|
174
|
+
message: `⚠️ Build validation failed after ${MAX_RETRIES} attempts. The change was reverted. Errors:\n${buildErrors.slice(0, 5).join("\n")}${buildErrors.length > 5 ? `\n... and ${buildErrors.length - 5} more errors` : ""}`
|
|
175
|
+
}));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Re-apply the original changes
|
|
179
|
+
try {
|
|
180
|
+
const planned = new Map();
|
|
181
|
+
for (const change of originalChanges) {
|
|
182
|
+
const isNew = !change.oldString;
|
|
183
|
+
const filePath = resolveProposedFile(cwd, change.filePath, true);
|
|
184
|
+
const exists = node_fs_1.default.existsSync(filePath);
|
|
185
|
+
const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
|
|
186
|
+
const prepared = (!exists || isNew)
|
|
187
|
+
? { start: 0, end: content.length, oldString: content, newString: change.newString }
|
|
188
|
+
: prepareChange(content, change);
|
|
189
|
+
const fileChanges = planned.get(filePath) || [];
|
|
190
|
+
if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
|
|
191
|
+
throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
|
|
192
|
+
}
|
|
193
|
+
fileChanges.push({ filePath, content, exists, ...prepared });
|
|
194
|
+
planned.set(filePath, fileChanges);
|
|
195
|
+
}
|
|
196
|
+
for (const [filePath, changes] of planned) {
|
|
197
|
+
const exists = changes[0].exists;
|
|
198
|
+
const content = changes[0].content;
|
|
199
|
+
const nextContent = exists
|
|
200
|
+
? [...changes]
|
|
201
|
+
.sort((a, b) => b.start - a.start)
|
|
202
|
+
.reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
|
|
203
|
+
: changes[0].newString;
|
|
204
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
205
|
+
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (applyError) {
|
|
209
|
+
socket.send(JSON.stringify({
|
|
210
|
+
type: "agent_status",
|
|
211
|
+
taskId,
|
|
212
|
+
status: "error",
|
|
213
|
+
message: `Failed to re-apply original changes: ${applyError instanceof Error ? applyError.message : "Unknown error"}`
|
|
214
|
+
}));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// Run agent to fix build errors
|
|
218
|
+
const { runOneShotAgent } = await import("./oneshot.js");
|
|
219
|
+
const fixPrompt = `The previous change caused build errors. Please fix them. Build command: ${buildCommand}\n\nBuild errors:\n${buildErrors.slice(0, 10).join("\n")}${buildErrors.length > 10 ? `\n... and ${buildErrors.length - 10} more errors` : ""}\n\nFix these errors by making minimal changes to the files. Attempt ${context.retryCount} of ${MAX_RETRIES}.`;
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
taskControllers.set(taskId, controller);
|
|
222
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: `Fixing build errors (attempt ${context.retryCount}/${MAX_RETRIES})...` }));
|
|
223
|
+
try {
|
|
224
|
+
const result = await runOneShotAgent(cwd, fixPrompt, "project", { provider: "google", model: "gemini-2.5-flash", lassoKey: resolvedLassoKey, serverUrl: "https://api.lasso.byorello.space" }, controller.signal, (status, message, detail) => {
|
|
225
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
226
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
|
|
227
|
+
}
|
|
228
|
+
}, (prompt) => {
|
|
229
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
230
|
+
socket.send(JSON.stringify({ type: "agent_prompt", taskId, prompt }));
|
|
231
|
+
}
|
|
232
|
+
}, undefined, "https://api.lasso.byorello.space", resolvedLassoKey || agentConfig?.apiKey || collabConfig?.apiKey, taskId, undefined);
|
|
233
|
+
if (result.ok && result.changes && result.changes.length > 0) {
|
|
234
|
+
// Apply the fix
|
|
235
|
+
const snapshots = [];
|
|
236
|
+
const planned = new Map();
|
|
237
|
+
for (const change of result.changes) {
|
|
238
|
+
const isNew = !change.oldString;
|
|
239
|
+
const filePath = resolveProposedFile(cwd, change.filePath, true);
|
|
240
|
+
const exists = node_fs_1.default.existsSync(filePath);
|
|
241
|
+
const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
|
|
242
|
+
const prepared = (!exists || isNew)
|
|
243
|
+
? { start: 0, end: content.length, oldString: content, newString: change.newString }
|
|
244
|
+
: prepareChange(content, change);
|
|
245
|
+
const fileChanges = planned.get(filePath) || [];
|
|
246
|
+
if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
|
|
247
|
+
throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
|
|
248
|
+
}
|
|
249
|
+
fileChanges.push({ filePath, content, exists, ...prepared });
|
|
250
|
+
planned.set(filePath, fileChanges);
|
|
251
|
+
}
|
|
252
|
+
taskSnapshots.set(taskId, snapshots);
|
|
253
|
+
for (const [filePath, changes] of planned) {
|
|
254
|
+
const exists = changes[0].exists;
|
|
255
|
+
const content = changes[0].content;
|
|
256
|
+
snapshots.push({ filePath, content: exists ? content : null });
|
|
257
|
+
const nextContent = exists
|
|
258
|
+
? [...changes]
|
|
259
|
+
.sort((a, b) => b.start - a.start)
|
|
260
|
+
.reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
|
|
261
|
+
: changes[0].newString;
|
|
262
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
263
|
+
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
264
|
+
}
|
|
265
|
+
// Validate the fix
|
|
266
|
+
const validation = await validateBuild(cwd);
|
|
267
|
+
if (validation.passed) {
|
|
268
|
+
// Build fixed!
|
|
269
|
+
buildErrorContext.delete(taskId);
|
|
270
|
+
socket.send(JSON.stringify({
|
|
271
|
+
type: "applied",
|
|
272
|
+
taskId,
|
|
273
|
+
message: `Build errors fixed after ${context.retryCount} attempt${context.retryCount > 1 ? "s" : ""}. ${result.changes.length} file${result.changes.length === 1 ? "" : "s"} updated.`,
|
|
274
|
+
validation
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Still failing - trigger another retry
|
|
279
|
+
socket.send(JSON.stringify({
|
|
280
|
+
type: "agent_status",
|
|
281
|
+
taskId,
|
|
282
|
+
status: "working",
|
|
283
|
+
message: `⚠️ Build still failing. Retrying...`,
|
|
284
|
+
detail: `Build command: ${validation.command}\nErrors:\n${validation.errors.slice(0, 5).join("\n")}${validation.errors.length > 5 ? `\n... and ${validation.errors.length - 5} more errors` : ""}`
|
|
285
|
+
}));
|
|
286
|
+
// Update context with new errors
|
|
287
|
+
buildErrorContext.set(taskId, {
|
|
288
|
+
errors: validation.errors,
|
|
289
|
+
command: validation.command,
|
|
290
|
+
originalChanges: originalChanges,
|
|
291
|
+
retryCount: context.retryCount
|
|
292
|
+
});
|
|
293
|
+
await triggerBuildFix(cwd, taskId, validation.errors, validation.command, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// Agent failed to produce a fix
|
|
298
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: result.error || "Failed to fix build errors" }));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "Build fix encountered an error" }));
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
if (taskControllers.get(taskId) === controller)
|
|
306
|
+
taskControllers.delete(taskId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
150
309
|
async function validateBuild(cwd) {
|
|
151
310
|
const pkgPath = node_path_1.default.join(cwd, "package.json");
|
|
152
311
|
if (!node_fs_1.default.existsSync(pkgPath)) {
|
|
@@ -257,6 +416,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
257
416
|
const editRequests = new Map();
|
|
258
417
|
const editConfigs = new Map();
|
|
259
418
|
const reviewRefreshAttempts = new Map();
|
|
419
|
+
const buildErrorContext = new Map();
|
|
260
420
|
const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
|
|
261
421
|
const fileEnv = envRoots.reduce((values, root) => ({
|
|
262
422
|
...values,
|
|
@@ -276,6 +436,120 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
276
436
|
fileEnv.NEXT_LASSO_API_KEY ||
|
|
277
437
|
"";
|
|
278
438
|
const lassoKeyConfigured = Boolean(resolvedLassoKey);
|
|
439
|
+
// Task queue for concurrent execution
|
|
440
|
+
const MAX_CONCURRENT_TASKS = 3;
|
|
441
|
+
const taskQueue = [];
|
|
442
|
+
const runningTasks = new Set();
|
|
443
|
+
const taskPriority = new Map(); // Lower number = higher priority
|
|
444
|
+
// Will be set when WebSocket connects
|
|
445
|
+
let currentSocket = null;
|
|
446
|
+
function processTaskQueue() {
|
|
447
|
+
if (runningTasks.size >= MAX_CONCURRENT_TASKS || taskQueue.length === 0 || !currentSocket)
|
|
448
|
+
return;
|
|
449
|
+
const socket = currentSocket; // Capture non-null reference
|
|
450
|
+
taskQueue.sort((a, b) => {
|
|
451
|
+
const priorityA = taskPriority.get(a.taskId) ?? 999;
|
|
452
|
+
const priorityB = taskPriority.get(b.taskId) ?? 999;
|
|
453
|
+
return priorityA - priorityB;
|
|
454
|
+
});
|
|
455
|
+
const task = taskQueue.shift();
|
|
456
|
+
if (!task)
|
|
457
|
+
return;
|
|
458
|
+
runningTasks.add(task.taskId);
|
|
459
|
+
if (task.message.type === "edit") {
|
|
460
|
+
const request = task.message;
|
|
461
|
+
taskControllers.get(request.taskId)?.abort();
|
|
462
|
+
const controller = new AbortController();
|
|
463
|
+
taskControllers.set(request.taskId, controller);
|
|
464
|
+
if (socket.readyState === socket.OPEN) {
|
|
465
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: "Working…" }));
|
|
466
|
+
}
|
|
467
|
+
void (0, agent_1.proposeChanges)(cwd, request, task.config, controller.signal, (message, detail, level) => {
|
|
468
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
469
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
|
|
470
|
+
}
|
|
471
|
+
}, (prompt) => {
|
|
472
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
473
|
+
socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
.then((proposal) => {
|
|
477
|
+
if (controller.signal.aborted || socket.readyState !== socket.OPEN)
|
|
478
|
+
return;
|
|
479
|
+
if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
|
|
480
|
+
const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
|
|
481
|
+
const MAX_REFRESH_ATTEMPTS = 3;
|
|
482
|
+
if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
|
|
483
|
+
reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
|
|
484
|
+
setTimeout(() => {
|
|
485
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
486
|
+
enqueueTask(request.taskId, request, task.config, 0);
|
|
487
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})` }));
|
|
488
|
+
}
|
|
489
|
+
}, 500 * (refreshAttempts + 1));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing after multiple refresh attempts. Stop the dev-server edit or try the request again." }));
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
|
|
497
|
+
})
|
|
498
|
+
.catch((error) => {
|
|
499
|
+
if (socket.readyState !== socket.OPEN)
|
|
500
|
+
return;
|
|
501
|
+
if (controller.signal.aborted) {
|
|
502
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
|
|
506
|
+
})
|
|
507
|
+
.finally(() => {
|
|
508
|
+
if (taskControllers.get(request.taskId) === controller)
|
|
509
|
+
taskControllers.delete(request.taskId);
|
|
510
|
+
runningTasks.delete(request.taskId);
|
|
511
|
+
taskPriority.delete(request.taskId);
|
|
512
|
+
processTaskQueue();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
else if (task.message.type === "ask") {
|
|
516
|
+
const controller = new AbortController();
|
|
517
|
+
taskControllers.set(task.taskId, controller);
|
|
518
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: "working", message: "Working…" }));
|
|
519
|
+
void (0, agent_1.answerQuestion)(cwd, {
|
|
520
|
+
taskId: task.taskId,
|
|
521
|
+
question: task.message.question,
|
|
522
|
+
context: task.message.context,
|
|
523
|
+
element: task.message.element,
|
|
524
|
+
messages: task.message.messages,
|
|
525
|
+
}, task.config, controller.signal, (message, detail, level) => {
|
|
526
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
527
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: level === "error" ? "error" : "working", message, detail }));
|
|
528
|
+
}
|
|
529
|
+
}, (prompt) => {
|
|
530
|
+
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
531
|
+
socket.send(JSON.stringify({ type: "agent_prompt", taskId: task.taskId, prompt }));
|
|
532
|
+
}
|
|
533
|
+
}).then((answer) => {
|
|
534
|
+
if (!controller.signal.aborted)
|
|
535
|
+
socket.send(JSON.stringify({ type: "assistant_message", taskId: task.taskId, message: answer }));
|
|
536
|
+
}).catch((error) => {
|
|
537
|
+
if (!controller.signal.aborted)
|
|
538
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
|
|
539
|
+
}).finally(() => {
|
|
540
|
+
if (taskControllers.get(task.taskId) === controller)
|
|
541
|
+
taskControllers.delete(task.taskId);
|
|
542
|
+
runningTasks.delete(task.taskId);
|
|
543
|
+
taskPriority.delete(task.taskId);
|
|
544
|
+
processTaskQueue();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function enqueueTask(taskId, message, config, priority = 0) {
|
|
549
|
+
taskPriority.set(taskId, priority);
|
|
550
|
+
taskQueue.push({ taskId, message, config });
|
|
551
|
+
processTaskQueue();
|
|
552
|
+
}
|
|
279
553
|
// Fetch workspace configuration from server (dashboard-configured providers)
|
|
280
554
|
let workspaceConfig = null;
|
|
281
555
|
if (lassoKeyConfigured) {
|
|
@@ -539,6 +813,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
539
813
|
};
|
|
540
814
|
wss.on("connection", (socket) => {
|
|
541
815
|
overlaySocket = socket;
|
|
816
|
+
currentSocket = socket;
|
|
542
817
|
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
543
818
|
const creds = (0, auth_1.loadCredentials)();
|
|
544
819
|
const userInfo = creds?.userName
|
|
@@ -553,57 +828,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
553
828
|
if (socket.readyState === socket.OPEN)
|
|
554
829
|
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
555
830
|
});
|
|
556
|
-
const runEditReview = (request, config, statusMessage) => {
|
|
557
|
-
taskControllers.get(request.taskId)?.abort();
|
|
558
|
-
const controller = new AbortController();
|
|
559
|
-
taskControllers.set(request.taskId, controller);
|
|
560
|
-
if (socket.readyState === socket.OPEN) {
|
|
561
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage || "Working…" }));
|
|
562
|
-
}
|
|
563
|
-
void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail, level) => {
|
|
564
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
565
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
|
|
566
|
-
}
|
|
567
|
-
}, (prompt) => {
|
|
568
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
569
|
-
socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
|
|
570
|
-
}
|
|
571
|
-
})
|
|
572
|
-
.then((proposal) => {
|
|
573
|
-
if (controller.signal.aborted || socket.readyState !== socket.OPEN)
|
|
574
|
-
return;
|
|
575
|
-
if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
|
|
576
|
-
const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
|
|
577
|
-
const MAX_REFRESH_ATTEMPTS = 3;
|
|
578
|
-
if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
|
|
579
|
-
reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
|
|
580
|
-
setTimeout(() => {
|
|
581
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
582
|
-
runEditReview(request, config, `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})`);
|
|
583
|
-
}
|
|
584
|
-
}, 500 * (refreshAttempts + 1));
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing after multiple refresh attempts. Stop the dev-server edit or try the request again." }));
|
|
588
|
-
}
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
|
|
592
|
-
})
|
|
593
|
-
.catch((error) => {
|
|
594
|
-
if (socket.readyState !== socket.OPEN)
|
|
595
|
-
return;
|
|
596
|
-
if (controller.signal.aborted) {
|
|
597
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
|
|
601
|
-
})
|
|
602
|
-
.finally(() => {
|
|
603
|
-
if (taskControllers.get(request.taskId) === controller)
|
|
604
|
-
taskControllers.delete(request.taskId);
|
|
605
|
-
});
|
|
606
|
-
};
|
|
607
831
|
socket.on("message", async (raw) => {
|
|
608
832
|
const msg = JSON.parse(raw.toString());
|
|
609
833
|
if (msg.type === "edit" || msg.type === "ask") {
|
|
@@ -712,23 +936,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
712
936
|
serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv),
|
|
713
937
|
};
|
|
714
938
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
718
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: level === "error" ? "error" : "working", message, detail }));
|
|
719
|
-
}, (prompt) => {
|
|
720
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
721
|
-
socket.send(JSON.stringify({ type: "agent_prompt", taskId: msg.taskId, prompt }));
|
|
722
|
-
}).then((answer) => {
|
|
723
|
-
if (!controller.signal.aborted)
|
|
724
|
-
socket.send(JSON.stringify({ type: "assistant_message", taskId: msg.taskId, message: answer }));
|
|
725
|
-
}).catch((error) => {
|
|
726
|
-
if (!controller.signal.aborted)
|
|
727
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
|
|
728
|
-
}).finally(() => {
|
|
729
|
-
if (taskControllers.get(msg.taskId) === controller)
|
|
730
|
-
taskControllers.delete(msg.taskId);
|
|
731
|
-
});
|
|
939
|
+
// Enqueue task for concurrent execution
|
|
940
|
+
enqueueTask(msg.taskId, msg, selectedConfig, 1); // Priority 1 for ask tasks
|
|
732
941
|
}
|
|
733
942
|
else if (msg.type === "edit") {
|
|
734
943
|
const localProvider = msg.provider === "cli";
|
|
@@ -767,17 +976,29 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
767
976
|
editRequests.set(msg.taskId, msg);
|
|
768
977
|
editConfigs.set(msg.taskId, selectedConfig);
|
|
769
978
|
reviewRefreshAttempts.set(msg.taskId, 0);
|
|
770
|
-
|
|
979
|
+
// Enqueue task for concurrent execution with higher priority (0)
|
|
980
|
+
enqueueTask(msg.taskId, msg, selectedConfig, 0);
|
|
771
981
|
}
|
|
772
982
|
else if (msg.type === "agent_prompt_response") {
|
|
773
983
|
(0, agent_1.respondToAgentPrompt)(msg.taskId, msg.response);
|
|
774
984
|
}
|
|
775
985
|
else if (msg.type === "stop") {
|
|
776
|
-
if (msg.taskId)
|
|
986
|
+
if (msg.taskId) {
|
|
777
987
|
taskControllers.get(msg.taskId)?.abort();
|
|
778
|
-
|
|
988
|
+
// Remove from queue and running tasks
|
|
989
|
+
const queueIndex = taskQueue.findIndex(t => t.taskId === msg.taskId);
|
|
990
|
+
if (queueIndex !== -1)
|
|
991
|
+
taskQueue.splice(queueIndex, 1);
|
|
992
|
+
runningTasks.delete(msg.taskId);
|
|
993
|
+
taskPriority.delete(msg.taskId);
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
779
996
|
for (const controller of taskControllers.values())
|
|
780
997
|
controller.abort();
|
|
998
|
+
taskQueue.length = 0;
|
|
999
|
+
runningTasks.clear();
|
|
1000
|
+
taskPriority.clear();
|
|
1001
|
+
}
|
|
781
1002
|
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "stopped", message: "Agent stopped." }));
|
|
782
1003
|
}
|
|
783
1004
|
else if (msg.type === "runtime_error") {
|
|
@@ -962,8 +1183,28 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
962
1183
|
}
|
|
963
1184
|
else if (msg.type === "apply") {
|
|
964
1185
|
try {
|
|
1186
|
+
// Create snapshot before applying changes
|
|
1187
|
+
const filePaths = [...new Set(msg.changes.map(c => c.filePath))];
|
|
1188
|
+
const parentSnapshot = (0, snapshot_1.getSnapshotByTaskId)(cwd, msg.taskId);
|
|
1189
|
+
// Create pre-apply snapshot
|
|
1190
|
+
const preSnapshot = (0, snapshot_1.createSnapshot)(cwd, msg.taskId, `Pre-apply: ${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"}`, filePaths, parentSnapshot?.id);
|
|
965
1191
|
const snapshots = [];
|
|
966
1192
|
const planned = new Map();
|
|
1193
|
+
// Check for conflicts before applying
|
|
1194
|
+
const expectedFiles = msg.changes.map(change => ({
|
|
1195
|
+
filePath: change.filePath,
|
|
1196
|
+
content: change.oldString || null,
|
|
1197
|
+
}));
|
|
1198
|
+
const conflicts = (0, snapshot_1.detectConflicts)(cwd, expectedFiles);
|
|
1199
|
+
if (conflicts.length > 0) {
|
|
1200
|
+
socket.send(JSON.stringify({
|
|
1201
|
+
type: "conflict_detected",
|
|
1202
|
+
taskId: msg.taskId,
|
|
1203
|
+
conflicts,
|
|
1204
|
+
message: `Conflict detected in ${conflicts.length} file${conflicts.length === 1 ? "" : "s"}. The source has changed since the suggestion was generated.`
|
|
1205
|
+
}));
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
967
1208
|
for (const change of msg.changes) {
|
|
968
1209
|
const isNew = !change.oldString;
|
|
969
1210
|
const filePath = resolveProposedFile(cwd, change.filePath, true);
|
|
@@ -994,8 +1235,31 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
994
1235
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
995
1236
|
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
996
1237
|
}
|
|
1238
|
+
// Create post-apply snapshot
|
|
1239
|
+
(0, snapshot_1.createSnapshot)(cwd, msg.taskId, `Applied: ${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"}`, filePaths, preSnapshot.id);
|
|
997
1240
|
// Run build validation after applying changes
|
|
998
1241
|
const validation = await validateBuild(cwd);
|
|
1242
|
+
if (!validation.passed) {
|
|
1243
|
+
// Build failed - automatically trigger retry with build error context
|
|
1244
|
+
const errorContext = validation.errors.slice(0, 10).join("\n");
|
|
1245
|
+
socket.send(JSON.stringify({
|
|
1246
|
+
type: "agent_status",
|
|
1247
|
+
taskId: msg.taskId,
|
|
1248
|
+
status: "working",
|
|
1249
|
+
message: `⚠️ Build validation failed. Attempting to fix build errors automatically...`,
|
|
1250
|
+
detail: `Build command: ${validation.command}\nErrors:\n${errorContext}${validation.errors.length > 10 ? `\n... and ${validation.errors.length - 10} more errors` : ""}`
|
|
1251
|
+
}));
|
|
1252
|
+
// Store context for retry
|
|
1253
|
+
buildErrorContext.set(msg.taskId, {
|
|
1254
|
+
errors: validation.errors,
|
|
1255
|
+
command: validation.command,
|
|
1256
|
+
originalChanges: msg.changes,
|
|
1257
|
+
retryCount: 0
|
|
1258
|
+
});
|
|
1259
|
+
// Trigger automatic build fix
|
|
1260
|
+
await triggerBuildFix(cwd, msg.taskId, validation.errors, validation.command, msg.changes, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
999
1263
|
socket.send(JSON.stringify({
|
|
1000
1264
|
type: "applied",
|
|
1001
1265
|
taskId: msg.taskId,
|
|
@@ -1023,7 +1287,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1023
1287
|
const refreshAttempts = reviewRefreshAttempts.get(msg.taskId) || 0;
|
|
1024
1288
|
if (sourceChanged && editRequest && editConfig && refreshAttempts < 1) {
|
|
1025
1289
|
reviewRefreshAttempts.set(msg.taskId, refreshAttempts + 1);
|
|
1026
|
-
|
|
1290
|
+
enqueueTask(msg.taskId, editRequest, editConfig, 0);
|
|
1291
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "The source changed. Refreshing the review against the current file…" }));
|
|
1027
1292
|
}
|
|
1028
1293
|
else {
|
|
1029
1294
|
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message }));
|
|
@@ -1044,9 +1309,117 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1044
1309
|
taskSnapshots.delete(msg.taskId || "");
|
|
1045
1310
|
socket.send(JSON.stringify({ type: "undone", taskId: msg.taskId, message: snapshots.length ? "The accepted change was reverted." : "There is no accepted change to undo." }));
|
|
1046
1311
|
}
|
|
1312
|
+
else if (msg.type === "resolve_conflict") {
|
|
1313
|
+
const { taskId, action, conflicts } = msg;
|
|
1314
|
+
if (action === "retry") {
|
|
1315
|
+
// Re-run the edit request with current file content
|
|
1316
|
+
const editRequest = editRequests.get(taskId);
|
|
1317
|
+
const editConfig = editConfigs.get(taskId);
|
|
1318
|
+
if (editRequest && editConfig) {
|
|
1319
|
+
reviewRefreshAttempts.set(taskId, 0);
|
|
1320
|
+
enqueueTask(taskId, editRequest, editConfig, 0);
|
|
1321
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "working", message: "Retrying with current file content..." }));
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: "Could not retry: original request not found." }));
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
else if (action === "force") {
|
|
1328
|
+
// Force apply despite conflicts (user's choice)
|
|
1329
|
+
try {
|
|
1330
|
+
const snapshots = [];
|
|
1331
|
+
const planned = new Map();
|
|
1332
|
+
// Get the original changes from the edit request
|
|
1333
|
+
const editRequest = editRequests.get(taskId);
|
|
1334
|
+
if (!editRequest) {
|
|
1335
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: "Could not force apply: original request not found." }));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
// Re-prepare changes with current content (may fail, but that's expected)
|
|
1339
|
+
for (const change of editRequest.changesHistory?.[editRequest.changesHistory.length - 1]?.changes || []) {
|
|
1340
|
+
const isNew = !change.oldString;
|
|
1341
|
+
const filePath = resolveProposedFile(cwd, change.filePath, true);
|
|
1342
|
+
const exists = node_fs_1.default.existsSync(filePath);
|
|
1343
|
+
const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
|
|
1344
|
+
const prepared = (!exists || isNew)
|
|
1345
|
+
? { start: 0, end: content.length, oldString: content, newString: change.newString }
|
|
1346
|
+
: prepareChange(content, change);
|
|
1347
|
+
const fileChanges = planned.get(filePath) || [];
|
|
1348
|
+
if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
|
|
1349
|
+
throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
|
|
1350
|
+
}
|
|
1351
|
+
fileChanges.push({ filePath, content, exists, ...prepared });
|
|
1352
|
+
planned.set(filePath, fileChanges);
|
|
1353
|
+
}
|
|
1354
|
+
taskSnapshots.set(taskId, snapshots);
|
|
1355
|
+
for (const [filePath, changes] of planned) {
|
|
1356
|
+
const exists = changes[0].exists;
|
|
1357
|
+
const content = changes[0].content;
|
|
1358
|
+
snapshots.push({ filePath, content: exists ? content : null });
|
|
1359
|
+
const nextContent = exists
|
|
1360
|
+
? [...changes]
|
|
1361
|
+
.sort((a, b) => b.start - a.start)
|
|
1362
|
+
.reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
|
|
1363
|
+
: changes[0].newString;
|
|
1364
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
1365
|
+
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
1366
|
+
}
|
|
1367
|
+
socket.send(JSON.stringify({
|
|
1368
|
+
type: "applied",
|
|
1369
|
+
taskId,
|
|
1370
|
+
message: "Changes force-applied despite conflicts."
|
|
1371
|
+
}));
|
|
1372
|
+
}
|
|
1373
|
+
catch (error) {
|
|
1374
|
+
for (const snapshot of taskSnapshots.get(taskId) || []) {
|
|
1375
|
+
if (snapshot.content === null) {
|
|
1376
|
+
if (node_fs_1.default.existsSync(snapshot.filePath))
|
|
1377
|
+
node_fs_1.default.unlinkSync(snapshot.filePath);
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
taskSnapshots.delete(taskId);
|
|
1384
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "Force apply failed." }));
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
else if (action === "rebase") {
|
|
1388
|
+
// User wants to rebase - re-run the edit with updated context
|
|
1389
|
+
const editRequest = editRequests.get(taskId);
|
|
1390
|
+
const editConfig = editConfigs.get(taskId);
|
|
1391
|
+
if (editRequest && editConfig) {
|
|
1392
|
+
reviewRefreshAttempts.set(taskId, 0);
|
|
1393
|
+
enqueueTask(taskId, editRequest, editConfig, 0);
|
|
1394
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "working", message: "Rebasing changes against current file content..." }));
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: "Could not rebase: original request not found." }));
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
else if (msg.type === "restore_snapshot") {
|
|
1402
|
+
const success = (0, snapshot_1.restoreSnapshot)(cwd, msg.snapshotId);
|
|
1403
|
+
socket.send(JSON.stringify({
|
|
1404
|
+
type: "agent_status",
|
|
1405
|
+
message: success ? "Snapshot restored successfully." : "Failed to restore snapshot."
|
|
1406
|
+
}));
|
|
1407
|
+
}
|
|
1408
|
+
else if (msg.type === "get_timeline") {
|
|
1409
|
+
const timeline = (0, snapshot_1.getTimeline)(cwd);
|
|
1410
|
+
socket.send(JSON.stringify({ type: "timeline", snapshots: timeline }));
|
|
1411
|
+
}
|
|
1412
|
+
else if (msg.type === "delete_snapshot") {
|
|
1413
|
+
const success = (0, snapshot_1.deleteSnapshot)(cwd, msg.snapshotId);
|
|
1414
|
+
socket.send(JSON.stringify({
|
|
1415
|
+
type: "agent_status",
|
|
1416
|
+
message: success ? "Snapshot deleted." : "Failed to delete snapshot."
|
|
1417
|
+
}));
|
|
1418
|
+
}
|
|
1047
1419
|
});
|
|
1048
1420
|
socket.on("close", () => {
|
|
1049
1421
|
overlaySocket = null;
|
|
1422
|
+
currentSocket = null;
|
|
1050
1423
|
console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
|
|
1051
1424
|
});
|
|
1052
1425
|
});
|
|
@@ -1063,8 +1436,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1063
1436
|
});
|
|
1064
1437
|
bridgeServer.listen(bridgePort, "127.0.0.1");
|
|
1065
1438
|
function send(msg) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1439
|
+
const socket = currentSocket;
|
|
1440
|
+
if (socket && socket.readyState === socket.OPEN) {
|
|
1441
|
+
socket.send(JSON.stringify(msg));
|
|
1068
1442
|
}
|
|
1069
1443
|
}
|
|
1070
1444
|
return {
|