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