@lasso-ai/cli 2.1.3 → 2.1.5
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/dist/cli/backends.d.ts +2 -0
- package/dist/cli/backends.js +8 -4
- package/dist/cli/bridge.d.ts +4 -0
- package/dist/cli/bridge.js +238 -58
- package/dist/cli/local-runtime.d.ts +5 -0
- package/dist/cli/local-runtime.js +2 -0
- package/dist/cli/oneshot.js +37 -16
- package/dist/cli/runtime-client.d.ts +13 -0
- package/dist/cli/runtime-client.js +1 -1
- package/dist/overlay.js +195 -203
- package/dist/runtime/action-executor.d.ts +2 -1
- package/dist/runtime/action-executor.js +14 -5
- package/dist/runtime/adapters/codex-cli-adapter.d.ts +1 -0
- package/dist/runtime/adapters/codex-cli-adapter.js +32 -4
- package/dist/runtime/adapters/cursor-adapter.js +1 -1
- package/dist/runtime/adapters/local-agent-adapter.js +10 -3
- package/dist/runtime/adapters/opencode-adapter.js +19 -7
- package/dist/runtime/agent-runtime.d.ts +4 -0
- package/dist/runtime/agent-state-machine.d.ts +4 -0
- package/dist/runtime/agent-state-machine.js +10 -2
- package/dist/runtime/backend-interface.d.ts +2 -0
- package/package.json +1 -1
package/dist/cli/backends.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface DetectedBackend {
|
|
|
33
33
|
error?: string;
|
|
34
34
|
fixInstructions?: string;
|
|
35
35
|
}
|
|
36
|
+
/** OpenCode-hosted free models are restricted to OpenCode's own product UI. */
|
|
37
|
+
export declare function isOpenCodeHeadlessRestrictedModel(modelId: string): boolean;
|
|
36
38
|
/**
|
|
37
39
|
* Probe all supported local agents in parallel.
|
|
38
40
|
* Fault-tolerant: a missing or broken CLI never rejects the whole list.
|
package/dist/cli/backends.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.LOCAL_MODEL_PREFIXES = void 0;
|
|
19
|
+
exports.isOpenCodeHeadlessRestrictedModel = isOpenCodeHeadlessRestrictedModel;
|
|
19
20
|
exports.detectBackends = detectBackends;
|
|
20
21
|
exports.modelIdForBackend = modelIdForBackend;
|
|
21
22
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -43,6 +44,11 @@ async function probe(bin, args, timeout = 10000) {
|
|
|
43
44
|
function stripAnsi(text) {
|
|
44
45
|
return text.replace(/\u001b\[[0-9;]*m/g, "").replace(/\u001b\][^\u0007]*\u0007/g, "");
|
|
45
46
|
}
|
|
47
|
+
/** OpenCode-hosted free models are restricted to OpenCode's own product UI. */
|
|
48
|
+
function isOpenCodeHeadlessRestrictedModel(modelId) {
|
|
49
|
+
const id = modelId.trim().toLowerCase();
|
|
50
|
+
return id === "opencode/big-pickle" || id.endsWith("-free") || id.includes("-free-");
|
|
51
|
+
}
|
|
46
52
|
function parseJson(stdout) {
|
|
47
53
|
const start = stdout.indexOf("{");
|
|
48
54
|
if (start < 0)
|
|
@@ -135,6 +141,7 @@ async function detectOpenCode() {
|
|
|
135
141
|
.split("\n")
|
|
136
142
|
.map((line) => line.trim())
|
|
137
143
|
.filter((line) => /^[a-z0-9-]+\/[a-z0-9._-]+$/i.test(line))
|
|
144
|
+
.filter((line) => !isOpenCodeHeadlessRestrictedModel(line))
|
|
138
145
|
.slice(0, 30);
|
|
139
146
|
base.models = ids.map((full) => {
|
|
140
147
|
const name = full.split("/")[1];
|
|
@@ -146,10 +153,7 @@ async function detectOpenCode() {
|
|
|
146
153
|
});
|
|
147
154
|
}
|
|
148
155
|
if (base.models.length === 0) {
|
|
149
|
-
base.models = [
|
|
150
|
-
{ id: "opencode:big-pickle", label: "Big Pickle", modelId: "opencode/big-pickle" },
|
|
151
|
-
{ id: "opencode:deepseek-v4-flash", label: "DeepSeek V4 Flash", modelId: "opencode/deepseek-v4-flash" },
|
|
152
|
-
];
|
|
156
|
+
base.models = [{ id: "opencode:claude-sonnet-4", label: "Claude Sonnet 4", modelId: "opencode/claude-sonnet-4" }];
|
|
153
157
|
}
|
|
154
158
|
return base;
|
|
155
159
|
}
|
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -145,7 +145,11 @@ export type BridgeMessage = {
|
|
|
145
145
|
content: string;
|
|
146
146
|
}>;
|
|
147
147
|
taskId?: string;
|
|
148
|
+
} | {
|
|
149
|
+
type: "page_context_response";
|
|
150
|
+
requestId: string;
|
|
148
151
|
pageContext?: any;
|
|
152
|
+
error?: string;
|
|
149
153
|
} | {
|
|
150
154
|
type: "restore_snapshot";
|
|
151
155
|
snapshotId: string;
|
package/dist/cli/bridge.js
CHANGED
|
@@ -22,6 +22,28 @@ const backends_1 = require("./backends");
|
|
|
22
22
|
const local_runtime_1 = require("./local-runtime");
|
|
23
23
|
exports.DEFAULT_BRIDGE_PORT = 3056;
|
|
24
24
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
25
|
+
function cleanConversationReply(value) {
|
|
26
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
27
|
+
if (!text)
|
|
28
|
+
return "";
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(text);
|
|
31
|
+
if (typeof parsed?.reply === "string")
|
|
32
|
+
return parsed.reply.trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
const match = text.match(/"reply"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
36
|
+
if (match) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(`"${match[1]}"`).trim();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return match[1].replace(/\\n/g, "\n").replace(/\\"/g, '"').trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
25
47
|
async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
|
|
26
48
|
try {
|
|
27
49
|
const response = await fetch(`http://127.0.0.1:${port}/__lasso/bridge/restart`, { method: "POST" });
|
|
@@ -143,6 +165,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
143
165
|
// in this process), RuntimeClient for hosted models (via Lasso server).
|
|
144
166
|
const taskFacades = new Map();
|
|
145
167
|
const taskControllers = new Map();
|
|
168
|
+
const pendingBrowserContext = new Map();
|
|
146
169
|
const correctiveAttempts = new Map();
|
|
147
170
|
const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
|
|
148
171
|
const fileEnv = envRoots.reduce((values, root) => ({
|
|
@@ -167,6 +190,16 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
167
190
|
}
|
|
168
191
|
return undefined;
|
|
169
192
|
}
|
|
193
|
+
function requestBrowserContext(action, taskId) {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
if (!overlaySocket || overlaySocket.readyState !== overlaySocket.OPEN) {
|
|
196
|
+
reject(new Error("Live browser context is unavailable because the overlay is disconnected."));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
pendingBrowserContext.set(action.id, { resolve, reject });
|
|
200
|
+
overlaySocket.send(JSON.stringify({ type: "page_context_request", requestId: action.id, taskId, check: action.data || {} }));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
170
203
|
const cliCredentials = (0, auth_1.loadCredentials)();
|
|
171
204
|
const resolvedLassoKey = cliCredentials?.apiKey ||
|
|
172
205
|
(0, project_1.resolveLassoApiKey)(fileEnv) ||
|
|
@@ -271,55 +304,95 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
271
304
|
return;
|
|
272
305
|
switch (event.type) {
|
|
273
306
|
case "agent_started":
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
type: "agent_status",
|
|
277
|
-
taskId,
|
|
278
|
-
status: "thinking",
|
|
279
|
-
message: event.data.summary || "Working...",
|
|
280
|
-
}));
|
|
307
|
+
// The command bar owns the neutral initial status; real runtime events
|
|
308
|
+
// replace it as soon as the agent has useful progress to report.
|
|
281
309
|
break;
|
|
282
|
-
case "
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
310
|
+
case "agent_reasoning": {
|
|
311
|
+
// The model's actual reasoning summary — dynamic content only.
|
|
312
|
+
// Raw thoughts (when the provider exposes them) ride along as the
|
|
313
|
+
// step's detail line.
|
|
314
|
+
const summary = event.data?.summary;
|
|
315
|
+
if (summary) {
|
|
316
|
+
const raw = event.data?.raw;
|
|
317
|
+
const detail = typeof raw === "string" && raw.trim()
|
|
318
|
+
? raw.length > 800
|
|
319
|
+
? `${raw.slice(0, 800)}…`
|
|
320
|
+
: raw
|
|
321
|
+
: undefined;
|
|
322
|
+
socket.send(JSON.stringify({
|
|
323
|
+
type: "agent_status",
|
|
324
|
+
taskId,
|
|
325
|
+
status: "thinking",
|
|
326
|
+
message: summary,
|
|
327
|
+
detail,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
289
330
|
break;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
331
|
+
}
|
|
332
|
+
case "tool_started": {
|
|
333
|
+
const data = event.data || {};
|
|
334
|
+
// Model invocations are internal — the reasoning summaries bracket
|
|
335
|
+
// them; never surface "Running model" style steps.
|
|
336
|
+
if (data.actionType === "model")
|
|
337
|
+
break;
|
|
338
|
+
const message = data.label || data.tool;
|
|
339
|
+
if (message) {
|
|
340
|
+
socket.send(JSON.stringify({
|
|
341
|
+
type: "agent_status",
|
|
342
|
+
taskId,
|
|
343
|
+
status: "working",
|
|
344
|
+
message,
|
|
345
|
+
detail: data.detail,
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
297
348
|
break;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
349
|
+
}
|
|
350
|
+
case "tool_completed": {
|
|
351
|
+
const data = event.data || {};
|
|
352
|
+
if (data.actionType === "model")
|
|
353
|
+
break;
|
|
354
|
+
// Success: the step auto-completes when the next event arrives.
|
|
355
|
+
// Only failures surface, with the real error.
|
|
356
|
+
if (data.success === false) {
|
|
357
|
+
const message = data.label || data.tool;
|
|
358
|
+
socket.send(JSON.stringify({
|
|
359
|
+
type: "agent_status",
|
|
360
|
+
taskId,
|
|
361
|
+
status: "working",
|
|
362
|
+
message: message ? `${message} didn't finish` : "A step didn't finish",
|
|
363
|
+
detail: data.error || data.detail,
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
306
366
|
break;
|
|
307
367
|
}
|
|
368
|
+
case "phase_changed":
|
|
369
|
+
// Internal phase names ("execute", "verify") are jargon — the
|
|
370
|
+
// reasoning and action labels already tell the real story.
|
|
371
|
+
break;
|
|
308
372
|
case "verification_started":
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
taskId,
|
|
312
|
-
status: "review",
|
|
313
|
-
message: "Verifying changes...",
|
|
314
|
-
}));
|
|
373
|
+
// No canned status line — the check names/results arrive with
|
|
374
|
+
// verification_completed and become the real progress copy.
|
|
315
375
|
break;
|
|
316
376
|
case "verification_completed": {
|
|
317
|
-
const
|
|
377
|
+
const data = event.data || {};
|
|
378
|
+
const passed = data.success ?? data.passed;
|
|
379
|
+
const summaries = (data.observations || [])
|
|
380
|
+
.map((o) => o?.summary)
|
|
381
|
+
.filter(Boolean);
|
|
382
|
+
const errList = (data.errors || [])
|
|
383
|
+
.map((e) => e?.message)
|
|
384
|
+
.filter(Boolean);
|
|
385
|
+
const message = summaries.length
|
|
386
|
+
? summaries.join(" · ")
|
|
387
|
+
: passed
|
|
388
|
+
? "Checks passed"
|
|
389
|
+
: "Checks failed";
|
|
318
390
|
socket.send(JSON.stringify({
|
|
319
391
|
type: "agent_status",
|
|
320
392
|
taskId,
|
|
321
393
|
status: passed ? "working" : "error",
|
|
322
|
-
message
|
|
394
|
+
message,
|
|
395
|
+
...(errList.length ? { detail: errList.join("\n") } : {}),
|
|
323
396
|
}));
|
|
324
397
|
break;
|
|
325
398
|
}
|
|
@@ -348,6 +421,28 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
348
421
|
break;
|
|
349
422
|
}
|
|
350
423
|
}
|
|
424
|
+
async function sendLocalConversationReply(facade, session, taskId, socket) {
|
|
425
|
+
const finalSession = await facade.getSession(session.id);
|
|
426
|
+
const messages = finalSession.conversation || [];
|
|
427
|
+
const reply = [...messages]
|
|
428
|
+
.reverse()
|
|
429
|
+
.find((message) => message?.role === "assistant" && typeof message.content === "string")?.content?.trim();
|
|
430
|
+
if (reply) {
|
|
431
|
+
socket.send(JSON.stringify({
|
|
432
|
+
type: "assistant_message",
|
|
433
|
+
taskId,
|
|
434
|
+
message: reply,
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
socket.send(JSON.stringify({
|
|
439
|
+
type: "agent_status",
|
|
440
|
+
taskId,
|
|
441
|
+
status: "stopped",
|
|
442
|
+
message: "Complete",
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
351
446
|
// The server rolled the workspace back before review, so the current files
|
|
352
447
|
// are the originals: capture them for undo, then open the review panel.
|
|
353
448
|
async function handleReviewReady(session, socket, taskId, instruction) {
|
|
@@ -368,37 +463,124 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
368
463
|
return { filePath: change.filePath, content };
|
|
369
464
|
}));
|
|
370
465
|
(0, snapshot_1.createSnapshot)(cwd, taskId, instruction, changes.map((change) => change.filePath), undefined, session.id);
|
|
466
|
+
socket.send(JSON.stringify({
|
|
467
|
+
type: "agent_status",
|
|
468
|
+
taskId,
|
|
469
|
+
status: "review",
|
|
470
|
+
message: "Review changes",
|
|
471
|
+
changes,
|
|
472
|
+
}));
|
|
371
473
|
}
|
|
474
|
+
else {
|
|
475
|
+
await sendLocalConversationReply(facade, session, taskId, socket);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
372
479
|
socket.send(JSON.stringify({
|
|
373
480
|
type: "agent_status",
|
|
374
481
|
taskId,
|
|
375
|
-
status: "
|
|
376
|
-
message:
|
|
377
|
-
changes,
|
|
482
|
+
status: "error",
|
|
483
|
+
message: error instanceof Error ? error.message : "Failed to load review changes",
|
|
378
484
|
}));
|
|
379
485
|
}
|
|
486
|
+
}
|
|
487
|
+
// Handler for edit/ask - uses runtime
|
|
488
|
+
async function handleHostedConversation(taskId, prompt, model, provider, messages, socket, signal) {
|
|
489
|
+
const requestController = new AbortController();
|
|
490
|
+
const abortRequest = () => requestController.abort();
|
|
491
|
+
signal.addEventListener("abort", abortRequest, { once: true });
|
|
492
|
+
const timeout = setTimeout(() => {
|
|
493
|
+
if (!signal.aborted)
|
|
494
|
+
socket.send(JSON.stringify({
|
|
495
|
+
type: "agent_status",
|
|
496
|
+
taskId,
|
|
497
|
+
status: "error",
|
|
498
|
+
message: "The conversation timed out. Please try again.",
|
|
499
|
+
}));
|
|
500
|
+
requestController.abort();
|
|
501
|
+
}, 30_000);
|
|
502
|
+
try {
|
|
503
|
+
const response = await fetch(`${serverUrl}/api/v1/agent/session`, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
Accept: "text/event-stream",
|
|
507
|
+
"Content-Type": "application/json",
|
|
508
|
+
...(resolvedLassoKey ? { Authorization: `Bearer ${resolvedLassoKey}` } : {}),
|
|
509
|
+
},
|
|
510
|
+
body: JSON.stringify({
|
|
511
|
+
prompt,
|
|
512
|
+
model: { id: model, provider },
|
|
513
|
+
messages: Array.isArray(messages) ? messages : undefined,
|
|
514
|
+
}),
|
|
515
|
+
signal: requestController.signal,
|
|
516
|
+
});
|
|
517
|
+
if (!response.ok || !response.body) {
|
|
518
|
+
throw new Error(`Conversation request failed (${response.status})`);
|
|
519
|
+
}
|
|
520
|
+
const reader = response.body.getReader();
|
|
521
|
+
const decoder = new TextDecoder();
|
|
522
|
+
let buffer = "";
|
|
523
|
+
while (true) {
|
|
524
|
+
const { done, value } = await reader.read();
|
|
525
|
+
if (done)
|
|
526
|
+
break;
|
|
527
|
+
buffer += decoder.decode(value, { stream: true });
|
|
528
|
+
const lines = buffer.split("\n");
|
|
529
|
+
buffer = lines.pop() || "";
|
|
530
|
+
for (const line of lines) {
|
|
531
|
+
if (!line.startsWith("data: "))
|
|
532
|
+
continue;
|
|
533
|
+
const payload = line.slice(6).trim();
|
|
534
|
+
if (!payload || payload === "[DONE]")
|
|
535
|
+
continue;
|
|
536
|
+
const event = JSON.parse(payload);
|
|
537
|
+
if (event.type === "error")
|
|
538
|
+
throw new Error(event.error || "Conversation failed");
|
|
539
|
+
if (event.type === "done") {
|
|
540
|
+
socket.send(JSON.stringify({
|
|
541
|
+
type: "assistant_message",
|
|
542
|
+
taskId,
|
|
543
|
+
message: cleanConversationReply(event.reply || event.summary),
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
380
549
|
catch (error) {
|
|
550
|
+
if (signal.aborted)
|
|
551
|
+
return;
|
|
381
552
|
socket.send(JSON.stringify({
|
|
382
553
|
type: "agent_status",
|
|
383
554
|
taskId,
|
|
384
555
|
status: "error",
|
|
385
|
-
message: error instanceof Error ? error.message : "
|
|
556
|
+
message: error instanceof Error ? error.message : "Conversation failed",
|
|
386
557
|
}));
|
|
387
558
|
}
|
|
559
|
+
finally {
|
|
560
|
+
clearTimeout(timeout);
|
|
561
|
+
signal.removeEventListener("abort", abortRequest);
|
|
562
|
+
taskControllers.delete(taskId);
|
|
563
|
+
}
|
|
388
564
|
}
|
|
389
|
-
// Handler for edit/ask - uses runtime
|
|
390
565
|
async function handleEditOrAsk(message, socket, taskId, instruction, model, provider, context, element, messages) {
|
|
391
566
|
taskControllers.get(taskId)?.abort();
|
|
392
567
|
const controller = new AbortController();
|
|
393
568
|
taskControllers.set(taskId, controller);
|
|
569
|
+
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
570
|
+
if (message.type === "ask" && !isLocalAgent) {
|
|
571
|
+
await handleHostedConversation(taskId, instruction, model, provider, messages, socket, controller.signal);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// Do not echo the user's prompt into the thinking feed. The prompt is
|
|
575
|
+
// already visible in the conversation; use a neutral status while the
|
|
576
|
+
// runtime produces its first useful event.
|
|
394
577
|
socket.send(JSON.stringify({
|
|
395
578
|
type: "agent_status",
|
|
396
579
|
taskId,
|
|
397
580
|
status: "thinking",
|
|
398
|
-
message: "
|
|
581
|
+
message: "Thinking…",
|
|
399
582
|
}));
|
|
400
583
|
try {
|
|
401
|
-
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
402
584
|
// Overlay model option ids are prefixed ("claude_code:sonnet") — the
|
|
403
585
|
// agent CLIs need the unprefixed/normalized form.
|
|
404
586
|
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
@@ -441,7 +623,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
441
623
|
// already rolled back at that point).
|
|
442
624
|
await facade.runSession(session.id, cwd, (event) => {
|
|
443
625
|
handleRuntimeEvent(event, socket, taskId, session, `Runtime session: ${instruction}`, true);
|
|
444
|
-
}, controller.signal, {
|
|
626
|
+
}, controller.signal, {
|
|
627
|
+
providerApiKey: providerApiKeyFor(provider),
|
|
628
|
+
browserContextProvider: ((action) => requestBrowserContext(action, taskId)),
|
|
629
|
+
});
|
|
445
630
|
}
|
|
446
631
|
catch (error) {
|
|
447
632
|
socket.send(JSON.stringify({
|
|
@@ -706,12 +891,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
706
891
|
taskControllers.get(taskId)?.abort();
|
|
707
892
|
const controller = new AbortController();
|
|
708
893
|
taskControllers.set(taskId, controller);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
status: "thinking",
|
|
713
|
-
message: "Starting oneshot...",
|
|
714
|
-
}));
|
|
894
|
+
// Greetings and conversational messages still go through the selected
|
|
895
|
+
// agent/model. The overlay only skips expensive page capture for them;
|
|
896
|
+
// there are no canned assistant responses in the bridge.
|
|
715
897
|
try {
|
|
716
898
|
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
717
899
|
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
@@ -743,13 +925,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
743
925
|
// Oneshot results are reviewed exactly like edit/ask: the workspace is
|
|
744
926
|
// rolled back and only approval re-applies the changes.
|
|
745
927
|
handleRuntimeEvent(event, socket, taskId, session, `Oneshot: ${instruction}`, true);
|
|
746
|
-
if (event.type === "agent_completed") {
|
|
747
|
-
socket.send(JSON.stringify({
|
|
748
|
-
type: "assistant_message",
|
|
749
|
-
taskId,
|
|
750
|
-
message: "Oneshot completed",
|
|
751
|
-
}));
|
|
752
|
-
}
|
|
753
928
|
}, controller.signal, { providerApiKey: providerApiKeyFor(provider) });
|
|
754
929
|
}
|
|
755
930
|
catch (error) {
|
|
@@ -990,6 +1165,11 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
990
1165
|
}));
|
|
991
1166
|
}
|
|
992
1167
|
break;
|
|
1168
|
+
case "runtime_error":
|
|
1169
|
+
// Overlay-side crash (e.g. ReferenceError) — surface it in the
|
|
1170
|
+
// daemon log so it's diagnosable.
|
|
1171
|
+
console.error("[overlay error]", msg.selectionId || "", msg.details || msg.message || msg);
|
|
1172
|
+
break;
|
|
993
1173
|
default:
|
|
994
1174
|
console.warn("Unknown bridge message type:", msg.type);
|
|
995
1175
|
}
|
|
@@ -16,6 +16,11 @@ export declare class LocalRuntime implements RuntimeFacade {
|
|
|
16
16
|
getSession(sessionId: string): Promise<RuntimeSession>;
|
|
17
17
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, _opts?: {
|
|
18
18
|
providerApiKey?: string;
|
|
19
|
+
browserContextProvider?: (action: {
|
|
20
|
+
id: string;
|
|
21
|
+
type: "browser_check";
|
|
22
|
+
data?: Record<string, unknown>;
|
|
23
|
+
}) => Promise<unknown>;
|
|
19
24
|
}): Promise<void>;
|
|
20
25
|
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
21
26
|
switchModel(sessionId: string, model: {
|
|
@@ -36,6 +36,7 @@ class LocalRuntime {
|
|
|
36
36
|
status: "running",
|
|
37
37
|
iteration: 0,
|
|
38
38
|
userPrompt: input.userPrompt,
|
|
39
|
+
mode: input.mode || "task",
|
|
39
40
|
conversation: [
|
|
40
41
|
{ id: node_crypto_1.default.randomUUID(), role: "user", content: input.userPrompt, timestamp: now.getTime() },
|
|
41
42
|
],
|
|
@@ -99,6 +100,7 @@ class LocalRuntime {
|
|
|
99
100
|
const outcome = await runner.run({
|
|
100
101
|
taskId: record.id,
|
|
101
102
|
userPrompt: record.userPrompt,
|
|
103
|
+
mode: record.mode,
|
|
102
104
|
project: {
|
|
103
105
|
projectId: record.project.projectId || "local",
|
|
104
106
|
framework: record.project.framework,
|
package/dist/cli/oneshot.js
CHANGED
|
@@ -116,31 +116,52 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
116
116
|
return;
|
|
117
117
|
switch (event.type) {
|
|
118
118
|
case "agent_started":
|
|
119
|
-
onProgress("thinking", "Agent started", "Initializing runtime");
|
|
120
119
|
break;
|
|
121
|
-
case "agent_reasoning":
|
|
122
|
-
const summary = event.data
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
case "agent_reasoning": {
|
|
121
|
+
const summary = event.data?.summary;
|
|
122
|
+
if (summary) {
|
|
123
|
+
thinkingSteps.push({ title: summary, durationMs: 0 });
|
|
124
|
+
onProgress("thinking", summary);
|
|
125
|
+
}
|
|
125
126
|
break;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
}
|
|
128
|
+
case "tool_started": {
|
|
129
|
+
const data = event.data || {};
|
|
130
|
+
if (data.actionType === "model")
|
|
131
|
+
break;
|
|
132
|
+
const message = data.label || data.tool;
|
|
133
|
+
if (message)
|
|
134
|
+
onProgress("working", message);
|
|
129
135
|
break;
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
}
|
|
137
|
+
case "tool_completed": {
|
|
138
|
+
const data = event.data || {};
|
|
139
|
+
if (data.actionType === "model")
|
|
140
|
+
break;
|
|
141
|
+
if (data.success === false) {
|
|
142
|
+
const message = data.label || data.tool;
|
|
143
|
+
onProgress("working", message ? `${message} didn't finish` : "A step didn't finish");
|
|
144
|
+
}
|
|
132
145
|
break;
|
|
146
|
+
}
|
|
133
147
|
case "phase_changed":
|
|
134
|
-
const phase = event.data.phase;
|
|
135
|
-
onProgress("working", `Phase: ${phase}`);
|
|
136
148
|
break;
|
|
137
149
|
case "verification_started":
|
|
138
|
-
|
|
150
|
+
// Real check names/results arrive with verification_completed.
|
|
139
151
|
break;
|
|
140
|
-
case "verification_completed":
|
|
141
|
-
const
|
|
142
|
-
|
|
152
|
+
case "verification_completed": {
|
|
153
|
+
const data = event.data || {};
|
|
154
|
+
const passed = data.success ?? data.passed;
|
|
155
|
+
const summaries = (data.observations || [])
|
|
156
|
+
.map((o) => o?.summary)
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
onProgress(passed ? "working" : "error", summaries.length
|
|
159
|
+
? summaries.join(" · ")
|
|
160
|
+
: passed
|
|
161
|
+
? "Checks passed"
|
|
162
|
+
: "Checks failed");
|
|
143
163
|
break;
|
|
164
|
+
}
|
|
144
165
|
case "waiting_for_user":
|
|
145
166
|
const question = event.data?.question || "Input required";
|
|
146
167
|
onPrompt({ question });
|
|
@@ -11,6 +11,8 @@ import { type ToolCallRequest } from "../runtime/index.js";
|
|
|
11
11
|
export type RuntimeProvider = "anthropic" | "openai" | "google" | "nvidia" | "ollama" | "claude_code" | "opencode" | "codex_cli" | "cursor";
|
|
12
12
|
export interface RuntimeSessionInput {
|
|
13
13
|
userPrompt: string;
|
|
14
|
+
/** Conversation-only requests must not enter the coding/tool workflow. */
|
|
15
|
+
mode?: "conversation" | "task";
|
|
14
16
|
project: {
|
|
15
17
|
projectId: string;
|
|
16
18
|
framework?: string;
|
|
@@ -50,6 +52,11 @@ export interface RuntimeSessionInput {
|
|
|
50
52
|
projectPath?: string;
|
|
51
53
|
};
|
|
52
54
|
}
|
|
55
|
+
export type BrowserContextProvider = (action: {
|
|
56
|
+
id: string;
|
|
57
|
+
type: "browser_check";
|
|
58
|
+
data?: Record<string, unknown>;
|
|
59
|
+
}) => Promise<unknown>;
|
|
53
60
|
export interface RuntimeSession {
|
|
54
61
|
id: string;
|
|
55
62
|
phase: "understand" | "inspect" | "plan" | "execute" | "verify" | "recover" | "complete";
|
|
@@ -139,6 +146,7 @@ export interface RuntimeFacade {
|
|
|
139
146
|
createSession(input: RuntimeSessionInput): Promise<RuntimeSession>;
|
|
140
147
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
|
|
141
148
|
providerApiKey?: string;
|
|
149
|
+
browserContextProvider?: BrowserContextProvider;
|
|
142
150
|
}): Promise<void>;
|
|
143
151
|
getSession(sessionId: string): Promise<RuntimeSession>;
|
|
144
152
|
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
@@ -159,6 +167,7 @@ export type RuntimeEvent = {
|
|
|
159
167
|
sessionId: string;
|
|
160
168
|
data: {
|
|
161
169
|
summary: string;
|
|
170
|
+
raw?: string;
|
|
162
171
|
};
|
|
163
172
|
timestamp: number;
|
|
164
173
|
} | {
|
|
@@ -174,6 +183,7 @@ export type RuntimeEvent = {
|
|
|
174
183
|
sessionId: string;
|
|
175
184
|
data: {
|
|
176
185
|
actionType: string;
|
|
186
|
+
label?: string;
|
|
177
187
|
};
|
|
178
188
|
timestamp: number;
|
|
179
189
|
} | {
|
|
@@ -181,7 +191,9 @@ export type RuntimeEvent = {
|
|
|
181
191
|
sessionId: string;
|
|
182
192
|
data: {
|
|
183
193
|
actionType: string;
|
|
194
|
+
label?: string;
|
|
184
195
|
success: boolean;
|
|
196
|
+
error?: string;
|
|
185
197
|
};
|
|
186
198
|
timestamp: number;
|
|
187
199
|
} | {
|
|
@@ -292,6 +304,7 @@ export declare class RuntimeClient {
|
|
|
292
304
|
*/
|
|
293
305
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
|
|
294
306
|
providerApiKey?: string;
|
|
307
|
+
browserContextProvider?: BrowserContextProvider;
|
|
295
308
|
}): Promise<void>;
|
|
296
309
|
/** Execute a tool_call request locally against the real project. */
|
|
297
310
|
private executeToolCallLocally;
|
|
@@ -86,7 +86,7 @@ class RuntimeClient {
|
|
|
86
86
|
catch (error) {
|
|
87
87
|
console.error("WorkspaceGuard capture failed:", error);
|
|
88
88
|
}
|
|
89
|
-
const executor = new index_js_1.ActionExecutor(projectPath);
|
|
89
|
+
const executor = new index_js_1.ActionExecutor(projectPath, opts?.browserContextProvider);
|
|
90
90
|
const verifier = new index_js_1.VerificationEngine(projectPath);
|
|
91
91
|
const finalizeGuard = async () => {
|
|
92
92
|
if (!guard)
|