wiggum-cli 0.16.0 → 0.17.1
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/bin/ralph.js +0 -0
- package/dist/agent/memory/ingest.d.ts +14 -0
- package/dist/agent/memory/ingest.js +77 -0
- package/dist/agent/memory/store.d.ts +15 -0
- package/dist/agent/memory/store.js +98 -0
- package/dist/agent/memory/types.d.ts +16 -0
- package/dist/agent/memory/types.js +14 -0
- package/dist/agent/orchestrator.d.ts +7 -0
- package/dist/agent/orchestrator.js +266 -0
- package/dist/agent/resolve-config.d.ts +26 -0
- package/dist/agent/resolve-config.js +43 -0
- package/dist/agent/tools/backlog.d.ts +27 -0
- package/dist/agent/tools/backlog.js +51 -0
- package/dist/agent/tools/dry-run.d.ts +106 -0
- package/dist/agent/tools/dry-run.js +119 -0
- package/dist/agent/tools/execution.d.ts +51 -0
- package/dist/agent/tools/execution.js +256 -0
- package/dist/agent/tools/feature-state.d.ts +43 -0
- package/dist/agent/tools/feature-state.js +184 -0
- package/dist/agent/tools/introspection.d.ts +23 -0
- package/dist/agent/tools/introspection.js +40 -0
- package/dist/agent/tools/memory.d.ts +44 -0
- package/dist/agent/tools/memory.js +99 -0
- package/dist/agent/tools/preflight.d.ts +7 -0
- package/dist/agent/tools/preflight.js +137 -0
- package/dist/agent/tools/reporting.d.ts +58 -0
- package/dist/agent/tools/reporting.js +119 -0
- package/dist/agent/tools/schemas.d.ts +2 -0
- package/dist/agent/tools/schemas.js +3 -0
- package/dist/agent/types.d.ts +45 -0
- package/dist/agent/types.js +1 -0
- package/dist/ai/conversation/conversation-manager.js +8 -0
- package/dist/ai/conversation/url-fetcher.js +27 -0
- package/dist/ai/providers.js +5 -5
- package/dist/commands/agent.d.ts +17 -0
- package/dist/commands/agent.js +114 -0
- package/dist/commands/monitor.js +50 -183
- package/dist/commands/new-auto.d.ts +15 -0
- package/dist/commands/new-auto.js +237 -0
- package/dist/commands/run.js +20 -10
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.js +68 -0
- package/dist/generator/config.d.ts +1 -41
- package/dist/generator/config.js +7 -0
- package/dist/generator/index.d.ts +2 -2
- package/dist/generator/templates.d.ts +2 -0
- package/dist/generator/templates.js +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +115 -4
- package/dist/repl/command-parser.d.ts +10 -0
- package/dist/repl/command-parser.js +10 -0
- package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
- package/dist/tui/app.d.ts +19 -2
- package/dist/tui/app.js +23 -4
- package/dist/tui/components/IssuePicker.d.ts +27 -0
- package/dist/tui/components/IssuePicker.js +73 -0
- package/dist/tui/components/RunCompletionSummary.js +6 -3
- package/dist/tui/components/StatusLine.d.ts +3 -1
- package/dist/tui/components/StatusLine.js +2 -2
- package/dist/tui/hooks/useAgentOrchestrator.d.ts +40 -0
- package/dist/tui/hooks/useAgentOrchestrator.js +491 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
- package/dist/tui/orchestration/interview-orchestrator.js +27 -6
- package/dist/tui/screens/AgentScreen.d.ts +24 -0
- package/dist/tui/screens/AgentScreen.js +209 -0
- package/dist/tui/screens/InitScreen.js +4 -0
- package/dist/tui/screens/InterviewScreen.d.ts +3 -1
- package/dist/tui/screens/InterviewScreen.js +146 -10
- package/dist/tui/screens/MainShell.d.ts +1 -1
- package/dist/tui/screens/MainShell.js +115 -4
- package/dist/tui/screens/RunScreen.js +72 -16
- package/dist/tui/utils/build-run-summary.d.ts +1 -1
- package/dist/tui/utils/build-run-summary.js +40 -84
- package/dist/tui/utils/clear-screen.d.ts +14 -0
- package/dist/tui/utils/clear-screen.js +16 -0
- package/dist/tui/utils/git-summary.d.ts +1 -0
- package/dist/tui/utils/git-summary.js +16 -0
- package/dist/tui/utils/loop-status.d.ts +41 -1
- package/dist/tui/utils/loop-status.js +243 -35
- package/dist/tui/utils/pr-summary.d.ts +3 -2
- package/dist/tui/utils/pr-summary.js +41 -6
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/github.d.ts +32 -0
- package/dist/utils/github.js +106 -0
- package/package.json +4 -1
- package/src/templates/prompts/PROMPT.md.tmpl +13 -10
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
- package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
- package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
- package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
- package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
|
@@ -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(),
|
|
@@ -171,6 +190,92 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
171
190
|
}
|
|
172
191
|
addSystemMessage(`No running loop found for "${featureName}".`);
|
|
173
192
|
}, [addSystemMessage, backgroundRuns, onNavigate]);
|
|
193
|
+
const handleAgent = useCallback((args) => {
|
|
194
|
+
if (!sessionState.initialized) {
|
|
195
|
+
addSystemMessage('Project not initialized. Run /init first.');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Parse optional flags
|
|
199
|
+
let dryRun = false;
|
|
200
|
+
let maxItems;
|
|
201
|
+
let reviewMode;
|
|
202
|
+
for (let i = 0; i < args.length; i++) {
|
|
203
|
+
if (args[i] === '--dry-run') {
|
|
204
|
+
dryRun = true;
|
|
205
|
+
}
|
|
206
|
+
else if (args[i] === '--max-items' && i + 1 < args.length) {
|
|
207
|
+
maxItems = parseInt(args[i + 1], 10);
|
|
208
|
+
if (Number.isNaN(maxItems)) {
|
|
209
|
+
addSystemMessage(`Invalid --max-items value '${args[i + 1]}'. Must be a number.`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
else if (args[i] === '--review-mode' && i + 1 < args.length) {
|
|
215
|
+
reviewMode = args[i + 1];
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (reviewMode !== undefined && !['manual', 'auto', 'merge'].includes(reviewMode)) {
|
|
220
|
+
addSystemMessage(`Invalid --review-mode value '${reviewMode}'. Use 'manual', 'auto', or 'merge'.`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
onNavigate('agent', { dryRun, maxItems, reviewMode });
|
|
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]);
|
|
174
279
|
const handleConfig = useCallback((args) => {
|
|
175
280
|
if (args.length === 0) {
|
|
176
281
|
addSystemMessage('Config management - not yet implemented in TUI mode. Use CLI: wiggum config');
|
|
@@ -220,6 +325,12 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
220
325
|
case 'monitor':
|
|
221
326
|
handleMonitor(args);
|
|
222
327
|
break;
|
|
328
|
+
case 'issue':
|
|
329
|
+
handleIssueCommand(args.join(' ') || undefined);
|
|
330
|
+
break;
|
|
331
|
+
case 'agent':
|
|
332
|
+
handleAgent(args);
|
|
333
|
+
break;
|
|
223
334
|
case 'config':
|
|
224
335
|
handleConfig(args);
|
|
225
336
|
break;
|
|
@@ -229,7 +340,7 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
229
340
|
default:
|
|
230
341
|
addSystemMessage(`Unknown command: ${commandName}`);
|
|
231
342
|
}
|
|
232
|
-
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleConfig, handleExit, addSystemMessage]);
|
|
343
|
+
}, [handleHelp, handleInit, handleSync, handleNew, handleRun, handleMonitor, handleIssueCommand, handleAgent, handleConfig, handleExit, addSystemMessage]);
|
|
233
344
|
const handleNaturalLanguage = useCallback((_text) => {
|
|
234
345
|
addSystemMessage('Tip: Use /help to see available commands, or /new <feature> to create a spec.');
|
|
235
346
|
}, [addSystemMessage]);
|
|
@@ -264,10 +375,10 @@ export function MainShell({ header, sessionState, onNavigate, backgroundRuns, in
|
|
|
264
375
|
});
|
|
265
376
|
// Build tips text
|
|
266
377
|
const tips = sessionState.initialized
|
|
267
|
-
? 'Tip: /new <feature> to
|
|
378
|
+
? 'Tip: /new <feature> or /issue to browse issues, /help for commands'
|
|
268
379
|
: 'Tip: /init to set up, /help for commands';
|
|
269
380
|
const specSuggestions = useMemo(() => sessionState.specNames?.map((name) => ({ name, description: '' })), [sessionState.specNames]);
|
|
270
|
-
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 }))] }));
|
|
271
382
|
return (_jsxs(AppShell, { header: header, tips: tips, isWorking: syncStatus === 'running', workingStatus: syncStatus === 'running' ? 'Syncing project context\u2026' : undefined, input: inputElement, footerStatus: {
|
|
272
383
|
action: projectLabel || 'Main Shell',
|
|
273
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)) {
|
|
@@ -101,6 +102,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
101
102
|
tasksPending: 0,
|
|
102
103
|
e2eDone: 0,
|
|
103
104
|
e2ePending: 0,
|
|
105
|
+
planExists: false,
|
|
104
106
|
});
|
|
105
107
|
const [branch, setBranch] = useState('-');
|
|
106
108
|
const [error, setError] = useState(null);
|
|
@@ -111,6 +113,9 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
111
113
|
const [activityEvents, setActivityEvents] = useState([]);
|
|
112
114
|
const [latestCommit, setLatestCommit] = useState(null);
|
|
113
115
|
const [baselineCommit, setBaselineCommit] = useState(null);
|
|
116
|
+
const [recentCommits, setRecentCommits] = useState([]);
|
|
117
|
+
const [reviewMode, setReviewMode] = useState(reviewModeProp ?? 'manual');
|
|
118
|
+
const [loopModel, setLoopModel] = useState(null);
|
|
114
119
|
const childRef = useRef(null);
|
|
115
120
|
const stopRequestedRef = useRef(false);
|
|
116
121
|
const isMountedRef = useRef(true);
|
|
@@ -124,6 +129,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
124
129
|
const handledActionIdRef = useRef(null);
|
|
125
130
|
const lastLogLineCountRef = useRef(0);
|
|
126
131
|
const lastKnownPhasesRef = useRef(undefined);
|
|
132
|
+
const lastActivityTimeRef = useRef(Date.now());
|
|
133
|
+
const lastCommitForEventRef = useRef(null);
|
|
127
134
|
// Read baseline commit once on mount
|
|
128
135
|
useEffect(() => {
|
|
129
136
|
const baselinePath = `/tmp/ralph-loop-${featureName}.baseline`;
|
|
@@ -167,6 +174,14 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
167
174
|
else {
|
|
168
175
|
onCancel();
|
|
169
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
|
+
});
|
|
170
185
|
}
|
|
171
186
|
});
|
|
172
187
|
const refreshStatus = useCallback(async () => {
|
|
@@ -181,10 +196,12 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
181
196
|
if (!isMountedRef.current)
|
|
182
197
|
return;
|
|
183
198
|
setBranch(getGitBranch(projectRoot));
|
|
184
|
-
// Update latest commit hash
|
|
199
|
+
// Update latest commit hash and recent commits
|
|
185
200
|
const head = getCurrentCommitHash(projectRoot);
|
|
186
|
-
if (head && isMountedRef.current)
|
|
201
|
+
if (head && isMountedRef.current) {
|
|
187
202
|
setLatestCommit(head);
|
|
203
|
+
setRecentCommits(getRecentCommits(projectRoot, 3));
|
|
204
|
+
}
|
|
188
205
|
// Collect new activity events from log and phase changes
|
|
189
206
|
const logPath = getLoopLogPath(featureName);
|
|
190
207
|
const allLogEvents = parseLoopLog(logPath);
|
|
@@ -198,11 +215,33 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
198
215
|
if (currentPhases) {
|
|
199
216
|
lastKnownPhasesRef.current = currentPhases;
|
|
200
217
|
}
|
|
218
|
+
// Emit a commit activity event when HEAD changes
|
|
219
|
+
if (head && head !== lastCommitForEventRef.current && lastCommitForEventRef.current !== null) {
|
|
220
|
+
newLogEvents.push({
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
message: `New commit: ${head.slice(0, 7)}`,
|
|
223
|
+
status: 'success',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
lastCommitForEventRef.current = head ?? null;
|
|
201
227
|
const MAX_STORED_EVENTS = 100;
|
|
202
228
|
const newEvents = [...newLogEvents, ...phaseEvents];
|
|
203
229
|
if (newEvents.length > 0 && isMountedRef.current) {
|
|
230
|
+
lastActivityTimeRef.current = Date.now();
|
|
204
231
|
setActivityEvents((prev) => [...prev, ...newEvents].slice(-MAX_STORED_EVENTS));
|
|
205
232
|
}
|
|
233
|
+
else if (nextStatus.running &&
|
|
234
|
+
nextStatus.phase !== 'Idle' &&
|
|
235
|
+
Date.now() - lastActivityTimeRef.current > 30_000 &&
|
|
236
|
+
isMountedRef.current) {
|
|
237
|
+
// Inject a synthetic "session in progress" event when stale
|
|
238
|
+
// Update lastActivityTimeRef so this doesn't fire every poll cycle
|
|
239
|
+
lastActivityTimeRef.current = Date.now();
|
|
240
|
+
setActivityEvents((prev) => [
|
|
241
|
+
...prev,
|
|
242
|
+
{ timestamp: Date.now(), message: `${nextStatus.phase} session in progress...`, status: 'in-progress' },
|
|
243
|
+
].slice(-MAX_STORED_EVENTS));
|
|
244
|
+
}
|
|
206
245
|
// Check for pending action request (loop waiting for user input)
|
|
207
246
|
const request = readActionRequest(featureName);
|
|
208
247
|
if (!isMountedRef.current)
|
|
@@ -230,6 +269,9 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
230
269
|
const tasksDone = nextTasks.tasksDone + nextTasks.e2eDone;
|
|
231
270
|
const tasksTotal = tasksDone + nextTasks.tasksPending + nextTasks.e2ePending;
|
|
232
271
|
const errorTail = exitCode !== 0 ? readLogTail(logPath, ERROR_TAIL_LINES) || undefined : undefined;
|
|
272
|
+
// Use feat/<feature> as the branch name for summary. getGitBranch() returns
|
|
273
|
+
// "main" after squash-merge + worktree cleanup, which breaks PR/issue detection.
|
|
274
|
+
const summaryBranch = `feat/${featureName}`;
|
|
233
275
|
const basicSummary = {
|
|
234
276
|
feature: featureName,
|
|
235
277
|
iterations: nextStatus.iteration,
|
|
@@ -242,13 +284,13 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
242
284
|
cacheRead: nextStatus.cacheRead,
|
|
243
285
|
exitCode,
|
|
244
286
|
exitCodeInferred: true,
|
|
245
|
-
branch:
|
|
287
|
+
branch: summaryBranch,
|
|
246
288
|
logPath,
|
|
247
289
|
errorTail,
|
|
248
290
|
};
|
|
249
291
|
let enhancedSummary;
|
|
250
292
|
try {
|
|
251
|
-
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
|
|
293
|
+
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName, baselineCommit);
|
|
252
294
|
}
|
|
253
295
|
catch (err) {
|
|
254
296
|
logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -260,7 +302,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
260
302
|
logger.error(`Failed to persist summary file for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
261
303
|
});
|
|
262
304
|
}
|
|
263
|
-
}, [featureName, projectRoot, monitorOnly]);
|
|
305
|
+
}, [featureName, projectRoot, monitorOnly, baselineCommit]);
|
|
264
306
|
// Keep a stable ref to the latest refreshStatus so the spawn effect
|
|
265
307
|
// can schedule polls without re-running when refreshStatus changes.
|
|
266
308
|
const refreshStatusRef = useRef(refreshStatus);
|
|
@@ -308,6 +350,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
308
350
|
try {
|
|
309
351
|
const config = sessionState.config ?? await loadConfigWithDefaults(projectRoot);
|
|
310
352
|
specsDirRef.current = config.paths.specs;
|
|
353
|
+
if (!cancelled) {
|
|
354
|
+
setLoopModel(config.loop.defaultModel);
|
|
355
|
+
setReviewMode((prev) => reviewModeProp ?? config.loop.reviewMode ?? prev);
|
|
356
|
+
}
|
|
311
357
|
}
|
|
312
358
|
catch (err) {
|
|
313
359
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -350,6 +396,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
350
396
|
configRootRef.current = config.paths.root;
|
|
351
397
|
maxIterationsRef.current = config.loop.maxIterations;
|
|
352
398
|
maxE2eAttemptsRef.current = config.loop.maxE2eAttempts;
|
|
399
|
+
setLoopModel(config.loop.defaultModel);
|
|
353
400
|
const specFile = findSpecFile(projectRoot, featureName, config.paths.specs);
|
|
354
401
|
if (!specFile) {
|
|
355
402
|
setError(`Spec file not found for "${featureName}". Run /new ${featureName} first.`);
|
|
@@ -362,9 +409,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
362
409
|
setIsStarting(false);
|
|
363
410
|
return;
|
|
364
411
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
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'.`);
|
|
368
416
|
setIsStarting(false);
|
|
369
417
|
return;
|
|
370
418
|
}
|
|
@@ -381,7 +429,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
381
429
|
String(config.loop.maxIterations),
|
|
382
430
|
String(config.loop.maxE2eAttempts),
|
|
383
431
|
'--review-mode',
|
|
384
|
-
|
|
432
|
+
effectiveReviewMode,
|
|
385
433
|
];
|
|
386
434
|
const child = spawn('bash', [scriptPath, ...args], {
|
|
387
435
|
cwd: dirname(scriptPath),
|
|
@@ -425,6 +473,10 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
425
473
|
closeSync(logFd);
|
|
426
474
|
logFdClosed = true;
|
|
427
475
|
}
|
|
476
|
+
if (!isMountedRef.current)
|
|
477
|
+
return;
|
|
478
|
+
// Wait for bash to flush state files (.phases, .tokens, .final)
|
|
479
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
428
480
|
if (!isMountedRef.current)
|
|
429
481
|
return;
|
|
430
482
|
let latestStatus;
|
|
@@ -436,7 +488,7 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
436
488
|
catch (err) {
|
|
437
489
|
logger.error(`Failed to read final run status for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
438
490
|
latestStatus = { running: false, iteration: 0, maxIterations: config.loop.maxIterations, phase: 'unknown', tokensInput: 0, tokensOutput: 0, cacheCreate: 0, cacheRead: 0 };
|
|
439
|
-
latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0 };
|
|
491
|
+
latestTasks = { tasksDone: 0, tasksPending: 0, e2eDone: 0, e2ePending: 0, planExists: false };
|
|
440
492
|
}
|
|
441
493
|
const tasksDone = latestTasks.tasksDone + latestTasks.e2eDone;
|
|
442
494
|
const tasksTotal = tasksDone + latestTasks.tasksPending + latestTasks.e2ePending;
|
|
@@ -453,14 +505,14 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
453
505
|
cacheCreate: latestStatus.cacheCreate,
|
|
454
506
|
cacheRead: latestStatus.cacheRead,
|
|
455
507
|
exitCode,
|
|
456
|
-
branch:
|
|
508
|
+
branch: `feat/${featureName}`,
|
|
457
509
|
logPath,
|
|
458
510
|
errorTail,
|
|
459
511
|
};
|
|
460
512
|
// Build enhanced summary with phases, git stats, PR/issue metadata
|
|
461
513
|
let enhancedSummary;
|
|
462
514
|
try {
|
|
463
|
-
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName);
|
|
515
|
+
enhancedSummary = buildEnhancedRunSummary(basicSummary, projectRoot, featureName, baselineCommit);
|
|
464
516
|
}
|
|
465
517
|
catch (err) {
|
|
466
518
|
logger.error(`Failed to build enhanced summary for ${featureName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -516,8 +568,11 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
516
568
|
: actionRequest
|
|
517
569
|
? 'Select an option, Esc for default'
|
|
518
570
|
: monitorOnly
|
|
519
|
-
? 'Ctrl+C stop, Esc back'
|
|
520
|
-
: '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);
|
|
521
576
|
// Action select handler — awaits write before clearing prompt
|
|
522
577
|
const handleActionSelect = useCallback(async (choiceId) => {
|
|
523
578
|
if (!actionRequest)
|
|
@@ -558,7 +613,8 @@ export function RunScreen({ header, featureName, projectRoot, sessionState, moni
|
|
|
558
613
|
action: 'Run Loop',
|
|
559
614
|
phase: phaseLine,
|
|
560
615
|
path: featureName,
|
|
561
|
-
|
|
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
|
|
562
618
|
? `${baselineCommit} \u2192 ${latestCommit}`
|
|
563
619
|
: latestCommit || undefined })] })] }))) }));
|
|
564
620
|
}
|
|
@@ -21,4 +21,4 @@ import type { RunSummary } from '../screens/RunScreen.js';
|
|
|
21
21
|
* which blocks the event loop. Callers should wrap in try-catch to handle
|
|
22
22
|
* failures gracefully.
|
|
23
23
|
*/
|
|
24
|
-
export declare function buildEnhancedRunSummary(basicSummary: RunSummary, projectRoot: string, feature: string): RunSummary;
|
|
24
|
+
export declare function buildEnhancedRunSummary(basicSummary: RunSummary, projectRoot: string, feature: string, baselineOverride?: string | null): RunSummary;
|
|
@@ -3,86 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
6
7
|
import { getCurrentCommitHash, getDiffStats, getCommitList } from './git-summary.js';
|
|
7
8
|
import { getPrForBranch, getLinkedIssue } from './pr-summary.js';
|
|
8
|
-
|
|
9
|
-
* Phase ID to human-readable label mapping
|
|
10
|
-
*/
|
|
11
|
-
const PHASE_LABELS = {
|
|
12
|
-
planning: 'Planning',
|
|
13
|
-
implementation: 'Implementation',
|
|
14
|
-
e2e_testing: 'E2E Testing',
|
|
15
|
-
verification: 'Verification',
|
|
16
|
-
pr_review: 'PR & Review',
|
|
17
|
-
};
|
|
18
|
-
const VALID_PHASE_STATUSES = new Set(['success', 'skipped', 'failed']);
|
|
19
|
-
/**
|
|
20
|
-
* Parse phase information from the phases file written by feature-loop.sh.
|
|
21
|
-
*
|
|
22
|
-
* Format: phase_id|status|start_timestamp|end_timestamp
|
|
23
|
-
* The parser handles duplicate phase entries defensively (last status wins,
|
|
24
|
-
* durations aggregate), though feature-loop.sh normally writes one final
|
|
25
|
-
* line per phase.
|
|
26
|
-
*
|
|
27
|
-
* @param phasesFilePath - Path to the phases file
|
|
28
|
-
* @returns Array of phase info objects
|
|
29
|
-
*/
|
|
30
|
-
function parsePhases(phasesFilePath) {
|
|
31
|
-
if (!existsSync(phasesFilePath)) {
|
|
32
|
-
logger.debug(`Phases file not found: ${phasesFilePath}`);
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const content = readFileSync(phasesFilePath, 'utf-8').trim();
|
|
37
|
-
if (!content) {
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
40
|
-
const lines = content.split('\n');
|
|
41
|
-
const phaseMap = new Map();
|
|
42
|
-
for (const line of lines) {
|
|
43
|
-
const parts = line.split('|');
|
|
44
|
-
if (parts.length < 4) {
|
|
45
|
-
logger.warn(`Skipping malformed phase line: ${line}`);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const [id, status, startStr, endStr] = parts;
|
|
49
|
-
// Validate status
|
|
50
|
-
if (!VALID_PHASE_STATUSES.has(status)) {
|
|
51
|
-
logger.warn(`Unknown phase status "${status}" for phase "${id}", treating as failed`);
|
|
52
|
-
}
|
|
53
|
-
const validatedStatus = VALID_PHASE_STATUSES.has(status)
|
|
54
|
-
? status
|
|
55
|
-
: 'failed';
|
|
56
|
-
// Parse timestamps
|
|
57
|
-
const startTime = parseInt(startStr, 10) || 0;
|
|
58
|
-
const endTime = parseInt(endStr, 10) || 0;
|
|
59
|
-
// Calculate duration (end - start) in milliseconds
|
|
60
|
-
const durationMs = endTime > 0 && startTime > 0 ? (endTime - startTime) * 1000 : undefined;
|
|
61
|
-
// Get or create phase entry
|
|
62
|
-
let phase = phaseMap.get(id);
|
|
63
|
-
if (!phase) {
|
|
64
|
-
phase = {
|
|
65
|
-
id,
|
|
66
|
-
label: PHASE_LABELS[id] || id,
|
|
67
|
-
status: validatedStatus,
|
|
68
|
-
durationMs: 0,
|
|
69
|
-
};
|
|
70
|
-
phaseMap.set(id, phase);
|
|
71
|
-
}
|
|
72
|
-
// Update status (last status wins)
|
|
73
|
-
phase.status = validatedStatus;
|
|
74
|
-
// Aggregate duration
|
|
75
|
-
if (durationMs !== undefined) {
|
|
76
|
-
phase.durationMs = (phase.durationMs || 0) + durationMs;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return Array.from(phaseMap.values());
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
logger.warn(`Failed to parse phases file: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
9
|
+
import { parsePhases } from './loop-status.js';
|
|
86
10
|
/**
|
|
87
11
|
* Read baseline commit hash from the baseline file.
|
|
88
12
|
*
|
|
@@ -127,7 +51,7 @@ function readBaselineCommit(baselineFilePath) {
|
|
|
127
51
|
* which blocks the event loop. Callers should wrap in try-catch to handle
|
|
128
52
|
* failures gracefully.
|
|
129
53
|
*/
|
|
130
|
-
export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
|
|
54
|
+
export function buildEnhancedRunSummary(basicSummary, projectRoot, feature, baselineOverride) {
|
|
131
55
|
const phasesFilePath = `/tmp/ralph-loop-${feature}.phases`;
|
|
132
56
|
const baselineFilePath = `/tmp/ralph-loop-${feature}.baseline`;
|
|
133
57
|
// Parse phases and set implementation iterations from actual loop count
|
|
@@ -153,8 +77,10 @@ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
|
|
|
153
77
|
completed: basicSummary.tasksDone,
|
|
154
78
|
total: basicSummary.tasksTotal,
|
|
155
79
|
};
|
|
156
|
-
// Git changes and commits
|
|
157
|
-
const baselineCommit =
|
|
80
|
+
// Git changes and commits — use override if provided (avoids re-reading a cleaned-up file)
|
|
81
|
+
const baselineCommit = baselineOverride !== undefined
|
|
82
|
+
? (baselineOverride ? baselineOverride.substring(0, 7) : null)
|
|
83
|
+
: readBaselineCommit(baselineFilePath);
|
|
158
84
|
const currentCommit = getCurrentCommitHash(projectRoot);
|
|
159
85
|
let changes = { available: false };
|
|
160
86
|
let commits = { available: false };
|
|
@@ -205,13 +131,19 @@ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
|
|
|
205
131
|
available: true,
|
|
206
132
|
created: true,
|
|
207
133
|
};
|
|
208
|
-
// Try to get linked issue, passing prInfo
|
|
209
|
-
const issueInfo = getLinkedIssue(projectRoot, basicSummary.branch, prInfo);
|
|
134
|
+
// Try to get linked issue, passing prInfo and feature name for fallback detection
|
|
135
|
+
const issueInfo = getLinkedIssue(projectRoot, basicSummary.branch, prInfo, feature);
|
|
210
136
|
if (issueInfo) {
|
|
137
|
+
// When PR is merged with "Closes #N", GitHub auto-closes the issue.
|
|
138
|
+
// The summary may be built before GitHub processes the webhook, so
|
|
139
|
+
// infer closure from PR state to avoid showing stale "OPEN" status.
|
|
140
|
+
const inferredState = prInfo.state === 'MERGED' && issueInfo.state === 'OPEN'
|
|
141
|
+
? 'CLOSED'
|
|
142
|
+
: issueInfo.state;
|
|
211
143
|
issue = {
|
|
212
144
|
number: issueInfo.number,
|
|
213
145
|
url: issueInfo.url,
|
|
214
|
-
status:
|
|
146
|
+
status: inferredState,
|
|
215
147
|
available: true,
|
|
216
148
|
linked: true,
|
|
217
149
|
};
|
|
@@ -219,6 +151,30 @@ export function buildEnhancedRunSummary(basicSummary, projectRoot, feature) {
|
|
|
219
151
|
else {
|
|
220
152
|
issue = { available: true, linked: false };
|
|
221
153
|
}
|
|
154
|
+
// Enrich commits from PR when squash-merge detected (1 local commit + merged PR)
|
|
155
|
+
if (prInfo.state === 'MERGED' &&
|
|
156
|
+
commits.available &&
|
|
157
|
+
commits.commitList &&
|
|
158
|
+
commits.commitList.length <= 1) {
|
|
159
|
+
try {
|
|
160
|
+
const prCommitsOutput = execFileSync('gh', ['pr', 'view', String(prInfo.number), '--json', 'commits'], { cwd: projectRoot, encoding: 'utf-8', timeout: 10_000 }).trim();
|
|
161
|
+
const prCommitsData = JSON.parse(prCommitsOutput);
|
|
162
|
+
const prCommits = prCommitsData.commits;
|
|
163
|
+
if (Array.isArray(prCommits) && prCommits.length > 1) {
|
|
164
|
+
commits = {
|
|
165
|
+
...commits,
|
|
166
|
+
commitList: prCommits.map((c) => ({
|
|
167
|
+
hash: c.oid?.substring(0, 7) ?? '',
|
|
168
|
+
title: c.messageHeadline ?? '',
|
|
169
|
+
})),
|
|
170
|
+
mergeType: 'squash',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
logger.debug(`Failed to fetch PR commits: ${err instanceof Error ? err.message : String(err)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
222
178
|
}
|
|
223
179
|
else {
|
|
224
180
|
pr = { available: true, created: false };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clear the terminal screen to prevent stale PTY scroll buffer artifacts.
|
|
3
|
+
*
|
|
4
|
+
* When Ink re-renders significantly shorter output (e.g. after dismissing a
|
|
5
|
+
* 25-row IssuePicker), the old content remains in the PTY scroll buffer,
|
|
6
|
+
* causing visual duplication of the banner and other content. This sends
|
|
7
|
+
* ANSI escape sequences to clear the entire screen and reset the cursor.
|
|
8
|
+
*
|
|
9
|
+
* This is an intentional mix of imperative stdout writes with Ink's
|
|
10
|
+
* declarative rendering — Ink has no API to flush the PTY scroll buffer.
|
|
11
|
+
* Call this *before* state updates that shrink the rendered output so
|
|
12
|
+
* Ink's next render paints onto a clean screen.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clearScreen(stdout: NodeJS.WriteStream): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clear the terminal screen to prevent stale PTY scroll buffer artifacts.
|
|
3
|
+
*
|
|
4
|
+
* When Ink re-renders significantly shorter output (e.g. after dismissing a
|
|
5
|
+
* 25-row IssuePicker), the old content remains in the PTY scroll buffer,
|
|
6
|
+
* causing visual duplication of the banner and other content. This sends
|
|
7
|
+
* ANSI escape sequences to clear the entire screen and reset the cursor.
|
|
8
|
+
*
|
|
9
|
+
* This is an intentional mix of imperative stdout writes with Ink's
|
|
10
|
+
* declarative rendering — Ink has no API to flush the PTY scroll buffer.
|
|
11
|
+
* Call this *before* state updates that shrink the rendered output so
|
|
12
|
+
* Ink's next render paints onto a clean screen.
|
|
13
|
+
*/
|
|
14
|
+
export function clearScreen(stdout) {
|
|
15
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
16
|
+
}
|
|
@@ -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;
|