wiggum-cli 0.13.2 → 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.
@@ -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
- cat "$PROMPTS_DIR/PROMPT_verify.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT" || true
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" || true
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" || true
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" 2>/dev/null || true
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
- return (_jsx(RunScreen, { header: headerElement, featureName: featureName, projectRoot: sessionState.projectRoot, sessionState: sessionState, monitorOnly: monitorOnly, onComplete: handleRunComplete, onBackground: handleRunBackground, onCancel: () => navigate('shell') }));
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, deleteCharAfter, moveCursorByWordLeft, moveCursorByWordRight, } from '../utils/input-utils.js';
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
- const isBackspace = key.backspace ||
117
- input === '\x7f' ||
118
- input === '\b' ||
119
- (key.ctrl && input === 'h');
120
- if (isBackspace) {
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
- const isDelete = key.delete || input === '\u001b[3~';
126
- if (isDelete) {
127
- const { newValue, newCursorIndex } = deleteCharAfter(value, cursorOffset);
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 the feature, iterations, tasks, tokens, branch, exit status,
5
- * log tail, and "what's next" section after a feature loop completes.
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 { StatusLine } from './StatusLine.js';
4
- import { colors, theme } from '../theme.js';
5
- import { formatNumber } from '../utils/loop-status.js';
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 recap inline within the
10
- * RunScreen content area.
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
- const totalTokens = summary.tokensInput + summary.tokensOutput;
14
- const stoppedCodes = new Set([130, 143]);
15
- const exitState = summary.exitCode === 0
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, message: 'Stopped. Feature loop interrupted.' }
33
+ ? { label: 'Stopped', color: colors.orange }
19
34
  : summary.exitCodeInferred
20
- ? { label: 'Unknown', color: colors.orange, message: `Done. Exit status uncertain (inferred code ${summary.exitCode}). Check logs for details.` }
21
- : { label: 'Failed', color: colors.pink, message: `Done. Feature loop exited with code ${summary.exitCode}.` };
22
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(StatusLine, { action: "Run Loop", phase: exitState.label, path: summary.feature }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Summary" }), _jsxs(Text, { children: ["- Feature: ", summary.feature] }), _jsxs(Text, { children: ["- Iterations: ", summary.iterations, "/", summary.maxIterations] }), _jsxs(Text, { children: ["- Tasks: ", summary.tasksDone, "/", summary.tasksTotal] }), _jsxs(Text, { children: ["- Tokens: ", formatNumber(totalTokens), " (in:", formatNumber(summary.tokensInput), " out:", formatNumber(summary.tokensOutput), ")"] }), summary.branch && summary.branch !== '-' && (_jsxs(Text, { children: ["- Branch: ", summary.branch] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsxs(Text, { color: exitState.color, children: [theme.chars.bullet, " "] }), _jsx(Text, { children: exitState.message })] }), (summary.errorTail || summary.logPath) && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [summary.logPath && (_jsxs(Text, { dimColor: true, children: ["Log: ", summary.logPath] })), summary.errorTail && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Last output:" }), summary.errorTail.split('\n').map((line, idx) => (_jsx(Text, { dimColor: true, children: line }, `${line}-${idx}`)))] }))] })), _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 }), _jsx(Text, { dimColor: true, children: "Review changes and open a PR if needed" })] }), _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 another feature specification" })] }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: colors.green, children: theme.chars.prompt }), _jsx(Text, { color: colors.blue, children: "/help" }), _jsx(Text, { dimColor: true, children: "See all commands" })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter or Esc to return to shell" }) })] }));
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
- const featureName = args[0];
120
- onNavigate('run', { featureName });
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) {