@pellux/goodvibes-tui 0.19.96 → 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/CHANGELOG.md +15 -0
- package/README.md +23 -3
- package/docs/foundation-artifacts/operator-contract.json +1376 -290
- package/package.json +2 -2
- package/src/input/command-registry.ts +6 -1
- package/src/input/commands/mcp-runtime.ts +237 -7
- package/src/input/commands/shell-core.ts +18 -1
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +27 -0
- package/src/input/handler-content-actions.ts +36 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +1 -0
- package/src/input/handler-modal-stack.ts +3 -0
- package/src/input/handler-modal-token-routes.ts +11 -0
- package/src/input/handler-ui-state.ts +10 -0
- package/src/input/handler.ts +17 -1
- package/src/input/mcp-workspace.ts +554 -0
- package/src/main.ts +1 -0
- package/src/mcp/runtime-reload.ts +81 -0
- package/src/panels/builtin/operations.ts +1 -11
- package/src/panels/builtin/shared.ts +2 -2
- package/src/panels/index.ts +0 -1
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/mcp-workspace.ts +297 -0
- package/src/runtime/bootstrap-command-parts.ts +2 -4
- package/src/runtime/bootstrap.ts +26 -2
- package/src/shell/ui-openers.ts +5 -0
- package/src/version.ts +1 -1
- package/src/panels/mcp-panel.ts +0 -215
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
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.
|
|
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",
|
|
@@ -61,6 +61,11 @@ export interface CommandUiActions {
|
|
|
61
61
|
submitInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers').ContentPart[]) => void;
|
|
62
62
|
submitSpokenInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers').ContentPart[]) => void;
|
|
63
63
|
stopSpokenOutput?: () => void;
|
|
64
|
+
pasteFromClipboard?: () => {
|
|
65
|
+
pasted: boolean;
|
|
66
|
+
kind: 'image' | 'text' | 'none';
|
|
67
|
+
marker?: string;
|
|
68
|
+
};
|
|
64
69
|
executeCommand?: (name: string, args: string[]) => Promise<boolean>;
|
|
65
70
|
cancelGeneration?: () => void;
|
|
66
71
|
completeModelSelection?: (selection: {
|
|
@@ -110,7 +115,7 @@ export interface CommandShellUiOpeners {
|
|
|
110
115
|
openPolicyPanel?: () => void;
|
|
111
116
|
openHooksPanel?: () => void;
|
|
112
117
|
openCommunicationPanel?: () => void;
|
|
113
|
-
|
|
118
|
+
openMcpWorkspace?: () => void;
|
|
114
119
|
openSecurityPanel?: () => void;
|
|
115
120
|
openKnowledgePanel?: () => void;
|
|
116
121
|
openRemotePanel?: () => void;
|
|
@@ -1,20 +1,150 @@
|
|
|
1
|
-
import type { CommandRegistry } from '../command-registry.ts';
|
|
2
|
-
import {
|
|
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: '
|
|
10
|
-
usage: '[review|tools [<server>]|auth-review|repair [server]]',
|
|
11
|
-
argsHint: '[
|
|
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.
|
|
17
|
-
ctx.
|
|
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.');
|
|
@@ -91,6 +91,22 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
91
91
|
},
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
registry.register({
|
|
95
|
+
name: 'paste',
|
|
96
|
+
aliases: ['clip'],
|
|
97
|
+
description: 'Insert clipboard text or image into the prompt',
|
|
98
|
+
handler(_args, ctx) {
|
|
99
|
+
if (!ctx.pasteFromClipboard) {
|
|
100
|
+
ctx.print('Paste is not available in this context.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const result = ctx.pasteFromClipboard();
|
|
104
|
+
if (!result.pasted) {
|
|
105
|
+
ctx.print('Clipboard does not contain supported text or image data.');
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
registry.register({
|
|
95
111
|
name: 'help',
|
|
96
112
|
aliases: ['h', '?'],
|
|
@@ -135,6 +151,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
135
151
|
{ id: '/template save', label: '/template save <name>', detail: 'Save prompt as template', category: 'Templates' },
|
|
136
152
|
{ id: '/template use', label: '/template use <name>', detail: 'Execute template', category: 'Templates' },
|
|
137
153
|
{ id: '/tools', label: '/tools', detail: 'List available tools', category: 'Tools & System' },
|
|
154
|
+
{ id: '/paste', label: '/paste', detail: 'Insert clipboard text or image into the prompt', category: 'Tools & System' },
|
|
138
155
|
{ id: '/shortcuts', label: '/shortcuts', detail: 'View keyboard shortcuts reference', category: 'Tools & System' },
|
|
139
156
|
{ id: '/commands', label: '/commands', detail: 'Browse all commands in a scrollable list', category: 'Tools & System' },
|
|
140
157
|
{ id: '/secrets', label: '/secrets set|link|get|test|list|delete', detail: 'Manage encrypted and provider-backed secrets', category: 'Tools & System' },
|
|
@@ -154,7 +171,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
154
171
|
});
|
|
155
172
|
return;
|
|
156
173
|
}
|
|
157
|
-
ctx.print('Use /help to open the help modal. Commands: /model, /provider, /config, /template, /tools, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit, /wq');
|
|
174
|
+
ctx.print('Use /help to open the help modal. Commands: /model, /provider, /config, /template, /tools, /paste, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit, /wq');
|
|
158
175
|
},
|
|
159
176
|
});
|
|
160
177
|
|
|
@@ -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';
|
|
@@ -98,9 +99,11 @@ export interface FeedContextStableRefs {
|
|
|
98
99
|
selection: SelectionManager;
|
|
99
100
|
pasteRegistry: Map<string, string>;
|
|
100
101
|
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
102
|
+
projectRoot: string;
|
|
101
103
|
selectionModal: SelectionModal;
|
|
102
104
|
bookmarkModal: BookmarkModal;
|
|
103
105
|
settingsModal: SettingsModal;
|
|
106
|
+
mcpWorkspace: McpWorkspace;
|
|
104
107
|
sessionPickerModal: SessionPickerModal;
|
|
105
108
|
profilePickerModal: ProfilePickerModal;
|
|
106
109
|
historySearch: HistorySearch;
|
|
@@ -4,6 +4,7 @@ import type { AutocompleteEngine } from './autocomplete.ts';
|
|
|
4
4
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
5
5
|
import type { ConversationManager } from '../core/conversation';
|
|
6
6
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
7
|
+
import { handleClipboardPaste, type ClipboardPasteSource } from './handler-content-actions.ts';
|
|
7
8
|
|
|
8
9
|
export type CommandModeRouteState = {
|
|
9
10
|
commandMode: boolean;
|
|
@@ -18,6 +19,14 @@ export type CommandModeRouteState = {
|
|
|
18
19
|
conversationManager: ConversationManager | null;
|
|
19
20
|
requestRender: () => void;
|
|
20
21
|
handleEscape: () => void;
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
pasteRegistry: Map<string, string>;
|
|
24
|
+
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
25
|
+
nextPasteId: number;
|
|
26
|
+
nextImageId: number;
|
|
27
|
+
saveUndoState: () => void;
|
|
28
|
+
ensureInputCursorVisible: () => void;
|
|
29
|
+
clipboard?: ClipboardPasteSource;
|
|
21
30
|
};
|
|
22
31
|
|
|
23
32
|
export function handleCommandModeToken(state: CommandModeRouteState, token: InputToken): boolean {
|
|
@@ -141,6 +150,24 @@ function withPanelFocusSync(context: CommandContext, state: CommandModeRouteStat
|
|
|
141
150
|
state.panelFocused = false;
|
|
142
151
|
}
|
|
143
152
|
: undefined,
|
|
153
|
+
pasteFromClipboard: () => {
|
|
154
|
+
const result = handleClipboardPaste({
|
|
155
|
+
prompt: state.prompt,
|
|
156
|
+
cursorPos: state.cursorPos,
|
|
157
|
+
pasteRegistry: state.pasteRegistry,
|
|
158
|
+
nextPasteId: state.nextPasteId,
|
|
159
|
+
imageRegistry: state.imageRegistry,
|
|
160
|
+
nextImageId: state.nextImageId,
|
|
161
|
+
saveUndoState: state.saveUndoState,
|
|
162
|
+
ensureInputCursorVisible: state.ensureInputCursorVisible,
|
|
163
|
+
requestRender: state.requestRender,
|
|
164
|
+
}, context.workspace.shellPaths?.workingDirectory ?? state.projectRoot, state.clipboard);
|
|
165
|
+
state.prompt = result.prompt;
|
|
166
|
+
state.cursorPos = result.cursorPos;
|
|
167
|
+
state.nextImageId = result.nextImageId;
|
|
168
|
+
state.nextPasteId = result.nextPasteId;
|
|
169
|
+
return result;
|
|
170
|
+
},
|
|
144
171
|
executeCommand: async (name, args) => {
|
|
145
172
|
const wrapped = withPanelFocusSync(context, state);
|
|
146
173
|
const handled = state.commandRegistry?.get(name)
|
|
@@ -56,6 +56,23 @@ export type PasteRegistryState = {
|
|
|
56
56
|
nextImageId: number;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
export type ClipboardPasteKind = 'image' | 'text' | 'none';
|
|
60
|
+
|
|
61
|
+
export interface ClipboardPasteResult {
|
|
62
|
+
prompt: string;
|
|
63
|
+
cursorPos: number;
|
|
64
|
+
nextImageId: number;
|
|
65
|
+
nextPasteId: number;
|
|
66
|
+
pasted: boolean;
|
|
67
|
+
kind: ClipboardPasteKind;
|
|
68
|
+
marker?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ClipboardPasteSource {
|
|
72
|
+
pasteImageFromClipboard: typeof pasteImageFromClipboard;
|
|
73
|
+
pasteFromClipboard: typeof pasteFromClipboard;
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
export function registerPaste(
|
|
60
77
|
state: PasteRegistryState,
|
|
61
78
|
content: string,
|
|
@@ -436,22 +453,34 @@ export function handleClipboardPaste(
|
|
|
436
453
|
requestRender: () => void;
|
|
437
454
|
},
|
|
438
455
|
projectRoot: string,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const img = pasteImageFromClipboard();
|
|
456
|
+
clipboard: ClipboardPasteSource = { pasteImageFromClipboard, pasteFromClipboard },
|
|
457
|
+
): ClipboardPasteResult {
|
|
458
|
+
const img = clipboard.pasteImageFromClipboard();
|
|
459
|
+
let pasted = false;
|
|
460
|
+
let kind: ClipboardPasteKind = 'none';
|
|
461
|
+
let insertedMarker: string | undefined;
|
|
462
|
+
|
|
442
463
|
if (img) {
|
|
464
|
+
state.saveUndoState();
|
|
443
465
|
const id = `img${state.nextImageId++}`;
|
|
444
466
|
const sizeKB = Math.round(img.data.length * 3 / 4 / 1024);
|
|
445
467
|
state.imageRegistry.set(id, img);
|
|
446
468
|
const marker = `[IMAGE: ${id}, clipboard, ${sizeKB}KB]`;
|
|
447
469
|
state.prompt = state.prompt.slice(0, state.cursorPos) + marker + state.prompt.slice(state.cursorPos);
|
|
448
470
|
state.cursorPos += marker.length;
|
|
471
|
+
pasted = true;
|
|
472
|
+
kind = 'image';
|
|
473
|
+
insertedMarker = marker;
|
|
449
474
|
} else {
|
|
450
|
-
const raw = pasteFromClipboard();
|
|
475
|
+
const raw = clipboard.pasteFromClipboard();
|
|
451
476
|
if (raw) {
|
|
477
|
+
state.saveUndoState();
|
|
452
478
|
const { marker } = registerPaste(state, raw, projectRoot);
|
|
453
479
|
state.prompt = state.prompt.slice(0, state.cursorPos) + marker + state.prompt.slice(state.cursorPos);
|
|
454
480
|
state.cursorPos += marker.length;
|
|
481
|
+
pasted = true;
|
|
482
|
+
kind = marker.startsWith('[IMAGE:') ? 'image' : 'text';
|
|
483
|
+
insertedMarker = marker;
|
|
455
484
|
}
|
|
456
485
|
}
|
|
457
486
|
state.ensureInputCursorVisible();
|
|
@@ -461,5 +490,8 @@ export function handleClipboardPaste(
|
|
|
461
490
|
cursorPos: state.cursorPos,
|
|
462
491
|
nextImageId: state.nextImageId,
|
|
463
492
|
nextPasteId: state.nextPasteId,
|
|
493
|
+
pasted,
|
|
494
|
+
kind,
|
|
495
|
+
marker: insertedMarker,
|
|
464
496
|
};
|
|
465
497
|
}
|
|
@@ -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';
|
|
@@ -100,11 +101,13 @@ export interface InputFeedContext {
|
|
|
100
101
|
contentWidth: number;
|
|
101
102
|
readonly pasteRegistry: Map<string, string>;
|
|
102
103
|
readonly imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
104
|
+
readonly projectRoot: string;
|
|
103
105
|
readonly selection: SelectionManager;
|
|
104
106
|
readonly selectionModal: SelectionModal;
|
|
105
107
|
selectionCallback: ((result: SelectionResult | null) => void) | null;
|
|
106
108
|
readonly bookmarkModal: BookmarkModal;
|
|
107
109
|
readonly settingsModal: SettingsModal;
|
|
110
|
+
readonly mcpWorkspace: McpWorkspace;
|
|
108
111
|
readonly sessionPickerModal: SessionPickerModal;
|
|
109
112
|
readonly profilePickerModal: ProfilePickerModal;
|
|
110
113
|
readonly historySearch: HistorySearch;
|
|
@@ -186,6 +189,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
186
189
|
},
|
|
187
190
|
bookmarkModal: context.bookmarkModal,
|
|
188
191
|
settingsModal: context.settingsModal,
|
|
192
|
+
mcpWorkspace: context.mcpWorkspace,
|
|
189
193
|
sessionPickerModal: context.sessionPickerModal,
|
|
190
194
|
profilePickerModal: context.profilePickerModal,
|
|
191
195
|
onboardingWizard: context.onboardingWizard,
|
|
@@ -354,12 +358,21 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
354
358
|
conversationManager: context.conversationManager,
|
|
355
359
|
requestRender: context.requestRender,
|
|
356
360
|
handleEscape: context.handleEscape,
|
|
361
|
+
projectRoot: context.projectRoot,
|
|
362
|
+
pasteRegistry: context.pasteRegistry,
|
|
363
|
+
imageRegistry: context.imageRegistry,
|
|
364
|
+
nextPasteId: context.nextPasteId,
|
|
365
|
+
nextImageId: context.nextImageId,
|
|
366
|
+
saveUndoState: context.saveUndoState,
|
|
367
|
+
ensureInputCursorVisible: () => context.ensureInputCursorVisible(),
|
|
357
368
|
};
|
|
358
369
|
if (handleCommandModeToken(commandState, token)) {
|
|
359
370
|
context.commandMode = commandState.commandMode;
|
|
360
371
|
context.prompt = commandState.prompt;
|
|
361
372
|
context.cursorPos = commandState.cursorPos;
|
|
362
373
|
context.panelFocused = commandState.panelFocused;
|
|
374
|
+
context.nextPasteId = commandState.nextPasteId;
|
|
375
|
+
context.nextImageId = commandState.nextImageId;
|
|
363
376
|
continue;
|
|
364
377
|
}
|
|
365
378
|
|
|
@@ -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;
|