oh-my-opencode-slim 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js CHANGED
@@ -75,17 +75,592 @@ var TMUX_SPAWN_DELAY_MS = 500;
75
75
  var COUNCILLOR_STAGGER_MS = 250;
76
76
  var DEFAULT_DISABLED_AGENTS = ["observer"];
77
77
 
78
- // src/tui-state.ts
78
+ // src/config/loader.ts
79
79
  import * as fs from "node:fs";
80
- import * as os from "node:os";
81
80
  import * as path from "node:path";
81
+
82
+ // src/cli/paths.ts
83
+ import { homedir } from "node:os";
84
+ import { dirname, join } from "node:path";
85
+ function getDefaultOpenCodeConfigDir() {
86
+ const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
87
+ return join(userConfigDir, "opencode");
88
+ }
89
+ function getCustomOpenCodeConfigDir() {
90
+ const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
91
+ return configDir || undefined;
92
+ }
93
+ function getConfigSearchDirs() {
94
+ const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
95
+ return dirs.filter((dir, index) => {
96
+ return Boolean(dir) && dirs.indexOf(dir) === index;
97
+ });
98
+ }
99
+ function getOpenCodeConfigPaths() {
100
+ const configDir = getDefaultOpenCodeConfigDir();
101
+ return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
102
+ }
103
+ // src/config/council-schema.ts
104
+ import { z } from "zod";
105
+ var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.4-mini")');
106
+ var CouncillorConfigSchema = z.object({
107
+ model: ModelIdSchema.describe('Model ID in provider/model format (e.g. "openai/gpt-5.4-mini")'),
108
+ variant: z.string().optional(),
109
+ prompt: z.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
110
+ });
111
+ var CouncilPresetSchema = z.record(z.string(), z.record(z.string(), z.unknown())).transform((entries, ctx) => {
112
+ const councillors = {};
113
+ for (const [key, raw] of Object.entries(entries)) {
114
+ if (key === "master")
115
+ continue;
116
+ if (key === "councillors" && typeof raw === "object" && raw !== null) {
117
+ for (const [innerKey, innerRaw] of Object.entries(raw)) {
118
+ const innerParsed = CouncillorConfigSchema.safeParse(innerRaw);
119
+ if (!innerParsed.success) {
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ message: `Invalid councillor "${innerKey}" (nested under legacy "councillors" key): ${innerParsed.error.issues.map((i) => i.message).join(", ")}`
123
+ });
124
+ return z.NEVER;
125
+ }
126
+ councillors[innerKey] = innerParsed.data;
127
+ }
128
+ continue;
129
+ }
130
+ const parsed = CouncillorConfigSchema.safeParse(raw);
131
+ if (!parsed.success) {
132
+ ctx.addIssue({
133
+ code: z.ZodIssueCode.custom,
134
+ message: `Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(", ")}`
135
+ });
136
+ return z.NEVER;
137
+ }
138
+ councillors[key] = parsed.data;
139
+ }
140
+ return councillors;
141
+ });
142
+ var CouncillorExecutionModeSchema = z.enum(["parallel", "serial"]).default("parallel").describe('Execution mode for councillors. Use "serial" for single-model systems to avoid conflicts. ' + 'Use "parallel" for multi-model systems for faster execution.');
143
+ var CouncilConfigSchema = z.object({
144
+ presets: z.record(z.string(), CouncilPresetSchema),
145
+ timeout: z.number().min(0).default(180000),
146
+ default_preset: z.string().default("default"),
147
+ 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).'),
148
+ councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
149
+ master: z.unknown().optional().describe("DEPRECATED — ignored. Council agent synthesizes directly."),
150
+ master_timeout: z.unknown().optional().describe('DEPRECATED — ignored. Use "timeout" instead.'),
151
+ master_fallback: z.unknown().optional().describe("DEPRECATED — ignored. No separate master session.")
152
+ }).transform((data) => {
153
+ const deprecated = [];
154
+ if (data.master !== undefined)
155
+ deprecated.push("master");
156
+ if (data.master_timeout !== undefined)
157
+ deprecated.push("master_timeout");
158
+ if (data.master_fallback !== undefined)
159
+ deprecated.push("master_fallback");
160
+ const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
161
+ return {
162
+ presets: data.presets,
163
+ timeout: data.timeout,
164
+ default_preset: data.default_preset,
165
+ councillor_execution_mode: data.councillor_execution_mode,
166
+ councillor_retries: data.councillor_retries,
167
+ _deprecated: deprecated.length > 0 ? deprecated : undefined,
168
+ _legacyMasterModel: legacyMasterModel
169
+ };
170
+ });
171
+ // src/config/schema.ts
172
+ import { z as z2 } from "zod";
173
+ var ProviderModelIdSchema = z2.string().regex(/^[^/\s]+\/[^\s]+$/, "Expected provider/model format (provider/.../model)");
174
+ var ManualAgentPlanSchema = z2.object({
175
+ primary: ProviderModelIdSchema,
176
+ fallback1: ProviderModelIdSchema,
177
+ fallback2: ProviderModelIdSchema,
178
+ fallback3: ProviderModelIdSchema
179
+ }).superRefine((value, ctx) => {
180
+ const unique = new Set([
181
+ value.primary,
182
+ value.fallback1,
183
+ value.fallback2,
184
+ value.fallback3
185
+ ]);
186
+ if (unique.size !== 4) {
187
+ ctx.addIssue({
188
+ code: z2.ZodIssueCode.custom,
189
+ message: "primary and fallbacks must be unique per agent"
190
+ });
191
+ }
192
+ });
193
+ var ManualPlanSchema = z2.object({
194
+ orchestrator: ManualAgentPlanSchema,
195
+ oracle: ManualAgentPlanSchema,
196
+ designer: ManualAgentPlanSchema,
197
+ explorer: ManualAgentPlanSchema,
198
+ librarian: ManualAgentPlanSchema,
199
+ fixer: ManualAgentPlanSchema
200
+ }).strict();
201
+ var AgentModelChainSchema = z2.array(z2.string()).min(1);
202
+ var FallbackChainsSchema = z2.object({
203
+ orchestrator: AgentModelChainSchema.optional(),
204
+ oracle: AgentModelChainSchema.optional(),
205
+ designer: AgentModelChainSchema.optional(),
206
+ explorer: AgentModelChainSchema.optional(),
207
+ librarian: AgentModelChainSchema.optional(),
208
+ fixer: AgentModelChainSchema.optional()
209
+ }).catchall(AgentModelChainSchema);
210
+ var AgentOverrideConfigSchema = z2.object({
211
+ model: z2.union([
212
+ z2.string(),
213
+ z2.array(z2.union([
214
+ z2.string(),
215
+ z2.object({
216
+ id: z2.string(),
217
+ variant: z2.string().optional()
218
+ })
219
+ ])).min(1)
220
+ ]).optional(),
221
+ temperature: z2.number().min(0).max(2).optional(),
222
+ variant: z2.string().optional().catch(undefined),
223
+ skills: z2.array(z2.string()).optional(),
224
+ mcps: z2.array(z2.string()).optional(),
225
+ prompt: z2.string().min(1).optional(),
226
+ orchestratorPrompt: z2.string().min(1).optional(),
227
+ options: z2.record(z2.string(), z2.unknown()).optional(),
228
+ displayName: z2.string().min(1).optional()
229
+ }).strict();
230
+ var MultiplexerTypeSchema = z2.enum(["auto", "tmux", "zellij", "none"]);
231
+ var MultiplexerLayoutSchema = z2.enum([
232
+ "main-horizontal",
233
+ "main-vertical",
234
+ "tiled",
235
+ "even-horizontal",
236
+ "even-vertical"
237
+ ]);
238
+ var TmuxLayoutSchema = MultiplexerLayoutSchema;
239
+ var MultiplexerConfigSchema = z2.object({
240
+ type: MultiplexerTypeSchema.default("none"),
241
+ layout: MultiplexerLayoutSchema.default("main-vertical"),
242
+ main_pane_size: z2.number().min(20).max(80).default(60)
243
+ });
244
+ var TmuxConfigSchema = z2.object({
245
+ enabled: z2.boolean().default(false),
246
+ layout: TmuxLayoutSchema.default("main-vertical"),
247
+ main_pane_size: z2.number().min(20).max(80).default(60)
248
+ });
249
+ var PresetSchema = z2.record(z2.string(), AgentOverrideConfigSchema);
250
+ var WebsearchConfigSchema = z2.object({
251
+ provider: z2.enum(["exa", "tavily"]).default("exa")
252
+ });
253
+ var McpNameSchema = z2.enum(["websearch", "context7", "grep_app"]);
254
+ var InterviewConfigSchema = z2.object({
255
+ maxQuestions: z2.number().int().min(1).max(10).default(2),
256
+ outputFolder: z2.string().min(1).default("interview"),
257
+ autoOpenBrowser: z2.boolean().default(true).describe("Automatically open the interview UI in your default browser during interactive runs. Disabled automatically in tests and CI."),
258
+ port: z2.number().int().min(0).max(65535).default(0),
259
+ dashboard: z2.boolean().default(false)
260
+ });
261
+ var SessionManagerConfigSchema = z2.object({
262
+ maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
263
+ readContextMinLines: z2.number().int().min(0).max(1000).default(10),
264
+ readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
265
+ });
266
+ var DivoomConfigSchema = z2.object({
267
+ enabled: z2.boolean().default(false),
268
+ python: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python"),
269
+ script: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py"),
270
+ size: z2.number().int().min(1).max(1024).default(128),
271
+ fps: z2.number().int().min(1).max(60).default(8),
272
+ speed: z2.number().int().min(1).max(1e4).default(125),
273
+ maxFrames: z2.number().int().min(1).max(500).default(24),
274
+ posterizeBits: z2.number().int().min(1).max(8).default(3),
275
+ gifs: z2.record(z2.string(), z2.string().min(1)).optional()
276
+ });
277
+ var TodoContinuationConfigSchema = z2.object({
278
+ maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
279
+ cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
280
+ autoEnable: z2.boolean().default(false).describe("Automatically enable auto-continue when the orchestrator session has enough todos"),
281
+ autoEnableThreshold: z2.number().int().min(1).max(50).default(4).describe("Number of todos that triggers auto-enable (only used when autoEnable is true)")
282
+ });
283
+ var FailoverConfigSchema = z2.object({
284
+ enabled: z2.boolean().default(true),
285
+ timeoutMs: z2.number().min(0).default(15000),
286
+ retryDelayMs: z2.number().min(0).default(500),
287
+ chains: FallbackChainsSchema.default({}),
288
+ retry_on_empty: z2.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.")
289
+ });
290
+ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
291
+ for (const [name, override] of Object.entries(overrides)) {
292
+ const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
293
+ if (!isBuiltInOrAlias) {
294
+ continue;
295
+ }
296
+ if (override.prompt !== undefined) {
297
+ ctx.addIssue({
298
+ code: z2.ZodIssueCode.custom,
299
+ path: [...pathPrefix, name, "prompt"],
300
+ message: "prompt is only supported for custom agents"
301
+ });
302
+ }
303
+ if (override.orchestratorPrompt !== undefined) {
304
+ ctx.addIssue({
305
+ code: z2.ZodIssueCode.custom,
306
+ path: [...pathPrefix, name, "orchestratorPrompt"],
307
+ message: "orchestratorPrompt is only supported for custom agents"
308
+ });
309
+ }
310
+ }
311
+ }
312
+ var PluginConfigSchema = z2.object({
313
+ preset: z2.string().optional(),
314
+ setDefaultAgent: z2.boolean().optional(),
315
+ scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
316
+ balanceProviderUsage: z2.boolean().optional(),
317
+ autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
318
+ manualPlan: ManualPlanSchema.optional(),
319
+ presets: z2.record(z2.string(), PresetSchema).optional(),
320
+ agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
321
+ disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
322
+ disabled_mcps: z2.array(z2.string()).optional(),
323
+ multiplexer: MultiplexerConfigSchema.optional(),
324
+ tmux: TmuxConfigSchema.optional(),
325
+ websearch: WebsearchConfigSchema.optional(),
326
+ interview: InterviewConfigSchema.optional(),
327
+ sessionManager: SessionManagerConfigSchema.optional(),
328
+ divoom: DivoomConfigSchema.optional(),
329
+ todoContinuation: TodoContinuationConfigSchema.optional(),
330
+ fallback: FailoverConfigSchema.optional(),
331
+ council: CouncilConfigSchema.optional()
332
+ }).superRefine((value, ctx) => {
333
+ if (value.agents) {
334
+ validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
335
+ }
336
+ if (value.presets) {
337
+ for (const [presetName, preset] of Object.entries(value.presets)) {
338
+ validateCustomOnlyPromptFields(preset, ctx, ["presets", presetName]);
339
+ }
340
+ }
341
+ });
342
+ // src/config/utils.ts
343
+ function getAgentOverride(config, name) {
344
+ const overrides = config?.agents ?? {};
345
+ return overrides[name] ?? overrides[Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === name) ?? ""];
346
+ }
347
+ function getCustomAgentNames(config) {
348
+ const overrides = config?.agents ?? {};
349
+ return Object.keys(overrides).filter((name) => {
350
+ if (AGENT_ALIASES[name] !== undefined) {
351
+ return false;
352
+ }
353
+ return !ALL_AGENT_NAMES.includes(name);
354
+ });
355
+ }
356
+ // src/config/agent-mcps.ts
357
+ var DEFAULT_AGENT_MCPS = {
358
+ orchestrator: ["*", "!context7"],
359
+ designer: [],
360
+ oracle: [],
361
+ librarian: ["websearch", "context7", "grep_app"],
362
+ explorer: [],
363
+ fixer: [],
364
+ observer: [],
365
+ council: [],
366
+ councillor: []
367
+ };
368
+ function parseList(items, allAvailable) {
369
+ if (!items || items.length === 0) {
370
+ return [];
371
+ }
372
+ const allow = items.filter((i) => !i.startsWith("!"));
373
+ const deny = items.filter((i) => i.startsWith("!")).map((i) => i.slice(1));
374
+ if (deny.includes("*")) {
375
+ return [];
376
+ }
377
+ if (allow.includes("*")) {
378
+ return allAvailable.filter((item) => !deny.includes(item));
379
+ }
380
+ return allow.filter((item) => !deny.includes(item) && allAvailable.includes(item));
381
+ }
382
+ function getAgentMcpList(agentName, config) {
383
+ const agentConfig = getAgentOverride(config, agentName);
384
+ if (agentConfig?.mcps !== undefined) {
385
+ return agentConfig.mcps;
386
+ }
387
+ const defaultMcps = DEFAULT_AGENT_MCPS[agentName];
388
+ return defaultMcps ?? [];
389
+ }
390
+
391
+ // src/cli/custom-skills.ts
392
+ var CUSTOM_SKILLS = [
393
+ {
394
+ name: "simplify",
395
+ description: "Code simplification and readability-focused refactoring",
396
+ allowedAgents: ["oracle"],
397
+ sourcePath: "src/skills/simplify"
398
+ },
399
+ {
400
+ name: "codemap",
401
+ description: "Repository understanding and hierarchical codemap generation",
402
+ allowedAgents: ["orchestrator"],
403
+ sourcePath: "src/skills/codemap"
404
+ }
405
+ ];
406
+
407
+ // src/cli/skills.ts
408
+ var RECOMMENDED_SKILLS = [
409
+ {
410
+ name: "agent-browser",
411
+ repo: "https://github.com/vercel-labs/agent-browser",
412
+ skillName: "agent-browser",
413
+ allowedAgents: ["designer"],
414
+ description: "High-performance browser automation",
415
+ postInstallCommands: [
416
+ "npm install -g agent-browser",
417
+ "agent-browser install"
418
+ ]
419
+ }
420
+ ];
421
+ var PERMISSION_ONLY_SKILLS = [
422
+ {
423
+ name: "requesting-code-review",
424
+ allowedAgents: ["oracle"],
425
+ description: "Code review template for reviewer subagents in multi-step workflows"
426
+ }
427
+ ];
428
+ function getSkillPermissionsForAgent(agentName, skillList) {
429
+ const permissions = {
430
+ "*": agentName === "orchestrator" ? "allow" : "deny"
431
+ };
432
+ if (skillList) {
433
+ permissions["*"] = "deny";
434
+ for (const name of skillList) {
435
+ if (name === "*") {
436
+ permissions["*"] = "allow";
437
+ } else if (name.startsWith("!")) {
438
+ permissions[name.slice(1)] = "deny";
439
+ } else {
440
+ permissions[name] = "allow";
441
+ }
442
+ }
443
+ return permissions;
444
+ }
445
+ for (const skill of RECOMMENDED_SKILLS) {
446
+ const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
447
+ if (isAllowed) {
448
+ permissions[skill.skillName] = "allow";
449
+ }
450
+ }
451
+ for (const skill of CUSTOM_SKILLS) {
452
+ const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
453
+ if (isAllowed) {
454
+ permissions[skill.name] = "allow";
455
+ }
456
+ }
457
+ for (const skill of PERMISSION_ONLY_SKILLS) {
458
+ const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
459
+ if (isAllowed) {
460
+ permissions[skill.name] = "allow";
461
+ }
462
+ }
463
+ return permissions;
464
+ }
465
+
466
+ // src/cli/config-io.ts
467
+ function stripJsonComments(json) {
468
+ const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
469
+ const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
470
+ return json.replace(commentPattern, (match, commentGroup) => commentGroup ? "" : match).replace(trailingCommaPattern, (match, comma, closing) => comma ? closing : match);
471
+ }
472
+
473
+ // src/config/loader.ts
474
+ var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
475
+ function loadConfigFromPath(configPath, options) {
476
+ try {
477
+ const content = fs.readFileSync(configPath, "utf-8");
478
+ let rawConfig;
479
+ try {
480
+ rawConfig = JSON.parse(stripJsonComments(content));
481
+ } catch (error) {
482
+ const message = error instanceof Error ? error.message : String(error);
483
+ options?.onWarning?.({
484
+ path: configPath,
485
+ kind: "invalid-json",
486
+ message
487
+ });
488
+ if (!options?.silent) {
489
+ console.warn(`[oh-my-opencode-slim] Invalid JSON in ${configPath}:`, message);
490
+ }
491
+ return null;
492
+ }
493
+ const result = PluginConfigSchema.safeParse(rawConfig);
494
+ if (!result.success) {
495
+ options?.onWarning?.({
496
+ path: configPath,
497
+ kind: "invalid-schema",
498
+ message: "Config does not match schema",
499
+ formatted: result.error.format()
500
+ });
501
+ if (!options?.silent) {
502
+ console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
503
+ console.warn(result.error.format());
504
+ }
505
+ return null;
506
+ }
507
+ return result.data;
508
+ } catch (error) {
509
+ if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
510
+ options?.onWarning?.({
511
+ path: configPath,
512
+ kind: "read-error",
513
+ message: error.message
514
+ });
515
+ if (!options?.silent) {
516
+ console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
517
+ }
518
+ }
519
+ return null;
520
+ }
521
+ }
522
+ function findConfigPath(basePath) {
523
+ const jsoncPath = `${basePath}.jsonc`;
524
+ const jsonPath = `${basePath}.json`;
525
+ if (fs.existsSync(jsoncPath)) {
526
+ return jsoncPath;
527
+ }
528
+ if (fs.existsSync(jsonPath)) {
529
+ return jsonPath;
530
+ }
531
+ return null;
532
+ }
533
+ function findConfigPathInDirs(configDirs, baseName) {
534
+ for (const configDir of configDirs) {
535
+ const configPath = findConfigPath(path.join(configDir, baseName));
536
+ if (configPath) {
537
+ return configPath;
538
+ }
539
+ }
540
+ return null;
541
+ }
542
+ function findPluginConfigPaths(directory) {
543
+ const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
544
+ const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
545
+ const projectConfigPath = findConfigPath(projectConfigBasePath);
546
+ return { userConfigPath, projectConfigPath };
547
+ }
548
+ function mergePluginConfigs(base, override) {
549
+ return {
550
+ ...base,
551
+ ...override,
552
+ agents: deepMerge(base.agents, override.agents),
553
+ tmux: deepMerge(base.tmux, override.tmux),
554
+ multiplexer: deepMerge(base.multiplexer, override.multiplexer),
555
+ interview: deepMerge(base.interview, override.interview),
556
+ sessionManager: deepMerge(base.sessionManager, override.sessionManager),
557
+ divoom: deepMerge(base.divoom, override.divoom),
558
+ fallback: deepMerge(base.fallback, override.fallback),
559
+ council: deepMerge(base.council, override.council)
560
+ };
561
+ }
562
+ function deepMerge(base, override) {
563
+ if (!base)
564
+ return override;
565
+ if (!override)
566
+ return base;
567
+ const result = { ...base };
568
+ for (const key of Object.keys(override)) {
569
+ const baseVal = base[key];
570
+ const overrideVal = override[key];
571
+ if (typeof baseVal === "object" && baseVal !== null && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(baseVal) && !Array.isArray(overrideVal)) {
572
+ result[key] = deepMerge(baseVal, overrideVal);
573
+ } else {
574
+ result[key] = overrideVal;
575
+ }
576
+ }
577
+ return result;
578
+ }
579
+ function loadPluginConfig(directory, options) {
580
+ const { userConfigPath, projectConfigPath } = findPluginConfigPaths(directory);
581
+ let config = userConfigPath ? loadConfigFromPath(userConfigPath, options) ?? {} : {};
582
+ const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, options) : null;
583
+ if (projectConfig) {
584
+ config = mergePluginConfigs(config, projectConfig);
585
+ }
586
+ config = migrateTmuxToMultiplexer(config);
587
+ const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
588
+ if (envPreset) {
589
+ config.preset = envPreset;
590
+ }
591
+ if (config.preset) {
592
+ const preset = config.presets?.[config.preset];
593
+ if (preset) {
594
+ config.agents = deepMerge(preset, config.agents);
595
+ } else {
596
+ const presetSource = envPreset === config.preset ? "environment variable" : "config file";
597
+ const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
598
+ const message = `Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`;
599
+ options?.onWarning?.({
600
+ path: projectConfigPath ?? userConfigPath ?? "",
601
+ kind: "missing-preset",
602
+ message
603
+ });
604
+ if (!options?.silent) {
605
+ console.warn(`[oh-my-opencode-slim] ${message}`);
606
+ }
607
+ }
608
+ }
609
+ return config;
610
+ }
611
+ function loadAgentPrompt(agentName, preset) {
612
+ const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
613
+ const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
614
+ const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
615
+ return presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
616
+ });
617
+ const result = {};
618
+ const readFirstPrompt = (fileName, errorPrefix) => {
619
+ for (const dir of promptSearchDirs) {
620
+ const promptPath = path.join(dir, fileName);
621
+ if (!fs.existsSync(promptPath)) {
622
+ continue;
623
+ }
624
+ try {
625
+ return fs.readFileSync(promptPath, "utf-8");
626
+ } catch (error) {
627
+ console.warn(`[oh-my-opencode-slim] ${errorPrefix} ${promptPath}:`, error instanceof Error ? error.message : String(error));
628
+ }
629
+ }
630
+ return;
631
+ };
632
+ result.prompt = readFirstPrompt(`${agentName}.md`, "Error reading prompt file");
633
+ result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
634
+ return result;
635
+ }
636
+ function migrateTmuxToMultiplexer(config) {
637
+ if (config.multiplexer?.type && config.multiplexer.type !== "none") {
638
+ return config;
639
+ }
640
+ if (config.tmux?.enabled) {
641
+ return {
642
+ ...config,
643
+ multiplexer: {
644
+ type: "tmux",
645
+ layout: config.tmux.layout ?? "main-vertical",
646
+ main_pane_size: config.tmux.main_pane_size ?? 60
647
+ }
648
+ };
649
+ }
650
+ return config;
651
+ }
652
+
653
+ // src/tui-state.ts
654
+ import * as fs2 from "node:fs";
655
+ import * as os from "node:os";
656
+ import * as path2 from "node:path";
82
657
  var STATE_DIR = "oh-my-opencode-slim";
