oh-my-opencode-slim 0.9.0 → 0.9.2

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";
@@ -17241,7 +17242,7 @@ import * as path from "path";
17241
17242
 
17242
17243
  // src/config/agent-mcps.ts
17243
17244
  var DEFAULT_AGENT_MCPS = {
17244
- orchestrator: ["websearch"],
17245
+ orchestrator: ["*"],
17245
17246
  designer: [],
17246
17247
  oracle: [],
17247
17248
  librarian: ["websearch", "context7", "grep_app"],
@@ -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);
@@ -19893,20 +20260,17 @@ async function runBackgroundUpdateCheck(ctx, autoUpdate) {
19893
20260
  return;
19894
20261
  }
19895
20262
  log(`[auto-update-checker] Update available (${channel}): ${currentVersion} \u2192 ${latestVersion}`);
20263
+ if (pluginInfo.isPinned) {
20264
+ showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available.
20265
+ Version is pinned. Update your plugin config to apply.`, "info", 8000);
20266
+ log(`[auto-update-checker] Version is pinned; skipping auto-update.`);
20267
+ return;
20268
+ }
19896
20269
  if (!autoUpdate) {
19897
- showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available. Restart to apply.`, "info", 8000);
20270
+ showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available. Auto-update is disabled.`, "info", 8000);
19898
20271
  log("[auto-update-checker] Auto-update disabled, notification only");
19899
20272
  return;
19900
20273
  }
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
20274
  invalidatePackage(PACKAGE_NAME);
19911
20275
  const installSuccess = await runBunInstallSafe(ctx);
19912
20276
  if (installSuccess) {
@@ -20103,6 +20467,76 @@ ${buildRetryGuidance(detected)}`;
20103
20467
  }
20104
20468
  };
20105
20469
  }
20470
+ // src/hooks/filter-available-skills/index.ts
20471
+ var AVAILABLE_SKILLS_BLOCK_REGEX = /<available_skills>\s*([\s\S]*?)\s*<\/available_skills>/g;
20472
+ var SKILL_NAME_REGEX = /<name>([^<]+)<\/name>/;
20473
+ function getCurrentAgent(messages) {
20474
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
20475
+ const message = messages[index];
20476
+ if (message.info.role === "user") {
20477
+ return message.info.agent ?? "orchestrator";
20478
+ }
20479
+ }
20480
+ return "orchestrator";
20481
+ }
20482
+ function extractSkillEntries(blockContent) {
20483
+ const entries = [];
20484
+ const skillEntryRegex = /<skill>\s*([\s\S]*?)\s*<\/skill>/g;
20485
+ for (const match of blockContent.matchAll(skillEntryRegex)) {
20486
+ const block = match[0];
20487
+ const nameMatch = block.match(SKILL_NAME_REGEX);
20488
+ if (!nameMatch) {
20489
+ continue;
20490
+ }
20491
+ entries.push({
20492
+ name: nameMatch[1].trim(),
20493
+ block
20494
+ });
20495
+ }
20496
+ return entries;
20497
+ }
20498
+ function isSkillAllowed(skillName, permissionRules) {
20499
+ const specificRule = permissionRules[skillName];
20500
+ if (specificRule !== undefined) {
20501
+ return specificRule === "allow";
20502
+ }
20503
+ return permissionRules["*"] === "allow";
20504
+ }
20505
+ function filterAvailableSkillsText(text, permissionRules) {
20506
+ return text.replace(AVAILABLE_SKILLS_BLOCK_REGEX, (_fullMatch, blockContent) => {
20507
+ const allowedEntries = extractSkillEntries(blockContent).filter((entry) => isSkillAllowed(entry.name, permissionRules));
20508
+ if (allowedEntries.length === 0) {
20509
+ return `<available_skills>
20510
+ No skills available.
20511
+ </available_skills>`;
20512
+ }
20513
+ return `<available_skills>
20514
+ ${allowedEntries.map((entry) => entry.block).join(`
20515
+ `)}
20516
+ </available_skills>`;
20517
+ });
20518
+ }
20519
+ function createFilterAvailableSkillsHook(_ctx, config2) {
20520
+ return {
20521
+ "experimental.chat.messages.transform": async (_input, output) => {
20522
+ const { messages } = output;
20523
+ if (messages.length === 0) {
20524
+ return;
20525
+ }
20526
+ const agentName = getCurrentAgent(messages);
20527
+ const configuredSkills = getAgentOverride(config2, agentName)?.skills;
20528
+ const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
20529
+ for (const message of messages) {
20530
+ for (const part of message.parts) {
20531
+ if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
20532
+ continue;
20533
+ }
20534
+ part.text = filterAvailableSkillsText(part.text, permissionRules);
20535
+ }
20536
+ }
20537
+ }
20538
+ };
20539
+ }
20106
20540
  // src/hooks/foreground-fallback/index.ts
