@lasso-ai/cli 2.1.15 → 2.1.17

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.
@@ -24,6 +24,11 @@ export type BridgeMessage = {
24
24
  content: string;
25
25
  createdAt?: string;
26
26
  }>;
27
+ conversation?: Array<{
28
+ role: "user" | "assistant" | "system";
29
+ content: string;
30
+ createdAt?: string;
31
+ }>;
27
32
  changesHistory?: Array<{
28
33
  summary: string;
29
34
  changes: any[];
@@ -59,6 +64,11 @@ export type BridgeMessage = {
59
64
  content: string;
60
65
  createdAt?: string;
61
66
  }>;
67
+ conversation?: Array<{
68
+ role: "user" | "assistant" | "system";
69
+ content: string;
70
+ createdAt?: string;
71
+ }>;
62
72
  context?: {
63
73
  selectionId?: string;
64
74
  position?: Record<string, number>;
@@ -96,6 +106,11 @@ export type BridgeMessage = {
96
106
  type: "agent_prompt_response";
97
107
  taskId: string;
98
108
  response: string;
109
+ } | {
110
+ type: "steer";
111
+ taskId: string;
112
+ queueId: string;
113
+ prompt: string;
99
114
  } | {
100
115
  type: "list_page_folders";
101
116
  path?: string;
@@ -144,6 +159,12 @@ export type BridgeMessage = {
144
159
  role: "user" | "assistant";
145
160
  content: string;
146
161
  }>;
162
+ conversation?: Array<{
163
+ role: "user" | "assistant" | "system";
164
+ content: string;
165
+ timestamp?: number;
166
+ }>;
167
+ conversationId?: string;
147
168
  taskId?: string;
148
169
  } | {
149
170
  type: "page_context_response";
@@ -221,6 +242,12 @@ export type ServerBridgeMessage = {
221
242
  kind: "permission" | "input" | "multiselect";
222
243
  options?: string[];
223
244
  };
245
+ } | {
246
+ type: "steer_result";
247
+ taskId: string;
248
+ queueId: string;
249
+ success: boolean;
250
+ error?: string;
224
251
  } | {
225
252
  type: "page_folders";
226
253
  path: string;
@@ -31,7 +31,7 @@ function toRuntimeConversation(messages) {
31
31
  .map((m) => ({
32
32
  role: (m.role === "user" ? "user" : m.role === "assistant" ? "assistant" : "system"),
33
33
  content: m.content,
34
- timestamp: m.createdAt ? Date.parse(m.createdAt) || Date.now() : Date.now(),
34
+ timestamp: typeof m.timestamp === "number" ? m.timestamp : m.createdAt ? Date.parse(m.createdAt) || Date.now() : Date.now(),
35
35
  }));
36
36
  return turns.length > 0 ? turns : undefined;
37
37
  }
@@ -392,11 +392,21 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
392
392
  }));
393
393
  break;
394
394
  }
395
- case "agent_completed":
396
- if (!review)
395
+ case "agent_completed": {
396
+ const data = event.data || {};
397
+ const reply = typeof data.reply === "string" ? data.reply.trim() : "";
398
+ if (reply && data.conversational === true) {
399
+ socket.send(JSON.stringify({
400
+ type: "assistant_message",
401
+ taskId,
402
+ message: reply,
403
+ }));
397
404
  break;
398
- void handleReviewReady(session, socket, taskId, instruction);
405
+ }
406
+ if (review)
407
+ void handleReviewReady(session, socket, taskId, instruction);
399
408
  break;
409
+ }
400
410
  case "error":
401
411
  socket.send(JSON.stringify({
402
412
  type: "agent_status",
@@ -900,6 +910,40 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
900
910
  message: "Agent stopped",
901
911
  }));
902
912
  }
