@pellux/goodvibes-tui 0.19.52 → 0.19.54

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +10 -13
  3. package/docs/foundation-artifacts/knowledge-store.sql +27 -0
  4. package/docs/foundation-artifacts/operator-contract.json +15850 -7037
  5. package/package.json +2 -2
  6. package/src/audio/spoken-turn-controller.ts +4 -1
  7. package/src/input/command-args-hint.ts +36 -0
  8. package/src/input/command-registry.ts +3 -1
  9. package/src/input/commands/config.ts +7 -521
  10. package/src/input/commands/knowledge.ts +164 -1
  11. package/src/input/commands/local-runtime.ts +0 -80
  12. package/src/input/commands/operator-runtime.ts +3 -3
  13. package/src/input/commands/planning-runtime.ts +83 -34
  14. package/src/input/commands/shell-core.ts +2 -34
  15. package/src/input/commands/tts-runtime.ts +1 -389
  16. package/src/input/commands.ts +0 -2
  17. package/src/input/handler-modal-routes.ts +61 -7
  18. package/src/input/handler-modal-token-routes.ts +1 -0
  19. package/src/input/handler-picker-routes.ts +50 -4
  20. package/src/input/model-picker-provider-filter.ts +28 -0
  21. package/src/input/model-picker-types.ts +12 -0
  22. package/src/input/model-picker.ts +65 -23
  23. package/src/input/selection-modal.ts +1 -1
  24. package/src/input/settings-modal-behavior.ts +2 -0
  25. package/src/input/settings-modal-subscriptions.ts +95 -0
  26. package/src/input/settings-modal-types.ts +50 -3
  27. package/src/input/settings-modal.ts +106 -134
  28. package/src/input/tts-settings-actions.ts +100 -0
  29. package/src/main.ts +50 -45
  30. package/src/panels/builtin/agent.ts +15 -0
  31. package/src/panels/builtin/shared.ts +17 -0
  32. package/src/panels/project-planning-panel.ts +370 -0
  33. package/src/planning/project-planning-coordinator.ts +249 -0
  34. package/src/renderer/compositor.ts +2 -1
  35. package/src/renderer/conversation-overlays.ts +4 -5
  36. package/src/renderer/model-workspace.ts +488 -0
  37. package/src/renderer/settings-modal-helpers.ts +16 -1
  38. package/src/renderer/settings-modal.ts +616 -716
  39. package/src/runtime/bootstrap-command-context.ts +6 -0
  40. package/src/runtime/bootstrap-command-parts.ts +5 -0
  41. package/src/runtime/bootstrap-shell.ts +2 -0
  42. package/src/runtime/services.ts +33 -2
  43. package/src/runtime/terminal-output-guard.ts +228 -0
  44. package/src/runtime/ui-services.ts +4 -0
  45. package/src/shell/ui-openers.ts +59 -3
  46. package/src/utils/clipboard.ts +2 -1
  47. package/src/version.ts +1 -1
  48. package/src/input/commands/permissions-runtime.ts +0 -104
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.52",
3
+ "version": "0.19.54",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -91,7 +91,7 @@
91
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
92
92
  "@ast-grep/napi": "^0.42.0",
93
93
  "@aws/bedrock-token-generator": "^1.1.0",
94
- "@pellux/goodvibes-sdk": "0.26.7",
94
+ "@pellux/goodvibes-sdk": "0.28.0",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
@@ -80,7 +80,10 @@ export class SpokenTurnController {
80
80
  this.enqueueChunks(this.chunker?.push(event.content) ?? []);
81
81
  return;
82
82
  }