20107
20541
  var RATE_LIMIT_PATTERNS = [
20108
20542
  /\b429\b/,
@@ -20438,12 +20872,31 @@ var grep_app = {
20438
20872
  };
20439
20873
 
20440
20874
  // 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
- };
20875
+ function createWebsearchConfig(config2) {
20876
+ const provider = config2?.provider || "exa";
20877
+ if (provider === "tavily") {
20878
+ const tavilyKey = process.env.TAVILY_API_KEY;
20879
+ if (!tavilyKey) {
20880
+ throw new Error("TAVILY_API_KEY environment variable is required for Tavily provider");
20881
+ }
20882
+ return {
20883
+ type: "remote",
20884
+ url: "https://mcp.tavily.com/mcp/",
20885
+ headers: {
20886
+ Authorization: `Bearer ${tavilyKey}`
20887
+ },
20888
+ oauth: false
20889
+ };
20890
+ }
20891
+ const exaKey = process.env.EXA_API_KEY;
20892
+ const exaUrl = exaKey ? `https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=${encodeURIComponent(exaKey)}` : "https://mcp.exa.ai/mcp?tools=web_search_exa";
20893
+ return {
20894
+ type: "remote",
20895
+ url: exaUrl,
20896
+ oauth: false
20897
+ };
20898
+ }
20899
+ var websearch = createWebsearchConfig();
20447
20900
 
20448
20901
  // src/mcp/index.ts
20449
20902
  var allBuiltinMcps = {
@@ -20451,8 +20904,12 @@ var allBuiltinMcps = {
20451
20904
  context7,
20452
20905
  grep_app
20453
20906
  };
20454
- function createBuiltinMcps(disabledMcps = []) {
20455
- return Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name)));
20907
+ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
20908
+ const mcps = Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name)));
20909
+ if (!disabledMcps.includes("websearch")) {
20910
+ mcps.websearch = createWebsearchConfig(websearchConfig);
20911
+ }
20912
+ return mcps;
20456
20913
  }
20457
20914
 
20458
20915
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
@@ -32778,15 +33235,15 @@ tool.schema = exports_external2;
32778
33235
 
32779
33236
  // src/tools/ast-grep/cli.ts
32780
33237
  import { existsSync as existsSync6 } from "fs";
32781
- var {spawn: spawn3 } = globalThis.Bun;
33238
+ var {spawn: spawn4 } = globalThis.Bun;
32782
33239
 
32783
33240
  // src/tools/ast-grep/constants.ts
32784
33241
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
32785
33242
  import { createRequire as createRequire2 } from "module";
32786
- import { dirname as dirname3, join as join8 } from "path";
33243
+ import { dirname as dirname4, join as join8 } from "path";
32787
33244
 
32788
33245
  // src/tools/ast-grep/downloader.ts
32789
- import { chmodSync, existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
33246
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, unlinkSync } from "fs";
32790
33247
  import { createRequire } from "module";
32791
33248
  import { homedir as homedir3 } from "os";
32792
33249
  import { join as join7 } from "path";
