ralph-review 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-review",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Orchestrating coding agents for code review, verification and fixing via the ralph loop.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -291,18 +291,24 @@ const FIXER_AGENT_PRIORITY: readonly AgentType[] = ["codex", "claude", "droid",
291
291
 
292
292
  const MODEL_PRIORITY_MATCHERS: Record<ConfiguredRole, readonly ((model: string) => boolean)[]> = {
293
293
  reviewer: [
294
- (model) => matchesModelId(model, "gpt-5.4"),
295
- (model) => matchesModelId(model, "gpt-5.3-codex"),
296
294
  (model) => matchesModelId(model, "gpt-5.2"),
297
- (model) => matchesModelId(model, "gpt-5.2-codex"),
298
295
  (model) => matchesModelId(model, "claude-opus-4-6"),
299
- (model) => matchesModelId(model, "gemini-3-pro-preview"),
296
+ (model) => matchesModelId(model, "claude-sonnet-4-6"),
297
+ (model) => matchesModelId(model, "claude-opus-4-7"),
298
+ (model) => matchesModelId(model, "glm-5.1"),
299
+ (model) => matchesModelId(model, "gpt-5.3-codex"),
300
+ (model) => matchesModelId(model, "gemini-3.1-pro-preview"),
301
+ (model) => matchesModelId(model, "kimi-k2.6"),
300
302
  ],
301
303
  fixer: [
304
+ (model) => matchesModelId(model, "gpt-5.5"),
302
305
  (model) => matchesModelId(model, "gpt-5.4"),
303
- (model) => matchesModelId(model, "gpt-5.3-codex"),
304
306
  (model) => matchesModelId(model, "claude-opus-4-6"),
305
- (model) => matchesModelId(model, "gemini-3-pro-preview"),
307
+ (model) => matchesModelId(model, "gpt-5.3-codex"),
308
+ (model) => matchesModelId(model, "kimi-k2.6"),
309
+ (model) => matchesModelId(model, "gemini-3.1-pro-preview"),
310
+ (model) => matchesModelId(model, "glm-5.1"),
311
+ (model) => matchesModelId(model, "claude-sonnet-4-6"),
306
312
  ],
307
313
  };
308
314
 
