oh-my-opencode-slim 0.8.6 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3529,7 +3529,7 @@ var RECOMMENDED_SKILLS = [
3529
3529
  name: "simplify",
3530
3530
  repo: "https://github.com/brianlovin/claude-config",
3531
3531
  skillName: "simplify",
3532
- allowedAgents: ["orchestrator"],
3532
+ allowedAgents: ["oracle"],
3533
3533
  description: "YAGNI code simplification expert"
3534
3534
  },
3535
3535
  {
@@ -17217,6 +17217,7 @@ var CouncilMasterConfigSchema = exports_external.object({
17217
17217
  variant: exports_external.string().optional(),
17218
17218
  prompt: exports_external.string().optional().describe("Optional role/guidance injected into the master synthesis prompt")
17219
17219
  });
17220
+ var CouncillorExecutionModeSchema = exports_external.enum(["parallel", "serial"]).default("parallel").describe('Execution mode for councillors. Use "serial" for single-model systems to avoid conflicts. ' + 'Use "parallel" for multi-model systems for faster execution.');
17220
17221
  var CouncilConfigSchema = exports_external.object({
17221
17222
  master: CouncilMasterConfigSchema,
17222
17223
  presets: exports_external.record(exports_external.string(), CouncilPresetSchema),
@@ -17231,7 +17232,9 @@ var CouncilConfigSchema = exports_external.object({
17231
17232
  return unique;
17232
17233
  }
17233
17234
  return val;
17234
- }).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"]')
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).'),
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.")
17235
17238
  });
17236
17239
  // src/config/loader.ts
17237
17240
  import * as fs from "fs";
@@ -17261,7 +17264,7 @@ function parseList(items, allAvailable) {
17261
17264
  if (allow.includes("*")) {
17262
17265
  return allAvailable.filter((item) => !deny.includes(item));
17263
17266
  }
17264
- return allow.filter((item) => !deny.includes(item));
17267
+ return allow.filter((item) => !deny.includes(item) && allAvailable.includes(item));
17265
17268
  }