@@ -32846,7 +33303,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
32846
33303
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
32847
33304
  try {
32848
33305
  if (!existsSync4(cacheDir)) {
32849
- mkdirSync(cacheDir, { recursive: true });
33306
+ mkdirSync2(cacheDir, { recursive: true });
32850
33307
  }
32851
33308
  const response = await fetch(downloadUrl, { redirect: "follow" });
32852
33309
  if (!response.ok) {
@@ -32940,7 +33397,7 @@ function findSgCliPathSync() {
32940
33397
  try {
32941
33398
  const require2 = createRequire2(import.meta.url);
32942
33399
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
32943
- const cliDir = dirname3(cliPkgPath);
33400
+ const cliDir = dirname4(cliPkgPath);
32944
33401
  const sgPath = join8(cliDir, binaryName);
32945
33402
  if (existsSync5(sgPath) && isValidBinary(sgPath)) {
32946
33403
  return sgPath;
@@ -32951,7 +33408,7 @@ function findSgCliPathSync() {
32951
33408
  try {
32952
33409
  const require2 = createRequire2(import.meta.url);
32953
33410
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
32954
- const pkgDir = dirname3(pkgPath);
33411
+ const pkgDir = dirname4(pkgPath);
32955
33412
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
32956
33413
  const binaryPath = join8(pkgDir, astGrepName);
32957
33414
  if (existsSync5(binaryPath) && isValidBinary(binaryPath)) {
@@ -33045,7 +33502,7 @@ async function runSg(options) {
33045
33502
  }
33046
33503
  }
33047
33504
  const timeout = DEFAULT_TIMEOUT_MS2;
33048
- const proc = spawn3([cliPath, ...args], {
33505
+ const proc = spawn4([cliPath, ...args], {
33049
33506
  stdout: "pipe",
33050
33507
  stderr: "pipe"
33051
33508
  });
@@ -33325,7 +33782,7 @@ var ast_grep_replace = tool({
33325
33782
  });
33326
33783
  // src/tools/background.ts
33327
33784
  var z2 = tool.schema;
33328
- function createBackgroundTools(_ctx, manager, _tmuxConfig, _pluginConfig) {
33785
+ function createBackgroundTools(_ctx, manager, _multiplexerConfig, _pluginConfig) {
33329
33786
  const agentNames = SUBAGENT_NAMES.join(", ");
33330
33787
  const background_task = tool({
33331
33788
  description: `Launch background agent task. Returns task_id immediately.
@@ -33495,16 +33952,16 @@ Returns the synthesized result with councillor summary.`,
33495
33952
  // src/tools/lsp/client.ts
33496
33953
  var import_node = __toESM(require_main(), 1);
33497
33954
  import { readFileSync as readFileSync4 } from "fs";
33498
- import { extname, resolve as resolve2 } from "path";
33955
+ import { extname, resolve as resolve3 } from "path";
33499
33956
  import { Readable, Writable } from "stream";
33500
33957
  import { pathToFileURL } from "url";
33501
- var {spawn: spawn4 } = globalThis.Bun;
33958
+ var {spawn: spawn5 } = globalThis.Bun;
33502
33959
 
33503
33960
  // src/tools/lsp/config.ts
33504
33961
  var import_which = __toESM(require_lib(), 1);
33505
33962
  import { existsSync as existsSync8 } from "fs";
33506
33963
  import { homedir as homedir4 } from "os";
33507
- import { join as join9 } from "path";
33964
+ import { dirname as dirname6, join as join9, resolve as resolve2 } from "path";
33508
33965
 
33509
33966
  // src/tools/lsp/config-store.ts
33510
33967
  var userConfig = new Map;
@@ -33535,7 +33992,7 @@ function hasUserLspConfig() {
33535
33992
 
33536
33993
  // src/tools/lsp/constants.ts
33537
33994
  import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
33538
- import { dirname as dirname4, resolve } from "path";
33995
+ import { dirname as dirname5, resolve } from "path";
33539
33996
  var SEVERITY_MAP = {
33540
33997
  1: "error",
33541
33998
  2: "warning",
@@ -33555,10 +34012,10 @@ function* walkUpDirectories(start, stop) {
33555
34012
  let dir = resolve(start);
33556
34013
  try {
33557
34014
  if (!statSync3(dir).isDirectory()) {
33558
- dir = dirname4(dir);
34015
+ dir = dirname5(dir);
33559
34016
  }
33560
34017
  } catch {
33561
- dir = dirname4(dir);
34018
+ dir = dirname5(dir);
33562
34019
  }
33563
34020
  let prevDir = "";
33564
34021
  while (dir !== prevDir && dir !== "/") {
@@ -33566,7 +34023,7 @@ function* walkUpDirectories(start, stop) {
33566
34023
  prevDir = dir;
33567
34024
  if (dir === stop)
33568
34025
  break;
33569
- dir = dirname4(dir);
34026
+ dir = dirname5(dir);
33570
34027
  }
33571
34028
  }
33572
34029
  function NearestRoot(includePatterns, excludePatterns) {
@@ -34107,39 +34564,79 @@ function buildMergedServers() {
34107
34564
  }
34108
34565
  return servers;
34109
34566
  }
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 {
34567
+ function getServerWorkspace(config3, filePath) {
34568
+ if (!filePath) {
34569
+ return;
34570
+ }
34571
+ if (!config3.root) {
34572
+ return dirname6(resolve2(filePath));
34573
+ }
34574
+ return config3.root(filePath);
34575
+ }
34576
+ function shouldSkipServer(config3, filePath) {
34577
+ if (!filePath) {
34578
+ return false;
34579
+ }
34580
+ return config3.id === "deno" && getServerWorkspace(config3, filePath) === undefined;
34581
+ }
34582
+ function toResolvedServer(config3, command) {
34583
+ return {
34584
+ id: config3.id,
34585
+ command: command ?? config3.command,
34586
+ extensions: config3.extensions,
34587
+ root: config3.root,
34588
+ env: config3.env,
34589
+ initialization: config3.initialization
34590
+ };
34591
+ }
34592
+ function findInstalledServer(configs, filePath) {
34593
+ let firstNotInstalled = null;
34594
+ for (const config3 of configs) {
34595
+ const workspace = getServerWorkspace(config3, filePath);
34596
+ const resolvedCommand = resolveServerCommand(config3.command, workspace ?? (filePath ? dirname6(resolve2(filePath)) : undefined));
34597
+ const server = toResolvedServer(config3, resolvedCommand ?? undefined);
34598
+ log(`[LSP] Considering server for ${config3.extensions.join(", ")}: ${config3.id} with command ${config3.command.join(" ")}`);
34599
+ if (resolvedCommand) {
34600
+ return { status: "found", server };
34601
+ }
34602
+ if (!firstNotInstalled) {
34603
+ log(`[LSP] Server ${config3.id} not found in PATH or local node_modules`);
34604
+ firstNotInstalled = {
34126
34605
  status: "not_installed",
34127
34606
  server,
34128
34607
  installHint: LSP_INSTALL_HINTS[config3.id] || `Install '${config3.command[0]}' and add to PATH`
34129
34608
  };
34130
34609
  }
34131
34610
  }
34611
+ return firstNotInstalled ?? undefined;
34612
+ }
34613
+ function findServerForExtension(ext, filePath) {
34614
+ const servers = [...buildMergedServers().values()].filter((config3) => config3.extensions.includes(ext));
34615
+ if (servers.length === 0) {
34616
+ log(`[LSP] No server config found for ${ext}`);
34617
+ return { status: "not_configured", extension: ext };
34618
+ }
34619
+ const candidateServers = servers.filter((config3) => !shouldSkipServer(config3, filePath));
34620
+ if (candidateServers.length === 0) {
34621
+ log(`[LSP] No applicable server config found for ${ext} at ${filePath}`);
34622
+ return { status: "not_configured", extension: ext };
34623
+ }
34624
+ const result = findInstalledServer(candidateServers, filePath);
34625
+ if (result) {
34626
+ return result;
34627
+ }
34628
+ log(`[LSP] No applicable server config found for ${ext}`);
34132
34629
  return { status: "not_configured", extension: ext };
34133
34630
  }
34134
34631
  function getLanguageId(ext) {
34135
34632
  return LANGUAGE_EXTENSIONS[ext] || "plaintext";
34136
34633
  }
34137
- function isServerInstalled(command) {
34634
+ function resolveServerCommand(command, cwd) {
34138
34635
  if (command.length === 0)
34139
- return false;
34140
- const cmd = command[0];
34636
+ return null;
34637
+ const [cmd, ...args] = command;
34141
34638
  if (cmd.includes("/") || cmd.includes("\\")) {
34142
- return existsSync8(cmd);
34639
+ return existsSync8(cmd) ? command : null;
34143
34640
  }
34144
34641
  const isWindows = process.platform === "win32";
34145
34642
  const ext = isWindows ? ".exe" : "";
@@ -34151,17 +34648,94 @@ function isServerInstalled(command) {
34151
34648
  nothrow: true
34152
34649
  });
34153
34650
  if (result !== null) {
34154
- return true;
34651
+ return [result, ...args];
34155
34652
  }
34156
- const cwd = process.cwd();
34157
- const localBin = join9(cwd, "node_modules", ".bin", cmd);
34158
- if (existsSync8(localBin) || existsSync8(localBin + ext)) {
34159
- return true;
34653
+ const localBinRoot = cwd ?? process.cwd();
34654
+ const localBin = join9(localBinRoot, "node_modules", ".bin", cmd);
34655
+ if (existsSync8(localBin)) {
34656
+ return [localBin, ...args];
34160
34657
  }
34161
- return false;
34658
+ if (existsSync8(localBin + ext)) {
34659
+ return [localBin + ext, ...args];
34660
+ }
34661
+ return null;
34162
34662
  }
34163
34663
 
34164
34664
  // src/tools/lsp/client.ts
34665
+ var START_TIMEOUT_MS = 5000;
34666
+ var REQUEST_TIMEOUT_MS = 5000;
34667
+ var OPEN_FILE_DELAY_MS = 250;
34668
+ var INITIALIZE_DELAY_MS = 100;
34669
+ var DIAGNOSTIC_SETTLE_DELAY_MS = 250;
34670
+ var LSP_TIMEOUTS = {
34671
+ start: START_TIMEOUT_MS,
34672
+ request: REQUEST_TIMEOUT_MS,
34673
+ openFileDelay: OPEN_FILE_DELAY_MS,
34674
+ initializeDelay: INITIALIZE_DELAY_MS,
34675
+ diagnosticSettleDelay: DIAGNOSTIC_SETTLE_DELAY_MS
34676
+ };
34677
+ function getDiagnosticsCapabilitySummary({
34678
+ diagnosticProvider,
34679
+ publishDiagnosticsObserved = false,
34680
+ workspaceConfigurationRequested = false
34681
+ }) {
34682
+ const pull = Boolean(diagnosticProvider);
34683
+ const workspaceDiagnostics = Boolean(diagnosticProvider?.workspaceDiagnostics);
34684
+ const interFileDependencies = Boolean(diagnosticProvider?.interFileDependencies);
34685
+ const availableModes = [
34686
+ ...pull ? ["pull", "pull/full", "pull/unchanged"] : ["push"],
34687
+ ...workspaceDiagnostics ? ["workspace-pull"] : [],
34688
+ ...publishDiagnosticsObserved ? ["push"] : []
34689
+ ];
34690
+ return {
34691
+ availableModes: Array.from(new Set(availableModes)),
34692
+ preferredMode: pull ? "pull" : "push",
34693
+ inferredTransport: pull && publishDiagnosticsObserved ? "hybrid" : pull ? "pull" : "push",
34694
+ pull,
34695
+ pushObserved: publishDiagnosticsObserved,
34696
+ pullResultTracking: pull,
34697
+ workspaceDiagnostics,
34698
+ interFileDependencies,
34699
+ workspaceConfiguration: workspaceConfigurationRequested
34700
+ };
34701
+ }
34702
+ function withTimeout(promise3, ms, label, onTimeout) {
34703
+ return new Promise((resolve4, reject) => {
34704
+ let settled = false;
34705
+ const timer = setTimeout(() => {
34706
+ if (settled) {
34707
+ return;
34708
+ }
34709
+ settled = true;
34710
+ Promise.resolve(onTimeout?.()).catch(() => {});
34711
+ reject(new Error(`${label} timeout after ${ms}ms`));
34712
+ }, ms);
34713
+ promise3.then((value) => {
34714
+ if (settled) {
34715
+ return;
34716
+ }
34717
+ settled = true;
34718
+ clearTimeout(timer);
34719
+ resolve4(value);
34720
+ }, (error92) => {
34721
+ if (settled) {
34722
+ return;
34723
+ }
34724
+ settled = true;
34725
+ clearTimeout(timer);
34726
+ reject(error92);
34727
+ });
34728
+ });
34729
+ }
34730
+ function getWorkspaceConfiguration(items) {
34731
+ return items.map((item) => {
34732
+ if (item?.section === "json") {
34733
+ return { validate: { enable: true } };
34734
+ }
34735
+ return null;
34736
+ });
34737
+ }
34738
+
34165
34739
  class LSPServerManager {
34166
34740
  static instance;
34167
34741
  clients = new Map;
@@ -34326,17 +34900,27 @@ class LSPClient {
34326
34900
  stderrBuffer = [];
34327
34901
  processExited = false;
34328
34902
  diagnosticsStore = new Map;
34903
+ diagnosticResultIds = new Map;
34904
+ documents = new Map;
34905
+ diagnosticProvider = null;
34906
+ publishDiagnosticsObserved = false;
34907
+ supportsPullDiagnostics = false;
34908
+ workspaceConfigurationRequested = false;
34329
34909
  constructor(root, server) {
34330
34910
  this.root = root;
34331
34911
  this.server = server;
34332
34912
  }
34333
34913
  async start() {
34914
+ const command = resolveServerCommand(this.server.command, this.root);
34915
+ if (!command) {
34916
+ throw new Error(`Failed to resolve LSP server command: ${this.server.command.join(" ")}`);
34917
+ }
34334
34918
  log("[lsp] LSPClient.start: spawning server", {
34335
34919
  server: this.server.id,
34336
- command: this.server.command.join(" "),
34920
+ command: command.join(" "),
34337
34921
  root: this.root
34338
34922
  });
34339
- this.proc = spawn4(this.server.command, {
34923
+ this.proc = spawn5(command, {
34340
34924
  stdin: "pipe",
34341
34925
  stdout: "pipe",
34342
34926
  stderr: "pipe",
@@ -34386,18 +34970,35 @@ class LSPClient {
34386
34970
  });
34387
34971
  this.connection = import_node.createMessageConnection(new import_node.StreamMessageReader(nodeReadable), new import_node.StreamMessageWriter(nodeWritable));
34388
34972
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
34973
+ if (!this.publishDiagnosticsObserved) {
34974
+ this.publishDiagnosticsObserved = true;
34975
+ log("[lsp] diagnostics capabilities: publishDiagnostics observed", {
34976
+ server: this.server.id,
34977
+ ...getDiagnosticsCapabilitySummary({
34978
+ diagnosticProvider: this.diagnosticProvider,
34979
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
34980
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
34981
+ })
34982
+ });
34983
+ }
34389
34984
  if (params.uri) {
34390
34985
  this.diagnosticsStore.set(params.uri, params.diagnostics ?? []);
34391
34986
  }
34392
34987
  });
34393
34988
  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
- });
34989
+ if (!this.workspaceConfigurationRequested) {
34990
+ this.workspaceConfigurationRequested = true;
34991
+ log("[lsp] diagnostics capabilities: workspace configuration requested", {
34992
+ server: this.server.id,
34993
+ sections: (params.items ?? []).map((item) => item && typeof item === "object" && ("section" in item) ? item.section ?? null : null),
34994
+ ...getDiagnosticsCapabilitySummary({
34995
+ diagnosticProvider: this.diagnosticProvider,
34996
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
34997
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
34998
+ })
34999
+ });
35000
+ }
35001
+ return getWorkspaceConfiguration(params.items ?? []);
34401
35002
  });
34402
35003
  this.connection.onRequest("client/registerCapability", () => null);
34403
35004
  this.connection.onRequest("window/workDoneProgress/create", () => null);
@@ -34405,7 +35006,7 @@ class LSPClient {
34405
35006
  this.processExited = true;
34406
35007
  });
34407
35008
  this.connection.listen();
34408
- await new Promise((resolve3) => setTimeout(resolve3, 100));
35009
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
34409
35010
  if (this.proc.exitCode !== null) {
34410
35011
  const stderr = this.stderrBuffer.join(`
34411
35012
  `);
@@ -34448,13 +35049,14 @@ stderr: ${stderr}` : ""));
34448
35049
  root: this.root
34449
35050
  });
34450
35051
  const rootUri = pathToFileURL(this.root).href;
34451
- await this.connection.sendRequest("initialize", {
35052
+ const result = await withTimeout(this.connection.sendRequest("initialize", {
34452
35053
  processId: process.pid,
34453
35054
  rootUri,
34454
35055
  rootPath: this.root,
34455
35056
  workspaceFolders: [{ uri: rootUri, name: "workspace" }],
34456
35057
  capabilities: {
34457
35058
  textDocument: {
35059
+ diagnostic: {},
34458
35060
  hover: { contentFormat: ["markdown", "plaintext"] },
34459
35061
  definition: { linkSupport: true },
34460
35062
  references: {},
@@ -34475,76 +35077,163 @@ stderr: ${stderr}` : ""));
34475
35077
  }
34476
35078
  },
34477
35079
  ...this.server.initialization
35080
+ }), LSP_TIMEOUTS.request, `LSP initialize (${this.server.id})`);
35081
+ const capabilities = result && typeof result === "object" && "capabilities" in result && result.capabilities && typeof result.capabilities === "object" ? result.capabilities : undefined;
35082
+ this.diagnosticProvider = capabilities && "diagnosticProvider" in capabilities ? capabilities.diagnosticProvider : null;
35083
+ this.supportsPullDiagnostics = Boolean(this.diagnosticProvider);
35084
+ log("[lsp] diagnostics capabilities negotiated", {
35085
+ server: this.server.id,
35086
+ diagnosticProvider: this.diagnosticProvider,
35087
+ ...getDiagnosticsCapabilitySummary({
35088
+ diagnosticProvider: this.diagnosticProvider,
35089
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
35090
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
35091
+ })
34478
35092
  });
34479
- this.connection.sendNotification("initialized");
34480
- await new Promise((r) => setTimeout(r, 300));
35093
+ this.connection.sendNotification("initialized", {});
35094
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.initializeDelay));
34481
35095
  log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
34482
35096
  }
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;
35097
+ async waitForPublishedDiagnostics(uri, timeoutMs = LSP_TIMEOUTS.request) {
35098
+ const cachedDiagnostics = this.diagnosticsStore.get(uri);
35099
+ if (cachedDiagnostics) {
35100
+ return cachedDiagnostics;
35101
+ }
35102
+ const startedAt = Date.now();
35103
+ while (Date.now() - startedAt < timeoutMs) {
35104
+ await new Promise((r) => setTimeout(r, 100));
35105
+ const diagnostics = this.diagnosticsStore.get(uri);
35106
+ if (diagnostics) {
35107
+ return diagnostics;
35108
+ }
34488
35109
  }
35110
+ return this.diagnosticsStore.get(uri);
35111
+ }
35112
+ async openFile(filePath) {
35113
+ await this.ensureDocumentSynced(filePath);
35114
+ }
35115
+ async ensureDocumentSynced(filePath) {
35116
+ const absPath = resolve3(filePath);
35117
+ const uri = pathToFileURL(absPath).href;
34489
35118
  const text = readFileSync4(absPath, "utf-8");
34490
35119
  const ext = extname(absPath);
34491
35120
  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,
35121
+ const existing = this.documents.get(uri);
35122
+ if (!existing) {
35123
+ log("[lsp] ensureDocumentSynced: didOpen", {
35124
+ filePath: absPath,
34500
35125
  languageId,
34501
- version: 1,
34502
- text
34503
- }
34504
- });
34505
- this.openedFiles.add(absPath);
34506
- await new Promise((r) => setTimeout(r, 1000));
35126
+ size: text.length
35127
+ });
35128
+ this.connection?.sendNotification("textDocument/didOpen", {
35129
+ textDocument: { uri, languageId, version: 1, text }
35130
+ });
35131
+ this.documents.set(uri, { version: 1, text, languageId });
35132
+ this.openedFiles.add(absPath);
35133
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.openFileDelay));
35134
+ return;
35135
+ }
35136
+ if (existing.text !== text) {
35137
+ const newVersion = existing.version + 1;
35138
+ log("[lsp] ensureDocumentSynced: didChange", {
35139
+ filePath: absPath,
35140
+ languageId,
35141
+ oldVersion: existing.version,
35142
+ newVersion,
35143
+ size: text.length
35144
+ });
35145
+ this.connection?.sendNotification("textDocument/didChange", {
35146
+ textDocument: { uri, version: newVersion },
35147
+ contentChanges: [{ text }]
35148
+ });
35149
+ this.documents.set(uri, { version: newVersion, text, languageId });
35150
+ this.diagnosticsStore.delete(uri);
35151
+ this.diagnosticResultIds.delete(uri);
35152
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.openFileDelay));
35153
+ } else {
35154
+ log("[lsp] ensureDocumentSynced: already synced", { filePath: absPath });
35155
+ }
34507
35156
  }
34508
35157
  async definition(filePath, line, character) {
34509
- const absPath = resolve2(filePath);
35158
+ const absPath = resolve3(filePath);
34510
35159
  await this.openFile(absPath);
34511
- return this.connection?.sendRequest("textDocument/definition", {
35160
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/definition", {
34512
35161
  textDocument: { uri: pathToFileURL(absPath).href },
34513
35162
  position: { line: line - 1, character }
34514
- });
35163
+ }), LSP_TIMEOUTS.request, `LSP definition (${this.server.id})`) : undefined;
34515
35164
  }
