wiggum-cli 0.13.1 → 0.14.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/templates/scripts/feature-loop.sh.tmpl +81 -4
- package/dist/tui/app.js +2 -1
- package/dist/tui/components/ChatInput.js +27 -9
- package/dist/tui/components/RunCompletionSummary.d.ts +3 -9
- package/dist/tui/components/RunCompletionSummary.js +59 -14
- package/dist/tui/components/SummaryBox.d.ts +59 -0
- package/dist/tui/components/SummaryBox.js +97 -0
- package/dist/tui/screens/MainShell.js +23 -2
- package/dist/tui/screens/RunScreen.d.ts +116 -1
- package/dist/tui/screens/RunScreen.js +33 -5
- package/dist/tui/utils/build-run-summary.d.ts +24 -0
- package/dist/tui/utils/build-run-summary.js +241 -0
- package/dist/tui/utils/git-summary.d.ts +24 -0
- package/dist/tui/utils/git-summary.js +63 -0
- package/dist/tui/utils/input-utils.d.ts +20 -0
- package/dist/tui/utils/input-utils.js +27 -0
- package/dist/tui/utils/pr-summary.d.ts +34 -0
- package/dist/tui/utils/pr-summary.js +84 -0
- package/dist/utils/summary-file.d.ts +25 -0
- package/dist/utils/summary-file.js +37 -0
- package/package.json +1 -1
- package/src/templates/scripts/feature-loop.sh.tmpl +81 -4
|
@@ -105,6 +105,8 @@ TOKENS_FILE="/tmp/ralph-loop-${1}.tokens"
|
|
|
105
105
|
CLAUDE_OUTPUT="/tmp/ralph-loop-${1}.output"
|
|
106
106
|
STATUS_FILE="/tmp/ralph-loop-${1}.status"
|
|
107
107
|
FINAL_STATUS_FILE="/tmp/ralph-loop-${1}.final"
|
|
108
|
+
PHASES_FILE="/tmp/ralph-loop-${1}.phases"
|
|
109
|
+
BASELINE_FILE="/tmp/ralph-loop-${1}.baseline"
|
|
108
110
|
|
|
109
111
|
# Initialize token tracking
|
|
110
112
|
init_tokens() {
|
|
@@ -139,6 +141,33 @@ parse_and_accumulate_tokens() {
|
|
|
139
141
|
# Initialize tokens
|
|
140
142
|
init_tokens
|
|
141
143
|
|
|
144
|
+
# Phase tracking helpers
|
|
145
|
+
write_phase_start() {
|
|
146
|
+
local phase_id="$1"
|
|
147
|
+
local timestamp=$(date +%s)
|
|
148
|
+
echo "${phase_id}|started|${timestamp}|" >> "$PHASES_FILE"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
write_phase_end() {
|
|
152
|
+
local phase_id="$1"
|
|
153
|
+
local status="$2" # success, failed, skipped
|
|
154
|
+
local timestamp=$(date +%s)
|
|
155
|
+
|
|
156
|
+
# Find the last line for this phase and update it
|
|
157
|
+
if [ -f "$PHASES_FILE" ]; then
|
|
158
|
+
# Get all lines except the last matching phase line
|
|
159
|
+
grep -v "^${phase_id}|started|" "$PHASES_FILE" > "${PHASES_FILE}.tmp" 2>/dev/null || true
|
|
160
|
+
# Find the start timestamp from the original file
|
|
161
|
+
local start_ts=$(grep "^${phase_id}|started|" "$PHASES_FILE" | tail -1 | cut -d'|' -f3)
|
|
162
|
+
# Write updated line
|
|
163
|
+
echo "${phase_id}|${status}|${start_ts}|${timestamp}" >> "${PHASES_FILE}.tmp"
|
|
164
|
+
mv "${PHASES_FILE}.tmp" "$PHASES_FILE"
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Initialize phase tracking
|
|
169
|
+
> "$PHASES_FILE"
|
|
170
|
+
|
|
142
171
|
FEATURE="${1:?Usage: ./feature-loop.sh <feature-name> [max-iterations] [max-e2e-attempts] [--worktree] [--resume] [--model MODEL]}"
|
|
143
172
|
MAX_ITERATIONS="${2:-$DEFAULT_MAX_ITERATIONS}"
|
|
144
173
|
MAX_E2E_ATTEMPTS="${3:-$DEFAULT_MAX_E2E}"
|
|
@@ -194,22 +223,42 @@ fi
|
|
|
194
223
|
# Create output file for monitoring
|
|
195
224
|
touch "$CLAUDE_OUTPUT"
|
|
196
225
|
|
|
226
|
+
# Record baseline commit
|
|
227
|
+
if git rev-parse --git-dir > /dev/null 2>&1; then
|
|
228
|
+
BASELINE_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
229
|
+
if [ -n "$BASELINE_COMMIT" ]; then
|
|
230
|
+
echo "$BASELINE_COMMIT" > "$BASELINE_FILE"
|
|
231
|
+
echo "Baseline commit: $BASELINE_COMMIT"
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
|
|
197
235
|
# Phase 3: Planning (if no implementation plan exists)
|
|
198
236
|
if [ ! -f "$PLAN_FILE" ]; then
|
|
199
237
|
echo "======================== PLANNING PHASE ========================"
|
|
238
|
+
write_phase_start "planning"
|
|
200
239
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
201
240
|
cat "$PROMPTS_DIR/PROMPT_feature.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT" || {
|
|
202
241
|
echo "ERROR: Planning phase failed"
|
|
242
|
+
write_phase_end "planning" "failed"
|
|
203
243
|
exit 1
|
|
204
244
|
}
|
|
205
245
|
parse_and_accumulate_tokens "$CLAUDE_OUTPUT"
|
|
246
|
+
write_phase_end "planning" "success"
|
|
247
|
+
else
|
|
248
|
+
echo "Plan file exists, skipping planning phase"
|
|
249
|
+
write_phase_start "planning"
|
|
250
|
+
write_phase_end "planning" "skipped"
|
|
206
251
|
fi
|
|
207
252
|
|
|
208
253
|
# Phase 4: Implementation loop
|
|
209
254
|
echo "======================== IMPLEMENTATION PHASE ========================"
|
|
255
|
+
write_phase_start "implementation"
|
|
256
|
+
IMPL_SUCCESS=true
|
|
210
257
|
while true; do
|
|
211
258
|
if [ $ITERATION -ge $MAX_ITERATIONS ]; then
|
|
212
259
|
echo "Reached max iterations: $MAX_ITERATIONS"
|
|
260
|
+
IMPL_SUCCESS=false
|
|
261
|
+
write_phase_end "implementation" "failed"
|
|
213
262
|
exit 1
|
|
214
263
|
fi
|
|
215
264
|
|
|
@@ -231,13 +280,20 @@ while true; do
|
|
|
231
280
|
|
|
232
281
|
sleep 2
|
|
233
282
|
done
|
|
283
|
+
if [ "$IMPL_SUCCESS" = true ]; then
|
|
284
|
+
write_phase_end "implementation" "success"
|
|
285
|
+
fi
|
|
234
286
|
|
|
235
287
|
# Phase 5: E2E Testing
|
|
236
288
|
echo "======================== E2E TESTING PHASE ========================"
|
|
237
289
|
E2E_TOTAL=$({ grep "^- \[.\].*E2E:" "$PLAN_FILE" 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
238
290
|
if [ "$E2E_TOTAL" -eq 0 ]; then
|
|
239
291
|
echo "No E2E scenarios defined, skipping E2E phase."
|
|
292
|
+
write_phase_start "e2e_testing"
|
|
293
|
+
write_phase_end "e2e_testing" "skipped"
|
|
240
294
|
else
|
|
295
|
+
write_phase_start "e2e_testing"
|
|
296
|
+
E2E_SUCCESS=false
|
|
241
297
|
E2E_ATTEMPT=0
|
|
242
298
|
while [ $E2E_ATTEMPT -lt $MAX_E2E_ATTEMPTS ]; do
|
|
243
299
|
E2E_ATTEMPT=$((E2E_ATTEMPT + 1))
|
|
@@ -253,6 +309,7 @@ else
|
|
|
253
309
|
|
|
254
310
|
if [ "$E2E_FAILED" -eq 0 ] && [ "$E2E_PENDING" -eq 0 ]; then
|
|
255
311
|
echo "All E2E tests passed!"
|
|
312
|
+
E2E_SUCCESS=true
|
|
256
313
|
break
|
|
257
314
|
fi
|
|
258
315
|
|
|
@@ -262,26 +319,46 @@ else
|
|
|
262
319
|
parse_and_accumulate_tokens "$CLAUDE_OUTPUT"
|
|
263
320
|
fi
|
|
264
321
|
done
|
|
322
|
+
|
|
323
|
+
if [ "$E2E_SUCCESS" = true ]; then
|
|
324
|
+
write_phase_end "e2e_testing" "success"
|
|
325
|
+
else
|
|
326
|
+
write_phase_end "e2e_testing" "failed"
|
|
327
|
+
fi
|
|
265
328
|
fi
|
|
266
329
|
|
|
267
330
|
# Phase 6: Spec Verification
|
|
268
331
|
echo "======================== SPEC VERIFICATION PHASE ========================"
|
|
332
|
+
write_phase_start "verification"
|
|
269
333
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
270
|
-
|
|
334
|
+
VERIFY_STATUS="success"
|
|
335
|
+
if ! cat "$PROMPTS_DIR/PROMPT_verify.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"; then
|
|
336
|
+
VERIFY_STATUS="failed"
|
|
337
|
+
fi
|
|
271
338
|
parse_and_accumulate_tokens "$CLAUDE_OUTPUT"
|
|
339
|
+
write_phase_end "verification" "$VERIFY_STATUS"
|
|
272
340
|
|
|
273
341
|
# Phase 7: PR and Review
|
|
274
342
|
echo "======================== PR & REVIEW PHASE ========================"
|
|
343
|
+
write_phase_start "pr_review"
|
|
275
344
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
345
|
+
PR_STATUS="success"
|
|
276
346
|
if [ "$REVIEW_MODE" = "manual" ]; then
|
|
277
|
-
cat "$PROMPTS_DIR/PROMPT_review_manual.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"
|
|
347
|
+
if ! cat "$PROMPTS_DIR/PROMPT_review_manual.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"; then
|
|
348
|
+
PR_STATUS="failed"
|
|
349
|
+
fi
|
|
278
350
|
else
|
|
279
|
-
cat "$PROMPTS_DIR/PROMPT_review_auto.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"
|
|
351
|
+
if ! cat "$PROMPTS_DIR/PROMPT_review_auto.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"; then
|
|
352
|
+
PR_STATUS="failed"
|
|
353
|
+
fi
|
|
280
354
|
fi
|
|
281
355
|
parse_and_accumulate_tokens "$CLAUDE_OUTPUT"
|
|
356
|
+
write_phase_end "pr_review" "$PR_STATUS"
|
|
282
357
|
|
|
283
358
|
# Persist final status for TUI summaries
|
|
284
|
-
echo "$ITERATION|$MAX_ITERATIONS|$(date +%s)|done" > "$FINAL_STATUS_FILE"
|
|
359
|
+
if ! echo "$ITERATION|$MAX_ITERATIONS|$(date +%s)|done" > "$FINAL_STATUS_FILE"; then
|
|
360
|
+
echo "WARNING: Failed to write final status file: $FINAL_STATUS_FILE" >&2
|
|
361
|
+
fi
|
|
285
362
|
|
|
286
363
|
# Cleanup temp files
|
|
287
364
|
rm -f "$STATUS_FILE" 2>/dev/null || true
|
package/dist/tui/app.js
CHANGED
|
@@ -182,7 +182,8 @@ interviewProps, onComplete, onExit, }) {
|
|
|
182
182
|
if (!featureName || typeof featureName !== 'string') {
|
|
183
183
|
return null; // useEffect will redirect to shell
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
const reviewMode = screenProps?.reviewMode;
|
|
186
|
+
return (_jsx(RunScreen, { header: headerElement, featureName: featureName, projectRoot: sessionState.projectRoot, sessionState: sessionState, monitorOnly: monitorOnly, reviewMode: reviewMode, onComplete: handleRunComplete, onBackground: handleRunBackground, onCancel: () => navigate('shell') }));
|
|
186
187
|
}
|
|
187
188
|
default: {
|
|
188
189
|
// Return fallback UI instead of calling navigate() during render (which would be setState during render).
|
|
@@ -18,7 +18,7 @@ import { Box, Text, useInput } from 'ink';
|
|
|
18
18
|
import { theme } from '../theme.js';
|
|
19
19
|
import { CommandDropdown, DEFAULT_COMMANDS } from './CommandDropdown.js';
|
|
20
20
|
import { useCommandHistory } from '../hooks/useCommandHistory.js';
|
|
21
|
-
import { normalizePastedText, insertTextAtCursor, deleteCharBefore,
|
|
21
|
+
import { normalizePastedText, insertTextAtCursor, deleteCharBefore, deleteWordBefore, moveCursorByWordLeft, moveCursorByWordRight, } from '../utils/input-utils.js';
|
|
22
22
|
/**
|
|
23
23
|
* ChatInput component
|
|
24
24
|
*
|
|
@@ -113,21 +113,39 @@ export function ChatInput({ onSubmit, placeholder = 'Type your message...', disa
|
|
|
113
113
|
handleSubmit(value);
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
// Backspace: Ink v5 maps macOS Backspace (\x7f) to key.delete, not
|
|
117
|
+
// key.backspace. Since there is no reliable way to distinguish it from
|
|
118
|
+
// forward-Delete (\u001b[3~] — also key.delete), treat both as backspace,
|
|
119
|
+
// matching ink-text-input's approach.
|
|
120
|
+
const isBackspaceOrDelete = key.backspace || key.delete;
|
|
121
|
+
if (isBackspaceOrDelete) {
|
|
121
122
|
const { newValue, newCursorIndex } = deleteCharBefore(value, cursorOffset);
|
|
122
123
|
updateValue(newValue, newCursorIndex);
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
126
|
+
// Readline keybindings (before the blanket ctrl guard)
|
|
127
|
+
if (key.ctrl && input === 'a') {
|
|
128
|
+
updateValue(value, 0, true);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (key.ctrl && input === 'e') {
|
|
132
|
+
updateValue(value, value.length, true);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (key.ctrl && input === 'w') {
|
|
136
|
+
const { newValue, newCursorIndex } = deleteWordBefore(value, cursorOffset);
|
|
128
137
|
updateValue(newValue, newCursorIndex);
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
140
|
+
if (key.ctrl && input === 'u') {
|
|
141
|
+
updateValue(value.slice(cursorOffset), 0);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (key.ctrl && input === 'k') {
|
|
145
|
+
updateValue(value.slice(0, cursorOffset), cursorOffset, true);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Blanket guard for remaining unhandled ctrl combos
|
|
131
149
|
if (key.ctrl) {
|
|
132
150
|
return;
|
|
133
151
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RunCompletionSummary - Displays run loop completion recap
|
|
2
|
+
* RunCompletionSummary - Displays enhanced run loop completion recap
|
|
3
3
|
*
|
|
4
|
-
* Shows
|
|
5
|
-
*
|
|
4
|
+
* Shows a bordered summary box with timing, phases, iterations, tasks, code changes,
|
|
5
|
+
* commits, and PR/issue links after a feature loop completes.
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import type { RunSummary } from '../screens/RunScreen.js';
|
|
@@ -13,10 +13,4 @@ export interface RunCompletionSummaryProps {
|
|
|
13
13
|
/** Run summary data */
|
|
14
14
|
summary: RunSummary;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
* RunCompletionSummary component
|
|
18
|
-
*
|
|
19
|
-
* Renders the run loop completion recap inline within the
|
|
20
|
-
* RunScreen content area.
|
|
21
|
-
*/
|
|
22
16
|
export declare function RunCompletionSummary({ summary, }: RunCompletionSummaryProps): React.ReactElement;
|
|
@@ -1,23 +1,68 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
4
|
-
import { colors,
|
|
5
|
-
|
|
3
|
+
import { SummaryBox, SummaryBoxSection } from './SummaryBox.js';
|
|
4
|
+
import { colors, phase } from '../theme.js';
|
|
5
|
+
/**
|
|
6
|
+
* Format milliseconds to human-readable duration (e.g., "12m 34s", "1h 15m 0s")
|
|
7
|
+
*/
|
|
8
|
+
function formatDurationMs(ms) {
|
|
9
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
10
|
+
return 'Unknown';
|
|
11
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
12
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
13
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
14
|
+
const seconds = totalSeconds % 60;
|
|
15
|
+
if (hours > 0)
|
|
16
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
17
|
+
if (minutes > 0)
|
|
18
|
+
return `${minutes}m ${seconds}s`;
|
|
19
|
+
return `${seconds}s`;
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* RunCompletionSummary component
|
|
8
23
|
*
|
|
9
|
-
* Renders the run loop completion
|
|
10
|
-
*
|
|
24
|
+
* Renders the enhanced run loop completion summary using SummaryBox.
|
|
25
|
+
* Displays header, timing/iterations/tasks, phases, changes/commits, and PR/issue links.
|
|
11
26
|
*/
|
|
27
|
+
const stoppedCodes = new Set([130, 143]);
|
|
12
28
|
export function RunCompletionSummary({ summary, }) {
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
? { label: 'Complete', color: colors.green, message: 'Done. Feature loop completed successfully.' }
|
|
29
|
+
// Determine final status and color
|
|
30
|
+
const exitStatus = summary.exitCode === 0
|
|
31
|
+
? { label: 'Complete', color: colors.green }
|
|
17
32
|
: stoppedCodes.has(summary.exitCode)
|
|
18
|
-
? { label: 'Stopped', color: colors.orange
|
|
33
|
+
? { label: 'Stopped', color: colors.orange }
|
|
19
34
|
: summary.exitCodeInferred
|
|
20
|
-
? { label: 'Unknown', color: colors.orange
|
|
21
|
-
: { label: 'Failed', color: colors.pink
|
|
22
|
-
|
|
35
|
+
? { label: 'Unknown', color: colors.orange }
|
|
36
|
+
: { label: 'Failed', color: colors.pink };
|
|
37
|
+
// Use enhanced iteration data if available, fallback to legacy
|
|
38
|
+
const iterationsTotal = summary.iterationBreakdown?.total ?? summary.iterations;
|
|
39
|
+
const iterationsImpl = summary.iterationBreakdown?.implementation;
|
|
40
|
+
const iterationsResumes = summary.iterationBreakdown?.resumes;
|
|
41
|
+
// Format iterations with breakdown if available
|
|
42
|
+
const iterationsDisplay = iterationsImpl !== undefined && iterationsResumes !== undefined
|
|
43
|
+
? `${iterationsTotal} (${iterationsImpl} impl + ${iterationsResumes} resume)`
|
|
44
|
+
: String(iterationsTotal);
|
|
45
|
+
// Tasks: use enhanced field if available, fallback to legacy
|
|
46
|
+
const tasksCompleted = summary.tasks?.completed ?? summary.tasksDone;
|
|
47
|
+
const tasksTotal = summary.tasks?.total ?? summary.tasksTotal;
|
|
48
|
+
const tasksDisplay = tasksCompleted !== null && tasksTotal !== null
|
|
49
|
+
? `${tasksCompleted}/${tasksTotal} completed`
|
|
50
|
+
: 'Not available';
|
|
51
|
+
return (_jsxs(SummaryBox, { minWidth: 60, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: summary.feature }), _jsx(Text, { bold: true, color: exitStatus.color, children: exitStatus.label })] }), _jsxs(SummaryBoxSection, { children: [summary.totalDurationMs !== undefined ? (_jsxs(Text, { children: ["Duration: ", formatDurationMs(summary.totalDurationMs)] })) : (_jsx(Text, { children: "Duration: Not available" })), _jsxs(Text, { children: ["Iterations: ", iterationsDisplay] }), _jsxs(Text, { children: ["Tasks: ", tasksDisplay] })] }), _jsxs(SummaryBoxSection, { children: [_jsx(Text, { bold: true, children: "Phases" }), summary.phases && summary.phases.length > 0 ? (summary.phases.map((phaseInfo) => {
|
|
52
|
+
const statusIcon = phaseInfo.status === 'success' ? phase.complete :
|
|
53
|
+
phaseInfo.status === 'failed' ? phase.error :
|
|
54
|
+
phase.pending;
|
|
55
|
+
const statusColor = phaseInfo.status === 'success' ? colors.green :
|
|
56
|
+
phaseInfo.status === 'failed' ? colors.pink :
|
|
57
|
+
colors.gray;
|
|
58
|
+
const durationText = phaseInfo.durationMs !== undefined
|
|
59
|
+
? formatDurationMs(phaseInfo.durationMs)
|
|
60
|
+
: 'Not available';
|
|
61
|
+
const iterationsText = phaseInfo.iterations !== undefined && phaseInfo.iterations > 0
|
|
62
|
+
? ` (${phaseInfo.iterations} iterations)`
|
|
63
|
+
: '';
|
|
64
|
+
const statusText = phaseInfo.status === 'skipped' ? ' skipped' :
|
|
65
|
+
phaseInfo.status === 'failed' ? ' failed' : '';
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " "] }), _jsxs(Text, { children: [phaseInfo.label, " ", durationText, iterationsText, statusText] })] }, phaseInfo.id));
|
|
67
|
+
})) : (_jsx(Text, { children: "No phase information available" }))] }), _jsxs(SummaryBoxSection, { children: [_jsx(Text, { bold: true, children: "Changes" }), summary.changes ? (!summary.changes.available ? (_jsx(Text, { children: "Changes: Not available" })) : summary.changes.totalFilesChanged === 0 || (summary.changes.files && summary.changes.files.length === 0) ? (_jsx(Text, { children: "No changes" })) : summary.changes.totalFilesChanged !== undefined || summary.changes.files ? (_jsxs(_Fragment, { children: [summary.changes.totalFilesChanged !== undefined && (_jsxs(Text, { children: [summary.changes.totalFilesChanged, " file", summary.changes.totalFilesChanged !== 1 ? 's' : '', " changed"] })), summary.changes.files && summary.changes.files.map((file) => (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { children: [file.path, " "] }), _jsxs(Text, { color: colors.green, children: ["+", file.added, " "] }), _jsxs(Text, { color: colors.pink, children: ["-", file.removed] }), _jsx(Text, { children: " lines" })] }, file.path)))] })) : (_jsx(Text, { children: "Changes: Could not compute diff" }))) : (_jsx(Text, { children: "Changes: Not available" })), summary.commits ? (!summary.commits.available ? (_jsx(Text, { children: "Commit: Not available" })) : summary.commits.fromHash && summary.commits.toHash ? (_jsxs(Text, { children: ["Commit: ", summary.commits.fromHash, " \u2192 ", summary.commits.toHash, summary.commits.mergeType === 'squash' && ' (squash-merged)', summary.commits.mergeType === 'normal' && ' (merged)'] })) : summary.commits.toHash ? (_jsxs(Text, { children: ["Commit: ", summary.commits.toHash] })) : (_jsx(Text, { children: "Commit: Not available" }))) : (_jsx(Text, { children: "Commit: Not available" }))] }), _jsxs(SummaryBoxSection, { children: [summary.pr ? (_jsx(_Fragment, { children: !summary.pr.available ? (_jsx(Text, { children: "PR: Not available" })) : summary.pr.created && summary.pr.number && summary.pr.url ? (_jsxs(Text, { children: ["PR #", summary.pr.number, ": ", summary.pr.url] })) : (_jsx(Text, { children: "PR: Not created" })) })) : (_jsx(Text, { children: "PR: Not available" })), summary.issue ? (_jsx(_Fragment, { children: !summary.issue.available ? (_jsx(Text, { children: "Issue: Not available" })) : summary.issue.linked && summary.issue.number ? (_jsxs(Text, { children: ["Issue #", summary.issue.number, ": ", summary.issue.status || 'Linked', summary.issue.url && ` (${summary.issue.url})`] })) : (_jsx(Text, { children: "Issue: Not linked" })) })) : (_jsx(Text, { children: "Issue: Not available" }))] })] }));
|
|
23
68
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SummaryBox - Bordered box wrapper for enhanced run summary
|
|
3
|
+
*
|
|
4
|
+
* Draws a bordered box using box-drawing characters that adapts to
|
|
5
|
+
* terminal width. Provides section separators and content padding.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
/**
|
|
9
|
+
* Props for SummaryBox component
|
|
10
|
+
*/
|
|
11
|
+
export interface SummaryBoxProps {
|
|
12
|
+
/** Child content to render inside the box */
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/** Minimum box width in columns (default: 60) */
|
|
15
|
+
minWidth?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Props for SummaryBoxSection component
|
|
19
|
+
*/
|
|
20
|
+
export interface SummaryBoxSectionProps {
|
|
21
|
+
/** Section content */
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* SummaryBox component
|
|
26
|
+
*
|
|
27
|
+
* Renders a bordered box with top/bottom borders and section separators.
|
|
28
|
+
* Adapts to terminal width while respecting minimum width.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* <SummaryBox>
|
|
33
|
+
* <Text>Header content</Text>
|
|
34
|
+
* <SummaryBoxSection>
|
|
35
|
+
* <Text>Section 1</Text>
|
|
36
|
+
* </SummaryBoxSection>
|
|
37
|
+
* </SummaryBox>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function SummaryBox({ children, minWidth, }: SummaryBoxProps): React.ReactElement;
|
|
41
|
+
/**
|
|
42
|
+
* SummaryBoxSection component
|
|
43
|
+
*
|
|
44
|
+
* Marks a section boundary within a SummaryBox. The parent SummaryBox
|
|
45
|
+
* will render a separator line (├─────┤) before this section's content.
|
|
46
|
+
*
|
|
47
|
+
* This is a marker component - the actual rendering is handled by SummaryBox.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <SummaryBox>
|
|
52
|
+
* <Text>Header</Text>
|
|
53
|
+
* <SummaryBoxSection>
|
|
54
|
+
* <Text>Section content</Text>
|
|
55
|
+
* </SummaryBoxSection>
|
|
56
|
+
* </SummaryBox>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function SummaryBoxSection({ children, }: SummaryBoxSectionProps): React.ReactElement;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* SummaryBox - Bordered box wrapper for enhanced run summary
|
|
4
|
+
*
|
|
5
|
+
* Draws a bordered box using box-drawing characters that adapts to
|
|
6
|
+
* terminal width. Provides section separators and content padding.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { Box, Text, useStdout } from 'ink';
|
|
10
|
+
import { box, colors } from '../theme.js';
|
|
11
|
+
/**
|
|
12
|
+
* SummaryBox component
|
|
13
|
+
*
|
|
14
|
+
* Renders a bordered box with top/bottom borders and section separators.
|
|
15
|
+
* Adapts to terminal width while respecting minimum width.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <SummaryBox>
|
|
20
|
+
* <Text>Header content</Text>
|
|
21
|
+
* <SummaryBoxSection>
|
|
22
|
+
* <Text>Section 1</Text>
|
|
23
|
+
* </SummaryBoxSection>
|
|
24
|
+
* </SummaryBox>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function SummaryBox({ children, minWidth = 60, }) {
|
|
28
|
+
const { stdout } = useStdout();
|
|
29
|
+
const terminalWidth = stdout?.columns ?? 80;
|
|
30
|
+
// Use terminal width, but respect minimum width
|
|
31
|
+
const boxWidth = Math.max(minWidth, terminalWidth);
|
|
32
|
+
const contentWidth = boxWidth - 4; // Account for borders and padding
|
|
33
|
+
// Top border: ┌─────┐
|
|
34
|
+
const topBorder = box.topLeft + box.horizontal.repeat(boxWidth - 2) + box.topRight;
|
|
35
|
+
// Bottom border: └─────┘
|
|
36
|
+
const bottomBorder = box.bottomLeft + box.horizontal.repeat(boxWidth - 2) + box.bottomRight;
|
|
37
|
+
// Section separator: ├─────┤
|
|
38
|
+
const leftJunction = '\u251c'; // ├
|
|
39
|
+
const rightJunction = '\u2524'; // ┤
|
|
40
|
+
const sectionSeparator = leftJunction + box.horizontal.repeat(boxWidth - 2) + rightJunction;
|
|
41
|
+
const elements = [];
|
|
42
|
+
React.Children.forEach(children, (child) => {
|
|
43
|
+
if (!child)
|
|
44
|
+
return;
|
|
45
|
+
// Check if this is a SummaryBoxSection
|
|
46
|
+
if (React.isValidElement(child) &&
|
|
47
|
+
child.type === SummaryBoxSection) {
|
|
48
|
+
// Add section separator line
|
|
49
|
+
elements.push(_jsx(Text, { color: colors.separator, children: sectionSeparator }, `sep-${elements.length}`));
|
|
50
|
+
// Add the section content (children of SummaryBoxSection)
|
|
51
|
+
const sectionChildren = child.props.children;
|
|
52
|
+
React.Children.forEach(sectionChildren, (sectionChild) => {
|
|
53
|
+
if (sectionChild) {
|
|
54
|
+
elements.push(sectionChild);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Regular content
|
|
60
|
+
elements.push(child);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return (_jsxs(Box, { flexDirection: "column", width: boxWidth, children: [_jsx(Text, { color: colors.separator, children: topBorder }), _jsx(Box, { flexDirection: "column", children: elements.map((child, index) => {
|
|
64
|
+
// Check if this is a separator line (Text with the separator)
|
|
65
|
+
if (React.isValidElement(child) &&
|
|
66
|
+
child.type === Text &&
|
|
67
|
+
typeof child.props.children === 'string' &&
|
|
68
|
+
child.props.children.startsWith(leftJunction)) {
|
|
69
|
+
// Render separator without side borders
|
|
70
|
+
return _jsx(React.Fragment, { children: child }, index);
|
|
71
|
+
}
|
|
72
|
+
// Wrap regular content with vertical borders
|
|
73
|
+
return (_jsxs(Box, { flexDirection: "row", width: boxWidth, children: [_jsxs(Text, { color: colors.separator, children: [box.vertical, " "] }), _jsx(Box, { width: contentWidth, flexShrink: 0, overflow: "hidden", children: child }), _jsxs(Text, { color: colors.separator, children: [" ", box.vertical] })] }, index));
|
|
74
|
+
}) }), _jsx(Text, { color: colors.separator, children: bottomBorder })] }));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* SummaryBoxSection component
|
|
78
|
+
*
|
|
79
|
+
* Marks a section boundary within a SummaryBox. The parent SummaryBox
|
|
80
|
+
* will render a separator line (├─────┤) before this section's content.
|
|
81
|
+
*
|
|
82
|
+
* This is a marker component - the actual rendering is handled by SummaryBox.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```tsx
|
|
86
|
+
* <SummaryBox>
|
|
87
|
+
* <Text>Header</Text>
|
|
88
|
+
* <SummaryBoxSection>
|
|
89
|
+
* <Text>Section content</Text>
|
|
90
|
+
* </SummaryBoxSection>
|
|
91
|
+
* </SummaryBox>
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function SummaryBoxSection({ children, }) {
|
|
95
|
+
// This component is just a marker - actual rendering happens in SummaryBox
|
|
96
|
+
return _jsx(_Fragment, { children: children });
|
|
97
|
+
}
|
|
@@ -116,8 +116,29 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
116
116
|
addSystemMessage('Project not initialized. Run /init first.');
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Parse optional flags, separating them from positional args
|
|
120
|
+
let reviewMode;
|
|
121
|
+
const positional = [];
|
|
122
|
+
for (let i = 0; i < args.length; i++) {
|
|
123
|
+
if (args[i] === '--review-mode') {
|
|
124
|
+
if (i + 1 < args.length) {
|
|
125
|
+
reviewMode = args[i + 1];
|
|
126
|
+
i++; // skip the value
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
positional.push(args[i]);
|
|
131
|
+
}
|
|
132
|
+
if (reviewMode !== undefined && reviewMode !== 'manual' && reviewMode !== 'auto') {
|
|
133
|
+
addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual' or 'auto'.`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const featureName = positional[0];
|
|
137
|
+
if (!featureName) {
|
|
138
|
+
addSystemMessage('Feature name required. Usage: /run <feature-name> [--review-mode auto|manual]');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
onNavigate('run', { featureName, reviewMode });
|
|
121
142
|
}, [sessionState.initialized, addSystemMessage, onNavigate]);
|
|
122
143
|
const handleMonitor = useCallback((args) => {
|
|
123
144
|
if (args.length === 0) {
|