@pellux/goodvibes-tui 0.19.53 → 0.19.55
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 +35 -0
- package/README.md +10 -13
- package/docs/foundation-artifacts/knowledge-store.sql +27 -0
- package/docs/foundation-artifacts/operator-contract.json +15736 -7265
- 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 +111 -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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SettingsModal — state management for the /settings config
|
|
2
|
+
* SettingsModal — state management for the /settings and /config fullscreen workspace.
|
|
3
3
|
*
|
|
4
4
|
* Loads CONFIG_SCHEMA, groups settings by category, and tracks UI state:
|
|
5
5
|
* - Active category (Tab to cycle)
|
|
@@ -14,11 +14,9 @@ import { CONFIG_SCHEMA, type ConfigKey, type PersistedFlagState } from '@pellux/
|
|
|
14
14
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
15
15
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
|
|
16
16
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
|
|
17
|
-
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
|
|
18
|
-
import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
19
17
|
import { getResolvedSettingLookup } from '@pellux/goodvibes-sdk/platform/runtime/settings/control-plane';
|
|
20
18
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
21
|
-
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
19
|
+
import { buildGoodVibesSecretKey, isSecretConfigKey } from '../config/secret-config.ts';
|
|
22
20
|
import {
|
|
23
21
|
getNumericAdjustmentMeta,
|
|
24
22
|
modelPickerLaunchForKey,
|
|
@@ -28,6 +26,7 @@ import {
|
|
|
28
26
|
setSecretBackedSettingValue,
|
|
29
27
|
type SettingsSecretsManager,
|
|
30
28
|
} from './settings-modal-secrets.ts';
|
|
29
|
+
import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
|
|
31
30
|
import type { FeatureFlagManager } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/index';
|
|
32
31
|
import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
|
|
33
32
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp/registry';
|
|
@@ -39,6 +38,7 @@ import {
|
|
|
39
38
|
type McpEntry,
|
|
40
39
|
type SettingEntry,
|
|
41
40
|
type SettingsCategory,
|
|
41
|
+
type SettingsFocusPane,
|
|
42
42
|
type SubscriptionEntry,
|
|
43
43
|
} from './settings-modal-types.ts';
|
|
44
44
|
|
|
@@ -48,6 +48,7 @@ export {
|
|
|
48
48
|
type McpEntry,
|
|
49
49
|
type SettingEntry,
|
|
50
50
|
type SettingsCategory,
|
|
51
|
+
type SettingsFocusPane,
|
|
51
52
|
type SubscriptionEntry,
|
|
52
53
|
} from './settings-modal-types.ts';
|
|
53
54
|
|
|
@@ -64,6 +65,9 @@ export class SettingsModal {
|
|
|
64
65
|
/** Selected setting index within the current category. */
|
|
65
66
|
public selectedIndex = 0;
|
|
66
67
|
|
|
68
|
+
/** Which pane receives up/down navigation and Enter/Space actions. */
|
|
69
|
+
public focusPane: SettingsFocusPane = 'settings';
|
|
70
|
+
|
|
67
71
|
/** Whether we're in inline edit mode for the selected string/number setting. */
|
|
68
72
|
public editingMode = false;
|
|
69
73
|
|
|
@@ -79,6 +83,8 @@ export class SettingsModal {
|
|
|
79
83
|
public pendingModelPickerTarget: ModelPickerTarget | null = null;
|
|
80
84
|
/** Set when the highlighted setting should open provider selection before model selection. */
|
|
81
85
|
public pendingProviderModelPickerTarget: ModelPickerTarget | null = null;
|
|
86
|
+
/** Set when a highlighted setting needs an external picker owned by the shell route. */
|
|
87
|
+
public pendingSettingsPickerAction: 'tts-provider' | 'tts-voice' | null = null;
|
|
82
88
|
/** Provider awaiting explicit logout confirmation, if any. */
|
|
83
89
|
public subscriptionLogoutConfirmationTarget: string | null = null;
|
|
84
90
|
|
|
@@ -132,10 +138,12 @@ export class SettingsModal {
|
|
|
132
138
|
this._loadSubscriptionEntries();
|
|
133
139
|
this.categoryIndex = 0;
|
|
134
140
|
this.selectedIndex = 0;
|
|
141
|
+
this.focusPane = 'settings';
|
|
135
142
|
this.editingMode = false;
|
|
136
143
|
this.editBuffer = '';
|
|
137
144
|
this.pendingModelPickerTarget = null;
|
|
138
145
|
this.pendingProviderModelPickerTarget = null;
|
|
146
|
+
this.pendingSettingsPickerAction = null;
|
|
139
147
|
this.mcpAllowAllConfirmationTarget = null;
|
|
140
148
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
141
149
|
this.lastSaveTriggeredRestart = null;
|
|
@@ -148,11 +156,13 @@ export class SettingsModal {
|
|
|
148
156
|
this.editBuffer = '';
|
|
149
157
|
this.pendingModelPickerTarget = null;
|
|
150
158
|
this.pendingProviderModelPickerTarget = null;
|
|
159
|
+
this.pendingSettingsPickerAction = null;
|
|
151
160
|
this.mcpAllowAllConfirmationTarget = null;
|
|
152
161
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
153
162
|
this.lastSaveTriggeredRestart = null;
|
|
154
163
|
this.serviceRegistry = null;
|
|
155
164
|
this.secretsManager = null;
|
|
165
|
+
this.focusPane = 'settings';
|
|
156
166
|
}
|
|
157
167
|
|
|
158
168
|
/** Cycle to the next category (Tab). */
|
|
@@ -185,6 +195,31 @@ export class SettingsModal {
|
|
|
185
195
|
}
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
focusCategories(): void {
|
|
199
|
+
if (this.editingMode) return;
|
|
200
|
+
this.focusPane = 'categories';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
focusSettings(): void {
|
|
204
|
+
if (this.editingMode) return;
|
|
205
|
+
this.focusPane = 'settings';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
toggleFocusPane(): void {
|
|
209
|
+
if (this.editingMode) return;
|
|
210
|
+
this.focusPane = this.focusPane === 'settings' ? 'categories' : 'settings';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
moveFocusedUp(): void {
|
|
214
|
+
if (this.focusPane === 'categories') this.prevCategory();
|
|
215
|
+
else this.moveUp();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
moveFocusedDown(): void {
|
|
219
|
+
if (this.focusPane === 'categories') this.nextCategory();
|
|
220
|
+
else this.moveDown();
|
|
221
|
+
}
|
|
222
|
+
|
|
188
223
|
moveUp(): void {
|
|
189
224
|
if (this.editingMode) return;
|
|
190
225
|
const items = this._currentItems();
|
|
@@ -222,23 +257,28 @@ export class SettingsModal {
|
|
|
222
257
|
}
|
|
223
258
|
|
|
224
259
|
getSelected(): SettingEntry | null {
|
|
225
|
-
|
|
260
|
+
const items = this._currentItems();
|
|
261
|
+
if (items.length === 0) return null;
|
|
262
|
+
return items[Math.max(0, Math.min(items.length - 1, this.selectedIndex))] ?? null;
|
|
226
263
|
}
|
|
227
264
|
|
|
228
265
|
/** Get the currently selected flag entry (flags tab only). */
|
|
229
266
|
getSelectedFlag(): FlagEntry | null {
|
|
230
267
|
if (this.currentCategory !== 'flags') return null;
|
|
231
|
-
|
|
268
|
+
if (this.flagEntries.length === 0) return null;
|
|
269
|
+
return this.flagEntries[Math.max(0, Math.min(this.flagEntries.length - 1, this.selectedIndex))] ?? null;
|
|
232
270
|
}
|
|
233
271
|
|
|
234
272
|
getSelectedMcp(): McpEntry | null {
|
|
235
273
|
if (this.currentCategory !== 'mcp') return null;
|
|
236
|
-
|
|
274
|
+
if (this.mcpEntries.length === 0) return null;
|
|
275
|
+
return this.mcpEntries[Math.max(0, Math.min(this.mcpEntries.length - 1, this.selectedIndex))] ?? null;
|
|
237
276
|
}
|
|
238
277
|
|
|
239
278
|
getSelectedSubscription(): SubscriptionEntry | null {
|
|
240
279
|
if (this.currentCategory !== 'subscriptions') return null;
|
|
241
|
-
|
|
280
|
+
if (this.subscriptionEntries.length === 0) return null;
|
|
281
|
+
return this.subscriptionEntries[Math.max(0, Math.min(this.subscriptionEntries.length - 1, this.selectedIndex))] ?? null;
|
|
242
282
|
}
|
|
243
283
|
|
|
244
284
|
get currentCategory(): SettingsCategory {
|
|
@@ -249,6 +289,31 @@ export class SettingsModal {
|
|
|
249
289
|
return this._currentItems();
|
|
250
290
|
}
|
|
251
291
|
|
|
292
|
+
selectTarget(target?: string): void {
|
|
293
|
+
const normalized = target?.trim();
|
|
294
|
+
if (!normalized) return;
|
|
295
|
+
|
|
296
|
+
const categoryIndex = SETTINGS_CATEGORIES.indexOf(normalized as SettingsCategory);
|
|
297
|
+
if (categoryIndex >= 0) {
|
|
298
|
+
this.categoryIndex = categoryIndex;
|
|
299
|
+
this.selectedIndex = 0;
|
|
300
|
+
this.focusPane = 'settings';
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (let index = 0; index < SETTINGS_CATEGORIES.length; index += 1) {
|
|
305
|
+
const category = SETTINGS_CATEGORIES[index]!;
|
|
306
|
+
const entries = this.groups.get(category) ?? [];
|
|
307
|
+
const entryIndex = entries.findIndex((entry) => entry.setting.key === normalized);
|
|
308
|
+
if (entryIndex >= 0) {
|
|
309
|
+
this.categoryIndex = index;
|
|
310
|
+
this.selectedIndex = entryIndex;
|
|
311
|
+
this.focusPane = 'settings';
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
252
317
|
/**
|
|
253
318
|
* Toggle boolean or begin cycling enum values, or enter edit mode for string/number.
|
|
254
319
|
*/
|
|
@@ -283,6 +348,15 @@ export class SettingsModal {
|
|
|
283
348
|
const { setting } = entry;
|
|
284
349
|
|
|
285
350
|
// Delegate provider/model picker settings to the model picker UI
|
|
351
|
+
if (setting.key === 'tts.provider') {
|
|
352
|
+
this.pendingSettingsPickerAction = 'tts-provider';
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (setting.key === 'tts.voice') {
|
|
356
|
+
this.pendingSettingsPickerAction = 'tts-voice';
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
286
360
|
const pickerLaunch = modelPickerLaunchForKey(setting.key);
|
|
287
361
|
if (pickerLaunch !== null) {
|
|
288
362
|
if (pickerLaunch.flow === 'providerModel') {
|
|
@@ -497,6 +571,20 @@ export class SettingsModal {
|
|
|
497
571
|
this.mcpAllowAllConfirmationTarget = null;
|
|
498
572
|
}
|
|
499
573
|
|
|
574
|
+
resetSelected(): { key: ConfigKey; value: unknown } | null {
|
|
575
|
+
if (this.editingMode || !this.configManager) return null;
|
|
576
|
+
const entry = this.getSelected();
|
|
577
|
+
if (!entry) return null;
|
|
578
|
+
const key = entry.setting.key as ConfigKey;
|
|
579
|
+
this._setValue(key, entry.setting.default);
|
|
580
|
+
if (isSecretConfigKey(key) && this.secretsManager) {
|
|
581
|
+
void this.secretsManager.delete(buildGoodVibesSecretKey(key), { scope: 'user' }).catch((error) => {
|
|
582
|
+
logger.error('SettingsModal: failed to clear secret while resetting setting', { key, error: summarizeError(error) });
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return { key, value: entry.setting.default };
|
|
586
|
+
}
|
|
587
|
+
|
|
500
588
|
/** Handle a keystroke in edit mode: regular chars appended, Backspace removes last char. */
|
|
501
589
|
editChar(char: string): void {
|
|
502
590
|
if (!this.editingMode) return;
|
|
@@ -519,21 +607,7 @@ export class SettingsModal {
|
|
|
519
607
|
|
|
520
608
|
for (const setting of CONFIG_SCHEMA) {
|
|
521
609
|
const rawCat = setting.key.split('.')[0] as string;
|
|
522
|
-
|
|
523
|
-
// Route controlPlane.* and httpListener.* into the network group
|
|
524
|
-
let cat: SettingsCategory;
|
|
525
|
-
if (rawCat === 'helper') {
|
|
526
|
-
cat = 'tools';
|
|
527
|
-
} else if (rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') {
|
|
528
|
-
cat = 'network';
|
|
529
|
-
} else if (rawCat === 'surfaces') {
|
|
530
|
-
cat = 'surfaces';
|
|
531
|
-
} else if (rawCat === 'cloudflare' || rawCat === 'batch') {
|
|
532
|
-
cat = 'cloudflare';
|
|
533
|
-
} else {
|
|
534
|
-
cat = rawCat as SettingsCategory;
|
|
535
|
-
}
|
|
536
|
-
if (!this.groups.has(cat)) continue;
|
|
610
|
+
const cat = rawCat as SettingsCategory;
|
|
537
611
|
const currentValue = configManager.get(setting.key as ConfigKey);
|
|
538
612
|
const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
|
|
539
613
|
const entry: SettingEntry = {
|
|
@@ -546,7 +620,10 @@ export class SettingsModal {
|
|
|
546
620
|
sourceLabel: resolved?.sourceLabel,
|
|
547
621
|
lockReason: resolved?.lockReason,
|
|
548
622
|
};
|
|
549
|
-
this.groups.get(cat)!.push(entry);
|
|
623
|
+
if (this.groups.has(cat)) this.groups.get(cat)!.push(entry);
|
|
624
|
+
if ((rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') && this.groups.has('network')) {
|
|
625
|
+
this.groups.get('network')!.push(entry);
|
|
626
|
+
}
|
|
550
627
|
}
|
|
551
628
|
|
|
552
629
|
const uiEntries = this.groups.get('ui');
|
|
@@ -589,94 +666,7 @@ export class SettingsModal {
|
|
|
589
666
|
}
|
|
590
667
|
|
|
591
668
|
private _loadSubscriptionEntries(): void {
|
|
592
|
-
|
|
593
|
-
if (!manager) {
|
|
594
|
-
this.subscriptionEntries = [];
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
const services = this.serviceRegistry?.getAll() ?? {};
|
|
598
|
-
const providers = new Map<string, SubscriptionEntry>();
|
|
599
|
-
const builtinProviders = new Set(listBuiltinSubscriptionProviders().map((builtin) => builtin.provider));
|
|
600
|
-
|
|
601
|
-
const determineFreshness = (expiresAt?: number): ProviderAuthFreshness => {
|
|
602
|
-
if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) return 'healthy';
|
|
603
|
-
if (expiresAt <= Date.now()) return 'expired';
|
|
604
|
-
if (expiresAt <= Date.now() + 24 * 60 * 60 * 1000) return 'expiring';
|
|
605
|
-
return 'healthy';
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
for (const provider of builtinProviders) {
|
|
609
|
-
providers.set(provider, {
|
|
610
|
-
provider,
|
|
611
|
-
state: 'available',
|
|
612
|
-
oauthConfigured: true,
|
|
613
|
-
preferredRoute: 'subscription',
|
|
614
|
-
activeRoute: 'unconfigured',
|
|
615
|
-
authFreshness: 'unconfigured',
|
|
616
|
-
routeReason: 'Built-in subscription adapter is available, but no active subscription session is stored yet.',
|
|
617
|
-
nextActions: [`Use /subscription login ${provider} start to begin browser sign-in.`],
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
for (const service of Object.values(services)) {
|
|
622
|
-
if (service.authType === 'oauth' && service.oauth) {
|
|
623
|
-
const provider = service.providerId ?? service.name;
|
|
624
|
-
providers.set(provider, {
|
|
625
|
-
provider,
|
|
626
|
-
state: 'available',
|
|
627
|
-
oauthConfigured: true,
|
|
628
|
-
preferredRoute: 'subscription',
|
|
629
|
-
activeRoute: providers.get(provider)?.activeRoute ?? 'unconfigured',
|
|
630
|
-
authFreshness: providers.get(provider)?.authFreshness ?? 'unconfigured',
|
|
631
|
-
routeReason: providers.get(provider)?.routeReason ?? 'OAuth metadata is configured for this provider.',
|
|
632
|
-
nextActions: providers.get(provider)?.nextActions ?? [`Use /subscription login ${provider} start to begin browser sign-in.`],
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
for (const pending of manager.listPending()) {
|
|
638
|
-
providers.set(pending.provider, {
|
|
639
|
-
provider: pending.provider,
|
|
640
|
-
state: 'pending',
|
|
641
|
-
oauthConfigured: providers.get(pending.provider)?.oauthConfigured ?? false,
|
|
642
|
-
preferredRoute: 'subscription',
|
|
643
|
-
activeRoute: 'unconfigured',
|
|
644
|
-
authFreshness: 'pending',
|
|
645
|
-
routeReason: 'OAuth login is pending completion for this provider.',
|
|
646
|
-
nextActions: [`Finish /subscription login ${pending.provider} finish <code> to activate this session.`],
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
for (const subscription of manager.list()) {
|
|
651
|
-
const freshness = determineFreshness(subscription.expiresAt);
|
|
652
|
-
const issues = freshness === 'expired'
|
|
653
|
-
? ['Stored subscription session is expired and needs refresh.']
|
|
654
|
-
: freshness === 'expiring'
|
|
655
|
-
? ['Stored subscription session expires within 24 hours.']
|
|
656
|
-
: [];
|
|
657
|
-
const nextActions = freshness === 'expired'
|
|
658
|
-
? [`Refresh or replace the ${subscription.provider} subscription session.`]
|
|
659
|
-
: freshness === 'expiring'
|
|
660
|
-
? [`Verify or renew the ${subscription.provider} subscription session soon.`]
|
|
661
|
-
: [];
|
|
662
|
-
providers.set(subscription.provider, {
|
|
663
|
-
provider: subscription.provider,
|
|
664
|
-
state: 'active',
|
|
665
|
-
tokenType: subscription.tokenType,
|
|
666
|
-
expiresAt: subscription.expiresAt,
|
|
667
|
-
oauthConfigured: providers.get(subscription.provider)?.oauthConfigured ?? builtinProviders.has(subscription.provider),
|
|
668
|
-
activeRoute: freshness === 'expired' ? 'unconfigured' : 'subscription',
|
|
669
|
-
preferredRoute: 'subscription',
|
|
670
|
-
authFreshness: freshness,
|
|
671
|
-
routeReason: subscription.overrideAmbientApiKeys
|
|
672
|
-
? 'Subscription route overrides ambient API-key resolution for this provider.'
|
|
673
|
-
: 'Subscription route is stored for supported flows without automatically replacing ambient API-key resolution.',
|
|
674
|
-
issues,
|
|
675
|
-
nextActions,
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
this.subscriptionEntries = [...providers.values()].sort((a, b) => a.provider.localeCompare(b.provider));
|
|
669
|
+
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
680
670
|
}
|
|
681
671
|
|
|
682
672
|
/**
|
|
@@ -706,7 +696,6 @@ export class SettingsModal {
|
|
|
706
696
|
if (this.currentCategory === 'flags' || this.currentCategory === 'mcp' || this.currentCategory === 'subscriptions') return [];
|
|
707
697
|
const items = this.groups.get(this.currentCategory) ?? [];
|
|
708
698
|
if (this.currentCategory === 'network') {
|
|
709
|
-
// Hide host fields when the corresponding hostMode is not 'custom'
|
|
710
699
|
return items.filter(entry => {
|
|
711
700
|
if (entry.setting.key === 'controlPlane.host') {
|
|
712
701
|
const hostMode = this.configManager?.get('controlPlane.hostMode');
|
|
@@ -733,40 +722,23 @@ export class SettingsModal {
|
|
|
733
722
|
const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
|
|
734
723
|
try {
|
|
735
724
|
this.configManager.setDynamic(key, value);
|
|
736
|
-
// Update the cached entry in-place — avoids full schema re-scan on each edit
|
|
737
725
|
const rawCat = key.split('.')[0] as string;
|
|
738
|
-
|
|
739
|
-
let cat: SettingsCategory;
|
|
740
|
-
if (rawCat === 'helper') {
|
|
741
|
-
cat = 'tools';
|
|
742
|
-
} else if (rawCat === 'controlPlane') {
|
|
743
|
-
cat = 'network';
|
|
744
|
-
// SDK auto-restarts the daemon server on controlPlane binding changes
|
|
726
|
+
if (rawCat === 'controlPlane') {
|
|
745
727
|
if (isRestartKey && previousValue !== value) {
|
|
746
728
|
this.lastSaveTriggeredRestart = 'control-plane';
|
|
747
729
|
}
|
|
748
730
|
} else if (rawCat === 'httpListener') {
|
|
749
|
-
cat = 'network';
|
|
750
|
-
// SDK auto-restarts the HTTP listener on binding changes
|
|
751
731
|
if (isRestartKey && previousValue !== value) {
|
|
752
732
|
this.lastSaveTriggeredRestart = 'http-listener';
|
|
753
733
|
}
|
|
754
734
|
} else if (rawCat === 'web') {
|
|
755
|
-
cat = 'network';
|
|
756
|
-
// SDK auto-restarts the web server on binding changes
|
|
757
735
|
if (isRestartKey && previousValue !== value) {
|
|
758
736
|
this.lastSaveTriggeredRestart = 'web';
|
|
759
737
|
}
|
|
760
|
-
} else if (rawCat === 'surfaces') {
|
|
761
|
-
cat = 'surfaces';
|
|
762
|
-
} else if (rawCat === 'cloudflare' || rawCat === 'batch') {
|
|
763
|
-
cat = 'cloudflare';
|
|
764
|
-
} else {
|
|
765
|
-
cat = rawCat as SettingsCategory;
|
|
766
738
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const entry = entries.find(
|
|
739
|
+
|
|
740
|
+
for (const entries of this.groups.values()) {
|
|
741
|
+
const entry = entries.find((candidate) => candidate.setting.key === key);
|
|
770
742
|
if (entry) {
|
|
771
743
|
entry.currentValue = this.configManager!.get(key);
|
|
772
744
|
entry.isDefault = entry.currentValue === entry.setting.default;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config/schema';
|
|
2
|
+
import type { CommandContext } from './command-registry.ts';
|
|
3
|
+
import type { SelectionItem } from './selection-modal.ts';
|
|
4
|
+
|
|
5
|
+
function getStreamingTtsProviders(ctx: CommandContext): Array<{ id: string; label: string; capabilities: readonly string[] }> {
|
|
6
|
+
const registry = ctx.platform.voiceProviderRegistry;
|
|
7
|
+
if (!registry) return [];
|
|
8
|
+
return registry.list().filter((provider) => provider.capabilities.includes('tts-stream'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setTtsConfigValue(ctx: CommandContext, key: ConfigKey, value: string): void {
|
|
12
|
+
ctx.platform.configManager.setDynamic(key, value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function openTtsProviderPicker(ctx: CommandContext): boolean {
|
|
16
|
+
if (!ctx.openSelection) return false;
|
|
17
|
+
const registry = ctx.platform.voiceProviderRegistry;
|
|
18
|
+
if (!registry) {
|
|
19
|
+
ctx.print('Voice provider registry is not available in this runtime.');
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const providers = getStreamingTtsProviders(ctx);
|
|
24
|
+
if (providers.length === 0) {
|
|
25
|
+
ctx.print('No streaming TTS providers are registered.');
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const current = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
|
|
30
|
+
const items: SelectionItem[] = providers.map((provider) => ({
|
|
31
|
+
id: provider.id,
|
|
32
|
+
label: provider.label,
|
|
33
|
+
detail: provider.id === current ? `${provider.id} (current)` : provider.id,
|
|
34
|
+
category: 'streaming TTS providers',
|
|
35
|
+
primaryAction: 'select',
|
|
36
|
+
actions: '[Enter] set provider',
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
ctx.openSelection('Choose TTS Provider', items, { preSelectId: current, allowSearch: true }, (result) => {
|
|
40
|
+
if (!result) return;
|
|
41
|
+
const previous = String(ctx.platform.configManager.get('tts.provider') ?? '').trim();
|
|
42
|
+
setTtsConfigValue(ctx, 'tts.provider', result.item.id);
|
|
43
|
+
if (previous && previous !== result.item.id) {
|
|
44
|
+
setTtsConfigValue(ctx, 'tts.voice', '');
|
|
45
|
+
ctx.print(`TTS provider set to ${result.item.id}. TTS voice was cleared because voices are provider-specific.`);
|
|
46
|
+
} else {
|
|
47
|
+
ctx.print(`TTS provider set to ${result.item.id}.`);
|
|
48
|
+
}
|
|
49
|
+
ctx.renderRequest();
|
|
50
|
+
});
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function openTtsVoicePicker(ctx: CommandContext, providerArg?: string): Promise<boolean> {
|
|
55
|
+
if (!ctx.openSelection) return false;
|
|
56
|
+
const service = ctx.platform.voiceService;
|
|
57
|
+
if (!service) {
|
|
58
|
+
ctx.print('Voice service is not available in this runtime.');
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const providerId = (providerArg ?? String(ctx.platform.configManager.get('tts.provider') ?? '')).trim() || undefined;
|
|
63
|
+
try {
|
|
64
|
+
const voices = await service.listVoices(providerId);
|
|
65
|
+
if (voices.length === 0) {
|
|
66
|
+
ctx.print(providerId ? `No voices returned for ${providerId}.` : 'No TTS voices returned.');
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const current = String(ctx.platform.configManager.get('tts.voice') ?? '').trim();
|
|
70
|
+
const items: SelectionItem[] = [
|
|
71
|
+
{
|
|
72
|
+
id: '__default__',
|
|
73
|
+
label: 'Use provider default voice',
|
|
74
|
+
detail: current ? 'clears tts.voice' : '(current)',
|
|
75
|
+
category: 'voice',
|
|
76
|
+
primaryAction: 'select',
|
|
77
|
+
},
|
|
78
|
+
...voices.map((voice) => ({
|
|
79
|
+
id: voice.id,
|
|
80
|
+
label: voice.label || voice.id,
|
|
81
|
+
detail: voice.id === current ? `${voice.id} (current)` : voice.id,
|
|
82
|
+
category: providerId ?? 'voices',
|
|
83
|
+
primaryAction: 'select' as const,
|
|
84
|
+
actions: '[Enter] set voice',
|
|
85
|
+
})),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
ctx.openSelection(`Choose TTS Voice${providerId ? ` (${providerId})` : ''}`, items, { preSelectId: current || '__default__', allowSearch: true }, (result) => {
|
|
89
|
+
if (!result) return;
|
|
90
|
+
const nextVoice = result.item.id === '__default__' ? '' : result.item.id;
|
|
91
|
+
setTtsConfigValue(ctx, 'tts.voice', nextVoice);
|
|
92
|
+
ctx.print(nextVoice ? `TTS voice set to ${nextVoice}.` : 'TTS voice cleared. The provider default voice will be used.');
|
|
93
|
+
ctx.renderRequest();
|
|
94
|
+
});
|
|
95
|
+
return true;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
ctx.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -56,6 +56,9 @@ import {
|
|
|
56
56
|
attachSpokenTurnModelRouting,
|
|
57
57
|
createSpokenTurnInputOptions,
|
|
58
58
|
} from './audio/spoken-turn-model-routing.ts';
|
|
59
|
+
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
60
|
+
import { ProjectPlanningCoordinator } from './planning/project-planning-coordinator.ts';
|
|
61
|
+
import { buildCommandArgsHint } from './input/command-args-hint.ts';
|
|
59
62
|
|
|
60
63
|
const ALT_SCREEN_ENTER = '\x1b[?1049h';
|
|
61
64
|
const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
@@ -240,7 +243,8 @@ async function main() {
|
|
|
240
243
|
process.removeListener('SIGINT', sigintHandler);
|
|
241
244
|
process.removeListener('unhandledRejection', unhandledRejectionHandler);
|
|
242
245
|
const exitScreen = cli.flags.noAltScreen ? CLEAR_SCREEN : CLEAR_SCREEN + ALT_SCREEN_EXIT;
|
|
243
|
-
stdout.write(PASTE_DISABLE + KEYBOARD_EXT_DISABLE + MOUSE_DISABLE + CURSOR_SHOW + exitScreen);
|
|
246
|
+
allowTerminalWrite(() => stdout.write(PASTE_DISABLE + KEYBOARD_EXT_DISABLE + MOUSE_DISABLE + CURSOR_SHOW + exitScreen));
|
|
247
|
+
terminalOutputGuard.dispose();
|
|
244
248
|
stdin.setRawMode(false);
|
|
245
249
|
process.exit(0);
|
|
246
250
|
};
|
|
@@ -261,6 +265,17 @@ async function main() {
|
|
|
261
265
|
configManager,
|
|
262
266
|
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
263
267
|
}));
|
|
268
|
+
const projectPlanningCoordinator = new ProjectPlanningCoordinator({
|
|
269
|
+
service: ctx.services.projectPlanningService,
|
|
270
|
+
projectId: ctx.services.projectPlanningProjectId,
|
|
271
|
+
workingDirectory: workingDir,
|
|
272
|
+
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
273
|
+
openPanel: () => {
|
|
274
|
+
panelManager.open('project-planning');
|
|
275
|
+
panelManager.show();
|
|
276
|
+
render();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
264
279
|
|
|
265
280
|
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
266
281
|
input.clearModalStack();
|
|
@@ -296,13 +311,36 @@ async function main() {
|
|
|
296
311
|
}
|
|
297
312
|
}
|
|
298
313
|
if (processedText || content) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
314
|
+
void (async () => {
|
|
315
|
+
let inputOptions = options.spokenOutput ? createSpokenTurnInputOptions() : undefined;
|
|
316
|
+
if (!options.spokenOutput && processedText) {
|
|
317
|
+
try {
|
|
318
|
+
const planning = await projectPlanningCoordinator.prepareTurn(processedText);
|
|
319
|
+
if (planning) {
|
|
320
|
+
conversation.addSystemMessage(planning.systemMessage);
|
|
321
|
+
inputOptions = {
|
|
322
|
+
origin: {
|
|
323
|
+
source: 'project-planning',
|
|
324
|
+
surface: 'tui',
|
|
325
|
+
metadata: {
|
|
326
|
+
projectId: ctx.services.projectPlanningProjectId,
|
|
327
|
+
knowledgeSpaceId: planning.state.knowledgeSpaceId,
|
|
328
|
+
readiness: planning.evaluation.readiness,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
systemMessageRouter.high(`[Planning] ${summarizeError(err)}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (options.spokenOutput && processedText) {
|
|
338
|
+
spokenTurns.submitNextTurn(processedText);
|
|
339
|
+
}
|
|
340
|
+
orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
|
|
341
|
+
logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
|
|
342
|
+
});
|
|
343
|
+
})();
|
|
306
344
|
} else {
|
|
307
345
|
render();
|
|
308
346
|
}
|
|
@@ -345,7 +383,7 @@ async function main() {
|
|
|
345
383
|
commandContext.scrollToLine = scrollToLine;
|
|
346
384
|
commandContext.clearScreen = () => {
|
|
347
385
|
compositor.resetDiff();
|
|
348
|
-
stdout.write(CLEAR_SCREEN);
|
|
386
|
+
allowTerminalWrite(() => stdout.write(CLEAR_SCREEN));
|
|
349
387
|
render();
|
|
350
388
|
};
|
|
351
389
|
permissionPromptRef.requestPermission = (request) =>
|
|
@@ -469,41 +507,7 @@ async function main() {
|
|
|
469
507
|
const runningProcessCount = processManager.list().filter((p) => !p.status.startsWith('done')).length;
|
|
470
508
|
const cw = getPromptContentWidth();
|
|
471
509
|
const promptInfo = input.getWrappedPromptInfo(cw);
|
|
472
|
-
|
|
473
|
-
const commandArgsHint = (() => {
|
|
474
|
-
const p = input.prompt;
|
|
475
|
-
if (!p.startsWith('/')) return undefined;
|
|
476
|
-
// Extract the command name (everything up to first space)
|
|
477
|
-
const spaceIdx = p.indexOf(' ');
|
|
478
|
-
if (spaceIdx !== -1) {
|
|
479
|
-
// User has already typed args — check for subcommand hints
|
|
480
|
-
const cmdName = p.slice(1, spaceIdx);
|
|
481
|
-
const cmd = commandRegistry.get(cmdName);
|
|
482
|
-
if (!cmd) return undefined;
|
|
483
|
-
// Sub-command awareness: check if there's a matching sub-hint pattern
|
|
484
|
-
const afterCmd = p.slice(spaceIdx + 1);
|
|
485
|
-
const subSpaceIdx = afterCmd.indexOf(' ');
|
|
486
|
-
if (subSpaceIdx !== -1) return undefined; // deeper args, no hint
|
|
487
|
-
// User typed one subcommand word, check for known subcommand hints
|
|
488
|
-
const subHints: Record<string, Record<string, string>> = {
|
|
489
|
-
session: { rename: '<name>', resume: '<id|name>', info: '<id>', export: '<id> [format]', search: '<query>', delete: '<id>' },
|
|
490
|
-
template: { save: '<name>', use: '<name> [args]', edit: '<name>', delete: '<name>' },
|
|
491
|
-
secrets: { set: '<KEY> <value>', get: '<KEY>', delete: '<KEY>' },
|
|
492
|
-
permissions: { tool: '<name> allow|prompt|deny' },
|
|
493
|
-
config: { reset: '<key>' },
|
|
494
|
-
danger: {},
|
|
495
|
-
plugin: { enable: '<name>', disable: '<name>', reload: '' },
|
|
496
|
-
};
|
|
497
|
-
const subMap = subHints[cmdName];
|
|
498
|
-
if (subMap && afterCmd in subMap) return subMap[afterCmd];
|
|
499
|
-
return undefined;
|
|
500
|
-
}
|
|
501
|
-
// No space yet — user is still typing the command name
|
|
502
|
-
const cmdName = p.slice(1);
|
|
503
|
-
const cmd = commandRegistry.get(cmdName);
|
|
504
|
-
if (!cmd) return undefined;
|
|
505
|
-
return cmd.argsHint ?? cmd.usage;
|
|
506
|
-
})();
|
|
510
|
+
const commandArgsHint = buildCommandArgsHint(input.prompt, commandRegistry);
|
|
507
511
|
const composerState = deriveComposerState({
|
|
508
512
|
text: input.prompt,
|
|
509
513
|
commandMode: input.commandMode,
|
|
@@ -661,6 +665,7 @@ async function main() {
|
|
|
661
665
|
panelWidth: panelComposite.panelWidth,
|
|
662
666
|
});
|
|
663
667
|
};
|
|
668
|
+
const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, notify: (message) => { systemMessageRouter.low(message); render(); } });
|
|
664
669
|
|
|
665
670
|
setRenderRequest(render);
|
|
666
671
|
orchestratorRefs.requestRender = render;
|
|
@@ -726,7 +731,7 @@ async function main() {
|
|
|
726
731
|
stdin.setRawMode(true);
|
|
727
732
|
stdin.resume();
|
|
728
733
|
stdin.setEncoding('utf8');
|
|
729
|
-
stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE);
|
|
734
|
+
allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE));
|
|
730
735
|
|
|
731
736
|
applyInitialTuiCliState({
|
|
732
737
|
cli,
|
|
@@ -5,6 +5,7 @@ import { ThinkingPanel } from '../thinking-panel.ts';
|
|
|
5
5
|
import { ToolInspectorPanel } from '../tool-inspector-panel.ts';
|
|
6
6
|
import { WrfcPanel } from '../wrfc-panel.ts';
|
|
7
7
|
import { SchedulePanel } from '../schedule-panel.ts';
|
|
8
|
+
import { ProjectPlanningPanel } from '../project-planning-panel.ts';
|
|
8
9
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
9
10
|
import { requireAutomationManager, requireUiServices } from './shared.ts';
|
|
10
11
|
|
|
@@ -78,6 +79,20 @@ export function registerAgentPanels(manager: PanelManager, deps: ResolvedBuiltin
|
|
|
78
79
|
},
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
manager.registerType({
|
|
83
|
+
id: 'project-planning',
|
|
84
|
+
name: 'Planning',
|
|
85
|
+
icon: 'P',
|
|
86
|
+
category: 'agent',
|
|
87
|
+
description: 'Passive project planning artifacts: readiness, questions, decisions, language, task graph, and agent handoff metadata',
|
|
88
|
+
preload: true,
|
|
89
|
+
factory: () => new ProjectPlanningPanel({
|
|
90
|
+
service: deps.projectPlanningService,
|
|
91
|
+
projectId: deps.projectPlanningProjectId,
|
|
92
|
+
requestRender: deps.requestRender,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
81
96
|
manager.registerType({
|
|
82
97
|
id: 'schedule',
|
|
83
98
|
name: 'Schedule',
|