wiggum-cli 0.8.0 → 0.9.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 (75) 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/package.json +1 -1
  61. package/src/ai/conversation/conversation-manager.ts +22 -0
  62. package/src/cli.ts +4 -0
  63. package/src/commands/new.ts +79 -27
  64. package/src/index.ts +109 -27
  65. package/src/tui/app.tsx +222 -63
  66. package/src/tui/components/WiggumBanner.tsx +66 -0
  67. package/src/tui/demo.tsx +111 -0
  68. package/src/tui/hooks/useSpecGenerator.ts +73 -0
  69. package/src/tui/orchestration/index.ts +10 -0
  70. package/src/tui/orchestration/interview-orchestrator.ts +559 -0
  71. package/src/tui/screens/InitScreen.tsx +63 -0
  72. package/src/tui/screens/InterviewScreen.tsx +201 -46
  73. package/src/tui/screens/MainShell.tsx +290 -0
  74. package/src/tui/screens/WelcomeScreen.tsx +141 -0
  75. package/src/tui/theme.ts +4 -0
package/src/tui/app.tsx CHANGED
@@ -2,16 +2,26 @@
2
2
  * Main Ink Application Entry Point
3
3
  *
4
4
  * The root component for the Ink-based TUI. Routes to different screens
5
- * based on the mode/screen prop. Currently supports the interview screen
6
- * for the /new command, with room to add more screens (init, main shell,
7
- * monitor) as needed.
5
+ * based on the current screen state. Manages session state and navigation.
8
6
  */
9
7
 
10
- import React from 'react';
8
+ import React, { useState, useCallback } from 'react';
11
9
  import { render, type Instance } from 'ink';
10
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
12
  import type { AIProvider } from '../ai/providers.js';
13
13
  import type { ScanResult } from '../scanner/types.js';
14
+ import type { SessionState } from '../repl/session-state.js';
15
+ import { loadConfigWithDefaults } from '../utils/config.js';
14
16
  import { InterviewScreen } from './screens/InterviewScreen.js';
17
+ import { WelcomeScreen } from './screens/WelcomeScreen.js';
18
+ import { InitScreen } from './screens/InitScreen.js';
19
+ import { MainShell, type NavigationTarget, type NavigationProps } from './screens/MainShell.js';
20
+
21
+ /**
22
+ * Available screen types for the App component
23
+ */
24
+ export type AppScreen = 'welcome' | 'shell' | 'interview' | 'init';
15
25
 
16
26
  /**
17
27
  * Props for the interview screen
@@ -29,88 +39,226 @@ export interface InterviewAppProps {
29
39
  scanResult?: ScanResult;
30
40
  }
31
41
 
32
- /**
33
- * Available screen types for the App component
34
- * Start with just 'interview', add more screens later as needed:
35
- * - 'init' - Project initialization wizard
36
- * - 'shell' - Main interactive shell
37
- * - 'monitor' - Agent monitoring dashboard
38
- */
39
- export type AppScreen = 'interview';
40
-
41
42
  /**
42
43
  * Props for the main App component
43
44
  */
44
45
  export interface AppProps {
45
- /** Screen to display */
46
+ /** Initial screen to display */
46
47
  screen: AppScreen;
48
+ /** Initial session state */
49
+ initialSessionState: SessionState;
50
+ /** CLI version */
51
+ version?: string;
47
52
  /** Props for the interview screen (required when screen is 'interview') */
48
53
  interviewProps?: InterviewAppProps;
49
54
  /** Called when the screen completes successfully */
50
55
  onComplete?: (result: string) => void;
51
56
  /** Called when the user exits/cancels */
52
57
  onExit?: () => void;
58
+ /** Called when init workflow should run (outside of Ink) */
59
+ onRunInit?: () => void;
53
60
  }
54
61
 
55
62
  /**
56
63
  * Main App component for the Ink-based TUI
57
64
  *
58
- * Routes to different screens based on the `screen` prop. Currently
59
- * only supports the interview screen for spec generation. The component
60
- * structure allows easy addition of new screens in the future.
65
+ * Routes to different screens based on the current screen state.
66
+ * Manages session state and provides navigation between screens.
61
67
  *
62
68
  * @example
63
69
  * ```tsx
64
- * // Render the interview screen
65
70
  * renderApp({
66
- * screen: 'interview',
67
- * interviewProps: {
68
- * featureName: 'user-auth',
69
- * projectRoot: '/path/to/project',
70
- * provider: 'anthropic',
71
- * model: 'claude-sonnet-4-5-20250514',
72
- * },
73
- * onComplete: (spec) => {
74
- * fs.writeFileSync('spec.md', spec);
75
- * },
76
- * onExit: () => {
77
- * process.exit(0);
78
- * },
71
+ * screen: 'welcome',
72
+ * initialSessionState: sessionState,
73
+ * version: '0.8.0',
74
+ * onExit: () => process.exit(0),
79
75
  * });
80
76
  * ```
81
77
  */
