@lasso-ai/cli 2.1.8 → 2.1.10

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.
@@ -23,13 +23,34 @@ const local_runtime_1 = require("./local-runtime");
23
23
  exports.DEFAULT_BRIDGE_PORT = 3056;
24
24
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
25
25
  function isConversationalPrompt(prompt) {
26
- const value = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
27
- if (/^(hi|hello|hey|heya|howdy|sup|yo|good morning|good afternoon|good evening|thanks|thank you|thx|ty|what can you do|what can i ask you|who are you|how do you work|how does this work)$/.test(value))
26
+ const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
27
+ // 1. Direct greetings and conversational niceties
28
+ const greetings = [
29
+ "hi", "hello", "hey", "heya", "howdy", "sup", "yo",
30
+ "good morning", "good afternoon", "good evening",
31
+ "thanks", "thank you", "thx", "ty", "cool", "awesome", "great", "nice",
32
+ "who are you", "what are you", "what can you do", "help", "help me",
33
+ "what is lasso", "how do you work", "how does this work",
34
+ ];
35
+ if (greetings.includes(p))
28
36
  return true;
29
- if (/^(what can you do|what can i ask|who are you|how do you work|how does this work)\b/.test(value))
37
+ // Short greetings
38
+ if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
39
+ const buildKeywords = /\b(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)\b/i;
40
+ if (!buildKeywords.test(p))
41
+ return true;
42
+ }
43
+ // 2. Action verbs indicating deliberate code changes
44
+ const mutationVerbs = /\b(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code|replace|set|switch|swap|rewrite|restyle|rebuild|apply)\b/i;
45
+ // 3. Question / informational prefixes
46
+ const infoPrefixes = /^(what|how|why|when|where|who|which|can you explain|explain|describe|tell me|check out|check|look at|summarize|give me an overview|overview|inspect)\b/i;
47
+ if (infoPrefixes.test(p) && !mutationVerbs.test(p)) {
30
48
  return true;
31
- if (/^(what is|what's|how do i|why does|can you explain|tell me about)\b/.test(value))
49
+ }
50
+ // 4. Questions about the project / app / page without mutation verbs
51
+ if (/\b(about|doing|does this|is this|mean|purpose|work|repo|project|codebase|app|application|page|overview|summary)\b/i.test(p) && !mutationVerbs.test(p)) {
32
52
  return true;
53
+ }
33
54
  return false;
34
55
  }
35
56
  function cleanConversationReply(value) {
@@ -473,11 +494,18 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
473
494
  return { filePath: change.filePath, content };
474
495
  }));
475
496
  (0, snapshot_1.createSnapshot)(cwd, taskId, instruction, changes.map((change) => change.filePath), undefined, session.id);
497
+ // Extract the assistant's reply from the session conversation so the user
498
+ // sees both the explanation and the review panel — not just "Review changes".
499
+ const sessionConvo = finalSession.conversation || [];
500
+ const assistantReply = [...sessionConvo]
501
+ .reverse()
502
+ .find((m) => m?.role === "assistant" && typeof m.content === "string")
503
+ ?.content?.trim();
476
504
  socket.send(JSON.stringify({
477
505
  type: "agent_status",
478
506
  taskId,
479
507
  status: "review",
480
- message: "Review changes",
508
+ message: assistantReply || "Review changes",
481
509
  changes,
482
510
  }));
483
511
  }
@@ -495,7 +523,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
495
523
  }
496
524
  }
497
525
  // Handler for edit/ask - uses runtime
498
- async function handleHostedConversation(taskId, prompt, model, provider, messages, socket, signal) {
526
+ async function handleHostedConversation(taskId, prompt, model, provider, messages, socket, signal, conversationId) {
499
527
  const requestController = new AbortController();
500
528
  const abortRequest = () => requestController.abort();
501
529
  signal.addEventListener("abort", abortRequest, { once: true });
@@ -521,6 +549,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
521
549
  prompt,
522
550
  model: { id: model, provider },
523
551
  messages: Array.isArray(messages) ? messages : undefined,
552
+ conversationId: conversationId || undefined,
553
+ projectId: projectId || undefined,
524
554
  }),
525
555
  signal: requestController.signal,
526
556
  });
@@ -578,7 +608,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
578
608
  taskControllers.set(taskId, controller);
579
609
  const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
580
610
  if ((message.type === "ask" || isConversationalPrompt(instruction)) && !isLocalAgent) {
581
- await handleHostedConversation(taskId, instruction, model, provider, messages, socket, controller.signal);
611
+ await handleHostedConversation(taskId, instruction, model, provider, messages, socket, controller.signal, message.conversationId);
582
612
  return;
583
613
  }
