@lasso-ai/cli 1.0.29 → 1.1.1

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.
package/ARCHITECTURE.md CHANGED
@@ -67,9 +67,11 @@ Three parties, all coordinating around one shared filesystem:
67
67
  └──────────────────────┘
68
68
  ```
69
69
 
70
- Everything except the coding agent call runs locally. Nothing is ever written to disk
71
- without an explicit accept from the user. The agent is the only party that ever leaves the
72
- machine, and only with the exact context the CLI assembled for it.
70
+ Everything except the coding agent call runs locally. Edit proposals are never written to
71
+ disk without an explicit accept from the user. The New Page workflow is a separate explicit
72
+ write path: it creates only the file at the folder the user selected in the project
73
+ explorer. The agent is the only party that ever leaves the machine, and only with the
74
+ exact context the CLI assembled for it.
73
75
 
74
76
  ## 4. Browser layer (capture only)
75
77
 
@@ -85,6 +87,17 @@ Responsibilities, and _only_ these:
85
87
  edit quality even when the AI has the source.
86
88
  - Show the inline prompt box anchored to the selection (never a blocking `prompt()`).
87
89
  - Render the diff/accept/undo UI once the CLI streams a proposed change back.
90
+ - Render concurrent Agent Tasks with In progress/Completed tabs and task-level review
91
+ actions; acceptance is the transition that completes an edit task.
92
+ - Keep a stable component conversation identity so repeated prompts reuse conversation
93
+ and change history across DOM replacement during HMR.
94
+ - Render the secondary toolbar, custom tooltips, and the New Page dialog with a lazy
95
+ project-folder tree. The tree is populated per-directory on expand; the CLI never
96
+ sends the whole project at once. Selecting a folder sets the write target, and a
97
+ new nested folder is created inline through its own bridge message.
98
+ - Render task-scoped permission/input prompts and send explicit responses back over
99
+ the bridge. The prompt must re-enable pointer events on its panel, because the
100
+ overlay host disables them to keep the toolbar click-through.
88
101
  - **Never mutate the DOM.** All visual changes the user sees post-edit come from the
89
102
  framework's own HMR reload, not from this layer.
90
103
 
@@ -111,6 +124,17 @@ A Node process that wraps the user's existing dev server. Entry: `src/cli/index.
111
124
  on receipt.
112
125
  6. On accept: snapshot the file's prior content (for undo), then apply the diff.
113
126
  7. On undo: restore the snapshot, full stop — no re-generation needed, so retries are free.
127
+ 8. For New Page: answer `list_page_folders` one directory at a time so the overlay can
128
+ build its tree lazily, skipping dot-entries and build/vendor directories. Every
129
+ request resolves against the project root, so `..` and absolute paths are refused.
130
+ Create a file only after the user confirms, and refuse to overwrite. Nested folder
131
+ creation arrives separately as `create_page_folder` and never accepts a slash or a
132
+ dotted name.
133
+ 9. For local agents: keep child-process stdin attached to its task and forward explicit
134
+ overlay responses when the agent requests permission or input. Classify stderr
135
+ before prompting: a filesystem or transport failure is reported as an error with a
136
+ suggested fix, because Allow/Deny cannot resolve it. Only a genuine approval or
137
+ sign-in request becomes a prompt, and the text is cleaned of log prefixes first.
114
138
 
115
139
  ### 5.2 Source resolution strategies
116
140
 
