@lasso-ai/cli 2.1.3 → 2.1.6

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.
@@ -33,6 +33,8 @@ export interface DetectedBackend {
33
33
  error?: string;
34
34
  fixInstructions?: string;
35
35
  }
36
+ /** OpenCode-hosted free models are restricted to OpenCode's own product UI. */
37
+ export declare function isOpenCodeHeadlessRestrictedModel(modelId: string): boolean;
36
38
  /**
37
39
  * Probe all supported local agents in parallel.
38
40
  * Fault-tolerant: a missing or broken CLI never rejects the whole list.
@@ -16,6 +16,7 @@
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.LOCAL_MODEL_PREFIXES = void 0;
19
+ exports.isOpenCodeHeadlessRestrictedModel = isOpenCodeHeadlessRestrictedModel;
19
20
  exports.detectBackends = detectBackends;
20
21
  exports.modelIdForBackend = modelIdForBackend;
21
22
  const node_child_process_1 = require("node:child_process");
@@ -43,6 +44,11 @@ async function probe(bin, args, timeout = 10000) {
43
44
  function stripAnsi(text) {
44
45
  return text.replace(/\u001b\[[0-9;]*m/g, "").replace(/\u001b\][^\u0007]*\u0007/g, "");
45
46
  }
47
+ /** OpenCode-hosted free models are restricted to OpenCode's own product UI. */
48
+ function isOpenCodeHeadlessRestrictedModel(modelId) {
49
+ const id = modelId.trim().toLowerCase();
50
+ return id === "opencode/big-pickle" || id.endsWith("-free") || id.includes("-free-");
51
+ }
46
52
  function parseJson(stdout) {
47
53
  const start = stdout.indexOf("{");
48
54
  if (start < 0)
@@ -135,6 +141,7 @@ async function detectOpenCode() {
135
141
  .split("\n")
136
142
  .map((line) => line.trim())
137
143
  .filter((line) => /^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(line))
144
+ .filter((line) => !isOpenCodeHeadlessRestrictedModel(line))
138
145
  .slice(0, 30);
139
146
  base.models = ids.map((full) => {
140
147
  const name = full.split("/")[1];
@@ -146,10 +153,7 @@ async function detectOpenCode() {
146
153
  });
147
154
  }
148
155
  if (base.models.length === 0) {
149
- base.models = [
150
- { id: "opencode:big-pickle", label: "Big Pickle", modelId: "opencode/big-pickle" },
151
- { id: "opencode:deepseek-v4-flash", label: "DeepSeek V4 Flash", modelId: "opencode/deepseek-v4-flash" },
152
- ];
156
+ base.models = [{ id: "opencode:claude-sonnet-4", label: "Claude Sonnet 4", modelId: "opencode/claude-sonnet-4" }];
153
157
  }
154
158
  return base;
155
159
  }
@@ -145,7 +145,11 @@ export type BridgeMessage = {
145
145
  content: string;
146
146
  }>;
147
147
  taskId?: string;
148
+ } | {
149
+ type: "page_context_response";
150
+ requestId: string;
148
151
  pageContext?: any;
152
+ error?: string;
149
153
  } | {
150
154
  type: "restore_snapshot";
151
155
  snapshotId: string;
@@ -22,6 +22,28 @@ const backends_1 = require("./backends");
22
22
  const local_runtime_1 = require("./local-runtime");
23
23
  exports.DEFAULT_BRIDGE_PORT = 3056;
24
24
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
25
+ function cleanConversationReply(value) {
26
+ const text = typeof value === "string" ? value.trim() : "";
27
+ if (!text)
28
+ return "";
29
+ try {
30
+ const parsed = JSON.parse(text);
31
+ if (typeof parsed?.reply === "string")
32
+ return parsed.reply.trim();
33
+ }
34
+ catch {
35
+ const match = text.match(/"reply"\s*:\s*"((?:\\.|[^"\\])*)"/);
36
+ if (match) {
37
+ try {
38
+ return JSON.parse(`"${match[1]}"`).trim();
39
+ }
40
+ catch {
41
+ return match[1].replace(/\\n/g, "\n").replace(/\\"/g, '"').trim();
42
+ }
43
+ }
44
+ }
45
+ return text;
46
+ }
25
47
  async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