83
- if (event.type === 'STREAM_END' || event.type === 'TURN_COMPLETED') {
83
+ if (event.type === 'STREAM_END') {
84
+ return;
85
+ }
86
+ if (event.type === 'TURN_COMPLETED') {
84
87
  this.finishTurn(event.turnId);
85
88
  return;
86
89
  }
@@ -0,0 +1,36 @@
1
+ import type { CommandRegistry } from './command-registry.ts';
2
+
3
+ const SUBCOMMAND_HINTS: Record<string, Record<string, string>> = {
4
+ session: { rename: '<name>', resume: '<id|name>', info: '<id>', export: '<id> [format]', search: '<query>', delete: '<id>' },
5
+ template: { save: '<name>', use: '<name> [args]', edit: '<name>', delete: '<name>' },
6
+ secrets: { set: '<KEY> <value>', get: '<KEY>', delete: '<KEY>' },
7
+ permissions: { tool: '<name> allow|prompt|deny' },
8
+ config: { reset: '<key>' },
9
+ danger: {},
10
+ plugin: { enable: '<name>', disable: '<name>', reload: '' },
11
+ };
12
+
13
+ export function buildCommandArgsHint(
14
+ prompt: string,
15
+ commandRegistry: Pick<CommandRegistry, 'get'>,
16
+ ): string | undefined {
17
+ if (!prompt.startsWith('/')) return undefined;
18
+
19
+ const spaceIdx = prompt.indexOf(' ');
20
+ if (spaceIdx === -1) {
21
+ const cmd = commandRegistry.get(prompt.slice(1));
22
+ return cmd?.argsHint ?? cmd?.usage;
23
+ }
24
+
25
+ const cmdName = prompt.slice(1, spaceIdx);
26
+ const cmd = commandRegistry.get(cmdName);
27
+ if (!cmd) return undefined;
28
+
29
+ const afterCmd = prompt.slice(spaceIdx + 1);
30
+ const subSpaceIdx = afterCmd.indexOf(' ');
31
+ if (subSpaceIdx !== -1) return undefined;
32
+
33
+ const subMap = SUBCOMMAND_HINTS[cmdName];
34
+ if (subMap && afterCmd in subMap) return subMap[afterCmd];
35
+ return undefined;
36
+ }
@@ -93,7 +93,7 @@ export interface CommandShellUiOpeners {
93
93
  opts: { preSelectId?: string; allowSearch?: boolean; customActions?: Map<string, SelectionAction> } | undefined,
94
94
  callback: (result: SelectionResult | null) => void,
95
95
  ) => void;
96
- openSettingsModal?: () => void;
96
+ openSettingsModal?: (target?: string) => void;
97
97
  openSessionPicker?: () => void;
98
98
  openProfilePicker?: () => void;
99
99
  openShortcutsOverlay?: () => void;
@@ -142,6 +142,8 @@ export interface CommandWorkspaceUiServices {
142
142
  panelManager?: PanelManager;
143
143
  profileManager?: import('@pellux/goodvibes-sdk/platform/profiles/manager').ProfileManager;
144
144
  bookmarkManager?: import('@pellux/goodvibes-sdk/platform/bookmarks/manager').BookmarkManager;
145
+ projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge/index').ProjectPlanningService;
146
+ projectPlanningProjectId?: string;
145
147
  }
146
148
 
147
149
  export interface CommandWorkspaceServices
@@ -1,532 +1,18 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
- import { CONFIG_SCHEMA, type ConfigKey } from '../../config/index.ts';
3
- import { buildGoodVibesSecretKey, isSecretConfigKey, isSecretReferenceValue, persistSecretBackedConfigValue } from '../../config/secret-config.ts';
4
- import { configSnapshotToProfileData, profileDataToConfigSnapshot } from '@pellux/goodvibes-sdk/platform/profiles/shape';
5
- import { dirname, join, resolve } from 'node:path';
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
- import { requireProfileManager, requireProviderApi, requireSecretsManager, requireShellPaths } from './runtime-services.ts';
8
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
9
-
10
- interface ConfigBundle {
11
- readonly schemaVersion: 'v1';
12
- readonly exportedAt: number;
13
- readonly config: Record<string, unknown>;
14
- readonly services?: Record<string, unknown>;
15
- readonly ecosystem?: {
16
- readonly plugins?: Record<string, unknown>;
17
- readonly skills?: Record<string, unknown>;
18
- };
19
- }
20
-
21
- function roundToPrecision(value: number, precision: number): number {
22
- const factor = 10 ** precision;
23
- return Math.round(value * factor) / factor;
24
- }
25
-
26
- function getConfigSelectionAdjustmentMeta(schema: { key: ConfigKey; type: 'boolean' | 'number' | 'string' | 'enum' }) {
27
- if (schema.type !== 'number') return {};
28
- if (schema.key === 'wrfc.scoreThreshold') {
29
- return {
30
- adjustStep: 0.1,
31
- adjustMin: 0,
32
- adjustMax: 10,
33
- adjustPrecision: 1,
34
- };
35
- }
36
- return {
37
- adjustStep: 1,
38
- };
39
- }
40
-
41
- function inspectConfigBundle(bundle: ConfigBundle): string {
42
- const ecosystemPluginCount = bundle.ecosystem?.plugins && Array.isArray((bundle.ecosystem.plugins as { entries?: unknown[] }).entries)
43
- ? ((bundle.ecosystem.plugins as { entries: unknown[] }).entries.length)
44
- : 0;
45
- const ecosystemSkillCount = bundle.ecosystem?.skills && Array.isArray((bundle.ecosystem.skills as { entries?: unknown[] }).entries)
46
- ? ((bundle.ecosystem.skills as { entries: unknown[] }).entries.length)
47
- : 0;
48
- return [
49
- 'Config Bundle Review',
50
- ` schemaVersion: ${bundle.schemaVersion}`,
51
- ` exportedAt: ${new Date(bundle.exportedAt).toISOString()}`,
52
- ` config keys: ${Object.keys(bundle.config ?? {}).length}`,
53
- ` includes services: ${bundle.services ? 'yes' : 'no'}`,
54
- ` curated plugins: ${ecosystemPluginCount}`,
55
- ` curated skills: ${ecosystemSkillCount}`,
56
- ].join('\n');
57
- }
58
-
59
- function readOptionalJson(path: string): Record<string, unknown> | undefined {
60
- if (!existsSync(path)) return undefined;
61
- try {
62
- return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>;
63
- } catch {
64
- return undefined;
65
- }
66
- }
67
-
68
- function buildConfigSnapshot(
69
- manager: { get: (key: ConfigKey) => unknown },
70
- ): Record<string, unknown> {
71
- const snapshot: Record<string, unknown> = {};
72
- for (const entry of CONFIG_SCHEMA) {
73
- try {
74
- snapshot[entry.key] = structuredClone(manager.get(entry.key));
75
- } catch {
76
- // ignore unreadable keys
77
- }
78
- }
79
- return snapshot;
80
- }
81
-
82
- function formatConfigSchemaEntry(
83
- manager: { get: (key: ConfigKey) => unknown },
84
- schema: { readonly key: ConfigKey; readonly description: string },
85
- ): string {
86
- let value: unknown = '(unavailable)';
87
- try {
88
- value = manager.get(schema.key);
89
- } catch {
90
- // keep unavailable marker
91
- }
92
- const displayValue = typeof value === 'string' && isSecretConfigKey(schema.key) && !isSecretReferenceValue(value)
93
- ? value.length === 0 ? '(empty)' : '[secret]'
94
- : String(value);
95
- return ` ${schema.key.padEnd(46)} ${displayValue} — ${schema.description}`;
96
- }
97
-
98
- function coerceValue(
99
- raw: string,
100
- type: 'boolean' | 'number' | 'string' | 'enum',
101
- enumValues?: string[],
102
- ): unknown {
103
- switch (type) {
104
- case 'boolean':
105
- if (raw === 'true' || raw === '1' || raw === 'yes') return true;
106
- if (raw === 'false' || raw === '0' || raw === 'no') return false;
107
- throw new Error(`Expected true/false, got: ${raw}`);
108
- case 'number': {
109
- const n = Number(raw);
110
- if (isNaN(n)) throw new Error(`Expected a number, got: ${raw}`);
111
- return n;
112
- }
113
- case 'enum':
114
- if (enumValues && !enumValues.includes(raw)) {
115
- throw new Error(`Expected one of: ${enumValues.join(', ')}; got: ${raw}`);
116
- }
117
- return raw;
118
- case 'string':
119
- default:
120
- return raw;
121
- }
122
- }
123
2
 
124
3
  export function registerConfigCommand(registry: CommandRegistry): void {
125
4
  registry.register({
126
5
  name: 'config',
127
6
  aliases: ['cfg'],
128
- description: 'Show or set config values',
129
- usage: '[category|key] [value] | reset [key]',
130
- argsHint: '<key> [value]',
131
- async handler(args, ctx) {
132
- const cm = ctx.platform.configManager;
133
- const categories = ['display', 'ui', 'provider', 'behavior', 'storage', 'permissions', 'surfaces', 'cloudflare', 'batch', 'tts', 'danger', 'tools'] as const;
134
-
135
- if (args[0] === 'profile') {
136
- const sub = args[1];
137
- const profileName = args[2];
138
- const pm = requireProfileManager(ctx);
139
- const currentConfig = cm.getAll();
140
-
141
- if (!sub || sub === 'list') {
142
- const profiles = pm.list();
143
- if (profiles.length === 0) {
144
- ctx.print('No saved profiles.\nUse /config profile save <name> to save current settings.');
145
- return;
146
- }
147
- const lines = ['Saved profiles:', ''];
148
- for (const p of profiles) {
149
- const date = new Date(p.timestamp).toLocaleString();
150
- lines.push(` ${p.name.padEnd(28)} ${date}`);
151
- }
152
- ctx.print(lines.join('\n'));
153
- return;
154
- }
155
-
156
- if (sub === 'save') {
157
- if (!profileName) {
158
- ctx.print('Usage: /config profile save <name>');
159
- return;
160
- }
161
- try {
162
- const data = configSnapshotToProfileData(currentConfig as Record<string, unknown>);
163
- const filePath = pm.save(profileName, data);
164
- ctx.print(`Profile saved: ${profileName}\n → ${filePath}`);
165
- } catch (e) {
166
- ctx.print(`Failed to save profile: ${summarizeError(e)}`);
167
- }
168
- return;
169
- }
170
-
171
- if (sub === 'load') {
172
- if (!profileName) {
173
- ctx.print('Usage: /config profile load <name>');
174
- return;
175
- }
176
- try {
177
- const { data } = pm.load(profileName);
178
- for (const [key, value] of Object.entries(profileDataToConfigSnapshot(data))) {
179
- const schema = CONFIG_SCHEMA.find((entry) => entry.key === key as ConfigKey);
180
- if (!schema) continue;
181
- cm.setDynamic(key as ConfigKey, value);
182
- if (key === 'provider.model') ctx.session.runtime.model = value as string;
183
- if (key === 'provider.provider') ctx.session.runtime.provider = value as string;
184
- if (key === 'provider.reasoningEffort') ctx.session.runtime.reasoningEffort = value as string;
185
- }
186
- ctx.print(`Profile loaded: ${profileName}`);
187
- ctx.renderRequest();
188
- } catch (e) {
189
- ctx.print(`Failed to load profile: ${summarizeError(e)}`);
190
- }
191
- return;
192
- }
193
-
194
- if (sub === 'delete') {
195
- if (!profileName) {
196
- ctx.print('Usage: /config profile delete <name>');
197
- return;
198
- }
199
- const deleted = pm.delete(profileName);
200
- if (deleted) ctx.print(`Profile deleted: ${profileName}`);
201
- else ctx.print(`Profile not found: ${profileName}`);
202
- return;
203
- }
204
-
205
- ctx.print(`Unknown profile subcommand: ${sub}\nUsage: /config profile save|load|list|delete <name>`);
206
- return;
207
- }
208
-
209
- if (args[0] === 'diff') {
210
- const lines = ['Changed settings:'];
211
- let changed = 0;
212
- for (const setting of CONFIG_SCHEMA) {
213
- try {
214
- const value = cm.get(setting.key);
215
- if (JSON.stringify(value) !== JSON.stringify(setting.default)) {
216
- changed++;
217
- lines.push(` ${setting.key.padEnd(36)} ${String(value)} (default: ${String(setting.default)})`);
218
- }
219
- } catch {
220
- // ignore invalid reads
221
- }
222
- }
223
- if (changed === 0) lines.push(' (none)');
224
- ctx.print(lines.join('\n'));
225
- return;
226
- }
227
-
228
- if (args[0] === 'bundle') {
229
- const sub = args[1];
230
- const bundlePath = args[2];
231
- const shellPaths = requireShellPaths(ctx);
232
- if (sub === 'export') {
233
- if (!bundlePath) {
234
- ctx.print('Usage: /config bundle export <path>');
235
- return;
236
- }
237
- const targetPath = shellPaths.resolveWorkspacePath(bundlePath);
238
- const servicesPath = shellPaths.resolveProjectPath('tui', 'services.json');
239
- const pluginCatalogPath = shellPaths.resolveProjectPath('tui', 'ecosystem', 'plugins.json');
240
- const skillCatalogPath = shellPaths.resolveProjectPath('tui', 'ecosystem', 'skills.json');
241
- const bundle: ConfigBundle = {
242
- schemaVersion: 'v1',
243
- exportedAt: Date.now(),
244
- config: buildConfigSnapshot(cm),
245
- services: readOptionalJson(servicesPath),
246
- ecosystem: {
247
- plugins: readOptionalJson(pluginCatalogPath),
248
- skills: readOptionalJson(skillCatalogPath),
249
- },
250
- };
251
- mkdirSync(dirname(targetPath), { recursive: true });
252
- writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
253
- ctx.print(`Config bundle exported to ${targetPath}`);
254
- return;
255
- }
256
-
257
- if (sub === 'inspect') {
258
- if (!bundlePath) {
259
- ctx.print('Usage: /config bundle inspect <path>');
260
- return;
261
- }
262
- const sourcePath = shellPaths.resolveWorkspacePath(bundlePath);
263
- try {
264
- const bundle = JSON.parse(readFileSync(sourcePath, 'utf-8')) as ConfigBundle;
265
- ctx.print(`${inspectConfigBundle(bundle)}\n path: ${sourcePath}`);
266
- } catch (error) {
267
- ctx.print(`Failed to read config bundle: ${summarizeError(error)}`);
268
- }
269
- return;
270
- }
271
-
272
- if (sub === 'import') {
273
- if (!bundlePath) {
274
- ctx.print('Usage: /config bundle import <path>');
275
- return;
276
- }
277
- const sourcePath = shellPaths.resolveWorkspacePath(bundlePath);
278
- let bundle: ConfigBundle;
279
- try {
280
- bundle = JSON.parse(readFileSync(sourcePath, 'utf-8')) as ConfigBundle;
281
- } catch (error) {
282
- ctx.print(`Failed to read config bundle: ${summarizeError(error)}`);
283
- return;
284
- }
285
- for (const entry of CONFIG_SCHEMA) {
286
- const value = (bundle.config as Record<string, unknown>)[entry.key];
287
- if (value === undefined) continue;
288
- cm.setDynamic(entry.key, value);
289
- if (entry.key === 'provider.model') ctx.session.runtime.model = value as string;
290
- if (entry.key === 'provider.provider') ctx.session.runtime.provider = value as string;
291
- if (entry.key === 'provider.reasoningEffort') ctx.session.runtime.reasoningEffort = value as string;
292
- }
293
-
294
- const ecosystemDir = shellPaths.resolveProjectPath('tui', 'ecosystem');
295
- if (bundle.services) {
296
- const servicesPath = shellPaths.resolveProjectPath('tui', 'services.json');
297
- mkdirSync(dirname(servicesPath), { recursive: true });
298
- writeFileSync(servicesPath, JSON.stringify(bundle.services, null, 2) + '\n', 'utf-8');
299
- }
300
- if (bundle.ecosystem?.plugins) {
301
- mkdirSync(ecosystemDir, { recursive: true });
302
- writeFileSync(join(ecosystemDir, 'plugins.json'), JSON.stringify(bundle.ecosystem.plugins, null, 2) + '\n', 'utf-8');
303
- }
304
- if (bundle.ecosystem?.skills) {
305
- mkdirSync(ecosystemDir, { recursive: true });
306
- writeFileSync(join(ecosystemDir, 'skills.json'), JSON.stringify(bundle.ecosystem.skills, null, 2) + '\n', 'utf-8');
307
- }
308
- ctx.print(`Config bundle imported from ${sourcePath}`);
309
- return;
310
- }
311
-
312
- ctx.print('Usage: /config bundle export <path> | inspect <path> | import <path>');
313
- return;
314
- }
315
-
316
- if (args[0] === 'reset') {
317
- const resetKey = args[1];
318
- if (!resetKey) {
319
- ctx.print('Usage: /config reset <key>');
320
- return;
321
- }
322
- const schema = CONFIG_SCHEMA.find((entry) => entry.key === resetKey);
323
- if (!schema) {
324
- ctx.print(`Unknown config key: ${resetKey}`);
325
- return;
326
- }
327
- try {
328
- cm.setDynamic(resetKey as ConfigKey, schema.default);
329
- if (isSecretConfigKey(resetKey)) {
330
- const secretsManager = requireSecretsManager(ctx);
331
- await secretsManager.delete(buildGoodVibesSecretKey(resetKey), { scope: 'user' });
332
- }
333
- if (resetKey === 'provider.model') ctx.session.runtime.model = schema.default as string;
334
- if (resetKey === 'provider.provider') ctx.session.runtime.provider = schema.default as string;
335
- if (resetKey === 'provider.reasoningEffort') ctx.session.runtime.reasoningEffort = schema.default as string;
336
- ctx.print(`Reset ${resetKey} to default: ${String(schema.default)}`);
337
- } catch (e) {
338
- ctx.print(`Error: ${summarizeError(e)}`);
339
- }
7
+ description: 'Open the fullscreen configuration workspace',
8
+ usage: '[category|key]',
9
+ argsHint: '[category|key]',
10
+ handler(args, ctx) {
11
+ if (ctx.openSettingsModal) {
12
+ ctx.openSettingsModal(args[0]);
340
13
  return;
341
14
  }
342
-
343
- if (args.length === 0) {
344
- if (ctx.openSelection) {
345
- const items = CONFIG_SCHEMA.map((schema) => {
346
- let current = '';
347
- try {
348
- current = String(cm.get(schema.key));
349
- } catch {
350
- current = '(unavailable)';
351
- }
352
- const toggleable = schema.type === 'boolean' || schema.type === 'enum';
353
- const adjustable = toggleable || schema.type === 'number';
354
- const adjustmentMeta = getConfigSelectionAdjustmentMeta(schema);
355
- return {
356
- id: schema.key,
357
- label: schema.key,
358
- detail: `${current} — ${schema.description}`,
359
- category: schema.key.split('.')[0],
360
- primaryAction: toggleable ? 'toggle' as const : 'select' as const,
361
- adjustable,
362
- ...adjustmentMeta,
363
- actions: schema.type === 'number'
364
- ? '[←/→] adjust [⇧←/⇧→] ±10 [Enter] inspect'
365
- : toggleable
366
- ? '[Space/Enter] toggle [←/→] adjust'
367
- : '[Enter] inspect',
368
- };
369
- });
370
- ctx.openSelection('Config Settings', items, { allowSearch: true }, (result) => {
371
- if (!result) return;
372
- const key = result.item.id as ConfigKey;
373
- const schema = CONFIG_SCHEMA.find((entry) => entry.key === key);
374
- if (!schema) return;
375
- if ((result.action === 'toggle' || result.action === 'increment' || result.action === 'decrement')
376
- && (schema.type === 'boolean' || schema.type === 'enum' || schema.type === 'number')) {
377
- const currentValue = cm.get(key);
378
- let nextValue: unknown = currentValue;
379
- if (schema.type === 'boolean') {
380
- if (result.action === 'increment') nextValue = true;
381
- else if (result.action === 'decrement') nextValue = false;
382
- else {
383
- nextValue = !Boolean(currentValue);
384
- }
385
- } else if (schema.type === 'enum' && schema.enumValues && schema.enumValues.length > 0) {
386
- const currentIndex = Math.max(0, schema.enumValues.indexOf(String(currentValue)));
387
- if (result.action === 'decrement') {
388
- nextValue = schema.enumValues[(currentIndex - 1 + schema.enumValues.length) % schema.enumValues.length]!;
389
- } else {
390
- nextValue = schema.enumValues[(currentIndex + 1) % schema.enumValues.length]!;
391
- }
392
- } else if (schema.type === 'number') {
393
- const currentNumber = Number(currentValue);
394
- const delta = result.action === 'decrement' ? -(result.step ?? 1) : (result.step ?? 1);
395
- const precision = result.item.adjustPrecision ?? 0;
396
- const rounded = roundToPrecision(currentNumber + delta, precision);
397
- const clamped = Math.min(
398
- result.item.adjustMax ?? rounded,
399
- Math.max(result.item.adjustMin ?? rounded, rounded),
400
- );
401
- nextValue = clamped;
402
- }
403
- cm.setDynamic(key, nextValue);
404
- if (key === 'provider.model') ctx.session.runtime.model = nextValue as string;
405
- if (key === 'provider.provider') ctx.session.runtime.provider = nextValue as string;
406
- if (key === 'provider.reasoningEffort') ctx.session.runtime.reasoningEffort = nextValue as string;
407
- result.item.detail = `${String(nextValue)} — ${schema.description}`;
408
- ctx.renderRequest();
409
- return;
410
- }
411
- const value = cm.get(key);
412
- const lines = [
413
- `${key}`,
414
- ` value: ${String(value)}`,
415
- ` default: ${String(schema.default)}`,
416
- ` type: ${schema.type}${schema.enumValues ? ` (${schema.enumValues.join(', ')})` : ''}`,
417
- ` desc: ${schema.description}`,
418
- ];
419
- ctx.print(lines.join('\n'));
420
- });
421
- return;
422
- }
423
- const lines: string[] = ['Config settings:'];
424
- for (const cat of categories) {
425
- lines.push(` [${cat}]`);
426
- for (const schema of CONFIG_SCHEMA.filter((entry) => entry.key.startsWith(`${cat}.`))) {
427
- lines.push(` ${formatConfigSchemaEntry(cm, schema)}`);
428
- }
429
- }
430
- ctx.print(lines.join('\n'));
431
- return;
432
- }
433
-
434
- const firstArg = args[0];
435
- if (categories.includes(firstArg as typeof categories[number]) && args.length === 1) {
436
- const cat = firstArg as typeof categories[number];
437
- const lines: string[] = [`[${cat}]`];
438
- for (const schema of CONFIG_SCHEMA.filter((entry) => entry.key.startsWith(`${cat}.`))) {
439
- lines.push(formatConfigSchemaEntry(cm, schema));
440
- }
441
- ctx.print(lines.join('\n'));
442
- return;
443
- }
444
-
445
- if (args.length === 1 && firstArg.includes('.')) {
446
- const key = firstArg as ConfigKey;
447
- const schema = CONFIG_SCHEMA.find((entry) => entry.key === key);
448
- if (!schema) {
449
- ctx.print(`Unknown config key: ${key}\nRun /config to see all keys.`);
450
- return;
451
- }
452
- try {
453
- const val = cm.get(key);
454
- const lines = [
455
- `${key}`,
456
- ` value: ${String(val)}`,
457
- ` default: ${String(schema.default)}`,
458
- ` type: ${schema.type}${schema.enumValues ? ` (${schema.enumValues.join(', ')})` : ''}`,
459
- ` desc: ${schema.description}`,
460
- ];
461
- ctx.print(lines.join('\n'));
462
- } catch (e) {
463
- ctx.print(`Error: ${summarizeError(e)}`);
464
- }
465
- return;
466
- }
467
-
468
- if (args.length >= 2 && firstArg.includes('.')) {
469
- const key = firstArg as ConfigKey;
470
- const rawValue = args.slice(1).join(' ');
471
- const schema = CONFIG_SCHEMA.find((entry) => entry.key === key);
472
- if (!schema) {
473
- ctx.print(`Unknown config key: ${key}\nRun /config to see all keys.`);
474
- return;
475
- }
476
-
477
- let coerced: unknown;
478
- try {
479
- coerced = coerceValue(rawValue, schema.type, schema.enumValues);
480
- } catch (e) {
481
- ctx.print(`Invalid value for ${key}: ${summarizeError(e)}`);
482
- return;
483
- }
484
-
485
- try {
486
- if (schema.type === 'string' && isSecretConfigKey(key)) {
487
- const configValue = await persistSecretBackedConfigValue(cm, requireSecretsManager(ctx), key, String(coerced), { scope: 'user' });
488
- ctx.print(`Set ${key} = ${configValue}`);
489
- } else {
490
- cm.setDynamic(key, coerced);
491
- ctx.print(`Set ${key} = ${String(coerced)}`);
492
- }
493
- if (key === 'provider.model') ctx.session.runtime.model = coerced as string;
494
- if (key === 'provider.provider') ctx.session.runtime.provider = coerced as string;
495
- if (key === 'provider.reasoningEffort') ctx.session.runtime.reasoningEffort = coerced as string;
496
- } catch (e) {
497
- ctx.print(`Error: ${summarizeError(e)}`);
498
- }
499
- return;
500
- }
501
-
502
- if (args.length >= 2) {
503
- const [key, ...rest] = args;
504
- const value = rest.join(' ');
505
- switch (key) {
506
- case 'system':
507
- case 'systemPrompt':
508
- ctx.session.runtime.systemPrompt = value;
509
- ctx.print('System prompt updated (runtime only; use provider.systemPromptFile for persistence).');
510
- break;
511
- case 'model':
512
- try {
513
- const selected = await requireProviderApi(ctx).selectModel(value);
514
- ctx.session.runtime.model = selected.registryKey;
515
- ctx.session.runtime.provider = selected.providerId;
516
- cm.set('provider.model', selected.registryKey);
517
- cm.set('provider.provider', selected.providerId);
518
- ctx.print(`Model set to: ${selected.displayName}`);
519
- } catch (e) {
520
- ctx.print(`Error: ${summarizeError(e)}`);
521
- }
522
- break;
523
- default:
524
- ctx.print(`Unknown config key: ${key}\nRun /config to see all keys.`);
525
- }
526
- return;
527
- }
528
-
529
- ctx.print('Usage: /config [category|key] [value]\n/config reset [key]');
15
+ ctx.print('Fullscreen config workspace is not available in this runtime.');
530
16
  },
531
17
  });
532
18
  }