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.
- package/README.md +20 -19
- package/dist/cli/cli-help.js +4 -7
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/interactive-entrypoint-dispatcher.js +150 -0
- package/dist/cli/commands/setup-dispatcher.js +11 -8
- package/dist/cli/dispatcher.js +1 -8
- package/dist/cli/doctor-renderer.js +1 -1
- package/dist/cli/index.js +0 -0
- package/dist/cli/sdd-renderer.js +7 -7
- package/dist/cli/setup-status-renderer.js +1 -0
- package/dist/cli/tui/main-menu/index.js +0 -1
- package/dist/cli/tui/main-menu/main-menu-controller.js +0 -2
- package/dist/cli/tui/main-menu/main-menu-read-model.js +10 -8
- package/dist/cli/tui/main-menu/main-menu-render-shape.js +19 -2
- package/dist/cli/tui/main-menu/main-menu-state.js +1 -1
- package/dist/cli/tui/opentui/code/index.js +210 -0
- package/dist/cli/tui/opentui/code/screen.js +107 -0
- package/dist/cli/tui/opentui/code/smoke.js +32 -0
- package/dist/cli/tui/opentui/main-menu/index.js +3 -0
- package/dist/cli/tui/opentui/main-menu/renderer.js +68 -0
- package/dist/cli/tui/opentui/main-menu/screen.js +68 -0
- package/dist/cli/tui/opentui/main-menu/smoke.js +17 -0
- package/dist/cli/tui/opentui/main-menu/view.js +8 -0
- package/dist/cli/tui/opentui/setup/index.js +3 -0
- package/dist/cli/tui/opentui/setup/renderer.js +87 -0
- package/dist/cli/tui/opentui/setup/screen.js +170 -0
- package/dist/cli/tui/opentui/setup/smoke.js +42 -0
- package/dist/cli/tui/opentui/setup/view.js +12 -0
- package/dist/cli/tui/setup/setup-tui-input.js +43 -0
- package/dist/cli/tui/setup/setup-tui-read-model.js +4 -4
- package/dist/cli/tui/setup/setup-tui-render-shape.js +9 -10
- package/dist/cli/tui/setup/setup-tui-state.js +1 -1
- package/dist/cli/tui/setup/setup-tui-view-helpers.js +46 -0
- package/dist/cli/tui/visual/index.js +0 -2
- package/dist/code/runtime/sdd-context.js +2 -2
- package/dist/code/tui/approval-actions.js +33 -0
- package/dist/code/tui/prompt-mode.js +11 -0
- package/dist/code/tui/runtime-events.js +320 -0
- package/dist/mcp/validation.js +6 -2
- package/dist/orchestrator/natural-language-planner.js +1 -1
- package/dist/sdd/sdd-workflow-service.js +1 -25
- package/dist/setup/backup-rollback-service.js +2 -2
- package/dist/setup/providers/antigravity-setup-adapter.js +1 -1
- package/dist/setup/providers/claude-setup-adapter.js +2 -2
- package/dist/setup/providers/custom-setup-adapter.js +1 -1
- package/dist/setup/providers/opencode-setup-adapter.js +3 -3
- package/dist/setup/setup-lifecycle-service.js +6 -6
- package/dist/setup/setup-plan.js +3 -3
- package/dist/verification/verification-plan-service.js +1 -1
- package/docs/architecture.md +43 -42
- package/docs/cli.md +141 -133
- package/docs/funcionamiento-del-sistema.md +22 -23
- package/docs/harness-gap-analysis.md +15 -1
- package/docs/prd.md +14 -14
- package/docs/vgxcode.md +87 -0
- package/docs/vgxness-code.md +6 -4
- package/package.json +5 -6
- package/dist/cli/commands/dashboard-dispatcher.js +0 -560
- package/dist/cli/dashboard-operational-read-models.js +0 -428
- package/dist/cli/dashboard-renderer.js +0 -158
- package/dist/cli/dashboard-screen-renderers.js +0 -256
- package/dist/cli/dashboard-tui-read-model.js +0 -73
- package/dist/cli/dashboard-tui-state.js +0 -314
- package/dist/cli/guided-main-menu.js +0 -470
- package/dist/cli/interactive-dashboard.js +0 -34
- package/dist/cli/setup-wizard-read-model.js +0 -72
- package/dist/cli/setup-wizard-renderer.js +0 -155
- package/dist/cli/setup-wizard-state.js +0 -82
- package/dist/cli/tui/dashboard/dashboard-adapter.js +0 -4
- package/dist/cli/tui/main-menu/main-menu-app.js +0 -28
- package/dist/cli/tui/render-ink-app.js +0 -10
- package/dist/cli/tui/setup/screens/applying-screen.js +0 -6
- package/dist/cli/tui/setup/screens/cancellation-screen.js +0 -6
- package/dist/cli/tui/setup/screens/error-recovery-screen.js +0 -6
- package/dist/cli/tui/setup/screens/final-confirmation-screen.js +0 -6
- package/dist/cli/tui/setup/screens/opencode-details-screen.js +0 -10
- package/dist/cli/tui/setup/screens/plan-review-screen.js +0 -6
- package/dist/cli/tui/setup/screens/project-database-screen.js +0 -6
- package/dist/cli/tui/setup/screens/provider-screen.js +0 -7
- package/dist/cli/tui/setup/screens/result-screen.js +0 -16
- package/dist/cli/tui/setup/screens/screen-components.js +0 -103
- package/dist/cli/tui/setup/screens/welcome-screen.js +0 -6
- package/dist/cli/tui/setup/setup-tui-app.js +0 -113
- package/dist/cli/tui/visual/choice-list.js +0 -10
- package/dist/cli/tui/visual/layout.js +0 -10
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createCliRenderer } from '@opentui/core';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { ApprovalDecisionWriter, getApprovalActionsState } from '../../../../code/tui/approval-actions.js';
|
|
6
|
+
import { buildSafetyReadModel, parseVgxcodeJsonl, parseVgxcodeJsonlLine } from '../../../../code/tui/runtime-events.js';
|
|
7
|
+
import { parsePromptSubmission, toggleReadOnlyMode } from '../../../../code/tui/prompt-mode.js';
|
|
8
|
+
import { createVgxcodeScreen } from './screen.js';
|
|
9
|
+
async function main() {
|
|
10
|
+
const project = process.argv[2] ?? 'vgxness';
|
|
11
|
+
const parsedStdin = await readPipedRuntimeEvents();
|
|
12
|
+
const state = {
|
|
13
|
+
project,
|
|
14
|
+
prompt: '',
|
|
15
|
+
submittedPrompt: '',
|
|
16
|
+
events: [...parsedStdin.events],
|
|
17
|
+
errors: [...parsedStdin.errors],
|
|
18
|
+
status: parsedStdin.errors.length > 0 ? 'error' : parsedStdin.events.length > 0 ? 'completed' : 'idle',
|
|
19
|
+
source: parsedStdin.events.length > 0 || parsedStdin.errors.length > 0 ? 'stdin' : 'interactive',
|
|
20
|
+
mode: 'inspect',
|
|
21
|
+
childStdinOpen: false,
|
|
22
|
+
decisionWriter: undefined,
|
|
23
|
+
};
|
|
24
|
+
const renderer = await createCliRenderer({
|
|
25
|
+
exitOnCtrlC: true,
|
|
26
|
+
clearOnShutdown: true,
|
|
27
|
+
screenMode: 'alternate-screen',
|
|
28
|
+
consoleMode: 'disabled',
|
|
29
|
+
});
|
|
30
|
+
const render = () => {
|
|
31
|
+
const current = renderer.root.getRenderable('vgxcode-screen');
|
|
32
|
+
if (current)
|
|
33
|
+
renderer.root.remove('vgxcode-screen');
|
|
34
|
+
const safety = buildSafetyReadModel(state.events);
|
|
35
|
+
renderer.root.add(createVgxcodeScreen({
|
|
36
|
+
...state,
|
|
37
|
+
approvalActionsEnabled: getApprovalActionsState({
|
|
38
|
+
source: state.source,
|
|
39
|
+
status: state.status,
|
|
40
|
+
mode: state.mode,
|
|
41
|
+
childStdinOpen: state.childStdinOpen,
|
|
42
|
+
pendingApprovalCount: safety.pendingApprovalCount,
|
|
43
|
+
...(safety.latestPendingApproval === undefined ? {} : { latestPendingApproval: safety.latestPendingApproval }),
|
|
44
|
+
}).enabled,
|
|
45
|
+
}));
|
|
46
|
+
renderer.requestRender();
|
|
47
|
+
};
|
|
48
|
+
render();
|
|
49
|
+
if (state.source === 'interactive') {
|
|
50
|
+
renderer.keyInput.on('keypress', (key) => {
|
|
51
|
+
if (key.ctrl && key.name === 'c')
|
|
52
|
+
return;
|
|
53
|
+
if (state.status === 'running') {
|
|
54
|
+
if (key.name === 'a' || key.sequence === 'a')
|
|
55
|
+
writePendingApprovalDecision(state, 'approved', render);
|
|
56
|
+
if (key.name === 'd' || key.sequence === 'd')
|
|
57
|
+
writePendingApprovalDecision(state, 'denied', render);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (key.name === 'tab') {
|
|
61
|
+
state.mode = toggleReadOnlyMode(state.mode);
|
|
62
|
+
render();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
66
|
+
void runReadOnlyBridge(state, render);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.name === 'backspace' || key.name === 'delete') {
|
|
70
|
+
state.prompt = state.prompt.slice(0, -1);
|
|
71
|
+
render();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const text = printableKeyText(key.sequence);
|
|
75
|
+
if (text !== '') {
|
|
76
|
+
state.prompt += text;
|
|
77
|
+
render();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function readPipedRuntimeEvents() {
|
|
83
|
+
if (process.stdin.isTTY)
|
|
84
|
+
return { events: [], errors: [] };
|
|
85
|
+
let input = '';
|
|
86
|
+
for await (const chunk of process.stdin)
|
|
87
|
+
input += String(chunk);
|
|
88
|
+
return parseVgxcodeJsonl(input);
|
|
89
|
+
}
|
|
90
|
+
async function runReadOnlyBridge(state, render) {
|
|
91
|
+
const submission = parsePromptSubmission(state.prompt, state.mode);
|
|
92
|
+
const prompt = submission.prompt;
|
|
93
|
+
if (prompt === '') {
|
|
94
|
+
state.status = 'error';
|
|
95
|
+
state.errors = ['Enter a prompt before running inspect, plan, craft-preview, or craft. Prefix with /inspect, /plan, /craft-preview, or /craft; press Tab to switch inspect/plan.'];
|
|
96
|
+
render();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
state.mode = submission.mode;
|
|
100
|
+
state.submittedPrompt = prompt;
|
|
101
|
+
state.prompt = '';
|
|
102
|
+
state.status = 'running';
|
|
103
|
+
state.errors = [];
|
|
104
|
+
state.events = [];
|
|
105
|
+
state.childStdinOpen = false;
|
|
106
|
+
state.decisionWriter = undefined;
|
|
107
|
+
render();
|
|
108
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
|
109
|
+
const approvalCapableCraft = state.mode === 'craft';
|
|
110
|
+
const child = spawn('bun', commandArgsForMode(state.mode, prompt), {
|
|
111
|
+
cwd: root,
|
|
112
|
+
stdio: approvalCapableCraft ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
113
|
+
env: process.env,
|
|
114
|
+
});
|
|
115
|
+
state.childStdinOpen = approvalCapableCraft && child.stdin !== null;
|
|
116
|
+
state.decisionWriter = approvalCapableCraft && child.stdin !== null ? new ApprovalDecisionWriter(child.stdin) : undefined;
|
|
117
|
+
if (child.stdout === null || child.stderr === null) {
|
|
118
|
+
state.status = 'error';
|
|
119
|
+
state.childStdinOpen = false;
|
|
120
|
+
state.decisionWriter = undefined;
|
|
121
|
+
state.errors.push('Failed to start root CLI with JSONL output streams.');
|
|
122
|
+
render();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
child.stdin?.on('close', () => {
|
|
126
|
+
state.childStdinOpen = false;
|
|
127
|
+
render();
|
|
128
|
+
});
|
|
129
|
+
child.stdin?.on('error', () => {
|
|
130
|
+
state.childStdinOpen = false;
|
|
131
|
+
render();
|
|
132
|
+
});
|
|
133
|
+
let lineNumber = 0;
|
|
134
|
+
let stdoutBuffer = '';
|
|
135
|
+
let stderr = '';
|
|
136
|
+
child.stdout.setEncoding('utf8');
|
|
137
|
+
child.stdout.on('data', (chunk) => {
|
|
138
|
+
stdoutBuffer += chunk;
|
|
139
|
+
const lines = stdoutBuffer.split(/\r?\n/);
|
|
140
|
+
stdoutBuffer = lines.pop() ?? '';
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
lineNumber += 1;
|
|
143
|
+
const parsed = parseVgxcodeJsonlLine(line, lineNumber);
|
|
144
|
+
if (parsed.event)
|
|
145
|
+
state.events.push(parsed.event);
|
|
146
|
+
if (parsed.error)
|
|
147
|
+
state.errors.push(parsed.error);
|
|
148
|
+
}
|
|
149
|
+
render();
|
|
150
|
+
});
|
|
151
|
+
child.stderr.setEncoding('utf8');
|
|
152
|
+
child.stderr.on('data', (chunk) => {
|
|
153
|
+
stderr += chunk;
|
|
154
|
+
});
|
|
155
|
+
child.on('error', (error) => {
|
|
156
|
+
state.status = 'error';
|
|
157
|
+
state.childStdinOpen = false;
|
|
158
|
+
state.errors.push(`Failed to start root CLI: ${error.message}`);
|
|
159
|
+
render();
|
|
160
|
+
});
|
|
161
|
+
child.on('close', (code, signal) => {
|
|
162
|
+
state.childStdinOpen = false;
|
|
163
|
+
state.decisionWriter = undefined;
|
|
164
|
+
if (stdoutBuffer.trim() !== '') {
|
|
165
|
+
lineNumber += 1;
|
|
166
|
+
const parsed = parseVgxcodeJsonlLine(stdoutBuffer, lineNumber);
|
|
167
|
+
if (parsed.event)
|
|
168
|
+
state.events.push(parsed.event);
|
|
169
|
+
if (parsed.error)
|
|
170
|
+
state.errors.push(parsed.error);
|
|
171
|
+
}
|
|
172
|
+
state.status = state.errors.length > 0 ? 'error' : 'completed';
|
|
173
|
+
if (code !== 0 && !(state.mode === 'craft-preview' && code === 3)) {
|
|
174
|
+
const detail = stderr.trim() === '' ? `signal ${signal ?? 'none'}` : stderr.trim();
|
|
175
|
+
state.status = 'error';
|
|
176
|
+
state.errors.push(`Root Bun CLI ${state.mode} failed (exit ${code ?? 'signal'}): ${detail}`);
|
|
177
|
+
}
|
|
178
|
+
render();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function commandArgsForMode(mode, prompt) {
|
|
182
|
+
const args = ['run', '--silent', 'cli:bun', '--', 'code', mode, prompt, '--events-jsonl'];
|
|
183
|
+
if (mode === 'craft')
|
|
184
|
+
args.push('--approval-channel', 'stdio');
|
|
185
|
+
return args;
|
|
186
|
+
}
|
|
187
|
+
function writePendingApprovalDecision(state, status, render) {
|
|
188
|
+
const model = buildSafetyReadModel(state.events);
|
|
189
|
+
const actions = getApprovalActionsState({
|
|
190
|
+
source: state.source,
|
|
191
|
+
status: state.status,
|
|
192
|
+
mode: state.mode,
|
|
193
|
+
childStdinOpen: state.childStdinOpen,
|
|
194
|
+
pendingApprovalCount: model.pendingApprovalCount,
|
|
195
|
+
...(model.latestPendingApproval === undefined ? {} : { latestPendingApproval: model.latestPendingApproval }),
|
|
196
|
+
});
|
|
197
|
+
if (!actions.enabled)
|
|
198
|
+
return;
|
|
199
|
+
const wrote = state.decisionWriter?.write(actions.approval, status, status === 'approved' ? 'Approved from vgxcode.' : 'Denied from vgxcode.') ?? false;
|
|
200
|
+
if (!wrote)
|
|
201
|
+
return;
|
|
202
|
+
render();
|
|
203
|
+
}
|
|
204
|
+
function printableKeyText(sequence) {
|
|
205
|
+
if (sequence.length !== 1)
|
|
206
|
+
return '';
|
|
207
|
+
const code = sequence.charCodeAt(0);
|
|
208
|
+
return code >= 32 && code !== 127 ? sequence : '';
|
|
209
|
+
}
|
|
210
|
+
await main();
|
|
@@ -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,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,68 @@
|
|
|
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
|
+
border: true,
|
|
23
|
+
borderStyle: 'rounded',
|
|
24
|
+
borderColor: '#14B8A6',
|
|
25
|
+
paddingX: 1,
|
|
26
|
+
title: ` ${vm.statusSnapshot.title} ${vm.statusSnapshot.badges.join(' ')} `,
|
|
27
|
+
}, Text({ content: vm.statusSnapshot.lines.join('\n'), fg: '#CCFBF1', wrapMode: 'word' })), Box({
|
|
28
|
+
flexDirection: state.viewport.mode === 'narrow' ? 'column' : 'row',
|
|
29
|
+
flexGrow: 1,
|
|
30
|
+
gap: 1,
|
|
31
|
+
}, Box({
|
|
32
|
+
border: true,
|
|
33
|
+
borderStyle: 'rounded',
|
|
34
|
+
borderColor: '#A78BFA',
|
|
35
|
+
flexGrow: 1,
|
|
36
|
+
padding: 1,
|
|
37
|
+
title: ' Menu ',
|
|
38
|
+
}, Text({ content: formatMainMenuOptions(vm.options), fg: '#F8FAFC', wrapMode: 'word' })), Box({
|
|
39
|
+
border: true,
|
|
40
|
+
borderStyle: 'rounded',
|
|
41
|
+
borderColor: '#22C55E',
|
|
42
|
+
flexGrow: 1,
|
|
43
|
+
padding: 1,
|
|
44
|
+
title: ` ${vm.detail.title}${vm.detail.badges.length === 0 ? '' : ` ${vm.detail.badges.join(' ')}`} `,
|
|
45
|
+
}, Text({ content: vm.detail.lines.join('\n'), fg: '#DCFCE7', wrapMode: 'word' }))), Box({
|
|
46
|
+
border: true,
|
|
47
|
+
borderStyle: 'rounded',
|
|
48
|
+
borderColor: '#F59E0B',
|
|
49
|
+
paddingX: 1,
|
|
50
|
+
title: ` Safety ${tuiBadges.noProviderWrites} `,
|
|
51
|
+
}, Text({ content: vm.safetyLines.join('\n'), fg: '#FEF3C7', wrapMode: 'word' })), ...(vm.helpLines.length === 0
|
|
52
|
+
? []
|
|
53
|
+
: [
|
|
54
|
+
Box({
|
|
55
|
+
border: true,
|
|
56
|
+
borderStyle: 'rounded',
|
|
57
|
+
borderColor: '#38BDF8',
|
|
58
|
+
paddingX: 1,
|
|
59
|
+
title: ` Help ${tuiBadges.readOnly} `,
|
|
60
|
+
}, Text({ content: vm.helpLines.join('\n'), fg: '#E0F2FE', wrapMode: 'word' })),
|
|
61
|
+
]), Box({
|
|
62
|
+
border: true,
|
|
63
|
+
borderStyle: 'rounded',
|
|
64
|
+
borderColor: '#475569',
|
|
65
|
+
paddingX: 1,
|
|
66
|
+
title: ' Shortcuts ',
|
|
67
|
+
}, Text({ content: vm.footer, fg: '#CBD5E1', wrapMode: 'word' })));
|
|
68
|
+
}
|
|
@@ -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,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
|
+
}
|