@@ -13,8 +13,8 @@ export const claudeModelOptions = [
13
13
  { value: "claude-opus-4-7", label: "Claude Opus 4.7" },
14
14
  { value: "claude-opus-4-6", label: "Claude Opus 4.6" },
15
15
  { value: "claude-opus-4-5", label: "Claude Opus 4.5" },
16
- { value: "sonnet", label: "Claude Sonnet 4.6" },
17
- { value: "haiku", label: "Claude Haiku 4.5" },
16
+ { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
17
+ { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
18
18
  ] as const;
19
19
 
20
20
  export const geminiModelOptions = [
@@ -240,7 +240,6 @@ export function Dashboard({ projectPath, branch, refreshInterval = 1000 }: Dashb
240
240
  findings={state.findings}
241
241
  storedFindings={state.storedFindings}
242
242
  selectedFindingIds={state.selectedFindingIds}
243
- selectedFindings={state.selectedFindings}
244
243
  fixResults={state.fixResults}
245
244
  unresolvedSelectedFindings={state.unresolvedSelectedFindings}
246
245
  auditRegressionFindings={state.auditRegressionFindings}
@@ -13,11 +13,34 @@ type ReviewModeInputMode = Exclude<ReviewModeSelection, "uncommitted">;
13
13
  type ReviewModeStep = "picker" | "branch-picker" | "commit-picker" | "options";
14
14
  type ReviewExecutionMode = "review-only" | "auto-all" | "auto-priority";
15
15
  type OptionsFocusTarget =
16
- | "max-iterations"
16
+ | "iterations"
17
17
  | "force-max-iterations"
18
- | "execution-mode"
18
+ | "execution-review-only"
19
+ | "execution-auto-all"
20
+ | "execution-auto-priority"
19
21
  | "custom-instructions";
20
22
 
23
+ const OPTIONS_FOCUS_ORDER: OptionsFocusTarget[] = [
24
+ "iterations",
25
+ "force-max-iterations",
26
+ "execution-review-only",
27
+ "execution-auto-all",
28
+ "execution-auto-priority",
29
+ ];
30
+
31
+ function executionFocusToMode(focus: OptionsFocusTarget): ReviewExecutionMode | null {
32
+ if (focus === "execution-review-only") return "review-only";
33
+ if (focus === "execution-auto-all") return "auto-all";
34
+ if (focus === "execution-auto-priority") return "auto-priority";
35
+ return null;
36
+ }
37
+
38
+ function executionModeToFocus(mode: ReviewExecutionMode): OptionsFocusTarget {
39
+ if (mode === "review-only") return "execution-review-only";
40
+ if (mode === "auto-all") return "execution-auto-all";
41
+ return "execution-auto-priority";
42
+ }
43
+
21
44
  interface ReviewModeOption {
22
45
  label: string;
23
46
  description: string;
@@ -347,30 +370,6 @@ function clampMaxIterations(value: number): number {
347
370
  return Math.min(MAX_MAX_ITERATIONS, Math.max(MIN_MAX_ITERATIONS, Math.trunc(value)));
348
371
  }
349
372
 
350
- function cycleExecutionMode(current: ReviewExecutionMode, direction: 1 | -1): ReviewExecutionMode {
351
- const currentIndex = REVIEW_EXECUTION_OPTIONS.findIndex((option) => option.mode === current);
352
- const nextIndex = Math.min(
353
- REVIEW_EXECUTION_OPTIONS.length - 1,
354
- Math.max(0, currentIndex + direction)
355
- );
356
-
357
- return REVIEW_EXECUTION_OPTIONS[nextIndex]?.mode ?? current;
358
- }
359
-
360
- function getOptionsFocusOrder(showCustomInstructions: boolean): OptionsFocusTarget[] {
361
- const focusOrder: OptionsFocusTarget[] = [
362
- "max-iterations",
363
- "force-max-iterations",
364
- "execution-mode",
365
- ];
366
-
367
- if (showCustomInstructions) {
368
- focusOrder.push("custom-instructions");
369
- }
370
-
371
- return focusOrder;
372
- }
373
-
374
373
  export function ReviewModeOverlay({
375
374
  defaultReview,
376
375
  defaultMaxIterations,
@@ -396,7 +395,8 @@ export function ReviewModeOverlay({
396
395
  const [priorityCursorIndex, setPriorityCursorIndex] = useState(0);
397
396
  const [customInstructionsDraft, setCustomInstructionsDraft] = useState("");
398
397
  const [showCustomInstructions, setShowCustomInstructions] = useState(false);
399
- const [optionsFocus, setOptionsFocus] = useState<OptionsFocusTarget>("max-iterations");
398
+ const [optionsFocus, setOptionsFocus] = useState<OptionsFocusTarget>("iterations");
399
+ const lastNonCustomFocusRef = useRef<OptionsFocusTarget>("iterations");
400
400
  const customInstructionsRef = useRef<TextareaRenderable>(null);
401
401
  const maxIterationsInputRef = useRef<InputRenderable>(null);
402
402
 
@@ -490,34 +490,13 @@ export function ReviewModeOverlay({
490
490
  Math.min(68, configurationContentWidth - (showCustomInstructions ? 2 : 0))
491
491
  );
492
492
 
493
- if (step === "options" && !getOptionsFocusOrder(showCustomInstructions).includes(optionsFocus)) {
494
- setOptionsFocus("execution-mode");
495
- }
496
-
497
- function pruneEmptyCustomInstructions() {
498
- const nextValue = customInstructionsRef.current?.plainText ?? customInstructionsDraft;
499
- const normalizedValue = nextValue.trim().length === 0 ? "" : nextValue;
500
- if (normalizedValue !== customInstructionsDraft) {
501
- setCustomInstructionsDraft(normalizedValue);
502
- }
503
- if (normalizedValue.length === 0) {
504
- setShowCustomInstructions(false);
505
- }
506
- }
507
-
508
493
  function movePriorityCursor(direction: 1 | -1) {
509
494
  setPriorityCursorIndex((current) => clampPriorityCursorIndex(current + direction));
510
495
  setError(null);
511
496
  }
512
497
 
513
- function adjustMaxIterations(direction: 1 | -1) {
514
- const current = parseInt(maxIterationsDraft, 10);
515
- const base = Number.isFinite(current)
516
- ? current
517
- : direction > 0
518
- ? initialMaxIterations - 1
519
- : initialMaxIterations + 1;
520
- setMaxIterationsDraft(String(clampMaxIterations(base + direction)));
498
+ function advancePriorityCursor() {
499
+ setPriorityCursorIndex((current) => (current + 1) % PRIORITIES.length);
521
500
  setError(null);
522
501
  }
523
502
 
@@ -544,22 +523,6 @@ export function ReviewModeOverlay({
544
523
 
545
524
  useKeyboard((key) => {
546
525
  if (step === "options") {
547
- if (key.name === "tab") {
548
- const focusOrder = getOptionsFocusOrder(showCustomInstructions);
549
- const currentIndex = focusOrder.indexOf(optionsFocus);
550
- const direction = key.shift ? -1 : 1;
551
- const nextIndex = (currentIndex + direction + focusOrder.length) % focusOrder.length;
552
- const nextFocus = focusOrder[nextIndex];
553
- if (nextFocus) {
554
- if (optionsFocus === "custom-instructions" && nextFocus !== "custom-instructions") {
555
- pruneEmptyCustomInstructions();
556
- }
557
- setOptionsFocus(nextFocus);
558
- setError(null);
559
- }
560
- return;
561
- }
562
-
563
526
  if (isCustomInstructionsFocused) {
564
527
  if (key.name === "escape") {
565
528
  hideCustomInstructions();
@@ -579,52 +542,50 @@ export function ReviewModeOverlay({
579
542
  return;
580
543
  }
581
544
 
582
- if (optionsFocus === "max-iterations") {
583
- if (isUpNavigationKey(key.name)) {
584
- adjustMaxIterations(1);
585
- return;
586
- }
587
-
588
- if (isDownNavigationKey(key.name)) {
589
- adjustMaxIterations(-1);
590
- return;
545
+ if (isUpNavigationKey(key.name) || isDownNavigationKey(key.name)) {
546
+ const direction = isUpNavigationKey(key.name) ? -1 : 1;
547
+ const currentIndex = OPTIONS_FOCUS_ORDER.indexOf(optionsFocus);
548
+ const nextIndex = Math.min(
549
+ OPTIONS_FOCUS_ORDER.length - 1,
550
+ Math.max(0, currentIndex + direction)
551
+ );
552
+ const nextFocus = OPTIONS_FOCUS_ORDER[nextIndex];
553
+ if (nextFocus && nextFocus !== optionsFocus) {
554
+ setOptionsFocus(nextFocus);
555
+ const mode = executionFocusToMode(nextFocus);
556
+ if (mode) {
557
+ setExecutionMode(mode);
558
+ }
559
+ setError(null);
591
560
  }
561
+ return;
592
562
  }
593
563
 
594
- if (optionsFocus === "force-max-iterations") {
595
- if (key.name === "space") {
596
- toggleForceMaxIterations();
597
- return;
598
- }
564
+ if (optionsFocus === "force-max-iterations" && key.name === "space") {
565
+ toggleForceMaxIterations();
566
+ return;
599
567
  }
600
568
 
601
- if (optionsFocus === "execution-mode") {
602
- if (isUpNavigationKey(key.name)) {
603
- setExecutionMode((current) => cycleExecutionMode(current, -1));
604
- setError(null);
605
- return;
606
- }
607
-
608
- if (isDownNavigationKey(key.name)) {
609
- setExecutionMode((current) => cycleExecutionMode(current, 1));
610
- setError(null);
611
- return;
612
- }
613
-
614
- if (executionMode === "auto-priority" && isLeftNavigationKey(key.name)) {
569
+ if (optionsFocus === "execution-auto-priority") {
570
+ if (isLeftNavigationKey(key.name)) {
615
571
  movePriorityCursor(-1);
616
572
  return;
617
573
  }
618
574
 
619
- if (executionMode === "auto-priority" && isRightNavigationKey(key.name)) {
575
+ if (isRightNavigationKey(key.name)) {
620
576
  movePriorityCursor(1);
621
577
  return;
622
578
  }
623
579
 
624
- if (executionMode === "auto-priority" && key.name === "space") {
580
+ if (key.name === "space") {
625
581
  toggleSelectedPriority();
626
582
  return;
627
583
  }
584
+
585
+ if (key.name === "tab") {
586
+ advancePriorityCursor();
587
+ return;
588
+ }
628
589
  }
629
590
 
630
591
  if (key.name === "enter" || key.name === "return") {
@@ -712,7 +673,8 @@ export function ReviewModeOverlay({
712
673
  setSelectedPriorities([]);
713
674
  setPriorityCursorIndex(0);
714
675
  setShowCustomInstructions(false);
715
- setOptionsFocus("max-iterations");
676
+ setOptionsFocus("iterations");
677
+ lastNonCustomFocusRef.current = "iterations";
716
678
  setError(null);
717
679
  setStep("options");
718
680
  }
@@ -770,6 +732,9 @@ export function ReviewModeOverlay({
770
732
  }
771
733
 
772
734
  function openCustomInstructions() {
735
+ if (optionsFocus !== "custom-instructions") {
736
+ lastNonCustomFocusRef.current = optionsFocus;
737
+ }
773
738
  setShowCustomInstructions(true);
774
739
  setOptionsFocus("custom-instructions");
775
740
  setError(null);
@@ -778,7 +743,7 @@ export function ReviewModeOverlay({
778
743
  function hideCustomInstructions() {
779
744
  syncCustomInstructionsDraft();
780
745
  setShowCustomInstructions(false);
781
- setOptionsFocus("max-iterations");
746
+ setOptionsFocus(lastNonCustomFocusRef.current);
782
747
  setError(null);
783
748
  }
784
749
 
@@ -928,14 +893,13 @@ export function ReviewModeOverlay({
928
893
  <box flexDirection="column">
929
894
  {REVIEW_EXECUTION_OPTIONS.map((option) => {
930
895
  const isSelected = option.mode === executionMode;
931
- const isFocused = optionsFocus === "execution-mode" && isSelected;
932
- const showFocusMarker = isFocused && option.mode !== "auto-priority";
896
+ const isFocused = optionsFocus === executionModeToFocus(option.mode);
933
897
 
934
898
  return (
935
899
  <box key={option.mode} flexDirection="column" paddingX={1} paddingY={0}>
936
900
  <box flexDirection="row">
937
- <text fg={showFocusMarker ? TUI_COLORS.accent.key : TUI_COLORS.text.dim}>
938
- {showFocusMarker ? "▶ " : " "}
901
+ <text fg={isFocused ? TUI_COLORS.accent.key : TUI_COLORS.text.dim}>
902
+ {isFocused ? "▶ " : " "}
939
903
  </text>
940
904
  <text fg={isSelected ? TUI_COLORS.status.success : TUI_COLORS.text.dim}>
941
905
  {isSelected ? "◉" : "◎"}
@@ -989,30 +953,21 @@ export function ReviewModeOverlay({
989
953
  </text>
990
954
  <input
991
955
  ref={attachMaxIterationsInput}
992
- focused={optionsFocus === "max-iterations"}
956
+ focused={optionsFocus === "iterations"}
993
957
  value={maxIterationsDraft}
994
958
  placeholder={String(initialMaxIterations)}
995
959
  width={12}
996
960
  onChange={handleMaxIterationsInput}
997
961
  onInput={handleMaxIterationsInput}
998
- onKeyDown={(key) => {
999
- if (isUpNavigationKey(key.name)) {
1000
- key.preventDefault();
1001
- key.stopPropagation();
1002
- adjustMaxIterations(1);
1003
- return;
1004
- }
1005
-
1006
- if (isDownNavigationKey(key.name)) {
1007
- key.preventDefault();
1008
- key.stopPropagation();
1009
- adjustMaxIterations(-1);
1010
- }
1011
- }}
1012
962
  />
1013
- <box flexDirection="row" marginTop={1}>
963
+ </box>
964
+
965
+ <box marginTop={1} paddingX={1} paddingY={0} flexDirection="column" gap={0}>
966
+ <text fg={TUI_COLORS.text.dim}>
967
+ <strong>Force Max Iterations</strong>
968
+ </text>
969
+ <box flexDirection="row">
1014
970
  <text fg={isForceFocused ? TUI_COLORS.accent.key : TUI_COLORS.text.dim}>
1015
- {" "}
1016
971
  {isForceFocused ? "▶ " : " "}
1017
972
  </text>
1018
973
  <text fg={forceMaxIterations ? TUI_COLORS.status.success : TUI_COLORS.text.dim}>
@@ -1020,7 +975,7 @@ export function ReviewModeOverlay({
1020
975
  </text>
1021
976
  <text fg={forceMaxIterations ? TUI_COLORS.text.primary : TUI_COLORS.text.secondary}>
1022
977
  {" "}
1023
- Force max iterations
978
+ {forceMaxIterations ? "Enabled" : "Disabled"}
1024
979
  </text>
1025
980
  </box>
1026
981
  </box>
@@ -1032,15 +987,12 @@ export function ReviewModeOverlay({
1032
987
  {renderExecutionModeOptions()}
1033
988
  </box>
1034
989
 
1035
- {executionMode === "auto-priority" && (
990
+ {optionsFocus === "execution-auto-priority" && (
1036
991
  <box paddingX={1} paddingY={0} flexDirection="column" gap={0}>
1037
992
  <box flexDirection="row" paddingLeft={2}>
1038
993
  {PRIORITIES.map((priority, index) => {
1039
994
  const isSelected = selectedPriorities.includes(priority);
1040
- const isHighlighted =
1041
- optionsFocus === "execution-mode" &&
1042
- executionMode === "auto-priority" &&
1043
- priorityCursorIndex === index;
995
+ const isHighlighted = priorityCursorIndex === index;
1044
996
 
1045
997
  return (
1046
998
  <box key={priority} paddingLeft={1}>
@@ -1136,8 +1088,7 @@ export function ReviewModeOverlay({
1136
1088
  }
1137
1089
 
1138
1090
  function renderOptions() {
1139
- const isInlinePriorityControlActive =
1140
- optionsFocus === "execution-mode" && executionMode === "auto-priority";
1091
+ const isPriorityFocusActive = optionsFocus === "execution-auto-priority";
1141
1092
  const isForceControlActive = optionsFocus === "force-max-iterations";
1142
1093
  const reviewStartKeyLabel = isCustomInstructionsFocused ? "[Shift+Enter]" : "[Enter]";
1143
1094
 
@@ -1150,27 +1101,28 @@ export function ReviewModeOverlay({
1150
1101
  <text fg={optionsStatusColor}>
1151
1102
  {error ?? (
1152
1103
  <>
1153
- <span fg={TUI_COLORS.accent.key}>[Tab]</span>
1154
- <span fg={TUI_COLORS.text.muted}> moves focus </span>
1155
- {isInlinePriorityControlActive && (
1104
+ <span fg={TUI_COLORS.accent.key}>[↑/↓]</span>
1105
+ <span fg={TUI_COLORS.text.muted}> navigates </span>
1106
+ {isPriorityFocusActive && (
1156
1107
  <>
1108
+ <span fg={TUI_COLORS.accent.key}>[←/→]</span>
1109
+ <span fg={TUI_COLORS.text.muted}> priority cursor </span>
1110
+ <span fg={TUI_COLORS.accent.key}>[Tab]</span>
1111
+ <span fg={TUI_COLORS.text.muted}> next priority </span>
1157
1112
  <span fg={TUI_COLORS.accent.key}>[Space]</span>
1158
- <span fg={TUI_COLORS.text.muted}> to select </span>
1113
+ <span fg={TUI_COLORS.text.muted}> toggles priority </span>
1159
1114
  </>
1160
1115
  )}
1161
- {isForceControlActive ? (
1116
+ {isForceControlActive && (
1162
1117
  <>
1163
1118
  <span fg={TUI_COLORS.accent.key}>[Space]</span>
1164
1119
  <span fg={TUI_COLORS.text.muted}> toggles force </span>
1165
- <span fg={TUI_COLORS.accent.key}>{reviewStartKeyLabel}</span>
1166
- <span fg={TUI_COLORS.text.muted}> starts review</span>
1167
- </>
1168
- ) : (
1169
- <>
1170
- <span fg={TUI_COLORS.accent.key}>{reviewStartKeyLabel}</span>
1171
- <span fg={TUI_COLORS.text.muted}> starts review</span>
1172
1120
  </>
1173
1121
  )}
1122
+ <span fg={TUI_COLORS.accent.key}>[C]</span>
1123
+ <span fg={TUI_COLORS.text.muted}> custom instructions </span>
1124
+ <span fg={TUI_COLORS.accent.key}>{reviewStartKeyLabel}</span>
1125
+ <span fg={TUI_COLORS.text.muted}> starts review</span>
1174
1126
  </>
1175
1127
  )}
1176
1128
  </text>
@@ -25,7 +25,6 @@ interface DetailPaneProps {
25
25
  findings: Finding[];
26
26
  storedFindings: StoredFinding[];
27
27
  selectedFindingIds: FindingId[];
28
- selectedFindings: StoredFinding[];
29
28
  fixResults: FindingFixResult[];
30
29
  unresolvedSelectedFindings: StoredFinding[];
31
30
  auditRegressionFindings: StoredFinding[];
@@ -53,7 +52,6 @@ export function DetailPane({
53
52
  findings,
54
53
  storedFindings,
55
54
  selectedFindingIds,
56
- selectedFindings,
57
55
  fixResults,
58
56
  unresolvedSelectedFindings,
59
57
  auditRegressionFindings,
@@ -112,7 +110,6 @@ export function DetailPane({
112
110
  findings={findings}
113
111
  storedFindings={storedFindings}
114
112
  selectedFindingIds={selectedFindingIds}
115
- selectedFindings={selectedFindings}
116
113
  fixResults={fixResults}
117
114
  unresolvedSelectedFindings={unresolvedSelectedFindings}
118
115
  auditRegressionFindings={auditRegressionFindings}
@@ -30,6 +30,7 @@ import {
30
30
  FindingsList,
31
31
  FixList,
32
32
  SectionHeader,
33
+ SelectableStoredFindingsList,
33
34
  SkippedList,
34
35
  StoredFindingsList,
35
36
  toSingleLine,
@@ -42,7 +43,6 @@ interface SessionDetailViewProps {
42
43
  findings: Finding[];
43
44
  storedFindings: StoredFinding[];
44
45
  selectedFindingIds: FindingId[];
45
- selectedFindings: StoredFinding[];
46
46
  fixResults: FindingFixResult[];
47
47
  unresolvedSelectedFindings: StoredFinding[];
48
48
  auditRegressionFindings: StoredFinding[];
@@ -157,7 +157,6 @@ export function SessionDetailView({
157
157
  findings,
158
158
  storedFindings,
159
159
  selectedFindingIds,
160
- selectedFindings,
161
160
  fixResults,
162
161
  unresolvedSelectedFindings,
163
162
  auditRegressionFindings,
@@ -255,12 +254,6 @@ export function SessionDetailView({
255
254
  session.selectedFindingIds && session.selectedFindingIds.length > 0
256
255
  ? session.selectedFindingIds
257
256
  : selectedFindingIds;
258
- const workflowSelectedFindings =
259
- selectedFindings.length > 0
260
- ? selectedFindings
261
- : workflowSelectedIds
262
- .map((findingId) => workflowFindingsById.get(findingId))
263
- .filter((finding): finding is StoredFinding => finding !== undefined);
264
257
  const workflowUnresolvedFindings =
265
258
  unresolvedSelectedFindings.length > 0
266
259
  ? unresolvedSelectedFindings
@@ -334,10 +327,24 @@ export function SessionDetailView({
334
327
  {batchFirstMode ? (
335
328
  <>
336
329
  <box flexDirection="column" flexBasis={0} flexGrow={5} minHeight={0}>
337
- <SectionHeader title="Findings inventory" count={batchDisplayFindings.length} />
330
+ <SectionHeader
331
+ title="Findings"
332
+ count={batchDisplayFindings.length}
333
+ suffix={
334
+ workflowSelectedIds.length > 0 ? (
335
+ <span fg={TUI_COLORS.text.dim}> · {workflowSelectedIds.length} selected</span>
336
+ ) : undefined
337
+ }
338
+ />
338
339
  <box flexGrow={1} minHeight={0}>
339
340
  {inventoryFindings.length > 0 ? (
340
- <StoredFindingsList findings={inventoryFindings} height="100%" focused={focused} />
341
+ <SelectableStoredFindingsList
342
+ findings={inventoryFindings}
343
+ selectedFindingIds={workflowSelectedIds}
344
+ height="100%"
345
+ focused={focused}
346
+ selectedFirst
347
+ />
341
348
  ) : showingCodex ? (
342
349
  <CodexReviewDisplay text={displayCodexText ?? ""} height="100%" focused={focused} />
343
350
  ) : (
@@ -346,15 +353,6 @@ export function SessionDetailView({
346
353
  </box>
347
354
  </box>
348
355
 
349
- {workflowSelectedFindings.length > 0 && (
350
- <box flexDirection="column" flexBasis={0} flexGrow={2} minHeight={0}>
351
- <SectionHeader title="Selected findings" count={workflowSelectedFindings.length} />
352
- <box flexGrow={1} minHeight={0}>
353
- <StoredFindingsList findings={workflowSelectedFindings} height="100%" />
354
- </box>
355
- </box>
356
- )}
357
-
358
356
  {fixResults.length > 0 && (
359
357
  <box flexDirection="column" flexBasis={0} flexGrow={2} minHeight={0}>
360
358
  <SectionHeader title="Fix results" count={fixResults.length} />
@@ -1,4 +1,8 @@
1
- import type { FindingFixResult, StoredFinding } from "@/lib/review-workflow/findings/types";
1
+ import type {
2
+ FindingFixResult,
3
+ FindingId,
4
+ StoredFinding,
5
+ } from "@/lib/review-workflow/findings/types";
2
6
  import { storedFindingToFinding } from "@/lib/review-workflow/presentation";
3
7
  import { formatFindingTitleForDisplay } from "@/lib/tui/sessions/finding-title";
4
8
  import { PriorityText } from "@/lib/tui/sessions/priority-text";
@@ -40,12 +44,14 @@ export function FindingsList({
40
44
  height = 8,
41
45
  focused = false,
42
46
  scrollable = true,
47
+ showBody = false,
43
48
  showConfidence = false,
44
49
  }: {
45
50
  findings: Finding[];
46
51
  height?: BoxHeight;
47
52
  focused?: boolean;
48
53
  scrollable?: boolean;
54
+ showBody?: boolean;
49
55
  showConfidence?: boolean;
50
56
  }) {
51
57
  if (findings.length === 0) {
@@ -63,6 +69,7 @@ export function FindingsList({
63
69
 
64
70
  return (
65
71
  <box key={key} flexDirection="column">
72
+ {showBody && index > 0 && <text> </text>}
66
73
  <box flexDirection="row">
67
74
  <text>
68
75
  <PriorityText priority={finding.priority} />
@@ -72,6 +79,14 @@ export function FindingsList({
72
79
  {toSingleLine(formatFindingTitleForDisplay(finding.title))}
73
80
  </text>
74
81
  </box>
82
+ {showBody && (
83
+ <>
84
+ <text> </text>
85
+ <text fg={TUI_COLORS.text.secondary} paddingLeft={5} wrapMode="word">
86
+ {finding.body.trim()}
87
+ </text>
88
+ </>
89
+ )}
75
90
  {showConfidence && (
76
91
  <text fg={TUI_COLORS.text.dim} paddingLeft={5} wrapMode="none">
77
92
  Confidence: {formatConfidenceScore(finding.confidence_score)}
@@ -100,12 +115,14 @@ export function StoredFindingsList({
100
115
  height = 8,
101
116
  focused = false,
102
117
  scrollable = true,
118
+ showBody = false,
103
119
  showConfidence = false,
104
120
  }: {
105
121
  findings: StoredFinding[];
106
122
  height?: BoxHeight;
107
123
  focused?: boolean;
108
124
  scrollable?: boolean;
125
+ showBody?: boolean;
109
126
  showConfidence?: boolean;
110
127
  }) {
111
128
  return (
@@ -114,11 +131,79 @@ export function StoredFindingsList({
114
131
  height={height}
115
132
  focused={focused}
116
133
  scrollable={scrollable}
134
+ showBody={showBody}
117
135
  showConfidence={showConfidence}
118
136
  />
119
137
  );
120
138
  }
121
139
 
140
+ export function SelectableStoredFindingsList({
141
+ findings,
142
+ selectedFindingIds,
143
+ height = 8,
144
+ focused = false,
145
+ scrollable = true,
146
+ selectedFirst = false,
147
+ }: {
148
+ findings: StoredFinding[];
149
+ selectedFindingIds: FindingId[];
150
+ height?: BoxHeight;
151
+ focused?: boolean;
152
+ scrollable?: boolean;
153
+ selectedFirst?: boolean;
154
+ }) {
155
+ if (findings.length === 0) {
156
+ return (
157
+ <text fg={TUI_COLORS.text.dim} paddingLeft={2}>
158
+ None yet
159
+ </text>
160
+ );
161
+ }
162
+
163
+ const selectedIdSet = new Set(selectedFindingIds);
164
+ const displayFindings = selectedFirst
165
+ ? [
166
+ ...findings.filter((finding) => selectedIdSet.has(finding.id)),
167
+ ...findings.filter((finding) => !selectedIdSet.has(finding.id)),
168
+ ]
169
+ : findings;
170
+
171
+ const content = displayFindings.map((finding) => {
172
+ const isSelected = selectedIdSet.has(finding.id);
173
+ const lineRange = `${finding.startLine}-${finding.endLine}`;
174
+
175
+ return (
176
+ <box key={finding.id} flexDirection="column">
177
+ <box flexDirection="row" gap={1}>
178
+ <text fg={isSelected ? TUI_COLORS.status.success : TUI_COLORS.text.dim}>
179
+ {isSelected ? "◉" : "◎"}
180
+ </text>
181
+ <text>
182
+ <PriorityText priority={finding.priority} />
183
+ </text>
184
+ <text fg={TUI_COLORS.text.dim}>▸</text>
185
+ <text fg={TUI_COLORS.text.secondary} wrapMode="none">
186
+ {toSingleLine(formatFindingTitleForDisplay(finding.title))}
187
+ </text>
188
+ </box>
189
+ <text fg={TUI_COLORS.text.dim} paddingLeft={7} wrapMode="none">
190
+ {toSingleLine(finding.filePath)}:{lineRange}
191
+ </text>
192
+ </box>
193
+ );
194
+ });
195
+
196
+ if (!scrollable) {
197
+ return <box paddingLeft={2}>{content}</box>;
198
+ }
199
+
200
+ return (
201
+ <scrollbox paddingLeft={2} height={height} focused={focused}>
202
+ {content}
203
+ </scrollbox>
204
+ );
205
+ }
206
+
122
207
  export function FixList({
123
208
  fixes,
124
209
  showFiles,
@@ -453,7 +453,12 @@ export function SessionDetailPane({
453
453
  <text fg={TUI_COLORS.text.secondary} paddingLeft={2}>
454
454
  {entry.findings.length} issues found
455
455
  </text>
456
- <StoredFindingsList findings={entry.findings} scrollable={false} showConfidence />
456
+ <StoredFindingsList
457
+ findings={entry.findings}
458
+ scrollable={false}
459
+ showBody
460
+ showConfidence
461
+ />
457
462
  </WorkflowSection>
458
463
  ))}
459
464
 
@@ -29,7 +29,6 @@ interface WorkspaceProps {
29
29
  findings: Finding[];
30
30
  storedFindings: StoredFinding[];
31
31
  selectedFindingIds: FindingId[];
32
- selectedFindings: StoredFinding[];
33
32
  fixResults: FindingFixResult[];
34
33
  unresolvedSelectedFindings: StoredFinding[];
35
34
  auditRegressionFindings: StoredFinding[];
@@ -61,7 +60,6 @@ export function Workspace({
61
60
  findings,
62
61
  storedFindings,
63
62
  selectedFindingIds,
64
- selectedFindings,
65
63
  fixResults,
66
64
  unresolvedSelectedFindings,
67
65
  auditRegressionFindings,
@@ -103,7 +101,6 @@ export function Workspace({
103
101
  findings={findings}
104
102
  storedFindings={storedFindings}
105
103
  selectedFindingIds={selectedFindingIds}
106
- selectedFindings={selectedFindings}
107
104
  fixResults={fixResults}
108
105
  unresolvedSelectedFindings={unresolvedSelectedFindings}
109
106
  auditRegressionFindings={auditRegressionFindings}