@pellux/goodvibes-tui 0.19.98 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +31 -6
- package/docs/foundation-artifacts/operator-contract.json +1376 -290
- package/package.json +2 -2
- package/src/input/command-registry.ts +1 -1
- package/src/input/commands/local-setup-review.ts +4 -1
- package/src/input/commands/mcp-runtime.ts +237 -7
- package/src/input/commands/platform-sandbox-qemu.ts +17 -35
- package/src/input/commands/platform-sandbox-runtime.ts +4 -2
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -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 +10 -0
- package/src/input/mcp-workspace.ts +554 -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/renderer/settings-modal-helpers.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +2 -4
- package/src/runtime/bootstrap.ts +26 -2
- package/src/runtime/sandbox-public-gaps.ts +158 -38
- package/src/runtime/sandbox-qemu-templates.ts +340 -0
- 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.
|
|
3
|
+
"version": "0.20.0",
|
|
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.30",
|
|
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
|
-
|
|
118
|
+
openMcpWorkspace?: () => void;
|
|
119
119
|
openSecurityPanel?: () => void;
|
|
120
120
|
openKnowledgePanel?: () => void;
|
|
121
121
|
openRemotePanel?: () => void;
|
|
@@ -155,10 +155,13 @@ export function renderSetupSandboxReview(ctx: CommandContext, snapshot: SetupRev
|
|
|
155
155
|
];
|
|
156
156
|
if (backend === 'local') {
|
|
157
157
|
lines.push(' /sandbox qemu bootstrap .goodvibes/tui/sandbox 20');
|
|
158
|
+
lines.push(' .goodvibes/tui/sandbox/create-image.sh .goodvibes/tui/sandbox/goodvibes-sandbox.qcow2 20G');
|
|
159
|
+
lines.push(' GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh');
|
|
158
160
|
lines.push(' /sandbox doctor');
|
|
159
161
|
} else if (!image || !wrapper) {
|
|
160
162
|
lines.push(' /sandbox qemu setup .goodvibes/tui/sandbox');
|
|
161
|
-
lines.push(' /sandbox
|
|
163
|
+
lines.push(' .goodvibes/tui/sandbox/create-image.sh .goodvibes/tui/sandbox/goodvibes-sandbox.qcow2 20G');
|
|
164
|
+
lines.push(' GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh');
|
|
162
165
|
lines.push(' /sandbox doctor');
|
|
163
166
|
} else {
|
|
164
167
|
lines.push(' /sandbox guest-test eval-js');
|
|
@@ -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.');
|
|
@@ -4,7 +4,6 @@ import type { CommandContext } from '../command-registry.ts';
|
|
|
4
4
|
import {
|
|
5
5
|
applySandboxQemuSetupManifest,
|
|
6
6
|
bootstrapSandboxQemuSetupBundle,
|
|
7
|
-
createSandboxQemuImage,
|
|
8
7
|
inspectSandboxQemuSetupManifest,
|
|
9
8
|
loadSandboxQemuSetupManifest,
|
|
10
9
|
scaffoldSandboxQemuSetupBundle,
|
|
@@ -27,29 +26,25 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
27
26
|
return true;
|
|
28
27
|
}
|
|
29
28
|
const bundle = scaffoldSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, dirArg, { surfaceRoot: 'tui' });
|
|
30
|
-
|
|
31
|
-
ctx.platform.configManager
|
|
32
|
-
ctx.platform.configManager.setDynamic('sandbox.
|
|
33
|
-
|
|
34
|
-
ctx.platform.configManager.setDynamic('sandbox.qemuGuestHost', '127.0.0.1');
|
|
35
|
-
}
|
|
36
|
-
if (!`${ctx.platform.configManager.get('sandbox.qemuGuestUser') ?? ''}`.trim()) {
|
|
37
|
-
ctx.platform.configManager.setDynamic('sandbox.qemuGuestUser', 'goodvibes');
|
|
38
|
-
}
|
|
39
|
-
if (!`${ctx.platform.configManager.get('sandbox.qemuWorkspacePath') ?? ''}`.trim()) {
|
|
40
|
-
ctx.platform.configManager.setDynamic('sandbox.qemuWorkspacePath', '/workspace');
|
|
41
|
-
}
|
|
29
|
+
const manifest = loadSandboxQemuSetupManifest(shellPaths.workingDirectory, bundle.manifestPath);
|
|
30
|
+
applySandboxQemuSetupManifest(ctx.platform.configManager, manifest);
|
|
31
|
+
ctx.platform.configManager.setDynamic('sandbox.replIsolation', 'shared-vm');
|
|
32
|
+
ctx.platform.configManager.setDynamic('sandbox.mcpIsolation', 'shared-vm');
|
|
42
33
|
ctx.print([
|
|
43
34
|
`Initialized QEMU sandbox setup bundle in ${bundle.directory}`,
|
|
44
35
|
` wrapper: ${bundle.wrapperPath}`,
|
|
45
36
|
` image path: ${bundle.imagePath}`,
|
|
46
37
|
` image create script: ${bundle.imageCreateScriptPath}`,
|
|
47
38
|
` guest bootstrap: ${bundle.guestBootstrapScriptPath}`,
|
|
39
|
+
` seed directory: ${bundle.seedDirectory}`,
|
|
40
|
+
` seed ISO: ${bundle.seedIsoPath}`,
|
|
41
|
+
` ssh key: ${bundle.sshKeyPath}`,
|
|
48
42
|
` projection policy: ${bundle.projectionPolicyPath}`,
|
|
49
43
|
` ssh config: ${bundle.sshConfigPath}`,
|
|
50
44
|
` manifest: ${bundle.manifestPath}`,
|
|
51
|
-
' applied: backend=qemu, wrapper path, image path,
|
|
52
|
-
` next:
|
|
45
|
+
' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
|
|
46
|
+
` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} 20G`,
|
|
47
|
+
` then provision runtimes: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`,
|
|
53
48
|
].join('\n'));
|
|
54
49
|
return true;
|
|
55
50
|
}
|
|
@@ -67,33 +62,20 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
67
62
|
` wrapper: ${bundle.wrapperPath}`,
|
|
68
63
|
` image path: ${bundle.imagePath}`,
|
|
69
64
|
` guest bootstrap: ${bundle.guestBootstrapScriptPath}`,
|
|
65
|
+
` seed ISO: ${bundle.seedIsoPath}`,
|
|
66
|
+
` ssh key: ${bundle.sshKeyPath}`,
|
|
70
67
|
` projection policy: ${bundle.projectionPolicyPath}`,
|
|
71
68
|
` manifest: ${bundle.manifestPath}`,
|
|
72
|
-
' applied: backend=qemu, wrapper path, image path,
|
|
73
|
-
|
|
69
|
+
' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
|
|
70
|
+
` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${sizeGb}G`,
|
|
71
|
+
` then provision runtimes: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`,
|
|
72
|
+
' then: /sandbox guest-test eval-py',
|
|
74
73
|
].join('\n'));
|
|
75
74
|
} catch (error) {
|
|
76
75
|
ctx.print(summarizeError(error));
|
|
77
76
|
}
|
|
78
77
|
return true;
|
|
79
78
|
}
|
|
80
|
-
if (sub === 'create-image') {
|
|
81
|
-
const imagePath = args[2];
|
|
82
|
-
const sizeGb = Number.parseInt(args[3] ?? '20', 10);
|
|
83
|
-
if (!imagePath || !Number.isInteger(sizeGb) || sizeGb < 1) {
|
|
84
|
-
ctx.print('Usage: /sandbox qemu create-image <path> [size-gb]');
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
const created = createSandboxQemuImage(shellPaths.workingDirectory, imagePath, sizeGb);
|
|
89
|
-
ctx.platform.configManager.setDynamic('sandbox.qemuImagePath', created.path);
|
|
90
|
-
ctx.platform.configManager.setDynamic('sandbox.vmBackend', 'qemu');
|
|
91
|
-
ctx.print(`Created QEMU image ${created.path} (${created.sizeGb}G).`);
|
|
92
|
-
} catch (error) {
|
|
93
|
-
ctx.print(summarizeError(error));
|
|
94
|
-
}
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
79
|
if (sub === 'recover') {
|
|
98
80
|
const sessionId = args[2];
|
|
99
81
|
if (!sessionId) {
|
|
@@ -132,6 +114,6 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
132
114
|
ctx.print(`Applied QEMU sandbox setup from ${shellPaths.resolveWorkspacePath(pathArg)}.`);
|
|
133
115
|
return true;
|
|
134
116
|
}
|
|
135
|
-
ctx.print('Usage: /sandbox qemu <setup <directory>|bootstrap <directory> [size-gb]|
|
|
117
|
+
ctx.print('Usage: /sandbox qemu <setup <directory>|bootstrap <directory> [size-gb]|recover <session-id>|inspect-setup <setup-manifest.json>|apply-setup <setup-manifest.json>>');
|
|
136
118
|
return true;
|
|
137
119
|
}
|
|
@@ -61,6 +61,7 @@ function applySandboxPreset(
|
|
|
61
61
|
configManager.setDynamic('sandbox.qemuGuestUser' as never, preset.config.qemuGuestUser);
|
|
62
62
|
configManager.setDynamic('sandbox.qemuWorkspacePath' as never, preset.config.qemuWorkspacePath);
|
|
63
63
|
configManager.setDynamic('sandbox.qemuSessionMode' as never, preset.config.qemuSessionMode);
|
|
64
|
+
configManager.setDynamic('sandbox.replJavaScriptCommand' as never, preset.config.replJavaScriptCommand);
|
|
64
65
|
return true;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -83,6 +84,7 @@ function renderSandboxPresetDetail(presetId: string): string | null {
|
|
|
83
84
|
` qemu guest user: ${preset.config.qemuGuestUser || '(not configured)'}`,
|
|
84
85
|
` qemu workspace: ${preset.config.qemuWorkspacePath || '(not configured)'}`,
|
|
85
86
|
` qemu session mode: ${preset.config.qemuSessionMode}`,
|
|
87
|
+
` repl JavaScript command: ${preset.config.replJavaScriptCommand || 'bun'}`,
|
|
86
88
|
...preset.notes.map((note) => ` note: ${note}`),
|
|
87
89
|
].join('\n');
|
|
88
90
|
}
|
|
@@ -91,7 +93,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
91
93
|
registry.register({
|
|
92
94
|
name: 'sandbox',
|
|
93
95
|
description: 'Review and configure VM isolation policy for MCP and evaluation runtimes',
|
|
94
|
-
usage: '[open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|
|
|
96
|
+
usage: '[open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|recover|inspect-setup|apply-setup> ...|session ...|bundle ...|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]',
|
|
95
97
|
async handler(args, ctx) {
|
|
96
98
|
const shellPaths = requireShellPaths(ctx);
|
|
97
99
|
const sub = args[0] ?? 'open';
|
|
@@ -400,7 +402,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
400
402
|
ctx.print([`Scaffolded QEMU wrapper to ${targetPath}`, ' bridge test mode: GV_SANDBOX_WRAPPER_MODE=host-exec', ' next: /sandbox set-qemu-wrapper ' + targetPath].join('\n'));
|
|
401
403
|
return;
|
|
402
404
|
}
|
|
403
|
-
ctx.print('Usage: /sandbox [open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|
|
|
405
|
+
ctx.print('Usage: /sandbox [open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|recover|inspect-setup|apply-setup> ...|session ...|bundle export <path>|bundle inspect <path>|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]');
|
|
404
406
|
},
|
|
405
407
|
});
|
|
406
408
|
}
|
|
@@ -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;
|
package/src/input/handler.ts
CHANGED
|
@@ -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); }
|