vgxness 1.2.0 → 1.3.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.
Files changed (70) hide show
  1. package/README.md +7 -6
  2. package/dist/cli/cli-help.js +8 -7
  3. package/dist/cli/commands/index.js +1 -1
  4. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +150 -0
  5. package/dist/cli/commands/setup-dispatcher.js +7 -4
  6. package/dist/cli/dispatcher.js +10 -8
  7. package/dist/cli/index.js +0 -0
  8. package/dist/cli/setup-wizard-renderer.js +1 -1
  9. package/dist/cli/tui/main-menu/index.js +0 -1
  10. package/dist/cli/tui/main-menu/main-menu-controller.js +0 -2
  11. package/dist/cli/tui/main-menu/main-menu-read-model.js +2 -8
  12. package/dist/cli/tui/main-menu/main-menu-render-shape.js +5 -1
  13. package/dist/cli/tui/main-menu/main-menu-state.js +1 -1
  14. package/dist/cli/tui/opentui/code/index.js +210 -0
  15. package/dist/cli/tui/opentui/code/screen.js +107 -0
  16. package/dist/cli/tui/opentui/code/smoke.js +32 -0
  17. package/dist/cli/tui/opentui/main-menu/index.js +3 -0
  18. package/dist/cli/tui/opentui/main-menu/renderer.js +68 -0
  19. package/dist/cli/tui/opentui/main-menu/screen.js +62 -0
  20. package/dist/cli/tui/opentui/main-menu/smoke.js +17 -0
  21. package/dist/cli/tui/opentui/main-menu/view.js +8 -0
  22. package/dist/cli/tui/opentui/setup/index.js +3 -0
  23. package/dist/cli/tui/opentui/setup/renderer.js +87 -0
  24. package/dist/cli/tui/opentui/setup/screen.js +170 -0
  25. package/dist/cli/tui/opentui/setup/smoke.js +42 -0
  26. package/dist/cli/tui/opentui/setup/view.js +12 -0
  27. package/dist/cli/tui/setup/setup-tui-input.js +43 -0
  28. package/dist/cli/tui/setup/setup-tui-read-model.js +1 -1
  29. package/dist/cli/tui/setup/setup-tui-render-shape.js +1 -1
  30. package/dist/cli/tui/setup/setup-tui-view-helpers.js +46 -0
  31. package/dist/cli/tui/visual/index.js +0 -2
  32. package/dist/code/tui/approval-actions.js +33 -0
  33. package/dist/code/tui/prompt-mode.js +11 -0
  34. package/dist/code/tui/runtime-events.js +320 -0
  35. package/dist/sdd/sdd-workflow-service.js +0 -24
  36. package/dist/setup/providers/antigravity-setup-adapter.js +1 -1
  37. package/dist/setup/providers/claude-setup-adapter.js +2 -2
  38. package/dist/setup/providers/custom-setup-adapter.js +1 -1
  39. package/dist/setup/providers/opencode-setup-adapter.js +3 -3
  40. package/dist/setup/setup-lifecycle-service.js +1 -1
  41. package/docs/architecture.md +4 -4
  42. package/docs/cli.md +11 -10
  43. package/docs/funcionamiento-del-sistema.md +6 -7
  44. package/docs/prd.md +4 -4
  45. package/docs/vgxcode.md +76 -0
  46. package/package.json +5 -6
  47. package/dist/cli/commands/dashboard-dispatcher.js +0 -560
  48. package/dist/cli/dashboard-operational-read-models.js +0 -428
  49. package/dist/cli/dashboard-renderer.js +0 -158
  50. package/dist/cli/dashboard-screen-renderers.js +0 -256
  51. package/dist/cli/dashboard-tui-read-model.js +0 -73
  52. package/dist/cli/dashboard-tui-state.js +0 -314
  53. package/dist/cli/interactive-dashboard.js +0 -34
  54. package/dist/cli/tui/dashboard/dashboard-adapter.js +0 -4
  55. package/dist/cli/tui/main-menu/main-menu-app.js +0 -28
  56. package/dist/cli/tui/render-ink-app.js +0 -10
  57. package/dist/cli/tui/setup/screens/applying-screen.js +0 -6
  58. package/dist/cli/tui/setup/screens/cancellation-screen.js +0 -6
  59. package/dist/cli/tui/setup/screens/error-recovery-screen.js +0 -6
  60. package/dist/cli/tui/setup/screens/final-confirmation-screen.js +0 -6
  61. package/dist/cli/tui/setup/screens/opencode-details-screen.js +0 -10
  62. package/dist/cli/tui/setup/screens/plan-review-screen.js +0 -6
  63. package/dist/cli/tui/setup/screens/project-database-screen.js +0 -6
  64. package/dist/cli/tui/setup/screens/provider-screen.js +0 -7
  65. package/dist/cli/tui/setup/screens/result-screen.js +0 -16
  66. package/dist/cli/tui/setup/screens/screen-components.js +0 -103
  67. package/dist/cli/tui/setup/screens/welcome-screen.js +0 -6
  68. package/dist/cli/tui/setup/setup-tui-app.js +0 -113
  69. package/dist/cli/tui/visual/choice-list.js +0 -10
  70. package/dist/cli/tui/visual/layout.js +0 -10
