@lasso-ai/cli 1.0.29 → 1.1.0

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 manual New Page workflow is a separate
72
+ explicit write path: it creates only the filename/content the user confirms. The agent is
73
+ the only party that ever leaves the machine, and only with the exact context the CLI
74
+ assembled for it.
73
75
 
74
76
  ## 4. Browser layer (capture only)
75
77
 
@@ -85,6 +87,14 @@ 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 manual New Page form with a
95
+ project-folder explorer.
96
+ - Render task-scoped permission/input prompts and send explicit responses back over
97
+ the bridge.
88
98
  - **Never mutate the DOM.** All visual changes the user sees post-edit come from the
89
99
  framework's own HMR reload, not from this layer.
90
100
 
@@ -111,6 +121,10 @@ A Node process that wraps the user's existing dev server. Entry: `src/cli/index.
111
121
  on receipt.
112
122
  6. On accept: snapshot the file's prior content (for undo), then apply the diff.
113
123
  7. On undo: restore the snapshot, full stop — no re-generation needed, so retries are free.
124
+ 8. For New Page: list project-relative folders, validate the requested target against
125
+ the project root, and create the file only after the user confirms the form.
126
+ 9. For local agents: keep child-process stdin attached to its task and forward explicit
127
+ overlay responses when the agent requests permission or input.
114
128
 
115
129
  ### 5.2 Source resolution strategies
116
130
 
package/CHANGELOG.md CHANGED
@@ -9,6 +9,21 @@ 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
+ - **Custom toolbar tooltips**: toolbar labels now use an accessible Lasso-styled
24
+ tooltip on hover and keyboard focus instead of browser-native title popovers.
25
+ - **Filtered agent progress**: useful tool/progress events remain visible while
26
+ process IDs, startup messages, and heartbeat logs are hidden.
12
27
  - Open-source documentation: `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`,
13
28
  `SECURITY.md`, `SUPPORT.md`, `LICENSE` (ISC), and this changelog.
14
29
  - **Lasso Host** (`src/cli/host/`) — a user-level local domain server + runtime
@@ -99,4 +114,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
114
  ### Notes
100
115
 
101
116
  - 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**.
117
+ behavior from that point forward. The project uses the product name **Lasso**.
package/README.md CHANGED
@@ -87,6 +87,14 @@ 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 manual page-creation form where
94
+ you can choose a project folder, enter a filename, edit starter content, and create
95
+ the file explicitly. Lasso never chooses a source folder or overwrites an existing
96
+ file without confirmation.
97
+
90
98
  ### Source-aware editing
91
99
 
92
100
  Lasso doesn't stop at the DOM.
@@ -133,6 +141,20 @@ Accept ────────→ Write to source
133
141
 
134
142
  Accepted changes are written to the filesystem and your framework's normal development workflow takes over.
135
143
 
144
+ ### Agent tasks and review
145
+
146
+ Multiple prompts can run concurrently from the **Agent Tasks** panel. Tasks are
147
+ separated into **In progress** and **Completed** tabs. Edit tasks expose a Review
148
+ action when a proposal is ready; accepting the diff applies the changes and moves
149
+ the task to Completed. Conversation and change history are retained for the same
150
+ component during the overlay session, including when the framework replaces its DOM
151
+ node during HMR.
152
+
153
+ Local CLI agents stream useful tool/progress events while they work. Infrastructure
154
+ noise such as process IDs and heartbeat messages is hidden. If a local agent needs
155
+ permission or input, Lasso presents a task-scoped prompt with Allow, Deny, or a
156
+ custom response field.
157
+
136
158
  ### Bring your own agent
137
159
 
138
160
  Lasso's editing pipeline is agent-agnostic.
@@ -145,8 +167,8 @@ Use:
145
167
 
146
168
  Your selection and project context stay inside the Lasso pipeline while the actual coding agent can be swapped independently.
147
169
 
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
170
+ When installed, Lasso automatically detects the `claude`, `codex`, and
171
+ `opencode` commands on your PATH and adds the available local agents to the
150
172
  prompt model menu. Local agents run in read-only/planning mode and return a
151
173
  reviewable proposal; Lasso remains the only process that writes accepted
152
174
  changes to your source files.
@@ -336,6 +358,13 @@ Undo → restore the previous snapshot
336
358
 
337
359
  That's it.
338
360
 
361
+ ### Create a page manually
362
+
363
+ Use **New Page** in the secondary toolbar to open the page form. Select the target
364
+ folder in the project explorer, choose a filename, edit the starter content if
365
+ needed, and select **Create page**. The bridge validates that the folder stays
366
+ inside the project and refuses to overwrite an existing file.
367
+
339
368
  ---
340
369
 
341
370
  ## Agent configuration
@@ -462,6 +491,9 @@ The CLI:
462
491
  8. Shows you the diff
463
492
  9. Writes only after you accept
464
493
 
494
+ The separate New Page workflow is also explicit: it writes only after you select
495
+ a folder, confirm the filename/content form, and choose **Create page**.
496
+
465
497
  The goal is simple:
466
498
 
467
499
  **Your development environment stays yours.**
@@ -504,6 +536,11 @@ The core visual editing pipeline is being built around:
504
536
  - Explicit accept/reject
505
537
  - Undo
506
538
  - Pluggable coding agents
539
+ - Concurrent agent tasks with review/completed states
540
+ - Component-scoped conversation and change history
541
+ - Manual page creation with project-folder selection
542
+ - Interactive permission/input prompts for local agents
543
+ - Custom toolbar tooltips and filtered agent progress
507
544
 
508
545
  Expect breaking changes before `1.0`.
509
546
 
