ralph-review 0.2.2 → 0.2.4

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.
Files changed (56) hide show
  1. package/README.md +123 -16
  2. package/package.json +7 -5
  3. package/src/cli-core.ts +51 -88
  4. package/src/cli-rrr.ts +1 -4
  5. package/src/cli.ts +1 -2
  6. package/src/commands/apply.ts +35 -20
  7. package/src/commands/config-handlers.ts +68 -69
  8. package/src/commands/config-model.ts +147 -125
  9. package/src/commands/doctor.ts +2 -4
  10. package/src/commands/fix.ts +73 -51
  11. package/src/commands/handoff-selection.ts +6 -8
  12. package/src/commands/interactive-deps.ts +43 -0
  13. package/src/commands/list.ts +24 -7
  14. package/src/commands/log.ts +12 -12
  15. package/src/commands/run.ts +32 -33
  16. package/src/commands/status.ts +25 -4
  17. package/src/commands/stop.ts +99 -62
  18. package/src/commands/update.ts +2 -4
  19. package/src/lib/agents/claude.ts +4 -16
  20. package/src/lib/agents/core.ts +16 -0
  21. package/src/lib/agents/droid.ts +4 -15
  22. package/src/lib/agents/models.ts +9 -0
  23. package/src/lib/cli-parser.ts +19 -14
  24. package/src/lib/handoff.ts +16 -7
  25. package/src/lib/logging/session-log.ts +2 -1
  26. package/src/lib/prompts/defaults/review.md +1 -1
  27. package/src/lib/prompts/protocol.ts +2 -1
  28. package/src/lib/review-workflow/findings/artifact.ts +3 -1
  29. package/src/lib/review-workflow/findings/types.ts +1 -1
  30. package/src/lib/review-workflow/remediation/prompt.ts +7 -7
  31. package/src/lib/review-workflow/remediation/run-batch-fix-phase.ts +30 -20
  32. package/src/lib/review-workflow/remediation/run-fix-session.ts +70 -68
  33. package/src/lib/review-workflow/results/finalize-result.ts +20 -3
  34. package/src/lib/review-workflow/run-review-cycle.ts +1 -12
  35. package/src/lib/review-workflow/session-status.ts +13 -0
  36. package/src/lib/review-workflow/shared/framed-json.ts +2 -47
  37. package/src/lib/session/state.ts +50 -38
  38. package/src/lib/structured-output.ts +24 -9
  39. package/src/lib/tui/dashboard/HelpOverlay.tsx +13 -57
  40. package/src/lib/tui/dashboard/ReviewModeOverlay.tsx +2 -2
  41. package/src/lib/tui/dashboard/StatusBar.tsx +12 -50
  42. package/src/lib/tui/dashboard/StopSessionPickerOverlay.tsx +4 -22
  43. package/src/lib/tui/sessions/detail/DetailPane.tsx +6 -64
  44. package/src/lib/tui/sessions/detail/IdleStateView.tsx +10 -12
  45. package/src/lib/tui/sessions/detail/SessionDetailView.tsx +1 -1
  46. package/src/lib/tui/sessions/detail/session-detail-parts.tsx +66 -87
  47. package/src/lib/tui/sessions/history/SessionListOverlay.tsx +17 -75
  48. package/src/lib/tui/sessions/review-summary-parser.ts +2 -68
  49. package/src/lib/tui/shared/CenteredModal.tsx +44 -0
  50. package/src/lib/tui/shared/KeyboardShortcutsModal.tsx +14 -0
  51. package/src/lib/tui/shared/ShortcutHint.tsx +33 -0
  52. package/src/lib/tui/workspace/Workspace.tsx +6 -91
  53. package/src/lib/tui/workspace/use-workspace-state.ts +113 -61
  54. package/src/lib/types/fix.ts +15 -48
  55. package/src/lib/types/guards.ts +47 -0
  56. package/src/lib/types/review.ts +5 -39
@@ -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
- <box
81
- position="absolute"
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
- </box>
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
- <text>
344
- <span fg={TUI_COLORS.accent.key}>[Tab]</span>
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 = extractBalancedJsonObjects(text);
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
  />