package/CHANGELOG.md CHANGED
@@ -9,6 +9,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - **Agent task workflow**: concurrent prompts now appear in In progress and
13
+ Completed tabs. Review-ready tasks expose a Review action, and accepted changes
14
+ move the task to Completed.
15
+ - **Component conversation continuity**: prompts reuse the latest conversation and
16
+ change history for a stable component identity across repeated prompts and DOM
17
+ replacement during HMR.
18
+ - **Interactive local-agent prompts**: Claude Code, Codex, and OpenCode run with
19
+ piped stdin. Permission or input requests can be answered from a task-scoped
20
+ overlay prompt with Allow, Deny, or custom text.
21
+ - **Secondary overlay toolbar**: added a New Page workflow with a project-folder
22
+ explorer, filename/content form, path validation, and explicit file creation.
23
+ - **Redesigned New Page dialog**: replaced the flat folder dropdown with a
24
+ VS Code-style lazy tree of the project. You type only a page name and pick a
25
+ folder; nested folders are created inline with a `+` button, folders load on
26
+ demand, a filter box narrows deep trees, and clicking a file reuses its name.
27
+ Selecting a folder no longer collapses it, and the starter file is generated
28
+ from the extension (`.tsx`, `.ts`, `.css`, `.json`, `.md`).
29
+ - **Readable agent permission prompts**: the prompt no longer dumps raw local-agent
30
+ log lines. Filesystem failures such as `EACCES: permission denied` and
31
+ `Cause([Die([…])])` now surface as errors with a suggested fix, because
32
+ Allow/Deny can never resolve them. Only genuine approval and sign-in requests
33
+ open the Allow/Deny prompt.
34
+ - **Clickable agent prompt**: the prompt panel opts back into pointer events, so
35
+ Allow, Deny, and custom options respond to clicks again; Enter submits typed
36
+ input and Escape sends Deny, matching the close button.
37
+ - **Custom toolbar tooltips**: toolbar labels now use an accessible Lasso-styled
38
+ tooltip on hover and keyboard focus instead of browser-native title popovers.
39
+ - **Filtered agent progress**: useful tool/progress events remain visible while
40
+ process IDs, startup messages, and heartbeat logs are hidden.
12
41
  - Open-source documentation: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`,
13
42
  `SECURITY.md`, `SUPPORT.md`, `LICENSE` (ISC), and this changelog.
14
43
  - **Lasso Host** (`src/cli/host/`) — a user-level local domain server + runtime
@@ -99,4 +128,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
128
  ### Notes
100
129
 
101
130
  - Version 0.1.0 was developed as an internal prototype. This changelog records
102
- behavior from that point forward. The project uses the product name **Lasso**.
131
+ behavior from that point forward. The project uses the product name **Lasso**.
package/README.md CHANGED
@@ -87,6 +87,17 @@ Turn on Lasso Mode and interact with your application normally.
87
87
  - Hover to see what will be selected
88
88
  - Select empty space to insert new UI
89
89
 
90
+ ### Overlay toolbars
91
+
92
+ The overlay includes a primary editing toolbar and a compact secondary toolbar.
93
+ The secondary toolbar's **New Page** action opens a project explorer. You type a page
94
+ name and pick the folder it should live in — the tree loads lazily as you expand
95
+ it, a filter box narrows deep trees, and the `+` button next to **Folder** creates
96
+ a new nested folder inline. Clicking a file in the tree reuses its name so you only
97
+ type the part that is missing. Starter content is generated from the extension, and
98
+ Lasso never chooses a source folder for you or overwrites an existing file without
99
+ confirmation.
100
+
90
101
  ### Source-aware editing
91
102
 
92
103
  Lasso doesn't stop at the DOM.
@@ -133,6 +144,20 @@ Accept ────────→ Write to source
133
144
 
134
145
  Accepted changes are written to the filesystem and your framework's normal development workflow takes over.
135
146
 
147
+ ### Agent tasks and review
148
+
149
+ Multiple prompts can run concurrently from the **Agent Tasks** panel. Tasks are
150
+ separated into **In progress** and **Completed** tabs. Edit tasks expose a Review
151
+ action when a proposal is ready; accepting the diff applies the changes and moves
152
+ the task to Completed. Conversation and change history are retained for the same
153
+ component during the overlay session, including when the framework replaces its DOM
154
+ node during HMR.
155
+
156
+ Local CLI agents stream useful tool/progress events while they work. Infrastructure
157
+ noise such as process IDs and heartbeat messages is hidden. If a local agent needs
158
+ permission or input, Lasso presents a task-scoped prompt with Allow, Deny, or a
159
+ custom response field.
160
+
136
161
  ### Bring your own agent
137
162
 
138
163
  Lasso's editing pipeline is agent-agnostic.
@@ -145,8 +170,8 @@ Use:
145
170
 
146
171
  Your selection and project context stay inside the Lasso pipeline while the actual coding agent can be swapped independently.
147
172
 
148
- When installed, Lasso automatically detects the `claude` and `codex` commands
149
- on your PATH and adds **Claude Code · Local** and **Codex · Local** to the
173
+ When installed, Lasso automatically detects the `claude`, `codex`, and
174
+ `opencode` commands on your PATH and adds the available local agents to the
150
175
  prompt model menu. Local agents run in read-only/planning mode and return a
151
176
  reviewable proposal; Lasso remains the only process that writes accepted
152
177
  changes to your source files.
@@ -336,6 +361,14 @@ Undo → restore the previous snapshot
336
361
 
337
362
  That's it.
338
363
 
364
+ ### Create a page manually
365
+
366
+ Use **New Page** in the secondary toolbar to open the project explorer. Type a page
367
+ name, expand the tree to pick the target folder, and select **Create page**. Use the
368
+ `+` button beside **Folder** to create a nested folder without leaving the dialog,
369
+ or the filter box to narrow a large tree. The bridge validates that the folder stays
370
+ inside the project and refuses to overwrite an existing file.
371
+
339
372
  ---
340
373
 
341
374
  ## Agent configuration
@@ -462,6 +495,9 @@ The CLI:
462
495
  8. Shows you the diff
463
496
  9. Writes only after you accept
464
497
 
498
+ The separate New Page workflow is also explicit: it writes only after you pick
499
+ a folder in the project explorer, type a name, and choose **Create page**.
500
+
465
501
  The goal is simple:
466
502
 
467
503
  **Your development environment stays yours.**
@@ -504,6 +540,11 @@ The core visual editing pipeline is being built around:
504
540
  - Explicit accept/reject
505
541
  - Undo
506
542
  - Pluggable coding agents
543
+ - Concurrent agent tasks with review/completed states
544
+ - Component-scoped conversation and change history
545
+ - Manual page creation with project-folder selection
546
+ - Interactive permission/input prompts for local agents
547
+ - Custom toolbar tooltips and filtered agent progress
507
548
 
508
549
  Expect breaking changes before `1.0`.
509
550
 
@@ -4,6 +4,7 @@ export type SourceChange = {
4
4
  newString: string;
5
5
  };
6
6
  type AgentInput = {
7
+ taskId?: string;
7
8
  instruction: string;
8
9
  model: string;
9
10
  messages?: Array<{
@@ -45,17 +46,25 @@ export type AgentConfig = {
45
46
  };
46
47
  export type LocalAgent = "claude-code" | "codex" | "opencode";
47
48
  export type AgentProgress = (message: string, detail?: string) => void;
49
+ export type AgentPrompt = {
50
+ message: string;
51
+ kind: "permission" | "input";
52
+ options?: string[];
53
+ };
54
+ export type AgentPromptHandler = (prompt: AgentPrompt) => void;
48
55
  export type AgentAnswer = {
56
+ taskId?: string;
49
57
  question: string;
50
58
  context?: AgentInput["context"];
51
59
  element: AgentInput["element"];
52
60
  messages?: AgentInput["messages"];
53
61
  };
62
+ export declare function respondToAgentPrompt(taskId: string, value: string): boolean;
54
63
  export declare function detectLocalAgents(): Promise<Set<LocalAgent>>;
55
- export declare function proposeChanges(cwd: string, input: AgentInput, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress): Promise<{
64
+ export declare function proposeChanges(cwd: string, input: AgentInput, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress, onPrompt?: AgentPromptHandler): Promise<{
56
65
  summary: string;
57
66
  changes: SourceChange[];
58
67
  }>;
59
- export declare function answerQuestion(cwd: string, input: AgentAnswer, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress): Promise<string>;
68
+ export declare function answerQuestion(cwd: string, input: AgentAnswer, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress, onPrompt?: AgentPromptHandler): Promise<string>;
60
69
  export declare function generateCommitMessage(cwd: string, status: string[], config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress): Promise<string>;
61
70
  export {};
package/dist/cli/agent.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.respondToAgentPrompt = respondToAgentPrompt;
6
7
  exports.detectLocalAgents = detectLocalAgents;
7
8
  exports.proposeChanges = proposeChanges;
8
9
  exports.answerQuestion = answerQuestion;
@@ -12,6 +13,14 @@ const node_path_1 = __importDefault(require("node:path"));
12
13
  const node_child_process_1 = require("node:child_process");
13
14
  const node_util_1 = require("node:util");
14
15
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
16
+ const activeAgentInputs = new Map();
17
+ function respondToAgentPrompt(taskId, value) {
18
+ const respond = activeAgentInputs.get(taskId);
19
+ if (!respond)
20
+ return false;
21
+ respond(value);
22
+ return true;
23
+ }
15
24
  async function detectLocalAgents() {
16
25
  const found = new Set();
17
26
  for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"]]) {
@@ -202,7 +211,7 @@ function extractLocalAgentText(raw, provider) {
202
211
  }
203
212
  return texts.at(-1) || raw;
204
213
  }
205
- function collectTextFields(value, fields = new Set(["text", "output_text", "result", "content", "message"]), depth = 0) {
214
+ function collectTextFields(value, fields = new Set(["text", "output_text", "result", "content", "message", "output", "value"]), depth = 0) {
206
215
  if (depth > 8 || value == null)
207
216
  return [];
208
217
  if (typeof value === "string")
@@ -220,6 +229,17 @@ function collectTextFields(value, fields = new Set(["text", "output_text", "resu
220
229
  }
221
230
  return output;
222
231
  }
232
+ function findStructuredProposals(value, depth = 0) {
233
+ if (depth > 8 || value == null || typeof value !== "object")
234
+ return [];
235
+ if (!Array.isArray(value)) {
236
+ const record = value;
237
+ if (Array.isArray(record.changes))
238
+ return [JSON.stringify(record)];
239
+ return Object.values(record).flatMap((entry) => findStructuredProposals(entry, depth + 1));
240
+ }
241
+ return value.flatMap((entry) => findStructuredProposals(entry, depth + 1));
242
+ }
223
243
  function extractLocalAgentProposal(raw, provider) {
224
244
  const outputs = [];
225
245
  const lines = raw.split(/\r?\n/).filter((line) => line.trim());
@@ -259,6 +279,14 @@ function extractLocalAgentProposal(raw, provider) {
259
279
  if (part?.type === "text" && typeof part.text === "string")
260
280
  outputs.push(part.text);
261
281
  }
282
+ for (const proposal of streamObjects.flatMap((event) => findStructuredProposals(event)).reverse()) {
283
+ try {
284
+ return jsonFrom(proposal);
285
+ }
286
+ catch {
287
+ // Continue through text candidates below.
288
+ }
289
+ }
262
290
  // Tool events are interleaved with the final answer. Try text events in
263
291
  // reverse order, then the complete stream as a final fallback.
264
292
  for (const output of [...outputs.reverse(), raw]) {
@@ -269,12 +297,76 @@ function extractLocalAgentProposal(raw, provider) {
269
297
  // Keep looking; this output may only be a progress or tool event.
270
298
  }
271
299
  }
272
- throw new Error("The agent returned no valid reviewable changes. Progress output may have been mixed with the final JSON.");
300
+ throw new Error(`${provider === "opencode" ? "OpenCode" : "The agent"} did not return a reviewable proposal. It produced progress/tool events but no final JSON containing file changes.`);
273
301
  }
274
302
  function snippet(value, max = 80) {
275
303
  const s = String(value ?? "").trim().replace(/\s+/g, " ");
276
304
  return s.length > max ? `${s.slice(0, max)}…` : s;
277
305
  }
306
+ const ANSI_PATTERN = /\u001B\[[0-9;?]*[ -/]*[@-~]/g;
307
+ const LOG_FIELD_PATTERN = /([A-Za-z_][\w.-]*)=("(?:[^"\\]|\\.)*"|[^\s]*)/g;
308
+ function cleanAgentText(value) {
309
+ return value.replace(ANSI_PATTERN, "").replace(/\s+/g, " ").trim();
310
+ }
311
+ /**
312
+ * Local agents (notably OpenCode) print structured logs such as
313
+ * `timestamp=… level=ERROR run=… message="Failed to fetch models.dev" cause="…"`.
314
+ * Those lines are machine diagnostics, never user-facing copy, so they are parsed
315
+ * into their fields instead of being shown verbatim.
316
+ */
317
+ function parseAgentLogFields(line) {
318
+ if (!/^\s*(?:timestamp|time|ts)=\S/.test(line))
319
+ return null;
320
+ const fields = {};
321
+ for (const match of line.matchAll(LOG_FIELD_PATTERN)) {
322
+ const key = match[1];
323
+ const raw = match[2] ?? "";
324
+ fields[key] = raw.startsWith('"')
325
+ ? raw.slice(1, -1).replace(/\\(["\\])/g, "$1")
326
+ : raw;
327
+ }
328
+ return fields;
329
+ }
330
+ // Filesystem failures that a permission request can never resolve.
331
+ const AGENT_FAILURE_MARKERS = /\b(?:EACCES|EPERM|ENOENT|EEXIST|EISDIR|ENOTDIR|ENOSPC|EMFILE|ENOTEMPTY)\b|\bCause\(\[Die\(/i;
332
+ const AGENT_FAILURE_HINTS = [
333
+ {
334
+ pattern: /EACCES: permission denied[\s\S]*(?:locks?|state|storage|\.config|config\/)/i,
335
+ message: "The local agent cannot write to its own state folder. Fix that folder's ownership (for example `sudo chown -R $(whoami) ~/.local/state/opencode`) and retry.",
336
+ },
337
+ {
338
+ pattern: /EACCES: permission denied/i,
339
+ message: "The local agent was denied access to a file or folder it needs. Check the permissions on its state directory, then retry.",
340
+ },
341
+ {
342
+ pattern: /failed to fetch models\.dev/i,
343
+ message: "The local agent could not reach models.dev to refresh its model list. Check the network or proxy it uses, then retry.",
344
+ },
345
+ ];
346
+ function agentFailureHint(line) {
347
+ return AGENT_FAILURE_HINTS.find((hint) => hint.pattern.test(line))?.message;
348
+ }
349
+ const PERMISSION_PROMPT_PATTERN = /permission\s+(?:is\s+)?(?:required|requested|needed|denied)|needs?\s+(?:your\s+)?permission|permission\s+to\s+(?:run|edit|write|read|execute|create|delete|modify|access|apply)|approve\s+(?:this|the|tool|action|command)|authori[sz]e\s+(?:this|the|tool|action|command)|authentication\s+(?:is\s+)?required|(?:please\s+)?log\s?in|sign\s?in\s+(?:required|to\s+continue)|press\s+.{1,24}?to\s+(?:continue|approve|confirm)|waiting for (?:your )?(?:permission|approval|input|response)/i;
350
+ /**
351
+ * Returns human-readable copy for a real approval/authentication request, or
352
+ * `null` when the line is anything else. A structured error log never becomes a
353
+ * permission prompt: Allow/Deny cannot fix a filesystem failure, and hiding the
354
+ * error behind those buttons is what made the prompt unreadable.
355
+ */
356
+ function permissionPromptMessage(raw) {
357
+ const line = cleanAgentText(raw);
358
+ if (!line)
359
+ return null;
360
+ if (parseAgentLogFields(line) || AGENT_FAILURE_MARKERS.test(line))
361
+ return null;
362
+ if (!PERMISSION_PROMPT_PATTERN.test(line))
363
+ return null;
364
+ const detail = line
365
+ .replace(/^[A-Za-z0-9_.-]+:\s*/, "")
366
+ .replace(/^["'\s]+|["'\s]+$/g, "")
367
+ .trim();
368
+ return snippet(detail || line, 240);
369
+ }
278
370
  function progressEvent(message, detail) {
279
371
  const value = detail == null ? "" : String(detail).trim();
280
372
  return value ? { message, detail: value } : { message };
@@ -382,6 +474,12 @@ function progressEventsFromRaw(raw, provider) {
382
474
  }
383
475
  function localAgentError(command, stderr, exitCode) {
384
476
  const output = stderr.trim();
477
+ const hint = agentFailureHint(output);
478
+ if (hint)
479
+ return `${command === "opencode" ? "OpenCode" : command} could not finish: ${hint}`;
480
+ if (command === "opencode" && /waiting for permission or authentication/i.test(output)) {
481
+ return "OpenCode needs permission or authentication. Run OpenCode once in a terminal, approve the requested access or sign in, then retry in Lasso.";
482
+ }
385
483
  if (command === "codex" && output.includes("missing field `base_instructions`")) {
386
484
  return "Codex could not read its models cache because it uses an older cache format. Update Codex, then retry. Lasso already bypasses the repository trust check.";
387
485
  }
@@ -406,9 +504,9 @@ function localCommand(provider, model, prompt) {
406
504
  }
407
505
  return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
408
506
  }
409
- function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
507
+ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
410
508
  return new Promise((resolve, reject) => {
411
- const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
509
+ const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
412
510
  const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : "Claude Code";
413
511
  let stdout = "";
414
512
  let stderr = "";
@@ -416,6 +514,18 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
416
514
  let pendingStderr = "";
417
515
  let settled = false;
418
516
  let timedOut = false;
517
+ let promptShown = false;
518
+ const sendPrompt = (message, kind = "permission") => {
519
+ if (!taskId || promptShown)
520
+ return;
521
+ promptShown = true;
522
+ activeAgentInputs.set(taskId, (value) => {
523
+ promptShown = false;
524
+ activeAgentInputs.delete(taskId);
525
+ child.stdin.write(`${value}\n`);
526
+ });
527
+ onPrompt?.({ message, kind, options: kind === "permission" ? ["Allow", "Deny"] : undefined });
528
+ };
419
529
  const timeout = setTimeout(() => {
420
530
  timedOut = true;
421
531
  child.kill("SIGTERM");
@@ -443,7 +553,14 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
443
553
  for (const progress of progressEventsFromRaw(line, provider)) {
444
554
  onProgress?.(progress.message, progress.detail);
445
555
  }
446
- if (stream === "stderr" && line.trim()) {
556
+ const promptMessage = permissionPromptMessage(line);
557
+ if (promptMessage) {
558
+ sendPrompt(promptMessage);
559
+ }
560
+ else if (stream === "stderr" && line.trim()) {
561
+ const hint = agentFailureHint(line);
562
+ if (hint)
563
+ onProgress?.(`${label} · ${hint}`);
447
564
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
448
565
  onProgress?.("OpenCode · provider rejected the request; stopping this task…");
449
566
  child.kill("SIGTERM");
@@ -463,6 +580,8 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
463
580
  return;
464
581
  settled = true;
465
582
  clearTimeout(timeout);
583
+ if (taskId)
584
+ activeAgentInputs.delete(taskId);
466
585
  signal?.removeEventListener("abort", abort);
467
586
  reject(error);
468
587
  });
@@ -471,6 +590,8 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
471
590
  return;
472
591
  settled = true;
473
592
  clearTimeout(timeout);
593
+ if (taskId)
594
+ activeAgentInputs.delete(taskId);
474
595
  signal?.removeEventListener("abort", abort);
475
596
  if (timedOut) {
476
597
  reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
@@ -488,6 +609,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
488
609
  for (const progress of progressEventsFromRaw(stderrTail, provider)) {
489
610
  onProgress?.(progress.message, progress.detail);
490
611
  }
612
+ const hint = agentFailureHint(stderrTail);
613
+ if (hint)
614
+ onProgress?.(`${label} · ${hint}`);
491
615
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
492
616
  onProgress?.("OpenCode · provider rejected the request; stopping this task…");
493
617
  }
@@ -496,18 +620,18 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
496
620
  });
497
621
  });
498
622
  }
499
- async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
623
+ async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, taskId, onPrompt) {
500
624
  const outputContract = `Return ONLY valid JSON in this exact shape: {"summary":"short explanation","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}. Treat the supplied source context as read-only. Before returning, verify every oldString against that context. Use project-relative paths only. Do not edit files, run write commands, commit, or produce markdown fences.`;
501
625
  const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
502
626
  const local = localCommand(config.provider, config.model, prompt);
503
627
  const command = local.command;
504
628
  const args = local.args;
505
- const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress);
629
+ const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress, taskId, onPrompt);
506
630
  if (result.exitCode !== 0)
