@lasso-ai/cli 1.0.18 → 1.0.19

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.
@@ -14,6 +14,7 @@ export type BridgeMessage = {
14
14
  from: "overlay" | "cli";
15
15
  } | {
16
16
  type: "edit";
17
+ taskId: string;
17
18
  instruction: string;
18
19
  model: string;
19
20
  provider?: "anthropic" | "openai" | "google" | "ollama" | "cli";
@@ -48,6 +49,7 @@ export type BridgeMessage = {
48
49
  };
49
50
  } | {
50
51
  type: "ask";
52
+ taskId: string;
51
53
  question: string;
52
54
  model: string;
53
55
  provider?: "anthropic" | "openai" | "google" | "ollama" | "cli";
@@ -81,11 +83,14 @@ export type BridgeMessage = {
81
83
  details: string;
82
84
  } | {
83
85
  type: "apply";
86
+ taskId: string;
84
87
  changes: SourceChange[];
85
88
  } | {
86
89
  type: "undo";
90
+ taskId?: string;
87
91
  } | {
88
92
  type: "stop";
93
+ taskId?: string;
89
94
  } | {
90
95
  type: "git_status";
91
96
  } | {
@@ -101,6 +106,7 @@ export type BridgeMessage = {
101
106
  type: "git_push";
102
107
  } | {
103
108
  type: "agent_status";
109
+ taskId?: string;
104
110
  status: "thinking" | "working" | "review" | "error" | "stopped";
105
111
  message: string;
106
112
  } | {
@@ -134,18 +140,22 @@ export type ServerBridgeMessage = {
134
140
  error?: string;
135
141
  } | {
136
142
  type: "agent_status";
143
+ taskId?: string;
137
144
  status: "thinking" | "working" | "review" | "error" | "stopped";
138
145
  message: string;
139
146
  detail?: string;
140
147
  changes?: SourceChange[];
141
148
  } | {
142
149
  type: "assistant_message";
150
+ taskId?: string;
143
151
  message: string;
144
152
  } | {
145
153
  type: "applied";
154
+ taskId?: string;
146
155
  message: string;
147
156
  } | {
148
157
  type: "undone";
158
+ taskId?: string;
149
159
  message: string;
150
160
  } | {
151
161
  type: "transcribe_result";
@@ -147,10 +147,10 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
147
147
  const bridgeServer = node_http_1.default.createServer(); // dedicated, empty HTTP server
148
148
  const wss = new ws_1.WebSocketServer({ server: bridgeServer });
149
149
  let overlaySocket = null;
150
- let lastSnapshot = [];
151
- let lastEditRequest = null;
152
- let lastEditConfig = null;
153
- let reviewRefreshAttempts = 0;
150
+ const taskSnapshots = new Map();
151
+ const editRequests = new Map();
152
+ const editConfigs = new Map();
153
+ const reviewRefreshAttempts = new Map();
154
154
  const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
155
155
  const fileEnv = envRoots.reduce((values, root) => ({
156
156
  ...values,
@@ -179,11 +179,15 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
179
179
  };
180
180
  })();
181
181
  let activeAgentController = null;
182
+ const taskControllers = new Map();
182
183
  let localAgents = new Set();
183
184
  bridgeServer.on("request", (req, res) => {
184
185
  if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
185
186
  return;
186
187
  activeAgentController?.abort();
188
+ for (const controller of taskControllers.values())
189
+ controller.abort();
190
+ taskControllers.clear();
187
191
  activeAgentController = null;
188
192
  for (const socket of wss.clients)
189
193
  socket.close(1000, "Bridge restarted by the CLI");
@@ -270,40 +274,41 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
270
274
  socket.send(JSON.stringify({ type: "git_state", git }));
271
275
  });
272
276
  const runEditReview = (request, config, statusMessage) => {
273
- activeAgentController?.abort();
277
+ taskControllers.get(request.taskId)?.abort();
274
278
  const controller = new AbortController();
275
- activeAgentController = controller;
279
+ taskControllers.set(request.taskId, controller);
276
280
  if (statusMessage && socket.readyState === socket.OPEN) {
277
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message: statusMessage }));
281
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage }));
278
282
  }
279
283
  void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail) => {
280
284
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
281
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
285
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message, detail }));
282
286
  }
283
287
  })
284
288
  .then((proposal) => {
285
289
  if (controller.signal.aborted || socket.readyState !== socket.OPEN)
286
290
  return;
287
291
  if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
288
- if (reviewRefreshAttempts < 1) {
289
- reviewRefreshAttempts += 1;
292
+ const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
293
+ if (refreshAttempts < 1) {
294
+ reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
290
295
  runEditReview(request, config, "The source changed while the proposal was being prepared. Refreshing the review…");
291
296
  }
292
297
  else {
293
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "The source is still changing. Stop the dev-server edit or try the request again." }));
298
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing. Stop the dev-server edit or try the request again." }));
294
299
  }
295
300
  return;
296
301
  }
297
- socket.send(JSON.stringify({ type: "agent_status", status: "review", message: proposal.summary, changes: proposal.changes }));
302
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
298
303
  })