26
48
  try {
27
49
  const response = await fetch(`http://127.0.0.1:${port}/__lasso/bridge/restart`, { method: "POST" });
@@ -143,6 +165,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
143
165
  // in this process), RuntimeClient for hosted models (via Lasso server).
144
166
  const taskFacades = new Map();
145
167
  const taskControllers = new Map();
168
+ const pendingBrowserContext = new Map();
146
169
  const correctiveAttempts = new Map();
147
170
  const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
148
171
  const fileEnv = envRoots.reduce((values, root) => ({
@@ -167,6 +190,16 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
167
190
  }
168
191
  return undefined;
169
192
  }
193
+ function requestBrowserContext(action, taskId) {
194
+ return new Promise((resolve, reject) => {
195
+ if (!overlaySocket || overlaySocket.readyState !== overlaySocket.OPEN) {
196
+ reject(new Error("Live browser context is unavailable because the overlay is disconnected."));
197
+ return;
198
+ }
199
+ pendingBrowserContext.set(action.id, { resolve, reject });
200
+ overlaySocket.send(JSON.stringify({ type: "page_context_request", requestId: action.id, taskId, check: action.data || {} }));
201
+ });
202
+ }
170
203
  const cliCredentials = (0, auth_1.loadCredentials)();
171
204
  const resolvedLassoKey = cliCredentials?.apiKey ||
172
205
  (0, project_1.resolveLassoApiKey)(fileEnv) ||
@@ -271,55 +304,95 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
271
304
  return;
272
305
  switch (event.type) {
273
306
  case "agent_started":
274
- case "agent_reasoning":
275
- socket.send(JSON.stringify({
276
- type: "agent_status",
277
- taskId,
278
- status: "thinking",
279
- message: event.data.summary || "Working...",
280
- }));
307
+ // The command bar owns the neutral initial status; real runtime events
308
+ // replace it as soon as the agent has useful progress to report.
281
309
  break;
282
- case "tool_started":
283
- socket.send(JSON.stringify({
284
- type: "agent_status",
285
- taskId,
286
- status: "working",
287
- message: `Running ${event.data.tool || event.data.actionType || "tool"}...`,
288
- }));
310
+ case "agent_reasoning": {
311
+ // The model's actual reasoning summary — dynamic content only.
312
+ // Raw thoughts (when the provider exposes them) ride along as the
313
+ // step's detail line.
314
+ const summary = event.data?.summary;
315
+ if (summary) {
316
+ const raw = event.data?.raw;
317
+ const detail = typeof raw === "string" && raw.trim()
318
+ ? raw.length > 800
319
+ ? `${raw.slice(0, 800)}…`
320
+ : raw
321
+ : undefined;
322
+ socket.send(JSON.stringify({
323
+ type: "agent_status",
324
+ taskId,
325
+ status: "thinking",
326
+ message: summary,
327
+ detail,
328
+ }));
329
+ }
289
330
  break;
290
- case "tool_completed":
291
- socket.send(JSON.stringify({
292
- type: "agent_status",
293
- taskId,
294
- status: "working",
295
- message: "Completed tool",
296
- }));
331
+ }
332
+ case "tool_started": {
333
+ const data = event.data || {};
334
+ // Model invocations are internal — the reasoning summaries bracket
335
+ // them; never surface "Running model" style steps.
336
+ if (data.actionType === "model")
337
+ break;
338
+ const message = data.label || data.tool;
339
+ if (message) {
340
+ socket.send(JSON.stringify({
341
+ type: "agent_status",
342
+ taskId,
343
+ status: "working",
344
+ message,
345
+ detail: data.detail,
346
+ }));
347
+ }
297
348
  break;
298
- case "phase_changed": {
299
- const phase = event.data.phase;
300
- socket.send(JSON.stringify({
301
- type: "agent_status",
302
- taskId,
303
- status: "working",
304
- message: `Phase: ${phase}`,
305
- }));
349
+ }
350
+ case "tool_completed": {
351
+ const data = event.data || {};
352
+ if (data.actionType === "model")
353
+ break;
354
+ // Success: the step auto-completes when the next event arrives.
355
+ // Only failures surface, with the real error.
356
+ if (data.success === false) {
357
+ const message = data.label || data.tool;
358
+ socket.send(JSON.stringify({
359
+ type: "agent_status",
360
+ taskId,
361
+ status: "working",
362
+ message: message ? `${message} didn't finish` : "A step didn't finish",
363
+ detail: data.error || data.detail,
364
+ }));
365
+ }
306
366
  break;
307
367
  }
368
+ case "phase_changed":
369
+ // Internal phase names ("execute", "verify") are jargon — the
370
+ // reasoning and action labels already tell the real story.
371
+ break;
308
372
  case "verification_started":
309
- socket.send(JSON.stringify({
310
- type: "agent_status",
311
- taskId,
312
- status: "review",
313
- message: "Verifying changes...",
314
- }));
373
+ // No canned status line — the check names/results arrive with
374
+ // verification_completed and become the real progress copy.
315
375
  break;
316
376
  case "verification_completed": {
317
- const passed = event.data.success ?? event.data.passed;
377
+ const data = event.data || {};
378
+ const passed = data.success ?? data.passed;
379
+ const summaries = (data.observations || [])
380
+ .map((o) => o?.summary)
381
+ .filter(Boolean);
382
+ const errList = (data.errors || [])
383
+ .map((e) => e?.message)
384
+ .filter(Boolean);
385
+ const message = summaries.length
386
+ ? summaries.join(" · ")
387
+ : passed
388
+ ? "Checks passed"
389
+ : "Checks failed";
318
390
  socket.send(JSON.stringify({
319
391
  type: "agent_status",
320
392
  taskId,
321
393
  status: passed ? "working" : "error",
322
- message: passed ? "Verification passed" : "Verification failed",
394
+ message,
395
+ ...(errList.length ? { detail: errList.join("\n") } : {}),
323
396
  }));
324
397
  break;
325
398
  }
@@ -348,6 +421,28 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
348
421
  break;
349
422
  }
350
423
  }
424
+ async function sendLocalConversationReply(facade, session, taskId, socket) {
425
+ const finalSession = await facade.getSession(session.id);
426
+ const messages = finalSession.conversation || [];
427
+ const reply = [...messages]
428
+ .reverse()
429
+ .find((message) => message?.role === "assistant" && typeof message.content === "string")?.content?.trim();
430
+ if (reply) {
431
+ socket.send(JSON.stringify({
432
+ type: "assistant_message",
433
+ taskId,
434
+ message: reply,
435
+ }));
436
+ }
437
+ else {
438
+ socket.send(JSON.stringify({
439
+ type: "agent_status",
440
+ taskId,
441
+ status: "stopped",
442
+ message: "Complete",
443
+ }));
444
+ }
445
+ }
351
446
  // The server rolled the workspace back before review, so the current files
352
447
  // are the originals: capture them for undo, then open the review panel.
353
448
  async function handleReviewReady(session, socket, taskId, instruction) {
@@ -368,37 +463,124 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
368
463
  return { filePath: change.filePath, content };
369
464
  }));
