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.
- 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/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/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
|
|
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
|
-
/**
|
|
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
|
|
59
|
-
*
|
|
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: '
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Handle session state changes
|
|
164
|
+
*/
|
|
165
|
+
const handleSessionStateChange = useCallback((newState: SessionState) => {
|
|
166
|
+
setSessionState(newState);
|
|
167
|
+
}, []);
|
|
111
168
|
|
|
112
|
-
//
|
|
113
|
-
|
|
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
|
|
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: '
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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(
|
|
137
|
-
return render(
|
|
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
|
+
}
|
package/src/tui/demo.tsx
ADDED
|
@@ -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
|
}
|