@@ -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,7 +297,7 @@ 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, " ");
@@ -382,6 +410,9 @@ function progressEventsFromRaw(raw, provider) {
382
410
  }
383
411
  function localAgentError(command, stderr, exitCode) {
384
412
  const output = stderr.trim();
413
+ if (command === "opencode" && /waiting for permission or authentication/i.test(output)) {
414
+ return "OpenCode needs permission or authentication. Run OpenCode once in a terminal, approve the requested access or sign in, then retry in Lasso.";
415
+ }
385
416
  if (command === "codex" && output.includes("missing field `base_instructions`")) {
386
417
  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
418
  }
@@ -406,9 +437,9 @@ function localCommand(provider, model, prompt) {
406
437
  }
407
438
  return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
408
439
  }
409
- function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
440
+ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
410
441
  return new Promise((resolve, reject) => {
411
- const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
442
+ const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
412
443
  const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : "Claude Code";
413
444
  let stdout = "";
414
445
  let stderr = "";
@@ -416,6 +447,18 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
416
447
  let pendingStderr = "";
417
448
  let settled = false;
418
449
  let timedOut = false;
450
+ let promptShown = false;
451
+ const sendPrompt = (message, kind = "permission") => {
452
+ if (!taskId || promptShown)
453
+ return;
454
+ promptShown = true;
455
+ activeAgentInputs.set(taskId, (value) => {
456
+ promptShown = false;
457
+ activeAgentInputs.delete(taskId);
458
+ child.stdin.write(`${value}\n`);
459
+ });
460
+ onPrompt?.({ message, kind, options: kind === "permission" ? ["Allow", "Deny"] : undefined });
461
+ };
419
462
  const timeout = setTimeout(() => {
420
463
  timedOut = true;
421
464
  child.kill("SIGTERM");
@@ -443,6 +486,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
443
486
  for (const progress of progressEventsFromRaw(line, provider)) {
444
487
  onProgress?.(progress.message, progress.detail);
445
488
  }
489
+ if (/permission\s+(required|denied)|approve\s+(this|the|tool)|authentication\s+required|login\s+required|press\s+.+to\s+continue/i.test(line)) {
490
+ sendPrompt(line.replace(/\s+/g, " ").trim());
491
+ }
446
492
  if (stream === "stderr" && line.trim()) {
447
493
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
448
494
  onProgress?.("OpenCode · provider rejected the request; stopping this task…");
@@ -463,6 +509,8 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
463
509
  return;
464
510
  settled = true;
465
511
  clearTimeout(timeout);
512
+ if (taskId)
513
+ activeAgentInputs.delete(taskId);
466
514
  signal?.removeEventListener("abort", abort);
467
515
  reject(error);
468
516
  });
@@ -471,6 +519,8 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
471
519
  return;
472
520
  settled = true;
473
521
  clearTimeout(timeout);
522
+ if (taskId)
523
+ activeAgentInputs.delete(taskId);
474
524
  signal?.removeEventListener("abort", abort);
475
525
  if (timedOut) {
476
526
  reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
@@ -496,18 +546,18 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
496
546
  });
497
547
  });
498
548
  }
499
- async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
549
+ async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, taskId, onPrompt) {
500
550
  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
551
  const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
502
552
  const local = localCommand(config.provider, config.model, prompt);
503
553
  const command = local.command;
504
554
  const args = local.args;
505
- const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress);
555
+ const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress, taskId, onPrompt);
506
556
  if (result.exitCode !== 0)
507
557
  throw new Error(localAgentError(command, result.stderr, result.exitCode));
508
558
  return extractLocalAgentProposal(result.stdout, config.provider);
509
559
  }
510
- async function proposeChanges(cwd, input, config, signal, onProgress) {
560
+ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
511
561
  const context = await contextFor(cwd, input.element);
512
562
  const visualContext = input.context ? { ...input.context, screenshots: undefined } : undefined;
513
563
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
@@ -517,7 +567,7 @@ async function proposeChanges(cwd, input, config, signal, onProgress) {
517
567
  : "";
518
568
  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
569
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
520
- return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress);
570
+ return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
521
571
  }
522
572
  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
573
  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 +618,13 @@ async function proposeChanges(cwd, input, config, signal, onProgress) {
568
618
  throw new Error("The agent returned an empty response.");
569
619
  return jsonFrom(text);
570
620
  }
571
- async function answerQuestion(cwd, input, config, signal, onProgress) {
621
+ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
572
622
  const context = await contextFor(cwd, input.element);
573
623
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
574
624
  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
625
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
576
626
  const local = localCommand(config.provider, config.model, prompt);
577
- const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress);
627
+ const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress, input.taskId, onPrompt);
578
628
  if (result.exitCode !== 0)
579
629
  throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
580
630
  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,17 @@ 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
