visual-remote 0.3.1 → 0.3.3

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.
@@ -1,4 +1,5 @@
1
1
  // src/vite.ts
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import { resolve as resolve7 } from "node:path";
3
4
 
4
5
  // ../../packages/bridge-core/src/agents/types.ts
@@ -25,27 +26,530 @@ var AgentCanceledError = class extends Error {
25
26
  }
26
27
  };
27
28
 
29
+ // ../../packages/bridge-core/src/agents/claude-event-parser.ts
30
+ function asRecord(value) {
31
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
32
+ }
33
+ function asText(value) {
34
+ return typeof value === "string" && value.length > 0 ? value : void 0;
35
+ }
36
+ function asNumber(value) {
37
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
38
+ }
39
+ function contentBlocks(record) {
40
+ const message = asRecord(record.message);
41
+ return Array.isArray(message?.content) ? message.content.flatMap((block) => {
42
+ const parsed = asRecord(block);
43
+ return parsed === void 0 ? [] : [parsed];
44
+ }) : [];
45
+ }
46
+ function toolSummary(name, input) {
47
+ return asText(input.description) ?? asText(input.command) ?? asText(input.file_path) ?? asText(input.path) ?? (name === "Bash" ? "Run command" : void 0);
48
+ }
49
+ function filePath(input) {
50
+ return asText(input.file_path) ?? asText(input.path) ?? asText(input.notebook_path);
51
+ }
52
+ var ClaudeEventParser = class {
53
+ #defaultCwd;
54
+ #tools = /* @__PURE__ */ new Map();
55
+ #sessionId;
56
+ constructor(defaultCwd = "") {
57
+ this.#defaultCwd = defaultCwd;
58
+ }
59
+ parse(line) {
60
+ const trimmed = line.trim();
61
+ if (!trimmed) return [];
62
+ let value;
63
+ try {
64
+ value = JSON.parse(trimmed);
65
+ } catch {
66
+ return [{ type: "warning", text: trimmed }];
67
+ }
68
+ const record = asRecord(value);
69
+ if (!record) return [{ type: "message", text: trimmed }];
70
+ const events = [];
71
+ const foundSession = asText(record.session_id) ?? asText(record.sessionId);
72
+ if (foundSession !== void 0 && foundSession !== this.#sessionId) {
73
+ this.#sessionId = foundSession;
74
+ events.push({ type: "session", sessionId: foundSession });
75
+ }
76
+ const type = asText(record.type) ?? "unknown";
77
+ if (type === "system") {
78
+ const subtype = asText(record.subtype);
79
+ if (subtype) events.push({ type: "phase", name: subtype });
80
+ return events;
81
+ }
82
+ if (type === "assistant") {
83
+ for (const block of contentBlocks(record)) {
84
+ if (block.type === "text") {
85
+ const text = asText(block.text);
86
+ if (text) events.push({ type: "message", text });
87
+ continue;
88
+ }
89
+ if (block.type !== "tool_use") continue;
90
+ const name = asText(block.name) ?? "tool";
91
+ const id = asText(block.id);
92
+ const input = asRecord(block.input) ?? {};
93
+ if (id) this.#tools.set(id, name);
94
+ const summary = toolSummary(name, input);
95
+ events.push(
96
+ summary ? { type: "tool_start", name, summary } : { type: "tool_start", name }
97
+ );
98
+ const command = name === "Bash" ? asText(input.command) : void 0;
99
+ if (command) {
100
+ events.push({ type: "command", command, cwd: this.#defaultCwd });
101
+ }
102
+ const path = filePath(input);
103
+ if (path && ["Edit", "Write", "NotebookEdit"].includes(name)) {
104
+ events.push({ type: "file_hint", path });
105
+ }
106
+ }
107
+ return events;
108
+ }
109
+ if (type === "user") {
110
+ for (const block of contentBlocks(record)) {
111
+ if (block.type !== "tool_result") continue;
112
+ const id = asText(block.tool_use_id);
113
+ const name = (id ? this.#tools.get(id) : void 0) ?? "tool";
114
+ if (id) this.#tools.delete(id);
115
+ events.push({ type: "tool_end", name, ok: block.is_error !== true });
116
+ }
117
+ return events;
118
+ }
119
+ if (type === "result") {
120
+ const usage = asRecord(record.usage);
121
+ if (usage) {
122
+ const cachedInputTokens = asNumber(usage.cache_read_input_tokens);
123
+ events.push({
124
+ type: "usage",
125
+ inputTokens: asNumber(usage.input_tokens) + asNumber(usage.cache_creation_input_tokens) + cachedInputTokens,
126
+ outputTokens: asNumber(usage.output_tokens),
127
+ ...cachedInputTokens > 0 ? { cachedInputTokens } : {}
128
+ });
129
+ }
130
+ const result = asText(record.result);
131
+ const subtype = asText(record.subtype);
132
+ if (record.is_error === true || subtype?.startsWith("error") === true) {
133
+ events.push({ type: "error", text: result ?? "Claude reported an error" });
134
+ } else {
135
+ events.push(result ? { type: "complete", summary: result } : { type: "complete" });
136
+ }
137
+ return events;
138
+ }
139
+ return events;
140
+ }
141
+ };
142
+
143
+ // ../../packages/bridge-core/src/agents/claude-adapter.ts
144
+ import { execFile, spawn } from "node:child_process";
145
+ import { promisify } from "node:util";
146
+
147
+ // ../../packages/bridge-core/src/runtime/managed-process.ts
148
+ function safeChildProcessId(child) {
149
+ const pid = child.pid;
150
+ if (pid === void 0 || !Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid) {
151
+ return void 0;
152
+ }
153
+ return pid;
154
+ }
155
+ function safeDetachedProcessGroupId(child) {
156
+ return process.platform === "win32" ? void 0 : safeChildProcessId(child);
157
+ }
158
+ function isMissingProcess(error) {
159
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
160
+ }
161
+ function processGroupIsAlive(processGroupId) {
162
+ try {
163
+ process.kill(-processGroupId, 0);
164
+ return true;
165
+ } catch (error) {
166
+ if (isMissingProcess(error)) return false;
167
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "EPERM") {
168
+ return true;
169
+ }
170
+ throw error;
171
+ }
172
+ }
173
+ function childIsAlive(child) {
174
+ return child.exitCode === null && child.signalCode === null;
175
+ }
176
+ function signalChildProcessTree(child, signal) {
177
+ const pid = safeChildProcessId(child);
178
+ if (pid === void 0) return false;
179
+ try {
180
+ if (process.platform === "win32") {
181
+ if (!childIsAlive(child)) return false;
182
+ return child.kill(signal);
183
+ }
184
+ process.kill(-pid, signal);
185
+ return true;
186
+ } catch (error) {
187
+ if (isMissingProcess(error)) return false;
188
+ throw error;
189
+ }
190
+ }
191
+ function installEmergencyChildExitHook(child, processLike = process) {
192
+ let removed = false;
193
+ const emergencyExit = () => {
194
+ removed = true;
195
+ try {
196
+ signalChildProcessTree(child, "SIGKILL");
197
+ } catch {
198
+ }
199
+ };
200
+ processLike.once("exit", emergencyExit);
201
+ return () => {
202
+ if (removed) return;
203
+ removed = true;
204
+ processLike.off("exit", emergencyExit);
205
+ };
206
+ }
207
+ async function waitForProcessTreeExit(child, processGroupId, timeoutMs) {
208
+ const deadline = Date.now() + timeoutMs;
209
+ while (processGroupId === void 0 ? childIsAlive(child) : processGroupIsAlive(processGroupId)) {
210
+ const remaining = deadline - Date.now();
211
+ if (remaining <= 0) return false;
212
+ await new Promise((resolve8) => {
213
+ setTimeout(resolve8, Math.min(25, remaining));
214
+ });
215
+ }
216
+ return true;
217
+ }
218
+ async function terminateChildProcessTree(child, killGraceMs = 3e3) {
219
+ const processGroupId = safeDetachedProcessGroupId(child);
220
+ if (processGroupId === void 0 && !childIsAlive(child)) return;
221
+ const sendSignal = (signal) => {
222
+ signalChildProcessTree(child, signal);
223
+ };
224
+ sendSignal("SIGTERM");
225
+ if (await waitForProcessTreeExit(
226
+ child,
227
+ processGroupId,
228
+ Math.max(0, killGraceMs)
229
+ )) {
230
+ return;
231
+ }
232
+ sendSignal("SIGKILL");
233
+ await waitForProcessTreeExit(
234
+ child,
235
+ processGroupId,
236
+ Math.min(Math.max(0, killGraceMs), 1e3)
237
+ );
238
+ }
239
+
240
+ // ../../packages/bridge-core/src/agents/async-queue.ts
241
+ var AsyncQueue = class {
242
+ #values = [];
243
+ #waiters = [];
244
+ #ended = false;
245
+ #error;
246
+ push(value) {
247
+ if (this.#ended) return;
248
+ const waiter = this.#waiters.shift();
249
+ if (waiter) waiter.resolve({ value, done: false });
250
+ else this.#values.push(value);
251
+ }
252
+ end(error) {
253
+ if (this.#ended) return;
254
+ this.#ended = true;
255
+ this.#error = error;
256
+ for (const waiter of this.#waiters.splice(0)) {
257
+ if (error !== void 0) waiter.reject(error);
258
+ else waiter.resolve({ value: void 0, done: true });
259
+ }
260
+ }
261
+ [Symbol.asyncIterator]() {
262
+ return {
263
+ next: async () => {
264
+ const value = this.#values.shift();
265
+ if (value !== void 0) return { value, done: false };
266
+ if (this.#ended) {
267
+ if (this.#error !== void 0) throw this.#error;
268
+ return { value: void 0, done: true };
269
+ }
270
+ return await new Promise((resolve8, reject) => {
271
+ this.#waiters.push({ resolve: resolve8, reject });
272
+ });
273
+ }
274
+ };
275
+ }
276
+ };
277
+
278
+ // ../../packages/bridge-core/src/agents/claude-adapter.ts
279
+ var execFileAsync = promisify(execFile);
280
+ var INHERITED_ENVIRONMENT = [
281
+ "PATH",
282
+ "HOME",
283
+ "USER",
284
+ "LOGNAME",
285
+ "SHELL",
286
+ "LANG",
287
+ "LC_ALL",
288
+ "TERM",
289
+ "TMPDIR",
290
+ "XDG_CONFIG_HOME",
291
+ "XDG_DATA_HOME",
292
+ "XDG_STATE_HOME",
293
+ "ANTHROPIC_API_KEY",
294
+ "CLAUDE_CODE_OAUTH_TOKEN",
295
+ "HTTPS_PROXY",
296
+ "HTTP_PROXY",
297
+ "NO_PROXY",
298
+ "USERPROFILE",
299
+ "APPDATA",
300
+ "LOCALAPPDATA",
301
+ "SystemRoot",
302
+ "COMSPEC",
303
+ "PATHEXT"
304
+ ];
305
+ var EMPTY_MCP_CONFIG = JSON.stringify({ mcpServers: {} });
306
+ var CLAUDE_SETTINGS = JSON.stringify({
307
+ permissions: {
308
+ disableBypassPermissionsMode: "disable",
309
+ deny: [
310
+ "Read(./.env)",
311
+ "Read(./.env.*)",
312
+ "Read(./**/*.pem)",
313
+ "Read(./**/*.key)",
314
+ "Edit(./.git/**)",
315
+ "Edit(./.visualdev/runtime/**)",
316
+ "Edit(./node_modules/**)"
317
+ ]
318
+ },
319
+ sandbox: {
320
+ enabled: true,
321
+ autoAllowBashIfSandboxed: true,
322
+ allowUnsandboxedCommands: false,
323
+ network: { strictAllowlist: true }
324
+ }
325
+ });
326
+ function processEnv(overrides) {
327
+ const environment = {};
328
+ for (const key of INHERITED_ENVIRONMENT) {
329
+ const value = process.env[key];
330
+ if (value !== void 0) environment[key] = value;
331
+ }
332
+ return { ...environment, ...overrides };
333
+ }
334
+ function splitLines(chunk, previous, onLine) {
335
+ const combined = previous + chunk.toString();
336
+ const lines = combined.split(/\r?\n/);
337
+ const remainder = lines.pop() ?? "";
338
+ for (const line of lines) onLine(line);
339
+ return remainder;
340
+ }
341
+ var ClaudeAdapter = class {
342
+ id = "claude";
343
+ #executable;
344
+ #killGraceMs;
345
+ #model;
346
+ #reasoningEffort;
347
+ #rtkExecutable;
348
+ #rtkVersion;
349
+ constructor(options = {}) {
350
+ this.#executable = options.executable ?? "claude";
351
+ this.#killGraceMs = options.killGraceMs ?? 2e3;
352
+ this.#model = options.model;
353
+ this.#reasoningEffort = options.reasoningEffort;
354
+ this.#rtkExecutable = options.rtkExecutable ?? "rtk";
355
+ }
356
+ #probeRtk(environment) {
357
+ if (this.#rtkExecutable === false) return Promise.resolve(void 0);
358
+ this.#rtkVersion ??= execFileAsync(this.#rtkExecutable, ["--version"], {
359
+ encoding: "utf8",
360
+ env: environment,
361
+ timeout: 1e3,
362
+ windowsHide: true,
363
+ maxBuffer: 16 * 1024
364
+ }).then(({ stdout }) => stdout.trim().split(/\r?\n/, 1)[0] || void 0).catch(() => void 0);
365
+ return this.#rtkVersion;
366
+ }
367
+ async #runtimePrompt(input, environment) {
368
+ if (this.#rtkExecutable === false) return input.prompt;
369
+ const guidance = await this.#probeRtk(environment).then(
370
+ (version) => version ? `RTK command proxy:
371
+ - ${version} is installed and available in this runtime.
372
+ - Prefix shell commands with RTK by default (for example: rtk git status, rtk rg <pattern>, rtk read <file>, rtk npm test).
373
+ - Use the native command only when RTK has no suitable proxy or RTK execution fails. Do not spend time rediscovering or reinstalling RTK.` : `RTK command proxy:
374
+ - RTK was not detected in this runtime. Use native repository commands directly and do not spend time searching for RTK.`
375
+ );
376
+ return `${input.prompt.trimEnd()}
377
+
378
+ ${guidance}
379
+ `;
380
+ }
381
+ #baseArgs() {
382
+ return [
383
+ "-p",
384
+ "--output-format",
385
+ "stream-json",
386
+ "--verbose",
387
+ "--permission-mode",
388
+ "acceptEdits",
389
+ "--strict-mcp-config",
390
+ "--mcp-config",
391
+ EMPTY_MCP_CONFIG,
392
+ "--no-chrome",
393
+ "--tools",
394
+ "Read,Glob,Grep,Edit,Write,Bash",
395
+ "--settings",
396
+ CLAUDE_SETTINGS,
397
+ ...this.#model === void 0 ? [] : ["--model", this.#model],
398
+ ...this.#reasoningEffort === void 0 ? [] : ["--effort", this.#reasoningEffort]
399
+ ];
400
+ }
401
+ async probe() {
402
+ return await new Promise((resolve8) => {
403
+ const child = spawn(this.#executable, ["--version"], {
404
+ stdio: ["ignore", "pipe", "ignore"],
405
+ shell: false
406
+ });
407
+ let output = "";
408
+ child.stdout?.on("data", (chunk) => {
409
+ output += chunk.toString();
410
+ });
411
+ child.once("error", () => {
412
+ resolve8({ available: false, supportsResume: true, structuredOutput: true });
413
+ });
414
+ child.once("close", (code) => {
415
+ const match = output.match(/(\d+\.\d+\.\d+(?:[-+][^\s]+)?)/);
416
+ const capabilities = {
417
+ available: code === 0,
418
+ supportsResume: true,
419
+ structuredOutput: true
420
+ };
421
+ if (match?.[1]) capabilities.version = match[1];
422
+ resolve8(capabilities);
423
+ });
424
+ });
425
+ }
426
+ async *run(input, signal) {
427
+ yield* this.#execute(input, signal, this.#baseArgs());
428
+ }
429
+ async *resume(input, signal) {
430
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(input.sessionId)) {
431
+ throw new Error("Refusing to resume an invalid Claude session id");
432
+ }
433
+ yield* this.#execute(
434
+ input,
435
+ signal,
436
+ [...this.#baseArgs(), "--resume", input.sessionId]
437
+ );
438
+ }
439
+ async *#execute(input, signal, args) {
440
+ const queue = new AsyncQueue();
441
+ const environment = processEnv(input.environment);
442
+ const prompt = await this.#runtimePrompt(input, environment);
443
+ const parser = new ClaudeEventParser(input.workspaceRoot);
444
+ const child = spawn(this.#executable, args, {
445
+ cwd: input.workspaceRoot,
446
+ env: environment,
447
+ detached: process.platform !== "win32",
448
+ shell: false,
449
+ stdio: ["pipe", "pipe", "pipe"]
450
+ });
451
+ const removeEmergencyExitHook = installEmergencyChildExitHook(child);
452
+ let stdoutRemainder = "";
453
+ let stderrRemainder = "";
454
+ let timedOut = false;
455
+ let aborted = signal.aborted;
456
+ let termination;
457
+ const requestTermination = () => {
458
+ termination ??= terminateChildProcessTree(
459
+ child,
460
+ this.#killGraceMs
461
+ ).finally(removeEmergencyExitHook);
462
+ return termination;
463
+ };
464
+ const timeout = setTimeout(() => {
465
+ timedOut = true;
466
+ void requestTermination();
467
+ }, Math.max(1, input.maxRunMs));
468
+ timeout.unref();
469
+ const abort = () => {
470
+ aborted = true;
471
+ void requestTermination();
472
+ };
473
+ signal.addEventListener("abort", abort, { once: true });
474
+ if (signal.aborted) abort();
475
+ child.stdout.on("data", (chunk) => {
476
+ stdoutRemainder = splitLines(chunk, stdoutRemainder, (line) => {
477
+ for (const event of parser.parse(line)) queue.push(event);
478
+ });
479
+ });
480
+ child.stderr.on("data", (chunk) => {
481
+ stderrRemainder = splitLines(chunk, stderrRemainder, (line) => {
482
+ if (line.trim()) queue.push({ type: "warning", text: line });
483
+ });
484
+ });
485
+ child.once("error", (error) => queue.end(error));
486
+ child.once("close", (code, closeSignal) => {
487
+ clearTimeout(timeout);
488
+ signal.removeEventListener("abort", abort);
489
+ void (async () => {
490
+ await requestTermination();
491
+ if (stdoutRemainder.trim()) {
492
+ for (const event of parser.parse(stdoutRemainder)) queue.push(event);
493
+ }
494
+ if (stderrRemainder.trim()) queue.push({ type: "warning", text: stderrRemainder });
495
+ if (timedOut) queue.end(new AgentTimeoutError());
496
+ else if (aborted) queue.end(new AgentCanceledError());
497
+ else if (code !== 0) {
498
+ queue.end(
499
+ new AgentProcessError(
500
+ `Claude exited with code ${String(code)}`,
501
+ code,
502
+ closeSignal
503
+ )
504
+ );
505
+ } else {
506
+ queue.end();
507
+ }
508
+ })().catch((error) => queue.end(error));
509
+ });
510
+ child.stdin.on("error", (error) => {
511
+ if (error.code !== "EPIPE") queue.end(error);
512
+ });
513
+ child.stdin.end(prompt);
514
+ try {
515
+ for await (const event of queue) yield event;
516
+ } finally {
517
+ clearTimeout(timeout);
518
+ signal.removeEventListener("abort", abort);
519
+ try {
520
+ if (child.exitCode === null && child.signalCode === null) {
521
+ await requestTermination();
522
+ } else if (termination) {
523
+ await termination;
524
+ }
525
+ } finally {
526
+ removeEmergencyExitHook();
527
+ }
528
+ }
529
+ }
530
+ };
531
+
28
532
  // ../../packages/bridge-core/src/agents/codex-event-parser.ts