370
465
  (0, snapshot_1.createSnapshot)(cwd, taskId, instruction, changes.map((change) => change.filePath), undefined, session.id);
466
+ socket.send(JSON.stringify({
467
+ type: "agent_status",
468
+ taskId,
469
+ status: "review",
470
+ message: "Review changes",
471
+ changes,
472
+ }));
371
473
  }
474
+ else {
475
+ await sendLocalConversationReply(facade, session, taskId, socket);
476
+ }
477
+ }
478
+ catch (error) {
372
479
  socket.send(JSON.stringify({
373
480
  type: "agent_status",
374
481
  taskId,
375
- status: "review",
376
- message: changes.length > 0 ? "Review changes" : "No file changes were produced",
377
- changes,
482
+ status: "error",
483
+ message: error instanceof Error ? error.message : "Failed to load review changes",
378
484
  }));
379
485
  }
486
+ }
487
+ // Handler for edit/ask - uses runtime
488
+ async function handleHostedConversation(taskId, prompt, model, provider, messages, socket, signal) {
489
+ const requestController = new AbortController();
490
+ const abortRequest = () => requestController.abort();
491
+ signal.addEventListener("abort", abortRequest, { once: true });
492
+ const timeout = setTimeout(() => {
493
+ if (!signal.aborted)
494
+ socket.send(JSON.stringify({
495
+ type: "agent_status",
496
+ taskId,
497
+ status: "error",
498
+ message: "The conversation timed out. Please try again.",
499
+ }));
500
+ requestController.abort();
501
+ }, 30_000);
502
+ try {
503
+ const response = await fetch(`${serverUrl}/api/v1/agent/session`, {
504
+ method: "POST",
505
+ headers: {
506
+ Accept: "text/event-stream",
507
+ "Content-Type": "application/json",
508
+ ...(resolvedLassoKey ? { Authorization: `Bearer ${resolvedLassoKey}` } : {}),
509
+ },
510
+ body: JSON.stringify({
511
+ prompt,
512
+ model: { id: model, provider },
513
+ messages: Array.isArray(messages) ? messages : undefined,
514
+ }),
515
+ signal: requestController.signal,
516
+ });
517
+ if (!response.ok || !response.body) {
518
+ throw new Error(`Conversation request failed (${response.status})`);
519
+ }
520
+ const reader = response.body.getReader();
521
+ const decoder = new TextDecoder();
522
+ let buffer = "";
523
+ while (true) {
524
+ const { done, value } = await reader.read();
525
+ if (done)
526
+ break;
527
+ buffer += decoder.decode(value, { stream: true });
528
+ const lines = buffer.split("\n");
529
+ buffer = lines.pop() || "";
530
+ for (const line of lines) {
531
+ if (!line.startsWith("data: "))
532
+ continue;
533
+ const payload = line.slice(6).trim();
534
+ if (!payload || payload === "[DONE]")
535
+ continue;
536
+ const event = JSON.parse(payload);
537
+ if (event.type === "error")
538
+ throw new Error(event.error || "Conversation failed");
539
+ if (event.type === "done") {
540
+ socket.send(JSON.stringify({
541
+ type: "assistant_message",
542
+ taskId,
543
+ message: cleanConversationReply(event.reply || event.summary),
544
+ }));
545
+ }
546
+ }
547
+ }
548
+ }
380
549
  catch (error) {
550
+ if (signal.aborted)
551
+ return;
381
552
  socket.send(JSON.stringify({
382
553
  type: "agent_status",
383
554
  taskId,
384
555
  status: "error",
385
- message: error instanceof Error ? error.message : "Failed to load review changes",
556
+ message: error instanceof Error ? error.message : "Conversation failed",
386
557
  }));
387
558
  }
559
+ finally {
560
+ clearTimeout(timeout);
561
+ signal.removeEventListener("abort", abortRequest);
562
+ taskControllers.delete(taskId);
563
+ }
388
564
  }
