@oxbit/cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import * as fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { spawn, execFileSync } from "node:child_process";
7
7
  import { parseArgs } from "node:util";
8
- import http from "node:http";
8
+ import http, { createServer } from "node:http";
9
9
  import os, { userInfo } from "node:os";
10
10
  import { createHash, randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
11
11
  import { WebSocketServer, WebSocket } from "ws";
@@ -43,6 +43,13 @@ function workspaceDataDir(root, home = os.homedir()) {
43
43
  return path.join(home, ".oxbit", "workspaces", id);
44
44
  }
45
45
  const MAX_MESSAGE_BYTES = 2 * 1024 * 1024;
46
+ const MAX_FILE_BYTES = 20 * 1024 * 1024;
47
+ const MAX_FILE_WRITE_MESSAGE_BYTES = MAX_FILE_BYTES * 6 + MAX_MESSAGE_BYTES;
48
+ const MAX_COLLABORATION_UPDATE_BYTES = Math.ceil(MAX_FILE_BYTES / 3) * 4 + MAX_MESSAGE_BYTES;
49
+ const requestMessageLimit = (method) => method === "fs.write" || method === "collab.save" ? MAX_FILE_WRITE_MESSAGE_BYTES : method === "collab.update" ? MAX_COLLABORATION_UPDATE_BYTES + MAX_MESSAGE_BYTES : MAX_MESSAGE_BYTES;
50
+ function operationEpoch(id) {
51
+ return /^op:([0-9a-f-]{36}):[0-9a-f-]{36}$/.exec(id)?.[1];
52
+ }
46
53
  const MAX_BUFFER_BYTES = 1024 * 1024;
47
54
  const READ_CHUNK_BYTES = 512 * 1024;
48
55
  const RUNTIME_SERVICE_TYPE = "_oxbit._tcp";
@@ -57,8 +64,9 @@ class RpcError extends Error {
57
64
  data;
58
65
  }
59
66
  function parseClientMessage(raw) {
60
- if (new TextEncoder().encode(raw).byteLength > MAX_MESSAGE_BYTES)
61
- throw new RpcError("TOO_LARGE", "Message exceeds 2 MiB");
67
+ const bytes = new TextEncoder().encode(raw).byteLength;
68
+ if (bytes > MAX_FILE_WRITE_MESSAGE_BYTES)
69
+ throw new RpcError("TOO_LARGE", "Message exceeds the file save limit");
62
70
  let m;
63
71
  try {
64
72
  m = JSON.parse(raw);
@@ -67,6 +75,8 @@ function parseClientMessage(raw) {
67
75
  }
68
76
  if (!m || m.v !== 1 || !["request", "cancel", "ack"].includes(m.type))
69
77
  throw new RpcError("INVALID_MESSAGE", "Invalid protocol message");
78
+ if (bytes > (m.type === "request" ? requestMessageLimit(m.method) : MAX_MESSAGE_BYTES))
79
+ throw new RpcError("TOO_LARGE", "Message exceeds 2 MiB");
70
80
  if (m.type === "ack") {
71
81
  if (typeof m.stream !== "string" || !m.stream || m.stream.length > 256 || !Number.isSafeInteger(m.seq) || m.seq < 0)
72
82
  throw new RpcError("INVALID_MESSAGE", "Invalid acknowledgement");
@@ -224,6 +234,10 @@ class WorkspaceFiles {
224
234
  protected(candidate) {
225
235
  return [...this.protectedRoots].some((root) => candidate === root || candidate.startsWith(root + path.sep));
226
236
  }
237
+ protectDescendants(candidate) {
238
+ if ([...this.protectedRoots].some((root) => root.startsWith(candidate + path.sep)))
239
+ throw new RpcError("PATH_DENIED", "Directory contains protected files");
240
+ }
227
241
  inside(candidate) {
228
242
  const rel = path.relative(this.root, candidate);
229
243
  return rel === "" || !rel.startsWith(`..${path.sep}`) && rel !== ".." && !path.isAbsolute(rel);
@@ -311,7 +325,7 @@ class WorkspaceFiles {
311
325
  const stat = await fs.stat(full);
312
326
  if (!stat.isFile())
313
327
  throw new RpcError("NOT_FILE", "Path is not a regular file");
314
- if (stat.size > 20 * 1024 * 1024)
328
+ if (stat.size > MAX_FILE_BYTES)
315
329
  throw new RpcError(
316
330
  "FILE_TOO_LARGE",
317
331
  "Files above 20 MiB must be opened outside this editor"
@@ -329,7 +343,7 @@ class WorkspaceFiles {
329
343
  const stat = await fs.stat(full);
330
344
  if (!stat.isFile())
331
345
  throw new RpcError("NOT_FILE", "Path is not a regular file");
332
- if (stat.size > 20 * 1024 * 1024)
346
+ if (stat.size > MAX_FILE_BYTES)
333
347
  throw new RpcError(
334
348
  "FILE_TOO_LARGE",
335
349
  "Files above 20 MiB must be opened outside this editor"
@@ -393,7 +407,7 @@ class WorkspaceFiles {
393
407
  actualRevision: actual
394
408
  });
395
409
  const bytes = encode(text2, options2.encoding, options2.eol);
396
- if (bytes.length > 20 * 1024 * 1024)
410
+ if (bytes.length > MAX_FILE_BYTES)
397
411
  throw new RpcError(
398
412
  "FILE_TOO_LARGE",
399
413
  "Files above 20 MiB are unsupported"
@@ -427,6 +441,8 @@ class WorkspaceFiles {
427
441
  const target = await this.resolve(to, true);
428
442
  if (from === this.root || target === this.root)
429
443
  throw new RpcError("PATH_DENIED", "Cannot rename workspace root");
444
+ this.protectDescendants(from);
445
+ this.protectDescendants(target);
430
446
  try {
431
447
  await fs.lstat(target);
432
448
  throw new RpcError("EXISTS", "Destination already exists");
@@ -439,9 +455,103 @@ class WorkspaceFiles {
439
455
  const full = await this.resolve(relative);
440
456
  if (full === this.root)
441
457
  throw new RpcError("PATH_DENIED", "Cannot delete workspace root");
458
+ this.protectDescendants(full);
442
459
  await fs.rm(path.resolve(this.root, relative), { recursive: true });
443
460
  }
444
461
  }
462
+ const OPERATIONS_PER_EPOCH = 128;
463
+ const MAX_RETAINED_OPERATIONS = OPERATIONS_PER_EPOCH * 2;
464
+ const MAX_RUNNING_OPERATIONS = 64;
465
+ const MAX_OPERATION_RESULT_BYTES = 1024 * 1024;
466
+ class OperationHistory {
467
+ records = /* @__PURE__ */ new Map();
468
+ state;
469
+ constructor(state2, records2 = []) {
470
+ this.state = state2 ? { ...state2 } : { epoch: randomUUID(), admitted: 0 };
471
+ for (const record of records2) {
472
+ const operation = { ...record };
473
+ if (operation.status === "running") {
474
+ operation.status = "interrupted";
475
+ operation.error = { code: "INTERRUPTED", message: "Runtime stopped before operation completion was recorded; inspect state before retrying with a new request ID" };
476
+ }
477
+ this.records.set(this.key(operation.owner, operation.id), operation);
478
+ }
479
+ if (!state2 && this.records.size > OPERATIONS_PER_EPOCH || this.records.size > MAX_RETAINED_OPERATIONS + MAX_RUNNING_OPERATIONS) {
480
+ this.state = { epoch: randomUUID(), admitted: 0 };
481
+ while (this.records.size > OPERATIONS_PER_EPOCH) this.records.delete(this.records.keys().next().value);
482
+ } else if (!state2) this.state.admitted = this.records.size;
483
+ this.compactResults();
484
+ }
485
+ get epoch() {
486
+ return this.state.epoch;
487
+ }
488
+ checkpoint() {
489
+ return { ...this.state };
490
+ }
491
+ values() {
492
+ return this.records.values();
493
+ }
494
+ key(owner, id) {
495
+ return `${owner}:${id}`;
496
+ }
497
+ get(owner, id) {
498
+ return this.records.get(this.key(owner, id));
499
+ }
500
+ accepts(id) {
501
+ const epoch = operationEpoch(id);
502
+ return epoch === void 0 ? !id.startsWith("op:") : epoch === this.state.epoch || epoch === this.state.previousEpoch;
503
+ }
504
+ status(owner, id) {
505
+ return this.get(owner, id) ?? (this.accepts(id) ? { id, status: "unknown" } : { id, status: "expired", error: { code: "OPERATION_EXPIRED", message: "Operation history expired; inspect current state before submitting a new operation" } });
506
+ }
507
+ begin(owner, id, method) {
508
+ if (this.get(owner, id)) throw new RpcError("DUPLICATE_ID", "Operation already exists");
509
+ if (!this.accepts(id)) throw new RpcError("OPERATION_EXPIRED", "Operation ID expired; inspect current state before submitting a new operation");
510
+ if ([...this.records.values()].filter((operation2) => operation2.status === "running").length >= MAX_RUNNING_OPERATIONS)
511
+ throw new RpcError("BUSY", "Maximum 64 concurrent operations");
512
+ const operation = { id, owner, method, status: "running", startedAt: Date.now() };
513
+ this.records.set(this.key(owner, id), operation);
514
+ if (++this.state.admitted >= OPERATIONS_PER_EPOCH) {
515
+ this.state = { epoch: randomUUID(), previousEpoch: this.state.epoch, admitted: 0 };
516
+ let retained = 0;
517
+ for (const [key, record] of [...this.records].reverse())
518
+ if (record.status !== "running" && (!this.accepts(record.id) || ++retained > OPERATIONS_PER_EPOCH)) this.records.delete(key);
519
+ }
520
+ return operation;
521
+ }
522
+ complete(operation, result) {
523
+ operation.status = "completed";
524
+ operation.result = result;
525
+ this.compactResults();
526
+ }
527
+ fail(operation, error) {
528
+ operation.status = "failed";
529
+ operation.error = error;
530
+ delete operation.result;
531
+ this.compactResults();
532
+ }
533
+ compactResults() {
534
+ let remaining = MAX_OPERATION_RESULT_BYTES;
535
+ for (const operation of [...this.records.values()].reverse()) {
536
+ if (operation.error) {
537
+ operation.error.code = operation.error.code.slice(0, 128);
538
+ if (operation.error.message.length > 4096) {
539
+ operation.error.message = operation.error.message.slice(0, 4096);
540
+ operation.resultExpired = true;
541
+ }
542
+ }
543
+ const result = operation.result;
544
+ const largeText = (operation.method === "fs.write" || operation.method === "collab.save") && typeof result?.text === "string" && result.text.length > remaining;
545
+ const body = largeText ? void 0 : JSON.stringify({ result: operation.result, data: operation.error?.data });
546
+ const bytes = body === void 0 ? remaining + 1 : body === "{}" ? 0 : Buffer.byteLength(body);
547
+ if (bytes > remaining) {
548
+ delete operation.result;
549
+ if (operation.error) delete operation.error.data;
550
+ operation.resultExpired = true;
551
+ } else remaining -= bytes;
552
+ }
553
+ }
554
+ }
445
555
  function killProcess(child) {
446
556
  if (child.exitCode !== null) return;
447
557
  try {
@@ -776,7 +886,7 @@ function validateJson(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
776
886
  }
777
887
  seen.delete(value);
778
888
  }
779
- function object$2(value) {
889
+ function object$3(value) {
780
890
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a JSON object");
781
891
  }
782
892
  function strings$1(value) {
@@ -784,17 +894,17 @@ function strings$1(value) {
784
894
  }
785
895
  function validateFileAssociations(value) {
786
896
  validateJson(value);
787
- object$2(value);
897
+ object$3(value);
788
898
  for (const [pattern, id] of Object.entries(value))
789
899
  if (!pattern || pattern.length > 1024 || typeof id !== "string" || !/^[\w+-]+$/.test(id)) throw new Error("File associations map glob patterns to language IDs");
790
900
  }
791
901
  function validateLanguageServers(value) {
792
902
  validateJson(value);
793
- object$2(value);
903
+ object$3(value);
794
904
  const keys = ["enabled", "selectors", "rootMarkers", "executable", "args", "env", "initializationOptions", "settings", "priority"];
795
905
  for (const [id, config] of Object.entries(value)) {
796
906
  if (!/^[a-zA-Z0-9._-]+$/.test(id)) throw new Error("Invalid language server ID");
797
- object$2(config);
907
+ object$3(config);
798
908
  for (const key of Object.keys(config)) if (!keys.includes(key)) throw new Error(`Unknown language server option: ${key}`);
799
909
  if (config.enabled !== void 0 && typeof config.enabled !== "boolean") throw new Error("enabled must be boolean");
800
910
  if (config.priority !== void 0 && (typeof config.priority !== "number" || !Number.isFinite(config.priority))) throw new Error("priority must be finite");
@@ -807,11 +917,11 @@ function validateLanguageServers(value) {
807
917
  if (config.selectors !== void 0) {
808
918
  if (!Array.isArray(config.selectors)) throw new Error("selectors must be an array");
809
919
  for (const selector of config.selectors) {
810
- object$2(selector);
920
+ object$3(selector);
811
921
  if (Object.keys(selector).some((key) => !["language", "pattern", "scheme"].includes(key)) || !Object.keys(selector).length || Object.values(selector).some((item) => typeof item !== "string") || selector.scheme !== void 0 && selector.scheme !== "file") throw new Error("Invalid document selector");
812
922
  }
813
923
  }
814
- for (const key of ["env", "settings", "initializationOptions"]) if (config[key] !== void 0) object$2(config[key]);
924
+ for (const key of ["env", "settings", "initializationOptions"]) if (config[key] !== void 0) object$3(config[key]);
815
925
  if (config.env && Object.entries(config.env).some(([key, item]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof item !== "string" || item.includes("\0"))) throw new Error("Invalid environment override");
816
926
  const credentials = (item) => Boolean(item && typeof item === "object" && Object.entries(item).some(([key, child]) => /(?:^|[._-])licen[cs]e(?:[._-]?key)?$/i.test(key) || credentials(child)));
817
927
  if (credentials(config.initializationOptions) || credentials(config.settings) || credentials(config.env)) throw new Error("Store license credentials in the runtime-local language server credential file");
@@ -822,7 +932,7 @@ const $id$1 = "https://oxbit.dev/schemas/settings.v1.schema.json";
822
932
  const title$2 = "Oxbit Settings";
823
933
  const description$2 = "User, private workspace and repository settings. Literal dotted IDs; objects merge recursively, arrays and scalar values replace. Unrecognized extension settings are preserved. Null is a value, not a reset, and must be valid for its setting.";
824
934
  const type$2 = "object";
825
- const properties$2 = /* @__PURE__ */ JSON.parse(`{"$schema":{"type":"string","description":"JSON Schema URI. Ignored when merging effective preferences."},"agentACP.provider":{"title":"Default agent","type":"string","default":"codex","enum":["codex","cursor","amp"]},"agentACP.codex.command":{"title":"Codex ACP executable","type":"string","default":"npx"},"agentACP.codex.args":{"title":"Codex ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/codex-acp@1.10.0\\"]"},"agentACP.cursor.command":{"title":"Cursor ACP executable","type":"string","default":"agent"},"agentACP.cursor.args":{"title":"Cursor ACP arguments (JSON array)","type":"string","default":"[\\"acp\\"]"},"agentACP.amp.command":{"title":"Amp Agent ACP executable","type":"string","default":"npx"},"agentACP.amp.args":{"title":"Amp Agent ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"amp-acp@0.9.0\\"]"},"workbench.keymap":{"title":"Keymap","description":"Keyboard layout applied on top of the Oxbit defaults. User keybindings always win.","type":"string","default":"default","enum":["default","vscode","jetbrains","macos","sublime","atom","visual-studio","emacs"]},"runtime.keepAlive":{"title":"Keep Runtime Alive","description":"How long a runtime Oxbit started keeps running after the last client disconnects or its connection heartbeats stop. Terminals and tasks continue while it runs.","type":"string","default":"75s","enum":["75s","15m","1h","8h","untilStopped"]},"runtime.autoReconnect":{"title":"Reconnect Automatically","description":"Reconnect to the runtime after the connection drops, including when the iOS app returns to the foreground.","type":"boolean","default":true},"workbench.tooltipDelay":{"title":"Tooltip Delay","description":"Delay in milliseconds before showing a tooltip on hover. Keyboard focus shows tooltips immediately.","type":"number","default":400,"minimum":0,"maximum":5000},"workbench.colorTheme":{"title":"Color Theme","description":"Specifies the color theme used in the workbench.","type":"string","default":"Graphite (dark)"},"workbench.density":{"title":"Layout Density","description":"Controls row heights, padding and label sizes across the workbench.","type":"string","default":"compact","enum":["compact","comfortable"]},"workbench.sidebarLocation":{"title":"Sidebar Location","description":"Where the primary sidebar is docked.","type":"string","default":"left","enum":["left","right"]},"editor.fontFamily":{"title":"Font Family","description":"Controls the font family of the editor.","type":"string","default":"'JetBrains Mono', ui-monospace, monospace"},"editor.fontSize":{"title":"Font Size","description":"Font size in pixels.","type":"number","default":13,"minimum":8,"maximum":32},"editor.lineHeight":{"title":"Line Height","description":"Line height in pixels. 0 uses 1.55 × font size.","type":"number","default":20,"minimum":0,"maximum":60},"editor.fontLigatures":{"title":"Font Ligatures","description":"Enables font ligatures when the font supports them.","type":"boolean","default":false},"editor.tabSize":{"title":"Tab Size","description":"The number of spaces a tab is equal to.","type":"number","default":2,"minimum":1,"maximum":8},"editor.insertSpaces":{"title":"Insert Spaces","description":"Insert spaces when pressing Tab.","type":"boolean","default":true},"editor.detectIndentation":{"title":"Detect Indentation","description":"Detect Tab Size and Insert Spaces from file contents on open.","type":"boolean","default":true},"editor.renderIndentGuides":{"title":"Indentation Guides","description":"Render vertical guides for each indent level.","type":"boolean","default":true},"editor.wordWrap":{"title":"Word Wrap","description":"Controls how lines should wrap.","type":"string","default":"off","enum":["off","on","bounded"]},"editor.renderWhitespace":{"title":"Render Whitespace","description":"Controls how the editor renders whitespace characters.","type":"string","default":"selection","enum":["none","boundary","selection","all"]},"editor.cursorBlinking":{"title":"Cursor Blinking","description":"Cursor animation style.","type":"string","default":"blink","enum":["blink","smooth","phase","solid"]},"editor.minimap":{"title":"Minimap","description":"Show a minimap of the document.","type":"boolean","default":false},"files.autoSave":{"title":"Auto Save","description":"Controls auto save of editors that have unsaved changes.","type":"string","default":"off","enum":["off","afterDelay","onFocusChange","onWindowChange"]},"files.autoSaveDelay":{"title":"Auto Save Delay","description":"Delay in milliseconds after which an editor is saved automatically. Applies when Auto Save is afterDelay.","type":"number","default":1000,"minimum":100,"maximum":60000},"files.trimTrailingWhitespace":{"title":"Trim Trailing Whitespace","description":"Remove trailing whitespace when saving.","type":"boolean","default":true},"editor.formatOnSave":{"title":"Format On Save","description":"Format a file on save. A formatter must be available.","type":"boolean","default":false},"editor.defaultFormatter":{"title":"Default Formatter","description":"Formatter used when several are available.","type":"string","default":"oxbit.prettier","anyOf":[{"enum":["oxbit.prettier","oxbit.builtin-ts"]},{"type":"string"}]},"terminal.fontSize":{"title":"Terminal Font Size","description":"Font size in pixels for the terminal.","type":"number","default":12,"minimum":8,"maximum":32},"terminal.scrollback":{"title":"Scrollback","description":"Maximum number of lines kept in the terminal buffer.","type":"number","default":5000,"minimum":100,"maximum":100000},"terminal.confirmOnKill":{"title":"Confirm On Kill","description":"Ask before killing a terminal with a running process.","type":"boolean","default":true},"scm.autoFetch":{"title":"Auto Fetch","description":"Periodically fetch from the default remote.","type":"boolean","default":true},"scm.diffLayout":{"title":"Diff Layout","description":"Default layout for diff editors.","type":"string","default":"side-by-side","enum":["side-by-side","inline"]},"files.associations":{"title":"File Associations","description":"Map filename or workspace-relative glob patterns to language IDs. Explicit associations take precedence over built-in detection.","type":"object","default":{},"propertyNames":{"minLength":1,"maxLength":1024},"additionalProperties":{"type":"string","pattern":"^[\\\\w+-]+$"}},"languageServers":{"title":"Language Servers","description":"Configure enabled state, selectors, rootMarkers, executable, args, env, initializationOptions, settings, and priority by server ID. Servers install on first use in a trusted runtime. License keys belong in the runtime-local credential file.","type":"object","default":{},"propertyNames":{"pattern":"^[a-zA-Z0-9._-]+$"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Enable this language server."},"selectors":{"type":"array","items":{"type":"object","properties":{"language":{"type":"string"},"pattern":{"type":"string"},"scheme":{"const":"file"}},"additionalProperties":false,"minProperties":1}},"rootMarkers":{"type":"array","items":{"type":"string","pattern":"^(?!/)(?!.*\\\\\\\\)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))[^\\\\u0000]+$","minLength":1}},"executable":{"type":"string","pattern":"^[^\\\\u0000]*$","minLength":1},"args":{"type":"array","items":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"env":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"additionalProperties":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"initializationOptions":{"type":"object","additionalProperties":true,"description":"Server-specific initialization options. Store license credentials in the runtime credential file."},"settings":{"type":"object","additionalProperties":true,"description":"Server-specific configuration; nested objects merge across files."},"priority":{"type":"number"}},"additionalProperties":false}},"ui.fontFamily":{"title":"UI Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"ui.fontSize":{"title":"UI Font Size","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":12,"minimum":8,"maximum":72},"ui.fontWeight":{"title":"UI Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"ui.fontStyle":{"title":"UI Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"ui.lineHeight":{"title":"UI Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1.4,"minimum":1,"maximum":3},"ui.letterSpacing":{"title":"UI Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"ui.fontLigatures":{"title":"UI Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"terminal.fontFamily":{"title":"Terminal Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"terminal.fontWeight":{"title":"Terminal Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"terminal.fontStyle":{"title":"Terminal Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"terminal.lineHeight":{"title":"Terminal Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1,"minimum":1,"maximum":3},"terminal.letterSpacing":{"title":"Terminal Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"terminal.fontLigatures":{"title":"Terminal Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"workbench.iconTheme":{"title":"File Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"workbench.productIconTheme":{"title":"Product Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"editor.semanticHighlighting":{"title":"Semantic Highlighting","description":"Color symbols using language server information.","type":"boolean","default":true},"editor.inlayHints.types":{"title":"Type Inlay Hints","description":"Show supported type hints in the visible editor.","type":"boolean","default":true},"editor.inlayHints.parameters":{"title":"Parameter Inlay Hints","description":"Show supported parameter hints in the visible editor.","type":"boolean","default":true},"editor.largeFileIntelligence":{"title":"Large File Intelligence","description":"Allow full semantic highlighting and inlay hints for files larger than 1 MiB.","type":"boolean","default":false},"workbench.locale":{"title":"Display Language","type":"string","default":"en","enum":["en","de","es","ja","zh"]},"workbench.reducedMotion":{"title":"Reduced Motion","type":"boolean","default":false},"desktop.projects.openBehavior":{"title":"Open projects","type":"string","default":"currentWindow","enum":["currentWindow","newWindow"]},"desktop.tools.gitPath":{"title":"git executable path","type":"string","default":""},"desktop.tools.ghPath":{"title":"gh executable path","type":"string","default":""},"project.intelligence":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Index project dependencies, imports and related files."},"exclude":{"type":"array","items":{"type":"string","maxLength":1024},"default":[]},"maxFiles":{"type":"integer","minimum":1,"maximum":100000,"default":20000},"maxFileBytes":{"type":"integer","minimum":1,"maximum":5242880,"default":1048576}},"additionalProperties":false,"title":"Project Intelligence","description":"Partial overrides of project.json intelligence. Objects merge across files."},"project.schemas":{"type":"object","properties":{"catalog":{"type":"boolean","default":true,"description":"Discover file schemas through SchemaStore. Bundled Oxbit settings support remains available."},"download":{"type":"boolean","default":true,"description":"Allow remote schema downloads; cached and bundled schemas work offline."},"associations":{"type":"array","maxItems":1000,"default":[],"items":{"type":"object","properties":{"url":{"type":"string","minLength":1,"description":"Schema URL or workspace file URI."},"fileMatch":{"type":"array","items":{"type":"string","maxLength":1024}},"schema":{"type":["object","boolean"],"description":"An inline JSON Schema."}},"additionalProperties":false,"anyOf":[{"required":["url"]},{"required":["schema"]}]}}},"additionalProperties":false,"title":"JSON Schemas","description":"Partial overrides of project.json schemas. Association arrays replace lower arrays."}}`);
935
+ const properties$2 = /* @__PURE__ */ JSON.parse(`{"$schema":{"type":"string","description":"JSON Schema URI. Ignored when merging effective preferences."},"agentACP.provider":{"title":"Default agent","type":"string","default":"codex","enum":["codex","cursor","amp"]},"agentACP.codex.command":{"title":"Codex ACP executable","type":"string","default":"npx"},"agentACP.codex.args":{"title":"Codex ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"@agentclientprotocol/codex-acp@2.2.2\\"]"},"agentACP.cursor.command":{"title":"Cursor ACP executable","type":"string","default":"agent"},"agentACP.cursor.args":{"title":"Cursor ACP arguments (JSON array)","type":"string","default":"[\\"acp\\"]"},"agentACP.amp.command":{"title":"Amp Agent ACP executable","type":"string","default":"npx"},"agentACP.amp.args":{"title":"Amp Agent ACP arguments (JSON array)","type":"string","default":"[\\"-y\\",\\"amp-acp@0.10.0\\"]"},"workbench.keymap":{"title":"Keymap","description":"Keyboard layout applied on top of the Oxbit defaults. User keybindings always win.","type":"string","default":"default","enum":["default","vscode","jetbrains","macos","sublime","atom","visual-studio","emacs"]},"runtime.keepAlive":{"title":"Keep Runtime Alive","description":"How long a runtime Oxbit started keeps running after the last client disconnects or its connection heartbeats stop. Terminals and tasks continue while it runs.","type":"string","default":"75s","enum":["75s","15m","1h","8h","untilStopped"]},"runtime.autoReconnect":{"title":"Reconnect Automatically","description":"Reconnect to the runtime after the connection drops, including when the iOS app returns to the foreground.","type":"boolean","default":true},"workbench.tooltipDelay":{"title":"Tooltip Delay","description":"Delay in milliseconds before showing a tooltip on hover. Keyboard focus shows tooltips immediately.","type":"number","default":400,"minimum":0,"maximum":5000},"workbench.colorTheme":{"title":"Color Theme","description":"Specifies the color theme used in the workbench.","type":"string","default":"Graphite (dark)"},"workbench.density":{"title":"Layout Density","description":"Controls row heights, padding and label sizes across the workbench.","type":"string","default":"compact","enum":["compact","comfortable"]},"workbench.sidebarLocation":{"title":"Sidebar Location","description":"Where the primary sidebar is docked.","type":"string","default":"left","enum":["left","right"]},"editor.fontFamily":{"title":"Font Family","description":"Controls the font family of the editor.","type":"string","default":"'JetBrains Mono', ui-monospace, monospace"},"editor.fontSize":{"title":"Font Size","description":"Font size in pixels.","type":"number","default":13,"minimum":8,"maximum":32},"editor.lineHeight":{"title":"Line Height","description":"Line height in pixels. 0 uses 1.55 × font size.","type":"number","default":20,"minimum":0,"maximum":60},"editor.fontLigatures":{"title":"Font Ligatures","description":"Enables font ligatures when the font supports them.","type":"boolean","default":false},"editor.tabSize":{"title":"Tab Size","description":"The number of spaces a tab is equal to.","type":"number","default":2,"minimum":1,"maximum":8},"editor.insertSpaces":{"title":"Insert Spaces","description":"Insert spaces when pressing Tab.","type":"boolean","default":true},"editor.detectIndentation":{"title":"Detect Indentation","description":"Detect Tab Size and Insert Spaces from file contents on open.","type":"boolean","default":true},"editor.renderIndentGuides":{"title":"Indentation Guides","description":"Render vertical guides for each indent level.","type":"boolean","default":true},"editor.wordWrap":{"title":"Word Wrap","description":"Controls how lines should wrap.","type":"string","default":"off","enum":["off","on","bounded"]},"editor.renderWhitespace":{"title":"Render Whitespace","description":"Controls how the editor renders whitespace characters.","type":"string","default":"selection","enum":["none","boundary","selection","all"]},"editor.cursorBlinking":{"title":"Cursor Blinking","description":"Cursor animation style.","type":"string","default":"blink","enum":["blink","smooth","phase","solid"]},"editor.minimap":{"title":"Minimap","description":"Show a minimap of the document.","type":"boolean","default":false},"files.autoSave":{"title":"Auto Save","description":"Controls auto save of editors that have unsaved changes.","type":"string","default":"off","enum":["off","afterDelay","onFocusChange","onWindowChange"]},"files.autoSaveDelay":{"title":"Auto Save Delay","description":"Delay in milliseconds after which an editor is saved automatically. Applies when Auto Save is afterDelay.","type":"number","default":1000,"minimum":100,"maximum":60000},"files.trimTrailingWhitespace":{"title":"Trim Trailing Whitespace","description":"Remove trailing whitespace when saving.","type":"boolean","default":true},"editor.formatOnSave":{"title":"Format On Save","description":"Format a file on save. A formatter must be available.","type":"boolean","default":false},"editor.defaultFormatter":{"title":"Default Formatter","description":"Formatter used when several are available.","type":"string","default":"oxbit.prettier","anyOf":[{"enum":["oxbit.prettier","oxbit.builtin-ts"]},{"type":"string"}]},"terminal.fontSize":{"title":"Terminal Font Size","description":"Font size in pixels for the terminal.","type":"number","default":12,"minimum":8,"maximum":32},"terminal.scrollback":{"title":"Scrollback","description":"Maximum number of lines kept in the terminal buffer.","type":"number","default":5000,"minimum":100,"maximum":100000},"terminal.confirmOnKill":{"title":"Confirm On Kill","description":"Ask before killing a terminal with a running process.","type":"boolean","default":true},"scm.autoFetch":{"title":"Auto Fetch","description":"Periodically fetch from the default remote.","type":"boolean","default":true},"scm.diffLayout":{"title":"Diff Layout","description":"Default layout for diff editors.","type":"string","default":"side-by-side","enum":["side-by-side","inline"]},"files.associations":{"title":"File Associations","description":"Map filename or workspace-relative glob patterns to language IDs. Explicit associations take precedence over built-in detection.","type":"object","default":{},"propertyNames":{"minLength":1,"maxLength":1024},"additionalProperties":{"type":"string","pattern":"^[\\\\w+-]+$"}},"languageServers":{"title":"Language Servers","description":"Configure enabled state, selectors, rootMarkers, executable, args, env, initializationOptions, settings, and priority by server ID. Servers install on first use in a trusted runtime. License keys belong in the runtime-local credential file.","type":"object","default":{},"propertyNames":{"pattern":"^[a-zA-Z0-9._-]+$"},"additionalProperties":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Enable this language server."},"selectors":{"type":"array","items":{"type":"object","properties":{"language":{"type":"string"},"pattern":{"type":"string"},"scheme":{"const":"file"}},"additionalProperties":false,"minProperties":1}},"rootMarkers":{"type":"array","items":{"type":"string","pattern":"^(?!/)(?!.*\\\\\\\\)(?!.*(?:^|/)\\\\.\\\\.(?:/|$))[^\\\\u0000]+$","minLength":1}},"executable":{"type":"string","pattern":"^[^\\\\u0000]*$","minLength":1},"args":{"type":"array","items":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"env":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$"},"additionalProperties":{"type":"string","pattern":"^[^\\\\u0000]*$"}},"initializationOptions":{"type":"object","additionalProperties":true,"description":"Server-specific initialization options. Store license credentials in the runtime credential file."},"settings":{"type":"object","additionalProperties":true,"description":"Server-specific configuration; nested objects merge across files."},"priority":{"type":"number"}},"additionalProperties":false}},"ui.fontFamily":{"title":"UI Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"ui.fontSize":{"title":"UI Font Size","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":12,"minimum":8,"maximum":72},"ui.fontWeight":{"title":"UI Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"ui.fontStyle":{"title":"UI Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"ui.lineHeight":{"title":"UI Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1.4,"minimum":1,"maximum":3},"ui.letterSpacing":{"title":"UI Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"ui.fontLigatures":{"title":"UI Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"terminal.fontFamily":{"title":"Terminal Font Family","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":""},"terminal.fontWeight":{"title":"Terminal Font Weight","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":400,"minimum":100,"maximum":900},"terminal.fontStyle":{"title":"Terminal Font Style","description":"Overrides theme typography. Reset restores the theme value.","type":"string","default":"normal","enum":["normal","italic","oblique"]},"terminal.lineHeight":{"title":"Terminal Line Height","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":1,"minimum":1,"maximum":3},"terminal.letterSpacing":{"title":"Terminal Letter Spacing","description":"Overrides theme typography. Reset restores the theme value.","type":"number","default":0,"minimum":-3,"maximum":10},"terminal.fontLigatures":{"title":"Terminal Font Ligatures","description":"Overrides theme typography. Reset restores the theme value.","type":"boolean","default":false},"workbench.iconTheme":{"title":"File Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"workbench.productIconTheme":{"title":"Product Icon Theme","description":"Select a locally installed icon theme. Unavailable selections use Oxbit defaults until restored.","type":"string","default":"oxbit.default"},"editor.semanticHighlighting":{"title":"Semantic Highlighting","description":"Color symbols using language server information.","type":"boolean","default":true},"editor.inlayHints.types":{"title":"Type Inlay Hints","description":"Show supported type hints in the visible editor.","type":"boolean","default":true},"editor.inlayHints.parameters":{"title":"Parameter Inlay Hints","description":"Show supported parameter hints in the visible editor.","type":"boolean","default":true},"editor.largeFileIntelligence":{"title":"Large File Intelligence","description":"Allow full semantic highlighting and inlay hints for files larger than 1 MiB.","type":"boolean","default":false},"workbench.locale":{"title":"Display Language","type":"string","default":"en","enum":["en","de","es","ja","zh"]},"workbench.reducedMotion":{"title":"Reduced Motion","type":"boolean","default":false},"desktop.projects.openBehavior":{"title":"Open projects","type":"string","default":"currentWindow","enum":["currentWindow","newWindow"]},"desktop.tools.gitPath":{"title":"git executable path","type":"string","default":""},"desktop.tools.ghPath":{"title":"gh executable path","type":"string","default":""},"project.intelligence":{"type":"object","properties":{"enabled":{"type":"boolean","default":true,"description":"Index project dependencies, imports and related files."},"exclude":{"type":"array","items":{"type":"string","maxLength":1024},"default":[]},"maxFiles":{"type":"integer","minimum":1,"maximum":100000,"default":20000},"maxFileBytes":{"type":"integer","minimum":1,"maximum":5242880,"default":1048576}},"additionalProperties":false,"title":"Project Intelligence","description":"Partial overrides of project.json intelligence. Objects merge across files."},"project.schemas":{"type":"object","properties":{"catalog":{"type":"boolean","default":true,"description":"Discover file schemas through SchemaStore. Bundled Oxbit settings support remains available."},"download":{"type":"boolean","default":true,"description":"Allow remote schema downloads; cached and bundled schemas work offline."},"associations":{"type":"array","maxItems":1000,"default":[],"items":{"type":"object","properties":{"url":{"type":"string","minLength":1,"description":"Schema URL or workspace file URI."},"fileMatch":{"type":"array","items":{"type":"string","maxLength":1024}},"schema":{"type":["object","boolean"],"description":"An inline JSON Schema."}},"additionalProperties":false,"anyOf":[{"required":["url"]},{"required":["schema"]}]}}},"additionalProperties":false,"title":"JSON Schemas","description":"Partial overrides of project.json schemas. Association arrays replace lower arrays."}}`);
826
936
  const propertyNames = { "anyOf": [{ "not": { "pattern": "^\\[.*\\]$" } }, { "pattern": "^\\[[^\\[\\]]+\\]$" }] };
827
937
  const patternProperties = { "^\\[[^\\[\\]]+\\]$": { "$ref": "#/definitions/languageOverrides" } };
828
938
  const additionalProperties$1 = true;
@@ -1000,7 +1110,7 @@ const ACP_PROVIDERS = [
1000
1110
  id: "codex",
1001
1111
  name: "Codex ACP",
1002
1112
  command: "npx",
1003
- args: ["-y", "@agentclientprotocol/codex-acp@1.10.0"],
1113
+ args: ["-y", "@agentclientprotocol/codex-acp@2.2.2"],
1004
1114
  setup: "Uses the Codex ACP adapter. Sign in through an advertised authentication method, or use your existing Codex credentials.",
1005
1115
  url: "https://github.com/agentclientprotocol/codex-acp"
1006
1116
  },
@@ -1016,7 +1126,7 @@ const ACP_PROVIDERS = [
1016
1126
  id: "amp",
1017
1127
  name: "Amp Agent ACP",
1018
1128
  command: "npx",
1019
- args: ["-y", "amp-acp@0.9.0"],
1129
+ args: ["-y", "amp-acp@0.10.0"],
1020
1130
  setup: "Uses the community Amp ACP adapter. Install Amp CLI and run amp login first. Set AMP_CLI_PATH in the runtime environment if needed.",
1021
1131
  url: "https://github.com/tao12345666333/amp-acp"
1022
1132
  }
@@ -1060,7 +1170,7 @@ const SDK_VERSION = "1.0.0";
1060
1170
  function invalid(message) {
1061
1171
  throw new RpcError("INVALID_TASK_CONFIG", message);
1062
1172
  }
1063
- function object$1(value, label) {
1173
+ function object$2(value, label) {
1064
1174
  if (!value || typeof value !== "object" || Array.isArray(value))
1065
1175
  invalid(`${label} must be an object`);
1066
1176
  return value;
@@ -1071,7 +1181,7 @@ function taskName(value) {
1071
1181
  return value;
1072
1182
  }
1073
1183
  function stringMap(value, label) {
1074
- const record = object$1(value, label);
1184
+ const record = object$2(value, label);
1075
1185
  for (const [key, item] of Object.entries(record))
1076
1186
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ["__proto__", "constructor", "prototype"].includes(key) || typeof item !== "string" || item.length > 32768 || item.includes("\0"))
1077
1187
  invalid(`${label}: invalid environment entry ${key}`);
@@ -1087,13 +1197,13 @@ function port(value) {
1087
1197
  number(value, 1, 65535, "port");
1088
1198
  return value;
1089
1199
  }
1090
- const range2 = object$1(value, "port range");
1200
+ const range2 = object$2(value, "port range");
1091
1201
  number(range2.min, 1, 65535, "port minimum");
1092
1202
  number(range2.max, range2.min, 65535, "port maximum");
1093
1203
  return { min: range2.min, max: range2.max };
1094
1204
  }
1095
1205
  function hooks(value) {
1096
- const data = object$1(value ?? {}, "worktree");
1206
+ const data = object$2(value ?? {}, "worktree");
1097
1207
  for (const key of Object.keys(data))
1098
1208
  if (!taskPhases.includes(key))
1099
1209
  invalid(`Unknown worktree phase: ${key}`);
@@ -1110,7 +1220,7 @@ function hooks(value) {
1110
1220
  );
1111
1221
  }
1112
1222
  function definition(value) {
1113
- const data = object$1(value, "task");
1223
+ const data = object$2(value, "task");
1114
1224
  const known = /* @__PURE__ */ new Set([
1115
1225
  "command",
1116
1226
  "type",
@@ -1152,7 +1262,7 @@ function definition(value) {
1152
1262
  if (data.env !== void 0) stringMap(data.env, "task.env");
1153
1263
  if (data.port !== void 0) port(data.port);
1154
1264
  if (data.ports !== void 0) {
1155
- const entries = Object.entries(object$1(data.ports, "ports"));
1265
+ const entries = Object.entries(object$2(data.ports, "ports"));
1156
1266
  if (entries.length > 16) invalid("Maximum 16 named ports per task");
1157
1267
  for (const [key, value2] of entries) {
1158
1268
  if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key) || key.toLowerCase() === "port")
@@ -1163,7 +1273,7 @@ function definition(value) {
1163
1273
  invalid("Port names must be unique ignoring case");
1164
1274
  }
1165
1275
  if (data.ready !== void 0) {
1166
- const ready = object$1(data.ready, "ready");
1276
+ const ready = object$2(data.ready, "ready");
1167
1277
  for (const key of Object.keys(ready))
1168
1278
  if (!["pattern", "url", "timeoutMs", "intervalMs"].includes(key))
1169
1279
  invalid(`Unknown readiness field: ${key}`);
@@ -1179,7 +1289,7 @@ function definition(value) {
1179
1289
  number(ready.intervalMs, 100, 6e4, "ready.intervalMs");
1180
1290
  }
1181
1291
  if (data.restart !== void 0) {
1182
- const restart = object$1(data.restart, "restart");
1292
+ const restart = object$2(data.restart, "restart");
1183
1293
  for (const key of Object.keys(restart))
1184
1294
  if (!["policy", "maxAttempts", "delayMs"].includes(key))
1185
1295
  invalid(`Unknown restart field: ${key}`);
@@ -1191,7 +1301,7 @@ function definition(value) {
1191
1301
  number(restart.delayMs, 100, 6e4, "restart.delayMs");
1192
1302
  }
1193
1303
  if (data.stop !== void 0) {
1194
- const stop2 = object$1(data.stop, "stop");
1304
+ const stop2 = object$2(data.stop, "stop");
1195
1305
  for (const key of Object.keys(stop2))
1196
1306
  if (!["signal", "timeoutMs"].includes(key))
1197
1307
  invalid(`Unknown stop field: ${key}`);
@@ -1208,7 +1318,7 @@ function json(text2) {
1208
1318
  const errors = [];
1209
1319
  const result = parse$1(text2, errors, { allowTrailingComma: true });
1210
1320
  if (errors.length) invalid(`Invalid JSON near offset ${errors[0].offset}`);
1211
- return object$1(result, "configuration");
1321
+ return object$2(result, "configuration");
1212
1322
  }
1213
1323
  function editJson(text2, location, value) {
1214
1324
  const tree = value === void 0 ? parseTree(text2) : void 0;
@@ -1410,7 +1520,7 @@ function readSource(kind, text2) {
1410
1520
  );
1411
1521
  } else {
1412
1522
  const data = json(text2);
1413
- const meta = object$1(data.oxbit ?? {}, "oxbit metadata");
1523
+ const meta = object$2(data.oxbit ?? {}, "oxbit metadata");
1414
1524
  result.env = stringMap(
1415
1525
  (kind === "oxbit" ? data.env : meta.env) ?? {},
1416
1526
  "env"
@@ -1430,7 +1540,7 @@ function readSource(kind, text2) {
1430
1540
  ].includes(key))
1431
1541
  invalid(`Unknown Oxbit configuration field: ${key}`);
1432
1542
  for (const [name, task] of Object.entries(
1433
- object$1(data.tasks ?? {}, "tasks")
1543
+ object$2(data.tasks ?? {}, "tasks")
1434
1544
  ))
1435
1545
  add(name, definition(task));
1436
1546
  } else if (kind === "paseo") {
@@ -1442,9 +1552,9 @@ function readSource(kind, text2) {
1442
1552
  ...result.worktree
1443
1553
  };
1444
1554
  for (const [name, value] of Object.entries(
1445
- object$1(data.scripts ?? {}, "scripts")
1555
+ object$2(data.scripts ?? {}, "scripts")
1446
1556
  )) {
1447
- const script = object$1(value, name);
1557
+ const script = object$2(value, name);
1448
1558
  add(
1449
1559
  name,
1450
1560
  definition({
@@ -1496,10 +1606,10 @@ function readSource(kind, text2) {
1496
1606
  invalid("Only VS Code tasks version 2.0.0 is supported");
1497
1607
  if (!Array.isArray(data.tasks)) invalid("VS Code tasks must be an array");
1498
1608
  for (const raw of data.tasks) {
1499
- const entry = object$1(raw, "VS Code task"), platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1609
+ const entry = object$2(raw, "VS Code task"), platform = process.platform === "darwin" ? "osx" : process.platform === "win32" ? "windows" : "linux";
1500
1610
  const item = { ...entry, ...entry[platform] };
1501
1611
  const args = (item.args ?? []).map(
1502
- (arg) => typeof arg === "string" ? arg : object$1(arg, "argument").value
1612
+ (arg) => typeof arg === "string" ? arg : object$2(arg, "argument").value
1503
1613
  );
1504
1614
  const opts = { ...data.options, ...item.options };
1505
1615
  let command = item.command ?? data.command ?? "", execution = item.type === "process" ? "process" : "shell";
@@ -1538,7 +1648,7 @@ function readSource(kind, text2) {
1538
1648
  }
1539
1649
  } else {
1540
1650
  for (const [name, script] of Object.entries(
1541
- object$1(data.scripts ?? {}, "scripts")
1651
+ object$2(data.scripts ?? {}, "scripts")
1542
1652
  )) {
1543
1653
  if (typeof script !== "string" && !(kind === "composer" && Array.isArray(script)))
1544
1654
  continue;
@@ -6527,8 +6637,8 @@ class Collaboration {
6527
6637
  )
6528
6638
  );
6529
6639
  }
6530
- bytes(encoded) {
6531
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length > 2 * 1024 * 1024)
6640
+ bytes(encoded, limit2 = MAX_MESSAGE_BYTES) {
6641
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length > limit2)
6532
6642
  throw new RpcError("INVALID_PARAMS", "Invalid collaboration update");
6533
6643
  return Buffer.from(encoded, "base64");
6534
6644
  }
@@ -6564,14 +6674,14 @@ class Collaboration {
6564
6674
  }
6565
6675
  async update(relative, connection, encoded) {
6566
6676
  const room = await this.member(relative, connection);
6567
- const bytes = this.bytes(encoded);
6677
+ const bytes = this.bytes(encoded, MAX_COLLABORATION_UPDATE_BYTES);
6568
6678
  try {
6569
- if (room.doc.getText("content").length + bytes.length > 20 * 1024 * 1024) {
6679
+ if (room.doc.getText("content").length + bytes.length > MAX_FILE_BYTES) {
6570
6680
  const candidate = new Y.Doc();
6571
6681
  try {
6572
6682
  Y.applyUpdate(candidate, Y.encodeStateAsUpdate(room.doc));
6573
6683
  Y.applyUpdate(candidate, bytes);
6574
- if (candidate.getText("content").length > 20 * 1024 * 1024) throw new RpcError("FILE_TOO_LARGE", "Shared documents are limited to 20 MiB");
6684
+ if (candidate.getText("content").length > MAX_FILE_BYTES) throw new RpcError("FILE_TOO_LARGE", "Shared documents are limited to 20 MiB");
6575
6685
  } finally {
6576
6686
  candidate.destroy();
6577
6687
  }
@@ -6740,7 +6850,7 @@ class Collaboration {
6740
6850
  }
6741
6851
  }
6742
6852
  const terminal = (state2) => !["pending", "running", "unknown"].includes(state2);
6743
- const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
6853
+ const object$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
6744
6854
  const str = (value, max = 4096) => typeof value === "string" ? value.slice(0, max) : void 0;
6745
6855
  const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 512 && !value.includes("\0");
6746
6856
  const preview = (value, max = 8192) => value === void 0 ? void 0 : (typeof value === "string" ? value : JSON.stringify(value)).slice(0, max);
@@ -6770,11 +6880,11 @@ ${item.newText ?? ""}` : JSON.stringify(item))
6770
6880
  ).join("\n");
6771
6881
  }
6772
6882
  function normalizeDelegation(provider, update) {
6773
- const raw = object(update.rawInput), meta = object(update._meta?.codex);
6883
+ const raw = object$1(update.rawInput), meta = object$1(update._meta?.codex);
6774
6884
  const toolId = update.toolCallId;
6775
6885
  if (!identity(toolId)) return [];
6776
6886
  if (provider === "codex") {
6777
- const collab = object(meta.collaboration), sub = object(meta.subagent);
6887
+ const collab = object$1(meta.collaboration), sub = object$1(meta.subagent);
6778
6888
  if (identity(sub.threadId))
6779
6889
  return [
6780
6890
  {
@@ -6788,7 +6898,7 @@ function normalizeDelegation(provider, update) {
6788
6898
  ];
6789
6899
  if (!identity(collab.tool)) return [];
6790
6900
  const receivers = Array.isArray(collab.receiverThreadIds) ? collab.receiverThreadIds.filter(identity) : [];
6791
- const states = object(raw.agentsStates);
6901
+ const states = object$1(raw.agentsStates);
6792
6902
  const ids = [
6793
6903
  .../* @__PURE__ */ new Set([...receivers, ...Object.keys(states).filter(identity)])
6794
6904
  ];
@@ -6816,8 +6926,8 @@ function normalizeDelegation(provider, update) {
6816
6926
  }));
6817
6927
  }
6818
6928
  if (provider === "cursor" && update.taskMeta) {
6819
- const task = object(update.taskMeta), output = object(update.rawOutput);
6820
- const outcome = object(task.outcome ?? output.outcome);
6929
+ const task = object$1(update.taskMeta), output = object$1(update.rawOutput);
6930
+ const outcome = object$1(task.outcome ?? output.outcome);
6821
6931
  const outcomeName = typeof (task.outcome ?? output.outcome) === "string" ? task.outcome ?? output.outcome : outcome.outcome;
6822
6932
  return [
6823
6933
  {
@@ -7237,7 +7347,273 @@ class SubagentRegistry {
7237
7347
  }
7238
7348
  }
7239
7349
  }
7350
+ let cached;
7351
+ function runtimeVersion() {
7352
+ if (cached) return cached;
7353
+ return cached = "0.7.0";
7354
+ }
7355
+ const MAX_REQUEST_BYTES = 1024 * 1024;
7356
+ const MAX_CONCURRENT_REQUESTS = 16;
7357
+ const PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
7358
+ const SERVER_NAME = "oxbit_workspace";
7359
+ const NATIVE_AGENT_TOOLS = [
7360
+ {
7361
+ name: "oxbit_get_workspace",
7362
+ description: "Get the workspace, active editor, and current selection when an editor is connected.",
7363
+ inputSchema: { type: "object", properties: {} }
7364
+ },
7365
+ {
7366
+ name: "oxbit_list_files",
7367
+ description: "List files in a workspace directory. Paths are workspace relative or absolute within the workspace.",
7368
+ inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path; defaults to the workspace root." } } }
7369
+ },
7370
+ {
7371
+ name: "oxbit_read_file",
7372
+ description: "Read a bounded range of a workspace file, including live unsaved editor content. Paths are workspace relative or absolute within the workspace.",
7373
+ inputSchema: { type: "object", properties: { path: { type: "string" }, line: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1 } }, required: ["path"] }
7374
+ },
7375
+ {
7376
+ name: "oxbit_get_diagnostics",
7377
+ description: "Get live editor diagnostics for a workspace file or the workspace. Requires a connected editor; headless runtimes have no diagnostics.",
7378
+ inputSchema: { type: "object", properties: { path: { type: "string", description: "Optional workspace file path." } } }
7379
+ },
7380
+ {
7381
+ name: "oxbit_open_file",
7382
+ description: "Open a workspace file in the editor at an optional line.",
7383
+ inputSchema: { type: "object", properties: { path: { type: "string" }, line: { type: "integer", minimum: 1 } }, required: ["path"] }
7384
+ },
7385
+ {
7386
+ name: "oxbit_propose_edit",
7387
+ description: "Propose replacement content for a workspace file. The tool waits for engineer review and returns the decision.",
7388
+ inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] }
7389
+ },
7390
+ {
7391
+ name: "oxbit_update_plan",
7392
+ description: "Publish the current task plan with at most 50 entries.",
7393
+ inputSchema: { type: "object", properties: { entries: { type: "array", maxItems: 50, items: { type: "object", properties: { content: { type: "string" }, status: { type: "string", enum: ["pending", "in_progress", "completed"] }, priority: { type: "string" } }, required: ["content", "status"] } } }, required: ["entries"] }
7394
+ }
7395
+ ];
7396
+ const STDIO_PROXY = String.raw`
7397
+ const readline = require('node:readline');
7398
+ const input = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
7399
+ const pending = new Set();
7400
+ let closing = false;
7401
+ const maxBytes = 1024 * 1024;
7402
+ const send = (message) => {
7403
+ if (!closing) process.stdout.write(JSON.stringify(message) + '\n');
7404
+ };
7405
+ const fail = (id, message) => send({ jsonrpc: '2.0', id: id ?? null, error: { code: -32603, message } });
7406
+ input.on('line', (line) => {
7407
+ if (Buffer.byteLength(line) > maxBytes) return fail(null, 'MCP input line is too large');
7408
+ let message;
7409
+ try { message = JSON.parse(line); }
7410
+ catch { return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Invalid JSON' } }); }
7411
+ const hasId = message !== null && typeof message === 'object' && Object.prototype.hasOwnProperty.call(message, 'id');
7412
+ if (pending.size >= 16) {
7413
+ if (hasId) fail(message.id, 'MCP proxy is busy');
7414
+ return;
7415
+ }
7416
+ const controller = new AbortController();
7417
+ pending.add(controller);
7418
+ void (async () => {
7419
+ try {
7420
+ const response = await fetch(process.env.OXBIT_MCP_URL, {
7421
+ method: 'POST',
7422
+ headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', authorization: 'Bearer ' + process.env.OXBIT_MCP_TOKEN },
7423
+ body: line,
7424
+ signal: controller.signal,
7425
+ });
7426
+ if (!hasId) { await response.body?.cancel(); return; }
7427
+ const length = Number(response.headers.get('content-length'));
7428
+ if (length > maxBytes) throw new Error('MCP response is too large');
7429
+ const reader = response.body?.getReader();
7430
+ if (!reader) throw new Error(response.ok ? 'MCP proxy returned an empty response' : 'MCP proxy HTTP ' + response.status);
7431
+ const chunks = [];
7432
+ let size = 0;
7433
+ while (true) {
7434
+ const { done, value } = await reader.read();
7435
+ if (done) break;
7436
+ size += value.byteLength;
7437
+ if (size > maxBytes) { await reader.cancel(); throw new Error('MCP response is too large'); }
7438
+ chunks.push(value);
7439
+ }
7440
+ let result;
7441
+ try { result = JSON.parse(Buffer.concat(chunks, size).toString('utf8')); }
7442
+ catch { throw new Error(response.ok ? 'MCP proxy returned invalid JSON' : 'MCP proxy HTTP ' + response.status); }
7443
+ if (result?.jsonrpc !== '2.0' || result.id !== message.id || (!result.result && !result.error)) throw new Error(response.ok ? 'MCP proxy returned an invalid response' : 'MCP proxy HTTP ' + response.status);
7444
+ if (!response.ok && !result.error) throw new Error('MCP proxy HTTP ' + response.status);
7445
+ send(result);
7446
+ } catch (error) {
7447
+ if (hasId && !closing) fail(message.id, error instanceof Error ? error.message : String(error));
7448
+ } finally {
7449
+ pending.delete(controller);
7450
+ }
7451
+ })();
7452
+ });
7453
+ input.on('close', () => {
7454
+ closing = true;
7455
+ for (const controller of pending) controller.abort();
7456
+ });`;
7457
+ function object(value) {
7458
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7459
+ }
7460
+ function validateArguments(name, args) {
7461
+ if (!NATIVE_AGENT_TOOLS.some((tool) => tool.name === name)) return "Unknown tool";
7462
+ const keys = Object.keys(args);
7463
+ const allowed = {
7464
+ oxbit_get_workspace: [],
7465
+ oxbit_list_files: ["path"],
7466
+ oxbit_read_file: ["path", "line", "limit"],
7467
+ oxbit_get_diagnostics: ["path"],
7468
+ oxbit_open_file: ["path", "line"],
7469
+ oxbit_propose_edit: ["path", "content"],
7470
+ oxbit_update_plan: ["entries"]
7471
+ };
7472
+ if (keys.some((key) => !allowed[name].includes(key))) return "Unknown tool argument";
7473
+ if (["oxbit_read_file", "oxbit_open_file", "oxbit_propose_edit"].includes(name) && (typeof args.path !== "string" || !args.path)) return "path must be a nonempty string";
7474
+ if (["oxbit_list_files", "oxbit_get_diagnostics"].includes(name) && args.path !== void 0 && typeof args.path !== "string") return "path must be a string";
7475
+ if (args.line !== void 0 && (!Number.isSafeInteger(args.line) || args.line < 1)) return "line must be a positive integer";
7476
+ if (args.limit !== void 0 && (!Number.isSafeInteger(args.limit) || args.limit < 1)) return "limit must be a positive integer";
7477
+ if (name === "oxbit_propose_edit" && typeof args.content !== "string") return "content must be a string";
7478
+ if (name === "oxbit_update_plan" && (!Array.isArray(args.entries) || args.entries.length > 50 || args.entries.some((entry) => !object(entry) || typeof entry.content !== "string" || !["pending", "in_progress", "completed"].includes(String(entry.status)) || entry.priority !== void 0 && typeof entry.priority !== "string"))) return "entries must contain plan content and status";
7479
+ return void 0;
7480
+ }
7481
+ function reply(response, status2, body) {
7482
+ response.statusCode = status2;
7483
+ if (body === void 0) {
7484
+ response.end();
7485
+ return;
7486
+ }
7487
+ response.setHeader("content-type", "application/json; charset=utf-8");
7488
+ response.end(JSON.stringify(body));
7489
+ }
7490
+ function rpcError(id, code, message) {
7491
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
7492
+ }
7493
+ class AgentMCP {
7494
+ constructor(handler) {
7495
+ this.handler = handler;
7496
+ }
7497
+ handler;
7498
+ listener;
7499
+ starting;
7500
+ url;
7501
+ tokens = /* @__PURE__ */ new Map();
7502
+ activeRequests = 0;
7503
+ async server(connectionId, httpSupported) {
7504
+ if (!this.listener && !this.starting) this.starting = this.startListener();
7505
+ if (this.starting) await this.starting;
7506
+ let token = this.tokens.get(connectionId);
7507
+ if (!token) {
7508
+ token = randomBytes(32).toString("hex");
7509
+ this.tokens.set(connectionId, token);
7510
+ }
7511
+ if (httpSupported) return { name: SERVER_NAME, type: "http", url: this.url, headers: [{ name: "Authorization", value: `Bearer ${token}` }] };
7512
+ return { name: SERVER_NAME, command: process.execPath, args: ["-e", STDIO_PROXY], env: [
7513
+ { name: "OXBIT_MCP_URL", value: this.url },
7514
+ { name: "OXBIT_MCP_TOKEN", value: token },
7515
+ ...process.versions.electron ? [{ name: "ELECTRON_RUN_AS_NODE", value: "1" }] : []
7516
+ ] };
7517
+ }
7518
+ async startListener() {
7519
+ const listener = createServer((request, response) => {
7520
+ void this.handle(request, response);
7521
+ });
7522
+ try {
7523
+ await new Promise((resolve, reject) => {
7524
+ listener.once("error", reject);
7525
+ listener.listen(0, "127.0.0.1", () => {
7526
+ listener.off("error", reject);
7527
+ resolve();
7528
+ });
7529
+ });
7530
+ const address = listener.address();
7531
+ if (!address || typeof address === "string") throw new Error("MCP listener has no TCP address");
7532
+ this.listener = listener;
7533
+ this.url = "http://127.0.0.1:" + address.port + "/mcp";
7534
+ } catch (error) {
7535
+ listener.close();
7536
+ throw error;
7537
+ } finally {
7538
+ this.starting = void 0;
7539
+ }
7540
+ }
7541
+ revoke(connectionId) {
7542
+ this.tokens.delete(connectionId);
7543
+ }
7544
+ async close() {
7545
+ this.tokens.clear();
7546
+ if (this.starting) await this.starting.catch(() => {
7547
+ });
7548
+ const listener = this.listener;
7549
+ this.listener = void 0;
7550
+ this.url = void 0;
7551
+ if (listener) await new Promise((resolve) => listener.close(() => resolve()));
7552
+ }
7553
+ async handle(request, response) {
7554
+ const address = this.listener?.address();
7555
+ if (!address || typeof address === "string") return reply(response, 503);
7556
+ const origin = `http://127.0.0.1:${address.port}`;
7557
+ if (request.headers.host !== `127.0.0.1:${address.port}` || request.headers.origin && request.headers.origin !== origin) return reply(response, 403);
7558
+ if (request.url !== "/mcp") return reply(response, 404);
7559
+ if (request.method !== "POST") return reply(response, 405);
7560
+ const authorization = request.headers.authorization;
7561
+ const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : void 0;
7562
+ const connectionId = token && [...this.tokens].find(([, candidate]) => candidate === token)?.[0];
7563
+ if (!connectionId) return reply(response, 401);
7564
+ if (this.activeRequests >= MAX_CONCURRENT_REQUESTS) return reply(response, 429);
7565
+ const contentLength = Number(request.headers["content-length"]);
7566
+ if (contentLength > MAX_REQUEST_BYTES) return reply(response, 413);
7567
+ this.activeRequests++;
7568
+ try {
7569
+ let length = 0;
7570
+ const chunks = [];
7571
+ for await (const chunk of request) {
7572
+ length += chunk.length;
7573
+ if (length > MAX_REQUEST_BYTES) return reply(response, 413);
7574
+ chunks.push(chunk);
7575
+ }
7576
+ let message;
7577
+ try {
7578
+ message = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7579
+ } catch {
7580
+ return reply(response, 400, rpcError(null, -32700, "Invalid JSON"));
7581
+ }
7582
+ if (!object(message) || message.jsonrpc !== "2.0" || typeof message.method !== "string" || message.id !== void 0 && typeof message.id !== "string" && typeof message.id !== "number") return reply(response, 400, rpcError(object(message) ? message.id : null, -32600, "Invalid request"));
7583
+ const version = request.headers["mcp-protocol-version"];
7584
+ if (version && !PROTOCOL_VERSIONS.includes(version)) return reply(response, 400, rpcError(message.id, -32600, "Unsupported MCP protocol version"));
7585
+ if (message.id === void 0) return reply(response, 202);
7586
+ const id = message.id;
7587
+ if (message.method === "initialize") {
7588
+ const requested = object(message.params) && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0;
7589
+ return reply(response, 200, { jsonrpc: "2.0", id, result: { protocolVersion: requested && PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0], capabilities: { tools: { listChanged: false } }, serverInfo: { name: "oxbit-workspace", version: runtimeVersion() } } });
7590
+ }
7591
+ if (message.method === "ping") return reply(response, 200, { jsonrpc: "2.0", id, result: {} });
7592
+ if (message.method === "tools/list") return reply(response, 200, { jsonrpc: "2.0", id, result: { tools: NATIVE_AGENT_TOOLS } });
7593
+ if (message.method !== "tools/call") return reply(response, 200, rpcError(id, -32601, "Method not found"));
7594
+ const params = message.params;
7595
+ if (!object(params) || typeof params.name !== "string" || !object(params.arguments ?? {})) return reply(response, 200, rpcError(id, -32602, "Invalid tool call"));
7596
+ const args = params.arguments ?? {};
7597
+ const invalid2 = validateArguments(params.name, args);
7598
+ if (invalid2) return reply(response, 200, { jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: invalid2 }] } });
7599
+ try {
7600
+ const value = await this.handler(connectionId, params.name, args);
7601
+ if (this.tokens.get(connectionId) !== token) return reply(response, 401);
7602
+ const structuredContent = object(value) ? value : { value: value ?? null };
7603
+ return reply(response, 200, { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(value ?? null) }], structuredContent } });
7604
+ } catch (error) {
7605
+ if (this.tokens.get(connectionId) !== token) return reply(response, 401);
7606
+ return reply(response, 200, { jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }] } });
7607
+ }
7608
+ } catch (error) {
7609
+ if (!response.writableEnded) reply(response, 400, rpcError(null, -32600, error instanceof Error ? error.message : String(error)));
7610
+ } finally {
7611
+ this.activeRequests--;
7612
+ }
7613
+ }
7614
+ }
7240
7615
  const MAX_BYTES = 1024 * 1024;
7616
+ const EDITOR_UNSUPPORTED = "The connected editor does not support this tool; update Oxbit";
7241
7617
  function strings(value) {
7242
7618
  if (!Array.isArray(value) || value.length > 128 || value.some(
7243
7619
  (v) => typeof v !== "string" || v.length > 16384 || v.includes("\0")
@@ -7249,13 +7625,17 @@ function strings(value) {
7249
7625
  return value;
7250
7626
  }
7251
7627
  class AgentACP {
7252
- constructor(files, emit) {
7628
+ constructor(files, emit, stateChanged = () => {
7629
+ }) {
7253
7630
  this.files = files;
7254
7631
  this.emit = emit;
7632
+ this.stateChanged = stateChanged;
7255
7633
  }
7256
7634
  files;
7257
7635
  emit;
7636
+ stateChanged;
7258
7637
  agents = /* @__PURE__ */ new Map();
7638
+ mcp = new AgentMCP((id, name, args) => this.tool(id, name, args));
7259
7639
  get(owner, id) {
7260
7640
  const agent = this.agents.get(id);
7261
7641
  if (!agent || agent.owner !== owner || agent.stopped)
@@ -7290,15 +7670,89 @@ class AgentACP {
7290
7670
  });
7291
7671
  }
7292
7672
  publish(agent, event, params) {
7293
- this.emit(agent.owner, event, { id: agent.connection.id, ...params });
7673
+ const payload = { id: agent.connection.id, ...params, seq: ++agent.eventSequence };
7674
+ if (["acp.update", "acp.subagent", "acp.terminal", "acp.turnStarted", "acp.turnEnded"].includes(event)) {
7675
+ const entry = JSON.parse(JSON.stringify({ name: event, params: payload }));
7676
+ agent.events.push(entry);
7677
+ agent.eventBytes += Buffer.byteLength(JSON.stringify(entry));
7678
+ while (agent.events.length > 2e3 || agent.eventBytes > 2 * MAX_BYTES) {
7679
+ agent.eventBytes -= Buffer.byteLength(JSON.stringify(agent.events.shift()));
7680
+ agent.truncated = true;
7681
+ }
7682
+ }
7683
+ if (agent.client) this.emit(agent.client, event, payload);
7684
+ this.stateChanged();
7685
+ }
7686
+ list(owner) {
7687
+ return { sessions: [...this.agents.values()].filter((a) => a.owner === owner && !a.stopped).map((a) => ({
7688
+ connection: a.connection,
7689
+ busy: a.busy,
7690
+ title: a.title,
7691
+ pendingRequests: a.approvals.size,
7692
+ queued: a.queue.length
7693
+ })) };
7694
+ }
7695
+ control(owner, id, client) {
7696
+ if (this.get(owner, id).client !== client)
7697
+ throw new RpcError("BUSY", "Attach this agent session before controlling it");
7698
+ }
7699
+ attach(owner, id, client = owner, editorTools = false) {
7700
+ const agent = this.get(owner, id);
7701
+ if (agent.client && agent.client !== client)
7702
+ this.emit(agent.client, "acp.attachedElsewhere", { id, reason: "This session was attached in another Oxbit window" });
7703
+ this.detach(client, id);
7704
+ agent.client = client;
7705
+ agent.editorTools = editorTools;
7706
+ if (!editorTools) this.rejectEditorRequests(agent, EDITOR_UNSUPPORTED);
7707
+ return {
7708
+ connection: agent.connection,
7709
+ busy: agent.busy,
7710
+ events: [...agent.events],
7711
+ requests: [...agent.approvals].map(([requestId, request]) => this.requestEvent(agent, requestId, request)),
7712
+ queue: [...agent.queue],
7713
+ queuePaused: agent.queuePaused,
7714
+ sequence: agent.eventSequence,
7715
+ truncated: agent.truncated
7716
+ };
7294
7717
  }
7295
- async start(owner, launch, signal) {
7718
+ detach(client, exceptId) {
7719
+ for (const agent of this.agents.values()) {
7720
+ if (agent.client !== client || agent.connection.id === exceptId) continue;
7721
+ agent.client = void 0;
7722
+ for (const [id, request] of agent.approvals) {
7723
+ if (request.method === "fs/read_text_file") {
7724
+ agent.approvals.delete(id);
7725
+ void this.readFile(request.params).then(request.resolve, request.reject);
7726
+ }
7727
+ }
7728
+ this.rejectEditorRequests(agent, "The editor disconnected; attach an Oxbit window to use this tool");
7729
+ }
7730
+ }
7731
+ rejectEditorRequests(agent, reason) {
7732
+ for (const [id, request] of agent.approvals) {
7733
+ if (!request.method.startsWith("oxbit/")) continue;
7734
+ agent.approvals.delete(id);
7735
+ request.reject(new Error(reason));
7736
+ }
7737
+ }
7738
+ working(agent) {
7739
+ return agent.busy || agent.queueRunning || !!agent.approvals.size || !!agent.subagents?.activeCount || !!agent.queue.length;
7740
+ }
7741
+ get hasWork() {
7742
+ return [...this.agents.values()].some((a) => this.working(a));
7743
+ }
7744
+ async start(owner, launch, signal, client = owner) {
7296
7745
  const preset2 = ACP_PROVIDERS.find((p) => p.id === launch.provider);
7297
7746
  if (!preset2)
7298
7747
  throw new RpcError("INVALID_PARAMS", "Choose Codex, Cursor, or Amp");
7299
7748
  if (signal?.aborted) throw new Error("Agent connection cancelled");
7300
- if ([...this.agents.values()].filter((a) => a.owner === owner).length >= 3)
7301
- throw new RpcError("BUSY", "Disconnect an agent before starting another");
7749
+ const owned = [...this.agents.values()].filter((a) => a.owner === owner);
7750
+ if (owned.length >= 3) {
7751
+ const idle = owned.find((a) => !a.client && !this.working(a));
7752
+ if (!idle)
7753
+ throw new RpcError("BUSY", "3 agents are running for this device; stop one in Runtime sessions");
7754
+ this.stopAgent(idle, "Stopped to start another agent");
7755
+ }
7302
7756
  const command = requireString(
7303
7757
  { command: launch.command ?? preset2.command },
7304
7758
  "command"
@@ -7316,6 +7770,8 @@ class AgentACP {
7316
7770
  trackChild(child);
7317
7771
  const agent = {
7318
7772
  owner,
7773
+ client,
7774
+ editorTools: launch.clientCapabilities?.editorTools === true,
7319
7775
  child,
7320
7776
  connection: {
7321
7777
  id: randomUUID(),
@@ -7330,7 +7786,15 @@ class AgentACP {
7330
7786
  busy: false,
7331
7787
  stopped: false,
7332
7788
  sequence: 0,
7333
- turn: 0
7789
+ turn: 0,
7790
+ events: [],
7791
+ eventBytes: 0,
7792
+ eventSequence: 0,
7793
+ truncated: false,
7794
+ title: "New conversation",
7795
+ queue: [],
7796
+ queuePaused: false,
7797
+ queueRunning: false
7334
7798
  };
7335
7799
  this.agents.set(agent.connection.id, agent);
7336
7800
  const abort = () => this.stopAgent(agent, "Agent connection cancelled");
@@ -7390,7 +7854,7 @@ class AgentACP {
7390
7854
  terminal: true,
7391
7855
  ...preset2.id === "codex" ? { subagents: {} } : {}
7392
7856
  },
7393
- clientInfo: { name: "oxbit", title: "Oxbit", version: "0.1.0" }
7857
+ clientInfo: { name: "oxbit", title: "Oxbit", version: runtimeVersion() }
7394
7858
  });
7395
7859
  if (initialized.protocolVersion !== 1)
7396
7860
  throw new Error("Agent does not support ACP version 1");
@@ -7460,12 +7924,23 @@ class AgentACP {
7460
7924
  const previous = { ...agent.connection };
7461
7925
  const previousRegistry = agent.subagents;
7462
7926
  const previousTerminals = agent.terminals;
7927
+ const previousEvents = agent.events;
7928
+ const previousBytes = agent.eventBytes;
7929
+ const previousTitle = agent.title;
7930
+ const previousTruncated = agent.truncated;
7463
7931
  const target = method === "session/new" ? void 0 : requireString(params, "sessionId");
7464
7932
  if (agent.approvals.size)
7465
7933
  throw new Error(
7466
7934
  "Resolve pending requests before changing conversations"
7467
7935
  );
7936
+ if (agent.queue.length)
7937
+ throw new Error("Remove queued messages before changing conversations");
7468
7938
  agent.busy = true;
7939
+ agent.events = [];
7940
+ agent.eventBytes = 0;
7941
+ agent.truncated = false;
7942
+ agent.title = "New conversation";
7943
+ agent.queuePaused = false;
7469
7944
  agent.connection.sessionId = target;
7470
7945
  agent.subagents = target ? this.registry(agent, target, true) : void 0;
7471
7946
  agent.terminals = /* @__PURE__ */ new Map();
@@ -7473,7 +7948,7 @@ class AgentACP {
7473
7948
  try {
7474
7949
  const result = await this.request(agent, method, {
7475
7950
  cwd: this.files.root,
7476
- mcpServers: [],
7951
+ mcpServers: [await this.mcp.server(agent.connection.id, !!agent.connection.capabilities?.mcpCapabilities?.http)],
7477
7952
  ...target ? { sessionId: target } : {}
7478
7953
  });
7479
7954
  const sessionId2 = target ?? result?.sessionId;
@@ -7498,6 +7973,10 @@ class AgentACP {
7498
7973
  killProcess(terminal2.child);
7499
7974
  agent.terminals = previousTerminals;
7500
7975
  agent.subagents = previousRegistry;
7976
+ agent.events = previousEvents;
7977
+ agent.eventBytes = previousBytes;
7978
+ agent.title = previousTitle;
7979
+ agent.truncated = previousTruncated;
7501
7980
  throw error;
7502
7981
  } finally {
7503
7982
  agent.newSessionUpdates = void 0;
@@ -7568,35 +8047,119 @@ ${content}`
7568
8047
  throw new Error("The message and attachments are too large");
7569
8048
  if (agent.busy || agent.stopped || agent.connection.sessionId !== sessionId)
7570
8049
  throw new Error("Agent connection is busy or ended");
8050
+ const messageId = params.messageId === void 0 ? randomUUID() : requireString(params, "messageId", 256);
7571
8051
  agent.busy = true;
7572
8052
  agent.turn++;
8053
+ if (agent.title === "New conversation") agent.title = text2.slice(0, 100);
8054
+ this.publish(agent, "acp.turnStarted", { messageId, text: text2, context: params.context ?? [] });
7573
8055
  const abort = () => this.stopAgent(agent, "Agent request interrupted");
7574
8056
  signal?.addEventListener("abort", abort, { once: true });
8057
+ let stopReason;
8058
+ let failure2;
7575
8059
  try {
7576
- return await this.request(
8060
+ const result = await this.request(
7577
8061
  agent,
7578
8062
  method,
7579
8063
  { sessionId, prompt },
7580
8064
  30 * 60 * 1e3
7581
8065
  );
8066
+ stopReason = result?.stopReason;
8067
+ if (stopReason !== "end_turn" && stopReason !== "cancelled") agent.queuePaused = true;
8068
+ return result;
8069
+ } catch (error) {
8070
+ failure2 = error instanceof Error ? error.message : String(error);
8071
+ agent.queuePaused = true;
8072
+ throw error;
7582
8073
  } finally {
7583
8074
  this.expireRequests(agent, sessionId);
7584
8075
  agent.busy = false;
7585
8076
  signal?.removeEventListener("abort", abort);
8077
+ if (!agent.stopped) {
8078
+ this.publish(agent, "acp.turnEnded", { stopReason, error: failure2 });
8079
+ this.publishQueue(agent);
8080
+ queueMicrotask(() => this.runQueue(agent));
8081
+ }
7586
8082
  }
7587
8083
  }
7588
- cancel(owner, id) {
8084
+ publishQueue(agent) {
8085
+ this.publish(agent, "acp.queue", { queue: agent.queue, paused: agent.queuePaused });
8086
+ return { queue: [...agent.queue], paused: agent.queuePaused };
8087
+ }
8088
+ async enqueue(owner, id, params, interrupt = false) {
8089
+ const agent = this.get(owner, id);
8090
+ if (!agent.connection.sessionId) throw new Error("Start a conversation first");
8091
+ const text2 = requireString(params, "text", MAX_BYTES).trim();
8092
+ if (!text2) throw new Error("Enter a message");
8093
+ const messageId = params.messageId === void 0 ? randomUUID() : requireString(params, "messageId", 256);
8094
+ if (params.context !== void 0) {
8095
+ if (!Array.isArray(params.context) || params.context.length > 8)
8096
+ throw new Error("Attach up to eight context items");
8097
+ for (const item of params.context) {
8098
+ requireString(item, "text", 2e5);
8099
+ await this.files.resolve(requireString(item, "path"));
8100
+ }
8101
+ }
8102
+ if (agent.stopped) throw new Error("Agent connection has ended");
8103
+ if (agent.queue.some((p) => p.id === messageId)) return this.publishQueue(agent);
8104
+ const prompt = { id: messageId, text: text2, ...params.context ? { context: structuredClone(params.context) } : {} };
8105
+ if (agent.queue.length >= 16 || Buffer.byteLength(JSON.stringify([...agent.queue, prompt])) > MAX_BYTES)
8106
+ throw new Error("Queued messages exceed the limit; remove a message first");
8107
+ if (interrupt) {
8108
+ agent.queue.unshift(prompt);
8109
+ agent.queuePaused = false;
8110
+ if (agent.busy) this.cancel(owner, id, true);
8111
+ } else agent.queue.push(prompt);
8112
+ const result = this.publishQueue(agent);
8113
+ queueMicrotask(() => this.runQueue(agent));
8114
+ return result;
8115
+ }
8116
+ dequeue(owner, id, promptId) {
8117
+ const agent = this.get(owner, id);
8118
+ const index = agent.queue.findIndex((p) => p.id === promptId);
8119
+ if (index < 0) throw new Error("The queued message has already started or was removed");
8120
+ const [prompt] = agent.queue.splice(index, 1);
8121
+ this.publishQueue(agent);
8122
+ return { prompt };
8123
+ }
8124
+ resumeQueue(owner, id) {
7589
8125
  const agent = this.get(owner, id);
8126
+ agent.queuePaused = false;
8127
+ this.publishQueue(agent);
8128
+ queueMicrotask(() => this.runQueue(agent));
8129
+ return {};
8130
+ }
8131
+ runQueue(agent) {
8132
+ if (agent.busy || agent.queueRunning || agent.stopped || agent.queuePaused || !agent.queue.length) return;
8133
+ agent.queueRunning = true;
8134
+ const prompt = agent.queue.shift();
8135
+ this.publishQueue(agent);
8136
+ void this.call(agent.owner, agent.connection.id, "session/prompt", {
8137
+ text: prompt.text,
8138
+ context: prompt.context,
8139
+ messageId: prompt.id
8140
+ }).catch(() => {
8141
+ if (agent.stopped) return;
8142
+ agent.queuePaused = true;
8143
+ agent.queue.unshift(prompt);
8144
+ this.publishQueue(agent);
8145
+ }).finally(() => {
8146
+ agent.queueRunning = false;
8147
+ this.runQueue(agent);
8148
+ });
8149
+ }
8150
+ cancel(owner, id, continueQueue = false) {
8151
+ const agent = this.get(owner, id);
8152
+ agent.queuePaused = !continueQueue;
8153
+ this.publishQueue(agent);
7590
8154
  this.send(agent, {
7591
8155
  method: "session/cancel",
7592
8156
  params: { sessionId: agent.connection.sessionId }
7593
8157
  });
7594
8158
  for (const [requestId, request] of agent.approvals) {
7595
8159
  if (request.params.sessionId !== agent.connection.sessionId) continue;
7596
- this.send(agent, {
7597
- id: request.rpcId,
7598
- ...request.method === "session/request_permission" ? { result: { outcome: { outcome: "cancelled" } } } : { error: { code: -32800, message: "Cancelled" } }
7599
- });
8160
+ if (request.method === "session/request_permission")
8161
+ request.resolve({ outcome: { outcome: "cancelled" } });
8162
+ else request.reject(new Error("Cancelled"));
7600
8163
  agent.approvals.delete(requestId);
7601
8164
  agent.subagents?.waiting(
7602
8165
  request.params.sessionId,
@@ -7634,6 +8197,8 @@ ${content}`
7634
8197
  routeUpdate(agent, sessionId, update) {
7635
8198
  if (!update || typeof update !== "object" || !agent.subagents?.observe(sessionId, update))
7636
8199
  return;
8200
+ if (sessionId === agent.connection.sessionId && update.sessionUpdate === "session_info_update" && typeof update.title === "string")
8201
+ agent.title = update.title.slice(0, 200);
7637
8202
  this.publish(agent, "acp.update", {
7638
8203
  rootSessionId: agent.subagents.root,
7639
8204
  sessionId,
@@ -7650,11 +8215,9 @@ ${content}`
7650
8215
  const requestIds = [];
7651
8216
  for (const [id, request] of agent.approvals) {
7652
8217
  if (request.params.sessionId !== sessionId) continue;
7653
- if (!agent.stopped)
7654
- this.send(agent, {
7655
- id: request.rpcId,
7656
- ...request.method === "session/request_permission" ? { result: { outcome: { outcome: "cancelled" } } } : { error: { code: -32800, message: "Session turn ended" } }
7657
- });
8218
+ if (request.method === "session/request_permission")
8219
+ request.resolve({ outcome: { outcome: "cancelled" } });
8220
+ else request.reject(new Error("Session turn ended"));
7658
8221
  agent.approvals.delete(id);
7659
8222
  requestIds.push(id);
7660
8223
  agent.subagents?.waiting(
@@ -7677,6 +8240,136 @@ ${content}`
7677
8240
  await this.files.resolve(relative, missing2);
7678
8241
  return relative;
7679
8242
  }
8243
+ requestEvent(agent, requestId, request) {
8244
+ const sessionId = request.params.sessionId;
8245
+ return {
8246
+ id: agent.connection.id,
8247
+ requestId,
8248
+ sessionId,
8249
+ rootSessionId: agent.subagents.root,
8250
+ subagentId: agent.subagents.idForSession(sessionId) ?? agent.subagents.idForTool(sessionId, request.params.toolCallId ?? request.params.toolCall?.toolCallId),
8251
+ method: request.method,
8252
+ params: request.params
8253
+ };
8254
+ }
8255
+ async readFile(params) {
8256
+ const file = await this.files.read(params.path);
8257
+ const start = (params.line ?? 1) - 1;
8258
+ const content = params.line !== void 0 || params.limit !== void 0 ? file.text.split("\n").slice(start, params.limit ? start + params.limit : void 0).join("\n") : file.text;
8259
+ if (Buffer.byteLength(content) > MAX_BYTES)
8260
+ throw new Error("File content exceeds 1 MiB; request a smaller line range");
8261
+ return { content };
8262
+ }
8263
+ async clientRequest(agent, method, params) {
8264
+ const registry = agent.subagents;
8265
+ const sessionId = params.sessionId;
8266
+ if (!registry?.acceptsRequest(sessionId)) throw new Error("Unknown ACP session");
8267
+ if (agent.approvals.size >= 32) throw new Error("Too many pending agent requests");
8268
+ if (method.startsWith("fs/")) {
8269
+ params.path = await this.relative(params.path, method === "fs/write_text_file");
8270
+ for (const field of ["line", "limit"])
8271
+ if (params[field] !== void 0 && (!Number.isSafeInteger(params[field]) || params[field] < 1))
8272
+ throw new Error(`Invalid ${field}`);
8273
+ if (method === "fs/write_text_file") requireString(params, "content", MAX_BYTES);
8274
+ }
8275
+ if (agent.stopped || registry !== agent.subagents || !registry.acceptsRequest(sessionId))
8276
+ throw new Error("ACP session ended");
8277
+ if (!agent.client && method === "fs/read_text_file") return this.readFile(params);
8278
+ return new Promise((resolve, reject) => {
8279
+ const requestId = randomUUID();
8280
+ const request = { method, params, resolve, reject };
8281
+ agent.approvals.set(requestId, request);
8282
+ if (method !== "fs/read_text_file" && !method.startsWith("oxbit/"))
8283
+ registry.waiting(sessionId, requestId, true, params.toolCallId ?? params.toolCall?.toolCallId);
8284
+ this.publish(agent, "acp.request", this.requestEvent(agent, requestId, request));
8285
+ });
8286
+ }
8287
+ async tool(id, name, args) {
8288
+ const agent = this.agents.get(id);
8289
+ const sessionId = agent?.connection.sessionId;
8290
+ if (!agent || agent.stopped || !sessionId) throw new Error("Start an agent conversation first");
8291
+ const registry = agent.subagents;
8292
+ const toolCallId = `oxbit-${randomUUID()}`;
8293
+ const write = name === "oxbit_propose_edit";
8294
+ let relative;
8295
+ if (args.path !== void 0) {
8296
+ const input = requireString(args, "path");
8297
+ relative = path.isAbsolute(input) ? await this.relative(input, write) : input;
8298
+ await this.files.resolve(relative, write);
8299
+ }
8300
+ for (const key of ["line", "limit"])
8301
+ if (args[key] !== void 0 && (!Number.isSafeInteger(args[key]) || args[key] < 1))
8302
+ throw new Error(`Invalid ${key}`);
8303
+ const publish = (update) => {
8304
+ if (!agent.stopped && registry === agent.subagents)
8305
+ this.routeUpdate(agent, sessionId, update);
8306
+ };
8307
+ const editor = () => {
8308
+ if (agent.client && !agent.editorTools) throw new Error(EDITOR_UNSUPPORTED);
8309
+ return !!agent.client;
8310
+ };
8311
+ publish({
8312
+ sessionUpdate: "tool_call",
8313
+ toolCallId,
8314
+ title: name,
8315
+ kind: write ? "edit" : "read",
8316
+ status: "in_progress",
8317
+ rawInput: args,
8318
+ ...relative ? { locations: [{ path: path.join(this.files.root, relative), line: args.line }] } : {}
8319
+ });
8320
+ try {
8321
+ let result;
8322
+ const params = { ...args, path: relative, sessionId, toolCallId };
8323
+ switch (name) {
8324
+ case "oxbit_get_workspace":
8325
+ result = {
8326
+ root: this.files.root,
8327
+ clientConnected: !!agent.client,
8328
+ ...editor() ? await this.clientRequest(agent, "oxbit/workspace", params) : { activeFile: null, openFiles: [], selection: null, diagnostics: [] }
8329
+ };
8330
+ break;
8331
+ case "oxbit_list_files": {
8332
+ const entries = await this.files.list(relative ?? "");
8333
+ result = { entries: entries.slice(0, 1e3), truncated: entries.length > 1e3 };
8334
+ break;
8335
+ }
8336
+ case "oxbit_read_file":
8337
+ case "oxbit_propose_edit":
8338
+ if (relative === void 0) throw new Error("A workspace path is required");
8339
+ result = await this.clientRequest(agent, write ? "fs/write_text_file" : "fs/read_text_file", {
8340
+ ...params,
8341
+ path: path.join(this.files.root, relative)
8342
+ });
8343
+ break;
8344
+ case "oxbit_get_diagnostics":
8345
+ result = editor() ? await this.clientRequest(agent, "oxbit/diagnostics", params) : { diagnostics: [], available: false, reason: "Attach an Oxbit editor for language diagnostics" };
8346
+ break;
8347
+ case "oxbit_open_file":
8348
+ if (relative === void 0) throw new Error("A workspace path is required");
8349
+ if (!editor()) throw new Error("Attach an Oxbit editor to open a file");
8350
+ result = await this.clientRequest(agent, "oxbit/open_file", params);
8351
+ break;
8352
+ case "oxbit_update_plan": {
8353
+ if (!Array.isArray(args.entries) || args.entries.length > 50) throw new Error("Provide up to 50 plan entries");
8354
+ const entries = args.entries.map((entry) => {
8355
+ const content = requireString(entry, "content", 2e3);
8356
+ if (!["pending", "in_progress", "completed"].includes(entry.status)) throw new Error("Invalid plan status");
8357
+ return { content, status: entry.status, ...typeof entry.priority === "string" ? { priority: entry.priority.slice(0, 40) } : {} };
8358
+ });
8359
+ publish({ sessionUpdate: "plan", entries });
8360
+ result = { entries };
8361
+ break;
8362
+ }
8363
+ default:
8364
+ throw new Error("Unknown Oxbit tool");
8365
+ }
8366
+ publish({ sessionUpdate: "tool_call_update", toolCallId, status: "completed", rawOutput: JSON.stringify(result ?? {}).slice(0, 8e3) });
8367
+ return result ?? {};
8368
+ } catch (error) {
8369
+ publish({ sessionUpdate: "tool_call_update", toolCallId, status: "failed", rawOutput: String(error) });
8370
+ throw error;
8371
+ }
8372
+ }
7680
8373
  async message(agent, message) {
7681
8374
  if (agent.stopped) return;
7682
8375
  if (!message.method) {
@@ -7721,8 +8414,8 @@ ${content}`
7721
8414
  throw new Error("Unknown ACP session");
7722
8415
  params.sessionId = sessionId;
7723
8416
  if (message.method.startsWith("terminal/")) {
7724
- const result = await this.terminal(agent, message.method, params);
7725
- this.send(agent, { id: message.id, result });
8417
+ const result2 = await this.terminal(agent, message.method, params);
8418
+ this.send(agent, { id: message.id, result: result2 });
7726
8419
  return;
7727
8420
  }
7728
8421
  if (![
@@ -7738,46 +8431,8 @@ ${content}`
7738
8431
  });
7739
8432
  return;
7740
8433
  }
7741
- if (agent.approvals.size >= 32)
7742
- throw new Error("Too many pending agent requests");
7743
- if (message.method.startsWith("fs/")) {
7744
- params.path = await this.relative(
7745
- params.path,
7746
- message.method === "fs/write_text_file"
7747
- );
7748
- for (const field of ["line", "limit"])
7749
- if (params[field] !== void 0 && (!Number.isSafeInteger(params[field]) || params[field] < 1))
7750
- throw new Error(`Invalid ${field}`);
7751
- if (message.method === "fs/write_text_file")
7752
- requireString(params, "content", MAX_BYTES);
7753
- }
7754
- if (agent.stopped) return;
7755
- if (registry !== agent.subagents || !registry.acceptsRequest(sessionId))
7756
- throw new Error("ACP session ended");
7757
- const requestId = randomUUID();
7758
- agent.approvals.set(requestId, {
7759
- rpcId: message.id,
7760
- method: message.method,
7761
- params
7762
- });
7763
- if (message.method !== "fs/read_text_file")
7764
- registry.waiting(
7765
- sessionId,
7766
- requestId,
7767
- true,
7768
- params.toolCallId ?? params.toolCall?.toolCallId
7769
- );
7770
- this.publish(agent, "acp.request", {
7771
- sessionId,
7772
- rootSessionId: registry.root,
7773
- subagentId: registry.idForSession(sessionId) ?? registry.idForTool(
7774
- sessionId,
7775
- params.toolCallId ?? params.toolCall?.toolCallId
7776
- ),
7777
- requestId,
7778
- method: message.method,
7779
- params
7780
- });
8434
+ const result = await this.clientRequest(agent, message.method, params);
8435
+ if (!agent.stopped) this.send(agent, { id: message.id, result });
7781
8436
  } catch (error) {
7782
8437
  if (!agent.stopped)
7783
8438
  this.send(agent, {
@@ -7796,10 +8451,8 @@ ${content}`
7796
8451
  )))
7797
8452
  throw new Error("Invalid permission response");
7798
8453
  }
7799
- this.send(agent, {
7800
- id: request.rpcId,
7801
- ...error ? { error: { code: -32603, message: error.slice(0, 4096) } } : { result }
7802
- });
8454
+ if (error) request.reject(new Error(error.slice(0, 4096)));
8455
+ else request.resolve(result);
7803
8456
  agent.approvals.delete(requestId);
7804
8457
  agent.subagents?.waiting(
7805
8458
  request.params.sessionId,
@@ -7934,26 +8587,29 @@ ${content}`
7934
8587
  pending.reject(new Error(reason));
7935
8588
  }
7936
8589
  agent.pending.clear();
8590
+ for (const approval of agent.approvals.values()) approval.reject(new Error(reason));
7937
8591
  agent.approvals.clear();
7938
8592
  for (const terminal2 of agent.terminals.values())
7939
8593
  killProcess(terminal2.child);
7940
8594
  agent.terminals.clear();
7941
8595
  killProcess(agent.child);
7942
8596
  this.agents.delete(agent.connection.id);
8597
+ this.mcp.revoke(agent.connection.id);
7943
8598
  this.publish(agent, "acp.exit", { reason });
7944
8599
  }
7945
8600
  stop(owner, id) {
7946
8601
  this.stopAgent(this.get(owner, id), "Agent disconnected");
7947
8602
  return {};
7948
8603
  }
7949
- disconnect(owner) {
8604
+ disconnect(owner, client) {
7950
8605
  for (const agent of this.agents.values())
7951
- if (agent.owner === owner)
8606
+ if (agent.owner === owner && (client === void 0 || agent.client === client))
7952
8607
  this.stopAgent(agent, "Agent client disconnected");
7953
8608
  }
7954
8609
  dispose() {
7955
8610
  for (const agent of this.agents.values())
7956
8611
  this.stopAgent(agent, "Agent tooling stopped");
8612
+ return this.mcp.close();
7957
8613
  }
7958
8614
  }
7959
8615
  var re = { exports: {} };
@@ -10150,12 +10806,12 @@ function createKernel({
10150
10806
  get(id, language) {
10151
10807
  return configuration.inspect(id, language).value;
10152
10808
  },
10153
- inspect(id, language) {
10809
+ inspect(id, language, scope = "workspace") {
10154
10810
  const scopes = language ? [.../* @__PURE__ */ new Set([language, canonicalLanguageId(language), ...languages.find((item) => item.id === canonicalLanguageId(language))?.settingsAliases ?? []])] : [];
10155
10811
  const ordered = [
10156
- ...scopes.map((id2) => configurationData.workspaceLanguages[id2]),
10812
+ ...scope === "workspace" ? scopes.map((id2) => configurationData.workspaceLanguages[id2]) : [],
10157
10813
  ...scopes.map((id2) => configurationData.userLanguages[id2]),
10158
- configurationData.workspace,
10814
+ ...scope === "workspace" ? [configurationData.workspace] : [],
10159
10815
  configurationData.user
10160
10816
  ];
10161
10817
  let value = structuredClone(settings.get(id)?.default);
@@ -10942,11 +11598,6 @@ class RuntimeExtensions {
10942
11598
  this.watchers.clear();
10943
11599
  }
10944
11600
  }
10945
- let cached;
10946
- function runtimeVersion() {
10947
- if (cached) return cached;
10948
- return cached = "0.6.1";
10949
- }
10950
11601
  const wildcardHost = (host) => host === "0.0.0.0" || host === "::";
10951
11602
  const localHost = (host) => wildcardHost(host) ? "127.0.0.1" : host;
10952
11603
  const localName = (hostname = os.hostname()) => hostname.split(".")[0];
@@ -10979,7 +11630,7 @@ function lanUrl(port2, interfaces = os.networkInterfaces()) {
10979
11630
  const address = lanAddresses$1(interfaces).v4[0];
10980
11631
  return address ? `http://${address}:${port2}` : void 0;
10981
11632
  }
10982
- const MAX_SOCKET_BACKLOG_BYTES = 16 * MAX_BUFFER_BYTES;
11633
+ const MAX_SOCKET_BACKLOG_BYTES = MAX_FILE_WRITE_MESSAGE_BYTES;
10983
11634
  const allCapabilities = [
10984
11635
  "filesystem.read",
10985
11636
  "filesystem.write",
@@ -11024,7 +11675,8 @@ async function createRuntime(options2) {
11024
11675
  const dataDir = path.resolve(options2.dataDir ?? workspaceDataDir(root));
11025
11676
  await fs.mkdir(dataDir, { recursive: true, mode: 448 });
11026
11677
  const stateFile = path.join(dataDir, "runtime.json");
11027
- const files = new WorkspaceFiles(root, dataDir), sessions = /* @__PURE__ */ new Map(), operations = /* @__PURE__ */ new Map(), inflight = /* @__PURE__ */ new Map(), connections = /* @__PURE__ */ new Map();
11678
+ const files = new WorkspaceFiles(root, dataDir), sessions = /* @__PURE__ */ new Map(), inflight = /* @__PURE__ */ new Map(), connections = /* @__PURE__ */ new Map();
11679
+ let operations = new OperationHistory();
11028
11680
  let trusted = false, closed = false, runtimeId = randomUUID();
11029
11681
  try {
11030
11682
  const state2 = JSON.parse(await fs.readFile(stateFile, "utf8"));
@@ -11032,16 +11684,7 @@ async function createRuntime(options2) {
11032
11684
  if (typeof state2.id === "string" && /^[A-Za-z0-9-]{8,64}$/.test(state2.id)) runtimeId = state2.id;
11033
11685
  for (const session of state2.sessions ?? [])
11034
11686
  sessions.set(session.hash, session);
11035
- for (const operation of state2.operations ?? []) {
11036
- if (operation.status === "running") {
11037
- operation.status = "interrupted";
11038
- operation.error = {
11039
- code: "INTERRUPTED",
11040
- message: "Runtime stopped before operation completion was recorded; inspect state before retrying with a new request ID"
11041
- };
11042
- }
11043
- operations.set(`${operation.owner}:${operation.id}`, operation);
11044
- }
11687
+ operations = new OperationHistory(state2.operationHistory, state2.operations ?? []);
11045
11688
  } catch (error) {
11046
11689
  if (error.code !== "ENOENT")
11047
11690
  throw new Error(`Cannot recover runtime state: ${String(error)}`);
@@ -11064,6 +11707,7 @@ async function createRuntime(options2) {
11064
11707
  id: runtimeId,
11065
11708
  trusted,
11066
11709
  sessions: [...sessions.values()],
11710
+ operationHistory: operations.checkpoint(),
11067
11711
  operations: [...operations.values()]
11068
11712
  });
11069
11713
  persistence = persistence.catch(() => {
@@ -11130,7 +11774,7 @@ async function createRuntime(options2) {
11130
11774
  const agents = new AgentACP(files, (connectionId, name, params) => {
11131
11775
  const connection = connections.get(connectionId);
11132
11776
  if (connection?.session && !connection.session.revoked && trusted) event(connection, name, params);
11133
- });
11777
+ }, () => updateIdle());
11134
11778
  const processEvent = ({ event: name, params, stream, seq }, ownerId) => {
11135
11779
  const cap = name.startsWith("terminal.") ? "terminal" : "tasks";
11136
11780
  for (const c of connections.values()) {
@@ -11220,7 +11864,8 @@ async function createRuntime(options2) {
11220
11864
  capabilities: session.capabilities,
11221
11865
  owner: session.owner,
11222
11866
  trusted,
11223
- sessionId: session.id
11867
+ sessionId: session.id,
11868
+ operationEpoch: operations.epoch
11224
11869
  });
11225
11870
  const createSession = async (isOwner, capabilities) => {
11226
11871
  const token = randomBytes(32).toString("base64url"), session = {
@@ -11239,6 +11884,7 @@ async function createRuntime(options2) {
11239
11884
  const session = [...sessions.values()].find((s) => s.id === id);
11240
11885
  if (!session) throw new RpcError("NOT_FOUND", "Grant does not exist");
11241
11886
  session.revoked = true;
11887
+ agents.disconnect(session.id);
11242
11888
  processes.revoke(session.id);
11243
11889
  tasks.revoke(session.id);
11244
11890
  for (const c of connections.values())
@@ -11442,7 +12088,7 @@ async function createRuntime(options2) {
11442
12088
  });
11443
12089
  const websocket = new WebSocketServer({
11444
12090
  noServer: true,
11445
- maxPayload: MAX_MESSAGE_BYTES,
12091
+ maxPayload: MAX_FILE_WRITE_MESSAGE_BYTES,
11446
12092
  perMessageDeflate: false
11447
12093
  });
11448
12094
  server.on("upgrade", (request, socket, head) => {
@@ -11502,7 +12148,11 @@ async function createRuntime(options2) {
11502
12148
  if (workspace !== "default")
11503
12149
  throw new RpcError("FORBIDDEN", "Workspace is not granted");
11504
12150
  const required = permission(method), session = authorized(connection, required.cap, required.trust);
11505
- if (method.startsWith("acp.")) owner(connection);
12151
+ if (method.startsWith("acp.")) {
12152
+ owner(connection);
12153
+ if (!["acp.start", "acp.list", "acp.attach", "acp.disconnect"].includes(method))
12154
+ agents.control(session.id, requireString(params, "id"), connection.id);
12155
+ }
11506
12156
  if (method.startsWith("project.")) owner(connection);
11507
12157
  if (method.startsWith("settings.")) owner(connection);
11508
12158
  switch (method) {
@@ -11525,17 +12175,28 @@ async function createRuntime(options2) {
11525
12175
  case "project.relations":
11526
12176
  return project.relations(requireString(params, "path"));
11527
12177
  case "acp.start":
11528
- return agents.start(connection.id, params, signal);
12178
+ return agents.start(session.id, params, signal, connection.id);
12179
+ case "acp.list":
12180
+ return agents.list(session.id);
12181
+ case "acp.attach":
12182
+ return agents.attach(session.id, requireString(params, "id"), connection.id, params.clientCapabilities?.editorTools === true);
12183
+ case "acp.enqueue":
12184
+ case "acp.interrupt":
12185
+ return agents.enqueue(session.id, requireString(params, "id"), params, method === "acp.interrupt");
12186
+ case "acp.dequeue":
12187
+ return agents.dequeue(session.id, requireString(params, "id"), requireString(params, "promptId"));
12188
+ case "acp.resumeQueue":
12189
+ return agents.resumeQueue(session.id, requireString(params, "id"));
11529
12190
  case "acp.call":
11530
- return agents.call(connection.id, requireString(params, "id"), requireString(params, "method"), params.params ?? {}, signal);
12191
+ return agents.call(session.id, requireString(params, "id"), requireString(params, "method"), params.params ?? {});
11531
12192
  case "acp.respond":
11532
- return agents.respond(connection.id, requireString(params, "id"), requireString(params, "requestId"), params.result, params.error === void 0 ? void 0 : requireString(params, "error"));
12193
+ return agents.respond(session.id, requireString(params, "id"), requireString(params, "requestId"), params.result, params.error === void 0 ? void 0 : requireString(params, "error"));
11533
12194
  case "acp.cancel":
11534
- return agents.cancel(connection.id, requireString(params, "id"));
12195
+ return agents.cancel(session.id, requireString(params, "id"));
11535
12196
  case "acp.stop":
11536
- return agents.stop(connection.id, requireString(params, "id"));
12197
+ return agents.stop(session.id, requireString(params, "id"));
11537
12198
  case "acp.disconnect":
11538
- agents.disconnect(connection.id);
12199
+ agents.disconnect(session.id, connection.id);
11539
12200
  return {};
11540
12201
  case "workspace.info":
11541
12202
  return sessionInfo(session);
@@ -11545,7 +12206,7 @@ async function createRuntime(options2) {
11545
12206
  throw new RpcError("INVALID_PARAMS", "trusted must be boolean");
11546
12207
  trusted = params.trusted;
11547
12208
  if (!trusted) {
11548
- agents.dispose();
12209
+ await agents.dispose();
11549
12210
  await extensions.suspend();
11550
12211
  for (const c of connections.values()) for (const pending of c.pending.values()) pending.abort();
11551
12212
  for (const s of sessions.values()) {
@@ -11573,10 +12234,7 @@ async function createRuntime(options2) {
11573
12234
  owner(connection);
11574
12235
  return revoke(requireString(params, "id"));
11575
12236
  case "operation.status": {
11576
- const value = operations.get(
11577
- `${session.id}:${requireString(params, "id")}`
11578
- );
11579
- return value ?? { id: params.id, status: "unknown" };
12237
+ return operations.status(session.id, requireString(params, "id"));
11580
12238
  }
11581
12239
  case "fs.list":
11582
12240
  return files.list(typeof params.path === "string" ? params.path : "");
@@ -11594,7 +12252,7 @@ async function createRuntime(options2) {
11594
12252
  case "fs.write": {
11595
12253
  const snapshot = await files.write(
11596
12254
  requireString(params, "path"),
11597
- requireString(params, "text", 20 * 1024 * 1024),
12255
+ requireString(params, "text", MAX_FILE_BYTES),
11598
12256
  {
11599
12257
  expectedRevision: requireRevision(params),
11600
12258
  encoding: params.encoding,
@@ -11857,7 +12515,7 @@ async function createRuntime(options2) {
11857
12515
  return collaboration.update(
11858
12516
  requireString(params, "path"),
11859
12517
  connection.id,
11860
- requireString(params, "update", MAX_MESSAGE_BYTES)
12518
+ requireString(params, "update", MAX_COLLABORATION_UPDATE_BYTES)
11861
12519
  );
11862
12520
  case "collab.awareness":
11863
12521
  return collaboration.awareness(
@@ -11967,7 +12625,7 @@ async function createRuntime(options2) {
11967
12625
  };
11968
12626
  let idleSince, idleTimer;
11969
12627
  const updateIdle = () => {
11970
- if ([...connections.values()].some((c) => c.session && !c.session.revoked)) {
12628
+ if (agents.hasWork || [...connections.values()].some((c) => c.session && !c.session.revoked)) {
11971
12629
  idleSince = void 0;
11972
12630
  clearTimeout(idleTimer);
11973
12631
  return;
@@ -12057,7 +12715,7 @@ async function createRuntime(options2) {
12057
12715
  let result;
12058
12716
  if (durableMethods.has(method)) {
12059
12717
  const required = permission(method), session = authorized(connection, required.cap, required.trust);
12060
- const key = `${session.id}:${id}`, existing = operations.get(key);
12718
+ const key = `${session.id}:${id}`, existing = operations.get(session.id, id);
12061
12719
  if (existing) {
12062
12720
  if (existing.method !== method)
12063
12721
  throw new RpcError(
@@ -12065,6 +12723,8 @@ async function createRuntime(options2) {
12065
12723
  "Request ID was used for another operation"
12066
12724
  );
12067
12725
  if (inflight.has(key)) result = await inflight.get(key);
12726
+ else if (existing.resultExpired)
12727
+ throw new RpcError("OPERATION_RESULT_EXPIRED", "Operation result expired; inspect current state before submitting a new operation", { status: existing.status });
12068
12728
  else if (existing.status === "completed")
12069
12729
  result = existing.result;
12070
12730
  else
@@ -12074,14 +12734,11 @@ async function createRuntime(options2) {
12074
12734
  existing.error?.data
12075
12735
  );
12076
12736
  } else {
12077
- const operation = {
12078
- id,
12079
- owner: session.id,
12080
- method,
12081
- status: "running",
12082
- startedAt: Date.now()
12083
- };
12084
- operations.set(key, operation);
12737
+ const previousEpoch = operations.epoch;
12738
+ const operation = operations.begin(session.id, id, method);
12739
+ if (operations.epoch !== previousEpoch) {
12740
+ for (const c of connections.values()) if (c.session && !c.session.revoked) event(c, "operation.epoch", { epoch: operations.epoch });
12741
+ }
12085
12742
  const run2 = (async () => {
12086
12743
  await persist();
12087
12744
  try {
@@ -12092,13 +12749,11 @@ async function createRuntime(options2) {
12092
12749
  controller.signal,
12093
12750
  id
12094
12751
  );
12095
- operation.status = "completed";
12096
- operation.result = value;
12752
+ operations.complete(operation, value);
12097
12753
  await persist();
12098
12754
  return value;
12099
12755
  } catch (error) {
12100
- operation.status = "failed";
12101
- operation.error = errorShape(error);
12756
+ operations.fail(operation, errorShape(error));
12102
12757
  await persist();
12103
12758
  throw error;
12104
12759
  }
@@ -12143,7 +12798,7 @@ async function createRuntime(options2) {
12143
12798
  });
12144
12799
  ws.on("close", () => {
12145
12800
  clearTimeout(authDeadline);
12146
- agents.disconnect(connection.id);
12801
+ agents.detach(connection.id);
12147
12802
  lsp.detach(connection.id);
12148
12803
  connections.delete(connection.id);
12149
12804
  updateIdle();
@@ -12155,7 +12810,7 @@ async function createRuntime(options2) {
12155
12810
  processes.disconnect(connection.id);
12156
12811
  void collaboration.leave(connection.id);
12157
12812
  for (const [id, controller] of connection.pending) {
12158
- const operation = connection.session && operations.get(`${connection.session.id}:${id}`);
12813
+ const operation = connection.session && operations.get(connection.session.id, id);
12159
12814
  if (!operation) controller.abort();
12160
12815
  }
12161
12816
  });
@@ -12285,7 +12940,7 @@ async function createRuntime(options2) {
12285
12940
  for (const controller of c.pending.values()) controller.abort();
12286
12941
  c.ws.terminate();
12287
12942
  }
12288
- agents.dispose();
12943
+ await agents.dispose();
12289
12944
  extensions.dispose();
12290
12945
  processes.close();
12291
12946
  await tasks.close();
@@ -12535,17 +13190,17 @@ function advertise(ad, log = stderrLog) {
12535
13190
  try {
12536
13191
  const query = parseMessage(packet);
12537
13192
  if (query.flags & 32768) return;
12538
- const reply = answer(query.questions, current());
12539
- if (!reply) return;
12540
- if (from.port === PORT) return send(encodeMessage(reply));
13193
+ const reply2 = answer(query.questions, current());
13194
+ if (!reply2) return;
13195
+ if (from.port === PORT) return send(encodeMessage(reply2));
12541
13196
  const legacy = (r) => ({ ...r, ttl: Math.min(r.ttl, 10), flush: false });
12542
13197
  send(
12543
13198
  encodeMessage({
12544
13199
  id: query.id,
12545
13200
  flags: FLAGS_RESPONSE,
12546
13201
  questions: query.questions,
12547
- answers: reply.answers.map(legacy),
12548
- additionals: reply.additionals.map(legacy)
13202
+ answers: reply2.answers.map(legacy),
13203
+ additionals: reply2.additionals.map(legacy)
12549
13204
  }),
12550
13205
  from.port,
12551
13206
  from.address