299
304
  .catch((error) => {
300
305
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
301
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
306
+ 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." }));
302
307
  }
303
308
  })
304
309
  .finally(() => {
305
- if (activeAgentController === controller)
306
- activeAgentController = null;
310
+ if (taskControllers.get(request.taskId) === controller)
311
+ taskControllers.delete(request.taskId);
307
312
  });
308
313
  };
309
314
  socket.on("message", async (raw) => {
@@ -318,56 +323,59 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
318
323
  const localProvider = msg.provider === "cli";
319
324
  const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
320
325
  if (!lassoKeyConfigured && !localProvider) {
321
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
326
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
322
327
  return;
323
328
  }
324
329
  if (!agentConfig && !localProvider) {
325
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "No hosted agent is configured. Select Claude Code/Codex or add a provider key." }));
330
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "No hosted agent is configured. Select Claude Code/Codex or add a provider key." }));
326
331
  return;
327
332
  }
328
- activeAgentController?.abort();
333
+ taskControllers.get(msg.taskId)?.abort();
329
334
  const controller = new AbortController();
330
- activeAgentController = controller;
335
+ taskControllers.set(msg.taskId, controller);
331
336
  const selectedConfig = localProvider
332
337
  ? { provider: cliProvider, model: msg.model }
333
338
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
334
339
  void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
335
340
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
336
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
341
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message, detail }));
337
342
  }).then((answer) => {
338
343
  if (!controller.signal.aborted)
339
- socket.send(JSON.stringify({ type: "assistant_message", message: answer }));
344
+ socket.send(JSON.stringify({ type: "assistant_message", taskId: msg.taskId, message: answer }));
340
345
  }).catch((error) => {
341
346
  if (!controller.signal.aborted)
342
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
347
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
343
348
  }).finally(() => {
344
- if (activeAgentController === controller)
345
- activeAgentController = null;
349
+ if (taskControllers.get(msg.taskId) === controller)
350
+ taskControllers.delete(msg.taskId);
346
351
  });
347
352
  }
348
353
  else if (msg.type === "edit") {
349
354
  const localProvider = msg.provider === "cli";
350
355
  const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
351
356
  if (!lassoKeyConfigured && !localProvider) {
352
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
357
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
353
358
  return;
354
359
  }
355
360
  if (!agentConfig && !localProvider) {
356
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Add a supported agent key: GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." }));
361
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Add a supported agent key: GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." }));
357
362
  return;
358
363
  }
359
364
  const selectedConfig = localProvider
360
365
  ? { provider: cliProvider, model: msg.model }
361
366
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
362
- lastEditRequest = msg;
363
- lastEditConfig = selectedConfig;
364
- reviewRefreshAttempts = 0;
367
+ editRequests.set(msg.taskId, msg);
368
+ editConfigs.set(msg.taskId, selectedConfig);
369
+ reviewRefreshAttempts.set(msg.taskId, 0);
365
370
  runEditReview(msg, selectedConfig);
366
371
  }
367
372
  else if (msg.type === "stop") {
368
- activeAgentController?.abort();
369
- activeAgentController = null;
370
- socket.send(JSON.stringify({ type: "agent_status", status: "stopped", message: "Agent stopped." }));
373
+ if (msg.taskId)
374
+ taskControllers.get(msg.taskId)?.abort();
375
+ else
376
+ for (const controller of taskControllers.values())
377
+ controller.abort();
378
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "stopped", message: "Agent stopped." }));
371
379
  }
372
380
  else if (msg.type === "runtime_error") {
373
381
  socket.send(JSON.stringify({ type: "agent_status", status: "error", message: `Runtime error detected${msg.selectionId ? ` for selection ${msg.selectionId}` : ""}: ${msg.details}` }));
@@ -468,7 +476,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
468
476
  }
469
477
  else if (msg.type === "apply") {
470
478
  try {
471
- lastSnapshot = [];
479
+ const snapshots = [];
472
480
  const planned = new Map();
473
481
  for (const change of msg.changes) {
474
482
  const filePath = resolveProposedFile(cwd, change.filePath);
@@ -483,24 +491,31 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
483
491
  }
484
492
  // Validate every change before writing any file, then apply each
485
493
  // file's replacements from the end toward the beginning.
494
+ taskSnapshots.set(msg.taskId, snapshots);
486
495
  for (const [filePath, changes] of planned) {
487
496
  const content = changes[0].content;
488
- lastSnapshot.push({ filePath, content });
497
+ snapshots.push({ filePath, content });
489
498
  const nextContent = [...changes]
490
499
  .sort((a, b) => b.start - a.start)
491
500
  .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content);
492
501
  node_fs_1.default.writeFileSync(filePath, nextContent);
493
502
  }
494
- socket.send(JSON.stringify({ type: "applied", message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.` }));
503
+ socket.send(JSON.stringify({ type: "applied", taskId: msg.taskId, message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.` }));
495
504
  }