389
- // Handler for edit/ask - uses runtime
390
565
  async function handleEditOrAsk(message, socket, taskId, instruction, model, provider, context, element, messages) {
391
566
  taskControllers.get(taskId)?.abort();
392
567
  const controller = new AbortController();
393
568
  taskControllers.set(taskId, controller);
569
+ const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
570
+ if (message.type === "ask" && !isLocalAgent) {
571
+ await handleHostedConversation(taskId, instruction, model, provider, messages, socket, controller.signal);
572
+ return;
573
+ }
574
+ // Do not echo the user's prompt into the thinking feed. The prompt is
575
+ // already visible in the conversation; use a neutral status while the
576
+ // runtime produces its first useful event.
394
577
  socket.send(JSON.stringify({
395
578
  type: "agent_status",
396
579
  taskId,
397
580
  status: "thinking",
398
- message: "Starting...",
581
+ message: "Thinking…",
399
582
  }));
400
583
  try {
401
- const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
402
584
  // Overlay model option ids are prefixed ("claude_code:sonnet") — the
403
585
  // agent CLIs need the unprefixed/normalized form.
404
586
  const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
@@ -441,7 +623,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
441
623
  // already rolled back at that point).
442
624
  await facade.runSession(session.id, cwd, (event) => {
443
625
  handleRuntimeEvent(event, socket, taskId, session, `Runtime session: ${instruction}`, true);
444
- }, controller.signal, { providerApiKey: providerApiKeyFor(provider) });
626
+ }, controller.signal, {
627
+ providerApiKey: providerApiKeyFor(provider),
628
+ browserContextProvider: ((action) => requestBrowserContext(action, taskId)),
629
+ });
445
630
  }