913
+ /** Forward an overlay answer to the runtime's pending user question. */
914
+ async function handleAgentPromptResponse(message, socket) {
915
+ const session = activeSessions.get(message.taskId);
916
+ const facade = taskFacades.get(message.taskId);
917
+ if (!session || !facade) {
918
+ throw new Error("No active agent session is waiting for a response.");
919
+ }
920
+ if (!facade.respondToPrompt) {
921
+ throw new Error("This agent backend does not support interactive prompts.");
922
+ }
923
+ await facade.respondToPrompt(session.id, message.response);
924
+ socket.send(JSON.stringify({
925
+ type: "agent_status",
926
+ taskId: message.taskId,
927
+ status: "working",
928
+ message: "Continuing with your response…",
929
+ }));
930
+ }
931
+ /** Queue a prompt for the active runtime's next model turn. */
932
+ async function handleSteer(message, socket) {
933
+ const session = activeSessions.get(message.taskId);
934
+ const facade = taskFacades.get(message.taskId);
935
+ if (!session || !facade?.steerSession) {
936
+ socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: false, error: "This agent backend does not support live steering." }));
937
+ return;
938
+ }
939
+ try {
940
+ await facade.steerSession(session.id, message.prompt);
941
+ socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: true }));
942
+ }
943
+ catch (error) {
944
+ socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: false, error: error instanceof Error ? error.message : "Failed to steer the agent." }));
945
+ }
946
+ }
903
947
  // Handler for switch_model
904
948
  async function handleSwitchModel(message, socket) {
905
949
  const { taskId, provider, modelId } = message;
@@ -945,7 +989,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
945
989
  }
946
990
  // Handler for oneshot