29
- function asRecord(value) {
533
+ function asRecord2(value) {
30
534
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
31
535
  }
32
- function asText(value) {
536
+ function asText2(value) {
33
537
  return typeof value === "string" && value.length > 0 ? value : void 0;
34
538
  }
35
539
  function sessionId(record) {
36
- const thread = asRecord(record.thread);
37
- return asText(record.thread_id) ?? asText(record.threadId) ?? asText(record.session_id) ?? asText(record.sessionId) ?? (thread ? asText(thread.id) : void 0);
540
+ const thread = asRecord2(record.thread);
541
+ return asText2(record.thread_id) ?? asText2(record.threadId) ?? asText2(record.session_id) ?? asText2(record.sessionId) ?? (thread ? asText2(thread.id) : void 0);
38
542
  }
39
543
  function itemFiles(item) {
40
544
  const changes = Array.isArray(item.changes) ? item.changes : [];
41
545
  const files = changes.flatMap((change) => {
42
- const record = asRecord(change);
546
+ const record = asRecord2(change);
43
547
  if (!record) return [];
44
- return [asText(record.path) ?? asText(record.file_path) ?? asText(record.filePath)].filter(
548
+ return [asText2(record.path) ?? asText2(record.file_path) ?? asText2(record.filePath)].filter(
45
549
  (path) => path !== void 0
46
550
  );
47
551
  });
48
- const direct = asText(item.path) ?? asText(item.file_path) ?? asText(item.filePath);
552
+ const direct = asText2(item.path) ?? asText2(item.file_path) ?? asText2(item.filePath);
49
553
  if (direct) files.push(direct);
50
554
  return [...new Set(files)];
51
555
  }
@@ -59,28 +563,28 @@ function isDirectExecItem(item) {
59
563
  return item.type === "mcp_tool_call" && item.server === "visual_remote_exec" && item.tool === "run_readonly";
60
564
  }
61
565
  function directExecSummary(item) {
62
- const arguments_ = asRecord(item.arguments);
566
+ const arguments_ = asRecord2(item.arguments);
63
567
  const commands = Array.isArray(arguments_?.commands) ? arguments_.commands : [];
64
568
  const summaries = commands.flatMap((candidate) => {
65
- const command = asRecord(candidate);
569
+ const command = asRecord2(candidate);
66
570
  const argv = stringArray(command?.argv);
67
571
  return argv === void 0 ? [] : [formatArgv(argv)];
68
572
  });
69
573
  return summaries.length === 0 ? void 0 : summaries.join(" \xB7 ");
70
574
  }
71
575
  function directExecResults(item, defaultCwd) {
72
- const result = asRecord(item.result);
73
- const structured = asRecord(result?.structured_content ?? result?.structuredContent);
576
+ const result = asRecord2(item.result);
577
+ const structured = asRecord2(result?.structured_content ?? result?.structuredContent);
74
578
  const results = Array.isArray(structured?.results) ? structured.results : [];
75
579
  return results.flatMap((candidate) => {
76
- const command = asRecord(candidate);
580
+ const command = asRecord2(candidate);
77
581
  const argv = stringArray(command?.argv);
78
582
  if (argv === void 0) return [];
79
583
  const exitCode = typeof command?.exitCode === "number" ? command.exitCode : void 0;
80
584
  const durationMs = typeof command?.durationMs === "number" ? command.durationMs : void 0;
81
585
  return [{
82
586
  command: formatArgv(argv),
83
- cwd: asText(command?.cwd) ?? defaultCwd,
587
+ cwd: asText2(command?.cwd) ?? defaultCwd,
84
588
  ok: exitCode === 0,
85
589
  ...exitCode === void 0 ? {} : { exitCode },
86
590
  ...durationMs === void 0 ? {} : { durationMs },
@@ -91,8 +595,8 @@ function directExecResults(item, defaultCwd) {
91
595
  });
92
596
  }
93
597
  function normalizedUsage(record) {
94
- const result = asRecord(record.result);
95
- const usage = asRecord(record.usage) ?? (result ? asRecord(result.usage) : void 0);
598
+ const result = asRecord2(record.result);
599
+ const usage = asRecord2(record.usage) ?? (result ? asRecord2(result.usage) : void 0);
96
600
  if (usage === void 0) return void 0;
97
601
  const inputTokens = usage.input_tokens ?? usage.inputTokens;
98
602
  const outputTokens = usage.output_tokens ?? usage.outputTokens;
@@ -116,17 +620,17 @@ function parseCodexJsonLine(line, defaultCwd = "") {
116
620
  } catch {
117
621
  return [{ type: "warning", text: trimmed }];
118
622
  }
119
- const record = asRecord(value);
623
+ const record = asRecord2(value);
120
624
  if (!record) return [{ type: "message", text: trimmed }];
121
- const type = asText(record.type) ?? "unknown";
625
+ const type = asText2(record.type) ?? "unknown";
122
626
  const events = [];
123
627
  const foundSession = sessionId(record);
124
628
  if (foundSession) events.push({ type: "session", sessionId: foundSession });
125
629
  if (type === "thread.started" || type === "thread.created") return events;
126
630
  if (type === "turn.started") return [...events, { type: "phase", name: "turn.started" }];
127
631
  if (type === "turn.completed") {
128
- const result = asRecord(record.result);
129
- const summary = asText(record.summary) ?? (result ? asText(result.summary) : void 0);
632
+ const result = asRecord2(record.result);
633
+ const summary = asText2(record.summary) ?? (result ? asText2(result.summary) : void 0);
130
634
  const usage = normalizedUsage(record);
131
635
  return [
132
636
  ...events,
@@ -135,31 +639,31 @@ function parseCodexJsonLine(line, defaultCwd = "") {
135
639
  ];
136
640
  }
137
641
  if (type === "turn.failed" || type === "error") {
138
- const error = asRecord(record.error);
139
- const text = asText(record.message) ?? (error ? asText(error.message) : void 0) ?? "Codex reported an error";
642
+ const error = asRecord2(record.error);
643
+ const text = asText2(record.message) ?? (error ? asText2(error.message) : void 0) ?? "Codex reported an error";
140
644
  return [...events, { type: "error", text }];
141
645
  }
142
- const item = asRecord(record.item);
646
+ const item = asRecord2(record.item);
143
647
  if (type === "item.started" && item) {
144
- const itemType = asText(item.type) ?? "item";
648
+ const itemType = asText2(item.type) ?? "item";
145
649
  const directExec = isDirectExecItem(item);
146
- const summary = directExec ? directExecSummary(item) : asText(item.command) ?? asText(item.text);
650
+ const summary = directExec ? directExecSummary(item) : asText2(item.command) ?? asText2(item.text);
147
651
  const start = summary ? { type: "tool_start", name: directExec ? "direct_exec" : itemType, summary } : { type: "tool_start", name: directExec ? "direct_exec" : itemType };
148
652
  return [...events, start];
149
653
  }
150
654
  if (type === "item.completed" && item) {
151
- const itemType = asText(item.type) ?? "item";
655
+ const itemType = asText2(item.type) ?? "item";
152
656
  if (itemType === "agent_message") {
153
- const text = asText(item.text) ?? asText(item.message);
657
+ const text = asText2(item.text) ?? asText2(item.message);
154
658
  return text ? [...events, { type: "message", text }] : events;
155
659
  }
156
660
  if (itemType === "command_execution") {
157
- const command = asText(item.command);
661
+ const command = asText2(item.command);
158
662
  if (command) {
159
663
  events.push({
160
664
  type: "command",
161
665
  command,
162
- cwd: asText(item.cwd) ?? defaultCwd
666
+ cwd: asText2(item.cwd) ?? defaultCwd
163
667
  });
164
668
  }
165
669
  }
@@ -185,151 +689,18 @@ function parseCodexJsonLine(line, defaultCwd = "") {
185
689
  });
186
690
  return events;
187
691
  }
188
- const message = asText(record.message);
692
+ const message = asText2(record.message);
189
693
  if (message) events.push({ type: "message", text: message });
190
694
  return events;
191
695
  }
192
696
 
193
697
  // ../../packages/bridge-core/src/agents/codex-adapter.ts
194
- import { execFile, spawn } from "node:child_process";
698
+ import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
195
699
  import { existsSync } from "node:fs";
196
700
  import { fileURLToPath } from "node:url";
197
- import { promisify } from "node:util";
198
-
199
- // ../../packages/bridge-core/src/agents/async-queue.ts
200
- var AsyncQueue = class {
201
- #values = [];
202
- #waiters = [];
203
- #ended = false;
204
- #error;
205
- push(value) {
206
- if (this.#ended) return;
207
- const waiter = this.#waiters.shift();
208
- if (waiter) waiter.resolve({ value, done: false });
209
- else this.#values.push(value);
210
- }
211
- end(error) {
212
- if (this.#ended) return;
213
- this.#ended = true;
214
- this.#error = error;
215
- for (const waiter of this.#waiters.splice(0)) {
216
- if (error !== void 0) waiter.reject(error);
217
- else waiter.resolve({ value: void 0, done: true });
218
- }
219
- }
220
- [Symbol.asyncIterator]() {
221
- return {
222
- next: async () => {
223
- const value = this.#values.shift();
224
- if (value !== void 0) return { value, done: false };
225
- if (this.#ended) {
226
- if (this.#error !== void 0) throw this.#error;
227
- return { value: void 0, done: true };
228
- }
229
- return await new Promise((resolve8, reject) => {
230
- this.#waiters.push({ resolve: resolve8, reject });
231
- });
232
- }
233
- };
234
- }
235
- };
236
-
237
- // ../../packages/bridge-core/src/runtime/managed-process.ts
238
- function safeChildProcessId(child) {
239
- const pid = child.pid;
240
- if (pid === void 0 || !Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid) {
241
- return void 0;
242
- }
243
- return pid;
244
- }
245
- function safeDetachedProcessGroupId(child) {
246
- return process.platform === "win32" ? void 0 : safeChildProcessId(child);
247
- }
248
- function isMissingProcess(error) {
249
- return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
250
- }
251
- function processGroupIsAlive(processGroupId) {
252
- try {
253
- process.kill(-processGroupId, 0);
254
- return true;
255
- } catch (error) {
256
- if (isMissingProcess(error)) return false;
257
- if (typeof error === "object" && error !== null && "code" in error && error.code === "EPERM") {
258
- return true;
259
- }
260
- throw error;
261
- }
262
- }
263
- function childIsAlive(child) {
264
- return child.exitCode === null && child.signalCode === null;
265
- }
266
- function signalChildProcessTree(child, signal) {
267
- const pid = safeChildProcessId(child);
268
- if (pid === void 0) return false;
269
- try {
270
- if (process.platform === "win32") {
271
- if (!childIsAlive(child)) return false;
272
- return child.kill(signal);
273
- }
274
- process.kill(-pid, signal);
275
- return true;
276
- } catch (error) {
277
- if (isMissingProcess(error)) return false;
278
- throw error;
279
- }
280
- }
281
- function installEmergencyChildExitHook(child, processLike = process) {
282
- let removed = false;
283
- const emergencyExit = () => {
284
- removed = true;
285
- try {
286
- signalChildProcessTree(child, "SIGKILL");
287
- } catch {
288
- }
289
- };
290
- processLike.once("exit", emergencyExit);
291
- return () => {
292
- if (removed) return;
293
- removed = true;
294
- processLike.off("exit", emergencyExit);
295
- };
296
- }
297
- async function waitForProcessTreeExit(child, processGroupId, timeoutMs) {
298
- const deadline = Date.now() + timeoutMs;
299
- while (processGroupId === void 0 ? childIsAlive(child) : processGroupIsAlive(processGroupId)) {
300
- const remaining = deadline - Date.now();
301
- if (remaining <= 0) return false;
302
- await new Promise((resolve8) => {
303
- setTimeout(resolve8, Math.min(25, remaining));
304
- });
305
- }
306
- return true;
307
- }
308
- async function terminateChildProcessTree(child, killGraceMs = 3e3) {
309
- const processGroupId = safeDetachedProcessGroupId(child);
310
- if (processGroupId === void 0 && !childIsAlive(child)) return;
311
- const sendSignal = (signal) => {
312
- signalChildProcessTree(child, signal);
313
- };
314
- sendSignal("SIGTERM");
315
- if (await waitForProcessTreeExit(
316
- child,
317
- processGroupId,
318
- Math.max(0, killGraceMs)
319
- )) {
320
- return;
321
- }
322
- sendSignal("SIGKILL");
323
- await waitForProcessTreeExit(
324
- child,
325
- processGroupId,
326
- Math.min(Math.max(0, killGraceMs), 1e3)
327
- );
328
- }
329
-
330
- // ../../packages/bridge-core/src/agents/codex-adapter.ts
331
- var execFileAsync = promisify(execFile);
332
- var INHERITED_ENVIRONMENT = [
701
+ import { promisify as promisify2 } from "node:util";
702
+ var execFileAsync2 = promisify2(execFile2);
703
+ var INHERITED_ENVIRONMENT2 = [
333
704
  "PATH",
334
705
  "HOME",
335
706
  "USER",
@@ -354,15 +725,15 @@ var INHERITED_ENVIRONMENT = [
354
725
  "COMSPEC",
355
726
  "PATHEXT"
356
727
  ];
357
- function processEnv(overrides) {
728
+ function processEnv2(overrides) {
358
729
  const environment = {};
359
- for (const key of INHERITED_ENVIRONMENT) {
730
+ for (const key of INHERITED_ENVIRONMENT2) {
360
731
  const value = process.env[key];
361
732
  if (value !== void 0) environment[key] = value;
362
733
  }
363
734
  return { ...environment, ...overrides };
364
735
  }
365
- function splitLines(chunk, previous, onLine) {
736
+ function splitLines2(chunk, previous, onLine) {
366
737
  const combined = previous + chunk.toString();
367
738
  const lines = combined.split(/\r?\n/);
368
739
  const remainder = lines.pop() ?? "";
@@ -382,18 +753,24 @@ var CodexAdapter = class {
382
753
  id = "codex";
383
754
  #executable;
384
755
  #killGraceMs;
756
+ #model;
757
+ #profile;
758
+ #reasoningEffort;
385
759
  #rtkExecutable;
386
760
  #directExecMcpScript;
387
761
  #rtkVersion;
388
762
  constructor(options = {}) {
389
763
  this.#executable = options.executable ?? "codex";
390
764
  this.#killGraceMs = options.killGraceMs ?? 2e3;
765
+ this.#model = options.model;
766
+ this.#profile = options.profile;
767
+ this.#reasoningEffort = options.reasoningEffort;
391
768
  this.#rtkExecutable = options.rtkExecutable ?? "rtk";
392
769
  this.#directExecMcpScript = options.directExecMcpScript === false ? void 0 : options.directExecMcpScript ?? defaultDirectExecMcpScript();
393
770
  }
394
771
  #probeRtk(environment) {
395
772
  if (this.#rtkExecutable === false) return Promise.resolve(void 0);
396
- this.#rtkVersion ??= execFileAsync(this.#rtkExecutable, ["--version"], {
773
+ this.#rtkVersion ??= execFileAsync2(this.#rtkExecutable, ["--version"], {
397
774
  encoding: "utf8",
398
775
  env: environment,
399
776
  timeout: 1e3,
@@ -436,9 +813,16 @@ ${guidance}
436
813
  `mcp_servers.visual_remote_exec.args=${JSON.stringify(serverArgs)}`
437
814
  ];
438
815
  }
816
+ #modelConfig() {
817
+ return [
818
+ ...this.#profile === void 0 ? [] : ["--profile", this.#profile],
819
+ ...this.#model === void 0 ? [] : ["--model", this.#model],
820
+ ...this.#reasoningEffort === void 0 ? [] : ["-c", `model_reasoning_effort=${JSON.stringify(this.#reasoningEffort)}`]
821
+ ];
822
+ }
439
823
  async probe() {
440
824
  return await new Promise((resolve8) => {
441
- const child = spawn(this.#executable, ["--version"], {
825
+ const child = spawn2(this.#executable, ["--version"], {
442
826
  stdio: ["ignore", "pipe", "ignore"],
443
827
  shell: false
444
828
  });
@@ -471,6 +855,7 @@ ${guidance}
471
855
  "workspace-write",
472
856
  "-C",
473
857
  input.workspaceRoot,
858
+ ...this.#modelConfig(),
474
859
  ...this.#directExecConfig(input),
475
860
  "-"
476
861
  ];
@@ -489,6 +874,7 @@ ${guidance}
489
874
  "workspace-write",
490
875
  "-C",
491
876
  input.workspaceRoot,
877
+ ...this.#modelConfig(),
492
878
  ...this.#directExecConfig(input),
493
879
  "resume",
494
880
  input.sessionId,
@@ -498,9 +884,9 @@ ${guidance}
498
884
  }
499
885
  async *#execute(input, signal, args) {
500
886
  const queue = new AsyncQueue();
501
- const environment = processEnv(input.environment);
887
+ const environment = processEnv2(input.environment);
502
888
  const prompt = await this.#runtimePrompt(input, environment);
503
- const child = spawn(this.#executable, args, {
889
+ const child = spawn2(this.#executable, args, {
504
890
  cwd: input.workspaceRoot,
505
891
  env: environment,
506
892
  detached: process.platform !== "win32",
@@ -532,12 +918,12 @@ ${guidance}
532
918
  signal.addEventListener("abort", abort, { once: true });
533
919
  if (signal.aborted) abort();
534
920
  child.stdout.on("data", (chunk) => {
535
- stdoutRemainder = splitLines(chunk, stdoutRemainder, (line) => {
921
+ stdoutRemainder = splitLines2(chunk, stdoutRemainder, (line) => {
536
922
  for (const event of parseCodexJsonLine(line, input.workspaceRoot)) queue.push(event);
537
923
  });
538
924
  });
539
925
  child.stderr.on("data", (chunk) => {
540
- stderrRemainder = splitLines(chunk, stderrRemainder, (line) => {
926
+ stderrRemainder = splitLines2(chunk, stderrRemainder, (line) => {
541
927
  if (line.trim()) queue.push({ type: "warning", text: line });
542
928
  });
543
929
  });
@@ -615,6 +1001,7 @@ import { ZodError } from "zod";
615
1001
  import { z } from "zod";
616
1002
  var servicePortSchema = z.number().int().min(10001).max(65535);
617
1003
  var commandSchema = z.array(z.string().min(1)).min(1);
1004
+ var environmentVariableSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "Expected an environment variable name");
618
1005
  var readySchema = z.object({
619
1006
  path: z.string().startsWith("/").default("/"),
620
1007
  timeoutMs: z.number().int().positive().default(6e4)
@@ -643,9 +1030,38 @@ var visualDevConfigSchema = z.object({
643
1030
  }).strict(),
644
1031
  agent: z.object({
645
1032
  adapter: z.enum(["codex", "claude", "opencode"]),
1033
+ model: z.string().trim().min(1).optional(),
1034
+ reasoningEffort: z.enum(["minimal", "low", "medium", "high", "xhigh", "max"]).optional(),
1035
+ profile: z.string().trim().regex(
1036
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/,
1037
+ "Expected a Codex profile name"
1038
+ ).optional(),
1039
+ inheritEnv: z.array(environmentVariableSchema).default([]),
646
1040
  maxRunMs: z.number().int().positive(),
647
1041
  resumeMode: z.enum(["auto", "new"]).default("auto")
648
- }).strict(),
1042
+ }).strict().superRefine((agent, context) => {
1043
+ if (agent.adapter === "claude" && agent.reasoningEffort === "minimal") {
1044
+ context.addIssue({
1045
+ code: "custom",
1046
+ path: ["reasoningEffort"],
1047
+ message: "minimal reasoning effort is only supported by the Codex adapter"
1048
+ });
1049
+ }
1050
+ if (agent.adapter === "codex" && agent.reasoningEffort === "max") {
1051
+ context.addIssue({
1052
+ code: "custom",
1053
+ path: ["reasoningEffort"],
1054
+ message: "max reasoning effort is only supported by the Claude adapter"
1055
+ });
1056
+ }
1057
+ if (agent.adapter !== "codex" && agent.profile !== void 0) {
1058
+ context.addIssue({
1059
+ code: "custom",
1060
+ path: ["profile"],
1061
+ message: "agent.profile is only supported by the Codex adapter"
1062
+ });
1063
+ }
1064
+ }),
649
1065
  queue: z.object({
650
1066
  maxPending: z.number().int().positive()
651
1067
  }).strict(),
@@ -689,6 +1105,7 @@ function createDefaultConfig(projectId) {
689
1105
  },
690
1106
  agent: {
691
1107
  adapter: "codex",
1108
+ inheritEnv: [],
692
1109
  maxRunMs: 9e5,
693
1110
  resumeMode: "auto"
694
1111
  },
@@ -761,17 +1178,17 @@ function mergeConfigValues(base, override) {
761
1178
  }
762
1179
  return merged;
763
1180
  }
764
- async function readYamlMapping(filePath, required) {
1181
+ async function readYamlMapping(filePath2, required) {
765
1182
  let source;
766
1183
  try {
767
- source = await readFile(filePath, "utf8");
1184
+ source = await readFile(filePath2, "utf8");
768
1185
  } catch (error) {
769
1186
  if (!required && typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
770
1187
  return void 0;
771
1188
  }
772
- throw new VisualDevConfigError(`Unable to read config: ${filePath}`, {
1189
+ throw new VisualDevConfigError(`Unable to read config: ${filePath2}`, {
773
1190
  cause: error,
774
- filePath
1191
+ filePath: filePath2
775
1192
  });
776
1193
  }
777
1194
  try {
@@ -784,9 +1201,9 @@ async function readYamlMapping(filePath, required) {
784
1201
  }
785
1202
  return value;
786
1203
  } catch (error) {
787
- throw new VisualDevConfigError(`Invalid YAML in ${filePath}`, {
1204
+ throw new VisualDevConfigError(`Invalid YAML in ${filePath2}`, {
788
1205
  cause: error,
789
- filePath
1206
+ filePath: filePath2
790
1207
  });
791
1208
  }
792
1209
  }
@@ -925,10 +1342,10 @@ var RevertConflictError = class extends RepositorySafetyError {
925
1342
  };
926
1343
 
927
1344
  // ../../packages/bridge-core/src/git/git-command.ts
928
- import { spawn as spawn2 } from "node:child_process";
1345
+ import { spawn as spawn3 } from "node:child_process";
929
1346
  async function runGit(args, options) {
930
1347
  return await new Promise((resolve8, reject) => {
931
- const child = spawn2("git", [...args], {
1348
+ const child = spawn3("git", [...args], {
932
1349
  cwd: options.cwd,
933
1350
  env: { ...process.env, ...options.env ?? {} },
934
1351
  shell: false,
@@ -1997,7 +2414,7 @@ import { relative as relative5, resolve as resolve6, sep as sep5 } from "node:pa
1997
2414
  // ../../packages/bridge-core/src/source/path-normalizer.ts
1998
2415
  import { access, readFile as readFile3, realpath as realpath6 } from "node:fs/promises";
1999
2416
  import { isAbsolute as isAbsolute3, relative as relative4, resolve as resolve5, sep as sep4 } from "node:path";
2000
- import { spawn as spawn3 } from "node:child_process";
2417
+ import { spawn as spawn4 } from "node:child_process";
2001
2418
  function toPosix(value) {
2002
2419
  return value.split(sep4).join("/").replaceAll("\\", "/");
2003
2420
  }
@@ -2032,7 +2449,7 @@ function isWithin2(root, candidate) {
2032
2449
  }
2033
2450
  async function gitFiles(repoRoot) {
2034
2451
  return await new Promise((resolvePromise, reject) => {
2035
- const child = spawn3(
2452
+ const child = spawn4(
2036
2453
  "git",
2037
2454
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
2038
2455
  {
@@ -2102,8 +2519,8 @@ async function normalizeSourceLocation(input, repoRootInput) {
2102
2519
  candidates: matches.slice(0, 20)
2103
2520
  };
2104
2521
  }
2105
- const filePath = matches[0];
2106
- const absolutePath = resolve5(repoRoot, filePath);
2522
+ const filePath2 = matches[0];
2523
+ const absolutePath = resolve5(repoRoot, filePath2);
2107
2524
  const canonical = await realpath6(absolutePath);
2108
2525
  if (!isWithin2(repoRoot, canonical)) {
2109
2526
  return { input, confidence: "unknown", candidates: [] };
@@ -2111,7 +2528,7 @@ async function normalizeSourceLocation(input, repoRootInput) {
2111
2528
  const lineNumber = await boundedLine(canonical, input.lineNumber);
2112
2529
  return {
2113
2530
  input,
2114
- filePath,
2531
+ filePath: filePath2,
2115
2532
  absolutePath: canonical,
2116
2533
  ...lineNumber === void 0 ? {} : { lineNumber },
2117
2534
  ...input.columnNumber === void 0 ? {} : { columnNumber: input.columnNumber },
@@ -2616,6 +3033,7 @@ var TaskService = class {
2616
3033
  return publicTask(accepted);
2617
3034
  }
2618
3035
  async revert(id) {
3036
+ if (this.#closed) throw new TaskServiceError("SERVICE_CLOSED", "Task service is closed");
2619
3037
  if (this.#activeTaskId || this.#recovering || this.#recoveryQueue.length > 0) {
2620
3038
  throw new TaskServiceError(
2621
3039
  "WRITER_BUSY",
@@ -2631,12 +3049,19 @@ var TaskService = class {
2631
3049
  if (!latest || latest.id !== id) {
2632
3050
  throw new TaskServiceError("NOT_LATEST_TASK", "Only the latest completed task can be reverted");
2633
3051
  }
2634
- await this.#git.revert(id, task.beforeRef, task.afterRef);
2635
- const reverted = this.#transition(id, "reverted", {
2636
- completedAt: this.#now().toISOString()
2637
- });
2638
- this.#emit("task.reverted", { task: publicTask(reverted) }, id);
2639
- return publicTask(reverted);
3052
+ this.#activeTaskId = id;
3053
+ try {
3054
+ await this.#git.revert(id, task.beforeRef, task.afterRef);
3055
+ const reverted = this.#transition(id, "reverted", {
3056
+ completedAt: this.#now().toISOString()
3057
+ });
3058
+ this.#emit("task.reverted", { task: publicTask(reverted) }, id);
3059
+ return publicTask(reverted);
3060
+ } finally {
3061
+ this.#activeTaskId = void 0;
3062
+ if (!this.#closed) void this.#drain();
3063
+ this.#resolveIdleIfNeeded();
3064
+ }
2640
3065
  }
2641
3066
  async waitForIdle() {
2642
3067
  if (!this.#activeTaskId && this.#queue.length === 0 && this.#recoveryQueue.length === 0 && !this.#recovering && !this.#draining) {
@@ -2647,7 +3072,7 @@ var TaskService = class {
2647
3072
  async close() {
2648
3073
  if (this.#closed) return;
2649
3074
  this.#closed = true;
2650
- if (this.#activeTaskId && !this.#recovering) {
3075
+ if (this.#activeTaskId && this.#activeAbort) {
2651
3076
  this.#cancelRequested.add(this.#activeTaskId);
2652
3077
  this.#activeAbort?.abort(new AgentCanceledError("Task service is closing"));
2653
3078
  }
@@ -2712,9 +3137,12 @@ var TaskService = class {
2712
3137
  if (!task.beforeRef) {
2713
3138
  throw new Error("Interrupted task is missing its before snapshot");
2714
3139
  }
2715
- const after = await this.#git.createSnapshot(taskId, "after");
2716
- this.#store.updateTask(taskId, { afterRef: after.ref });
2717
- const diff = await this.#git.diff(task.beforeRef, after.ref);
3140
+ let afterRef = task.afterRef;
3141
+ if (!afterRef) {
3142
+ afterRef = (await this.#git.createSnapshot(taskId, "after")).ref;
3143
+ this.#store.updateTask(taskId, { afterRef });
3144
+ }
3145
+ const diff = await this.#git.diff(task.beforeRef, afterRef);
2718
3146
  this.#store.updateTask(taskId, {
2719
3147
  diffText: diff.text,
2720
3148
  changedFiles: diff.files
@@ -2786,7 +3214,7 @@ var TaskService = class {
2786
3214
  }
2787
3215
  }
2788
3216
  async #drain() {
2789
- if (this.#draining || this.#closed || this.#recovering || this.#recoveryQueue.length > 0) {
3217
+ if (this.#draining || this.#activeTaskId || this.#closed || this.#recovering || this.#recoveryQueue.length > 0) {
2790
3218
  return;
2791
3219
  }
2792
3220
  this.#draining = true;
@@ -3039,11 +3467,11 @@ var TaskService = class {
3039
3467
  for (const target of result.selection.targets) {
3040
3468
  const sanitize = async (location) => {
3041
3469
  try {
3042
- const filePath = await this.#git.pathPolicy.assertFilesystemPathAllowed(
3470
+ const filePath2 = await this.#git.pathPolicy.assertFilesystemPathAllowed(
3043
3471
  location.filePath,
3044
3472
  false
3045
3473
  );
3046
- return { ...location, filePath };
3474
+ return { ...location, filePath: filePath2 };
3047
3475
  } catch {
3048
3476
  return void 0;
3049
3477
  }
@@ -3255,7 +3683,7 @@ var BrowserSessionManager = class {
3255
3683
  };
3256
3684
 
3257
3685
  // ../../packages/bridge-core/src/verification/commands.ts
3258
- import { spawn as spawn4 } from "node:child_process";
3686
+ import { spawn as spawn5 } from "node:child_process";
3259
3687
  var MAX_OUTPUT_CHARS = 8e3;
3260
3688
  var KILL_GRACE_MS = 250;
3261
3689
  function appendOutput(current, chunk) {
@@ -3269,7 +3697,7 @@ async function runVerificationCommand(configured, cwd, signal) {
3269
3697
  }
3270
3698
  const startedAt = Date.now();
3271
3699
  return await new Promise((resolveResult, rejectResult) => {
3272
- const child = spawn4(executable, arguments_, {
3700
+ const child = spawn5(executable, arguments_, {
3273
3701
  cwd,
3274
3702
  detached: process.platform !== "win32",
3275
3703
  shell: false,
@@ -3844,15 +4272,40 @@ ${output}` : ""}`
3844
4272
  }
3845
4273
 
3846
4274
  // ../../packages/bridge-core/src/bridge/default-control-service.ts
4275
+ function createAgentAdapter(agent) {
4276
+ if (agent.adapter === "claude") {
4277
+ if (agent.reasoningEffort === "minimal") {
4278
+ throw new Error("Claude does not support minimal reasoning effort");
4279
+ }
4280
+ return new ClaudeAdapter({
4281
+ ...agent.model === void 0 ? {} : { model: agent.model },
4282
+ ...agent.reasoningEffort === void 0 ? {} : { reasoningEffort: agent.reasoningEffort }
4283
+ });
4284
+ }
4285
+ if (agent.adapter === "codex") {
4286
+ if (agent.reasoningEffort === "max") {
4287
+ throw new Error("Codex does not support max reasoning effort");
4288
+ }
4289
+ return new CodexAdapter({
4290
+ ...agent.model === void 0 ? {} : { model: agent.model },
4291
+ ...agent.reasoningEffort === void 0 ? {} : { reasoningEffort: agent.reasoningEffort },
4292
+ ...agent.profile === void 0 ? {} : { profile: agent.profile }
4293
+ });
4294
+ }
4295
+ throw new Error(`Agent adapter ${agent.adapter} is not implemented in this build`);
4296
+ }
4297
+ function inheritedAgentEnvironment(names, environment) {
4298
+ return Object.fromEntries(
4299
+ names.flatMap((name) => {
4300
+ const value = environment[name];
4301
+ return value === void 0 ? [] : [[name, value]];
4302
+ })
4303
+ );
4304
+ }
3847
4305
  async function createDefaultControlService(context, environment = process.env) {
3848
4306
  const loaded = await loadVisualDevConfig(context.repoRoot, {
3849
4307
  ...context.configRoot === void 0 ? {} : { configRoot: context.configRoot }
3850
4308
  });
3851
- if (loaded.config.agent.adapter !== "codex") {
3852
- throw new Error(
3853
- `Agent adapter ${loaded.config.agent.adapter} is not implemented in this MVP build`
3854
- );
3855
- }
3856
4309
  const git = await GitTransactionManager.open(context.repoRoot, {
3857
4310
  allowed: rebaseWorkspacePatterns(
3858
4311
  context.repoRoot,
@@ -3871,13 +4324,16 @@ async function createDefaultControlService(context, environment = process.env) {
3871
4324
  projectId: context.projectId,
3872
4325
  workspaceRoot: context.workspaceRoot,
3873
4326
  upstreamUrl: context.upstreamUrl,
3874
- adapter: new CodexAdapter(),
4327
+ adapter: createAgentAdapter(loaded.config.agent),
3875
4328
  store,
3876
4329
  git,
3877
4330
  maxRunMs: loaded.config.agent.maxRunMs,
3878
4331
  maxPending: loaded.config.queue.maxPending,
3879
4332
  resumeMode: loaded.config.agent.resumeMode,
3880
- environment: {}
4333
+ environment: inheritedAgentEnvironment(
4334
+ loaded.config.agent.inheritEnv,
4335
+ environment
4336
+ )
3881
4337
  });
3882
4338
  const controlService = createTaskControlService({
3883
4339
  taskService,
@@ -4208,10 +4664,10 @@ async function acquireWorktreeLock(repositoryRoot, options = {}) {
4208
4664
  }
4209
4665
 
4210
4666
  // ../../packages/bridge-core/src/runtime/repository.ts
4211
- import { execFile as execFile2 } from "node:child_process";
4667
+ import { execFile as execFile3 } from "node:child_process";
4212
4668
  import { realpath as realpath9 } from "node:fs/promises";
4213
- import { promisify as promisify2 } from "node:util";
4214
- var execFileAsync2 = promisify2(execFile2);
4669
+ import { promisify as promisify3 } from "node:util";
4670
+ var execFileAsync3 = promisify3(execFile3);
4215
4671
  var GitWorktreeNotFoundError = class extends Error {
4216
4672
  constructor(cwd, options = {}) {
4217
4673
  super(
@@ -4223,7 +4679,7 @@ var GitWorktreeNotFoundError = class extends Error {
4223
4679
  };
4224
4680
  async function discoverGitWorktreeRoot(cwd = process.cwd()) {
4225
4681
  try {
4226
- const { stdout } = await execFileAsync2(
4682
+ const { stdout } = await execFileAsync3(
4227
4683
  "git",
4228
4684
  ["-C", cwd, "rev-parse", "--show-toplevel"],
4229
4685
  {
@@ -5253,7 +5709,7 @@ async function startBridgeCore(options, dependencies) {
5253
5709
  try {
5254
5710
  lock = options.lock ?? await acquireWorktreeLock(loadedConfig.repoRoot, { environment });
5255
5711
  const host = options.host ?? loadedConfig.config.gateway.host;
5256
- const configuredPublicUrl = options.publicUrl ?? loadedConfig.config.gateway.publicUrl;
5712
+ const configuredPublicUrl = options.publicUrl ?? loadedConfig.config.gateway.publicUrl ?? options.fallbackPublicUrl;
5257
5713
  const publicUrl = configuredPublicUrl === void 0 ? void 0 : normalizePublicUrl(configuredPublicUrl);
5258
5714
  const gatewayPort = await findAvailablePort(startPort(loadedConfig, options.listen), host);
5259
5715
  const token = generatePairingToken();
@@ -5271,6 +5727,13 @@ async function startBridgeCore(options, dependencies) {
5271
5727
  controlService = await resolveControlService(dependencies, controlContext);
5272
5728
  const allowedOrigins = new Set(loadedConfig.config.security.allowedOrigins);
5273
5729
  if (publicUrl !== void 0) allowedOrigins.add(new URL(publicUrl).origin);
5730
+ if (options.fallbackLoopbackOrigins === true && options.publicUrl === void 0 && loadedConfig.config.gateway.publicUrl === void 0 && loadedConfig.config.security.allowedOrigins.length === 0 && publicUrl !== void 0) {
5731
+ const loopbackUrl = new URL(publicUrl);
5732
+ if (loopbackUrl.hostname === "localhost" || loopbackUrl.hostname === "127.0.0.1") {
5733
+ loopbackUrl.hostname = loopbackUrl.hostname === "localhost" ? "127.0.0.1" : "localhost";
5734
+ allowedOrigins.add(loopbackUrl.origin);
5735
+ }
5736
+ }
5274
5737
  gateway = createGatewayServer({
5275
5738
  upstream: options.upstreamUrl,
5276
5739
  pairingToken: token,
@@ -5373,7 +5836,9 @@ async function startAttachBridge(options, dependencies = {}) {
5373
5836
  upstreamUrl: normalizeUpstream(options.upstream),
5374
5837
  ...options.listen === void 0 ? {} : { listen: options.listen },
5375
5838
  ...options.host === void 0 ? {} : { host: options.host },
5376
- ...options.publicUrl === void 0 ? {} : { publicUrl: options.publicUrl }
5839
+ ...options.publicUrl === void 0 ? {} : { publicUrl: options.publicUrl },
5840
+ ...options.fallbackPublicUrl === void 0 ? {} : { fallbackPublicUrl: options.fallbackPublicUrl },
5841
+ ...options.fallbackLoopbackOrigins === void 0 ? {} : { fallbackLoopbackOrigins: options.fallbackLoopbackOrigins }
5377
5842
  },
5378
5843
  dependencies
5379
5844
  );
@@ -5387,6 +5852,49 @@ async function startAttachBridge(options, dependencies = {}) {
5387
5852
  }
5388
5853
 
5389
5854
  // src/vite.ts
5855
+ var bridgesKey = /* @__PURE__ */ Symbol.for("visual-remote.vite.bridges");
5856
+ var bridgeState = globalThis;
5857
+ var bridges = bridgeState[bridgesKey] ??= /* @__PURE__ */ new Map();
5858
+ var restartsKey = /* @__PURE__ */ Symbol.for("visual-remote.vite.restarts");
5859
+ var restartState = globalThis;
5860
+ var restarts = restartState[restartsKey] ??= new AsyncLocalStorage();
5861
+ async function acquireBridge(options, config) {
5862
+ const root = projectRoot(config, options.cwd);
5863
+ let shared = bridges.get(root);
5864
+ if (shared?.closing !== void 0) {
5865
+ await shared.closing;
5866
+ return acquireBridge(options, config);
5867
+ }
5868
+ if (shared === void 0) {
5869
+ shared = { bridge: startOrReuseBridge(options, config), users: 0 };
5870
+ bridges.set(root, shared);
5871
+ }
5872
+ const entry = shared;
5873
+ entry.users += 1;
5874
+ let releasePromise;
5875
+ const release = () => {
5876
+ releasePromise ??= (async () => {
5877
+ entry.users -= 1;
5878
+ if (entry.users !== 0) return;
5879
+ entry.closing = (async () => {
5880
+ try {
5881
+ const bridge = await entry.bridge.catch(() => void 0);
5882
+ await bridge?.ownedBridge?.close();
5883
+ } finally {
5884
+ bridges.delete(root);
5885
+ }
5886
+ })();
5887
+ await entry.closing;
5888
+ })();
5889
+ return releasePromise;
5890
+ };
5891
+ try {
5892
+ return { bridge: await entry.bridge, release };
5893
+ } catch (error) {
5894
+ await release();
5895
+ throw error;
5896
+ }
5897
+ }
5390
5898
  function projectRoot(config, configuredRoot) {
5391
5899
  if (configuredRoot !== void 0) return resolve7(configuredRoot);
5392
5900
  return resolve7(process.cwd(), typeof config.root === "string" ? config.root : ".");
@@ -5424,32 +5932,11 @@ async function startOrReuseBridge(options, config) {
5424
5932
  }
5425
5933
  }
5426
5934
  function visualRemote(options = {}) {
5427
- let bridgePromise;
5428
5935
  let pairingUrlAnnounced = false;
5429
- const closeBridge = async () => {
5430
- if (bridgePromise === void 0) return;
5431
- const bridge = await bridgePromise.catch(() => void 0);
5432
- await bridge?.ownedBridge?.close();
5433
- };
5434
5936
  return {
5435
5937
  name: "visual-remote",
5436
5938
  apply: "serve",
5437
5939
  enforce: "pre",
5438
- async config(config) {
5439
- bridgePromise ??= startOrReuseBridge(options, config);
5440
- const bridge = await bridgePromise;
5441
- bridge.ownedBridge?.gateway.server.unref();
5442
- return {
5443
- server: {
5444
- proxy: {
5445
- "/_visual": {
5446
- target: bridge.gatewayUrl,
5447
- ws: true
5448
- }
5449
- }
5450
- }
5451
- };
5452
- },
5453
5940
  transformIndexHtml: {
5454
5941
  order: "pre",
5455
5942
  handler(html) {
@@ -5465,21 +5952,48 @@ function visualRemote(options = {}) {
5465
5952
  ];
5466
5953
  }
5467
5954
  },
5468
- configureServer(server) {
5469
- if (!pairingUrlAnnounced && bridgePromise !== void 0) {
5470
- pairingUrlAnnounced = true;
5471
- void bridgePromise.then((bridge) => {
5472
- server.config.logger.info(
5473
- bridge.openUrl === void 0 ? `[visual-remote] Reusing Bridge: ${bridge.gatewayUrl}` : `[visual-remote] Pair: ${bridge.openUrl}`
5474
- );
5955
+ async configureServer(server) {
5956
+ const candidate = { config: server.config, close: server.close };
5957
+ restarts.getStore()?.push(candidate);
5958
+ const restart = server.restart;
5959
+ server.restart = (forceOptimize) => {
5960
+ const candidates = [];
5961
+ return restarts.run(candidates, async () => {
5962
+ try {
5963
+ return await restart(forceOptimize);
5964
+ } finally {
5965
+ for (const replacement of candidates) {
5966
+ if (replacement.config !== server.config) await replacement.close();
5967
+ }
5968
+ }
5475
5969
  });
5476
- }
5970
+ };
5971
+ const serverLease = await acquireBridge(options, server.config);
5972
+ const close = server.close;
5973
+ server.close = async () => {
5974
+ try {
5975
+ await close();
5976
+ } finally {
5977
+ await serverLease.release();
5978
+ }
5979
+ };
5980
+ candidate.close = server.close;
5477
5981
  server.httpServer?.once("close", () => {
5478
- void closeBridge();
5982
+ void serverLease.release();
5479
5983
  });
5480
- },
5481
- async closeBundle() {
5482
- await closeBridge();
5984
+ const { bridge } = serverLease;
5985
+ bridge.ownedBridge?.gateway.server.unref();
5986
+ server.config.server.proxy ??= {};
5987
+ server.config.server.proxy["/_visual"] = {
5988
+ target: bridge.gatewayUrl,
5989
+ ws: true
5990
+ };
5991
+ if (!pairingUrlAnnounced) {
5992
+ pairingUrlAnnounced = true;
5993
+ server.config.logger.info(
5994
+ bridge.openUrl === void 0 ? `[visual-remote] Reusing Bridge: ${bridge.gatewayUrl}` : `[visual-remote] Pair: ${bridge.openUrl}`
5995
+ );
5996
+ }
5483
5997
  }
5484
5998
  };
5485
5999
  }