@pellux/goodvibes-tui 0.19.53 → 0.19.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +10 -13
  3. package/docs/foundation-artifacts/knowledge-store.sql +27 -0
  4. package/docs/foundation-artifacts/operator-contract.json +15736 -7265
  5. package/package.json +2 -2
  6. package/src/audio/spoken-turn-controller.ts +4 -1
  7. package/src/input/command-args-hint.ts +36 -0
  8. package/src/input/command-registry.ts +3 -1
  9. package/src/input/commands/config.ts +7 -521
  10. package/src/input/commands/knowledge.ts +111 -1
  11. package/src/input/commands/local-runtime.ts +0 -80
  12. package/src/input/commands/operator-runtime.ts +3 -3
  13. package/src/input/commands/planning-runtime.ts +83 -34
  14. package/src/input/commands/shell-core.ts +2 -34
  15. package/src/input/commands/tts-runtime.ts +1 -389
  16. package/src/input/commands.ts +0 -2
  17. package/src/input/handler-modal-routes.ts +61 -7
  18. package/src/input/handler-modal-token-routes.ts +1 -0
  19. package/src/input/handler-picker-routes.ts +50 -4
  20. package/src/input/model-picker-provider-filter.ts +28 -0
  21. package/src/input/model-picker-types.ts +12 -0
  22. package/src/input/model-picker.ts +65 -23
  23. package/src/input/selection-modal.ts +1 -1
  24. package/src/input/settings-modal-behavior.ts +2 -0
  25. package/src/input/settings-modal-subscriptions.ts +95 -0
  26. package/src/input/settings-modal-types.ts +50 -3
  27. package/src/input/settings-modal.ts +106 -134
  28. package/src/input/tts-settings-actions.ts +100 -0
  29. package/src/main.ts +50 -45
  30. package/src/panels/builtin/agent.ts +15 -0
  31. package/src/panels/builtin/shared.ts +17 -0
  32. package/src/panels/project-planning-panel.ts +370 -0
  33. package/src/planning/project-planning-coordinator.ts +249 -0
  34. package/src/renderer/compositor.ts +2 -1
  35. package/src/renderer/conversation-overlays.ts +4 -5
  36. package/src/renderer/model-workspace.ts +488 -0
  37. package/src/renderer/settings-modal-helpers.ts +16 -1
  38. package/src/renderer/settings-modal.ts +616 -716
  39. package/src/runtime/bootstrap-command-context.ts +6 -0
  40. package/src/runtime/bootstrap-command-parts.ts +5 -0
  41. package/src/runtime/bootstrap-shell.ts +2 -0
  42. package/src/runtime/services.ts +33 -2
  43. package/src/runtime/terminal-output-guard.ts +228 -0
  44. package/src/runtime/ui-services.ts +4 -0
  45. package/src/shell/ui-openers.ts +59 -3
  46. package/src/utils/clipboard.ts +2 -1
  47. package/src/version.ts +1 -1
  48. package/src/input/commands/permissions-runtime.ts +0 -104
@@ -0,0 +1,28 @@
1
+ import { POPULAR_PROVIDERS } from './model-picker-types.ts';
2
+
3
+ export function groupProviders(providers: readonly string[]): { popular: string[]; all: string[] } {
4
+ const popular: string[] = [];
5
+ const all: string[] = [];
6
+
7
+ for (const provider of providers) {
8
+ if (POPULAR_PROVIDERS.has(provider.toLowerCase())) {
9
+ popular.push(provider);
10
+ } else {
11
+ all.push(provider);
12
+ }
13
+ }
14
+
15
+ popular.sort((a, b) => a.localeCompare(b));
16
+ all.sort((a, b) => a.localeCompare(b));
17
+
18
+ return { popular, all };
19
+ }
20
+
21
+ export function filterProviders(providers: readonly string[], query: string): string[] {
22
+ const { popular, all } = groupProviders(providers);
23
+ const ordered = [...popular, ...all];
24
+ const normalized = query.trim().toLowerCase();
25
+ return normalized.length === 0
26
+ ? ordered
27
+ : ordered.filter((provider) => provider.toLowerCase().includes(normalized));
28
+ }
@@ -11,6 +11,18 @@ export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap';
11
11
  */