+ } | {
101
+ type: "create_page";
102
+ folder: string;
103
+ fileName: string;
104
+ content: string;
94
105
  } | {
95
106
  type: "git_status";
96
107
  } | {
@@ -148,6 +159,17 @@ export type ServerBridgeMessage = {
148
159
  message: string;
149
160
  detail?: string;
150
161
  changes?: SourceChange[];
162
+ } | {
163
+ type: "agent_prompt";
164
+ taskId: string;
165
+ prompt: AgentPrompt;
166
+ } | {
167
+ type: "page_folders";
168
+ folders: string[];
169
+ } | {
170
+ type: "page_created";
171
+ path: string;
172
+ error?: string;
151
173
  } | {
152
174
  type: "assistant_message";
153
175
  taskId?: string;
@@ -181,6 +181,23 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
181
181
  let activeAgentController = null;
182
182
  const taskControllers = new Map();
183
183
  let localAgents = new Set();
184
+ const pageFolders = (directory, relative = "", depth = 0) => {
185
+ if (depth > 5)
186
+ return [];
187
+ const folders = [relative || "."];
188
+ try {
189
+ for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true })) {
190
+ if (!entry.isDirectory() || ["node_modules", ".git", ".next", "dist", "build", ".turbo"].includes(entry.name) || entry.name.startsWith("."))
191
+ continue;
192
+ const nextRelative = relative ? node_path_1.default.join(relative, entry.name) : entry.name;
193
+ folders.push(...pageFolders(node_path_1.default.join(directory, entry.name), nextRelative, depth + 1));
194
+ }
195
+ }
196
+ catch {
197
+ // Ignore folders that cannot be read.
198
+ }
199
+ return folders;
200
+ };
184
201
  bridgeServer.on("request", (req, res) => {
185
202
  if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
186
203
  return;
@@ -284,6 +301,10 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
284
301
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
285
302
  socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message, detail }));
286
303
  }
304
+ }, (prompt) => {
305
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
306
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
307
+ }
287
308
  })