947
991
  async function handleOneshot(message, socket, cwd) {
948
- const { taskId, prompt, model, provider } = message;
992
+ const { taskId, prompt, model, provider, messages, conversation, conversationId } = message;
949
993
  const instruction = prompt;
950
994
  taskControllers.get(taskId)?.abort();
951
995
  const controller = new AbortController();
@@ -957,6 +1001,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
957
1001
  const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
958
1002
  const sessionInput = {
959
1003
  userPrompt: prompt,
1004
+ // The sidebar is the source of truth for history. `conversation` is
1005
+ // preferred; `messages` remains a compatibility alias for older overlays.
1006
+ conversation: toRuntimeConversation(conversation || messages),
1007
+ conversationUid: conversationId || undefined,
960
1008
  project: {
961
1009
  projectId,
962
1010
  framework,
@@ -1138,6 +1186,12 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
1138
1186
  case "stop":
1139
1187
  await handleStop(msg, socket);
1140
1188
  break;
1189
+ case "agent_prompt_response":
1190
+ await handleAgentPromptResponse(msg, socket);
1191
+ break;
1192
+ case "steer":
1193
+ await handleSteer(msg, socket);
1194
+ break;
1141
1195
  case "switch_model":
1142
1196
  await handleSwitchModel(msg, socket);
1143
1197
  break;
@@ -157,6 +157,7 @@ export interface RuntimeFacade {
157
157
  runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
158
158
  providerApiKey?: string;
159
159
  browserContextProvider?: BrowserContextProvider;
160
+ devServerUrl?: string;
160
161
  }): Promise<void>;
161
162
  getSession(sessionId: string): Promise<RuntimeSession>;
162
163
  verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
@@ -166,6 +167,8 @@ export interface RuntimeFacade {
166
167
  name?: string;
167
168
  }, apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
168
169
  cancelSession(sessionId: string): Promise<void>;
170
+ respondToPrompt?(sessionId: string, response: string): Promise<void>;
171
+ steerSession?(sessionId: string, prompt: string): Promise<void>;
169
172
  }
170
173
  export type RuntimeEvent = {
171
174
  type: "agent_started";
@@ -315,6 +318,7 @@ export declare class RuntimeClient {
315
318
  runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
316
319
  providerApiKey?: string;
317
320
  browserContextProvider?: BrowserContextProvider;
321
+ devServerUrl?: string;
318
322
  }): Promise<void>;
319
323
  /** Parse and dispatch one complete SSE frame. */
320
324
  private processSseFrame;
@@ -326,6 +330,10 @@ export declare class RuntimeClient {
326
330
  * Cancel a running session
327
331
  */
328
332
  cancelSession(sessionId: string): Promise<void>;
333
+ /** Resume a hosted runtime that is waiting for an answer from the user. */
334
+ respondToPrompt(sessionId: string, response: string): Promise<void>;
335
+ /** Queue a prompt for the next turn of an active hosted runtime. */
336
+ steerSession(sessionId: string, prompt: string): Promise<void>;
329
337
  /**
330
338
  * Switch model/backend for a session
331
339
  */
@@ -83,7 +83,10 @@ class RuntimeClient {
83
83
  // pre-run workspace capture could not be established.
84
84
  const guard = await index_js_1.WorkspaceGuard.capture(projectPath);
85
85
  const executor = new index_js_1.ActionExecutor(projectPath, opts?.browserContextProvider);
86
- const verifier = new index_js_1.VerificationEngine(projectPath);
86
+ const verifier = new index_js_1.VerificationEngine(projectPath, {
87
+ devServerUrl: opts?.devServerUrl || process.env.LASSO_DEV_SERVER_URL,
88
+ browserContextProvider: opts?.browserContextProvider,
89
+ });
87
90
  // Idempotent: called on agent_completed AND on stream failure, so the
88
91
  // diff is always cached before the safety-net restore wipes the edits.
89
92
  let finalized = false;
@@ -268,6 +271,36 @@ class RuntimeClient {
268
271
  throw new Error(`Failed to cancel session: ${response.statusText}`);
269
272
  }
270
273
  }
274
+ /** Resume a hosted runtime that is waiting for an answer from the user. */
275
+ async respondToPrompt(sessionId, response) {
276
+ const result = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/user-response`, {
277
+ method: "POST",
278
+ headers: {
279
+ "Content-Type": "application/json",
280
+ ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
281
+ },
282
+ body: JSON.stringify({ response }),
283
+ });
284
+ if (!result.ok) {
285
+ const body = (await result.json().catch(() => ({})));
286
+ throw new Error(body.error || `Failed to submit agent response: ${result.statusText}`);
287
+ }
288
+ }
289
+ /** Queue a prompt for the next turn of an active hosted runtime. */
290
+ async steerSession(sessionId, prompt) {
291
+ const result = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/steer`, {
292
+ method: "POST",
293
+ headers: {
294
+ "Content-Type": "application/json",
295
+ ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
296
+ },
297
+ body: JSON.stringify({ prompt }),
298
+ });
299
+ if (!result.ok) {
300
+ const body = (await result.json().catch(() => ({})));
301
+ throw new Error(body.error || `Failed to queue steering prompt: ${result.statusText}`);
302
+ }
303
+ }
271
304
  /**
272
305
  * Switch model/backend for a session
273
306
  */
package/dist/overlay.js CHANGED
@@ -11522,7 +11522,6 @@
11522
11522
  align-items: center;
11523
11523
  justify-content: space-between;
11524
11524
  padding: 6px 8px 8px;
11525
- border-bottom: 1px solid var(--lo-border-subtle);
11526
11525
  font-size: 11px;
11527
11526
  font-weight: 600;
11528
11527
  color: var(--lo-text-3);
@@ -11553,6 +11552,48 @@
11553
11552
  width: 100%;
11554
11553
  }
11555
11554
 