34516
35165
  async references(filePath, line, character, includeDeclaration = true) {
34517
- const absPath = resolve2(filePath);
35166
+ const absPath = resolve3(filePath);
34518
35167
  await this.openFile(absPath);
34519
- return this.connection?.sendRequest("textDocument/references", {
35168
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/references", {
34520
35169
  textDocument: { uri: pathToFileURL(absPath).href },
34521
35170
  position: { line: line - 1, character },
34522
35171
  context: { includeDeclaration }
34523
- });
35172
+ }), LSP_TIMEOUTS.request, `LSP references (${this.server.id})`) : undefined;
34524
35173
  }
34525
35174
  async diagnostics(filePath) {
34526
- const absPath = resolve2(filePath);
35175
+ const absPath = resolve3(filePath);
34527
35176
  const uri = pathToFileURL(absPath).href;
34528
35177
  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;
35178
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.diagnosticSettleDelay));
35179
+ log("[lsp] diagnostics mode selected", {
35180
+ server: this.server.id,
35181
+ filePath: absPath,
35182
+ activeMode: this.supportsPullDiagnostics ? "pull" : "push",
35183
+ ...getDiagnosticsCapabilitySummary({
35184
+ diagnosticProvider: this.diagnosticProvider,
35185
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
35186
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
35187
+ })
35188
+ });
35189
+ if (this.supportsPullDiagnostics) {
35190
+ try {
35191
+ const result = this.connection ? await withTimeout(this.connection.sendRequest("textDocument/diagnostic", {
35192
+ textDocument: { uri },
35193
+ previousResultId: this.diagnosticResultIds.get(uri)
35194
+ }), LSP_TIMEOUTS.request, `LSP diagnostics (${this.server.id})`) : undefined;
35195
+ const report = result;
35196
+ if (report?.kind === "full") {
35197
+ if (report.resultId) {
35198
+ this.diagnosticResultIds.set(uri, report.resultId);
35199
+ } else {
35200
+ this.diagnosticResultIds.delete(uri);
35201
+ }
35202
+ this.diagnosticsStore.set(uri, report.items);
35203
+ return { items: report.items };
35204
+ }
35205
+ if (report?.kind === "unchanged") {
35206
+ if (report.resultId) {
35207
+ this.diagnosticResultIds.set(uri, report.resultId);
35208
+ }
35209
+ return { items: this.diagnosticsStore.get(uri) ?? [] };
35210
+ }
35211
+ if (result && typeof result === "object" && "items" in result) {
35212
+ const legacyResult = result;
35213
+ this.diagnosticsStore.set(uri, legacyResult.items);
35214
+ return legacyResult;
35215
+ }
35216
+ } catch (error92) {
35217
+ log("[lsp] diagnostics: falling back to cached publishDiagnostics", {
35218
+ server: this.server.id,
35219
+ error: String(error92)
35220
+ });
34536
35221
  }
34537
- } catch {}
34538
- return { items: this.diagnosticsStore.get(uri) ?? [] };
35222
+ }
35223
+ const cachedDiagnostics = await this.waitForPublishedDiagnostics(uri);
35224
+ if (cachedDiagnostics) {
35225
+ return { items: cachedDiagnostics };
35226
+ }
35227
+ throw new Error(`Unable to retrieve diagnostics from ${this.server.id}: request timed out or is unsupported.`);
34539
35228
  }
