wiggum-cli 0.7.8 → 0.8.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 (72) hide show
  1. package/dist/tui/app.d.ts +98 -0
  2. package/dist/tui/app.d.ts.map +1 -0
  3. package/dist/tui/app.js +70 -0
  4. package/dist/tui/app.js.map +1 -0
  5. package/dist/tui/components/ChatInput.d.ts +40 -0
  6. package/dist/tui/components/ChatInput.d.ts.map +1 -0
  7. package/dist/tui/components/ChatInput.js +61 -0
  8. package/dist/tui/components/ChatInput.js.map +1 -0
  9. package/dist/tui/components/MessageList.d.ts +79 -0
  10. package/dist/tui/components/MessageList.d.ts.map +1 -0
  11. package/dist/tui/components/MessageList.js +68 -0
  12. package/dist/tui/components/MessageList.js.map +1 -0
  13. package/dist/tui/components/PhaseHeader.d.ts +36 -0
  14. package/dist/tui/components/PhaseHeader.d.ts.map +1 -0
  15. package/dist/tui/components/PhaseHeader.js +31 -0
  16. package/dist/tui/components/PhaseHeader.js.map +1 -0
  17. package/dist/tui/components/StreamingText.d.ts +47 -0
  18. package/dist/tui/components/StreamingText.d.ts.map +1 -0
  19. package/dist/tui/components/StreamingText.js +38 -0
  20. package/dist/tui/components/StreamingText.js.map +1 -0
  21. package/dist/tui/components/ToolCallCard.d.ts +65 -0
  22. package/dist/tui/components/ToolCallCard.d.ts.map +1 -0
  23. package/dist/tui/components/ToolCallCard.js +100 -0
  24. package/dist/tui/components/ToolCallCard.js.map +1 -0
  25. package/dist/tui/components/WorkingIndicator.d.ts +45 -0
  26. package/dist/tui/components/WorkingIndicator.d.ts.map +1 -0
  27. package/dist/tui/components/WorkingIndicator.js +31 -0
  28. package/dist/tui/components/WorkingIndicator.js.map +1 -0
  29. package/dist/tui/components/index.d.ts +16 -0
  30. package/dist/tui/components/index.d.ts.map +1 -0
  31. package/dist/tui/components/index.js +10 -0
  32. package/dist/tui/components/index.js.map +1 -0
  33. package/dist/tui/hooks/index.d.ts +7 -0
  34. package/dist/tui/hooks/index.d.ts.map +1 -0
  35. package/dist/tui/hooks/index.js +6 -0
  36. package/dist/tui/hooks/index.js.map +1 -0
  37. package/dist/tui/hooks/useSpecGenerator.d.ts +168 -0
  38. package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -0
  39. package/dist/tui/hooks/useSpecGenerator.js +405 -0
  40. package/dist/tui/hooks/useSpecGenerator.js.map +1 -0
  41. package/dist/tui/index.d.ts +14 -0
  42. package/dist/tui/index.d.ts.map +1 -0
  43. package/dist/tui/index.js +18 -0
  44. package/dist/tui/index.js.map +1 -0
  45. package/dist/tui/screens/InterviewScreen.d.ts +55 -0
  46. package/dist/tui/screens/InterviewScreen.d.ts.map +1 -0
  47. package/dist/tui/screens/InterviewScreen.js +84 -0
  48. package/dist/tui/screens/InterviewScreen.js.map +1 -0
  49. package/dist/tui/screens/index.d.ts +6 -0
  50. package/dist/tui/screens/index.d.ts.map +1 -0
  51. package/dist/tui/screens/index.js +5 -0
  52. package/dist/tui/screens/index.js.map +1 -0
  53. package/dist/tui/theme.d.ts +62 -0
  54. package/dist/tui/theme.d.ts.map +1 -0
  55. package/dist/tui/theme.js +58 -0
  56. package/dist/tui/theme.js.map +1 -0
  57. package/package.json +6 -1
  58. package/src/tui/app.tsx +138 -0
  59. package/src/tui/components/ChatInput.tsx +105 -0
  60. package/src/tui/components/MessageList.tsx +186 -0
  61. package/src/tui/components/PhaseHeader.tsx +63 -0
  62. package/src/tui/components/StreamingText.tsx +69 -0
  63. package/src/tui/components/ToolCallCard.tsx +215 -0
  64. package/src/tui/components/WorkingIndicator.tsx +72 -0
  65. package/src/tui/components/index.ts +21 -0
  66. package/src/tui/hooks/index.ts +13 -0
  67. package/src/tui/hooks/useSpecGenerator.ts +589 -0
  68. package/src/tui/index.ts +23 -0
  69. package/src/tui/screens/InterviewScreen.tsx +164 -0
  70. package/src/tui/screens/index.ts +6 -0
  71. package/src/tui/theme.ts +72 -0
  72. package/tsconfig.json +2 -1
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Theme configuration for Ink-based Terminal UI
3
+ *
4
+ * Adapts the Simpson color palette for Ink's styling system.
5
+ * Import from './theme.js' to use these constants in Ink components.
6
+ */
7
+ /**
8
+ * Simpson color palette - hex values for Ink components
9
+ *
10
+ * Usage in Ink:
11
+ * <Text color={colors.yellow}>Yellow text</Text>
12
+ * <Box borderColor={colors.brown}>...</Box>
13
+ */
14
+ export declare const colors: {
15
+ /** Marge Simpson hair / sky blue */
16
+ readonly blue: "#2f64d6";
17
+ /** Simpson family skin tone - primary accent */
18
+ readonly yellow: "#f8db27";
19
+ /** Homer's hair / wood tones - secondary/muted */
20
+ readonly brown: "#9c5b01";
21
+ /** Highlights and emphasis */
22
+ readonly white: "#ffffff";
23
+ /** Danger/warnings/errors */
24
+ readonly pink: "#ff81c1";
25
+ };
26
+ /**
27
+ * Box drawing characters for custom borders
28
+ */
29
+ export declare const box: {
30
+ readonly topLeft: "┌";
31
+ readonly topRight: "┐";
32
+ readonly bottomLeft: "└";
33
+ readonly bottomRight: "┘";
34
+ readonly horizontal: "─";
35
+ readonly vertical: "│";
36
+ };
37
+ /**
38
+ * Phase/progress indicator characters
39
+ */
40
+ export declare const phase: {
41
+ readonly pending: "○";
42
+ readonly active: "◐";
43
+ readonly complete: "✓";
44
+ readonly error: "✗";
45
+ };
46
+ /**
47
+ * Phase status type for type-safe status handling
48
+ */
49
+ export type PhaseStatus = keyof typeof phase;
50
+ /**
51
+ * Color name type for type-safe color selection
52
+ */
53
+ export type ColorName = keyof typeof colors;
54
+ /**
55
+ * Get color hex value by name
56
+ */
57
+ export declare function getColor(name: ColorName): string;
58
+ /**
59
+ * Get phase character by status
60
+ */
61
+ export declare function getPhaseChar(status: PhaseStatus): string;
62
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/tui/theme.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,MAAM;IACjB,oCAAoC;;IAEpC,gDAAgD;;IAEhD,kDAAkD;;IAElD,8BAA8B;;IAE9B,6BAA6B;;CAErB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,GAAG;;;;;;;CAON,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,KAAK;;;;;CAKR,CAAC;AAEX;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,KAAK,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,MAAM,CAAC;AAE5C;;GAEG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEhD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAExD"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Theme configuration for Ink-based Terminal UI
3
+ *
4
+ * Adapts the Simpson color palette for Ink's styling system.
5
+ * Import from './theme.js' to use these constants in Ink components.
6
+ */
7
+ /**
8
+ * Simpson color palette - hex values for Ink components
9
+ *
10
+ * Usage in Ink:
11
+ * <Text color={colors.yellow}>Yellow text</Text>
12
+ * <Box borderColor={colors.brown}>...</Box>
13
+ */
14
+ export const colors = {
15
+ /** Marge Simpson hair / sky blue */
16
+ blue: '#2f64d6',
17
+ /** Simpson family skin tone - primary accent */
18
+ yellow: '#f8db27',
19
+ /** Homer's hair / wood tones - secondary/muted */
20
+ brown: '#9c5b01',
21
+ /** Highlights and emphasis */
22
+ white: '#ffffff',
23
+ /** Danger/warnings/errors */
24
+ pink: '#ff81c1',
25
+ };
26
+ /**
27
+ * Box drawing characters for custom borders
28
+ */
29
+ export const box = {
30
+ topLeft: '\u250c',
31
+ topRight: '\u2510',
32
+ bottomLeft: '\u2514',
33
+ bottomRight: '\u2518',
34
+ horizontal: '\u2500',
35
+ vertical: '\u2502',
36
+ };
37
+ /**
38
+ * Phase/progress indicator characters
39
+ */
40
+ export const phase = {
41
+ pending: '\u25cb', // ○
42
+ active: '\u25d0', // ◐
43
+ complete: '\u2713', // ✓
44
+ error: '\u2717', // ✗
45
+ };
46
+ /**
47
+ * Get color hex value by name
48
+ */
49
+ export function getColor(name) {
50
+ return colors[name];
51
+ }
52
+ /**
53
+ * Get phase character by status
54
+ */
55
+ export function getPhaseChar(status) {
56
+ return phase[status];
57
+ }
58
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/tui/theme.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,oCAAoC;IACpC,IAAI,EAAE,SAAS;IACf,gDAAgD;IAChD,MAAM,EAAE,SAAS;IACjB,kDAAkD;IAClD,KAAK,EAAE,SAAS;IAChB,8BAA8B;IAC9B,KAAK,EAAE,SAAS;IAChB,6BAA6B;IAC7B,IAAI,EAAE,SAAS;CACP,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,QAAQ;IACpB,WAAW,EAAE,QAAQ;IACrB,UAAU,EAAE,QAAQ;IACpB,QAAQ,EAAE,QAAQ;CACV,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,OAAO,EAAE,QAAQ,EAAE,IAAI;IACvB,MAAM,EAAE,QAAQ,EAAE,IAAI;IACtB,QAAQ,EAAE,QAAQ,EAAE,IAAI;IACxB,KAAK,EAAE,QAAQ,EAAE,IAAI;CACb,CAAC;AAYX;;GAEG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAe;IACtC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC;AACvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiggum-cli",
3
- "version": "0.7.8",
3
+ "version": "0.8.0",
4
4
  "description": "AI-powered feature development loop CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,11 +37,16 @@