288
309
  .then((proposal) => {
289
310
  if (controller.signal.aborted || socket.readyState !== socket.OPEN)
@@ -319,6 +340,28 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
319
340
  else {
320
341
  console.log("[lasso] received from overlay:", msg);
321
342
  }
343
+ if (msg.type === "list_page_folders") {
344
+ socket.send(JSON.stringify({ type: "page_folders", folders: pageFolders(cwd).sort() }));
345
+ return;
346
+ }
347
+ if (msg.type === "create_page") {
348
+ const root = node_path_1.default.resolve(cwd);
349
+ const folder = msg.folder === "." ? root : node_path_1.default.resolve(root, msg.folder);
350
+ const fileName = node_path_1.default.basename(msg.fileName);
351
+ const target = node_path_1.default.resolve(folder, fileName);
352
+ if (!target.startsWith(`${root}${node_path_1.default.sep}`) || target === root || !fileName || fileName !== msg.fileName || !node_fs_1.default.existsSync(folder)) {
353
+ socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "Choose a valid project folder and file name." }));
354
+ return;
355
+ }
356
+ try {
357
+ node_fs_1.default.writeFileSync(target, msg.content.slice(0, 200000), { encoding: "utf8", flag: "wx" });
358
+ socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root, target) }));
359
+ }
360
+ catch (error) {
361
+ socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root, target), error: error instanceof Error && error.message.includes("EEXIST") ? "A file with that name already exists." : "The page could not be created." }));
362
+ }
363
+ return;
364
+ }
322
365
  if (msg.type === "ask") {
323
366
  const localProvider = msg.provider === "cli";
324
367
  const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
@@ -337,9 +380,12 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
337
380
  ? { provider: cliProvider, model: msg.model }
338
381
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
339
382
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "Working…" }));
340
- void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
383
+ void (0, agent_1.answerQuestion)(cwd, { taskId: msg.taskId, question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
341
384
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
342
385
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message, detail }));
386
+ }, (prompt) => {
387
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN)
388
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId: msg.taskId, prompt }));
343
389
  }).then((answer) => {
344
390
  if (!controller.signal.aborted)
345
391
  socket.send(JSON.stringify({ type: "assistant_message", taskId: msg.taskId, message: answer }));
@@ -370,6 +416,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
370
416
  reviewRefreshAttempts.set(msg.taskId, 0);
371
417
  runEditReview(msg, selectedConfig);
372
418
  }
419
+ else if (msg.type === "agent_prompt_response") {
420
+ (0, agent_1.respondToAgentPrompt)(msg.taskId, msg.response);
421
+ }
373
422
  else if (msg.type === "stop") {
374
423
  if (msg.taskId)
375
424
  taskControllers.get(msg.taskId)?.abort();
package/dist/overlay.js CHANGED
@@ -8092,6 +8092,57 @@
8092
8092
  display: none !important;
8093
8093
  }
8094
8094
 
8095
+ .lasso-toolbar-mini {
8096
+ position: fixed;
8097
+ left: calc(50% + 280px);
8098
+ bottom: 24px;
8099
+ display: inline-flex;
8100
+ align-items: center;
8101
+ min-height: 48px;
8102
+ padding: 6px;
8103
+ border: 1px solid var(--lo-border);
8104
+ border-radius: var(--lo-radius-full);
8105
+ background: rgba(22, 23, 27, 0.94);
8106
+ box-shadow: var(--lo-shadow-lg);
8107
+ backdrop-filter: blur(20px);
8108
+ -webkit-backdrop-filter: blur(20px);
8109
+ pointer-events: auto;
8110
+ z-index: 10;
8111
+ }
8112
+ .lasso-toolbar-mini.dismissed { display: none; }
8113
+ .lasso-new-page-btn {
8114
+ display: inline-flex;
8115
+ align-items: center;
8116
+ gap: 6px;
8117
+ height: 34px;
8118
+ padding: 0 11px;
8119
+ border: 0;
8120
+ border-radius: var(--lo-radius-full);
8121
+ background: var(--lo-primary);
8122
+ color: #111214;
8123
+ font: inherit;
8124
+ font-size: 12px;
8125
+ font-weight: 650;
8126
+ cursor: pointer;
8127
+ transition: transform 150ms ease, filter 150ms ease;
8128
+ }
8129
+ .lasso-new-page-btn span:first-child { font-size: 19px; font-weight: 400; line-height: 1; }
8130
+ .lasso-new-page-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
8131
+ .lasso-new-page-btn:active { transform: scale(.97); }
8132
+ .lasso-new-page-panel { position: fixed; left: calc(50% + 280px); bottom: 82px; width: 310px; max-width: calc(100vw - 24px); padding: 14px; border: 1px solid var(--lo-border); border-radius: var(--lo-radius-lg); background: var(--lo-surface); color: var(--lo-text); box-shadow: var(--lo-shadow-lg); pointer-events: auto; z-index: 20; }
8133
+ .lasso-new-page-panel[hidden] { display: none; }
8134
+ .lasso-new-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; font-size: 13px; }
8135
+ .lasso-new-page-close { border: 0; background: transparent; color: var(--lo-text-3); font-size: 20px; line-height: 1; cursor: pointer; }
8136
+ .lasso-new-page-panel label { display: block; margin: 9px 0; color: var(--lo-text-2); font-size: 11px; }
8137
+ .lasso-new-page-name, .lasso-new-page-content { width: 100%; box-sizing: border-box; margin-top: 5px; padding: 8px; border: 1px solid var(--lo-border); border-radius: var(--lo-radius-md); background: var(--lo-surface-2); color: var(--lo-text); font: inherit; font-size: 12px; }
8138
+ .lasso-new-page-content { resize: vertical; line-height: 1.4; }
8139
+ .lasso-page-folder-list { display: flex; flex-direction: column; gap: 3px; max-height: 130px; margin-top: 5px; padding: 4px; overflow: auto; border: 1px solid var(--lo-border); border-radius: var(--lo-radius-md); background: var(--lo-surface-2); }
8140
+ .lasso-page-folder-list button { padding: 6px 8px; border: 0; border-radius: 5px; background: transparent; color: var(--lo-text-2); text-align: left; font: inherit; font-size: 11px; cursor: pointer; }
8141
+ .lasso-page-folder-list button:hover, .lasso-page-folder-list button.selected { background: var(--lo-surface-hover); color: var(--lo-text); }
8142
+ .lasso-new-page-status { min-height: 16px; margin: 7px 0; color: var(--lo-text-3); font-size: 11px; }
8143
+ .lasso-new-page-status.error { color: #f87171; }
8144
+ .lasso-new-page-create { width: 100%; padding: 8px; border: 0; border-radius: var(--lo-radius-full); background: var(--lo-primary); color: #111214; font: inherit; font-size: 12px; font-weight: 650; cursor: pointer; }
8145
+
8095
8146
  .lasso-tb-sep {
8096
8147
  width: 1px;
8097
8148
  height: 20px;
@@ -8138,6 +8189,24 @@
8138
8189
  transform: scale(0.96);
8139
8190
  }
8140
8191
 
8192
+ .lasso-toolbar-tooltip {
8193
+ position: fixed;
8194
+ z-index: 100;
8195
+ transform: translateX(-50%);
8196
+ max-width: 220px;
8197
+ padding: 6px 9px;
8198
+ border: 1px solid var(--lo-border);
8199
+ border-radius: 7px;
8200
+ background: var(--lo-surface-2);
8201
+ color: var(--lo-text);
8202
+ box-shadow: 0 8px 24px rgba(0,0,0,.28);
8203
+ font-size: 11px;
8204
+ line-height: 1.2;
8205
+ white-space: nowrap;
8206
+ pointer-events: none;
8207
+ }
8208
+ .lasso-toolbar-tooltip[hidden] { display: none; }
8209
+
8141
8210
  /* Animated label transition */
8142
8211
  .lasso-tool-label {
8143
8212
  max-width: 0;
@@ -8306,6 +8375,11 @@
8306
8375
  color: #ffffff;
8307
8376
  }
8308
8377
 
8378
+ @media (max-width: 760px) {
8379
+ .lasso-toolbar-mini { left: 50%; bottom: 82px; transform: translateX(-50%); }
8380
+ .lasso-new-page-panel { left: 50%; transform: translateX(-50%); }
8381
+ }
8382
+
8309
8383
  /* Presence Avatars in toolbar with color outlines */
8310
8384
  .lasso-presence {
8311
8385
  display: flex;
@@ -10943,6 +11017,14 @@
10943
11017
  .lasso-task-apply { padding: 7px 11px; border: 0; border-radius: var(--lo-radius-full); background: var(--lo-primary); color: #111214; font: inherit; font-size: 11px; font-weight: 600; cursor: pointer; }
10944
11018
 
10945
11019
  .lasso-agent-tasks-badge { position: absolute; top: 5px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--lo-primary); }
11020
+ .lasso-agent-prompt { position: fixed; left: 50%; top: 50%; width: min(360px, calc(100vw - 32px)); transform: translate(-50%, -50%); padding: 16px; border: 1px solid var(--lo-border); border-radius: var(--lo-radius-lg); background: var(--lo-surface); color: var(--lo-text); box-shadow: 0 18px 60px rgba(0,0,0,.42); z-index: 80; }
11021
+ .lasso-agent-prompt[hidden] { display: none; }
11022
+ .lasso-agent-prompt-title { margin-bottom: 7px; font-size: 13px; font-weight: 700; }
11023
+ .lasso-agent-prompt-message { margin-bottom: 12px; color: var(--lo-text-2); font-size: 12px; line-height: 1.45; white-space: pre-wrap; }
11024
+ .lasso-agent-prompt-input { width: 100%; box-sizing: border-box; margin-bottom: 12px; padding: 8px 10px; border: 1px solid var(--lo-border); border-radius: var(--lo-radius-md); background: var(--lo-surface-2); color: var(--lo-text); font: inherit; font-size: 12px; }
11025
+ .lasso-agent-prompt-actions { display: flex; justify-content: flex-end; gap: 7px; }
11026
+ .lasso-agent-prompt-actions button { padding: 7px 11px; border: 0; border-radius: var(--lo-radius-full); background: var(--lo-surface-2); color: var(--lo-text); font: inherit; font-size: 11px; cursor: pointer; }
11027
+ .lasso-agent-prompt-actions button[data-agent-response="Allow"], .lasso-agent-prompt-actions button[data-agent-response="input"] { background: var(--lo-primary); color: #111214; font-weight: 600; }
10946
11028
 
10947
11029
  .lasso-todo-header {
10948
11030
  display: flex;
@@ -12129,7 +12211,6 @@
12129
12211
  // src/overlay/tasks/tasks.ts
12130
12212
  var panel = null;
12131
12213
  var list = null;
12132
- var detail = null;
12133
12214
  var badge = null;
12134
12215
  var selectionListeners = /* @__PURE__ */ new Set();
12135
12216
  var reviewListeners = /* @__PURE__ */ new Set();
@@ -12160,6 +12241,17 @@
12160
12241
  (task) => task.status !== "complete" && task.status !== "error" && task.status !== "stopped" && normalizedInstruction(task.instruction) === normalized && task.element === (element || void 0)
12161
12242
  );
12162
12243
  }
12244
+ function getComponentConversation(selectionId) {
12245
+ const task = state.agentTasks.find((candidate) => candidate.selectionId === selectionId);
12246
+ if (!task) return { messages: [], changesHistory: [] };
12247
+ return {
12248
+ messages: task.messages.map((message) => ({ ...message })),
12249
+ changesHistory: task.changesHistory.map((entry) => ({
12250
+ ...entry,
12251
+ changes: entry.changes.map((change) => ({ ...change }))
12252
+ }))
12253
+ };
12254
+ }
12163
12255
  function onAgentTaskSelected(listener) {
12164
12256
  selectionListeners.add(listener);
12165
12257
  return () => selectionListeners.delete(listener);
@@ -12274,12 +12366,10 @@
12274
12366
  </div>
12275
12367
  <div class="lasso-task-body">
12276
12368
  <div class="lasso-task-list"></div>
12277
- <div class="lasso-task-detail" hidden></div>
12278
12369
  </div>
12279
12370
  `;
12280
12371
  dom.shadow.appendChild(panel);
12281
12372
  list = panel.querySelector(".lasso-task-list");
12282
- detail = panel.querySelector(".lasso-task-detail");
12283
12373
  badge = panel.querySelector(".lasso-task-badge");
12284
12374
  panel.querySelector(".lasso-task-close").addEventListener("click", () => toggleTaskPanel(false));
12285
12375
  panel.querySelectorAll(".lasso-task-tab").forEach((tab) => {
@@ -12297,7 +12387,7 @@
12297
12387
  }
12298
12388
  function renderTasks() {
12299
12389
  const panelEl = panel;
12300
- if (!panelEl || !list || !detail || !badge) return;
12390
+ if (!panelEl || !list || !badge) return;
12301
12391
  const activeCount = state.agentTasks.filter((task) => task.status !== "complete" && task.status !== "error" && task.status !== "stopped").length;
12302
12392
  badge.hidden = state.agentTasks.length === 0;
12303
12393
  badge.textContent = String(activeCount || state.agentTasks.length);
@@ -12326,49 +12416,6 @@
12326
12416
  list.appendChild(button);
12327
12417
  }
12328
12418
  }
12329
- const selected = getAgentTask(state.activeTaskId);
12330
- if (!selected) {
12331
- detail.hidden = true;
12332
- return;
12333
- }
12334
- detail.hidden = false;
12335
- detail.replaceChildren();
12336
- const heading = document.createElement("div");
12337
- heading.className = "lasso-task-detail-heading";
12338
- heading.innerHTML = `<span class="lasso-task-detail-status ${selected.status}">${statusLabel[selected.status]}</span><button type="button" class="lasso-task-back">All tasks</button>`;
12339
- heading.querySelector("button").addEventListener("click", () => selectAgentTask(null));
12340
- const prompt = document.createElement("p");
12341
- prompt.className = "lasso-task-prompt";
12342
- prompt.textContent = selected.instruction;
12343
- const activity = document.createElement("p");
12344
- activity.className = "lasso-task-message";
12345
- activity.textContent = selected.response || selected.message;
12346
- detail.append(heading, prompt, activity);
12347
- if (selected.activity.length) {
12348
- const activityList = document.createElement("div");
12349
- activityList.className = "lasso-task-activity";
12350
- for (const entry of selected.activity.slice(-8)) {
12351
- const line = document.createElement("div");
12352
- line.textContent = entry;
12353
- activityList.appendChild(line);
12354
- }
12355
- detail.appendChild(activityList);
12356
- }
12357
- const pendingChanges = selected.pendingChanges.length ? selected.pendingChanges : selected.changes || [];
12358
- if (pendingChanges.length) {
12359
- const changes = document.createElement("div");
12360
- changes.className = "lasso-task-changes";
12361
- changes.textContent = `${pendingChanges.length} proposed file change${pendingChanges.length === 1 ? "" : "s"}`;
12362
- detail.appendChild(changes);
12363
- if (selected.status === "review") {
12364
- const review = document.createElement("button");
12365
- review.type = "button";
12366
- review.className = "lasso-task-apply";
12367
- review.textContent = "Review changes";
12368
- review.addEventListener("click", () => requestAgentTaskReview(selected.id));
12369
- detail.appendChild(review);
12370
- }
12371
- }
12372
12419
  }
12373
12420
 
12374
12421
  // src/overlay/notifications.ts
@@ -18494,6 +18541,8 @@
18494
18541
  var agentLogLines = [];
18495
18542
  var agentLogExpanded = false;
18496
18543
  var reviewPanel = null;
18544
+ var agentPromptPanel = null;
18545
+ var agentPromptTaskId = null;
18497
18546
  var modelBtn = null;
18498
18547
  var modelName = null;
18499
18548
  var modelMenu = null;
@@ -18519,6 +18568,17 @@
18519
18568
  const icon = providerIcons[provider];
18520
18569
  return `<span class="${active ? "lasso-model-active-icon" : "lasso-model-item-icon"} provider-${provider}" aria-hidden="true"><svg viewBox="0 0 ${icon.width} ${icon.height}" xmlns="http://www.w3.org/2000/svg" focusable="false">${icon.body}</svg></span>`;
18521
18570
  }
18571
+ function componentConversationId(element) {
18572
+ const source = element.getAttribute("data-source") || element.getAttribute("data-lasso-source") || getSourceHint(element);
18573
+ return `component:${source || elementKey(element)}`;
18574
+ }
18575
+ function loadComponentConversation(element) {
18576
+ const selectionId = componentConversationId(element);
18577
+ const conversation = getComponentConversation(selectionId);
18578
+ state.selectionId = selectionId;
18579
+ state.chatHistory = conversation.messages;
18580
+ state.changesHistory = conversation.changesHistory;
18581
+ }
18522
18582
  function buildPrompt() {
18523
18583
  const dom = getDOM();
18524
18584
  const el = document.createElement("div");
@@ -18610,6 +18670,31 @@
18610
18670
  `;
18611
18671
  dom.shadow.appendChild(el);
18612
18672
  promptEl = el;
18673
+ const agentPrompt = document.createElement("div");
18674
+ agentPrompt.className = "lasso-agent-prompt";
18675
+ agentPrompt.hidden = true;
18676
+ agentPrompt.innerHTML = `
18677
+ <div class="lasso-agent-prompt-title">Agent needs your input</div>
18678
+ <div class="lasso-agent-prompt-message"></div>
18679
+ <input class="lasso-agent-prompt-input" type="text" placeholder="Type a response\u2026" />
18680
+ <div class="lasso-agent-prompt-actions">
18681
+ <button type="button" data-agent-response="Allow">Allow</button>
18682
+ <button type="button" data-agent-response="Deny">Deny</button>
18683
+ <button type="button" data-agent-response="input">Send</button>
18684
+ </div>
18685
+ `;
18686
+ dom.shadow.appendChild(agentPrompt);
18687
+ agentPromptPanel = agentPrompt;
18688
+ agentPrompt.querySelectorAll("[data-agent-response]").forEach((button) => {
18689
+ button.addEventListener("click", () => {
18690
+ if (!agentPromptTaskId || state.bridgeSocket?.readyState !== WebSocket.OPEN) return;
18691
+ const response = button.dataset.agentResponse === "input" ? agentPrompt.querySelector(".lasso-agent-prompt-input")?.value.trim() || "" : button.dataset.agentResponse || "";
18692
+ if (!response) return;
18693
+ state.bridgeSocket.send(JSON.stringify({ type: "agent_prompt_response", taskId: agentPromptTaskId, response }));
18694
+ agentPromptTaskId = null;
18695
+ agentPrompt.hidden = true;
18696
+ });
18697
+ });
18613
18698
  promptInput = el.querySelector(".lasso-prompt-input");
18614
18699
  promptElement = el.querySelector(".lasso-prompt-element-name");
18615
18700
  sendButton = el.querySelector(".lasso-prompt-send");
@@ -18809,6 +18894,21 @@
18809
18894
  });
18810
18895
  return { prompt: el, review: rev };
18811
18896
  }
18897
+ function showAgentPrompt(taskId2, prompt) {
18898
+ if (!agentPromptPanel) return;
18899
+ agentPromptTaskId = taskId2;
18900
+ const message = agentPromptPanel.querySelector(".lasso-agent-prompt-message");
18901
+ const input = agentPromptPanel.querySelector(".lasso-agent-prompt-input");
18902
+ if (message) message.textContent = prompt.message;
18903
+ if (input) {
18904
+ input.value = "";
18905
+ input.hidden = prompt.kind === "permission";
18906
+ }
18907
+ agentPromptPanel.querySelector('[data-agent-response="Allow"]').hidden = prompt.kind !== "permission";
18908
+ agentPromptPanel.querySelector('[data-agent-response="Deny"]').hidden = prompt.kind !== "permission";
18909
+ agentPromptPanel.hidden = false;
18910
+ if (prompt.kind === "input") input?.focus();
18911
+ }
18812
18912
  function refreshModelMenu() {
18813
18913
  if (!modelMenu) return;
18814
18914
  const filters = document.createElement("div");
@@ -19035,9 +19135,9 @@
19035
19135
  container.append(document.createTextNode(text.slice(lastIndex)));
19036
19136
  }
19037
19137
  }
