@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
@@ -72,6 +72,8 @@ export type CreateBootstrapCommandContextOptions = {
72
72
  integrationHelpers?: IntegrationHelperService;
73
73
  automationManager?: ShellAutomationManagerRuntimeService;
74
74
  knowledgeService?: KnowledgeService;
75
+ projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge/index').ProjectPlanningService;
76
+ projectPlanningProjectId?: string;
75
77
  providerOptimizer?: import('@pellux/goodvibes-sdk/platform/providers/optimizer').ProviderOptimizer;
76
78
  pluginManager?: PluginManager;
77
79
  hookWorkbench?: HookWorkbench;
@@ -138,6 +140,8 @@ export function createBootstrapCommandContext(
138
140
  integrationHelpers,
139
141
  automationManager,
140
142
  knowledgeService,
143
+ projectPlanningService,
144
+ projectPlanningProjectId,
141
145
  providerOptimizer,
142
146
  pluginManager,
143
147
  hookWorkbench,
@@ -227,6 +231,8 @@ export function createBootstrapCommandContext(
227
231
  panelManager,
228
232
  profileManager,
229
233
  bookmarkManager,
234
+ projectPlanningService,
235
+ projectPlanningProjectId,
230
236
  }, shellServices);
231
237
  const platform = createBootstrapCommandPlatformSection({ configManager, voiceProviderRegistry, voiceService }, shellServices);
232
238
  const extensions = createBootstrapCommandExtensionsSection({
@@ -85,6 +85,8 @@ export interface BootstrapCommandSectionOptions {
85
85
  readonly memoryRegistry?: MemoryRegistry;
86
86
  readonly integrationHelpers?: IntegrationHelperService;
87
87
  readonly knowledgeService?: KnowledgeService;
88
+ readonly projectPlanningService?: import('@pellux/goodvibes-sdk/platform/knowledge/index').ProjectPlanningService;
89
+ readonly projectPlanningProjectId?: string;
88
90
  readonly pluginManager?: PluginManager;
89
91
  readonly hookWorkbench?: HookWorkbench;
90
92
  readonly providerOptimizer?: import('@pellux/goodvibes-sdk/platform/providers/optimizer').ProviderOptimizer;
@@ -312,6 +314,7 @@ export function createBootstrapCommandWorkspaceSection(
312
314
  options: Pick<
313
315
  BootstrapCommandSectionOptions,
314
316
  'keybindingsManager' | 'fileUndoManager' | 'panelManager' | 'profileManager' | 'bookmarkManager'
317
+ | 'projectPlanningService' | 'projectPlanningProjectId'
315
318
  >,
316
319
  shellServices: BootstrapCommandShellServices,
317
320
  ): BootstrapCommandWorkspaceSection {
@@ -321,6 +324,8 @@ export function createBootstrapCommandWorkspaceSection(
321
324
  panelManager: options.panelManager,
322
325
  profileManager: options.profileManager,
323
326
  bookmarkManager: options.bookmarkManager,
327
+ projectPlanningService: options.projectPlanningService,
328
+ projectPlanningProjectId: options.projectPlanningProjectId,
324
329
  ...shellServices.workspace,
325
330
  };
326
331
  }
@@ -196,6 +196,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
196
196
  integrationHelpers: services.integrationHelpers,
197
197
  automationManager: services.automationManager,
198
198
  knowledgeService: services.knowledgeService,
199
+ projectPlanningService: services.projectPlanningService,
200
+ projectPlanningProjectId: services.projectPlanningProjectId,
199
201
  providerOptimizer: services.providerOptimizer,
200
202
  pluginManager: services.pluginManager,
201
203
  hookWorkbench: services.hookWorkbench,
@@ -9,7 +9,16 @@ import { ChannelDeliveryRouter } from '@pellux/goodvibes-sdk/platform/channels/d
9
9
  import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane/index';
10
10
  import { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers/index';
11
11
  import { ArtifactStore } from '@pellux/goodvibes-sdk/platform/artifacts/index';
12
- import { HomeGraphService, KnowledgeService, KnowledgeStore } from '@pellux/goodvibes-sdk/platform/knowledge/index';
12
+ import {
13
+ HomeGraphService,
14
+ KnowledgeService,
15
+ KnowledgeSemanticService,
16
+ KnowledgeStore,
17
+ ProjectPlanningService,
18
+ createProviderBackedKnowledgeSemanticLlm,
19
+ createWebKnowledgeGapRepairer,
20
+ projectPlanningProjectIdFromPath,
21
+ } from '@pellux/goodvibes-sdk/platform/knowledge/index';
13
22
  import { MediaProviderRegistry, ensureBuiltinMediaProviders } from '@pellux/goodvibes-sdk/platform/media/index';
14
23
  import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal/index';
15
24
  import { AgentManager } from '@pellux/goodvibes-sdk/platform/tools/agent/index';
@@ -109,6 +118,8 @@ export interface RuntimeServices {
109
118
  readonly artifactStore: ArtifactStore;
110
119
  readonly knowledgeService: KnowledgeService;
111
120
  readonly homeGraphService: HomeGraphService;
121
+ readonly projectPlanningService: ProjectPlanningService;
122
+ readonly projectPlanningProjectId: string;
112
123
  readonly memoryStore: MemoryStore;
113
124
  readonly memoryRegistry: MemoryRegistry;
114
125
  readonly serviceRegistry: ServiceRegistry;
@@ -352,12 +363,26 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
352
363
  },
353
364
  });
354
365
  const knowledgeStore = new KnowledgeStore({ configManager });
366
+ const knowledgeSemanticService = new KnowledgeSemanticService(knowledgeStore, {
367
+ llm: createProviderBackedKnowledgeSemanticLlm(providerRegistry, {
368
+ timeoutMs: 20_000,
369
+ maxConcurrent: 1,
370
+ }),
371
+ maxLlmSourcesPerReindex: 3,
372
+ });
355
373
  const knowledgeService = new KnowledgeService(knowledgeStore, artifactStore, undefined, {
356
374
  memoryRegistry,
357
375
  runtimeBus: options.runtimeBus,
376
+ semanticService: knowledgeSemanticService,
358
377
  });
359
378
  knowledgeService.attachRuntimeBus(options.runtimeBus);
360
- const homeGraphService = new HomeGraphService(knowledgeStore, artifactStore);
379
+ const homeGraphService = new HomeGraphService(knowledgeStore, artifactStore, {
380
+ semanticService: knowledgeSemanticService,
381
+ });
382
+ const projectPlanningProjectId = projectPlanningProjectIdFromPath(workingDirectory);
383
+ const projectPlanningService = new ProjectPlanningService(knowledgeStore, {
384
+ defaultProjectId: projectPlanningProjectId,
385
+ });
361
386
  const voiceProviders = new VoiceProviderRegistry();
362
387
  ensureBuiltinVoiceProviders(voiceProviders);
363
388
  const voiceService = new VoiceService(voiceProviders);
@@ -369,6 +394,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
369
394
  serviceRegistry,
370
395
  featureFlags,
371
396
  });
397
+ knowledgeSemanticService.setGapRepairer(createWebKnowledgeGapRepairer({
398
+ searchService: webSearchService,
399
+ ingestService: knowledgeService,
400
+ }));
372
401
  const mediaProviders = new MediaProviderRegistry();
373
402
  ensureBuiltinMediaProviders(mediaProviders, artifactStore, providerRegistry);
374
403
  const multimodalService = new MultimodalService(artifactStore, mediaProviders, voiceService, knowledgeService);
@@ -497,6 +526,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
497
526
  artifactStore,
498
527
  knowledgeService,
499
528
  homeGraphService,
529
+ projectPlanningService,
530
+ projectPlanningProjectId,
500
531
  memoryStore,
501
532
  memoryRegistry,
502
533
  serviceRegistry,
@@ -0,0 +1,228 @@
1
+ import { format } from 'node:util';
2
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
3
+
4
+ type WritableStreamLike = {
5
+ write: {
6
+ (buffer: string | Uint8Array, cb?: (error?: Error | null) => void): boolean;
7
+ (buffer: string | Uint8Array, encoding?: BufferEncoding, cb?: (error?: Error | null) => void): boolean;
8
+ };
9
+ };
10
+
11
+ export type TerminalOutputInterceptSource =
12
+ | 'stdout'
13
+ | 'stderr'
14
+ | 'console.debug'
15
+ | 'console.error'
16
+ | 'console.info'
17
+ | 'console.log'
18
+ | 'console.warn';
19
+
20
+ export type TerminalOutputIntercept = {
21
+ readonly source: TerminalOutputInterceptSource;
22
+ readonly text: string;
23
+ readonly preview: string;
24
+ };
25
+
26
+ export type TerminalOutputGuard = {
27
+ setActive(active: boolean): void;
28
+ allowTerminalWrite<T>(fn: () => T): T;
29
+ dispose(): void;
30
+ };
31
+
32
+ export type TerminalOutputGuardOptions = {
33
+ readonly stdout: WritableStreamLike;
34
+ readonly stderr?: WritableStreamLike;
35
+ readonly active?: boolean;
36
+ readonly onIntercept?: (event: TerminalOutputIntercept) => void;
37
+ };
38
+
39
+ export type TuiTerminalOutputGuardOptions = {
40
+ readonly stdout: WritableStreamLike;
41
+ readonly stderr?: WritableStreamLike;
42
+ readonly active?: boolean;
43
+ readonly notify: (message: string) => void;
44
+ };
45
+
46
+ const MAX_LOG_TEXT = 4_000;
47
+ const MAX_PREVIEW_TEXT = 180;
48
+ const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
49
+
50
+ let currentGuard: TerminalOutputGuard | null = null;
51
+
52
+ function writeCallback(args: unknown[]): ((error?: Error | null) => void) | undefined {
53
+ const maybeCallback = args[args.length - 1];
54
+ return typeof maybeCallback === 'function'
55
+ ? maybeCallback as (error?: Error | null) => void
56
+ : undefined;
57
+ }
58
+
59
+ function chunkToText(chunk: unknown): string {
60
+ if (typeof chunk === 'string') return chunk;
61
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString('utf8');
62
+ return String(chunk);
63
+ }
64
+
65
+ function normalizeText(text: string): string {
66
+ return text.replace(ANSI_RE, '').replace(/\r/g, '').trim();
67
+ }
68
+
69
+ function previewText(text: string): string {
70
+ const singleLine = normalizeText(text).replace(/\s+/g, ' ');
71
+ if (singleLine.length <= MAX_PREVIEW_TEXT) return singleLine;
72
+ return `${singleLine.slice(0, MAX_PREVIEW_TEXT - 1)}...`;
73
+ }
74
+
75
+ function truncateForLog(text: string): string {
76
+ if (text.length <= MAX_LOG_TEXT) return text;
77
+ return `${text.slice(0, MAX_LOG_TEXT)}\n[truncated ${text.length - MAX_LOG_TEXT} byte(s)]`;
78
+ }
79
+
80
+ function invokeSuppressedCallback(args: unknown[]): void {
81
+ const callback = writeCallback(args);
82
+ if (callback) {
83
+ queueMicrotask(() => callback(null));
84
+ }
85
+ }
86
+
87
+ export function allowTerminalWrite<T>(fn: () => T): T {
88
+ return currentGuard ? currentGuard.allowTerminalWrite(fn) : fn();
89
+ }
90
+
91
+ export function installTerminalOutputGuard(options: TerminalOutputGuardOptions): TerminalOutputGuard {
92
+ const stdout = options.stdout;
93
+ const stderr = options.stderr ?? process.stderr;
94
+ const originalStdoutWriteMethod = stdout.write;
95
+ const originalStderrWriteMethod = stderr.write;
96
+ const originalStdoutWrite = (...args: unknown[]): boolean =>
97
+ Reflect.apply(originalStdoutWriteMethod, stdout, args) as boolean;
98
+ const originalStderrWrite = (...args: unknown[]): boolean =>
99
+ Reflect.apply(originalStderrWriteMethod, stderr, args) as boolean;
100
+ const originalConsole = {
101
+ debug: console.debug.bind(console),
102
+ error: console.error.bind(console),
103
+ info: console.info.bind(console),
104
+ log: console.log.bind(console),
105
+ warn: console.warn.bind(console),
106
+ };
107
+
108
+ let active = options.active ?? true;
109
+ let disposed = false;
110
+ let allowDepth = 0;
111
+ let captureDepth = 0;
112
+
113
+ const record = (source: TerminalOutputInterceptSource, text: string): void => {
114
+ if (disposed || !active) return;
115
+ const normalized = normalizeText(text);
116
+ if (!normalized) return;
117
+ if (normalized.startsWith('[ActivityLogger]')) return;
118
+ if (captureDepth > 0) return;
119
+
120
+ captureDepth++;
121
+ try {
122
+ const event: TerminalOutputIntercept = {
123
+ source,
124
+ text: truncateForLog(normalized),
125
+ preview: previewText(normalized),
126
+ };
127
+ logger.warn('Intercepted terminal output while TUI renderer was active', {
128
+ source: event.source,
129
+ text: event.text,
130
+ });
131
+ options.onIntercept?.(event);
132
+ } finally {
133
+ captureDepth--;
134
+ }
135
+ };
136
+
137
+ const shouldPassThrough = (): boolean => !active || allowDepth > 0 || disposed;
138
+
139
+ stdout.write = ((...args: unknown[]) => {
140
+ if (shouldPassThrough()) {
141
+ return originalStdoutWrite(...args);
142
+ }
143
+ record('stdout', chunkToText(args[0]));
144
+ invokeSuppressedCallback(args);
145
+ return true;
146
+ }) as WritableStreamLike['write'];
147
+
148
+ stderr.write = ((...args: unknown[]) => {
149
+ if (shouldPassThrough()) {
150
+ return originalStderrWrite(...args);
151
+ }
152
+ record('stderr', chunkToText(args[0]));
153
+ invokeSuppressedCallback(args);
154
+ return true;
155
+ }) as WritableStreamLike['write'];
156
+
157
+ console.debug = (...args: unknown[]) => {
158
+ if (!active || disposed) return originalConsole.debug(...args);
159
+ record('console.debug', format(...args));
160
+ };
161
+ console.error = (...args: unknown[]) => {
162
+ if (!active || disposed) return originalConsole.error(...args);
163
+ record('console.error', format(...args));
164
+ };
165
+ console.info = (...args: unknown[]) => {
166
+ if (!active || disposed) return originalConsole.info(...args);
167
+ record('console.info', format(...args));
168
+ };
169
+ console.log = (...args: unknown[]) => {
170
+ if (!active || disposed) return originalConsole.log(...args);
171
+ record('console.log', format(...args));
172
+ };
173
+ console.warn = (...args: unknown[]) => {
174
+ if (!active || disposed) return originalConsole.warn(...args);
175
+ record('console.warn', format(...args));
176
+ };
177
+
178
+ const guard: TerminalOutputGuard = {
179
+ setActive(nextActive) {
180
+ active = nextActive;
181
+ },
182
+ allowTerminalWrite<T>(fn: () => T): T {
183
+ allowDepth++;
184
+ try {
185
+ return fn();
186
+ } finally {
187
+ allowDepth--;
188
+ }
189
+ },
190
+ dispose() {
191
+ if (disposed) return;
192
+ disposed = true;
193
+ stdout.write = originalStdoutWriteMethod;
194
+ stderr.write = originalStderrWriteMethod;
195
+ console.debug = originalConsole.debug;
196
+ console.error = originalConsole.error;
197
+ console.info = originalConsole.info;
198
+ console.log = originalConsole.log;
199
+ console.warn = originalConsole.warn;
200
+ if (currentGuard === guard) {
201
+ currentGuard = null;
202
+ }
203
+ },
204
+ };
205
+
206
+ currentGuard = guard;
207
+ return guard;
208
+ }
209
+
210
+ export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOptions): TerminalOutputGuard {
211
+ let capturedWriteCount = 0;
212
+ let lastNoticeAt = 0;
213
+ return installTerminalOutputGuard({
214
+ stdout: options.stdout,
215
+ stderr: options.stderr,
216
+ active: options.active,
217
+ onIntercept: (event) => {
218
+ capturedWriteCount++;
219
+ const now = Date.now();
220
+ if (now - lastNoticeAt < 5_000) return;
221
+ const count = capturedWriteCount;
222
+ capturedWriteCount = 0;
223
+ lastNoticeAt = now;
224
+ const plural = count === 1 ? '' : 's';
225
+ options.notify(`[Terminal] Captured ${count} direct ${event.source} write${plural} that would have corrupted the TUI: ${event.preview}`);
226
+ },
227
+ });
228
+ }
@@ -79,6 +79,8 @@ export interface UiPlatformServices {
79
79
  export interface UiPlanningServices {
80
80
  readonly planManager: RuntimeServices['planManager'];
81
81
  readonly adaptivePlanner: RuntimeServices['adaptivePlanner'];
82
+ readonly projectPlanningService: RuntimeServices['projectPlanningService'];
83
+ readonly projectPlanningProjectId: RuntimeServices['projectPlanningProjectId'];
82
84
  }
83
85
 
84
86
  export interface UiCoordinationServices {
@@ -169,6 +171,8 @@ export function createUiRuntimeServices(
169
171
  planning: {
170
172
  planManager: runtimeServices.planManager,
171
173
  adaptivePlanner: runtimeServices.adaptivePlanner,
174
+ projectPlanningService: runtimeServices.projectPlanningService,
175
+ projectPlanningProjectId: runtimeServices.projectPlanningProjectId,
172
176
  },
173
177
  coordination: {
174
178
  approvalBroker: runtimeServices.approvalBroker,
@@ -10,6 +10,7 @@ import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp/registry';
10
10
  import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
11
11
  import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config/secrets';
12
12
  import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
13
+ import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
13
14
 
14
15
  type WireShellUiOpenersOptions = {
15
16
  commandContext: CommandContext;
@@ -112,7 +113,8 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
112
113
  }
113
114
 
114
115
  const getCurrentModelForPickerTarget = (): string => {
115
- const target = input.modelPicker.target;
116
+ const selectedTarget = input.modelPicker.getSelectedTargetInfo();
117
+ const target = selectedTarget?.target ?? input.modelPicker.target;
116
118
  if (target === 'helper') return String(configManager.get('helper.globalModel') || runtime.model);
117
119
  if (target === 'tool') return String(configManager.get('tools.llmModel') || runtime.model);
118
120
  if (target === 'tts') return String(configManager.get('tts.llmModel') || runtime.model);
@@ -120,13 +122,64 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
120
122
  };
121
123
 
122
124
  const getCurrentProviderForPickerTarget = (): string => {
123
- const target = input.modelPicker.target;
125
+ const selectedTarget = input.modelPicker.getSelectedTargetInfo();
126
+ const target = selectedTarget?.target ?? input.modelPicker.target;
124
127
  if (target === 'helper') return String(configManager.get('helper.globalProvider') || runtime.provider);
125
128
  if (target === 'tool') return String(configManager.get('tools.llmProvider') || runtime.provider);
126
129
  if (target === 'tts') return String(configManager.get('tts.llmProvider') || runtime.provider);
127
130
  return runtime.provider;
128
131
  };
129
132
 
133
+ const buildModelPickerTargets = (): ModelPickerTargetInfo[] => {
134
+ const mainProvider = String(configManager.get('provider.provider') || runtime.provider || '').trim();
135
+ const mainModel = String(configManager.get('provider.model') || runtime.model || '').trim();
136
+ const helperProvider = String(configManager.get('helper.globalProvider') ?? '').trim();
137
+ const helperModel = String(configManager.get('helper.globalModel') ?? '').trim();
138
+ const toolProvider = String(configManager.get('tools.llmProvider') ?? '').trim();
139
+ const toolModel = String(configManager.get('tools.llmModel') ?? '').trim();
140
+ const ttsProvider = String(configManager.get('tts.llmProvider') ?? '').trim();
141
+ const ttsModel = String(configManager.get('tts.llmModel') ?? '').trim();
142
+
143
+ return [
144
+ {
145
+ target: 'main',
146
+ label: 'Main Chat',
147
+ description: 'Default provider and model for normal chat turns in this TUI session.',
148
+ provider: mainProvider,
149
+ model: mainModel,
150
+ enabled: true,
151
+ inherited: false,
152
+ },
153
+ {
154
+ target: 'helper',
155
+ label: 'Helper Model',
156
+ description: 'Optional helper route used for supporting work. Empty provider/model values inherit Main Chat.',
157
+ provider: helperProvider || mainProvider,
158
+ model: helperModel || mainModel,
159
+ enabled: Boolean(configManager.get('helper.enabled')),
160
+ inherited: helperProvider.length === 0 && helperModel.length === 0,
161
+ },
162
+ {
163
+ target: 'tool',
164
+ label: 'Tool LLM',
165
+ description: 'Optional LLM route for tool-specific reasoning. Selecting a model enables the tool LLM route.',
166
+ provider: toolProvider || mainProvider,
167
+ model: toolModel || mainModel,
168
+ enabled: Boolean(configManager.get('tools.llmEnabled')),
169
+ inherited: toolProvider.length === 0 && toolModel.length === 0,
170
+ },
171
+ {
172
+ target: 'tts',
173
+ label: 'TTS LLM',
174
+ description: 'Optional LLM override for /tts response generation. Empty values use the current chat model.',
175
+ provider: ttsProvider || mainProvider,
176
+ model: ttsModel || mainModel,
177
+ enabled: true,
178
+ inherited: ttsProvider.length === 0 && ttsModel.length === 0,
179
+ },
180
+ ];
181
+ };
182
+
130
183
  commandContext.openModelPicker = () => {
131
184
  void (async () => {
132
185
  const models = providerRegistry.getSelectableModels();
@@ -140,6 +193,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
140
193
  });
141
194
  void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
142
195
  input.modalOpened('modelPicker');
196
+ input.modelPicker.setTargetInfos(buildModelPickerTargets());
143
197
  input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
144
198
  render();
145
199
  })().catch((error: unknown) => {
@@ -159,6 +213,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
159
213
  const secretProviderIds = await resolveSecretProviderIds();
160
214
  input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager, secretProviderIds);
161
215
  input.modalOpened('modelPicker');
216
+ input.modelPicker.setTargetInfos(buildModelPickerTargets());
162
217
  input.modelPicker.openProviders(providers, getCurrentProviderForPickerTarget());
163
218
  render();
164
219
  })().catch((error: unknown) => {
@@ -206,9 +261,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
206
261
  render();
207
262
  };
208
263
 
209
- commandContext.openSettingsModal = () => {
264
+ commandContext.openSettingsModal = (target?: string) => {
210
265
  input.modalOpened('settings');
211
266
  input.settingsModal.open(configManager, featureFlags, subscriptionManager, serviceRegistry, mcpRegistry, secretsManager);
267
+ input.settingsModal.selectTarget(target);
212
268
  render();
213
269
  };
214
270
 
@@ -1,5 +1,6 @@
1
1
  import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
2
2
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
3
+ import { allowTerminalWrite } from '../runtime/terminal-output-guard.ts';
3
4
 
4
5
  /**
5
6
  * copyToClipboard - Uses OSC 52 escape sequence to copy text to the terminal clipboard.
@@ -11,7 +12,7 @@ export function copyToClipboard(text: string) {
11
12
  try {
12
13
  const base64 = Buffer.from(text).toString('base64');
13
14
  const sequence = `\x1b]52;c;${base64}\x07`;
14
- process.stdout.write(sequence);
15
+ allowTerminalWrite(() => process.stdout.write(sequence));
15
16
  logger.info('Clipboard: OSC 52 sequence written');
16
17
  } catch (err: unknown) {
17
18
  logger.error('Clipboard: OSC 52 copy failed', { error: summarizeError(err) });
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.19.53';
9
+ let _version = '0.19.55';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;
@@ -1,104 +0,0 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
2
- import type { SelectionItem } from '../selection-modal.ts';
3
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
4
-
5
- const VALID_MODES = ['allow-all', 'prompt', 'custom'] as const;
6
- const VALID_ACTIONS = ['allow', 'prompt', 'deny'] as const;
7
- const VALID_TOOLS = ['read', 'write', 'edit', 'exec', 'find', 'fetch', 'analyze', 'inspect', 'agent', 'state', 'workflow', 'registry', 'delegate', 'mcp'] as const;
8
- type PermTool = typeof VALID_TOOLS[number];
9
-
10
- export function registerPermissionsRuntimeCommands(registry: CommandRegistry): void {
11
- registry.register({
12
- name: 'permissions',
13
- aliases: ['perms'],
14
- description: 'Show or set permission mode and per-tool settings',
15
- usage: '[allow-all|prompt|custom] | [tool <name> allow|prompt|deny]',
16
- argsHint: '[allow-all|prompt|custom]',
17
- handler(args, ctx) {
18
- const cm = ctx.platform.configManager;
19
- if (args.length === 0) {
20
- if (ctx.openSelection) {
21
- const items: SelectionItem[] = VALID_TOOLS.map((tool) => ({
22
- id: tool,
23
- label: tool,
24
- detail: cm.get(`permissions.tools.${tool}` as Parameters<typeof cm.get>[0]) as string,
25
- category: 'tools',
26
- adjustable: true,
27
- primaryAction: 'toggle',
28
- actions: '[Space/Enter] cycle [←/→] adjust',
29
- }));
30
- items.unshift({
31
- id: '__mode__',
32
- label: 'permission mode',
33
- detail: cm.get('permissions.mode') as string,
34
- category: 'global',
35
- adjustable: true,
36
- primaryAction: 'toggle',
37
- actions: '[Space/Enter] cycle [←/→] adjust',
38
- });
39
- ctx.openSelection('Permissions', items, { allowSearch: true }, (result) => {
40
- if (!result) return;
41
- if (result.item.id === '__mode__') {
42
- const currentMode = cm.get('permissions.mode') as string;
43
- const currentIndex = Math.max(0, VALID_MODES.indexOf(currentMode as typeof VALID_MODES[number]));
44
- const nextMode = result.action === 'decrement'
45
- ? VALID_MODES[(currentIndex - 1 + VALID_MODES.length) % VALID_MODES.length]
46
- : VALID_MODES[(currentIndex + 1) % VALID_MODES.length];
47
- cm.setDynamic('permissions.mode', nextMode);
48
- result.item.detail = nextMode;
49
- } else {
50
- const toolKey = `permissions.tools.${result.item.id}` as Parameters<typeof cm.get>[0];
51
- const currentAction = cm.get(toolKey) as string;
52
- const currentIndex = Math.max(0, VALID_ACTIONS.indexOf(currentAction as typeof VALID_ACTIONS[number]));
53
- const nextAction = result.action === 'decrement'
54
- ? VALID_ACTIONS[(currentIndex - 1 + VALID_ACTIONS.length) % VALID_ACTIONS.length]
55
- : VALID_ACTIONS[(currentIndex + 1) % VALID_ACTIONS.length];
56
- cm.setDynamic(toolKey, nextAction);
57
- result.item.detail = nextAction;
58
- }
59
- ctx.renderRequest();
60
- });
61
- return;
62
- }
63
- const lines = [`Permission mode: ${cm.get('permissions.mode')}`, ' Tool settings:'];
64
- for (const tool of VALID_TOOLS) lines.push(` ${tool.padEnd(16)} ${cm.get(`permissions.tools.${tool}` as Parameters<typeof cm.get>[0])}`);
65
- lines.push('', ' Modes: prompt (default), allow-all, custom', ' Usage: /permissions <mode> | /permissions tool <name> allow|prompt|deny');
66
- ctx.print(lines.join('\n'));
67
- return;
68
- }
69
- if (args[0] === 'tool') {
70
- const toolName = args[1];
71
- const action = args[2];
72
- if (!toolName || !action) {
73
- ctx.print('Usage: /permissions tool <name> allow|prompt|deny');
74
- return;
75
- }
76
- if (!VALID_TOOLS.includes(toolName as PermTool)) {
77
- ctx.print(`Unknown tool: ${toolName}\nValid tools: ${VALID_TOOLS.join(', ')}`);
78
- return;
79
- }
80
- if (!VALID_ACTIONS.includes(action as typeof VALID_ACTIONS[number])) {
81
- ctx.print(`Invalid action: ${action}\nValid actions: allow, prompt, deny`);
82
- return;
83
- }
84
- try {
85
- cm.setDynamic(`permissions.tools.${toolName}` as Parameters<typeof cm.set>[0], action);
86
- ctx.print(`Permission for ${toolName} set to: ${action}`);
87
- } catch (e) {
88
- ctx.print(`Error: ${summarizeError(e)}`);
89
- }
90
- return;
91
- }
92
- if (!VALID_MODES.includes(args[0] as typeof VALID_MODES[number])) {
93
- ctx.print(`Invalid mode: ${args[0]}\nValid modes: ${VALID_MODES.join(', ')}`);
94
- return;
95
- }
96
- try {
97
- cm.setDynamic('permissions.mode', args[0]);
98
- ctx.print(`Permission mode set to: ${args[0]}`);
99
- } catch (e) {
100
- ctx.print(`Error: ${summarizeError(e)}`);
101
- }
102
- },
103
- });
104
- }