507
631
  throw new Error(localAgentError(command, result.stderr, result.exitCode));
508
632
  return extractLocalAgentProposal(result.stdout, config.provider);
509
633
  }
510
- async function proposeChanges(cwd, input, config, signal, onProgress) {
634
+ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
511
635
  const context = await contextFor(cwd, input.element);
512
636
  const visualContext = input.context ? { ...input.context, screenshots: undefined } : undefined;
513
637
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
@@ -517,7 +641,7 @@ async function proposeChanges(cwd, input, config, signal, onProgress) {
517
641
  : "";
518
642
  const instruction = `Selection context:\n${JSON.stringify(input.element, null, 2)}\n${JSON.stringify(visualContext, null, 2)}\n\nConversation history:\n${history}\n\nPrevious change history for this selection:\n${priorChanges}\n\nRelevant source context:\n${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}\n\nReturn ONLY JSON: {"summary":"short explanation","changes":[{"filePath":"relative/path","oldString":"exact text","newString":"replacement text"}]}`;
519
643
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
520
- return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress);
644
+ return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
521
645
  }
522
646
  const system = "You are Lasso, a careful source-code editing agent. Return only valid JSON. Each oldString must occur exactly once in its file. Never rewrite whole files. Keep changes focused on the request.";
523
647
  const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : "claude-sonnet-4-20250514");