584
614
  // Do not echo the user's prompt into the thinking feed. The prompt is
@@ -907,7 +937,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
907
937
  try {
908
938
  const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
909
939
  if (isConversationalPrompt(prompt) && !isLocalAgent) {
910
- await handleHostedConversation(taskId, prompt, model, provider, message.messages, socket, controller.signal);
940
+ await handleHostedConversation(taskId, prompt, model, provider, message.messages, socket, controller.signal, message.conversationId);
911
941
  return;
912
942
  }
913
943
  const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
package/dist/overlay.js CHANGED
@@ -19632,12 +19632,15 @@
19632
19632
  ];
19633
19633
  if (greetings.includes(p)) return true;
19634
19634
  if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
19635
- const buildKeywords = /(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)/i;
19635
+ const buildKeywords = /\b(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)\b/i;
19636
19636
  if (!buildKeywords.test(p)) return true;
19637
19637
  }
19638
- const questionOnly = /^(what is|what's|how do i|why does|can you explain|tell me about)\b/i.test(p);
19639
- const actionVerbs = /(build|create|add|make|fix|update|modify|change|refactor|implement|write code|code this|delete|remove|generate)/i;
19640
- if (questionOnly && !actionVerbs.test(p)) {
19638
+ const mutationVerbs = /\b(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code|replace|set|switch|swap|rewrite|restyle|rebuild|apply)\b/i;
19639
+ const infoPrefixes = /^(what|how|why|when|where|who|which|can you explain|explain|describe|tell me|check out|check|look at|summarize|give me an overview|overview|inspect)\b/i;
19640
+ if (infoPrefixes.test(p) && !mutationVerbs.test(p)) {
19641
+ return true;
19642
+ }
19643
+ if (/\b(about|doing|does this|is this|mean|purpose|work|repo|project|codebase|app|application|page|overview|summary)\b/i.test(p) && !mutationVerbs.test(p)) {
19641
19644
  return true;
19642
19645
  }
19643
19646
  return false;
@@ -19676,6 +19679,63 @@
19676
19679
  return null;
19677
19680
  }
19678
19681
  }
19682
+ function getStorageProjectKey() {
19683
+ const proj = state.collab?.projectId || state.collabProjectId || "default";
19684
+ return `lasso:sessions:${proj}`;
19685
+ }
19686
+ function saveLocalSession() {
19687
+ try {
19688
+ const projKey = getStorageProjectKey();
19689
+ if (!activeConversationId) {
19690
+ activeConversationId = `local-session-${Date.now()}`;
19691
+ }
19692
+ const sessionData = {
19693
+ id: activeConversationId,
19694
+ messages: commandMessages,
19695
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
19696
+ title: commandMessages.find((m) => m.role === "user")?.content?.slice(0, 80) || "Untitled Session"
19697
+ };
19698
+ localStorage.setItem(`lasso:session:${activeConversationId}`, JSON.stringify(sessionData));
19699
+ const listRaw = localStorage.getItem(projKey);
19700
+ let list2 = listRaw ? JSON.parse(listRaw) : [];
19701
+ list2 = list2.filter((item) => item.id !== activeConversationId);
19702
+ list2.unshift({
19703
+ id: activeConversationId,
19704
+ title: sessionData.title,
19705
+ updatedAt: sessionData.updatedAt
19706
+ });
19707
+ localStorage.setItem(projKey, JSON.stringify(list2.slice(0, 30)));
19708
+ } catch (err) {
19709
+ }
19710
+ }
19711
+ function loadLocalSession(convId) {
19712
+ try {
19713
+ const raw = localStorage.getItem(`lasso:session:${convId}`);
19714
+ if (!raw) return false;
19715
+ const session = JSON.parse(raw);
19716
+ if (session && Array.isArray(session.messages)) {
19717
+ commandMessages = session.messages;
19718
+ activeConversationId = convId;
19719
+ renderMessages();
19720
+ scrollToBottom();
19721
+ return true;
19722
+ }
19723
+ } catch {
19724
+ }
19725
+ return false;
19726
+ }
19727
+ function restoreLatestSession() {
19728
+ try {
19729
+ const projKey = getStorageProjectKey();
19730
+ const listRaw = localStorage.getItem(projKey);
19731
+ if (!listRaw) return;
19732
+ const list2 = JSON.parse(listRaw);
19733
+ if (list2 && list2.length > 0 && !activeConversationId) {
19734
+ loadLocalSession(list2[0].id);
19735
+ }
19736
+ } catch {
19737
+ }
19738
+ }
19679
19739
  async function createServerConversation(scope2 = "private") {
19680
19740
  const projectId = state.collab?.projectId || state.collabProjectId;
19681
19741
  if (!projectId) return null;
@@ -19687,10 +19747,29 @@
19687
19747
  }
