wiggum-cli 0.7.8 → 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 +108 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +169 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/components/ChatInput.d.ts +40 -0
- package/dist/tui/components/ChatInput.d.ts.map +1 -0
- package/dist/tui/components/ChatInput.js +61 -0
- package/dist/tui/components/ChatInput.js.map +1 -0
- package/dist/tui/components/MessageList.d.ts +79 -0
- package/dist/tui/components/MessageList.d.ts.map +1 -0
- package/dist/tui/components/MessageList.js +68 -0
- package/dist/tui/components/MessageList.js.map +1 -0
- package/dist/tui/components/PhaseHeader.d.ts +36 -0
- package/dist/tui/components/PhaseHeader.d.ts.map +1 -0
- package/dist/tui/components/PhaseHeader.js +31 -0
- package/dist/tui/components/PhaseHeader.js.map +1 -0
- package/dist/tui/components/StreamingText.d.ts +47 -0
- package/dist/tui/components/StreamingText.d.ts.map +1 -0
- package/dist/tui/components/StreamingText.js +38 -0
- package/dist/tui/components/StreamingText.js.map +1 -0
- package/dist/tui/components/ToolCallCard.d.ts +65 -0
- package/dist/tui/components/ToolCallCard.d.ts.map +1 -0
- package/dist/tui/components/ToolCallCard.js +100 -0
- package/dist/tui/components/ToolCallCard.js.map +1 -0
- 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/components/WorkingIndicator.d.ts +45 -0
- package/dist/tui/components/WorkingIndicator.d.ts.map +1 -0
- package/dist/tui/components/WorkingIndicator.js +31 -0
- package/dist/tui/components/WorkingIndicator.js.map +1 -0
- package/dist/tui/components/index.d.ts +16 -0
- package/dist/tui/components/index.d.ts.map +1 -0
- package/dist/tui/components/index.js +10 -0
- package/dist/tui/components/index.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/index.d.ts +7 -0
- package/dist/tui/hooks/index.d.ts.map +1 -0
- package/dist/tui/hooks/index.js +6 -0
- package/dist/tui/hooks/index.js.map +1 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts +184 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -0
- package/dist/tui/hooks/useSpecGenerator.js +452 -0
- package/dist/tui/hooks/useSpecGenerator.js.map +1 -0
- package/dist/tui/index.d.ts +14 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +18 -0
- package/dist/tui/index.js.map +1 -0
- 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 +44 -0
- package/dist/tui/screens/InterviewScreen.d.ts.map +1 -0
- package/dist/tui/screens/InterviewScreen.js +212 -0
- package/dist/tui/screens/InterviewScreen.js.map +1 -0
- 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/screens/index.d.ts +6 -0
- package/dist/tui/screens/index.d.ts.map +1 -0
- package/dist/tui/screens/index.js +5 -0
- package/dist/tui/screens/index.js.map +1 -0
- package/dist/tui/theme.d.ts +66 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +62 -0
- package/dist/tui/theme.js.map +1 -0
- package/package.json +6 -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 +297 -0
- package/src/tui/components/ChatInput.tsx +105 -0
- package/src/tui/components/MessageList.tsx +186 -0
- package/src/tui/components/PhaseHeader.tsx +63 -0
- package/src/tui/components/StreamingText.tsx +69 -0
- package/src/tui/components/ToolCallCard.tsx +215 -0
- package/src/tui/components/WiggumBanner.tsx +66 -0
- package/src/tui/components/WorkingIndicator.tsx +72 -0
- package/src/tui/components/index.ts +21 -0
- package/src/tui/demo.tsx +111 -0
- package/src/tui/hooks/index.ts +13 -0
- package/src/tui/hooks/useSpecGenerator.ts +662 -0
- package/src/tui/index.ts +23 -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 +319 -0
- package/src/tui/screens/MainShell.tsx +290 -0
- package/src/tui/screens/WelcomeScreen.tsx +141 -0
- package/src/tui/screens/index.ts +6 -0
- package/src/tui/theme.ts +76 -0
- package/tsconfig.json +2 -1
package/src/tui/app.tsx
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Ink Application Entry Point
|
|
3
|
+
*
|
|
4
|
+
* The root component for the Ink-based TUI. Routes to different screens
|
|
5
|
+
* based on the current screen state. Manages session state and navigation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useCallback } from 'react';
|
|
9
|
+
import { render, type Instance } from 'ink';
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import type { AIProvider } from '../ai/providers.js';
|
|
13
|
+
import type { ScanResult } from '../scanner/types.js';
|
|
14
|
+
import type { SessionState } from '../repl/session-state.js';
|
|
15
|
+
import { loadConfigWithDefaults } from '../utils/config.js';
|
|
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';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Props for the interview screen
|
|
28
|
+
*/
|
|
29
|
+
export interface InterviewAppProps {
|
|
30
|
+
/** Name of the feature being specified */
|
|
31
|
+
featureName: string;
|
|
32
|
+
/** Project root directory path */
|
|
33
|
+
projectRoot: string;
|
|
34
|
+
/** AI provider to use */
|
|
35
|
+
provider: AIProvider;
|
|
36
|
+
/** Model ID to use */
|
|
37
|
+
model: string;
|
|
38
|
+
/** Optional scan result with detected tech stack */
|
|
39
|
+
scanResult?: ScanResult;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Props for the main App component
|
|
44
|
+
*/
|
|
45
|
+
export interface AppProps {
|
|
46
|
+
/** Initial screen to display */
|
|
47
|
+
screen: AppScreen;
|
|
48
|
+
/** Initial session state */
|
|
49
|
+
initialSessionState: SessionState;
|
|
50
|
+
/** CLI version */
|
|
51
|
+
version?: string;
|
|
52
|
+
/** Props for the interview screen (required when screen is 'interview') */
|
|
53
|
+
interviewProps?: InterviewAppProps;
|
|
54
|
+
/** Called when the screen completes successfully */
|
|
55
|
+
onComplete?: (result: string) => void;
|
|
56
|
+
/** Called when the user exits/cancels */
|
|
57
|
+
onExit?: () => void;
|
|
58
|
+
/** Called when init workflow should run (outside of Ink) */
|
|
59
|
+
onRunInit?: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Main App component for the Ink-based TUI
|
|
64
|
+
*
|
|
65
|
+
* Routes to different screens based on the current screen state.
|
|
66
|
+
* Manages session state and provides navigation between screens.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* renderApp({
|
|
71
|
+
* screen: 'welcome',
|
|
72
|
+
* initialSessionState: sessionState,
|
|
73
|
+
* version: '0.8.0',
|
|
74
|
+
* onExit: () => process.exit(0),
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function App({
|
|
79
|
+
screen: initialScreen,
|
|
80
|
+
initialSessionState,
|
|
81
|
+
version = '0.8.0',
|
|
82
|
+
interviewProps,
|
|
83
|
+
onComplete,
|
|
84
|
+
onExit,
|
|
85
|
+
onRunInit,
|
|
86
|
+
}: AppProps): React.ReactElement | null {
|
|
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]);
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle session state changes
|
|
164
|
+
*/
|
|
165
|
+
const handleSessionStateChange = useCallback((newState: SessionState) => {
|
|
166
|
+
setSessionState(newState);
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
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;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Render the App component to the terminal
|
|
266
|
+
*
|
|
267
|
+
* Helper function that wraps Ink's render() to provide a clean API
|
|
268
|
+
* for starting the TUI from command handlers.
|
|
269
|
+
*
|
|
270
|
+
* @param options - Render options
|
|
271
|
+
* @returns Ink Instance that can be used to control/cleanup the render
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* const instance = renderApp({
|
|
276
|
+
* screen: 'welcome',
|
|
277
|
+
* initialSessionState: state,
|
|
278
|
+
* version: '0.8.0',
|
|
279
|
+
* onExit: () => instance.unmount(),
|
|
280
|
+
* });
|
|
281
|
+
*
|
|
282
|
+
* await instance.waitUntilExit();
|
|
283
|
+
* ```
|
|
284
|
+
*/
|
|
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
|
+
);
|
|
297
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatInput - Multi-line input with history for chat interactions
|
|
3
|
+
*
|
|
4
|
+
* Displays a prompt character followed by a text input.
|
|
5
|
+
* Handles submission on Enter and clears input after submit.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState } from 'react';
|
|
9
|
+
import { Box, Text } from 'ink';
|
|
10
|
+
import TextInput from 'ink-text-input';
|
|
11
|
+
import { colors } from '../theme.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props for the ChatInput component
|
|
15
|
+
*/
|
|
16
|
+
export interface ChatInputProps {
|
|
17
|
+
/** Called when user presses Enter with the current input value */
|
|
18
|
+
onSubmit: (value: string) => void;
|
|
19
|
+
/** Placeholder text when empty */
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Whether input is disabled (e.g., during AI processing) */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** Prompt character/text shown before input (default "> ") */
|
|
24
|
+
prompt?: string;
|
|
25
|
+
/** Allow empty submissions (e.g., to continue/skip phases) */
|
|
26
|
+
allowEmpty?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ChatInput component
|
|
31
|
+
*
|
|
32
|
+
* Provides a text input with a prompt character for chat-style interactions.
|
|
33
|
+
* Clears input after submission. Shows dimmed appearance when disabled.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <ChatInput
|
|
38
|
+
* onSubmit={(value) => console.log('User said:', value)}
|
|
39
|
+
* placeholder="Type your response..."
|
|
40
|
+
* disabled={isProcessing}
|
|
41
|
+
* />
|
|
42
|
+
* // Renders: > Type your response...
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function ChatInput({
|
|
46
|
+
onSubmit,
|
|
47
|
+
placeholder = 'Type your message...',
|
|
48
|
+
disabled = false,
|
|
49
|
+
prompt = '> ',
|
|
50
|
+
allowEmpty = false,
|
|
51
|
+
}: ChatInputProps): React.ReactElement {
|
|
52
|
+
const [value, setValue] = useState('');
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handle input submission
|
|
56
|
+
* Calls onSubmit with current value and clears the input
|
|
57
|
+
*/
|
|
58
|
+
const handleSubmit = (submittedValue: string): void => {
|
|
59
|
+
// Don't submit when disabled
|
|
60
|
+
if (disabled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Don't submit empty values unless allowEmpty is true
|
|
65
|
+
if (!submittedValue.trim() && !allowEmpty) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onSubmit(submittedValue);
|
|
70
|
+
setValue('');
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle value changes
|
|
75
|
+
* Only update if not disabled
|
|
76
|
+
*/
|
|
77
|
+
const handleChange = (newValue: string): void => {
|
|
78
|
+
if (!disabled) {
|
|
79
|
+
setValue(newValue);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// When disabled, show a waiting message
|
|
84
|
+
if (disabled) {
|
|
85
|
+
return (
|
|
86
|
+
<Box flexDirection="row">
|
|
87
|
+
<Text dimColor color={colors.brown}>
|
|
88
|
+
{prompt}[waiting for AI...]
|
|
89
|
+
</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box flexDirection="row">
|
|
96
|
+
<Text color={colors.yellow}>{prompt}</Text>
|
|
97
|
+
<TextInput
|
|
98
|
+
value={value}
|
|
99
|
+
onChange={handleChange}
|
|
100
|
+
onSubmit={handleSubmit}
|
|
101
|
+
placeholder={placeholder}
|
|
102
|
+
/>
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageList - Scrollable conversation history display
|
|
3
|
+
*
|
|
4
|
+
* Displays the full conversation history including:
|
|
5
|
+
* - User messages
|
|
6
|
+
* - Assistant messages (with optional streaming)
|
|
7
|
+
* - System messages
|
|
8
|
+
* - Tool call cards inline with assistant messages
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { Box, Text } from 'ink';
|
|
13
|
+
import { colors } from '../theme.js';
|
|
14
|
+
import { StreamingText } from './StreamingText.js';
|
|
15
|
+
import { ToolCallCard, type ToolCallStatus } from './ToolCallCard.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tool call information for assistant messages
|
|
19
|
+
*/
|
|
20
|
+
export interface ToolCall {
|
|
21
|
+
/** Name of the tool being executed */
|
|
22
|
+
toolName: string;
|
|
23
|
+
/** Current execution status */
|
|
24
|
+
status: ToolCallStatus;
|
|
25
|
+
/** Input passed to the tool */
|
|
26
|
+
input: string;
|
|
27
|
+
/** Output when complete */
|
|
28
|
+
output?: string;
|
|
29
|
+
/** Error message if failed */
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Message in the conversation history
|
|
35
|
+
*/
|
|
36
|
+
export interface Message {
|
|
37
|
+
/** Unique identifier for the message */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Who sent the message */
|
|
40
|
+
role: 'user' | 'assistant' | 'system';
|
|
41
|
+
/** Text content of the message */
|
|
42
|
+
content: string;
|
|
43
|
+
/** Tool calls included in assistant messages */
|
|
44
|
+
toolCalls?: ToolCall[];
|
|
45
|
+
/** Whether this message is currently streaming */
|
|
46
|
+
isStreaming?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Props for the MessageList component
|
|
51
|
+
*/
|
|
52
|
+
export interface MessageListProps {
|
|
53
|
+
/** Array of messages to display */
|
|
54
|
+
messages: Message[];
|
|
55
|
+
/** Optional max height in lines (for future scrolling support) */
|
|
56
|
+
maxHeight?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Renders a single user message
|
|
61
|
+
*/
|
|
62
|
+
function UserMessage({ content }: { content: string }): React.ReactElement {
|
|
63
|
+
return (
|
|
64
|
+
<Box flexDirection="row" marginY={1}>
|
|
65
|
+
<Text color={colors.white} bold>
|
|
66
|
+
You:{' '}
|
|
67
|
+
</Text>
|
|
68
|
+
<Text color={colors.white}>{content}</Text>
|
|
69
|
+
</Box>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Renders a single assistant message with optional tool calls and streaming
|
|
75
|
+
*/
|
|
76
|
+
function AssistantMessage({
|
|
77
|
+
content,
|
|
78
|
+
toolCalls,
|
|
79
|
+
isStreaming,
|
|
80
|
+
}: {
|
|
81
|
+
content: string;
|
|
82
|
+
toolCalls?: ToolCall[];
|
|
83
|
+
isStreaming?: boolean;
|
|
84
|
+
}): React.ReactElement {
|
|
85
|
+
return (
|
|
86
|
+
<Box flexDirection="column" marginY={1}>
|
|
87
|
+
{/* Tool calls appear before the message content */}
|
|
88
|
+
{toolCalls &&
|
|
89
|
+
toolCalls.length > 0 &&
|
|
90
|
+
toolCalls.map((toolCall, index) => (
|
|
91
|
+
<Box key={`tool-${index}`} marginBottom={1}>
|
|
92
|
+
<ToolCallCard
|
|
93
|
+
toolName={toolCall.toolName}
|
|
94
|
+
status={toolCall.status}
|
|
95
|
+
input={toolCall.input}
|
|
96
|
+
output={toolCall.output}
|
|
97
|
+
error={toolCall.error}
|
|
98
|
+
/>
|
|
99
|
+
</Box>
|
|
100
|
+
))}
|
|
101
|
+
|
|
102
|
+
{/* Message content with prefix */}
|
|
103
|
+
<Box flexDirection="row">
|
|
104
|
+
<Text color={colors.yellow} bold>
|
|
105
|
+
AI:{' '}
|
|
106
|
+
</Text>
|
|
107
|
+
{isStreaming ? (
|
|
108
|
+
<StreamingText text={content} isStreaming={true} color={colors.yellow} />
|
|
109
|
+
) : (
|
|
110
|
+
<Text color={colors.yellow}>{content}</Text>
|
|
111
|
+
)}
|
|
112
|
+
</Box>
|
|
113
|
+
</Box>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Renders a system message (dimmed text)
|
|
119
|
+
*/
|
|
120
|
+
function SystemMessage({ content }: { content: string }): React.ReactElement {
|
|
121
|
+
return (
|
|
122
|
+
<Box flexDirection="row" marginY={1}>
|
|
123
|
+
<Text color={colors.brown} dimColor>
|
|
124
|
+
{content}
|
|
125
|
+
</Text>
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* MessageList component
|
|
132
|
+
*
|
|
133
|
+
* Displays the full conversation history. Each message type has
|
|
134
|
+
* distinct styling:
|
|
135
|
+
* - User messages: "You: " prefix in white
|
|
136
|
+
* - Assistant messages: "AI: " prefix in yellow, with inline tool cards
|
|
137
|
+
* - System messages: dimmed brown text
|
|
138
|
+
*
|
|
139
|
+
* For streaming messages, uses the StreamingText component to show
|
|
140
|
+
* the cursor indicator.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```tsx
|
|
144
|
+
* <MessageList
|
|
145
|
+
* messages={[
|
|
146
|
+
* { id: '1', role: 'system', content: 'Interview started' },
|
|
147
|
+
* { id: '2', role: 'assistant', content: 'Hello! What would you like to build?' },
|
|
148
|
+
* { id: '3', role: 'user', content: 'A todo app' },
|
|
149
|
+
* { id: '4', role: 'assistant', content: 'Let me check...',
|
|
150
|
+
* toolCalls: [{ toolName: 'Read File', status: 'running', input: 'package.json' }],
|
|
151
|
+
* isStreaming: true
|
|
152
|
+
* },
|
|
153
|
+
* ]}
|
|
154
|
+
* />
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export function MessageList({ messages, maxHeight }: MessageListProps): React.ReactElement {
|
|
158
|
+
// Note: maxHeight is accepted for future scrolling support
|
|
159
|
+
// Currently renders all messages - parent handles any scroll-like behavior
|
|
160
|
+
// by controlling which messages are passed in
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Box
|
|
164
|
+
flexDirection="column"
|
|
165
|
+
{...(maxHeight ? { height: maxHeight } : {})}
|
|
166
|
+
>
|
|
167
|
+
{messages.map((message) => {
|
|
168
|
+
switch (message.role) {
|
|
169
|
+
case 'user':
|
|
170
|
+
return <UserMessage key={message.id} content={message.content} />;
|
|
171
|
+
case 'assistant':
|
|
172
|
+
return (
|
|
173
|
+
<AssistantMessage
|
|
174
|
+
key={message.id}
|
|
175
|
+
content={message.content}
|
|
176
|
+
toolCalls={message.toolCalls}
|
|
177
|
+
isStreaming={message.isStreaming}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
case 'system':
|
|
181
|
+
return <SystemMessage key={message.id} content={message.content} />;
|
|
182
|
+
}
|
|
183
|
+
})}
|
|
184
|
+
</Box>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhaseHeader - Current phase indicator for multi-step workflows
|
|
3
|
+
*
|
|
4
|
+
* Displays the current phase with a horizontal line border.
|
|
5
|
+
* Format: ━━━ Phase X of Y: PhaseName ━━━
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Box, Text } from 'ink';
|
|
10
|
+
import { colors } from '../theme.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for the PhaseHeader component
|
|
14
|
+
*/
|
|
15
|
+
export interface PhaseHeaderProps {
|
|
16
|
+
/** Current phase number (1-based) */
|
|
17
|
+
currentPhase: number;
|
|
18
|
+
/** Total number of phases */
|
|
19
|
+
totalPhases: number;
|
|
20
|
+
/** Name of the current phase */
|
|
21
|
+
phaseName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Heavy horizontal box drawing character (U+2501)
|
|
26
|
+
*/
|
|
27
|
+
const HEAVY_HORIZONTAL = '\u2501';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* PhaseHeader component
|
|
31
|
+
*
|
|
32
|
+
* Shows the current phase progress with surrounding horizontal lines.
|
|
33
|
+
* Uses Simpson yellow for visibility.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <PhaseHeader
|
|
38
|
+
* currentPhase={2}
|
|
39
|
+
* totalPhases={4}
|
|
40
|
+
* phaseName="Understanding Requirements"
|
|
41
|
+
* />
|
|
42
|
+
* // Renders: ━━━ Phase 2 of 4: Understanding Requirements ━━━
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function PhaseHeader({
|
|
46
|
+
currentPhase,
|
|
47
|
+
totalPhases,
|
|
48
|
+
phaseName,
|
|
49
|
+
}: PhaseHeaderProps): React.ReactElement {
|
|
50
|
+
// Build the phase text
|
|
51
|
+
const phaseText = `Phase ${currentPhase} of ${totalPhases}: ${phaseName}`;
|
|
52
|
+
|
|
53
|
+
// Create horizontal line segments (3 characters each side)
|
|
54
|
+
const lineSegment = HEAVY_HORIZONTAL.repeat(3);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Box flexDirection="row" justifyContent="center" width="100%">
|
|
58
|
+
<Text color={colors.yellow}>
|
|
59
|
+
{lineSegment} {phaseText} {lineSegment}
|
|
60
|
+
</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|