34540
35229
  async rename(filePath, line, character, newName) {
34541
- const absPath = resolve2(filePath);
35230
+ const absPath = resolve3(filePath);
34542
35231
  await this.openFile(absPath);
34543
- return this.connection?.sendRequest("textDocument/rename", {
35232
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/rename", {
34544
35233
  textDocument: { uri: pathToFileURL(absPath).href },
34545
35234
  position: { line: line - 1, character },
34546
35235
  newName
34547
- });
35236
+ }), LSP_TIMEOUTS.request, `LSP rename (${this.server.id})`) : undefined;
34548
35237
  }
34549
35238
  isAlive() {
34550
35239
  return this.proc !== null && !this.processExited && this.proc.exitCode === null;
@@ -34553,7 +35242,7 @@ stderr: ${stderr}` : ""));
34553
35242
  log("[lsp] LSPClient.stop: stopping", { server: this.server.id });
34554
35243
  try {
34555
35244
  if (this.connection) {
34556
- await this.connection.sendRequest("shutdown");
35245
+ await withTimeout(this.connection.sendRequest("shutdown"), 1000, `LSP shutdown (${this.server.id})`);
34557
35246
  this.connection.sendNotification("exit");
34558
35247
  this.connection.dispose();
34559
35248
  }
@@ -34562,7 +35251,13 @@ stderr: ${stderr}` : ""));
34562
35251
  this.proc = null;
