@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 +17 -3
- package/CHANGELOG.md +16 -1
- package/README.md +39 -2
- package/dist/cli/agent.d.ts +11 -2
- package/dist/cli/agent.js +60 -10
- package/dist/cli/bridge.d.ts +23 -1
- package/dist/cli/bridge.js +50 -1
- package/dist/overlay.js +293 -56
- package/package.json +1 -1
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.
|
|
71
|
-
without an explicit accept from the user. The
|
|
72
|
-
|
|
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
|
|
149
|
-
on your PATH and adds
|
|
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
|
|
package/dist/cli/agent.d.ts
CHANGED
|
@@ -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
|
|
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: ["
|
|
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();
|
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli/bridge.js
CHANGED
|
@@ -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 || !
|
|
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,
|
|
19138
|
+
function setAgentStatus(status, message, detail, taskId2 = state.promptTaskId, appendMessage = true) {
|
|
19039
19139
|
if (!agentStatusElement || !agentStatusMessage || !sendButton) return;
|
|
19040
|
-
const logMessage =
|
|
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
|
-
|
|
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
|
-
|
|
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