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.
Files changed (78) hide show
  1. package/dist/commands/init.d.ts +8 -19
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +5 -351
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/new.d.ts +21 -13
  6. package/dist/commands/new.d.ts.map +1 -1
  7. package/dist/commands/new.js +10 -267
  8. package/dist/commands/new.js.map +1 -1
  9. package/dist/index.d.ts +1 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +77 -85
  12. package/dist/index.js.map +1 -1
  13. package/dist/repl/index.d.ts +3 -3
  14. package/dist/repl/index.d.ts.map +1 -1
  15. package/dist/repl/index.js +3 -3
  16. package/dist/repl/index.js.map +1 -1
  17. package/dist/tui/app.d.ts +1 -5
  18. package/dist/tui/app.d.ts.map +1 -1
  19. package/dist/tui/app.js +7 -12
  20. package/dist/tui/app.js.map +1 -1
  21. package/dist/tui/components/Confirm.d.ts +39 -0
  22. package/dist/tui/components/Confirm.d.ts.map +1 -0
  23. package/dist/tui/components/Confirm.js +60 -0
  24. package/dist/tui/components/Confirm.js.map +1 -0
  25. package/dist/tui/components/PasswordInput.d.ts +39 -0
  26. package/dist/tui/components/PasswordInput.d.ts.map +1 -0
  27. package/dist/tui/components/PasswordInput.js +56 -0
  28. package/dist/tui/components/PasswordInput.js.map +1 -0
  29. package/dist/tui/components/Select.d.ts +55 -0
  30. package/dist/tui/components/Select.d.ts.map +1 -0
  31. package/dist/tui/components/Select.js +63 -0
  32. package/dist/tui/components/Select.js.map +1 -0
  33. package/dist/tui/components/index.d.ts +6 -0
  34. package/dist/tui/components/index.d.ts.map +1 -1
  35. package/dist/tui/components/index.js +3 -0
  36. package/dist/tui/components/index.js.map +1 -1
  37. package/dist/tui/hooks/index.d.ts +3 -0
  38. package/dist/tui/hooks/index.d.ts.map +1 -1
  39. package/dist/tui/hooks/index.js +2 -0
  40. package/dist/tui/hooks/index.js.map +1 -1
  41. package/dist/tui/hooks/useInit.d.ts +130 -0
  42. package/dist/tui/hooks/useInit.d.ts.map +1 -0
  43. package/dist/tui/hooks/useInit.js +326 -0
  44. package/dist/tui/hooks/useInit.js.map +1 -0
  45. package/dist/tui/hooks/useSpecGenerator.d.ts +4 -0
  46. package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
  47. package/dist/tui/hooks/useSpecGenerator.js +12 -1
  48. package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
  49. package/dist/tui/screens/InitScreen.d.ts +17 -10
  50. package/dist/tui/screens/InitScreen.d.ts.map +1 -1
  51. package/dist/tui/screens/InitScreen.js +317 -18
  52. package/dist/tui/screens/InitScreen.js.map +1 -1
  53. package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
  54. package/dist/tui/screens/InterviewScreen.js +2 -2
  55. package/dist/tui/screens/InterviewScreen.js.map +1 -1
  56. package/dist/tui/screens/index.d.ts +6 -0
  57. package/dist/tui/screens/index.d.ts.map +1 -1
  58. package/dist/tui/screens/index.js +3 -0
  59. package/dist/tui/screens/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/commands/init.ts +8 -432
  62. package/src/commands/new.ts +13 -319
  63. package/src/index.ts +85 -93
  64. package/src/repl/index.ts +3 -3
  65. package/src/tui/app.tsx +7 -15
  66. package/src/tui/components/Confirm.tsx +109 -0
  67. package/src/tui/components/PasswordInput.tsx +106 -0
  68. package/src/tui/components/Select.tsx +132 -0
  69. package/src/tui/components/index.ts +9 -0
  70. package/src/tui/hooks/index.ts +9 -0
  71. package/src/tui/hooks/useInit.ts +472 -0
  72. package/src/tui/hooks/useSpecGenerator.ts +17 -1
  73. package/src/tui/screens/InitScreen.tsx +562 -29
  74. package/src/tui/screens/InterviewScreen.tsx +2 -1
  75. package/src/tui/screens/index.ts +9 -0
  76. package/src/cli.ts +0 -274
  77. package/src/repl/repl-loop.ts +0 -389
  78. package/src/utils/repl-prompts.ts +0 -381
@@ -1,63 +1,596 @@
1
1
  /**
2
- * InitScreen - Screen for the /init command workflow
2
+ * InitScreen - Full Ink-based init workflow
3
3
  *
4
- * Handles project initialization within the TUI context.
5
- * Since the init workflow uses readline-based prompts, this screen
6
- * signals that init should run outside of Ink.
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
- /** Called to trigger the init workflow (runs outside Ink) */
18
- onRunInit: () => void;
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
- * Displays a message and triggers the init workflow.
27
- * The actual init workflow runs outside of Ink because it uses
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
- onRunInit,
32
- onCancel: _onCancel,
108
+ projectRoot,
109
+ sessionState,
110
+ onComplete,
111
+ onCancel,
33
112
  }: InitScreenProps): React.ReactElement {
34
- // Trigger init workflow on mount
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
- // Small delay to allow the screen to render before unmounting
37
- const timer = setTimeout(() => {
38
- onRunInit();
39
- }, 100);
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
- return () => clearTimeout(timer);
42
- }, [onRunInit]);
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
- Initializing Project
577
+ Initialize Project
49
578
  </Text>
579
+ <Text dimColor> │ {projectRoot}</Text>
50
580
  </Box>
51
581
 
52
- <Box marginBottom={1}>
53
- <Text>Starting initialization workflow...</Text>
54
- </Box>
582
+ {/* Phase progress */}
583
+ <PhaseHeader
584
+ currentPhase={phaseConfig.number}
585
+ totalPhases={INIT_TOTAL_PHASES}
586
+ phaseName={phaseConfig.name}
587
+ />
55
588
 
56
- <Box>
57
- <Text dimColor>
58
- Press Ctrl+C to cancel
59
- </Text>
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
- addMessage('assistant', chunk);
128
+ addStreamingMessage(chunk);
128
129
  } else {
129
130
  // Append to existing streaming content
130
131
  streamContentRef.current += chunk;
@@ -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';