34563
35252
  this.connection = null;
34564
35253
  this.processExited = true;
35254
+ this.diagnosticProvider = null;
35255
+ this.publishDiagnosticsObserved = false;
35256
+ this.supportsPullDiagnostics = false;
35257
+ this.workspaceConfigurationRequested = false;
34565
35258
  this.diagnosticsStore.clear();
35259
+ this.diagnosticResultIds.clear();
35260
+ this.documents.clear();
34566
35261
  log("[lsp] LSPClient.stop: complete", { server: this.server.id });
34567
35262
  }
34568
35263
  }
@@ -34574,13 +35269,13 @@ import {
34574
35269
  unlinkSync as unlinkSync2,
34575
35270
  writeFileSync as writeFileSync3
34576
35271
  } from "fs";
34577
- import { dirname as dirname5, extname as extname2, join as join10, resolve as resolve3 } from "path";
35272
+ import { dirname as dirname7, extname as extname2, join as join10, resolve as resolve4 } from "path";
34578
35273
  import { fileURLToPath as fileURLToPath2 } from "url";
34579
35274
  function findServerProjectRoot(filePath, server) {
34580
35275
  if (server.root) {
34581
- return server.root(filePath) ?? dirname5(resolve3(filePath));
35276
+ return server.root(filePath) ?? dirname7(resolve4(filePath));
34582
35277
  }
34583
- return dirname5(resolve3(filePath));
35278
+ return dirname7(resolve4(filePath));
34584
35279
  }
