@pellux/goodvibes-tui 0.22.0 → 0.24.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 +47 -0
- package/README.md +17 -8
- package/package.json +1 -1
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +116 -344
- package/src/cli/surface-command.ts +1 -1
- package/src/core/context-auto-compact.ts +43 -10
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/long-task-notifier.ts +145 -0
- package/src/core/session-recovery.ts +147 -0
- package/src/core/stream-event-wiring.ts +199 -7
- package/src/core/transcript-journal.ts +339 -0
- package/src/core/turn-event-wiring.ts +67 -4
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +0 -2
- package/src/input/commands/diff-runtime.ts +1 -1
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +23 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-runtime.ts +1 -2
- package/src/input/commands/memory-product-runtime.ts +2 -2
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/onboarding-runtime.ts +0 -1
- package/src/input/commands/policy.ts +1 -1
- package/src/input/commands/profile-sync-runtime.ts +4 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +0 -1
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/session-workflow.ts +32 -2
- package/src/input/commands/session.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +9 -9
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +56 -6
- package/src/input/commands/work-plan-runtime.ts +8 -8
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +6 -0
- package/src/input/handler-feed-routes.ts +19 -1
- package/src/input/handler-feed.ts +11 -0
- package/src/input/handler-prompt-buffer.ts +28 -0
- package/src/input/handler-shortcuts.ts +88 -2
- package/src/input/handler-ui-state.ts +2 -2
- package/src/input/handler.ts +39 -3
- package/src/input/keybindings.ts +33 -3
- package/src/input/kill-ring.ts +134 -0
- package/src/input/model-picker.ts +18 -1
- package/src/input/search.ts +18 -6
- package/src/input/settings-modal-activation.ts +134 -0
- package/src/input/settings-modal-adjustment.ts +124 -0
- package/src/input/settings-modal-data.ts +53 -0
- package/src/input/settings-modal.ts +48 -145
- package/src/main.ts +50 -50
- package/src/panels/base-panel.ts +2 -1
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/panels/provider-health-panel.ts +13 -9
- package/src/panels/provider-health-tracker.ts +7 -4
- package/src/panels/settings-sync-panel.ts +3 -3
- package/src/panels/work-plan-panel.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/diff-view.ts +2 -2
- package/src/renderer/help-overlay.ts +1 -0
- package/src/renderer/model-picker-overlay.ts +23 -11
- package/src/renderer/progress.ts +3 -3
- package/src/renderer/search-overlay.ts +8 -5
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +1 -1
- package/src/renderer/ui-factory.ts +11 -0
- package/src/runtime/bootstrap-core.ts +92 -0
- package/src/runtime/bootstrap-hook-bridge.ts +18 -0
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/shell/blocking-input.ts +32 -0
- package/src/shell/recovery-input-helpers.ts +71 -0
- package/src/utils/browser.ts +29 -0
- package/src/utils/terminal-width.ts +10 -3
- package/src/version.ts +1 -1
package/src/input/search.ts
CHANGED
|
@@ -17,6 +17,12 @@ export class SearchManager {
|
|
|
17
17
|
public query = '';
|
|
18
18
|
public matches: SearchMatch[] = [];
|
|
19
19
|
public currentMatch = 0;
|
|
20
|
+
/**
|
|
21
|
+
* Set to true when nextMatch/prevMatch wraps around the match list.
|
|
22
|
+
* Cleared on each navigation call before the wrap check, so it only
|
|
23
|
+
* reflects the most recent navigation step.
|
|
24
|
+
*/
|
|
25
|
+
public wrapAround = false;
|
|
20
26
|
|
|
21
27
|
/** Open search mode. */
|
|
22
28
|
open(): void {
|
|
@@ -25,6 +31,7 @@ export class SearchManager {
|
|
|
25
31
|
this.query = '';
|
|
26
32
|
this.matches = [];
|
|
27
33
|
this.currentMatch = 0;
|
|
34
|
+
this.wrapAround = false;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
/** Lock the query — switches from typing mode to navigation mode. */
|
|
@@ -49,6 +56,7 @@ export class SearchManager {
|
|
|
49
56
|
this.matches = [];
|
|
50
57
|
this.currentMatch = 0;
|
|
51
58
|
|
|
59
|
+
this.wrapAround = false;
|
|
52
60
|
if (query.length === 0) return;
|
|
53
61
|
|
|
54
62
|
const lowerQuery = query.toLowerCase();
|
|
@@ -68,16 +76,20 @@ export class SearchManager {
|
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
/** Jump to next match. */
|
|
79
|
+
/** Jump to next match. Wraps around; sets wrapAround when it does. */
|
|
72
80
|
nextMatch(): void {
|
|
73
|
-
if (this.matches.length === 0) return;
|
|
74
|
-
|
|
81
|
+
if (this.matches.length === 0) { this.wrapAround = false; return; }
|
|
82
|
+
const next = (this.currentMatch + 1) % this.matches.length;
|
|
83
|
+
this.wrapAround = next < this.currentMatch || (this.currentMatch === this.matches.length - 1);
|
|
84
|
+
this.currentMatch = next;
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
/** Jump to previous match. */
|
|
87
|
+
/** Jump to previous match. Wraps around; sets wrapAround when it does. */
|
|
78
88
|
prevMatch(): void {
|
|
79
|
-
if (this.matches.length === 0) return;
|
|
80
|
-
|
|
89
|
+
if (this.matches.length === 0) { this.wrapAround = false; return; }
|
|
90
|
+
const prev = (this.currentMatch - 1 + this.matches.length) % this.matches.length;
|
|
91
|
+
this.wrapAround = prev > this.currentMatch || (this.currentMatch === 0);
|
|
92
|
+
this.currentMatch = prev;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
/** Get the line number of the current match (for scroll). */
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-activation — pure action helpers for SettingsModal.
|
|
3
|
+
*
|
|
4
|
+
* These functions encapsulate the two activation/interaction operations:
|
|
5
|
+
* - activateSelected: toggle boolean, cycle enum, enter edit mode, launch pickers
|
|
6
|
+
* - handleSubscriptionLogoutKey: route a keypress through the logout confirm gate
|
|
7
|
+
*
|
|
8
|
+
* Each function takes its dependencies as explicit arguments rather than
|
|
9
|
+
* accessing class-level state directly, following the same pattern as
|
|
10
|
+
* settings-modal-reset.ts and settings-modal-mutations.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { handleConfirmInput } from '../panels/confirm-state.ts';
|
|
14
|
+
import type { FlagEntry, McpEntry, SettingEntry, SubscriptionEntry } from './settings-modal-types.ts';
|
|
15
|
+
import { buildMcpEntries, buildSubscriptionEntries } from './settings-modal-data.ts';
|
|
16
|
+
import { modelPickerLaunchForKey } from './settings-modal-behavior.ts';
|
|
17
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
18
|
+
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
19
|
+
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
20
|
+
import type { ModelPickerTarget } from './model-picker.ts';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// activateSelected
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export interface ActivateSelectedContext {
|
|
27
|
+
readonly currentCategory: string;
|
|
28
|
+
readonly configManager: { get(key: ConfigKey): unknown } | null;
|
|
29
|
+
getSelectedMcp(): McpEntry | null;
|
|
30
|
+
getSelectedSubscription(): SubscriptionEntry | null;
|
|
31
|
+
getSelected(): SettingEntry | null;
|
|
32
|
+
setValue(key: ConfigKey, value: unknown): void;
|
|
33
|
+
setEditingMode(value: boolean): void;
|
|
34
|
+
setEditBuffer(value: string): void;
|
|
35
|
+
setMcpAllowAllConfirmationTarget(value: string | null): void;
|
|
36
|
+
setSubscriptionLogoutConfirmationTarget(value: string | null): void;
|
|
37
|
+
setPendingSettingsPickerAction(value: 'tts-provider' | 'tts-voice' | null): void;
|
|
38
|
+
setPendingModelPickerTarget(value: ModelPickerTarget | null): void;
|
|
39
|
+
setPendingProviderModelPickerTarget(value: ModelPickerTarget | null): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function activateSelected(ctx: ActivateSelectedContext): void {
|
|
43
|
+
if (ctx.currentCategory === 'mcp') {
|
|
44
|
+
const entry = ctx.getSelectedMcp();
|
|
45
|
+
if (!entry) return;
|
|
46
|
+
ctx.setEditingMode(true);
|
|
47
|
+
ctx.setEditBuffer(entry.trustMode);
|
|
48
|
+
ctx.setMcpAllowAllConfirmationTarget(null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (ctx.currentCategory === 'subscriptions') {
|
|
53
|
+
const entry = ctx.getSelectedSubscription();
|
|
54
|
+
if (!entry) return;
|
|
55
|
+
if (entry.state === 'active' || entry.state === 'pending') {
|
|
56
|
+
// First press: arm the confirm gate. Subsequent key handling routes
|
|
57
|
+
// through handleSubscriptionLogoutKey() before normal dispatch.
|
|
58
|
+
ctx.setSubscriptionLogoutConfirmationTarget(entry.provider);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const entry = ctx.getSelected();
|
|
64
|
+
if (!entry || !ctx.configManager) return;
|
|
65
|
+
|
|
66
|
+
const { setting } = entry;
|
|
67
|
+
|
|
68
|
+
// Delegate provider/model picker settings to the model picker UI
|
|
69
|
+
if (setting.key === 'tts.provider') {
|
|
70
|
+
ctx.setPendingSettingsPickerAction('tts-provider');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (setting.key === 'tts.voice') {
|
|
74
|
+
ctx.setPendingSettingsPickerAction('tts-voice');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pickerLaunch = modelPickerLaunchForKey(setting.key);
|
|
79
|
+
if (pickerLaunch !== null) {
|
|
80
|
+
if (pickerLaunch.flow === 'providerModel') {
|
|
81
|
+
ctx.setPendingProviderModelPickerTarget(pickerLaunch.target);
|
|
82
|
+
} else {
|
|
83
|
+
ctx.setPendingModelPickerTarget(pickerLaunch.target);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (setting.type === 'boolean') {
|
|
89
|
+
const newVal = !entry.currentValue;
|
|
90
|
+
ctx.setValue(setting.key as ConfigKey, newVal);
|
|
91
|
+
} else if (setting.type === 'enum' && setting.enumValues) {
|
|
92
|
+
const idx = setting.enumValues.indexOf(entry.currentValue as string);
|
|
93
|
+
const nextIdx = (idx + 1) % setting.enumValues.length;
|
|
94
|
+
ctx.setValue(setting.key as ConfigKey, setting.enumValues[nextIdx]);
|
|
95
|
+
} else if (setting.type === 'string' || setting.type === 'number') {
|
|
96
|
+
// Enter inline edit mode
|
|
97
|
+
ctx.setEditingMode(true);
|
|
98
|
+
ctx.setEditBuffer(String(entry.currentValue ?? ''));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// handleSubscriptionLogoutKey
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
export interface HandleSubscriptionLogoutKeyContext {
|
|
107
|
+
readonly subscriptionLogoutConfirmationTarget: string | null;
|
|
108
|
+
readonly subscriptionManager: SubscriptionManager | null;
|
|
109
|
+
readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'> | null;
|
|
110
|
+
setSubscriptionEntries(entries: SubscriptionEntry[]): void;
|
|
111
|
+
setSubscriptionLogoutConfirmationTarget(value: string | null): void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function handleSubscriptionLogoutKey(
|
|
115
|
+
ctx: HandleSubscriptionLogoutKeyContext,
|
|
116
|
+
key: string,
|
|
117
|
+
): 'confirmed' | 'cancelled' | 'absorbed' | 'inactive' {
|
|
118
|
+
const target = ctx.subscriptionLogoutConfirmationTarget;
|
|
119
|
+
if (!target) return 'inactive';
|
|
120
|
+
const confirmState = { subject: target, label: target };
|
|
121
|
+
const result = handleConfirmInput(confirmState, key);
|
|
122
|
+
if (result === 'confirmed') {
|
|
123
|
+
ctx.subscriptionManager?.logout(target);
|
|
124
|
+
ctx.setSubscriptionEntries(buildSubscriptionEntries(ctx.subscriptionManager, ctx.serviceRegistry));
|
|
125
|
+
ctx.setSubscriptionLogoutConfirmationTarget(null);
|
|
126
|
+
} else if (result === 'cancelled') {
|
|
127
|
+
ctx.setSubscriptionLogoutConfirmationTarget(null);
|
|
128
|
+
}
|
|
129
|
+
// 'absorbed': confirm remains pending
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Re-export FlagEntry so callers need not import from two modules.
|
|
134
|
+
export type { FlagEntry };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-adjustment — pure adjustment helpers for SettingsModal.
|
|
3
|
+
*
|
|
4
|
+
* These functions encapsulate the two directional-adjustment operations:
|
|
5
|
+
* - adjustSelected: cycle enum/boolean/number values via left/right arrow keys
|
|
6
|
+
* - toggleSelectedFlag: toggle a feature flag between enabled and disabled
|
|
7
|
+
*
|
|
8
|
+
* Each function takes its dependencies as explicit arguments rather than
|
|
9
|
+
* accessing class-level state directly, following the same pattern as
|
|
10
|
+
* settings-modal-reset.ts and settings-modal-mutations.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
15
|
+
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
16
|
+
import type { FlagState } from '@/runtime/index.ts';
|
|
17
|
+
import type { FlagEntry, McpEntry, SettingEntry } from './settings-modal-types.ts';
|
|
18
|
+
import { buildMcpEntries } from './settings-modal-data.ts';
|
|
19
|
+
import { getNumericAdjustmentMeta, roundToPrecision } from './settings-modal-behavior.ts';
|
|
20
|
+
import { applyFlagState } from './settings-modal-mutations.ts';
|
|
21
|
+
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// adjustSelected
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface AdjustSelectedContext {
|
|
28
|
+
readonly editingMode: boolean;
|
|
29
|
+
readonly currentCategory: string;
|
|
30
|
+
readonly configManager: ConfigManager | null;
|
|
31
|
+
readonly featureFlagManager: FeatureFlagManager | null;
|
|
32
|
+
readonly mcpRegistry: McpRegistry | null;
|
|
33
|
+
getSelectedFlag(): FlagEntry | null;
|
|
34
|
+
getSelectedMcp(): McpEntry | null;
|
|
35
|
+
getSelected(): SettingEntry | null;
|
|
36
|
+
setValue(key: ConfigKey, value: unknown): void;
|
|
37
|
+
setMcpEntries(entries: McpEntry[]): void;
|
|
38
|
+
setMcpAllowAllConfirmationTarget(value: string | null): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function adjustSelected(
|
|
42
|
+
ctx: AdjustSelectedContext,
|
|
43
|
+
direction: 'left' | 'right',
|
|
44
|
+
step = 1,
|
|
45
|
+
): void {
|
|
46
|
+
if (ctx.editingMode) return;
|
|
47
|
+
|
|
48
|
+
if (ctx.currentCategory === 'flags') {
|
|
49
|
+
const flagEntry = ctx.getSelectedFlag();
|
|
50
|
+
if (!flagEntry || flagEntry.state === 'killed' || !ctx.featureFlagManager || !ctx.configManager) return;
|
|
51
|
+
const targetState: FlagState = direction === 'right' ? 'enabled' : 'disabled';
|
|
52
|
+
if (flagEntry.state !== targetState) applyFlagState(flagEntry, targetState, ctx.featureFlagManager, ctx.configManager);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (ctx.currentCategory === 'mcp') {
|
|
57
|
+
const entry = ctx.getSelectedMcp();
|
|
58
|
+
if (!entry || !ctx.mcpRegistry) return;
|
|
59
|
+
const modes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
60
|
+
const currentIndex = Math.max(0, modes.indexOf(entry.trustMode));
|
|
61
|
+
const nextIndex = direction === 'right'
|
|
62
|
+
? (currentIndex + 1) % modes.length
|
|
63
|
+
: (currentIndex - 1 + modes.length) % modes.length;
|
|
64
|
+
ctx.mcpRegistry.setServerTrustMode(entry.name, modes[nextIndex]!);
|
|
65
|
+
ctx.setMcpEntries(buildMcpEntries(ctx.mcpRegistry));
|
|
66
|
+
ctx.setMcpAllowAllConfirmationTarget(null);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const entry = ctx.getSelected();
|
|
71
|
+
if (!entry || !ctx.configManager) return;
|
|
72
|
+
const { setting } = entry;
|
|
73
|
+
|
|
74
|
+
if (setting.type === 'boolean') {
|
|
75
|
+
ctx.setValue(setting.key as ConfigKey, direction === 'right');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (setting.type === 'enum' && setting.enumValues && setting.enumValues.length > 0) {
|
|
80
|
+
const currentIndex = Math.max(0, setting.enumValues.indexOf(String(entry.currentValue)));
|
|
81
|
+
const nextIndex = direction === 'right'
|
|
82
|
+
? (currentIndex + 1) % setting.enumValues.length
|
|
83
|
+
: (currentIndex - 1 + setting.enumValues.length) % setting.enumValues.length;
|
|
84
|
+
ctx.setValue(setting.key as ConfigKey, setting.enumValues[nextIndex]!);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (setting.type === 'number') {
|
|
89
|
+
const currentNumber = Number(entry.currentValue ?? 0);
|
|
90
|
+
if (!Number.isFinite(currentNumber)) return;
|
|
91
|
+
const adjustment = getNumericAdjustmentMeta(setting);
|
|
92
|
+
const delta = adjustment.step * step;
|
|
93
|
+
const rounded = roundToPrecision(currentNumber + (direction === 'right' ? delta : -delta), adjustment.precision);
|
|
94
|
+
const nextValue = Math.min(
|
|
95
|
+
adjustment.max ?? rounded,
|
|
96
|
+
Math.max(adjustment.min ?? rounded, rounded),
|
|
97
|
+
);
|
|
98
|
+
if (setting.validate && !setting.validate(nextValue)) return;
|
|
99
|
+
ctx.setValue(setting.key as ConfigKey, nextValue);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// toggleSelectedFlag
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export interface ToggleSelectedFlagContext {
|
|
108
|
+
readonly featureFlagManager: FeatureFlagManager | null;
|
|
109
|
+
readonly configManager: ConfigManager | null;
|
|
110
|
+
getSelectedFlag(): FlagEntry | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function toggleSelectedFlag(ctx: ToggleSelectedFlagContext): void {
|
|
114
|
+
const flagEntry = ctx.getSelectedFlag();
|
|
115
|
+
if (!flagEntry || !ctx.featureFlagManager || !ctx.configManager) return;
|
|
116
|
+
|
|
117
|
+
const { state } = flagEntry;
|
|
118
|
+
|
|
119
|
+
// Killed flags are blocked
|
|
120
|
+
if (state === 'killed') return;
|
|
121
|
+
|
|
122
|
+
const newState: FlagState = state === 'enabled' ? 'disabled' : 'enabled';
|
|
123
|
+
applyFlagState(flagEntry, newState, ctx.featureFlagManager, ctx.configManager);
|
|
124
|
+
}
|
|
@@ -122,6 +122,14 @@ export function buildSettingGroups(
|
|
|
122
122
|
ttsEntries.push(buildTtsSpeedSyntheticEntry(configManager));
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// Inject the synthetic behavior.notifyAfterSeconds entry into the behavior
|
|
126
|
+
// category. This key is TUI-local (not in the SDK ConfigKey union) and
|
|
127
|
+
// controls the long-task push notification threshold.
|
|
128
|
+
const behaviorEntries = groups.get('behavior');
|
|
129
|
+
if (behaviorEntries && !behaviorEntries.some((e) => e.setting.key === ('behavior.notifyAfterSeconds' as ConfigKey))) {
|
|
130
|
+
behaviorEntries.push(buildNotifyAfterSecondsSyntheticEntry(configManager));
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
return groups;
|
|
126
134
|
}
|
|
127
135
|
|
|
@@ -171,6 +179,51 @@ export function buildTtsSpeedSyntheticEntry(configManager: Pick<ConfigManager, '
|
|
|
171
179
|
};
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// behavior.notifyAfterSeconds synthetic setting
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** Default threshold in seconds for the synthetic notifyAfterSeconds setting. */
|
|
187
|
+
export const NOTIFY_AFTER_SECONDS_DEFAULT_SETTING = 60;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The synthetic ConfigSetting descriptor for behavior.notifyAfterSeconds.
|
|
191
|
+
*
|
|
192
|
+
* This key is TUI-local and is not yet in the SDK ConfigKey union. The
|
|
193
|
+
* descriptor is injected into the behavior settings group so users can
|
|
194
|
+
* configure the long-task push notification threshold from /config behavior.
|
|
195
|
+
*
|
|
196
|
+
* 0 = off (no notifications). Any positive integer = threshold in seconds.
|
|
197
|
+
* Default 60s matches the default in long-task-notifier.ts.
|
|
198
|
+
*
|
|
199
|
+
* The key is cast to ConfigKey because ConfigSetting requires it. The cast
|
|
200
|
+
* is safe: configManager.get returns undefined for unknown keys rather than
|
|
201
|
+
* throwing.
|
|
202
|
+
*/
|
|
203
|
+
export const NOTIFY_AFTER_SECONDS_SYNTHETIC_SETTING: ConfigSetting = {
|
|
204
|
+
key: 'behavior.notifyAfterSeconds' as ConfigKey,
|
|
205
|
+
type: 'number',
|
|
206
|
+
default: NOTIFY_AFTER_SECONDS_DEFAULT_SETTING,
|
|
207
|
+
description: 'Seconds a turn must run before a push notification fires (0 = off). Delivers to desktop (notify-send/osascript) and configured ntfy/webhook URLs.',
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build the synthetic SettingEntry for behavior.notifyAfterSeconds.
|
|
212
|
+
*
|
|
213
|
+
* Reads the raw value from configManager using a cast key. Falls back to
|
|
214
|
+
* NOTIFY_AFTER_SECONDS_DEFAULT_SETTING when absent or invalid.
|
|
215
|
+
*/
|
|
216
|
+
export function buildNotifyAfterSecondsSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
|
|
217
|
+
const raw = configManager.get('behavior.notifyAfterSeconds' as ConfigKey);
|
|
218
|
+
const parsed = typeof raw === 'number' ? raw : parseFloat(String(raw ?? ''));
|
|
219
|
+
const currentValue: number = Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed) : NOTIFY_AFTER_SECONDS_DEFAULT_SETTING;
|
|
220
|
+
return {
|
|
221
|
+
setting: NOTIFY_AFTER_SECONDS_SYNTHETIC_SETTING,
|
|
222
|
+
currentValue,
|
|
223
|
+
isDefault: currentValue === NOTIFY_AFTER_SECONDS_DEFAULT_SETTING,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
174
227
|
// ---------------------------------------------------------------------------
|
|
175
228
|
// buildFlagEntries — snapshot of current feature flag states
|
|
176
229
|
// ---------------------------------------------------------------------------
|
|
@@ -14,23 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { type ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
|
-
import { handleConfirmInput } from '../panels/confirm-state.ts';
|
|
18
17
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
19
18
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
20
19
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
21
20
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
22
21
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
23
|
-
import {
|
|
24
|
-
getNumericAdjustmentMeta,
|
|
25
|
-
modelPickerLaunchForKey,
|
|
26
|
-
roundToPrecision,
|
|
27
|
-
} from './settings-modal-behavior.ts';
|
|
28
22
|
import {
|
|
29
23
|
setSecretBackedSettingValue,
|
|
30
24
|
type SettingsSecretsManager,
|
|
31
25
|
} from './settings-modal-secrets.ts';
|
|
32
26
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
33
|
-
import type { FlagState } from '@/runtime/index.ts';
|
|
34
27
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
35
28
|
|
|
36
29
|
import {
|
|
@@ -50,16 +43,21 @@ import {
|
|
|
50
43
|
buildSubscriptionEntries,
|
|
51
44
|
buildNetworkFilteredItems,
|
|
52
45
|
refreshEntryValues,
|
|
53
|
-
updateEntryForKey,
|
|
54
46
|
searchSettingEntries,
|
|
55
47
|
} from './settings-modal-data.ts';
|
|
56
48
|
import { getSettingLabel } from '../renderer/settings-modal-helpers.ts';
|
|
57
49
|
import {
|
|
58
50
|
applySettingValue,
|
|
59
|
-
applyFlagState,
|
|
60
|
-
persistFlagState,
|
|
61
51
|
type SettingAppliedCallback,
|
|
62
52
|
} from './settings-modal-mutations.ts';
|
|
53
|
+
import {
|
|
54
|
+
activateSelected as _activateSelected,
|
|
55
|
+
handleSubscriptionLogoutKey as _handleSubscriptionLogoutKey,
|
|
56
|
+
} from './settings-modal-activation.ts';
|
|
57
|
+
import {
|
|
58
|
+
adjustSelected as _adjustSelected,
|
|
59
|
+
toggleSelectedFlag as _toggleSelectedFlag,
|
|
60
|
+
} from './settings-modal-adjustment.ts';
|
|
63
61
|
import {
|
|
64
62
|
resetSelected as _resetSelected,
|
|
65
63
|
initiateResetCategory as _initiateResetCategory,
|
|
@@ -436,63 +434,21 @@ export class SettingsModal {
|
|
|
436
434
|
* Toggle boolean or begin cycling enum values, or enter edit mode for string/number.
|
|
437
435
|
*/
|
|
438
436
|
activateSelected(): void {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
this.
|
|
443
|
-
|
|
444
|
-
this.
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this.subscriptionLogoutConfirmationTarget = entry.provider;
|
|
455
|
-
}
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const entry = this.getSelected();
|
|
460
|
-
if (!entry || !this.configManager) return;
|
|
461
|
-
|
|
462
|
-
const { setting } = entry;
|
|
463
|
-
|
|
464
|
-
// Delegate provider/model picker settings to the model picker UI
|
|
465
|
-
if (setting.key === 'tts.provider') {
|
|
466
|
-
this.pendingSettingsPickerAction = 'tts-provider';
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (setting.key === 'tts.voice') {
|
|
470
|
-
this.pendingSettingsPickerAction = 'tts-voice';
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const pickerLaunch = modelPickerLaunchForKey(setting.key);
|
|
475
|
-
if (pickerLaunch !== null) {
|
|
476
|
-
if (pickerLaunch.flow === 'providerModel') {
|
|
477
|
-
this.pendingProviderModelPickerTarget = pickerLaunch.target;
|
|
478
|
-
} else {
|
|
479
|
-
this.pendingModelPickerTarget = pickerLaunch.target;
|
|
480
|
-
}
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (setting.type === 'boolean') {
|
|
485
|
-
const newVal = !entry.currentValue;
|
|
486
|
-
this._setValue(setting.key as ConfigKey, newVal);
|
|
487
|
-
} else if (setting.type === 'enum' && setting.enumValues) {
|
|
488
|
-
const idx = setting.enumValues.indexOf(entry.currentValue as string);
|
|
489
|
-
const nextIdx = (idx + 1) % setting.enumValues.length;
|
|
490
|
-
this._setValue(setting.key as ConfigKey, setting.enumValues[nextIdx]);
|
|
491
|
-
} else if (setting.type === 'string' || setting.type === 'number') {
|
|
492
|
-
// Enter inline edit mode
|
|
493
|
-
this.editingMode = true;
|
|
494
|
-
this.editBuffer = String(entry.currentValue ?? '');
|
|
495
|
-
}
|
|
437
|
+
_activateSelected({
|
|
438
|
+
currentCategory: this.currentCategory,
|
|
439
|
+
configManager: this.configManager,
|
|
440
|
+
getSelectedMcp: () => this.getSelectedMcp(),
|
|
441
|
+
getSelectedSubscription: () => this.getSelectedSubscription(),
|
|
442
|
+
getSelected: () => this.getSelected(),
|
|
443
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
444
|
+
setEditingMode: (v) => { this.editingMode = v; },
|
|
445
|
+
setEditBuffer: (v) => { this.editBuffer = v; },
|
|
446
|
+
setMcpAllowAllConfirmationTarget: (v) => { this.mcpAllowAllConfirmationTarget = v; },
|
|
447
|
+
setSubscriptionLogoutConfirmationTarget: (v) => { this.subscriptionLogoutConfirmationTarget = v; },
|
|
448
|
+
setPendingSettingsPickerAction: (v) => { this.pendingSettingsPickerAction = v; },
|
|
449
|
+
setPendingModelPickerTarget: (v) => { this.pendingModelPickerTarget = v; },
|
|
450
|
+
setPendingProviderModelPickerTarget: (v) => { this.pendingProviderModelPickerTarget = v; },
|
|
451
|
+
});
|
|
496
452
|
}
|
|
497
453
|
|
|
498
454
|
/**
|
|
@@ -505,77 +461,29 @@ export class SettingsModal {
|
|
|
505
461
|
* - INACTIVE: no confirm pending → returns 'inactive' (caller continues)
|
|
506
462
|
*/
|
|
507
463
|
handleSubscriptionLogoutKey(key: string): 'confirmed' | 'cancelled' | 'absorbed' | 'inactive' {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
this.
|
|
514
|
-
|
|
515
|
-
this.subscriptionLogoutConfirmationTarget = null;
|
|
516
|
-
} else if (result === 'cancelled') {
|
|
517
|
-
this.subscriptionLogoutConfirmationTarget = null;
|
|
518
|
-
}
|
|
519
|
-
// 'absorbed': confirm remains pending
|
|
520
|
-
return result;
|
|
464
|
+
return _handleSubscriptionLogoutKey({
|
|
465
|
+
subscriptionLogoutConfirmationTarget: this.subscriptionLogoutConfirmationTarget,
|
|
466
|
+
subscriptionManager: this.subscriptionManager,
|
|
467
|
+
serviceRegistry: this.serviceRegistry,
|
|
468
|
+
setSubscriptionEntries: (entries) => { this.subscriptionEntries = entries; },
|
|
469
|
+
setSubscriptionLogoutConfirmationTarget: (v) => { this.subscriptionLogoutConfirmationTarget = v; },
|
|
470
|
+
}, key);
|
|
521
471
|
}
|
|
522
472
|
|
|
523
473
|
adjustSelected(direction: 'left' | 'right', step = 1): void {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const modes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
538
|
-
const currentIndex = Math.max(0, modes.indexOf(entry.trustMode));
|
|
539
|
-
const nextIndex = direction === 'right'
|
|
540
|
-
? (currentIndex + 1) % modes.length
|
|
541
|
-
: (currentIndex - 1 + modes.length) % modes.length;
|
|
542
|
-
this.mcpRegistry.setServerTrustMode(entry.name, modes[nextIndex]!);
|
|
543
|
-
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
544
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const entry = this.getSelected();
|
|
549
|
-
if (!entry || !this.configManager) return;
|
|
550
|
-
const { setting } = entry;
|
|
551
|
-
|
|
552
|
-
if (setting.type === 'boolean') {
|
|
553
|
-
this._setValue(setting.key as ConfigKey, direction === 'right');
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (setting.type === 'enum' && setting.enumValues && setting.enumValues.length > 0) {
|
|
558
|
-
const currentIndex = Math.max(0, setting.enumValues.indexOf(String(entry.currentValue)));
|
|
559
|
-
const nextIndex = direction === 'right'
|
|
560
|
-
? (currentIndex + 1) % setting.enumValues.length
|
|
561
|
-
: (currentIndex - 1 + setting.enumValues.length) % setting.enumValues.length;
|
|
562
|
-
this._setValue(setting.key as ConfigKey, setting.enumValues[nextIndex]!);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (setting.type === 'number') {
|
|
567
|
-
const currentNumber = Number(entry.currentValue ?? 0);
|
|
568
|
-
if (!Number.isFinite(currentNumber)) return;
|
|
569
|
-
const adjustment = getNumericAdjustmentMeta(setting);
|
|
570
|
-
const delta = adjustment.step * step;
|
|
571
|
-
const rounded = roundToPrecision(currentNumber + (direction === 'right' ? delta : -delta), adjustment.precision);
|
|
572
|
-
const nextValue = Math.min(
|
|
573
|
-
adjustment.max ?? rounded,
|
|
574
|
-
Math.max(adjustment.min ?? rounded, rounded),
|
|
575
|
-
);
|
|
576
|
-
if (setting.validate && !setting.validate(nextValue)) return;
|
|
577
|
-
this._setValue(setting.key as ConfigKey, nextValue);
|
|
578
|
-
}
|
|
474
|
+
_adjustSelected({
|
|
475
|
+
editingMode: this.editingMode,
|
|
476
|
+
currentCategory: this.currentCategory,
|
|
477
|
+
configManager: this.configManager,
|
|
478
|
+
featureFlagManager: this.featureFlagManager,
|
|
479
|
+
mcpRegistry: this.mcpRegistry,
|
|
480
|
+
getSelectedFlag: () => this.getSelectedFlag(),
|
|
481
|
+
getSelectedMcp: () => this.getSelectedMcp(),
|
|
482
|
+
getSelected: () => this.getSelected(),
|
|
483
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
484
|
+
setMcpEntries: (entries) => { this.mcpEntries = entries; },
|
|
485
|
+
setMcpAllowAllConfirmationTarget: (v) => { this.mcpAllowAllConfirmationTarget = v; },
|
|
486
|
+
}, direction, step);
|
|
579
487
|
}
|
|
580
488
|
|
|
581
489
|
/**
|
|
@@ -585,16 +493,11 @@ export class SettingsModal {
|
|
|
585
493
|
* only (require restart). runtimeToggleable flags toggle immediately.
|
|
586
494
|
*/
|
|
587
495
|
toggleSelectedFlag(): void {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
// Killed flags are blocked
|
|
594
|
-
if (state === 'killed') return;
|
|
595
|
-
|
|
596
|
-
const newState: FlagState = state === 'enabled' ? 'disabled' : 'enabled';
|
|
597
|
-
applyFlagState(flagEntry, newState, this.featureFlagManager, this.configManager);
|
|
496
|
+
_toggleSelectedFlag({
|
|
497
|
+
featureFlagManager: this.featureFlagManager,
|
|
498
|
+
configManager: this.configManager,
|
|
499
|
+
getSelectedFlag: () => this.getSelectedFlag(),
|
|
500
|
+
});
|
|
598
501
|
}
|
|
599
502
|
|
|
600
503
|
/**
|