oh-my-opencode-slim 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17233,7 +17233,8 @@ var CouncilConfigSchema = exports_external.object({
17233
17233
  }
17234
17234
  return val;
17235
17235
  }).describe("Fallback models for the council master. Tried in order if the primary model fails. " + 'Example: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"]'),
17236
- councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).')
17236
+ councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
17237
+ councillor_retries: exports_external.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors and master that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries.")
17237
17238
  });
17238
17239
  // src/config/loader.ts
17239
17240
  import * as fs from "fs";
@@ -17263,7 +17264,7 @@ function parseList(items, allAvailable) {
17263
17264
  if (allow.includes("*")) {
17264
17265
  return allAvailable.filter((item) => !deny.includes(item));
17265
17266
  }
17266
- return allow.filter((item) => !deny.includes(item));
17267
+ return allow.filter((item) => !deny.includes(item) && allAvailable.includes(item));
17267
17268
  }
17268
17269
  function getAgentMcpList(agentName, config2) {
17269
17270
  const agentConfig = getAgentOverride(config2, agentName);
@@ -17335,19 +17336,29 @@ var AgentOverrideConfigSchema = exports_external.object({
17335
17336
  skills: exports_external.array(exports_external.string()).optional(),
17336
17337
  mcps: exports_external.array(exports_external.string()).optional()
17337
17338
  });
17338
- var TmuxLayoutSchema = exports_external.enum([
17339
+ var MultiplexerTypeSchema = exports_external.enum(["auto", "tmux", "zellij", "none"]);
17340
+ var MultiplexerLayoutSchema = exports_external.enum([
17339
17341
  "main-horizontal",
17340
17342
  "main-vertical",
17341
17343
  "tiled",
17342
17344
  "even-horizontal",
17343
17345
  "even-vertical"
17344
17346
  ]);
17347
+ var TmuxLayoutSchema = MultiplexerLayoutSchema;
17348
+ var MultiplexerConfigSchema = exports_external.object({
17349
+ type: MultiplexerTypeSchema.default("none"),
17350
+ layout: MultiplexerLayoutSchema.default("main-vertical"),
17351
+ main_pane_size: exports_external.number().min(20).max(80).default(60)
17352
+ });
17345
17353
  var TmuxConfigSchema = exports_external.object({
17346
17354
  enabled: exports_external.boolean().default(false),
17347
17355
  layout: TmuxLayoutSchema.default("main-vertical"),
17348
17356
  main_pane_size: exports_external.number().min(20).max(80).default(60)
17349
17357
  });
17350
17358
  var PresetSchema = exports_external.record(exports_external.string(), AgentOverrideConfigSchema);
17359
+ var WebsearchConfigSchema = exports_external.object({
17360
+ provider: exports_external.enum(["exa", "tavily"]).default("exa")
17361
+ });
17351
17362
  var McpNameSchema = exports_external.enum(["websearch", "context7", "grep_app"]);
17352
17363
  var BackgroundTaskConfigSchema = exports_external.object({
17353
17364
  maxConcurrentStarts: exports_external.number().min(1).max(50).default(10)
@@ -17356,7 +17367,8 @@ var FailoverConfigSchema = exports_external.object({
17356
17367
  enabled: exports_external.boolean().default(true),
17357
17368
  timeoutMs: exports_external.number().min(0).default(15000),
17358
17369
  retryDelayMs: exports_external.number().min(0).default(500),
17359
- chains: FallbackChainsSchema.default({})
17370
+ chains: FallbackChainsSchema.default({}),
17371
+ retry_on_empty: exports_external.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes.")
17360
17372
  });
17361
17373
  var PluginConfigSchema = exports_external.object({
17362
17374
  preset: exports_external.string().optional(),
@@ -17367,7 +17379,9 @@ var PluginConfigSchema = exports_external.object({
17367
17379
  presets: exports_external.record(exports_external.string(), PresetSchema).optional(),
17368
17380
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
17369
17381
  disabled_mcps: exports_external.array(exports_external.string()).optional(),
17382
+ multiplexer: MultiplexerConfigSchema.optional(),
17370
17383
  tmux: TmuxConfigSchema.optional(),
17384
+ websearch: WebsearchConfigSchema.optional(),
17371
17385
  background: BackgroundTaskConfigSchema.optional(),
17372
17386
  fallback: FailoverConfigSchema.optional(),
17373
17387
  council: CouncilConfigSchema.optional()
@@ -17442,9 +17456,12 @@ function loadPluginConfig(directory) {
17442
17456
  ...projectConfig,
17443
17457
  agents: deepMerge(config2.agents, projectConfig.agents),
17444
17458
  tmux: deepMerge(config2.tmux, projectConfig.tmux),
17445
- fallback: deepMerge(config2.fallback, projectConfig.fallback)
17459
+ multiplexer: deepMerge(config2.multiplexer, projectConfig.multiplexer),
17460
+ fallback: deepMerge(config2.fallback, projectConfig.fallback),
17461
+ council: deepMerge(config2.council, projectConfig.council)
17446
17462
  };
17447
17463
  }
17464
+ config2 = migrateTmuxToMultiplexer(config2);
17448
17465
  const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
17449
17466
  if (envPreset) {
17450
17467
  config2.preset = envPreset;
@@ -17486,6 +17503,22 @@ function loadAgentPrompt(agentName, preset) {
17486
17503
  result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
17487
17504
  return result;
17488
17505
  }
17506
+ function migrateTmuxToMultiplexer(config2) {
17507
+ if (config2.multiplexer?.type && config2.multiplexer.type !== "none") {
17508
+ return config2;
17509
+ }
17510
+ if (config2.tmux?.enabled) {
17511
+ return {
17512
+ ...config2,
17513
+ multiplexer: {
17514
+ type: "tmux",
17515
+ layout: config2.tmux.layout ?? "main-vertical",
17516
+ main_pane_size: config2.tmux.main_pane_size ?? 60
17517
+ }
17518
+ };
17519
+ }
17520
+ return config2;
17521
+ }
17489
17522
  // src/config/utils.ts
17490
17523
  function getAgentOverride(config2, name) {
17491
17524
  const overrides = config2?.agents ?? {};
@@ -17544,9 +17577,10 @@ async function extractSessionResult(client, sessionId, options) {
17544
17577
  }
17545
17578
  }
17546
17579
  }
17547
- return extractedContent.filter((t) => t.length > 0).join(`
17580
+ const text = extractedContent.filter((t) => t.length > 0).join(`
17548
17581
 
17549
17582
  `);
17583
+ return { text, empty: text.length === 0 };
17550
17584
  }
17551
17585
 
17552
17586
  // src/agents/orchestrator.ts
@@ -18236,6 +18270,9 @@ function createAgents(config2) {
18236
18270
  }
18237
18271
  return librarianModel ?? DEFAULT_MODELS.librarian;
18238
18272
  }
18273
+ if ((name === "council" || name === "council-master") && config2?.council?.master?.model) {
18274
+ return config2.council.master.model;
18275
+ }
18239
18276
  return DEFAULT_MODELS[name];
18240
18277
  };
18241
18278
  const protoSubAgents = Object.entries(SUBAGENT_FACTORIES).map(([name, factory]) => {
@@ -18286,7 +18323,10 @@ function getAgentConfigs(config2) {
18286
18323
  import * as fs2 from "fs";
18287
18324
  import * as os from "os";
18288
18325
  import * as path2 from "path";
18289
- var logFile = path2.join(os.tmpdir(), "oh-my-opencode-slim.log");
18326
+ var logFile = path2.join(process.env.HOME || os.tmpdir(), ".local/share/opencode/oh-my-opencode-slim.log");
18327
+ try {
18328
+ fs2.mkdirSync(path2.dirname(logFile), { recursive: true });
18329
+ } catch {}
18290
18330
  function log(message, data) {
18291
18331
  try {
18292
18332
  const timestamp = new Date().toISOString();
@@ -18296,293 +18336,608 @@ function log(message, data) {
18296
18336
  } catch {}
18297
18337
  }
18298
18338
 
18299
- // src/utils/agent-variant.ts
18300
- function normalizeAgentName(agentName) {
18301
- const trimmed = agentName.trim();
18302
- return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
18303
- }
18304
- function resolveAgentVariant(config2, agentName) {
18305
- const normalized = normalizeAgentName(agentName);
18306
- const rawVariant = config2?.agents?.[normalized]?.variant;
18307
- if (typeof rawVariant !== "string") {
18308
- return;
18309
- }
18310
- const trimmed = rawVariant.trim();
18311
- if (trimmed.length === 0) {
18312
- return;
18313
- }
18314
- log(`[variant] resolved variant="${trimmed}" for agent "${normalized}"`);
18315
- return trimmed;
18316
- }
18317
- function applyAgentVariant(variant, body) {
18318
- if (!variant) {
18319
- return body;
18320
- }
18321
- if (body.variant) {
18322
- return body;
18323
- }
18324
- return { ...body, variant };
18325
- }
18326
- // src/utils/internal-initiator.ts
18327
- var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
18328
- function isRecord(value) {
18329
- return typeof value === "object" && value !== null;
18330
- }
18331
- function createInternalAgentTextPart(text) {
18332
- return {
18333
- type: "text",
18334
- text: `${text}
18335
- ${SLIM_INTERNAL_INITIATOR_MARKER}`
18336
- };
18337
- }
18338
- function hasInternalInitiatorMarker(part) {
18339
- if (!isRecord(part) || part.type !== "text") {
18340
- return false;
18341
- }
18342
- if (typeof part.text !== "string") {
18343
- return false;
18344
- }
18345
- return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
18346
- }
18347
- // src/utils/tmux.ts
18339
+ // src/multiplexer/tmux/index.ts
18348
18340
  var {spawn } = globalThis.Bun;
18349
- var tmuxPath = null;
18350
- var tmuxChecked = false;
18351
- var storedConfig = null;
18352
- var serverAvailable = null;
18353
- var serverCheckUrl = null;
18354
- async function isServerRunning(serverUrl) {
18355
- if (serverCheckUrl === serverUrl && serverAvailable === true) {
18356
- return true;
18357
- }
18358
- const healthUrl = new URL("/health", serverUrl).toString();
18359
- const timeoutMs = 3000;
18360
- const maxAttempts = 2;
18361
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
18362
- const controller = new AbortController;
18363
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
18364
- let response = null;
18341
+ class TmuxMultiplexer {
18342
+ type = "tmux";
18343
+ binaryPath = null;
18344
+ hasChecked = false;
18345
+ storedLayout;
18346
+ storedMainPaneSize;
18347
+ constructor(layout = "main-vertical", mainPaneSize = 60) {
18348
+ this.storedLayout = layout;
18349
+ this.storedMainPaneSize = mainPaneSize;
18350
+ }
18351
+ async isAvailable() {
18352
+ if (this.hasChecked) {
18353
+ return this.binaryPath !== null;
18354
+ }
18355
+ this.binaryPath = await this.findBinary();
18356
+ this.hasChecked = true;
18357
+ return this.binaryPath !== null;
18358
+ }
18359
+ isInsideSession() {
18360
+ return !!process.env.TMUX;
18361
+ }
18362
+ async spawnPane(sessionId, description, serverUrl) {
18363
+ const tmux = await this.getBinary();
18364
+ if (!tmux) {
18365
+ log("[tmux] spawnPane: tmux binary not found");
18366
+ return { success: false };
18367
+ }
18365
18368
  try {
18366
- response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
18367
- } finally {
18368
- clearTimeout(timeout);
18369
+ const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
18370
+ const args = [
18371
+ "split-window",
18372
+ "-h",
18373
+ "-d",
18374
+ "-P",
18375
+ "-F",
18376
+ "#{pane_id}",
18377
+ opencodeCmd
18378
+ ];
18379
+ log("[tmux] spawnPane: executing", { tmux, args });
18380
+ const proc = spawn([tmux, ...args], {
18381
+ stdout: "pipe",
18382
+ stderr: "pipe"
18383
+ });
18384
+ const exitCode = await proc.exited;
18385
+ const stdout = await new Response(proc.stdout).text();
18386
+ const stderr = await new Response(proc.stderr).text();
18387
+ const paneId = stdout.trim();
18388
+ log("[tmux] spawnPane: result", {
18389
+ exitCode,
18390
+ paneId,
18391
+ stderr: stderr.trim()
18392
+ });
18393
+ if (exitCode === 0 && paneId) {
18394
+ const renameProc = spawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
18395
+ await renameProc.exited;
18396
+ await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
18397
+ log("[tmux] spawnPane: SUCCESS", { paneId });
18398
+ return { success: true, paneId };
18399
+ }
18400
+ return { success: false };
18401
+ } catch (err) {
18402
+ log("[tmux] spawnPane: exception", { error: String(err) });
18403
+ return { success: false };
18369
18404
  }
18370
- const available = response?.ok ?? false;
18371
- if (available) {
18372
- serverCheckUrl = serverUrl;
18373
- serverAvailable = true;
18374
- log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
18375
- return true;
18405
+ }
18406
+ async closePane(paneId) {
18407
+ if (!paneId) {
18408
+ log("[tmux] closePane: no paneId provided");
18409
+ return false;
18376
18410
  }
18377
- if (attempt < maxAttempts) {
18411
+ const tmux = await this.getBinary();
18412
+ if (!tmux) {
18413
+ log("[tmux] closePane: tmux binary not found");
18414
+ return false;
18415
+ }
18416
+ try {
18417
+ log("[tmux] closePane: sending Ctrl+C", { paneId });
18418
+ const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
18419
+ stdout: "pipe",
18420
+ stderr: "pipe"
18421
+ });
18422
+ await ctrlCProc.exited;
18378
18423
  await new Promise((r) => setTimeout(r, 250));
18424
+ log("[tmux] closePane: killing pane", { paneId });
18425
+ const proc = spawn([tmux, "kill-pane", "-t", paneId], {
18426
+ stdout: "pipe",
18427
+ stderr: "pipe"
18428
+ });
18429
+ const exitCode = await proc.exited;
18430
+ const stderr = await new Response(proc.stderr).text();
18431
+ log("[tmux] closePane: result", { exitCode, stderr: stderr.trim() });
18432
+ if (exitCode === 0) {
18433
+ await this.applyLayout(this.storedLayout, this.storedMainPaneSize);
18434
+ return true;
18435
+ }
18436
+ log("[tmux] closePane: failed (pane may already be closed)", { paneId });
18437
+ return false;
18438
+ } catch (err) {
18439
+ log("[tmux] closePane: exception", { error: String(err) });
18440
+ return false;
18379
18441
  }
18380
18442
  }
18381
- log("[tmux] isServerRunning: checked", { serverUrl, available: false });
18382
- return false;
18383
- }
18384
- async function findTmuxPath() {
18385
- const isWindows = process.platform === "win32";
18386
- const cmd = isWindows ? "where" : "which";
18387
- try {
18388
- const proc = spawn([cmd, "tmux"], {
18389
- stdout: "pipe",
18390
- stderr: "pipe"
18391
- });
18392
- const exitCode = await proc.exited;
18393
- if (exitCode !== 0) {
18394
- log("[tmux] findTmuxPath: 'which tmux' failed", { exitCode });
18395
- return null;
18396
- }
18397
- const stdout = await new Response(proc.stdout).text();
18398
- const path3 = stdout.trim().split(`
18399
- `)[0];
18400
- if (!path3) {
18401
- log("[tmux] findTmuxPath: no path in output");
18402
- return null;
18403
- }
18404
- const verifyProc = spawn([path3, "-V"], {
18405
- stdout: "pipe",
18406
- stderr: "pipe"
18407
- });
18408
- const verifyExit = await verifyProc.exited;
18409
- if (verifyExit !== 0) {
18410
- log("[tmux] findTmuxPath: tmux -V failed", { path: path3, verifyExit });
18411
- return null;
18443
+ async applyLayout(layout, mainPaneSize) {
18444
+ const tmux = await this.getBinary();
18445
+ if (!tmux)
18446
+ return;
18447
+ this.storedLayout = layout;
18448
+ this.storedMainPaneSize = mainPaneSize;
18449
+ try {
18450
+ const layoutProc = spawn([tmux, "select-layout", layout], {
18451
+ stdout: "pipe",
18452
+ stderr: "pipe"
18453
+ });
18454
+ await layoutProc.exited;
18455
+ if (layout === "main-horizontal" || layout === "main-vertical") {
18456
+ const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
18457
+ const sizeProc = spawn([tmux, "set-window-option", sizeOption, `${mainPaneSize}%`], {
18458
+ stdout: "pipe",
18459
+ stderr: "pipe"
18460
+ });
18461
+ await sizeProc.exited;
18462
+ const reapplyProc = spawn([tmux, "select-layout", layout], {
18463
+ stdout: "pipe",
18464
+ stderr: "pipe"
18465
+ });
18466
+ await reapplyProc.exited;
18467
+ }
18468
+ log("[tmux] applyLayout: applied", { layout, mainPaneSize });
18469
+ } catch (err) {
18470
+ log("[tmux] applyLayout: exception", { error: String(err) });
18412
18471
  }
18413
- log("[tmux] findTmuxPath: found tmux", { path: path3 });
18414
- return path3;
18415
- } catch (err) {
18416
- log("[tmux] findTmuxPath: exception", { error: String(err) });
18417
- return null;
18418
18472
  }
18419
- }
18420
- async function getTmuxPath() {
18421
- if (tmuxChecked) {
18422
- return tmuxPath;
18473
+ async getBinary() {
18474
+ await this.isAvailable();
18475
+ return this.binaryPath;
18423
18476
  }
18424
- tmuxPath = await findTmuxPath();
18425
- tmuxChecked = true;
18426
- log("[tmux] getTmuxPath: initialized", { tmuxPath });
18427
- return tmuxPath;
18428
- }
18429
- function isInsideTmux() {
18430
- return !!process.env.TMUX;
18431
- }
18432
- async function applyLayout(tmux, layout, mainPaneSize) {
18433
- try {
18434
- const layoutProc = spawn([tmux, "select-layout", layout], {
18435
- stdout: "pipe",
18436
- stderr: "pipe"
18437
- });
18438
- await layoutProc.exited;
18439
- if (layout === "main-horizontal" || layout === "main-vertical") {
18440
- const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
18441
- const sizeProc = spawn([tmux, "set-window-option", sizeOption, `${mainPaneSize}%`], {
18477
+ async findBinary() {
18478
+ const isWindows = process.platform === "win32";
18479
+ const cmd = isWindows ? "where" : "which";
18480
+ try {
18481
+ const proc = spawn([cmd, "tmux"], {
18442
18482
  stdout: "pipe",
18443
18483
  stderr: "pipe"
18444
18484
  });
18445
- await sizeProc.exited;
18446
- const reapplyProc = spawn([tmux, "select-layout", layout], {
18485
+ const exitCode = await proc.exited;
18486
+ if (exitCode !== 0) {
18487
+ log("[tmux] findBinary: 'which tmux' failed", { exitCode });
18488
+ return null;
18489
+ }
18490
+ const stdout = await new Response(proc.stdout).text();
18491
+ const path3 = stdout.trim().split(`
18492
+ `)[0];
18493
+ if (!path3) {
18494
+ log("[tmux] findBinary: no path in output");
18495
+ return null;
18496
+ }
18497
+ const verifyProc = spawn([path3, "-V"], {
18447
18498
  stdout: "pipe",
18448
18499
  stderr: "pipe"
18449
18500
  });
18450
- await reapplyProc.exited;
18501
+ const verifyExit = await verifyProc.exited;
18502
+ if (verifyExit !== 0) {
18503
+ log("[tmux] findBinary: tmux -V failed", { path: path3, verifyExit });
18504
+ return null;
18505
+ }
18506
+ log("[tmux] findBinary: found", { path: path3 });
18507
+ return path3;
18508
+ } catch (err) {
18509
+ log("[tmux] findBinary: exception", { error: String(err) });
18510
+ return null;
18451
18511
  }
18452
- log("[tmux] applyLayout: applied", { layout, mainPaneSize });
18453
- } catch (err) {
18454
- log("[tmux] applyLayout: exception", { error: String(err) });
18455
18512
  }
18456
18513
  }
18457
- async function spawnTmuxPane(sessionId, description, config2, serverUrl) {
18458
- log("[tmux] spawnTmuxPane called", {
18459
- sessionId,
18460
- description,
18461
- config: config2,
18462
- serverUrl
18463
- });
18464
- if (!config2.enabled) {
18465
- log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
18466
- return { success: false };
18514
+
18515
+ // src/multiplexer/zellij/index.ts
18516
+ var {spawn: spawn2 } = globalThis.Bun;
18517
+
18518
+ class ZellijMultiplexer {
18519
+ type = "zellij";
18520
+ binaryPath = null;
18521
+ hasChecked = false;
18522
+ storedLayout;
18523
+ storedMainPaneSize;
18524
+ agentTabId = null;
18525
+ firstPaneId = null;
18526
+ firstPaneUsed = false;
18527
+ constructor(layout = "main-vertical", mainPaneSize = 60) {
18528
+ this.storedLayout = layout;
18529
+ this.storedMainPaneSize = mainPaneSize;
18467
18530
  }
18468
- if (!isInsideTmux()) {
18469
- log("[tmux] spawnTmuxPane: not inside tmux, skipping");
18470
- return { success: false };
18531
+ async isAvailable() {
18532
+ if (this.hasChecked) {
18533
+ return this.binaryPath !== null;
18534
+ }
18535
+ this.binaryPath = await this.findBinary();
18536
+ this.hasChecked = true;
18537
+ return this.binaryPath !== null;
18471
18538
  }
18472
- const serverRunning = await isServerRunning(serverUrl);
18473
- if (!serverRunning) {
18474
- const defaultPort = process.env.OPENCODE_PORT ?? "4096";
18475
- log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
18476
- serverUrl,
18477
- hint: `Start opencode with --port ${defaultPort}`
18478
- });
18479
- return { success: false };
18539
+ isInsideSession() {
18540
+ return !!process.env.ZELLIJ;
18480
18541
  }
18481
- const tmux = await getTmuxPath();
18482
- if (!tmux) {
18483
- log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
18484
- return { success: false };
18542
+ async spawnPane(sessionId, description, serverUrl) {
18543
+ const zellij = await this.getBinary();
18544
+ if (!zellij)
18545
+ return { success: false };
18546
+ try {
18547
+ if (!this.agentTabId) {
18548
+ const result = await this.ensureAgentTab(zellij);
18549
+ if (!result)
18550
+ return { success: false };
18551
+ this.agentTabId = result.tabId;
18552
+ this.firstPaneId = result.firstPaneId;
18553
+ }
18554
+ if (!this.firstPaneUsed && this.firstPaneId) {
18555
+ const success2 = await this.runInPane(zellij, this.firstPaneId, sessionId, serverUrl, description);
18556
+ if (success2) {
18557
+ this.firstPaneUsed = true;
18558
+ return { success: true, paneId: this.firstPaneId };
18559
+ }
18560
+ }
18561
+ return await this.createPaneInAgentTab(zellij, sessionId, serverUrl, description);
18562
+ } catch {
18563
+ return { success: false };
18564
+ }
18485
18565
  }
18486
- storedConfig = config2;
18487
- try {
18566
+ async createPaneInAgentTab(zellij, sessionId, serverUrl, description) {
18488
18567
  const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
18568
+ const paneName = description.slice(0, 30).replace(/"/g, "\\\"");
18569
+ const currentTabId = await this.getCurrentTabId(zellij);
18570
+ const inAgentTab = currentTabId === this.agentTabId;
18571
+ if (inAgentTab) {
18572
+ const args2 = [
18573
+ "action",
18574
+ "new-pane",
18575
+ "--name",
18576
+ paneName,
18577
+ "--close-on-exit",
18578
+ "--",
18579
+ "sh",
18580
+ "-c",
18581
+ opencodeCmd
18582
+ ];
18583
+ const proc2 = spawn2([zellij, ...args2], {
18584
+ stdout: "pipe",
18585
+ stderr: "pipe"
18586
+ });
18587
+ const exitCode2 = await proc2.exited;
18588
+ const stdout2 = await new Response(proc2.stdout).text();
18589
+ const paneId2 = stdout2.trim();
18590
+ if (exitCode2 === 0 && paneId2?.startsWith("terminal_")) {
18591
+ return { success: true, paneId: paneId2 };
18592
+ }
18593
+ return { success: false };
18594
+ }
18595
+ if (!this.agentTabId) {
18596
+ return { success: false };
18597
+ }
18598
+ const originalTab = await this.getCurrentTabId(zellij);
18599
+ await spawn2([zellij, "action", "go-to-tab-by-id", this.agentTabId], {
18600
+ stdout: "ignore",
18601
+ stderr: "ignore"
18602
+ }).exited;
18489
18603
  const args = [
18490
- "split-window",
18491
- "-h",
18492
- "-d",
18493
- "-P",
18494
- "-F",
18495
- "#{pane_id}",
18604
+ "action",
18605
+ "new-pane",
18606
+ "--name",
18607
+ paneName,
18608
+ "--close-on-exit",
18609
+ "--",
18610
+ "sh",
18611
+ "-c",
18496
18612
  opencodeCmd
18497
18613
  ];
18498
- log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
18499
- const proc = spawn([tmux, ...args], {
18614
+ const proc = spawn2([zellij, ...args], {
18500
18615
  stdout: "pipe",
18501
18616
  stderr: "pipe"
18502
18617
  });
18503
18618
  const exitCode = await proc.exited;
18504
18619
  const stdout = await new Response(proc.stdout).text();
18505
- const stderr = await new Response(proc.stderr).text();
18506
18620
  const paneId = stdout.trim();
18507
- log("[tmux] spawnTmuxPane: split result", {
18508
- exitCode,
18509
- paneId,
18510
- stderr: stderr.trim()
18511
- });
18512
- if (exitCode === 0 && paneId) {
18513
- const renameProc = spawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
18514
- await renameProc.exited;
18515
- const layout = config2.layout ?? "main-vertical";
18516
- const mainPaneSize = config2.main_pane_size ?? 60;
18517
- await applyLayout(tmux, layout, mainPaneSize);
18518
- log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
18519
- paneId,
18520
- layout
18521
- });
18621
+ if (originalTab) {
18622
+ await spawn2([zellij, "action", "go-to-tab-by-id", String(originalTab)], {
18623
+ stdout: "ignore",
18624
+ stderr: "ignore"
18625
+ }).exited;
18626
+ }
18627
+ if (exitCode === 0 && paneId?.startsWith("terminal_")) {
18522
18628
  return { success: true, paneId };
18523
18629
  }
18524
18630
  return { success: false };
18525
- } catch (err) {
18526
- log("[tmux] spawnTmuxPane: exception", { error: String(err) });
18527
- return { success: false };
18631
+ }
18632
+ async runInPane(zellij, paneId, sessionId, serverUrl, description) {
18633
+ try {
18634
+ const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
18635
+ await spawn2([zellij, "action", "focus-pane", "--pane-id", paneId], {
18636
+ stdout: "ignore",
18637
+ stderr: "ignore"
18638
+ }).exited;
18639
+ await spawn2([zellij, "action", "rename-pane", "--name", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" }).exited;
18640
+ await spawn2([zellij, "action", "write-chars", opencodeCmd], {
18641
+ stdout: "ignore",
18642
+ stderr: "ignore"
18643
+ }).exited;
18644
+ await spawn2([zellij, "action", "write-chars", `
18645
+ `], {
18646
+ stdout: "ignore",
18647
+ stderr: "ignore"
18648
+ }).exited;
18649
+ return true;
18650
+ } catch {
18651
+ return false;
18652
+ }
18653
+ }
18654
+ async ensureAgentTab(zellij) {
18655
+ try {
18656
+ const existingTab = await this.findTabByName(zellij, "opencode-agents");
18657
+ if (existingTab) {
18658
+ const firstPane = await this.getFirstPaneInTab(zellij, existingTab.tabId);
18659
+ return {
18660
+ tabId: existingTab.tabId,
18661
+ firstPaneId: firstPane || "terminal_0"
18662
+ };
18663
+ }
18664
+ const beforePanes = await this.listPanes(zellij);
18665
+ const createProc = spawn2([zellij, "action", "new-tab", "--name", "opencode-agents"], { stdout: "pipe", stderr: "pipe" });
18666
+ const createExit = await createProc.exited;
18667
+ if (createExit !== 0)
18668
+ return null;
18669
+ const newTab = await this.findTabByName(zellij, "opencode-agents");
18670
+ if (!newTab)
18671
+ return null;
18672
+ const afterPanes = await this.listPanes(zellij);
18673
+ const newPane = afterPanes.find((p) => !beforePanes.includes(p));
18674
+ return { tabId: newTab.tabId, firstPaneId: newPane || "terminal_0" };
18675
+ } catch {
18676
+ return null;
18677
+ }
18678
+ }
18679
+ async getFirstPaneInTab(zellij, tabId) {
18680
+ const originalTab = await this.getCurrentTabId(zellij);
18681
+ await spawn2([zellij, "action", "go-to-tab-by-id", tabId], {
18682
+ stdout: "ignore",
18683
+ stderr: "ignore"
18684
+ }).exited;
18685
+ const panes = await this.listPanes(zellij);
18686
+ if (originalTab) {
18687
+ await spawn2([zellij, "action", "go-to-tab-by-id", String(originalTab)], {
18688
+ stdout: "ignore",
18689
+ stderr: "ignore"
18690
+ }).exited;
18691
+ }
18692
+ return panes[0] || null;
18693
+ }
18694
+ async findTabByName(zellij, name) {
18695
+ try {
18696
+ const proc = spawn2([zellij, "action", "list-tabs", "--json"], {
18697
+ stdout: "pipe",
18698
+ stderr: "pipe"
18699
+ });
18700
+ const exitCode = await proc.exited;
18701
+ if (exitCode !== 0)
18702
+ return this.findTabByNameText(zellij, name);
18703
+ const stdout = await new Response(proc.stdout).text();
18704
+ try {
18705
+ const tabs = JSON.parse(stdout);
18706
+ for (const tab of tabs) {
18707
+ if (tab.name === name) {
18708
+ return { tabId: String(tab.tab_id), name: tab.name };
18709
+ }
18710
+ }
18711
+ } catch {
18712
+ return this.findTabByNameText(zellij, name);
18713
+ }
18714
+ return null;
18715
+ } catch {
18716
+ return null;
18717
+ }
18718
+ }
18719
+ async findTabByNameText(zellij, name) {
18720
+ try {
18721
+ const proc = spawn2([zellij, "action", "list-tabs"], {
18722
+ stdout: "pipe",
18723
+ stderr: "pipe"
18724
+ });
18725
+ const exitCode = await proc.exited;
18726
+ if (exitCode !== 0)
18727
+ return null;
18728
+ const stdout = await new Response(proc.stdout).text();
18729
+ const lines = stdout.split(`
18730
+ `);
18731
+ for (const line of lines) {
18732
+ const parts = line.trim().split(/\s+/);
18733
+ if (parts.length >= 3 && parts[2] === name) {
18734
+ return { tabId: parts[0], name: parts[2] };
18735
+ }
18736
+ }
18737
+ return null;
18738
+ } catch {
18739
+ return null;
18740
+ }
18741
+ }
18742
+ async getCurrentTabId(zellij) {
18743
+ try {
18744
+ const proc = spawn2([zellij, "action", "current-tab-info", "--json"], {
18745
+ stdout: "pipe",
18746
+ stderr: "pipe"
18747
+ });
18748
+ const exitCode = await proc.exited;
18749
+ if (exitCode !== 0)
18750
+ return null;
18751
+ const stdout = await new Response(proc.stdout).text();
18752
+ try {
18753
+ const info = JSON.parse(stdout);
18754
+ return String(info.tab_id);
18755
+ } catch {
18756
+ return null;
18757
+ }
18758
+ } catch {
18759
+ return null;
18760
+ }
18761
+ }
18762
+ async listPanes(zellij) {
18763
+ try {
18764
+ const proc = spawn2([zellij, "action", "list-panes"], {
18765
+ stdout: "pipe",
18766
+ stderr: "pipe"
18767
+ });
18768
+ const exitCode = await proc.exited;
18769
+ if (exitCode !== 0)
18770
+ return [];
18771
+ const stdout = await new Response(proc.stdout).text();
18772
+ return stdout.split(`
18773
+ `).slice(1).map((line) => line.trim().split(/\s+/)[0]).filter((id) => id?.startsWith("terminal_"));
18774
+ } catch {
18775
+ return [];
18776
+ }
18777
+ }
18778
+ async closePane(paneId) {
18779
+ if (!paneId || paneId === "unknown")
18780
+ return true;
18781
+ const zellij = await this.getBinary();
18782
+ if (!zellij)
18783
+ return false;
18784
+ try {
18785
+ await spawn2([zellij, "action", "write", "--pane-id", paneId, "\x03"], {
18786
+ stdout: "ignore",
18787
+ stderr: "ignore"
18788
+ }).exited;
18789
+ await new Promise((r) => setTimeout(r, 250));
18790
+ const proc = spawn2([zellij, "action", "close-pane", "--pane-id", paneId], { stdout: "pipe", stderr: "pipe" });
18791
+ const exitCode = await proc.exited;
18792
+ return exitCode === 0 || exitCode === 1;
18793
+ } catch {
18794
+ return false;
18795
+ }
18796
+ }
18797
+ async applyLayout(_layout, _mainPaneSize) {}
18798
+ async getBinary() {
18799
+ await this.isAvailable();
18800
+ return this.binaryPath;
18801
+ }
18802
+ async findBinary() {
18803
+ const cmd = process.platform === "win32" ? "where" : "which";
18804
+ try {
18805
+ const proc = spawn2([cmd, "zellij"], {
18806
+ stdout: "pipe",
18807
+ stderr: "pipe"
18808
+ });
18809
+ if (await proc.exited !== 0)
18810
+ return null;
18811
+ const stdout = await new Response(proc.stdout).text();
18812
+ return stdout.trim().split(`
18813
+ `)[0] || null;
18814
+ } catch {
18815
+ return null;
18816
+ }
18528
18817
  }
18529
18818
  }
18530
- async function closeTmuxPane(paneId) {
18531
- log("[tmux] closeTmuxPane called", { paneId });
18532
- if (!paneId) {
18533
- log("[tmux] closeTmuxPane: no paneId provided");
18534
- return false;
18819
+
18820
+ // src/multiplexer/factory.ts
18821
+ var multiplexerCache = new Map;
18822
+ function getMultiplexer(config2) {
18823
+ const { type } = config2;
18824
+ if (type === "none") {
18825
+ return null;
18535
18826
  }
18536
- const tmux = await getTmuxPath();
18537
- if (!tmux) {
18538
- log("[tmux] closeTmuxPane: tmux binary not found");
18539
- return false;
18827
+ const cached2 = multiplexerCache.get(type);
18828
+ if (cached2) {
18829
+ return cached2;
18540
18830
  }
18541
- try {
18542
- log("[tmux] closeTmuxPane: sending Ctrl+C for graceful shutdown", {
18543
- paneId
18544
- });
18545
- const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
18546
- stdout: "pipe",
18547
- stderr: "pipe"
18548
- });
18549
- await ctrlCProc.exited;
18550
- await new Promise((r) => setTimeout(r, 250));
18551
- log("[tmux] closeTmuxPane: killing pane", { paneId });
18552
- const proc = spawn([tmux, "kill-pane", "-t", paneId], {
18553
- stdout: "pipe",
18554
- stderr: "pipe"
18555
- });
18556
- const exitCode = await proc.exited;
18557
- const stderr = await new Response(proc.stderr).text();
18558
- log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
18559
- if (exitCode === 0) {
18560
- log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
18561
- if (storedConfig) {
18562
- const layout = storedConfig.layout ?? "main-vertical";
18563
- const mainPaneSize = storedConfig.main_pane_size ?? 60;
18564
- await applyLayout(tmux, layout, mainPaneSize);
18565
- log("[tmux] closeTmuxPane: layout reapplied", { layout });
18831
+ let multiplexer;
18832
+ let actualType;
18833
+ switch (type) {
18834
+ case "tmux":
18835
+ multiplexer = new TmuxMultiplexer(config2.layout, config2.main_pane_size);
18836
+ actualType = "tmux";
18837
+ break;
18838
+ case "zellij":
18839
+ multiplexer = new ZellijMultiplexer(config2.layout, config2.main_pane_size);
18840
+ actualType = "zellij";
18841
+ break;
18842
+ case "auto": {
18843
+ if (process.env.TMUX) {
18844
+ multiplexer = new TmuxMultiplexer(config2.layout, config2.main_pane_size);
18845
+ actualType = "tmux";
18846
+ } else if (process.env.ZELLIJ) {
18847
+ multiplexer = new ZellijMultiplexer(config2.layout, config2.main_pane_size);
18848
+ actualType = "zellij";
18849
+ } else {
18850
+ log("[multiplexer] auto: not inside any session, disabling");
18851
+ return null;
18566
18852
  }
18853
+ break;
18854
+ }
18855
+ default:
18856
+ log(`[multiplexer] Unknown type: ${type}`);
18857
+ return null;
18858
+ }
18859
+ multiplexerCache.set(actualType, multiplexer);
18860
+ log(`[multiplexer] Created ${actualType} instance`);
18861
+ return multiplexer;
18862
+ }
18863
+ function startAvailabilityCheck(config2) {
18864
+ const multiplexer = getMultiplexer(config2);
18865
+ if (multiplexer) {
18866
+ multiplexer.isAvailable().catch(() => {});
18867
+ }
18868
+ }
18869
+ // src/multiplexer/types.ts
18870
+ async function isServerRunning(serverUrl, timeoutMs = 3000, maxAttempts = 2) {
18871
+ const healthUrl = new URL("/health", serverUrl).toString();
18872
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
18873
+ const controller = new AbortController;
18874
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
18875
+ let response = null;
18876
+ try {
18877
+ response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
18878
+ } finally {
18879
+ clearTimeout(timeout);
18880
+ }
18881
+ if (response?.ok) {
18567
18882
  return true;
18568
18883
  }
18569
- log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
18570
- paneId
18571
- });
18572
- return false;
18573
- } catch (err) {
18574
- log("[tmux] closeTmuxPane: exception", { error: String(err) });
18884
+ if (attempt < maxAttempts) {
18885
+ await new Promise((r) => setTimeout(r, 250));
18886
+ }
18887
+ }
18888
+ return false;
18889
+ }
18890
+ // src/utils/agent-variant.ts
18891
+ function normalizeAgentName(agentName) {
18892
+ const trimmed = agentName.trim();
18893
+ return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
18894
+ }
18895
+ function resolveAgentVariant(config2, agentName) {
18896
+ const normalized = normalizeAgentName(agentName);
18897
+ const rawVariant = config2?.agents?.[normalized]?.variant;
18898
+ if (typeof rawVariant !== "string") {
18899
+ return;
18900
+ }
18901
+ const trimmed = rawVariant.trim();
18902
+ if (trimmed.length === 0) {
18903
+ return;
18904
+ }
18905
+ log(`[variant] resolved variant="${trimmed}" for agent "${normalized}"`);
18906
+ return trimmed;
18907
+ }
18908
+ function applyAgentVariant(variant, body) {
18909
+ if (!variant) {
18910
+ return body;
18911
+ }
18912
+ if (body.variant) {
18913
+ return body;
18914
+ }
18915
+ return { ...body, variant };
18916
+ }
18917
+ // src/utils/internal-initiator.ts
18918
+ var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
18919
+ function isRecord(value) {
18920
+ return typeof value === "object" && value !== null;
18921
+ }
18922
+ function createInternalAgentTextPart(text) {
18923
+ return {
18924
+ type: "text",
18925
+ text: `${text}
18926
+ ${SLIM_INTERNAL_INITIATOR_MARKER}`
18927
+ };
18928
+ }
18929
+ function hasInternalInitiatorMarker(part) {
18930
+ if (!isRecord(part) || part.type !== "text") {
18575
18931
  return false;
18576
18932
  }
18577
- }
18578
- function startTmuxCheck() {
18579
- if (!tmuxChecked) {
18580
- getTmuxPath().catch(() => {});
18933
+ if (typeof part.text !== "string") {
18934
+ return false;
18581
18935
  }
18936
+ return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
18582
18937
  }
18583
18938
  // src/utils/zip-extractor.ts
18584
18939
  import { release } from "os";
18585
- var {spawn: spawn2, spawnSync } = globalThis.Bun;
18940
+ var {spawn: spawn3, spawnSync } = globalThis.Bun;
18586
18941
  var WINDOWS_BUILD_WITH_TAR = 17134;
18587
18942
  function getWindowsBuildNumber() {
18588
18943
  if (process.platform !== "win32")
@@ -18623,13 +18978,13 @@ async function extractZip(archivePath, destDir) {
18623
18978
  const extractor = getWindowsZipExtractor();
18624
18979
  switch (extractor) {
18625
18980
  case "tar":
18626
- proc = spawn2(["tar", "-xf", archivePath, "-C", destDir], {
18981
+ proc = spawn3(["tar", "-xf", archivePath, "-C", destDir], {
18627
18982
  stdout: "ignore",
18628
18983
  stderr: "pipe"
18629
18984
  });
18630
18985
  break;
18631
18986
  case "pwsh":
18632
- proc = spawn2([
18987
+ proc = spawn3([
18633
18988
  "pwsh",
18634
18989
  "-Command",
18635
18990
  `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
@@ -18639,7 +18994,7 @@ async function extractZip(archivePath, destDir) {
18639
18994
  });
18640
18995
  break;
18641
18996
  default:
18642
- proc = spawn2([
18997
+ proc = spawn3([
18643
18998
  "powershell",
18644
18999
  "-Command",
18645
19000
  `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
@@ -18650,7 +19005,7 @@ async function extractZip(archivePath, destDir) {
18650
19005
  break;
18651
19006
  }
18652
19007
  } else {
18653
- proc = spawn2(["unzip", "-o", archivePath, "-d", destDir], {
19008
+ proc = spawn3(["unzip", "-o", archivePath, "-d", destDir], {
18654
19009
  stdout: "ignore",
18655
19010
  stderr: "pipe"
18656
19011
  });
@@ -18721,10 +19076,10 @@ class BackgroundTaskManager {
18721
19076
  activeStarts = 0;
18722
19077
  maxConcurrentStarts;
18723
19078
  completionResolvers = new Map;
18724
- constructor(ctx, tmuxConfig, config2) {
19079
+ constructor(ctx, multiplexerConfig, config2) {
18725
19080
  this.client = ctx.client;
18726
19081
  this.directory = ctx.directory;
18727
- this.tmuxEnabled = tmuxConfig?.enabled ?? false;
19082
+ this.tmuxEnabled = multiplexerConfig !== undefined && multiplexerConfig.type !== "none" && multiplexerConfig.type !== undefined && getMultiplexer(multiplexerConfig) !== null;
18728
19083
  this.config = config2;
18729
19084
  this.backgroundConfig = config2?.background ?? {
18730
19085
  maxConcurrentStarts: 10
@@ -18862,6 +19217,7 @@ class BackgroundTaskManager {
18862
19217
  const errors3 = [];
18863
19218
  let succeeded = false;
18864
19219
  const sessionId = session2.data.id;
19220
+ const retryOnEmpty = this.config?.fallback?.retry_on_empty ?? true;
18865
19221
  for (let i = 0;i < attemptModels.length; i++) {
18866
19222
  const model = attemptModels[i];
18867
19223
  const modelLabel = model ?? "default-model";
@@ -18885,6 +19241,10 @@ class BackgroundTaskManager {
18885
19241
  body,
18886
19242
  query: promptQuery
18887
19243
  }, timeoutMs);
19244
+ const extraction = await extractSessionResult(this.client, sessionId);
19245
+ if (retryOnEmpty && extraction.empty) {
19246
+ throw new Error("Empty response from provider");
19247
+ }
18888
19248
  succeeded = true;
18889
19249
  break;
18890
19250
  } catch (error48) {
@@ -18964,12 +19324,13 @@ class BackgroundTaskManager {
18964
19324
  async extractAndCompleteTask(task) {
18965
19325
  if (!task.sessionId)
18966
19326
  return;
19327
+ const retryOnEmpty = this.config?.fallback?.retry_on_empty ?? true;
18967
19328
  try {
18968
- const responseText = await extractSessionResult(this.client, task.sessionId);
18969
- if (responseText) {
18970
- this.completeTask(task, "completed", responseText);
19329
+ const extraction = await extractSessionResult(this.client, task.sessionId);
19330
+ if (extraction.empty && retryOnEmpty) {
19331
+ this.completeTask(task, "failed", "Empty response from provider");
18971
19332
  } else {
18972
- this.completeTask(task, "completed", "(No output)");
19333
+ this.completeTask(task, "completed", extraction.text);
18973
19334
  }
18974
19335
  } catch (error48) {
18975
19336
  this.completeTask(task, "failed", error48 instanceof Error ? error48.message : String(error48));
@@ -19087,31 +19448,31 @@ class BackgroundTaskManager {
19087
19448
  return this.depthTracker;
19088
19449
  }
19089
19450
  }
19090
- // src/background/tmux-session-manager.ts
19451
+ // src/background/multiplexer-session-manager.ts
19091
19452
  var SESSION_TIMEOUT_MS = 10 * 60 * 1000;
19092
19453
  var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
19093
19454
 
19094
- class TmuxSessionManager {
19455
+ class MultiplexerSessionManager {
19095
19456
  client;
19096
- tmuxConfig;
19097
19457
  serverUrl;
19458
+ multiplexer = null;
19098
19459
  sessions = new Map;
19099
19460
  pollInterval;
19100
19461
  enabled = false;
19101
- constructor(ctx, tmuxConfig) {
19462
+ constructor(ctx, config2) {
19102
19463
  this.client = ctx.client;
19103
- this.tmuxConfig = tmuxConfig;
19104
19464
  const defaultPort = process.env.OPENCODE_PORT ?? "4096";
19105
19465
  this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
19106
- this.enabled = tmuxConfig.enabled && isInsideTmux();
19107
- log("[tmux-session-manager] initialized", {
19466
+ this.multiplexer = getMultiplexer(config2);
19467
+ this.enabled = config2.type !== "none" && this.multiplexer !== null && this.multiplexer.isInsideSession();
19468
+ log("[multiplexer-session-manager] initialized", {
19108
19469
  enabled: this.enabled,
19109
- tmuxConfig: this.tmuxConfig,
19470
+ type: config2.type,
19110
19471
  serverUrl: this.serverUrl
19111
19472
  });
19112
19473
  }
19113
19474
  async onSessionCreated(event) {
19114
- if (!this.enabled)
19475
+ if (!this.enabled || !this.multiplexer)
19115
19476
  return;
19116
19477
  if (event.type !== "session.created")
19117
19478
  return;
@@ -19123,16 +19484,25 @@ class TmuxSessionManager {
19123
19484
  const parentId = info.parentID;
19124
19485
  const title = info.title ?? "Subagent";
19125
19486
  if (this.sessions.has(sessionId)) {
19126
- log("[tmux-session-manager] session already tracked", { sessionId });
19487
+ log("[multiplexer-session-manager] session already tracked", {
19488
+ sessionId
19489
+ });
19490
+ return;
19491
+ }
19492
+ const serverRunning = await isServerRunning(this.serverUrl);
19493
+ if (!serverRunning) {
19494
+ log("[multiplexer-session-manager] server not running, skipping", {
19495
+ serverUrl: this.serverUrl
19496
+ });
19127
19497
  return;
19128
19498
  }
19129
- log("[tmux-session-manager] child session created, spawning pane", {
19499
+ log("[multiplexer-session-manager] child session created, spawning pane", {
19130
19500
  sessionId,
19131
19501
  parentId,
19132
19502
  title
19133
19503
  });
19134
- const paneResult = await spawnTmuxPane(sessionId, title, this.tmuxConfig, this.serverUrl).catch((err) => {
19135
- log("[tmux-session-manager] failed to spawn pane", {
19504
+ const paneResult = await this.multiplexer.spawnPane(sessionId, title, this.serverUrl).catch((err) => {
19505
+ log("[multiplexer-session-manager] failed to spawn pane", {
19136
19506
  error: String(err)
19137
19507
  });
19138
19508
  return { success: false, paneId: undefined };
@@ -19147,7 +19517,7 @@ class TmuxSessionManager {
19147
19517
  createdAt: now,
19148
19518
  lastSeenAt: now
19149
19519
  });
19150
- log("[tmux-session-manager] pane spawned", {
19520
+ log("[multiplexer-session-manager] pane spawned", {
19151
19521
  sessionId,
19152
19522
  paneId: paneResult.paneId
19153
19523
  });
@@ -19174,7 +19544,7 @@ class TmuxSessionManager {
19174
19544
  const sessionId = event.properties?.sessionID;
19175
19545
  if (!sessionId)
19176
19546
  return;
19177
- log("[tmux-session-manager] session deleted, closing pane", {
19547
+ log("[multiplexer-session-manager] session deleted, closing pane", {
19178
19548
  sessionId
19179
19549
  });
19180
19550
  await this.closeSession(sessionId);
@@ -19183,13 +19553,13 @@ class TmuxSessionManager {
19183
19553
  if (this.pollInterval)
19184
19554
  return;
19185
19555
  this.pollInterval = setInterval(() => this.pollSessions(), POLL_INTERVAL_BACKGROUND_MS);
19186
- log("[tmux-session-manager] polling started");
19556
+ log("[multiplexer-session-manager] polling started");
19187
19557
  }
19188
19558
  stopPolling() {
19189
19559
  if (this.pollInterval) {
19190
19560
  clearInterval(this.pollInterval);
19191
19561
  this.pollInterval = undefined;
19192
- log("[tmux-session-manager] polling stopped");
19562
+ log("[multiplexer-session-manager] polling stopped");
19193
19563
  }
19194
19564
  }
19195
19565
  async pollSessions() {
@@ -19221,18 +19591,18 @@ class TmuxSessionManager {
19221
19591
  await this.closeSession(sessionId);
19222
19592
  }
19223
19593
  } catch (err) {
19224
- log("[tmux-session-manager] poll error", { error: String(err) });
19594
+ log("[multiplexer-session-manager] poll error", { error: String(err) });
19225
19595
  }
19226
19596
  }
19227
19597
  async closeSession(sessionId) {
19228
19598
  const tracked = this.sessions.get(sessionId);
19229
- if (!tracked)
19599
+ if (!tracked || !this.multiplexer)
19230
19600
  return;
19231
- log("[tmux-session-manager] closing session pane", {
19601
+ log("[multiplexer-session-manager] closing session pane", {
19232
19602
  sessionId,
19233
19603
  paneId: tracked.paneId
19234
19604
  });
19235
- await closeTmuxPane(tracked.paneId);
19605
+ await this.multiplexer.closePane(tracked.paneId);
19236
19606
  this.sessions.delete(sessionId);
19237
19607
  if (this.sessions.size === 0) {
19238
19608
  this.stopPolling();
@@ -19240,18 +19610,19 @@ class TmuxSessionManager {
19240
19610
  }
19241
19611
  async cleanup() {
19242
19612
  this.stopPolling();
19243
- if (this.sessions.size > 0) {
19244
- log("[tmux-session-manager] closing all panes", {
19613
+ if (this.sessions.size > 0 && this.multiplexer) {
19614
+ log("[multiplexer-session-manager] closing all panes", {
19245
19615
  count: this.sessions.size
19246
19616
  });
19247
- const closePromises = Array.from(this.sessions.values()).map((s) => closeTmuxPane(s.paneId).catch((err) => log("[tmux-session-manager] cleanup error for pane", {
19617
+ const multiplexer = this.multiplexer;
19618
+ const closePromises = Array.from(this.sessions.values()).map((s) => multiplexer.closePane(s.paneId).catch((err) => log("[multiplexer-session-manager] cleanup error for pane", {
19248
19619
  paneId: s.paneId,
19249
19620
  error: String(err)
19250
19621
  })));
19251
19622
  await Promise.all(closePromises);
19252
19623
  this.sessions.clear();
19253
19624
  }
19254
- log("[tmux-session-manager] cleanup complete");
19625
+ log("[multiplexer-session-manager] cleanup complete");
19255
19626
  }
19256
19627
  }
19257
19628
  // src/council/council-manager.ts
@@ -19296,10 +19667,11 @@ class CouncilManager {
19296
19667
  const resolvedPreset = presetName ?? councilConfig.default_preset ?? "default";
19297
19668
  const preset = councilConfig.presets[resolvedPreset];
19298
19669
  if (!preset) {
19670
+ const available = Object.keys(councilConfig.presets).join(", ");
19299
19671
  log(`[council-manager] Preset "${resolvedPreset}" not found`);
19300
19672
  return {
19301
19673
  success: false,
19302
- error: `Preset "${resolvedPreset}" not found`,
19674
+ error: `Preset "${resolvedPreset}" does not exist. Omit the preset parameter to use the default, or call again with one of: ${available}`,
19303
19675
  councillorResults: []
19304
19676
  };
19305
19677
  }
@@ -19314,6 +19686,7 @@ class CouncilManager {
19314
19686
  const councillorsTimeout = councilConfig.councillors_timeout ?? 180000;
19315
19687
  const masterTimeout = councilConfig.master_timeout ?? 300000;
19316
19688
  const executionMode = councilConfig.councillor_execution_mode ?? "parallel";
19689
+ const maxRetries = councilConfig.councillor_retries ?? 3;
19317
19690
  const councillorCount = Object.keys(preset.councillors).length;
19318
19691
  log(`[council-manager] Starting council with preset "${resolvedPreset}"`, {
19319
19692
  councillors: Object.keys(preset.councillors)
@@ -19323,7 +19696,7 @@ class CouncilManager {
19323
19696
  error: err instanceof Error ? err.message : String(err)
19324
19697
  });
19325
19698
  });
19326
- const councillorResults = await this.runCouncillors(prompt, preset.councillors, parentSessionId, councillorsTimeout, executionMode);
19699
+ const councillorResults = await this.runCouncillors(prompt, preset.councillors, parentSessionId, councillorsTimeout, executionMode, maxRetries);
19327
19700
  const completedCount = councillorResults.filter((r) => r.status === "completed").length;
19328
19701
  log(`[council-manager] Councillors completed: ${completedCount}/${councillorResults.length}`);
19329
19702
  if (completedCount === 0) {
@@ -19411,10 +19784,16 @@ ${bestResult.result}` : undefined,
19411
19784
  body,
19412
19785
  query: { directory: this.directory }
19413
19786
  }, options.timeout);
19414
- const result = await extractSessionResult(this.client, sessionId, {
19787
+ const extraction = await extractSessionResult(this.client, sessionId, {
19415
19788
  includeReasoning: options.includeReasoning
19416
19789
  });
19417
- return result || "(No output)";
19790
+ if (extraction.empty) {
19791
+ const retryOnEmpty = this.config?.fallback?.retry_on_empty ?? true;
19792
+ if (retryOnEmpty) {
19793
+ throw new Error("Empty response from provider");
19794
+ }
19795
+ }
19796
+ return extraction.text;
19418
19797
  } finally {
19419
19798
  if (sessionId) {
19420
19799
  this.client.session.abort({ path: { id: sessionId } }).catch(() => {});
@@ -19424,72 +19803,19 @@ ${bestResult.result}` : undefined,
19424
19803
  }
19425
19804
  }
19426
19805
  }
19427
- async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode = "parallel") {
19806
+ async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode = "parallel", maxRetries = 1) {
19428
19807
  const entries = Object.entries(councillors);
19429
19808
  const results = [];
19430
19809
  if (executionMode === "serial") {
19431
19810
  for (const [name, config2] of entries) {
19432
- const modelLabel = shortModelLabel(config2.model);
19433
- log(`[council-manager] Running councillor "${name}" (${modelLabel}) serially`);
19434
- try {
19435
- const result = await this.runAgentSession({
19436
- parentSessionId,
19437
- title: `Council ${name} (${modelLabel})`,
19438
- agent: "councillor",
19439
- model: config2.model,
19440
- promptText: formatCouncillorPrompt(prompt, config2.prompt),
19441
- variant: config2.variant,
19442
- timeout,
19443
- includeReasoning: false
19444
- });
19445
- results.push({
19446
- name,
19447
- model: config2.model,
19448
- status: "completed",
19449
- result
19450
- });
19451
- } catch (error48) {
19452
- const msg = error48 instanceof Error ? error48.message : String(error48);
19453
- results.push({
19454
- name,
19455
- model: config2.model,
19456
- status: msg.includes("timed out") ? "timed_out" : "failed",
19457
- error: `Councillor "${name}": ${msg}`
19458
- });
19459
- }
19811
+ results.push(await this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries));
19460
19812
  }
19461
19813
  } else {
19462
19814
  const promises = entries.map(([name, config2], index) => (async () => {
19463
19815
  if (index > 0) {
19464
19816
  await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
19465
19817
  }
19466
- const modelLabel = shortModelLabel(config2.model);
19467
- try {
19468
- const result = await this.runAgentSession({
19469
- parentSessionId,
19470
- title: `Council ${name} (${modelLabel})`,
19471
- agent: "councillor",
19472
- model: config2.model,
19473
- promptText: formatCouncillorPrompt(prompt, config2.prompt),
19474
- variant: config2.variant,
19475
- timeout,
19476
- includeReasoning: false
19477
- });
19478
- return {
19479
- name,
19480
- model: config2.model,
19481
- status: "completed",
19482
- result
19483
- };
19484
- } catch (error48) {
19485
- const msg = error48 instanceof Error ? error48.message : String(error48);
19486
- return {
19487
- name,
19488
- model: config2.model,
19489
- status: msg.includes("timed out") ? "timed_out" : "failed",
19490
- error: `Councillor "${name}": ${msg}`
19491
- };
19492
- }
19818
+ return this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries);
19493
19819
  })());
19494
19820
  const settled = await Promise.allSettled(promises);
19495
19821
  for (let index = 0;index < settled.length; index++) {
@@ -19515,6 +19841,78 @@ ${bestResult.result}` : undefined,
19515
19841
  }
19516
19842
  return results;
19517
19843
  }
19844
+ async runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries) {
19845
+ const modelLabel = shortModelLabel(config2.model);
19846
+ const totalAttempts = 1 + maxRetries;
19847
+ for (let attempt = 1;attempt <= totalAttempts; attempt++) {
19848
+ if (attempt > 1) {
19849
+ log(`[council-manager] Retrying councillor "${name}" (${modelLabel}), attempt ${attempt}/${totalAttempts}`);
19850
+ }
19851
+ try {
19852
+ const result = await this.runAgentSession({
19853
+ parentSessionId,
19854
+ title: `Council ${name} (${modelLabel})`,
19855
+ agent: "councillor",
19856
+ model: config2.model,
19857
+ promptText: formatCouncillorPrompt(prompt, config2.prompt),
19858
+ variant: config2.variant,
19859
+ timeout,
19860
+ includeReasoning: false
19861
+ });
19862
+ return {
19863
+ name,
19864
+ model: config2.model,
19865
+ status: "completed",
19866
+ result
19867
+ };
19868
+ } catch (error48) {
19869
+ const msg = error48 instanceof Error ? error48.message : String(error48);
19870
+ const isEmptyResponse = msg.includes("Empty response from provider");
19871
+ const canRetry = attempt < totalAttempts && isEmptyResponse;
19872
+ if (!canRetry) {
19873
+ return {
19874
+ name,
19875
+ model: config2.model,
19876
+ status: msg.includes("timed out") ? "timed_out" : "failed",
19877
+ error: `Councillor "${name}": ${msg}`
19878
+ };
19879
+ }
19880
+ }
19881
+ }
19882
+ return {
19883
+ name,
19884
+ model: config2.model,
19885
+ status: "failed",
19886
+ error: `Councillor "${name}": max retries exhausted`
19887
+ };
19888
+ }
19889
+ async runMasterModelWithRetry(parentSessionId, model, modelLabel, promptText, variant, timeout, maxRetries) {
19890
+ const totalAttempts = 1 + maxRetries;
19891
+ for (let attempt = 1;attempt <= totalAttempts; attempt++) {
19892
+ if (attempt > 1) {
19893
+ log(`[council-manager] Retrying master (${modelLabel}), attempt ${attempt}/${totalAttempts}`);
19894
+ }
19895
+ try {
19896
+ return await this.runAgentSession({
19897
+ parentSessionId,
19898
+ title: `Council Master (${modelLabel})`,
19899
+ agent: "council-master",
19900
+ model,
19901
+ promptText,
19902
+ variant,
19903
+ timeout
19904
+ });
19905
+ } catch (error48) {
19906
+ const msg = error48 instanceof Error ? error48.message : String(error48);
19907
+ const isEmptyResponse = msg.includes("Empty response from provider");
19908
+ const canRetry = attempt < totalAttempts && isEmptyResponse;
19909
+ if (!canRetry) {
19910
+ throw error48;
19911
+ }
19912
+ }
19913
+ }
19914
+ throw new Error(`Master model ${modelLabel}: max retries exhausted`);
19915
+ }
19518
19916
  async runMaster(prompt, councillorResults, councilConfig, parentSessionId, timeout, presetMasterOverride) {
19519
19917
  const masterConfig = councilConfig.master;
19520
19918
  const fallbackModels = councilConfig.master_fallback ?? [];
@@ -19523,6 +19921,7 @@ ${bestResult.result}` : undefined,
19523
19921
  const effectivePrompt = presetMasterOverride?.prompt ?? masterConfig.prompt;
19524
19922
  const attemptModels = [effectiveModel, ...fallbackModels];
19525
19923
  const synthesisPrompt = formatMasterSynthesisPrompt(prompt, councillorResults, effectivePrompt);
19924
+ const maxRetries = councilConfig.councillor_retries ?? 3;
19526
19925
  const errors3 = [];
19527
19926
  for (let i = 0;i < attemptModels.length; i++) {
19528
19927
  const model = attemptModels[i];
@@ -19531,15 +19930,7 @@ ${bestResult.result}` : undefined,
19531
19930
  if (i > 0) {
19532
19931
  log(`[council-manager] master fallback ${i}/${attemptModels.length - 1}: ${currentLabel}`);
19533
19932
  }
19534
- const result = await this.runAgentSession({
19535
- parentSessionId,
19536
- title: `Council Master (${currentLabel})`,
19537
- agent: "council-master",
19538
- model,
19539
- promptText: synthesisPrompt,
19540
- variant: effectiveVariant,
19541
- timeout
19542
- });
19933
+ const result = await this.runMasterModelWithRetry(parentSessionId, model, currentLabel, synthesisPrompt, effectiveVariant, timeout, maxRetries);
19543
19934
  return { success: true, result };
19544
19935
  } catch (error48) {
19545
19936
  const msg = error48 instanceof Error ? error48.message : String(error48);
@@ -19792,30 +20183,6 @@ function getCachedVersion() {
19792
20183
  }
19793
20184
  return null;
19794
20185
  }
19795
- function updatePinnedVersion(configPath, oldEntry, newVersion) {
19796
- try {
19797
- if (!fs4.existsSync(configPath))
19798
- return false;
19799
- const content = fs4.readFileSync(configPath, "utf-8");
19800
- const newEntry = `${PACKAGE_NAME}@${newVersion}`;
19801
- const escapedOldEntry = oldEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19802
- const entryRegex = new RegExp(`(["'])${escapedOldEntry}\\1`, "g");
19803
- if (!entryRegex.test(content)) {
19804
- log(`[auto-update-checker] Entry "${oldEntry}" not found in ${configPath}`);
19805
- return false;
19806
- }
19807
- const updatedContent = content.replace(entryRegex, `$1${newEntry}$1`);
19808
- if (updatedContent === content) {
19809
- return false;
19810
- }
19811
- fs4.writeFileSync(configPath, updatedContent, "utf-8");
19812
- log(`[auto-update-checker] Updated ${configPath}: ${oldEntry} \u2192 ${newEntry}`);
19813
- return true;
19814
- } catch (err) {
19815
- log(`[auto-update-checker] Failed to update config file ${configPath}:`, err);
19816
- return false;
19817
- }
19818
- }
19819
20186
  async function getLatestVersion(channel = "latest") {
19820
20187
  const controller = new AbortController;
19821
20188
  const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
@@ -19882,6 +20249,10 @@ async function runBackgroundUpdateCheck(ctx, autoUpdate) {
19882
20249
  log("[auto-update-checker] No version found (cached or pinned)");
19883
20250
  return;
19884
20251
  }
20252
+ if (pluginInfo.isPinned) {
20253
+ log(`[auto-update-checker] Version is pinned; skipping update check.`);
20254
+ return;
20255
+ }
19885
20256
  const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
19886
20257
  const latestVersion = await getLatestVersion(channel);
19887
20258
  if (!latestVersion) {
@@ -19898,15 +20269,6 @@ async function runBackgroundUpdateCheck(ctx, autoUpdate) {
19898
20269
  log("[auto-update-checker] Auto-update disabled, notification only");
19899
20270
  return;
19900
20271
  }
19901
- if (pluginInfo.isPinned) {
19902
- const updated = updatePinnedVersion(pluginInfo.configPath, pluginInfo.entry, latestVersion);
19903
- if (!updated) {
19904
- showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available. Restart to apply.`, "info", 8000);
19905
- log("[auto-update-checker] Failed to update pinned version in config");
19906
- return;
19907
- }
19908
- log(`[auto-update-checker] Config updated: ${pluginInfo.entry} \u2192 ${PACKAGE_NAME}@${latestVersion}`);
19909
- }
19910
20272
  invalidatePackage(PACKAGE_NAME);
19911
20273
  const installSuccess = await runBunInstallSafe(ctx);
19912
20274
  if (installSuccess) {
@@ -20103,6 +20465,76 @@ ${buildRetryGuidance(detected)}`;
20103
20465
  }
20104
20466
  };
20105
20467
  }
20468
+ // src/hooks/filter-available-skills/index.ts
20469
+ var AVAILABLE_SKILLS_BLOCK_REGEX = /<available_skills>\s*([\s\S]*?)\s*<\/available_skills>/g;
20470
+ var SKILL_NAME_REGEX = /<name>([^<]+)<\/name>/;
20471
+ function getCurrentAgent(messages) {
20472
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
20473
+ const message = messages[index];
20474
+ if (message.info.role === "user") {
20475
+ return message.info.agent ?? "orchestrator";
20476
+ }
20477
+ }
20478
+ return "orchestrator";
20479
+ }
20480
+ function extractSkillEntries(blockContent) {
20481
+ const entries = [];
20482
+ const skillEntryRegex = /<skill>\s*([\s\S]*?)\s*<\/skill>/g;
20483
+ for (const match of blockContent.matchAll(skillEntryRegex)) {
20484
+ const block = match[0];
20485
+ const nameMatch = block.match(SKILL_NAME_REGEX);
20486
+ if (!nameMatch) {
20487
+ continue;
20488
+ }
20489
+ entries.push({
20490
+ name: nameMatch[1].trim(),
20491
+ block
20492
+ });
20493
+ }
20494
+ return entries;
20495
+ }
20496
+ function isSkillAllowed(skillName, permissionRules) {
20497
+ const specificRule = permissionRules[skillName];
20498
+ if (specificRule !== undefined) {
20499
+ return specificRule === "allow";
20500
+ }
20501
+ return permissionRules["*"] === "allow";
20502
+ }
20503
+ function filterAvailableSkillsText(text, permissionRules) {
20504
+ return text.replace(AVAILABLE_SKILLS_BLOCK_REGEX, (_fullMatch, blockContent) => {
20505
+ const allowedEntries = extractSkillEntries(blockContent).filter((entry) => isSkillAllowed(entry.name, permissionRules));
20506
+ if (allowedEntries.length === 0) {
20507
+ return `<available_skills>
20508
+ No skills available.
20509
+ </available_skills>`;
20510
+ }
20511
+ return `<available_skills>
20512
+ ${allowedEntries.map((entry) => entry.block).join(`
20513
+ `)}
20514
+ </available_skills>`;
20515
+ });
20516
+ }
20517
+ function createFilterAvailableSkillsHook(_ctx, config2) {
20518
+ return {
20519
+ "experimental.chat.messages.transform": async (_input, output) => {
20520
+ const { messages } = output;
20521
+ if (messages.length === 0) {
20522
+ return;
20523
+ }
20524
+ const agentName = getCurrentAgent(messages);
20525
+ const configuredSkills = getAgentOverride(config2, agentName)?.skills;
20526
+ const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
20527
+ for (const message of messages) {
20528
+ for (const part of message.parts) {
20529
+ if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
20530
+ continue;
20531
+ }
20532
+ part.text = filterAvailableSkillsText(part.text, permissionRules);
20533
+ }
20534
+ }
20535
+ }
20536
+ };
20537
+ }
20106
20538
  // src/hooks/foreground-fallback/index.ts
20107
20539
  var RATE_LIMIT_PATTERNS = [
20108
20540
  /\b429\b/,
@@ -20438,12 +20870,31 @@ var grep_app = {
20438
20870
  };
20439
20871
 
20440
20872
  // src/mcp/websearch.ts
20441
- var websearch = {
20442
- type: "remote",
20443
- url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
20444
- headers: process.env.EXA_API_KEY ? { "x-api-key": process.env.EXA_API_KEY } : undefined,
20445
- oauth: false
20446
- };
20873
+ function createWebsearchConfig(config2) {
20874
+ const provider = config2?.provider || "exa";
20875
+ if (provider === "tavily") {
20876
+ const tavilyKey = process.env.TAVILY_API_KEY;
20877
+ if (!tavilyKey) {
20878
+ throw new Error("TAVILY_API_KEY environment variable is required for Tavily provider");
20879
+ }
20880
+ return {
20881
+ type: "remote",
20882
+ url: "https://mcp.tavily.com/mcp/",
20883
+ headers: {
20884
+ Authorization: `Bearer ${tavilyKey}`
20885
+ },
20886
+ oauth: false
20887
+ };
20888
+ }
20889
+ const exaKey = process.env.EXA_API_KEY;
20890
+ const exaUrl = exaKey ? `https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=${encodeURIComponent(exaKey)}` : "https://mcp.exa.ai/mcp?tools=web_search_exa";
20891
+ return {
20892
+ type: "remote",
20893
+ url: exaUrl,
20894
+ oauth: false
20895
+ };
20896
+ }
20897
+ var websearch = createWebsearchConfig();
20447
20898
 
20448
20899
  // src/mcp/index.ts
20449
20900
  var allBuiltinMcps = {
@@ -20451,8 +20902,12 @@ var allBuiltinMcps = {
20451
20902
  context7,
20452
20903
  grep_app
20453
20904
  };
20454
- function createBuiltinMcps(disabledMcps = []) {
20455
- return Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name)));
20905
+ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
20906
+ const mcps = Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name)));
20907
+ if (!disabledMcps.includes("websearch")) {
20908
+ mcps.websearch = createWebsearchConfig(websearchConfig);
20909
+ }
20910
+ return mcps;
20456
20911
  }
20457
20912
 
20458
20913
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
@@ -32778,15 +33233,15 @@ tool.schema = exports_external2;
32778
33233
 
32779
33234
  // src/tools/ast-grep/cli.ts
32780
33235
  import { existsSync as existsSync6 } from "fs";
32781
- var {spawn: spawn3 } = globalThis.Bun;
33236
+ var {spawn: spawn4 } = globalThis.Bun;
32782
33237
 
32783
33238
  // src/tools/ast-grep/constants.ts
32784
33239
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
32785
33240
  import { createRequire as createRequire2 } from "module";
32786
- import { dirname as dirname3, join as join8 } from "path";
33241
+ import { dirname as dirname4, join as join8 } from "path";
32787
33242
 
32788
33243
  // src/tools/ast-grep/downloader.ts
32789
- import { chmodSync, existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
33244
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, unlinkSync } from "fs";
32790
33245
  import { createRequire } from "module";
32791
33246
  import { homedir as homedir3 } from "os";
32792
33247
  import { join as join7 } from "path";
@@ -32846,7 +33301,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
32846
33301
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
32847
33302
  try {
32848
33303
  if (!existsSync4(cacheDir)) {
32849
- mkdirSync(cacheDir, { recursive: true });
33304
+ mkdirSync2(cacheDir, { recursive: true });
32850
33305
  }
32851
33306
  const response = await fetch(downloadUrl, { redirect: "follow" });
32852
33307
  if (!response.ok) {
@@ -32940,7 +33395,7 @@ function findSgCliPathSync() {
32940
33395
  try {
32941
33396
  const require2 = createRequire2(import.meta.url);
32942
33397
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
32943
- const cliDir = dirname3(cliPkgPath);
33398
+ const cliDir = dirname4(cliPkgPath);
32944
33399
  const sgPath = join8(cliDir, binaryName);
32945
33400
  if (existsSync5(sgPath) && isValidBinary(sgPath)) {
32946
33401
  return sgPath;
@@ -32951,7 +33406,7 @@ function findSgCliPathSync() {
32951
33406
  try {
32952
33407
  const require2 = createRequire2(import.meta.url);
32953
33408
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
32954
- const pkgDir = dirname3(pkgPath);
33409
+ const pkgDir = dirname4(pkgPath);
32955
33410
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
32956
33411
  const binaryPath = join8(pkgDir, astGrepName);
32957
33412
  if (existsSync5(binaryPath) && isValidBinary(binaryPath)) {
@@ -33045,7 +33500,7 @@ async function runSg(options) {
33045
33500
  }
33046
33501
  }
33047
33502
  const timeout = DEFAULT_TIMEOUT_MS2;
33048
- const proc = spawn3([cliPath, ...args], {
33503
+ const proc = spawn4([cliPath, ...args], {
33049
33504
  stdout: "pipe",
33050
33505
  stderr: "pipe"
33051
33506
  });
@@ -33325,7 +33780,7 @@ var ast_grep_replace = tool({
33325
33780
  });
33326
33781
  // src/tools/background.ts
33327
33782
  var z2 = tool.schema;
33328
- function createBackgroundTools(_ctx, manager, _tmuxConfig, _pluginConfig) {
33783
+ function createBackgroundTools(_ctx, manager, _multiplexerConfig, _pluginConfig) {
33329
33784
  const agentNames = SUBAGENT_NAMES.join(", ");
33330
33785
  const background_task = tool({
33331
33786
  description: `Launch background agent task. Returns task_id immediately.
@@ -33495,16 +33950,16 @@ Returns the synthesized result with councillor summary.`,
33495
33950
  // src/tools/lsp/client.ts
33496
33951
  var import_node = __toESM(require_main(), 1);
33497
33952
  import { readFileSync as readFileSync4 } from "fs";
33498
- import { extname, resolve as resolve2 } from "path";
33953
+ import { extname, resolve as resolve3 } from "path";
33499
33954
  import { Readable, Writable } from "stream";
33500
33955
  import { pathToFileURL } from "url";
33501
- var {spawn: spawn4 } = globalThis.Bun;
33956
+ var {spawn: spawn5 } = globalThis.Bun;
33502
33957
 
33503
33958
  // src/tools/lsp/config.ts
33504
33959
  var import_which = __toESM(require_lib(), 1);
33505
33960
  import { existsSync as existsSync8 } from "fs";
33506
33961
  import { homedir as homedir4 } from "os";
33507
- import { join as join9 } from "path";
33962
+ import { dirname as dirname6, join as join9, resolve as resolve2 } from "path";
33508
33963
 
33509
33964
  // src/tools/lsp/config-store.ts
33510
33965
  var userConfig = new Map;
@@ -33535,7 +33990,7 @@ function hasUserLspConfig() {
33535
33990
 
33536
33991
  // src/tools/lsp/constants.ts
33537
33992
  import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
33538
- import { dirname as dirname4, resolve } from "path";
33993
+ import { dirname as dirname5, resolve } from "path";
33539
33994
  var SEVERITY_MAP = {
33540
33995
  1: "error",
33541
33996
  2: "warning",
@@ -33555,10 +34010,10 @@ function* walkUpDirectories(start, stop) {
33555
34010
  let dir = resolve(start);
33556
34011
  try {
33557
34012
  if (!statSync3(dir).isDirectory()) {
33558
- dir = dirname4(dir);
34013
+ dir = dirname5(dir);
33559
34014
  }
33560
34015
  } catch {
33561
- dir = dirname4(dir);
34016
+ dir = dirname5(dir);
33562
34017
  }
33563
34018
  let prevDir = "";
33564
34019
  while (dir !== prevDir && dir !== "/") {
@@ -33566,7 +34021,7 @@ function* walkUpDirectories(start, stop) {
33566
34021
  prevDir = dir;
33567
34022
  if (dir === stop)
33568
34023
  break;
33569
- dir = dirname4(dir);
34024
+ dir = dirname5(dir);
33570
34025
  }
33571
34026
  }
33572
34027
  function NearestRoot(includePatterns, excludePatterns) {
@@ -34107,39 +34562,79 @@ function buildMergedServers() {
34107
34562
  }
34108
34563
  return servers;
34109
34564
  }
34110
- function findServerForExtension(ext) {
34111
- const servers = buildMergedServers();
34112
- for (const [, config3] of servers) {
34113
- if (config3.extensions.includes(ext)) {
34114
- const server = {
34115
- id: config3.id,
34116
- command: config3.command,
34117
- extensions: config3.extensions,
34118
- root: config3.root,
34119
- env: config3.env,
34120
- initialization: config3.initialization
34121
- };
34122
- if (isServerInstalled(config3.command)) {
34123
- return { status: "found", server };
34124
- }
34125
- return {
34565
+ function getServerWorkspace(config3, filePath) {
34566
+ if (!filePath) {
34567
+ return;
34568
+ }
34569
+ if (!config3.root) {
34570
+ return dirname6(resolve2(filePath));
34571
+ }
34572
+ return config3.root(filePath);
34573
+ }
34574
+ function shouldSkipServer(config3, filePath) {
34575
+ if (!filePath) {
34576
+ return false;
34577
+ }
34578
+ return config3.id === "deno" && getServerWorkspace(config3, filePath) === undefined;
34579
+ }
34580
+ function toResolvedServer(config3, command) {
34581
+ return {
34582
+ id: config3.id,
34583
+ command: command ?? config3.command,
34584
+ extensions: config3.extensions,
34585
+ root: config3.root,
34586
+ env: config3.env,
34587
+ initialization: config3.initialization
34588
+ };
34589
+ }
34590
+ function findInstalledServer(configs, filePath) {
34591
+ let firstNotInstalled = null;
34592
+ for (const config3 of configs) {
34593
+ const workspace = getServerWorkspace(config3, filePath);
34594
+ const resolvedCommand = resolveServerCommand(config3.command, workspace ?? (filePath ? dirname6(resolve2(filePath)) : undefined));
34595
+ const server = toResolvedServer(config3, resolvedCommand ?? undefined);
34596
+ log(`[LSP] Considering server for ${config3.extensions.join(", ")}: ${config3.id} with command ${config3.command.join(" ")}`);
34597
+ if (resolvedCommand) {
34598
+ return { status: "found", server };
34599
+ }
34600
+ if (!firstNotInstalled) {
34601
+ log(`[LSP] Server ${config3.id} not found in PATH or local node_modules`);
34602
+ firstNotInstalled = {
34126
34603
  status: "not_installed",
34127
34604
  server,
34128
34605
  installHint: LSP_INSTALL_HINTS[config3.id] || `Install '${config3.command[0]}' and add to PATH`
34129
34606
  };
34130
34607
  }
34131
34608
  }
34609
+ return firstNotInstalled ?? undefined;
34610
+ }
34611
+ function findServerForExtension(ext, filePath) {
34612
+ const servers = [...buildMergedServers().values()].filter((config3) => config3.extensions.includes(ext));
34613
+ if (servers.length === 0) {
34614
+ log(`[LSP] No server config found for ${ext}`);
34615
+ return { status: "not_configured", extension: ext };
34616
+ }
34617
+ const candidateServers = servers.filter((config3) => !shouldSkipServer(config3, filePath));
34618
+ if (candidateServers.length === 0) {
34619
+ log(`[LSP] No applicable server config found for ${ext} at ${filePath}`);
34620
+ return { status: "not_configured", extension: ext };
34621
+ }
34622
+ const result = findInstalledServer(candidateServers, filePath);
34623
+ if (result) {
34624
+ return result;
34625
+ }
34626
+ log(`[LSP] No applicable server config found for ${ext}`);
34132
34627
  return { status: "not_configured", extension: ext };
34133
34628
  }
34134
34629
  function getLanguageId(ext) {
34135
34630
  return LANGUAGE_EXTENSIONS[ext] || "plaintext";
34136
34631
  }
34137
- function isServerInstalled(command) {
34632
+ function resolveServerCommand(command, cwd) {
34138
34633
  if (command.length === 0)
34139
- return false;
34140
- const cmd = command[0];
34634
+ return null;
34635
+ const [cmd, ...args] = command;
34141
34636
  if (cmd.includes("/") || cmd.includes("\\")) {
34142
- return existsSync8(cmd);
34637
+ return existsSync8(cmd) ? command : null;
34143
34638
  }
34144
34639
  const isWindows = process.platform === "win32";
34145
34640
  const ext = isWindows ? ".exe" : "";
@@ -34151,17 +34646,94 @@ function isServerInstalled(command) {
34151
34646
  nothrow: true
34152
34647
  });
34153
34648
  if (result !== null) {
34154
- return true;
34649
+ return [result, ...args];
34155
34650
  }
34156
- const cwd = process.cwd();
34157
- const localBin = join9(cwd, "node_modules", ".bin", cmd);
34158
- if (existsSync8(localBin) || existsSync8(localBin + ext)) {
34159
- return true;
34651
+ const localBinRoot = cwd ?? process.cwd();
34652
+ const localBin = join9(localBinRoot, "node_modules", ".bin", cmd);
34653
+ if (existsSync8(localBin)) {
34654
+ return [localBin, ...args];
34160
34655
  }
34161
- return false;
34656
+ if (existsSync8(localBin + ext)) {
34657
+ return [localBin + ext, ...args];
34658
+ }
34659
+ return null;
34162
34660
  }
34163
34661
 
34164
34662
  // src/tools/lsp/client.ts
34663
+ var START_TIMEOUT_MS = 5000;
34664
+ var REQUEST_TIMEOUT_MS = 5000;
34665
+ var OPEN_FILE_DELAY_MS = 250;
34666
+ var INITIALIZE_DELAY_MS = 100;
34667
+ var DIAGNOSTIC_SETTLE_DELAY_MS = 250;
34668
+ var LSP_TIMEOUTS = {
34669
+ start: START_TIMEOUT_MS,
34670
+ request: REQUEST_TIMEOUT_MS,
34671
+ openFileDelay: OPEN_FILE_DELAY_MS,
34672
+ initializeDelay: INITIALIZE_DELAY_MS,
34673
+ diagnosticSettleDelay: DIAGNOSTIC_SETTLE_DELAY_MS
34674
+ };
34675
+ function getDiagnosticsCapabilitySummary({
34676
+ diagnosticProvider,
34677
+ publishDiagnosticsObserved = false,
34678
+ workspaceConfigurationRequested = false
34679
+ }) {
34680
+ const pull = Boolean(diagnosticProvider);
34681
+ const workspaceDiagnostics = Boolean(diagnosticProvider?.workspaceDiagnostics);
34682
+ const interFileDependencies = Boolean(diagnosticProvider?.interFileDependencies);
34683
+ const availableModes = [
34684
+ ...pull ? ["pull", "pull/full", "pull/unchanged"] : ["push"],
34685
+ ...workspaceDiagnostics ? ["workspace-pull"] : [],
34686
+ ...publishDiagnosticsObserved ? ["push"] : []
34687
+ ];
34688
+ return {
34689
+ availableModes: Array.from(new Set(availableModes)),
34690
+ preferredMode: pull ? "pull" : "push",
34691
+ inferredTransport: pull && publishDiagnosticsObserved ? "hybrid" : pull ? "pull" : "push",
34692
+ pull,
34693
+ pushObserved: publishDiagnosticsObserved,
34694
+ pullResultTracking: pull,
34695
+ workspaceDiagnostics,
34696
+ interFileDependencies,
34697
+ workspaceConfiguration: workspaceConfigurationRequested
34698
+ };
34699
+ }
34700
+ function withTimeout(promise3, ms, label, onTimeout) {
34701
+ return new Promise((resolve4, reject) => {
34702
+ let settled = false;
34703
+ const timer = setTimeout(() => {
34704
+ if (settled) {
34705
+ return;
34706
+ }
34707
+ settled = true;
34708
+ Promise.resolve(onTimeout?.()).catch(() => {});
34709
+ reject(new Error(`${label} timeout after ${ms}ms`));
34710
+ }, ms);
34711
+ promise3.then((value) => {
34712
+ if (settled) {
34713
+ return;
34714
+ }
34715
+ settled = true;
34716
+ clearTimeout(timer);
34717
+ resolve4(value);
34718
+ }, (error92) => {
34719
+ if (settled) {
34720
+ return;
34721
+ }
34722
+ settled = true;
34723
+ clearTimeout(timer);
34724
+ reject(error92);
34725
+ });
34726
+ });
34727
+ }
34728
+ function getWorkspaceConfiguration(items) {
34729
+ return items.map((item) => {
34730
+ if (item?.section === "json") {
34731
+ return { validate: { enable: true } };
34732
+ }
34733
+ return null;
34734
+ });
34735
+ }
34736
+
34165
34737
  class LSPServerManager {
34166
34738
  static instance;
34167
34739
  clients = new Map;
@@ -34326,17 +34898,27 @@ class LSPClient {
34326
34898
  stderrBuffer = [];
34327
34899
  processExited = false;
34328
34900
  diagnosticsStore = new Map;
34901
+ diagnosticResultIds = new Map;
34902
+ documents = new Map;
34903
+ diagnosticProvider = null;
34904
+ publishDiagnosticsObserved = false;
34905
+ supportsPullDiagnostics = false;
34906
+ workspaceConfigurationRequested = false;
34329
34907
  constructor(root, server) {
34330
34908
  this.root = root;
34331
34909
  this.server = server;
34332
34910
  }
34333
34911
  async start() {
34912
+ const command = resolveServerCommand(this.server.command, this.root);
34913
+ if (!command) {
34914
+ throw new Error(`Failed to resolve LSP server command: ${this.server.command.join(" ")}`);
34915
+ }
34334
34916
  log("[lsp] LSPClient.start: spawning server", {
34335
34917
  server: this.server.id,
34336
- command: this.server.command.join(" "),
34918
+ command: command.join(" "),
34337
34919
  root: this.root
34338
34920
  });
34339
- this.proc = spawn4(this.server.command, {
34921
+ this.proc = spawn5(command, {
34340
34922
  stdin: "pipe",
34341
34923
  stdout: "pipe",
34342
34924
  stderr: "pipe",
@@ -34386,18 +34968,35 @@ class LSPClient {
34386
34968
  });
34387
34969
  this.connection = import_node.createMessageConnection(new import_node.StreamMessageReader(nodeReadable), new import_node.StreamMessageWriter(nodeWritable));
34388
34970
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
34971
+ if (!this.publishDiagnosticsObserved) {
34972
+ this.publishDiagnosticsObserved = true;
34973
+ log("[lsp] diagnostics capabilities: publishDiagnostics observed", {
34974
+ server: this.server.id,
34975
+ ...getDiagnosticsCapabilitySummary({
34976
+ diagnosticProvider: this.diagnosticProvider,
34977
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
34978
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
34979
+ })
34980
+ });
34981
+ }
34389
34982
  if (params.uri) {
34390
34983
  this.diagnosticsStore.set(params.uri, params.diagnostics ?? []);
34391
34984
  }
34392
34985
  });
34393
34986
  this.connection.onRequest("workspace/configuration", (params) => {
34394
- const items = params.items ?? [];
34395
- return items.map((item) => {
34396
- const configItem = item;
34397
- if (configItem.section === "json")
34398
- return { validate: { enable: true } };
34399
- return {};
34400
- });
34987
+ if (!this.workspaceConfigurationRequested) {
34988
+ this.workspaceConfigurationRequested = true;
34989
+ log("[lsp] diagnostics capabilities: workspace configuration requested", {
34990
+ server: this.server.id,
34991
+ sections: (params.items ?? []).map((item) => item && typeof item === "object" && ("section" in item) ? item.section ?? null : null),
34992
+ ...getDiagnosticsCapabilitySummary({
34993
+ diagnosticProvider: this.diagnosticProvider,
34994
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
34995
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
34996
+ })
34997
+ });
34998
+ }
34999
+ return getWorkspaceConfiguration(params.items ?? []);
34401
35000
  });
34402
35001
  this.connection.onRequest("client/registerCapability", () => null);
34403
35002
  this.connection.onRequest("window/workDoneProgress/create", () => null);
@@ -34405,7 +35004,7 @@ class LSPClient {
34405
35004
  this.processExited = true;
34406
35005
  });
34407
35006
  this.connection.listen();
34408
- await new Promise((resolve3) => setTimeout(resolve3, 100));
35007
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
34409
35008
  if (this.proc.exitCode !== null) {
34410
35009
  const stderr = this.stderrBuffer.join(`
34411
35010
  `);
@@ -34448,13 +35047,14 @@ stderr: ${stderr}` : ""));
34448
35047
  root: this.root
34449
35048
  });
34450
35049
  const rootUri = pathToFileURL(this.root).href;
34451
- await this.connection.sendRequest("initialize", {
35050
+ const result = await withTimeout(this.connection.sendRequest("initialize", {
34452
35051
  processId: process.pid,
34453
35052
  rootUri,
34454
35053
  rootPath: this.root,
34455
35054
  workspaceFolders: [{ uri: rootUri, name: "workspace" }],
34456
35055
  capabilities: {
34457
35056
  textDocument: {
35057
+ diagnostic: {},
34458
35058
  hover: { contentFormat: ["markdown", "plaintext"] },
34459
35059
  definition: { linkSupport: true },
34460
35060
  references: {},
@@ -34475,76 +35075,163 @@ stderr: ${stderr}` : ""));
34475
35075
  }
34476
35076
  },
34477
35077
  ...this.server.initialization
35078
+ }), LSP_TIMEOUTS.request, `LSP initialize (${this.server.id})`);
35079
+ const capabilities = result && typeof result === "object" && "capabilities" in result && result.capabilities && typeof result.capabilities === "object" ? result.capabilities : undefined;
35080
+ this.diagnosticProvider = capabilities && "diagnosticProvider" in capabilities ? capabilities.diagnosticProvider : null;
35081
+ this.supportsPullDiagnostics = Boolean(this.diagnosticProvider);
35082
+ log("[lsp] diagnostics capabilities negotiated", {
35083
+ server: this.server.id,
35084
+ diagnosticProvider: this.diagnosticProvider,
35085
+ ...getDiagnosticsCapabilitySummary({
35086
+ diagnosticProvider: this.diagnosticProvider,
35087
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
35088
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
35089
+ })
34478
35090
  });
34479
- this.connection.sendNotification("initialized");
34480
- await new Promise((r) => setTimeout(r, 300));
35091
+ this.connection.sendNotification("initialized", {});
35092
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.initializeDelay));
34481
35093
  log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
34482
35094
  }
34483
- async openFile(filePath) {
34484
- const absPath = resolve2(filePath);
34485
- if (this.openedFiles.has(absPath)) {
34486
- log("[lsp] openFile: already open, skipping", { filePath: absPath });
34487
- return;
35095
+ async waitForPublishedDiagnostics(uri, timeoutMs = LSP_TIMEOUTS.request) {
35096
+ const cachedDiagnostics = this.diagnosticsStore.get(uri);
35097
+ if (cachedDiagnostics) {
35098
+ return cachedDiagnostics;
35099
+ }
35100
+ const startedAt = Date.now();
35101
+ while (Date.now() - startedAt < timeoutMs) {
35102
+ await new Promise((r) => setTimeout(r, 100));
35103
+ const diagnostics = this.diagnosticsStore.get(uri);
35104
+ if (diagnostics) {
35105
+ return diagnostics;
35106
+ }
34488
35107
  }
35108
+ return this.diagnosticsStore.get(uri);
35109
+ }
35110
+ async openFile(filePath) {
35111
+ await this.ensureDocumentSynced(filePath);
35112
+ }
35113
+ async ensureDocumentSynced(filePath) {
35114
+ const absPath = resolve3(filePath);
35115
+ const uri = pathToFileURL(absPath).href;
34489
35116
  const text = readFileSync4(absPath, "utf-8");
34490
35117
  const ext = extname(absPath);
34491
35118
  const languageId = getLanguageId(ext);
34492
- log("[lsp] openFile: opening document", {
34493
- filePath: absPath,
34494
- languageId,
34495
- size: text.length
34496
- });
34497
- this.connection?.sendNotification("textDocument/didOpen", {
34498
- textDocument: {
34499
- uri: pathToFileURL(absPath).href,
35119
+ const existing = this.documents.get(uri);
35120
+ if (!existing) {
35121
+ log("[lsp] ensureDocumentSynced: didOpen", {
35122
+ filePath: absPath,
34500
35123
  languageId,
34501
- version: 1,
34502
- text
34503
- }
34504
- });
34505
- this.openedFiles.add(absPath);
34506
- await new Promise((r) => setTimeout(r, 1000));
35124
+ size: text.length
35125
+ });
35126
+ this.connection?.sendNotification("textDocument/didOpen", {
35127
+ textDocument: { uri, languageId, version: 1, text }
35128
+ });
35129
+ this.documents.set(uri, { version: 1, text, languageId });
35130
+ this.openedFiles.add(absPath);
35131
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.openFileDelay));
35132
+ return;
35133
+ }
35134
+ if (existing.text !== text) {
35135
+ const newVersion = existing.version + 1;
35136
+ log("[lsp] ensureDocumentSynced: didChange", {
35137
+ filePath: absPath,
35138
+ languageId,
35139
+ oldVersion: existing.version,
35140
+ newVersion,
35141
+ size: text.length
35142
+ });
35143
+ this.connection?.sendNotification("textDocument/didChange", {
35144
+ textDocument: { uri, version: newVersion },
35145
+ contentChanges: [{ text }]
35146
+ });
35147
+ this.documents.set(uri, { version: newVersion, text, languageId });
35148
+ this.diagnosticsStore.delete(uri);
35149
+ this.diagnosticResultIds.delete(uri);
35150
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.openFileDelay));
35151
+ } else {
35152
+ log("[lsp] ensureDocumentSynced: already synced", { filePath: absPath });
35153
+ }
34507
35154
  }
34508
35155
  async definition(filePath, line, character) {
34509
- const absPath = resolve2(filePath);
35156
+ const absPath = resolve3(filePath);
34510
35157
  await this.openFile(absPath);
34511
- return this.connection?.sendRequest("textDocument/definition", {
35158
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/definition", {
34512
35159
  textDocument: { uri: pathToFileURL(absPath).href },
34513
35160
  position: { line: line - 1, character }
34514
- });
35161
+ }), LSP_TIMEOUTS.request, `LSP definition (${this.server.id})`) : undefined;
34515
35162
  }
34516
35163
  async references(filePath, line, character, includeDeclaration = true) {
34517
- const absPath = resolve2(filePath);
35164
+ const absPath = resolve3(filePath);
34518
35165
  await this.openFile(absPath);
34519
- return this.connection?.sendRequest("textDocument/references", {
35166
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/references", {
34520
35167
  textDocument: { uri: pathToFileURL(absPath).href },
34521
35168
  position: { line: line - 1, character },
34522
35169
  context: { includeDeclaration }
34523
- });
35170
+ }), LSP_TIMEOUTS.request, `LSP references (${this.server.id})`) : undefined;
34524
35171
  }
34525
35172
  async diagnostics(filePath) {
34526
- const absPath = resolve2(filePath);
35173
+ const absPath = resolve3(filePath);
34527
35174
  const uri = pathToFileURL(absPath).href;
34528
35175
  await this.openFile(absPath);
34529
- await new Promise((r) => setTimeout(r, 500));
34530
- try {
34531
- const result = await this.connection?.sendRequest("textDocument/diagnostic", {
34532
- textDocument: { uri }
34533
- });
34534
- if (result && typeof result === "object" && "items" in result) {
34535
- return result;
35176
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.diagnosticSettleDelay));
35177
+ log("[lsp] diagnostics mode selected", {
35178
+ server: this.server.id,
35179
+ filePath: absPath,
35180
+ activeMode: this.supportsPullDiagnostics ? "pull" : "push",
35181
+ ...getDiagnosticsCapabilitySummary({
35182
+ diagnosticProvider: this.diagnosticProvider,
35183
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
35184
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
35185
+ })
35186
+ });
35187
+ if (this.supportsPullDiagnostics) {
35188
+ try {
35189
+ const result = this.connection ? await withTimeout(this.connection.sendRequest("textDocument/diagnostic", {
35190
+ textDocument: { uri },
35191
+ previousResultId: this.diagnosticResultIds.get(uri)
35192
+ }), LSP_TIMEOUTS.request, `LSP diagnostics (${this.server.id})`) : undefined;
35193
+ const report = result;
35194
+ if (report?.kind === "full") {
35195
+ if (report.resultId) {
35196
+ this.diagnosticResultIds.set(uri, report.resultId);
35197
+ } else {
35198
+ this.diagnosticResultIds.delete(uri);
35199
+ }
35200
+ this.diagnosticsStore.set(uri, report.items);
35201
+ return { items: report.items };
35202
+ }
35203
+ if (report?.kind === "unchanged") {
35204
+ if (report.resultId) {
35205
+ this.diagnosticResultIds.set(uri, report.resultId);
35206
+ }
35207
+ return { items: this.diagnosticsStore.get(uri) ?? [] };
35208
+ }
35209
+ if (result && typeof result === "object" && "items" in result) {
35210
+ const legacyResult = result;
35211
+ this.diagnosticsStore.set(uri, legacyResult.items);
35212
+ return legacyResult;
35213
+ }
35214
+ } catch (error92) {
35215
+ log("[lsp] diagnostics: falling back to cached publishDiagnostics", {
35216
+ server: this.server.id,
35217
+ error: String(error92)
35218
+ });
34536
35219
  }
34537
- } catch {}
34538
- return { items: this.diagnosticsStore.get(uri) ?? [] };
35220
+ }
35221
+ const cachedDiagnostics = await this.waitForPublishedDiagnostics(uri);
35222
+ if (cachedDiagnostics) {
35223
+ return { items: cachedDiagnostics };
35224
+ }
35225
+ throw new Error(`Unable to retrieve diagnostics from ${this.server.id}: request timed out or is unsupported.`);
34539
35226
  }
34540
35227
  async rename(filePath, line, character, newName) {
34541
- const absPath = resolve2(filePath);
35228
+ const absPath = resolve3(filePath);
34542
35229
  await this.openFile(absPath);
34543
- return this.connection?.sendRequest("textDocument/rename", {
35230
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/rename", {
34544
35231
  textDocument: { uri: pathToFileURL(absPath).href },
34545
35232
  position: { line: line - 1, character },
34546
35233
  newName
34547
- });
35234
+ }), LSP_TIMEOUTS.request, `LSP rename (${this.server.id})`) : undefined;
34548
35235
  }
34549
35236
  isAlive() {
34550
35237
  return this.proc !== null && !this.processExited && this.proc.exitCode === null;
@@ -34553,7 +35240,7 @@ stderr: ${stderr}` : ""));
34553
35240
  log("[lsp] LSPClient.stop: stopping", { server: this.server.id });
34554
35241
  try {
34555
35242
  if (this.connection) {
34556
- await this.connection.sendRequest("shutdown");
35243
+ await withTimeout(this.connection.sendRequest("shutdown"), 1000, `LSP shutdown (${this.server.id})`);
34557
35244
  this.connection.sendNotification("exit");
34558
35245
  this.connection.dispose();
34559
35246
  }
@@ -34562,7 +35249,13 @@ stderr: ${stderr}` : ""));
34562
35249
  this.proc = null;
34563
35250
  this.connection = null;
34564
35251
  this.processExited = true;
35252
+ this.diagnosticProvider = null;
35253
+ this.publishDiagnosticsObserved = false;
35254
+ this.supportsPullDiagnostics = false;
35255
+ this.workspaceConfigurationRequested = false;
34565
35256
  this.diagnosticsStore.clear();
35257
+ this.diagnosticResultIds.clear();
35258
+ this.documents.clear();
34566
35259
  log("[lsp] LSPClient.stop: complete", { server: this.server.id });
34567
35260
  }
34568
35261
  }
@@ -34574,13 +35267,13 @@ import {
34574
35267
  unlinkSync as unlinkSync2,
34575
35268
  writeFileSync as writeFileSync3
34576
35269
  } from "fs";
34577
- import { dirname as dirname5, extname as extname2, join as join10, resolve as resolve3 } from "path";
35270
+ import { dirname as dirname7, extname as extname2, join as join10, resolve as resolve4 } from "path";
34578
35271
  import { fileURLToPath as fileURLToPath2 } from "url";
34579
35272
  function findServerProjectRoot(filePath, server) {
34580
35273
  if (server.root) {
34581
- return server.root(filePath) ?? dirname5(resolve3(filePath));
35274
+ return server.root(filePath) ?? dirname7(resolve4(filePath));
34582
35275
  }
34583
- return dirname5(resolve3(filePath));
35276
+ return dirname7(resolve4(filePath));
34584
35277
  }
34585
35278
  function uriToPath(uri) {
34586
35279
  return fileURLToPath2(uri);
@@ -34599,9 +35292,9 @@ function formatServerLookupError(result) {
34599
35292
  return `No LSP server configured for extension: ${result.extension}`;
34600
35293
  }
34601
35294
  async function withLspClient(filePath, fn) {
34602
- const absPath = resolve3(filePath);
35295
+ const absPath = resolve4(filePath);
34603
35296
  const ext = extname2(absPath);
34604
- const result = findServerForExtension(ext);
35297
+ const result = findServerForExtension(ext, absPath);
34605
35298
  if (result.status !== "found") {
34606
35299
  log("[lsp] withLspClient: server not found", {
34607
35300
  filePath: absPath,
@@ -34610,7 +35303,14 @@ async function withLspClient(filePath, fn) {
34610
35303
  throw new Error(formatServerLookupError(result));
34611
35304
  }
34612
35305
  const server = result.server;
34613
- const root = findServerProjectRoot(absPath, server) ?? dirname5(absPath);
35306
+ const root = findServerProjectRoot(absPath, server) ?? dirname7(absPath);
35307
+ log("[lsp] withLspClient: selected server", {
35308
+ filePath: absPath,
35309
+ extension: ext,
35310
+ server: server.id,
35311
+ command: server.command.join(" "),
35312
+ root
35313
+ });
34614
35314
  log("[lsp] withLspClient: acquiring client", {
34615
35315
  filePath: absPath,
34616
35316
  server: server.id,
@@ -34970,29 +35670,32 @@ var OhMyOpenCodeLite = async (ctx) => {
34970
35670
  runtimeChains[agentName] = existing;
34971
35671
  }
34972
35672
  }
34973
- const tmuxConfig = {
34974
- enabled: config3.tmux?.enabled ?? false,
34975
- layout: config3.tmux?.layout ?? "main-vertical",
34976
- main_pane_size: config3.tmux?.main_pane_size ?? 60
35673
+ const multiplexerConfig = {
35674
+ type: config3.multiplexer?.type ?? "none",
35675
+ layout: config3.multiplexer?.layout ?? "main-vertical",
35676
+ main_pane_size: config3.multiplexer?.main_pane_size ?? 60
34977
35677
  };
34978
- log("[plugin] initialized with tmux config", {
34979
- tmuxConfig,
34980
- rawTmuxConfig: config3.tmux,
35678
+ const multiplexer = getMultiplexer(multiplexerConfig);
35679
+ const multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
35680
+ log("[plugin] initialized with multiplexer config", {
35681
+ multiplexerConfig,
35682
+ enabled: multiplexerEnabled,
34981
35683
  directory: ctx.directory
34982
35684
  });
34983
- if (tmuxConfig.enabled) {
34984
- startTmuxCheck();
35685
+ if (multiplexerEnabled) {
35686
+ startAvailabilityCheck(multiplexerConfig);
34985
35687
  }
34986
- const backgroundManager = new BackgroundTaskManager(ctx, tmuxConfig, config3);
34987
- const backgroundTools = createBackgroundTools(ctx, backgroundManager, tmuxConfig, config3);
34988
- const councilTools = config3.council ? createCouncilTool(ctx, new CouncilManager(ctx, config3, backgroundManager.getDepthTracker(), tmuxConfig.enabled)) : {};
34989
- const mcps = createBuiltinMcps(config3.disabled_mcps);
34990
- const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
35688
+ const backgroundManager = new BackgroundTaskManager(ctx, multiplexerConfig, config3);
35689
+ const backgroundTools = createBackgroundTools(ctx, backgroundManager, multiplexerConfig, config3);
35690
+ const councilTools = config3.council ? createCouncilTool(ctx, new CouncilManager(ctx, config3, backgroundManager.getDepthTracker(), multiplexerEnabled)) : {};
35691
+ const mcps = createBuiltinMcps(config3.disabled_mcps, config3.websearch);
35692
+ const multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
34991
35693
  const autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
34992
35694
  showStartupToast: true,
34993
35695
  autoUpdate: true
34994
35696
  });
34995
35697
  const phaseReminderHook = createPhaseReminderHook();
35698
+ const filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config3);
34996
35699
  const postFileToolNudgeHook = createPostFileToolNudgeHook();
34997
35700
  const chatHeadersHook = createChatHeadersHook(ctx);
34998
35701
  const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
@@ -35083,7 +35786,8 @@ var OhMyOpenCodeLite = async (ctx) => {
35083
35786
  } else {
35084
35787
  Object.assign(configMcp, mcps);
35085
35788
  }
35086
- const allMcpNames = Object.keys(mcps);
35789
+ const mergedMcpConfig = opencodeConfig.mcp;
35790
+ const allMcpNames = Object.keys(mergedMcpConfig ?? mcps);
35087
35791
  for (const [agentName, agentConfig] of Object.entries(agents)) {
35088
35792
  const agentMcps = agentConfig?.mcps;
35089
35793
  if (!agentMcps)
@@ -35108,14 +35812,18 @@ var OhMyOpenCodeLite = async (ctx) => {
35108
35812
  event: async (input) => {
35109
35813
  await foregroundFallback.handleEvent(input.event);
35110
35814
  await autoUpdateChecker.event(input);
35111
- await tmuxSessionManager.onSessionCreated(input.event);
35815
+ await multiplexerSessionManager.onSessionCreated(input.event);
35112
35816
  await backgroundManager.handleSessionStatus(input.event);
35113
- await tmuxSessionManager.onSessionStatus(input.event);
35817
+ await multiplexerSessionManager.onSessionStatus(input.event);
35114
35818
  await backgroundManager.handleSessionDeleted(input.event);
35115
- await tmuxSessionManager.onSessionDeleted(input.event);
35819
+ await multiplexerSessionManager.onSessionDeleted(input.event);
35116
35820
  },
35117
35821
  "chat.headers": chatHeadersHook["chat.headers"],
35118
- "experimental.chat.messages.transform": phaseReminderHook["experimental.chat.messages.transform"],
35822
+ "experimental.chat.messages.transform": async (input, output) => {
35823
+ const typedOutput = output;
35824
+ await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
35825
+ await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
35826
+ },
35119
35827
  "tool.execute.after": async (input, output) => {
35120
35828
  await delegateTaskRetryHook["tool.execute.after"](input, output);
35121
35829
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);