wiggum-cli 0.12.1 → 0.13.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/index.js +7 -6
- package/dist/tui/app.d.ts +12 -22
- package/dist/tui/app.js +130 -314
- package/dist/tui/components/AppShell.d.ts +47 -0
- package/dist/tui/components/AppShell.js +19 -0
- package/dist/tui/components/FooterStatusBar.js +2 -3
- package/dist/tui/components/HeaderContent.d.ts +28 -0
- package/dist/tui/components/HeaderContent.js +16 -0
- package/dist/tui/components/MessageList.d.ts +9 -7
- package/dist/tui/components/MessageList.js +23 -17
- package/dist/tui/components/RunCompletionSummary.d.ts +22 -0
- package/dist/tui/components/RunCompletionSummary.js +23 -0
- package/dist/tui/components/SpecCompletionSummary.d.ts +47 -0
- package/dist/tui/components/SpecCompletionSummary.js +124 -0
- package/dist/tui/components/TipsBar.d.ts +24 -0
- package/dist/tui/components/TipsBar.js +23 -0
- package/dist/tui/components/WiggumBanner.js +8 -3
- package/dist/tui/hooks/useBackgroundRuns.d.ts +52 -0
- package/dist/tui/hooks/useBackgroundRuns.js +121 -0
- package/dist/tui/orchestration/interview-orchestrator.js +1 -1
- package/dist/tui/screens/InitScreen.d.ts +13 -8
- package/dist/tui/screens/InitScreen.js +86 -87
- package/dist/tui/screens/InterviewScreen.d.ts +11 -8
- package/dist/tui/screens/InterviewScreen.js +145 -99
- package/dist/tui/screens/MainShell.d.ts +13 -12
- package/dist/tui/screens/MainShell.js +65 -69
- package/dist/tui/screens/RunScreen.d.ts +17 -1
- package/dist/tui/screens/RunScreen.js +235 -80
- package/dist/tui/screens/index.d.ts +0 -2
- package/dist/tui/screens/index.js +0 -1
- package/dist/tui/utils/loop-status.d.ts +22 -3
- package/dist/tui/utils/loop-status.js +65 -15
- package/package.json +5 -1
- package/dist/tui/screens/WelcomeScreen.d.ts +0 -44
- package/dist/tui/screens/WelcomeScreen.js +0 -54
|
@@ -4,17 +4,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
*
|
|
5
5
|
* The main interactive shell for Wiggum CLI, replacing the readline REPL.
|
|
6
6
|
* Handles slash commands and provides navigation to other screens.
|
|
7
|
+
* Wrapped in AppShell for consistent layout.
|
|
7
8
|
*/
|
|
8
9
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
9
10
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
10
11
|
import { MessageList } from '../components/MessageList.js';
|
|
11
12
|
import { ChatInput } from '../components/ChatInput.js';
|
|
12
|
-
import { WorkingIndicator } from '../components/WorkingIndicator.js';
|
|
13
13
|
import { ActionOutput } from '../components/ActionOutput.js';
|
|
14
|
-
import {
|
|
15
|
-
import { colors, theme } from '../theme.js';
|
|
14
|
+
import { AppShell } from '../components/AppShell.js';
|
|
15
|
+
import { colors, theme, phase } from '../theme.js';
|
|
16
16
|
import { loadContext, getContextAge } from '../../context/index.js';
|
|
17
|
+
import { logger } from '../../utils/logger.js';
|
|
17
18
|
import { parseInput, resolveCommandAlias, formatHelpText, } from '../../repl/command-parser.js';
|
|
19
|
+
import { readLoopStatus } from '../utils/loop-status.js';
|
|
18
20
|
import { useSync } from '../hooks/useSync.js';
|
|
19
21
|
import path from 'node:path';
|
|
20
22
|
/**
|
|
@@ -28,24 +30,24 @@ function generateId() {
|
|
|
28
30
|
*
|
|
29
31
|
* The main interactive shell that handles slash commands and navigation.
|
|
30
32
|
* Replaces the readline-based REPL with an Ink-powered TUI.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```tsx
|
|
34
|
-
* <MainShell
|
|
35
|
-
* sessionState={state}
|
|
36
|
-
* onNavigate={(target, props) => setScreen(target, props)}
|
|
37
|
-
* />
|
|
38
|
-
* ```
|
|
39
33
|
*/
|
|
40
|
-
export function MainShell({ sessionState, onNavigate, }) {
|
|
34
|
+
export function MainShell({ header, sessionState, onNavigate, backgroundRuns, initialMessage, initialFiles, }) {
|
|
41
35
|
const { exit } = useApp();
|
|
42
|
-
const [messages, setMessages] = useState(
|
|
36
|
+
const [messages, setMessages] = useState(() => {
|
|
37
|
+
const initial = [];
|
|
38
|
+
if (initialMessage) {
|
|
39
|
+
initial.push({ id: generateId(), role: 'system', content: initialMessage });
|
|
40
|
+
}
|
|
41
|
+
if (initialFiles && initialFiles.length > 0) {
|
|
42
|
+
for (const file of initialFiles) {
|
|
43
|
+
initial.push({ id: generateId(), role: 'system', content: ` ${file}` });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return initial;
|
|
47
|
+
});
|
|
43
48
|
const [contextAge, setContextAge] = useState(null);
|
|
44
49
|
// Sync hook
|
|
45
50
|
const { status: syncStatus, error: syncError, sync } = useSync();
|
|
46
|
-
/**
|
|
47
|
-
* Add a system message to the conversation
|
|
48
|
-
*/
|
|
49
51
|
const addSystemMessage = useCallback((content) => {
|
|
50
52
|
const message = {
|
|
51
53
|
id: generateId(),
|
|
@@ -74,9 +76,10 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
74
76
|
setContextAge(null);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
|
-
catch {
|
|
79
|
+
catch (err) {
|
|
80
|
+
logger.error(`Failed to load context: ${err instanceof Error ? err.message : String(err)}`);
|
|
78
81
|
if (!cancelled) {
|
|
79
|
-
setContextAge(
|
|
82
|
+
setContextAge('load error');
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
};
|
|
@@ -86,22 +89,12 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
86
89
|
};
|
|
87
90
|
}, [sessionState.projectRoot, sessionState.initialized, syncStatus]);
|
|
88
91
|
const projectLabel = useMemo(() => path.basename(sessionState.projectRoot), [sessionState.projectRoot]);
|
|
89
|
-
/**
|
|
90
|
-
* Handle /help command
|
|
91
|
-
*/
|
|
92
92
|
const handleHelp = useCallback(() => {
|
|
93
93
|
addSystemMessage(formatHelpText());
|
|
94
94
|
}, [addSystemMessage]);
|
|
95
|
-
/**
|
|
96
|
-
* Handle /init command
|
|
97
|
-
*/
|
|
98
95
|
const handleInit = useCallback(() => {
|
|
99
|
-
// Navigate to init screen
|
|
100
96
|
onNavigate('init');
|
|
101
97
|
}, [onNavigate]);
|
|
102
|
-
/**
|
|
103
|
-
* Handle /new command
|
|
104
|
-
*/
|
|
105
98
|
const handleNew = useCallback((args) => {
|
|
106
99
|
if (args.length === 0) {
|
|
107
100
|
addSystemMessage('Feature name required. Usage: /new <feature-name>');
|
|
@@ -114,9 +107,6 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
114
107
|
const featureName = args[0];
|
|
115
108
|
onNavigate('interview', { featureName });
|
|
116
109
|
}, [sessionState.initialized, onNavigate, addSystemMessage]);
|
|
117
|
-
/**
|
|
118
|
-
* Handle /run command
|
|
119
|
-
*/
|
|
120
110
|
const handleRun = useCallback((args) => {
|
|
121
111
|
if (args.length === 0) {
|
|
122
112
|
addSystemMessage('Feature name required. Usage: /run <feature-name>');
|
|
@@ -129,22 +119,38 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
129
119
|
const featureName = args[0];
|
|
130
120
|
onNavigate('run', { featureName });
|
|
131
121
|
}, [sessionState.initialized, addSystemMessage, onNavigate]);
|
|
132
|
-
/**
|
|
133
|
-
* Handle /monitor command
|
|
134
|
-
*/
|
|
135
122
|
const handleMonitor = useCallback((args) => {
|
|
136
123
|
if (args.length === 0) {
|
|
137
124
|
addSystemMessage('Feature name required. Usage: /monitor <feature-name>');
|
|
138
125
|
return;
|
|
139
126
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
127
|
+
const featureName = args[0];
|
|
128
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(featureName)) {
|
|
129
|
+
addSystemMessage('Feature name must contain only letters, numbers, hyphens, and underscores.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Check if it's a tracked background run
|
|
133
|
+
const bgRun = backgroundRuns?.find((r) => r.featureName === featureName);
|
|
134
|
+
if (bgRun) {
|
|
135
|
+
onNavigate('run', { featureName, monitorOnly: true });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Check if the process is running even if not tracked
|
|
139
|
+
try {
|
|
140
|
+
const status = readLoopStatus(featureName);
|
|
141
|
+
if (status.running) {
|
|
142
|
+
onNavigate('run', { featureName, monitorOnly: true });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
148
|
+
addSystemMessage(`Could not check loop status for "${featureName}": ${reason}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
addSystemMessage(`No running loop found for "${featureName}".`);
|
|
152
|
+
}, [addSystemMessage, backgroundRuns, onNavigate]);
|
|
146
153
|
const handleConfig = useCallback((args) => {
|
|
147
|
-
// TODO: Implement config screen or inline config
|
|
148
154
|
if (args.length === 0) {
|
|
149
155
|
addSystemMessage('Config management - not yet implemented in TUI mode. Use CLI: wiggum config');
|
|
150
156
|
}
|
|
@@ -152,19 +158,12 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
152
158
|
addSystemMessage(`Config: ${args.join(' ')} - not yet implemented in TUI mode.`);
|
|
153
159
|
}
|
|
154
160
|
}, [addSystemMessage]);
|
|
155
|
-
/**
|
|
156
|
-
* Handle /exit command
|
|
157
|
-
*/
|
|
158
161
|
const handleExit = useCallback(() => {
|
|
159
162
|
addSystemMessage('Goodbye!');
|
|
160
|
-
// Small delay to show message before exit
|
|
161
163
|
setTimeout(() => {
|
|
162
164
|
exit();
|
|
163
165
|
}, 100);
|
|
164
166
|
}, [addSystemMessage, exit]);
|
|
165
|
-
/**
|
|
166
|
-
* Handle /sync command
|
|
167
|
-
*/
|
|
168
167
|
const handleSync = useCallback(() => {
|
|
169
168
|
if (!sessionState.initialized) {
|
|
170
169
|
addSystemMessage('Project not initialized. Run /init first.');
|
|
@@ -180,9 +179,6 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
180
179
|
}
|
|
181
180
|
sync(sessionState.projectRoot, sessionState.provider, sessionState.model);
|
|
182
181
|
}, [sessionState, syncStatus, addSystemMessage, sync]);
|
|
183
|
-
/**
|
|
184
|
-
* Execute a slash command
|
|
185
|
-
*/
|
|
186
182
|
const executeCommand = useCallback((commandName, args) => {
|
|
187
183
|
switch (commandName) {
|
|
188
184
|
case 'help':
|
|
@@ -213,21 +209,13 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
213
209
|
addSystemMessage(`Unknown command: ${commandName}`);
|
|
214
210
|
}
|
|
215
211
|
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleConfig, handleExit, addSystemMessage]);
|
|
216
|
-
/**
|
|
217
|
-
* Handle natural language input
|
|
218
|
-
*/
|
|
219
212
|
const handleNaturalLanguage = useCallback((_text) => {
|
|
220
|
-
// For now, just show a tip (text parameter reserved for future AI chat)
|
|
221
213
|
addSystemMessage('Tip: Use /help to see available commands, or /new <feature> to create a spec.');
|
|
222
214
|
}, [addSystemMessage]);
|
|
223
|
-
/**
|
|
224
|
-
* Handle user input submission
|
|
225
|
-
*/
|
|
226
215
|
const handleSubmit = useCallback((value) => {
|
|
227
216
|
const parsed = parseInput(value);
|
|
228
217
|
switch (parsed.type) {
|
|
229
218
|
case 'empty':
|
|
230
|
-
// Ignore empty input
|
|
231
219
|
break;
|
|
232
220
|
case 'slash-command': {
|
|
233
221
|
const { command } = parsed;
|
|
@@ -253,20 +241,28 @@ export function MainShell({ sessionState, onNavigate, }) {
|
|
|
253
241
|
addSystemMessage('Use /exit to quit');
|
|
254
242
|
}
|
|
255
243
|
});
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
244
|
+
// Build tips text
|
|
245
|
+
const tips = sessionState.initialized
|
|
246
|
+
? 'Tip: /new <feature> to create spec, /help for commands'
|
|
247
|
+
: 'Tip: /init to set up, /help for commands';
|
|
248
|
+
const inputElement = (_jsx(ChatInput, { onSubmit: handleSubmit, disabled: false, placeholder: "Enter command or type /help...", onCommand: (cmd) => handleSubmit(`/${cmd}`) }));
|
|
249
|
+
return (_jsxs(AppShell, { header: header, tips: tips, isWorking: syncStatus === 'running', workingStatus: syncStatus === 'running' ? 'Syncing project context\u2026' : undefined, input: inputElement, footerStatus: {
|
|
250
|
+
action: projectLabel || 'Main Shell',
|
|
251
|
+
phase: sessionState.provider ? `${sessionState.provider}/${sessionState.model}` : 'No provider',
|
|
252
|
+
path: sessionState.initialized
|
|
253
|
+
? contextAge === 'load error'
|
|
254
|
+
? 'Context: unavailable \u2014 /sync to refresh'
|
|
255
|
+
: contextAge
|
|
256
|
+
? `Context: cached ${contextAge}`
|
|
257
|
+
: 'Context: none \u2014 /sync'
|
|
258
|
+
: 'Not initialized \u2014 /init',
|
|
259
|
+
}, children: [messages.length > 0 && (_jsx(Box, { flexDirection: "column", children: _jsx(MessageList, { messages: messages }) })), syncStatus !== 'idle' && (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(ActionOutput, { actionName: "Sync", description: "Project context", status: syncStatus === 'running'
|
|
260
260
|
? 'running'
|
|
261
261
|
: syncStatus === 'success'
|
|
262
262
|
? 'success'
|
|
263
263
|
: 'error', output: syncStatus === 'running'
|
|
264
|
-
? 'Scanning + AI analysis
|
|
264
|
+
? 'Scanning + AI analysis\u2026'
|
|
265
265
|
: syncStatus === 'success'
|
|
266
266
|
? 'Updated .ralph/.context.json'
|
|
267
|
-
: undefined, error: syncStatus === 'error' ? (syncError?.message || 'Unknown error') : undefined, previewLines: 2 }), syncStatus === 'success' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: colors.green, children: [
|
|
268
|
-
? contextAge
|
|
269
|
-
? `Context: cached ${contextAge}`
|
|
270
|
-
: 'Context: none — /sync'
|
|
271
|
-
: 'Not initialized — /init' })] }));
|
|
267
|
+
: undefined, error: syncStatus === 'error' ? (syncError?.message || 'Unknown error') : undefined, previewLines: 2 }), syncStatus === 'success' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: colors.green, children: [phase.complete, " "] }), _jsx(Text, { children: "Done. Project context updated." })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "What's next:" }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: colors.green, children: theme.chars.prompt }), _jsxs(Text, { color: colors.blue, children: ["/new ", '<feature>'] }), _jsx(Text, { dimColor: true, children: "Create a feature specification" })] })] })] }))] }))] }));
|
|
272
268
|
}
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* RunScreen - TUI screen for running feature loop
|
|
3
3
|
*
|
|
4
4
|
* Spawns feature-loop.sh and polls status files for progress.
|
|
5
|
+
* Wrapped in AppShell for consistent layout.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* - Foreground: spawns the process and monitors it
|
|
9
|
+
* - Monitor-only: polls status files without spawning (for /monitor)
|
|
10
|
+
*
|
|
11
|
+
* Esc backgrounds the run in foreground mode; in monitor mode, Esc returns to shell.
|
|
12
|
+
* On completion, shows RunCompletionSummary inline.
|
|
5
13
|
*/
|
|
6
14
|
import React from 'react';
|
|
7
15
|
import type { SessionState } from '../../repl/session-state.js';
|
|
@@ -14,15 +22,23 @@ export interface RunSummary {
|
|
|
14
22
|
tokensInput: number;
|
|
15
23
|
tokensOutput: number;
|
|
16
24
|
exitCode: number;
|
|
25
|
+
/** True when the exit code was inferred (e.g. monitor mode heuristic) rather than observed directly */
|
|
26
|
+
exitCodeInferred?: boolean;
|
|
17
27
|
branch?: string;
|
|
18
28
|
logPath?: string;
|
|
19
29
|
errorTail?: string;
|
|
20
30
|
}
|
|
21
31
|
export interface RunScreenProps {
|
|
32
|
+
/** Pre-built header element from App */
|
|
33
|
+
header: React.ReactNode;
|
|
22
34
|
featureName: string;
|
|
23
35
|
projectRoot: string;
|
|
24
36
|
sessionState: SessionState;
|
|
37
|
+
/** Monitor-only mode: don't spawn, just poll status */
|
|
38
|
+
monitorOnly?: boolean;
|
|
25
39
|
onComplete: (summary: RunSummary) => void;
|
|
40
|
+
/** Called when user presses Esc to background the run */
|
|
41
|
+
onBackground?: (featureName: string) => void;
|
|
26
42
|
onCancel: () => void;
|
|
27
43
|
}
|
|
28
|
-
export declare function RunScreen({ featureName, projectRoot, sessionState, onComplete, onCancel, }: RunScreenProps): React.ReactElement;
|
|
44
|
+
export declare function RunScreen({ header, featureName, projectRoot, sessionState, monitorOnly, onComplete, onBackground, onCancel, }: RunScreenProps): React.ReactElement;
|