@lasso-ai/cli 1.3.2 → 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 CHANGED
@@ -19,6 +19,7 @@ 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
+ const layout_intelligence_1 = require("@lasso-ai/layout-intelligence");
22
23
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
23
24
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
24
25
  function detectProjectStructure(cwd) {
@@ -1002,6 +1003,22 @@ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt)
1002
1003
  : "";
1003
1004
  // Detect project structure for instruction
1004
1005
  const structure = detectProjectStructure(cwd);
1006
+ // Query layout intelligence for relevant patterns
1007
+ let layoutContext = "";
1008
+ try {
1009
+ const layoutQuery = input.instruction;
1010
+ const relevantLayouts = await (0, layout_intelligence_1.searchLayouts)(layoutQuery);
1011
+ if (relevantLayouts.length > 0) {
1012
+ layoutContext = `\n\nLAYOUT INTELLIGENCE:
1013
+ Based on your request, here are relevant layout patterns you should consider:
1014
+ ${relevantLayouts.slice(0, 3).map((result) => `- ${result.pattern.name}: ${result.pattern.description}`).join("\n")}
1015
+ Use these patterns as inspiration for your implementation. Focus on matching the layout structure, spacing, and composition principles.`;
1016
+ }
1017
+ }
1018
+ catch (error) {
1019
+ // Layout intelligence is optional; continue without it if it fails
1020
+ console.warn("Layout intelligence query failed:", error);
1021
+ }
1005
1022
  const instruction = `Selection context:
1006
1023
  ${JSON.stringify(input.element, null, 2)}
1007
1024
  ${JSON.stringify(visualContext, null, 2)}
@@ -1016,7 +1033,7 @@ Previous change history for this selection:
1016
1033
  ${priorChanges}
1017
1034
 
1018
1035
  Relevant source context:
1019
- ${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}
1036
+ ${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}${layoutContext}
1020
1037
 
1021
1038
  CRITICAL INSTRUCTIONS:
1022
1039
  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 {
@@ -209,16 +209,13 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
209
209
  const isConversational = await isConversationalPrompt(prompt, config, apiKey);
210
210
  console.log('[Lasso] Is conversational:', isConversational);
211
211
  // Browser inspection & snapshot steps - skip for simple conversational queries
212
+ // FAST CONTEXT: Skip detailed DOM analysis if pageContext is already provided
212
213
  if (pageContext && !isConversational) {
213
- onProgress("thinking", "Inspecting browser page...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
214
+ onProgress("thinking", "Analyzing page context...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
215
+ // Skip the slow "Scrolling & analyzing" steps - the pageContext already has the data
216
+ // Only show viewport info if available
214
217
  if (pageContext.viewport) {
215
- onProgress("thinking", "Capturing visual snapshot...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height} (Document height: ${pageContext.viewport.scrollHeight}px)`);
216
- }
217
- if (pageContext.domSummary) {
218
- const sectionsCount = pageContext.domSummary.sections?.length || 0;
219
- const headingsCount = pageContext.domSummary.headings?.length || 0;
220
- const buttonsCount = pageContext.domSummary.buttons?.length || 0;
221
- onProgress("thinking", "Scrolling & analyzing page sections...", `${sectionsCount} sections, ${headingsCount} headings, ${buttonsCount} CTAs detected`);
218
+ onProgress("thinking", "Ready to proceed...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height}`);
222
219
  }
223
220
  }