@@ -0,0 +1,107 @@
1
+ import { Box, Text } from '@opentui/core';
2
+ import { buildSafetyReadModel, summarizeEvent } from '../../../../code/tui/runtime-events.js';
3
+ export function createVgxcodeScreen(input) {
4
+ const latest = input.events.at(-1);
5
+ const transcript = input.events.filter((event) => event.type === 'provider.message').map(summarizeEvent);
6
+ const activity = input.events.filter((event) => event.type !== 'provider.message').map(summarizeEvent);
7
+ const safety = buildSafetyReadModel(input.events);
8
+ const status = input.errors.length > 0 ? 'error' : input.status;
9
+ const latestText = latest ? ` Last event: ${latest.type}` : '';
10
+ const modeDescription = describeMode(input.mode);
11
+ const promptLabel = input.source === 'stdin' ? 'piped JSONL replay (safe)' : `${input.mode} prompt (${modeDescription})`;
12
+ const submittedText = input.submittedPrompt === '' ? 'Last submitted: none' : `Last submitted (${input.mode}): ${input.submittedPrompt}`;
13
+ const promptText = input.source === 'stdin'
14
+ ? 'stdin JSONL detected; not spawning root CLI.'
15
+ : `Mode: ${input.mode} (${modeDescription}) State: ${status}\n${submittedText}\n> ${input.prompt}${input.status === 'running' ? ' [running]' : ''}`;
16
+ const errorText = input.errors.length === 0 ? '' : `\n\nErrors (${input.errors.length}):\n${input.errors.map((error) => `! ${error}`).join('\n')}`;
17
+ const safetyText = formatSafetyDetails(safety);
18
+ return Box({
19
+ id: 'vgxcode-screen',
20
+ flexDirection: 'column',
21
+ width: '100%',
22
+ height: '100%',
23
+ padding: 1,
24
+ gap: 1,
25
+ backgroundColor: '#080B12',
26
+ }, Box({
27
+ border: true,
28
+ borderStyle: 'rounded',
29
+ borderColor: '#7DD3FC',
30
+ paddingX: 1,
31
+ title: ' vgxcode ',
32
+ }, Text({ content: `Project: ${input.project} UI: OpenTUI/Bun experimental Source: ${input.source} State: ${status}${latestText}`, fg: '#E0F2FE', wrapMode: 'word' })), Box({
33
+ border: true,
34
+ borderStyle: 'rounded',
35
+ borderColor: '#A78BFA',
36
+ paddingX: 1,
37
+ title: ` Prompt — ${promptLabel} `,
38
+ }, Text({ content: promptText, fg: '#F8FAFC', wrapMode: 'word' })), Box({
39
+ flexDirection: 'row',
40
+ flexGrow: 1,
41
+ gap: 1,
42
+ }, Box({
43
+ border: true,
44
+ borderStyle: 'rounded',
45
+ borderColor: '#22C55E',
46
+ flexGrow: 2,
47
+ padding: 1,
48
+ title: ' Transcript ',
49
+ }, Text({ content: transcript.length === 0 ? 'No assistant output yet.' : transcript.join('\n'), fg: '#DCFCE7', wrapMode: 'word' })), Box({
50
+ border: true,
51
+ borderStyle: 'rounded',
52
+ borderColor: '#F59E0B',
53
+ flexGrow: 1,
54
+ padding: 1,
55
+ title: ' Activity ',
56
+ }, Text({ content: `${activity.length === 0 ? 'No runtime activity yet.' : activity.join('\n')}${errorText}`, fg: input.errors.length > 0 ? '#FECACA' : '#FEF3C7', wrapMode: 'word' }))), Box({
57
+ border: true,
58
+ borderStyle: 'rounded',
59
+ borderColor: '#38BDF8',
60
+ paddingX: 1,
61
+ title: ` Safety details — ${input.mode === 'craft' ? 'approval-capable craft' : 'preview/read-only'} `,
62
+ }, Text({ content: safetyText, fg: '#E0F2FE', wrapMode: 'word' })), Box({
63
+ border: true,
64
+ borderStyle: 'rounded',
65
+ borderColor: '#475569',
66
+ paddingX: 1,
67
+ title: ' Shortcuts ',
68
+ }, Text({ content: formatFooter(input.approvalActionsEnabled === true), fg: '#CBD5E1', wrapMode: 'word' })));
69
+ }
70
+ function formatSafetyDetails(safety) {
71
+ const approvalText = safety.latestPendingApproval
72
+ ? `! Pending approvals: ${safety.pendingApprovalCount} (latest: ${safety.latestPendingApproval.toolName})`
73
+ : `✓ Pending approvals: ${safety.pendingApprovalCount}`;
74
+ const decisionText = safety.latestApprovalDecision
75
+ ? `${approvalDecisionCue(safety.latestApprovalDecision.final)} Latest approval decision: ${safety.latestApprovalDecision.final} — ${safety.latestApprovalDecision.toolName}: ${safety.latestApprovalDecision.reason}`
76
+ : '· Latest approval decision: none';
77
+ const previewText = safety.latestApprovalPreview
78
+ ? `! Approval preview: ${safety.latestApprovalPreview.toolName} (${safety.latestApprovalPreview.capability}) — Approve disabled, Deny disabled. No mutation executed.${safety.latestApprovalPreview.targetPath ? ` Target: ${safety.latestApprovalPreview.targetPath}.` : ''}`
79
+ : '· Approval preview: none';
80
+ const changedFilesText = safety.changedFiles.length > 0
81
+ ? `! Changed files (${safety.changedFiles.length}): ${safety.changedFiles.join(', ')}`
82
+ : '✓ Changed files: none reported';
83
+ const diffText = safety.diffAvailability === 'filenames-only'
84
+ ? '· Diff preview: unavailable in current event stream; only changed filenames were reported.'
85
+ : safety.diffAvailability === 'preview' && safety.latestDiffPreview
86
+ ? formatDiffPreview(safety.latestDiffPreview)
87
+ : '· Diff preview: no changed files reported.';
88
+ return `${approvalText}\n${decisionText}\n${previewText}\n${changedFilesText}\n${diffText}\n· Craft is approval-capable and may mutate after you approve. Craft-preview is preview-only and never sends approval decisions.`;
89
+ }
90
+ function describeMode(mode) {
91
+ if (mode === 'craft')
92
+ return 'approval-capable, mutating after approval';
93
+ if (mode === 'craft-preview')
94
+ return 'preview-only, no mutation executed';
95
+ return 'read-only';
96
+ }
97
+ function formatFooter(approvalActionsEnabled) {
98
+ const approvalText = approvalActionsEnabled ? '[a] Approve pending [d] Deny pending' : 'Approve/Deny hidden until a live craft run has a pending approval';
99
+ return `[Enter] Run mode [Tab] Toggle inspect/plan Prefix: /inspect, /plan, /craft, or /craft-preview [Backspace] Edit ${approvalText} [Ctrl+C] Quit.`;
100
+ }
101
+ function formatDiffPreview(preview) {
102
+ const metadata = `· Diff preview: ${preview.files.length} files, ${preview.bodyBytes}/${preview.originalBytes} bytes, sha256:${preview.hash.slice(0, 12)}${preview.truncated ? ', truncated' : ''}${preview.redacted ? ', redacted' : ''}`;
103
+ return `${metadata}\n${preview.body}`;
104
+ }
105
+ function approvalDecisionCue(final) {
106
+ return final === 'allow' ? '✓' : '✕';
107
+ }
@@ -0,0 +1,32 @@
1
+ import { createTestRenderer } from '@opentui/core/testing';
2
+ import { parseVgxcodeJsonl, sampleEvents } from '../../../../code/tui/runtime-events.js';
3
+ import { createVgxcodeScreen } from './screen.js';
4
+ const input = await readPipedInput();
5
+ const parsed = input === '' ? { events: [], errors: [] } : parseVgxcodeJsonl(input);
6
+ if (parsed.errors.length > 0) {
7
+ throw new Error(`Smoke input contained invalid events: ${parsed.errors.join('; ')}`);
8
+ }
9
+ const events = parsed.events.length > 0 ? parsed.events : sampleEvents();
10
+ const setup = await createTestRenderer({ width: 140, height: 36 });
11
+ try {
12
+ setup.renderer.root.add(createVgxcodeScreen({ project: 'vgxness-smoke', events, prompt: 'smoke test prompt', submittedPrompt: 'previous smoke prompt', status: 'completed', errors: [], source: 'interactive', mode: 'inspect' }));
13
+ await setup.renderOnce();
14
+ const frame = setup.captureCharFrame();
15
+ const requiredText = ['vgxcode', 'vgxness-smoke', 'State: completed', 'Mode: inspect', 'Last submitted', 'Prompt', 'Transcript', 'Activity', 'Safety details', 'hidden until a live craft run', '/craft-preview'];
16
+ const missing = requiredText.filter((text) => !frame.includes(text));
17
+ if (missing.length > 0) {
18
+ throw new Error(`Smoke frame is missing expected text: ${missing.join(', ')}`);
19
+ }
20
+ console.log('vgxcode OpenTUI smoke passed');
21
+ }
22
+ finally {
23
+ setup.renderer.destroy();
24
+ }
25
+ async function readPipedInput() {
26
+ if (process.stdin.isTTY)
27
+ return '';
28
+ let input = '';
29
+ for await (const chunk of process.stdin)
30
+ input += String(chunk);
31
+ return input;
32
+ }
@@ -0,0 +1,3 @@
1
+ export * from './renderer.js';
2
+ export * from './screen.js';
3
+ export * from './view.js';
@@ -0,0 +1,68 @@
1
+ import { createCliRenderer } from '@opentui/core';
2
+ import { mainMenuActionFromInput, reduceMainMenuState } from '../../main-menu/main-menu-actions.js';
3
+ import { createMainMenuState } from '../../main-menu/main-menu-state.js';
4
+ import { createTuiViewport } from '../../visual/viewport.js';
5
+ import { createOpenTuiMainMenuScreen } from './screen.js';
6
+ export async function renderOpenTuiMainMenu(options = {}) {
7
+ const width = terminalWidth(options.stdout);
8
+ let state = options.initialState ?? createMainMenuState(width === undefined ? {} : { width });
9
+ const renderer = await createCliRenderer({
10
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin }),
11
+ ...(options.stdout === undefined ? {} : { stdout: options.stdout }),
12
+ exitOnCtrlC: true,
13
+ clearOnShutdown: true,
14
+ screenMode: 'alternate-screen',
15
+ consoleMode: 'disabled',
16
+ });
17
+ await new Promise((resolve) => {
18
+ let finished = false;
19
+ const finish = (result) => {
20
+ if (finished)
21
+ return;
22
+ finished = true;
23
+ options.onResult?.(result);
24
+ renderer.destroy();
25
+ resolve();
26
+ };
27
+ const render = () => {
28
+ const current = renderer.root.getRenderable('main-menu-screen');
29
+ if (current)
30
+ renderer.root.remove('main-menu-screen');
31
+ renderer.root.add(createOpenTuiMainMenuScreen(state));
32
+ renderer.requestRender();
33
+ };
34
+ renderer.on('destroy', () => {
35
+ if (!finished)
36
+ resolve();
37
+ });
38
+ renderer.on('resize', () => {
39
+ state = { ...state, viewport: createTuiViewport(renderer.terminalWidth, state.viewport.width) };
40
+ render();
41
+ });
42
+ renderer.keyInput.on('keypress', (key) => {
43
+ if (key.ctrl && key.name === 'c')
44
+ return;
45
+ const action = mainMenuActionFromInput(key.sequence, keyToMainMenuInputKey(key));
46
+ if (action === undefined)
47
+ return;
48
+ const reduced = reduceMainMenuState(state, action);
49
+ state = reduced.state;
50
+ if (reduced.result !== undefined)
51
+ finish(reduced.result);
52
+ else
53
+ render();
54
+ });
55
+ render();
56
+ });
57
+ }
58
+ function terminalWidth(stdout) {
59
+ return stdout?.columns;
60
+ }
61
+ function keyToMainMenuInputKey(key) {
62
+ return {
63
+ upArrow: key.name === 'up',
64
+ downArrow: key.name === 'down',
65
+ return: key.name === 'return' || key.name === 'enter',
66
+ escape: key.name === 'escape',
67
+ };
68
+ }
@@ -0,0 +1,62 @@
1
+ import { Box, Text } from '@opentui/core';
2
+ import { tuiBadges } from '../../visual/badges.js';
3
+ import { buildMainMenuViewModel } from '../../main-menu/main-menu-read-model.js';
4
+ import { formatMainMenuOptions } from './view.js';
5
+ export function createOpenTuiMainMenuScreen(state) {
6
+ const vm = buildMainMenuViewModel(state);
7
+ return Box({
8
+ id: 'main-menu-screen',
9
+ flexDirection: 'column',
10
+ width: '100%',
11
+ height: '100%',
12
+ padding: 1,
13
+ gap: 1,
14
+ backgroundColor: '#080B12',
15
+ }, Box({
16
+ border: true,
17
+ borderStyle: 'rounded',
18
+ borderColor: '#7DD3FC',
19
+ paddingX: 1,
20
+ title: ' VGXNESS ',
21
+ }, Text({ content: `${vm.title}\n${vm.subtitle} ${tuiBadges.readOnly}\n${vm.contextLines.join('\n')}`, fg: '#E0F2FE', wrapMode: 'word' })), Box({
22
+ flexDirection: state.viewport.mode === 'narrow' ? 'column' : 'row',
23
+ flexGrow: 1,
24
+ gap: 1,
25
+ }, Box({
26
+ border: true,
27
+ borderStyle: 'rounded',
28
+ borderColor: '#A78BFA',
29
+ flexGrow: 1,
30
+ padding: 1,
31
+ title: ' Menu ',
32
+ }, Text({ content: formatMainMenuOptions(vm.options), fg: '#F8FAFC', wrapMode: 'word' })), Box({
33
+ border: true,
34
+ borderStyle: 'rounded',
35
+ borderColor: '#22C55E',
36
+ flexGrow: 1,
37
+ padding: 1,
38
+ title: ` ${vm.detail.title}${vm.detail.badges.length === 0 ? '' : ` ${vm.detail.badges.join(' ')}`} `,
39
+ }, Text({ content: vm.detail.lines.join('\n'), fg: '#DCFCE7', wrapMode: 'word' }))), Box({
40
+ border: true,
41
+ borderStyle: 'rounded',
42
+ borderColor: '#F59E0B',
43
+ paddingX: 1,
44
+ title: ` Safety ${tuiBadges.noProviderWrites} `,
45
+ }, Text({ content: vm.safetyLines.join('\n'), fg: '#FEF3C7', wrapMode: 'word' })), ...(vm.helpLines.length === 0
46
+ ? []
47
+ : [
48
+ Box({
49
+ border: true,
50
+ borderStyle: 'rounded',
51
+ borderColor: '#38BDF8',
52
+ paddingX: 1,
53
+ title: ` Help ${tuiBadges.readOnly} `,
54
+ }, Text({ content: vm.helpLines.join('\n'), fg: '#E0F2FE', wrapMode: 'word' })),
55
+ ]), Box({
56
+ border: true,
57
+ borderStyle: 'rounded',
58
+ borderColor: '#475569',
59
+ paddingX: 1,
60
+ title: ' Shortcuts ',
61
+ }, Text({ content: vm.footer, fg: '#CBD5E1', wrapMode: 'word' })));
62
+ }
@@ -0,0 +1,17 @@
1
+ import { createTestRenderer } from '@opentui/core/testing';
2
+ import { createMainMenuState } from '../../main-menu/main-menu-state.js';
3
+ import { createOpenTuiMainMenuScreen } from './screen.js';
4
+ const setup = await createTestRenderer({ width: 120, height: 32 });
5
+ try {
6
+ setup.renderer.root.add(createOpenTuiMainMenuScreen(createMainMenuState({ focusedOptionId: 'setup', helpVisible: true, width: 120 })));
7
+ await setup.renderOnce();
8
+ const frame = setup.captureCharFrame();
9
+ const requiredText = ['VGXNESS Main Menu', 'Installation', 'Requires confirmation', 'No provider writes', 'final confirmation', 'q/Esc'];
10
+ const missing = requiredText.filter((text) => !frame.includes(text));
11
+ if (missing.length > 0)
12
+ throw new Error(`Main menu smoke frame is missing expected text: ${missing.join(', ')}`);
13
+ console.log('main-menu OpenTUI smoke passed');
14
+ }
15
+ finally {
16
+ setup.renderer.destroy();
17
+ }
@@ -0,0 +1,8 @@
1
+ import { formatBadges, tuiBadges } from '../../visual/badges.js';
2
+ export function formatMainMenuOptions(options) {
3
+ return options.map(formatMainMenuOption).join('\n');
4
+ }
5
+ function formatMainMenuOption(option) {
6
+ const badges = formatBadges([...(option.focused ? [tuiBadges.focused] : []), ...option.badges]);
7
+ return `${option.focused ? '›' : ' '} ${option.label}${badges.length === 0 ? '' : ` ${badges}`} — ${option.description}`;
8
+ }
@@ -0,0 +1,3 @@
1
+ export { renderOpenTuiSetup } from './renderer.js';
2
+ export { createOpenTuiSetupScreen } from './screen.js';
3
+ export { formatOpenTuiSetupScreen } from './view.js';
@@ -0,0 +1,87 @@
1
+ import { createCliRenderer } from '@opentui/core';
2
+ import { createSetupTuiController } from '../../setup/setup-tui-controller.js';
3
+ import { setupTuiActionFromInput } from '../../setup/setup-tui-input.js';
4
+ import { createSetupTuiState } from '../../setup/setup-tui-state.js';
5
+ import { createOpenTuiSetupScreen } from './screen.js';
6
+ export async function renderOpenTuiSetup(options) {
7
+ let state = options.initialState ?? createSetupTuiState({ selections: options.runtime.selections });
8
+ const renderer = await createCliRenderer({
9
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin }),
10
+ ...(options.stdout === undefined ? {} : { stdout: options.stdout }),
11
+ exitOnCtrlC: true,
12
+ clearOnShutdown: true,
13
+ screenMode: 'alternate-screen',
14
+ consoleMode: 'disabled',
15
+ });
16
+ await new Promise((resolve) => {
17
+ let finished = false;
18
+ let dispatching = false;
19
+ const finish = () => {
20
+ if (finished)
21
+ return;
22
+ finished = true;
23
+ renderer.destroy();
24
+ resolve();
25
+ };
26
+ const render = () => {
27
+ const current = renderer.root.getRenderable('setup-screen');
28
+ if (current)
29
+ renderer.root.remove('setup-screen');
30
+ renderer.root.add(createOpenTuiSetupScreen(state, renderer.terminalWidth));
31
+ renderer.requestRender();
32
+ options.onState?.(state);
33
+ };
34
+ const dispatch = async (action) => {
35
+ dispatching = true;
36
+ try {
37
+ const controller = await createSetupTuiController({ state, services: options.services, runtime: options.runtime }).dispatch(action);
38
+ state = controller.state;
39
+ render();
40
+ if (state.screen === 'cancelled' && action.type === 'cancel')
41
+ finish();
42
+ }
43
+ finally {
44
+ dispatching = false;
45
+ }
46
+ };
47
+ renderer.on('destroy', () => {
48
+ if (!finished)
49
+ resolve();
50
+ });
51
+ renderer.on('resize', render);
52
+ renderer.keyInput.on('keypress', (key) => {
53
+ if (key.ctrl && key.name === 'c')
54
+ return;
55
+ if (dispatching)
56
+ return;
57
+ if (isCloseKey(key) && (state.screen === 'cancelled' || state.screen === 'result' || state.screen === 'error-recovery')) {
58
+ finish();
59
+ return;
60
+ }
61
+ const action = setupTuiActionFromInput(key.sequence, keyToSetupTuiInputKey(key), state);
62
+ if (action !== undefined)
63
+ void dispatch(action);
64
+ });
65
+ void createSetupTuiController({ state, services: options.services, runtime: options.runtime })
66
+ .load()
67
+ .then((controller) => {
68
+ if (finished)
69
+ return;
70
+ state = controller.state;
71
+ render();
72
+ });
73
+ });
74
+ }
75
+ function isCloseKey(key) {
76
+ return key.name === 'escape' || key.sequence === 'q';
77
+ }
78
+ function keyToSetupTuiInputKey(key) {
79
+ return {
80
+ upArrow: key.name === 'up',
81
+ downArrow: key.name === 'down',
82
+ return: key.name === 'return' || key.name === 'enter',
83
+ escape: key.name === 'escape',
84
+ tab: key.name === 'tab',
85
+ shift: key.shift === true,
86
+ };
87
+ }
@@ -0,0 +1,170 @@
1
+ import { Box, Text } from '@opentui/core';
2
+ import { buildSetupTuiViewModel } from '../../setup/setup-tui-read-model.js';
3
+ import { formatFooter, setupFooterHints } from '../../setup/setup-tui-view-helpers.js';
4
+ export function createOpenTuiSetupScreen(state, width) {
5
+ const vm = buildSetupTuiViewModel(state);
6
+ const screen = setupScreenContent(state, vm, width);
7
+ return Box({
8
+ id: 'setup-screen',
9
+ flexDirection: 'column',
10
+ width: '100%',
11
+ height: '100%',
12
+ padding: 1,
13
+ backgroundColor: '#080B12',
14
+ gap: 1,
15
+ }, headerPanel(vm), Box({ flexDirection: width < 100 ? 'column' : 'row', flexGrow: 1, gap: 1 }, setupPanel({ title: screen.leftTitle, content: screen.left, borderColor: '#A78BFA', grow: 1 }), setupPanel({ title: screen.rightTitle, content: screen.right, borderColor: '#22C55E', grow: 1 })), setupPanel({ title: ' Safety ', content: screen.safety, borderColor: state.screen === 'final-confirmation' ? '#F59E0B' : '#38BDF8' }), ...(screen.help.length === 0 ? [] : [setupPanel({ title: ' Help [read-only] ', content: screen.help, borderColor: '#38BDF8' })]), Box({ border: true, borderStyle: 'rounded', borderColor: '#475569', paddingX: 1, title: ' Shortcuts ' }, Text({ content: screen.footer, fg: '#CBD5E1', wrapMode: 'word' })));
16
+ }
17
+ function headerPanel(vm) {
18
+ return Box({ border: true, borderStyle: 'rounded', borderColor: '#7DD3FC', paddingX: 1, title: ' VGXNESS Setup ' }, Text({
19
+ content: `${vm.title}\n${vm.progressLabel}\nProject: ${vm.projectLabel} · Provider: ${vm.providerLabel} · Database: ${vm.databaseLabel}`,
20
+ fg: '#E0F2FE',
21
+ wrapMode: 'word',
22
+ }));
23
+ }
24
+ function setupPanel(input) {
25
+ return Box({
26
+ border: true,
27
+ borderStyle: 'rounded',
28
+ borderColor: input.borderColor,
29
+ paddingX: 1,
30
+ title: input.title,
31
+ ...(input.grow === undefined ? {} : { flexGrow: input.grow }),
32
+ }, Text({ content: input.content.join('\n'), fg: '#F8FAFC', wrapMode: 'word' }));
33
+ }
34
+ function setupScreenContent(state, vm, width) {
35
+ const footer = footerForScreen(state.screen, width);
36
+ const safety = safetyLines(vm, state.screen);
37
+ const help = state.helpVisible ? vm.helpLines : [];
38
+ switch (state.screen) {
39
+ case 'welcome':
40
+ return {
41
+ leftTitle: ' Flow ',
42
+ left: ['1. Choose storage', '2. Choose provider', '3. Review OpenCode details', '4. Read-only plan preview', '5. Final confirmation', '6. Result and next steps'],
43
+ rightTitle: ' Current context ',
44
+ right: previewLines(vm),
45
+ safety,
46
+ help,
47
+ footer,
48
+ };
49
+ case 'project-database':
50
+ return {
51
+ leftTitle: ' Database choices ',
52
+ left: choiceLines(vm.databaseChoices),
53
+ rightTitle: focusedChoiceTitle(vm.databaseChoices, 'Database details'),
54
+ right: [...focusedChoiceDetails(vm.databaseChoices), `Mode: ${vm.databaseLabel}`, `Path: ${vm.databasePathLabel}`, `Source: ${vm.databaseSourceLabel}`],
55
+ safety,
56
+ help,
57
+ footer,
58
+ };
59
+ case 'provider':
60
+ return {
61
+ leftTitle: ' Provider choices ',
62
+ left: choiceLines(vm.providerChoices),
63
+ rightTitle: focusedChoiceTitle(vm.providerChoices, 'Provider details'),
64
+ right: [...focusedChoiceDetails(vm.providerChoices), vm.providerInstallabilityLabel],
65
+ safety,
66
+ help,
67
+ footer,
68
+ };
69
+ case 'opencode-details':
70
+ return {
71
+ leftTitle: ' OpenCode controls ',
72
+ left: [...choiceLines(vm.scopeChoices), '', ...choiceLines(vm.installModeChoices)].filter(Boolean),
73
+ rightTitle: ' Plan target ',
74
+ right: [`Scope: ${vm.scopeLabel}`, `Install mode: ${vm.installModeLabel}`, `Action: ${vm.opencodeActionLabel}`, `Target: ${vm.targetPathLabel}`, `Agents: ${vm.agentReadinessLabel}`],
75
+ safety,
76
+ help,
77
+ footer,
78
+ };
79
+ case 'plan-review':
80
+ return {
81
+ leftTitle: ' Read-only plan ',
82
+ left: planSummaryLines(vm),
83
+ rightTitle: ' Blockers / next ',
84
+ right: blockerAndNextLines(vm),
85
+ safety,
86
+ help,
87
+ footer,
88
+ };
89
+ case 'final-confirmation':
90
+ return {
91
+ leftTitle: ' Final confirmation ',
92
+ left: ['! This is the first write-capable step.', 'Press Enter only if the target and backups look correct.', 'Esc/q cancels without writing.', '', ...planSummaryLines(vm)],
93
+ rightTitle: ' Before apply ',
94
+ right: blockerAndNextLines(vm),
95
+ safety,
96
+ help,
97
+ footer,
98
+ };
99
+ case 'applying':
100
+ return { leftTitle: ' Applying ', left: ['Applying confirmed OpenCode setup...', 'Waiting for the apply operation to finish.'], rightTitle: ' Status ', right: previewLines(vm), safety, help, footer };
101
+ case 'result':
102
+ return { leftTitle: ' Result ', left: ['Setup finished.', ...vm.nextCommands.map((command) => `Next: ${command}`)], rightTitle: ' Summary ', right: previewLines(vm), safety, help, footer };
103
+ case 'error-recovery':
104
+ return { leftTitle: ' Recovery ', left: ['! Setup hit an error.', 'No unconfirmed provider config write was performed.', 'Inspect `vgx setup plan`, resolve blockers, then retry.'], rightTitle: ' Context ', right: previewLines(vm), safety, help, footer };
105
+ case 'cancelled':
106
+ return { leftTitle: ' Cancelled ', left: ['Setup was cancelled.', 'No provider config was written.', 'No agent seeding was performed.'], rightTitle: ' Context ', right: previewLines(vm), safety, help, footer };
107
+ }
108
+ }
109
+ function choiceLines(choices) {
110
+ if (choices.length === 0)
111
+ return ['No editable choices on this step.'];
112
+ return choices.map((choice) => `${choice.focused ? '›' : ' '} ${choice.label}${choiceBadges(choice)}\n ${choice.description}`);
113
+ }
114
+ function choiceBadges(choice) {
115
+ const badges = [...(choice.selected ? ['[selected]'] : []), ...(choice.focused ? ['[focused]'] : []), ...choice.badges];
116
+ return badges.length === 0 ? '' : ` ${badges.join(' ')}`;
117
+ }
118
+ function focusedChoiceTitle(choices, fallback) {
119
+ return ` ${choices.find((choice) => choice.focused)?.label ?? fallback} `;
120
+ }
121
+ function focusedChoiceDetails(choices) {
122
+ const focused = choices.find((choice) => choice.focused) ?? choices.find((choice) => choice.selected) ?? choices[0];
123
+ if (focused === undefined)
124
+ return ['No focused choice.'];
125
+ return [focused.description, focused.badges.length === 0 ? 'Badges: none' : `Badges: ${focused.badges.join(' ')}`];
126
+ }
127
+ function previewLines(vm) {
128
+ return [
129
+ `Readiness: ${vm.readinessBadge} ${vm.readinessLabel}`,
130
+ `Provider: ${vm.providerLabel}`,
131
+ `Database: ${vm.databaseLabel}`,
132
+ `Path: ${vm.databasePathLabel}`,
133
+ `Target: ${vm.targetPathLabel}`,
134
+ `Backups: ${vm.backupLabel.replace(/^Backup(?: planned)?:\s*/u, '')}`,
135
+ ];
136
+ }
137
+ function planSummaryLines(vm) {
138
+ return [
139
+ `Provider: ${vm.providerLabel}`,
140
+ `Database: ${vm.databaseLabel}`,
141
+ `Target: ${vm.targetPathLabel}`,
142
+ `Agents: ${vm.agentReadinessLabel}`,
143
+ vm.backupLabel,
144
+ ...vm.plannedActions.slice(0, 3).map((action) => `${action.safetyBadge} ${action.label}`),
145
+ ];
146
+ }
147
+ function blockerAndNextLines(vm) {
148
+ return [
149
+ ...(vm.blockers.length === 0 ? ['Blockers: none'] : vm.blockers.map((blocker) => `! ${blocker}`)),
150
+ ...(vm.warnings.length === 0 ? ['Warnings: none'] : vm.warnings.map((warning) => `! ${warning}`)),
151
+ '',
152
+ ...vm.nextCommands.slice(0, 3).map((command) => `Next: ${command}`),
153
+ ].filter(Boolean);
154
+ }
155
+ function safetyLines(vm, screen) {
156
+ return [
157
+ screen === 'final-confirmation' ? '! Final confirmation can apply OpenCode setup.' : 'Read-only until final confirmation.',
158
+ vm.footerSafetyLabel,
159
+ vm.canAutoApply ? 'Apply is available only from final confirmation.' : 'Automatic apply is currently unavailable for this selection.',
160
+ ];
161
+ }
162
+ function footerForScreen(screen, width) {
163
+ if (screen === 'final-confirmation')
164
+ return formatFooter([setupFooterHints.confirmApply, setupFooterHints.back, setupFooterHints.cancel], width);
165
+ if (screen === 'project-database' || screen === 'provider' || screen === 'opencode-details')
166
+ return formatFooter([setupFooterHints.select, setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel], width);
167
+ if (screen === 'result' || screen === 'error-recovery' || screen === 'cancelled')
168
+ return formatFooter([setupFooterHints.close], width);
169
+ return formatFooter([setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel], width);
170
+ }
@@ -0,0 +1,42 @@
1
+ import { createTestRenderer } from '@opentui/core/testing';
2
+ import { createSetupTuiState } from '../../setup/setup-tui-state.js';
3
+ import { createOpenTuiSetupScreen } from './screen.js';
4
+ const setup = await createTestRenderer({ width: 160, height: 70 });
5
+ try {
6
+ setup.renderer.root.add(createOpenTuiSetupScreen(createSetupTuiState({
7
+ screen: 'final-confirmation',
8
+ plan: {
9
+ version: 1,
10
+ kind: 'setup-plan',
11
+ status: 'ready',
12
+ project: 'vgxness',
13
+ workspaceRoot: '/tmp/project',
14
+ db: { mode: 'global', path: '/tmp/db.sqlite', source: 'flag' },
15
+ provider: 'opencode',
16
+ opencode: { scope: 'user', action: 'merge', targetPath: '/tmp/.config/opencode/opencode.json', installsAgents: true, agentNames: ['vgxness-manager'] },
17
+ actions: [
18
+ {
19
+ id: 'opencode-merge',
20
+ description: 'Merge OpenCode config with mcp.vgxness.',
21
+ mutating: false,
22
+ targetPath: '/tmp/.config/opencode/opencode.json',
23
+ backupRequired: true,
24
+ },
25
+ ],
26
+ conflicts: [],
27
+ backupsPlanned: [{ targetPath: '/tmp/.config/opencode/opencode.json', reason: 'Existing OpenCode config would be backed up before merge.' }],
28
+ safety: { mutating: false, writesProviderConfig: false, requiresConfirmationForApply: true },
29
+ nextCommands: ['vgx setup apply --yes', 'vgx doctor'],
30
+ },
31
+ }), 120));
32
+ await setup.renderOnce();
33
+ const frame = setup.captureCharFrame();
34
+ const requiredText = ['VGXNESS Setup Assistant', 'Final confirmation', 'confirm and apply', 'will write after confirm', 'opencode.json'];
35
+ const missing = requiredText.filter((text) => !frame.includes(text));
36
+ if (missing.length > 0)
37
+ throw new Error(`Setup OpenTUI smoke frame is missing expected text: ${missing.join(', ')}`);
38
+ console.log('setup OpenTUI smoke passed');
39
+ }
40
+ finally {
41
+ setup.renderer.destroy();
42
+ }
@@ -0,0 +1,12 @@
1
+ import { buildSetupTuiViewModel } from '../../setup/setup-tui-read-model.js';
2
+ import { renderSetupTuiShape } from '../../setup/setup-tui-render-shape.js';
3
+ export function formatOpenTuiSetupScreen(state, width = 100) {
4
+ return renderSetupTuiShape({
5
+ screen: state.screen,
6
+ viewModel: buildSetupTuiViewModel(state),
7
+ ...(state.result === undefined ? {} : { result: state.result }),
8
+ ...(state.error === undefined ? {} : { error: state.error }),
9
+ width,
10
+ color: false,
11
+ });
12
+ }