19688
19748
  async function loadConversationList() {
19689
19749
  const projectId = state.collab?.projectId || state.collabProjectId;
19750
+ try {
19751
+ const projKey = getStorageProjectKey();
19752
+ const listRaw = localStorage.getItem(projKey);
19753
+ if (listRaw) {
19754
+ const localList = JSON.parse(listRaw);
19755
+ conversationList = localList.map((item) => ({
19756
+ id: item.id,
19757
+ title: item.title,
19758
+ scope: "local",
19759
+ updatedAt: item.updatedAt
19760
+ }));
19761
+ }
19762
+ } catch {
19763
+ }
19690
19764
  if (!projectId) return;
19691
19765
  const data6 = await apiFetch(`/agent/conversations?projectId=${encodeURIComponent(projectId)}`);
19692
- if (data6?.conversations) {
19693
- conversationList = data6.conversations;
19766
+ if (data6?.conversations && Array.isArray(data6.conversations)) {
19767
+ const serverIds = new Set(data6.conversations.map((c) => c.id));
19768
+ const merged = [
19769
+ ...data6.conversations,
19770
+ ...conversationList.filter((local) => !serverIds.has(local.id))
19771
+ ];
19772
+ conversationList = merged;
19694
19773
  }
19695
19774
  }
19696
19775
  function formatDateAgo(dateStr) {
@@ -19742,6 +19821,9 @@
19742
19821
  activeConversationId = convId;
19743
19822
  if (commandHistoryPopover) commandHistoryPopover.hidden = true;
19744
19823
  commandHistoryBtn?.classList.remove("active");
19824
+ if (loadLocalSession(convId)) {
19825
+ return;
19826
+ }
19745
19827
  const data6 = await apiFetch(`/agent/conversations/${convId}`);
19746
19828
  if (data6?.conversation?.messages && Array.isArray(data6.conversation.messages)) {
19747
19829
  commandMessages = data6.conversation.messages.map((m, idx) => ({
@@ -19759,6 +19841,7 @@
19759
19841
  changesApplied: m.changesApplied,
19760
19842
  error: m.error
19761
19843
  }));
19844
+ saveLocalSession();
19762
19845
  } else {
19763
19846
  commandMessages = [];
19764
19847
  }
@@ -20255,6 +20338,12 @@
20255
20338
  }
20256
20339
  return;
20257
20340
  }
20341
+ const continueBtn = target.closest(".lasso-command-continue-btn");
20342
+ if (continueBtn) {
20343
+ const msgId = continueBtn.closest(".lasso-command-msg")?.dataset.msgId;
20344
+ continueWithThoughts(msgId);
20345
+ return;
20346
+ }
20258
20347
  const retryBtn = target.closest(".lasso-command-retry-btn");
20259
20348
  if (retryBtn) {
20260
20349
  const msgId = retryBtn.closest(".lasso-command-msg")?.dataset.msgId;
@@ -20286,6 +20375,9 @@
20286
20375
  updateEmptyStateUserName();
20287
20376
  renderSuggestions();
20288
20377
  populateModelMenu();
20378
+ if (commandMessages.length === 0) {
20379
+ restoreLatestSession();
20380
+ }
20289
20381
  renderMessages();
20290
20382
  loadConversationList();
20291
20383
  }
@@ -20356,6 +20448,24 @@
20356
20448
  commandMessages.splice(failedIndex, 1);
20357
20449
  void runPrompt(promptToRetry, true);
20358
20450
  }
