vgxness 1.2.1 → 1.3.1

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 (85) hide show
  1. package/README.md +20 -19
  2. package/dist/cli/cli-help.js +4 -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 +11 -8
  6. package/dist/cli/dispatcher.js +1 -8
  7. package/dist/cli/doctor-renderer.js +1 -1
  8. package/dist/cli/index.js +0 -0
  9. package/dist/cli/sdd-renderer.js +7 -7
  10. package/dist/cli/setup-status-renderer.js +1 -0
  11. package/dist/cli/tui/main-menu/index.js +0 -1
  12. package/dist/cli/tui/main-menu/main-menu-controller.js +0 -2
  13. package/dist/cli/tui/main-menu/main-menu-read-model.js +10 -8
  14. package/dist/cli/tui/main-menu/main-menu-render-shape.js +19 -2
  15. package/dist/cli/tui/main-menu/main-menu-state.js +1 -1
  16. package/dist/cli/tui/opentui/code/index.js +210 -0
  17. package/dist/cli/tui/opentui/code/screen.js +107 -0
  18. package/dist/cli/tui/opentui/code/smoke.js +32 -0
  19. package/dist/cli/tui/opentui/main-menu/index.js +3 -0
  20. package/dist/cli/tui/opentui/main-menu/renderer.js +68 -0
  21. package/dist/cli/tui/opentui/main-menu/screen.js +68 -0
  22. package/dist/cli/tui/opentui/main-menu/smoke.js +17 -0
  23. package/dist/cli/tui/opentui/main-menu/view.js +8 -0
  24. package/dist/cli/tui/opentui/setup/index.js +3 -0
  25. package/dist/cli/tui/opentui/setup/renderer.js +87 -0
  26. package/dist/cli/tui/opentui/setup/screen.js +170 -0
  27. package/dist/cli/tui/opentui/setup/smoke.js +42 -0
  28. package/dist/cli/tui/opentui/setup/view.js +12 -0
  29. package/dist/cli/tui/setup/setup-tui-input.js +43 -0
  30. package/dist/cli/tui/setup/setup-tui-read-model.js +4 -4
  31. package/dist/cli/tui/setup/setup-tui-render-shape.js +9 -10
  32. package/dist/cli/tui/setup/setup-tui-state.js +1 -1
  33. package/dist/cli/tui/setup/setup-tui-view-helpers.js +46 -0
  34. package/dist/cli/tui/visual/index.js +0 -2
  35. package/dist/code/runtime/sdd-context.js +2 -2
  36. package/dist/code/tui/approval-actions.js +33 -0
  37. package/dist/code/tui/prompt-mode.js +11 -0
  38. package/dist/code/tui/runtime-events.js +320 -0
  39. package/dist/mcp/validation.js +6 -2
  40. package/dist/orchestrator/natural-language-planner.js +1 -1
  41. package/dist/sdd/sdd-workflow-service.js +1 -25
  42. package/dist/setup/backup-rollback-service.js +2 -2
  43. package/dist/setup/providers/antigravity-setup-adapter.js +1 -1
  44. package/dist/setup/providers/claude-setup-adapter.js +2 -2
  45. package/dist/setup/providers/custom-setup-adapter.js +1 -1
  46. package/dist/setup/providers/opencode-setup-adapter.js +3 -3
  47. package/dist/setup/setup-lifecycle-service.js +6 -6
  48. package/dist/setup/setup-plan.js +3 -3
  49. package/dist/verification/verification-plan-service.js +1 -1
  50. package/docs/architecture.md +43 -42
  51. package/docs/cli.md +141 -133
  52. package/docs/funcionamiento-del-sistema.md +22 -23
  53. package/docs/harness-gap-analysis.md +15 -1
  54. package/docs/prd.md +14 -14
  55. package/docs/vgxcode.md +87 -0
  56. package/docs/vgxness-code.md +6 -4
  57. package/package.json +5 -6
  58. package/dist/cli/commands/dashboard-dispatcher.js +0 -560
  59. package/dist/cli/dashboard-operational-read-models.js +0 -428
  60. package/dist/cli/dashboard-renderer.js +0 -158
  61. package/dist/cli/dashboard-screen-renderers.js +0 -256
  62. package/dist/cli/dashboard-tui-read-model.js +0 -73
  63. package/dist/cli/dashboard-tui-state.js +0 -314
  64. package/dist/cli/guided-main-menu.js +0 -470
  65. package/dist/cli/interactive-dashboard.js +0 -34
  66. package/dist/cli/setup-wizard-read-model.js +0 -72
  67. package/dist/cli/setup-wizard-renderer.js +0 -155
  68. package/dist/cli/setup-wizard-state.js +0 -82
  69. package/dist/cli/tui/dashboard/dashboard-adapter.js +0 -4
  70. package/dist/cli/tui/main-menu/main-menu-app.js +0 -28
  71. package/dist/cli/tui/render-ink-app.js +0 -10
  72. package/dist/cli/tui/setup/screens/applying-screen.js +0 -6
  73. package/dist/cli/tui/setup/screens/cancellation-screen.js +0 -6
  74. package/dist/cli/tui/setup/screens/error-recovery-screen.js +0 -6
  75. package/dist/cli/tui/setup/screens/final-confirmation-screen.js +0 -6
  76. package/dist/cli/tui/setup/screens/opencode-details-screen.js +0 -10
  77. package/dist/cli/tui/setup/screens/plan-review-screen.js +0 -6
  78. package/dist/cli/tui/setup/screens/project-database-screen.js +0 -6
  79. package/dist/cli/tui/setup/screens/provider-screen.js +0 -7
  80. package/dist/cli/tui/setup/screens/result-screen.js +0 -16
  81. package/dist/cli/tui/setup/screens/screen-components.js +0 -103
  82. package/dist/cli/tui/setup/screens/welcome-screen.js +0 -6
  83. package/dist/cli/tui/setup/setup-tui-app.js +0 -113
  84. package/dist/cli/tui/visual/choice-list.js +0 -10
  85. package/dist/cli/tui/visual/layout.js +0 -10
