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