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.
Files changed (80) hide show
  1. package/dist/ai/conversation/conversation-manager.d.ts +11 -0
  2. package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
  3. package/dist/ai/conversation/conversation-manager.js +14 -0
  4. package/dist/ai/conversation/conversation-manager.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/new.d.ts +2 -0
  9. package/dist/commands/new.d.ts.map +1 -1
  10. package/dist/commands/new.js +63 -22
  11. package/dist/commands/new.js.map +1 -1
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +97 -22
  15. package/dist/index.js.map +1 -1
  16. package/dist/tui/app.d.ts +46 -36
  17. package/dist/tui/app.d.ts.map +1 -1
  18. package/dist/tui/app.js +136 -37
  19. package/dist/tui/app.js.map +1 -1
  20. package/dist/tui/components/WiggumBanner.d.ts +30 -0
  21. package/dist/tui/components/WiggumBanner.d.ts.map +1 -0
  22. package/dist/tui/components/WiggumBanner.js +34 -0
  23. package/dist/tui/components/WiggumBanner.js.map +1 -0
  24. package/dist/tui/demo.d.ts +8 -0
  25. package/dist/tui/demo.d.ts.map +1 -0
  26. package/dist/tui/demo.js +69 -0
  27. package/dist/tui/demo.js.map +1 -0
  28. package/dist/tui/hooks/useSpecGenerator.d.ts +16 -0
  29. package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
  30. package/dist/tui/hooks/useSpecGenerator.js +47 -0
  31. package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
  32. package/dist/tui/orchestration/index.d.ts +6 -0
  33. package/dist/tui/orchestration/index.d.ts.map +1 -0
  34. package/dist/tui/orchestration/index.js +6 -0
  35. package/dist/tui/orchestration/index.js.map +1 -0
  36. package/dist/tui/orchestration/interview-orchestrator.d.ts +136 -0
  37. package/dist/tui/orchestration/interview-orchestrator.d.ts.map +1 -0
  38. package/dist/tui/orchestration/interview-orchestrator.js +437 -0
  39. package/dist/tui/orchestration/interview-orchestrator.js.map +1 -0
  40. package/dist/tui/screens/InitScreen.d.ts +26 -0
  41. package/dist/tui/screens/InitScreen.d.ts.map +1 -0
  42. package/dist/tui/screens/InitScreen.js +30 -0
  43. package/dist/tui/screens/InitScreen.js.map +1 -0
  44. package/dist/tui/screens/InterviewScreen.d.ts +2 -13
  45. package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
  46. package/dist/tui/screens/InterviewScreen.js +162 -34
  47. package/dist/tui/screens/InterviewScreen.js.map +1 -1
  48. package/dist/tui/screens/MainShell.d.ts +46 -0
  49. package/dist/tui/screens/MainShell.d.ts.map +1 -0
  50. package/dist/tui/screens/MainShell.js +196 -0
  51. package/dist/tui/screens/MainShell.js.map +1 -0
  52. package/dist/tui/screens/WelcomeScreen.d.ts +45 -0
  53. package/dist/tui/screens/WelcomeScreen.d.ts.map +1 -0
  54. package/dist/tui/screens/WelcomeScreen.js +56 -0
  55. package/dist/tui/screens/WelcomeScreen.js.map +1 -0
  56. package/dist/tui/theme.d.ts +4 -0
  57. package/dist/tui/theme.d.ts.map +1 -1
  58. package/dist/tui/theme.js +4 -0
  59. package/dist/tui/theme.js.map +1 -1
  60. package/dist/utils/repl-prompts.d.ts +1 -1
  61. package/dist/utils/repl-prompts.d.ts.map +1 -1
  62. package/dist/utils/repl-prompts.js +77 -22
  63. package/dist/utils/repl-prompts.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/ai/conversation/conversation-manager.ts +22 -0
  66. package/src/cli.ts +4 -0
  67. package/src/commands/new.ts +79 -27
  68. package/src/index.ts +109 -27
  69. package/src/tui/app.tsx +222 -63
  70. package/src/tui/components/WiggumBanner.tsx +66 -0
  71. package/src/tui/demo.tsx +111 -0
  72. package/src/tui/hooks/useSpecGenerator.ts +73 -0
  73. package/src/tui/orchestration/index.ts +10 -0
  74. package/src/tui/orchestration/interview-orchestrator.ts +559 -0
  75. package/src/tui/screens/InitScreen.tsx +63 -0
  76. package/src/tui/screens/InterviewScreen.tsx +201 -46
  77. package/src/tui/screens/MainShell.tsx +290 -0
  78. package/src/tui/screens/WelcomeScreen.tsx +141 -0
  79. package/src/tui/theme.ts +4 -0
  80. 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 to manage state and actions.
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
- // Initialize the generator when the component mounts
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
- // Call onComplete when spec is generated
93
- useEffect(() => {
94
- if (state.generatedSpec) {
95
- onComplete(state.generatedSpec);
96
- }
97
- }, [state.generatedSpec, onComplete]);
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
- // Handle user input submission
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
- await submitAnswer(value);
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
- [submitAnswer]
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
- // Input is enabled when awaiting input and not working
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
- <Box marginY={1}>
144
- <WorkingIndicator state={workingState} />
145
- </Box>
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
- <Box marginTop={1}>
149
- <ChatInput
150
- onSubmit={handleSubmit}
151
- disabled={inputDisabled}
152
- allowEmpty={state.phase === 'context'}
153
- placeholder={
154
- state.phase === 'context'
155
- ? 'Enter URL or file path, or press Enter to continue...'
156
- : state.phase === 'goals'
157
- ? 'Describe what you want to build...'
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
+ }