82
78
  export function App({
83
- screen,
79
+ screen: initialScreen,
80
+ initialSessionState,
81
+ version = '0.8.0',
84
82
  interviewProps,
85
83
  onComplete,
86
84
  onExit,
85
+ onRunInit,
87
86
  }: AppProps): React.ReactElement | null {
88
- // Route to the appropriate screen based on the screen prop
89
- if (screen === 'interview' && interviewProps) {
90
- return (
91
- <InterviewScreen
92
- featureName={interviewProps.featureName}
93
- projectRoot={interviewProps.projectRoot}
94
- provider={interviewProps.provider}
95
- model={interviewProps.model}
96
- scanResult={interviewProps.scanResult}
97
- onComplete={(spec) => {
98
- onComplete?.(spec);
99
- }}
100
- onCancel={() => {
101
- onExit?.();
102
- }}
103
- />
104
- );
105
- }
87
+ const [currentScreen, setCurrentScreen] = useState<AppScreen>(initialScreen);
88
+ const [screenProps, setScreenProps] = useState<NavigationProps | null>(
89
+ interviewProps ? { featureName: interviewProps.featureName } : null
90
+ );
91
+ const [sessionState, setSessionState] = useState<SessionState>(initialSessionState);
92
+
93
+ /**
94
+ * Navigate to a different screen
95
+ */
96
+ const navigate = useCallback((target: NavigationTarget, props?: NavigationProps) => {
97
+ setScreenProps(props || null);
98
+ setCurrentScreen(target);
99
+ }, []);
100
+
101
+ /**
102
+ * Handle interview completion - save spec to disk and notify
103
+ */
104
+ const handleInterviewComplete = useCallback(async (spec: string) => {
105
+ // Get feature name from navigation props or initial interview props
106
+ const featureName = screenProps?.featureName || interviewProps?.featureName;
107
+
108
+ if (featureName && typeof featureName === 'string') {
109
+ try {
110
+ // Load config to get specs directory
111
+ const config = await loadConfigWithDefaults(sessionState.projectRoot);
112
+ const specsDir = join(sessionState.projectRoot, config.paths.specs);
113
+
114
+ // Create specs directory if it doesn't exist
115
+ if (!existsSync(specsDir)) {
116
+ mkdirSync(specsDir, { recursive: true });
117
+ }
118
+
119
+ // Write spec to file
120
+ const specPath = join(specsDir, `${featureName}.md`);
121
+ writeFileSync(specPath, spec, 'utf-8');
122
+
123
+ // Call onComplete with the spec path for logging
124
+ onComplete?.(specPath);
125
+ } catch (error) {
126
+ // If saving fails, still call onComplete with spec content
127
+ onComplete?.(spec);
128
+ }
129
+ } else {
130
+ onComplete?.(spec);
131
+ }
132
+
133
+ // If started on interview (--tui mode), call onExit to resolve promise
134
+ // Otherwise, return to shell
135
+ if (initialScreen === 'interview') {
136
+ onExit?.();
137
+ } else {
138
+ navigate('shell');
139
+ }
140
+ }, [onComplete, navigate, initialScreen, onExit, screenProps, interviewProps, sessionState.projectRoot]);
141
+
142
+ /**
143
+ * Handle interview cancel
144
+ */
145
+ const handleInterviewCancel = useCallback(() => {
146
+ // If started on interview (--tui mode), call onExit to resolve promise
147
+ // Otherwise, return to shell
148
+ if (initialScreen === 'interview') {
149
+ onExit?.();
150
+ } else {
151
+ navigate('shell');
152
+ }
153
+ }, [navigate, initialScreen, onExit]);
154
+
155
+ /**
156
+ * Handle welcome continue
157
+ */
158
+ const handleWelcomeContinue = useCallback(() => {
159
+ navigate('shell');
160
+ }, [navigate]);
106
161
 
107
- // Future screens would be added here:
108
- // if (screen === 'init' && initProps) { ... }
109
- // if (screen === 'shell' && shellProps) { ... }
110
- // if (screen === 'monitor' && monitorProps) { ... }
162
+ /**
163
+ * Handle session state changes
164
+ */
165
+ const handleSessionStateChange = useCallback((newState: SessionState) => {
166
+ setSessionState(newState);
167
+ }, []);
111
168
 
112
- // Fallback - shouldn't happen in normal usage
113
- return null;
169
+ // Route to the appropriate screen
170
+ switch (currentScreen) {
171
+ case 'welcome':
172
+ return (
173
+ <WelcomeScreen
174
+ provider={sessionState.provider}
175
+ model={sessionState.model}
176
+ version={version}
177
+ isInitialized={sessionState.initialized}
178
+ onContinue={handleWelcomeContinue}
179
+ />
180
+ );
181
+
182
+ case 'shell':
183
+ return (
184
+ <MainShell
185
+ sessionState={sessionState}
186
+ onNavigate={navigate}
187
+ onSessionStateChange={handleSessionStateChange}
188
+ />
189
+ );
190
+
191
+ case 'interview': {
192
+ // Get feature name from props or navigation
193
+ const featureName = screenProps?.featureName || interviewProps?.featureName;
194
+
195
+ if (!featureName || typeof featureName !== 'string') {
196
+ // Missing feature name, go back to shell
197
+ navigate('shell');
198
+ return null;
199
+ }
200
+
201
+ if (!sessionState.provider) {
202
+ // No provider configured, can't run interview
203
+ navigate('shell');
204
+ return null;
205
+ }
206
+
207
+ return (
208
+ <InterviewScreen
209
+ featureName={featureName}
210
+ projectRoot={sessionState.projectRoot}
211
+ provider={sessionState.provider}
212
+ model={sessionState.model}
213
+ scanResult={sessionState.scanResult}
214
+ onComplete={handleInterviewComplete}
215
+ onCancel={handleInterviewCancel}
216
+ />
217
+ );
218
+ }
219
+
220
+ case 'init': {
221
+ // Handle init workflow - requires running outside Ink due to readline prompts
222
+ const handleRunInit = () => {
223
+ if (onRunInit) {
224
+ onRunInit();
225
+ } else {
226
+ // No init handler provided, return to shell with message
227
+ navigate('shell');
228
+ }
229
+ };
230
+
231
+ return (
232
+ <InitScreen
233
+ onRunInit={handleRunInit}
234
+ onCancel={() => navigate('shell')}
235
+ />
236
+ );
237
+ }
238
+
239
+ default:
240
+ return null;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Render options for renderApp
246
+ */
247
+ export interface RenderAppOptions {
248
+ /** Initial screen to display */
249
+ screen: AppScreen;
250
+ /** Initial session state */
251
+ initialSessionState: SessionState;
252
+ /** CLI version */
253
+ version?: string;
254
+ /** Props for interview screen (if starting directly on interview) */
255
+ interviewProps?: InterviewAppProps;
256
+ /** Called when spec generation completes */
257
+ onComplete?: (result: string) => void;
258
+ /** Called when user exits */
259
+ onExit?: () => void;
260
+ /** Called when init workflow should run (outside of Ink) */
261
+ onRunInit?: () => void;
114
262
  }
115
263
 
116
264
  /**
@@ -119,20 +267,31 @@ export function App({
119
267
  * Helper function that wraps Ink's render() to provide a clean API
120
268
  * for starting the TUI from command handlers.
121
269
  *
122
- * @param props - App component props
270
+ * @param options - Render options
123
271
  * @returns Ink Instance that can be used to control/cleanup the render
124
272
  *
125
273
  * @example
126
274
  * ```typescript
127
- * // In a command handler
128
275
  * const instance = renderApp({
129
- * screen: 'interview',
130
- * interviewProps: { ... },
131
- * onComplete: (spec) => { ... },
276
+ * screen: 'welcome',
277
+ * initialSessionState: state,
278
+ * version: '0.8.0',
132
279
  * onExit: () => instance.unmount(),
133
280
  * });
281
+ *
282
+ * await instance.waitUntilExit();
134
283
  * ```
135
284
  */
136
- export function renderApp(props: AppProps): Instance {
137
- return render(<App {...props} />);
285
+ export function renderApp(options: RenderAppOptions): Instance {
286
+ return render(
287
+ <App
288
+ screen={options.screen}
289
+ initialSessionState={options.initialSessionState}
290
+ version={options.version}
291
+ interviewProps={options.interviewProps}
292
+ onComplete={options.onComplete}
293
+ onExit={options.onExit}
294
+ onRunInit={options.onRunInit}
295
+ />
296
+ );
138
297
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * WiggumBanner - ASCII art banner component
3
+ *
4
+ * Displays the Wiggum CLI ASCII art logo in Simpson yellow.
5
+ * Inspired by Claude Code's welcome banner style.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { Text, Box } from 'ink';
10
+ import { colors } from '../theme.js';
11
+
12
+ /**
13
+ * ASCII art banner for Wiggum CLI
14
+ * Block-style font to match the cfonts 'block' style
15
+ */
16
+ const BANNER = `
17
+ ██╗ ██╗██╗ ██████╗ ██████╗ ██╗ ██╗███╗ ███╗
18
+ ██║ ██║██║██╔════╝ ██╔════╝ ██║ ██║████╗ ████║
19
+ ██║ █╗ ██║██║██║ ███╗██║ ███╗██║ ██║██╔████╔██║
20
+ ██║███╗██║██║██║ ██║██║ ██║██║ ██║██║╚██╔╝██║
21
+ ╚███╔███╔╝██║╚██████╔╝╚██████╔╝╚██████╔╝██║ ╚═╝ ██║
22
+ ╚══╝╚══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
23
+ `;
24
+
25
+ /**
26
+ * Props for WiggumBanner component
27
+ */
28
+ export interface WiggumBannerProps {
29
+ /** Optional color override (defaults to Simpson yellow) */
30
+ color?: string;
31
+ /** Whether to show a compact version */
32
+ compact?: boolean;
33
+ }
34
+
35
+ /**
36
+ * WiggumBanner component
37
+ *
38
+ * Displays the Wiggum CLI ASCII art logo.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * <WiggumBanner />
43
+ * <WiggumBanner color="blue" />
44
+ * <WiggumBanner compact />
45
+ * ```
46
+ */
47
+ export function WiggumBanner({
48
+ color = colors.yellow,
49
+ compact = false,
50
+ }: WiggumBannerProps): React.ReactElement {
51
+ if (compact) {
52
+ return (
53
+ <Box>
54
+ <Text color={color} bold>
55
+ WIGGUM CLI
56
+ </Text>
57
+ </Box>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <Box flexDirection="column">
63
+ <Text color={color}>{BANNER}</Text>
64
+ </Box>
65
+ );
66
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TUI Demo - Test the Ink components visually
4
+ *
5
+ * Run with: npx tsx src/tui/demo.tsx
6
+ */
7
+
8
+ import React, { useState, useEffect } from 'react';
9
+ import { render, Box, Text, useInput } from 'ink';
10
+ import { PhaseHeader } from './components/PhaseHeader.js';
11
+ import { MessageList, type Message } from './components/MessageList.js';
12
+ import { WorkingIndicator } from './components/WorkingIndicator.js';
13
+ import { ChatInput } from './components/ChatInput.js';
14
+ import { colors } from './theme.js';
15
+
16
+ function Demo(): React.ReactElement {
17
+ const [messages, setMessages] = useState<Message[]>([
18
+ { id: '1', role: 'system', content: 'Spec Generator initialized for feature: demo-feature' },
19
+ { id: '2', role: 'assistant', content: 'Welcome! Let\'s create a specification for your feature.' },
20
+ ]);
21
+ const [phase, setPhase] = useState(1);
22
+ const [isWorking, setIsWorking] = useState(false);
23
+ const [workingStatus, setWorkingStatus] = useState('');
24
+
25
+ // Handle user input
26
+ const handleSubmit = (value: string) => {
27
+ // Add user message
28
+ setMessages(prev => [...prev, {
29
+ id: String(Date.now()),
30
+ role: 'user' as const,
31
+ content: value || '(empty - continue)',
32
+ }]);
33
+
34
+ // Simulate AI working
35
+ setIsWorking(true);
36
+ setWorkingStatus('Thinking...');
37
+
38
+ setTimeout(() => {
39
+ // Simulate tool call
40
+ setMessages(prev => [...prev, {
41
+ id: String(Date.now()),
42
+ role: 'assistant' as const,
43
+ content: 'Great! Let me analyze that...',
44
+ toolCalls: [{
45
+ toolName: 'Read File',
46
+ status: 'complete' as const,
47
+ input: 'package.json',
48
+ output: '42 lines read',
49
+ }],
50
+ }]);
51
+
52
+ setIsWorking(false);
53
+ setPhase(p => Math.min(p + 1, 4));
54
+ }, 1500);
55
+ };
56
+
57
+ // Handle escape to exit
58
+ useInput((input, key) => {
59
+ if (key.escape) {
60
+ process.exit(0);
61
+ }
62
+ });
63
+
64
+ const phaseNames = ['Context', 'Goals', 'Interview', 'Generation'];
65
+
66
+ return (
67
+ <Box flexDirection="column" padding={1}>
68
+ <Box marginBottom={1}>
69
+ <Text color={colors.yellow} bold>Ink TUI Demo</Text>
70
+ <Text color={colors.brown}> - Press Esc to exit</Text>
71
+ </Box>
72
+
73
+ <PhaseHeader
74
+ currentPhase={phase}
75
+ totalPhases={4}
76
+ phaseName={phaseNames[phase - 1]}
77
+ />
78
+
79
+ <Box marginY={1}>
80
+ <MessageList messages={messages} />
81
+ </Box>
82
+
83
+ <Box marginY={1}>
84
+ <WorkingIndicator
85
+ state={{
86
+ isWorking,
87
+ status: workingStatus,
88
+ hint: 'esc to cancel',
89
+ }}
90
+ />
91
+ </Box>
92
+
93
+ <Box marginTop={1}>
94
+ <ChatInput
95
+ onSubmit={handleSubmit}
96
+ disabled={isWorking}
97
+ allowEmpty={phase === 1}
98
+ placeholder={
99
+ phase === 1
100
+ ? 'Enter URL or file path, or press Enter to continue...'
101
+ : 'Type your response...'
102
+ }
103
+ />
104
+ </Box>
105
+ </Box>
106
+ );
107
+ }
108
+
109
+ // Render the demo
110
+ console.clear();
111
+ render(<Demo />);
@@ -178,6 +178,28 @@ export interface UseSpecGeneratorReturn {
178
178
  * Complete a tool execution
179
179
  */
180
180
  completeToolCall: (toolId: string, output?: string, error?: string) => void;
181
+
182
+ // Orchestrator-specific actions
183
+
184
+ /**
185
+ * Set the current phase (used by orchestrator)
186
+ */
187
+ setPhase: (phase: GeneratorPhase) => void;
188
+
189
+ /**
190
+ * Set the generated spec (used by orchestrator on completion)
191
+ */
192
+ setGeneratedSpec: (spec: string) => void;
193
+
194
+ /**
195
+ * Set an error state (used by orchestrator on error)
196
+ */
197
+ setError: (error: string) => void;
198
+
199
+ /**
200
+ * Set working state with status message (used by orchestrator)
201
+ */
202
+ setWorking: (isWorking: boolean, status: string) => void;
181
203
  }
182
204
 
183
205
  /**
@@ -571,6 +593,52 @@ export function useSpecGenerator(): UseSpecGeneratorReturn {
571
593
  setState(initialState);
572
594
  }, []);
573
595
 
596
+ /**
597
+ * Set the current phase (used by orchestrator)
598
+ */
599
+ const setPhase = useCallback((phase: GeneratorPhase) => {
600
+ setState((prev) => ({
601
+ ...prev,
602
+ phase,
603
+ }));
604
+ }, []);
605
+
606
+ /**
607
+ * Set the generated spec (used by orchestrator on completion)
608
+ */
609
+ const setGeneratedSpec = useCallback((spec: string) => {
610
+ setState((prev) => ({
611
+ ...prev,
612
+ generatedSpec: spec,
613
+ phase: 'complete',
614
+ isWorking: false,
615
+ awaitingInput: false,
616
+ }));
617
+ }, []);
618
+
619
+ /**
620
+ * Set an error state (used by orchestrator on error)
621
+ */
622
+ const setError = useCallback((error: string) => {
623
+ setState((prev) => ({
624
+ ...prev,
625
+ error,
626
+ isWorking: false,
627
+ }));
628
+ }, []);
629
+
630
+ /**
631
+ * Set working state with status message (used by orchestrator)
632
+ */
633
+ const setWorking = useCallback((isWorking: boolean, status: string) => {
634
+ setState((prev) => ({
635
+ ...prev,
636
+ isWorking,
637
+ workingStatus: status,
638
+ awaitingInput: !isWorking && prev.phase !== 'complete',
639
+ }));
640
+ }, []);
641
+
574
642
  return {
575
643
  state,
576
644
  submitAnswer,
@@ -585,5 +653,10 @@ export function useSpecGenerator(): UseSpecGeneratorReturn {
585
653
  setReady,
586
654
  startToolCall,
587
655
  completeToolCall,
656
+ // Orchestrator-specific actions
657
+ setPhase,
658
+ setGeneratedSpec,
659
+ setError,
660
+ setWorking,
588
661
  };
589
662
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * TUI Orchestration
3
+ * Exports orchestration classes that bridge TUI components to AI services
4
+ */
5
+
6
+ export {
7
+ InterviewOrchestrator,
8
+ type InterviewOrchestratorOptions,
9
+ type SessionContext,
10
+ } from './interview-orchestrator.js';