446
631
  catch (error) {
447
632
  socket.send(JSON.stringify({
@@ -706,12 +891,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
706
891
  taskControllers.get(taskId)?.abort();
707
892
  const controller = new AbortController();
708
893
  taskControllers.set(taskId, controller);
709
- socket.send(JSON.stringify({
710
- type: "agent_status",
711
- taskId,
712
- status: "thinking",
713
- message: "Starting oneshot...",
714
- }));
894
+ // Greetings and conversational messages still go through the selected
895
+ // agent/model. The overlay only skips expensive page capture for them;
896
+ // there are no canned assistant responses in the bridge.
715
897
  try {
716
898
  const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
717
899
  const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
@@ -743,13 +925,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
743
925
  // Oneshot results are reviewed exactly like edit/ask: the workspace is
744
926
  // rolled back and only approval re-applies the changes.
745
927
  handleRuntimeEvent(event, socket, taskId, session, `Oneshot: ${instruction}`, true);
746
- if (event.type === "agent_completed") {
747
- socket.send(JSON.stringify({
748
- type: "assistant_message",
749
- taskId,
750
- message: "Oneshot completed",
751
- }));
752
- }
753
928
  }, controller.signal, { providerApiKey: providerApiKeyFor(provider) });
754
929
  }
755
930
  catch (error) {
@@ -990,6 +1165,11 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
990
1165
  }));
991
1166
  }
992
1167
  break;
1168
+ case "runtime_error":
1169
+ // Overlay-side crash (e.g. ReferenceError) — surface it in the
1170
+ // daemon log so it's diagnosable.
1171
+ console.error("[overlay error]", msg.selectionId || "", msg.details || msg.message || msg);
1172
+ break;
993
1173
  default:
994
1174
  console.warn("Unknown bridge message type:", msg.type);
995
1175
  }
@@ -16,6 +16,11 @@ export declare class LocalRuntime implements RuntimeFacade {
16
16
  getSession(sessionId: string): Promise<RuntimeSession>;
17
17
  runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, _opts?: {
18
18
  providerApiKey?: string;
19
+ browserContextProvider?: (action: {
20
+ id: string;
21
+ type: "browser_check";
22
+ data?: Record<string, unknown>;
23
+ }) => Promise<unknown>;
19
24
  }): Promise<void>;
20
25
  verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