11555
+ .lasso-command-history-load {
11556
+ display: flex;
11557
+ align-items: center;
11558
+ justify-content: space-between;
11559
+ gap: 8px;
11560
+ min-width: 0;
11561
+ flex: 1;
11562
+ padding: 0;
11563
+ border: 0;
11564
+ background: transparent;
11565
+ color: inherit;
11566
+ cursor: pointer;
11567
+ text-align: left;
11568
+ }
11569
+
11570
+ .lasso-command-history-delete {
11571
+ display: inline-flex;
11572
+ align-items: center;
11573
+ justify-content: center;
11574
+ flex: 0 0 auto;
11575
+ width: 24px;
11576
+ height: 24px;
11577
+ padding: 0;
11578
+ border: 0;
11579
+ border-radius: 5px;
11580
+ background: transparent;
11581
+ color: var(--lo-text-3);
11582
+ cursor: pointer;
11583
+ opacity: 0;
11584
+ transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
11585
+ }
11586
+
11587
+ .lasso-command-history-item:hover .lasso-command-history-delete,
11588
+ .lasso-command-history-delete:focus-visible {
11589
+ opacity: 1;
11590
+ }
11591
+
11592
+ .lasso-command-history-delete:hover {
11593
+ color: #fca5a5;
11594
+ background: rgba(239, 68, 68, 0.14);
11595
+ }
11596
+
11556
11597
  .lasso-command-history-item:hover {
11557
11598
  background: var(--lo-surface-hover);
11558
11599
  }
@@ -19700,17 +19741,44 @@
19700
19741
  </div>
19701
19742
  ${conversationList.map(
19702
19743
  (conv) => `
19703
- <button class="lasso-command-history-item ${conv.id === activeConversationId ? "active" : ""}" type="button" data-conv-id="${conv.id}">
19704
- <span class="lasso-command-history-title" title="${escapeHtml4(conv.title || "Untitled Session")}">${escapeHtml4(conv.title || "Untitled Session")}</span>
19705
- <div class="lasso-command-history-meta">
19706
- <span class="lasso-command-history-scope ${conv.scope || "private"}">${conv.scope || "private"}</span>
19707
- <span>${formatDateAgo(conv.updatedAt)}</span>
19708
- </div>
19709
- </button>
19744
+ <div class="lasso-command-history-item ${conv.id === activeConversationId ? "active" : ""}" data-conv-id="${escapeHtml4(conv.id)}">
19745
+ <button class="lasso-command-history-load" type="button" title="Open session">
19746
+ <span class="lasso-command-history-title" title="${escapeHtml4(conv.title || "Untitled Session")}">${escapeHtml4(conv.title || "Untitled Session")}</span>
19747
+ <div class="lasso-command-history-meta">
19748
+ <span class="lasso-command-history-scope ${conv.scope || "private"}">${conv.scope || "private"}</span>
19749
+ <span>${formatDateAgo(conv.updatedAt)}</span>
19750
+ </div>
19751
+ </button>
19752
+ <button class="lasso-command-history-delete" type="button" title="Delete session" aria-label="Delete session">
19753
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5M14 11v5"/></svg>
19754
+ </button>
19755
+ </div>
19710
19756
  `
19711
19757
  ).join("")}