@@ -568,13 +692,13 @@ async function proposeChanges(cwd, input, config, signal, onProgress) {
568
692
  throw new Error("The agent returned an empty response.");
569
693
  return jsonFrom(text);
570
694
  }
571
- async function answerQuestion(cwd, input, config, signal, onProgress) {
695
+ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
572
696
  const context = await contextFor(cwd, input.element);
573
697
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
574
698
  const prompt = `Answer the user's question conversationally and directly. Do not propose file changes and do not return JSON. If the question is about the selected UI, use the selection and source context below.\n\nUser question:\n${input.question}\n\nSelected element:\n${JSON.stringify(input.element, null, 2)}\n\nVisual context:\n${JSON.stringify({ ...input.context, screenshots: undefined }, null, 2)}\n\nConversation:\n${history}\n\nRelevant source context:\n${context || "No matching source context was found."}`;
575
699
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
576
700
  const local = localCommand(config.provider, config.model, prompt);
577
- const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress);
701
+ const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress, input.taskId, onPrompt);
578
702
  if (result.exitCode !== 0)
579
703
  throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
580
704
  return extractLocalAgentText(result.stdout, config.provider).trim();
@@ -1,4 +1,4 @@
1
- import { type SourceChange } from "./agent";
1
+ import { type SourceChange, type AgentPrompt } from "./agent";
2
2
  import type { CollabConfig } from "./project";
