@pellux/goodvibes-tui 0.20.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/platform-sandbox-qemu.ts +60 -16
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/sandbox-qemu-templates.ts +15 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
* - Feature flags tab with runtime toggle support
|
|
9
9
|
*
|
|
10
10
|
* Saves changes via configManager.set(key, value) or featureFlagManager methods.
|
|
11
|
+
*
|
|
12
|
+
* Data assembly delegates to settings-modal-data.ts (buildSettingGroups, etc.).
|
|
13
|
+
* Mutation logic delegates to settings-modal-mutations.ts (applySettingValue, etc.).
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import {
|
|
16
|
+
import { type ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
|
+
import { handleConfirmInput } from '../panels/confirm-state.ts';
|
|
14
18
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
15
19
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
16
20
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
|
-
import { getResolvedSettingLookup } from '@/runtime/index.ts';
|
|
18
21
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
19
22
|
import { buildGoodVibesSecretKey, isSecretConfigKey } from '../config/secret-config.ts';
|
|
20
23
|
import {
|
|
@@ -26,12 +29,10 @@ import {
|
|
|
26
29
|
setSecretBackedSettingValue,
|
|
27
30
|
type SettingsSecretsManager,
|
|
28
31
|
} from './settings-modal-secrets.ts';
|
|
29
|
-
import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
|
|
30
32
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
31
|
-
import type {
|
|
33
|
+
import type { FlagState } from '@/runtime/index.ts';
|
|
32
34
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
33
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
34
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
36
|
import {
|
|
36
37
|
SETTINGS_CATEGORIES,
|
|
37
38
|
SETTINGS_CATEGORY_GROUPS,
|
|
@@ -42,6 +43,23 @@ import {
|
|
|
42
43
|
type SettingsFocusPane,
|
|
43
44
|
type SubscriptionEntry,
|
|
44
45
|
} from './settings-modal-types.ts';
|
|
46
|
+
import {
|
|
47
|
+
buildSettingGroups,
|
|
48
|
+
buildFlagEntries,
|
|
49
|
+
buildMcpEntries,
|
|
50
|
+
buildSubscriptionEntries,
|
|
51
|
+
buildNetworkFilteredItems,
|
|
52
|
+
refreshEntryValues,
|
|
53
|
+
updateEntryForKey,
|
|
54
|
+
searchSettingEntries,
|
|
55
|
+
} from './settings-modal-data.ts';
|
|
56
|
+
import { getSettingLabel } from '../renderer/settings-modal-helpers.ts';
|
|
57
|
+
import {
|
|
58
|
+
applySettingValue,
|
|
59
|
+
applyFlagState,
|
|
60
|
+
persistFlagState,
|
|
61
|
+
type SettingAppliedCallback,
|
|
62
|
+
} from './settings-modal-mutations.ts';
|
|
45
63
|
|
|
46
64
|
export interface SettingsModalChange {
|
|
47
65
|
readonly key: ConfigKey;
|
|
@@ -116,6 +134,27 @@ export class SettingsModal {
|
|
|
116
134
|
/** Provider subscription entries (populated when subscriptions tab is active). */
|
|
117
135
|
public subscriptionEntries: SubscriptionEntry[] = [];
|
|
118
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Whether the user has entered search mode (pressed / or a printable key
|
|
139
|
+
* in the search input). Distinct from searchQuery.length > 0 because the
|
|
140
|
+
* user may have deleted all chars while remaining in search mode.
|
|
141
|
+
* Renderer should display the search prompt when this is true.
|
|
142
|
+
*/
|
|
143
|
+
public searchFocused = false;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Current search query. Non-empty activates cross-category search mode.
|
|
147
|
+
* Renderer should display searchResults instead of the per-category list
|
|
148
|
+
* when searchQuery is non-empty.
|
|
149
|
+
*/
|
|
150
|
+
public searchQuery = '';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Ranked cross-category results when searchQuery is non-empty.
|
|
154
|
+
* Populated by setSearchQuery(); empty when searchQuery is ''.
|
|
155
|
+
*/
|
|
156
|
+
public searchResults: SettingEntry[] = [];
|
|
157
|
+
|
|
119
158
|
/**
|
|
120
159
|
* Set after a network-category save that touches controlPlane or httpListener
|
|
121
160
|
* config keys. Renderer reads this to display a transient restart notice.
|
|
@@ -154,10 +193,10 @@ export class SettingsModal {
|
|
|
154
193
|
this.serviceRegistry = serviceRegistry;
|
|
155
194
|
this.mcpRegistry = mcpRegistry ?? null;
|
|
156
195
|
this.onSettingApplied = options?.onSettingApplied ?? null;
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
160
|
-
this.
|
|
196
|
+
this.groups = buildSettingGroups(configManager);
|
|
197
|
+
this.flagEntries = buildFlagEntries(featureFlagManager);
|
|
198
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
199
|
+
this.subscriptionEntries = buildSubscriptionEntries(subscriptionManager, serviceRegistry);
|
|
161
200
|
this.categoryIndex = 0;
|
|
162
201
|
this.selectedIndex = 0;
|
|
163
202
|
this.focusPane = 'categories';
|
|
@@ -170,6 +209,9 @@ export class SettingsModal {
|
|
|
170
209
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
171
210
|
this.lastSaveTriggeredRestart = null;
|
|
172
211
|
this.lastSettingEffectMessage = null;
|
|
212
|
+
this.searchQuery = '';
|
|
213
|
+
this.searchResults = [];
|
|
214
|
+
this.searchFocused = false;
|
|
173
215
|
this.active = true;
|
|
174
216
|
}
|
|
175
217
|
|
|
@@ -184,25 +226,55 @@ export class SettingsModal {
|
|
|
184
226
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
185
227
|
this.lastSaveTriggeredRestart = null;
|
|
186
228
|
this.lastSettingEffectMessage = null;
|
|
229
|
+
this.searchQuery = '';
|
|
230
|
+
this.searchResults = [];
|
|
231
|
+
this.searchFocused = false;
|
|
187
232
|
this.serviceRegistry = null;
|
|
188
233
|
this.secretsManager = null;
|
|
189
234
|
this.onSettingApplied = null;
|
|
190
235
|
this.focusPane = 'settings';
|
|
191
236
|
}
|
|
192
237
|
|
|
238
|
+
/** Enter search mode (focus the search input bar). */
|
|
239
|
+
focusSearch(): void {
|
|
240
|
+
this.searchFocused = true;
|
|
241
|
+
this.selectedIndex = 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Exit search mode without clearing the query. */
|
|
245
|
+
blurSearch(): void {
|
|
246
|
+
this.searchFocused = false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Update the search query and recompute cross-category ranked results.
|
|
251
|
+
* Setting an empty string clears search mode.
|
|
252
|
+
*/
|
|
253
|
+
setSearchQuery(query: string): void {
|
|
254
|
+
this.searchQuery = query;
|
|
255
|
+
this.searchFocused = true;
|
|
256
|
+
if (query.trim().length === 0) {
|
|
257
|
+
this.searchResults = [];
|
|
258
|
+
} else {
|
|
259
|
+
this.searchResults = searchSettingEntries(query, this.groups, getSettingLabel);
|
|
260
|
+
}
|
|
261
|
+
this.selectedIndex = 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Clear search query, results, and exit search focus mode. */
|
|
265
|
+
clearSearch(): void {
|
|
266
|
+
this.searchQuery = '';
|
|
267
|
+
this.searchResults = [];
|
|
268
|
+
this.searchFocused = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
193
271
|
/** Cycle to the next category (Tab). */
|
|
194
272
|
nextCategory(): void {
|
|
195
273
|
if (this.editingMode) return;
|
|
196
274
|
this.categoryIndex = (this.categoryIndex + 1) % SETTINGS_CATEGORIES.length;
|
|
197
275
|
this.selectedIndex = 0;
|
|
198
276
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
199
|
-
|
|
200
|
-
this._loadFlagEntries();
|
|
201
|
-
} else if (this.currentCategory === 'mcp') {
|
|
202
|
-
this._loadMcpEntries();
|
|
203
|
-
} else if (this.currentCategory === 'subscriptions') {
|
|
204
|
-
this._loadSubscriptionEntries();
|
|
205
|
-
}
|
|
277
|
+
this._reloadTabEntries();
|
|
206
278
|
}
|
|
207
279
|
|
|
208
280
|
/** Cycle to the previous category (Shift+Tab). */
|
|
@@ -211,13 +283,7 @@ export class SettingsModal {
|
|
|
211
283
|
this.categoryIndex = (this.categoryIndex - 1 + SETTINGS_CATEGORIES.length) % SETTINGS_CATEGORIES.length;
|
|
212
284
|
this.selectedIndex = 0;
|
|
213
285
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
214
|
-
|
|
215
|
-
this._loadFlagEntries();
|
|
216
|
-
} else if (this.currentCategory === 'mcp') {
|
|
217
|
-
this._loadMcpEntries();
|
|
218
|
-
} else if (this.currentCategory === 'subscriptions') {
|
|
219
|
-
this._loadSubscriptionEntries();
|
|
220
|
-
}
|
|
286
|
+
this._reloadTabEntries();
|
|
221
287
|
}
|
|
222
288
|
|
|
223
289
|
focusCategories(): void {
|
|
@@ -247,6 +313,12 @@ export class SettingsModal {
|
|
|
247
313
|
|
|
248
314
|
moveUp(): void {
|
|
249
315
|
if (this.editingMode) return;
|
|
316
|
+
if (this.searchFocused) {
|
|
317
|
+
if (this.searchResults.length > 0) {
|
|
318
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.searchResults.length) % this.searchResults.length;
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
250
322
|
const items = this._currentItems();
|
|
251
323
|
if (items.length === 0) {
|
|
252
324
|
if (this.currentCategory === 'flags' && this.flagEntries.length > 0) {
|
|
@@ -265,6 +337,12 @@ export class SettingsModal {
|
|
|
265
337
|
|
|
266
338
|
moveDown(): void {
|
|
267
339
|
if (this.editingMode) return;
|
|
340
|
+
if (this.searchFocused) {
|
|
341
|
+
if (this.searchResults.length > 0) {
|
|
342
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.searchResults.length;
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
268
346
|
const items = this._currentItems();
|
|
269
347
|
if (items.length === 0) {
|
|
270
348
|
if (this.currentCategory === 'flags' && this.flagEntries.length > 0) {
|
|
@@ -282,6 +360,9 @@ export class SettingsModal {
|
|
|
282
360
|
}
|
|
283
361
|
|
|
284
362
|
getSelected(): SettingEntry | null {
|
|
363
|
+
if (this.searchFocused && this.searchResults.length > 0) {
|
|
364
|
+
return this.searchResults[Math.max(0, Math.min(this.searchResults.length - 1, this.selectedIndex))] ?? null;
|
|
365
|
+
}
|
|
285
366
|
const items = this._currentItems();
|
|
286
367
|
if (items.length === 0) return null;
|
|
287
368
|
return items[Math.max(0, Math.min(items.length - 1, this.selectedIndex))] ?? null;
|
|
@@ -356,13 +437,9 @@ export class SettingsModal {
|
|
|
356
437
|
const entry = this.getSelectedSubscription();
|
|
357
438
|
if (!entry) return;
|
|
358
439
|
if (entry.state === 'active' || entry.state === 'pending') {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
this.subscriptionManager?.logout(entry.provider);
|
|
364
|
-
this._loadSubscriptionEntries();
|
|
365
|
-
this.subscriptionLogoutConfirmationTarget = null;
|
|
440
|
+
// First press: arm the confirm gate. Subsequent key handling routes
|
|
441
|
+
// through handleSubscriptionLogoutKey() before normal dispatch.
|
|
442
|
+
this.subscriptionLogoutConfirmationTarget = entry.provider;
|
|
366
443
|
}
|
|
367
444
|
return;
|
|
368
445
|
}
|
|
@@ -394,11 +471,11 @@ export class SettingsModal {
|
|
|
394
471
|
|
|
395
472
|
if (setting.type === 'boolean') {
|
|
396
473
|
const newVal = !entry.currentValue;
|
|
397
|
-
this._setValue(setting.key, newVal);
|
|
474
|
+
this._setValue(setting.key as ConfigKey, newVal);
|
|
398
475
|
} else if (setting.type === 'enum' && setting.enumValues) {
|
|
399
476
|
const idx = setting.enumValues.indexOf(entry.currentValue as string);
|
|
400
477
|
const nextIdx = (idx + 1) % setting.enumValues.length;
|
|
401
|
-
this._setValue(setting.key, setting.enumValues[nextIdx]);
|
|
478
|
+
this._setValue(setting.key as ConfigKey, setting.enumValues[nextIdx]);
|
|
402
479
|
} else if (setting.type === 'string' || setting.type === 'number') {
|
|
403
480
|
// Enter inline edit mode
|
|
404
481
|
this.editingMode = true;
|
|
@@ -406,6 +483,31 @@ export class SettingsModal {
|
|
|
406
483
|
}
|
|
407
484
|
}
|
|
408
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Handle a keystroke while a subscription logout confirm is pending.
|
|
488
|
+
*
|
|
489
|
+
* Follows the project-standard confirm contract (confirm-state.ts):
|
|
490
|
+
* - CONFIRM: Enter, Return, or y → executes logout, clears target
|
|
491
|
+
* - CANCEL: Esc or n → clears target, no logout
|
|
492
|
+
* - ABSORBED: any other key → keeps confirm pending, swallows key
|
|
493
|
+
* - INACTIVE: no confirm pending → returns 'inactive' (caller continues)
|
|
494
|
+
*/
|
|
495
|
+
handleSubscriptionLogoutKey(key: string): 'confirmed' | 'cancelled' | 'absorbed' | 'inactive' {
|
|
496
|
+
const target = this.subscriptionLogoutConfirmationTarget;
|
|
497
|
+
if (!target) return 'inactive';
|
|
498
|
+
const confirmState = { subject: target, label: target };
|
|
499
|
+
const result = handleConfirmInput(confirmState, key);
|
|
500
|
+
if (result === 'confirmed') {
|
|
501
|
+
this.subscriptionManager?.logout(target);
|
|
502
|
+
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
503
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
504
|
+
} else if (result === 'cancelled') {
|
|
505
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
506
|
+
}
|
|
507
|
+
// 'absorbed': confirm remains pending
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
|
|
409
511
|
adjustSelected(direction: 'left' | 'right', step = 1): void {
|
|
410
512
|
if (this.editingMode) return;
|
|
411
513
|
|
|
@@ -413,7 +515,7 @@ export class SettingsModal {
|
|
|
413
515
|
const flagEntry = this.getSelectedFlag();
|
|
414
516
|
if (!flagEntry || flagEntry.state === 'killed' || !this.featureFlagManager || !this.configManager) return;
|
|
415
517
|
const targetState: FlagState = direction === 'right' ? 'enabled' : 'disabled';
|
|
416
|
-
if (flagEntry.state !== targetState)
|
|
518
|
+
if (flagEntry.state !== targetState) applyFlagState(flagEntry, targetState, this.featureFlagManager, this.configManager);
|
|
417
519
|
return;
|
|
418
520
|
}
|
|
419
521
|
|
|
@@ -426,7 +528,7 @@ export class SettingsModal {
|
|
|
426
528
|
? (currentIndex + 1) % modes.length
|
|
427
529
|
: (currentIndex - 1 + modes.length) % modes.length;
|
|
428
530
|
this.mcpRegistry.setServerTrustMode(entry.name, modes[nextIndex]!);
|
|
429
|
-
this.
|
|
531
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
430
532
|
this.mcpAllowAllConfirmationTarget = null;
|
|
431
533
|
return;
|
|
432
534
|
}
|
|
@@ -436,7 +538,7 @@ export class SettingsModal {
|
|
|
436
538
|
const { setting } = entry;
|
|
437
539
|
|
|
438
540
|
if (setting.type === 'boolean') {
|
|
439
|
-
this._setValue(setting.key, direction === 'right');
|
|
541
|
+
this._setValue(setting.key as ConfigKey, direction === 'right');
|
|
440
542
|
return;
|
|
441
543
|
}
|
|
442
544
|
|
|
@@ -445,7 +547,7 @@ export class SettingsModal {
|
|
|
445
547
|
const nextIndex = direction === 'right'
|
|
446
548
|
? (currentIndex + 1) % setting.enumValues.length
|
|
447
549
|
: (currentIndex - 1 + setting.enumValues.length) % setting.enumValues.length;
|
|
448
|
-
this._setValue(setting.key, setting.enumValues[nextIndex]!);
|
|
550
|
+
this._setValue(setting.key as ConfigKey, setting.enumValues[nextIndex]!);
|
|
449
551
|
return;
|
|
450
552
|
}
|
|
451
553
|
|
|
@@ -460,7 +562,7 @@ export class SettingsModal {
|
|
|
460
562
|
Math.max(adjustment.min ?? rounded, rounded),
|
|
461
563
|
);
|
|
462
564
|
if (setting.validate && !setting.validate(nextValue)) return;
|
|
463
|
-
this._setValue(setting.key, nextValue);
|
|
565
|
+
this._setValue(setting.key as ConfigKey, nextValue);
|
|
464
566
|
}
|
|
465
567
|
}
|
|
466
568
|
|
|
@@ -474,38 +576,13 @@ export class SettingsModal {
|
|
|
474
576
|
const flagEntry = this.getSelectedFlag();
|
|
475
577
|
if (!flagEntry || !this.featureFlagManager || !this.configManager) return;
|
|
476
578
|
|
|
477
|
-
const {
|
|
579
|
+
const { state } = flagEntry;
|
|
478
580
|
|
|
479
581
|
// Killed flags are blocked
|
|
480
582
|
if (state === 'killed') return;
|
|
481
583
|
|
|
482
584
|
const newState: FlagState = state === 'enabled' ? 'disabled' : 'enabled';
|
|
483
|
-
|
|
484
|
-
this._setSelectedFlagState(flagEntry, newState);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
private _setSelectedFlagState(flagEntry: FlagEntry, newState: FlagState): void {
|
|
488
|
-
if (!this.featureFlagManager || !this.configManager) return;
|
|
489
|
-
const { flag } = flagEntry;
|
|
490
|
-
|
|
491
|
-
if (!flag.runtimeToggleable) {
|
|
492
|
-
// Persist to config only — takes effect on restart
|
|
493
|
-
this._persistFlagState(flag.id, newState, flag.defaultState as FlagState);
|
|
494
|
-
flagEntry.state = newState;
|
|
495
|
-
} else {
|
|
496
|
-
// Toggle immediately in manager
|
|
497
|
-
try {
|
|
498
|
-
if (newState === 'enabled') {
|
|
499
|
-
this.featureFlagManager.enable(flag.id);
|
|
500
|
-
} else {
|
|
501
|
-
this.featureFlagManager.disable(flag.id);
|
|
502
|
-
}
|
|
503
|
-
this._persistFlagState(flag.id, newState, flag.defaultState as FlagState);
|
|
504
|
-
flagEntry.state = newState;
|
|
505
|
-
} catch (e) {
|
|
506
|
-
logger.error('SettingsModal: failed to toggle feature flag', { flag: flag.id, error: summarizeError(e) });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
585
|
+
applyFlagState(flagEntry, newState, this.featureFlagManager, this.configManager);
|
|
509
586
|
}
|
|
510
587
|
|
|
511
588
|
/**
|
|
@@ -524,7 +601,7 @@ export class SettingsModal {
|
|
|
524
601
|
return false;
|
|
525
602
|
}
|
|
526
603
|
this.mcpRegistry.setServerTrustMode(entry.name, 'allow-all');
|
|
527
|
-
this.
|
|
604
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
528
605
|
this.editingMode = false;
|
|
529
606
|
this.editBuffer = '';
|
|
530
607
|
this.mcpAllowAllConfirmationTarget = null;
|
|
@@ -545,7 +622,7 @@ export class SettingsModal {
|
|
|
545
622
|
return false;
|
|
546
623
|
}
|
|
547
624
|
this.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
548
|
-
this.
|
|
625
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
549
626
|
this.editingMode = false;
|
|
550
627
|
this.editBuffer = '';
|
|
551
628
|
this.mcpAllowAllConfirmationTarget = null;
|
|
@@ -582,7 +659,7 @@ export class SettingsModal {
|
|
|
582
659
|
setConfigValue: (key, value) => this._setValue(key, value),
|
|
583
660
|
});
|
|
584
661
|
} else {
|
|
585
|
-
this._setValue(setting.key, parsed);
|
|
662
|
+
this._setValue(setting.key as ConfigKey, parsed);
|
|
586
663
|
}
|
|
587
664
|
this.editingMode = false;
|
|
588
665
|
this.editBuffer = '';
|
|
@@ -621,173 +698,58 @@ export class SettingsModal {
|
|
|
621
698
|
this.editBuffer = this.editBuffer.slice(0, -1);
|
|
622
699
|
}
|
|
623
700
|
|
|
624
|
-
// ── Private helpers
|
|
625
|
-
|
|
626
|
-
private _loadGroups(configManager: ConfigManager): void {
|
|
627
|
-
this.groups.clear();
|
|
628
|
-
for (const cat of SETTINGS_CATEGORIES) {
|
|
629
|
-
if (cat === 'flags') continue; // flags tab handled separately
|
|
630
|
-
this.groups.set(cat, []);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
for (const setting of CONFIG_SCHEMA) {
|
|
634
|
-
const rawCat = setting.key.split('.')[0] as string;
|
|
635
|
-
const cat = rawCat as SettingsCategory;
|
|
636
|
-
const currentValue = configManager.get(setting.key as ConfigKey);
|
|
637
|
-
const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
|
|
638
|
-
const entry: SettingEntry = {
|
|
639
|
-
setting,
|
|
640
|
-
currentValue,
|
|
641
|
-
isDefault: currentValue === setting.default,
|
|
642
|
-
effectiveSource: resolved?.effectiveSource,
|
|
643
|
-
locked: resolved?.locked,
|
|
644
|
-
conflict: resolved?.conflict,
|
|
645
|
-
sourceLabel: resolved?.sourceLabel,
|
|
646
|
-
lockReason: resolved?.lockReason,
|
|
647
|
-
};
|
|
648
|
-
if (this.groups.has(cat)) this.groups.get(cat)!.push(entry);
|
|
649
|
-
if ((rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') && this.groups.has('network')) {
|
|
650
|
-
this.groups.get('network')!.push(entry);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const uiEntries = this.groups.get('ui');
|
|
655
|
-
if (uiEntries) {
|
|
656
|
-
const uiPriority: Record<string, number> = {
|
|
657
|
-
'ui.systemMessages': 0,
|
|
658
|
-
'ui.operationalMessages': 1,
|
|
659
|
-
'ui.wrfcMessages': 2,
|
|
660
|
-
'ui.voiceEnabled': 3,
|
|
661
|
-
};
|
|
662
|
-
uiEntries.sort((a, b) => (uiPriority[a.setting.key] ?? 99) - (uiPriority[b.setting.key] ?? 99));
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/** Load or refresh the flags tab entries from the feature flag manager. */
|
|
667
|
-
private _loadFlagEntries(): void {
|
|
668
|
-
if (!this.featureFlagManager) {
|
|
669
|
-
this.flagEntries = [];
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
this.flagEntries = Array.from(this.featureFlagManager.getAll().values()).map(({ flag, state }) => ({
|
|
673
|
-
flag,
|
|
674
|
-
state,
|
|
675
|
-
}));
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
private _loadMcpEntries(): void {
|
|
679
|
-
if (!this.mcpRegistry) {
|
|
680
|
-
this.mcpEntries = [];
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
this.mcpEntries = this.mcpRegistry.listServerSecurity().map((entry) => ({
|
|
684
|
-
name: entry.name,
|
|
685
|
-
connected: entry.connected,
|
|
686
|
-
role: entry.role,
|
|
687
|
-
trustMode: entry.trustMode,
|
|
688
|
-
allowedPaths: [...entry.allowedPaths],
|
|
689
|
-
allowedHosts: [...entry.allowedHosts],
|
|
690
|
-
}));
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
private _loadSubscriptionEntries(): void {
|
|
694
|
-
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Persist a flag state override to config.
|
|
699
|
-
* Deletes the entry when reverting to defaultState. Skips killed state.
|
|
700
|
-
*/
|
|
701
|
-
private _persistFlagState(flagId: string, newState: FlagState, defaultState: FlagState): void {
|
|
702
|
-
if (!this.configManager) return;
|
|
703
|
-
if (newState === 'killed') return; // never persist killed state
|
|
701
|
+
// ── Private helpers ────────────────────────────────────────────────────────────────
|
|
704
702
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.configManager.mergeCategory('featureFlags', current);
|
|
714
|
-
} catch (e) {
|
|
715
|
-
logger.error('SettingsModal: failed to persist flag state', { flagId, error: summarizeError(e) });
|
|
703
|
+
/** Reload flag/mcp/subscription entries when the active tab changes. */
|
|
704
|
+
private _reloadTabEntries(): void {
|
|
705
|
+
if (this.currentCategory === 'flags') {
|
|
706
|
+
this.flagEntries = buildFlagEntries(this.featureFlagManager);
|
|
707
|
+
} else if (this.currentCategory === 'mcp') {
|
|
708
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
709
|
+
} else if (this.currentCategory === 'subscriptions') {
|
|
710
|
+
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
716
711
|
}
|
|
717
712
|
}
|
|
718
713
|
|
|
719
|
-
/** Returns [] for the flags
|
|
714
|
+
/** Returns [] for the flags/mcp/subscriptions categories. */
|
|
720
715
|
private _currentItems(): SettingEntry[] {
|
|
721
|
-
if (
|
|
716
|
+
if (
|
|
717
|
+
this.currentCategory === 'flags'
|
|
718
|
+
|| this.currentCategory === 'mcp'
|
|
719
|
+
|| this.currentCategory === 'subscriptions'
|
|
720
|
+
) return [];
|
|
722
721
|
const items = this.groups.get(this.currentCategory) ?? [];
|
|
723
722
|
if (this.currentCategory === 'network') {
|
|
724
|
-
return items.
|
|
725
|
-
if (entry.setting.key === 'controlPlane.host') {
|
|
726
|
-
const hostMode = this.configManager?.get('controlPlane.hostMode');
|
|
727
|
-
return hostMode === 'custom';
|
|
728
|
-
}
|
|
729
|
-
if (entry.setting.key === 'httpListener.host') {
|
|
730
|
-
const hostMode = this.configManager?.get('httpListener.hostMode');
|
|
731
|
-
return hostMode === 'custom';
|
|
732
|
-
}
|
|
733
|
-
if (entry.setting.key === 'web.host') {
|
|
734
|
-
const hostMode = this.configManager?.get('web.hostMode');
|
|
735
|
-
return hostMode === 'custom';
|
|
736
|
-
}
|
|
737
|
-
return true;
|
|
738
|
-
});
|
|
723
|
+
return buildNetworkFilteredItems(items, this.configManager);
|
|
739
724
|
}
|
|
740
725
|
return items;
|
|
741
726
|
}
|
|
742
727
|
|
|
743
|
-
private _refreshAllEntries(): void {
|
|
744
|
-
if (!this.configManager) return;
|
|
745
|
-
for (const entries of this.groups.values()) {
|
|
746
|
-
for (const entry of entries) {
|
|
747
|
-
entry.currentValue = this.configManager.get(entry.setting.key as ConfigKey);
|
|
748
|
-
entry.isDefault = entry.currentValue === entry.setting.default;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
728
|
private _setValue(key: ConfigKey, value: unknown): void {
|
|
754
729
|
if (!this.configManager) return;
|
|
755
|
-
// Diff previous value before writing — avoids false restart notices on no-op saves
|
|
756
|
-
const previousValue = this.configManager.get(key);
|
|
757
|
-
const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
|
|
758
|
-
try {
|
|
759
|
-
this.configManager.setDynamic(key, value);
|
|
760
|
-
const rawCat = key.split('.')[0] as string;
|
|
761
|
-
if (rawCat === 'controlPlane') {
|
|
762
|
-
if (isRestartKey && previousValue !== value) {
|
|
763
|
-
this.lastSaveTriggeredRestart = 'control-plane';
|
|
764
|
-
}
|
|
765
|
-
} else if (rawCat === 'httpListener') {
|
|
766
|
-
if (isRestartKey && previousValue !== value) {
|
|
767
|
-
this.lastSaveTriggeredRestart = 'http-listener';
|
|
768
|
-
}
|
|
769
|
-
} else if (rawCat === 'web') {
|
|
770
|
-
if (isRestartKey && previousValue !== value) {
|
|
771
|
-
this.lastSaveTriggeredRestart = 'web';
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
730
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
731
|
+
const callback: SettingAppliedCallback | null = this.onSettingApplied
|
|
732
|
+
? (change) => this.onSettingApplied!(change)
|
|
733
|
+
: null;
|
|
734
|
+
|
|
735
|
+
const result = applySettingValue({
|
|
736
|
+
key,
|
|
737
|
+
value,
|
|
738
|
+
configManager: this.configManager,
|
|
739
|
+
groups: this.groups,
|
|
740
|
+
onSettingApplied: callback,
|
|
741
|
+
refreshGroups: () => {
|
|
742
|
+
if (this.configManager) refreshEntryValues(this.groups, this.configManager);
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
if (result.restartDomain !== null) {
|
|
747
|
+
this.lastSaveTriggeredRestart = result.restartDomain;
|
|
748
|
+
}
|
|
749
|
+
if (result.effectMessage !== null) {
|
|
750
|
+
this.lastSettingEffectMessage = result.effectMessage;
|
|
790
751
|
}
|
|
752
|
+
// No-op (result.changed === false, effectMessage === null): leave lastSettingEffectMessage untouched.
|
|
791
753
|
}
|
|
792
754
|
|
|
793
755
|
}
|