ralph-review 0.2.4 → 0.2.5
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/package.json +1 -1
- package/src/lib/review-workflow/review/run-review-session.ts +14 -0
- package/src/lib/stop-session.ts +63 -1
- package/src/lib/tui/dashboard/Dashboard.tsx +14 -9
- package/src/lib/tui/dashboard/use-dashboard-run-control.ts +11 -3
- package/src/lib/tui/dashboard/use-dashboard-stop-control.ts +17 -10
- package/src/lib/tui/shared/types.ts +10 -0
package/package.json
CHANGED
|
@@ -325,6 +325,20 @@ export async function runReviewSession(
|
|
|
325
325
|
if (entry.type === "review_iteration") {
|
|
326
326
|
latestPersistedFindings = [...latestPersistedFindings, ...entry.findings];
|
|
327
327
|
completedReviewIterations = entry.iteration;
|
|
328
|
+
|
|
329
|
+
if (worktree && latestPersistedFindings.length > 0) {
|
|
330
|
+
await deps.saveFindingsArtifact(
|
|
331
|
+
CONFIG_DIR,
|
|
332
|
+
createFindingsArtifact(
|
|
333
|
+
sessionId,
|
|
334
|
+
projectPath,
|
|
335
|
+
sessionPath,
|
|
336
|
+
worktree,
|
|
337
|
+
latestPersistedFindings
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
shouldDeleteSessionRefs = false;
|
|
341
|
+
}
|
|
328
342
|
}
|
|
329
343
|
};
|
|
330
344
|
|
package/src/lib/stop-session.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { discardSessionWorktree, type GitSessionWorktree } from "@/lib/git";
|
|
2
|
-
import { deleteSessionFiles, readLog } from "@/lib/logger";
|
|
2
|
+
import { appendLog, deleteSessionFiles, readLog } from "@/lib/logger";
|
|
3
3
|
import {
|
|
4
4
|
type ActiveSession,
|
|
5
5
|
readSessionState,
|
|
@@ -27,6 +27,7 @@ interface WaitForGracefulStopDeps {
|
|
|
27
27
|
|
|
28
28
|
interface StopActiveSessionDeps {
|
|
29
29
|
readLog: typeof readLog;
|
|
30
|
+
appendLog: typeof appendLog;
|
|
30
31
|
deleteSessionFiles: typeof deleteSessionFiles;
|
|
31
32
|
updateSessionState: typeof updateSessionState;
|
|
32
33
|
sendInterrupt: typeof sendInterrupt;
|
|
@@ -41,6 +42,7 @@ interface StopActiveSessionDeps {
|
|
|
41
42
|
|
|
42
43
|
const DEFAULT_STOP_ACTIVE_SESSION_DEPS: StopActiveSessionDeps = {
|
|
43
44
|
readLog,
|
|
45
|
+
appendLog,
|
|
44
46
|
deleteSessionFiles,
|
|
45
47
|
updateSessionState,
|
|
46
48
|
sendInterrupt,
|
|
@@ -137,6 +139,7 @@ interface SessionIterationState {
|
|
|
137
139
|
hasRecordedIteration: boolean;
|
|
138
140
|
hasRecordedReviewProgress: boolean;
|
|
139
141
|
hasSuccessfulReviewIteration: boolean;
|
|
142
|
+
entries: LogEntry[];
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
async function resolveSessionIterationState(
|
|
@@ -153,12 +156,58 @@ async function resolveSessionIterationState(
|
|
|
153
156
|
hasRecordedIteration: hasRecordedIteration(entries),
|
|
154
157
|
hasRecordedReviewProgress: hasRecordedReviewProgress(entries),
|
|
155
158
|
hasSuccessfulReviewIteration: hasSuccessfulReviewIteration(entries),
|
|
159
|
+
entries,
|
|
156
160
|
};
|
|
157
161
|
} catch {
|
|
158
162
|
return null;
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
165
|
|
|
166
|
+
function getLatestLifecycleEntry(entries: LogEntry[]): LogEntry | undefined {
|
|
167
|
+
return [...entries].reverse().find((entry) => entry.type !== "handoff");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getLatestReviewIteration(entries: LogEntry[]) {
|
|
171
|
+
return entries.filter((entry) => entry.type === "review_iteration").at(-1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getAccumulatedReviewFindings(entries: LogEntry[]) {
|
|
175
|
+
return entries.flatMap((entry) => (entry.type === "review_iteration" ? entry.findings : []));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function terminalizeForceStoppedReviewSession(
|
|
179
|
+
session: ActiveSession,
|
|
180
|
+
iterationState: SessionIterationState | null,
|
|
181
|
+
deps: StopActiveSessionDeps
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
if (!session.sessionPath || !iterationState?.hasRecordedReviewProgress) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (getLatestLifecycleEntry(iterationState.entries)?.type === "session_end") {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const latestReviewIteration = getLatestReviewIteration(iterationState.entries);
|
|
192
|
+
if (!latestReviewIteration) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await deps.appendLog(session.sessionPath, {
|
|
197
|
+
type: "session_end",
|
|
198
|
+
timestamp: Date.now(),
|
|
199
|
+
status: "interrupted",
|
|
200
|
+
reason: "Review stopped by user.",
|
|
201
|
+
iterations: latestReviewIteration.iteration,
|
|
202
|
+
phase: "review",
|
|
203
|
+
sessionStatus: "interrupted",
|
|
204
|
+
reviewOutcome:
|
|
205
|
+
getAccumulatedReviewFindings(iterationState.entries).length > 0
|
|
206
|
+
? "findings-pending"
|
|
207
|
+
: "incomplete",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
162
211
|
function createCleanupWorktree(
|
|
163
212
|
session: ActiveSession,
|
|
164
213
|
sourceRepoPath: string
|
|
@@ -239,6 +288,15 @@ export async function stopActiveSession(
|
|
|
239
288
|
}
|
|
240
289
|
|
|
241
290
|
const finalIterationState = await resolveSessionIterationState(session, stopDeps);
|
|
291
|
+
let terminalizeSessionError: unknown;
|
|
292
|
+
if (!stoppedGracefully) {
|
|
293
|
+
try {
|
|
294
|
+
await terminalizeForceStoppedReviewSession(session, finalIterationState, stopDeps);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
terminalizeSessionError = error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
242
300
|
if (finalIterationState?.hasSuccessfulReviewIteration === false) {
|
|
243
301
|
cleanupUnpromotedSessionWorktree(session, stopDeps);
|
|
244
302
|
}
|
|
@@ -256,6 +314,10 @@ export async function stopActiveSession(
|
|
|
256
314
|
expectedSessionId: session.sessionId,
|
|
257
315
|
});
|
|
258
316
|
|
|
317
|
+
if (terminalizeSessionError) {
|
|
318
|
+
throw terminalizeSessionError;
|
|
319
|
+
}
|
|
320
|
+
|
|
259
321
|
if (deleteSessionFilesError) {
|
|
260
322
|
throw deleteSessionFilesError;
|
|
261
323
|
}
|
|
@@ -17,9 +17,11 @@ import { StatusBar } from "./StatusBar";
|
|
|
17
17
|
import { useDashboardRunControl } from "./use-dashboard-run-control";
|
|
18
18
|
import { useDashboardStopControl } from "./use-dashboard-stop-control";
|
|
19
19
|
|
|
20
|
-
export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: DashboardProps) {
|
|
20
|
+
export function Dashboard({ projectPath, branch, refreshInterval = 1000, deps }: DashboardProps) {
|
|
21
21
|
const renderer = useRenderer();
|
|
22
|
-
const
|
|
22
|
+
const resolvedUseWorkspaceState = deps?.useWorkspaceState ?? useWorkspaceState;
|
|
23
|
+
const ResolvedDashboardOverlays = deps?.DashboardOverlays ?? DashboardOverlays;
|
|
24
|
+
const state = resolvedUseWorkspaceState(projectPath, branch, refreshInterval);
|
|
23
25
|
const {
|
|
24
26
|
runError,
|
|
25
27
|
startupMode,
|
|
@@ -29,7 +31,7 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
|
|
|
29
31
|
spawnRunProcess,
|
|
30
32
|
spawnFixProcess,
|
|
31
33
|
isStartupSpawning,
|
|
32
|
-
} = useDashboardRunControl(projectPath);
|
|
34
|
+
} = useDashboardRunControl(projectPath, { spawn: deps?.spawn });
|
|
33
35
|
const [focusedPane, setFocusedPane] = useState<FocusedPane>("detail");
|
|
34
36
|
const [outputVisible, setOutputVisible] = useState(false);
|
|
35
37
|
const [showHelp, setShowHelp] = useState(false);
|
|
@@ -37,11 +39,14 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
|
|
|
37
39
|
const [showSession, setShowSession] = useState(false);
|
|
38
40
|
const [showReviewModeOverlay, setShowReviewModeOverlay] = useState(false);
|
|
39
41
|
const [showStopPicker, setShowStopPicker] = useState(false);
|
|
40
|
-
const { isStoppingRun, stopSelectedSession } = useDashboardStopControl(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const { isStoppingRun, stopSelectedSession } = useDashboardStopControl(
|
|
43
|
+
{
|
|
44
|
+
currentSession: state.currentSession,
|
|
45
|
+
setShowStopPicker,
|
|
46
|
+
onError: setRunError,
|
|
47
|
+
},
|
|
48
|
+
{ stopActiveSession: deps?.stopActiveSession }
|
|
49
|
+
);
|
|
45
50
|
|
|
46
51
|
const projectName = basename(projectPath);
|
|
47
52
|
const isExitingRef = useRef(false);
|
|
@@ -271,7 +276,7 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
|
|
|
271
276
|
liveRefreshError={state.liveRefreshError}
|
|
272
277
|
configWarning={state.configWarning}
|
|
273
278
|
/>
|
|
274
|
-
<
|
|
279
|
+
<ResolvedDashboardOverlays
|
|
275
280
|
showHelp={showHelp}
|
|
276
281
|
showRunOverlay={showRunOverlay}
|
|
277
282
|
showFixFindings={showFixFindings}
|
|
@@ -15,10 +15,18 @@ export interface DashboardRunControl {
|
|
|
15
15
|
isStartupSpawning: () => boolean;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
interface DashboardRunControlDeps {
|
|
19
|
+
spawn?: typeof Bun.spawn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useDashboardRunControl(
|
|
23
|
+
projectPath: string,
|
|
24
|
+
deps?: DashboardRunControlDeps
|
|
25
|
+
): DashboardRunControl {
|
|
19
26
|
const [runError, setRunError] = useState<string | null>(null);
|
|
20
27
|
const [startupMode, setStartupMode] = useState<DashboardStartupMode>(null);
|
|
21
28
|
const isStartupSpawningRef = useRef(false);
|
|
29
|
+
const spawn = deps?.spawn ?? Bun.spawn;
|
|
22
30
|
|
|
23
31
|
const clearRunError = useCallback(() => {
|
|
24
32
|
setRunError(null);
|
|
@@ -40,7 +48,7 @@ export function useDashboardRunControl(projectPath: string): DashboardRunControl
|
|
|
40
48
|
setStartupMode(nextStartupMode);
|
|
41
49
|
|
|
42
50
|
try {
|
|
43
|
-
const subprocess =
|
|
51
|
+
const subprocess = spawn([process.execPath, CLI_PATH, command, ...argv], {
|
|
44
52
|
cwd: projectPath,
|
|
45
53
|
stdin: "ignore",
|
|
46
54
|
stdout: "pipe",
|
|
@@ -73,7 +81,7 @@ export function useDashboardRunControl(projectPath: string): DashboardRunControl
|
|
|
73
81
|
setRunError(getErrorMessage(error));
|
|
74
82
|
}
|
|
75
83
|
},
|
|
76
|
-
[projectPath]
|
|
84
|
+
[projectPath, spawn]
|
|
77
85
|
);
|
|
78
86
|
|
|
79
87
|
const spawnRunProcess = useCallback(
|
|
@@ -22,13 +22,17 @@ export interface DashboardStopControl {
|
|
|
22
22
|
stopSelectedSession: (session: ActiveSession) => Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
interface DashboardStopControlDeps {
|
|
26
|
+
stopActiveSession?: typeof stopActiveSession;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useDashboardStopControl(
|
|
30
|
+
options: DashboardStopControlOptions,
|
|
31
|
+
deps?: DashboardStopControlDeps
|
|
32
|
+
): DashboardStopControl {
|
|
30
33
|
const [stoppingSession, setStoppingSession] = useState<StoppingSessionState | null>(null);
|
|
31
34
|
const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
35
|
+
const stopSession = deps?.stopActiveSession ?? stopActiveSession;
|
|
32
36
|
|
|
33
37
|
const clearSettleTimer = useMemo(
|
|
34
38
|
() => () => {
|
|
@@ -42,7 +46,10 @@ export function useDashboardStopControl({
|
|
|
42
46
|
|
|
43
47
|
if (
|
|
44
48
|
stoppingSession &&
|
|
45
|
-
shouldClearStoppingSessionState({
|
|
49
|
+
shouldClearStoppingSessionState({
|
|
50
|
+
marker: stoppingSession,
|
|
51
|
+
currentSession: options.currentSession,
|
|
52
|
+
})
|
|
46
53
|
) {
|
|
47
54
|
clearSettleTimer();
|
|
48
55
|
setStoppingSession(null);
|
|
@@ -57,8 +64,8 @@ export function useDashboardStopControl({
|
|
|
57
64
|
|
|
58
65
|
try {
|
|
59
66
|
await stopSelectedDashboardSession(session, {
|
|
60
|
-
setShowStopPicker,
|
|
61
|
-
stopActiveSession,
|
|
67
|
+
setShowStopPicker: options.setShowStopPicker,
|
|
68
|
+
stopActiveSession: stopSession,
|
|
62
69
|
});
|
|
63
70
|
|
|
64
71
|
const settled = settleStoppingSessionState(createStoppingSessionState(session));
|
|
@@ -78,10 +85,10 @@ export function useDashboardStopControl({
|
|
|
78
85
|
} catch (error) {
|
|
79
86
|
clearSettleTimer();
|
|
80
87
|
setStoppingSession(null);
|
|
81
|
-
onError(getErrorMessage(error));
|
|
88
|
+
options.onError(getErrorMessage(error));
|
|
82
89
|
}
|
|
83
90
|
},
|
|
84
|
-
[clearSettleTimer, onError, setShowStopPicker]
|
|
91
|
+
[clearSettleTimer, options.onError, options.setShowStopPicker, stopSession]
|
|
85
92
|
);
|
|
86
93
|
|
|
87
94
|
return {
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import type { stopActiveSession } from "@/lib/stop-session";
|
|
2
|
+
import type { DashboardOverlays } from "@/lib/tui/dashboard/DashboardOverlays";
|
|
3
|
+
import type { useWorkspaceState } from "@/lib/tui/workspace/use-workspace-state";
|
|
4
|
+
|
|
1
5
|
export interface DashboardProps {
|
|
2
6
|
projectPath: string;
|
|
3
7
|
branch?: string;
|
|
4
8
|
refreshInterval?: number;
|
|
9
|
+
deps?: {
|
|
10
|
+
useWorkspaceState?: typeof useWorkspaceState;
|
|
11
|
+
DashboardOverlays?: typeof DashboardOverlays;
|
|
12
|
+
spawn?: typeof Bun.spawn;
|
|
13
|
+
stopActiveSession?: typeof stopActiveSession;
|
|
14
|
+
};
|
|
5
15
|
}
|