@lasso-ai/cli 1.0.28 → 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 +123 -30
- package/dist/cli/bridge.d.ts +23 -1
- package/dist/cli/bridge.js +52 -3
- package/dist/overlay.js +296 -68
- 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,9 +211,47 @@ function extractLocalAgentText(raw, provider) {
|
|
|
202
211
|
}
|
|
203
212
|
return texts.at(-1) || raw;
|
|
204
213
|
}
|
|
214
|
+
function collectTextFields(value, fields = new Set(["text", "output_text", "result", "content", "message", "output", "value"]), depth = 0) {
|
|
215
|
+
if (depth > 8 || value == null)
|
|
216
|
+
return [];
|
|
217
|
+
if (typeof value === "string")
|
|
218
|
+
return [value];
|
|
219
|
+
if (Array.isArray(value))
|
|
220
|
+
return value.flatMap((entry) => collectTextFields(entry, fields, depth + 1));
|
|
221
|
+
if (typeof value !== "object")
|
|
222
|
+
return [];
|
|
223
|
+
const output = [];
|
|
224
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
225
|
+
if (fields.has(key) && typeof entry === "string")
|
|
226
|
+
output.push(entry);
|
|
227
|
+
if (typeof entry === "object" && entry !== null)
|
|
228
|
+
output.push(...collectTextFields(entry, fields, depth + 1));
|
|
229
|
+
}
|
|
230
|
+
return output;
|
|
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
|
+
}
|
|
205
243
|
function extractLocalAgentProposal(raw, provider) {
|
|
206
244
|
const outputs = [];
|
|
207
|
-
|
|
245
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim());
|
|
246
|
+
const streamObjects = jsonObjectCandidates(raw).flatMap((candidate) => {
|
|
247
|
+
try {
|
|
248
|
+
return [JSON.parse(candidate)];
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
for (const line of lines) {
|
|
208
255
|
try {
|
|
209
256
|
const event = JSON.parse(line);
|
|
210
257
|
const item = event.item;
|
|
@@ -212,7 +259,7 @@ function extractLocalAgentProposal(raw, provider) {
|
|
|
212
259
|
const values = provider === "claude-code"
|
|
213
260
|
? [event.result, ...(event.message?.content || []).map((entry) => entry.text)]
|
|
214
261
|
: provider === "opencode"
|
|
215
|
-
? [part?.type === "text" ? part.text : undefined
|
|
262
|
+
? [...collectTextFields(event), part?.type === "text" ? part.text : undefined]
|
|
216
263
|
: [event.text, event.output_text, item?.text, item?.output_text, item?.message];
|
|
217
264
|
for (const value of values)
|
|
218
265
|
if (typeof value === "string" && value.trim())
|
|
@@ -223,6 +270,23 @@ function extractLocalAgentProposal(raw, provider) {
|
|
|
223
270
|
outputs.push(line);
|
|
224
271
|
}
|
|
225
272
|
}
|
|
273
|
+
// OpenCode can emit multiple JSON records on one physical line. The
|
|
274
|
+
// line-based pass above cannot see those records, so inspect each balanced
|
|
275
|
+
// JSON object as well and collect text nested in its final response event.
|
|
276
|
+
for (const event of streamObjects) {
|
|
277
|
+
outputs.push(...collectTextFields(event));
|
|
278
|
+
const part = event.part;
|
|
279
|
+
if (part?.type === "text" && typeof part.text === "string")
|
|
280
|
+
outputs.push(part.text);
|
|
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
|
+
}
|
|
226
290
|
// Tool events are interleaved with the final answer. Try text events in
|
|
227
291
|
// reverse order, then the complete stream as a final fallback.
|
|
228
292
|
for (const output of [...outputs.reverse(), raw]) {
|
|
@@ -233,7 +297,7 @@ function extractLocalAgentProposal(raw, provider) {
|
|
|
233
297
|
// Keep looking; this output may only be a progress or tool event.
|
|
234
298
|
}
|
|
235
299
|
}
|
|
236
|
-
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.`);
|
|
237
301
|
}
|
|
238
302
|
function snippet(value, max = 80) {
|
|
239
303
|
const s = String(value ?? "").trim().replace(/\s+/g, " ");
|
|
@@ -329,8 +393,26 @@ function progressFromLine(raw, provider) {
|
|
|
329
393
|
}
|
|
330
394
|
return null;
|
|
331
395
|
}
|
|
396
|
+
function progressEventsFromRaw(raw, provider) {
|
|
397
|
+
const records = (() => {
|
|
398
|
+
try {
|
|
399
|
+
JSON.parse(raw);
|
|
400
|
+
return [raw];
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return jsonObjectCandidates(raw);
|
|
404
|
+
}
|
|
405
|
+
})();
|
|
406
|
+
return records.flatMap((record) => {
|
|
407
|
+
const progress = progressFromLine(record, provider);
|
|
408
|
+
return progress ? [progress] : [];
|
|
409
|
+
});
|
|
410
|
+
}
|
|
332
411
|
function localAgentError(command, stderr, exitCode) {
|
|
333
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
|
+
}
|
|
334
416
|
if (command === "codex" && output.includes("missing field `base_instructions`")) {
|
|
335
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.";
|
|
336
418
|
}
|
|
@@ -351,13 +433,13 @@ function localCommand(provider, model, prompt) {
|
|
|
351
433
|
return { command: "claude", args: ["-p", prompt || "", "--output-format", "stream-json", "--verbose", "--permission-mode", "plan", "--max-turns", "3", ...(selectedModel ? ["--model", selectedModel] : [])] };
|
|
352
434
|
}
|
|
353
435
|
if (provider === "opencode") {
|
|
354
|
-
return { command: "opencode", args: ["run", "--format", "json", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
|
|
436
|
+
return { command: "opencode", args: ["run", "--format", "json", "--print-logs", "--log-level", "INFO", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
|
|
355
437
|
}
|
|
356
438
|
return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
|
|
357
439
|
}
|
|
358
|
-
function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
440
|
+
function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
|
|
359
441
|
return new Promise((resolve, reject) => {
|
|
360
|
-
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"] });
|
|
361
443
|
const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : "Claude Code";
|
|
362
444
|
let stdout = "";
|
|
363
445
|
let stderr = "";
|
|
@@ -365,9 +447,20 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
|
365
447
|
let pendingStderr = "";
|
|
366
448
|
let settled = false;
|
|
367
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
|
+
};
|
|
368
462
|
const timeout = setTimeout(() => {
|
|
369
463
|
timedOut = true;
|
|
370
|
-
onProgress?.(`${label} did not respond; stopping this task…`);
|
|
371
464
|
child.kill("SIGTERM");
|
|
372
465
|
setTimeout(() => child.kill("SIGKILL"), 3000).unref();
|
|
373
466
|
}, 5 * 60 * 1000);
|
|
@@ -390,10 +483,13 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
|
390
483
|
else
|
|
391
484
|
pendingStderr = remainder;
|
|
392
485
|
for (const line of lines) {
|
|
393
|
-
const progress
|
|
394
|
-
if (progress)
|
|
486
|
+
for (const progress of progressEventsFromRaw(line, provider)) {
|
|
395
487
|
onProgress?.(progress.message, progress.detail);
|
|
396
|
-
|
|
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
|
+
}
|
|
492
|
+
if (stream === "stderr" && line.trim()) {
|
|
397
493
|
if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
|
|
398
494
|
onProgress?.("OpenCode · provider rejected the request; stopping this task…");
|
|
399
495
|
child.kill("SIGTERM");
|
|
@@ -403,12 +499,6 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
|
403
499
|
};
|
|
404
500
|
child.stdout.on("data", (chunk) => consume(chunk, "stdout"));
|
|
405
501
|
child.stderr.on("data", (chunk) => consume(chunk, "stderr"));
|
|
406
|
-
child.once("spawn", () => onProgress?.(`${label} process started${child.pid ? ` · PID ${child.pid}` : ""}`));
|
|
407
|
-
const heartbeat = setInterval(() => {
|
|
408
|
-
if (!signal?.aborted)
|
|
409
|
-
onProgress?.(`${label} is still running${child.pid ? ` · PID ${child.pid}` : ""}…`);
|
|
410
|
-
}, 15000);
|
|
411
|
-
heartbeat.unref();
|
|
412
502
|
const abort = () => child.kill("SIGTERM");
|
|
413
503
|
if (signal?.aborted)
|
|
414
504
|
abort();
|
|
@@ -419,7 +509,8 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
|
419
509
|
return;
|
|
420
510
|
settled = true;
|
|
421
511
|
clearTimeout(timeout);
|
|
422
|
-
|
|
512
|
+
if (taskId)
|
|
513
|
+
activeAgentInputs.delete(taskId);
|
|
423
514
|
signal?.removeEventListener("abort", abort);
|
|
424
515
|
reject(error);
|
|
425
516
|
});
|
|
@@ -428,7 +519,8 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
|
428
519
|
return;
|
|
429
520
|
settled = true;
|
|
430
521
|
clearTimeout(timeout);
|
|
431
|
-
|
|
522
|
+
if (taskId)
|
|
523
|
+
activeAgentInputs.delete(taskId);
|
|
432
524
|
signal?.removeEventListener("abort", abort);
|
|
433
525
|
if (timedOut) {
|
|
434
526
|
reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
|
|
@@ -438,34 +530,35 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
|
|
|
438
530
|
const stderrTail = pendingStderr.trim();
|
|
439
531
|
if (stdoutTail) {
|
|
440
532
|
stdout += pendingStdout;
|
|
441
|
-
const progress
|
|
442
|
-
if (progress)
|
|
533
|
+
for (const progress of progressEventsFromRaw(stdoutTail, provider)) {
|
|
443
534
|
onProgress?.(progress.message, progress.detail);
|
|
535
|
+
}
|
|
444
536
|
}
|
|
445
537
|
if (stderrTail) {
|
|
446
|
-
const progress
|
|
447
|
-
if (progress)
|
|
538
|
+
for (const progress of progressEventsFromRaw(stderrTail, provider)) {
|
|
448
539
|
onProgress?.(progress.message, progress.detail);
|
|
540
|
+
}
|
|
541
|
+
if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
|
|
542
|
+
onProgress?.("OpenCode · provider rejected the request; stopping this task…");
|
|
543
|
+
}
|
|
449
544
|
}
|
|
450
545
|
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
451
546
|
});
|
|
452
547
|
});
|
|
453
548
|
}
|
|
454
|
-
async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
|
|
549
|
+
async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, taskId, onPrompt) {
|
|
455
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.`;
|
|
456
551
|
const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
|
|
457
552
|
const local = localCommand(config.provider, config.model, prompt);
|
|
458
553
|
const command = local.command;
|
|
459
554
|
const args = local.args;
|
|
460
|
-
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);
|
|
461
556
|
if (result.exitCode !== 0)
|
|
462
557
|
throw new Error(localAgentError(command, result.stderr, result.exitCode));
|
|
463
558
|
return extractLocalAgentProposal(result.stdout, config.provider);
|
|
464
559
|
}
|
|
465
|
-
async function proposeChanges(cwd, input, config, signal, onProgress) {
|
|
466
|
-
onProgress?.("Reading source context…");
|
|
560
|
+
async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
|
|
467
561
|
const context = await contextFor(cwd, input.element);
|
|
468
|
-
onProgress?.("Starting the coding agent…");
|
|
469
562
|
const visualContext = input.context ? { ...input.context, screenshots: undefined } : undefined;
|
|
470
563
|
const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
|
|
471
564
|
const priorChanges = input.changesHistory?.length ? JSON.stringify(input.changesHistory, null, 2) : "None";
|
|
@@ -474,7 +567,7 @@ async function proposeChanges(cwd, input, config, signal, onProgress) {
|
|
|
474
567
|
: "";
|
|
475
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"}]}`;
|
|
476
569
|
if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
|
|
477
|
-
return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress);
|
|
570
|
+
return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
|
|
478
571
|
}
|
|
479
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.";
|
|
480
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");
|
|
@@ -525,13 +618,13 @@ async function proposeChanges(cwd, input, config, signal, onProgress) {
|
|
|
525
618
|
throw new Error("The agent returned an empty response.");
|
|
526
619
|
return jsonFrom(text);
|
|
527
620
|
}
|
|
528
|
-
async function answerQuestion(cwd, input, config, signal, onProgress) {
|
|
621
|
+
async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
|
|
529
622
|
const context = await contextFor(cwd, input.element);
|
|
530
623
|
const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
|
|
531
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."}`;
|
|
532
625
|
if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
|
|
533
626
|
const local = localCommand(config.provider, config.model, prompt);
|
|
534
|
-
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);
|
|
535
628
|
if (result.exitCode !== 0)
|
|
536
629
|
throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
|
|
537
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;
|
|
@@ -278,12 +295,16 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
278
295
|
const controller = new AbortController();
|
|
279
296
|
taskControllers.set(request.taskId, controller);
|
|
280
297
|
if (socket.readyState === socket.OPEN) {
|
|
281
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage || "
|
|
298
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage || "Working…" }));
|
|
282
299
|
}
|
|
283
300
|
void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail) => {
|
|
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";
|
|
@@ -336,10 +379,13 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
336
379
|
const selectedConfig = localProvider
|
|
337
380
|
? { provider: cliProvider, model: msg.model }
|
|
338
381
|
: { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
|
|
339
|
-
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "
|
|
340
|
-
void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
|
|
382
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "Working…" }));
|
|
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,15 +19135,15 @@
|
|
|
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);
|
|
19044
19144
|
if (taskId2) recordTaskActivity(taskId2, logMessage);
|
|
19045
19145
|
const running = status === "thinking" || status === "working";
|
|
19046
|
-
agentStatusElement.hidden = status === "review";
|
|
19146
|
+
agentStatusElement.hidden = status === "review" || status === "complete";
|
|
19047
19147
|
agentStatusElement.dataset.status = status;
|
|
19048
19148
|
agentStatusMessage.textContent = message;
|
|
19049
19149
|
const badgeEl = agentStatusElement.querySelector(".lasso-agent-status-badge");
|
|
@@ -19074,18 +19174,9 @@
|
|
|
19074
19174
|
}
|
|
19075
19175
|
function resetAgentState() {
|
|
19076
19176
|
state.agentRunning = false;
|
|
19077
|
-
const hasLog = agentLogLines.length > 0;
|
|
19078
19177
|
if (agentStatusElement) {
|
|
19079
|
-
agentStatusElement.hidden =
|
|
19080
|
-
agentStatusElement.dataset.status =
|
|
19081
|
-
}
|
|
19082
|
-
if (hasLog && agentStatusMessage) agentStatusMessage.textContent = "Agent complete";
|
|
19083
|
-
if (hasLog) {
|
|
19084
|
-
const badgeEl = agentStatusElement?.querySelector(".lasso-agent-status-badge");
|
|
19085
|
-
if (badgeEl) {
|
|
19086
|
-
badgeEl.textContent = "done";
|
|
19087
|
-
badgeEl.className = "lasso-agent-status-badge complete";
|
|
19088
|
-
}
|
|
19178
|
+
agentStatusElement.hidden = true;
|
|
19179
|
+
agentStatusElement.dataset.status = "idle";
|
|
19089
19180
|
}
|
|
19090
19181
|
if (sendButton) {
|
|
19091
19182
|
sendButton.classList.remove("loading");
|
|
@@ -19101,13 +19192,17 @@
|
|
|
19101
19192
|
function hideReview() {
|
|
19102
19193
|
if (reviewPanel) reviewPanel.hidden = true;
|
|
19103
19194
|
}
|
|
19104
|
-
function resetPromptSession() {
|
|
19195
|
+
function resetPromptSession(element = state.selected) {
|
|
19105
19196
|
state.promptTaskId = null;
|
|
19106
|
-
state.chatHistory = [];
|
|
19107
|
-
state.changesHistory = [];
|
|
19108
19197
|
state.pendingChanges = [];
|
|
19109
19198
|
state.lastInstruction = "";
|
|
19110
|
-
|
|
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
|
+
}
|
|
19111
19206
|
state.screenshotPromise = Promise.resolve({});
|
|
19112
19207
|
hideReview();
|
|
19113
19208
|
clearAgentLog();
|
|
@@ -19280,7 +19375,7 @@
|
|
|
19280
19375
|
const changesHistory = state.changesHistory.map((entry) => ({ ...entry, changes: entry.changes.map((change) => ({ ...change })) }));
|
|
19281
19376
|
const task = createAgentTask(instruction, messages, changesHistory, selectedElement, selectionId);
|
|
19282
19377
|
updateAgentTask(task.id, { status: "thinking", message: "Thinking\u2026", activity: [...task.activity, "Thinking\u2026"] });
|
|
19283
|
-
resetPromptSession();
|
|
19378
|
+
resetPromptSession(selectedElement);
|
|
19284
19379
|
state.promptTaskId = task.id;
|
|
19285
19380
|
state.lastInstruction = instruction;
|
|
19286
19381
|
const rect = selectedElement.getBoundingClientRect();
|
|
@@ -19350,7 +19445,7 @@
|
|
|
19350
19445
|
}
|
|
19351
19446
|
function openPromptForSelected(selected) {
|
|
19352
19447
|
if (!promptEl || !promptElement) return;
|
|
19353
|
-
resetPromptSession();
|
|
19448
|
+
resetPromptSession(selected);
|
|
19354
19449
|
promptElement.textContent = getElementLabel(selected);
|
|
19355
19450
|
positionPrompt(selected);
|
|
19356
19451
|
promptEl.classList.add("visible");
|
|
@@ -20667,7 +20762,65 @@
|
|
|
20667
20762
|
</svg>
|
|
20668
20763
|
</button>
|
|
20669
20764
|
`;
|
|
20670
|
-
|
|
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
|
+
});
|
|
20671
20824
|
const selectBtn = toolbar.querySelector(".select-tool");
|
|
20672
20825
|
const dragBtn = toolbar.querySelector(".drag-tool");
|
|
20673
20826
|
const previewBtn = toolbar.querySelector(".preview-tool");
|
|
@@ -20679,6 +20832,67 @@
|
|
|
20679
20832
|
const clipboardBtn = toolbar.querySelector(".clipboard-tool");
|
|
20680
20833
|
const voiceBtn = toolbar.querySelector(".voice-tool");
|
|
20681
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
|
+
});
|
|
20682
20896
|
selectBtn.addEventListener("click", (e) => {
|
|
20683
20897
|
e.preventDefault();
|
|
20684
20898
|
e.stopPropagation();
|
|
@@ -20737,10 +20951,12 @@
|
|
|
20737
20951
|
});
|
|
20738
20952
|
dismissBtn.addEventListener("click", () => {
|
|
20739
20953
|
toolbar.classList.add("dismissed");
|
|
20954
|
+
miniToolbar.classList.add("dismissed");
|
|
20740
20955
|
toolbarReopen.classList.add("visible");
|
|
20741
20956
|
});
|
|
20742
20957
|
toolbarReopen.addEventListener("click", () => {
|
|
20743
20958
|
toolbar.classList.remove("dismissed");
|
|
20959
|
+
miniToolbar.classList.remove("dismissed");
|
|
20744
20960
|
toolbarReopen.classList.remove("visible");
|
|
20745
20961
|
});
|
|
20746
20962
|
voiceBar.querySelector(".mute-toggle").addEventListener("click", () => {
|
|
@@ -20813,6 +21029,18 @@
|
|
|
20813
21029
|
if (message.type === "git_progress" && message.message) {
|
|
20814
21030
|
setGitMessage(message.message);
|
|
20815
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
|
+
}
|
|
20816
21044
|
if (message.type === "agent_status" && message.status && message.message) {
|
|
20817
21045
|
const taskId2 = message.taskId;
|
|
20818
21046
|
const task = taskId2 ? getAgentTask(taskId2) : void 0;
|
package/package.json
CHANGED