oh-my-opencode-slim 1.0.5 → 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
@@ -34,8 +34,695 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
34
 
35
35
  // src/tui.ts
36
36
  import { createElement, insert, setProp } from "@opentui/solid";
37
+
38
+ // src/config/constants.ts
39
+ var AGENT_ALIASES = {
40
+ explore: "explorer",
41
+ "frontend-ui-ux-engineer": "designer"
42
+ };
43
+ var SUBAGENT_NAMES = [
44
+ "explorer",
45
+ "librarian",
46
+ "oracle",
47
+ "designer",
48
+ "fixer",
49
+ "observer",
50
+ "council",
51
+ "councillor"
52
+ ];
53
+ var ORCHESTRATOR_NAME = "orchestrator";
54
+ var ALL_AGENT_NAMES = [ORCHESTRATOR_NAME, ...SUBAGENT_NAMES];
55
+ var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
56
+ var DEFAULT_MODELS = {
57
+ orchestrator: undefined,
58
+ oracle: "openai/gpt-5.5",
59
+ librarian: "openai/gpt-5.4-mini",
60
+ explorer: "openai/gpt-5.4-mini",
61
+ designer: "openai/gpt-5.4-mini",
62
+ fixer: "openai/gpt-5.4-mini",
63
+ observer: "openai/gpt-5.4-mini",
64
+ council: "openai/gpt-5.4-mini",
65
+ councillor: "openai/gpt-5.4-mini"
66
+ };
67
+ var POLL_INTERVAL_BACKGROUND_MS = 2000;
68
+ var DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
69
+ var MAX_POLL_TIME_MS = 5 * 60 * 1000;
70
+ var DEFAULT_MAX_SUBAGENT_DEPTH = 3;
71
+ var PHASE_REMINDER_TEXT = `!IMPORTANT! Recall the workflow rules:
72
+ Understand → choose the best parallelized path based on your capabilities and agents delegation rules → recall session reuse rules → execute → verify.
73
+ If delegating, launch the specialist in the same turn you mention it !END!`;
74
+ var TMUX_SPAWN_DELAY_MS = 500;
75
+ var COUNCILLOR_STAGGER_MS = 250;
76
+ var DEFAULT_DISABLED_AGENTS = ["observer"];
77
+
78
+ // src/config/loader.ts
79
+ import * as fs from "node:fs";
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";
657
+ var STATE_DIR = "oh-my-opencode-slim";
658
+ var STATE_FILE = "tui-state.json";
659
+ function dataDir() {
660
+ return process.env.XDG_DATA_HOME ?? path2.join(os.homedir(), ".local", "share");
661
+ }
662
+ function getTuiStatePath() {
663
+ return path2.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
664
+ }
665
+ function emptySnapshot() {
666
+ return {
667
+ version: 1,
668
+ updatedAt: Date.now(),
669
+ agentModels: {}
670
+ };
671
+ }
672
+ function parseSnapshot(value) {
673
+ const parsed = JSON.parse(value);
674
+ if (parsed?.version !== 1)
675
+ return emptySnapshot();
676
+ return {
677
+ version: 1,
678
+ updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
679
+ agentModels: parsed.agentModels ?? {}
680
+ };
681
+ }
682
+ function readTuiSnapshot() {
683
+ try {
684
+ return parseSnapshot(fs2.readFileSync(getTuiStatePath(), "utf8"));
685
+ } catch {
686
+ return emptySnapshot();
687
+ }
688
+ }
689
+ async function readTuiSnapshotAsync() {
690
+ try {
691
+ return parseSnapshot(await fs2.promises.readFile(getTuiStatePath(), "utf8"));
692
+ } catch {
693
+ return emptySnapshot();
694
+ }
695
+ }
696
+ function writeTuiSnapshot(snapshot) {
697
+ try {
698
+ const filePath = getTuiStatePath();
699
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
700
+ fs2.writeFileSync(filePath, `${JSON.stringify(snapshot)}
701
+ `);
702
+ } catch {}
703
+ }
704
+ function updateSnapshot(mutator) {
705
+ const snapshot = readTuiSnapshot();
706
+ mutator(snapshot);
707
+ snapshot.updatedAt = Date.now();
708
+ writeTuiSnapshot(snapshot);
709
+ }
710
+ function recordTuiAgentModels(input) {
711
+ updateSnapshot((snapshot) => {
712
+ snapshot.agentModels = { ...input.agentModels };
713
+ });
714
+ }
715
+ function recordTuiAgentModel(input) {
716
+ updateSnapshot((snapshot) => {
717
+ snapshot.agentModels[input.agentName] = input.model;
718
+ });
719
+ }
720
+
721
+ // src/tui.ts
37
722
  var PLUGIN_NAME = "oh-my-opencode-slim";
