@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.
- package/CHANGELOG.md +41 -0
- package/README.md +10 -13
- package/docs/foundation-artifacts/knowledge-store.sql +27 -0
- package/docs/foundation-artifacts/operator-contract.json +15850 -7037
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +4 -1
- package/src/input/command-args-hint.ts +36 -0
- package/src/input/command-registry.ts +3 -1
- package/src/input/commands/config.ts +7 -521
- package/src/input/commands/knowledge.ts +164 -1
- package/src/input/commands/local-runtime.ts +0 -80
- package/src/input/commands/operator-runtime.ts +3 -3
- package/src/input/commands/planning-runtime.ts +83 -34
- package/src/input/commands/shell-core.ts +2 -34
- package/src/input/commands/tts-runtime.ts +1 -389
- package/src/input/commands.ts +0 -2
- package/src/input/handler-modal-routes.ts +61 -7
- package/src/input/handler-modal-token-routes.ts +1 -0
- package/src/input/handler-picker-routes.ts +50 -4
- package/src/input/model-picker-provider-filter.ts +28 -0
- package/src/input/model-picker-types.ts +12 -0
- package/src/input/model-picker.ts +65 -23
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-behavior.ts +2 -0
- package/src/input/settings-modal-subscriptions.ts +95 -0
- package/src/input/settings-modal-types.ts +50 -3
- package/src/input/settings-modal.ts +106 -134
- package/src/input/tts-settings-actions.ts +100 -0
- package/src/main.ts +50 -45
- package/src/panels/builtin/agent.ts +15 -0
- package/src/panels/builtin/shared.ts +17 -0
- package/src/panels/project-planning-panel.ts +370 -0
- package/src/planning/project-planning-coordinator.ts +249 -0
- package/src/renderer/compositor.ts +2 -1
- package/src/renderer/conversation-overlays.ts +4 -5
- package/src/renderer/model-workspace.ts +488 -0
- package/src/renderer/settings-modal-helpers.ts +16 -1
- package/src/renderer/settings-modal.ts +616 -716
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/services.ts +33 -2
- package/src/runtime/terminal-output-guard.ts +228 -0
- package/src/runtime/ui-services.ts +4 -0
- package/src/shell/ui-openers.ts +59 -3
- package/src/utils/clipboard.ts +2 -1
- package/src/version.ts +1 -1
- 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.
|
|
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.
|
|
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'
|
|
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: '
|
|
129
|
-
usage: '[category|key]
|
|
130
|
-
argsHint: '
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
}
|