@lasso-ai/cli 1.0.12 → 1.0.14

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.
@@ -44,7 +44,7 @@ export type AgentConfig = {
44
44
  baseUrl?: string;
45
45
  };
46
46
  export type LocalAgent = "claude-code" | "codex" | "opencode";
47
- export type AgentProgress = (message: string) => void;
47
+ export type AgentProgress = (message: string, detail?: string) => void;
48
48
  export type AgentAnswer = {
49
49
  question: string;
50
50
  context?: AgentInput["context"];
package/dist/cli/agent.js CHANGED
@@ -27,7 +27,11 @@ async function detectLocalAgents() {
27
27
  }
28
28
  const ignored = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo"]);
29
29
  const sourceExtensions = /\.(tsx?|jsx?|vue|svelte|css|scss|html)$/i;
30
+ const sourceFileCache = new Map();
30
31
  async function sourceFiles(directory) {
32
+ const cached = sourceFileCache.get(directory);
33
+ if (cached && cached.expiresAt > Date.now())
34
+ return cached.files;
31
35
  const entries = await promises_1.default.readdir(directory, { withFileTypes: true });
32
36
  const files = [];
33
37
  for (const entry of entries) {
@@ -38,28 +42,52 @@ async function sourceFiles(directory) {
38
42
  files.push(...(await sourceFiles(fullPath)));
39
43
  else if (sourceExtensions.test(entry.name))
40
44
  files.push(fullPath);
41
- if (files.length >= 80)
45
+ if (files.length >= 40)
42
46
  break;
43
47
  }
48
+ sourceFileCache.set(directory, { expiresAt: Date.now() + 5000, files });
44
49
  return files;
45
50
  }
46
51
  async function contextFor(cwd, element) {
52
+ const needle = element.label.replace(/^[^.#]+[.#]?/, "");
53
+ const hintedPath = element.sourceHint?.split(":")[0];
54
+ const sourceFile = hintedPath ? node_path_1.default.basename(hintedPath) : "";
55
+ if (hintedPath) {
56
+ const candidates = [
57
+ node_path_1.default.isAbsolute(hintedPath) ? hintedPath : node_path_1.default.resolve(cwd, hintedPath),
58
+ ];
59
+ const normalizedHint = hintedPath.replace(/\\/g, "/");
60
+ const srcMarker = "/src/";
61
+ const srcIndex = normalizedHint.lastIndexOf(srcMarker);
62
+ if (srcIndex >= 0)
63
+ candidates.push(node_path_1.default.join(cwd, normalizedHint.slice(srcIndex + 1)));
64
+ for (const candidate of candidates) {
65
+ try {
66
+ const content = await promises_1.default.readFile(candidate, "utf8");
67
+ return `FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
68
+ }
69
+ catch {
70
+ // The runtime source hint can point to a different checkout.
71
+ }
72
+ }
73
+ }
47
74
  const files = await sourceFiles(cwd);
48
- const needle = element.sourceHint || element.label.replace(/^[^.#]+[.#]?/, "");
49
- const sourceFile = element.sourceHint ? node_path_1.default.basename(element.sourceHint.split(":")[0]) : "";
50
75
  const snippets = [];
51
- for (const file of files) {
52
- if (snippets.length >= 8)
53
- break;
76
+ const results = await Promise.all(files.map(async (file) => {
54
77
  try {
55
78
  const content = await promises_1.default.readFile(file, "utf8");
56
79
  if (!needle || (sourceFile && file.endsWith(sourceFile)) || content.includes(needle) || content.includes(element.label)) {
57
- snippets.push(`FILE: ${node_path_1.default.relative(cwd, file)}\n${content.slice(0, 12000)}`);
80
+ return `FILE: ${node_path_1.default.relative(cwd, file)}\n${content.slice(0, 8000)}`;
58
81
  }
59
82
  }
60
83
  catch {
61
84
  // A file can disappear while a dev server is rebuilding; skip it.
62
85
  }
86
+ return null;
87
+ }));
88
+ for (const result of results) {
89
+ if (result && snippets.length < 6)
90
+ snippets.push(result);
63
91
  }
64
92
  return snippets.join("\n\n---\n\n");
65
93
  }
@@ -122,86 +150,93 @@ function extractLocalAgentText(raw, provider) {
122
150
  }
123
151
  return texts.at(-1) || raw;
124
152
  }
125
- /** Truncate a detail string to a readable length. */
126
153
  function snippet(value, max = 80) {
127
154
  const s = String(value ?? "").trim().replace(/\s+/g, " ");
128
155
  return s.length > max ? `${s.slice(0, max)}…` : s;
129
156
  }
157
+ function progressEvent(message, detail) {
158
+ const value = detail == null ? "" : String(detail).trim();
159
+ return value ? { message, detail: value } : { message };
160
+ }
130
161
  function progressFromLine(raw, provider) {
131
162
  try {
132
163
  const event = JSON.parse(raw);
133
164
  const item = event.item;
134
165
  if (provider === "claude-code") {
135
166
  if (event.type === "system")
136
- return "Claude Code connected";
167
+ return progressEvent("Claude Code connected");
137
168
  // tool_use blocks inside assistant messages
138
169
  const toolBlock = event.message?.content?.find?.((p) => p.type === "tool_use");
139
170
  if (toolBlock) {
140
171
  const toolName = toolBlock.name || "tool";
141
172
  const inp = toolBlock.input;
142
173
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
143
- return detail
174
+ return progressEvent(detail
144
175
  ? `Claude Code · ${toolName} ${snippet(detail)}`
145
- : `Claude Code · ${toolName}`;
176
+ : `Claude Code · ${toolName}`, detail);
146
177
  }
147
178
  // thinking blocks inside assistant messages
148
179
  const thinkBlock = event.message?.content?.find?.((p) => p.type === "thinking");
149
180
  if (thinkBlock?.thinking) {
150
- return `Claude Code · ${snippet(thinkBlock.thinking, 100)}`;
181
+ return progressEvent(`Claude Code · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
151
182
  }
152
183
  // top-level tool event fields (stream-json verbose format)
153
184
  const tool = event.tool_name || event.name || event.tool?.name;
154
185
  if (tool) {
155
186
  const inp = event.tool_input;
156
187
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
157
- return detail
188
+ return progressEvent(detail
158
189
  ? `Claude Code · ${tool} ${snippet(detail)}`
159
- : `Claude Code · ${tool}`;
190
+ : `Claude Code · ${tool}`, detail);
191
+ }
192
+ if (event.type === "result" || event.result) {
193
+ return progressEvent("Claude Code · preparing the proposal");
160
194
  }
161
- if (event.type === "result" || event.result)
162
- return "Claude Code · preparing the proposal";
163
195
  if (event.type === "assistant")
164
- return "Claude Code · reasoning about the change";
196
+ return progressEvent("Claude Code · reasoning about the change");
165
197
  }
166
198
  else if (provider === "codex") {
167
199
  const type = item?.type || event.type || "";
168
200
  if (type === "command_execution" || type === "command_execution_output") {
169
201
  const cmd = item?.command || event.command || "";
170
- return cmd ? `Codex · Bash ${snippet(cmd)}` : "Codex · running a command";
202
+ return progressEvent(cmd ? `Codex · Bash ${snippet(cmd)}` : "Codex · running a command", cmd);
171
203
  }
172
204
  if (type === "file_read" || type === "read_file") {
173
205
  const fp = item?.path || event.path || "";
174
- return fp ? `Codex · Read ${snippet(fp)}` : "Codex · reading a file";
206
+ return progressEvent(fp ? `Codex · Read ${snippet(fp)}` : "Codex · reading a file", fp);
207
+ }
208
+ if (type === "agent_message" || type === "message") {
209
+ return progressEvent("Codex · drafting the proposal");
175
210
  }
176
- if (type === "agent_message" || type === "message")
177
- return "Codex · drafting the proposal";
178
211
  if (type === "reasoning") {
179
212
  const text = item?.content || event.content || "";
180
- return text ? `Codex · ${snippet(text, 100)}` : "Codex · reasoning";
213
+ return progressEvent(text ? `Codex · ${snippet(text)}` : "Codex · reasoning", text);
214
+ }
215
+ if (type === "turn.started" || type === "turn_start") {
216
+ return progressEvent("Codex · starting a turn");
217
+ }
218
+ if (type === "turn.completed" || type === "turn_complete") {
219
+ return progressEvent("Codex · preparing the proposal");
181
220
  }
182
- if (type === "turn.started" || type === "turn_start")
183
- return "Codex · starting a turn";
184
- if (type === "turn.completed" || type === "turn_complete")
185
- return "Codex · preparing the proposal";
186
221
  }
187
222
  else if (provider === "opencode") {
188
223
  const part = event.part;
189
224
  if (event.type === "step-start")
190
- return "OpenCode · starting a step";
225
+ return progressEvent("OpenCode · starting a step");
191
226
  if (part?.type === "tool") {
192
227
  const toolName = part.tool || "tool";
193
228
  const inp = part.input;
194
229
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
195
- return detail
230
+ return progressEvent(detail
196
231
  ? `OpenCode · ${toolName} ${snippet(detail)}`
197
- : `OpenCode · ${toolName}`;
232
+ : `OpenCode · ${toolName}`, detail);
198
233
  }
199
234
  if (part?.type === "text") {
200
235
  const text = part.text || "";
201
- return text ? `OpenCode · ${snippet(text, 100)}` : "OpenCode · drafting the response";
236
+ return progressEvent(text ? `OpenCode · ${snippet(text)}` : "OpenCode · drafting the response", text);
202
237
  }
203
238
  if (event.type === "step-finish")
204
- return "OpenCode · finalizing the response";
239
+ return progressEvent("OpenCode · finalizing the response");
205
240
  }
206
241
  }
207
242
  catch {
@@ -253,7 +288,7 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
253
288
  stdout += `${line}\n`;
254
289
  const progress = progressFromLine(line, config.provider);
255
290
  if (progress)
256
- onProgress?.(progress);
291
+ onProgress?.(progress.message, progress.detail);
257
292
  }
258
293
  };
259
294
  child.stdout.on("data", consume);
@@ -273,7 +308,7 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
273
308
  stdout += pending;
274
309
  const progress = progressFromLine(pending, config.provider);
275
310
  if (progress)
276
- onProgress?.(progress);
311
+ onProgress?.(progress.message, progress.detail);
277
312
  }
278
313
  if (exitCode !== 0)
279
314
  throw new Error(localAgentError(command, stderr, exitCode));
@@ -360,7 +395,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
360
395
  output += `${line}\n`;
361
396
  const progress = progressFromLine(line, config.provider);
362
397
  if (progress)
363
- onProgress?.(progress);
398
+ onProgress?.(progress.message, progress.detail);
364
399
  }
365
400
  };
366
401
  child.stdout.on("data", consume);
@@ -371,8 +406,12 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
371
406
  child.once("error", reject);
372
407
  child.once("close", (status) => resolve(status ?? 1));
373
408
  });