17266
17269
  function getAgentMcpList(agentName, config2) {
17267
17270
  const agentConfig = getAgentOverride(config2, agentName);
@@ -17333,19 +17336,29 @@ var AgentOverrideConfigSchema = exports_external.object({
17333
17336
  skills: exports_external.array(exports_external.string()).optional(),
17334
17337
  mcps: exports_external.array(exports_external.string()).optional()
17335
17338
  });
17336
- var TmuxLayoutSchema = exports_external.enum([
17339
+ var MultiplexerTypeSchema = exports_external.enum(["auto", "tmux", "zellij", "none"]);
17340
+ var MultiplexerLayoutSchema = exports_external.enum([
17337
17341
  "main-horizontal",
17338
17342
  "main-vertical",
17339
17343
  "tiled",
17340
17344
  "even-horizontal",
17341
17345
  "even-vertical"
17342
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
+ });
17343
17353
  var TmuxConfigSchema = exports_external.object({
17344
17354
  enabled: exports_external.boolean().default(false),
17345
17355
  layout: TmuxLayoutSchema.default("main-vertical"),
17346
17356
  main_pane_size: exports_external.number().min(20).max(80).default(60)
17347
17357
  });
17348
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
+ });
17349
17362
  var McpNameSchema = exports_external.enum(["websearch", "context7", "grep_app"]);
17350
17363
  var BackgroundTaskConfigSchema = exports_external.object({
17351
17364
  maxConcurrentStarts: exports_external.number().min(1).max(50).default(10)
@@ -17354,7 +17367,8 @@ var FailoverConfigSchema = exports_external.object({
17354
17367
  enabled: exports_external.boolean().default(true),
17355
17368
  timeoutMs: exports_external.number().min(0).default(15000),
17356
17369
  retryDelayMs: exports_external.number().min(0).default(500),
17357
- 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.")
17358
17372
  });
17359
17373
  var PluginConfigSchema = exports_external.object({
17360
17374
  preset: exports_external.string().optional(),
@@ -17365,7 +17379,9 @@ var PluginConfigSchema = exports_external.object({
17365
17379
  presets: exports_external.record(exports_external.string(), PresetSchema).optional(),
17366
17380
  agents: exports_external.record(exports_external.string(), AgentOverrideConfigSchema).optional(),
17367
17381
  disabled_mcps: exports_external.array(exports_external.string()).optional(),
17382
+ multiplexer: MultiplexerConfigSchema.optional(),
17368
17383
  tmux: TmuxConfigSchema.optional(),
17384
+ websearch: WebsearchConfigSchema.optional(),
17369
17385
  background: BackgroundTaskConfigSchema.optional(),
17370
17386
  fallback: FailoverConfigSchema.optional(),
17371
17387
  council: CouncilConfigSchema.optional()
@@ -17440,9 +17456,12 @@ function loadPluginConfig(directory) {
17440
17456
  ...projectConfig,
17441
17457
  agents: deepMerge(config2.agents, projectConfig.agents),
17442
17458
  tmux: deepMerge(config2.tmux, projectConfig.tmux),
17443
- 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)
17444
17462
  };
17445
17463
  }
17464
+ config2 = migrateTmuxToMultiplexer(config2);
17446
17465
  const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
17447
17466
  if (envPreset) {
17448
17467
  config2.preset = envPreset;
@@ -17484,6 +17503,22 @@ function loadAgentPrompt(agentName, preset) {
17484
17503
  result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
17485
17504
  return result;
17486
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
+ }
17487
17522
  // src/config/utils.ts
17488
17523
  function getAgentOverride(config2, name) {
17489
17524
  const overrides = config2?.agents ?? {};
@@ -17542,9 +17577,10 @@ async function extractSessionResult(client, sessionId, options) {
17542
17577
  }
17543
17578
  }
17544
17579
  }
17545
- return extractedContent.filter((t) => t.length > 0).join(`
17580
+ const text = extractedContent.filter((t) => t.length > 0).join(`
17546
17581
 
17547
17582
  `);
17583
+ return { text, empty: text.length === 0 };
17548
17584
  }
17549
17585
 
17550
17586
  // src/agents/orchestrator.ts
@@ -17581,16 +17617,16 @@ You are an AI coding orchestrator that optimizes for quality, speed, cost, and r
17581
17617
  @oracle
17582
17618
  - Role: Strategic advisor for high-stakes decisions and persistent problems, code reviewer
17583
17619
  - Stats: 5x better decision maker, problem solver, investigator than orchestrator, 0.8x speed of orchestrator, same cost.
17584
- - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review
17585
- - **Delegate when:** Major architectural decisions with long-term impact \u2022 Problems persisting after 2+ fix attempts \u2022 High-risk multi-system refactors \u2022 Costly trade-offs (performance vs maintainability) \u2022 Complex debugging with unclear root cause \u2022 Security/scalability/data integrity decisions \u2022 Genuinely uncertain and cost of wrong choice is high \u2022 When a workflow calls for a **reviewer** subagent
17620
+ - Capabilities: Deep architectural reasoning, system-level trade-offs, complex debugging, code review, simplification, maintainability review
17621
+ - **Delegate when:** Major architectural decisions with long-term impact \u2022 Problems persisting after 2+ fix attempts \u2022 High-risk multi-system refactors \u2022 Costly trade-offs (performance vs maintainability) \u2022 Complex debugging with unclear root cause \u2022 Security/scalability/data integrity decisions \u2022 Genuinely uncertain and cost of wrong choice is high \u2022 When a workflow calls for a **reviewer** subagent \u2022 Code needs simplification or YAGNI scrutiny
17586
17622
  - **Don't delegate when:** Routine decisions you're confident about \u2022 First bug fix attempt \u2022 Straightforward trade-offs \u2022 Tactical "how" vs strategic "should" \u2022 Time-sensitive good-enough decisions \u2022 Quick research/testing can answer
17587
- - **Rule of thumb:** Need senior architect review? \u2192 @oracle. Need code review? \u2192 @oracle. Just do it and PR? \u2192 yourself.
17623
+ - **Rule of thumb:** Need senior architect review? \u2192 @oracle. Need code review or simplification? \u2192 @oracle. Just do it and PR? \u2192 yourself.
17588
17624
 
17589
17625
  @designer
17590
17626
  - Role: UI/UX specialist for intentional, polished experiences
17591
17627
  - Stats: 10x better UI/UX than orchestrator
17592
- - Capabilities: Visual direction, interactions, responsive layouts, design systems with aesthetic intent
17593
- - **Delegate when:** User-facing interfaces needing polish \u2022 Responsive layouts \u2022 UX-critical components (forms, nav, dashboards) \u2022 Visual consistency systems \u2022 Animations/micro-interactions \u2022 Landing/marketing pages \u2022 Refining functional\u2192delightful
17628
+ - Capabilities: Visual direction, interactions, responsive layouts, design systems with aesthetic intent, UI/UX review
17629
+ - **Delegate when:** User-facing interfaces needing polish \u2022 Responsive layouts \u2022 UX-critical components (forms, nav, dashboards) \u2022 Visual consistency systems \u2022 Animations/micro-interactions \u2022 Landing/marketing pages \u2022 Refining functional\u2192delightful \u2022 Reviewing existing UI/UX quality
17594
17630
  - **Don't delegate when:** Backend/logic with no visual \u2022 Quick prototypes where design doesn't matter yet
17595
17631
  - **Rule of thumb:** Users see it and polish matters? \u2192 @designer. Headless/functional? \u2192 yourself.
17596
17632
 
@@ -17598,9 +17634,9 @@ You are an AI coding orchestrator that optimizes for quality, speed, cost, and r
17598
17634
  - Role: Fast execution specialist for well-defined tasks, which empowers orchestrator with parallel, speedy executions
17599
17635
  - Stats: 2x faster code edits, 1/2 cost of orchestrator, 0.8x quality of orchestrator
17600
17636
  - Tools/Constraints: Execution-focused\u2014no research, no architectural decisions
17601
- - **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer
17637
+ - **Delegate when:** For implementation work, think and triage first. If the change is non-trivial or multi-file, hand bounded execution to @fixer \u2022 Writing or updating tests \u2022 Tasks that touch test files, fixtures, mocks, or test helpers
17602
17638
  - **Don't delegate when:** Needs discovery/research/decisions \u2022 Single small change (<20 lines, one file) \u2022 Unclear requirements needing iteration \u2022 Explaining to fixer > doing \u2022 Tight integration with your current work \u2022 Sequential dependencies
17603
- - **Rule of thumb:** Explaining > doing? \u2192 yourself. Orchestrator paths selection is vastly improved by Fixer. eg it can reduce overall speed if Orchestrator splits what's usually a single task into multiple subtasks and parallelize it with fixer.
17639
+ - **Rule of thumb:** Explaining > doing? \u2192 yourself. Test file modifications and bounded implementation work usually go to @fixer. Orchestrator paths selection is vastly improved by Fixer. eg it can reduce overall speed if Orchestrator splits what's usually a single task into multiple subtasks and parallelize it with fixer.
17604
17640
 
17605
17641
  @council
17606
17642
  - Role: Multi-LLM consensus engine for high-confidence answers
@@ -17648,9 +17684,17 @@ Balance: respect dependencies, avoid parallelizing what must be sequential.
17648
17684
  4. Integrate results
17649
17685
  5. Adjust if needed
17650
17686
 
17687
+ ### Validation routing
17688
+ - Validation is a workflow stage owned by the Orchestrator, not a separate specialist
17689
+ - Route UI/UX validation and review to @designer
17690
+ - Route code review, simplification, maintainability review, and YAGNI checks to @oracle
17691
+ - Route test writing, test updates, and changes touching test files to @fixer
17692
+ - If a request spans multiple lanes, delegate only the lanes that add clear value
17693
+
17651
17694
  ## 6. Verify
17652
17695
  - Run \`lsp_diagnostics\` for errors
17653
- - Suggest \`simplify\` skill when applicable
17696
+ - Use validation routing when applicable instead of doing all review work yourself
17697
+ - If test files are involved, prefer @fixer for bounded test changes and @oracle only for test strategy or quality review
17654
17698
  - Confirm specialists completed successfully
17655
17699
  - Verify solution meets requirements
17656
17700
 
@@ -17901,9 +17945,9 @@ function createCouncillorAgent(model, customPrompt, customAppendPrompt) {
17901
17945
  }
17902
17946
 
17903
17947
  // src/agents/designer.ts
17904
- var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who creates intentional, polished experiences.
17948
+ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who creates and reviews intentional, polished experiences.
17905
17949
 
17906
- **Role**: Craft cohesive UI/UX that balances visual impact with usability.
17950
+ **Role**: Craft and review cohesive UI/UX that balances visual impact with usability.
17907
17951
 
17908
17952
  ## Design Principles
17909
17953
 
@@ -17949,6 +17993,11 @@ var DESIGNER_PROMPT = `You are a Designer - a frontend UI/UX specialist who crea
17949
17993
  - Leverage component libraries where available
17950
17994
  - Prioritize visual excellence\u2014code perfection comes second
17951
17995
 
17996
+ ## Review Responsibilities
17997
+ - Review existing UI for usability, responsiveness, visual consistency, and polish when asked
17998
+ - Call out concrete UX issues and improvements, not just abstract design advice
17999
+ - When validating, focus on what users actually see and feel
18000
+
17952
18001
  ## Output Quality
17953
18002
  You're capable of extraordinary creative work. Commit fully to distinctive visions and show what's possible when breaking conventions thoughtfully.`;
17954
18003
  function createDesignerAgent(model, customPrompt, customAppendPrompt) {
@@ -17962,7 +18011,7 @@ ${customAppendPrompt}`;
17962
18011
  }
17963
18012
  return {
17964
18013
  name: "designer",
17965
- description: "UI/UX design and implementation. Use for styling, responsive design, component architecture and visual polish.",
18014
+ description: "UI/UX design, review, and implementation. Use for styling, responsive design, component architecture and visual polish.",
17966
18015
  config: {
17967
18016
  model,
17968
18017
  temperature: 0.7,
@@ -18030,6 +18079,7 @@ var FIXER_PROMPT = `You are Fixer - a fast, focused implementation specialist.
18030
18079
  - Use the research context (file paths, documentation, patterns) provided
18031
18080
  - Read files before using edit/write tools and gather exact content before making changes
18032
18081
  - Be fast and direct - no research, no delegation, No multi-step research/planning; minimal execution sequence ok
18082
+ - Write or update tests when requested, especially for bounded tasks involving test files, fixtures, mocks, or test helpers
18033
18083
  - Run tests/lsp_diagnostics when relevant or requested (otherwise note as skipped with reason)
18034
18084
  - Report completion with summary of changes
18035
18085
 
@@ -18039,6 +18089,7 @@ var FIXER_PROMPT = `You are Fixer - a fast, focused implementation specialist.
18039
18089
  - No multi-step research/planning; minimal execution sequence ok
18040
18090
  - If context is insufficient: use grep/glob/lsp_diagnostics directly \u2014 do not delegate
18041
18091
  - Only ask for missing inputs you truly cannot retrieve yourself
18092
+ - Do not act as the primary reviewer; implement requested changes and surface obvious issues briefly
18042
18093
 
18043
18094
  **Output Format**:
18044
18095
  <summary>
@@ -18123,14 +18174,15 @@ ${customAppendPrompt}`;
18123
18174
  }
18124
18175
 
18125
18176
  // src/agents/oracle.ts
18126
- var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor.
18177
+ var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor and code reviewer.
18127
18178
 
18128
- **Role**: High-IQ debugging, architecture decisions, code review, and engineering guidance.
18179
+ **Role**: High-IQ debugging, architecture decisions, code review, simplification, and engineering guidance.
18129
18180
 
18130
18181
  **Capabilities**:
18131
18182
  - Analyze complex codebases and identify root causes
18132
18183
  - Propose architectural solutions with tradeoffs
18133
- - Review code for correctness, performance, and maintainability
18184
+ - Review code for correctness, performance, maintainability, and unnecessary complexity
18185
+ - Enforce YAGNI and suggest simpler designs when abstractions are not pulling their weight
18134
18186
  - Guide debugging when standard approaches fail
18135
18187
 
18136
18188
  **Behavior**:
@@ -18138,6 +18190,7 @@ var ORACLE_PROMPT = `You are Oracle - a strategic technical advisor.
18138
18190
  - Provide actionable recommendations
18139
18191
  - Explain reasoning briefly
18140
18192
  - Acknowledge uncertainty when present
18193
+ - Prefer simpler designs unless complexity clearly earns its keep
18141
18194
 
18142
18195
  **Constraints**:
18143
18196
  - READ-ONLY: You advise, you don't implement
@@ -18154,7 +18207,7 @@ ${customAppendPrompt}`;
18154
18207
  }
18155
18208
  return {
18156
18209
  name: "oracle",
18157
- description: "Strategic technical advisor. Use for architecture decisions, complex debugging, code review, and engineering guidance.",
18210
+ description: "Strategic technical advisor. Use for architecture decisions, complex debugging, code review, simplification, and engineering guidance.",
18158
18211
  config: {
18159
18212
  model,
18160
18213
  temperature: 0.1,
@@ -18217,6 +18270,9 @@ function createAgents(config2) {
18217
18270
  }
18218
18271
  return librarianModel ?? DEFAULT_MODELS.librarian;
18219
18272
  }
18273
+ if ((name === "council" || name === "council-master") && config2?.council?.master?.model) {
18274
+ return config2.council.master.model;
18275
+ }
18220
18276
  return DEFAULT_MODELS[name];
18221
18277
  };
18222
18278
  const protoSubAgents = Object.entries(SUBAGENT_FACTORIES).map(([name, factory]) => {
@@ -18267,7 +18323,10 @@ function getAgentConfigs(config2) {
18267
18323
  import * as fs2 from "fs";
18268
18324
  import * as os from "os";
18269
18325
  import * as path2 from "path";
18270
- 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 {}
18271
18330
  function log(message, data) {
18272
18331
  try {
18273
18332
  const timestamp = new Date().toISOString();
@@ -18277,293 +18336,608 @@ function log(message, data) {
18277
18336
  } catch {}
18278
18337
  }
18279
18338
 
18280
- // src/utils/agent-variant.ts
18281
- function normalizeAgentName(agentName) {
18282
- const trimmed = agentName.trim();
18283
- return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
18284
- }
18285
- function resolveAgentVariant(config2, agentName) {
18286
- const normalized = normalizeAgentName(agentName);
18287
- const rawVariant = config2?.agents?.[normalized]?.variant;
18288
- if (typeof rawVariant !== "string") {
18289
- return;
18339
+ // src/multiplexer/tmux/index.ts
18340
+ var {spawn } = globalThis.Bun;
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
+ }
18368
+ try {
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 };
18404
+ }
18290
18405
  }
18291
- const trimmed = rawVariant.trim();
18292
- if (trimmed.length === 0) {
18293
- return;
18406
+ async closePane(paneId) {
18407
+ if (!paneId) {
18408
+ log("[tmux] closePane: no paneId provided");
18409
+ return false;
18410
+ }
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;
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;
18441
+ }
18294
18442
  }
18295
- log(`[variant] resolved variant="${trimmed}" for agent "${normalized}"`);
18296
- return trimmed;
18297
- }
18298
- function applyAgentVariant(variant, body) {
18299
- if (!variant) {
18300
- return body;
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) });
18471
+ }
18301
18472
  }
18302
- if (body.variant) {
18303
- return body;
18473
+ async getBinary() {
18474
+ await this.isAvailable();
18475
+ return this.binaryPath;
18476
+ }
18477
+ async findBinary() {
18478
+ const isWindows = process.platform === "win32";
18479
+ const cmd = isWindows ? "where" : "which";
18480
+ try {
18481
+ const proc = spawn([cmd, "tmux"], {
18482
+ stdout: "pipe",
18483
+ stderr: "pipe"
18484
+ });
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"], {
18498
+ stdout: "pipe",
18499
+ stderr: "pipe"
18500
+ });
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;
18511
+ }
18304
18512
  }
18305
- return { ...body, variant };
18306
- }
18307
- // src/utils/internal-initiator.ts
18308
- var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
18309
- function isRecord(value) {
18310
- return typeof value === "object" && value !== null;
18311
- }
18312
- function createInternalAgentTextPart(text) {
18313
- return {
18314
- type: "text",
18315
- text: `${text}
18316
- ${SLIM_INTERNAL_INITIATOR_MARKER}`
18317
- };
18318
18513
  }
18319
- function hasInternalInitiatorMarker(part) {
18320
- if (!isRecord(part) || part.type !== "text") {
18321
- return 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;
18322
18530
  }
18323
- if (typeof part.text !== "string") {
18324
- return 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;
18325
18538
  }
18326
- return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
18327
- }
18328
- // src/utils/tmux.ts
18329
- var {spawn } = globalThis.Bun;
18330
- var tmuxPath = null;
18331
- var tmuxChecked = false;
18332
- var storedConfig = null;
18333
- var serverAvailable = null;
18334
- var serverCheckUrl = null;
18335
- async function isServerRunning(serverUrl) {
18336
- if (serverCheckUrl === serverUrl && serverAvailable === true) {
18337
- return true;
18539
+ isInsideSession() {
18540
+ return !!process.env.ZELLIJ;
18338
18541
  }
18339
- const healthUrl = new URL("/health", serverUrl).toString();
18340
- const timeoutMs = 3000;
18341
- const maxAttempts = 2;
18342
- for (let attempt = 1;attempt <= maxAttempts; attempt++) {
18343
- const controller = new AbortController;
18344
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
18345
- let response = null;
18542
+ async spawnPane(sessionId, description, serverUrl) {
18543
+ const zellij = await this.getBinary();
18544
+ if (!zellij)
18545
+ return { success: false };
18346
18546
  try {
18347
- response = await fetch(healthUrl, { signal: controller.signal }).catch(() => null);
18348
- } finally {
18349
- clearTimeout(timeout);
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 };
18350
18564
  }
18351
- const available = response?.ok ?? false;
18352
- if (available) {
18353
- serverCheckUrl = serverUrl;
18354
- serverAvailable = true;
18355
- log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
18356
- return true;
18565
+ }
18566
+ async createPaneInAgentTab(zellij, sessionId, serverUrl, description) {
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 };
18357
18594
  }
18358
- if (attempt < maxAttempts) {
18359
- await new Promise((r) => setTimeout(r, 250));
18595
+ if (!this.agentTabId) {
18596
+ return { success: false };
18360
18597
  }
18361
- }
18362
- log("[tmux] isServerRunning: checked", { serverUrl, available: false });
18363
- return false;
18364
- }
18365
- async function findTmuxPath() {
18366
- const isWindows = process.platform === "win32";
18367
- const cmd = isWindows ? "where" : "which";
18368
- try {
18369
- const proc = spawn([cmd, "tmux"], {
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;
18603
+ const args = [
18604
+ "action",
18605
+ "new-pane",
18606
+ "--name",
18607
+ paneName,
18608
+ "--close-on-exit",
18609
+ "--",
18610
+ "sh",
18611
+ "-c",
18612
+ opencodeCmd
18613
+ ];
18614
+ const proc = spawn2([zellij, ...args], {
18370
18615
  stdout: "pipe",
18371
18616
  stderr: "pipe"
18372
18617
  });
18373
18618
  const exitCode = await proc.exited;
18374
- if (exitCode !== 0) {
18375
- log("[tmux] findTmuxPath: 'which tmux' failed", { exitCode });
18619
+ const stdout = await new Response(proc.stdout).text();
18620
+ const paneId = stdout.trim();
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_")) {
18628
+ return { success: true, paneId };
18629
+ }
18630
+ 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 {
18376
18676
  return null;
18377
18677
  }
18378
- const stdout = await new Response(proc.stdout).text();
18379
- const path3 = stdout.trim().split(`
18380
- `)[0];
18381
- if (!path3) {
18382
- log("[tmux] findTmuxPath: no path in output");
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 {
18383
18716
  return null;
18384
18717
  }
18385
- const verifyProc = spawn([path3, "-V"], {
18386
- stdout: "pipe",
18387
- stderr: "pipe"
18388
- });
18389
- const verifyExit = await verifyProc.exited;
18390
- if (verifyExit !== 0) {
18391
- log("[tmux] findTmuxPath: tmux -V failed", { path: path3, verifyExit });
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 {
18392
18739
  return null;
18393
18740
  }
18394
- log("[tmux] findTmuxPath: found tmux", { path: path3 });
18395
- return path3;
18396
- } catch (err) {
18397
- log("[tmux] findTmuxPath: exception", { error: String(err) });
18398
- return null;
18399
18741
  }
18400
- }
18401
- async function getTmuxPath() {
18402
- if (tmuxChecked) {
18403
- return tmuxPath;
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
+ }
18404
18761
  }
18405
- tmuxPath = await findTmuxPath();
18406
- tmuxChecked = true;
18407
- log("[tmux] getTmuxPath: initialized", { tmuxPath });
18408
- return tmuxPath;
18409
- }
18410
- function isInsideTmux() {
18411
- return !!process.env.TMUX;
18412
- }
18413
- async function applyLayout(tmux, layout, mainPaneSize) {
18414
- try {
18415
- const layoutProc = spawn([tmux, "select-layout", layout], {
18416
- stdout: "pipe",
18417
- stderr: "pipe"
18418
- });
18419
- await layoutProc.exited;
18420
- if (layout === "main-horizontal" || layout === "main-vertical") {
18421
- const sizeOption = layout === "main-horizontal" ? "main-pane-height" : "main-pane-width";
18422
- const sizeProc = spawn([tmux, "set-window-option", sizeOption, `${mainPaneSize}%`], {
18762
+ async listPanes(zellij) {
18763
+ try {
18764
+ const proc = spawn2([zellij, "action", "list-panes"], {
18423
18765
  stdout: "pipe",
18424
18766
  stderr: "pipe"
18425
18767
  });
18426
- await sizeProc.exited;
18427
- const reapplyProc = spawn([tmux, "select-layout", layout], {
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"], {
18428
18806
  stdout: "pipe",
18429
18807
  stderr: "pipe"
18430
18808
  });
18431
- await reapplyProc.exited;
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;
18432
18816
  }
18433
- log("[tmux] applyLayout: applied", { layout, mainPaneSize });
18434
- } catch (err) {
18435
- log("[tmux] applyLayout: exception", { error: String(err) });
18436
18817
  }
18437
18818
  }
18438
- async function spawnTmuxPane(sessionId, description, config2, serverUrl) {
18439
- log("[tmux] spawnTmuxPane called", {
18440
- sessionId,
18441
- description,
18442
- config: config2,
18443
- serverUrl
18444
- });
18445
- if (!config2.enabled) {
18446
- log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
18447
- return { success: 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;
18448
18826
  }
18449
- if (!isInsideTmux()) {
18450
- log("[tmux] spawnTmuxPane: not inside tmux, skipping");
18451
- return { success: false };
18827
+ const cached2 = multiplexerCache.get(type);
18828
+ if (cached2) {
18829
+ return cached2;
18452
18830
  }
18453
- const serverRunning = await isServerRunning(serverUrl);
18454
- if (!serverRunning) {
18455
- const defaultPort = process.env.OPENCODE_PORT ?? "4096";
18456
- log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
18457
- serverUrl,
18458
- hint: `Start opencode with --port ${defaultPort}`
18459
- });
18460
- return { success: false };
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;
18852
+ }
18853
+ break;
18854
+ }
18855
+ default:
18856
+ log(`[multiplexer] Unknown type: ${type}`);
18857
+ return null;
18461
18858
  }
18462
- const tmux = await getTmuxPath();
18463
- if (!tmux) {
18464
- log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
18465
- return { success: false };
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(() => {});
18466
18867
  }
18467
- storedConfig = config2;
18468
- try {
18469
- const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`;
18470
- const args = [
18471
- "split-window",
18472
- "-h",
18473
- "-d",
18474
- "-P",
18475
- "-F",
18476
- "#{pane_id}",
18477
- opencodeCmd
18478
- ];
18479
- log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
18480
- const proc = spawn([tmux, ...args], {
18481
- stdout: "pipe",
18482
- stderr: "pipe"
18483
- });
18484
- const exitCode = await proc.exited;
18485
- const stdout = await new Response(proc.stdout).text();
18486
- const stderr = await new Response(proc.stderr).text();
18487
- const paneId = stdout.trim();
18488
- log("[tmux] spawnTmuxPane: split result", {
18489
- exitCode,
18490
- paneId,
18491
- stderr: stderr.trim()
18492
- });
18493
- if (exitCode === 0 && paneId) {
18494
- const renameProc = spawn([tmux, "select-pane", "-t", paneId, "-T", description.slice(0, 30)], { stdout: "ignore", stderr: "ignore" });
18495
- await renameProc.exited;
18496
- const layout = config2.layout ?? "main-vertical";
18497
- const mainPaneSize = config2.main_pane_size ?? 60;
18498
- await applyLayout(tmux, layout, mainPaneSize);
18499
- log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", {
18500
- paneId,
18501
- layout
18502
- });
18503
- return { success: true, paneId };
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);
18504
18880
  }
18505
- return { success: false };
18506
- } catch (err) {
18507
- log("[tmux] spawnTmuxPane: exception", { error: String(err) });
18508
- return { success: false };
18881
+ if (response?.ok) {
18882
+ return true;
18883
+ }
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;
18509
18904
  }
18905
+ log(`[variant] resolved variant="${trimmed}" for agent "${normalized}"`);
18906
+ return trimmed;
18510
18907
  }
18511
- async function closeTmuxPane(paneId) {
18512
- log("[tmux] closeTmuxPane called", { paneId });
18513
- if (!paneId) {
18514
- log("[tmux] closeTmuxPane: no paneId provided");
18515
- return false;
18908
+ function applyAgentVariant(variant, body) {
18909
+ if (!variant) {
18910
+ return body;
18516
18911
  }
18517
- const tmux = await getTmuxPath();
18518
- if (!tmux) {
18519
- log("[tmux] closeTmuxPane: tmux binary not found");
18520
- return false;
18912
+ if (body.variant) {
18913
+ return body;
18521
18914
  }
18522
- try {
18523
- log("[tmux] closeTmuxPane: sending Ctrl+C for graceful shutdown", {
18524
- paneId
18525
- });
18526
- const ctrlCProc = spawn([tmux, "send-keys", "-t", paneId, "C-c"], {
18527
- stdout: "pipe",
18528
- stderr: "pipe"
18529
- });
18530
- await ctrlCProc.exited;
18531
- await new Promise((r) => setTimeout(r, 250));
18532
- log("[tmux] closeTmuxPane: killing pane", { paneId });
18533
- const proc = spawn([tmux, "kill-pane", "-t", paneId], {
18534
- stdout: "pipe",
18535
- stderr: "pipe"
18536
- });
18537
- const exitCode = await proc.exited;
18538
- const stderr = await new Response(proc.stderr).text();
18539
- log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
18540
- if (exitCode === 0) {
18541
- log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
18542
- if (storedConfig) {
18543
- const layout = storedConfig.layout ?? "main-vertical";
18544
- const mainPaneSize = storedConfig.main_pane_size ?? 60;
18545
- await applyLayout(tmux, layout, mainPaneSize);
18546
- log("[tmux] closeTmuxPane: layout reapplied", { layout });
18547
- }
18548
- return true;
18549
- }
18550
- log("[tmux] closeTmuxPane: failed (pane may already be closed)", {
18551
- paneId
18552
- });
18553
- return false;
18554
- } catch (err) {
18555
- log("[tmux] closeTmuxPane: exception", { error: String(err) });
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") {
18556
18931
  return false;
18557
18932
  }
18558
- }
18559
- function startTmuxCheck() {
18560
- if (!tmuxChecked) {
18561
- getTmuxPath().catch(() => {});
18933
+ if (typeof part.text !== "string") {
18934
+ return false;
18562
18935
  }
18936
+ return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
18563
18937
  }
18564
18938
  // src/utils/zip-extractor.ts
18565
18939
  import { release } from "os";
18566
- var {spawn: spawn2, spawnSync } = globalThis.Bun;
18940
+ var {spawn: spawn3, spawnSync } = globalThis.Bun;
18567
18941
  var WINDOWS_BUILD_WITH_TAR = 17134;
18568
18942
  function getWindowsBuildNumber() {
18569
18943
  if (process.platform !== "win32")
@@ -18604,13 +18978,13 @@ async function extractZip(archivePath, destDir) {
18604
18978
  const extractor = getWindowsZipExtractor();
18605
18979
  switch (extractor) {
18606
18980
  case "tar":
18607
- proc = spawn2(["tar", "-xf", archivePath, "-C", destDir], {
18981
+ proc = spawn3(["tar", "-xf", archivePath, "-C", destDir], {
18608
18982
  stdout: "ignore",
18609
18983
  stderr: "pipe"
18610
18984
  });
18611
18985
  break;
18612
18986
  case "pwsh":
18613
- proc = spawn2([
18987
+ proc = spawn3([
18614
18988
  "pwsh",
18615
18989
  "-Command",
18616
18990
  `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
@@ -18620,7 +18994,7 @@ async function extractZip(archivePath, destDir) {
18620
18994
  });
18621
18995
  break;
18622
18996
  default:
18623
- proc = spawn2([
18997
+ proc = spawn3([
18624
18998
  "powershell",
18625
18999
  "-Command",
18626
19000
  `Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
@@ -18631,7 +19005,7 @@ async function extractZip(archivePath, destDir) {
18631
19005
  break;
18632
19006
  }
18633
19007
  } else {
18634
- proc = spawn2(["unzip", "-o", archivePath, "-d", destDir], {
19008
+ proc = spawn3(["unzip", "-o", archivePath, "-d", destDir], {
18635
19009
  stdout: "ignore",
18636
19010
  stderr: "pipe"
18637
19011
  });
@@ -18702,10 +19076,10 @@ class BackgroundTaskManager {
18702
19076
  activeStarts = 0;
18703
19077
  maxConcurrentStarts;
18704
19078
  completionResolvers = new Map;
18705
- constructor(ctx, tmuxConfig, config2) {
19079
+ constructor(ctx, multiplexerConfig, config2) {
18706
19080
  this.client = ctx.client;
18707
19081
  this.directory = ctx.directory;
18708
- this.tmuxEnabled = tmuxConfig?.enabled ?? false;
19082
+ this.tmuxEnabled = multiplexerConfig !== undefined && multiplexerConfig.type !== "none" && multiplexerConfig.type !== undefined && getMultiplexer(multiplexerConfig) !== null;
18709
19083
  this.config = config2;
18710
19084
  this.backgroundConfig = config2?.background ?? {
18711
19085
  maxConcurrentStarts: 10
@@ -18843,6 +19217,7 @@ class BackgroundTaskManager {
18843
19217
  const errors3 = [];
18844
19218
  let succeeded = false;
18845
19219
  const sessionId = session2.data.id;
19220
+ const retryOnEmpty = this.config?.fallback?.retry_on_empty ?? true;
18846
19221
  for (let i = 0;i < attemptModels.length; i++) {
18847
19222
  const model = attemptModels[i];
18848
19223
  const modelLabel = model ?? "default-model";
@@ -18866,6 +19241,10 @@ class BackgroundTaskManager {
18866
19241
  body,
18867
19242
  query: promptQuery
18868
19243
  }, timeoutMs);
19244
+ const extraction = await extractSessionResult(this.client, sessionId);
19245
+ if (retryOnEmpty && extraction.empty) {
19246
+ throw new Error("Empty response from provider");
19247
+ }
18869
19248
  succeeded = true;
18870
19249
  break;
18871
19250
  } catch (error48) {
@@ -18945,12 +19324,13 @@ class BackgroundTaskManager {
18945
19324
  async extractAndCompleteTask(task) {
18946
19325
  if (!task.sessionId)
18947
19326
  return;
19327
+ const retryOnEmpty = this.config?.fallback?.retry_on_empty ?? true;
18948
19328
  try {
18949
- const responseText = await extractSessionResult(this.client, task.sessionId);
18950
- if (responseText) {
18951
- 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");
18952
19332
  } else {
18953
- this.completeTask(task, "completed", "(No output)");
19333
+ this.completeTask(task, "completed", extraction.text);
18954
19334
  }
18955
19335
  } catch (error48) {
18956
19336
  this.completeTask(task, "failed", error48 instanceof Error ? error48.message : String(error48));
@@ -19068,31 +19448,31 @@ class BackgroundTaskManager {
19068
19448
  return this.depthTracker;
19069
19449
  }
19070
19450
  }
19071
- // src/background/tmux-session-manager.ts
19451
+ // src/background/multiplexer-session-manager.ts
19072
19452
  var SESSION_TIMEOUT_MS = 10 * 60 * 1000;
19073
19453
  var SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
19074
19454
 
19075
- class TmuxSessionManager {
19455
+ class MultiplexerSessionManager {
19076
19456
  client;
19077
- tmuxConfig;
19078
19457
  serverUrl;
19458
+ multiplexer = null;
19079
19459
  sessions = new Map;
19080
19460
  pollInterval;
19081
19461
  enabled = false;
19082
- constructor(ctx, tmuxConfig) {
19462
+ constructor(ctx, config2) {
19083
19463
  this.client = ctx.client;
19084
- this.tmuxConfig = tmuxConfig;
19085
19464
  const defaultPort = process.env.OPENCODE_PORT ?? "4096";
19086
19465
  this.serverUrl = ctx.serverUrl?.toString() ?? `http://localhost:${defaultPort}`;
19087
- this.enabled = tmuxConfig.enabled && isInsideTmux();
19088
- 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", {
19089
19469
  enabled: this.enabled,
19090
- tmuxConfig: this.tmuxConfig,
19470
+ type: config2.type,
19091
19471
  serverUrl: this.serverUrl
19092
19472
  });
19093
19473
  }
19094
19474
  async onSessionCreated(event) {
19095
- if (!this.enabled)
19475
+ if (!this.enabled || !this.multiplexer)
19096
19476
  return;
19097
19477
  if (event.type !== "session.created")
19098
19478
  return;
@@ -19104,16 +19484,25 @@ class TmuxSessionManager {
19104
19484
  const parentId = info.parentID;
19105
19485
  const title = info.title ?? "Subagent";
19106
19486
  if (this.sessions.has(sessionId)) {
19107
- 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
+ });
19108
19497
  return;
19109
19498
  }
19110
- log("[tmux-session-manager] child session created, spawning pane", {
19499
+ log("[multiplexer-session-manager] child session created, spawning pane", {
19111
19500
  sessionId,
19112
19501
  parentId,
19113
19502
  title
19114
19503
  });
19115
- const paneResult = await spawnTmuxPane(sessionId, title, this.tmuxConfig, this.serverUrl).catch((err) => {
19116
- 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", {
19117
19506
  error: String(err)
19118
19507
  });
19119
19508
  return { success: false, paneId: undefined };
@@ -19128,7 +19517,7 @@ class TmuxSessionManager {
19128
19517
  createdAt: now,
19129
19518
  lastSeenAt: now
19130
19519
  });
19131
- log("[tmux-session-manager] pane spawned", {
19520
+ log("[multiplexer-session-manager] pane spawned", {
19132
19521
  sessionId,
19133
19522
  paneId: paneResult.paneId
19134
19523
  });
@@ -19155,7 +19544,7 @@ class TmuxSessionManager {
19155
19544
  const sessionId = event.properties?.sessionID;
19156
19545
  if (!sessionId)
19157
19546
  return;
19158
- log("[tmux-session-manager] session deleted, closing pane", {
19547
+ log("[multiplexer-session-manager] session deleted, closing pane", {
19159
19548
  sessionId
19160
19549
  });
19161
19550
  await this.closeSession(sessionId);
@@ -19164,13 +19553,13 @@ class TmuxSessionManager {
19164
19553
  if (this.pollInterval)
19165
19554
  return;
19166
19555
  this.pollInterval = setInterval(() => this.pollSessions(), POLL_INTERVAL_BACKGROUND_MS);
19167
- log("[tmux-session-manager] polling started");
19556
+ log("[multiplexer-session-manager] polling started");
19168
19557
  }
19169
19558
  stopPolling() {
19170
19559
  if (this.pollInterval) {
19171
19560
  clearInterval(this.pollInterval);
19172
19561
  this.pollInterval = undefined;
19173
- log("[tmux-session-manager] polling stopped");
19562
+ log("[multiplexer-session-manager] polling stopped");
19174
19563
  }
19175
19564
  }
19176
19565
  async pollSessions() {
@@ -19202,18 +19591,18 @@ class TmuxSessionManager {
19202
19591
  await this.closeSession(sessionId);
19203
19592
  }
19204
19593
  } catch (err) {
19205
- log("[tmux-session-manager] poll error", { error: String(err) });
19594
+ log("[multiplexer-session-manager] poll error", { error: String(err) });
19206
19595
  }
19207
19596
  }
19208
19597
  async closeSession(sessionId) {
19209
19598
  const tracked = this.sessions.get(sessionId);
19210
- if (!tracked)
19599
+ if (!tracked || !this.multiplexer)
19211
19600
  return;
19212
- log("[tmux-session-manager] closing session pane", {
19601
+ log("[multiplexer-session-manager] closing session pane", {
19213
19602
  sessionId,
19214
19603
  paneId: tracked.paneId
19215
19604
  });
19216
- await closeTmuxPane(tracked.paneId);
19605
+ await this.multiplexer.closePane(tracked.paneId);
19217
19606
  this.sessions.delete(sessionId);
19218
19607
  if (this.sessions.size === 0) {
19219
19608
  this.stopPolling();
@@ -19221,18 +19610,19 @@ class TmuxSessionManager {
19221
19610
  }
19222
19611
  async cleanup() {
19223
19612
  this.stopPolling();
19224
- if (this.sessions.size > 0) {
19225
- log("[tmux-session-manager] closing all panes", {
19613
+ if (this.sessions.size > 0 && this.multiplexer) {
19614
+ log("[multiplexer-session-manager] closing all panes", {
19226
19615
  count: this.sessions.size
19227
19616
  });
19228
- 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", {
19229
19619
  paneId: s.paneId,
19230
19620
  error: String(err)
19231
19621
  })));
19232
19622
  await Promise.all(closePromises);
19233
19623
  this.sessions.clear();
19234
19624
  }
19235
- log("[tmux-session-manager] cleanup complete");
19625
+ log("[multiplexer-session-manager] cleanup complete");
19236
19626
  }
19237
19627
  }
19238
19628
  // src/council/council-manager.ts
@@ -19277,10 +19667,11 @@ class CouncilManager {
19277
19667
  const resolvedPreset = presetName ?? councilConfig.default_preset ?? "default";
19278
19668
  const preset = councilConfig.presets[resolvedPreset];
19279
19669
  if (!preset) {
19670
+ const available = Object.keys(councilConfig.presets).join(", ");
19280
19671
  log(`[council-manager] Preset "${resolvedPreset}" not found`);
19281
19672
  return {
19282
19673
  success: false,
19283
- 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}`,
19284
19675
  councillorResults: []
19285
19676
  };
19286
19677
  }
@@ -19294,6 +19685,8 @@ class CouncilManager {
19294
19685
  }
19295
19686
  const councillorsTimeout = councilConfig.councillors_timeout ?? 180000;
19296
19687
  const masterTimeout = councilConfig.master_timeout ?? 300000;
19688
+ const executionMode = councilConfig.councillor_execution_mode ?? "parallel";
19689
+ const maxRetries = councilConfig.councillor_retries ?? 3;
19297
19690
  const councillorCount = Object.keys(preset.councillors).length;
19298
19691
  log(`[council-manager] Starting council with preset "${resolvedPreset}"`, {
19299
19692
  councillors: Object.keys(preset.councillors)
@@ -19303,7 +19696,7 @@ class CouncilManager {
19303
19696
  error: err instanceof Error ? err.message : String(err)
19304
19697
  });
19305
19698
  });
19306
- const councillorResults = await this.runCouncillors(prompt, preset.councillors, parentSessionId, councillorsTimeout);
19699
+ const councillorResults = await this.runCouncillors(prompt, preset.councillors, parentSessionId, councillorsTimeout, executionMode, maxRetries);
19307
19700
  const completedCount = councillorResults.filter((r) => r.status === "completed").length;
19308
19701
  log(`[council-manager] Councillors completed: ${completedCount}/${councillorResults.length}`);
19309
19702
  if (completedCount === 0) {
@@ -19391,10 +19784,16 @@ ${bestResult.result}` : undefined,
19391
19784
  body,
19392
19785
  query: { directory: this.directory }
19393
19786
  }, options.timeout);
19394
- const result = await extractSessionResult(this.client, sessionId, {
19787
+ const extraction = await extractSessionResult(this.client, sessionId, {
19395
19788
  includeReasoning: options.includeReasoning
19396
19789
  });
19397
- 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;
19398
19797
  } finally {
19399
19798
  if (sessionId) {
19400
19799
  this.client.session.abort({ path: { id: sessionId } }).catch(() => {});
@@ -19404,13 +19803,51 @@ ${bestResult.result}` : undefined,
19404
19803
  }
19405
19804
  }
19406
19805
  }
19407
- async runCouncillors(prompt, councillors, parentSessionId, timeout) {
19806
+ async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode = "parallel", maxRetries = 1) {
19408
19807
  const entries = Object.entries(councillors);
19409
- const promises = entries.map(([name, config2], index) => (async () => {
19410
- if (index > 0) {
19411
- await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
19808
+ const results = [];
19809
+ if (executionMode === "serial") {
19810
+ for (const [name, config2] of entries) {
19811
+ results.push(await this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries));
19812
+ }
19813
+ } else {
19814
+ const promises = entries.map(([name, config2], index) => (async () => {
19815
+ if (index > 0) {
19816
+ await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
19817
+ }
19818
+ return this.runCouncillorWithRetry(name, config2, prompt, parentSessionId, timeout, maxRetries);
19819
+ })());
19820
+ const settled = await Promise.allSettled(promises);
19821
+ for (let index = 0;index < settled.length; index++) {
19822
+ const result = settled[index];
19823
+ const [name, cfg] = entries[index];
19824
+ if (result.status === "fulfilled") {
19825
+ results.push({
19826
+ name,
19827
+ model: cfg.model,
19828
+ status: result.value.status,
19829
+ result: result.value.result,
19830
+ error: result.value.error
19831
+ });
19832
+ } else {
19833
+ results.push({
19834
+ name,
19835
+ model: cfg.model,
19836
+ status: "failed",
19837
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
19838
+ });
19839
+ }
19840
+ }
19841
+ }
19842
+ return results;
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}`);
19412
19850
  }
19413
- const modelLabel = shortModelLabel(config2.model);
19414
19851
  try {
19415
19852
  const result = await this.runAgentSession({
19416
19853
  parentSessionId,
@@ -19430,33 +19867,51 @@ ${bestResult.result}` : undefined,
19430
19867
  };
19431
19868
  } catch (error48) {
19432
19869
  const msg = error48 instanceof Error ? error48.message : String(error48);
19433
- return {
19434
- name,
19435
- model: config2.model,
19436
- status: msg.includes("timed out") ? "timed_out" : "failed",
19437
- error: `Councillor "${name}": ${msg}`
19438
- };
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
+ }
19439
19880
  }
19440
- })());
19441
- const settled = await Promise.allSettled(promises);
19442
- return settled.map((result, index) => {
19443
- const [name, cfg] = entries[index];
19444
- if (result.status === "fulfilled") {
19445
- return {
19446
- name,
19447
- model: cfg.model,
19448
- status: result.value.status,
19449
- result: result.value.result,
19450
- error: result.value.error
19451
- };
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}`);
19452
19894
  }
19453
- return {
19454
- name,
19455
- model: cfg.model,
19456
- status: "failed",
19457
- error: result.reason instanceof Error ? result.reason.message : String(result.reason)
19458
- };
19459
- });
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`);
19460
19915
  }
19461
19916
  async runMaster(prompt, councillorResults, councilConfig, parentSessionId, timeout, presetMasterOverride) {
19462
19917
  const masterConfig = councilConfig.master;
@@ -19466,6 +19921,7 @@ ${bestResult.result}` : undefined,
19466
19921
  const effectivePrompt = presetMasterOverride?.prompt ?? masterConfig.prompt;
19467
19922
  const attemptModels = [effectiveModel, ...fallbackModels];
19468
19923
  const synthesisPrompt = formatMasterSynthesisPrompt(prompt, councillorResults, effectivePrompt);
19924
+ const maxRetries = councilConfig.councillor_retries ?? 3;
19469
19925
  const errors3 = [];
19470
19926
  for (let i = 0;i < attemptModels.length; i++) {
19471
19927
  const model = attemptModels[i];
@@ -19474,15 +19930,7 @@ ${bestResult.result}` : undefined,
19474
19930
  if (i > 0) {
19475
19931
  log(`[council-manager] master fallback ${i}/${attemptModels.length - 1}: ${currentLabel}`);
19476
19932
  }
19477
- const result = await this.runAgentSession({
19478
- parentSessionId,
19479
- title: `Council Master (${currentLabel})`,
19480
- agent: "council-master",
19481
- model,
19482
- promptText: synthesisPrompt,
19483
- variant: effectiveVariant,
19484
- timeout
19485
- });
19933
+ const result = await this.runMasterModelWithRetry(parentSessionId, model, currentLabel, synthesisPrompt, effectiveVariant, timeout, maxRetries);
19486
19934
  return { success: true, result };
19487
19935
  } catch (error48) {
19488
19936
  const msg = error48 instanceof Error ? error48.message : String(error48);
@@ -19735,30 +20183,6 @@ function getCachedVersion() {
19735
20183
  }
19736
20184
  return null;
19737
20185
  }
19738
- function updatePinnedVersion(configPath, oldEntry, newVersion) {
19739
- try {
19740
- if (!fs4.existsSync(configPath))
19741
- return false;
19742
- const content = fs4.readFileSync(configPath, "utf-8");
19743
- const newEntry = `${PACKAGE_NAME}@${newVersion}`;
19744
- const escapedOldEntry = oldEntry.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19745
- const entryRegex = new RegExp(`(["'])${escapedOldEntry}\\1`, "g");
19746
- if (!entryRegex.test(content)) {
19747
- log(`[auto-update-checker] Entry "${oldEntry}" not found in ${configPath}`);
19748
- return false;
19749
- }
19750
- const updatedContent = content.replace(entryRegex, `$1${newEntry}$1`);
19751
- if (updatedContent === content) {
19752
- return false;
19753
- }
19754
- fs4.writeFileSync(configPath, updatedContent, "utf-8");
19755
- log(`[auto-update-checker] Updated ${configPath}: ${oldEntry} \u2192 ${newEntry}`);
19756
- return true;
19757
- } catch (err) {
19758
- log(`[auto-update-checker] Failed to update config file ${configPath}:`, err);
19759
- return false;
19760
- }
19761
- }
19762
20186
  async function getLatestVersion(channel = "latest") {
19763
20187
  const controller = new AbortController;
19764
20188
  const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
@@ -19825,6 +20249,10 @@ async function runBackgroundUpdateCheck(ctx, autoUpdate) {
19825
20249
  log("[auto-update-checker] No version found (cached or pinned)");
19826
20250
  return;
19827
20251
  }
20252
+ if (pluginInfo.isPinned) {
20253
+ log(`[auto-update-checker] Version is pinned; skipping update check.`);
20254
+ return;
20255
+ }
19828
20256
  const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
19829
20257
  const latestVersion = await getLatestVersion(channel);
19830
20258
  if (!latestVersion) {
@@ -19841,15 +20269,6 @@ async function runBackgroundUpdateCheck(ctx, autoUpdate) {
19841
20269
  log("[auto-update-checker] Auto-update disabled, notification only");
19842
20270
  return;
19843
20271
  }
19844
- if (pluginInfo.isPinned) {
19845
- const updated = updatePinnedVersion(pluginInfo.configPath, pluginInfo.entry, latestVersion);
19846
- if (!updated) {
19847
- showToast(ctx, `OMO-Slim ${latestVersion}`, `v${latestVersion} available. Restart to apply.`, "info", 8000);
19848
- log("[auto-update-checker] Failed to update pinned version in config");
19849
- return;
19850
- }
19851
- log(`[auto-update-checker] Config updated: ${pluginInfo.entry} \u2192 ${PACKAGE_NAME}@${latestVersion}`);
19852
- }
19853
20272
  invalidatePackage(PACKAGE_NAME);
19854
20273
  const installSuccess = await runBunInstallSafe(ctx);
19855
20274
  if (installSuccess) {
@@ -20046,6 +20465,76 @@ ${buildRetryGuidance(detected)}`;
20046
20465
  }
20047
20466
  };
20048
20467
  }
20468
+ // src/hooks/filter-available-skills/index.ts
20469
+ var AVAILABLE_SKILLS_BLOCK_REGEX = /<available_skills>\s*([\s\S]*?)\s*<\/available_skills>/g;
20470
+ var SKILL_NAME_REGEX = /<name>([^<]+)<\/name>/;
20471
+ function getCurrentAgent(messages) {
20472
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
20473
+ const message = messages[index];
20474
+ if (message.info.role === "user") {
20475
+ return message.info.agent ?? "orchestrator";
20476
+ }
20477
+ }
20478
+ return "orchestrator";
20479
+ }
20480
+ function extractSkillEntries(blockContent) {
20481
+ const entries = [];
20482
+ const skillEntryRegex = /<skill>\s*([\s\S]*?)\s*<\/skill>/g;
20483
+ for (const match of blockContent.matchAll(skillEntryRegex)) {
20484
+ const block = match[0];
20485
+ const nameMatch = block.match(SKILL_NAME_REGEX);
20486
+ if (!nameMatch) {
20487
+ continue;
20488
+ }
20489
+ entries.push({
20490
+ name: nameMatch[1].trim(),
20491
+ block
20492
+ });
20493
+ }
20494
+ return entries;
20495
+ }
20496
+ function isSkillAllowed(skillName, permissionRules) {
20497
+ const specificRule = permissionRules[skillName];
20498
+ if (specificRule !== undefined) {
20499
+ return specificRule === "allow";
20500
+ }
20501
+ return permissionRules["*"] === "allow";
20502
+ }
20503
+ function filterAvailableSkillsText(text, permissionRules) {
20504
+ return text.replace(AVAILABLE_SKILLS_BLOCK_REGEX, (_fullMatch, blockContent) => {
20505
+ const allowedEntries = extractSkillEntries(blockContent).filter((entry) => isSkillAllowed(entry.name, permissionRules));
20506
+ if (allowedEntries.length === 0) {
20507
+ return `<available_skills>
20508
+ No skills available.
20509
+ </available_skills>`;
20510
+ }
20511
+ return `<available_skills>
20512
+ ${allowedEntries.map((entry) => entry.block).join(`
20513
+ `)}
20514
+ </available_skills>`;
20515
+ });
20516
+ }
20517
+ function createFilterAvailableSkillsHook(_ctx, config2) {
20518
+ return {
20519
+ "experimental.chat.messages.transform": async (_input, output) => {
20520
+ const { messages } = output;
20521
+ if (messages.length === 0) {
20522
+ return;
20523
+ }
20524
+ const agentName = getCurrentAgent(messages);
20525
+ const configuredSkills = getAgentOverride(config2, agentName)?.skills;
20526
+ const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
20527
+ for (const message of messages) {
20528
+ for (const part of message.parts) {
20529
+ if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
20530
+ continue;
20531
+ }
20532
+ part.text = filterAvailableSkillsText(part.text, permissionRules);
20533
+ }
20534
+ }
20535
+ }
20536
+ };
20537
+ }
20049
20538
  // src/hooks/foreground-fallback/index.ts
20050
20539
  var RATE_LIMIT_PATTERNS = [
20051
20540
  /\b429\b/,
@@ -20381,12 +20870,31 @@ var grep_app = {
20381
20870
  };
20382
20871
 
20383
20872
  // src/mcp/websearch.ts
20384
- var websearch = {
20385
- type: "remote",
20386
- url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
20387
- headers: process.env.EXA_API_KEY ? { "x-api-key": process.env.EXA_API_KEY } : undefined,
20388
- oauth: false
20389
- };
20873
+ function createWebsearchConfig(config2) {
20874
+ const provider = config2?.provider || "exa";
20875
+ if (provider === "tavily") {
20876
+ const tavilyKey = process.env.TAVILY_API_KEY;
20877
+ if (!tavilyKey) {
20878
+ throw new Error("TAVILY_API_KEY environment variable is required for Tavily provider");
20879
+ }
20880
+ return {
20881
+ type: "remote",
20882
+ url: "https://mcp.tavily.com/mcp/",
20883
+ headers: {
20884
+ Authorization: `Bearer ${tavilyKey}`
20885
+ },
20886
+ oauth: false
20887
+ };
20888
+ }
20889
+ const exaKey = process.env.EXA_API_KEY;
20890
+ const exaUrl = exaKey ? `https://mcp.exa.ai/mcp?tools=web_search_exa&exaApiKey=${encodeURIComponent(exaKey)}` : "https://mcp.exa.ai/mcp?tools=web_search_exa";
20891
+ return {
20892
+ type: "remote",
20893
+ url: exaUrl,
20894
+ oauth: false
20895
+ };
20896
+ }
20897
+ var websearch = createWebsearchConfig();
20390
20898
 
20391
20899
  // src/mcp/index.ts
20392
20900
  var allBuiltinMcps = {
@@ -20394,8 +20902,12 @@ var allBuiltinMcps = {
20394
20902
  context7,
20395
20903
  grep_app
20396
20904
  };
20397
- function createBuiltinMcps(disabledMcps = []) {
20398
- return Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name)));
20905
+ function createBuiltinMcps(disabledMcps = [], websearchConfig) {
20906
+ const mcps = Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => !disabledMcps.includes(name)));
20907
+ if (!disabledMcps.includes("websearch")) {
20908
+ mcps.websearch = createWebsearchConfig(websearchConfig);
20909
+ }
20910
+ return mcps;
20399
20911
  }
20400
20912
 
20401
20913
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
@@ -32721,15 +33233,15 @@ tool.schema = exports_external2;
32721
33233
 
32722
33234
  // src/tools/ast-grep/cli.ts
32723
33235
  import { existsSync as existsSync6 } from "fs";
32724
- var {spawn: spawn3 } = globalThis.Bun;
33236
+ var {spawn: spawn4 } = globalThis.Bun;
32725
33237
 
32726
33238
  // src/tools/ast-grep/constants.ts
32727
33239
  import { existsSync as existsSync5, statSync as statSync2 } from "fs";
32728
33240
  import { createRequire as createRequire2 } from "module";
32729
- import { dirname as dirname3, join as join8 } from "path";
33241
+ import { dirname as dirname4, join as join8 } from "path";
32730
33242
 
32731
33243
  // src/tools/ast-grep/downloader.ts
32732
- import { chmodSync, existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
33244
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, unlinkSync } from "fs";
32733
33245
  import { createRequire } from "module";
32734
33246
  import { homedir as homedir3 } from "os";
32735
33247
  import { join as join7 } from "path";
@@ -32789,7 +33301,7 @@ async function downloadAstGrep(version3 = DEFAULT_VERSION) {
32789
33301
  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`);
32790
33302
  try {
32791
33303
  if (!existsSync4(cacheDir)) {
32792
- mkdirSync(cacheDir, { recursive: true });
33304
+ mkdirSync2(cacheDir, { recursive: true });
32793
33305
  }
32794
33306
  const response = await fetch(downloadUrl, { redirect: "follow" });
32795
33307
  if (!response.ok) {
@@ -32883,7 +33395,7 @@ function findSgCliPathSync() {
32883
33395
  try {
32884
33396
  const require2 = createRequire2(import.meta.url);
32885
33397
  const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
32886
- const cliDir = dirname3(cliPkgPath);
33398
+ const cliDir = dirname4(cliPkgPath);
32887
33399
  const sgPath = join8(cliDir, binaryName);
32888
33400
  if (existsSync5(sgPath) && isValidBinary(sgPath)) {
32889
33401
  return sgPath;
@@ -32894,7 +33406,7 @@ function findSgCliPathSync() {
32894
33406
  try {
32895
33407
  const require2 = createRequire2(import.meta.url);
32896
33408
  const pkgPath = require2.resolve(`${platformPkg}/package.json`);
32897
- const pkgDir = dirname3(pkgPath);
33409
+ const pkgDir = dirname4(pkgPath);
32898
33410
  const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
32899
33411
  const binaryPath = join8(pkgDir, astGrepName);
32900
33412
  if (existsSync5(binaryPath) && isValidBinary(binaryPath)) {
@@ -32988,7 +33500,7 @@ async function runSg(options) {
32988
33500
  }
32989
33501
  }
32990
33502
  const timeout = DEFAULT_TIMEOUT_MS2;
32991
- const proc = spawn3([cliPath, ...args], {
33503
+ const proc = spawn4([cliPath, ...args], {
32992
33504
  stdout: "pipe",
32993
33505
  stderr: "pipe"
32994
33506
  });
@@ -33268,7 +33780,7 @@ var ast_grep_replace = tool({
33268
33780
  });
33269
33781
  // src/tools/background.ts
33270
33782
  var z2 = tool.schema;
33271
- function createBackgroundTools(_ctx, manager, _tmuxConfig, _pluginConfig) {
33783
+ function createBackgroundTools(_ctx, manager, _multiplexerConfig, _pluginConfig) {
33272
33784
  const agentNames = SUBAGENT_NAMES.join(", ");
33273
33785
  const background_task = tool({
33274
33786
  description: `Launch background agent task. Returns task_id immediately.
@@ -33438,16 +33950,16 @@ Returns the synthesized result with councillor summary.`,
33438
33950
  // src/tools/lsp/client.ts
33439
33951
  var import_node = __toESM(require_main(), 1);
33440
33952
  import { readFileSync as readFileSync4 } from "fs";
33441
- import { extname, resolve as resolve2 } from "path";
33953
+ import { extname, resolve as resolve3 } from "path";
33442
33954
  import { Readable, Writable } from "stream";
33443
33955
  import { pathToFileURL } from "url";
33444
- var {spawn: spawn4 } = globalThis.Bun;
33956
+ var {spawn: spawn5 } = globalThis.Bun;
33445
33957
 
33446
33958
  // src/tools/lsp/config.ts
33447
33959
  var import_which = __toESM(require_lib(), 1);
33448
33960
  import { existsSync as existsSync8 } from "fs";
33449
33961
  import { homedir as homedir4 } from "os";
33450
- import { join as join9 } from "path";
33962
+ import { dirname as dirname6, join as join9, resolve as resolve2 } from "path";
33451
33963
 
33452
33964
  // src/tools/lsp/config-store.ts
33453
33965
  var userConfig = new Map;
@@ -33478,7 +33990,7 @@ function hasUserLspConfig() {
33478
33990
 
33479
33991
  // src/tools/lsp/constants.ts
33480
33992
  import { existsSync as existsSync7, readdirSync, statSync as statSync3 } from "fs";
33481
- import { dirname as dirname4, resolve } from "path";
33993
+ import { dirname as dirname5, resolve } from "path";
33482
33994
  var SEVERITY_MAP = {
33483
33995
  1: "error",
33484
33996
  2: "warning",
@@ -33498,10 +34010,10 @@ function* walkUpDirectories(start, stop) {
33498
34010
  let dir = resolve(start);
33499
34011
  try {
33500
34012
  if (!statSync3(dir).isDirectory()) {
33501
- dir = dirname4(dir);
34013
+ dir = dirname5(dir);
33502
34014
  }
33503
34015
  } catch {
33504
- dir = dirname4(dir);
34016
+ dir = dirname5(dir);
33505
34017
  }
33506
34018
  let prevDir = "";
33507
34019
  while (dir !== prevDir && dir !== "/") {
@@ -33509,7 +34021,7 @@ function* walkUpDirectories(start, stop) {
33509
34021
  prevDir = dir;
33510
34022
  if (dir === stop)
33511
34023
  break;
33512
- dir = dirname4(dir);
34024
+ dir = dirname5(dir);
33513
34025
  }
33514
34026
  }
33515
34027
  function NearestRoot(includePatterns, excludePatterns) {
@@ -34050,39 +34562,79 @@ function buildMergedServers() {
34050
34562
  }
34051
34563
  return servers;
34052
34564
  }
34053
- function findServerForExtension(ext) {
34054
- const servers = buildMergedServers();
34055
- for (const [, config3] of servers) {
34056
- if (config3.extensions.includes(ext)) {
34057
- const server = {
34058
- id: config3.id,
34059
- command: config3.command,
34060
- extensions: config3.extensions,
34061
- root: config3.root,
34062
- env: config3.env,
34063
- initialization: config3.initialization
34064
- };
34065
- if (isServerInstalled(config3.command)) {
34066
- return { status: "found", server };
34067
- }
34068
- return {
34565
+ function getServerWorkspace(config3, filePath) {
34566
+ if (!filePath) {
34567
+ return;
34568
+ }
34569
+ if (!config3.root) {
34570
+ return dirname6(resolve2(filePath));
34571
+ }
34572
+ return config3.root(filePath);
34573
+ }
34574
+ function shouldSkipServer(config3, filePath) {
34575
+ if (!filePath) {
34576
+ return false;
34577
+ }
34578
+ return config3.id === "deno" && getServerWorkspace(config3, filePath) === undefined;
34579
+ }
34580
+ function toResolvedServer(config3, command) {
34581
+ return {
34582
+ id: config3.id,
34583
+ command: command ?? config3.command,
34584
+ extensions: config3.extensions,
34585
+ root: config3.root,
34586
+ env: config3.env,
34587
+ initialization: config3.initialization
34588
+ };
34589
+ }
34590
+ function findInstalledServer(configs, filePath) {
34591
+ let firstNotInstalled = null;
34592
+ for (const config3 of configs) {
34593
+ const workspace = getServerWorkspace(config3, filePath);
34594
+ const resolvedCommand = resolveServerCommand(config3.command, workspace ?? (filePath ? dirname6(resolve2(filePath)) : undefined));
34595
+ const server = toResolvedServer(config3, resolvedCommand ?? undefined);
34596
+ log(`[LSP] Considering server for ${config3.extensions.join(", ")}: ${config3.id} with command ${config3.command.join(" ")}`);
34597
+ if (resolvedCommand) {
34598
+ return { status: "found", server };
34599
+ }
34600
+ if (!firstNotInstalled) {
34601
+ log(`[LSP] Server ${config3.id} not found in PATH or local node_modules`);
34602
+ firstNotInstalled = {
34069
34603
  status: "not_installed",
34070
34604
  server,
34071
34605
  installHint: LSP_INSTALL_HINTS[config3.id] || `Install '${config3.command[0]}' and add to PATH`
34072
34606
  };
34073
34607
  }
34074
34608
  }
34609
+ return firstNotInstalled ?? undefined;
34610
+ }
34611
+ function findServerForExtension(ext, filePath) {
34612
+ const servers = [...buildMergedServers().values()].filter((config3) => config3.extensions.includes(ext));
34613
+ if (servers.length === 0) {
34614
+ log(`[LSP] No server config found for ${ext}`);
34615
+ return { status: "not_configured", extension: ext };
34616
+ }
34617
+ const candidateServers = servers.filter((config3) => !shouldSkipServer(config3, filePath));
34618
+ if (candidateServers.length === 0) {
34619
+ log(`[LSP] No applicable server config found for ${ext} at ${filePath}`);
34620
+ return { status: "not_configured", extension: ext };
34621
+ }
34622
+ const result = findInstalledServer(candidateServers, filePath);
34623
+ if (result) {
34624
+ return result;
34625
+ }
34626
+ log(`[LSP] No applicable server config found for ${ext}`);
34075
34627
  return { status: "not_configured", extension: ext };
34076
34628
  }
34077
34629
  function getLanguageId(ext) {
34078
34630
  return LANGUAGE_EXTENSIONS[ext] || "plaintext";
34079
34631
  }
34080
- function isServerInstalled(command) {
34632
+ function resolveServerCommand(command, cwd) {
34081
34633
  if (command.length === 0)
34082
- return false;
34083
- const cmd = command[0];
34634
+ return null;
34635
+ const [cmd, ...args] = command;
34084
34636
  if (cmd.includes("/") || cmd.includes("\\")) {
34085
- return existsSync8(cmd);
34637
+ return existsSync8(cmd) ? command : null;
34086
34638
  }
34087
34639
  const isWindows = process.platform === "win32";
34088
34640
  const ext = isWindows ? ".exe" : "";
@@ -34094,17 +34646,94 @@ function isServerInstalled(command) {
34094
34646
  nothrow: true
34095
34647
  });
34096
34648
  if (result !== null) {
34097
- return true;
34649
+ return [result, ...args];
34098
34650
  }
34099
- const cwd = process.cwd();
34100
- const localBin = join9(cwd, "node_modules", ".bin", cmd);
34101
- if (existsSync8(localBin) || existsSync8(localBin + ext)) {
34102
- return true;
34651
+ const localBinRoot = cwd ?? process.cwd();
34652
+ const localBin = join9(localBinRoot, "node_modules", ".bin", cmd);
34653
+ if (existsSync8(localBin)) {
34654
+ return [localBin, ...args];
34103
34655
  }
34104
- return false;
34656
+ if (existsSync8(localBin + ext)) {
34657
+ return [localBin + ext, ...args];
34658
+ }
34659
+ return null;
34105
34660
  }
34106
34661
 
34107
34662
  // src/tools/lsp/client.ts
34663
+ var START_TIMEOUT_MS = 5000;
34664
+ var REQUEST_TIMEOUT_MS = 5000;
34665
+ var OPEN_FILE_DELAY_MS = 250;
34666
+ var INITIALIZE_DELAY_MS = 100;
34667
+ var DIAGNOSTIC_SETTLE_DELAY_MS = 250;
34668
+ var LSP_TIMEOUTS = {
34669
+ start: START_TIMEOUT_MS,
34670
+ request: REQUEST_TIMEOUT_MS,
34671
+ openFileDelay: OPEN_FILE_DELAY_MS,
34672
+ initializeDelay: INITIALIZE_DELAY_MS,
34673
+ diagnosticSettleDelay: DIAGNOSTIC_SETTLE_DELAY_MS
34674
+ };
34675
+ function getDiagnosticsCapabilitySummary({
34676
+ diagnosticProvider,
34677
+ publishDiagnosticsObserved = false,
34678
+ workspaceConfigurationRequested = false
34679
+ }) {
34680
+ const pull = Boolean(diagnosticProvider);
34681
+ const workspaceDiagnostics = Boolean(diagnosticProvider?.workspaceDiagnostics);
34682
+ const interFileDependencies = Boolean(diagnosticProvider?.interFileDependencies);
34683
+ const availableModes = [
34684
+ ...pull ? ["pull", "pull/full", "pull/unchanged"] : ["push"],
34685
+ ...workspaceDiagnostics ? ["workspace-pull"] : [],
34686
+ ...publishDiagnosticsObserved ? ["push"] : []
34687
+ ];
34688
+ return {
34689
+ availableModes: Array.from(new Set(availableModes)),
34690
+ preferredMode: pull ? "pull" : "push",
34691
+ inferredTransport: pull && publishDiagnosticsObserved ? "hybrid" : pull ? "pull" : "push",
34692
+ pull,
34693
+ pushObserved: publishDiagnosticsObserved,
34694
+ pullResultTracking: pull,
34695
+ workspaceDiagnostics,
34696
+ interFileDependencies,
34697
+ workspaceConfiguration: workspaceConfigurationRequested
34698
+ };
34699
+ }
34700
+ function withTimeout(promise3, ms, label, onTimeout) {
34701
+ return new Promise((resolve4, reject) => {
34702
+ let settled = false;
34703
+ const timer = setTimeout(() => {
34704
+ if (settled) {
34705
+ return;
34706
+ }
34707
+ settled = true;
34708
+ Promise.resolve(onTimeout?.()).catch(() => {});
34709
+ reject(new Error(`${label} timeout after ${ms}ms`));
34710
+ }, ms);
34711
+ promise3.then((value) => {
34712
+ if (settled) {
34713
+ return;
34714
+ }
34715
+ settled = true;
34716
+ clearTimeout(timer);
34717
+ resolve4(value);
34718
+ }, (error92) => {
34719
+ if (settled) {
34720
+ return;
34721
+ }
34722
+ settled = true;
34723
+ clearTimeout(timer);
34724
+ reject(error92);
34725
+ });
34726
+ });
34727
+ }
34728
+ function getWorkspaceConfiguration(items) {
34729
+ return items.map((item) => {
34730
+ if (item?.section === "json") {
34731
+ return { validate: { enable: true } };
34732
+ }
34733
+ return null;
34734
+ });
34735
+ }
34736
+
34108
34737
  class LSPServerManager {
34109
34738
  static instance;
34110
34739
  clients = new Map;
@@ -34269,17 +34898,27 @@ class LSPClient {
34269
34898
  stderrBuffer = [];
34270
34899
  processExited = false;
34271
34900
  diagnosticsStore = new Map;
34901
+ diagnosticResultIds = new Map;
34902
+ documents = new Map;
34903
+ diagnosticProvider = null;
34904
+ publishDiagnosticsObserved = false;
34905
+ supportsPullDiagnostics = false;
34906
+ workspaceConfigurationRequested = false;
34272
34907
  constructor(root, server) {
34273
34908
  this.root = root;
34274
34909
  this.server = server;
34275
34910
  }
34276
34911
  async start() {
34912
+ const command = resolveServerCommand(this.server.command, this.root);
34913
+ if (!command) {
34914
+ throw new Error(`Failed to resolve LSP server command: ${this.server.command.join(" ")}`);
34915
+ }
34277
34916
  log("[lsp] LSPClient.start: spawning server", {
34278
34917
  server: this.server.id,
34279
- command: this.server.command.join(" "),
34918
+ command: command.join(" "),
34280
34919
  root: this.root
34281
34920
  });
34282
- this.proc = spawn4(this.server.command, {
34921
+ this.proc = spawn5(command, {
34283
34922
  stdin: "pipe",
34284
34923
  stdout: "pipe",
34285
34924
  stderr: "pipe",
@@ -34329,18 +34968,35 @@ class LSPClient {
34329
34968
  });
34330
34969
  this.connection = import_node.createMessageConnection(new import_node.StreamMessageReader(nodeReadable), new import_node.StreamMessageWriter(nodeWritable));
34331
34970
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
34971
+ if (!this.publishDiagnosticsObserved) {
34972
+ this.publishDiagnosticsObserved = true;
34973
+ log("[lsp] diagnostics capabilities: publishDiagnostics observed", {
34974
+ server: this.server.id,
34975
+ ...getDiagnosticsCapabilitySummary({
34976
+ diagnosticProvider: this.diagnosticProvider,
34977
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
34978
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
34979
+ })
34980
+ });
34981
+ }
34332
34982
  if (params.uri) {
34333
34983
  this.diagnosticsStore.set(params.uri, params.diagnostics ?? []);
34334
34984
  }
34335
34985
  });
34336
34986
  this.connection.onRequest("workspace/configuration", (params) => {
34337
- const items = params.items ?? [];
34338
- return items.map((item) => {
34339
- const configItem = item;
34340
- if (configItem.section === "json")
34341
- return { validate: { enable: true } };
34342
- return {};
34343
- });
34987
+ if (!this.workspaceConfigurationRequested) {
34988
+ this.workspaceConfigurationRequested = true;
34989
+ log("[lsp] diagnostics capabilities: workspace configuration requested", {
34990
+ server: this.server.id,
34991
+ sections: (params.items ?? []).map((item) => item && typeof item === "object" && ("section" in item) ? item.section ?? null : null),
34992
+ ...getDiagnosticsCapabilitySummary({
34993
+ diagnosticProvider: this.diagnosticProvider,
34994
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
34995
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
34996
+ })
34997
+ });
34998
+ }
34999
+ return getWorkspaceConfiguration(params.items ?? []);
34344
35000
  });
34345
35001
  this.connection.onRequest("client/registerCapability", () => null);
34346
35002
  this.connection.onRequest("window/workDoneProgress/create", () => null);
@@ -34348,7 +35004,7 @@ class LSPClient {
34348
35004
  this.processExited = true;
34349
35005
  });
34350
35006
  this.connection.listen();
34351
- await new Promise((resolve3) => setTimeout(resolve3, 100));
35007
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
34352
35008
  if (this.proc.exitCode !== null) {
34353
35009
  const stderr = this.stderrBuffer.join(`
34354
35010
  `);
@@ -34391,13 +35047,14 @@ stderr: ${stderr}` : ""));
34391
35047
  root: this.root
34392
35048
  });
34393
35049
  const rootUri = pathToFileURL(this.root).href;
34394
- await this.connection.sendRequest("initialize", {
35050
+ const result = await withTimeout(this.connection.sendRequest("initialize", {
34395
35051
  processId: process.pid,
34396
35052
  rootUri,
34397
35053
  rootPath: this.root,
34398
35054
  workspaceFolders: [{ uri: rootUri, name: "workspace" }],
34399
35055
  capabilities: {
34400
35056
  textDocument: {
35057
+ diagnostic: {},
34401
35058
  hover: { contentFormat: ["markdown", "plaintext"] },
34402
35059
  definition: { linkSupport: true },
34403
35060
  references: {},
@@ -34418,76 +35075,163 @@ stderr: ${stderr}` : ""));
34418
35075
  }
34419
35076
  },
34420
35077
  ...this.server.initialization
35078
+ }), LSP_TIMEOUTS.request, `LSP initialize (${this.server.id})`);
35079
+ const capabilities = result && typeof result === "object" && "capabilities" in result && result.capabilities && typeof result.capabilities === "object" ? result.capabilities : undefined;
35080
+ this.diagnosticProvider = capabilities && "diagnosticProvider" in capabilities ? capabilities.diagnosticProvider : null;
35081
+ this.supportsPullDiagnostics = Boolean(this.diagnosticProvider);
35082
+ log("[lsp] diagnostics capabilities negotiated", {
35083
+ server: this.server.id,
35084
+ diagnosticProvider: this.diagnosticProvider,
35085
+ ...getDiagnosticsCapabilitySummary({
35086
+ diagnosticProvider: this.diagnosticProvider,
35087
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
35088
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
35089
+ })
34421
35090
  });
34422
- this.connection.sendNotification("initialized");
34423
- await new Promise((r) => setTimeout(r, 300));
35091
+ this.connection.sendNotification("initialized", {});
35092
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.initializeDelay));
34424
35093
  log("[lsp] LSPClient.initialize: complete", { server: this.server.id });
34425
35094
  }
34426
- async openFile(filePath) {
34427
- const absPath = resolve2(filePath);
34428
- if (this.openedFiles.has(absPath)) {
34429
- log("[lsp] openFile: already open, skipping", { filePath: absPath });
34430
- return;
35095
+ async waitForPublishedDiagnostics(uri, timeoutMs = LSP_TIMEOUTS.request) {
35096
+ const cachedDiagnostics = this.diagnosticsStore.get(uri);
35097
+ if (cachedDiagnostics) {
35098
+ return cachedDiagnostics;
35099
+ }
35100
+ const startedAt = Date.now();
35101
+ while (Date.now() - startedAt < timeoutMs) {
35102
+ await new Promise((r) => setTimeout(r, 100));
35103
+ const diagnostics = this.diagnosticsStore.get(uri);
35104
+ if (diagnostics) {
35105
+ return diagnostics;
35106
+ }
34431
35107
  }
35108
+ return this.diagnosticsStore.get(uri);
35109
+ }
35110
+ async openFile(filePath) {
35111
+ await this.ensureDocumentSynced(filePath);
35112
+ }
35113
+ async ensureDocumentSynced(filePath) {
35114
+ const absPath = resolve3(filePath);
35115
+ const uri = pathToFileURL(absPath).href;
34432
35116
  const text = readFileSync4(absPath, "utf-8");
34433
35117
  const ext = extname(absPath);
34434
35118
  const languageId = getLanguageId(ext);
34435
- log("[lsp] openFile: opening document", {
34436
- filePath: absPath,
34437
- languageId,
34438
- size: text.length
34439
- });
34440
- this.connection?.sendNotification("textDocument/didOpen", {
34441
- textDocument: {
34442
- uri: pathToFileURL(absPath).href,
35119
+ const existing = this.documents.get(uri);
35120
+ if (!existing) {
35121
+ log("[lsp] ensureDocumentSynced: didOpen", {
35122
+ filePath: absPath,
34443
35123
  languageId,
34444
- version: 1,
34445
- text
34446
- }
34447
- });
34448
- this.openedFiles.add(absPath);
34449
- await new Promise((r) => setTimeout(r, 1000));
35124
+ size: text.length
35125
+ });
35126
+ this.connection?.sendNotification("textDocument/didOpen", {
35127
+ textDocument: { uri, languageId, version: 1, text }
35128
+ });
35129
+ this.documents.set(uri, { version: 1, text, languageId });
35130
+ this.openedFiles.add(absPath);
35131
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.openFileDelay));
35132
+ return;
35133
+ }
35134
+ if (existing.text !== text) {
35135
+ const newVersion = existing.version + 1;
35136
+ log("[lsp] ensureDocumentSynced: didChange", {
35137
+ filePath: absPath,
35138
+ languageId,
35139
+ oldVersion: existing.version,
35140
+ newVersion,
35141
+ size: text.length
35142
+ });
35143
+ this.connection?.sendNotification("textDocument/didChange", {
35144
+ textDocument: { uri, version: newVersion },
35145
+ contentChanges: [{ text }]
35146
+ });
35147
+ this.documents.set(uri, { version: newVersion, text, languageId });
35148
+ this.diagnosticsStore.delete(uri);
35149
+ this.diagnosticResultIds.delete(uri);
35150
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.openFileDelay));
35151
+ } else {
35152
+ log("[lsp] ensureDocumentSynced: already synced", { filePath: absPath });
35153
+ }
34450
35154
  }
34451
35155
  async definition(filePath, line, character) {
34452
- const absPath = resolve2(filePath);
35156
+ const absPath = resolve3(filePath);
34453
35157
  await this.openFile(absPath);
34454
- return this.connection?.sendRequest("textDocument/definition", {
35158
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/definition", {
34455
35159
  textDocument: { uri: pathToFileURL(absPath).href },
34456
35160
  position: { line: line - 1, character }
34457
- });
35161
+ }), LSP_TIMEOUTS.request, `LSP definition (${this.server.id})`) : undefined;
34458
35162
  }
34459
35163
  async references(filePath, line, character, includeDeclaration = true) {
34460
- const absPath = resolve2(filePath);
35164
+ const absPath = resolve3(filePath);
34461
35165
  await this.openFile(absPath);
34462
- return this.connection?.sendRequest("textDocument/references", {
35166
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/references", {
34463
35167
  textDocument: { uri: pathToFileURL(absPath).href },
34464
35168
  position: { line: line - 1, character },
34465
35169
  context: { includeDeclaration }
34466
- });
35170
+ }), LSP_TIMEOUTS.request, `LSP references (${this.server.id})`) : undefined;
34467
35171
  }
34468
35172
  async diagnostics(filePath) {
34469
- const absPath = resolve2(filePath);
35173
+ const absPath = resolve3(filePath);
34470
35174
  const uri = pathToFileURL(absPath).href;
34471
35175
  await this.openFile(absPath);
34472
- await new Promise((r) => setTimeout(r, 500));
34473
- try {
34474
- const result = await this.connection?.sendRequest("textDocument/diagnostic", {
34475
- textDocument: { uri }
34476
- });
34477
- if (result && typeof result === "object" && "items" in result) {
34478
- return result;
35176
+ await new Promise((r) => setTimeout(r, LSP_TIMEOUTS.diagnosticSettleDelay));
35177
+ log("[lsp] diagnostics mode selected", {
35178
+ server: this.server.id,
35179
+ filePath: absPath,
35180
+ activeMode: this.supportsPullDiagnostics ? "pull" : "push",
35181
+ ...getDiagnosticsCapabilitySummary({
35182
+ diagnosticProvider: this.diagnosticProvider,
35183
+ publishDiagnosticsObserved: this.publishDiagnosticsObserved,
35184
+ workspaceConfigurationRequested: this.workspaceConfigurationRequested
35185
+ })
35186
+ });
35187
+ if (this.supportsPullDiagnostics) {
35188
+ try {
35189
+ const result = this.connection ? await withTimeout(this.connection.sendRequest("textDocument/diagnostic", {
35190
+ textDocument: { uri },
35191
+ previousResultId: this.diagnosticResultIds.get(uri)
35192
+ }), LSP_TIMEOUTS.request, `LSP diagnostics (${this.server.id})`) : undefined;
35193
+ const report = result;
35194
+ if (report?.kind === "full") {
35195
+ if (report.resultId) {
35196
+ this.diagnosticResultIds.set(uri, report.resultId);
35197
+ } else {
35198
+ this.diagnosticResultIds.delete(uri);
35199
+ }
35200
+ this.diagnosticsStore.set(uri, report.items);
35201
+ return { items: report.items };
35202
+ }
35203
+ if (report?.kind === "unchanged") {
35204
+ if (report.resultId) {
35205
+ this.diagnosticResultIds.set(uri, report.resultId);
35206
+ }
35207
+ return { items: this.diagnosticsStore.get(uri) ?? [] };
35208
+ }
35209
+ if (result && typeof result === "object" && "items" in result) {
35210
+ const legacyResult = result;
35211
+ this.diagnosticsStore.set(uri, legacyResult.items);
35212
+ return legacyResult;
35213
+ }
35214
+ } catch (error92) {
35215
+ log("[lsp] diagnostics: falling back to cached publishDiagnostics", {
35216
+ server: this.server.id,
35217
+ error: String(error92)
35218
+ });
34479
35219
  }
34480
- } catch {}
34481
- return { items: this.diagnosticsStore.get(uri) ?? [] };
35220
+ }
35221
+ const cachedDiagnostics = await this.waitForPublishedDiagnostics(uri);
35222
+ if (cachedDiagnostics) {
35223
+ return { items: cachedDiagnostics };
35224
+ }
35225
+ throw new Error(`Unable to retrieve diagnostics from ${this.server.id}: request timed out or is unsupported.`);
34482
35226
  }
34483
35227
  async rename(filePath, line, character, newName) {
34484
- const absPath = resolve2(filePath);
35228
+ const absPath = resolve3(filePath);
34485
35229
  await this.openFile(absPath);
34486
- return this.connection?.sendRequest("textDocument/rename", {
35230
+ return this.connection ? withTimeout(this.connection.sendRequest("textDocument/rename", {
34487
35231
  textDocument: { uri: pathToFileURL(absPath).href },
34488
35232
  position: { line: line - 1, character },
34489
35233
  newName
34490
- });
35234
+ }), LSP_TIMEOUTS.request, `LSP rename (${this.server.id})`) : undefined;
34491
35235
  }
34492
35236
  isAlive() {
34493
35237
  return this.proc !== null && !this.processExited && this.proc.exitCode === null;
@@ -34496,7 +35240,7 @@ stderr: ${stderr}` : ""));
34496
35240
  log("[lsp] LSPClient.stop: stopping", { server: this.server.id });
34497
35241
  try {
34498
35242
  if (this.connection) {
34499
- await this.connection.sendRequest("shutdown");
35243
+ await withTimeout(this.connection.sendRequest("shutdown"), 1000, `LSP shutdown (${this.server.id})`);
34500
35244
  this.connection.sendNotification("exit");
34501
35245
  this.connection.dispose();
34502
35246
  }
@@ -34505,7 +35249,13 @@ stderr: ${stderr}` : ""));
34505
35249
  this.proc = null;
34506
35250
  this.connection = null;
34507
35251
  this.processExited = true;
35252
+ this.diagnosticProvider = null;
35253
+ this.publishDiagnosticsObserved = false;
35254
+ this.supportsPullDiagnostics = false;
35255
+ this.workspaceConfigurationRequested = false;
34508
35256
  this.diagnosticsStore.clear();
35257
+ this.diagnosticResultIds.clear();
35258
+ this.documents.clear();
34509
35259
  log("[lsp] LSPClient.stop: complete", { server: this.server.id });
34510
35260
  }
34511
35261
  }
@@ -34517,13 +35267,13 @@ import {
34517
35267
  unlinkSync as unlinkSync2,
34518
35268
  writeFileSync as writeFileSync3
34519
35269
  } from "fs";
34520
- import { dirname as dirname5, extname as extname2, join as join10, resolve as resolve3 } from "path";
35270
+ import { dirname as dirname7, extname as extname2, join as join10, resolve as resolve4 } from "path";
34521
35271
  import { fileURLToPath as fileURLToPath2 } from "url";
34522
35272
  function findServerProjectRoot(filePath, server) {
34523
35273
  if (server.root) {
34524
- return server.root(filePath) ?? dirname5(resolve3(filePath));
35274
+ return server.root(filePath) ?? dirname7(resolve4(filePath));
34525
35275
  }
34526
- return dirname5(resolve3(filePath));
35276
+ return dirname7(resolve4(filePath));
34527
35277
  }
34528
35278
  function uriToPath(uri) {
34529
35279
  return fileURLToPath2(uri);
@@ -34542,9 +35292,9 @@ function formatServerLookupError(result) {
34542
35292
  return `No LSP server configured for extension: ${result.extension}`;
34543
35293
  }
34544
35294
  async function withLspClient(filePath, fn) {
34545
- const absPath = resolve3(filePath);
35295
+ const absPath = resolve4(filePath);
34546
35296
  const ext = extname2(absPath);
34547
- const result = findServerForExtension(ext);
35297
+ const result = findServerForExtension(ext, absPath);
34548
35298
  if (result.status !== "found") {
34549
35299
  log("[lsp] withLspClient: server not found", {
34550
35300
  filePath: absPath,
@@ -34553,7 +35303,14 @@ async function withLspClient(filePath, fn) {
34553
35303
  throw new Error(formatServerLookupError(result));
34554
35304
  }
34555
35305
  const server = result.server;
34556
- const root = findServerProjectRoot(absPath, server) ?? dirname5(absPath);
35306
+ const root = findServerProjectRoot(absPath, server) ?? dirname7(absPath);
35307
+ log("[lsp] withLspClient: selected server", {
35308
+ filePath: absPath,
35309
+ extension: ext,
35310
+ server: server.id,
35311
+ command: server.command.join(" "),
35312
+ root
35313
+ });
34557
35314
  log("[lsp] withLspClient: acquiring client", {
34558
35315
  filePath: absPath,
34559
35316
  server: server.id,
@@ -34913,29 +35670,32 @@ var OhMyOpenCodeLite = async (ctx) => {
34913
35670
  runtimeChains[agentName] = existing;
34914
35671
  }
34915
35672
  }
34916
- const tmuxConfig = {
34917
- enabled: config3.tmux?.enabled ?? false,
34918
- layout: config3.tmux?.layout ?? "main-vertical",
34919
- main_pane_size: config3.tmux?.main_pane_size ?? 60
35673
+ const multiplexerConfig = {
35674
+ type: config3.multiplexer?.type ?? "none",
35675
+ layout: config3.multiplexer?.layout ?? "main-vertical",
35676
+ main_pane_size: config3.multiplexer?.main_pane_size ?? 60
34920
35677
  };
34921
- log("[plugin] initialized with tmux config", {
34922
- tmuxConfig,
34923
- rawTmuxConfig: config3.tmux,
35678
+ const multiplexer = getMultiplexer(multiplexerConfig);
35679
+ const multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
35680
+ log("[plugin] initialized with multiplexer config", {
35681
+ multiplexerConfig,
35682
+ enabled: multiplexerEnabled,
34924
35683
  directory: ctx.directory
34925
35684
  });
34926
- if (tmuxConfig.enabled) {
34927
- startTmuxCheck();
35685
+ if (multiplexerEnabled) {
35686
+ startAvailabilityCheck(multiplexerConfig);
34928
35687
  }
34929
- const backgroundManager = new BackgroundTaskManager(ctx, tmuxConfig, config3);
34930
- const backgroundTools = createBackgroundTools(ctx, backgroundManager, tmuxConfig, config3);
34931
- const councilTools = config3.council ? createCouncilTool(ctx, new CouncilManager(ctx, config3, backgroundManager.getDepthTracker(), tmuxConfig.enabled)) : {};
34932
- const mcps = createBuiltinMcps(config3.disabled_mcps);
34933
- const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
35688
+ const backgroundManager = new BackgroundTaskManager(ctx, multiplexerConfig, config3);
35689
+ const backgroundTools = createBackgroundTools(ctx, backgroundManager, multiplexerConfig, config3);
35690
+ const councilTools = config3.council ? createCouncilTool(ctx, new CouncilManager(ctx, config3, backgroundManager.getDepthTracker(), multiplexerEnabled)) : {};
35691
+ const mcps = createBuiltinMcps(config3.disabled_mcps, config3.websearch);
35692
+ const multiplexerSessionManager = new MultiplexerSessionManager(ctx, multiplexerConfig);
34934
35693
  const autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
34935
35694
  showStartupToast: true,
34936
35695
  autoUpdate: true
34937
35696
  });
34938
35697
  const phaseReminderHook = createPhaseReminderHook();
35698
+ const filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config3);
34939
35699
  const postFileToolNudgeHook = createPostFileToolNudgeHook();
34940
35700
  const chatHeadersHook = createChatHeadersHook(ctx);
34941
35701
  const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
@@ -35026,7 +35786,8 @@ var OhMyOpenCodeLite = async (ctx) => {
35026
35786
  } else {
35027
35787
  Object.assign(configMcp, mcps);
35028
35788
  }
35029
- const allMcpNames = Object.keys(mcps);
35789
+ const mergedMcpConfig = opencodeConfig.mcp;
35790
+ const allMcpNames = Object.keys(mergedMcpConfig ?? mcps);
35030
35791
  for (const [agentName, agentConfig] of Object.entries(agents)) {
35031
35792
  const agentMcps = agentConfig?.mcps;
35032
35793
  if (!agentMcps)
@@ -35051,14 +35812,18 @@ var OhMyOpenCodeLite = async (ctx) => {
35051
35812
  event: async (input) => {
35052
35813
  await foregroundFallback.handleEvent(input.event);
35053
35814
  await autoUpdateChecker.event(input);
35054
- await tmuxSessionManager.onSessionCreated(input.event);
35815
+ await multiplexerSessionManager.onSessionCreated(input.event);
35055
35816
  await backgroundManager.handleSessionStatus(input.event);
35056
- await tmuxSessionManager.onSessionStatus(input.event);
35817
+ await multiplexerSessionManager.onSessionStatus(input.event);
35057
35818
  await backgroundManager.handleSessionDeleted(input.event);
35058
- await tmuxSessionManager.onSessionDeleted(input.event);
35819
+ await multiplexerSessionManager.onSessionDeleted(input.event);
35059
35820
  },
35060
35821
  "chat.headers": chatHeadersHook["chat.headers"],
35061
- "experimental.chat.messages.transform": phaseReminderHook["experimental.chat.messages.transform"],
35822
+ "experimental.chat.messages.transform": async (input, output) => {
35823
+ const typedOutput = output;
35824
+ await phaseReminderHook["experimental.chat.messages.transform"](input, typedOutput);
35825
+ await filterAvailableSkillsHook["experimental.chat.messages.transform"](input, typedOutput);
35826
+ },
35062
35827
  "tool.execute.after": async (input, output) => {
35063
35828
  await delegateTaskRetryHook["tool.execute.after"](input, output);
35064
35829
  await jsonErrorRecoveryHook["tool.execute.after"](input, output);