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