37
37
  "braintrust": "^2.0.2",
38
38
  "cfonts": "^3.2.0",
39
39
  "commander": "^12.1.0",
40
+ "ink": "^5.2.1",
41
+ "ink-spinner": "^5.0.0",
42
+ "ink-text-input": "^6.0.0",
40
43
  "picocolors": "^1.0.0",
44
+ "react": "^18.3.1",
41
45
  "zod": "^4.3.5"
42
46
  },
43
47
  "devDependencies": {
44
48
  "@types/node": "^20.10.0",
49
+ "@types/react": "^19.2.9",
45
50
  "typescript": "^5.3.0",
46
51
  "vitest": "^4.0.17"
47
52
  },
@@ -0,0 +1,138 @@
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 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.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { render, type Instance } from 'ink';
12
+ import type { AIProvider } from '../ai/providers.js';
13
+ import type { ScanResult } from '../scanner/types.js';
14
+ import { InterviewScreen } from './screens/InterviewScreen.js';
15
+
16
+ /**
17
+ * Props for the interview screen
18
+ */
19
+ export interface InterviewAppProps {
20
+ /** Name of the feature being specified */
21
+ featureName: string;
22
+ /** Project root directory path */
23
+ projectRoot: string;
24
+ /** AI provider to use */
25
+ provider: AIProvider;
26
+ /** Model ID to use */
27
+ model: string;
28
+ /** Optional scan result with detected tech stack */
29
+ scanResult?: ScanResult;
30
+ }
31
+
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
+ * Props for the main App component
43
+ */
44
+ export interface AppProps {
45
+ /** Screen to display */
46
+ screen: AppScreen;
47
+ /** Props for the interview screen (required when screen is 'interview') */
48
+ interviewProps?: InterviewAppProps;
49
+ /** Called when the screen completes successfully */
50
+ onComplete?: (result: string) => void;
51
+ /** Called when the user exits/cancels */
52
+ onExit?: () => void;
53
+ }
54
+
55
+ /**
56
+ * Main App component for the Ink-based TUI
57
+ *
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.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * // Render the interview screen
65
+ * 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
+ * },
79
+ * });
80
+ * ```
81
+ */
82
+ export function App({
83
+ screen,
84
+ interviewProps,
85
+ onComplete,
86
+ onExit,
87
+ }: 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
+ }
106
+
107
+ // Future screens would be added here:
108
+ // if (screen === 'init' && initProps) { ... }
109
+ // if (screen === 'shell' && shellProps) { ... }
110
+ // if (screen === 'monitor' && monitorProps) { ... }
111
+
112
+ // Fallback - shouldn't happen in normal usage
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Render the App component to the terminal
118
+ *
119
+ * Helper function that wraps Ink's render() to provide a clean API
120
+ * for starting the TUI from command handlers.
121
+ *
122
+ * @param props - App component props
123
+ * @returns Ink Instance that can be used to control/cleanup the render
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * // In a command handler
128
+ * const instance = renderApp({
129
+ * screen: 'interview',
130
+ * interviewProps: { ... },
131
+ * onComplete: (spec) => { ... },
132
+ * onExit: () => instance.unmount(),
133
+ * });
134
+ * ```
135
+ */
136
+ export function renderApp(props: AppProps): Instance {
137
+ return render(<App {...props} />);
138
+ }
@@ -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
+ }