wiggum-cli 0.9.8 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.d.ts +8 -19
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -351
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/new.d.ts +21 -13
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +10 -267
- package/dist/commands/new.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -85
- package/dist/index.js.map +1 -1
- package/dist/repl/index.d.ts +3 -3
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +3 -3
- package/dist/repl/index.js.map +1 -1
- package/dist/tui/app.d.ts +1 -5
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +7 -12
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/Confirm.d.ts +39 -0
- package/dist/tui/components/Confirm.d.ts.map +1 -0
- package/dist/tui/components/Confirm.js +60 -0
- package/dist/tui/components/Confirm.js.map +1 -0
- package/dist/tui/components/PasswordInput.d.ts +39 -0
- package/dist/tui/components/PasswordInput.d.ts.map +1 -0
- package/dist/tui/components/PasswordInput.js +56 -0
- package/dist/tui/components/PasswordInput.js.map +1 -0
- package/dist/tui/components/Select.d.ts +55 -0
- package/dist/tui/components/Select.d.ts.map +1 -0
- package/dist/tui/components/Select.js +63 -0
- package/dist/tui/components/Select.js.map +1 -0
- package/dist/tui/components/index.d.ts +6 -0
- package/dist/tui/components/index.d.ts.map +1 -1
- package/dist/tui/components/index.js +3 -0
- package/dist/tui/components/index.js.map +1 -1
- package/dist/tui/hooks/index.d.ts +3 -0
- package/dist/tui/hooks/index.d.ts.map +1 -1
- package/dist/tui/hooks/index.js +2 -0
- package/dist/tui/hooks/index.js.map +1 -1
- package/dist/tui/hooks/useInit.d.ts +130 -0
- package/dist/tui/hooks/useInit.d.ts.map +1 -0
- package/dist/tui/hooks/useInit.js +326 -0
- package/dist/tui/hooks/useInit.js.map +1 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts +4 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
- package/dist/tui/hooks/useSpecGenerator.js +12 -1
- package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
- package/dist/tui/screens/InitScreen.d.ts +17 -10
- package/dist/tui/screens/InitScreen.d.ts.map +1 -1
- package/dist/tui/screens/InitScreen.js +317 -18
- package/dist/tui/screens/InitScreen.js.map +1 -1
- package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
- package/dist/tui/screens/InterviewScreen.js +2 -2
- package/dist/tui/screens/InterviewScreen.js.map +1 -1
- package/dist/tui/screens/index.d.ts +6 -0
- package/dist/tui/screens/index.d.ts.map +1 -1
- package/dist/tui/screens/index.js +3 -0
- package/dist/tui/screens/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/init.ts +8 -432
- package/src/commands/new.ts +13 -319
- package/src/index.ts +85 -93
- package/src/repl/index.ts +3 -3
- package/src/tui/app.tsx +7 -15
- package/src/tui/components/Confirm.tsx +109 -0
- package/src/tui/components/PasswordInput.tsx +106 -0
- package/src/tui/components/Select.tsx +132 -0
- package/src/tui/components/index.ts +9 -0
- package/src/tui/hooks/index.ts +9 -0
- package/src/tui/hooks/useInit.ts +472 -0
- package/src/tui/hooks/useSpecGenerator.ts +17 -1
- package/src/tui/screens/InitScreen.tsx +562 -29
- package/src/tui/screens/InterviewScreen.tsx +2 -1
- package/src/tui/screens/index.ts +9 -0
- package/src/cli.ts +0 -274
- package/src/repl/repl-loop.ts +0 -389
- package/src/utils/repl-prompts.ts +0 -381
|
@@ -1,63 +1,596 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* InitScreen -
|
|
2
|
+
* InitScreen - Full Ink-based init workflow
|
|
3
3
|
*
|
|
4
|
-
* Handles project initialization within the TUI
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Handles the complete project initialization flow within the TUI:
|
|
5
|
+
* 1. Scanning project structure
|
|
6
|
+
* 2. API key collection (if needed)
|
|
7
|
+
* 3. Model selection
|
|
8
|
+
* 4. AI analysis
|
|
9
|
+
* 5. File generation
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import React, { useEffect } from 'react';
|
|
10
|
-
import { Box, Text } from 'ink';
|
|
12
|
+
import React, { useEffect, useRef, useCallback } from 'react';
|
|
13
|
+
import { Box, Text, useInput } from 'ink';
|
|
11
14
|
import { colors } from '../theme.js';
|
|
15
|
+
import { useInit, INIT_PHASE_CONFIGS, INIT_TOTAL_PHASES } from '../hooks/useInit.js';
|
|
16
|
+
import { PhaseHeader } from '../components/PhaseHeader.js';
|
|
17
|
+
import { WorkingIndicator } from '../components/WorkingIndicator.js';
|
|
18
|
+
import { Select, type SelectOption } from '../components/Select.js';
|
|
19
|
+
import { PasswordInput } from '../components/PasswordInput.js';
|
|
20
|
+
import { Confirm } from '../components/Confirm.js';
|
|
21
|
+
import { Scanner } from '../../scanner/index.js';
|
|
22
|
+
import {
|
|
23
|
+
AIEnhancer,
|
|
24
|
+
formatAIAnalysis,
|
|
25
|
+
type EnhancedScanResult,
|
|
26
|
+
} from '../../ai/index.js';
|
|
27
|
+
import {
|
|
28
|
+
hasApiKey,
|
|
29
|
+
getApiKeyEnvVar,
|
|
30
|
+
getAvailableProvider,
|
|
31
|
+
AVAILABLE_MODELS,
|
|
32
|
+
type AIProvider,
|
|
33
|
+
} from '../../ai/providers.js';
|
|
34
|
+
import { Generator } from '../../generator/index.js';
|
|
35
|
+
import { loadConfigWithDefaults } from '../../utils/config.js';
|
|
36
|
+
import { initTracing, flushTracing, traced } from '../../utils/tracing.js';
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import type { SessionState } from '../../repl/session-state.js';
|
|
40
|
+
import { updateSessionState } from '../../repl/session-state.js';
|
|
12
41
|
|
|
13
42
|
/**
|
|
14
43
|
* Props for the InitScreen component
|
|
15
44
|
*/
|
|
16
45
|
export interface InitScreenProps {
|
|
17
|
-
/**
|
|
18
|
-
|
|
46
|
+
/** Project root directory */
|
|
47
|
+
projectRoot: string;
|
|
48
|
+
/** Current session state */
|
|
49
|
+
sessionState: SessionState;
|
|
50
|
+
/** Called when initialization is complete */
|
|
51
|
+
onComplete: (newState: SessionState) => void;
|
|
19
52
|
/** Called when user cancels */
|
|
20
53
|
onCancel: () => void;
|
|
21
54
|
}
|
|
22
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Provider options for the select component
|
|
58
|
+
*/
|
|
59
|
+
const PROVIDER_OPTIONS: SelectOption<AIProvider>[] = [
|
|
60
|
+
{ value: 'anthropic', label: 'Anthropic', hint: 'recommended' },
|
|
61
|
+
{ value: 'openai', label: 'OpenAI' },
|
|
62
|
+
{ value: 'openrouter', label: 'OpenRouter', hint: 'multiple providers' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get model options for a provider
|
|
67
|
+
*/
|
|
68
|
+
function getModelOptions(provider: AIProvider): SelectOption<string>[] {
|
|
69
|
+
return AVAILABLE_MODELS[provider].map((m) => ({
|
|
70
|
+
value: m.value,
|
|
71
|
+
label: m.label,
|
|
72
|
+
hint: m.hint,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Save API keys to .env.local file
|
|
78
|
+
*/
|
|
79
|
+
function saveKeysToEnvLocal(projectRoot: string, keys: Record<string, string>): void {
|
|
80
|
+
const envLocalPath = path.join(projectRoot, '.env.local');
|
|
81
|
+
let envContent = '';
|
|
82
|
+
|
|
83
|
+
if (fs.existsSync(envLocalPath)) {
|
|
84
|
+
envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [envVar, value] of Object.entries(keys)) {
|
|
88
|
+
if (!value) continue;
|
|
89
|
+
|
|
90
|
+
const keyRegex = new RegExp(`^${envVar}=.*$`, 'm');
|
|
91
|
+
if (keyRegex.test(envContent)) {
|
|
92
|
+
envContent = envContent.replace(keyRegex, `${envVar}=${value}`);
|
|
93
|
+
} else {
|
|
94
|
+
envContent = envContent.trimEnd() + (envContent ? '\n' : '') + `${envVar}=${value}\n`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(envLocalPath, envContent);
|
|
99
|
+
}
|
|
100
|
+
|
|
23
101
|
/**
|
|
24
102
|
* InitScreen component
|
|
25
103
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* readline-based interactive prompts.
|
|
104
|
+
* The complete Ink-based init workflow. Replaces the readline-based
|
|
105
|
+
* init flow with native Ink components.
|
|
29
106
|
*/
|
|
30
107
|
export function InitScreen({
|
|
31
|
-
|
|
32
|
-
|
|
108
|
+
projectRoot,
|
|
109
|
+
sessionState,
|
|
110
|
+
onComplete,
|
|
111
|
+
onCancel,
|
|
33
112
|
}: InitScreenProps): React.ReactElement {
|
|
34
|
-
|
|
113
|
+
const {
|
|
114
|
+
state,
|
|
115
|
+
initialize,
|
|
116
|
+
setScanResult,
|
|
117
|
+
setExistingProvider,
|
|
118
|
+
selectProvider,
|
|
119
|
+
setApiKey,
|
|
120
|
+
setSaveKey,
|
|
121
|
+
selectModel,
|
|
122
|
+
setAiProgress,
|
|
123
|
+
setEnhancedResult,
|
|
124
|
+
setAiError,
|
|
125
|
+
confirmGeneration,
|
|
126
|
+
setGenerating,
|
|
127
|
+
setGenerationComplete,
|
|
128
|
+
setError,
|
|
129
|
+
} = useInit();
|
|
130
|
+
|
|
131
|
+
// Store API key in ref (not in state for security)
|
|
132
|
+
const apiKeyRef = useRef<string | null>(null);
|
|
133
|
+
|
|
134
|
+
// Track if AI analysis has started
|
|
135
|
+
const aiAnalysisStarted = useRef(false);
|
|
136
|
+
|
|
137
|
+
// Track if generation has started
|
|
138
|
+
const generationStarted = useRef(false);
|
|
139
|
+
|
|
140
|
+
// Handle Escape to cancel
|
|
141
|
+
useInput((input, key) => {
|
|
142
|
+
if (key.escape) {
|
|
143
|
+
onCancel();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Initialize on mount
|
|
35
148
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
149
|
+
initialize(projectRoot);
|
|
150
|
+
}, [projectRoot, initialize]);
|
|
151
|
+
|
|
152
|
+
// Run scan when in scanning phase
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (state.phase !== 'scanning' || state.scanResult) return;
|
|
155
|
+
|
|
156
|
+
const runScan = async () => {
|
|
157
|
+
try {
|
|
158
|
+
const scanner = new Scanner();
|
|
159
|
+
const result = await scanner.scan(projectRoot);
|
|
160
|
+
setScanResult(result);
|
|
161
|
+
|
|
162
|
+
// Check for existing API key
|
|
163
|
+
const existingProvider = getAvailableProvider();
|
|
164
|
+
if (existingProvider) {
|
|
165
|
+
setExistingProvider(existingProvider);
|
|
166
|
+
} else {
|
|
167
|
+
// Need to collect API key - go to provider select
|
|
168
|
+
// This is done by checking state.hasApiKey in render
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
setError(`Failed to scan project: ${error instanceof Error ? error.message : String(error)}`);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
runScan();
|
|
176
|
+
}, [state.phase, state.scanResult, projectRoot, setScanResult, setExistingProvider, setError]);
|
|
177
|
+
|
|
178
|
+
// Transition from scan to provider select if no API key
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (state.phase === 'scanning' && state.scanResult && !state.hasApiKey) {
|
|
181
|
+
// Check one more time for available provider (in case env was set after initial check)
|
|
182
|
+
const existingProvider = getAvailableProvider();
|
|
183
|
+
if (existingProvider) {
|
|
184
|
+
setExistingProvider(existingProvider);
|
|
185
|
+
}
|
|
186
|
+
// Otherwise stay at scanning which will show provider-select
|
|
187
|
+
}
|
|
188
|
+
}, [state.phase, state.scanResult, state.hasApiKey, setExistingProvider]);
|
|
189
|
+
|
|
190
|
+
// Run AI analysis when in ai-analysis phase
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (state.phase !== 'ai-analysis' || aiAnalysisStarted.current) return;
|
|
193
|
+
if (!state.scanResult || !state.provider || !state.model) return;
|
|
194
|
+
|
|
195
|
+
aiAnalysisStarted.current = true;
|
|
196
|
+
|
|
197
|
+
const runAnalysis = async () => {
|
|
198
|
+
initTracing();
|
|
199
|
+
|
|
200
|
+
const aiEnhancer = new AIEnhancer({
|
|
201
|
+
provider: state.provider!,
|
|
202
|
+
model: state.model!,
|
|
203
|
+
verbose: false,
|
|
204
|
+
agentic: true,
|
|
205
|
+
onProgress: (phase, detail) => {
|
|
206
|
+
if (detail) {
|
|
207
|
+
setAiProgress(`${phase} - ${detail}`);
|
|
208
|
+
} else {
|
|
209
|
+
setAiProgress(phase);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const enhancedResult = await traced(
|
|
216
|
+
async () => {
|
|
217
|
+
return await aiEnhancer.enhance(state.scanResult!);
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'ai-analysis',
|
|
221
|
+
type: 'task',
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (enhancedResult.aiEnhanced && enhancedResult.aiAnalysis) {
|
|
226
|
+
setEnhancedResult(enhancedResult, enhancedResult.tokenUsage);
|
|
227
|
+
} else if (enhancedResult.aiError) {
|
|
228
|
+
setAiError(enhancedResult.aiError);
|
|
229
|
+
} else {
|
|
230
|
+
setEnhancedResult(enhancedResult);
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
setAiError(error instanceof Error ? error.message : String(error));
|
|
234
|
+
} finally {
|
|
235
|
+
await flushTracing();
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
runAnalysis();
|
|
240
|
+
}, [state.phase, state.scanResult, state.provider, state.model, setAiProgress, setEnhancedResult, setAiError]);
|
|
241
|
+
|
|
242
|
+
// Run generation when in generating phase
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (state.phase !== 'generating' || generationStarted.current) return;
|
|
245
|
+
if (!state.scanResult || !state.model) return;
|
|
246
|
+
|
|
247
|
+
generationStarted.current = true;
|
|
248
|
+
|
|
249
|
+
const runGeneration = async () => {
|
|
250
|
+
// Use enhanced result if available, otherwise use scan result
|
|
251
|
+
const sourceResult = state.enhancedResult || state.scanResult;
|
|
252
|
+
|
|
253
|
+
const generator = new Generator({
|
|
254
|
+
existingFiles: 'backup',
|
|
255
|
+
generateConfig: true,
|
|
256
|
+
verbose: false,
|
|
257
|
+
customVariables: {
|
|
258
|
+
defaultModel: state.model!,
|
|
259
|
+
planningModel: state.model!,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
setGenerating('Writing configuration files...');
|
|
265
|
+
const generationResult = await generator.generate(sourceResult as EnhancedScanResult);
|
|
40
266
|
|
|
41
|
-
|
|
42
|
-
|
|
267
|
+
// Extract generated file paths
|
|
268
|
+
const generatedFiles = generationResult.writeSummary.results
|
|
269
|
+
.filter((f: { action: string }) =>
|
|
270
|
+
f.action === 'created' || f.action === 'backed_up' || f.action === 'overwritten'
|
|
271
|
+
)
|
|
272
|
+
.map((f: { path: string }) => {
|
|
273
|
+
const relativePath = path.relative(projectRoot, f.path);
|
|
274
|
+
return relativePath.replace(/^\.ralph[\\/]/, '');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Save API key to .env.local if requested
|
|
278
|
+
if (state.apiKeyEnteredThisSession && state.saveKeyToEnv && state.provider && apiKeyRef.current) {
|
|
279
|
+
const envVar = getApiKeyEnvVar(state.provider);
|
|
280
|
+
saveKeysToEnvLocal(projectRoot, { [envVar]: apiKeyRef.current });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
setGenerationComplete(generatedFiles);
|
|
284
|
+
|
|
285
|
+
// Load config and update session state
|
|
286
|
+
const config = await loadConfigWithDefaults(projectRoot);
|
|
287
|
+
const newSessionState = updateSessionState(sessionState, {
|
|
288
|
+
provider: state.provider ?? undefined,
|
|
289
|
+
model: state.model ?? undefined,
|
|
290
|
+
scanResult: sourceResult ?? undefined,
|
|
291
|
+
config,
|
|
292
|
+
initialized: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
onComplete(newSessionState);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
setError(`Failed to generate files: ${error instanceof Error ? error.message : String(error)}`);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
runGeneration();
|
|
302
|
+
}, [
|
|
303
|
+
state.phase,
|
|
304
|
+
state.scanResult,
|
|
305
|
+
state.enhancedResult,
|
|
306
|
+
state.model,
|
|
307
|
+
state.provider,
|
|
308
|
+
state.apiKeyEnteredThisSession,
|
|
309
|
+
state.saveKeyToEnv,
|
|
310
|
+
projectRoot,
|
|
311
|
+
sessionState,
|
|
312
|
+
setGenerating,
|
|
313
|
+
setGenerationComplete,
|
|
314
|
+
setError,
|
|
315
|
+
onComplete,
|
|
316
|
+
]);
|
|
317
|
+
|
|
318
|
+
// Handle provider selection
|
|
319
|
+
const handleProviderSelect = useCallback(
|
|
320
|
+
(provider: AIProvider) => {
|
|
321
|
+
selectProvider(provider);
|
|
322
|
+
},
|
|
323
|
+
[selectProvider]
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Handle API key input
|
|
327
|
+
const handleApiKeySubmit = useCallback(
|
|
328
|
+
(key: string) => {
|
|
329
|
+
if (!state.provider) return;
|
|
330
|
+
|
|
331
|
+
// Store key in ref
|
|
332
|
+
apiKeyRef.current = key;
|
|
333
|
+
|
|
334
|
+
// Set key in environment for this session
|
|
335
|
+
const envVar = getApiKeyEnvVar(state.provider);
|
|
336
|
+
process.env[envVar] = key;
|
|
337
|
+
|
|
338
|
+
setApiKey(key);
|
|
339
|
+
},
|
|
340
|
+
[state.provider, setApiKey]
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Handle save key confirmation
|
|
344
|
+
const handleSaveKeyConfirm = useCallback(
|
|
345
|
+
(save: boolean) => {
|
|
346
|
+
setSaveKey(save);
|
|
347
|
+
},
|
|
348
|
+
[setSaveKey]
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Handle model selection
|
|
352
|
+
const handleModelSelect = useCallback(
|
|
353
|
+
(model: string) => {
|
|
354
|
+
selectModel(model);
|
|
355
|
+
},
|
|
356
|
+
[selectModel]
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
// Handle generation confirmation
|
|
360
|
+
const handleConfirmGeneration = useCallback(
|
|
361
|
+
(confirmed: boolean) => {
|
|
362
|
+
confirmGeneration(confirmed);
|
|
363
|
+
},
|
|
364
|
+
[confirmGeneration]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Get current phase config
|
|
368
|
+
const phaseConfig = INIT_PHASE_CONFIGS[state.phase];
|
|
369
|
+
|
|
370
|
+
// Render based on current phase
|
|
371
|
+
const renderPhaseContent = () => {
|
|
372
|
+
switch (state.phase) {
|
|
373
|
+
case 'scanning':
|
|
374
|
+
if (state.scanResult && !state.hasApiKey) {
|
|
375
|
+
// Scan done but no API key - show provider select
|
|
376
|
+
return (
|
|
377
|
+
<Select
|
|
378
|
+
message="Select your AI provider:"
|
|
379
|
+
options={PROVIDER_OPTIONS}
|
|
380
|
+
onSelect={handleProviderSelect}
|
|
381
|
+
onCancel={onCancel}
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
// Still scanning
|
|
386
|
+
return (
|
|
387
|
+
<WorkingIndicator
|
|
388
|
+
state={{
|
|
389
|
+
isWorking: true,
|
|
390
|
+
status: state.workingStatus || 'Scanning project structure...',
|
|
391
|
+
hint: 'esc to cancel',
|
|
392
|
+
}}
|
|
393
|
+
/>
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
case 'provider-select':
|
|
397
|
+
return (
|
|
398
|
+
<Select
|
|
399
|
+
message="Select your AI provider:"
|
|
400
|
+
options={PROVIDER_OPTIONS}
|
|
401
|
+
onSelect={handleProviderSelect}
|
|
402
|
+
onCancel={onCancel}
|
|
403
|
+
/>
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
case 'key-input':
|
|
407
|
+
return (
|
|
408
|
+
<PasswordInput
|
|
409
|
+
message={`Enter your ${state.provider ? getApiKeyEnvVar(state.provider) : 'API key'}:`}
|
|
410
|
+
onSubmit={handleApiKeySubmit}
|
|
411
|
+
onCancel={onCancel}
|
|
412
|
+
/>
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
case 'key-save':
|
|
416
|
+
return (
|
|
417
|
+
<Confirm
|
|
418
|
+
message="Save API key to .env.local?"
|
|
419
|
+
onConfirm={handleSaveKeyConfirm}
|
|
420
|
+
onCancel={onCancel}
|
|
421
|
+
initialValue={true}
|
|
422
|
+
/>
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
case 'model-select':
|
|
426
|
+
if (!state.provider) return null;
|
|
427
|
+
return (
|
|
428
|
+
<Select
|
|
429
|
+
message="Select model:"
|
|
430
|
+
options={getModelOptions(state.provider)}
|
|
431
|
+
onSelect={handleModelSelect}
|
|
432
|
+
onCancel={onCancel}
|
|
433
|
+
/>
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
case 'ai-analysis':
|
|
437
|
+
return (
|
|
438
|
+
<Box flexDirection="column">
|
|
439
|
+
<Box marginBottom={1}>
|
|
440
|
+
<Text>
|
|
441
|
+
Running AI analysis with{' '}
|
|
442
|
+
<Text color={colors.blue}>
|
|
443
|
+
{state.provider}/{state.model}
|
|
444
|
+
</Text>
|
|
445
|
+
</Text>
|
|
446
|
+
</Box>
|
|
447
|
+
<WorkingIndicator
|
|
448
|
+
state={{
|
|
449
|
+
isWorking: true,
|
|
450
|
+
status: state.workingStatus || 'Analyzing codebase...',
|
|
451
|
+
hint: 'esc to cancel',
|
|
452
|
+
}}
|
|
453
|
+
/>
|
|
454
|
+
</Box>
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
case 'confirm':
|
|
458
|
+
return (
|
|
459
|
+
<Box flexDirection="column">
|
|
460
|
+
{/* Show AI analysis results if available */}
|
|
461
|
+
{state.enhancedResult?.aiAnalysis && (
|
|
462
|
+
<Box marginBottom={1} flexDirection="column">
|
|
463
|
+
<Text color={colors.green}>AI Analysis Complete</Text>
|
|
464
|
+
{state.tokenUsage && (
|
|
465
|
+
<Text dimColor>
|
|
466
|
+
Tokens: {state.tokenUsage.inputTokens} in / {state.tokenUsage.outputTokens} out
|
|
467
|
+
</Text>
|
|
468
|
+
)}
|
|
469
|
+
</Box>
|
|
470
|
+
)}
|
|
471
|
+
{state.error && (
|
|
472
|
+
<Box marginBottom={1}>
|
|
473
|
+
<Text color={colors.orange}>Warning: {state.error}</Text>
|
|
474
|
+
</Box>
|
|
475
|
+
)}
|
|
476
|
+
<Confirm
|
|
477
|
+
message="Generate Ralph configuration files?"
|
|
478
|
+
onConfirm={handleConfirmGeneration}
|
|
479
|
+
onCancel={onCancel}
|
|
480
|
+
initialValue={true}
|
|
481
|
+
/>
|
|
482
|
+
</Box>
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
case 'generating':
|
|
486
|
+
return (
|
|
487
|
+
<WorkingIndicator
|
|
488
|
+
state={{
|
|
489
|
+
isWorking: true,
|
|
490
|
+
status: state.workingStatus || 'Generating configuration files...',
|
|
491
|
+
hint: 'please wait',
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
case 'complete':
|
|
497
|
+
return (
|
|
498
|
+
<Box flexDirection="column">
|
|
499
|
+
<Text color={colors.green} bold>
|
|
500
|
+
Initialization Complete!
|
|
501
|
+
</Text>
|
|
502
|
+
<Box marginTop={1} flexDirection="column">
|
|
503
|
+
<Text>Generated files in .ralph/:</Text>
|
|
504
|
+
{state.generatedFiles.map((file) => (
|
|
505
|
+
<Text key={file} dimColor>
|
|
506
|
+
{' '}
|
|
507
|
+
{file}
|
|
508
|
+
</Text>
|
|
509
|
+
))}
|
|
510
|
+
</Box>
|
|
511
|
+
<Box marginTop={1}>
|
|
512
|
+
<Text dimColor>Press any key to continue...</Text>
|
|
513
|
+
</Box>
|
|
514
|
+
</Box>
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
case 'error':
|
|
518
|
+
return (
|
|
519
|
+
<Box flexDirection="column">
|
|
520
|
+
<Text color={colors.pink} bold>
|
|
521
|
+
Error
|
|
522
|
+
</Text>
|
|
523
|
+
<Text color={colors.pink}>{state.error}</Text>
|
|
524
|
+
<Box marginTop={1}>
|
|
525
|
+
<Text dimColor>Press Esc to go back</Text>
|
|
526
|
+
</Box>
|
|
527
|
+
</Box>
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
default:
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Show scan result summary when available
|
|
536
|
+
const renderScanSummary = () => {
|
|
537
|
+
if (!state.scanResult || state.phase === 'scanning') return null;
|
|
538
|
+
|
|
539
|
+
const { stack } = state.scanResult;
|
|
540
|
+
return (
|
|
541
|
+
<Box marginBottom={1} flexDirection="column">
|
|
542
|
+
<Text color={colors.yellow} bold>
|
|
543
|
+
Detected Stack
|
|
544
|
+
</Text>
|
|
545
|
+
<Box paddingLeft={2} flexDirection="column">
|
|
546
|
+
{stack.framework && (
|
|
547
|
+
<Text>
|
|
548
|
+
Framework:{' '}
|
|
549
|
+
<Text color={colors.blue}>
|
|
550
|
+
{stack.framework.name}
|
|
551
|
+
{stack.framework.version ? ` ${stack.framework.version}` : ''}
|
|
552
|
+
</Text>
|
|
553
|
+
</Text>
|
|
554
|
+
)}
|
|
555
|
+
<Text>
|
|
556
|
+
Language: <Text color={colors.blue}>TypeScript</Text>
|
|
557
|
+
</Text>
|
|
558
|
+
{stack.testing?.unit && (
|
|
559
|
+
<Text>
|
|
560
|
+
Testing: <Text color={colors.blue}>{stack.testing.unit.name}</Text>
|
|
561
|
+
</Text>
|
|
562
|
+
)}
|
|
563
|
+
<Text>
|
|
564
|
+
Package Manager:{' '}
|
|
565
|
+
<Text color={colors.blue}>{stack.packageManager?.name || 'npm'}</Text>
|
|
566
|
+
</Text>
|
|
567
|
+
</Box>
|
|
568
|
+
</Box>
|
|
569
|
+
);
|
|
570
|
+
};
|
|
43
571
|
|
|
44
572
|
return (
|
|
45
573
|
<Box flexDirection="column" padding={1}>
|
|
574
|
+
{/* Header */}
|
|
46
575
|
<Box marginBottom={1}>
|
|
47
576
|
<Text color={colors.yellow} bold>
|
|
48
|
-
|
|
577
|
+
Initialize Project
|
|
49
578
|
</Text>
|
|
579
|
+
<Text dimColor> │ {projectRoot}</Text>
|
|
50
580
|
</Box>
|
|
51
581
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
582
|
+
{/* Phase progress */}
|
|
583
|
+
<PhaseHeader
|
|
584
|
+
currentPhase={phaseConfig.number}
|
|
585
|
+
totalPhases={INIT_TOTAL_PHASES}
|
|
586
|
+
phaseName={phaseConfig.name}
|
|
587
|
+
/>
|
|
55
588
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
</Box>
|
|
589
|
+
{/* Scan summary */}
|
|
590
|
+
{renderScanSummary()}
|
|
591
|
+
|
|
592
|
+
{/* Phase-specific content */}
|
|
593
|
+
<Box marginTop={1}>{renderPhaseContent()}</Box>
|
|
61
594
|
</Box>
|
|
62
595
|
);
|
|
63
596
|
}
|
|
@@ -69,6 +69,7 @@ export function InterviewScreen({
|
|
|
69
69
|
state,
|
|
70
70
|
initialize,
|
|
71
71
|
addMessage,
|
|
72
|
+
addStreamingMessage,
|
|
72
73
|
updateStreamingMessage,
|
|
73
74
|
completeStreamingMessage,
|
|
74
75
|
startToolCall,
|
|
@@ -124,7 +125,7 @@ export function InterviewScreen({
|
|
|
124
125
|
// Start a new streaming message
|
|
125
126
|
isStreamingRef.current = true;
|
|
126
127
|
streamContentRef.current = chunk;
|
|
127
|
-
|
|
128
|
+
addStreamingMessage(chunk);
|
|
128
129
|
} else {
|
|
129
130
|
// Append to existing streaming content
|
|
130
131
|
streamContentRef.current += chunk;
|
package/src/tui/screens/index.ts
CHANGED
|
@@ -4,3 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
export { InterviewScreen } from './InterviewScreen.js';
|
|
6
6
|
export type { InterviewScreenProps } from './InterviewScreen.js';
|
|
7
|
+
|
|
8
|
+
export { InitScreen } from './InitScreen.js';
|
|
9
|
+
export type { InitScreenProps } from './InitScreen.js';
|
|
10
|
+
|
|
11
|
+
export { WelcomeScreen } from './WelcomeScreen.js';
|
|
12
|
+
export type { WelcomeScreenProps } from './WelcomeScreen.js';
|
|
13
|
+
|
|
14
|
+
export { MainShell } from './MainShell.js';
|
|
15
|
+
export type { MainShellProps, NavigationTarget, NavigationProps } from './MainShell.js';
|