ralph-review 0.2.2 → 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/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/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/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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TUI_COLORS } from "@/lib/tui/shared/colors";
|
|
2
|
+
import { ShortcutHint } from "@/lib/tui/shared/ShortcutHint";
|
|
2
3
|
import type { FocusedPane } from "@/lib/tui/workspace/workspace-types";
|
|
3
4
|
|
|
4
5
|
interface StatusBarProps {
|
|
@@ -41,18 +42,9 @@ export function StatusBar({
|
|
|
41
42
|
paddingTop={1}
|
|
42
43
|
>
|
|
43
44
|
<box flexDirection="row" gap={2}>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</text>
|
|
48
|
-
<text>
|
|
49
|
-
<span fg={TUI_COLORS.accent.key}>[Enter]</span>
|
|
50
|
-
<span fg={TUI_COLORS.text.muted}> Stop</span>
|
|
51
|
-
</text>
|
|
52
|
-
<text>
|
|
53
|
-
<span fg={TUI_COLORS.accent.key}>[Esc]</span>
|
|
54
|
-
<span fg={TUI_COLORS.text.muted}> Cancel</span>
|
|
55
|
-
</text>
|
|
45
|
+
<ShortcutHint keys="[↑/↓]" label="Choose" />
|
|
46
|
+
<ShortcutHint keys="[Enter]" label="Stop" />
|
|
47
|
+
<ShortcutHint keys="[Esc]" label="Cancel" />
|
|
56
48
|
</box>
|
|
57
49
|
<text fg={TUI_COLORS.text.dim}>Focus: Session Picker</text>
|
|
58
50
|
</box>
|
|
@@ -69,44 +61,14 @@ export function StatusBar({
|
|
|
69
61
|
paddingBottom={1}
|
|
70
62
|
>
|
|
71
63
|
<box flexDirection="row" gap={2}>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</text>
|
|
81
|
-
)}
|
|
82
|
-
{hasSession && (
|
|
83
|
-
<text>
|
|
84
|
-
<span fg={TUI_COLORS.accent.key}>[s]</span>
|
|
85
|
-
<span fg={TUI_COLORS.text.muted}> Stop Review</span>
|
|
86
|
-
</text>
|
|
87
|
-
)}
|
|
88
|
-
<text>
|
|
89
|
-
<span fg={TUI_COLORS.accent.key}>[o]</span>
|
|
90
|
-
<span fg={TUI_COLORS.text.muted}> {outputVisible ? "Hide Output" : "Output"}</span>
|
|
91
|
-
</text>
|
|
92
|
-
<text>
|
|
93
|
-
<span fg={TUI_COLORS.accent.key}>[Tab ←/→]</span>
|
|
94
|
-
<span fg={TUI_COLORS.text.muted}> Switch</span>
|
|
95
|
-
</text>
|
|
96
|
-
<text>
|
|
97
|
-
<span fg={TUI_COLORS.accent.key}>[l]</span>
|
|
98
|
-
<span fg={TUI_COLORS.text.muted}> Logs</span>
|
|
99
|
-
</text>
|
|
100
|
-
{canFixPendingSession && (
|
|
101
|
-
<text>
|
|
102
|
-
<span fg={TUI_COLORS.accent.key}>[f]</span>
|
|
103
|
-
<span fg={TUI_COLORS.text.muted}> Fix</span>
|
|
104
|
-
</text>
|
|
105
|
-
)}
|
|
106
|
-
<text>
|
|
107
|
-
<span fg={TUI_COLORS.accent.key}>[h]</span>
|
|
108
|
-
<span fg={TUI_COLORS.text.muted}> Help</span>
|
|
109
|
-
</text>
|
|
64
|
+
<ShortcutHint keys="[Esc/q]" label="Quit" />
|
|
65
|
+
{!hasSession && <ShortcutHint keys="[r]" label="Run Review" />}
|
|
66
|
+
{hasSession && <ShortcutHint keys="[s]" label="Stop Review" />}
|
|
67
|
+
<ShortcutHint keys="[o]" label={outputVisible ? "Hide Output" : "Output"} />
|
|
68
|
+
<ShortcutHint keys="[Tab ←/→]" label="Switch" />
|
|
69
|
+
<ShortcutHint keys="[l]" label="Logs" />
|
|
70
|
+
{canFixPendingSession && <ShortcutHint keys="[f]" label="Fix" />}
|
|
71
|
+
<ShortcutHint keys="[h]" label="Help" />
|
|
110
72
|
</box>
|
|
111
73
|
<box flexDirection="column" alignItems="flex-end">
|
|
112
74
|
{liveRefreshError && (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActiveSession } from "@/lib/session-state";
|
|
2
|
+
import { CenteredModal } from "@/lib/tui/shared/CenteredModal";
|
|
2
3
|
import { TUI_COLORS } from "@/lib/tui/shared/colors";
|
|
3
4
|
|
|
4
5
|
interface StopSessionPickerOverlayProps {
|
|
@@ -38,27 +39,8 @@ export function StopSessionPickerOverlay({
|
|
|
38
39
|
onClose,
|
|
39
40
|
}: StopSessionPickerOverlayProps) {
|
|
40
41
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
left={0}
|
|
44
|
-
top={0}
|
|
45
|
-
width="100%"
|
|
46
|
-
height="100%"
|
|
47
|
-
justifyContent="center"
|
|
48
|
-
alignItems="center"
|
|
49
|
-
>
|
|
50
|
-
<box
|
|
51
|
-
border
|
|
52
|
-
borderStyle="double"
|
|
53
|
-
title="Stop Review Session"
|
|
54
|
-
titleAlignment="left"
|
|
55
|
-
padding={1}
|
|
56
|
-
width={80}
|
|
57
|
-
height={16}
|
|
58
|
-
backgroundColor="#1a1a2e"
|
|
59
|
-
flexDirection="column"
|
|
60
|
-
gap={1}
|
|
61
|
-
>
|
|
42
|
+
<CenteredModal title="Stop Review Session" width={80} height={16} padding={1}>
|
|
43
|
+
<box flexDirection="column" gap={1}>
|
|
62
44
|
<text fg={TUI_COLORS.text.muted}>Choose which active worktree session to stop.</text>
|
|
63
45
|
<select
|
|
64
46
|
focused
|
|
@@ -84,6 +66,6 @@ export function StopSessionPickerOverlay({
|
|
|
84
66
|
}}
|
|
85
67
|
/>
|
|
86
68
|
</box>
|
|
87
|
-
</
|
|
69
|
+
</CenteredModal>
|
|
88
70
|
);
|
|
89
71
|
}
|
|
@@ -1,75 +1,31 @@
|
|
|
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
1
|
import type { DashboardStartupMode } from "@/lib/tui/dashboard/use-dashboard-run-control";
|
|
8
2
|
import { TUI_COLORS } from "@/lib/tui/shared/colors";
|
|
9
|
-
import type {
|
|
10
|
-
AgentRole,
|
|
11
|
-
Finding,
|
|
12
|
-
FixEntry,
|
|
13
|
-
ProjectStats,
|
|
14
|
-
ReviewOptions,
|
|
15
|
-
SessionStats,
|
|
16
|
-
SkippedEntry,
|
|
17
|
-
} from "@/lib/types";
|
|
3
|
+
import type { ProjectStats, SessionStats } from "@/lib/types";
|
|
18
4
|
import { IdleStateView } from "./IdleStateView";
|
|
5
|
+
import type { SessionDetailViewProps } from "./SessionDetailView";
|
|
19
6
|
import { SessionDetailView } from "./SessionDetailView";
|
|
20
7
|
|
|
21
|
-
interface DetailPaneProps {
|
|
22
|
-
session:
|
|
23
|
-
fixes: FixEntry[];
|
|
24
|
-
skipped: SkippedEntry[];
|
|
25
|
-
findings: Finding[];
|
|
26
|
-
storedFindings: StoredFinding[];
|
|
27
|
-
selectedFindingIds: FindingId[];
|
|
28
|
-
fixResults: FindingFixResult[];
|
|
29
|
-
unresolvedSelectedFindings: StoredFinding[];
|
|
30
|
-
auditRegressionFindings: StoredFinding[];
|
|
31
|
-
latestReviewIteration: number | null;
|
|
32
|
-
codexReviewText: string | null;
|
|
33
|
-
tmuxOutput: string;
|
|
34
|
-
maxIterations: number;
|
|
8
|
+
export interface DetailPaneProps extends Omit<SessionDetailViewProps, "session"> {
|
|
9
|
+
session: SessionDetailViewProps["session"] | null;
|
|
35
10
|
isLoading: boolean;
|
|
36
11
|
lastSessionStats?: SessionStats | null;
|
|
37
12
|
projectStats: ProjectStats | null;
|
|
38
13
|
isGitRepo: boolean;
|
|
39
|
-
currentAgent: AgentRole | null;
|
|
40
|
-
reviewOptions: ReviewOptions | undefined;
|
|
41
14
|
startupMode: DashboardStartupMode;
|
|
42
|
-
isStopping: boolean;
|
|
43
|
-
activeSessionCount: number;
|
|
44
15
|
canFixPendingSession?: boolean;
|
|
45
|
-
focused?: boolean;
|
|
46
16
|
}
|
|
47
17
|
|
|
48
18
|
export function DetailPane({
|
|
49
19
|
session,
|
|
50
|
-
fixes,
|
|
51
|
-
skipped,
|
|
52
|
-
findings,
|
|
53
|
-
storedFindings,
|
|
54
|
-
selectedFindingIds,
|
|
55
|
-
fixResults,
|
|
56
|
-
unresolvedSelectedFindings,
|
|
57
|
-
auditRegressionFindings,
|
|
58
|
-
latestReviewIteration,
|
|
59
|
-
codexReviewText,
|
|
60
|
-
tmuxOutput,
|
|
61
|
-
maxIterations,
|
|
62
20
|
isLoading,
|
|
63
21
|
lastSessionStats = null,
|
|
64
22
|
projectStats,
|
|
65
23
|
isGitRepo,
|
|
66
|
-
currentAgent,
|
|
67
|
-
reviewOptions,
|
|
68
24
|
startupMode,
|
|
69
25
|
isStopping,
|
|
70
|
-
activeSessionCount,
|
|
71
26
|
canFixPendingSession = false,
|
|
72
27
|
focused = false,
|
|
28
|
+
...sessionDetailProps
|
|
73
29
|
}: DetailPaneProps) {
|
|
74
30
|
const borderColor = focused ? TUI_COLORS.ui.borderFocused : TUI_COLORS.ui.border;
|
|
75
31
|
|
|
@@ -104,23 +60,9 @@ export function DetailPane({
|
|
|
104
60
|
<scrollbox flexGrow={1} focused={focused}>
|
|
105
61
|
{session ? (
|
|
106
62
|
<SessionDetailView
|
|
63
|
+
{...sessionDetailProps}
|
|
107
64
|
session={session}
|
|
108
|
-
fixes={fixes}
|
|
109
|
-
skipped={skipped}
|
|
110
|
-
findings={findings}
|
|
111
|
-
storedFindings={storedFindings}
|
|
112
|
-
selectedFindingIds={selectedFindingIds}
|
|
113
|
-
fixResults={fixResults}
|
|
114
|
-
unresolvedSelectedFindings={unresolvedSelectedFindings}
|
|
115
|
-
auditRegressionFindings={auditRegressionFindings}
|
|
116
|
-
latestReviewIteration={latestReviewIteration}
|
|
117
|
-
codexReviewText={codexReviewText}
|
|
118
|
-
tmuxOutput={tmuxOutput}
|
|
119
|
-
maxIterations={maxIterations}
|
|
120
|
-
currentAgent={currentAgent}
|
|
121
|
-
reviewOptions={reviewOptions}
|
|
122
65
|
isStopping={isStopping}
|
|
123
|
-
activeSessionCount={activeSessionCount}
|
|
124
66
|
focused={focused}
|
|
125
67
|
/>
|
|
126
68
|
) : (
|
|
@@ -19,19 +19,17 @@ import { Spinner } from "@/lib/tui/shared/Spinner";
|
|
|
19
19
|
import type { ProjectStats, SessionStats } from "@/lib/types";
|
|
20
20
|
import { toSingleLine } from "./session-detail-parts";
|
|
21
21
|
|
|
22
|
+
const LAST_RUN_STATUS_DISPLAY: Partial<
|
|
23
|
+
Record<SessionStats["status"], { text: string; color: string }>
|
|
24
|
+
> = {
|
|
25
|
+
completed: { text: "completed", color: TUI_COLORS.status.success },
|
|
26
|
+
failed: { text: "failed", color: TUI_COLORS.status.error },
|
|
27
|
+
interrupted: { text: "interrupted", color: TUI_COLORS.status.warning },
|
|
28
|
+
running: { text: "running", color: TUI_COLORS.status.pending },
|
|
29
|
+
};
|
|
30
|
+
|
|
22
31
|
function getLastRunStatusDisplay(status: SessionStats["status"]): { text: string; color: string } {
|
|
23
|
-
|
|
24
|
-
case "completed":
|
|
25
|
-
return { text: "completed", color: TUI_COLORS.status.success };
|
|
26
|
-
case "failed":
|
|
27
|
-
return { text: "failed", color: TUI_COLORS.status.error };
|
|
28
|
-
case "interrupted":
|
|
29
|
-
return { text: "interrupted", color: TUI_COLORS.status.warning };
|
|
30
|
-
case "running":
|
|
31
|
-
return { text: "running", color: TUI_COLORS.status.pending };
|
|
32
|
-
default:
|
|
33
|
-
return { text: "unknown", color: TUI_COLORS.status.inactive };
|
|
34
|
-
}
|
|
32
|
+
return LAST_RUN_STATUS_DISPLAY[status] ?? { text: "unknown", color: TUI_COLORS.status.inactive };
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
function getLastRunHandoffDisplay(stats: SessionStats): {
|
|
@@ -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>
|