20451
+ function continueWithThoughts(msgId) {
20452
+ if (activeTaskId) return;
20453
+ const targetMsg = msgId ? commandMessages.find((m) => m.id === msgId) : [...commandMessages].reverse().find((m) => m.role === "assistant");
20454
+ if (!targetMsg) return;
20455
+ const targetIndex = commandMessages.indexOf(targetMsg);
20456
+ const precedingUserMsg = [...commandMessages].slice(0, targetIndex).reverse().find((m) => m.role === "user");
20457
+ const originalPrompt = precedingUserMsg?.content || "";
20458
+ const thoughtSummary = (targetMsg.thinking?.steps || []).map((s) => `- ${s.title}${s.detail ? `: ${s.detail}` : ""}`).join("\n");
20459
+ const continuationPrompt = thoughtSummary ? `Continue executing the previous task: "${originalPrompt}".
20460
+
20461
+ Here is your prior analysis and plan:
20462
+ ${thoughtSummary}
20463
+
20464
+ Please proceed directly with the implementation and code changes from this point.` : originalPrompt;
20465
+ targetMsg.error = void 0;
20466
+ saveLocalSession();
20467
+ void runPrompt(continuationPrompt);
20468
+ }
20359
20469
  async function handleSubmit() {
20360
20470
  if (!commandInput) return;
20361
20471
  if (activeTaskId) {
@@ -20587,6 +20697,7 @@
20587
20697
  });
20588
20698
  }
20589
20699
  }
20700
+ saveLocalSession();
20590
20701
  renderMessages();
20591
20702
  scrollToBottom();
20592
20703
  } else if (message.status === "error" || message.status === "stopped") {
@@ -20607,6 +20718,7 @@
20607
20718
  sessionId: state.collabProjectId,
20608
20719
  status: "idle"
20609
20720
  });
20721
+ saveLocalSession();
20610
20722
  renderMessages();
20611
20723
  scrollToBottom();
20612
20724
  }
@@ -20685,10 +20797,18 @@
20685
20797
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
20686
20798
  ${escapeHtml4(msg.error)}
20687
20799
  </div>
20688
- <button class="lasso-command-retry-btn" type="button" title="Retry with current model">
20689
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
20690
- Retry
20691
- </button>
20800
+ <div class="lasso-command-error-actions" style="display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap;">
20801
+ ${msg.thinking && msg.thinking.steps.length > 0 ? `
20802
+ <button class="lasso-command-continue-btn" type="button" title="Continue execution using the thoughts and analysis generated so far" style="display: inline-flex; align-items: center; gap: 5px; padding: 5px 10px; font-size: 12px; font-weight: 500; border-radius: 6px; background: rgba(59, 130, 246, 0.15); border: 1px solid rgba(59, 130, 246, 0.3); color: #93c5fd; cursor: pointer;">
20803
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
20804
+ Continue with thoughts so far
20805
+ </button>
20806
+ ` : ""}
20807
+ <button class="lasso-command-retry-btn" type="button" title="Retry from start" style="display: inline-flex; align-items: center; gap: 5px; padding: 5px 10px; font-size: 12px; font-weight: 500; border-radius: 6px; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.15); color: #e2e8f0; cursor: pointer;">
20808
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
20809
+ Retry
20810
+ </button>
20811
+ </div>
20692
20812
  ` : ""}
20693
20813
  </div>
20694
20814
  `;
@@ -49,6 +49,15 @@ const DEFAULT_IGNORE_DIRS = new Set([
49
49
  ".gradle",
50
50
  ".terraform",
51
51
  ]);
52
+ const DEFAULT_IGNORE_FILES = new Set([
53
+ "next-env.d.ts",
54
+ "tsconfig.tsbuildinfo",
55
+ ".DS_Store",
56
+ "Thumbs.db",
57
+ ]);
58
+ function shouldIgnoreFile(name) {
59
+ return DEFAULT_IGNORE_FILES.has(name) || name.endsWith(".tsbuildinfo");
60
+ }
52
61
  function hashBuffer(buf) {
53
62
  return node_crypto_1.default.createHash("sha256").update(buf).digest("hex");
54
63
  }
@@ -110,8 +119,9 @@ class WorkspaceGuard {
110
119
  walk(absPath, relPath, depth + 1);
111
120
  continue;
112
121
  }
113
- if (!entry.isFile()) {
114
- guard.skipped.push(relPath);
122
+ if (!entry.isFile() || shouldIgnoreFile(entry.name)) {
123
+ if (!entry.isFile())
124
+ guard.skipped.push(relPath);
115
125
  continue;
116
126
  }
117
127
  try {
@@ -278,7 +288,7 @@ class WorkspaceGuard {
278
288
  walk(absPath, relPath, depth + 1);
279
289
  continue;
280
290
  }
281
- if (!entry.isFile())
291
+ if (!entry.isFile() || shouldIgnoreFile(entry.name))
282
292
  continue;
283
293
  if (this.ignoreFn && this.ignoreFn(relPath, false))
284
294
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "2.1.8",
3
+ "version": "2.1.10",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",