@lasso-ai/cli 1.0.12 → 1.0.13

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,43 @@ 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) {
47
- const files = await sourceFiles(cwd);
48
52
  const needle = element.sourceHint || element.label.replace(/^[^.#]+[.#]?/, "");
49
- const sourceFile = element.sourceHint ? node_path_1.default.basename(element.sourceHint.split(":")[0]) : "";
53
+ const hintedPath = element.sourceHint?.split(":")[0];
54
+ const sourceFile = hintedPath ? node_path_1.default.basename(hintedPath) : "";
55
+ if (hintedPath) {
56
+ const candidate = node_path_1.default.isAbsolute(hintedPath) ? hintedPath : node_path_1.default.resolve(cwd, hintedPath);
57
+ try {
58
+ const content = await promises_1.default.readFile(candidate, "utf8");
59
+ return `FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
60
+ }
61
+ catch {
62
+ // Fall back to the indexed search when the runtime source hint is stale.
63
+ }
64
+ }
65
+ const files = await sourceFiles(cwd);
50
66
  const snippets = [];
51
- for (const file of files) {
52
- if (snippets.length >= 8)
53
- break;
67
+ const results = await Promise.all(files.map(async (file) => {
54
68
  try {
55
69
  const content = await promises_1.default.readFile(file, "utf8");
56
70
  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)}`);
71
+ return `FILE: ${node_path_1.default.relative(cwd, file)}\n${content.slice(0, 8000)}`;
58
72
  }
59
73
  }
60
74
  catch {
61
75
  // A file can disappear while a dev server is rebuilding; skip it.
62
76
  }
77
+ return null;
78
+ }));
79
+ for (const result of results) {
80
+ if (result && snippets.length < 6)
81
+ snippets.push(result);
63
82
  }
64
83
  return snippets.join("\n\n---\n\n");
65
84
  }
@@ -122,86 +141,93 @@ function extractLocalAgentText(raw, provider) {
122
141
  }
123
142
  return texts.at(-1) || raw;
124
143
  }
125
- /** Truncate a detail string to a readable length. */
126
144
  function snippet(value, max = 80) {
127
145
  const s = String(value ?? "").trim().replace(/\s+/g, " ");
128
146
  return s.length > max ? `${s.slice(0, max)}…` : s;
129
147
  }