@@ -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 `vgxness 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: ['vgxness setup apply --yes', 'vgxness 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
+ }
@@ -0,0 +1,43 @@
1
+ import { navigationIntentFromInput } from '../keymap.js';
2
+ export function setupTuiActionFromInput(input, key, state) {
3
+ if (key.upArrow === true)
4
+ return { type: 'focus-previous-choice' };
5
+ if (key.downArrow === true)
6
+ return { type: 'focus-next-choice' };
7
+ if (key.escape === true)
8
+ return state.helpVisible ? { type: 'toggle-help' } : { type: 'cancel' };
9
+ if (key.tab === true && key.shift === true)
10
+ return { type: 'back' };
11
+ if (key.tab === true)
12
+ return { type: 'continue' };
13
+ const raw = key.return === true ? '\n' : input;
14
+ const intent = navigationIntentFromInput(raw);
15
+ if (intent === 'cancel')
16
+ return state.helpVisible ? { type: 'toggle-help' } : { type: 'cancel' };
17
+ if (intent === 'help')
18
+ return { type: 'toggle-help' };
19
+ if (intent === 'up')
20
+ return { type: 'focus-previous-choice' };
21
+ if (intent === 'down')
22
+ return { type: 'focus-next-choice' };
23
+ if (intent === 'select')
24
+ return { type: 'select-focused-choice' };
25
+ if (intent === 'back')
26
+ return { type: 'back' };
27
+ if (intent !== 'next')
28
+ return undefined;
29
+ if (key.return === true && isEditableChoiceScreen(state))
30
+ return { type: 'select-focused-choice' };
31
+ if (state.screen === 'plan-review')
32
+ return { type: 'request-apply' };
33
+ if (state.screen === 'final-confirmation')
34
+ return { type: 'confirm-apply' };
35
+ return { type: 'continue' };
36
+ }
37
+ function isEditableChoiceScreen(state) {
38
+ if (state.focusedChoiceId === undefined)
39
+ return false;
40
+ if (state.screen === 'project-database' || state.screen === 'provider')
41
+ return true;
42
+ return state.screen === 'opencode-details' && state.selections.provider === 'opencode';
43
+ }
@@ -1,4 +1,4 @@
1
- import { badgeLabels, compactPath } from './screens/screen-components.js';
1
+ import { badgeLabels, compactPath } from './setup-tui-view-helpers.js';
2
2
  export function buildSetupTuiViewModel(state) {
3
3
  return setupTuiViewModelFromPlan(state.plan, state.status, state);
4
4
  }
@@ -57,7 +57,7 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
57
57
  warnings,
58
58
  blockers,
59
59
  backupLabel: backups.length === 0 ? 'Backup: none planned' : `Backup planned: ${backups.join('; ')}`,
60
- nextCommands: plan?.nextCommands ?? ['vgx setup plan'],
60
+ nextCommands: plan?.nextCommands ?? ['vgxness setup plan'],
61
61
  canAutoApply: plan?.provider === 'opencode' && plan.status === 'ready' && state?.selections.provider !== 'none' && plan.opencode !== undefined,
62
62
  safetyWarning: 'Final confirmation is required before any provider config write. OpenCode config may be modified and backed up.',
63
63
  frameLabel: 'VGXNESS Setup Assistant workspace',
@@ -108,7 +108,7 @@ function previewDetailLines(input) {
108
108
  ...actions,
109
109
  input.backups.length === 0 ? 'Backups: none planned' : `Backups: ${input.backups.join('; ')}`,
110
110
  ...(input.warnings.length === 0 ? ['Warnings: none'] : input.warnings.map((warning) => `Warning: ${warning}`)),
111
- ...(input.blockers.length === 0 ? ['Blockers: none'] : input.blockers.map((blocker) => `Blocker: ${blocker}`)),
111
+ ...(input.blockers.length === 0 ? ['Blockers: none'] : input.blockers.map((blocker) => `Blocked because: ${blocker}`)),
112
112
  ];
113
113
  }
114
114
  function helpLines(screen) {
@@ -145,7 +145,7 @@ function agentReadinessLabels(plan, status) {
145
145
  if (status?.agents.status === 'blocked')
146
146
  return {
147
147
  label: 'Agents need attention',
148
- detail: `${status.agents.blocker ?? 'Required agents are not ready.'} Next: ${status.agents.nextAction ?? 'vgx agents seed --scope project'}`,
148
+ detail: `${status.agents.blocker ?? 'Required agents are not ready.'} Next: ${status.agents.nextAction ?? 'vgxness agents seed --scope project'}`,
149
149
  };
150
150
  return { label: 'Agent readiness preview', detail: `Required agents: ${names.join(', ')}. Preview/status screens do not seed agents.` };
151
151
  }
@@ -1,4 +1,4 @@
1
- import { badgeLabels, compactPath, formatFooter, setupFooterHints, wrapLabel } from './screens/screen-components.js';
1
+ import { badgeLabels, compactPath, formatFooter, setupFooterHints, wrapLabel } from './setup-tui-view-helpers.js';
2
2
  export function renderSetupTuiShape(input) {
3
3
  const width = input.width ?? 80;
4
4
  const lines = [input.viewModel.title, ...screenLines(input, width)];
@@ -59,11 +59,10 @@ function screenLines(input, width) {
59
59
  'Read-only plan review',
60
60
  'Plan summary ' + badgeLabels.readOnly,
61
61
  ...planLines(vm, width),
62
- `Doctor next step: ${doctorCommand(vm)}`,
63
- 'Restart next step: Restart OpenCode after confirmed setup so it reloads MCP configuration.',
62
+ ...nextCommandLines(vm),
64
63
  'Review only: this screen does not write provider config.',
65
- vm.canAutoApply ? 'Press Enter to continue to final confirmation.' : 'Manual setup required; no automatic apply is available.',
66
- ], width, [setupFooterHints.finalConfirmation, setupFooterHints.back, setupFooterHints.cancel]);
64
+ vm.canAutoApply ? 'Press Enter to continue to final confirmation.' : 'Press Enter to show manual next steps; no automatic apply is available.',
65
+ ], width, vm.canAutoApply ? [setupFooterHints.finalConfirmation, setupFooterHints.back, setupFooterHints.cancel] : [setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel]);
67
66
  case 'final-confirmation':
68
67
  return workspaceLines(vm, [
69
68
  'Final confirmation',
@@ -84,7 +83,7 @@ function screenLines(input, width) {
84
83
  badgeLabels.error,
85
84
  `Error: ${input.error ?? 'Unknown setup error'}`,
86
85
  'No unconfirmed provider config write was performed.',
87
- 'Next: inspect `vgx setup plan`, resolve blockers, then retry.',
86
+ 'Next: inspect `vgxness setup plan`, resolve blockers, then retry.',
88
87
  footer([setupFooterHints.close], width),
89
88
  ];
90
89
  case 'cancelled':
@@ -106,7 +105,7 @@ function planLines(vm, width) {
106
105
  vm.backupLabel,
107
106
  ...vm.plannedActions.map((action) => `Action: ${action.safetyBadge} ${action.label}${action.targetPathLabel === undefined ? '' : ` (${compactPath(action.targetPathLabel, width - 12)})`}`),
108
107
  ...vm.warnings.map((warning) => `Warning: ${warning}`),
109
- ...vm.blockers.map((blocker) => `Blocked: ${blocker}`),
108
+ ...vm.blockers.map((blocker) => `Blocked because: ${blocker}`),
110
109
  ];
111
110
  }
112
111
  function workspaceLines(vm, lines, width, hints = [setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel]) {
@@ -155,11 +154,11 @@ function resultLines(result, vm, width) {
155
154
  }
156
155
  function resultNextCommands(vm) {
157
156
  const filtered = vm.nextCommands.filter((command) => !/^vgx(?:ness)? setup apply\b/.test(command));
158
- const withDoctor = filtered.some((command) => /\bdoctor\b/.test(command)) ? filtered : [...filtered, 'vgx doctor'];
157
+ const withDoctor = filtered.some((command) => /\bdoctor\b/.test(command)) ? filtered : [...filtered, 'vgxness doctor'];
159
158
  return withDoctor.some((command) => /Restart OpenCode/i.test(command)) ? withDoctor : [...withDoctor, 'Restart OpenCode and verify the vgxness MCP server is visible.'];
160
159
  }
161
- function doctorCommand(vm) {
162
- return vm.nextCommands.find((command) => /\bdoctor\b/.test(command)) ?? 'vgx doctor';
160
+ function nextCommandLines(vm) {
161
+ return ['Next commands:', ...vm.nextCommands.map((command) => `Next: ${command}`)];
163
162
  }
164
163
  function footer(hints, width) {
165
164
  return formatFooter(hints, width);
@@ -81,7 +81,7 @@ export function reduceSetupTuiState(state, action) {
81
81
  result: {
82
82
  status: 'failed',
83
83
  message: action.message,
84
- recovery: ['Inspect the setup plan with `vgx setup plan`.', 'Resolve blockers before retrying.'],
84
+ recovery: ['Inspect the setup plan with `vgxness setup plan`.', 'Resolve blockers before retrying.'],
85
85
  writesOccurred: false,
86
86
  },
87
87
  };
@@ -0,0 +1,46 @@
1
+ import { formatTuiFooter } from '../visual/footer.js';
2
+ export const badgeLabels = {
3
+ recommended: '[recommended]',
4
+ selected: '[selected]',
5
+ focused: '[focused]',
6
+ warning: '[warning]',
7
+ error: '[error]',
8
+ writeAfterConfirm: '[will write after confirm]',
9
+ readOnly: '[read-only]',
10
+ manual: '[manual]',
11
+ deferred: '[deferred]',
12
+ cancelled: '[cancelled]',
13
+ pending: '[pending]',
14
+ success: '[success]',
15
+ };
16
+ export const setupFooterHints = {
17
+ continue: { key: 'Enter', label: 'continue' },
18
+ select: { key: 'Enter/Space', label: 'select focused row' },
19
+ finalConfirmation: { key: 'Enter', label: 'final confirmation' },
20
+ confirmApply: { key: 'Enter', label: 'confirm and apply' },
21
+ help: { key: '?/h', label: 'help' },
22
+ back: { key: 'Shift+Tab', label: 'back' },
23
+ cancel: { key: 'q/Esc', label: 'cancel' },
24
+ close: { key: 'q/Esc', label: 'close' },
25
+ };
26
+ export function compactPath(path, width) {
27
+ if (width <= 0 || path.length <= width)
28
+ return path;
29
+ const basename = path.split(/[\\/]/).filter(Boolean).pop() ?? path;
30
+ if (basename.length + 4 >= width)
31
+ return `.../${basename}`;
32
+ const prefixWidth = Math.max(1, width - basename.length - 5);
33
+ return `${path.slice(0, prefixWidth)}.../${basename}`;
34
+ }
35
+ export function wrapLabel(value, width) {
36
+ if (width <= 0 || value.length <= width)
37
+ return value;
38
+ const protectedPhrases = ['final confirmation', 'confirm and apply', 'not installable', 'read-only', 'requires confirmation', 'will write after confirm', 'error', 'cancelled'];
39
+ const phrase = protectedPhrases.find((candidate) => value.includes(candidate));
40
+ if (phrase !== undefined)
41
+ return `${value.slice(0, Math.max(0, width - phrase.length - 5)).trim()} ... ${phrase}`.trim();
42
+ return `${value.slice(0, Math.max(0, width - 1)).trim()}…`;
43
+ }
44
+ export function formatFooter(hints, width) {
45
+ return formatTuiFooter(hints, width);
46
+ }
@@ -1,5 +1,3 @@
1
1
  export * from './badges.js';
2
- export * from './choice-list.js';
3
2
  export * from './footer.js';
4
- export * from './layout.js';
5
3
  export * from './viewport.js';
@@ -84,8 +84,6 @@ export function validateSddReadiness(context) {
84
84
  if (context.next.status === 'complete')
85
85
  return { ok: true };
86
86
  const present = context.status.phases.find((candidate) => candidate.phase === context.phase)?.present === true;
87
- if (present)
88
- return { ok: true };
89
87
  const blockedPrerequisites = activeBlockedPrerequisites(context);
90
88
  if (blockedPrerequisites.length > 0)
91
89
  return {
@@ -100,6 +98,8 @@ export function validateSddReadiness(context) {
100
98
  recommendedAction: `Accept or restore prerequisites before running ${context.phase}.`,
101
99
  },
102
100
  };
101
+ if (present)
102
+ return { ok: true };
103
103
  if (context.status.nextReadyPhase === context.phase)
104
104
  return { ok: true };
105
105
  return {
@@ -0,0 +1,33 @@
1
+ export function getApprovalActionsState(input) {
2
+ if (input.source !== 'interactive' || input.status !== 'running' || input.mode !== 'craft' || !input.childStdinOpen)
3
+ return { enabled: false };
4
+ const approval = input.latestPendingApproval;
5
+ if (input.pendingApprovalCount < 1 || approval?.approvalId === undefined || approval.toolCallId === undefined)
6
+ return { enabled: false };
7
+ return { enabled: true, approval };
8
+ }
9
+ export class ApprovalDecisionWriter {
10
+ #sink;
11
+ #writtenKeys = new Set();
12
+ constructor(sink) {
13
+ this.#sink = sink;
14
+ }
15
+ write(approval, status, reason) {
16
+ if (approval?.approvalId === undefined || approval.toolCallId === undefined)
17
+ return false;
18
+ if (this.#sink.writableEnded === true || this.#sink.destroyed === true)
19
+ return false;
20
+ const key = `${approval.approvalId}:${approval.toolCallId}`;
21
+ if (this.#writtenKeys.has(key))
22
+ return false;
23
+ this.#sink.write(`${JSON.stringify({
24
+ type: 'approval.decision',
25
+ approvalId: approval.approvalId,
26
+ toolCallId: approval.toolCallId,
27
+ status,
28
+ reason,
29
+ })}\n`);
30
+ this.#writtenKeys.add(key);
31
+ return true;
32
+ }
33
+ }
@@ -0,0 +1,11 @@
1
+ export function parsePromptSubmission(input, currentMode) {
2
+ const trimmed = input.trim();
3
+ const prefixMatch = /^(\/(inspect|plan|craft|craft-preview))(?:\s+|$)/iu.exec(trimmed);
4
+ if (!prefixMatch)
5
+ return { mode: currentMode, prompt: trimmed };
6
+ const mode = prefixMatch[2]?.toLowerCase();
7
+ return { mode, prompt: trimmed.slice(prefixMatch[0].length).trim() };
8
+ }
9
+ export function toggleReadOnlyMode(mode) {
10
+ return mode === 'inspect' ? 'plan' : 'inspect';
11
+ }