3
3
  export declare const DEFAULT_BRIDGE_PORT = 3056;
4
4
  type GitState = {
@@ -91,6 +91,22 @@ export type BridgeMessage = {
91
91
  } | {
92
92
  type: "stop";
93
93
  taskId?: string;
94
+ } | {
95
+ type: "agent_prompt_response";
96
+ taskId: string;
97
+ response: string;
98
+ } | {
99
+ type: "list_page_folders";
100
+ path?: string;
101
+ } | {
102
+ type: "create_page";
103
+ folder: string;
104
+ fileName: string;
105
+ content: string;
106
+ } | {
107
+ type: "create_page_folder";
108
+ parent: string;
109
+ name: string;
94
110
  } | {
95
111
  type: "git_status";
96
112
  } | {
@@ -121,6 +137,11 @@ type ModelOption = {
121
137
  label: string;
122
138
  provider: "anthropic" | "openai" | "google" | "ollama" | "cli";
123
139
  };
140
+ type PageEntry = {
141
+ name: string;
142
+ path: string;
143
+ type: "directory" | "file";
144
+ };
124
145
  export type ServerBridgeMessage = {
125
146
  type: "config";
126
147
  apiKeyConfigured: boolean;
@@ -148,6 +169,24 @@ export type ServerBridgeMessage = {
148
169
  message: string;
149
170
  detail?: string;
150
171
  changes?: SourceChange[];
172
+ } | {
173
+ type: "agent_prompt";
174
+ taskId: string;
175
+ prompt: AgentPrompt;
176
+ } | {
177
+ type: "page_folders";
178
+ path: string;
179
+ entries: PageEntry[];
180
+ project?: string;
181
+ error?: string;
182
+ } | {
183
+ type: "page_created";
184
+ path: string;
185
+ error?: string;
186
+ } | {
187
+ type: "page_folder_created";
188
+ path: string;
189
+ error?: string;
151
190
  } | {
152
191
  type: "assistant_message";
153
192
  taskId?: string;