83
658
  var STATE_FILE = "tui-state.json";
84
659
  function dataDir() {
85
- return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");
660
+ return process.env.XDG_DATA_HOME ?? path2.join(os.homedir(), ".local", "share");
86
661
  }
87
662
  function getTuiStatePath() {
88
- return path.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
663
+ return path2.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
89
664
  }
90
665
  function emptySnapshot() {
91
666
  return {
@@ -106,14 +681,14 @@ function parseSnapshot(value) {
106
681
  }
107
682
  function readTuiSnapshot() {
108
683
  try {
109
- return parseSnapshot(fs.readFileSync(getTuiStatePath(), "utf8"));
684
+ return parseSnapshot(fs2.readFileSync(getTuiStatePath(), "utf8"));
110
685
  } catch {
111
686
  return emptySnapshot();
112
687
  }
113
688
  }
114
689
  async function readTuiSnapshotAsync() {
115
690
  try {
116
- return parseSnapshot(await fs.promises.readFile(getTuiStatePath(), "utf8"));
691
+ return parseSnapshot(await fs2.promises.readFile(getTuiStatePath(), "utf8"));
117
692
  } catch {
118
693
  return emptySnapshot();
119
694
  }
@@ -121,8 +696,8 @@ async function readTuiSnapshotAsync() {
121
696
  function writeTuiSnapshot(snapshot) {
122
697
  try {
123
698
  const filePath = getTuiStatePath();
124
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
125
- fs.writeFileSync(filePath, `${JSON.stringify(snapshot)}
699
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
700
+ fs2.writeFileSync(filePath, `${JSON.stringify(snapshot)}
126
701
  `);
127
702
  } catch {}
128
703
  }
@@ -145,6 +720,7 @@ function recordTuiAgentModel(input) {
145
720
 
146
721
  // src/tui.ts
147
722
  var PLUGIN_NAME = "oh-my-opencode-slim";
723
+ var CONFIG_WARNING_COLOR = "orange";
148
724
  var FALLBACK_SIDEBAR_AGENTS = SUBAGENT_NAMES.filter((agent) => agent !== "councillor" && agent !== "council" && !DEFAULT_DISABLED_AGENTS.includes(agent));
149
725
  var BORDER = { type: "single" };
150
726
  async function readPackageVersion() {
@@ -177,6 +753,9 @@ function box(props, children = []) {
177
753
  function truncate(value, max = 24) {
178
754
  return value.length > max ? `${value.slice(0, max - 1)}…` : value;
179
755
  }
756
+ function getTuiDirectory(api) {
757
+ return api.state?.path?.directory ?? process.cwd();
758
+ }
180
759
  function formatSidebarModelName(model) {
181
760
  const lastSlash = model.lastIndexOf("/");
182
761
  return lastSlash === -1 ? model : model.slice(lastSlash + 1);
@@ -191,7 +770,8 @@ function row(label, value, theme, valueColor) {
191
770
  text({ fg: valueColor ?? theme.text }, [value])
192
771
  ]);
193
772
  }
194
- function renderSidebar(snapshot, version, theme) {
773
+ function renderSidebar(snapshot, version, theme, configInvalid) {
774
+ const configStatusRow = buildConfigStatusRow(configInvalid, theme);
195
775
  return box({
196
776
  width: "100%",
197
777
  flexDirection: "column",
@@ -208,9 +788,10 @@ function renderSidebar(snapshot, version, theme) {
208
788
  justifyContent: "space-between",
209
789
  alignItems: "center"
210
790
  }, [
211
- box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.background }, ["omo-slim"])]),
791
+ box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.background }, ["OMO-Slim"])]),
212
792
  text({ fg: theme.textMuted }, [`v${version}`])
213
793
  ]),
794
+ configStatusRow,
214
795
  box({ width: "100%", marginTop: 1 }, [
215
796
  text({ fg: theme.text }, ["Agents"])
216
797
  ]),
@@ -220,14 +801,44 @@ function renderSidebar(snapshot, version, theme) {
220
801
  })
221
802
  ]);
222
803
  }
804
+ function buildConfigStatusRow(configInvalid, theme) {
805
+ if (!configInvalid)
806
+ return null;
807
+ return box({
808
+ width: "100%",
809
+ flexDirection: "column",
810
+ marginTop: 1,
811
+ marginBottom: 1
812
+ }, [
813
+ text({ fg: CONFIG_WARNING_COLOR }, ["Config invalid"]),
814
+ text({ fg: theme.textMuted }, ["Run doctor for details"])
815
+ ]);
816
+ }
817
+ function readConfigInvalid(directory) {
818
+ let configInvalid = false;
819
+ loadPluginConfig(directory, {
820
+ silent: true,
821
+ onWarning: () => {
822
+ configInvalid = true;
823
+ }
824
+ });
825
+ return configInvalid;
826
+ }
223
827
  var plugin = {
224
828
  id: `${PLUGIN_NAME}:tui`,
225
829
  tui: async (api, _options, meta) => {
226
830
  const version = meta.version ?? await readPackageVersion() ?? "dev";
831
+ let configDirectory = getTuiDirectory(api);
832
+ let configInvalid = readConfigInvalid(configDirectory);
227
833
  let snapshot = readTuiSnapshot();
228
834
  const renderTimer = setInterval(async () => {
229
835
  try {
230
836
  snapshot = await readTuiSnapshotAsync();
837
+ const currentDirectory = getTuiDirectory(api);
838
+ if (currentDirectory !== configDirectory) {
839
+ configDirectory = currentDirectory;
840
+ configInvalid = readConfigInvalid(configDirectory);
841
+ }
231
842
  api.renderer.requestRender();
232
843
  } catch {}
233
844
  }, 1000);
@@ -238,7 +849,7 @@ var plugin = {
238
849
  order: 900,
239
850
  slots: {
240
851
  sidebar_content() {
241
- return renderSidebar(snapshot, version, api.theme.current);
852
+ return renderSidebar(snapshot, version, api.theme.current, configInvalid);
242
853
  }
243
854
  }
244
855
  });
@@ -246,6 +857,7 @@ var plugin = {
246
857
  };
247
858
  var tui_default = plugin;
248
859
  export {
860
+ readConfigInvalid,
249
861
  getSidebarAgentNames,
250
862
  formatSidebarModelName,
251
863
  tui_default as default