19038
- function setAgentStatus(status, message, detail2, taskId2 = state.promptTaskId, appendMessage = true) {
19138
+ function setAgentStatus(status, message, detail, taskId2 = state.promptTaskId, appendMessage = true) {
19039
19139
  if (!agentStatusElement || !agentStatusMessage || !sendButton) return;
19040
- const logMessage = detail2?.trim() || message.trim();
19140
+ const logMessage = detail?.trim() || message.trim();
19041
19141
  const lastLog = agentLogLines[agentLogLines.length - 1];
19042
19142
  if (agentStatusElement.dataset.status === status && agentStatusMessage.textContent === message && (!logMessage || lastLog === logMessage)) return;
19043
19143
  appendAgentLog(logMessage);
@@ -19092,13 +19192,17 @@
19092
19192
  function hideReview() {
19093
19193
  if (reviewPanel) reviewPanel.hidden = true;
19094
19194
  }
19095
- function resetPromptSession() {
19195
+ function resetPromptSession(element = state.selected) {
19096
19196
  state.promptTaskId = null;
19097
- state.chatHistory = [];
19098
- state.changesHistory = [];
19099
19197
  state.pendingChanges = [];
19100
19198
  state.lastInstruction = "";
19101
- state.selectionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
19199
+ if (element) {
19200
+ loadComponentConversation(element);
19201
+ } else {
19202
+ state.chatHistory = [];
19203
+ state.changesHistory = [];
19204
+ state.selectionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
19205
+ }
19102
19206
  state.screenshotPromise = Promise.resolve({});
19103
19207
  hideReview();
19104
19208
  clearAgentLog();
@@ -19271,7 +19375,7 @@
19271
19375
  const changesHistory = state.changesHistory.map((entry) => ({ ...entry, changes: entry.changes.map((change) => ({ ...change })) }));
19272
19376
  const task = createAgentTask(instruction, messages, changesHistory, selectedElement, selectionId);
19273
19377
  updateAgentTask(task.id, { status: "thinking", message: "Thinking\u2026", activity: [...task.activity, "Thinking\u2026"] });
19274
- resetPromptSession();
19378
+ resetPromptSession(selectedElement);
19275
19379
  state.promptTaskId = task.id;
19276
19380
  state.lastInstruction = instruction;
19277
19381
  const rect = selectedElement.getBoundingClientRect();
@@ -19341,7 +19445,7 @@
19341
19445
  }
19342
19446
  function openPromptForSelected(selected) {
19343
19447
  if (!promptEl || !promptElement) return;
19344
- resetPromptSession();
19448
+ resetPromptSession(selected);
19345
19449
  promptElement.textContent = getElementLabel(selected);
19346
19450
  positionPrompt(selected);
19347
19451
  promptEl.classList.add("visible");
@@ -20658,7 +20762,65 @@
20658
20762
  </svg>
20659
20763
  </button>
20660
20764
  `;
20661
- dom.shadow.append(toolbar, toolbarReopen, voiceBar);
20765
+ const miniToolbar = document.createElement("div");
20766
+ miniToolbar.className = "lasso-toolbar-mini";
20767
+ miniToolbar.innerHTML = `
20768
+ <button class="lasso-new-page-btn" type="button" aria-label="Create a new page" title="New page">
20769
+ <span aria-hidden="true">+</span>
20770
+ <span>New Page</span>
20771
+ </button>
20772
+ `;
20773
+ const pagePanel = document.createElement("div");
20774
+ pagePanel.className = "lasso-new-page-panel";
20775
+ pagePanel.hidden = true;
20776
+ pagePanel.innerHTML = `
20777
+ <div class="lasso-new-page-header"><strong>New page</strong><button type="button" class="lasso-new-page-close" aria-label="Close">\xD7</button></div>
20778
+ <label>File name<input class="lasso-new-page-name" value="page.tsx" /></label>
20779
+ <label>Folder<div class="lasso-page-folder-list"><span>Loading folders\u2026</span></div></label>
20780
+ <label>Starter content<textarea class="lasso-new-page-content" rows="5">export default function Page() {
20781
+ return <main />;
20782
+ }</textarea></label>
20783
+ <div class="lasso-new-page-status" aria-live="polite"></div>
20784
+ <button type="button" class="lasso-new-page-create">Create page</button>
20785
+ `;
20786
+ dom.shadow.append(toolbar, miniToolbar, pagePanel, toolbarReopen, voiceBar);
20787
+ const tooltip = document.createElement("div");
20788
+ tooltip.className = "lasso-toolbar-tooltip";
20789
+ tooltip.hidden = true;
20790
+ tooltip.setAttribute("role", "tooltip");
20791
+ dom.shadow.appendChild(tooltip);
20792
+ let tooltipTarget = null;
20793
+ const showTooltip = (target) => {
20794
+ const label = target.dataset.tooltip || target.getAttribute("title") || target.getAttribute("aria-label");
20795
+ if (!label) return;
20796
+ target.removeAttribute("title");
20797
+ tooltipTarget = target;
20798
+ tooltip.textContent = label;
20799
+ tooltip.hidden = false;
20800
+ const rect = target.getBoundingClientRect();
20801
+ tooltip.style.left = `${rect.left + rect.width / 2}px`;
20802
+ tooltip.style.top = `${rect.bottom + 8}px`;
20803
+ };
20804
+ const hideTooltip = (target) => {
20805
+ if (!target || tooltipTarget === target) {
20806
+ tooltip.hidden = true;
20807
+ tooltipTarget = null;
20808
+ }
20809
+ };
20810
+ toolbar.querySelectorAll("[title]").forEach((target) => {
20811
+ target.dataset.tooltip = target.getAttribute("title") || "";
20812
+ target.addEventListener("pointerenter", () => showTooltip(target));
20813
+ target.addEventListener("pointerleave", () => hideTooltip(target));
20814
+ target.addEventListener("focusin", () => showTooltip(target));
20815
+ target.addEventListener("focusout", () => hideTooltip(target));
20816
+ });
20817
+ miniToolbar.querySelectorAll("[title]").forEach((target) => {
20818
+ target.dataset.tooltip = target.getAttribute("title") || "";
20819
+ target.addEventListener("pointerenter", () => showTooltip(target));
20820
+ target.addEventListener("pointerleave", () => hideTooltip(target));
20821
+ target.addEventListener("focusin", () => showTooltip(target));
20822
+ target.addEventListener("focusout", () => hideTooltip(target));
20823
+ });
20662
20824
  const selectBtn = toolbar.querySelector(".select-tool");
20663
20825
  const dragBtn = toolbar.querySelector(".drag-tool");
20664
20826
  const previewBtn = toolbar.querySelector(".preview-tool");
@@ -20670,6 +20832,67 @@
20670
20832
  const clipboardBtn = toolbar.querySelector(".clipboard-tool");
20671
20833
  const voiceBtn = toolbar.querySelector(".voice-tool");
20672
20834
  const dismissBtn = toolbar.querySelector(".lasso-toolbar-dismiss");
20835
+ const newPageBtn = miniToolbar.querySelector(".lasso-new-page-btn");
20836
+ const folderList = pagePanel.querySelector(".lasso-page-folder-list");
20837
+ const pageStatus = pagePanel.querySelector(".lasso-new-page-status");
20838
+ let selectedFolder = ".";
20839
+ const requestFolders = () => {
20840
+ if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
20841
+ folderList.innerHTML = "<span>Loading folders\u2026</span>";
20842
+ state.bridgeSocket.send(JSON.stringify({ type: "list_page_folders" }));
20843
+ } else {
20844
+ folderList.innerHTML = "<span>Bridge is not connected.</span>";
20845
+ }
20846
+ };
20847
+ window.addEventListener("lasso-page-folders", (event) => {
20848
+ const folders = event.detail || [];
20849
+ folderList.replaceChildren();
20850
+ for (const folder of folders) {
20851
+ const button = document.createElement("button");
20852
+ button.type = "button";
20853
+ button.textContent = folder === "." ? "Project root" : folder;
20854
+ button.className = folder === selectedFolder ? "selected" : "";
20855
+ button.addEventListener("click", () => {
20856
+ selectedFolder = folder;
20857
+ folderList.querySelectorAll("button").forEach((item) => item.classList.toggle("selected", item === button));
20858
+ });
20859
+ folderList.appendChild(button);
20860
+ }
20861
+ folderList.querySelector("button")?.click();
20862
+ });
20863
+ window.addEventListener("lasso-page-created", (event) => {
20864
+ const result = event.detail;
20865
+ pageStatus.textContent = result.error || `Created ${result.path}`;
20866
+ pageStatus.classList.toggle("error", Boolean(result.error));
20867
+ if (!result.error) pagePanel.hidden = true;
20868
+ });
20869
+ newPageBtn.addEventListener("click", (event) => {
20870
+ event.preventDefault();
20871
+ event.stopPropagation();
20872
+ pagePanel.hidden = false;
20873
+ pageStatus.textContent = "";
20874
+ pageStatus.classList.remove("error");
20875
+ requestFolders();
20876
+ });
20877
+ pagePanel.querySelector(".lasso-new-page-close").addEventListener("click", () => {
20878
+ pagePanel.hidden = true;
20879
+ });
20880
+ pagePanel.querySelector(".lasso-new-page-create").addEventListener("click", () => {
20881
+ const fileName = pagePanel.querySelector(".lasso-new-page-name").value.trim();
20882
+ const content = pagePanel.querySelector(".lasso-new-page-content").value;
20883
+ if (!fileName) {
20884
+ pageStatus.textContent = "Enter a file name.";
20885
+ pageStatus.classList.add("error");
20886
+ return;
20887
+ }
20888
+ if (state.bridgeSocket?.readyState !== WebSocket.OPEN) {
20889
+ pageStatus.textContent = "Bridge is not connected.";
20890
+ pageStatus.classList.add("error");
20891
+ return;
20892
+ }
20893
+ pageStatus.textContent = "Creating\u2026";
20894
+ state.bridgeSocket.send(JSON.stringify({ type: "create_page", folder: selectedFolder, fileName, content }));
20895
+ });
20673
20896
  selectBtn.addEventListener("click", (e) => {
20674
20897
  e.preventDefault();
20675
20898
  e.stopPropagation();
@@ -20728,10 +20951,12 @@
20728
20951
  });
20729
20952
  dismissBtn.addEventListener("click", () => {
20730
20953
  toolbar.classList.add("dismissed");
20954
+ miniToolbar.classList.add("dismissed");
20731
20955
  toolbarReopen.classList.add("visible");
20732
20956
  });
20733
20957
  toolbarReopen.addEventListener("click", () => {
20734
20958
  toolbar.classList.remove("dismissed");
20959
+ miniToolbar.classList.remove("dismissed");
20735
20960
  toolbarReopen.classList.remove("visible");
20736
20961
  });
20737
20962
  voiceBar.querySelector(".mute-toggle").addEventListener("click", () => {
@@ -20804,6 +21029,18 @@
20804
21029
  if (message.type === "git_progress" && message.message) {
20805
21030
  setGitMessage(message.message);
20806
21031
  }
21032
+ if (message.type === "agent_prompt" && message.taskId && message.prompt) {
21033
+ showAgentPrompt(message.taskId, message.prompt);
21034
+ return;
21035
+ }
21036
+ if (message.type === "page_folders") {
21037
+ window.dispatchEvent(new CustomEvent("lasso-page-folders", { detail: message.folders || [] }));
21038
+ return;
21039
+ }
21040
+ if (message.type === "page_created") {
21041
+ window.dispatchEvent(new CustomEvent("lasso-page-created", { detail: { path: message.path || "", error: message.error } }));
21042
+ return;
21043
+ }
20807
21044
  if (message.type === "agent_status" && message.status && message.message) {
20808
21045
  const taskId2 = message.taskId;
20809
21046
  const task = taskId2 ? getAgentTask(taskId2) : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.0.29",
3
+ "version": "1.1.0",
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",