148
+ function progressEvent(message, detail) {
149
+ const value = detail == null ? "" : String(detail).trim();
150
+ return value ? { message, detail: value } : { message };
151
+ }
130
152
  function progressFromLine(raw, provider) {
131
153
  try {
132
154
  const event = JSON.parse(raw);
133
155
  const item = event.item;
134
156
  if (provider === "claude-code") {
135
157
  if (event.type === "system")
136
- return "Claude Code connected";
158
+ return progressEvent("Claude Code connected");
137
159
  // tool_use blocks inside assistant messages
138
160
  const toolBlock = event.message?.content?.find?.((p) => p.type === "tool_use");
139
161
  if (toolBlock) {
140
162
  const toolName = toolBlock.name || "tool";
141
163
  const inp = toolBlock.input;
142
164
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
143
- return detail
165
+ return progressEvent(detail
144
166
  ? `Claude Code · ${toolName} ${snippet(detail)}`
145
- : `Claude Code · ${toolName}`;
167
+ : `Claude Code · ${toolName}`, detail);
146
168
  }
147
169
  // thinking blocks inside assistant messages
148
170
  const thinkBlock = event.message?.content?.find?.((p) => p.type === "thinking");
149
171
  if (thinkBlock?.thinking) {
150
- return `Claude Code · ${snippet(thinkBlock.thinking, 100)}`;
172
+ return progressEvent(`Claude Code · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
151
173
  }
152
174
  // top-level tool event fields (stream-json verbose format)
153
175
  const tool = event.tool_name || event.name || event.tool?.name;
154
176
  if (tool) {
155
177
  const inp = event.tool_input;
156
178
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
157
- return detail
179
+ return progressEvent(detail
158
180
  ? `Claude Code · ${tool} ${snippet(detail)}`
159
- : `Claude Code · ${tool}`;
181
+ : `Claude Code · ${tool}`, detail);
182
+ }
183
+ if (event.type === "result" || event.result) {
184
+ return progressEvent("Claude Code · preparing the proposal");
160
185
  }
161
- if (event.type === "result" || event.result)
162
- return "Claude Code · preparing the proposal";
163
186
  if (event.type === "assistant")
164
- return "Claude Code · reasoning about the change";
187
+ return progressEvent("Claude Code · reasoning about the change");
165
188
  }
166
189
  else if (provider === "codex") {
167
190
  const type = item?.type || event.type || "";
168
191
  if (type === "command_execution" || type === "command_execution_output") {
169
192
  const cmd = item?.command || event.command || "";
170
- return cmd ? `Codex · Bash ${snippet(cmd)}` : "Codex · running a command";
193
+ return progressEvent(cmd ? `Codex · Bash ${snippet(cmd)}` : "Codex · running a command", cmd);
171
194
  }
172
195
  if (type === "file_read" || type === "read_file") {
173
196
  const fp = item?.path || event.path || "";
174
- return fp ? `Codex · Read ${snippet(fp)}` : "Codex · reading a file";
197
+ return progressEvent(fp ? `Codex · Read ${snippet(fp)}` : "Codex · reading a file", fp);
198
+ }
199
+ if (type === "agent_message" || type === "message") {
200
+ return progressEvent("Codex · drafting the proposal");
175
201
  }
176
- if (type === "agent_message" || type === "message")
177
- return "Codex · drafting the proposal";
178
202
  if (type === "reasoning") {
179
203
  const text = item?.content || event.content || "";
180
- return text ? `Codex · ${snippet(text, 100)}` : "Codex · reasoning";
204
+ return progressEvent(text ? `Codex · ${snippet(text)}` : "Codex · reasoning", text);
205
+ }
206
+ if (type === "turn.started" || type === "turn_start") {
207
+ return progressEvent("Codex · starting a turn");
208
+ }
209
+ if (type === "turn.completed" || type === "turn_complete") {
210
+ return progressEvent("Codex · preparing the proposal");
181
211
  }
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
212
  }
187
213
  else if (provider === "opencode") {
188
214
  const part = event.part;
189
215
  if (event.type === "step-start")
190
- return "OpenCode · starting a step";
216
+ return progressEvent("OpenCode · starting a step");
191
217
  if (part?.type === "tool") {
192
218
  const toolName = part.tool || "tool";
193
219
  const inp = part.input;
194
220
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
195
- return detail
221
+ return progressEvent(detail
196
222
  ? `OpenCode · ${toolName} ${snippet(detail)}`
197
- : `OpenCode · ${toolName}`;
223
+ : `OpenCode · ${toolName}`, detail);
198
224
  }
199
225
  if (part?.type === "text") {
200
226
  const text = part.text || "";
201
- return text ? `OpenCode · ${snippet(text, 100)}` : "OpenCode · drafting the response";
227
+ return progressEvent(text ? `OpenCode · ${snippet(text)}` : "OpenCode · drafting the response", text);
202
228
  }
203
229
  if (event.type === "step-finish")
204
- return "OpenCode · finalizing the response";
230
+ return progressEvent("OpenCode · finalizing the response");
205
231
  }
206
232
  }
207
233
  catch {
@@ -253,7 +279,7 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
253
279
  stdout += `${line}\n`;
254
280
  const progress = progressFromLine(line, config.provider);
255
281
  if (progress)
256
- onProgress?.(progress);
282
+ onProgress?.(progress.message, progress.detail);
257
283
  }
258
284
  };
259
285
  child.stdout.on("data", consume);
@@ -273,7 +299,7 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
273
299
  stdout += pending;
274
300
  const progress = progressFromLine(pending, config.provider);
275
301
  if (progress)
276
- onProgress?.(progress);
302
+ onProgress?.(progress.message, progress.detail);
277
303
  }
278
304
  if (exitCode !== 0)
279
305
  throw new Error(localAgentError(command, stderr, exitCode));
@@ -360,7 +386,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
360
386
  output += `${line}\n`;
361
387
  const progress = progressFromLine(line, config.provider);
362
388
  if (progress)
363
- onProgress?.(progress);
389
+ onProgress?.(progress.message, progress.detail);
364
390
  }
365
391
  };
366
392
  child.stdout.on("data", consume);
@@ -371,8 +397,12 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
371
397
  child.once("error", reject);
372
398
  child.once("close", (status) => resolve(status ?? 1));
373
399
  });
374
- if (pending.trim())
400
+ if (pending.trim()) {
375
401
  output += pending;
402
+ const progress = progressFromLine(pending, config.provider);
403
+ if (progress)
404
+ onProgress?.(progress.message, progress.detail);
405
+ }
376
406
  if (code !== 0)
377
407
  throw new Error(localAgentError(command, stderr, code));
378
408
  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";
@@ -65,13 +65,30 @@ function occurrenceCount(content, needle) {
65
65
  from = index + needle.length;
66
66
  }
67
67
  }
68
+ function escapeRegExp(value) {
69
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
+ }
71
+ function whitespaceEquivalentRange(content, oldString) {
72
+ const trimmed = oldString.trim();
73
+ if (!trimmed)
74
+ return null;
75
+ const pattern = trimmed.split(/\s+/).map(escapeRegExp).join("\\s+");
76
+ const matches = Array.from(content.matchAll(new RegExp(pattern, "g")));
77
+ if (matches.length !== 1 || matches[0].index === undefined)
78
+ return null;
79
+ const start = matches[0].index;
80
+ return { start, end: start + matches[0][0].length };
81
+ }
68
82
  function prepareChange(content, change) {
69
83
  const lineEnding = fileLineEnding(content);
70
84
  const oldString = withLineEnding(change.oldString, lineEnding);
71
85
  const newString = withLineEnding(change.newString, lineEnding);
72
86
  const matches = occurrenceCount(content, oldString);
73
87
  if (matches === 0) {
74
- throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated.`);
88
+ const range = whitespaceEquivalentRange(content, oldString);
89
+ if (range)
90
+ return { ...range, oldString: content.slice(range.start, range.end), newString };
91
+ 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
92
  }
76
93
  if (matches > 1) {
77
94
  throw new Error(`Could not safely apply ${change.filePath}. The selected code is not unique (${matches} matches).`);
@@ -244,9 +261,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
244
261
  const selectedConfig = localProvider
245
262
  ? { provider: cliProvider, model: msg.model }
246
263
  : { ...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) => {
264
+ void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
248
265
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
249
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
266
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
250
267
  }).then((answer) => {
251
268
  if (!controller.signal.aborted)
252
269
  socket.send(JSON.stringify({ type: "assistant_message", message: answer }));
@@ -275,9 +292,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
275
292
  const selectedConfig = localProvider
276
293
  ? { provider: cliProvider, model: msg.model }
277
294
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
278
- void (0, agent_1.proposeChanges)(cwd, msg, selectedConfig, controller.signal, (message) => {
295
+ void (0, agent_1.proposeChanges)(cwd, msg, selectedConfig, controller.signal, (message, detail) => {
279
296
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
280
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
297
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
281
298
  }
282
299
  })
283
300
  .then((proposal) => {
@@ -320,9 +337,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
320
337
  activeAgentController?.abort();
321
338
  activeAgentController = controller;
322
339
  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) => {
340
+ void getGitState(cwd).then((git) => (0, agent_1.generateCommitMessage)(cwd, git.status || [], selectedConfig, controller.signal, (message, detail) => {
324
341
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
325
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message }));
342
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
326
343
  })).then((message) => {
327
344
  if (!controller.signal.aborted)
328
345
  socket.send(JSON.stringify({ type: "git_commit_message", message }));
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.13",
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",