@lasso-ai/cli 1.3.2 → 1.3.4
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 +28 -1
- package/dist/cli/bridge.d.ts +22 -0
- package/dist/cli/bridge.js +268 -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 +443 -2
- package/package.json +1 -2
package/dist/cli/agent.js
CHANGED
|
@@ -19,6 +19,15 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
19
19
|
const node_child_process_1 = require("node:child_process");
|
|
20
20
|
const node_util_1 = require("node:util");
|
|
21
21
|
const auth_1 = require("./auth");
|
|
22
|
+
// Optional layout-intelligence support (local dev only)
|
|
23
|
+
let searchLayouts = null;
|
|
24
|
+
try {
|
|
25
|
+
const layoutModule = require("@lasso-ai/layout-intelligence");
|
|
26
|
+
searchLayouts = layoutModule.searchLayouts;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// layout-intelligence not available in production builds
|
|
30
|
+
}
|
|
22
31
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
23
32
|
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
24
33
|
function detectProjectStructure(cwd) {
|
|
@@ -1002,6 +1011,24 @@ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt)
|
|
|
1002
1011
|
: "";
|
|
1003
1012
|
// Detect project structure for instruction
|
|
1004
1013
|
const structure = detectProjectStructure(cwd);
|
|
1014
|
+
// Query layout intelligence for relevant patterns (if available)
|
|
1015
|
+
let layoutContext = "";
|
|
1016
|
+
if (searchLayouts) {
|
|
1017
|
+
try {
|
|
1018
|
+
const layoutQuery = input.instruction;
|
|
1019
|
+
const relevantLayouts = await searchLayouts(layoutQuery);
|
|
1020
|
+
if (relevantLayouts.length > 0) {
|
|
1021
|
+
layoutContext = `\n\nLAYOUT INTELLIGENCE:
|
|
1022
|
+
Based on your request, here are relevant layout patterns you should consider:
|
|
1023
|
+
${relevantLayouts.slice(0, 3).map((result) => `- ${result.pattern.name}: ${result.pattern.description}`).join("\n")}
|
|
1024
|
+
Use these patterns as inspiration for your implementation. Focus on matching the layout structure, spacing, and composition principles.`;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
// Layout intelligence is optional; continue without it if it fails
|
|
1029
|
+
console.warn("Layout intelligence query failed:", error);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1005
1032
|
const instruction = `Selection context:
|
|
1006
1033
|
${JSON.stringify(input.element, null, 2)}
|
|
1007
1034
|
${JSON.stringify(visualContext, null, 2)}
|
|
@@ -1016,7 +1043,7 @@ Previous change history for this selection:
|
|
|
1016
1043
|
${priorChanges}
|
|
1017
1044
|
|
|
1018
1045
|
Relevant source context:
|
|
1019
|
-
${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}
|
|
1046
|
+
${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}${layoutContext}
|
|
1020
1047
|
|
|
1021
1048
|
CRITICAL INSTRUCTIONS:
|
|
1022
1049
|
0. PROJECT STRUCTURE AWARENESS: This project has specific directory structure. Before creating or moving files, verify the actual structure. Do not assume standard locations.
|
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SourceChange, type AgentPrompt } from "./agent";
|
|
2
2
|
import type { CollabConfig } from "./project";
|
|
3
|
+
import { type TimelineSnapshot, type ConflictInfo } from "./snapshot";
|
|
3
4
|
export declare const DEFAULT_BRIDGE_PORT = 3056;
|
|
4
5
|
type GitState = {
|
|
5
6
|
isRepo: boolean;
|
|
@@ -143,6 +144,19 @@ export type BridgeMessage = {
|
|
|
143
144
|
}>;
|
|
144
145
|
taskId?: string;
|
|
145
146
|
pageContext?: any;
|
|
147
|
+
} | {
|
|
148
|
+
type: "resolve_conflict";
|
|
149
|
+
taskId: string;
|
|
150
|
+
action: "retry" | "force" | "rebase";
|
|
151
|
+
conflicts: ConflictInfo[];
|
|
152
|
+
} | {
|
|
153
|
+
type: "restore_snapshot";
|
|
154
|
+
snapshotId: string;
|
|
155
|
+
} | {
|
|
156
|
+
type: "get_timeline";
|
|
157
|
+
} | {
|
|
158
|
+
type: "delete_snapshot";
|
|
159
|
+
snapshotId: string;
|
|
146
160
|
};
|
|
147
161
|
type ModelOption = {
|
|
148
162
|
id: string;
|
|
@@ -235,6 +249,14 @@ export type ServerBridgeMessage = {
|
|
|
235
249
|
text?: string;
|
|
236
250
|
provider?: string;
|
|
237
251
|
error?: string;
|
|
252
|
+
} | {
|
|
253
|
+
type: "conflict_detected";
|
|
254
|
+
taskId: string;
|
|
255
|
+
conflicts: ConflictInfo[];
|
|
256
|
+
message: string;
|
|
257
|
+
} | {
|
|
258
|
+
type: "timeline";
|
|
259
|
+
snapshots: TimelineSnapshot[];
|
|
238
260
|
};
|
|
239
261
|
export declare function restartBridge(port?: number): Promise<{
|
|
240
262
|
ok: boolean;
|
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) {
|
|
@@ -435,6 +436,120 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
435
436
|
fileEnv.NEXT_LASSO_API_KEY ||
|
|
436
437
|
"";
|
|
437
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
|
+
}
|
|
438
553
|
// Fetch workspace configuration from server (dashboard-configured providers)
|
|
439
554
|
let workspaceConfig = null;
|
|
440
555
|
if (lassoKeyConfigured) {
|
|
@@ -698,6 +813,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
698
813
|
};
|
|
699
814
|
wss.on("connection", (socket) => {
|
|
700
815
|
overlaySocket = socket;
|
|
816
|
+
currentSocket = socket;
|
|
701
817
|
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
702
818
|
const creds = (0, auth_1.loadCredentials)();
|
|
703
819
|
const userInfo = creds?.userName
|
|
@@ -712,57 +828,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
712
828
|
if (socket.readyState === socket.OPEN)
|
|
713
829
|
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
714
830
|
});
|
|
715
|
-
const runEditReview = (request, config, statusMessage) => {
|
|
716
|
-
taskControllers.get(request.taskId)?.abort();
|
|
717
|
-
const controller = new AbortController();
|
|
718
|
-
taskControllers.set(request.taskId, controller);
|
|
719
|
-
if (socket.readyState === socket.OPEN) {
|
|
720
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage || "Working…" }));
|
|
721
|
-
}
|
|
722
|
-
void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail, level) => {
|
|
723
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
724
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
|
|
725
|
-
}
|
|
726
|
-
}, (prompt) => {
|
|
727
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
728
|
-
socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
|
|
729
|
-
}
|
|
730
|
-
})
|
|
731
|
-
.then((proposal) => {
|
|
732
|
-
if (controller.signal.aborted || socket.readyState !== socket.OPEN)
|
|
733
|
-
return;
|
|
734
|
-
if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
|
|
735
|
-
const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
|
|
736
|
-
const MAX_REFRESH_ATTEMPTS = 3;
|
|
737
|
-
if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
|
|
738
|
-
reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
|
|
739
|
-
setTimeout(() => {
|
|
740
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
741
|
-
runEditReview(request, config, `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})`);
|
|
742
|
-
}
|
|
743
|
-
}, 500 * (refreshAttempts + 1));
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
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." }));
|
|
747
|
-
}
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
|
|
751
|
-
})
|
|
752
|
-
.catch((error) => {
|
|
753
|
-
if (socket.readyState !== socket.OPEN)
|
|
754
|
-
return;
|
|
755
|
-
if (controller.signal.aborted) {
|
|
756
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
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." }));
|
|
760
|
-
})
|
|
761
|
-
.finally(() => {
|
|
762
|
-
if (taskControllers.get(request.taskId) === controller)
|
|
763
|
-
taskControllers.delete(request.taskId);
|
|
764
|
-
});
|
|
765
|
-
};
|
|
766
831
|
socket.on("message", async (raw) => {
|
|
767
832
|
const msg = JSON.parse(raw.toString());
|
|
768
833
|
if (msg.type === "edit" || msg.type === "ask") {
|
|
@@ -871,23 +936,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
871
936
|
serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv),
|
|
872
937
|
};
|
|
873
938
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
877
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: level === "error" ? "error" : "working", message, detail }));
|
|
878
|
-
}, (prompt) => {
|
|
879
|
-
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
880
|
-
socket.send(JSON.stringify({ type: "agent_prompt", taskId: msg.taskId, prompt }));
|
|
881
|
-
}).then((answer) => {
|
|
882
|
-
if (!controller.signal.aborted)
|
|
883
|
-
socket.send(JSON.stringify({ type: "assistant_message", taskId: msg.taskId, message: answer }));
|
|
884
|
-
}).catch((error) => {
|
|
885
|
-
if (!controller.signal.aborted)
|
|
886
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
|
|
887
|
-
}).finally(() => {
|
|
888
|
-
if (taskControllers.get(msg.taskId) === controller)
|
|
889
|
-
taskControllers.delete(msg.taskId);
|
|
890
|
-
});
|
|
939
|
+
// Enqueue task for concurrent execution
|
|
940
|
+
enqueueTask(msg.taskId, msg, selectedConfig, 1); // Priority 1 for ask tasks
|
|
891
941
|
}
|
|
892
942
|
else if (msg.type === "edit") {
|
|
893
943
|
const localProvider = msg.provider === "cli";
|
|
@@ -926,17 +976,29 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
926
976
|
editRequests.set(msg.taskId, msg);
|
|
927
977
|
editConfigs.set(msg.taskId, selectedConfig);
|
|
928
978
|
reviewRefreshAttempts.set(msg.taskId, 0);
|
|
929
|
-
|
|
979
|
+
// Enqueue task for concurrent execution with higher priority (0)
|
|
980
|
+
enqueueTask(msg.taskId, msg, selectedConfig, 0);
|
|
930
981
|
}
|
|
931
982
|
else if (msg.type === "agent_prompt_response") {
|
|
932
983
|
(0, agent_1.respondToAgentPrompt)(msg.taskId, msg.response);
|
|
933
984
|
}
|
|
934
985
|
else if (msg.type === "stop") {
|
|
935
|
-
if (msg.taskId)
|
|
986
|
+
if (msg.taskId) {
|
|
936
987
|
taskControllers.get(msg.taskId)?.abort();
|
|
937
|
-
|
|
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 {
|
|
938
996
|
for (const controller of taskControllers.values())
|
|
939
997
|
controller.abort();
|
|
998
|
+
taskQueue.length = 0;
|
|
999
|
+
runningTasks.clear();
|
|
1000
|
+
taskPriority.clear();
|
|
1001
|
+
}
|
|
940
1002
|
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "stopped", message: "Agent stopped." }));
|
|
941
1003
|
}
|
|
942
1004
|
else if (msg.type === "runtime_error") {
|
|
@@ -1121,8 +1183,28 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1121
1183
|
}
|
|
1122
1184
|
else if (msg.type === "apply") {
|
|
1123
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);
|
|
1124
1191
|
const snapshots = [];
|
|
1125
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
|
+
}
|
|
1126
1208
|
for (const change of msg.changes) {
|
|
1127
1209
|
const isNew = !change.oldString;
|
|
1128
1210
|
const filePath = resolveProposedFile(cwd, change.filePath, true);
|
|
@@ -1153,6 +1235,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1153
1235
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
1154
1236
|
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
1155
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);
|
|
1156
1240
|
// Run build validation after applying changes
|
|
1157
1241
|
const validation = await validateBuild(cwd);
|
|
1158
1242
|
if (!validation.passed) {
|
|
@@ -1203,7 +1287,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1203
1287
|
const refreshAttempts = reviewRefreshAttempts.get(msg.taskId) || 0;
|
|
1204
1288
|
if (sourceChanged && editRequest && editConfig && refreshAttempts < 1) {
|
|
1205
1289
|
reviewRefreshAttempts.set(msg.taskId, refreshAttempts + 1);
|
|
1206
|
-
|
|
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…" }));
|
|
1207
1292
|
}
|
|
1208
1293
|
else {
|
|
1209
1294
|
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message }));
|
|
@@ -1224,9 +1309,117 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1224
1309
|
taskSnapshots.delete(msg.taskId || "");
|
|
1225
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." }));
|
|
1226
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
|
+
}
|
|
1227
1419
|
});
|
|
1228
1420
|
socket.on("close", () => {
|
|
1229
1421
|
overlaySocket = null;
|
|
1422
|
+
currentSocket = null;
|
|
1230
1423
|
console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
|
|
1231
1424
|
});
|
|
1232
1425
|
});
|
|
@@ -1243,8 +1436,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1243
1436
|
});
|
|
1244
1437
|
bridgeServer.listen(bridgePort, "127.0.0.1");
|
|
1245
1438
|
function send(msg) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1439
|
+
const socket = currentSocket;
|
|
1440
|
+
if (socket && socket.readyState === socket.OPEN) {
|
|
1441
|
+
socket.send(JSON.stringify(msg));
|
|
1248
1442
|
}
|
|
1249
1443
|
}
|
|
1250
1444
|
return {
|