12
12
  export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts';
13
13
 
14
+ export type ModelPickerFocusPane = 'targets' | 'items';
15
+
16
+ export interface ModelPickerTargetInfo {
17
+ readonly target: ModelPickerTarget;
18
+ readonly label: string;
19
+ readonly description: string;
20
+ readonly provider: string;
21
+ readonly model: string;
22
+ readonly enabled: boolean;
23
+ readonly inherited: boolean;
24
+ }
25
+
14
26
  /**
15
27
  * Pricing tier filter.
16
28
  * 'paid' matches ModelDefinition tiers 'standard' and 'premium' for forward-compat
@@ -5,10 +5,11 @@ import { getQualityTier, getQualityTierFromScore, compositeScore, A_TIER_THRESHO
5
5
  import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers/model-benchmarks';
6
6
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers/registry';
7
7
  import { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
8
- import type { BenchmarkSort, CapabilityFilter, CategoryFilter, FilteredModelsCache, FilteredProvidersCache, GroupByMode, ModelItemsCache, ModelPickerTarget, PickerItem, PickerMode, ProviderItemsCache } from './model-picker-types.ts';
8
+ import type { BenchmarkSort, CapabilityFilter, CategoryFilter, FilteredModelsCache, FilteredProvidersCache, GroupByMode, ModelItemsCache, ModelPickerFocusPane, ModelPickerTarget, ModelPickerTargetInfo, PickerItem, PickerMode, ProviderItemsCache } from './model-picker-types.ts';
9
+ import { filterProviders, groupProviders } from './model-picker-provider-filter.ts';
9
10
 
10
11
  export { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
11
- export type { BenchmarkSort, CapabilityFilter, CategoryFilter, GroupByMode, ModelFamily, ModelPickerTarget, PickerItem, PickerMode } from './model-picker-types.ts';
12
+ export type { BenchmarkSort, CapabilityFilter, CategoryFilter, GroupByMode, ModelFamily, ModelPickerFocusPane, ModelPickerTarget, ModelPickerTargetInfo, PickerItem, PickerMode } from './model-picker-types.ts';
12
13
 
13
14
  /**
14
15
  * ModelPickerModal - Multi-step interactive picker for model, provider, and effort.
@@ -35,6 +36,9 @@ export class ModelPickerModal {
35
36
  public mode: PickerMode = 'model';
36
37
  /** Which config target this picker session will write to on commit. */
37
38
  public target: ModelPickerTarget = 'main';
39
+ public focusPane: ModelPickerFocusPane = 'items';
40
+ public targetInfos: ModelPickerTargetInfo[] = [];
41
+ public targetIndex = 0;
38
42
  public searchFocused = false;
39
43
  /** Tracks the mode we came from, for back-navigation. */
40
44
  public previousMode: PickerMode | null = null;