19712
19758
  `;
19713
19759
  }
19760
+ async function deleteConversation(convId) {
19761
+ const conversation = conversationList.find((item) => item.id === convId);
19762
+ if (!conversation) return;
19763
+ if (!convId.startsWith("local-session-")) {
19764
+ const deleted = await apiFetch(`/agent/conversations/${encodeURIComponent(convId)}`, { method: "DELETE" });
19765
+ if (!deleted?.success) return;
19766
+ }
19767
+ try {
19768
+ localStorage.removeItem(`lasso:session:${convId}`);
19769
+ const key = getStorageProjectKey();
19770
+ const list2 = JSON.parse(localStorage.getItem(key) || "[]");
19771
+ localStorage.setItem(key, JSON.stringify(list2.filter((item) => item.id !== convId)));
19772
+ } catch {
19773
+ }
19774
+ conversationList = conversationList.filter((item) => item.id !== convId);
19775
+ if (activeConversationId === convId) {
19776
+ activeConversationId = null;
19777
+ commandMessages = [];
19778
+ renderMessages();
19779
+ }
19780
+ renderHistoryPopover();
19781
+ }
19714
19782
  async function switchConversation(convId) {
19715
19783
  if (activeThinkingInterval) {
19716
19784
  clearInterval(activeThinkingInterval);
@@ -20038,9 +20106,16 @@
20038
20106
  });
20039
20107
  commandHistoryPopover.addEventListener("click", async (event) => {
20040
20108
  event.stopPropagation();
20041
- const item = event.target.closest(".lasso-command-history-item");
20109
+ const target = event.target;
20110
+ const item = target.closest(".lasso-command-history-item");
20042
20111
  if (!item?.dataset.convId) return;
20043
- await switchConversation(item.dataset.convId);
20112
+ if (target.closest(".lasso-command-history-delete")) {
20113
+ await deleteConversation(item.dataset.convId);
20114
+ return;
20115
+ }
20116
+ if (target.closest(".lasso-command-history-load")) {
20117
+ await switchConversation(item.dataset.convId);
20118
+ }
20044
20119
  });
20045
20120
  const setCommandModelMenuOpen = (open) => {
20046
20121
  if (!commandModelMenu || !commandModelBtn) return;
@@ -20194,6 +20269,19 @@
20194
20269
  }
20195
20270
  return;
20196
20271
  }
20272
+ const steerBtn = target.closest(".lasso-command-steer-btn");
20273
+ if (steerBtn) {
20274
+ const msgId = steerBtn.closest(".lasso-command-msg")?.dataset.msgId;
20275
+ const msg = commandMessages.find((m) => m.id === msgId);
20276
+ if (msg?.queued && !msg.steering && activeTaskId && state.bridgeSocket?.readyState === WebSocket.OPEN) {
20277
+ steerBtn.disabled = true;
20278
+ steerBtn.textContent = "Steering\u2026";
20279
+ msg.steering = true;
20280
+ state.bridgeSocket.send(JSON.stringify({ type: "steer", taskId: activeTaskId, queueId: msg.id, prompt: msg.content }));
20281
+ renderMessages();
20282
+ }
20283
+ return;
20284
+ }
20197
20285
  const fileCheck = target.closest(".lasso-command-file-check");
20198
20286
  if (fileCheck) {
20199
20287
  const msgId = fileCheck.closest(".lasso-command-msg")?.dataset.msgId;
@@ -20405,6 +20493,21 @@ Please proceed directly with the implementation and code changes from this point
20405
20493
  async function handleSubmit() {
20406
20494
  if (!commandInput) return;
20407
20495
  if (activeTaskId) {
20496
+ const queuedPrompt = commandInput.value.trim();
20497
+ if (queuedPrompt) {
20498
+ commandMessages.push({
20499
+ id: `queued-${Date.now()}`,
20500
+ role: "user",
20501
+ content: queuedPrompt,
20502
+ timestamp: Date.now(),
20503
+ queued: true
20504
+ });
20505
+ commandInput.value = "";
20506
+ autoResizeTextarea(commandInput);
20507
+ renderMessages();
20508
+ scrollToBottom();
20509
+ return;
20510
+ }
20408
20511
  if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
20409
20512
  state.bridgeSocket.send(
20410
20513
  JSON.stringify({
@@ -20488,9 +20591,7 @@ Please proceed directly with the implementation and code changes from this point
20488
20591
  }
20489
20592
  }, 1e3);
20490
20593
  if (!activeConversationId) {
20491
- createServerConversation().then((id) => {
20492
- if (id) activeConversationId = id;
20493
- });
20594
+ activeConversationId = await createServerConversation();
20494
20595
  }
20495
20596
  collabEmit("collab:action", {
20496
20597
  sessionId: state.collabProjectId,
@@ -20510,7 +20611,7 @@ Please proceed directly with the implementation and code changes from this point
20510
20611
  // The current prompt is sent in its dedicated `prompt` field. Keep it
20511
20612
  // out of history so the model sees it once, not once as history and
20512
20613
  // once as the active request.
20513
- messages: commandMessages.map((message, index) => ({ message, index })).filter(({ message, index }) => Boolean(message.content) && !message.error && index !== commandMessages.reduce((last, item, itemIndex) => item.role === "user" && item.content === prompt ? itemIndex : last, -1)).map(({ message }) => message).map((m) => ({ role: m.role, content: m.content }))
20614
+ conversation: commandMessages.map((message, index) => ({ message, index })).filter(({ message, index }) => Boolean(message.content) && !message.error && index !== commandMessages.reduce((last, item, itemIndex) => item.role === "user" && item.content === prompt ? itemIndex : last, -1)).map(({ message }) => ({ role: message.role, content: message.content, timestamp: message.timestamp }))
20514
20615
  })
20515
20616
  );
20516
20617
  } else {
@@ -20663,6 +20764,18 @@ Please proceed directly with the implementation and code changes from this point
20663
20764
  renderMessages();
20664
20765
  }
20665
20766
  }
20767
+ function handleCommandBarSteerResult(message) {
20768
+ const queued = commandMessages.find((msg) => msg.id === message.queueId);
20769
+ if (!queued) return;
20770
+ queued.steering = false;
20771
+ if (message.success) {
20772
+ queued.queued = false;
20773
+ queued.steered = true;
20774
+ } else {
20775
+ queued.error = message.error || "The agent could not accept this steering prompt.";
20776
+ }
20777
+ renderMessages();
20778
+ }
20666
20779
  function handleCommandBarPrompt(message) {
20667
20780
  const taskId2 = message.taskId;
20668
20781
  let targetMsg = commandMessages.find((m) => m.id === taskId2);
@@ -20716,6 +20829,7 @@ Please proceed directly with the implementation and code changes from this point
20716
20829
  html += `
