@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 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.
@@ -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;
@@ -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
- socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "Working…" }));
875
- void (0, agent_1.answerQuestion)(cwd, { taskId: msg.taskId, question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail, level) => {
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
- runEditReview(msg, selectedConfig);
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
- else
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
- runEditReview(editRequest, editConfig, "The source changed. Refreshing the review against the current file…");
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
- if (overlaySocket?.readyState === overlaySocket?.OPEN) {
1247
- overlaySocket.send(JSON.stringify(msg));
1439
+ const socket = currentSocket;
1440
+ if (socket && socket.readyState === socket.OPEN) {
1441
+ socket.send(JSON.stringify(msg));
1248
1442
  }
1249
1443
  }
1250
1444
  return {