496
505
  catch (error) {
497
- for (const snapshot of lastSnapshot)
506
+ // Validation happens before writes, but restore this task's snapshot
507
+ // if a filesystem error occurs during the write phase.
508
+ for (const snapshot of taskSnapshots.get(msg.taskId) || [])
498
509
  node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
499
- lastSnapshot = [];
510
+ taskSnapshots.delete(msg.taskId);
500
511
  const message = error instanceof Error ? error.message : "The change could not be applied.";
501
512
  const sourceChanged = message.includes("The source changed after the suggestion was generated");
502
- if (sourceChanged && lastEditRequest && lastEditConfig && reviewRefreshAttempts < 1) {
503
- runEditReview(lastEditRequest, lastEditConfig, "The source changed. Refreshing the review against the current file…");
513
+ const editRequest = editRequests.get(msg.taskId);
514
+ const editConfig = editConfigs.get(msg.taskId);
515
+ const refreshAttempts = reviewRefreshAttempts.get(msg.taskId) || 0;
516
+ if (sourceChanged && editRequest && editConfig && refreshAttempts < 1) {
517
+ reviewRefreshAttempts.set(msg.taskId, refreshAttempts + 1);
518
+ runEditReview(editRequest, editConfig, "The source changed. Refreshing the review against the current file…");
504
519
  }
505
520
  else {
506
521
  socket.send(JSON.stringify({ type: "agent_status", status: "error", message }));
@@ -508,11 +523,11 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
508
523
  }
509
524
  }
510
525
  else if (msg.type === "undo") {
511
- for (const snapshot of lastSnapshot)
526
+ const snapshots = taskSnapshots.get(msg.taskId || "") || [];
527
+ for (const snapshot of snapshots)
512
528
  node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
513
- const count = lastSnapshot.length;
514
- lastSnapshot = [];
515
- socket.send(JSON.stringify({ type: "undone", message: count ? "The accepted change was reverted." : "There is no accepted change to undo." }));
529
+ taskSnapshots.delete(msg.taskId || "");
530
+ socket.send(JSON.stringify({ type: "undone", taskId: msg.taskId, message: snapshots.length ? "The accepted change was reverted." : "There is no accepted change to undo." }));
516
531
  }
517
532
  });