38
- var PLUGIN_LABEL = "OMOS";
723
+ var CONFIG_WARNING_COLOR = "orange";
724
+ var FALLBACK_SIDEBAR_AGENTS = SUBAGENT_NAMES.filter((agent) => agent !== "councillor" && agent !== "council" && !DEFAULT_DISABLED_AGENTS.includes(agent));
725
+ var BORDER = { type: "single" };
39
726
  async function readPackageVersion() {
40
727
  try {
41
728
  const packageJson = await Bun.file(new URL("../package.json", import.meta.url)).json();
@@ -60,21 +747,109 @@ function element(tag, props, children = []) {
60
747
  function text(props, children) {
61
748
  return element("text", props, children);
62
749
  }
750
+ function box(props, children = []) {
751
+ return element("box", props, children);
752
+ }
753
+ function truncate(value, max = 24) {
754
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
755
+ }
756
+ function getTuiDirectory(api) {
757
+ return api.state?.path?.directory ?? process.cwd();
758
+ }
759
+ function formatSidebarModelName(model) {
760
+ const lastSlash = model.lastIndexOf("/");
761
+ return lastSlash === -1 ? model : model.slice(lastSlash + 1);
762
+ }
763
+ function getSidebarAgentNames(snapshot) {
764
+ const configuredAgents = Object.keys(snapshot.agentModels);
765
+ return configuredAgents.length > 0 ? configuredAgents : FALLBACK_SIDEBAR_AGENTS;
766
+ }
767
+ function row(label, value, theme, valueColor) {
768
+ return box({ width: "100%", flexDirection: "row", justifyContent: "space-between" }, [
769
+ text({ fg: theme.textMuted }, [label]),
770
+ text({ fg: valueColor ?? theme.text }, [value])
771
+ ]);
772
+ }
773
+ function renderSidebar(snapshot, version, theme, configInvalid) {
774
+ const configStatusRow = buildConfigStatusRow(configInvalid, theme);
775
+ return box({
776
+ width: "100%",
777
+ flexDirection: "column",
778
+ border: BORDER,
779
+ borderColor: theme.borderActive,
780
+ paddingTop: 1,
781
+ paddingBottom: 1,
782
+ paddingLeft: 1,
783
+ paddingRight: 1
784
+ }, [
785
+ box({
786
+ width: "100%",
787
+ flexDirection: "row",
788
+ justifyContent: "space-between",
789
+ alignItems: "center"
790
+ }, [
791
+ box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.background }, ["OMO-Slim"])]),
792
+ text({ fg: theme.textMuted }, [`v${version}`])
793
+ ]),
794
+ configStatusRow,
795
+ box({ width: "100%", marginTop: 1 }, [
796
+ text({ fg: theme.text }, ["Agents"])
797
+ ]),
798
+ ...getSidebarAgentNames(snapshot).map((agentName) => {
799
+ const model = snapshot.agentModels[agentName] ?? "pending";
800
+ return row(agentName, truncate(formatSidebarModelName(model), 26), theme, theme.textMuted);
801
+ })
802
+ ]);
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
+ }
63
827
  var plugin = {
64
828
  id: `${PLUGIN_NAME}:tui`,
65
829
  tui: async (api, _options, meta) => {
66
830
  const version = meta.version ?? await readPackageVersion() ?? "dev";
67
- const versionText = `${PLUGIN_LABEL} ${version}`;
831
+ let configDirectory = getTuiDirectory(api);
832
+ let configInvalid = readConfigInvalid(configDirectory);
833
+ let snapshot = readTuiSnapshot();
834
+ const renderTimer = setInterval(async () => {
835
+ try {
836
+ snapshot = await readTuiSnapshotAsync();
837
+ const currentDirectory = getTuiDirectory(api);
838
+ if (currentDirectory !== configDirectory) {
839
+ configDirectory = currentDirectory;
840
+ configInvalid = readConfigInvalid(configDirectory);
841
+ }
842
+ api.renderer.requestRender();
843
+ } catch {}
844
+ }, 1000);
845
+ api.lifecycle.onDispose(() => {
846
+ clearInterval(renderTimer);
847
+ });
68
848
  api.slots.register({
69
849
  order: 900,
70
850
  slots: {
71
- home_prompt_right() {
72
- const theme = api.theme.current;
73
- return text({ fg: theme.textMuted }, [versionText]);
74
- },
75
- session_prompt_right() {
76
- const theme = api.theme.current;
77
- return text({ fg: theme.textMuted }, [versionText]);
851
+ sidebar_content() {
852
+ return renderSidebar(snapshot, version, api.theme.current, configInvalid);
78
853
  }
79
854
  }
80
855
  });
@@ -82,5 +857,8 @@ var plugin = {
82
857
  };
83
858
  var tui_default = plugin;
84
859
  export {
860
+ readConfigInvalid,
861
+ getSidebarAgentNames,
862
+ formatSidebarModelName,
85
863
  tui_default as default
86
864
  };