34585
35280
  function uriToPath(uri) {
34586
35281
  return fileURLToPath2(uri);
@@ -34599,9 +35294,9 @@ function formatServerLookupError(result) {
34599
35294
  return `No LSP server configured for extension: ${result.extension}`;
34600
35295
  }
34601
35296
  async function withLspClient(filePath, fn) {
34602
- const absPath = resolve3(filePath);
35297
+ const absPath = resolve4(filePath);
34603
35298
  const ext = extname2(absPath);
34604
- const result = findServerForExtension(ext);
35299
+ const result = findServerForExtension(ext, absPath);
34605
35300
  if (result.status !== "found") {
34606
35301
  log("[lsp] withLspClient: server not found", {
34607
35302
  filePath: absPath,
@@ -34610,7 +35305,14 @@ async function withLspClient(filePath, fn) {
34610
35305
  throw new Error(formatServerLookupError(result));
34611
35306
  }
34612
35307
  const server = result.server;
34613
- const root = findServerProjectRoot(absPath, server) ?? dirname5(absPath);
35308
+ const root = findServerProjectRoot(absPath, server) ?? dirname7(absPath);
35309
+ log("[lsp] withLspClient: selected server", {
35310
+ filePath: absPath,
35311
+ extension: ext,
35312
+ server: server.id,
35313
+ command: server.command.join(" "),
35314
+ root
35315
+ });
34614
35316
  log("[lsp] withLspClient: acquiring client", {
34615
35317
  filePath: absPath,
34616
35318
  server: server.id,
@@ -34970,29 +35672,32 @@ var OhMyOpenCodeLite = async (ctx) => {
34970
35672
  runtimeChains[agentName] = existing;
34971
35673
  }
34972
35674
  }
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
35675
+ const multiplexerConfig = {
35676
+ type: config3.multiplexer?.type ?? "none",
35677
+ layout: config3.multiplexer?.layout ?? "main-vertical",
35678
+ main_pane_size: config3.multiplexer?.main_pane_size ?? 60
34977
35679
  };
34978
- log("[plugin] initialized with tmux config", {
34979
- tmuxConfig,
34980
- rawTmuxConfig: config3.tmux,
35680
+ const multiplexer = getMultiplexer(multiplexerConfig);
35681
+ const multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
35682
+ log("[plugin] initialized with multiplexer config", {
35683
+ multiplexerConfig,
35684
+ enabled: multiplexerEnabled,
34981
35685
  directory: ctx.directory
34982
35686
  });
34983
- if (tmuxConfig.enabled) {
34984
- startTmuxCheck();
35687
+ if (multiplexerEnabled) {
35688
+ startAvailabilityCheck(multiplexerConfig);
34985
35689
  }
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);
35690
+ const backgroundManager = new BackgroundTaskManager(ctx, multiplexerConfig, config3);
35691
+ const backgroundTools = createBackgroundTools(ctx, backgroundManager, multiplexerConfig, config3);
35692
+ const councilTools = config3.council ? createCouncilTool(ctx, new CouncilManager(ctx, config3, backgroundManager.getDepthTracker(), multiplexerEnabled)) : {};
35693
+ const mcps = createBuiltinMcps(config3.disabled_mcps, config3.websearch);
35694
+ const multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
34991
35695
  const autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
34992
35696
  showStartupToast: true,
34993
35697
  autoUpdate: true
34994
35698
  });
34995
35699
  const phaseReminderHook = createPhaseReminderHook();
35700
+ const filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config3);
34996
35701
  const postFileToolNudgeHook = createPostFileToolNudgeHook();
34997
35702
  const chatHeadersHook = createChatHeadersHook(ctx);
34998
35703
  const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
@@ -35083,7 +35788,8 @@ var OhMyOpenCodeLite = async (ctx) => {
35083
35788
  } else {
35084
35789
  Object.assign(configMcp, mcps);
35085
35790
  }
35086
- const allMcpNames = Object.keys(mcps);
35791
+ const mergedMcpConfig = opencodeConfig.mcp;
35792
+ const allMcpNames = Object.keys(mergedMcpConfig ?? mcps);
35087
35793
  for (const [agentName, agentConfig] of Object.entries(agents)) {
35088
35794
  const agentMcps = agentConfig?.mcps;
35089
35795
  if (!agentMcps)
@@ -35108,14 +35814,18 @@ var OhMyOpenCodeLite = async (ctx) => {
35108
35814
  event: async (input) => {
35109
35815
  await foregroundFallback.handleEvent(input.event);
35110
35816
  await autoUpdateChecker.event(input);
35111
- await tmuxSessionManager.onSessionCreated(input.event);
35817
+ await multiplexerSessionManager.onSessionCreated(input.event);
35112
35818
  await backgroundManager.handleSessionStatus(input.event);
35113
- await tmuxSessionManager.onSessionStatus(input.event);
35819
+ await multiplexerSessionManager.onSessionStatus(input.event);
35114
35820
  await backgroundManager.handleSessionDeleted(input.event);
35115
- await tmuxSessionManager.onSessionDeleted(input.event);
35821
+ await multiplexerSessionManager.onSessionDeleted(input.event);
35116
35822
  },
35117
35823
  "chat.headers": chatHeadersHook["chat.headers"],
35118
- "experimental.chat.messages.transform": phaseReminderHook["experimental.chat.messages.transform"],
35824
+ "experimental.chat.messages.transform": async (input, output) => {
35825
+ const typedOutput = output;
35826
+ await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
35827
+ await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
35828
+ },
35119
35829
  "tool.execute.after": async (input, output) => {
35120
35830
  await delegateTaskRetryHook["tool.execute.after"](input, output);
35121
35831
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);