wiggum-cli 0.17.0 → 0.17.2
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/agent/orchestrator.js +8 -2
- package/dist/agent/tools/backlog.d.ts +1 -0
- package/dist/agent/tools/backlog.js +4 -1
- package/dist/agent/types.d.ts +1 -0
- package/dist/commands/agent.d.ts +1 -0
- package/dist/commands/agent.js +1 -0
- package/dist/index.js +13 -0
- package/dist/repl/command-parser.d.ts +5 -0
- package/dist/repl/command-parser.js +5 -0
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +3 -1
- package/dist/tui/components/IssuePicker.js +24 -15
- package/dist/tui/components/StatusLine.d.ts +3 -1
- package/dist/tui/components/StatusLine.js +2 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +12 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +48 -9
- package/dist/tui/screens/AgentScreen.d.ts +4 -1
- package/dist/tui/screens/AgentScreen.js +70 -19
- package/dist/tui/screens/MainShell.js +80 -4
- package/dist/tui/screens/RunScreen.js +34 -10
- package/dist/tui/utils/git-summary.d.ts +1 -0
- package/dist/tui/utils/git-summary.js +16 -0
- package/package.json +1 -1
|
@@ -152,6 +152,9 @@ export function buildConstraints(config) {
|
|
|
152
152
|
if (config.labels?.length) {
|
|
153
153
|
lines.push(`- Only work on issues with these labels: ${config.labels.join(', ')}. Ignore all others.`);
|
|
154
154
|
}
|
|
155
|
+
if (config.issues?.length) {
|
|
156
|
+
lines.push(`- ONLY work on these specific issues: ${config.issues.map(n => `#${n}`).join(', ')}. Ignore all others.`);
|
|
157
|
+
}
|
|
155
158
|
if (config.dryRun) {
|
|
156
159
|
lines.push('- DRY RUN MODE: Plan what you would do but do NOT execute. Execution and reporting tools return simulated results.');
|
|
157
160
|
}
|
|
@@ -165,6 +168,7 @@ export function createAgentOrchestrator(config) {
|
|
|
165
168
|
const store = new MemoryStore(memoryDir);
|
|
166
169
|
const backlog = createBacklogTools(owner, repo, {
|
|
167
170
|
defaultLabels: config.labels,
|
|
171
|
+
issueNumbers: config.issues,
|
|
168
172
|
});
|
|
169
173
|
const memory = createMemoryTools(store, projectRoot);
|
|
170
174
|
const execution = config.dryRun
|
|
@@ -185,7 +189,9 @@ export function createAgentOrchestrator(config) {
|
|
|
185
189
|
...introspection,
|
|
186
190
|
...featureState,
|
|
187
191
|
};
|
|
188
|
-
const
|
|
192
|
+
const effectiveMaxItems = config.maxItems ?? (config.issues?.length ? config.issues.length : undefined);
|
|
193
|
+
const constraintConfig = { ...config, maxItems: effectiveMaxItems };
|
|
194
|
+
const constraints = buildConstraints(constraintConfig);
|
|
189
195
|
const runtimeConfig = buildRuntimeConfig(config);
|
|
190
196
|
const fullPrompt = AGENT_SYSTEM_PROMPT + runtimeConfig + constraints;
|
|
191
197
|
const completedIssues = new Set();
|
|
@@ -205,7 +211,7 @@ export function createAgentOrchestrator(config) {
|
|
|
205
211
|
stopWhen: ({ steps }) => {
|
|
206
212
|
if (steps.length >= maxSteps)
|
|
207
213
|
return true;
|
|
208
|
-
if (
|
|
214
|
+
if (effectiveMaxItems != null && completedIssues.size >= effectiveMaxItems)
|
|
209
215
|
return true;
|
|
210
216
|
return false;
|
|
211
217
|
},
|
|
@@ -30,7 +30,10 @@ export function createBacklogTools(owner, repo, options = {}) {
|
|
|
30
30
|
return { issues: [], error: result.error };
|
|
31
31
|
// Sort by issue number ascending — lower numbers are typically more foundational
|
|
32
32
|
const sorted = [...result.issues].sort((a, b) => a.number - b.number);
|
|
33
|
-
|
|
33
|
+
const filtered = options.issueNumbers?.length
|
|
34
|
+
? sorted.filter(i => options.issueNumbers.includes(i.number))
|
|
35
|
+
: sorted;
|
|
36
|
+
return { issues: filtered };
|
|
34
37
|
},
|
|
35
38
|
});
|
|
36
39
|
const readIssue = tool({
|
package/dist/agent/types.d.ts
CHANGED
package/dist/commands/agent.d.ts
CHANGED
package/dist/commands/agent.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export function parseCliArgs(argv) {
|
|
|
49
49
|
'--max-items',
|
|
50
50
|
'--max-steps',
|
|
51
51
|
'--labels',
|
|
52
|
+
'--issues',
|
|
52
53
|
]);
|
|
53
54
|
// Flags that can be specified multiple times, accumulating into an array
|
|
54
55
|
const repeatableFlagSet = new Set(['--issue', '--context']);
|
|
@@ -261,6 +262,7 @@ Options for agent:
|
|
|
261
262
|
--max-items <n> Max issues to process before stopping
|
|
262
263
|
--max-steps <n> Max agent steps before stopping
|
|
263
264
|
--labels <l1,l2> Only work on issues with these labels (comma-separated)
|
|
265
|
+
--issues <n1,n2,...> Only work on these specific issue numbers (comma-separated)
|
|
264
266
|
--review-mode <mode> Review mode: 'manual', 'auto', or 'merge' (default: manual)
|
|
265
267
|
--dry-run Plan what would be done without executing
|
|
266
268
|
--stream Stream output in real-time (default: wait for completion)
|
|
@@ -410,6 +412,16 @@ Press Esc to cancel any operation.
|
|
|
410
412
|
maxItems: typeof parsed.flags.maxItems === 'string' ? parseIntFlag(parsed.flags.maxItems, '--max-items') : undefined,
|
|
411
413
|
maxSteps: typeof parsed.flags.maxSteps === 'string' ? parseIntFlag(parsed.flags.maxSteps, '--max-steps') : undefined,
|
|
412
414
|
labels: typeof parsed.flags.labels === 'string' ? parsed.flags.labels.split(',').map(l => l.trim()).filter(Boolean) : undefined,
|
|
415
|
+
issues: typeof parsed.flags.issues === 'string'
|
|
416
|
+
? parsed.flags.issues.split(',').map(s => {
|
|
417
|
+
const n = parseInt(s.trim(), 10);
|
|
418
|
+
if (Number.isNaN(n) || n < 1) {
|
|
419
|
+
console.error(`Error: Invalid issue number '${s.trim()}' in --issues`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
return n;
|
|
423
|
+
})
|
|
424
|
+
: undefined,
|
|
413
425
|
reviewMode: reviewModeFlag,
|
|
414
426
|
dryRun: parsed.flags.dryRun === true,
|
|
415
427
|
stream: parsed.flags.stream === true,
|
|
@@ -427,6 +439,7 @@ Press Esc to cancel any operation.
|
|
|
427
439
|
maxItems: agentOpts.maxItems,
|
|
428
440
|
maxSteps: agentOpts.maxSteps,
|
|
429
441
|
labels: agentOpts.labels,
|
|
442
|
+
issues: agentOpts.issues,
|
|
430
443
|
reviewMode: agentOpts.reviewMode,
|
|
431
444
|
dryRun: agentOpts.dryRun,
|
|
432
445
|
},
|
|
@@ -54,6 +54,11 @@ export declare const REPL_COMMANDS: {
|
|
|
54
54
|
readonly usage: "/monitor <feature-name>";
|
|
55
55
|
readonly aliases: readonly ["m"];
|
|
56
56
|
};
|
|
57
|
+
readonly issue: {
|
|
58
|
+
readonly description: "Browse GitHub issues and start a new spec from one";
|
|
59
|
+
readonly usage: "/issue [search terms]";
|
|
60
|
+
readonly aliases: readonly [];
|
|
61
|
+
};
|
|
57
62
|
readonly agent: {
|
|
58
63
|
readonly description: "Start the autonomous backlog agent";
|
|
59
64
|
readonly usage: "/agent [--dry-run] [--max-items <n>]";
|
|
@@ -31,6 +31,11 @@ export const REPL_COMMANDS = {
|
|
|
31
31
|
usage: '/monitor <feature-name>',
|
|
32
32
|
aliases: ['m'],
|
|
33
33
|
},
|
|
34
|
+
issue: {
|
|
35
|
+
description: 'Browse GitHub issues and start a new spec from one',
|
|
36
|
+
usage: '/issue [search terms]',
|
|
37
|
+
aliases: [],
|
|
38
|
+
},
|
|
34
39
|
agent: {
|
|
35
40
|
description: 'Start the autonomous backlog agent',
|
|
36
41
|
usage: '/agent [--dry-run] [--max-items <n>]',
|
package/dist/tui/app.d.ts
CHANGED
package/dist/tui/app.js
CHANGED
|
@@ -199,7 +199,8 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
199
199
|
if (!featureName || typeof featureName !== 'string' || !sessionState.provider) {
|
|
200
200
|
return null; // useEffect will redirect to shell
|
|
201
201
|
}
|
|
202
|
-
return (_jsx(InterviewScreen, { header: headerElement, featureName: featureName, projectRoot: sessionState.projectRoot, provider: sessionState.provider, model: sessionState.model, scanResult: sessionState.scanResult, specsPath: sessionState.config?.paths.specs, initialReferences:
|
|
202
|
+
return (_jsx(InterviewScreen, { header: headerElement, featureName: featureName, projectRoot: sessionState.projectRoot, provider: sessionState.provider, model: sessionState.model, scanResult: sessionState.scanResult, specsPath: sessionState.config?.paths.specs, initialReferences: screenProps?.initialReferences
|
|
203
|
+
?? interviewProps?.initialReferences, onComplete: handleInterviewComplete, onCancel: handleInterviewCancel }));
|
|
203
204
|
}
|
|
204
205
|
case 'init':
|
|
205
206
|
return (_jsx(InitScreen, { header: headerElement, projectRoot: sessionState.projectRoot, sessionState: sessionState, onComplete: handleInitComplete, onCancel: () => navigate('shell') }));
|
|
@@ -219,6 +220,7 @@ interviewProps, runProps, agentProps, onComplete, onExit, }) {
|
|
|
219
220
|
...(screenProps?.dryRun != null ? { dryRun: screenProps.dryRun } : {}),
|
|
220
221
|
...(screenProps?.maxItems != null ? { maxItems: screenProps.maxItems } : {}),
|
|
221
222
|
...(screenProps?.reviewMode != null ? { reviewMode: screenProps.reviewMode } : {}),
|
|
223
|
+
...(screenProps?.issues != null ? { issues: screenProps.issues } : {}),
|
|
222
224
|
};
|
|
223
225
|
return (_jsx(AgentScreen, { header: headerElement, projectRoot: sessionState.projectRoot, agentOptions: resolvedAgentOptions, onExit: () => {
|
|
224
226
|
if (initialScreen === 'agent') {
|
|
@@ -16,14 +16,23 @@ function truncate(text, maxLen) {
|
|
|
16
16
|
return text;
|
|
17
17
|
return text.slice(0, maxLen - 1) + '\u2026';
|
|
18
18
|
}
|
|
19
|
+
function pad(text, width) {
|
|
20
|
+
if (text.length >= width)
|
|
21
|
+
return text.slice(0, width);
|
|
22
|
+
return text + ' '.repeat(width - text.length);
|
|
23
|
+
}
|
|
24
|
+
// Fixed column widths
|
|
25
|
+
const COL_NUMBER = 6; // " #104 "
|
|
26
|
+
const COL_STATE = 8; // " closed " or " open "
|
|
27
|
+
const COL_LABELS = 14; // " enhancement "
|
|
28
|
+
const COL_CHROME = 2; // left + right border
|
|
19
29
|
export function IssuePicker({ issues, repoSlug, onSelect, onCancel, isLoading, error, }) {
|
|
20
30
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
21
31
|
const { stdout } = useStdout();
|
|
22
|
-
const
|
|
32
|
+
const termColumns = stdout?.columns ?? 80;
|
|
23
33
|
React.useEffect(() => {
|
|
24
34
|
setSelectedIndex(0);
|
|
25
35
|
}, [issues]);
|
|
26
|
-
// Handle keyboard input
|
|
27
36
|
useInput((input, key) => {
|
|
28
37
|
if (key.escape) {
|
|
29
38
|
onCancel();
|
|
@@ -42,23 +51,23 @@ export function IssuePicker({ issues, repoSlug, onSelect, onCancel, isLoading, e
|
|
|
42
51
|
return;
|
|
43
52
|
}
|
|
44
53
|
});
|
|
45
|
-
// Build border strings
|
|
46
54
|
const countSuffix = !isLoading && !error ? ` (${issues.length})` : '';
|
|
47
55
|
const headerLabel = ` GitHub Issues ${box.horizontal} ${repoSlug}${countSuffix} `;
|
|
48
|
-
const contentWidth = Math.min(Math.max(60, headerLabel.length + 10),
|
|
56
|
+
const contentWidth = Math.min(Math.max(60, headerLabel.length + 10), termColumns - 6);
|
|
49
57
|
const topBorderFill = contentWidth - headerLabel.length - 1;
|
|
50
58
|
const topBorder = box.topLeft + box.horizontal + headerLabel + box.horizontal.repeat(Math.max(0, topBorderFill)) + box.topRight;
|
|
51
59
|
const bottomBorder = box.bottomLeft + box.horizontal.repeat(contentWidth) + box.bottomRight;
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const chrome = 4; // borders + padding
|
|
56
|
-
const titleMaxLen = Math.max(20, contentWidth - numberWidth - labelBudget - chrome);
|
|
57
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: topBorder }), isLoading && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsx(Text, { children: " " }), _jsx(Spinner, { type: "dots" }), _jsx(Text, { color: colors.yellow, children: " Searching..." }), _jsx(Text, { children: ' '.repeat(Math.max(0, contentWidth - 16)) }), _jsx(Text, { dimColor: true, children: box.vertical })] })), !isLoading && error && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsxs(Text, { color: colors.pink, children: [" ", error] }), _jsx(Text, { children: ' '.repeat(Math.max(0, contentWidth - error.length - 1)) }), _jsx(Text, { dimColor: true, children: box.vertical })] })), !isLoading && !error && issues.length === 0 && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsx(Text, { dimColor: true, children: " No open issues found" }), _jsx(Text, { children: ' '.repeat(Math.max(0, contentWidth - 21)) }), _jsx(Text, { dimColor: true, children: box.vertical })] })), !isLoading && !error && issues.map((issue, index) => {
|
|
60
|
+
// Title column gets all remaining space
|
|
61
|
+
const colTitle = Math.max(20, contentWidth - COL_NUMBER - COL_STATE - COL_LABELS - COL_CHROME);
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: topBorder }), isLoading && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsx(Text, { children: " " }), _jsx(Spinner, { type: "dots" }), _jsx(Text, { color: colors.yellow, children: " Searching..." }), _jsx(Text, { children: ' '.repeat(Math.max(0, contentWidth - 16)) }), _jsx(Text, { dimColor: true, children: box.vertical })] })), !isLoading && error && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsx(Text, { color: colors.pink, children: pad(` ${error}`, contentWidth) }), _jsx(Text, { dimColor: true, children: box.vertical })] })), !isLoading && !error && issues.length === 0 && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsx(Text, { dimColor: true, children: pad(' No open issues found', contentWidth) }), _jsx(Text, { dimColor: true, children: box.vertical })] })), !isLoading && !error && issues.map((issue, index) => {
|
|
58
63
|
const isSelected = index === selectedIndex;
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const bg = isSelected ? colors.yellow : undefined;
|
|
65
|
+
const fg = isSelected ? colors.brown : undefined;
|
|
66
|
+
const numberCell = pad(` #${issue.number}`, COL_NUMBER);
|
|
67
|
+
const titleCell = pad(` ${truncate(issue.title, colTitle - 1)}`, colTitle);
|
|
68
|
+
const stateCell = pad(` ${issue.state}`, COL_STATE);
|
|
69
|
+
const labelText = issue.labels.slice(0, MAX_LABELS).join(' ');
|
|
70
|
+
const labelCell = pad(` ${truncate(labelText, COL_LABELS - 1)}`, COL_LABELS);
|
|
71
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: box.vertical }), _jsx(Text, { backgroundColor: bg, color: isSelected ? colors.brown : colors.yellow, children: numberCell }), _jsx(Text, { backgroundColor: bg, color: fg, children: titleCell }), _jsx(Text, { backgroundColor: bg, color: isSelected ? colors.brown : issue.state === 'open' ? colors.green : colors.gray, children: stateCell }), _jsx(Text, { backgroundColor: bg, color: isSelected ? colors.brown : colors.gray, children: labelCell }), _jsx(Text, { dimColor: true, children: box.vertical })] }, issue.number));
|
|
72
|
+
}), _jsx(Text, { dimColor: true, children: bottomBorder }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: colors.gray, children: ["(", '\u2191\u2193 navigate, Enter select, Esc cancel', ")"] }) })] }));
|
|
64
73
|
}
|
|
@@ -15,6 +15,8 @@ export interface StatusLineProps {
|
|
|
15
15
|
phase?: string;
|
|
16
16
|
/** Path or context info (e.g., working directory or feature name) */
|
|
17
17
|
path?: string;
|
|
18
|
+
/** Extra trailing segment (e.g., "review: auto") */
|
|
19
|
+
extra?: string;
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
22
|
* StatusLine component
|
|
@@ -32,4 +34,4 @@ export interface StatusLineProps {
|
|
|
32
34
|
* // Renders: Initialize Project │ Analysis (4/5) │ /Users/name/project
|
|
33
35
|
* ```
|
|
34
36
|
*/
|
|
35
|
-
export declare function StatusLine({ action, phase, path, }: StatusLineProps): React.ReactElement;
|
|
37
|
+
export declare function StatusLine({ action, phase, path, extra, }: StatusLineProps): React.ReactElement;
|
|
@@ -17,11 +17,11 @@ import { theme, colors } from '../theme.js';
|
|
|
17
17
|
* // Renders: Initialize Project │ Analysis (4/5) │ /Users/name/project
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export function StatusLine({ action, phase, path, }) {
|
|
20
|
+
export function StatusLine({ action, phase, path, extra, }) {
|
|
21
21
|
const separator = theme.statusLine.separator;
|
|
22
22
|
// Truncate path if too long (keep last 40 chars)
|
|
23
23
|
const displayPath = path && path.length > 40
|
|
24
24
|
? '...' + path.slice(-37)
|
|
25
25
|
: path;
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: colors.yellow, bold: true, children: action }), phase && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: separator }), _jsx(Text, { children: phase })] })), displayPath && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: separator }), _jsx(Text, { dimColor: true, children: displayPath })] }))] }));
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: colors.yellow, bold: true, children: action }), phase && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: separator }), _jsx(Text, { children: phase })] })), displayPath && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: separator }), _jsx(Text, { dimColor: true, children: displayPath })] })), extra && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: separator }), _jsx(Text, { dimColor: true, children: extra })] }))] }));
|
|
27
27
|
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* Exposes an abort() function for clean shutdown on q/Esc.
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentIssueState, AgentLogEntry, ReviewMode } from '../../agent/types.js';
|
|
10
|
+
import { type LoopStatus, type TaskCounts, type ActivityEvent } from '../utils/loop-status.js';
|
|
11
|
+
import { type CommitLogEntry } from '../utils/git-summary.js';
|
|
10
12
|
export type AgentStatus = 'idle' | 'running' | 'complete' | 'error';
|
|
11
13
|
export interface UseAgentOrchestratorOptions {
|
|
12
14
|
projectRoot: string;
|
|
@@ -14,15 +16,25 @@ export interface UseAgentOrchestratorOptions {
|
|
|
14
16
|
maxItems?: number;
|
|
15
17
|
maxSteps?: number;
|
|
16
18
|
labels?: string[];
|
|
19
|
+
issues?: number[];
|
|
17
20
|
reviewMode?: ReviewMode;
|
|
18
21
|
dryRun?: boolean;
|
|
19
22
|
}
|
|
23
|
+
export interface LoopMonitorData {
|
|
24
|
+
loopStatus: LoopStatus;
|
|
25
|
+
tasks: TaskCounts;
|
|
26
|
+
branch: string;
|
|
27
|
+
recentCommits: CommitLogEntry[];
|
|
28
|
+
activityEvents: ActivityEvent[];
|
|
29
|
+
startTime: number;
|
|
30
|
+
}
|
|
20
31
|
export interface UseAgentOrchestratorResult {
|
|
21
32
|
status: AgentStatus;
|
|
22
33
|
activeIssue: AgentIssueState | null;
|
|
23
34
|
queue: AgentIssueState[];
|
|
24
35
|
completed: AgentIssueState[];
|
|
25
36
|
logEntries: AgentLogEntry[];
|
|
37
|
+
loopMonitor: LoopMonitorData | null;
|
|
26
38
|
error: string | null;
|
|
27
39
|
abort: () => void;
|
|
28
40
|
}
|
|
@@ -10,7 +10,8 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
10
10
|
import { resolveAgentEnv } from '../../agent/resolve-config.js';
|
|
11
11
|
import { createAgentOrchestrator, } from '../../agent/orchestrator.js';
|
|
12
12
|
import { initTracing, flushTracing } from '../../utils/tracing.js';
|
|
13
|
-
import { readCurrentPhase, readLoopStatus, parseLoopLog, parsePhaseChanges, getLoopLogPath, shouldSkipLine, } from '../utils/loop-status.js';
|
|
13
|
+
import { readCurrentPhase, readLoopStatus, parseLoopLog, parsePhaseChanges, parseImplementationPlan, getLoopLogPath, shouldSkipLine, getGitBranch, } from '../utils/loop-status.js';
|
|
14
|
+
import { getRecentCommits } from '../utils/git-summary.js';
|
|
14
15
|
const MAX_LOG_ENTRIES = 500;
|
|
15
16
|
function now() {
|
|
16
17
|
return new Date().toISOString();
|
|
@@ -245,6 +246,7 @@ export function useAgentOrchestrator(options) {
|
|
|
245
246
|
const [queue, setQueue] = useState([]);
|
|
246
247
|
const [completed, setCompleted] = useState([]);
|
|
247
248
|
const [logEntries, setLogEntries] = useState([]);
|
|
249
|
+
const [loopMonitor, setLoopMonitor] = useState(null);
|
|
248
250
|
const [error, setError] = useState(null);
|
|
249
251
|
const abortRef = useRef(null);
|
|
250
252
|
const startedRef = useRef(false);
|
|
@@ -258,24 +260,30 @@ export function useAgentOrchestrator(options) {
|
|
|
258
260
|
clearInterval(pollingRef.current.interval);
|
|
259
261
|
pollingRef.current = null;
|
|
260
262
|
}
|
|
263
|
+
setLoopMonitor(null);
|
|
261
264
|
}, []);
|
|
262
265
|
const startLoopPolling = useCallback((featureName) => {
|
|
263
266
|
stopLoopPolling(); // clear any existing polling
|
|
267
|
+
const startTime = Date.now();
|
|
264
268
|
const state = {
|
|
265
269
|
featureName,
|
|
266
270
|
interval: null,
|
|
267
271
|
lastLogTimestamp: undefined,
|
|
268
272
|
lastPhases: undefined,
|
|
269
273
|
};
|
|
270
|
-
|
|
274
|
+
// Accumulated activity events across polls
|
|
275
|
+
const activityEventsAcc = [];
|
|
276
|
+
const MAX_ACTIVITY = 50;
|
|
277
|
+
const poll = async () => {
|
|
271
278
|
// Read current phase
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
setActiveIssue((prev) => prev ? { ...prev, loopPhase:
|
|
279
|
+
const currentPhase = readCurrentPhase(featureName);
|
|
280
|
+
if (currentPhase) {
|
|
281
|
+
setActiveIssue((prev) => prev ? { ...prev, loopPhase: currentPhase } : prev);
|
|
275
282
|
}
|
|
276
|
-
// Read loop status for iteration count
|
|
283
|
+
// Read loop status for iteration count + token data
|
|
284
|
+
let loopStatus = null;
|
|
277
285
|
try {
|
|
278
|
-
|
|
286
|
+
loopStatus = readLoopStatus(featureName);
|
|
279
287
|
if (loopStatus.iteration > 0) {
|
|
280
288
|
setActiveIssue((prev) => prev ? { ...prev, loopIterations: loopStatus.iteration } : prev);
|
|
281
289
|
}
|
|
@@ -283,11 +291,26 @@ export function useAgentOrchestrator(options) {
|
|
|
283
291
|
catch {
|
|
284
292
|
// Invalid feature name or file not ready — skip
|
|
285
293
|
}
|
|
294
|
+
// Read task progress
|
|
295
|
+
let tasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0, planExists: false };
|
|
296
|
+
try {
|
|
297
|
+
tasks = await parseImplementationPlan(options.projectRoot, featureName, '.ralph/specs');
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Plan not yet available
|
|
301
|
+
}
|
|
302
|
+
// Git info
|
|
303
|
+
const branch = getGitBranch(options.projectRoot);
|
|
304
|
+
const recentCommits = getRecentCommits(options.projectRoot, 3);
|
|
286
305
|
// Parse loop log for new events
|
|
287
306
|
const logPath = getLoopLogPath(featureName);
|
|
288
307
|
const logEvents = parseLoopLog(logPath, state.lastLogTimestamp);
|
|
289
308
|
if (logEvents.length > 0) {
|
|
290
309
|
state.lastLogTimestamp = logEvents[logEvents.length - 1].timestamp + 1;
|
|
310
|
+
activityEventsAcc.push(...logEvents);
|
|
311
|
+
if (activityEventsAcc.length > MAX_ACTIVITY) {
|
|
312
|
+
activityEventsAcc.splice(0, activityEventsAcc.length - MAX_ACTIVITY);
|
|
313
|
+
}
|
|
291
314
|
setLogEntries((prev) => {
|
|
292
315
|
let next = prev;
|
|
293
316
|
for (const evt of logEvents) {
|
|
@@ -302,6 +325,10 @@ export function useAgentOrchestrator(options) {
|
|
|
302
325
|
state.lastPhases = phaseResult.currentPhases;
|
|
303
326
|
}
|
|
304
327
|
if (phaseResult.events.length > 0) {
|
|
328
|
+
activityEventsAcc.push(...phaseResult.events);
|
|
329
|
+
if (activityEventsAcc.length > MAX_ACTIVITY) {
|
|
330
|
+
activityEventsAcc.splice(0, activityEventsAcc.length - MAX_ACTIVITY);
|
|
331
|
+
}
|
|
305
332
|
setLogEntries((prev) => {
|
|
306
333
|
let next = prev;
|
|
307
334
|
for (const evt of phaseResult.events) {
|
|
@@ -310,12 +337,23 @@ export function useAgentOrchestrator(options) {
|
|
|
310
337
|
return next;
|
|
311
338
|
});
|
|
312
339
|
}
|
|
340
|
+
// Update loop monitor data
|
|
341
|
+
if (loopStatus) {
|
|
342
|
+
setLoopMonitor({
|
|
343
|
+
loopStatus,
|
|
344
|
+
tasks,
|
|
345
|
+
branch,
|
|
346
|
+
recentCommits,
|
|
347
|
+
activityEvents: [...activityEventsAcc],
|
|
348
|
+
startTime,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
313
351
|
};
|
|
314
352
|
state.interval = setInterval(poll, 3000);
|
|
315
353
|
pollingRef.current = state;
|
|
316
354
|
// Run first poll immediately
|
|
317
355
|
poll();
|
|
318
|
-
}, [stopLoopPolling]);
|
|
356
|
+
}, [stopLoopPolling, options.projectRoot]);
|
|
319
357
|
const abort = useCallback(() => {
|
|
320
358
|
abortRef.current?.abort();
|
|
321
359
|
stopLoopPolling();
|
|
@@ -346,6 +384,7 @@ export function useAgentOrchestrator(options) {
|
|
|
346
384
|
maxSteps: options.maxSteps,
|
|
347
385
|
maxItems: options.maxItems,
|
|
348
386
|
labels: options.labels,
|
|
387
|
+
issues: options.issues,
|
|
349
388
|
reviewMode: options.reviewMode,
|
|
350
389
|
dryRun: options.dryRun,
|
|
351
390
|
onStepUpdate: (event) => {
|
|
@@ -449,5 +488,5 @@ export function useAgentOrchestrator(options) {
|
|
|
449
488
|
stopLoopPolling();
|
|
450
489
|
};
|
|
451
490
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- options are stable from parent
|
|
452
|
-
return { status, activeIssue, queue, completed, logEntries, error, abort };
|
|
491
|
+
return { status, activeIssue, queue, completed, logEntries, loopMonitor, error, abort };
|
|
453
492
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentScreen - TUI dashboard for the autonomous agent mode
|
|
3
3
|
*
|
|
4
|
-
* Displays issue processing status and an agent log.
|
|
4
|
+
* Displays issue processing status, a loop monitor, and an agent log.
|
|
5
5
|
* Two-column layout on wide terminals (>=65 cols), single-column on narrow.
|
|
6
6
|
*
|
|
7
|
+
* When a loop is running, the right panel splits: loop monitor (top) + log (bottom).
|
|
8
|
+
* When no loop is running, the right panel shows only the agent log.
|
|
9
|
+
*
|
|
7
10
|
* Wired to the orchestrator via useAgentOrchestrator hook, which
|
|
8
11
|
* interprets tool calls into structured React state. Console is patched
|
|
9
12
|
* on mount to prevent Ink rendering corruption.
|
|
@@ -2,9 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* AgentScreen - TUI dashboard for the autonomous agent mode
|
|
4
4
|
*
|
|
5
|
-
* Displays issue processing status and an agent log.
|
|
5
|
+
* Displays issue processing status, a loop monitor, and an agent log.
|
|
6
6
|
* Two-column layout on wide terminals (>=65 cols), single-column on narrow.
|
|
7
7
|
*
|
|
8
|
+
* When a loop is running, the right panel splits: loop monitor (top) + log (bottom).
|
|
9
|
+
* When no loop is running, the right panel shows only the agent log.
|
|
10
|
+
*
|
|
8
11
|
* Wired to the orchestrator via useAgentOrchestrator hook, which
|
|
9
12
|
* interprets tool calls into structured React state. Console is patched
|
|
10
13
|
* on mount to prevent Ink rendering corruption.
|
|
@@ -14,8 +17,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
17
|
import { useEffect, useCallback, useState, useRef } from 'react';
|
|
15
18
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
16
19
|
import { AppShell } from '../components/AppShell.js';
|
|
17
|
-
import {
|
|
20
|
+
import { ActivityFeed } from '../components/ActivityFeed.js';
|
|
21
|
+
import { colors, phase, theme } from '../theme.js';
|
|
18
22
|
import { useAgentOrchestrator } from '../hooks/useAgentOrchestrator.js';
|
|
23
|
+
import { formatNumber } from '../utils/loop-status.js';
|
|
19
24
|
const NARROW_BREAKPOINT = 65;
|
|
20
25
|
const SECTION_CHAR = '\u2500'; // ─
|
|
21
26
|
function phaseLabel(p, activeIssue) {
|
|
@@ -57,12 +62,12 @@ function footerPhase(status, activeIssue, completedCount, cancelling) {
|
|
|
57
62
|
if (status === 'idle')
|
|
58
63
|
return 'Waiting';
|
|
59
64
|
if (status === 'complete')
|
|
60
|
-
return `Done
|
|
65
|
+
return `Done \u2014 ${completedCount} issue${completedCount === 1 ? '' : 's'} processed`;
|
|
61
66
|
if (status === 'error')
|
|
62
67
|
return 'Error';
|
|
63
68
|
if (!activeIssue)
|
|
64
69
|
return 'Starting...';
|
|
65
|
-
const loopDetail = activeIssue.loopPhase ? `
|
|
70
|
+
const loopDetail = activeIssue.loopPhase ? ` \u00b7 ${activeIssue.loopPhase}` : '';
|
|
66
71
|
const iter = activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : '';
|
|
67
72
|
return `#${activeIssue.issueNumber}${loopDetail}${iter}`;
|
|
68
73
|
}
|
|
@@ -77,16 +82,49 @@ function SectionSeparator({ width }) {
|
|
|
77
82
|
* Issues panel content — shared between wide and narrow layouts
|
|
78
83
|
*/
|
|
79
84
|
function IssuesPanel({ activeIssue, queue, completed, panelWidth, }) {
|
|
80
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Active Issue" }), activeIssue ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: colors.blue, children: ["#", activeIssue.issueNumber] }), _jsxs(Text, { children: [" ", activeIssue.title] })] }), _jsxs(Text, { dimColor: true, children: [phase.active, " ", phaseLabel(activeIssue.phase, activeIssue), activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : ''] })] })) : (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "No active issue" }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Queue", _jsxs(Text, { dimColor: true, children: [" (", queue.length, ")"] })] }), queue.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "Empty" }) })) : (queue.slice(0, 5).map((issue) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["#", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }) }, issue.issueNumber)))), queue.length > 5 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["...and ", queue.length - 5, " more"] }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Completed", _jsxs(Text, { dimColor: true, children: [" (", completed.length, ")"] })] }), completed.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "None yet" }) })) : (completed.slice(-5).map((issue) => (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: issue.error ? colors.pink : colors.green, children: issue.error ? phase.error : phase.complete }), _jsxs(Text, { dimColor: true, children: [" #", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }), issue.prUrl && (_jsxs(Text, { dimColor: true, children: ["
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Active Issue" }), activeIssue ? (_jsxs(Box, { flexDirection: "column", marginLeft: 1, children: [_jsxs(Text, { children: [_jsxs(Text, { color: colors.blue, children: ["#", activeIssue.issueNumber] }), _jsxs(Text, { children: [" ", activeIssue.title] })] }), _jsxs(Text, { dimColor: true, children: [phase.active, " ", phaseLabel(activeIssue.phase, activeIssue), activeIssue.loopIterations != null ? ` (iter ${activeIssue.loopIterations})` : ''] })] })) : (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "No active issue" }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Queue", _jsxs(Text, { dimColor: true, children: [" (", queue.length, ")"] })] }), queue.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "Empty" }) })) : (queue.slice(0, 5).map((issue) => (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["#", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }) }, issue.issueNumber)))), queue.length > 5 && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["...and ", queue.length - 5, " more"] }) })), _jsx(SectionSeparator, { width: panelWidth }), _jsxs(Text, { bold: true, color: colors.yellow, children: ["Completed", _jsxs(Text, { dimColor: true, children: [" (", completed.length, ")"] })] }), completed.length === 0 ? (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { dimColor: true, children: "None yet" }) })) : (completed.slice(-5).map((issue) => (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: issue.error ? colors.pink : colors.green, children: issue.error ? phase.error : phase.complete }), _jsxs(Text, { dimColor: true, children: [" #", issue.issueNumber] }), _jsxs(Text, { children: [" ", issue.title] })] }), issue.prUrl && (_jsxs(Text, { dimColor: true, children: [" \\u2514 PR: ", issue.prUrl] }))] }, issue.issueNumber))))] }));
|
|
81
86
|
}
|
|
82
87
|
/**
|
|
83
|
-
* Log panel content — shared between wide and narrow layouts
|
|
88
|
+
* Log panel content — shared between wide and narrow layouts.
|
|
89
|
+
* tailSize controls how many entries to show (shrinks when monitor is visible).
|
|
84
90
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const visible = logEntries.slice(-LOG_TAIL_SIZE);
|
|
91
|
+
function LogPanel({ logEntries, tailSize = 20 }) {
|
|
92
|
+
const visible = logEntries.slice(-tailSize);
|
|
88
93
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: colors.yellow, children: "Agent Log" }), visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "Waiting for agent activity..." })) : (visible.map((entry, index) => (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: entry.timestamp.slice(11, 19) }), _jsx(Text, { children: " " }), _jsx(Text, { color: logLevelColor(entry.level), children: entry.message })] }) }, `${entry.timestamp}-${index}`))))] }));
|
|
89
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* ProgressBar — inline progress indicator
|
|
97
|
+
*/
|
|
98
|
+
function ProgressBar({ percent, width = 18 }) {
|
|
99
|
+
const safePercent = Math.max(0, Math.min(100, percent));
|
|
100
|
+
const filled = Math.round((safePercent / 100) * width);
|
|
101
|
+
const empty = Math.max(0, width - filled);
|
|
102
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: colors.green, children: '\u2588'.repeat(filled) }), _jsx(Text, { dimColor: true, children: '\u2591'.repeat(empty) })] }));
|
|
103
|
+
}
|
|
104
|
+
function formatDuration(start) {
|
|
105
|
+
const seconds = Math.max(0, Math.floor((Date.now() - start) / 1000));
|
|
106
|
+
const mins = Math.floor(seconds / 60);
|
|
107
|
+
const secs = seconds % 60;
|
|
108
|
+
return `${mins}:${String(secs).padStart(2, '0')}`;
|
|
109
|
+
}
|
|
110
|
+
const PROGRESS_LABEL_WIDTH = 17;
|
|
111
|
+
const padLabel = (label) => label.padEnd(PROGRESS_LABEL_WIDTH);
|
|
112
|
+
/**
|
|
113
|
+
* Loop Monitor panel — shows progress, commits, and activity for the active loop
|
|
114
|
+
*/
|
|
115
|
+
function LoopMonitorPanel({ monitor, featureName, panelWidth, }) {
|
|
116
|
+
const { loopStatus, tasks, branch, recentCommits, activityEvents, startTime } = monitor;
|
|
117
|
+
const totalTokens = loopStatus.tokensInput + loopStatus.tokensOutput + loopStatus.cacheCreate + loopStatus.cacheRead;
|
|
118
|
+
const totalTasks = tasks.tasksDone + tasks.tasksPending;
|
|
119
|
+
const totalE2e = tasks.e2eDone + tasks.e2ePending;
|
|
120
|
+
const totalAll = totalTasks + totalE2e;
|
|
121
|
+
const doneAll = tasks.tasksDone + tasks.e2eDone;
|
|
122
|
+
const percentTasks = totalTasks > 0 ? Math.round((tasks.tasksDone / totalTasks) * 100) : 0;
|
|
123
|
+
const percentE2e = totalE2e > 0 ? Math.round((tasks.e2eDone / totalE2e) * 100) : 0;
|
|
124
|
+
const percentAll = totalAll > 0 ? Math.round((doneAll / totalAll) * 100) : 0;
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: colors.yellow, children: ["Loop Monitor ", _jsxs(Text, { dimColor: true, children: ["\\u2014 ", featureName] })] }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: loopStatus.phase }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: loopStatus.iteration }), _jsxs(Text, { dimColor: true, children: ["/", loopStatus.maxIterations || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch })] }), totalTokens > 0 && (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(loopStatus.tokensInput), " out:", formatNumber(loopStatus.tokensOutput), " cache:", formatNumber(loopStatus.cacheRead), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTime)] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('Implementation:') }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [String(percentTasks).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.tasksDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.tasksPending] })] }), totalE2e > 0 && (_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('E2E Tests:') }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [String(percentE2e).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.e2eDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.e2ePending] })] })), _jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('Overall:') }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [String(percentAll).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", totalAll - doneAll] })] })] }), recentCommits.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Commits" }), recentCommits.map((c) => (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { dimColor: true, children: [" ", c.hash, " ", c.title] }) }, c.hash)))] })), activityEvents.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(ActivityFeed, { events: activityEvents, maxEvents: 6 })] }))] }));
|
|
126
|
+
}
|
|
127
|
+
const REVIEW_MODES = ['manual', 'auto', 'merge'];
|
|
90
128
|
export function AgentScreen({ header, projectRoot, agentOptions, onExit, }) {
|
|
91
129
|
const { stdout } = useStdout();
|
|
92
130
|
const columns = stdout?.columns ?? 80;
|
|
@@ -107,17 +145,19 @@ export function AgentScreen({ header, projectRoot, agentOptions, onExit, }) {
|
|
|
107
145
|
restore?.();
|
|
108
146
|
};
|
|
109
147
|
}, []);
|
|
110
|
-
const { status, activeIssue, queue, completed, logEntries, error, abort } = useAgentOrchestrator({
|
|
148
|
+
const { status, activeIssue, queue, completed, logEntries, loopMonitor, error, abort } = useAgentOrchestrator({
|
|
111
149
|
projectRoot,
|
|
112
150
|
modelOverride: agentOptions?.modelOverride,
|
|
113
151
|
maxItems: agentOptions?.maxItems,
|
|
114
152
|
maxSteps: agentOptions?.maxSteps,
|
|
115
153
|
labels: agentOptions?.labels,
|
|
154
|
+
issues: agentOptions?.issues,
|
|
116
155
|
reviewMode: agentOptions?.reviewMode,
|
|
117
156
|
dryRun: agentOptions?.dryRun,
|
|
118
157
|
});
|
|
119
158
|
// Track whether the user requested cancellation
|
|
120
159
|
const [cancelling, setCancelling] = useState(false);
|
|
160
|
+
const [reviewMode, setReviewMode] = useState(agentOptions?.reviewMode ?? 'manual');
|
|
121
161
|
const onExitRef = useRef(onExit);
|
|
122
162
|
onExitRef.current = onExit;
|
|
123
163
|
// When cancelling and the orchestrator finishes, exit
|
|
@@ -127,6 +167,7 @@ export function AgentScreen({ header, projectRoot, agentOptions, onExit, }) {
|
|
|
127
167
|
}
|
|
128
168
|
}, [cancelling, status]);
|
|
129
169
|
const isWorking = status === 'running' || cancelling;
|
|
170
|
+
const hasLoopMonitor = loopMonitor !== null && activeIssue?.phase === 'running_loop';
|
|
130
171
|
useInput(useCallback((input, key) => {
|
|
131
172
|
if (input === 'q' || key.escape) {
|
|
132
173
|
if (status === 'running') {
|
|
@@ -138,22 +179,32 @@ export function AgentScreen({ header, projectRoot, agentOptions, onExit, }) {
|
|
|
138
179
|
// Already done — exit immediately
|
|
139
180
|
onExitRef.current?.();
|
|
140
181
|
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Shift+R cycles review mode (manual → auto → merge)
|
|
185
|
+
if (input === 'R') {
|
|
186
|
+
setReviewMode((prev) => {
|
|
187
|
+
const idx = REVIEW_MODES.indexOf(prev);
|
|
188
|
+
return REVIEW_MODES[(idx + 1) % REVIEW_MODES.length];
|
|
189
|
+
});
|
|
141
190
|
}
|
|
142
191
|
}, [abort, status]));
|
|
143
|
-
const tips = cancelling
|
|
192
|
+
const tips = cancelling
|
|
193
|
+
? 'Cancelling...'
|
|
194
|
+
: 'q exit \u2502 Esc back \u2502 Shift+R review mode';
|
|
195
|
+
const footerStatus = {
|
|
196
|
+
action: 'Agent',
|
|
197
|
+
phase: footerPhase(status, activeIssue, completed.length, cancelling),
|
|
198
|
+
extra: `review: ${reviewMode}`,
|
|
199
|
+
};
|
|
144
200
|
if (isNarrow) {
|
|
145
201
|
const panelWidth = Math.max(20, columns - 4);
|
|
146
|
-
return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: {
|
|
147
|
-
action: 'Agent',
|
|
148
|
-
phase: footerPhase(status, activeIssue, completed.length, cancelling),
|
|
149
|
-
}, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(IssuesPanel, { activeIssue: activeIssue, queue: queue, completed: completed, panelWidth: panelWidth }) }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries }) })] }) }));
|
|
202
|
+
return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: footerStatus, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(IssuesPanel, { activeIssue: activeIssue, queue: queue, completed: completed, panelWidth: panelWidth }) }), hasLoopMonitor && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LoopMonitorPanel, { monitor: loopMonitor, featureName: activeIssue.loopFeatureName ?? `issue-${activeIssue.issueNumber}`, panelWidth: panelWidth }) })), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries, tailSize: hasLoopMonitor ? 8 : 20 }) })] }) }));
|
|
150
203
|
}
|
|
151
204
|
// Wide layout: two-column
|
|
152
205
|
const leftWidth = Math.max(30, Math.floor(columns * 0.4));
|
|
153
206
|
const rightWidth = Math.max(30, columns - leftWidth - 3);
|
|
154
207
|
const leftInnerWidth = leftWidth - 4; // border (2) + paddingX (2)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
phase: footerPhase(status, activeIssue, completed.length, cancelling),
|
|
158
|
-
}, children: _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Box, { flexDirection: "column", width: leftWidth, borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(IssuesPanel, { activeIssue: activeIssue, queue: queue, completed: completed, panelWidth: leftInnerWidth }) }), _jsx(Box, { flexDirection: "column", width: rightWidth, borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries }) })] }) }));
|
|
208
|
+
const rightInnerWidth = rightWidth - 4;
|
|
209
|
+
return (_jsx(AppShell, { header: header, tips: tips, isWorking: isWorking, workingStatus: workingLabel(activeIssue), footerStatus: footerStatus, children: _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Box, { flexDirection: "column", width: leftWidth, borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(IssuesPanel, { activeIssue: activeIssue, queue: queue, completed: completed, panelWidth: leftInnerWidth }) }), _jsxs(Box, { flexDirection: "column", width: rightWidth, children: [hasLoopMonitor && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, marginBottom: 1, children: _jsx(LoopMonitorPanel, { monitor: loopMonitor, featureName: activeIssue.loopFeatureName ?? `issue-${activeIssue.issueNumber}`, panelWidth: rightInnerWidth }) })), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.brown, paddingX: 1, children: _jsx(LogPanel, { logEntries: logEntries, tailSize: hasLoopMonitor ? 8 : 20 }) })] })] }) }));
|
|
159
210
|
}
|
|
@@ -7,9 +7,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* Wrapped in AppShell for consistent layout.
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
10
|
-
import { Box, Text, useInput, useApp } from 'ink';
|
|
10
|
+
import { Box, Text, useInput, useApp, useStdout } from 'ink';
|
|
11
11
|
import { MessageList } from '../components/MessageList.js';
|
|
12
12
|
import { ChatInput } from '../components/ChatInput.js';
|
|
13
|
+
import { IssuePicker } from '../components/IssuePicker.js';
|
|
13
14
|
import { ActionOutput } from '../components/ActionOutput.js';
|
|
14
15
|
import { AppShell } from '../components/AppShell.js';
|
|
15
16
|
import { colors, theme, phase } from '../theme.js';
|
|
@@ -18,7 +19,18 @@ import { logger } from '../../utils/logger.js';
|
|
|
18
19
|
import { parseInput, resolveCommandAlias, formatHelpText, } from '../../repl/command-parser.js';
|
|
19
20
|
import { readLoopStatus } from '../utils/loop-status.js';
|
|
20
21
|
import { useSync } from '../hooks/useSync.js';
|
|
22
|
+
import { isGhInstalled, detectGitHubRemote, listRepoIssues, } from '../../utils/github.js';
|
|
23
|
+
import { clearScreen } from '../utils/clear-screen.js';
|
|
21
24
|
import path from 'node:path';
|
|
25
|
+
function slugifyIssueTitle(title, maxWords = 4) {
|
|
26
|
+
return title
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
29
|
+
.trim()
|
|
30
|
+
.split(/\s+/)
|
|
31
|
+
.slice(0, maxWords)
|
|
32
|
+
.join('-') || 'untitled';
|
|
33
|
+
}
|
|
22
34
|
/**
|
|
23
35
|
* Generate a unique ID for messages
|
|
24
36
|
*/
|
|
@@ -33,6 +45,7 @@ function generateId() {
|
|
|
33
45
|
*/
|
|
34
46
|
export function MainShell({ header, sessionState, onNavigate, backgroundRuns, initialMessage, initialFiles, }) {
|
|
35
47
|
const { exit } = useApp();
|
|
48
|
+
const { stdout } = useStdout();
|
|
36
49
|
const [messages, setMessages] = useState(() => {
|
|
37
50
|
const initial = [];
|
|
38
51
|
if (initialMessage) {
|
|
@@ -48,6 +61,12 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
48
61
|
const [contextAge, setContextAge] = useState(null);
|
|
49
62
|
// Sync hook
|
|
50
63
|
const { status: syncStatus, error: syncError, sync } = useSync();
|
|
64
|
+
// Issue picker state
|
|
65
|
+
const [issuePickerVisible, setIssuePickerVisible] = useState(false);
|
|
66
|
+
const [issuePickerIssues, setIssuePickerIssues] = useState([]);
|
|
67
|
+
const [issuePickerLoading, setIssuePickerLoading] = useState(false);
|
|
68
|
+
const [issuePickerError, setIssuePickerError] = useState();
|
|
69
|
+
const [issuePickerRepo, setIssuePickerRepo] = useState(null);
|
|
51
70
|
const addSystemMessage = useCallback((content) => {
|
|
52
71
|
const message = {
|
|
53
72
|
id: generateId(),
|
|
@@ -203,6 +222,60 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
203
222
|
}
|
|
204
223
|
onNavigate('agent', { dryRun, maxItems, reviewMode });
|
|
205
224
|
}, [sessionState.initialized, addSystemMessage, onNavigate]);
|
|
225
|
+
const handleIssueCommand = useCallback(async (searchQuery) => {
|
|
226
|
+
if (!sessionState.initialized) {
|
|
227
|
+
addSystemMessage('Project not initialized. Run /init first.');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
setIssuePickerVisible(true);
|
|
231
|
+
setIssuePickerLoading(true);
|
|
232
|
+
setIssuePickerError(undefined);
|
|
233
|
+
try {
|
|
234
|
+
const ghAvailable = await isGhInstalled();
|
|
235
|
+
if (!ghAvailable) {
|
|
236
|
+
setIssuePickerError('Install GitHub CLI (gh) for issue browsing');
|
|
237
|
+
setIssuePickerLoading(false);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let repo = issuePickerRepo;
|
|
241
|
+
if (!repo) {
|
|
242
|
+
repo = await detectGitHubRemote(sessionState.projectRoot);
|
|
243
|
+
if (!repo) {
|
|
244
|
+
setIssuePickerError('No GitHub remote detected in this project');
|
|
245
|
+
setIssuePickerLoading(false);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
setIssuePickerRepo(repo);
|
|
249
|
+
}
|
|
250
|
+
const result = await listRepoIssues(repo.owner, repo.repo, searchQuery);
|
|
251
|
+
if (result.error) {
|
|
252
|
+
setIssuePickerError(result.error);
|
|
253
|
+
}
|
|
254
|
+
setIssuePickerIssues(result.issues);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
setIssuePickerError(err instanceof Error ? err.message : String(err));
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
setIssuePickerLoading(false);
|
|
261
|
+
}
|
|
262
|
+
}, [sessionState.initialized, sessionState.projectRoot, issuePickerRepo, addSystemMessage]);
|
|
263
|
+
const handleIssueSelect = useCallback((issue) => {
|
|
264
|
+
clearScreen(stdout);
|
|
265
|
+
setIssuePickerVisible(false);
|
|
266
|
+
setIssuePickerIssues([]);
|
|
267
|
+
const featureName = slugifyIssueTitle(issue.title);
|
|
268
|
+
onNavigate('interview', {
|
|
269
|
+
featureName,
|
|
270
|
+
initialReferences: [`issue:${issue.number}`],
|
|
271
|
+
});
|
|
272
|
+
}, [stdout, onNavigate]);
|
|
273
|
+
const handleIssueCancel = useCallback(() => {
|
|
274
|
+
clearScreen(stdout);
|
|
275
|
+
setIssuePickerVisible(false);
|
|
276
|
+
setIssuePickerIssues([]);
|
|
277
|
+
setIssuePickerError(undefined);
|
|
278
|
+
}, [stdout]);
|
|
206
279
|
const handleConfig = useCallback((args) => {
|
|
207
280
|
if (args.length === 0) {
|
|
208
281
|
addSystemMessage('Config management - not yet implemented in TUI mode. Use CLI: wiggum config');
|
|
@@ -252,6 +325,9 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
252
325
|
case 'monitor':
|
|
253
326
|
handleMonitor(args);
|
|
254
327
|
break;
|
|
328
|
+
case 'issue':
|
|
329
|
+
handleIssueCommand(args.join(' ') || undefined);
|
|
330
|
+
break;
|
|
255
331
|
case 'agent':
|
|
256
332
|
handleAgent(args);
|
|
257
333
|
break;
|
|
@@ -264,7 +340,7 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
264
340
|
default:
|
|
265
341
|
addSystemMessage(`Unknown command: ${commandName}`);
|
|
266
342
|
}
|
|
267
|
-
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleAgent, handleConfig, handleExit, addSystemMessage]);
|
|
343
|
+
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleIssueCommand, handleAgent, handleConfig, handleExit, addSystemMessage]);
|
|
268
344
|
const handleNaturalLanguage = useCallback((_text) => {
|
|
269
345
|
addSystemMessage('Tip: Use /help to see available commands, or /new <feature> to create a spec.');
|
|
270
346
|
}, [addSystemMessage]);
|
|
@@ -299,10 +375,10 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
299
375
|
});
|
|
300
376
|
// Build tips text
|
|
301
377
|
const tips = sessionState.initialized
|
|
302
|
-
? 'Tip: /new <feature> to
|
|
378
|
+
? 'Tip: /new <feature> or /issue to browse issues, /help for commands'
|
|
303
379
|
: 'Tip: /init to set up, /help for commands';
|
|
304
380
|
const specSuggestions = useMemo(() => sessionState.specNames?.map((name) => ({ name, description: '' })), [sessionState.specNames]);
|
|
305
|
-
const inputElement = (_jsx(ChatInput, { onSubmit: handleSubmit, disabled:
|
|
381
|
+
const inputElement = (_jsxs(Box, { flexDirection: "column", children: [_jsx(ChatInput, { onSubmit: handleSubmit, disabled: issuePickerVisible, placeholder: "Enter command or type /help...", onCommand: (cmd) => handleSubmit(`/${cmd}`), specSuggestions: specSuggestions }), issuePickerVisible && (_jsx(IssuePicker, { issues: issuePickerIssues, repoSlug: issuePickerRepo ? `${issuePickerRepo.owner}/${issuePickerRepo.repo}` : '...', onSelect: handleIssueSelect, onCancel: handleIssueCancel, isLoading: issuePickerLoading, error: issuePickerError }))] }));
|
|
306
382
|
return (_jsxs(AppShell, { header: header, tips: tips, isWorking: syncStatus === 'running', workingStatus: syncStatus === 'running' ? 'Syncing project context\u2026' : undefined, input: inputElement, footerStatus: {
|
|
307
383
|
action: projectLabel || 'Main Shell',
|
|
308
384
|
phase: sessionState.provider ? `${sessionState.provider}/${sessionState.model}` : 'No provider',
|
|
@@ -25,13 +25,14 @@ import { ActivityFeed } from '../components/ActivityFeed.js';
|
|
|
25
25
|
import { colors, theme } from '../theme.js';
|
|
26
26
|
import { readLoopStatus, parseImplementationPlan, getGitBranch, formatNumber, getLoopLogPath, parseLoopLog, parsePhaseChanges, } from '../utils/loop-status.js';
|
|
27
27
|
import { buildEnhancedRunSummary } from '../utils/build-run-summary.js';
|
|
28
|
-
import { getCurrentCommitHash } from '../utils/git-summary.js';
|
|
28
|
+
import { getCurrentCommitHash, getRecentCommits } from '../utils/git-summary.js';
|
|
29
29
|
import { writeRunSummaryFile } from '../../utils/summary-file.js';
|
|
30
30
|
import { loadConfigWithDefaults } from '../../utils/config.js';
|
|
31
31
|
import { logger } from '../../utils/logger.js';
|
|
32
32
|
import { readActionRequest, writeActionReply, cleanupActionFiles } from '../utils/action-inbox.js';
|
|
33
33
|
const POLL_INTERVAL_MS = 2500;
|
|
34
34
|
const ERROR_TAIL_LINES = 12;
|
|
35
|
+
const RUN_REVIEW_MODES = ['manual', 'auto', 'merge'];
|
|
35
36
|
function findFeatureLoopScript(projectRoot, scriptsDir) {
|
|
36
37
|
const localScript = join(projectRoot, scriptsDir, 'feature-loop.sh');
|
|
37
38
|
if (existsSync(localScript)) {
|
|
@@ -112,6 +113,9 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
112
113
|
const [activityEvents, setActivityEvents] = useState([]);
|
|
113
114
|
const [latestCommit, setLatestCommit] = useState(null);
|
|
114
115
|
const [baselineCommit, setBaselineCommit] = useState(null);
|
|
116
|
+
const [recentCommits, setRecentCommits] = useState([]);
|
|
117
|
+
const [reviewMode, setReviewMode] = useState(reviewModeProp ?? 'manual');
|
|
118
|
+
const [loopModel, setLoopModel] = useState(null);
|
|
115
119
|
const childRef = useRef(null);
|
|
116
120
|
const stopRequestedRef = useRef(false);
|
|
117
121
|
const isMountedRef = useRef(true);
|
|
@@ -170,6 +174,14 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
170
174
|
else {
|
|
171
175
|
onCancel();
|
|
172
176
|
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Shift+R cycles review mode (manual → auto → merge)
|
|
180
|
+
if (input === 'R') {
|
|
181
|
+
setReviewMode((prev) => {
|
|
182
|
+
const idx = RUN_REVIEW_MODES.indexOf(prev);
|
|
183
|
+
return RUN_REVIEW_MODES[(idx + 1) % RUN_REVIEW_MODES.length];
|
|
184
|
+
});
|
|
173
185
|
}
|
|
174
186
|
});
|
|
175
187
|
const refreshStatus = useCallback(async () => {
|
|
@@ -184,10 +196,12 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
184
196
|
if (!isMountedRef.current)
|
|
185
197
|
return;
|
|
186
198
|
setBranch(getGitBranch(projectRoot));
|
|
187
|
-
// Update latest commit hash
|
|
199
|
+
// Update latest commit hash and recent commits
|
|
188
200
|
const head = getCurrentCommitHash(projectRoot);
|
|
189
|
-
if (head && isMountedRef.current)
|
|
201
|
+
if (head && isMountedRef.current) {
|
|
190
202
|
setLatestCommit(head);
|
|
203
|
+
setRecentCommits(getRecentCommits(projectRoot, 3));
|
|
204
|
+
}
|
|
191
205
|
// Collect new activity events from log and phase changes
|
|
192
206
|
const logPath = getLoopLogPath(featureName);
|
|
193
207
|
const allLogEvents = parseLoopLog(logPath);
|
|
@@ -336,6 +350,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
336
350
|
try {
|
|
337
351
|
const config = sessionState.config ?? await loadConfigWithDefaults(projectRoot);
|
|
338
352
|
specsDirRef.current = config.paths.specs;
|
|
353
|
+
if (!cancelled) {
|
|
354
|
+
setLoopModel(config.loop.defaultModel);
|
|
355
|
+
setReviewMode((prev) => reviewModeProp ?? config.loop.reviewMode ?? prev);
|
|
356
|
+
}
|
|
339
357
|
}
|
|
340
358
|
catch (err) {
|
|
341
359
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -378,6 +396,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
378
396
|
configRootRef.current = config.paths.root;
|
|
379
397
|
maxIterationsRef.current = config.loop.maxIterations;
|
|
380
398
|
maxE2eAttemptsRef.current = config.loop.maxE2eAttempts;
|
|
399
|
+
setLoopModel(config.loop.defaultModel);
|
|
381
400
|
const specFile = findSpecFile(projectRoot, featureName, config.paths.specs);
|
|
382
401
|
if (!specFile) {
|
|
383
402
|
setError(`Spec file not found for "${featureName}". Run /new ${featureName} first.`);
|
|
@@ -390,9 +409,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
390
409
|
setIsStarting(false);
|
|
391
410
|
return;
|
|
392
411
|
}
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
412
|
+
const effectiveReviewMode = reviewModeProp ?? config.loop.reviewMode ?? 'manual';
|
|
413
|
+
setReviewMode(effectiveReviewMode);
|
|
414
|
+
if (effectiveReviewMode !== 'manual' && effectiveReviewMode !== 'auto' && effectiveReviewMode !== 'merge') {
|
|
415
|
+
setError(`Invalid reviewMode '${effectiveReviewMode}'. Allowed values are 'manual', 'auto', or 'merge'.`);
|
|
396
416
|
setIsStarting(false);
|
|
397
417
|
return;
|
|
398
418
|
}
|
|
@@ -409,7 +429,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
409
429
|
String(config.loop.maxIterations),
|
|
410
430
|
String(config.loop.maxE2eAttempts),
|
|
411
431
|
'--review-mode',
|
|
412
|
-
|
|
432
|
+
effectiveReviewMode,
|
|
413
433
|
];
|
|
414
434
|
const child = spawn('bash', [scriptPath, ...args], {
|
|
415
435
|
cwd: dirname(scriptPath),
|
|
@@ -548,8 +568,11 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
548
568
|
: actionRequest
|
|
549
569
|
? 'Select an option, Esc for default'
|
|
550
570
|
: monitorOnly
|
|
551
|
-
? 'Ctrl+C stop, Esc back'
|
|
552
|
-
: 'Ctrl+C stop, Esc background';
|
|
571
|
+
? 'Ctrl+C stop, Esc back, Shift+R review mode'
|
|
572
|
+
: 'Ctrl+C stop, Esc background, Shift+R review mode';
|
|
573
|
+
// Progress bar label padding — align all labels to the longest one
|
|
574
|
+
const PROGRESS_LABEL_WIDTH = 17; // "Implementation: " padded
|
|
575
|
+
const padLabel = (label) => label.padEnd(PROGRESS_LABEL_WIDTH);
|
|
553
576
|
// Action select handler — awaits write before clearing prompt
|
|
554
577
|
const handleActionSelect = useCallback(async (choiceId) => {
|
|
555
578
|
if (!actionRequest)
|
|
@@ -590,7 +613,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
590
613
|
action: 'Run Loop',
|
|
591
614
|
phase: phaseLine,
|
|
592
615
|
path: featureName,
|
|
593
|
-
|
|
616
|
+
extra: `review: ${reviewMode}`,
|
|
617
|
+
}, children: completionSummary ? (_jsx(RunCompletionSummary, { summary: completionSummary })) : (!error && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Phase: " }), _jsx(Text, { color: colors.yellow, children: phaseLine }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Iter: " }), _jsx(Text, { color: colors.green, children: status.iteration }), _jsxs(Text, { dimColor: true, children: ["/", status.maxIterations || maxIterationsRef.current || '-'] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Branch: " }), _jsx(Text, { color: colors.blue, children: branch }), loopModel && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsx(Text, { children: "Model: " }), _jsx(Text, { color: colors.blue, children: loopModel })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(Text, { children: "Tokens: " }), _jsx(Text, { color: colors.pink, children: formatNumber(totalTokens) }), _jsxs(Text, { dimColor: true, children: [" (in:", formatNumber(status.tokensInput), " out:", formatNumber(status.tokensOutput), " cache:", formatNumber(status.cacheRead), ")"] }), _jsx(Text, { dimColor: true, children: theme.statusLine.separator }), _jsxs(Text, { dimColor: true, children: ["Elapsed: ", formatDuration(startTimeRef.current)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('Implementation:') }), _jsx(ProgressBar, { percent: percentTasks }), _jsxs(Text, { children: [String(percentTasks).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.tasksDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.tasksPending] })] }), totalE2e > 0 && (_jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, children: [_jsx(Text, { bold: true, children: padLabel('E2E Tests:') }), _jsx(ProgressBar, { percent: percentE2e }), _jsxs(Text, { children: [String(percentE2e).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", tasks.e2eDone] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", tasks.e2ePending] })] })), _jsxs(Box, { flexDirection: "row", alignItems: "center", gap: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: padLabel('Overall:') }), _jsx(ProgressBar, { percent: percentAll }), _jsxs(Text, { children: [String(percentAll).padStart(3), "%"] }), _jsxs(Text, { color: colors.green, children: ['\u2713', " ", doneAll] }), _jsxs(Text, { color: colors.yellow, children: ['\u25cb', " ", totalAll - doneAll] })] })] }), recentCommits.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Commits" }), recentCommits.map((c) => (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { dimColor: true, children: [" ", c.hash, " ", c.title] }) }, c.hash)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(ActivityFeed, { events: activityEvents, latestCommit: baselineCommit && latestCommit && baselineCommit !== latestCommit
|
|
594
618
|
? `${baselineCommit} \u2192 ${latestCommit}`
|
|
595
619
|
: latestCommit || undefined })] })] }))) }));
|
|
596
620
|
}
|
|
@@ -34,4 +34,5 @@ export declare function getDiffStats(projectRoot: string, fromHash: string, toHa
|
|
|
34
34
|
* @param toHash - Ending commit hash (inclusive)
|
|
35
35
|
* @returns Array of commit entries, or null if not available
|
|
36
36
|
*/
|
|
37
|
+
export declare function getRecentCommits(projectRoot: string, count?: number): CommitLogEntry[];
|
|
37
38
|
export declare function getCommitList(projectRoot: string, fromHash: string, toHash: string): CommitLogEntry[] | null;
|
|
@@ -69,6 +69,22 @@ export function getDiffStats(projectRoot, fromHash, toHash) {
|
|
|
69
69
|
* @param toHash - Ending commit hash (inclusive)
|
|
70
70
|
* @returns Array of commit entries, or null if not available
|
|
71
71
|
*/
|
|
72
|
+
export function getRecentCommits(projectRoot, count = 3) {
|
|
73
|
+
try {
|
|
74
|
+
const output = execFileSync('git', ['log', '--oneline', `-${count}`], { cwd: projectRoot, encoding: 'utf-8', timeout: 10_000 }).trim();
|
|
75
|
+
if (!output)
|
|
76
|
+
return [];
|
|
77
|
+
return output.split('\n').map((line) => {
|
|
78
|
+
const spaceIdx = line.indexOf(' ');
|
|
79
|
+
if (spaceIdx === -1)
|
|
80
|
+
return { hash: line, title: '' };
|
|
81
|
+
return { hash: line.substring(0, spaceIdx), title: line.substring(spaceIdx + 1) };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
72
88
|
export function getCommitList(projectRoot, fromHash, toHash) {
|
|
73
89
|
try {
|
|
74
90
|
const output = execFileSync('git', ['log', '--oneline', `${fromHash}..${toHash}`], {
|