224
221
  else {
@@ -599,47 +596,69 @@ async function analyzeProject(cwd) {
599
596
  const hasReact = dependencies.includes("react") || devDependencies.includes("react");
600
597
  const hasNext = dependencies.includes("next") || devDependencies.includes("next");
601
598
  const hasVite = dependencies.includes("vite") || devDependencies.includes("vite");
602
- // Scan directory structure
599
+ // FAST CONTEXT: Only scan top-level directories, not full recursive scan
600
+ // This is the key optimization - let the server-side agent handle file discovery
603
601
  const files = [];
604
602
  const directories = [];
605
603
  const appDirs = [];
606
604
  const pagesDirs = [];
607
- const roots = ["src", "app", "pages", ".", "web", "lib", "components"];
605
+ const roots = ["src", "app", "pages", "components", "lib"];
608
606
  for (const root of roots) {
609
607
  const rootPath = node_path_1.default.join(cwd, root);
610
608
  if (!node_fs_1.default.existsSync(rootPath))
611
609
  continue;
612
- const scanDir = (dir, base = "", depth = 0) => {
613
- if (depth > 5)
614
- return;
615
- try {
616
- const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
617
- for (const entry of entries) {
618
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === ".next") {
619
- continue;
620
- }
621
- const rel = base ? node_path_1.default.join(base, entry.name) : entry.name;
622
- if (entry.isDirectory()) {
623
- directories.push(rel);
624
- // Track app and pages directories
625
- if (entry.name === "app")
626
- appDirs.push(rel);
627
- if (entry.name === "pages")
628
- pagesDirs.push(rel);
629
- scanDir(node_path_1.default.join(dir, entry.name), rel, depth + 1);
630
- }
631
- else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
632
- files.push(rel);
610
+ // Only scan top-level (depth 0-1), not recursive
611
+ try {
612
+ const entries = node_fs_1.default.readdirSync(rootPath, { withFileTypes: true });
613
+ for (const entry of entries) {
614
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
615
+ continue;
616
+ }
617
+ const rel = entry.name;
618
+ if (entry.isDirectory()) {
619
+ directories.push(rel);
620
+ // Track app and pages directories at top level
621
+ if (entry.name === "app")
622
+ appDirs.push(rel);
623
+ if (entry.name === "pages")
624
+ pagesDirs.push(rel);
625
+ // Only scan one level deep for key directories
626
+ if (["components", "lib", "hooks", "utils", "styles"].includes(entry.name)) {
627
+ try {
628
+ const subEntries = node_fs_1.default.readdirSync(node_path_1.default.join(rootPath, entry.name), { withFileTypes: true });
629
+ for (const subEntry of subEntries) {
630
+ if (!subEntry.name.startsWith(".") && !subEntry.isDirectory() && /\.(tsx?|jsx?|vue|svelte|css)$/i.test(subEntry.name)) {
631
+ files.push(node_path_1.default.join(rel, subEntry.name));
632
+ }
633
+ }
634
+ }
635
+ catch {
636
+ // ignore scan errors
637
+ }
633
638
  }
634
639
  }
640
+ else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
641
+ files.push(rel);
642
+ }
635
643
  }
636
- catch {
637
- // ignore scan errors
644
+ }
645
+ catch {
646
+ // ignore scan errors
647
+ }
648
+ if (files.length > 20)
649
+ break; // Limit to 20 files for fast context
650
+ }
651
+ // Also check root directory for key files
652
+ try {
653
+ const rootEntries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
654
+ for (const entry of rootEntries) {
655
+ if (!entry.isDirectory() && /\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
656
+ files.push(entry.name);
638
657
  }
639
- };
640
- scanDir(rootPath);
641
- if (files.length > 100)
642
- break;
658
+ }
659
+ }
660
+ catch {
661
+ // ignore scan errors
643
662
  }
644
663
  // Determine source root
645
664
  let sourceRoot = ".";
@@ -649,12 +668,9 @@ async function analyzeProject(cwd) {
649
668
  sourceRoot = ".";
650
669
  else if (directories.includes("src"))
651
670
  sourceRoot = "src";
652
- // Detect App Router vs Pages Router
653
- const appRouter = appDirs.length > 0 && appDirs.some(dir => {
654
- const appPath = node_path_1.default.join(cwd, dir);
655
- return node_fs_1.default.existsSync(node_path_1.default.join(appPath, "layout.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(appPath, "page.tsx"));
656
- });
657
- const pagesRouter = pagesDirs.length > 0 && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_app.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_document.tsx"));
671
+ // Detect App Router vs Pages Router (lightweight check)
672
+ const appRouter = appDirs.length > 0 && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "app", "layout.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "app", "page.tsx"));
673
+ const pagesRouter = pagesDirs.length > 0 && (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_app.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_document.tsx")));
658
674
  return {
659
675
  framework: (0, framework_1.detectFramework)(cwd),
660
676
  hasTypeScript,