@@ -78,6 +82,58 @@ export class ModelPickerModal {
78
82
  private modelItemsCache: ModelItemsCache | null = null;
79
83
  private providerItemsCache: ProviderItemsCache | null = null;
80
84
 
85
+ setTargetInfos(infos: ModelPickerTargetInfo[]): void {
86
+ this.targetInfos = infos;
87
+ const idx = infos.findIndex((entry) => entry.target === this.target);
88
+ this.targetIndex = idx >= 0 ? idx : 0;
89
+ }
90
+
91
+ getSelectedTargetInfo(): ModelPickerTargetInfo | null {
92
+ return this.targetInfos[this.targetIndex] ?? null;
93
+ }
94
+
95
+ focusTargets(): void {
96
+ this.focusPane = 'targets';
97
+ this.searchFocused = false;
98
+ }
99
+
100
+ focusItems(): void {
101
+ this.focusPane = 'items';
102
+ }
103
+
104
+ moveTarget(delta: number): void {
105
+ if (this.targetInfos.length === 0) return;
106
+ const nextIndex = (this.targetIndex + delta + this.targetInfos.length) % this.targetInfos.length;
107
+ this.setTarget(this.targetInfos[nextIndex]!.target);
108
+ }
109
+
110
+ setTarget(target: ModelPickerTarget): void {
111
+ this.target = target;
112
+ const idx = this.targetInfos.findIndex((entry) => entry.target === target);
113
+ this.targetIndex = idx >= 0 ? idx : this.targetIndex;
114
+ this.alignSelectionToTarget();
115
+ }
116
+
117
+ alignSelectionToTarget(): void {
118
+ const info = this.getSelectedTargetInfo();
119
+ if (!info) return;
120
+ if (this.mode === 'provider') {
121
+ const providers = this.getFilteredProviders();
122
+ const providerIdx = providers.findIndex((provider) => provider === info.provider);
123
+ this.selectedIndex = providerIdx >= 0 ? providerIdx : 0;
124
+ this.scrollOffset = 0;
125
+ this._scrollToSelection(20);
126
+ return;
127
+ }
128
+ if (this.mode === 'model') {
129
+ const models = this.getFilteredModels();
130
+ const modelIdx = models.findIndex((model) => model.registryKey === info.model || model.id === info.model);
131
+ this.selectedIndex = modelIdx >= 0 ? modelIdx : 0;
132
+ this.scrollOffset = 0;
133
+ this._scrollToSelection(20);
134
+ }
135
+ }
136
+
81
137
  // ── Category filter cycling ───────────────────────────────────────────────
82
138
  private static readonly CATEGORY_CYCLE: CategoryFilter[] = ['all', 'free', 'paid', 'subscription'];
83
139
  /** Cycle to next pricing tier filter. */
@@ -146,6 +202,7 @@ export class ModelPickerModal {
146
202
  this.mode = 'model';
147
203
  this.active = true;
148
204
  this.pendingModel = null;
205
+ this.focusPane = 'items';
149
206
  this.searchFocused = false;
150
207
  this.query = '';
151
208
  this.categoryFilter = 'all';
@@ -163,6 +220,7 @@ export class ModelPickerModal {
163
220
  this.mode = 'provider';
164
221
  this.active = true;
165
222
  this.pendingModel = null;
223
+ this.focusPane = 'items';
166
224
  this.searchFocused = false;
167
225
  this.query = '';
168
226
  this.categoryFilter = 'all';
@@ -206,6 +264,9 @@ export class ModelPickerModal {
206
264
  this.active = false;
207
265
  this.mode = 'model';
208
266
  this.target = 'main';
267
+ this.focusPane = 'items';
268
+ this.targetInfos = [];
269
+ this.targetIndex = 0;
209
270
  this.models = [];
210
271
  this.providers = [];
211
272
  this.pendingModel = null;
@@ -285,22 +346,7 @@ export class ModelPickerModal {
285
346
  * renderer and does not affect grouping.
286
347
  */
287
348
  getGroupedProviders(): { popular: string[]; all: string[] } {
288
- const popular: string[] = [];
289
- const all: string[] = [];
290
-
291
- for (const p of this.providers) {
292
- const pLower = p.toLowerCase();
293
- if (POPULAR_PROVIDERS.has(pLower)) {
294
- popular.push(p);
295
- } else {
296
- all.push(p);
297
- }
298
- }
299
-
300
- popular.sort((a, b) => a.localeCompare(b));
301
- all.sort((a, b) => a.localeCompare(b));
302
-
303
- return { popular, all };
349
+ return groupProviders(this.providers);
304
350
  }
305
351
 
306
352
  /** Return providers matching the current query (case-insensitive substring), in grouped order. */
@@ -314,11 +360,7 @@ export class ModelPickerModal {
314
360
  return cached.result;
315
361
  }
316
362
 
317
- const { popular, all } = this.getGroupedProviders();
318
- const ordered = [...popular, ...all];
319
- const result = this.query.trim().length === 0
320
- ? ordered
321
- : ordered.filter(p => p.toLowerCase().includes(this.query.toLowerCase()));
363
+ const result = filterProviders(this.providers, this.query);
322
364
  this.filteredProvidersCache = {
323
365
  providersRef: this.providers,
324
366
  query: this.query,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SelectionModal - Generic reusable selection modal with fuzzy search.
3
- * Used by /config, /template, /sessions, /bookmarks, /tools, /permissions.
3
+ * Used by /template, /sessions, /bookmarks, /tools, and focused pickers.
4
4
  */
5
5
 
6
6
  export interface SelectionItem {
@@ -10,6 +10,8 @@ export type ModelPickerLaunch =
10
10
  * provider first; model rows open directly to models for the same target.
11
11
  */
12
12
  export function modelPickerLaunchForKey(key: string): ModelPickerLaunch | null {
13
+ if (key === 'provider.provider') return { flow: 'providerModel', target: 'main' };
14
+ if (key === 'provider.model') return { flow: 'model', target: 'main' };
13
15
  if (key === 'helper.globalProvider') return { flow: 'providerModel', target: 'helper' };
14
16
  if (key === 'helper.globalModel') return { flow: 'model', target: 'helper' };
15
17
  if (key === 'tools.llmProvider') return { flow: 'providerModel', target: 'tool' };
@@ -0,0 +1,95 @@
1
+ import type { ProviderAuthFreshness } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
2
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
3
+ import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
4
+ import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
5
+ import type { SubscriptionEntry } from './settings-modal-types.ts';
6
+
7
+ export function buildSubscriptionEntries(
8
+ manager: SubscriptionManager | null,
9
+ serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'> | null,
10
+ ): SubscriptionEntry[] {
11
+ if (!manager) return [];
12
+
13
+ const services = serviceRegistry?.getAll() ?? {};
14
+ const providers = new Map<string, SubscriptionEntry>();
15
+ const builtinProviders = new Set(listBuiltinSubscriptionProviders().map((builtin) => builtin.provider));
16
+
17
+ for (const provider of builtinProviders) {
18
+ providers.set(provider, {
19
+ provider,
20
+ state: 'available',
21
+ oauthConfigured: true,
22
+ preferredRoute: 'subscription',
23
+ activeRoute: 'unconfigured',
24
+ authFreshness: 'unconfigured',
25
+ routeReason: 'Built-in subscription adapter is available, but no active subscription session is stored yet.',
26
+ nextActions: [`Use /subscription login ${provider} start to begin browser sign-in.`],
27
+ });
28
+ }
29
+
30
+ for (const service of Object.values(services)) {
31
+ if (service.authType !== 'oauth' || !service.oauth) continue;
32
+ const provider = service.providerId ?? service.name;
33
+ providers.set(provider, {
34
+ provider,
35
+ state: 'available',
36
+ oauthConfigured: true,
37
+ preferredRoute: 'subscription',
38
+ activeRoute: providers.get(provider)?.activeRoute ?? 'unconfigured',
39
+ authFreshness: providers.get(provider)?.authFreshness ?? 'unconfigured',
40
+ routeReason: providers.get(provider)?.routeReason ?? 'OAuth metadata is configured for this provider.',
41
+ nextActions: providers.get(provider)?.nextActions ?? [`Use /subscription login ${provider} start to begin browser sign-in.`],
42
+ });
43
+ }
44
+
45
+ for (const pending of manager.listPending()) {
46
+ providers.set(pending.provider, {
47
+ provider: pending.provider,
48
+ state: 'pending',
49
+ oauthConfigured: providers.get(pending.provider)?.oauthConfigured ?? false,
50
+ preferredRoute: 'subscription',
51
+ activeRoute: 'unconfigured',
52
+ authFreshness: 'pending',
53
+ routeReason: 'OAuth login is pending completion for this provider.',
54
+ nextActions: [`Finish /subscription login ${pending.provider} finish <code> to activate this session.`],
55
+ });
56
+ }
57
+
58
+ for (const subscription of manager.list()) {
59
+ const freshness = determineFreshness(subscription.expiresAt);
60
+ const issues = freshness === 'expired'
61
+ ? ['Stored subscription session is expired and needs refresh.']
62
+ : freshness === 'expiring'
63
+ ? ['Stored subscription session expires within 24 hours.']
64
+ : [];
65
+ const nextActions = freshness === 'expired'
66
+ ? [`Refresh or replace the ${subscription.provider} subscription session.`]
67
+ : freshness === 'expiring'
68
+ ? [`Verify or renew the ${subscription.provider} subscription session soon.`]
69
+ : [];
70
+ providers.set(subscription.provider, {
71
+ provider: subscription.provider,
72
+ state: 'active',
73
+ tokenType: subscription.tokenType,
74
+ expiresAt: subscription.expiresAt,
75
+ oauthConfigured: providers.get(subscription.provider)?.oauthConfigured ?? builtinProviders.has(subscription.provider),
76
+ activeRoute: freshness === 'expired' ? 'unconfigured' : 'subscription',
77
+ preferredRoute: 'subscription',
78
+ authFreshness: freshness,
79
+ routeReason: subscription.overrideAmbientApiKeys
80
+ ? 'Subscription route overrides ambient API-key resolution for this provider.'
81
+ : 'Subscription route is stored for supported flows without automatically replacing ambient API-key resolution.',
82
+ issues,
83
+ nextActions,
84
+ });
85
+ }
86
+
87
+ return [...providers.values()].sort((a, b) => a.provider.localeCompare(b.provider));
88
+ }
89
+
90
+ function determineFreshness(expiresAt?: number): ProviderAuthFreshness {
91
+ if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) return 'healthy';
92
+ if (expiresAt <= Date.now()) return 'expired';
93
+ if (expiresAt <= Date.now() + 24 * 60 * 60 * 1000) return 'expiring';
94
+ return 'healthy';
95
+ }
@@ -2,7 +2,39 @@ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema
2
2
  import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
3
3
  import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
4
4
 
5
- export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'cloudflare' | 'danger' | 'tools' | 'flags' | 'network';
5
+ export type SettingsCategory =
6
+ | 'display'
7
+ | 'ui'
8
+ | 'provider'
9
+ | 'subscriptions'
10
+ | 'behavior'
11
+ | 'storage'
12
+ | 'permissions'
13
+ | 'orchestration'
14
+ | 'wrfc'
15
+ | 'tools'
16
+ | 'helper'
17
+ | 'tts'
18
+ | 'service'
19
+ | 'controlPlane'
20
+ | 'httpListener'
21
+ | 'web'
22
+ | 'network'
23
+ | 'surfaces'
24
+ | 'cloudflare'
25
+ | 'batch'
26
+ | 'automation'
27
+ | 'watchers'
28
+ | 'runtime'
29
+ | 'telemetry'
30
+ | 'cache'
31
+ | 'sandbox'
32
+ | 'mcp'
33
+ | 'flags'
34
+ | 'release'
35
+ | 'danger';
36
+
37
+ export type SettingsFocusPane = 'categories' | 'settings';
6
38
 
7
39
  export const SETTINGS_CATEGORIES: SettingsCategory[] = [
8
40
  'display',
@@ -12,14 +44,29 @@ export const SETTINGS_CATEGORIES: SettingsCategory[] = [
12
44
  'behavior',
13
45
  'storage',
14
46
  'permissions',
47
+ 'orchestration',
48
+ 'wrfc',
49
+ 'tools',
50
+ 'helper',
51
+ 'tts',
52
+ 'service',
53
+ 'controlPlane',
54
+ 'httpListener',
55
+ 'web',
56
+ 'network',
15
57
  'mcp',
16
58
  'sandbox',
17
59
  'surfaces',
18
60
  'cloudflare',
61
+ 'batch',
62
+ 'automation',
63
+ 'watchers',
64
+ 'runtime',
65
+ 'telemetry',
66
+ 'cache',
19
67
  'danger',
20
- 'tools',
21
68
  'flags',
22
- 'network',
69
+ 'release',
23
70
  ];
24
71
 
25
72
  export interface SettingEntry {