ralph-review 0.2.1 → 0.2.3
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/README.md +123 -16
- package/package.json +6 -4
- package/src/cli-core.ts +51 -88
- package/src/cli-rrr.ts +1 -4
- package/src/cli.ts +1 -2
- package/src/commands/apply.ts +6 -14
- package/src/commands/config-handlers.ts +68 -69
- package/src/commands/config-model.ts +147 -125
- package/src/commands/doctor.ts +2 -4
- package/src/commands/fix.ts +73 -51
- package/src/commands/handoff-selection.ts +6 -8
- package/src/commands/init.ts +12 -6
- package/src/commands/interactive-deps.ts +18 -0
- package/src/commands/log.ts +12 -12
- package/src/commands/run.ts +32 -33
- package/src/commands/stop.ts +6 -13
- package/src/commands/update.ts +2 -4
- package/src/lib/agents/claude.ts +4 -16
- package/src/lib/agents/core.ts +16 -0
- package/src/lib/agents/droid.ts +4 -15
- package/src/lib/agents/models.ts +2 -2
- package/src/lib/cli-parser.ts +19 -14
- package/src/lib/handoff.ts +16 -7
- package/src/lib/logging/session-log.ts +2 -1
- package/src/lib/prompts/defaults/review.md +1 -1
- package/src/lib/prompts/protocol.ts +2 -1
- package/src/lib/review-workflow/findings/artifact.ts +3 -1
- package/src/lib/review-workflow/findings/types.ts +1 -1
- package/src/lib/review-workflow/remediation/prompt.ts +7 -7
- package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
- package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
- package/src/lib/review-workflow/results/finalize-result.ts +20 -3
- package/src/lib/review-workflow/run-review-cycle.ts +1 -12
- package/src/lib/review-workflow/session-status.ts +13 -0
- package/src/lib/review-workflow/shared/framed-json.ts +2 -47
- package/src/lib/session/state.ts +50 -38
- package/src/lib/structured-output.ts +24 -9
- package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
- package/src/lib/tui/dashboard/ReviewModeOverlay.tsx +91 -139
- package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
- package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
- package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
- package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
- package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
- package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
- package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
- package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
- package/src/lib/tui/shared/CenteredModal.tsx +44 -0
- package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
- package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
- package/src/lib/tui/workspace/Workspace.tsx +6 -91
- package/src/lib/tui/workspace/use-workspace-state.ts +44 -37
- package/src/lib/types/fix.ts +15 -48
- package/src/lib/types/guards.ts +47 -0
- package/src/lib/types/review.ts +5 -39
|
@@ -19,6 +19,56 @@ function formatConfidenceScore(value: number): string {
|
|
|
19
19
|
return `${Math.round(value * 100)}%`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function EmptyListMessage() {
|
|
23
|
+
return (
|
|
24
|
+
<text fg={TUI_COLORS.text.dim} paddingLeft={2}>
|
|
25
|
+
None yet
|
|
26
|
+
</text>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ScrollableList({
|
|
31
|
+
content,
|
|
32
|
+
focused,
|
|
33
|
+
height,
|
|
34
|
+
scrollable,
|
|
35
|
+
}: {
|
|
36
|
+
content: React.ReactNode;
|
|
37
|
+
focused: boolean;
|
|
38
|
+
height: BoxHeight;
|
|
39
|
+
scrollable: boolean;
|
|
40
|
+
}) {
|
|
41
|
+
if (!scrollable) {
|
|
42
|
+
return <box paddingLeft={2}>{content}</box>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<scrollbox paddingLeft={2} height={height} focused={focused}>
|
|
47
|
+
{content}
|
|
48
|
+
</scrollbox>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function PriorityTitleRow({
|
|
53
|
+
priority,
|
|
54
|
+
title,
|
|
55
|
+
}: {
|
|
56
|
+
priority: Finding["priority"] | FixEntry["priority"];
|
|
57
|
+
title: string;
|
|
58
|
+
}) {
|
|
59
|
+
return (
|
|
60
|
+
<box flexDirection="row">
|
|
61
|
+
<text>
|
|
62
|
+
<PriorityText priority={priority} />
|
|
63
|
+
</text>
|
|
64
|
+
<text fg={TUI_COLORS.text.dim}> ▸ </text>
|
|
65
|
+
<text fg={TUI_COLORS.text.secondary} wrapMode="none">
|
|
66
|
+
{toSingleLine(title)}
|
|
67
|
+
</text>
|
|
68
|
+
</box>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
22
72
|
export function SectionHeader({
|
|
23
73
|
title,
|
|
24
74
|
count,
|
|
@@ -55,11 +105,7 @@ export function FindingsList({
|
|
|
55
105
|
showConfidence?: boolean;
|
|
56
106
|
}) {
|
|
57
107
|
if (findings.length === 0) {
|
|
58
|
-
return
|
|
59
|
-
<text fg={TUI_COLORS.text.dim} paddingLeft={2}>
|
|
60
|
-
None yet
|
|
61
|
-
</text>
|
|
62
|
-
);
|
|
108
|
+
return <EmptyListMessage />;
|
|
63
109
|
}
|
|
64
110
|
|
|
65
111
|
const content = findings.map((finding, index) => {
|
|
@@ -70,15 +116,10 @@ export function FindingsList({
|
|
|
70
116
|
return (
|
|
71
117
|
<box key={key} flexDirection="column">
|
|
72
118
|
{showBody && index > 0 && <text> </text>}
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<text fg={TUI_COLORS.text.dim}> ▸ </text>
|
|
78
|
-
<text fg={TUI_COLORS.text.secondary} wrapMode="none">
|
|
79
|
-
{toSingleLine(formatFindingTitleForDisplay(finding.title))}
|
|
80
|
-
</text>
|
|
81
|
-
</box>
|
|
119
|
+
<PriorityTitleRow
|
|
120
|
+
priority={finding.priority}
|
|
121
|
+
title={formatFindingTitleForDisplay(finding.title)}
|
|
122
|
+
/>
|
|
82
123
|
{showBody && (
|
|
83
124
|
<>
|
|
84
125
|
<text> </text>
|
|
@@ -99,14 +140,8 @@ export function FindingsList({
|
|
|
99
140
|
);
|
|
100
141
|
});
|
|
101
142
|
|
|
102
|
-
if (!scrollable) {
|
|
103
|
-
return <box paddingLeft={2}>{content}</box>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
143
|
return (
|
|
107
|
-
<
|
|
108
|
-
{content}
|
|
109
|
-
</scrollbox>
|
|
144
|
+
<ScrollableList content={content} focused={focused} height={height} scrollable={scrollable} />
|
|
110
145
|
);
|
|
111
146
|
}
|
|
112
147
|
|
|
@@ -153,11 +188,7 @@ export function SelectableStoredFindingsList({
|
|
|
153
188
|
selectedFirst?: boolean;
|
|
154
189
|
}) {
|
|
155
190
|
if (findings.length === 0) {
|
|
156
|
-
return
|
|
157
|
-
<text fg={TUI_COLORS.text.dim} paddingLeft={2}>
|
|
158
|
-
None yet
|
|
159
|
-
</text>
|
|
160
|
-
);
|
|
191
|
+
return <EmptyListMessage />;
|
|
161
192
|
}
|
|
162
193
|
|
|
163
194
|
const selectedIdSet = new Set(selectedFindingIds);
|
|
@@ -193,14 +224,8 @@ export function SelectableStoredFindingsList({
|
|
|
193
224
|
);
|
|
194
225
|
});
|
|
195
226
|
|
|
196
|
-
if (!scrollable) {
|
|
197
|
-
return <box paddingLeft={2}>{content}</box>;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
227
|
return (
|
|
201
|
-
<
|
|
202
|
-
{content}
|
|
203
|
-
</scrollbox>
|
|
228
|
+
<ScrollableList content={content} focused={focused} height={height} scrollable={scrollable} />
|
|
204
229
|
);
|
|
205
230
|
}
|
|
206
231
|
|
|
@@ -218,24 +243,12 @@ export function FixList({
|
|
|
218
243
|
scrollable?: boolean;
|
|
219
244
|
}) {
|
|
220
245
|
if (fixes.length === 0) {
|
|
221
|
-
return
|
|
222
|
-
<text fg={TUI_COLORS.text.dim} paddingLeft={2}>
|
|
223
|
-
None yet
|
|
224
|
-
</text>
|
|
225
|
-
);
|
|
246
|
+
return <EmptyListMessage />;
|
|
226
247
|
}
|
|
227
248
|
|
|
228
249
|
const content = fixes.map((fix, index) => (
|
|
229
250
|
<box key={`${index}-${fix.id}`} flexDirection="column">
|
|
230
|
-
<
|
|
231
|
-
<text>
|
|
232
|
-
<PriorityText priority={fix.priority} />
|
|
233
|
-
</text>
|
|
234
|
-
<text fg={TUI_COLORS.text.dim}> ▸ </text>
|
|
235
|
-
<text fg={TUI_COLORS.text.secondary} wrapMode="none">
|
|
236
|
-
{toSingleLine(fix.title)}
|
|
237
|
-
</text>
|
|
238
|
-
</box>
|
|
251
|
+
<PriorityTitleRow priority={fix.priority} title={fix.title} />
|
|
239
252
|
{showFiles && fix.file && (
|
|
240
253
|
<text fg={TUI_COLORS.text.dim} paddingLeft={5} wrapMode="none">
|
|
241
254
|
{toSingleLine(fix.file)}
|
|
@@ -244,14 +257,8 @@ export function FixList({
|
|
|
244
257
|
</box>
|
|
245
258
|
));
|
|
246
259
|
|
|
247
|
-
if (!scrollable) {
|
|
248
|
-
return <box paddingLeft={2}>{content}</box>;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
260
|
return (
|
|
252
|
-
<
|
|
253
|
-
{content}
|
|
254
|
-
</scrollbox>
|
|
261
|
+
<ScrollableList content={content} focused={focused} height={height} scrollable={scrollable} />
|
|
255
262
|
);
|
|
256
263
|
}
|
|
257
264
|
|
|
@@ -267,38 +274,20 @@ export function SkippedList({
|
|
|
267
274
|
scrollable?: boolean;
|
|
268
275
|
}) {
|
|
269
276
|
if (skipped.length === 0) {
|
|
270
|
-
return
|
|
271
|
-
<text fg={TUI_COLORS.text.dim} paddingLeft={2}>
|
|
272
|
-
None yet
|
|
273
|
-
</text>
|
|
274
|
-
);
|
|
277
|
+
return <EmptyListMessage />;
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
const content = skipped.map((entry, index) => (
|
|
278
281
|
<box key={`${index}-${entry.id}`} flexDirection="column">
|
|
279
|
-
<
|
|
280
|
-
<text>
|
|
281
|
-
<PriorityText priority={entry.priority} />
|
|
282
|
-
</text>
|
|
283
|
-
<text fg={TUI_COLORS.text.dim}> ▸ </text>
|
|
284
|
-
<text fg={TUI_COLORS.text.secondary} wrapMode="none">
|
|
285
|
-
{toSingleLine(entry.title)}
|
|
286
|
-
</text>
|
|
287
|
-
</box>
|
|
282
|
+
<PriorityTitleRow priority={entry.priority} title={entry.title} />
|
|
288
283
|
<text fg={TUI_COLORS.text.dim} paddingLeft={5} wrapMode="none">
|
|
289
284
|
{toSingleLine(entry.reason)}
|
|
290
285
|
</text>
|
|
291
286
|
</box>
|
|
292
287
|
));
|
|
293
288
|
|
|
294
|
-
if (!scrollable) {
|
|
295
|
-
return <box paddingLeft={2}>{content}</box>;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
289
|
return (
|
|
299
|
-
<
|
|
300
|
-
{content}
|
|
301
|
-
</scrollbox>
|
|
290
|
+
<ScrollableList content={content} focused={focused} height={height} scrollable={scrollable} />
|
|
302
291
|
);
|
|
303
292
|
}
|
|
304
293
|
|
|
@@ -327,11 +316,7 @@ export function FindingFixResultList({
|
|
|
327
316
|
scrollable?: boolean;
|
|
328
317
|
}) {
|
|
329
318
|
if (results.length === 0) {
|
|
330
|
-
return
|
|
331
|
-
<text fg={TUI_COLORS.text.dim} paddingLeft={2}>
|
|
332
|
-
None yet
|
|
333
|
-
</text>
|
|
334
|
-
);
|
|
319
|
+
return <EmptyListMessage />;
|
|
335
320
|
}
|
|
336
321
|
|
|
337
322
|
const content = results.map((result) => {
|
|
@@ -359,13 +344,7 @@ export function FindingFixResultList({
|
|
|
359
344
|
);
|
|
360
345
|
});
|
|
361
346
|
|
|
362
|
-
if (!scrollable) {
|
|
363
|
-
return <box paddingLeft={2}>{content}</box>;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
347
|
return (
|
|
367
|
-
<
|
|
368
|
-
{content}
|
|
369
|
-
</scrollbox>
|
|
348
|
+
<ScrollableList content={content} focused={focused} height={height} scrollable={scrollable} />
|
|
370
349
|
);
|
|
371
350
|
}
|
|
@@ -2,7 +2,10 @@ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
|
2
2
|
import { useCallback, useMemo, useState } from "react";
|
|
3
3
|
import type { LogSession } from "@/lib/logger";
|
|
4
4
|
import { formatProjectNameForDisplay } from "@/lib/tui/sessions/session-display";
|
|
5
|
+
import { CenteredModal } from "@/lib/tui/shared/CenteredModal";
|
|
5
6
|
import { TUI_COLORS } from "@/lib/tui/shared/colors";
|
|
7
|
+
import { KeyboardShortcutsModal } from "@/lib/tui/shared/KeyboardShortcutsModal";
|
|
8
|
+
import { ShortcutHint } from "@/lib/tui/shared/ShortcutHint";
|
|
6
9
|
import { SessionDetailPane } from "./SessionListDetailPane";
|
|
7
10
|
import {
|
|
8
11
|
buildSessionOverlayOptions,
|
|
@@ -27,48 +30,16 @@ function SessionHelpModal({ onClose }: { onClose: () => void }) {
|
|
|
27
30
|
}
|
|
28
31
|
});
|
|
29
32
|
|
|
30
|
-
return
|
|
31
|
-
<box
|
|
32
|
-
position="absolute"
|
|
33
|
-
left={0}
|
|
34
|
-
top={0}
|
|
35
|
-
width="100%"
|
|
36
|
-
height="100%"
|
|
37
|
-
justifyContent="center"
|
|
38
|
-
alignItems="center"
|
|
39
|
-
>
|
|
40
|
-
<box
|
|
41
|
-
border
|
|
42
|
-
borderStyle="double"
|
|
43
|
-
title="Keyboard Shortcuts"
|
|
44
|
-
titleAlignment="left"
|
|
45
|
-
padding={2}
|
|
46
|
-
width={44}
|
|
47
|
-
backgroundColor="#1a1a2e"
|
|
48
|
-
>
|
|
49
|
-
<box flexDirection="column" gap={1}>
|
|
50
|
-
<text>
|
|
51
|
-
<span fg={TUI_COLORS.accent.key}>[Tab ←/→]</span>
|
|
52
|
-
<span fg={TUI_COLORS.text.muted}> Switch pane focus</span>
|
|
53
|
-
</text>
|
|
54
|
-
<text>
|
|
55
|
-
<span fg={TUI_COLORS.accent.key}>[↑/↓ j/k]</span>
|
|
56
|
-
<span fg={TUI_COLORS.text.muted}> Navigate / Scroll</span>
|
|
57
|
-
</text>
|
|
58
|
-
<text>
|
|
59
|
-
<span fg={TUI_COLORS.accent.key}>[d]</span>
|
|
60
|
-
<span fg={TUI_COLORS.text.muted}> Delete selected log</span>
|
|
61
|
-
</text>
|
|
62
|
-
<text>
|
|
63
|
-
<span fg={TUI_COLORS.accent.key}>[h/?]</span>
|
|
64
|
-
<span fg={TUI_COLORS.text.muted}> Toggle help</span>
|
|
65
|
-
</text>
|
|
66
|
-
</box>
|
|
67
|
-
</box>
|
|
68
|
-
</box>
|
|
69
|
-
);
|
|
33
|
+
return <KeyboardShortcutsModal shortcuts={SESSION_OVERLAY_SHORTCUTS} />;
|
|
70
34
|
}
|
|
71
35
|
|
|
36
|
+
const SESSION_OVERLAY_SHORTCUTS = [
|
|
37
|
+
{ keys: "[Tab ←/→]", label: "Switch pane focus" },
|
|
38
|
+
{ keys: "[↑/↓ j/k]", label: "Navigate / Scroll" },
|
|
39
|
+
{ keys: "[d]", label: "Delete selected log" },
|
|
40
|
+
{ keys: "[h/?]", label: "Toggle help" },
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
72
43
|
interface SessionDeleteModalProps {
|
|
73
44
|
sessionName: string;
|
|
74
45
|
error: string | null;
|
|
@@ -77,26 +48,8 @@ interface SessionDeleteModalProps {
|
|
|
77
48
|
|
|
78
49
|
function SessionDeleteModal({ sessionName, error, isDeleting }: SessionDeleteModalProps) {
|
|
79
50
|
return (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
left={0}
|
|
83
|
-
top={0}
|
|
84
|
-
width="100%"
|
|
85
|
-
height="100%"
|
|
86
|
-
justifyContent="center"
|
|
87
|
-
alignItems="center"
|
|
88
|
-
>
|
|
89
|
-
<box
|
|
90
|
-
border
|
|
91
|
-
borderStyle="double"
|
|
92
|
-
title="Delete Session Log"
|
|
93
|
-
titleAlignment="left"
|
|
94
|
-
padding={2}
|
|
95
|
-
width={58}
|
|
96
|
-
backgroundColor="#1a1a2e"
|
|
97
|
-
flexDirection="column"
|
|
98
|
-
gap={1}
|
|
99
|
-
>
|
|
51
|
+
<CenteredModal title="Delete Session Log" width={58}>
|
|
52
|
+
<box flexDirection="column" gap={1}>
|
|
100
53
|
<text fg={TUI_COLORS.text.primary}>{sessionName}</text>
|
|
101
54
|
<text fg={TUI_COLORS.status.error}>This cannot be undone.</text>
|
|
102
55
|
<text>
|
|
@@ -109,7 +62,7 @@ function SessionDeleteModal({ sessionName, error, isDeleting }: SessionDeleteMod
|
|
|
109
62
|
{isDeleting && <text fg={TUI_COLORS.text.muted}>Deleting...</text>}
|
|
110
63
|
{error && <text fg={TUI_COLORS.status.error}>{error}</text>}
|
|
111
64
|
</box>
|
|
112
|
-
</
|
|
65
|
+
</CenteredModal>
|
|
113
66
|
);
|
|
114
67
|
}
|
|
115
68
|
|
|
@@ -339,20 +292,9 @@ export function SessionOverlay({ onClose }: SessionOverlayProps) {
|
|
|
339
292
|
paddingBottom={1}
|
|
340
293
|
>
|
|
341
294
|
<box flexDirection="row" gap={2}>
|
|
342
|
-
{isNarrow &&
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
<span fg={TUI_COLORS.text.muted}> Switch</span>
|
|
346
|
-
</text>
|
|
347
|
-
)}
|
|
348
|
-
<text>
|
|
349
|
-
<span fg={TUI_COLORS.accent.key}>[d]</span>
|
|
350
|
-
<span fg={TUI_COLORS.text.muted}> Delete</span>
|
|
351
|
-
</text>
|
|
352
|
-
<text>
|
|
353
|
-
<span fg={TUI_COLORS.accent.key}>[h]</span>
|
|
354
|
-
<span fg={TUI_COLORS.text.muted}> Help</span>
|
|
355
|
-
</text>
|
|
295
|
+
{isNarrow && <ShortcutHint keys="[Tab]" label="Switch" />}
|
|
296
|
+
<ShortcutHint keys="[d]" label="Delete" />
|
|
297
|
+
<ShortcutHint keys="[h]" label="Help" />
|
|
356
298
|
</box>
|
|
357
299
|
<text fg={TUI_COLORS.text.dim}>Focus: {focusedPane === "list" ? "List" : "Detail"}</text>
|
|
358
300
|
</box>
|
|
@@ -1,72 +1,6 @@
|
|
|
1
|
+
import { extractBalancedJsonObjectSlices } from "@/lib/structured-output";
|
|
1
2
|
import { isReviewSummary, type ReviewSummary } from "@/lib/types";
|
|
2
3
|
|
|
3
|
-
interface JsonObjectSlice {
|
|
4
|
-
start: number;
|
|
5
|
-
end: number;
|
|
6
|
-
value: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function extractBalancedJsonObjects(text: string): JsonObjectSlice[] {
|
|
10
|
-
const results: JsonObjectSlice[] = [];
|
|
11
|
-
let depth = 0;
|
|
12
|
-
let startIndex = -1;
|
|
13
|
-
let inString = false;
|
|
14
|
-
let isEscaped = false;
|
|
15
|
-
|
|
16
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
17
|
-
const char = text[index];
|
|
18
|
-
|
|
19
|
-
if (inString) {
|
|
20
|
-
if (isEscaped) {
|
|
21
|
-
isEscaped = false;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (char === "\\") {
|
|
26
|
-
isEscaped = true;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (char === '"') {
|
|
31
|
-
inString = false;
|
|
32
|
-
}
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (char === '"') {
|
|
37
|
-
inString = true;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (char === "{") {
|
|
42
|
-
if (depth === 0) {
|
|
43
|
-
startIndex = index;
|
|
44
|
-
}
|
|
45
|
-
depth += 1;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (char === "}") {
|
|
50
|
-
if (depth === 0) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
depth -= 1;
|
|
55
|
-
if (depth === 0 && startIndex >= 0) {
|
|
56
|
-
const endIndex = index + 1;
|
|
57
|
-
results.push({
|
|
58
|
-
start: startIndex,
|
|
59
|
-
end: endIndex,
|
|
60
|
-
value: text.slice(startIndex, endIndex),
|
|
61
|
-
});
|
|
62
|
-
startIndex = -1;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return results;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
4
|
export function extractLatestReviewSummary(
|
|
71
5
|
text: string,
|
|
72
6
|
minIndex: number = 0
|
|
@@ -75,7 +9,7 @@ export function extractLatestReviewSummary(
|
|
|
75
9
|
return null;
|
|
76
10
|
}
|
|
77
11
|
|
|
78
|
-
const objects =
|
|
12
|
+
const objects = extractBalancedJsonObjectSlices(text);
|
|
79
13
|
for (let index = objects.length - 1; index >= 0; index -= 1) {
|
|
80
14
|
const candidate = objects[index];
|
|
81
15
|
if (!candidate) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
interface CenteredModalProps {
|
|
4
|
+
title: string;
|
|
5
|
+
width: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
padding?: number;
|
|
8
|
+
backgroundColor?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CenteredModal({
|
|
13
|
+
title,
|
|
14
|
+
width,
|
|
15
|
+
height,
|
|
16
|
+
padding = 2,
|
|
17
|
+
backgroundColor = "#1a1a2e",
|
|
18
|
+
children,
|
|
19
|
+
}: CenteredModalProps) {
|
|
20
|
+
return (
|
|
21
|
+
<box
|
|
22
|
+
position="absolute"
|
|
23
|
+
left={0}
|
|
24
|
+
top={0}
|
|
25
|
+
width="100%"
|
|
26
|
+
height="100%"
|
|
27
|
+
justifyContent="center"
|
|
28
|
+
alignItems="center"
|
|
29
|
+
>
|
|
30
|
+
<box
|
|
31
|
+
border
|
|
32
|
+
borderStyle="double"
|
|
33
|
+
title={title}
|
|
34
|
+
titleAlignment="left"
|
|
35
|
+
padding={padding}
|
|
36
|
+
width={width}
|
|
37
|
+
height={height}
|
|
38
|
+
backgroundColor={backgroundColor}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</box>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CenteredModal } from "@/lib/tui/shared/CenteredModal";
|
|
2
|
+
import { ShortcutHintList } from "@/lib/tui/shared/ShortcutHint";
|
|
3
|
+
|
|
4
|
+
interface KeyboardShortcutsModalProps {
|
|
5
|
+
shortcuts: readonly { keys: string; label: string }[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function KeyboardShortcutsModal({ shortcuts }: KeyboardShortcutsModalProps) {
|
|
9
|
+
return (
|
|
10
|
+
<CenteredModal title="Keyboard Shortcuts" width={44}>
|
|
11
|
+
<ShortcutHintList shortcuts={shortcuts} />
|
|
12
|
+
</CenteredModal>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TUI_COLORS } from "@/lib/tui/shared/colors";
|
|
2
|
+
|
|
3
|
+
interface ShortcutHintProps {
|
|
4
|
+
keys: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface ShortcutHintListProps {
|
|
9
|
+
shortcuts: readonly ShortcutHintProps[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ShortcutHint({ keys, label }: ShortcutHintProps) {
|
|
13
|
+
return (
|
|
14
|
+
<text>
|
|
15
|
+
<span fg={TUI_COLORS.accent.key}>{keys}</span>
|
|
16
|
+
<span fg={TUI_COLORS.text.muted}> {label}</span>
|
|
17
|
+
</text>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ShortcutHintList({ shortcuts }: ShortcutHintListProps) {
|
|
22
|
+
return (
|
|
23
|
+
<box flexDirection="column" gap={1}>
|
|
24
|
+
{shortcuts.map((shortcut, index) => (
|
|
25
|
+
<ShortcutHint
|
|
26
|
+
key={`${shortcut.keys}-${index}`}
|
|
27
|
+
keys={shortcut.keys}
|
|
28
|
+
label={shortcut.label}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
</box>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -1,51 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
FindingFixResult,
|
|
3
|
-
FindingId,
|
|
4
|
-
StoredFinding,
|
|
5
|
-
} from "@/lib/review-workflow/findings/types";
|
|
6
|
-
import type { SessionState } from "@/lib/session-state";
|
|
7
|
-
import type { DashboardStartupMode } from "@/lib/tui/dashboard/use-dashboard-run-control";
|
|
1
|
+
import type { DetailPaneProps } from "@/lib/tui/sessions/detail/DetailPane";
|
|
8
2
|
import { DetailPane } from "@/lib/tui/sessions/detail/DetailPane";
|
|
9
3
|
import { SessionSidebar } from "@/lib/tui/sessions/sidebar/SessionSidebar";
|
|
10
4
|
import { OutputDrawer } from "@/lib/tui/shared/OutputDrawer";
|
|
11
|
-
import type {
|
|
12
|
-
AgentRole,
|
|
13
|
-
Finding,
|
|
14
|
-
FixEntry,
|
|
15
|
-
ProjectStats,
|
|
16
|
-
ReviewOptions,
|
|
17
|
-
SessionStats,
|
|
18
|
-
SkippedEntry,
|
|
19
|
-
} from "@/lib/types";
|
|
20
5
|
import { resolveWorkspaceFocusState } from "./workspace-focus";
|
|
21
6
|
import type { FocusedPane, SessionGroupData } from "./workspace-types";
|
|
22
7
|
|
|
23
|
-
interface WorkspaceProps {
|
|
8
|
+
interface WorkspaceProps extends DetailPaneProps {
|
|
24
9
|
sessionGroups: SessionGroupData[];
|
|
25
10
|
selectedSessionId: string | null;
|
|
26
|
-
session: SessionState | null;
|
|
27
|
-
fixes: FixEntry[];
|
|
28
|
-
skipped: SkippedEntry[];
|
|
29
|
-
findings: Finding[];
|
|
30
|
-
storedFindings: StoredFinding[];
|
|
31
|
-
selectedFindingIds: FindingId[];
|
|
32
|
-
fixResults: FindingFixResult[];
|
|
33
|
-
unresolvedSelectedFindings: StoredFinding[];
|
|
34
|
-
auditRegressionFindings: StoredFinding[];
|
|
35
|
-
latestReviewIteration: number | null;
|
|
36
|
-
codexReviewText: string | null;
|
|
37
|
-
tmuxOutput: string;
|
|
38
|
-
maxIterations: number;
|
|
39
|
-
isLoading: boolean;
|
|
40
|
-
lastSessionStats: SessionStats | null;
|
|
41
|
-
projectStats: ProjectStats | null;
|
|
42
|
-
isGitRepo: boolean;
|
|
43
|
-
currentAgent: AgentRole | null;
|
|
44
|
-
reviewOptions: ReviewOptions | undefined;
|
|
45
|
-
startupMode: DashboardStartupMode;
|
|
46
|
-
isStopping: boolean;
|
|
47
|
-
activeSessionCount: number;
|
|
48
|
-
canFixPendingSession: boolean;
|
|
49
11
|
outputVisible: boolean;
|
|
50
12
|
focusedPane: FocusedPane;
|
|
51
13
|
overlayBlocked?: boolean;
|
|
@@ -54,32 +16,10 @@ interface WorkspaceProps {
|
|
|
54
16
|
export function Workspace({
|
|
55
17
|
sessionGroups,
|
|
56
18
|
selectedSessionId,
|
|
57
|
-
session,
|
|
58
|
-
fixes,
|
|
59
|
-
skipped,
|
|
60
|
-
findings,
|
|
61
|
-
storedFindings,
|
|
62
|
-
selectedFindingIds,
|
|
63
|
-
fixResults,
|
|
64
|
-
unresolvedSelectedFindings,
|
|
65
|
-
auditRegressionFindings,
|
|
66
|
-
latestReviewIteration,
|
|
67
|
-
codexReviewText,
|
|
68
|
-
tmuxOutput,
|
|
69
|
-
maxIterations,
|
|
70
|
-
isLoading,
|
|
71
|
-
lastSessionStats,
|
|
72
|
-
projectStats,
|
|
73
|
-
isGitRepo,
|
|
74
|
-
currentAgent,
|
|
75
|
-
reviewOptions,
|
|
76
|
-
startupMode,
|
|
77
|
-
isStopping,
|
|
78
|
-
activeSessionCount,
|
|
79
|
-
canFixPendingSession,
|
|
80
19
|
outputVisible,
|
|
81
20
|
focusedPane,
|
|
82
21
|
overlayBlocked = false,
|
|
22
|
+
...detailPaneProps
|
|
83
23
|
}: WorkspaceProps) {
|
|
84
24
|
const { sidebarFocused, detailFocused, outputFocused } = resolveWorkspaceFocusState(
|
|
85
25
|
focusedPane,
|
|
@@ -94,36 +34,11 @@ export function Workspace({
|
|
|
94
34
|
selectedSessionId={selectedSessionId}
|
|
95
35
|
focused={sidebarFocused}
|
|
96
36
|
/>
|
|
97
|
-
<DetailPane
|
|
98
|
-
session={session}
|
|
99
|
-
fixes={fixes}
|
|
100
|
-
skipped={skipped}
|
|
101
|
-
findings={findings}
|
|
102
|
-
storedFindings={storedFindings}
|
|
103
|
-
selectedFindingIds={selectedFindingIds}
|
|
104
|
-
fixResults={fixResults}
|
|
105
|
-
unresolvedSelectedFindings={unresolvedSelectedFindings}
|
|
106
|
-
auditRegressionFindings={auditRegressionFindings}
|
|
107
|
-
latestReviewIteration={latestReviewIteration}
|
|
108
|
-
codexReviewText={codexReviewText}
|
|
109
|
-
tmuxOutput={tmuxOutput}
|
|
110
|
-
maxIterations={maxIterations}
|
|
111
|
-
isLoading={isLoading}
|
|
112
|
-
lastSessionStats={lastSessionStats}
|
|
113
|
-
projectStats={projectStats}
|
|
114
|
-
isGitRepo={isGitRepo}
|
|
115
|
-
currentAgent={currentAgent}
|
|
116
|
-
reviewOptions={reviewOptions}
|
|
117
|
-
startupMode={startupMode}
|
|
118
|
-
isStopping={isStopping}
|
|
119
|
-
activeSessionCount={activeSessionCount}
|
|
120
|
-
canFixPendingSession={canFixPendingSession}
|
|
121
|
-
focused={detailFocused}
|
|
122
|
-
/>
|
|
37
|
+
<DetailPane {...detailPaneProps} focused={detailFocused} />
|
|
123
38
|
</box>
|
|
124
39
|
<OutputDrawer
|
|
125
|
-
output={tmuxOutput}
|
|
126
|
-
sessionName={session?.sessionName ?? null}
|
|
40
|
+
output={detailPaneProps.tmuxOutput}
|
|
41
|
+
sessionName={detailPaneProps.session?.sessionName ?? null}
|
|
127
42
|
visible={outputVisible}
|
|
128
43
|
focused={outputFocused}
|
|
129
44
|
/>
|