20717
20830
  <div class="lasso-command-msg user" data-msg-id="${msg.id}">
20718
20831
  <div class="lasso-command-msg-bubble">${escapeHtml4(msg.content)}</div>
20832
+ ${msg.queued ? `<button class="lasso-command-steer-btn" type="button"${msg.steering ? " disabled" : ""} style="margin-top: 6px; padding: 4px 9px; border: 1px solid rgba(96, 165, 250, 0.45); border-radius: 6px; background: rgba(59, 130, 246, 0.14); color: #bfdbfe; cursor: pointer;">${msg.steering ? "Steering\u2026" : "Steer"}</button>` : msg.steered ? `<span class="lasso-command-steered-label" style="display: inline-block; margin-top: 6px; color: #93c5fd; font-size: 12px;">Steered into next turn</span>` : ""}
20719
20833
  </div>
20720
20834
  `;
20721
20835
  } else {
@@ -21821,6 +21935,15 @@ Please proceed directly with the implementation and code changes from this point
21821
21935
  window.dispatchEvent(new CustomEvent("lasso-page-folder-created", { detail: { path: message.path || "", error: message.error } }));
21822
21936
  return;
21823
21937
  }
21938
+ if (message.type === "steer_result" && message.taskId && message.queueId) {
21939
+ handleCommandBarSteerResult({
21940
+ taskId: message.taskId,
21941
+ queueId: message.queueId,
21942
+ success: Boolean(message.success),
21943
+ error: message.error
21944
+ });
21945
+ return;
21946
+ }
21824
21947
  if (message.type === "agent_status" && message.status && message.message) {
21825
21948
  const taskId2 = message.taskId;
21826
21949
  if (isCommandBarTaskId(taskId2)) {
@@ -195,10 +195,12 @@ export interface LassoModelResponse {
195
195
  raw?: string;
196
196
  };
197
197
  decision: {
198
- type: "inspect" | "plan" | "execute" | "verify" | "recover" | "ask_user";
198
+ type: "respond" | "inspect" | "plan" | "execute" | "verify" | "recover" | "ask_user";
199
199
  goal: string;
200
200
  };
201
201
  actions: AgentAction[];
202
+ /** Direct conversational answer when no project work is requested. */
203
+ reply?: string;
202
204
  expectedOutcome?: string;
203
205
  }
204
206
  export type AgentEventType = "agent_started" | "agent_reasoning" | "agent_completed" | "agent_failed" | "agent_cancelled" | "phase_changed" | "tool_started" | "tool_completed" | "tool_call" | "observation" | "plan_created" | "file_changed" | "verification_started" | "verification_completed" | "error" | "model_changed" | "waiting_for_user";
@@ -10,6 +10,14 @@ export interface VerificationConfig {
10
10
  buildTimeout: number;
11
11
  devServerTimeout: number;
12
12
  browserTimeout: number;
13
+ /** URL of the project's already-running dev server, if available. */
14
+ devServerUrl?: string;
15
+ /** Optional browser bridge used for DOM/runtime verification. */
16
+ browserContextProvider?: (action: {
17
+ id: string;
18
+ type: "browser_check";
19
+ data?: Record<string, unknown>;
20
+ }) => Promise<unknown>;
13
21
  }
14
22
  export interface VerificationResult {
15
23
  type: "typecheck" | "build" | "dev_server" | "browser";
@@ -68,4 +76,6 @@ export declare class VerificationEngine {
68
76
  private hasTypeScriptConfig;
69
77
  /** package.json with a non-empty build script. */
70
78
  private hasBuildScript;
79
+ /** Fetch a health URL with a portable timeout for supported Node versions. */
80
+ private fetchWithTimeout;
71
81
  }
@@ -19,6 +19,8 @@ class VerificationEngine {
19
19
  buildTimeout: config?.buildTimeout || 120000,
20
20
  devServerTimeout: config?.devServerTimeout || 30000,
21
21
  browserTimeout: config?.browserTimeout || 60000,
22
+ devServerUrl: config?.devServerUrl,
23
+ browserContextProvider: config?.browserContextProvider,
22
24
  };
23
25
  }
24
26
  /**
@@ -43,7 +45,6 @@ class VerificationEngine {
43
45
  // Dev server health
44
46
  const devServerResult = await this.verifyDevServer(phase);
45
47
  results.push(devServerResult);
46
- // Browser check (placeholder)
47
48
  const browserResult = await this.verifyBrowser(phase);
48
49
  results.push(browserResult);
49
50
  return results;
@@ -283,23 +284,48 @@ class VerificationEngine {
283
284
  const startTime = Date.now();
284
285
  const observations = [];
285
286
  const errors = [];
286
- // TODO: Implement actual dev server health check
287
- // For now, just check if a dev server is likely running
288
- observations.push({
289
- id: this.generateId(),
290
- type: "build",
291
- summary: "Dev server health check skipped (not implemented)",
292
- data: { type: "dev_server", success: true },
293
- timestamp: Date.now(),
294
- phase,
295
- });
296
- return {
297
- type: "dev_server",
298
- success: true,
299
- observations,
300
- errors,
301
- duration: Date.now() - startTime,
302
- };
287
+ const url = this.config.devServerUrl;
288
+ if (!url) {
289
+ observations.push({
290
+ id: this.generateId(),
291
+ type: "build",
292
+ summary: "Dev server health check skipped (no devServerUrl configured)",
293
+ data: { type: "dev_server", success: true, skipped: true },
294
+ timestamp: Date.now(),
295
+ phase,
296
+ });
297
+ return { type: "dev_server", success: true, skipped: true, observations, errors, duration: Date.now() - startTime };
298
+ }
299
+ try {
300
+ const response = await this.fetchWithTimeout(url, this.config.devServerTimeout);
301
+ const success = response.status >= 200 && response.status < 400;
302
+ const summary = success ? `Dev server responded with HTTP ${response.status}` : `Dev server returned HTTP ${response.status}`;
303
+ observations.push({
304
+ id: this.generateId(),
305
+ type: "build",
306
+ summary,
307
+ data: { type: "dev_server", success, url, status: response.status },
308
+ timestamp: Date.now(),
309
+ phase,
310
+ });
311
+ if (!success) {
312
+ errors.push({
313
+ id: this.generateId(),
314
+ type: "verification",
315
+ message: summary,
316
+ phase,
317
+ timestamp: Date.now(),
318
+ recoverable: true,
319
+ });
320
+ }
321
+ return { type: "dev_server", success, command: `GET ${url}`, error: success ? undefined : summary, observations, errors, duration: Date.now() - startTime };
322
+ }
323
+ catch (error) {
324
+ const message = `Dev server health check failed: ${error?.message || String(error)}`;
325
+ observations.push({ id: this.generateId(), type: "build", summary: message, data: { type: "dev_server", success: false, url }, timestamp: Date.now(), phase });
326
+ errors.push({ id: this.generateId(), type: "verification", message, phase, timestamp: Date.now(), recoverable: true });
327
+ return { type: "dev_server", success: false, command: `GET ${url}`, error: message, observations, errors, duration: Date.now() - startTime };
328
+ }
303
329
  }
304
330
  /**
305
331
  * Verify browser/runtime state
@@ -308,23 +334,34 @@ class VerificationEngine {
308
334
  const startTime = Date.now();
309
335
  const observations = [];
310
336
  const errors = [];
311
- // TODO: Implement actual browser checking via Puppeteer or similar
312
- // For now, this is a placeholder
313
- observations.push({
314
- id: this.generateId(),
315
- type: "browser",
316
- summary: "Browser check skipped (not implemented)",
317
- data: { type: "browser", success: true },
318
- timestamp: Date.now(),
319
- phase,
320
- });
321
- return {
322
- type: "browser",
323
- success: true,
324
- observations,
325
- errors,
326
- duration: Date.now() - startTime,
327
- };
337
+ const provider = this.config.browserContextProvider;
338
+ if (!provider) {
339
+ observations.push({
340
+ id: this.generateId(),
341
+ type: "browser",
342
+ summary: "Browser verification skipped (no browser bridge configured)",
343
+ data: { type: "browser", success: true, skipped: true },
344
+ timestamp: Date.now(),
345
+ phase,
346
+ });
347
+ return { type: "browser", success: true, skipped: true, observations, errors, duration: Date.now() - startTime };
348
+ }
349
+ try {
350
+ const result = await Promise.race([provider({ id: this.generateId(), type: "browser_check", data: { phase } }), new Promise((_, reject) => setTimeout(() => reject(new Error("Browser verification timed out")), this.config.browserTimeout))]);
351
+ const details = result && typeof result === "object" ? result : {};
352
+ const success = details.success !== false;
353
+ const summary = typeof details.summary === "string" ? details.summary : success ? "Browser check passed" : "Browser check failed";
354
+ observations.push({ id: this.generateId(), type: "browser", summary, data: { type: "browser", success, result }, timestamp: Date.now(), phase });
355
+ if (!success)
356
+ errors.push({ id: this.generateId(), type: "verification", message: summary, phase, timestamp: Date.now(), recoverable: true });
357
+ return { type: "browser", success, error: success ? undefined : summary, observations, errors, duration: Date.now() - startTime };
358
+ }
359
+ catch (error) {
360
+ const message = `Browser verification failed: ${error?.message || String(error)}`;
361
+ observations.push({ id: this.generateId(), type: "browser", summary: message, data: { type: "browser", success: false }, timestamp: Date.now(), phase });
362
+ errors.push({ id: this.generateId(), type: "verification", message, phase, timestamp: Date.now(), recoverable: true });
363
+ return { type: "browser", success: false, error: message, observations, errors, duration: Date.now() - startTime };
364
+ }
328
365
  }
329
366
  /**
330
367
  * Check if verification passed overall
@@ -370,5 +407,16 @@ class VerificationEngine {
370
407
  return false;
371
408
  }
372
409
  }
410
+ /** Fetch a health URL with a portable timeout for supported Node versions. */
411
+ async fetchWithTimeout(url, timeoutMs) {
412
+ const controller = new AbortController();
413
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
414
+ try {
415
+ return await fetch(url, { signal: controller.signal, redirect: "manual" });
416
+ }
417
+ finally {
418
+ clearTimeout(timer);
419
+ }
420
+ }
373
421
  }
374
422
  exports.VerificationEngine = VerificationEngine;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "2.1.15",
3
+ "version": "2.1.17",
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",