@pellux/goodvibes-tui 0.20.3 → 0.22.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 +50 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +4 -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 +658 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/entrypoint.ts +6 -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 +31 -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 +14 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/context-auto-compact.ts +77 -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/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +32 -1
- package/src/input/commands/control-room-runtime.ts +10 -10
- 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/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/provider.ts +57 -3
- 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 +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -4
- 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 +128 -12
- package/src/input/handler-modal-token-routes.ts +22 -5
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +73 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +6 -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 +14 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -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 +24 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +378 -0
- package/src/input/settings-modal-mutations.ts +157 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +236 -232
- package/src/main.ts +93 -85
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +5 -1
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- 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/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/session-maintenance.ts +66 -15
- 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 +118 -13
- 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/context-status-hint.ts +54 -0
- 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 +90 -10
- package/src/renderer/shell-surface.ts +10 -0
- 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 +18 -0
- package/src/runtime/bootstrap-core.ts +145 -13
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +9 -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/services.ts +27 -1
- 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/panels/knowledge-panel.ts +0 -345
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -8,15 +8,18 @@
|
|
|
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
|
-
import {
|
|
22
|
+
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
20
23
|
import {
|
|
21
24
|
getNumericAdjustmentMeta,
|
|
22
25
|
modelPickerLaunchForKey,
|
|
@@ -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
|
-
|
|
34
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
35
|
+
|
|
35
36
|
import {
|
|
36
37
|
SETTINGS_CATEGORIES,
|
|
37
38
|
SETTINGS_CATEGORY_GROUPS,
|
|
@@ -42,6 +43,30 @@ 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';
|
|
63
|
+
import {
|
|
64
|
+
resetSelected as _resetSelected,
|
|
65
|
+
initiateResetCategory as _initiateResetCategory,
|
|
66
|
+
initiateResetAll as _initiateResetAll,
|
|
67
|
+
handleResetConfirmKey as _handleResetConfirmKey,
|
|
68
|
+
type ResetConfirmKeyResult,
|
|
69
|
+
} from './settings-modal-reset.ts';
|
|
45
70
|
|
|
46
71
|
export interface SettingsModalChange {
|
|
47
72
|
readonly key: ConfigKey;
|
|
@@ -106,6 +131,11 @@ export class SettingsModal {
|
|
|
106
131
|
/** Provider awaiting explicit logout confirmation, if any. */
|
|
107
132
|
public subscriptionLogoutConfirmationTarget: string | null = null;
|
|
108
133
|
|
|
134
|
+
/** Pending category-reset confirmation gate, or null when inactive. */
|
|
135
|
+
public resetCategoryConfirm: { readonly subject: string } | null = null;
|
|
136
|
+
/** Pending reset-all confirmation gate, or null when inactive. */
|
|
137
|
+
public resetAllConfirm: { readonly subject: 'all' } | null = null;
|
|
138
|
+
|
|
109
139
|
/** Settings grouped by category. */
|
|
110
140
|
public groups: Map<SettingsCategory, SettingEntry[]> = new Map();
|
|
111
141
|
|
|
@@ -116,6 +146,27 @@ export class SettingsModal {
|
|
|
116
146
|
/** Provider subscription entries (populated when subscriptions tab is active). */
|
|
117
147
|
public subscriptionEntries: SubscriptionEntry[] = [];
|
|
118
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Whether the user has entered search mode (pressed / or a printable key
|
|
151
|
+
* in the search input). Distinct from searchQuery.length > 0 because the
|
|
152
|
+
* user may have deleted all chars while remaining in search mode.
|
|
153
|
+
* Renderer should display the search prompt when this is true.
|
|
154
|
+
*/
|
|
155
|
+
public searchFocused = false;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Current search query. Non-empty activates cross-category search mode.
|
|
159
|
+
* Renderer should display searchResults instead of the per-category list
|
|
160
|
+
* when searchQuery is non-empty.
|
|
161
|
+
*/
|
|
162
|
+
public searchQuery = '';
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Ranked cross-category results when searchQuery is non-empty.
|
|
166
|
+
* Populated by setSearchQuery(); empty when searchQuery is ''.
|
|
167
|
+
*/
|
|
168
|
+
public searchResults: SettingEntry[] = [];
|
|
169
|
+
|
|
119
170
|
/**
|
|
120
171
|
* Set after a network-category save that touches controlPlane or httpListener
|
|
121
172
|
* config keys. Renderer reads this to display a transient restart notice.
|
|
@@ -154,10 +205,10 @@ export class SettingsModal {
|
|
|
154
205
|
this.serviceRegistry = serviceRegistry;
|
|
155
206
|
this.mcpRegistry = mcpRegistry ?? null;
|
|
156
207
|
this.onSettingApplied = options?.onSettingApplied ?? null;
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
160
|
-
this.
|
|
208
|
+
this.groups = buildSettingGroups(configManager);
|
|
209
|
+
this.flagEntries = buildFlagEntries(featureFlagManager);
|
|
210
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
211
|
+
this.subscriptionEntries = buildSubscriptionEntries(subscriptionManager, serviceRegistry);
|
|
161
212
|
this.categoryIndex = 0;
|
|
162
213
|
this.selectedIndex = 0;
|
|
163
214
|
this.focusPane = 'categories';
|
|
@@ -170,6 +221,9 @@ export class SettingsModal {
|
|
|
170
221
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
171
222
|
this.lastSaveTriggeredRestart = null;
|
|
172
223
|
this.lastSettingEffectMessage = null;
|
|
224
|
+
this.searchQuery = '';
|
|
225
|
+
this.searchResults = [];
|
|
226
|
+
this.searchFocused = false;
|
|
173
227
|
this.active = true;
|
|
174
228
|
}
|
|
175
229
|
|
|
@@ -184,25 +238,55 @@ export class SettingsModal {
|
|
|
184
238
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
185
239
|
this.lastSaveTriggeredRestart = null;
|
|
186
240
|
this.lastSettingEffectMessage = null;
|
|
241
|
+
this.searchQuery = '';
|
|
242
|
+
this.searchResults = [];
|
|
243
|
+
this.searchFocused = false;
|
|
187
244
|
this.serviceRegistry = null;
|
|
188
245
|
this.secretsManager = null;
|
|
189
246
|
this.onSettingApplied = null;
|
|
190
247
|
this.focusPane = 'settings';
|
|
191
248
|
}
|
|
192
249
|
|
|
250
|
+
/** Enter search mode (focus the search input bar). */
|
|
251
|
+
focusSearch(): void {
|
|
252
|
+
this.searchFocused = true;
|
|
253
|
+
this.selectedIndex = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Exit search mode without clearing the query. */
|
|
257
|
+
blurSearch(): void {
|
|
258
|
+
this.searchFocused = false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Update the search query and recompute cross-category ranked results.
|
|
263
|
+
* Setting an empty string clears search mode.
|
|
264
|
+
*/
|
|
265
|
+
setSearchQuery(query: string): void {
|
|
266
|
+
this.searchQuery = query;
|
|
267
|
+
this.searchFocused = true;
|
|
268
|
+
if (query.trim().length === 0) {
|
|
269
|
+
this.searchResults = [];
|
|
270
|
+
} else {
|
|
271
|
+
this.searchResults = searchSettingEntries(query, this.groups, getSettingLabel);
|
|
272
|
+
}
|
|
273
|
+
this.selectedIndex = 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Clear search query, results, and exit search focus mode. */
|
|
277
|
+
clearSearch(): void {
|
|
278
|
+
this.searchQuery = '';
|
|
279
|
+
this.searchResults = [];
|
|
280
|
+
this.searchFocused = false;
|
|
281
|
+
}
|
|
282
|
+
|
|
193
283
|
/** Cycle to the next category (Tab). */
|
|
194
284
|
nextCategory(): void {
|
|
195
285
|
if (this.editingMode) return;
|
|
196
286
|
this.categoryIndex = (this.categoryIndex + 1) % SETTINGS_CATEGORIES.length;
|
|
197
287
|
this.selectedIndex = 0;
|
|
198
288
|
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
|
-
}
|
|
289
|
+
this._reloadTabEntries();
|
|
206
290
|
}
|
|
207
291
|
|
|
208
292
|
/** Cycle to the previous category (Shift+Tab). */
|
|
@@ -211,13 +295,7 @@ export class SettingsModal {
|
|
|
211
295
|
this.categoryIndex = (this.categoryIndex - 1 + SETTINGS_CATEGORIES.length) % SETTINGS_CATEGORIES.length;
|
|
212
296
|
this.selectedIndex = 0;
|
|
213
297
|
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
|
-
}
|
|
298
|
+
this._reloadTabEntries();
|
|
221
299
|
}
|
|
222
300
|
|
|
223
301
|
focusCategories(): void {
|
|
@@ -247,6 +325,12 @@ export class SettingsModal {
|
|
|
247
325
|
|
|
248
326
|
moveUp(): void {
|
|
249
327
|
if (this.editingMode) return;
|
|
328
|
+
if (this.searchFocused) {
|
|
329
|
+
if (this.searchResults.length > 0) {
|
|
330
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.searchResults.length) % this.searchResults.length;
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
250
334
|
const items = this._currentItems();
|
|
251
335
|
if (items.length === 0) {
|
|
252
336
|
if (this.currentCategory === 'flags' && this.flagEntries.length > 0) {
|
|
@@ -265,6 +349,12 @@ export class SettingsModal {
|
|
|
265
349
|
|
|
266
350
|
moveDown(): void {
|
|
267
351
|
if (this.editingMode) return;
|
|
352
|
+
if (this.searchFocused) {
|
|
353
|
+
if (this.searchResults.length > 0) {
|
|
354
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.searchResults.length;
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
268
358
|
const items = this._currentItems();
|
|
269
359
|
if (items.length === 0) {
|
|
270
360
|
if (this.currentCategory === 'flags' && this.flagEntries.length > 0) {
|
|
@@ -282,6 +372,9 @@ export class SettingsModal {
|
|
|
282
372
|
}
|
|
283
373
|
|
|
284
374
|
getSelected(): SettingEntry | null {
|
|
375
|
+
if (this.searchFocused && this.searchResults.length > 0) {
|
|
376
|
+
return this.searchResults[Math.max(0, Math.min(this.searchResults.length - 1, this.selectedIndex))] ?? null;
|
|
377
|
+
}
|
|
285
378
|
const items = this._currentItems();
|
|
286
379
|
if (items.length === 0) return null;
|
|
287
380
|
return items[Math.max(0, Math.min(items.length - 1, this.selectedIndex))] ?? null;
|
|
@@ -356,13 +449,9 @@ export class SettingsModal {
|
|
|
356
449
|
const entry = this.getSelectedSubscription();
|
|
357
450
|
if (!entry) return;
|
|
358
451
|
if (entry.state === 'active' || entry.state === 'pending') {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
this.subscriptionManager?.logout(entry.provider);
|
|
364
|
-
this._loadSubscriptionEntries();
|
|
365
|
-
this.subscriptionLogoutConfirmationTarget = null;
|
|
452
|
+
// First press: arm the confirm gate. Subsequent key handling routes
|
|
453
|
+
// through handleSubscriptionLogoutKey() before normal dispatch.
|
|
454
|
+
this.subscriptionLogoutConfirmationTarget = entry.provider;
|
|
366
455
|
}
|
|
367
456
|
return;
|
|
368
457
|
}
|
|
@@ -394,11 +483,11 @@ export class SettingsModal {
|
|
|
394
483
|
|
|
395
484
|
if (setting.type === 'boolean') {
|
|
396
485
|
const newVal = !entry.currentValue;
|
|
397
|
-
this._setValue(setting.key, newVal);
|
|
486
|
+
this._setValue(setting.key as ConfigKey, newVal);
|
|
398
487
|
} else if (setting.type === 'enum' && setting.enumValues) {
|
|
399
488
|
const idx = setting.enumValues.indexOf(entry.currentValue as string);
|
|
400
489
|
const nextIdx = (idx + 1) % setting.enumValues.length;
|
|
401
|
-
this._setValue(setting.key, setting.enumValues[nextIdx]);
|
|
490
|
+
this._setValue(setting.key as ConfigKey, setting.enumValues[nextIdx]);
|
|
402
491
|
} else if (setting.type === 'string' || setting.type === 'number') {
|
|
403
492
|
// Enter inline edit mode
|
|
404
493
|
this.editingMode = true;
|
|
@@ -406,6 +495,31 @@ export class SettingsModal {
|
|
|
406
495
|
}
|
|
407
496
|
}
|
|
408
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Handle a keystroke while a subscription logout confirm is pending.
|
|
500
|
+
*
|
|
501
|
+
* Follows the project-standard confirm contract (confirm-state.ts):
|
|
502
|
+
* - CONFIRM: Enter, Return, or y → executes logout, clears target
|
|
503
|
+
* - CANCEL: Esc or n → clears target, no logout
|
|
504
|
+
* - ABSORBED: any other key → keeps confirm pending, swallows key
|
|
505
|
+
* - INACTIVE: no confirm pending → returns 'inactive' (caller continues)
|
|
506
|
+
*/
|
|
507
|
+
handleSubscriptionLogoutKey(key: string): 'confirmed' | 'cancelled' | 'absorbed' | 'inactive' {
|
|
508
|
+
const target = this.subscriptionLogoutConfirmationTarget;
|
|
509
|
+
if (!target) return 'inactive';
|
|
510
|
+
const confirmState = { subject: target, label: target };
|
|
511
|
+
const result = handleConfirmInput(confirmState, key);
|
|
512
|
+
if (result === 'confirmed') {
|
|
513
|
+
this.subscriptionManager?.logout(target);
|
|
514
|
+
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
515
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
516
|
+
} else if (result === 'cancelled') {
|
|
517
|
+
this.subscriptionLogoutConfirmationTarget = null;
|
|
518
|
+
}
|
|
519
|
+
// 'absorbed': confirm remains pending
|
|
520
|
+
return result;
|
|
521
|
+
}
|
|
522
|
+
|
|
409
523
|
adjustSelected(direction: 'left' | 'right', step = 1): void {
|
|
410
524
|
if (this.editingMode) return;
|
|
411
525
|
|
|
@@ -413,7 +527,7 @@ export class SettingsModal {
|
|
|
413
527
|
const flagEntry = this.getSelectedFlag();
|
|
414
528
|
if (!flagEntry || flagEntry.state === 'killed' || !this.featureFlagManager || !this.configManager) return;
|
|
415
529
|
const targetState: FlagState = direction === 'right' ? 'enabled' : 'disabled';
|
|
416
|
-
if (flagEntry.state !== targetState)
|
|
530
|
+
if (flagEntry.state !== targetState) applyFlagState(flagEntry, targetState, this.featureFlagManager, this.configManager);
|
|
417
531
|
return;
|
|
418
532
|
}
|
|
419
533
|
|
|
@@ -426,7 +540,7 @@ export class SettingsModal {
|
|
|
426
540
|
? (currentIndex + 1) % modes.length
|
|
427
541
|
: (currentIndex - 1 + modes.length) % modes.length;
|
|
428
542
|
this.mcpRegistry.setServerTrustMode(entry.name, modes[nextIndex]!);
|
|
429
|
-
this.
|
|
543
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
430
544
|
this.mcpAllowAllConfirmationTarget = null;
|
|
431
545
|
return;
|
|
432
546
|
}
|
|
@@ -436,7 +550,7 @@ export class SettingsModal {
|
|
|
436
550
|
const { setting } = entry;
|
|
437
551
|
|
|
438
552
|
if (setting.type === 'boolean') {
|
|
439
|
-
this._setValue(setting.key, direction === 'right');
|
|
553
|
+
this._setValue(setting.key as ConfigKey, direction === 'right');
|
|
440
554
|
return;
|
|
441
555
|
}
|
|
442
556
|
|
|
@@ -445,7 +559,7 @@ export class SettingsModal {
|
|
|
445
559
|
const nextIndex = direction === 'right'
|
|
446
560
|
? (currentIndex + 1) % setting.enumValues.length
|
|
447
561
|
: (currentIndex - 1 + setting.enumValues.length) % setting.enumValues.length;
|
|
448
|
-
this._setValue(setting.key, setting.enumValues[nextIndex]!);
|
|
562
|
+
this._setValue(setting.key as ConfigKey, setting.enumValues[nextIndex]!);
|
|
449
563
|
return;
|
|
450
564
|
}
|
|
451
565
|
|
|
@@ -460,7 +574,7 @@ export class SettingsModal {
|
|
|
460
574
|
Math.max(adjustment.min ?? rounded, rounded),
|
|
461
575
|
);
|
|
462
576
|
if (setting.validate && !setting.validate(nextValue)) return;
|
|
463
|
-
this._setValue(setting.key, nextValue);
|
|
577
|
+
this._setValue(setting.key as ConfigKey, nextValue);
|
|
464
578
|
}
|
|
465
579
|
}
|
|
466
580
|
|
|
@@ -474,38 +588,13 @@ export class SettingsModal {
|
|
|
474
588
|
const flagEntry = this.getSelectedFlag();
|
|
475
589
|
if (!flagEntry || !this.featureFlagManager || !this.configManager) return;
|
|
476
590
|
|
|
477
|
-
const {
|
|
591
|
+
const { state } = flagEntry;
|
|
478
592
|
|
|
479
593
|
// Killed flags are blocked
|
|
480
594
|
if (state === 'killed') return;
|
|
481
595
|
|
|
482
596
|
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
|
-
}
|
|
597
|
+
applyFlagState(flagEntry, newState, this.featureFlagManager, this.configManager);
|
|
509
598
|
}
|
|
510
599
|
|
|
511
600
|
/**
|
|
@@ -524,7 +613,7 @@ export class SettingsModal {
|
|
|
524
613
|
return false;
|
|
525
614
|
}
|
|
526
615
|
this.mcpRegistry.setServerTrustMode(entry.name, 'allow-all');
|
|
527
|
-
this.
|
|
616
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
528
617
|
this.editingMode = false;
|
|
529
618
|
this.editBuffer = '';
|
|
530
619
|
this.mcpAllowAllConfirmationTarget = null;
|
|
@@ -545,7 +634,7 @@ export class SettingsModal {
|
|
|
545
634
|
return false;
|
|
546
635
|
}
|
|
547
636
|
this.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
548
|
-
this.
|
|
637
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
549
638
|
this.editingMode = false;
|
|
550
639
|
this.editBuffer = '';
|
|
551
640
|
this.mcpAllowAllConfirmationTarget = null;
|
|
@@ -582,7 +671,7 @@ export class SettingsModal {
|
|
|
582
671
|
setConfigValue: (key, value) => this._setValue(key, value),
|
|
583
672
|
});
|
|
584
673
|
} else {
|
|
585
|
-
this._setValue(setting.key, parsed);
|
|
674
|
+
this._setValue(setting.key as ConfigKey, parsed);
|
|
586
675
|
}
|
|
587
676
|
this.editingMode = false;
|
|
588
677
|
this.editBuffer = '';
|
|
@@ -597,17 +686,47 @@ export class SettingsModal {
|
|
|
597
686
|
}
|
|
598
687
|
|
|
599
688
|
resetSelected(): { key: ConfigKey; value: unknown } | null {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
689
|
+
return _resetSelected({
|
|
690
|
+
editingMode: this.editingMode,
|
|
691
|
+
hasConfigManager: this.configManager !== null,
|
|
692
|
+
selected: this.getSelected(),
|
|
693
|
+
secretsManager: this.secretsManager,
|
|
694
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Arm a category-reset confirmation gate for the current category. */
|
|
699
|
+
initiateResetCategory(): void {
|
|
700
|
+
_initiateResetCategory({
|
|
701
|
+
hasConfigManager: this.configManager !== null,
|
|
702
|
+
currentCategory: this.currentCategory,
|
|
703
|
+
setResetCategoryConfirm: (v) => { this.resetCategoryConfirm = v; },
|
|
704
|
+
setResetAllConfirm: (v) => { this.resetAllConfirm = v; },
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Arm a reset-all confirmation gate. */
|
|
709
|
+
initiateResetAll(): void {
|
|
710
|
+
_initiateResetAll({
|
|
711
|
+
hasConfigManager: this.configManager !== null,
|
|
712
|
+
setResetCategoryConfirm: (v) => { this.resetCategoryConfirm = v; },
|
|
713
|
+
setResetAllConfirm: (v) => { this.resetAllConfirm = v; },
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Route a key through the active reset confirm gate. See ResetConfirmKeyResult for the return contract. */
|
|
718
|
+
handleResetConfirmKey(key: string): ResetConfirmKeyResult {
|
|
719
|
+
return _handleResetConfirmKey({
|
|
720
|
+
key,
|
|
721
|
+
resetCategoryConfirm: this.resetCategoryConfirm,
|
|
722
|
+
resetAllConfirm: this.resetAllConfirm,
|
|
723
|
+
hasConfigManager: this.configManager !== null,
|
|
724
|
+
currentItems: () => this._currentItems(),
|
|
725
|
+
groups: this.groups,
|
|
726
|
+
setValue: (k, value) => this._setValue(k, value),
|
|
727
|
+
setResetCategoryConfirm: (v) => { this.resetCategoryConfirm = v; },
|
|
728
|
+
setResetAllConfirm: (v) => { this.resetAllConfirm = v; },
|
|
729
|
+
});
|
|
611
730
|
}
|
|
612
731
|
|
|
613
732
|
/** Handle a keystroke in edit mode: regular chars appended, Backspace removes last char. */
|
|
@@ -621,173 +740,58 @@ export class SettingsModal {
|
|
|
621
740
|
this.editBuffer = this.editBuffer.slice(0, -1);
|
|
622
741
|
}
|
|
623
742
|
|
|
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
|
|
743
|
+
// ── Private helpers ────────────────────────────────────────────────────────────────
|
|
704
744
|
|
|
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) });
|
|
745
|
+
/** Reload flag/mcp/subscription entries when the active tab changes. */
|
|
746
|
+
private _reloadTabEntries(): void {
|
|
747
|
+
if (this.currentCategory === 'flags') {
|
|
748
|
+
this.flagEntries = buildFlagEntries(this.featureFlagManager);
|
|
749
|
+
} else if (this.currentCategory === 'mcp') {
|
|
750
|
+
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
751
|
+
} else if (this.currentCategory === 'subscriptions') {
|
|
752
|
+
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
716
753
|
}
|
|
717
754
|
}
|
|
718
755
|
|
|
719
|
-
/** Returns [] for the flags
|
|
756
|
+
/** Returns [] for the flags/mcp/subscriptions categories. */
|
|
720
757
|
private _currentItems(): SettingEntry[] {
|
|
721
|
-
if (
|
|
758
|
+
if (
|
|
759
|
+
this.currentCategory === 'flags'
|
|
760
|
+
|| this.currentCategory === 'mcp'
|
|
761
|
+
|| this.currentCategory === 'subscriptions'
|
|
762
|
+
) return [];
|
|
722
763
|
const items = this.groups.get(this.currentCategory) ?? [];
|
|
723
764
|
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
|
-
});
|
|
765
|
+
return buildNetworkFilteredItems(items, this.configManager);
|
|
739
766
|
}
|
|
740
767
|
return items;
|
|
741
768
|
}
|
|
742
769
|
|
|
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
770
|
private _setValue(key: ConfigKey, value: unknown): void {
|
|
754
771
|
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
772
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
773
|
+
const callback: SettingAppliedCallback | null = this.onSettingApplied
|
|
774
|
+
? (change) => this.onSettingApplied!(change)
|
|
775
|
+
: null;
|
|
776
|
+
|
|
777
|
+
const result = applySettingValue({
|
|
778
|
+
key,
|
|
779
|
+
value,
|
|
780
|
+
configManager: this.configManager,
|
|
781
|
+
groups: this.groups,
|
|
782
|
+
onSettingApplied: callback,
|
|
783
|
+
refreshGroups: () => {
|
|
784
|
+
if (this.configManager) refreshEntryValues(this.groups, this.configManager);
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
if (result.restartDomain !== null) {
|
|
789
|
+
this.lastSaveTriggeredRestart = result.restartDomain;
|
|
790
|
+
}
|
|
791
|
+
if (result.effectMessage !== null) {
|
|
792
|
+
this.lastSettingEffectMessage = result.effectMessage;
|
|
790
793
|
}
|
|
794
|
+
// No-op (result.changed === false, effectMessage === null): leave lastSettingEffectMessage untouched.
|
|
791
795
|
}
|
|
792
796
|
|
|
793
797
|
}
|