518
533
  socket.on("close", () => {
package/dist/overlay.js CHANGED
@@ -7878,6 +7878,8 @@
7878
7878
  chatHistory = [];
7879
7879
  changesHistory = [];
7880
7880
  pendingChanges = [];
7881
+ agentTasks = [];
7882
+ activeTaskId = null;
7881
7883
  runtimeErrors = [];
7882
7884
  screenshotPromise = Promise.resolve({});
7883
7885
  // Agent State
@@ -10882,6 +10884,60 @@
10882
10884
  display: none;
10883
10885
  }
10884
10886
 
10887
+ .lasso-task-panel {
10888
+ position: fixed;
10889
+ right: 12px;
10890
+ bottom: 82px;
10891
+ width: 390px;
10892
+ max-width: calc(100vw - 24px);
10893
+ max-height: min(560px, calc(100vh - 104px));
10894
+ padding: 14px;
10895
+ display: flex;
10896
+ flex-direction: column;
10897
+ gap: 10px;
10898
+ color: var(--lo-text);
10899
+ background: var(--lo-surface);
10900
+ border: 1px solid var(--lo-border);
10901
+ border-radius: var(--lo-radius-xl);
10902
+ box-shadow: 0 18px 50px rgba(0,0,0,.35);
10903
+ z-index: 30;
10904
+ opacity: 0;
10905
+ transform: translateY(8px) scale(.98);
10906
+ transition: opacity 150ms ease, transform 150ms ease;
10907
+ pointer-events: auto;
10908
+ }
10909
+
10910
+ .lasso-task-panel.visible { opacity: 1; transform: translateY(0) scale(1); }
10911
+ .lasso-task-panel[hidden] { display: none; }
10912
+ .lasso-task-header, .lasso-task-title-row, .lasso-task-detail-heading { display: flex; align-items: center; }
10913
+ .lasso-task-header { justify-content: space-between; }
10914
+ .lasso-task-title-row { gap: 7px; font-size: 13px; font-weight: 600; }
10915
+ .lasso-task-badge { min-width: 18px; padding: 2px 5px; border-radius: 999px; background: var(--lo-indigo); color: #fff; font-size: 10px; text-align: center; }
10916
+ .lasso-task-close, .lasso-task-back { border: 0; background: transparent; color: var(--lo-text-3); cursor: pointer; }
10917
+ .lasso-task-close { font-size: 20px; line-height: 20px; }
10918
+ .lasso-task-body { min-height: 90px; max-height: 470px; overflow: auto; }
10919
+ .lasso-task-list { display: flex; flex-direction: column; gap: 3px; }
10920
+ .lasso-task-row { width: 100%; display: flex; align-items: center; gap: 9px; padding: 9px; border: 0; border-radius: var(--lo-radius-md); background: transparent; color: var(--lo-text); text-align: left; cursor: pointer; }
10921
+ .lasso-task-row:hover, .lasso-task-row.active { background: var(--lo-surface-hover); }
10922
+ .lasso-task-status { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: var(--lo-text-3); }
10923
+ .lasso-task-status.thinking, .lasso-task-status.working { background: var(--lo-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--lo-primary) 18%, transparent); }
10924
+ .lasso-task-status.review { background: #fbbf24; }
10925
+ .lasso-task-status.complete { background: #34d399; }
10926
+ .lasso-task-status.error { background: #f87171; }
10927
+ .lasso-task-row-copy { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
10928
+ .lasso-task-row-copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 500; }
10929
+ .lasso-task-row-copy small, .lasso-task-message { color: var(--lo-text-3); font-size: 11px; }
10930
+ .lasso-task-empty { padding: 25px 10px; color: var(--lo-text-3); font-size: 12px; text-align: center; }
10931
+ .lasso-task-detail { padding: 4px 2px; }
10932
+ .lasso-task-detail-heading { justify-content: space-between; margin-bottom: 12px; }
10933
+ .lasso-task-detail-status { color: var(--lo-primary); font-size: 11px; font-weight: 600; }
10934
+ .lasso-task-prompt { margin: 0 0 10px; font-size: 13px; line-height: 1.45; }
10935
+ .lasso-task-message { margin: 0 0 12px; line-height: 1.45; }
10936
+ .lasso-task-changes { margin-bottom: 10px; color: var(--lo-text-2); font-size: 11px; }
10937
+ .lasso-task-apply { padding: 7px 11px; border: 0; border-radius: var(--lo-radius-full); background: var(--lo-primary); color: #111214; font: inherit; font-size: 11px; font-weight: 600; cursor: pointer; }
10938
+
10939
+ .lasso-agent-tasks-badge { position: absolute; top: 5px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--lo-primary); }
10940
+
10885
10941
  .lasso-todo-header {
10886
10942
  display: flex;
10887
10943
  align-items: center;
@@ -12064,6 +12120,138 @@
12064
12120
  };
12065
12121
  var terminal_default = data4;
12066
12122
 
12123
+ // src/overlay/tasks/tasks.ts
12124
+ var panel = null;
12125
+ var list = null;
12126
+ var detail = null;
12127
+ var badge = null;
12128
+ var statusLabel = {
12129
+ queued: "Queued",
12130
+ thinking: "Thinking",
12131
+ working: "Working",
12132
+ review: "Review",
12133
+ complete: "Complete",
12134
+ error: "Error",
12135
+ stopped: "Stopped"
12136
+ };
12137
+ function taskId() {
12138
+ return `task-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
12139
+ }
12140
+ function createAgentTask(instruction) {
12141
+ const now = (/* @__PURE__ */ new Date()).toISOString();
12142
+ const task = { id: taskId(), instruction, status: "queued", message: "Queued", createdAt: now, updatedAt: now };
12143
+ state.agentTasks.unshift(task);
12144
+ state.activeTaskId = task.id;
12145
+ renderTasks();
12146
+ return task;
12147
+ }
12148
+ function getAgentTask(id) {
12149
+ return id ? state.agentTasks.find((task) => task.id === id) : void 0;
12150
+ }
12151
+ function updateAgentTask(id, patch) {
12152
+ if (!id) return;
12153
+ const task = getAgentTask(id);
12154
+ if (!task) return;
12155
+ Object.assign(task, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
12156
+ renderTasks();
12157
+ }
12158
+ function selectAgentTask(id) {
12159
+ if (!getAgentTask(id)) return;
12160
+ state.activeTaskId = id;
12161
+ renderTasks();
12162
+ }
12163
+ function toggleTaskPanel(force) {
12164
+ if (!panel) return;
12165
+ panel.hidden = force === void 0 ? !panel.hidden : !force;
12166
+ panel.classList.toggle("visible", !panel.hidden);
12167
+ if (!panel.hidden) renderTasks();
12168
+ }
12169
+ function buildTaskPanel() {
12170
+ const dom = getDOM();
12171
+ panel = document.createElement("div");
12172
+ panel.className = "lasso-task-panel";
12173
+ panel.hidden = true;
12174
+ panel.innerHTML = `
12175
+ <div class="lasso-task-header">
12176
+ <div class="lasso-task-title-row">
12177
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M7 8h10M7 12h6M7 16h8"/></svg>
12178
+ <span>Agent tasks</span><span class="lasso-task-badge" hidden>0</span>
12179
+ </div>
12180
+ <button class="lasso-task-close" type="button" aria-label="Close tasks">\xD7</button>
12181
+ </div>
12182
+ <div class="lasso-task-body">
12183
+ <div class="lasso-task-list"></div>
12184
+ <div class="lasso-task-detail" hidden></div>
12185
+ </div>
12186
+ `;
12187
+ dom.shadow.appendChild(panel);
12188
+ list = panel.querySelector(".lasso-task-list");
12189
+ detail = panel.querySelector(".lasso-task-detail");
12190
+ badge = panel.querySelector(".lasso-task-badge");
12191
+ panel.querySelector(".lasso-task-close").addEventListener("click", () => toggleTaskPanel(false));
12192
+ renderTasks();
12193
+ return panel;
12194
+ }
12195
+ function renderTasks() {
12196
+ if (!list || !detail || !badge) return;
12197
+ const running = state.agentTasks.filter((task) => task.status === "thinking" || task.status === "working").length;
12198
+ badge.hidden = state.agentTasks.length === 0;
12199
+ badge.textContent = String(running || state.agentTasks.length);
12200
+ list.replaceChildren();
12201
+ if (!state.agentTasks.length) {
12202
+ list.innerHTML = `<div class="lasso-task-empty">Prompt an element to start a task.</div>`;
12203
+ } else {
12204
+ for (const task of state.agentTasks) {
12205
+ const button = document.createElement("button");
12206
+ button.type = "button";
12207
+ button.className = `lasso-task-row${task.id === state.activeTaskId ? " active" : ""}`;
12208
+ button.innerHTML = `<span class="lasso-task-status ${task.status}"></span><span class="lasso-task-row-copy"><strong></strong><small>${statusLabel[task.status]}</small></span>`;
12209
+ button.querySelector("strong").textContent = task.instruction;
12210
+ button.addEventListener("click", () => selectAgentTask(task.id));
12211
+ list.appendChild(button);
12212
+ }
12213
+ }
12214
+ const selected = getAgentTask(state.activeTaskId);
12215
+ if (!selected) {
12216
+ detail.hidden = true;
12217
+ return;
12218
+ }
12219
+ detail.hidden = false;
12220
+ detail.replaceChildren();
12221
+ const heading = document.createElement("div");
12222
+ heading.className = "lasso-task-detail-heading";
12223
+ heading.innerHTML = `<span class="lasso-task-detail-status ${selected.status}">${statusLabel[selected.status]}</span><button type="button" class="lasso-task-back">All tasks</button>`;
12224
+ heading.querySelector("button").addEventListener("click", () => {
12225
+ state.activeTaskId = null;
12226
+ renderTasks();
12227
+ });
12228
+ const prompt = document.createElement("p");
12229
+ prompt.className = "lasso-task-prompt";
12230
+ prompt.textContent = selected.instruction;
12231
+ const activity = document.createElement("p");
12232
+ activity.className = "lasso-task-message";
12233
+ activity.textContent = selected.response || selected.message;
12234
+ detail.append(heading, prompt, activity);
12235
+ if (selected.changes?.length) {
12236
+ const changes = document.createElement("div");
12237
+ changes.className = "lasso-task-changes";
12238
+ changes.textContent = `${selected.changes.length} proposed file change${selected.changes.length === 1 ? "" : "s"}`;
12239
+ detail.appendChild(changes);
12240
+ if (selected.status === "review") {
12241
+ const apply = document.createElement("button");
12242
+ apply.type = "button";
12243
+ apply.className = "lasso-task-apply";
12244
+ apply.textContent = "Apply changes";
12245
+ apply.addEventListener("click", () => {
12246
+ if (state.bridgeSocket?.readyState !== WebSocket.OPEN) return;
12247
+ state.bridgeSocket.send(JSON.stringify({ type: "apply", taskId: selected.id, changes: selected.changes }));
12248
+ updateAgentTask(selected.id, { message: "Applying changes\u2026" });
12249
+ });
12250
+ detail.appendChild(apply);
12251
+ }
12252
+ }
12253
+ }
12254
+
12067
12255
  // src/overlay/notifications.ts
12068
12256
  var permissionRequest = null;
12069
12257
  function requestAgentNotificationPermission() {
@@ -16841,11 +17029,11 @@
16841
17029
  }
16842
17030
  function updateCommentsBadge() {
16843
17031
  const dom = getDOM();
16844
- const badge = dom.shadow.querySelector(".lasso-comments-badge");
16845
- if (!badge) return;
17032
+ const badge2 = dom.shadow.querySelector(".lasso-comments-badge");
17033
+ if (!badge2) return;
16846
17034
  const openCount = threadsInScope().filter((t) => t.root.status === "OPEN").length;
16847
- badge.classList.toggle("visible", openCount > 0);
16848
- badge.textContent = String(openCount);
17035
+ badge2.classList.toggle("visible", openCount > 0);
17036
+ badge2.textContent = String(openCount);
16849
17037
  }
16850
17038
  function commentPosition(thread) {
16851
17039
  const saved = thread.root.meta?.position;
@@ -17596,7 +17784,7 @@
17596
17784
  ta.remove();
17597
17785
  }
17598
17786
  }
17599
- var panel = null;
17787
+ var panel2 = null;
17600
17788
  var items = [];
17601
17789
  var scope = "private";
17602
17790
  var PRIVATE_STORAGE_KEY = "lasso:clipboard:private";
@@ -17655,7 +17843,7 @@
17655
17843
  <div class="lasso-clipboard-list"></div>
17656
17844
  `;
17657
17845
  getDOM().shadow.appendChild(el);
17658
- panel = el;
17846
+ panel2 = el;
17659
17847
  el.querySelector(".lasso-clipboard-close").addEventListener("click", () => toggleClipboardPanel(false));
17660
17848
  el.querySelectorAll(".lasso-clipboard-tab").forEach((button) => {
17661
17849
  button.addEventListener("click", () => {
@@ -17762,14 +17950,14 @@
17762
17950
  return TYPE_COLORS[type] ?? TYPE_COLORS.text;
17763
17951
  }
17764
17952
  function render() {
17765
- const list = panel?.querySelector(".lasso-clipboard-list");
17766
- if (!list) return;
17953
+ const list2 = panel2?.querySelector(".lasso-clipboard-list");
17954
+ if (!list2) return;
17767
17955
  const visible = items.filter((item) => item.scope === scope);
17768
17956
  if (!visible.length) {
17769
- list.innerHTML = `<div class="lasso-clipboard-empty">${scope === "shared" && !state.collabJoined ? "Join a collaboration session to share items." : "No saved items yet."}</div>`;
17957
+ list2.innerHTML = `<div class="lasso-clipboard-empty">${scope === "shared" && !state.collabJoined ? "Join a collaboration session to share items." : "No saved items yet."}</div>`;
17770
17958
  return;
17771
17959
  }
17772
- list.innerHTML = visible.map((item) => {
17960
+ list2.innerHTML = visible.map((item) => {
17773
17961
  const color = typeColor(item.type);
17774
17962
  const icon = typeIcon(item.type);
17775
17963
  const preview = escapeHtml3(item.content).slice(0, 220);
@@ -17793,7 +17981,7 @@
17793
17981
  </div>
17794
17982
  </div>`;
17795
17983
  }).join("");
17796
- list.querySelectorAll(".lasso-clipboard-item").forEach((row) => {
17984
+ list2.querySelectorAll(".lasso-clipboard-item").forEach((row) => {
17797
17985
  const item = visible.find((candidate) => candidate.uid === row.dataset.id);
17798
17986
  if (!item) return;
17799
17987
  const copyBtn = row.querySelector(".lasso-clipboard-copy-item");
@@ -17828,7 +18016,7 @@
17828
18016
  });
17829
18017
  }
17830
18018
  function setStatus(message) {
17831
- const sub = panel?.querySelector(".lasso-clipboard-subtitle");
18019
+ const sub = panel2?.querySelector(".lasso-clipboard-subtitle");
17832
18020
  if (!sub) return;
17833
18021
  const previous = sub.textContent;
17834
18022
  sub.textContent = message;
@@ -17837,10 +18025,10 @@
17837
18025
  }, 1800);
17838
18026
  }
17839
18027
  function toggleClipboardPanel(force) {
17840
- if (!panel) return;
17841
- const next = force === void 0 ? Boolean(panel.hidden) : force;
17842
- panel.hidden = !next;
17843
- panel.classList.toggle("visible", next);
18028
+ if (!panel2) return;
18029
+ const next = force === void 0 ? Boolean(panel2.hidden) : force;
18030
+ panel2.hidden = !next;
18031
+ panel2.classList.toggle("visible", next);
17844
18032
  if (next) requestItems();
17845
18033
  }
17846
18034
  function escapeHtml3(value2) {
@@ -18396,7 +18584,7 @@
18396
18584
  event.preventDefault();
18397
18585
  event.stopPropagation();
18398
18586
  if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
18399
- state.bridgeSocket.send(JSON.stringify({ type: "stop" }));
18587
+ state.bridgeSocket.send(JSON.stringify({ type: "stop", taskId: state.activeTaskId || void 0 }));
18400
18588
  }
18401
18589
  appendChat("assistant", "Agent stopped.");
18402
18590
  resetAgentState();
@@ -18457,7 +18645,7 @@
18457
18645
  rev.querySelector(".lasso-review-undo").addEventListener("click", () => {
18458
18646
  if (state.pendingChanges.length && rev.querySelector(".lasso-review-apply").hidden) {
18459
18647
  if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
18460
- state.bridgeSocket.send(JSON.stringify({ type: "undo" }));
18648
+ state.bridgeSocket.send(JSON.stringify({ type: "undo", taskId: state.activeTaskId || void 0 }));
18461
18649
  }
18462
18650
  return;
18463
18651
  }
@@ -18467,7 +18655,7 @@
18467
18655
  rev.querySelector(".lasso-review-apply").addEventListener("click", () => {
18468
18656
  if (!state.bridgeSocket || state.bridgeSocket.readyState !== WebSocket.OPEN || !state.pendingChanges.length) return;
18469
18657
  requestAgentNotificationPermission();
18470
- state.bridgeSocket.send(JSON.stringify({ type: "apply", changes: state.pendingChanges }));
18658
+ state.bridgeSocket.send(JSON.stringify({ type: "apply", taskId: state.activeTaskId || "", changes: state.pendingChanges }));
18471
18659
  appendChat("assistant", "Applying the reviewed change\u2026");
18472
18660
  });
18473
18661
  return { prompt: el, review: rev };
@@ -18677,9 +18865,9 @@
18677
18865
  container.append(document.createTextNode(text.slice(lastIndex)));
18678
18866
  }
18679
18867
  }
18680
- function setAgentStatus(status, message, detail) {
18868
+ function setAgentStatus(status, message, detail2) {
18681
18869
  if (!agentStatusElement || !agentStatusMessage || !sendButton) return;
18682
- const logMessage = detail?.trim() || message.trim();
18870
+ const logMessage = detail2?.trim() || message.trim();
18683
18871
  const lastLog = agentLogLines[agentLogLines.length - 1];
18684
18872
  if (agentStatusElement.dataset.status === status && agentStatusMessage.textContent === message && (status === "thinking" || status === "working") && (!logMessage || lastLog === logMessage)) return;
18685
18873
  appendAgentLog(logMessage);
@@ -18694,7 +18882,7 @@
18694
18882
  state.agentRunning = status === "thinking" || status === "working";
18695
18883
  sendButton.classList.toggle("loading", state.agentRunning);
18696
18884
  if (stopButton) stopButton.hidden = !state.agentRunning;
18697
- if (promptInput) promptInput.disabled = state.agentRunning;
18885
+ if (promptInput) promptInput.disabled = false;
18698
18886
  const label = sendButton.querySelector("span");
18699
18887
  if (label) {
18700
18888
  label.textContent = status === "review" ? "Review" : status === "error" ? "Retry" : state.agentRunning ? "" : "Send";
@@ -18710,7 +18898,7 @@
18710
18898
  if (!promptInput || !sendButton) return;
18711
18899
  const hasInstruction = Boolean(promptInput.value.trim()) || sendButton.dataset.state === "retry" && Boolean(state.lastInstruction?.trim());
18712
18900
  const isReviewAction = sendButton.dataset.state === "review";
18713
- sendButton.disabled = state.agentRunning || !hasInstruction && !isReviewAction;
18901
+ sendButton.disabled = !hasInstruction && !isReviewAction;
18714
18902
  }
18715
18903
  function resetAgentState() {
18716
18904
  state.agentRunning = false;
@@ -18847,6 +19035,7 @@
18847
19035
  });
18848
19036
  }
18849
19037
  appendChat("user", instruction);
19038
+ const task = createAgentTask(instruction);
18850
19039
  setAgentStatus("thinking", "Thinking\u2026");
18851
19040
  state.lastInstruction = instruction;
18852
19041
  promptInput.value = "";
@@ -18860,6 +19049,7 @@
18860
19049
  state.bridgeSocket.send(
18861
19050
  JSON.stringify({
18862
19051
  type: wantsAnswer ? "ask" : "edit",
19052
+ taskId: task.id,
18863
19053
  question: wantsAnswer ? instruction : void 0,
18864
19054
  instruction,
18865
19055
  messages: state.chatHistory,
@@ -19711,25 +19901,25 @@
19711
19901
  const parts2 = filePath.split("/");
19712
19902
  const name = parts2.pop() || filePath;
19713
19903
  const dir = parts2.length ? parts2.join("/") + "/" : "";
19714
- let badge = "M";
19904
+ let badge2 = "M";
19715
19905
  let badgeClass = "mod";
19716
19906
  if (code.includes("A")) {
19717
- badge = "A";
19907
+ badge2 = "A";
19718
19908
  badgeClass = "add";
19719
19909
  } else if (code.includes("D")) {
19720
- badge = "D";
19910
+ badge2 = "D";
19721
19911
  badgeClass = "del";
19722
19912
  } else if (code.includes("R")) {
19723
- badge = "R";
19913
+ badge2 = "R";
19724
19914
  badgeClass = "ren";
19725
19915
  } else if (code === "??" || code.includes("?")) {
19726
- badge = "U";
19916
+ badge2 = "U";
19727
19917
  badgeClass = "unt";
19728
19918
  } else if (code.includes("M")) {
19729
- badge = "M";
19919
+ badge2 = "M";
19730
19920
  badgeClass = "mod";
19731
19921
  }
19732
- return { code, badge, badgeClass, path: filePath, dir, name };
19922
+ return { code, badge: badge2, badgeClass, path: filePath, dir, name };
19733
19923
  });
19734
19924
  }
19735
19925
  function escapeHtml4(text) {
@@ -19940,7 +20130,7 @@
19940
20130
  const uninitView = gitPanelEl.querySelector(".lasso-git-uninit-view");
19941
20131
  const repoView = gitPanelEl.querySelector(".lasso-git-repo-view");
19942
20132
  const filesList = gitPanelEl.querySelector(".lasso-git-files-list");
19943
- const badge = gitPanelEl.querySelector(".lasso-git-changes-badge");
20133
+ const badge2 = gitPanelEl.querySelector(".lasso-git-changes-badge");
19944
20134
  const commitBtn = gitPanelEl.querySelector(".lasso-git-commit-btn");
19945
20135
  const commitBtnText = gitPanelEl.querySelector(".lasso-git-commit-btn-text");
19946
20136
  const pushBtn = gitPanelEl.querySelector(".lasso-git-push-btn");
@@ -19963,12 +20153,12 @@
19963
20153
  uninitView.hidden = next.isRepo;
19964
20154
  repoView.hidden = !next.isRepo;
19965
20155
  }
19966
- if (filesList && badge && next.isRepo) {
20156
+ if (filesList && badge2 && next.isRepo) {
19967
20157
  const rawStatus = next.status || [];
19968
20158
  const parsed = parseGitStatus(rawStatus);
19969
20159
  const count = parsed.length;
19970
- badge.textContent = count > 0 ? String(count) : "Clean";
19971
- badge.className = `lasso-git-changes-badge${count > 0 ? " has-changes" : " clean"}`;
20160
+ badge2.textContent = count > 0 ? String(count) : "Clean";
20161
+ badge2.className = `lasso-git-changes-badge${count > 0 ? " has-changes" : " clean"}`;
19972
20162
  if (count === 0) {
19973
20163
  filesList.innerHTML = `
19974
20164
  <div class="lasso-git-clean-state">
@@ -20153,6 +20343,11 @@
20153
20343
  </svg>
20154
20344
  </button>
20155
20345
 
20346
+ <button class="lasso-tool-btn agent-tasks-tool" type="button" aria-label="Agent tasks" title="Agent tasks">
20347
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M7 8h10M7 12h6M7 16h8"/></svg>
20348
+ <i class="lasso-agent-tasks-badge" hidden></i>
20349
+ </button>
20350
+
20156
20351
  <button class="lasso-tool-btn notepad-tool" type="button" aria-label="Notepad" title="Scratchpad & Notes">
20157
20352
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
20158
20353
  <path d="M16 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8l-5-5z"/>
@@ -20228,6 +20423,7 @@
20228
20423
  const commentBtn = toolbar.querySelector(".comment-tool");
20229
20424
  const gitBtn = toolbar.querySelector(".git-tool");
20230
20425
  const todoBtn = toolbar.querySelector(".todo-tool");
20426
+ const agentTasksBtn = toolbar.querySelector(".agent-tasks-tool");
20231
20427
  const notepadBtn = toolbar.querySelector(".notepad-tool");
20232
20428
  const clipboardBtn = toolbar.querySelector(".clipboard-tool");
20233
20429
  const voiceBtn = toolbar.querySelector(".voice-tool");
@@ -20264,6 +20460,11 @@
20264
20460
  e.stopPropagation();
20265
20461
  toggleTodoPanel();
20266
20462
  });
20463
+ agentTasksBtn.addEventListener("click", (e) => {
20464
+ e.preventDefault();
20465
+ e.stopPropagation();
20466
+ toggleTaskPanel();
20467
+ });
20267
20468
  notepadBtn.addEventListener("click", (e) => {
20268
20469
  e.preventDefault();
20269
20470
  e.stopPropagation();
@@ -20359,6 +20560,9 @@
20359
20560
  setGeneratedCommitMessage(message.message || "", message.error);
20360
20561
  }
20361
20562
  if (message.type === "agent_status" && message.status && message.message) {
20563
+ const taskStatus = message.status === "error" ? "error" : message.status === "stopped" ? "stopped" : message.status;
20564
+ updateAgentTask(message.taskId, { status: taskStatus, message: message.message, detail: message.detail, changes: message.changes });
20565
+ if (message.taskId && message.taskId !== state.activeTaskId) return;
20362
20566
  setAgentStatus(message.status, message.message, message.detail);
20363
20567
  setDragCardStatus(message.status, message.message);
20364
20568
  if (message.status === "review" && message.changes?.length) {
@@ -20373,11 +20577,15 @@
20373
20577
  }
20374
20578
  }
20375
20579
  if (message.type === "assistant_message" && message.message) {
20580
+ updateAgentTask(message.taskId, { status: "complete", message: "Complete", response: message.message });
20581
+ if (message.taskId && message.taskId !== state.activeTaskId) return;
20376
20582
  void notifyAgent("Agent complete", message.message);
20377
20583
  appendChat("assistant", message.message);
20378
20584
  resetAgentState();
20379
20585
  }
20380
20586
  if (message.type === "applied" || message.type === "undone") {
20587
+ updateAgentTask(message.taskId, { status: "complete", message: message.message || "Done." });
20588
+ if (message.taskId && message.taskId !== state.activeTaskId) return;
20381
20589
  void notifyAgent(message.type === "applied" ? "Changes applied" : "Change undone", message.message || "Done.");
20382
20590
  appendChat("assistant", message.message || "Done.");
20383
20591
  releaseHeldLock();
@@ -20417,6 +20625,7 @@
20417
20625
  buildTodoPanel();
20418
20626
  buildNotepadPanel();
20419
20627
  buildClipboardPanel();
20628
+ buildTaskPanel();
20420
20629
  buildPrompt();
20421
20630
  buildDrag();
20422
20631
  initErrorListeners();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
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",