opencode-agent-variants 0.1.0

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 ADDED
@@ -0,0 +1,849 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { BUILTIN_AGENT_DESCRIPTIONS, debugLogPath, defaultConfigDir, defaultSidecarPath, diagnoseConfig, loadSidecar, saveSidecar, variantName, } from "./config.js";
3
+ // Constants.
4
+ const BUILTIN_AGENT_KEYS = Object.keys(BUILTIN_AGENT_DESCRIPTIONS);
5
+ const THEME_COLORS = ["primary", "secondary", "accent", "success", "warning", "error", "info"];
6
+ const EDITABLE_FIELDS = [
7
+ { key: "model", label: "Model", type: "string" },
8
+ { key: "variant", label: "Variant", type: "string" },
9
+ { key: "temperature", label: "Temperature", type: "number" },
10
+ { key: "top_p", label: "Top P", type: "number" },
11
+ { key: "prompt", label: "Prompt (replace)", type: "string" },
12
+ { key: "prompt_prepend", label: "Prompt prepend", type: "string" },
13
+ { key: "prompt_append", label: "Prompt append", type: "string" },
14
+ { key: "description", label: "Description (replace)", type: "string" },
15
+ { key: "description_prepend", label: "Description prepend", type: "string" },
16
+ { key: "description_append", label: "Description append", type: "string" },
17
+ { key: "options", label: "Options (JSON)", type: "json" },
18
+ { key: "color", label: "Color", type: "string" },
19
+ ];
20
+ // Helpers.
21
+ function agentsFromState(api) {
22
+ const configured = Object.keys(api.state.config.agent ?? {});
23
+ const merged = new Set([...BUILTIN_AGENT_KEYS, ...configured]);
24
+ return [...merged].sort();
25
+ }
26
+ function modelOptions(api, config) {
27
+ const opts = [];
28
+ for (const [key, raw] of Object.entries(config.models)) {
29
+ const entry = raw;
30
+ opts.push({
31
+ title: `${key} -> ${entry.model}`,
32
+ value: key,
33
+ description: entry.label ?? entry.model,
34
+ category: "Named shortcuts",
35
+ });
36
+ }
37
+ const seen = new Set();
38
+ for (const provider of api.state.provider) {
39
+ for (const model of Object.values(provider.models)) {
40
+ const ref = `${provider.id}/${model.id}`;
41
+ if (seen.has(ref))
42
+ continue;
43
+ seen.add(ref);
44
+ opts.push({
45
+ title: model.name,
46
+ value: ref,
47
+ description: `via ${provider.name}`,
48
+ category: provider.name,
49
+ });
50
+ }
51
+ }
52
+ opts.push({
53
+ title: "Custom model...",
54
+ value: "__custom__",
55
+ description: "Type a provider/model ID manually",
56
+ category: "Custom",
57
+ });
58
+ return opts;
59
+ }
60
+ function colorOptions() {
61
+ const opts = THEME_COLORS.map((c) => ({
62
+ title: c,
63
+ value: c,
64
+ category: "Theme colors",
65
+ }));
66
+ opts.push({
67
+ title: "Custom hex...",
68
+ value: "__custom__",
69
+ description: "Enter a hex color like #FF5733",
70
+ category: "Custom",
71
+ });
72
+ return opts;
73
+ }
74
+ function variantCount(config) {
75
+ let count = 0;
76
+ for (const raw of Object.values(config.agents)) {
77
+ count += Object.keys(raw.variants).length;
78
+ }
79
+ return count;
80
+ }
81
+ function agentEntries(config) {
82
+ return Object.entries(config.agents);
83
+ }
84
+ function variantEntries(entry) {
85
+ return Object.entries(entry.variants);
86
+ }
87
+ function generatedAliasSet(config) {
88
+ const aliases = new Set();
89
+ for (const [agent, entry] of agentEntries(config)) {
90
+ for (const [key, variant] of variantEntries(entry)) {
91
+ aliases.add(variantName(agent, key, variant));
92
+ }
93
+ }
94
+ return aliases;
95
+ }
96
+ function showSelect(ui, props) {
97
+ return new Promise((resolve) => {
98
+ ui.dialog.replace(() => ui.DialogSelect({
99
+ title: props.title,
100
+ placeholder: props.placeholder ?? "Type to filter...",
101
+ options: props.options,
102
+ current: props.current,
103
+ flat: props.options.length < 15,
104
+ onSelect: (opt) => {
105
+ ui.dialog.clear();
106
+ resolve(opt.value);
107
+ },
108
+ }));
109
+ });
110
+ }
111
+ function showPrompt(ui, props) {
112
+ return new Promise((resolve) => {
113
+ ui.dialog.replace(() => ui.DialogPrompt({
114
+ title: props.title,
115
+ placeholder: props.placeholder,
116
+ value: props.value ?? "",
117
+ onConfirm: (val) => {
118
+ ui.dialog.clear();
119
+ resolve(val || undefined);
120
+ },
121
+ onCancel: () => {
122
+ ui.dialog.clear();
123
+ resolve(undefined);
124
+ },
125
+ }));
126
+ });
127
+ }
128
+ function showConfirm(ui, props) {
129
+ return new Promise((resolve) => {
130
+ ui.dialog.replace(() => ui.DialogConfirm({
131
+ title: props.title,
132
+ message: props.message,
133
+ onConfirm: () => {
134
+ ui.dialog.clear();
135
+ resolve(true);
136
+ },
137
+ onCancel: () => {
138
+ ui.dialog.clear();
139
+ resolve(false);
140
+ },
141
+ }));
142
+ });
143
+ }
144
+ function showAlert(ui, props) {
145
+ return new Promise((resolve) => {
146
+ ui.dialog.replace(() => ui.DialogAlert({
147
+ title: props.title,
148
+ message: props.message,
149
+ onConfirm: () => {
150
+ ui.dialog.clear();
151
+ resolve();
152
+ },
153
+ }));
154
+ });
155
+ }
156
+ // Main wizard flows.
157
+ async function addVariant(api, config) {
158
+ const aliases = generatedAliasSet(config);
159
+ const agents = agentsFromState(api).filter((agent) => !aliases.has(agent));
160
+ if (agents.length === 0) {
161
+ await showAlert(api.ui, { title: "No agents", message: "No agents available to add a variant to." });
162
+ return config;
163
+ }
164
+ const agentOpts = agents.map((a) => ({
165
+ title: a,
166
+ value: a,
167
+ description: BUILTIN_AGENT_DESCRIPTIONS[a] ?? api.state.config.agent?.[a]?.description,
168
+ }));
169
+ const agent = await showSelect(api.ui, { title: "Add variant - pick parent agent", options: agentOpts });
170
+ if (!agent)
171
+ return config;
172
+ const existingKeys = Object.keys(config.agents[agent]?.variants ?? {});
173
+ const defaultKey = existingKeys.length === 0 ? "light" : undefined;
174
+ const key = await showPrompt(api.ui, {
175
+ title: `Variant key for "${agent}"`,
176
+ description: `This becomes the alias name (e.g. "${agent}-<key>"). Must be unique per agent.`,
177
+ placeholder: defaultKey ?? "variant-key",
178
+ value: defaultKey,
179
+ });
180
+ if (!key)
181
+ return config;
182
+ if (existingKeys.includes(key)) {
183
+ await showAlert(api.ui, { title: "Duplicate key", message: `"${key}" already exists for "${agent}".` });
184
+ return config;
185
+ }
186
+ const next = structuredClone(config);
187
+ if (!next.agents[agent]) {
188
+ next.agents[agent] = { parent: {}, variants: {} };
189
+ }
190
+ const variant = await editVariantFields(api, next, {}, agent, key);
191
+ next.agents[agent].variants[key] = variant;
192
+ const name = variantName(agent, key, variant);
193
+ api.ui.toast({ variant: "success", title: "Variant added", message: name });
194
+ return next;
195
+ }
196
+ async function editParentFields(api, config, agent) {
197
+ const next = structuredClone(config);
198
+ if (!next.agents[agent]) {
199
+ next.agents[agent] = { parent: {}, variants: {} };
200
+ }
201
+ const parent = next.agents[agent].parent;
202
+ const fieldOpts = EDITABLE_FIELDS.map((f) => {
203
+ const current = parent[f.key];
204
+ return {
205
+ title: f.label,
206
+ value: f.key,
207
+ description: current !== undefined ? `Current: ${truncate(String(current), 60)}` : "(not set)",
208
+ };
209
+ });
210
+ fieldOpts.push({
211
+ title: "< Back",
212
+ value: "__back__",
213
+ description: "Return to main menu",
214
+ });
215
+ const field = await showSelect(api.ui, {
216
+ title: `Edit parent fields - ${agent}`,
217
+ options: fieldOpts,
218
+ });
219
+ if (!field || field === "__back__")
220
+ return config;
221
+ const currentValue = formatInputValue(parent[field]);
222
+ const value = await promptForField(api, field, currentValue);
223
+ if (value === undefined)
224
+ return config;
225
+ if (value === "") {
226
+ delete parent[field];
227
+ }
228
+ else {
229
+ parent[field] = value;
230
+ }
231
+ api.ui.toast({ variant: "success", title: "Field updated", message: `${field} for ${agent}` });
232
+ return next;
233
+ }
234
+ async function editVariantFields(api, config, initial, agent, key) {
235
+ const variant = { ...initial };
236
+ const wantModel = await showConfirm(api.ui, {
237
+ title: "Set model?",
238
+ message: `Choose a model for variant "${key}" of "${agent}"?`,
239
+ });
240
+ if (wantModel) {
241
+ const models = modelOptions(api, config);
242
+ const picked = await showSelect(api.ui, { title: "Select model", options: models });
243
+ if (picked) {
244
+ if (picked === "__custom__") {
245
+ const custom = await showPrompt(api.ui, {
246
+ title: "Custom model ID",
247
+ placeholder: "provider/model-id",
248
+ });
249
+ if (custom)
250
+ variant.model = custom;
251
+ }
252
+ else {
253
+ variant.model = picked;
254
+ }
255
+ }
256
+ }
257
+ const wantName = await showConfirm(api.ui, {
258
+ title: "Custom display name?",
259
+ message: `Default is "${agent}-${key}". Set a custom name?`,
260
+ });
261
+ if (wantName) {
262
+ const name = await showPrompt(api.ui, {
263
+ title: "Variant display name",
264
+ value: variant.name,
265
+ placeholder: `${agent}-${key}`,
266
+ });
267
+ if (name)
268
+ variant.name = name;
269
+ }
270
+ const more = await showConfirm(api.ui, {
271
+ title: "Edit more fields?",
272
+ message: "Set temperature, prompt overrides, color, etc.?",
273
+ });
274
+ if (!more)
275
+ return variant;
276
+ const remaining = EDITABLE_FIELDS.filter((f) => f.key !== "model");
277
+ for (const field of remaining) {
278
+ if (field.key === "color") {
279
+ const wantColor = await showConfirm(api.ui, {
280
+ title: "Set color?",
281
+ message: `Current: ${variant.color ?? "(not set)"}`,
282
+ });
283
+ if (wantColor) {
284
+ const picked = await showSelect(api.ui, { title: "Pick color", options: colorOptions() });
285
+ if (picked === "__custom__") {
286
+ const hex = await showPrompt(api.ui, { title: "Hex color", placeholder: "#FF5733" });
287
+ if (hex)
288
+ variant.color = hex;
289
+ }
290
+ else if (picked) {
291
+ variant.color = picked;
292
+ }
293
+ }
294
+ continue;
295
+ }
296
+ if (variant[field.key] !== undefined)
297
+ continue;
298
+ const want = await showConfirm(api.ui, {
299
+ title: `Set ${field.label}?`,
300
+ message: `Not set. Configure this field?`,
301
+ });
302
+ if (!want)
303
+ continue;
304
+ const value = await promptForField(api, field.key, formatInputValue(variant[field.key]));
305
+ if (value !== undefined && value !== "") {
306
+ variant[field.key] = value;
307
+ }
308
+ }
309
+ return variant;
310
+ }
311
+ async function editVariant(api, config) {
312
+ const agentsWithVariants = agentEntries(config).filter(([, e]) => Object.keys(e.variants).length > 0);
313
+ if (agentsWithVariants.length === 0) {
314
+ await showAlert(api.ui, { title: "No variants", message: "Add a variant first." });
315
+ return config;
316
+ }
317
+ const agentOpts = agentsWithVariants.map(([a, e]) => ({
318
+ title: a,
319
+ value: a,
320
+ description: `${Object.keys(e.variants).length} variant(s)`,
321
+ }));
322
+ const agent = await showSelect(api.ui, { title: "Edit variant - pick agent", options: agentOpts });
323
+ if (!agent)
324
+ return config;
325
+ const entries = variantEntries(config.agents[agent]);
326
+ const varOpts = entries.map(([k, v]) => ({
327
+ title: `${variantName(agent, k, v)} (${k})`,
328
+ value: k,
329
+ description: modelDescription(v.model, config),
330
+ }));
331
+ const key = await showSelect(api.ui, { title: `Edit variant of "${agent}"`, options: varOpts });
332
+ if (!key)
333
+ return config;
334
+ const existing = config.agents[agent].variants[key];
335
+ const existingRec = existing;
336
+ const fieldOpts = EDITABLE_FIELDS.map((f) => ({
337
+ title: f.label,
338
+ value: f.key,
339
+ description: existingRec[f.key] !== undefined ? `Current: ${truncate(String(existingRec[f.key]), 60)}` : "(not set)",
340
+ }));
341
+ fieldOpts.push({
342
+ title: "Display name",
343
+ value: "name",
344
+ description: existing.name ? `Current: ${existing.name}` : `Default: ${agent}-${key}`,
345
+ });
346
+ fieldOpts.push({
347
+ title: "< Back",
348
+ value: "__back__",
349
+ description: "Return to main menu",
350
+ });
351
+ const field = await showSelect(api.ui, {
352
+ title: `Edit field - ${variantName(agent, key, existing)}`,
353
+ options: fieldOpts,
354
+ });
355
+ if (!field || field === "__back__")
356
+ return config;
357
+ const next = structuredClone(config);
358
+ const target = next.agents[agent].variants[key];
359
+ if (field === "name") {
360
+ const val = await showPrompt(api.ui, {
361
+ title: "Display name",
362
+ value: target.name,
363
+ placeholder: `${agent}-${key}`,
364
+ });
365
+ if (val === undefined)
366
+ return config;
367
+ target.name = val || undefined;
368
+ }
369
+ else if (field === "model") {
370
+ const models = modelOptions(api, next);
371
+ const picked = await showSelect(api.ui, { title: "Select model", options: models, current: existing.model });
372
+ if (!picked)
373
+ return config;
374
+ if (picked === "__custom__") {
375
+ const custom = await showPrompt(api.ui, { title: "Custom model ID", placeholder: "provider/model-id" });
376
+ if (custom)
377
+ target.model = custom;
378
+ }
379
+ else {
380
+ target.model = picked;
381
+ }
382
+ }
383
+ else if (field === "color") {
384
+ const picked = await showSelect(api.ui, { title: "Pick color", options: colorOptions() });
385
+ if (picked === "__custom__") {
386
+ const hex = await showPrompt(api.ui, { title: "Hex color", placeholder: "#FF5733" });
387
+ if (hex)
388
+ target.color = hex;
389
+ }
390
+ else if (picked) {
391
+ target.color = picked;
392
+ }
393
+ }
394
+ else {
395
+ const val = await promptForField(api, field, formatInputValue(existingRec[field]));
396
+ if (val === undefined)
397
+ return config;
398
+ if (val === "") {
399
+ delete target[field];
400
+ }
401
+ else {
402
+ target[field] = val;
403
+ }
404
+ }
405
+ const updated = next.agents[agent];
406
+ const updatedVariant = updated.variants[key];
407
+ api.ui.toast({ variant: "success", title: "Variant updated", message: variantName(agent, key, updatedVariant) });
408
+ return next;
409
+ }
410
+ async function toggleDisable(api, config) {
411
+ const items = [];
412
+ for (const [agent, raw] of agentEntries(config)) {
413
+ const entry = raw;
414
+ const parentDisabled = entry.disable === true;
415
+ items.push({
416
+ title: `${parentDisabled ? "x" : "ok"} ${agent} (parent)`,
417
+ value: { agent },
418
+ description: parentDisabled ? "Disabled - no variants active" : "Enabled",
419
+ category: "Parents",
420
+ });
421
+ for (const [key, rawVar] of variantEntries(entry)) {
422
+ const variant = rawVar;
423
+ const vDisabled = variant.disable === true;
424
+ items.push({
425
+ title: ` ${vDisabled ? "x" : "ok"} ${variantName(agent, key, variant)}`,
426
+ value: { agent, variant: key },
427
+ description: vDisabled ? "Disabled" : "Enabled",
428
+ category: "Variants",
429
+ });
430
+ }
431
+ }
432
+ if (items.length === 0) {
433
+ await showAlert(api.ui, { title: "Nothing to toggle", message: "Add agents or variants first." });
434
+ return config;
435
+ }
436
+ items.push({
437
+ title: "< Back",
438
+ value: { agent: "__back__" },
439
+ description: "Return to main menu",
440
+ });
441
+ const picked = await showSelect(api.ui, { title: "Toggle disable", options: items });
442
+ if (!picked || picked.agent === "__back__")
443
+ return config;
444
+ const next = structuredClone(config);
445
+ if (!picked.variant) {
446
+ if (!next.agents[picked.agent]) {
447
+ next.agents[picked.agent] = { parent: {}, variants: {} };
448
+ }
449
+ const entry = next.agents[picked.agent];
450
+ entry.disable = !entry.disable;
451
+ const state = entry.disable ? "disabled" : "enabled";
452
+ api.ui.toast({ variant: "info", title: `${picked.agent} ${state}`, message: `Parent ${state}` });
453
+ }
454
+ else {
455
+ const entry = next.agents[picked.agent];
456
+ const variant = entry?.variants[picked.variant];
457
+ if (variant) {
458
+ variant.disable = !variant.disable;
459
+ const state = variant.disable ? "disabled" : "enabled";
460
+ api.ui.toast({ variant: "info", title: `${variantName(picked.agent, picked.variant, variant)} ${state}`, message: `Variant ${state}` });
461
+ }
462
+ }
463
+ return next;
464
+ }
465
+ async function deleteVariant(api, config) {
466
+ const agentsWithVariants = agentEntries(config).filter(([, e]) => Object.keys(e.variants).length > 0);
467
+ if (agentsWithVariants.length === 0) {
468
+ await showAlert(api.ui, { title: "No variants", message: "Nothing to delete." });
469
+ return config;
470
+ }
471
+ const agentOpts = agentsWithVariants.map(([a]) => ({
472
+ title: a,
473
+ value: a,
474
+ }));
475
+ const agent = await showSelect(api.ui, { title: "Delete variant - pick agent", options: agentOpts });
476
+ if (!agent)
477
+ return config;
478
+ const agentEntry = config.agents[agent];
479
+ const entries = variantEntries(agentEntry);
480
+ const varOpts = entries.map(([k, v]) => ({
481
+ title: `${variantName(agent, k, v)} (${k})`,
482
+ value: k,
483
+ description: modelDescription(v.model, config),
484
+ }));
485
+ const key = await showSelect(api.ui, { title: `Delete variant from "${agent}"`, options: varOpts });
486
+ if (!key)
487
+ return config;
488
+ const name = variantName(agent, key, agentEntry.variants[key]);
489
+ const confirmed = await showConfirm(api.ui, {
490
+ title: "Delete variant?",
491
+ message: `Permanently remove "${name}"?`,
492
+ });
493
+ if (!confirmed)
494
+ return config;
495
+ const next = structuredClone(config);
496
+ const entry = next.agents[agent];
497
+ delete entry.variants[key];
498
+ api.ui.toast({ variant: "success", title: "Variant deleted", message: name });
499
+ return next;
500
+ }
501
+ async function previewConfig(api, config) {
502
+ const lines = [];
503
+ lines.push("=".repeat(50));
504
+ lines.push(" Agent Variants Configuration Preview");
505
+ lines.push("=".repeat(50));
506
+ lines.push("");
507
+ lines.push(`Debug mode: ${config.debug ? "enabled" : "disabled"}`);
508
+ if (Object.keys(config.models).length > 0) {
509
+ lines.push("");
510
+ lines.push("Named models:");
511
+ for (const [key, raw] of Object.entries(config.models)) {
512
+ const entry = raw;
513
+ lines.push(` ${key} -> ${entry.model}${entry.label ? ` (${entry.label})` : ""}`);
514
+ }
515
+ }
516
+ if (Object.keys(config.agents).length === 0) {
517
+ lines.push("");
518
+ lines.push(" (no agents configured)");
519
+ }
520
+ for (const [agent, raw] of agentEntries(config)) {
521
+ const entry = raw;
522
+ lines.push("");
523
+ const status = entry.disable ? " [DISABLED]" : "";
524
+ lines.push(` Agent: ${agent}${status}`);
525
+ if (hasOverrides(entry.parent)) {
526
+ lines.push(" Parent overrides:");
527
+ for (const field of EDITABLE_FIELDS) {
528
+ const val = entry.parent[field.key];
529
+ if (val !== undefined)
530
+ lines.push(` ${field.label}: ${truncate(JSON.stringify(val), 50)}`);
531
+ }
532
+ }
533
+ const varKeys = Object.keys(entry.variants);
534
+ if (varKeys.length > 0) {
535
+ lines.push(` Variants (${varKeys.length}):`);
536
+ for (const [key, rawVar] of variantEntries(entry)) {
537
+ const v = rawVar;
538
+ const vStatus = v.disable ? " [DISABLED]" : "";
539
+ lines.push(` ${variantName(agent, key, v)} (key: ${key})${vStatus}`);
540
+ if (v.model)
541
+ lines.push(` model: ${modelDescription(v.model, config)}`);
542
+ if (v.temperature !== undefined)
543
+ lines.push(` temperature: ${v.temperature}`);
544
+ if (v.top_p !== undefined)
545
+ lines.push(` top_p: ${v.top_p}`);
546
+ if (v.color)
547
+ lines.push(` color: ${v.color}`);
548
+ if (v.prompt)
549
+ lines.push(` prompt: ${truncate(v.prompt, 40)}`);
550
+ if (v.prompt_prepend)
551
+ lines.push(` prompt_prepend: ${truncate(v.prompt_prepend, 40)}`);
552
+ if (v.prompt_append)
553
+ lines.push(` prompt_append: ${truncate(v.prompt_append, 40)}`);
554
+ }
555
+ }
556
+ }
557
+ lines.push("");
558
+ lines.push(` File: ${defaultSidecarPath()}`);
559
+ lines.push("=".repeat(50));
560
+ await showAlert(api.ui, { title: "Configuration Preview", message: lines.join("\n") });
561
+ }
562
+ async function toggleDebug(api, config) {
563
+ const next = structuredClone(config);
564
+ next.debug = !next.debug;
565
+ try {
566
+ saveSidecar(next, defaultSidecarPath());
567
+ }
568
+ catch (err) {
569
+ await showAlert(api.ui, { title: "Save failed", message: String(err instanceof Error ? err.message : err) });
570
+ return config;
571
+ }
572
+ api.ui.toast({
573
+ variant: "info",
574
+ title: `Debug mode ${next.debug ? "enabled" : "disabled"}`,
575
+ message: next.debug
576
+ ? "Variant routing debug toasts are enabled immediately."
577
+ : "Variant routing debug toasts are disabled immediately.",
578
+ });
579
+ return next;
580
+ }
581
+ async function runDiagnostics(api, config) {
582
+ const generatedAliases = generatedAliasSet(config);
583
+ const diagnostics = diagnoseConfig(config, {
584
+ agents: agentsFromState(api).filter((agent) => !generatedAliases.has(agent)),
585
+ providers: api.state.provider,
586
+ pluginEntries: api.state.config.plugin,
587
+ });
588
+ const errors = diagnostics.filter((item) => item.level === "error").length;
589
+ const warnings = diagnostics.filter((item) => item.level === "warning").length;
590
+ const infos = diagnostics.filter((item) => item.level === "info").length;
591
+ const lines = [
592
+ "Agent Variants Diagnostics",
593
+ "=".repeat(50),
594
+ `Sidecar: ${defaultSidecarPath()}`,
595
+ `Agents configured: ${Object.keys(config.agents).length}`,
596
+ `Variants configured: ${variantCount(config)}`,
597
+ `Debug mode: ${config.debug ? "enabled" : "disabled"}`,
598
+ `Debug log: ${debugLogPath(defaultConfigDir())}`,
599
+ `Summary: ${errors} error(s), ${warnings} warning(s), ${infos} info`,
600
+ "",
601
+ ...(diagnostics.length === 0 ? ["No diagnostics."] : diagnostics.map((item) => `${item.level.toUpperCase()}: ${item.message}`)),
602
+ ];
603
+ api.ui.toast({
604
+ variant: errors > 0 ? "error" : warnings > 0 ? "warning" : "success",
605
+ title: "Agent Variants diagnostics",
606
+ message: `${errors} error(s), ${warnings} warning(s)`,
607
+ });
608
+ await showAlert(api.ui, { title: "Diagnostics", message: lines.join("\n") });
609
+ }
610
+ async function viewDebugLog(api) {
611
+ const file = debugLogPath(defaultConfigDir());
612
+ if (!existsSync(file)) {
613
+ await showAlert(api.ui, { title: "Debug log", message: `No debug log found at ${file}` });
614
+ return;
615
+ }
616
+ const lines = readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean);
617
+ await showAlert(api.ui, {
618
+ title: "Debug log",
619
+ message: [`File: ${file}`, "", ...lines.slice(-80)].join("\n"),
620
+ });
621
+ }
622
+ async function clearDebugLog(api) {
623
+ const file = debugLogPath(defaultConfigDir());
624
+ const confirmed = await showConfirm(api.ui, { title: "Clear debug log?", message: `Empty ${file}?` });
625
+ if (!confirmed)
626
+ return;
627
+ writeFileSync(file, "");
628
+ api.ui.toast({ variant: "success", title: "Debug log cleared", message: file });
629
+ }
630
+ async function saveConfig(api, config) {
631
+ try {
632
+ saveSidecar(config, defaultSidecarPath());
633
+ api.ui.toast({
634
+ variant: "success",
635
+ title: "Saved",
636
+ message: "Configuration written with backup. Restart OpenCode to apply.",
637
+ });
638
+ }
639
+ catch (err) {
640
+ await showAlert(api.ui, {
641
+ title: "Save failed",
642
+ message: String(err instanceof Error ? err.message : err),
643
+ });
644
+ }
645
+ }
646
+ // Field prompting.
647
+ async function promptForField(api, field, current) {
648
+ if (field === "model") {
649
+ const config = loadSidecar(defaultSidecarPath());
650
+ const models = modelOptions(api, config);
651
+ const picked = await showSelect(api.ui, { title: "Select model", options: models, current });
652
+ if (!picked)
653
+ return undefined;
654
+ if (picked === "__custom__") {
655
+ return showPrompt(api.ui, { title: "Custom model ID", placeholder: "provider/model-id" });
656
+ }
657
+ return picked;
658
+ }
659
+ if (field === "color") {
660
+ const picked = await showSelect(api.ui, { title: "Pick color", options: colorOptions() });
661
+ if (picked === "__custom__") {
662
+ return showPrompt(api.ui, { title: "Hex color", placeholder: "#FF5733" });
663
+ }
664
+ return picked;
665
+ }
666
+ if (field === "options") {
667
+ const val = await showPrompt(api.ui, {
668
+ title: "Options (JSON object)",
669
+ placeholder: current ?? '{"key": "value"}',
670
+ value: current ?? undefined,
671
+ });
672
+ if (val === undefined)
673
+ return undefined;
674
+ if (val === "")
675
+ return "";
676
+ try {
677
+ return JSON.parse(val);
678
+ }
679
+ catch {
680
+ await showAlert(api.ui, { title: "Invalid JSON", message: "Please enter a valid JSON object." });
681
+ return undefined;
682
+ }
683
+ }
684
+ if (field === "temperature" || field === "top_p") {
685
+ const label = field === "top_p" ? "Top P" : "Temperature";
686
+ const val = await showPrompt(api.ui, {
687
+ title: label,
688
+ placeholder: "0.0 - 2.0",
689
+ value: current ?? undefined,
690
+ });
691
+ if (val === undefined)
692
+ return undefined;
693
+ if (val === "")
694
+ return "";
695
+ const num = Number(val);
696
+ if (isNaN(num)) {
697
+ await showAlert(api.ui, { title: "Invalid number", message: `"${val}" is not a valid number.` });
698
+ return undefined;
699
+ }
700
+ return num;
701
+ }
702
+ return showPrompt(api.ui, {
703
+ title: field.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
704
+ value: current,
705
+ });
706
+ }
707
+ // Utility.
708
+ function truncate(s, max) {
709
+ if (s.length <= max)
710
+ return s;
711
+ return s.slice(0, max - 1) + "...";
712
+ }
713
+ function modelDescription(model, config) {
714
+ if (!model)
715
+ return "(inherit)";
716
+ const named = config.models[model];
717
+ if (named)
718
+ return `${model} -> ${named.model}`;
719
+ return model;
720
+ }
721
+ function hasOverrides(patch) {
722
+ return Object.keys(patch).length > 0;
723
+ }
724
+ function formatInputValue(value) {
725
+ if (value === undefined)
726
+ return undefined;
727
+ if (typeof value === "string")
728
+ return value;
729
+ return JSON.stringify(value);
730
+ }
731
+ // Main menu loop.
732
+ async function mainMenu(api, config) {
733
+ const agentCount = Object.keys(config.agents).length;
734
+ const vCount = variantCount(config);
735
+ const opts = [
736
+ { title: "Add variant...", value: "add", description: "Create a new agent variant" },
737
+ { title: "Edit parent fields...", value: "edit-parent", description: "Override fields on an agent parent" },
738
+ { title: "Edit variant...", value: "edit-variant", description: "Change fields on an existing variant" },
739
+ { title: "Toggle disable...", value: "toggle", description: "Enable or disable agents/variants" },
740
+ { title: "Run diagnostics", value: "diagnostics", description: "Check models, conflicts, plugin install, and disabled variants" },
741
+ {
742
+ title: `Debug mode: ${config.debug ? "on" : "off"}`,
743
+ value: "debug",
744
+ description: "Toggle routing/model diagnostic toasts immediately",
745
+ },
746
+ { title: "View debug log", value: "view-log", description: "Show recent agent-variants.debug.log entries" },
747
+ { title: "Clear debug log", value: "clear-log", description: "Empty agent-variants.debug.log" },
748
+ { title: "Delete variant...", value: "delete", description: "Remove a variant" },
749
+ { title: "Preview configuration", value: "preview", description: `View current config (${agentCount} agents, ${vCount} variants)` },
750
+ { title: "Save & exit", value: "save", description: "Write to disk with backup" },
751
+ ];
752
+ const action = await showSelect(api.ui, {
753
+ title: "Agent Variants",
754
+ options: opts,
755
+ placeholder: "Choose an action...",
756
+ });
757
+ switch (action) {
758
+ case "add":
759
+ return mainMenu(api, await addVariant(api, config));
760
+ case "edit-parent":
761
+ return editParentFlow(api, config);
762
+ case "edit-variant":
763
+ return mainMenu(api, await editVariant(api, config));
764
+ case "toggle":
765
+ return mainMenu(api, await toggleDisable(api, config));
766
+ case "diagnostics":
767
+ await runDiagnostics(api, config);
768
+ return mainMenu(api, config);
769
+ case "debug":
770
+ return mainMenu(api, await toggleDebug(api, config));
771
+ case "view-log":
772
+ await viewDebugLog(api);
773
+ return mainMenu(api, config);
774
+ case "clear-log":
775
+ await clearDebugLog(api);
776
+ return mainMenu(api, config);
777
+ case "delete":
778
+ return mainMenu(api, await deleteVariant(api, config));
779
+ case "preview":
780
+ await previewConfig(api, config);
781
+ return mainMenu(api, config);
782
+ case "save":
783
+ await saveConfig(api, config);
784
+ return config;
785
+ default:
786
+ return config;
787
+ }
788
+ }
789
+ async function editParentFlow(api, config) {
790
+ const aliases = generatedAliasSet(config);
791
+ const agents = agentsFromState(api).filter((agent) => !aliases.has(agent));
792
+ const opts = agents.map((a) => {
793
+ const entry = config.agents[a];
794
+ const overrides = entry ? Object.keys(entry.parent).length : 0;
795
+ return {
796
+ title: a,
797
+ value: a,
798
+ description: overrides > 0 ? `${overrides} parent override(s)` : "No overrides",
799
+ };
800
+ });
801
+ opts.push({
802
+ title: "< Back",
803
+ value: "__back__",
804
+ description: "Return to main menu",
805
+ });
806
+ const agent = await showSelect(api.ui, { title: "Edit parent fields - pick agent", options: opts });
807
+ if (!agent || agent === "__back__")
808
+ return mainMenu(api, config);
809
+ const updated = await editParentFields(api, config, agent);
810
+ return mainMenu(api, updated);
811
+ }
812
+ // Plugin entrypoint.
813
+ function registerConfigureCommand(api, run) {
814
+ const command = {
815
+ namespace: "palette",
816
+ name: "agent-variants.configure",
817
+ title: "Agent Variants: Configure",
818
+ desc: "Manage agent model variants",
819
+ category: "Plugins",
820
+ slashName: "agent-variants",
821
+ run,
822
+ };
823
+ const apiWithKeymap = api;
824
+ if (typeof apiWithKeymap.keymap?.registerLayer === "function") {
825
+ return apiWithKeymap.keymap.registerLayer({ commands: [command], bindings: [] });
826
+ }
827
+ return api.command?.register(() => [
828
+ {
829
+ title: "Agent Variants: Configure",
830
+ value: "agent-variants.configure",
831
+ description: "Manage agent model variants",
832
+ category: "Plugins",
833
+ slash: {
834
+ name: "agent-variants",
835
+ },
836
+ onSelect: run,
837
+ },
838
+ ]);
839
+ }
840
+ const tui = async (api) => {
841
+ const unregister = registerConfigureCommand(api, async () => {
842
+ const config = loadSidecar(defaultSidecarPath());
843
+ await mainMenu(api, config);
844
+ });
845
+ api.lifecycle.onDispose(() => {
846
+ unregister?.();
847
+ });
848
+ };
849
+ export default { id: "agent-variants", tui };