@pellux/goodvibes-tui 0.19.98 → 0.19.99

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.98",
3
+ "version": "0.19.99",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -97,7 +97,7 @@
97
97
  "@anthropic-ai/vertex-sdk": "^0.16.0",
98
98
  "@ast-grep/napi": "^0.42.0",
99
99
  "@aws/bedrock-token-generator": "^1.1.0",
100
- "@pellux/goodvibes-sdk": "0.33.26",
100
+ "@pellux/goodvibes-sdk": "0.33.27",
101
101
  "bash-language-server": "^5.6.0",
102
102
  "fuse.js": "^7.1.0",
103
103
  "graphql": "^16.13.2",
@@ -115,7 +115,7 @@ export interface CommandShellUiOpeners {
115
115
  openPolicyPanel?: () => void;
116
116
  openHooksPanel?: () => void;
117
117
  openCommunicationPanel?: () => void;
118
- openMcpPanel?: () => void;
118
+ openMcpWorkspace?: () => void;
119
119
  openSecurityPanel?: () => void;
120
120
  openKnowledgePanel?: () => void;
121
121
  openRemotePanel?: () => void;
@@ -1,20 +1,150 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
2
- import { requireMcpApi } from './runtime-services.ts';
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import type { McpConfigScope, McpReloadResult, McpServerConfig } from '@pellux/goodvibes-sdk/platform/mcp';
3
+ import { requireMcpApi, requireShellPaths } from './runtime-services.ts';
3
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
5
 
6
+ const MCP_ROLES = ['general', 'docs', 'filesystem', 'git', 'database', 'browser', 'automation', 'ops', 'remote'] as const;
7
+ const MCP_TRUST_MODES = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'] as const;
8
+
9
+ interface ParsedMcpAddArgs {
10
+ readonly scope: McpConfigScope;
11
+ readonly server: McpServerConfig;
12
+ }
13
+
14
+ function isMcpRole(value: string): value is NonNullable<McpServerConfig['role']> {
15
+ return MCP_ROLES.includes(value as NonNullable<McpServerConfig['role']>);
16
+ }
17
+
18
+ function isMcpTrustMode(value: string): value is NonNullable<McpServerConfig['trustMode']> {
19
+ return MCP_TRUST_MODES.includes(value as NonNullable<McpServerConfig['trustMode']>);
20
+ }
21
+
22
+ function isMcpScope(value: string): value is McpConfigScope {
23
+ return value === 'project' || value === 'global';
24
+ }
25
+
26
+ function validateServerName(name: string): string | null {
27
+ if (!name.trim()) return 'MCP server name is required.';
28
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
29
+ return 'MCP server names may contain letters, numbers, dot, underscore, and dash only.';
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function readFlagValue(tokens: string[], index: number, flag: string): string {
35
+ const value = tokens[index + 1];
36
+ if (!value) {
37
+ throw new Error(`Missing value after ${flag}.`);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ function parseAddServerArgs(args: string[]): ParsedMcpAddArgs {
43
+ const name = args[1]?.trim();
44
+ const command = args[2]?.trim();
45
+ if (!name || !command) {
46
+ throw new Error('Usage: /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>] [--env KEY=VALUE] [--path <path>] [--host <host>]');
47
+ }
48
+ const nameError = validateServerName(name);
49
+ if (nameError) throw new Error(nameError);
50
+
51
+ const serverArgs: string[] = [];
52
+ const env: Record<string, string> = {};
53
+ const allowedPaths: string[] = [];
54
+ const allowedHosts: string[] = [];
55
+ let role: McpServerConfig['role'];
56
+ let trustMode: McpServerConfig['trustMode'];
57
+ let scope: McpConfigScope = 'project';
58
+ let passthrough = false;
59
+ const tokens = args.slice(3);
60
+
61
+ for (let index = 0; index < tokens.length; index += 1) {
62
+ const token = tokens[index]!;
63
+ if (passthrough) {
64
+ serverArgs.push(token);
65
+ continue;
66
+ }
67
+ if (token === '--') {
68
+ passthrough = true;
69
+ continue;
70
+ }
71
+ if (token === '--role') {
72
+ const value = readFlagValue(tokens, index, token);
73
+ if (!isMcpRole(value)) throw new Error(`Invalid MCP role "${value}". Expected one of: ${MCP_ROLES.join(', ')}`);
74
+ role = value;
75
+ index += 1;
76
+ continue;
77
+ }
78
+ if (token === '--scope') {
79
+ const value = readFlagValue(tokens, index, token);
80
+ if (!isMcpScope(value)) throw new Error(`Invalid MCP scope "${value}". Expected project or global.`);
81
+ scope = value;
82
+ index += 1;
83
+ continue;
84
+ }
85
+ if (token === '--trust') {
86
+ const value = readFlagValue(tokens, index, token);
87
+ if (!isMcpTrustMode(value)) throw new Error(`Invalid MCP trust mode "${value}". Expected one of: ${MCP_TRUST_MODES.join(', ')}`);
88
+ trustMode = value;
89
+ index += 1;
90
+ continue;
91
+ }
92
+ if (token === '--env') {
93
+ const value = readFlagValue(tokens, index, token);
94
+ const eq = value.indexOf('=');
95
+ if (eq <= 0) throw new Error('MCP env entries must use KEY=VALUE.');
96
+ env[value.slice(0, eq)] = value.slice(eq + 1);
97
+ index += 1;
98
+ continue;
99
+ }
100
+ if (token === '--path') {
101
+ allowedPaths.push(readFlagValue(tokens, index, token));
102
+ index += 1;
103
+ continue;
104
+ }
105
+ if (token === '--host') {
106
+ allowedHosts.push(readFlagValue(tokens, index, token));
107
+ index += 1;
108
+ continue;
109
+ }
110
+ serverArgs.push(token);
111
+ }
112
+
113
+ return {
114
+ scope,
115
+ server: {
116
+ name,
117
+ command,
118
+ ...(serverArgs.length > 0 ? { args: serverArgs } : {}),
119
+ ...(Object.keys(env).length > 0 ? { env } : {}),
120
+ ...(role ? { role } : {}),
121
+ ...(trustMode ? { trustMode } : {}),
122
+ ...(allowedPaths.length > 0 ? { allowedPaths } : {}),
123
+ ...(allowedHosts.length > 0 ? { allowedHosts } : {}),
124
+ },
125
+ };
126
+ }
127
+
128
+ async function reloadMcpRuntime(ctx: CommandContext): Promise<McpReloadResult> {
129
+ const result = await requireMcpApi(ctx).reload(requireShellPaths(ctx));
130
+ ctx.renderRequest();
131
+ return result;
132
+ }
133
+
5
134
  export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
6
135
  registry.register({
7
136
  name: 'mcp',
8
137
  aliases: [],
9
- description: 'List connected MCP servers and their tools',
10
- usage: '[review|tools [<server>]|auth-review|repair [server]]',
11
- argsHint: '[review|tools [server]|auth-review|repair [server]]',
138
+ description: 'Manage MCP servers and their tools',
139
+ usage: '[add|remove|reload|config|review|tools [<server>]|auth-review|repair [server]]',
140
+ argsHint: '[add|remove|reload|config|review|tools [server]]',
12
141
  async handler(args, ctx) {
13
142
  const mcpApi = requireMcpApi(ctx);
14
143
  const listServerSecurity = () => mcpApi.listServerSecurity();
15
144
  const subcommand = args[0];
16
- if (!subcommand && ctx.openMcpPanel) {
17
- ctx.openMcpPanel();
145
+ if (!subcommand && ctx.openMcpWorkspace) {
146
+ ctx.openMcpWorkspace();
147
+ return;
18
148
  }
19
149
  if (subcommand === 'review') {
20
150
  const servers = listServerSecurity();
@@ -141,6 +271,102 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
141
271
  }
142
272
  }
143
273
 
274
+ if (subcommand === 'add') {
275
+ let parsed: ParsedMcpAddArgs;
276
+ try {
277
+ parsed = parseAddServerArgs(args);
278
+ } catch (error) {
279
+ ctx.print(summarizeError(error));
280
+ return;
281
+ }
282
+ const shellPaths = requireShellPaths(ctx);
283
+ try {
284
+ const result = await mcpApi.upsertServerConfig(shellPaths, parsed.scope, parsed.server);
285
+ const connected = listServerSecurity().find((entry) => entry.name === parsed.server.name)?.connected ?? false;
286
+ ctx.print([
287
+ `MCP server "${parsed.server.name}" saved to ${parsed.scope} config: ${result.path}.`,
288
+ `Runtime reload: ${connected ? 'connected' : 'server saved; connection needs attention'} (+${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}).`,
289
+ `Command: ${parsed.server.command}${parsed.server.args?.length ? ` ${parsed.server.args.join(' ')}` : ''}`,
290
+ 'Next: /mcp tools',
291
+ ].join('\n'));
292
+ } catch (error) {
293
+ ctx.print(`MCP add failed: ${summarizeError(error)}`);
294
+ }
295
+ return;
296
+ }
297
+
298
+ if (subcommand === 'remove') {
299
+ const serverName = args[1]?.trim();
300
+ if (!serverName) {
301
+ ctx.print('Usage: /mcp remove <server> [--scope project|global]');
302
+ return;
303
+ }
304
+ let scope: McpConfigScope = 'project';
305
+ try {
306
+ for (let index = 2; index < args.length; index += 1) {
307
+ if (args[index] === '--scope') {
308
+ const value = readFlagValue(args, index, '--scope');
309
+ if (!isMcpScope(value)) {
310
+ ctx.print(`Invalid MCP scope "${value}". Expected project or global.`);
311
+ return;
312
+ }
313
+ scope = value;
314
+ index += 1;
315
+ }
316
+ }
317
+ } catch (error) {
318
+ ctx.print(summarizeError(error));
319
+ return;
320
+ }
321
+ const shellPaths = requireShellPaths(ctx);
322
+ try {
323
+ const result = await mcpApi.removeServerConfig(shellPaths, scope, serverName);
324
+ ctx.print(result.removed
325
+ ? `Removed MCP server "${serverName}" from ${scope} config ${result.path}. Reload: +${result.reload.added} ~${result.reload.changed} -${result.reload.removed}, unchanged ${result.reload.unchanged}.`
326
+ : `No ${scope} MCP server named "${serverName}" exists in ${result.path}.\nIf it still appears, it is coming from another config scope or external MCP config.`);
327
+ } catch (error) {
328
+ ctx.print(`MCP remove failed: ${summarizeError(error)}`);
329
+ }
330
+ return;
331
+ }
332
+
333
+ if (subcommand === 'reload') {
334
+ try {
335
+ const result = await reloadMcpRuntime(ctx);
336
+ const servers = listServerSecurity();
337
+ ctx.print(`Reloaded MCP runtime from config. ${servers.filter((server) => server.connected).length}/${servers.length} server(s) connected. Result: +${result.added} ~${result.changed} -${result.removed}, unchanged ${result.unchanged}.`);
338
+ } catch (error) {
339
+ ctx.print(`MCP reload failed: ${summarizeError(error)}`);
340
+ }
341
+ return;
342
+ }
343
+
344
+ if (subcommand === 'config') {
345
+ const shellPaths = requireShellPaths(ctx);
346
+ try {
347
+ const effective = mcpApi.getEffectiveConfig(shellPaths);
348
+ ctx.print([
349
+ 'MCP Config',
350
+ ' locations:',
351
+ ...effective.locations.map((location) => ` ${location.scope}/${location.kind}${location.writable ? ' writable' : ' read-only'} ${location.path}`),
352
+ ` effective servers: ${effective.servers.length}`,
353
+ ...effective.servers.map((entry) => {
354
+ const server = entry.server;
355
+ const envKeys = Object.keys(server.env ?? {});
356
+ return ` - ${server.name}: ${server.command}${server.args?.length ? ` ${server.args.join(' ')}` : ''} source=${entry.source.scope}/${entry.source.kind}${envKeys.length ? ` envKeys=${envKeys.join(',')}` : ''}`;
357
+ }),
358
+ '',
359
+ 'Add or update from inside the TUI:',
360
+ ' /mcp add <name> <command> [args...] [--scope project|global] [--role <role>] [--trust <mode>]',
361
+ 'Example:',
362
+ ' /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem . --scope project --role filesystem --trust constrained',
363
+ ].join('\n'));
364
+ } catch (error) {
365
+ ctx.print(`MCP config read failed: ${summarizeError(error)}`);
366
+ }
367
+ return;
368
+ }
369
+
144
370
  if (subcommand === 'quarantine') {
145
371
  const serverName = args[1];
146
372
  const action = args[2];
@@ -170,6 +396,8 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
170
396
  + ' ~/.config/claude/claude_desktop_config.json (Claude Desktop)\n'
171
397
  + ' .mcp/mcp.json (project-local)\n'
172
398
  + ' .goodvibes/mcp.json (goodvibes project)\n'
399
+ + '\nAdd one from inside the TUI:\n'
400
+ + ' /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem . --scope project --role filesystem\n'
173
401
  + '\nFormat: { "servers": [{ "name": "my-server", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }] }'
174
402
  );
175
403
  return;
@@ -188,6 +416,8 @@ export function registerMcpRuntimeCommands(registry: CommandRegistry): void {
188
416
  if (connected.length > 0) {
189
417
  lines.push('');
190
418
  lines.push('Run "/mcp tools" to list all tools, or "/mcp tools <server>" for a specific server.');
419
+ lines.push('Run "/mcp" to open the fullscreen MCP workspace, or "/mcp add <name> <command> [args...] [--scope project|global]" to add/update without restarting.');
420
+ lines.push('Run "/mcp reload" after editing MCP config outside the TUI.');
191
421
  lines.push('Run "/mcp trust <server> <mode>" to change trust mode, or "/mcp role <server> <role>" to change its coherence role.');
192
422
  lines.push('Run "/mcp quarantine <server> [detail]" to block a server, or "/mcp quarantine <server> approve [operatorId]" to approve a temporary override.');
193
423
  lines.push('Use /settings → MCP to explicitly enable allow-all for a server.');
@@ -29,6 +29,7 @@ import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
29
29
  import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
30
30
  import type { BookmarkModal } from './bookmark-modal.ts';
31
31
  import type { SettingsModal } from './settings-modal.ts';
32
+ import type { McpWorkspace } from './mcp-workspace.ts';
32
33
  import type { SessionPickerModal } from './session-picker-modal.ts';
33
34
  import type { ProfilePickerModal } from './profile-picker-modal.ts';
34
35
  import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
@@ -102,6 +103,7 @@ export interface FeedContextStableRefs {
102
103
  selectionModal: SelectionModal;
103
104
  bookmarkModal: BookmarkModal;
104
105
  settingsModal: SettingsModal;
106
+ mcpWorkspace: McpWorkspace;
105
107
  sessionPickerModal: SessionPickerModal;
106
108
  profilePickerModal: ProfilePickerModal;
107
109
  historySearch: HistorySearch;
@@ -16,6 +16,7 @@ import { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
16
16
  import { ContextInspectorModal } from '../renderer/context-inspector.ts';
17
17
  import { BookmarkModal } from './bookmark-modal.ts';
18
18
  import { SettingsModal } from './settings-modal.ts';
19
+ import type { McpWorkspace } from './mcp-workspace.ts';
19
20
  import { SessionPickerModal } from './session-picker-modal.ts';
20
21
  import { ProfilePickerModal } from './profile-picker-modal.ts';
21
22
  import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
@@ -106,6 +107,7 @@ export interface InputFeedContext {
106
107
  selectionCallback: ((result: SelectionResult | null) => void) | null;
107
108
  readonly bookmarkModal: BookmarkModal;
108
109
  readonly settingsModal: SettingsModal;
110
+ readonly mcpWorkspace: McpWorkspace;
109
111
  readonly sessionPickerModal: SessionPickerModal;
110
112
  readonly profilePickerModal: ProfilePickerModal;
111
113
  readonly historySearch: HistorySearch;
@@ -187,6 +189,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
187
189
  },
188
190
  bookmarkModal: context.bookmarkModal,
189
191
  settingsModal: context.settingsModal,
192
+ mcpWorkspace: context.mcpWorkspace,
190
193
  sessionPickerModal: context.sessionPickerModal,
191
194
  profilePickerModal: context.profilePickerModal,
192
195
  onboardingWizard: context.onboardingWizard,
@@ -238,6 +238,7 @@ export function handleEscapeForHandler(handler: InputHandler): void {
238
238
  agentDetailModal: handler.agentDetailModal,
239
239
  liveTailModal: handler.liveTailModal,
240
240
  settingsModal: handler.settingsModal,
241
+ mcpWorkspace: handler.mcpWorkspace,
241
242
  sessionPickerModal: handler.sessionPickerModal,
242
243
  profilePickerModal: handler.profilePickerModal,
243
244
  contextInspectorModal: handler.contextInspectorModal,
@@ -45,6 +45,7 @@ export type EscapeState = ModalStackState & {
45
45
  editingMode: boolean;
46
46
  cancelEdit: () => void;
47
47
  };
48
+ mcpWorkspace?: ModalStackState['mcpWorkspace'];
48
49
  selectionModal: ModalStackState['selectionModal'];
49
50
  autocompleteReset: () => void;
50
51
  autocompleteUpdate?: (query: string) => void;
@@ -122,6 +123,7 @@ export function handleEscape(state: EscapeState): {
122
123
  closeAgentDetail: () => state.agentDetailModal.close(),
123
124
  closeLiveTail: () => state.liveTailModal.close(),
124
125
  closeSettings: () => state.settingsModal.close(),
126
+ closeMcpWorkspace: () => state.mcpWorkspace?.close(),
125
127
  closeSessionPicker: () => state.sessionPickerModal.close(),
126
128
  closeProfilePicker: () => state.profilePickerModal.close(),
127
129
  closeContextInspector: () => state.contextInspectorModal.close(),
@@ -161,6 +163,7 @@ export function handleEscape(state: EscapeState): {
161
163
  openBookmark: () => state.bookmarkModal.open(),
162
164
  openProcess: () => state.processModal.open(),
163
165
  openContextInspector: () => state.contextInspectorModal.open(),
166
+ openMcpWorkspace: () => state.mcpWorkspace?.reopen(),
164
167
  openOnboarding: () => state.onboardingWizard?.reopen(),
165
168
  openCommandMode: () => {
166
169
  commandMode = true;
@@ -7,6 +7,7 @@ import type { SessionPickerModal } from './session-picker-modal.ts';
7
7
  import type { ProfilePickerModal } from './profile-picker-modal.ts';
8
8
  import type { HistorySearch } from './input-history.ts';
9
9
  import type { ModelPickerModal } from './model-picker.ts';
10
+ import { handleMcpWorkspaceToken, type McpWorkspace } from './mcp-workspace.ts';
10
11
  import type { CommandContext } from './command-registry.ts';
11
12
  import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
12
13
  import type { ProcessModal } from '../renderer/process-modal.ts';
@@ -43,6 +44,7 @@ export type ModalTokenRouteState = {
43
44
  setSelectionCallback?: (callback: ((result: SelectionResult | null) => void) | null) => void;
44
45
  bookmarkModal: BookmarkModal;
45
46
  settingsModal: SettingsModal;
47
+ mcpWorkspace: McpWorkspace;
46
48
  sessionPickerModal: SessionPickerModal;
47
49
  profilePickerModal: ProfilePickerModal;
48
50
  onboardingWizard: OnboardingWizardController;
@@ -146,6 +148,15 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
146
148
  return withState(state, true);
147
149
  }
148
150
 
151
+ if (handleMcpWorkspaceToken(
152
+ state.mcpWorkspace,
153
+ token,
154
+ state.handleEscape,
155
+ state.requestRender,
156
+ )) {
157
+ return withState(state, true);
158
+ }
159
+
149
160
  if (handleSessionPickerToken({
150
161
  sessionPickerModal: state.sessionPickerModal,
151
162
  commandContext: state.commandContext,
@@ -58,6 +58,7 @@ export type ActiveModalState = {
58
58
  agentDetailModal: { active: boolean; close: () => void };
59
59
  liveTailModal: { active: boolean; close: () => void };
60
60
  settingsModal: { active: boolean; close: () => void };
61
+ mcpWorkspace?: { active: boolean; close: () => void; reopen: () => void };
61
62
  sessionPickerModal: { active: boolean; close: () => void };
62
63
  profilePickerModal: { active: boolean; close: () => void };
63
64
  contextInspectorModal: { active: boolean; close: () => void };
@@ -77,6 +78,7 @@ export function getActiveModalName(state: ActiveModalState): string | null {
77
78
  if (state.agentDetailModal.active) return 'agentDetail';
78
79
  if (state.liveTailModal.active) return 'liveTail';
79
80
  if (state.settingsModal.active) return 'settings';
81
+ if (state.mcpWorkspace?.active) return 'mcpWorkspace';
80
82
  if (state.sessionPickerModal.active) return 'sessionPicker';
81
83
  if (state.profilePickerModal.active) return 'profilePicker';
82
84
  if (state.contextInspectorModal.active) return 'contextInspector';
@@ -97,6 +99,7 @@ export type ModalCloseOps = {
97
99
  closeAgentDetail: () => void;
98
100
  closeLiveTail: () => void;
99
101
  closeSettings: () => void;
102
+ closeMcpWorkspace: () => void;
100
103
  closeSessionPicker: () => void;
101
104
  closeProfilePicker: () => void;
102
105
  closeContextInspector: () => void;
@@ -129,6 +132,9 @@ export function closeModalByName(name: string, ops: ModalCloseOps): void {
129
132
  case 'settings':
130
133
  ops.closeSettings();
131
134
  break;
135
+ case 'mcpWorkspace':
136
+ ops.closeMcpWorkspace();
137
+ break;
132
138
  case 'sessionPicker':
133
139
  ops.closeSessionPicker();
134
140
  break;
@@ -168,6 +174,7 @@ export type ModalOpenOps = {
168
174
  openBookmark: () => void;
169
175
  openProcess: () => void;
170
176
  openContextInspector: () => void;
177
+ openMcpWorkspace?: () => void;
171
178
  openOnboarding?: () => void;
172
179
  openCommandMode: () => void;
173
180
  };
@@ -189,6 +196,9 @@ export function reopenModalByName(name: string, ops: ModalOpenOps): void {
189
196
  case 'contextInspector':
190
197
  ops.openContextInspector();
191
198
  break;
199
+ case 'mcpWorkspace':
200
+ ops.openMcpWorkspace?.();
201
+ break;
192
202
  case 'onboarding':
193
203
  ops.openOnboarding?.();
194
204
  break;
@@ -25,6 +25,7 @@ import { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
25
25
  import { ContextInspectorModal } from '../renderer/context-inspector.ts';
26
26
  import { BookmarkModal } from './bookmark-modal.ts';
27
27
  import { SettingsModal } from './settings-modal.ts';
28
+ import { McpWorkspace } from './mcp-workspace.ts';
28
29
  import { SessionPickerModal } from './session-picker-modal.ts';
29
30
  import { ProfilePickerModal } from './profile-picker-modal.ts';
30
31
  import { OnboardingWizardController, type OnboardingWizardAction, type OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
@@ -157,6 +158,7 @@ export class InputHandler {
157
158
  public bookmarkModal: BookmarkModal;
158
159
  public blockActionsMenu = new BlockActionsMenu();
159
160
  public settingsModal = new SettingsModal();
161
+ public mcpWorkspace = new McpWorkspace();
160
162
  public onboardingWizard = new OnboardingWizardController();
161
163
  public onboardingModelPickerCancelSnapshot: OnboardingWizardSnapshot | null = null;
162
164
  public onboardingHydrationSerial = 0;
@@ -275,6 +277,7 @@ export class InputHandler {
275
277
  selectionModal: this.selectionModal,
276
278
  bookmarkModal: this.bookmarkModal,
277
279
  settingsModal: this.settingsModal,
280
+ mcpWorkspace: this.mcpWorkspace,
278
281
  sessionPickerModal: this.sessionPickerModal,
279
282
  profilePickerModal: this.profilePickerModal,
280
283
  historySearch: this.historySearch,
@@ -411,6 +414,13 @@ export class InputHandler {
411
414
  public restoreOnboardingModelPickerCancelState(): void { restoreOnboardingModelPickerCancelStateForHandler(this); }
412
415
  public openModelPickerWithTarget(target: ModelPickerTarget, source: 'settings' | 'onboarding' = 'settings'): boolean { return openModelPickerWithTargetForHandler(this, target, source); }
413
416
  public openProviderModelPickerWithTarget(target: ModelPickerTarget, source: 'settings' | 'onboarding' = 'settings'): boolean { return openProviderModelPickerWithTargetForHandler(this, target, source); }
417
+ public openMcpWorkspace(context: CommandContext): void {
418
+ this.panelFocused = false;
419
+ this.indicatorFocused = false;
420
+ this.modalOpened('mcpWorkspace');
421
+ this.mcpWorkspace.open(context);
422
+ this.requestRender();
423
+ }
414
424
  public handleModelPickerCommit(): boolean { return handleModelPickerCommitForHandler(this); }
415
425
  public async handleOnboardingAction(action: OnboardingWizardAction): Promise<void> { await handleOnboardingActionForHandler(this, action); }
416
426
  public async refreshOnboardingHydration(options: { readonly preserveValues?: boolean; readonly targetStepId?: string } = {}): Promise<void> { await refreshOnboardingHydrationForHandler(this, options); }