21
26
  switchModel(sessionId: string, model: {
@@ -36,6 +36,7 @@ class LocalRuntime {
36
36
  status: "running",
37
37
  iteration: 0,
38
38
  userPrompt: input.userPrompt,
39
+ mode: input.mode || "task",
39
40
  conversation: [
40
41
  { id: node_crypto_1.default.randomUUID(), role: "user", content: input.userPrompt, timestamp: now.getTime() },
41
42
  ],
@@ -99,6 +100,7 @@ class LocalRuntime {
99
100
  const outcome = await runner.run({
100
101
  taskId: record.id,
101
102
  userPrompt: record.userPrompt,
103
+ mode: record.mode,
102
104
  project: {
103
105
  projectId: record.project.projectId || "local",
104
106
  framework: record.project.framework,
@@ -116,31 +116,52 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
116
116
  return;
117
117
  switch (event.type) {
118
118
  case "agent_started":
119
- onProgress("thinking", "Agent started", "Initializing runtime");
120
119
  break;
121
- case "agent_reasoning":
122
- const summary = event.data.summary || "Thinking...";
123
- thinkingSteps.push({ title: summary, durationMs: 0 });
124
- onProgress("thinking", summary);
120
+ case "agent_reasoning": {
121
+ const summary = event.data?.summary;
122
+ if (summary) {
123
+ thinkingSteps.push({ title: summary, durationMs: 0 });
124
+ onProgress("thinking", summary);
125
+ }
125
126
  break;
126
- case "tool_started":
127
- const tool = event.data.tool || "tool";
128
- onProgress("working", `Running ${tool}...`);
127
+ }
128
+ case "tool_started": {
129
+ const data = event.data || {};
130
+ if (data.actionType === "model")
131
+ break;
132
+ const message = data.label || data.tool;
133
+ if (message)
134
+ onProgress("working", message);
129
135
  break;
130
- case "tool_completed":
131
- onProgress("working", "Tool completed");
136
+ }
137
+ case "tool_completed": {
138
+ const data = event.data || {};
139
+ if (data.actionType === "model")
140
+ break;
141
+ if (data.success === false) {
142
+ const message = data.label || data.tool;
143
+ onProgress("working", message ? `${message} didn't finish` : "A step didn't finish");
144
+ }
132
145
  break;
146
+ }
133
147
  case "phase_changed":
134
- const phase = event.data.phase;
135
- onProgress("working", `Phase: ${phase}`);
136
148
  break;
137
149
  case "verification_started":
138
- onProgress("review", "Verifying changes...");
150
+ // Real check names/results arrive with verification_completed.
139
151
  break;
140
- case "verification_completed":
141
- const passed = event.data.success ?? event.data.passed;
142
- onProgress(passed ? "working" : "error", passed ? "Verification passed" : "Verification failed");
152
+ case "verification_completed": {
153
+ const data = event.data || {};
154
+ const passed = data.success ?? data.passed;
155
+ const summaries = (data.observations || [])
156
+ .map((o) => o?.summary)
157
+ .filter(Boolean);
158
+ onProgress(passed ? "working" : "error", summaries.length
159
+ ? summaries.join(" · ")
160
+ : passed
161
+ ? "Checks passed"
162
+ : "Checks failed");
143
163
  break;
164
+ }
144
165
  case "waiting_for_user":
145
166
  const question = event.data?.question || "Input required";
146
167
  onPrompt({ question });
@@ -11,6 +11,8 @@ import { type ToolCallRequest } from "../runtime/index.js";
11
11
  export type RuntimeProvider = "anthropic" | "openai" | "google" | "nvidia" | "ollama" | "claude_code" | "opencode" | "codex_cli" | "cursor";
12
12
  export interface RuntimeSessionInput {
13
13
  userPrompt: string;
14
+ /** Conversation-only requests must not enter the coding/tool workflow. */
15
+ mode?: "conversation" | "task";
14
16
  project: {
15
17
  projectId: string;
16
18
  framework?: string;
@@ -50,6 +52,11 @@ export interface RuntimeSessionInput {
50
52
  projectPath?: string;
51
53
  };
52
54
  }
55
+ export type BrowserContextProvider = (action: {
56
+ id: string;
57
+ type: "browser_check";
58
+ data?: Record<string, unknown>;
59
+ }) => Promise<unknown>;
53
60
  export interface RuntimeSession {
54
61
  id: string;
55
62
  phase: "understand" | "inspect" | "plan" | "execute" | "verify" | "recover" | "complete";
@@ -139,6 +146,7 @@ export interface RuntimeFacade {
139
146
  createSession(input: RuntimeSessionInput): Promise<RuntimeSession>;
140
147
  runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
141
148
  providerApiKey?: string;
149
+ browserContextProvider?: BrowserContextProvider;
142
150
  }): Promise<void>;
143
151
  getSession(sessionId: string): Promise<RuntimeSession>;
144
152
  verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
@@ -159,6 +167,7 @@ export type RuntimeEvent = {
159
167
  sessionId: string;
160
168
  data: {
161
169
  summary: string;
170
+ raw?: string;
162
171
  };
163
172
  timestamp: number;
164
173
  } | {
@@ -174,6 +183,7 @@ export type RuntimeEvent = {
174
183
  sessionId: string;
175
184
  data: {
176
185
  actionType: string;
186
+ label?: string;
177
187
  };
178
188
  timestamp: number;
179
189
  } | {
@@ -181,7 +191,9 @@ export type RuntimeEvent = {
181
191
  sessionId: string;
182
192
  data: {
183
193
  actionType: string;
194
+ label?: string;
184
195
  success: boolean;
196
+ error?: string;
185
197
  };
186
198
  timestamp: number;
187
199
  } | {
@@ -292,6 +304,7 @@ export declare class RuntimeClient {
292
304
  */
293
305
  runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
294
306
  providerApiKey?: string;
307
+ browserContextProvider?: BrowserContextProvider;
295
308
  }): Promise<void>;
296
309
  /** Execute a tool_call request locally against the real project. */
297
310
  private executeToolCallLocally;
@@ -86,7 +86,7 @@ class RuntimeClient {
86
86
  catch (error) {
87
87
  console.error("WorkspaceGuard capture failed:", error);
88
88
  }
89
- const executor = new index_js_1.ActionExecutor(projectPath);
89
+ const executor = new index_js_1.ActionExecutor(projectPath, opts?.browserContextProvider);
90
90
  const verifier = new index_js_1.VerificationEngine(projectPath);
91
91
  const finalizeGuard = async () => {
92
92
  if (!guard)