374
- if (pending.trim())
409
+ if (pending.trim()) {
375
410
  output += pending;
411
+ const progress = progressFromLine(pending, config.provider);
412
+ if (progress)
413
+ onProgress?.(progress.message, progress.detail);
414
+ }
376
415
  if (code !== 0)
377
416
  throw new Error(localAgentError(command, stderr, code));
378
417
  return extractLocalAgentText(output, config.provider).trim();
@@ -136,6 +136,7 @@ export type ServerBridgeMessage = {
136
136
  type: "agent_status";
137
137
  status: "thinking" | "working" | "review" | "error" | "stopped";
138
138
  message: string;
139
+ detail?: string;
139
140
  changes?: SourceChange[];
140
141
  } | {
141
142
  type: "assistant_message";
@@ -52,6 +52,24 @@ function fileLineEnding(content) {
52
52
  function withLineEnding(value, lineEnding) {
53
53
  return value.replace(/\r\n?|\n/g, lineEnding);
54
54
  }
55
+ function resolveProposedFile(cwd, proposedPath) {
56
+ const root = node_path_1.default.resolve(cwd);
57
+ const candidate = node_path_1.default.resolve(root, proposedPath);
58
+ if (candidate.startsWith(`${root}${node_path_1.default.sep}`) && node_fs_1.default.existsSync(candidate))
59
+ return candidate;
60
+ // Some React/Next source maps contain an absolute path from an older
61
+ // checkout. Rebase only the recognizable src/... suffix into this project.
62
+ const normalized = proposedPath.replace(/\\/g, "/");
63
+ const srcIndex = normalized.lastIndexOf("/src/");
64
+ if (srcIndex >= 0) {
65
+ const rebased = node_path_1.default.join(root, normalized.slice(srcIndex + 1));
66
+ if (rebased.startsWith(`${root}${node_path_1.default.sep}`) && node_fs_1.default.existsSync(rebased))
67
+ return rebased;
68
+ }
69
+ if (!candidate.startsWith(`${root}${node_path_1.default.sep}`))
70
+ throw new Error(`The proposed file is outside the current project: ${proposedPath}`);
71
+ throw new Error(`Could not find the proposed file in the current project: ${proposedPath}`);
72
+ }
55
73
  function occurrenceCount(content, needle) {
56
74
  if (!needle)
57
75
  return 0;
@@ -65,13 +83,30 @@ function occurrenceCount(content, needle) {
65
83
  from = index + needle.length;
66
84
  }
67
85
  }
86
+ function escapeRegExp(value) {
87
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
+ }
89
+ function whitespaceEquivalentRange(content, oldString) {
90
+ const trimmed = oldString.trim();
91
+ if (!trimmed)
92
+ return null;
93
+ const pattern = trimmed.split(/\s+/).map(escapeRegExp).join("\\s+");
94
+ const matches = Array.from(content.matchAll(new RegExp(pattern, "g")));
95
+ if (matches.length !== 1 || matches[0].index === undefined)
96
+ return null;
97
+ const start = matches[0].index;
98
+ return { start, end: start + matches[0][0].length };
99
+ }
68
100
  function prepareChange(content, change) {
69
101
  const lineEnding = fileLineEnding(content);
70
102
  const oldString = withLineEnding(change.oldString, lineEnding);
71
103
  const newString = withLineEnding(change.newString, lineEnding);
72
104
  const matches = occurrenceCount(content, oldString);
73
105
  if (matches === 0) {
74
- throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated.`);
106
+ const range = whitespaceEquivalentRange(content, oldString);
107
+ if (range)
108
+ return { ...range, oldString: content.slice(range.start, range.end), newString };
109
+ throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated. Regenerate the review so it uses the current source.`);
75
110
  }
76
111
  if (matches > 1) {
77
112
  throw new Error(`Could not safely apply ${change.filePath}. The selected code is not unique (${matches} matches).`);
@@ -244,9 +279,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
244
279
  const selectedConfig = localProvider
245
280
  ? { provider: cliProvider, model: msg.model }
246
281
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
247
- void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message) => {
282
+ void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
248
283
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
249
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
284
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
250
285
  }).then((answer) => {
251
286
  if (!controller.signal.aborted)
252
287
  socket.send(JSON.stringify({ type: "assistant_message", message: answer }));
@@ -275,9 +310,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
275
310
  const selectedConfig = localProvider
276
311
  ? { provider: cliProvider, model: msg.model }
277
312
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
278
- void (0, agent_1.proposeChanges)(cwd, msg, selectedConfig, controller.signal, (message) => {
313
+ void (0, agent_1.proposeChanges)(cwd, msg, selectedConfig, controller.signal, (message, detail) => {
279
314
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
280
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
315
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
281
316
  }
282
317
  })
283
318
  .then((proposal) => {
@@ -320,9 +355,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
320
355
  activeAgentController?.abort();
321
356
  activeAgentController = controller;
322
357
  socket.send(JSON.stringify({ type: "agent_status", status: "thinking", message: "Generating a commit message…" }));
323
- void getGitState(cwd).then((git) => (0, agent_1.generateCommitMessage)(cwd, git.status || [], selectedConfig, controller.signal, (message) => {
358
+ void getGitState(cwd).then((git) => (0, agent_1.generateCommitMessage)(cwd, git.status || [], selectedConfig, controller.signal, (message, detail) => {
324
359
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
325
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
360
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
326
361
  })).then((message) => {
327
362
  if (!controller.signal.aborted)
328
363
  socket.send(JSON.stringify({ type: "git_commit_message", message }));
@@ -402,9 +437,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
402
437
  lastSnapshot = [];
403
438
  const planned = new Map();
404
439
  for (const change of msg.changes) {
405
- const filePath = node_path_1.default.resolve(cwd, change.filePath);
406
- if (!filePath.startsWith(`${node_path_1.default.resolve(cwd)}${node_path_1.default.sep}`))
407
- throw new Error("A proposed file was outside the project.");
440
+ const filePath = resolveProposedFile(cwd, change.filePath);
408
441
  const content = node_fs_1.default.readFileSync(filePath, "utf8");
409
442
  const prepared = prepareChange(content, change);
410
443
  const fileChanges = planned.get(filePath) || [];
package/dist/overlay.js CHANGED
@@ -9830,6 +9830,16 @@
9830
9830
  background: rgba(242, 139, 130, 0.12);
9831
9831
  }
9832
9832
 
9833
+ .lasso-chat-message code {
9834
+ padding: 1px 4px;
9835
+ border-radius: 3px;
9836
+ background: var(--lo-surface-2);
9837
+ color: #a5b4fc;
9838
+ font-family: var(--lo-font-mono);
9839
+ font-size: 0.92em;
9840
+ white-space: pre-wrap;
9841
+ }
9842
+
9833
9843
  /* CLI / Terminal Thinking Mode */
9834
9844
  .lasso-agent-status {
9835
9845
  display: flex;
@@ -9906,6 +9916,43 @@
9906
9916
  color: #f87171;
9907
9917
  }
9908
9918
 
9919
+ .lasso-agent-status-badge.complete {
9920
+ background: rgba(16, 185, 129, 0.16);
9921
+ color: #34d399;
9922
+ }
9923
+
9924
+ .lasso-agent-log-toggle {
9925
+ display: inline-flex;
9926
+ align-items: center;
9927
+ gap: 3px;
9928
+ padding: 2px 5px;
9929
+ border: 0;
9930
+ border-radius: 4px;
9931
+ background: transparent;
9932
+ color: #64748b;
9933
+ font: inherit;
9934
+ font-size: 9.5px;
9935
+ cursor: pointer;
9936
+ }
9937
+
9938
+ .lasso-agent-log-toggle:hover {
9939
+ background: rgba(255, 255, 255, 0.06);
9940
+ color: #cbd5e1;
9941
+ }
9942
+
9943
+ .lasso-agent-log-toggle[hidden] {
9944
+ display: none;
9945
+ }
9946
+
9947
+ .lasso-agent-log-chevron {
9948
+ display: inline-block;
9949
+ transition: transform 140ms ease;
9950
+ }
9951
+
9952
+ .lasso-agent-log-toggle[aria-expanded="true"] .lasso-agent-log-chevron {
9953
+ transform: rotate(180deg);
9954
+ }
9955
+
9909
9956
  .lasso-agent-status-line {
9910
9957
  display: flex;
9911
9958
  align-items: center;
@@ -9921,13 +9968,52 @@
9921
9968
  }
9922
9969
 
9923
9970
  .lasso-agent-status-message {
9971
+ flex: 1;
9972
+ min-width: 0;
9924
9973
  overflow: hidden;
9925
9974
  text-overflow: ellipsis;
9926
9975
  white-space: nowrap;
9927
9976
  }
9928
9977
 
9929
- /* The status line is the single latest activity message. */
9930
9978
  .lasso-agent-log {
9979
+ display: flex;
9980
+ flex-direction: column;
9981
+ gap: 5px;
9982
+ max-height: 180px;
9983
+ margin-top: 8px;
9984
+ padding-top: 7px;
9985
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
9986
+ overflow: auto;
9987
+ color: #64748b;
9988
+ font-size: 10px;
9989
+ }
9990
+
9991
+ .lasso-agent-log[hidden] {
9992
+ display: none;
9993
+ }
9994
+
9995
+ .lasso-agent-log-line {
9996
+ display: flex;
9997
+ gap: 5px;
9998
+ line-height: 1.45;
9999
+ }
10000
+
10001
+ .lasso-agent-log-prefix {
10002
+ flex: 0 0 auto;
10003
+ color: #475569;
10004
+ }
10005
+
10006
+ .lasso-agent-log-text {
10007
+ min-width: 0;
10008
+ white-space: pre-wrap;
10009
+ overflow-wrap: anywhere;
10010
+ }
10011
+
10012
+ .lasso-agent-status[data-status="complete"] .lasso-agent-status-line {
10013
+ animation: none;
10014
+ }
10015
+
10016
+ .lasso-agent-status[data-status="complete"] .lasso-agent-cursor {
9931
10017
  display: none;
9932
10018
  }
9933
10019
 
@@ -9945,40 +10031,11 @@
9945
10031
  50% { opacity: 0; }
9946
10032
  }
9947
10033
 
9948
- .lasso-agent-log {
9949
- display: flex;
9950
- flex-direction: column;
9951
- gap: 2px;
9952
- margin-top: 6px;
9953
- max-height: 80px;
9954
- overflow-y: auto;
9955
- font-size: 10px;
9956
- color: #64748b;
9957
- }
9958
-
9959
- .lasso-agent-log-line {
9960
- display: flex;
9961
- gap: 5px;
9962
- overflow: hidden;
9963
- text-overflow: ellipsis;
9964
- white-space: nowrap;
9965
- animation: lasso-agent-log-in 180ms ease-out both;
9966
- }
9967
-
9968
- .lasso-agent-log-prefix {
9969
- color: #475569;
9970
- }
9971
-
9972
10034
  @keyframes lasso-agent-line-pulse {
9973
10035
  0%, 100% { opacity: .72; }
9974
10036
  50% { opacity: 1; }
9975
10037
  }
9976
10038
 
9977
- @keyframes lasso-agent-log-in {
9978
- from { opacity: 0; transform: translateY(3px); }
9979
- to { opacity: 1; transform: translateY(0); }
9980
- }
9981
-
9982
10039
  .lasso-prompt-actions {
9983
10040
  display: flex;
9984
10041
  align-items: center;
@@ -12002,6 +12059,30 @@
12002
12059
  };
12003
12060
  var terminal_default = data4;
12004
12061
 
12062
+ // src/overlay/notifications.ts
12063
+ var permissionRequest = null;
12064
+ function requestAgentNotificationPermission() {
12065
+ if (typeof window === "undefined" || !("Notification" in window)) return;
12066
+ if (Notification.permission !== "default" || permissionRequest) return;
12067
+ permissionRequest = Notification.requestPermission().finally(() => {
12068
+ permissionRequest = null;
12069
+ });
12070
+ }
12071
+ async function notifyAgent(title, body) {
12072
+ if (typeof window === "undefined" || !("Notification" in window)) return;
12073
+ let permission = Notification.permission;
12074
+ if (permission === "default") permission = await (permissionRequest || Notification.requestPermission());
12075
+ if (permission !== "granted") return;
12076
+ try {
12077
+ const notification = new Notification(title, { body: body.slice(0, 240) });
12078
+ notification.onclick = () => {
12079
+ window.focus();
12080
+ notification.close();
12081
+ };
12082
+ } catch {
12083
+ }
12084
+ }
12085
+
12005
12086
  // node_modules/.pnpm/engine.io-parser@5.2.3/node_modules/engine.io-parser/build/esm/commons.js
12006
12087
  var PACKET_TYPES = /* @__PURE__ */ Object.create(null);
12007
12088
  PACKET_TYPES["open"] = "0";
@@ -18095,6 +18176,9 @@
18095
18176
  var agentStatusElement = null;
18096
18177
  var agentStatusMessage = null;
18097
18178
  var agentLogElement = null;
18179
+ var agentLogToggle = null;
18180
+ var agentLogLines = [];
18181
+ var agentLogExpanded = false;
18098
18182
  var reviewPanel = null;
18099
18183
  var modelBtn = null;
18100
18184
  var modelName = null;
@@ -18153,13 +18237,17 @@
18153
18237
  </div>
18154
18238
  <span class="lasso-agent-status-kicker">lasso-agent</span>
18155
18239
  <span class="lasso-agent-status-badge">active</span>
18240
+ <button class="lasso-agent-log-toggle" type="button" aria-label="Show agent activity" aria-expanded="false" hidden>
18241
+ <span>Activity</span>
18242
+ <span class="lasso-agent-log-chevron">\u2304</span>
18243
+ </button>
18156
18244
  </div>
18157
18245
  <div class="lasso-agent-status-line">
18158
18246
  <span class="lasso-agent-terminal-prompt">\u276F</span>
18159
18247
  <span class="lasso-agent-status-message"></span>
18160
18248
  <span class="lasso-agent-cursor"></span>
18161
18249
  </div>
18162
- <div class="lasso-agent-log" aria-label="Agent activity"></div>
18250
+ <div class="lasso-agent-log" aria-label="Agent activity" hidden></div>
18163
18251
  </div>
18164
18252
 
18165
18253
  <textarea class="lasso-prompt-input" placeholder="Ask anything about this element\u2026" rows="1"></textarea>
@@ -18213,9 +18301,15 @@
18213
18301
  agentStatusElement = el.querySelector(".lasso-agent-status");
18214
18302
  agentStatusMessage = el.querySelector(".lasso-agent-status-message");
18215
18303
  agentLogElement = el.querySelector(".lasso-agent-log");
18304
+ agentLogToggle = el.querySelector(".lasso-agent-log-toggle");
18216
18305
  modelBtn = el.querySelector(".lasso-prompt-model");
18217
18306
  modelName = el.querySelector(".lasso-prompt-model-name");
18218
18307
  modelMenu = el.querySelector(".lasso-prompt-model-menu");
18308
+ agentLogToggle.addEventListener("click", (event) => {
18309
+ event.preventDefault();
18310
+ event.stopPropagation();
18311
+ setAgentLogExpanded(!agentLogExpanded);
18312
+ });
18219
18313
  const rev = document.createElement("div");
18220
18314
  rev.className = "lasso-review";
18221
18315
  rev.hidden = true;
@@ -18367,6 +18461,7 @@
18367
18461
  });
18368
18462
  rev.querySelector(".lasso-review-apply").addEventListener("click", () => {
18369
18463
  if (!state.bridgeSocket || state.bridgeSocket.readyState !== WebSocket.OPEN || !state.pendingChanges.length) return;
18464
+ requestAgentNotificationPermission();
18370
18465
  state.bridgeSocket.send(JSON.stringify({ type: "apply", changes: state.pendingChanges }));
18371
18466
  appendChat("assistant", "Applying the reviewed change\u2026");
18372
18467
  });
@@ -18478,11 +18573,74 @@
18478
18573
  return {};
18479
18574
  }
18480
18575
  }
18576
+ async function captureElementScreenshot(el) {
18577
+ const options = {
18578
+ backgroundColor: null,
18579
+ useCORS: true,
18580
+ logging: false,
18581
+ scale: Math.min(window.devicePixelRatio || 1, 1),
18582
+ ignoreElements: (node) => node.id === "lasso-root" || Boolean(node.closest?.("#lasso-root"))
18583
+ };
18584
+ try {
18585
+ const canvas = await (0, import_html2canvas.default)(el, options);
18586
+ return { element: canvas.toDataURL("image/jpeg", 0.78) };
18587
+ } catch (error) {
18588
+ console.warn("[lasso] element screenshot unavailable", error);
18589
+ return {};
18590
+ }
18591
+ }
18592
+ function setAgentLogExpanded(expanded) {
18593
+ agentLogExpanded = expanded && agentLogLines.length > 0;
18594
+ if (agentLogElement) agentLogElement.hidden = !agentLogExpanded;
18595
+ if (agentLogToggle) {
18596
+ agentLogToggle.hidden = agentLogLines.length === 0;
18597
+ agentLogToggle.setAttribute("aria-expanded", String(agentLogExpanded));
18598
+ agentLogToggle.setAttribute("aria-label", agentLogExpanded ? "Hide agent activity" : "Show agent activity");
18599
+ }
18600
+ }
18601
+ function clearAgentLog() {
18602
+ agentLogLines = [];
18603
+ agentLogExpanded = false;
18604
+ if (agentLogElement) {
18605
+ agentLogElement.replaceChildren();
18606
+ agentLogElement.hidden = true;
18607
+ }
18608
+ if (agentLogToggle) {
18609
+ agentLogToggle.hidden = true;
18610
+ agentLogToggle.setAttribute("aria-expanded", "false");
18611
+ agentLogToggle.setAttribute("aria-label", "Show agent activity");
18612
+ }
18613
+ if (agentStatusElement) {
18614
+ agentStatusElement.hidden = true;
18615
+ agentStatusElement.dataset.status = "idle";
18616
+ }
18617
+ }
18618
+ function appendAgentLog(message) {
18619
+ const value2 = message.trim();
18620
+ if (!value2) return;
18621
+ const previous = agentLogLines[agentLogLines.length - 1];
18622
+ if (previous === value2) return;
18623
+ agentLogLines.push(value2);
18624
+ if (agentLogElement) {
18625
+ const line = document.createElement("div");
18626
+ line.className = "lasso-agent-log-line";
18627
+ const prefix = document.createElement("span");
18628
+ prefix.className = "lasso-agent-log-prefix";
18629
+ prefix.textContent = "\u203A";
18630
+ const text = document.createElement("span");
18631
+ text.className = "lasso-agent-log-text";
18632
+ text.textContent = value2;
18633
+ line.append(prefix, text);
18634
+ agentLogElement.append(line);
18635
+ if (agentLogExpanded) agentLogElement.scrollTop = agentLogElement.scrollHeight;
18636
+ }
18637
+ if (agentLogToggle) agentLogToggle.hidden = false;
18638
+ }
18481
18639
  function appendChat(role, text) {
18482
18640
  const thread = promptEl?.querySelector(".lasso-chat-thread");
18483
18641
  if (!thread || !text.trim()) return;
18484
18642
  const previous = thread.lastElementChild;
18485
- if (previous?.textContent === text && previous.classList.contains(role)) return;
18643
+ if (previous?.classList.contains(role) && previous.dataset.rawText === text) return;
18486
18644
  state.chatHistory.push({
18487
18645
  role,
18488
18646
  content: text,
@@ -18491,15 +18649,36 @@
18491
18649
  });
18492
18650
  const item = document.createElement("div");
18493
18651
  item.className = `lasso-chat-message ${role}`;
18494
- item.textContent = text;
18652
+ item.dataset.rawText = text;
18653
+ renderChatText(item, text);
18495
18654
  thread.appendChild(item);
18496
18655
  while (thread.children.length > 6) thread.firstElementChild?.remove();
18497
18656
  thread.scrollTop = thread.scrollHeight;
18498
18657
  }
18499
- function setAgentStatus(status, message) {
18658
+ function renderChatText(container, text) {
18659
+ const codePattern = /(`+)([\s\S]*?)\1/g;
18660
+ let lastIndex = 0;
18661
+ for (const match of text.matchAll(codePattern)) {
18662
+ const index = match.index ?? 0;
18663
+ if (index > lastIndex) {
18664
+ container.append(document.createTextNode(text.slice(lastIndex, index)));
18665
+ }
18666
+ const code = document.createElement("code");
18667
+ code.textContent = match[2] ?? "";
18668
+ container.append(code);
18669
+ lastIndex = index + match[0].length;
18670
+ }
18671
+ if (lastIndex < text.length) {
18672
+ container.append(document.createTextNode(text.slice(lastIndex)));
18673
+ }
18674
+ }
18675
+ function setAgentStatus(status, message, detail) {
18500
18676
  if (!agentStatusElement || !agentStatusMessage || !sendButton) return;
18501
- if (agentStatusElement.dataset.status === status && agentStatusMessage.textContent === message && (status === "thinking" || status === "working")) return;
18502
- agentStatusElement.hidden = status === "review" || status === "error" || status === "stopped";
18677
+ const logMessage = detail?.trim() || message.trim();
18678
+ const lastLog = agentLogLines[agentLogLines.length - 1];
18679
+ if (agentStatusElement.dataset.status === status && agentStatusMessage.textContent === message && (status === "thinking" || status === "working") && (!logMessage || lastLog === logMessage)) return;
18680
+ appendAgentLog(logMessage);
18681
+ agentStatusElement.hidden = status === "review";
18503
18682
  agentStatusElement.dataset.status = status;
18504
18683
  agentStatusMessage.textContent = message;
18505
18684
  const badgeEl = agentStatusElement.querySelector(".lasso-agent-status-badge");
@@ -18508,12 +18687,6 @@
18508
18687
  badgeEl.className = `lasso-agent-status-badge ${status}`;
18509
18688
  }
18510
18689
  state.agentRunning = status === "thinking" || status === "working";
18511
- if (state.agentRunning && agentLogElement) {
18512
- const line = document.createElement("div");
18513
- line.className = "lasso-agent-log-line";
18514
- line.innerHTML = `<span class="lasso-agent-log-prefix">\u203A</span> <span class="lasso-agent-log-text">${escapeHtml4(message)}</span>`;
18515
- agentLogElement.replaceChildren(line);
18516
- }
18517
18690
  sendButton.classList.toggle("loading", state.agentRunning);
18518
18691
  if (stopButton) stopButton.hidden = !state.agentRunning;
18519
18692
  if (promptInput) promptInput.disabled = state.agentRunning;
@@ -18528,11 +18701,6 @@
18528
18701
  appendChat(status === "error" ? "error" : "assistant", message);
18529
18702
  }
18530
18703
  }
18531
- function escapeHtml4(text) {
18532
- const div = document.createElement("div");
18533
- div.textContent = text;
18534
- return div.innerHTML;
18535
- }
18536
18704
  function syncSendButtonState() {
18537
18705
  if (!promptInput || !sendButton) return;
18538
18706
  const hasInstruction = Boolean(promptInput.value.trim()) || sendButton.dataset.state === "retry" && Boolean(state.lastInstruction?.trim());
@@ -18541,11 +18709,19 @@
18541
18709
  }
18542
18710
  function resetAgentState() {
18543
18711
  state.agentRunning = false;
18712
+ const hasLog = agentLogLines.length > 0;
18544
18713
  if (agentStatusElement) {
18545
- agentStatusElement.hidden = true;
18546
- agentStatusElement.dataset.status = "idle";
18714
+ agentStatusElement.hidden = !hasLog;
18715
+ agentStatusElement.dataset.status = hasLog ? "complete" : "idle";
18716
+ }
18717
+ if (hasLog && agentStatusMessage) agentStatusMessage.textContent = "Agent complete";
18718
+ if (hasLog) {
18719
+ const badgeEl = agentStatusElement?.querySelector(".lasso-agent-status-badge");
18720
+ if (badgeEl) {
18721
+ badgeEl.textContent = "done";
18722
+ badgeEl.className = "lasso-agent-status-badge complete";
18723
+ }
18547
18724
  }
18548
- if (agentLogElement) agentLogElement.replaceChildren();
18549
18725
  if (sendButton) {
18550
18726
  sendButton.classList.remove("loading");
18551
18727
  sendButton.dataset.state = "idle";
@@ -18641,6 +18817,8 @@
18641
18817
  promptInput.focus();
18642
18818
  return;
18643
18819
  }
18820
+ clearAgentLog();
18821
+ requestAgentNotificationPermission();
18644
18822
  const isQuestion = /^(hi|hello|hey|thanks|thank you|what|why|how|when|where|who|which|is|are|does|do|can|could|would|should|tell me|explain|describe)\b/i.test(instruction) || /\?$/.test(instruction);
18645
18823
  const isExplicitEdit = /\b(change|edit|update|make|add|remove|delete|fix|replace|turn|convert|style|restyle|move|rename|implement|build|create|increase|decrease|hide|show|align|resize|set|enable|disable)\b/i.test(instruction);
18646
18824
  const wantsAnswer = isQuestion && !/\b(can|could|would|please)\s+you\s+(change|edit|update|add|fix|make)\b/i.test(instruction) || !isExplicitEdit && !isQuestion;
@@ -18672,7 +18850,8 @@
18672
18850
  const attributes = Object.fromEntries(
18673
18851
  Array.from(state.selected.attributes).map((attr) => [attr.name, attr.value])
18674
18852
  );
18675
- const screenshots = await state.screenshotPromise;
18853
+ const needsVisualContext = /\b(look|visual|appearance|color|colour|background|image|icon|spacing|layout|position|align|responsive|style|restyle|font|size)\b/i.test(instruction);
18854
+ const screenshots = needsVisualContext && state.selected ? await captureElementScreenshot(state.selected) : await state.screenshotPromise;
18676
18855
  state.bridgeSocket.send(
18677
18856
  JSON.stringify({
18678
18857
  type: wantsAnswer ? "ask" : "edit",
@@ -18725,6 +18904,7 @@
18725
18904
  }
18726
18905
  function openPromptForSelected(selected) {
18727
18906
  if (!promptEl || !promptElement) return;
18907
+ clearAgentLog();
18728
18908
  promptElement.textContent = getElementLabel(selected);
18729
18909
  positionPrompt(selected);
18730
18910
  promptEl.classList.add("visible");
@@ -19547,7 +19727,7 @@
19547
19727
  return { code, badge, badgeClass, path: filePath, dir, name };
19548
19728
  });
19549
19729
  }
19550
- function escapeHtml5(text) {
19730
+ function escapeHtml4(text) {
19551
19731
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
19552
19732
  }
19553
19733
  function buildGitPanel() {
@@ -19802,9 +19982,9 @@
19802
19982
  (f) => `
19803
19983
  <div class="lasso-git-file-row">
19804
19984
  <span class="lasso-git-file-badge ${f.badgeClass}" title="${f.code}">${f.badge}</span>
19805
- <span class="lasso-git-file-path" title="${escapeHtml5(f.path)}">
19806
- ${f.dir ? `<span class="lasso-git-file-dir">${escapeHtml5(f.dir)}</span>` : ""}
19807
- <span class="lasso-git-file-name">${escapeHtml5(f.name)}</span>
19985
+ <span class="lasso-git-file-path" title="${escapeHtml4(f.path)}">
19986
+ ${f.dir ? `<span class="lasso-git-file-dir">${escapeHtml4(f.dir)}</span>` : ""}
19987
+ <span class="lasso-git-file-name">${escapeHtml4(f.name)}</span>
19808
19988
  </span>
19809
19989
  </div>
19810
19990
  `
@@ -20174,9 +20354,10 @@
20174
20354
  setGeneratedCommitMessage(message.message || "", message.error);
20175
20355
  }
20176
20356
  if (message.type === "agent_status" && message.status && message.message) {
20177
- setAgentStatus(message.status, message.message);
20357
+ setAgentStatus(message.status, message.message, message.detail);
20178
20358
  setDragCardStatus(message.status, message.message);
20179
20359
  if (message.status === "review" && message.changes?.length) {
20360
+ void notifyAgent("Review requested", message.message);
20180
20361
  state.pendingChanges = message.changes;
20181
20362
  state.changesHistory.push({
20182
20363
  summary: message.message,
@@ -20187,10 +20368,12 @@
20187
20368
  }
20188
20369
  }
20189
20370
  if (message.type === "assistant_message" && message.message) {
20371
+ void notifyAgent("Agent complete", message.message);
20190
20372
  appendChat("assistant", message.message);
20191
20373
  resetAgentState();
20192
20374
  }
20193
20375
  if (message.type === "applied" || message.type === "undone") {
20376
+ void notifyAgent(message.type === "applied" ? "Changes applied" : "Change undone", message.message || "Done.");
20194
20377
  appendChat("assistant", message.message || "Done.");
20195
20378
  releaseHeldLock();
20196
20379
  if (state.collabSocket?.connected && state.collabJoined) {
@@ -20290,7 +20473,7 @@
20290
20473
  state.selectionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
20291
20474
  state.chatHistory = [];
20292
20475
  state.changesHistory = [];
20293
- state.screenshotPromise = captureScreenshots(target);
20476
+ state.screenshotPromise = Promise.resolve({});
20294
20477
  dom.hoverBox.style.display = "none";
20295
20478
  updateSelectedVisual2();
20296
20479
  openPromptForSelected(target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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",