wiggum-cli 0.8.0 → 0.9.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/dist/ai/conversation/conversation-manager.d.ts +11 -0
- package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
- package/dist/ai/conversation/conversation-manager.js +14 -0
- package/dist/ai/conversation/conversation-manager.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +63 -22
- package/dist/commands/new.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -22
- package/dist/index.js.map +1 -1
- package/dist/tui/app.d.ts +46 -36
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +136 -37
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/WiggumBanner.d.ts +30 -0
- package/dist/tui/components/WiggumBanner.d.ts.map +1 -0
- package/dist/tui/components/WiggumBanner.js +34 -0
- package/dist/tui/components/WiggumBanner.js.map +1 -0
- package/dist/tui/demo.d.ts +8 -0
- package/dist/tui/demo.d.ts.map +1 -0
- package/dist/tui/demo.js +69 -0
- package/dist/tui/demo.js.map +1 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts +16 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
- package/dist/tui/hooks/useSpecGenerator.js +47 -0
- package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
- package/dist/tui/orchestration/index.d.ts +6 -0
- package/dist/tui/orchestration/index.d.ts.map +1 -0
- package/dist/tui/orchestration/index.js +6 -0
- package/dist/tui/orchestration/index.js.map +1 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts +136 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts.map +1 -0
- package/dist/tui/orchestration/interview-orchestrator.js +437 -0
- package/dist/tui/orchestration/interview-orchestrator.js.map +1 -0
- package/dist/tui/screens/InitScreen.d.ts +26 -0
- package/dist/tui/screens/InitScreen.d.ts.map +1 -0
- package/dist/tui/screens/InitScreen.js +30 -0
- package/dist/tui/screens/InitScreen.js.map +1 -0
- package/dist/tui/screens/InterviewScreen.d.ts +2 -13
- package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
- package/dist/tui/screens/InterviewScreen.js +162 -34
- package/dist/tui/screens/InterviewScreen.js.map +1 -1
- package/dist/tui/screens/MainShell.d.ts +46 -0
- package/dist/tui/screens/MainShell.d.ts.map +1 -0
- package/dist/tui/screens/MainShell.js +196 -0
- package/dist/tui/screens/MainShell.js.map +1 -0
- package/dist/tui/screens/WelcomeScreen.d.ts +45 -0
- package/dist/tui/screens/WelcomeScreen.d.ts.map +1 -0
- package/dist/tui/screens/WelcomeScreen.js +56 -0
- package/dist/tui/screens/WelcomeScreen.js.map +1 -0
- package/dist/tui/theme.d.ts +4 -0
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +4 -0
- package/dist/tui/theme.js.map +1 -1
- package/dist/utils/repl-prompts.d.ts +1 -1
- package/dist/utils/repl-prompts.d.ts.map +1 -1
- package/dist/utils/repl-prompts.js +77 -22
- package/dist/utils/repl-prompts.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/conversation/conversation-manager.ts +22 -0
- package/src/cli.ts +4 -0
- package/src/commands/new.ts +79 -27
- package/src/index.ts +109 -27
- package/src/tui/app.tsx +222 -63
- package/src/tui/components/WiggumBanner.tsx +66 -0
- package/src/tui/demo.tsx +111 -0
- package/src/tui/hooks/useSpecGenerator.ts +73 -0
- package/src/tui/orchestration/index.ts +10 -0
- package/src/tui/orchestration/interview-orchestrator.ts +559 -0
- package/src/tui/screens/InitScreen.tsx +63 -0
- package/src/tui/screens/InterviewScreen.tsx +201 -46
- package/src/tui/screens/MainShell.tsx +290 -0
- package/src/tui/screens/WelcomeScreen.tsx +141 -0
- package/src/tui/theme.ts +4 -0
- package/src/utils/repl-prompts.ts +83 -25
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InitScreen - Screen for the /init command workflow
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useEffect } from 'react';
|
|
10
|
+
import { Box, Text } from 'ink';
|
|
11
|
+
import { colors } from '../theme.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props for the InitScreen component
|
|
15
|
+
*/
|
|
16
|
+
export interface InitScreenProps {
|
|
17
|
+
/** Called to trigger the init workflow (runs outside Ink) */
|
|
18
|
+
onRunInit: () => void;
|
|
19
|
+
/** Called when user cancels */
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* InitScreen component
|
|
25
|
+
*
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
export function InitScreen({
|
|
31
|
+
onRunInit,
|
|
32
|
+
onCancel: _onCancel,
|
|
33
|
+
}: InitScreenProps): React.ReactElement {
|
|
34
|
+
// Trigger init workflow on mount
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// Small delay to allow the screen to render before unmounting
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
onRunInit();
|
|
39
|
+
}, 100);
|
|
40
|
+
|
|
41
|
+
return () => clearTimeout(timer);
|
|
42
|
+
}, [onRunInit]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Box flexDirection="column" padding={1}>
|
|
46
|
+
<Box marginBottom={1}>
|
|
47
|
+
<Text color={colors.yellow} bold>
|
|
48
|
+
Initializing Project
|
|
49
|
+
</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
|
|
52
|
+
<Box marginBottom={1}>
|
|
53
|
+
<Text>Starting initialization workflow...</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
|
|
56
|
+
<Box>
|
|
57
|
+
<Text dimColor>
|
|
58
|
+
Press Ctrl+C to cancel
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 4. Generation - Generate the specification
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import React, { useEffect, useCallback } from 'react';
|
|
13
|
-
import { Box, useInput } from 'ink';
|
|
12
|
+
import React, { useEffect, useCallback, useRef } from 'react';
|
|
13
|
+
import { Box, Text, useInput } from 'ink';
|
|
14
14
|
import type { AIProvider } from '../../ai/providers.js';
|
|
15
15
|
import type { ScanResult } from '../../scanner/types.js';
|
|
16
16
|
import { PhaseHeader } from '../components/PhaseHeader.js';
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
useSpecGenerator,
|
|
22
22
|
PHASE_CONFIGS,
|
|
23
23
|
TOTAL_DISPLAY_PHASES,
|
|
24
|
+
type GeneratorPhase,
|
|
24
25
|
} from '../hooks/useSpecGenerator.js';
|
|
26
|
+
import { InterviewOrchestrator } from '../orchestration/interview-orchestrator.js';
|
|
27
|
+
import { colors } from '../theme.js';
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* Props for the InterviewScreen component
|
|
@@ -50,19 +53,8 @@ export interface InterviewScreenProps {
|
|
|
50
53
|
* components (PhaseHeader, MessageList, WorkingIndicator, ChatInput) to
|
|
51
54
|
* create the complete interview experience.
|
|
52
55
|
*
|
|
53
|
-
* Uses the useSpecGenerator hook
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```tsx
|
|
57
|
-
* <InterviewScreen
|
|
58
|
-
* featureName="user-auth"
|
|
59
|
-
* projectRoot="/path/to/project"
|
|
60
|
-
* provider="anthropic"
|
|
61
|
-
* model="claude-sonnet-4-5-20250514"
|
|
62
|
-
* onComplete={(spec) => writeSpec(spec)}
|
|
63
|
-
* onCancel={() => process.exit(0)}
|
|
64
|
-
* />
|
|
65
|
-
* ```
|
|
56
|
+
* Uses the useSpecGenerator hook for state and InterviewOrchestrator
|
|
57
|
+
* to bridge to the AI conversation.
|
|
66
58
|
*/
|
|
67
59
|
export function InterviewScreen({
|
|
68
60
|
featureName,
|
|
@@ -75,33 +67,167 @@ export function InterviewScreen({
|
|
|
75
67
|
}: InterviewScreenProps): React.ReactElement {
|
|
76
68
|
const {
|
|
77
69
|
state,
|
|
78
|
-
submitAnswer,
|
|
79
70
|
initialize,
|
|
71
|
+
addMessage,
|
|
72
|
+
updateStreamingMessage,
|
|
73
|
+
completeStreamingMessage,
|
|
74
|
+
startToolCall,
|
|
75
|
+
completeToolCall,
|
|
76
|
+
setPhase,
|
|
77
|
+
setGeneratedSpec,
|
|
78
|
+
setError,
|
|
79
|
+
setWorking,
|
|
80
|
+
setReady,
|
|
80
81
|
} = useSpecGenerator();
|
|
81
82
|
|
|
82
|
-
//
|
|
83
|
+
// Track orchestrator instance
|
|
84
|
+
const orchestratorRef = useRef<InterviewOrchestrator | null>(null);
|
|
85
|
+
|
|
86
|
+
// Track if we're in streaming mode for the current message
|
|
87
|
+
const isStreamingRef = useRef(false);
|
|
88
|
+
const streamContentRef = useRef('');
|
|
89
|
+
|
|
90
|
+
// Track if component is unmounted to prevent callbacks after cleanup
|
|
91
|
+
const isCancelledRef = useRef(false);
|
|
92
|
+
|
|
93
|
+
// Use refs for callbacks to avoid stale closures and unnecessary effect re-runs
|
|
94
|
+
const onCompleteRef = useRef(onComplete);
|
|
95
|
+
onCompleteRef.current = onComplete;
|
|
96
|
+
|
|
97
|
+
// Initialize the orchestrator when the component mounts
|
|
83
98
|
useEffect(() => {
|
|
99
|
+
// Reset cancelled flag on mount
|
|
100
|
+
isCancelledRef.current = false;
|
|
101
|
+
|
|
102
|
+
// Initialize hook state
|
|
84
103
|
initialize({
|
|
85
104
|
featureName,
|
|
86
105
|
projectRoot,
|
|
87
106
|
provider,
|
|
88
107
|
model,
|
|
89
108
|
});
|
|
90
|
-
}, [featureName, projectRoot, provider, model, initialize]);
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
// Create orchestrator with callbacks that check for cancellation
|
|
111
|
+
const orchestrator = new InterviewOrchestrator({
|
|
112
|
+
featureName,
|
|
113
|
+
projectRoot,
|
|
114
|
+
provider,
|
|
115
|
+
model,
|
|
116
|
+
scanResult,
|
|
117
|
+
onMessage: (role, content) => {
|
|
118
|
+
if (isCancelledRef.current) return;
|
|
119
|
+
addMessage(role, content);
|
|
120
|
+
},
|
|
121
|
+
onStreamChunk: (chunk) => {
|
|
122
|
+
if (isCancelledRef.current) return;
|
|
123
|
+
if (!isStreamingRef.current) {
|
|
124
|
+
// Start a new streaming message
|
|
125
|
+
isStreamingRef.current = true;
|
|
126
|
+
streamContentRef.current = chunk;
|
|
127
|
+
addMessage('assistant', chunk);
|
|
128
|
+
} else {
|
|
129
|
+
// Append to existing streaming content
|
|
130
|
+
streamContentRef.current += chunk;
|
|
131
|
+
updateStreamingMessage(streamContentRef.current);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
onStreamComplete: () => {
|
|
135
|
+
if (isCancelledRef.current) return;
|
|
136
|
+
if (isStreamingRef.current) {
|
|
137
|
+
completeStreamingMessage();
|
|
138
|
+
isStreamingRef.current = false;
|
|
139
|
+
streamContentRef.current = '';
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
onToolStart: (toolName, input) => {
|
|
143
|
+
if (isCancelledRef.current) return '';
|
|
144
|
+
return startToolCall(toolName, input);
|
|
145
|
+
},
|
|
146
|
+
onToolEnd: (toolId, output, error) => {
|
|
147
|
+
if (isCancelledRef.current) return;
|
|
148
|
+
completeToolCall(toolId, output, error);
|
|
149
|
+
},
|
|
150
|
+
onPhaseChange: (phase: GeneratorPhase) => {
|
|
151
|
+
if (isCancelledRef.current) return;
|
|
152
|
+
setPhase(phase);
|
|
153
|
+
},
|
|
154
|
+
onComplete: (spec) => {
|
|
155
|
+
if (isCancelledRef.current) return;
|
|
156
|
+
setGeneratedSpec(spec);
|
|
157
|
+
// Use ref to avoid stale closure
|
|
158
|
+
onCompleteRef.current(spec);
|
|
159
|
+
},
|
|
160
|
+
onError: (error) => {
|
|
161
|
+
if (isCancelledRef.current) return;
|
|
162
|
+
setError(error);
|
|
163
|
+
},
|
|
164
|
+
onWorkingChange: (isWorking, status) => {
|
|
165
|
+
if (isCancelledRef.current) return;
|
|
166
|
+
setWorking(isWorking, status);
|
|
167
|
+
},
|
|
168
|
+
onReady: () => {
|
|
169
|
+
if (isCancelledRef.current) return;
|
|
170
|
+
setReady();
|
|
171
|
+
},
|
|
172
|
+
});
|
|
98
173
|
|
|
99
|
-
|
|
174
|
+
orchestratorRef.current = orchestrator;
|
|
175
|
+
|
|
176
|
+
// Start the orchestrator
|
|
177
|
+
orchestrator.start();
|
|
178
|
+
|
|
179
|
+
// Cleanup: mark as cancelled to prevent callbacks after unmount
|
|
180
|
+
return () => {
|
|
181
|
+
isCancelledRef.current = true;
|
|
182
|
+
orchestratorRef.current = null;
|
|
183
|
+
};
|
|
184
|
+
}, [featureName, projectRoot, provider, model, scanResult]);
|
|
185
|
+
|
|
186
|
+
// Handle user input submission based on current phase
|
|
100
187
|
const handleSubmit = useCallback(
|
|
101
188
|
async (value: string) => {
|
|
102
|
-
|
|
189
|
+
const orchestrator = orchestratorRef.current;
|
|
190
|
+
if (!orchestrator) return;
|
|
191
|
+
|
|
192
|
+
// Add user message to display
|
|
193
|
+
if (value) {
|
|
194
|
+
addMessage('user', value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const currentPhase = orchestrator.getPhase();
|
|
198
|
+
|
|
199
|
+
switch (currentPhase) {
|
|
200
|
+
case 'context':
|
|
201
|
+
if (value) {
|
|
202
|
+
// User entered a reference URL/path
|
|
203
|
+
await orchestrator.addReference(value);
|
|
204
|
+
} else {
|
|
205
|
+
// Empty input = done with context, advance to goals
|
|
206
|
+
await orchestrator.advanceToGoals();
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case 'goals':
|
|
211
|
+
// User entered their goals
|
|
212
|
+
await orchestrator.submitGoals(value);
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case 'interview':
|
|
216
|
+
if (value.toLowerCase() === 'done' || value.toLowerCase() === 'skip') {
|
|
217
|
+
// Skip to generation
|
|
218
|
+
await orchestrator.skipToGeneration();
|
|
219
|
+
} else {
|
|
220
|
+
// Submit answer
|
|
221
|
+
await orchestrator.submitAnswer(value);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
default:
|
|
226
|
+
// In generation or complete phase, ignore input
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
103
229
|
},
|
|
104
|
-
[
|
|
230
|
+
[addMessage]
|
|
105
231
|
);
|
|
106
232
|
|
|
107
233
|
// Handle keyboard input for Escape key
|
|
@@ -115,8 +241,7 @@ export function InterviewScreen({
|
|
|
115
241
|
const phaseConfig = PHASE_CONFIGS[state.phase];
|
|
116
242
|
|
|
117
243
|
// Determine if input should be disabled
|
|
118
|
-
|
|
119
|
-
const inputDisabled = !state.awaitingInput || state.isWorking;
|
|
244
|
+
const inputDisabled = !state.awaitingInput || state.isWorking || state.phase === 'complete';
|
|
120
245
|
|
|
121
246
|
// Build the working indicator state
|
|
122
247
|
const workingState = {
|
|
@@ -125,6 +250,24 @@ export function InterviewScreen({
|
|
|
125
250
|
hint: 'esc to cancel',
|
|
126
251
|
};
|
|
127
252
|
|
|
253
|
+
// Get placeholder text based on phase
|
|
254
|
+
const getPlaceholder = () => {
|
|
255
|
+
switch (state.phase) {
|
|
256
|
+
case 'context':
|
|
257
|
+
return 'Enter URL or file path, or press Enter to continue...';
|
|
258
|
+
case 'goals':
|
|
259
|
+
return 'Describe what you want to build...';
|
|
260
|
+
case 'interview':
|
|
261
|
+
return 'Type your response (or "done" to generate spec)...';
|
|
262
|
+
case 'generation':
|
|
263
|
+
return 'Generating specification...';
|
|
264
|
+
case 'complete':
|
|
265
|
+
return 'Specification complete!';
|
|
266
|
+
default:
|
|
267
|
+
return 'Type your response...';
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
128
271
|
return (
|
|
129
272
|
<Box flexDirection="column" padding={1}>
|
|
130
273
|
{/* Phase header showing current progress */}
|
|
@@ -134,31 +277,43 @@ export function InterviewScreen({
|
|
|
134
277
|
phaseName={phaseConfig.name}
|
|
135
278
|
/>
|
|
136
279
|
|
|
280
|
+
{/* Error display */}
|
|
281
|
+
{state.error && (
|
|
282
|
+
<Box marginY={1}>
|
|
283
|
+
<Text color="red">Error: {state.error}</Text>
|
|
284
|
+
</Box>
|
|
285
|
+
)}
|
|
286
|
+
|
|
137
287
|
{/* Conversation history */}
|
|
138
288
|
<Box marginY={1}>
|
|
139
289
|
<MessageList messages={state.messages} />
|
|
140
290
|
</Box>
|
|
141
291
|
|
|
142
292
|
{/* Working indicator when AI is processing */}
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
293
|
+
{state.isWorking && (
|
|
294
|
+
<Box marginY={1}>
|
|
295
|
+
<WorkingIndicator state={workingState} />
|
|
296
|
+
</Box>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{/* Completion message */}
|
|
300
|
+
{state.phase === 'complete' && (
|
|
301
|
+
<Box marginY={1}>
|
|
302
|
+
<Text color={colors.green}>Specification generated successfully!</Text>
|
|
303
|
+
</Box>
|
|
304
|
+
)}
|
|
146
305
|
|
|
147
306
|
{/* User input area */}
|
|
148
|
-
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
: 'Type your response...'
|
|
159
|
-
}
|
|
160
|
-
/>
|
|
161
|
-
</Box>
|
|
307
|
+
{state.phase !== 'complete' && (
|
|
308
|
+
<Box marginTop={1}>
|
|
309
|
+
<ChatInput
|
|
310
|
+
onSubmit={handleSubmit}
|
|
311
|
+
disabled={inputDisabled}
|
|
312
|
+
allowEmpty={state.phase === 'context'}
|
|
313
|
+
placeholder={getPlaceholder()}
|
|
314
|
+
/>
|
|
315
|
+
</Box>
|
|
316
|
+
)}
|
|
162
317
|
</Box>
|
|
163
318
|
);
|
|
164
319
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MainShell - Ink-based REPL replacement
|
|
3
|
+
*
|
|
4
|
+
* The main interactive shell for Wiggum CLI, replacing the readline REPL.
|
|
5
|
+
* Handles slash commands and provides navigation to other screens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback } from 'react';
|
|
9
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
10
|
+
import { MessageList, type Message } from '../components/MessageList.js';
|
|
11
|
+
import { ChatInput } from '../components/ChatInput.js';
|
|
12
|
+
import { colors } from '../theme.js';
|
|
13
|
+
import {
|
|
14
|
+
parseInput,
|
|
15
|
+
resolveCommandAlias,
|
|
16
|
+
formatHelpText,
|
|
17
|
+
type ReplCommandName,
|
|
18
|
+
} from '../../repl/command-parser.js';
|
|
19
|
+
import type { SessionState } from '../../repl/session-state.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Navigation targets for the shell
|
|
23
|
+
*/
|
|
24
|
+
export type NavigationTarget = 'welcome' | 'shell' | 'interview' | 'init';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Navigation props passed to target screens
|
|
28
|
+
*/
|
|
29
|
+
export interface NavigationProps {
|
|
30
|
+
featureName?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Props for MainShell component
|
|
36
|
+
*/
|
|
37
|
+
export interface MainShellProps {
|
|
38
|
+
/** Current session state */
|
|
39
|
+
sessionState: SessionState;
|
|
40
|
+
/** Called when navigating to another screen */
|
|
41
|
+
onNavigate: (target: NavigationTarget, props?: NavigationProps) => void;
|
|
42
|
+
/** Called when session state changes */
|
|
43
|
+
onSessionStateChange?: (state: SessionState) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a unique ID for messages
|
|
48
|
+
*/
|
|
49
|
+
function generateId(): string {
|
|
50
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* MainShell component
|
|
55
|
+
*
|
|
56
|
+
* The main interactive shell that handles slash commands and navigation.
|
|
57
|
+
* Replaces the readline-based REPL with an Ink-powered TUI.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* <MainShell
|
|
62
|
+
* sessionState={state}
|
|
63
|
+
* onNavigate={(target, props) => setScreen(target, props)}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function MainShell({
|
|
68
|
+
sessionState,
|
|
69
|
+
onNavigate,
|
|
70
|
+
}: MainShellProps): React.ReactElement {
|
|
71
|
+
const { exit } = useApp();
|
|
72
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Add a system message to the conversation
|
|
76
|
+
*/
|
|
77
|
+
const addSystemMessage = useCallback((content: string) => {
|
|
78
|
+
const message: Message = {
|
|
79
|
+
id: generateId(),
|
|
80
|
+
role: 'system',
|
|
81
|
+
content,
|
|
82
|
+
};
|
|
83
|
+
setMessages((prev) => [...prev, message]);
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle /help command
|
|
88
|
+
*/
|
|
89
|
+
const handleHelp = useCallback(() => {
|
|
90
|
+
addSystemMessage(formatHelpText());
|
|
91
|
+
}, [addSystemMessage]);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Handle /init command
|
|
95
|
+
*/
|
|
96
|
+
const handleInit = useCallback(() => {
|
|
97
|
+
// Navigate to init screen
|
|
98
|
+
onNavigate('init');
|
|
99
|
+
}, [onNavigate]);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle /new command
|
|
103
|
+
*/
|
|
104
|
+
const handleNew = useCallback((args: string[]) => {
|
|
105
|
+
if (args.length === 0) {
|
|
106
|
+
addSystemMessage('Feature name required. Usage: /new <feature-name>');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!sessionState.initialized) {
|
|
111
|
+
addSystemMessage('Project not initialized. Run /init first.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const featureName = args[0];
|
|
116
|
+
onNavigate('interview', { featureName });
|
|
117
|
+
}, [sessionState.initialized, onNavigate, addSystemMessage]);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle /run command
|
|
121
|
+
*/
|
|
122
|
+
const handleRun = useCallback((args: string[]) => {
|
|
123
|
+
if (args.length === 0) {
|
|
124
|
+
addSystemMessage('Feature name required. Usage: /run <feature-name>');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!sessionState.initialized) {
|
|
129
|
+
addSystemMessage('Project not initialized. Run /init first.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// TODO: Implement run screen navigation
|
|
134
|
+
addSystemMessage(`Run command for "${args[0]}" - not yet implemented in TUI mode.`);
|
|
135
|
+
}, [sessionState.initialized, addSystemMessage]);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle /monitor command
|
|
139
|
+
*/
|
|
140
|
+
const handleMonitor = useCallback((args: string[]) => {
|
|
141
|
+
if (args.length === 0) {
|
|
142
|
+
addSystemMessage('Feature name required. Usage: /monitor <feature-name>');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// TODO: Implement monitor screen navigation
|
|
147
|
+
addSystemMessage(`Monitor command for "${args[0]}" - not yet implemented in TUI mode.`);
|
|
148
|
+
}, [addSystemMessage]);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle /config command
|
|
152
|
+
*/
|
|
153
|
+
const handleConfig = useCallback((args: string[]) => {
|
|
154
|
+
// TODO: Implement config screen or inline config
|
|
155
|
+
if (args.length === 0) {
|
|
156
|
+
addSystemMessage('Config management - not yet implemented in TUI mode. Use CLI: wiggum config');
|
|
157
|
+
} else {
|
|
158
|
+
addSystemMessage(`Config: ${args.join(' ')} - not yet implemented in TUI mode.`);
|
|
159
|
+
}
|
|
160
|
+
}, [addSystemMessage]);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle /exit command
|
|
164
|
+
*/
|
|
165
|
+
const handleExit = useCallback(() => {
|
|
166
|
+
addSystemMessage('Goodbye!');
|
|
167
|
+
// Small delay to show message before exit
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
exit();
|
|
170
|
+
}, 100);
|
|
171
|
+
}, [addSystemMessage, exit]);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Execute a slash command
|
|
175
|
+
*/
|
|
176
|
+
const executeCommand = useCallback((commandName: ReplCommandName, args: string[]) => {
|
|
177
|
+
switch (commandName) {
|
|
178
|
+
case 'help':
|
|
179
|
+
handleHelp();
|
|
180
|
+
break;
|
|
181
|
+
case 'init':
|
|
182
|
+
handleInit();
|
|
183
|
+
break;
|
|
184
|
+
case 'new':
|
|
185
|
+
handleNew(args);
|
|
186
|
+
break;
|
|
187
|
+
case 'run':
|
|
188
|
+
handleRun(args);
|
|
189
|
+
break;
|
|
190
|
+
case 'monitor':
|
|
191
|
+
handleMonitor(args);
|
|
192
|
+
break;
|
|
193
|
+
case 'config':
|
|
194
|
+
handleConfig(args);
|
|
195
|
+
break;
|
|
196
|
+
case 'exit':
|
|
197
|
+
handleExit();
|
|
198
|
+
break;
|
|
199
|
+
default:
|
|
200
|
+
addSystemMessage(`Unknown command: ${commandName}`);
|
|
201
|
+
}
|
|
202
|
+
}, [handleHelp, handleInit, handleNew, handleRun, handleMonitor, handleConfig, handleExit, addSystemMessage]);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handle natural language input
|
|
206
|
+
*/
|
|
207
|
+
const handleNaturalLanguage = useCallback((_text: string) => {
|
|
208
|
+
// For now, just show a tip (text parameter reserved for future AI chat)
|
|
209
|
+
addSystemMessage('Tip: Use /help to see available commands, or /new <feature> to create a spec.');
|
|
210
|
+
}, [addSystemMessage]);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handle user input submission
|
|
214
|
+
*/
|
|
215
|
+
const handleSubmit = useCallback((value: string) => {
|
|
216
|
+
const parsed = parseInput(value);
|
|
217
|
+
|
|
218
|
+
switch (parsed.type) {
|
|
219
|
+
case 'empty':
|
|
220
|
+
// Ignore empty input
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case 'slash-command': {
|
|
224
|
+
const { command } = parsed;
|
|
225
|
+
if (!command) break;
|
|
226
|
+
|
|
227
|
+
const resolvedName = resolveCommandAlias(command.name);
|
|
228
|
+
if (!resolvedName) {
|
|
229
|
+
addSystemMessage(`Unknown command: /${command.name}. Type /help for available commands.`);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
executeCommand(resolvedName, command.args);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case 'natural-language': {
|
|
238
|
+
handleNaturalLanguage(parsed.text!);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}, [executeCommand, handleNaturalLanguage, addSystemMessage]);
|
|
243
|
+
|
|
244
|
+
// Handle Ctrl+C
|
|
245
|
+
useInput((input, key) => {
|
|
246
|
+
if (key.ctrl && input === 'c') {
|
|
247
|
+
addSystemMessage('Use /exit to quit');
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<Box flexDirection="column" padding={1}>
|
|
253
|
+
{/* Header */}
|
|
254
|
+
<Box marginBottom={1}>
|
|
255
|
+
<Text color={colors.yellow} bold>Wiggum Interactive Mode</Text>
|
|
256
|
+
<Text dimColor> │ </Text>
|
|
257
|
+
{sessionState.initialized ? (
|
|
258
|
+
<Text color={colors.green}>Ready</Text>
|
|
259
|
+
) : (
|
|
260
|
+
<Text color={colors.orange}>Not initialized - run /init</Text>
|
|
261
|
+
)}
|
|
262
|
+
</Box>
|
|
263
|
+
|
|
264
|
+
{/* Status bar */}
|
|
265
|
+
<Box marginBottom={1}>
|
|
266
|
+
<Text dimColor>
|
|
267
|
+
{sessionState.provider ? `${sessionState.provider}/${sessionState.model}` : 'No provider configured'}
|
|
268
|
+
</Text>
|
|
269
|
+
<Text dimColor> │ Type /help for commands</Text>
|
|
270
|
+
</Box>
|
|
271
|
+
|
|
272
|
+
{/* Message history */}
|
|
273
|
+
{messages.length > 0 && (
|
|
274
|
+
<Box marginY={1} flexDirection="column">
|
|
275
|
+
<MessageList messages={messages} />
|
|
276
|
+
</Box>
|
|
277
|
+
)}
|
|
278
|
+
|
|
279
|
+
{/* Input */}
|
|
280
|
+
<Box marginTop={1}>
|
|
281
|
+
<ChatInput
|
|
282
|
+
onSubmit={handleSubmit}
|
|
283
|
+
disabled={false}
|
|
284
|
+
placeholder="Enter command or type /help..."
|
|
285
|
+
prompt="wiggum> "
|
|
286
|
+
/>
|
|
287
|
+
</Box>
|
|
288
|
+
</Box>
|
|
289
|
+
);
|
|
290
|
+
}
|