@mrclrchtr/supi-review 0.1.0
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 +78 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +43 -0
- package/src/format-content.ts +71 -0
- package/src/git.ts +197 -0
- package/src/index.ts +1 -0
- package/src/progress-widget.ts +82 -0
- package/src/prompts.ts +116 -0
- package/src/renderer.ts +181 -0
- package/src/review.ts +351 -0
- package/src/runner-types.ts +32 -0
- package/src/runner.ts +424 -0
- package/src/settings.ts +246 -0
- package/src/target-resolution.ts +102 -0
- package/src/types.ts +49 -0
- package/src/ui.ts +116 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getCommitFileNames,
|
|
3
|
+
getCommitShow,
|
|
4
|
+
getDiff,
|
|
5
|
+
getDiffFileNames,
|
|
6
|
+
getMergeBase,
|
|
7
|
+
getUncommittedDiff,
|
|
8
|
+
getUncommittedFileNames,
|
|
9
|
+
} from "./git.ts";
|
|
10
|
+
import type { ReviewResult, ReviewTarget } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
interface TargetResolutionContext {
|
|
13
|
+
cwd: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type TargetResolutionSuccess = {
|
|
17
|
+
kind: "success";
|
|
18
|
+
target: ReviewTarget;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function resolveGitTarget(
|
|
22
|
+
target: ReviewTarget,
|
|
23
|
+
ctx: TargetResolutionContext,
|
|
24
|
+
): Promise<TargetResolutionSuccess | ReviewResult> {
|
|
25
|
+
switch (target.type) {
|
|
26
|
+
case "base-branch":
|
|
27
|
+
return resolveBaseBranchTarget(target, ctx);
|
|
28
|
+
case "uncommitted":
|
|
29
|
+
return resolveUncommittedTarget(target, ctx);
|
|
30
|
+
case "commit":
|
|
31
|
+
return resolveCommitTarget(target, ctx);
|
|
32
|
+
case "custom":
|
|
33
|
+
return { kind: "success", target };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function resolveBaseBranchTarget(
|
|
38
|
+
target: Extract<ReviewTarget, { type: "base-branch" }>,
|
|
39
|
+
ctx: TargetResolutionContext,
|
|
40
|
+
): Promise<TargetResolutionSuccess | ReviewResult> {
|
|
41
|
+
if (target.diff) {
|
|
42
|
+
return { kind: "success", target };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const baseSha = await getMergeBase(ctx.cwd, target.branch);
|
|
46
|
+
if (!baseSha) {
|
|
47
|
+
return { kind: "failed", reason: `No merge base found for ${target.branch}`, target };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [diff, changedFiles] = await Promise.all([
|
|
51
|
+
getDiff(ctx.cwd, baseSha),
|
|
52
|
+
resolveChangedFiles(target.changedFiles, () => getDiffFileNames(ctx.cwd, baseSha)),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
return { kind: "success", target: { ...target, diff, changedFiles } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveUncommittedTarget(
|
|
59
|
+
target: Extract<ReviewTarget, { type: "uncommitted" }>,
|
|
60
|
+
ctx: TargetResolutionContext,
|
|
61
|
+
): Promise<TargetResolutionSuccess | ReviewResult> {
|
|
62
|
+
if (target.diff) {
|
|
63
|
+
return { kind: "success", target };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [diff, changedFiles] = await Promise.all([
|
|
67
|
+
getUncommittedDiff(ctx.cwd),
|
|
68
|
+
resolveChangedFiles(target.changedFiles, () => getUncommittedFileNames(ctx.cwd)),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (!diff) {
|
|
72
|
+
return { kind: "failed", reason: "No uncommitted changes", target };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { kind: "success", target: { ...target, diff, changedFiles } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolveCommitTarget(
|
|
79
|
+
target: Extract<ReviewTarget, { type: "commit" }>,
|
|
80
|
+
ctx: TargetResolutionContext,
|
|
81
|
+
): Promise<TargetResolutionSuccess | ReviewResult> {
|
|
82
|
+
if (target.show) {
|
|
83
|
+
return { kind: "success", target };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const [show, changedFiles] = await Promise.all([
|
|
87
|
+
getCommitShow(ctx.cwd, target.sha),
|
|
88
|
+
resolveChangedFiles(target.changedFiles, () => getCommitFileNames(ctx.cwd, target.sha)),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
kind: "success",
|
|
93
|
+
target: { ...target, show, changedFiles },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveChangedFiles(
|
|
98
|
+
changedFiles: string[] | undefined,
|
|
99
|
+
load: () => Promise<string[]>,
|
|
100
|
+
): Promise<string[]> {
|
|
101
|
+
return changedFiles ? Promise.resolve(changedFiles) : load();
|
|
102
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// TypeScript interfaces for structured review output.
|
|
2
|
+
|
|
3
|
+
export interface ReviewLineRange {
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ReviewCodeLocation {
|
|
9
|
+
absolute_file_path: string;
|
|
10
|
+
line_range: ReviewLineRange;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReviewFinding {
|
|
14
|
+
title: string;
|
|
15
|
+
body: string;
|
|
16
|
+
confidence_score: number;
|
|
17
|
+
priority: 0 | 1 | 2 | 3;
|
|
18
|
+
code_location: ReviewCodeLocation;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ReviewOutputEvent {
|
|
22
|
+
findings: ReviewFinding[];
|
|
23
|
+
overall_correctness: string;
|
|
24
|
+
overall_explanation: string;
|
|
25
|
+
overall_confidence_score: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ReviewTarget =
|
|
29
|
+
| { type: "base-branch"; branch: string; diff: string; changedFiles?: string[] }
|
|
30
|
+
| { type: "uncommitted"; diff: string; changedFiles?: string[] }
|
|
31
|
+
| { type: "commit"; sha: string; show: string; changedFiles?: string[] }
|
|
32
|
+
| { type: "custom"; instructions: string; changedFiles?: string[] };
|
|
33
|
+
|
|
34
|
+
export interface ReviewSettings {
|
|
35
|
+
reviewModel: string;
|
|
36
|
+
maxDiffBytes: number;
|
|
37
|
+
autoFix: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ReviewResult =
|
|
41
|
+
| { kind: "success"; output: ReviewOutputEvent; target: ReviewTarget }
|
|
42
|
+
| { kind: "failed"; reason: string; target: ReviewTarget }
|
|
43
|
+
| { kind: "canceled"; target: ReviewTarget }
|
|
44
|
+
| {
|
|
45
|
+
kind: "timeout";
|
|
46
|
+
target: ReviewTarget;
|
|
47
|
+
timeoutMs: number;
|
|
48
|
+
partialOutput?: string;
|
|
49
|
+
};
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { DynamicBorder, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { getLocalBranches, getRecentCommits } from "./git.ts";
|
|
4
|
+
|
|
5
|
+
export type Preset = "base-branch" | "uncommitted" | "commit" | "custom";
|
|
6
|
+
|
|
7
|
+
interface SelectFromListOptions<T> {
|
|
8
|
+
items: SelectItem[];
|
|
9
|
+
title: string;
|
|
10
|
+
maxHeight: number;
|
|
11
|
+
onSelect: (item: SelectItem) => T | undefined;
|
|
12
|
+
initialIndex?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shared TUI helper: renders a SelectList inside a bordered container with
|
|
17
|
+
* standard theme colors, keyboard hints, and cancel-on-escape behavior.
|
|
18
|
+
*/
|
|
19
|
+
function selectFromList<T>(
|
|
20
|
+
ctx: ExtensionContext,
|
|
21
|
+
options: SelectFromListOptions<T>,
|
|
22
|
+
): Promise<T | undefined> {
|
|
23
|
+
const { items, title, maxHeight, onSelect, initialIndex } = options;
|
|
24
|
+
return ctx.ui.custom<T | undefined>((tui, theme, _kb, done) => {
|
|
25
|
+
const container = new Container();
|
|
26
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
27
|
+
container.addChild(new Text(theme.fg("accent", title), 1, 0));
|
|
28
|
+
|
|
29
|
+
const selectList = new SelectList(items, Math.min(items.length, maxHeight), {
|
|
30
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
31
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
32
|
+
description: (t) => theme.fg("muted", t),
|
|
33
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
34
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
35
|
+
});
|
|
36
|
+
if (initialIndex !== undefined) {
|
|
37
|
+
selectList.setSelectedIndex(initialIndex);
|
|
38
|
+
}
|
|
39
|
+
selectList.onSelect = (item) => done(onSelect(item));
|
|
40
|
+
selectList.onCancel = () => done(undefined);
|
|
41
|
+
container.addChild(selectList);
|
|
42
|
+
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
|
43
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
render: (w) => container.render(w),
|
|
47
|
+
invalidate: () => container.invalidate(),
|
|
48
|
+
handleInput: (data) => {
|
|
49
|
+
selectList.handleInput(data);
|
|
50
|
+
tui.requestRender();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function selectPreset(ctx: ExtensionContext): Promise<Preset | undefined> {
|
|
57
|
+
return selectFromList(ctx, {
|
|
58
|
+
items: [
|
|
59
|
+
{ value: "base-branch", label: "Review against a base branch" },
|
|
60
|
+
{ value: "uncommitted", label: "Review uncommitted changes" },
|
|
61
|
+
{ value: "commit", label: "Review a commit" },
|
|
62
|
+
{ value: "custom", label: "Custom review instructions" },
|
|
63
|
+
],
|
|
64
|
+
title: "Select review target",
|
|
65
|
+
maxHeight: 10,
|
|
66
|
+
onSelect: (item) => item.value as Preset,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function selectAutoFix(
|
|
71
|
+
ctx: ExtensionContext,
|
|
72
|
+
defaultValue: boolean,
|
|
73
|
+
): Promise<boolean | undefined> {
|
|
74
|
+
return selectFromList(ctx, {
|
|
75
|
+
items: [
|
|
76
|
+
{ value: "true", label: "Yes — fix all findings" },
|
|
77
|
+
{ value: "false", label: "No — review only" },
|
|
78
|
+
],
|
|
79
|
+
title: "Auto-fix after review?",
|
|
80
|
+
maxHeight: 2,
|
|
81
|
+
onSelect: (item) => item.value === "true",
|
|
82
|
+
initialIndex: defaultValue ? 0 : 1,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function selectBranch(ctx: ExtensionContext): Promise<string | undefined> {
|
|
87
|
+
const branches = await getLocalBranches(ctx.cwd);
|
|
88
|
+
if (branches.length === 0) {
|
|
89
|
+
ctx.ui.notify("No local branches found", "warning");
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return selectFromList(ctx, {
|
|
93
|
+
items: branches.map((b) => ({ value: b, label: b })),
|
|
94
|
+
title: "Select base branch",
|
|
95
|
+
maxHeight: 15,
|
|
96
|
+
onSelect: (item) => item.value,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function selectCommit(ctx: ExtensionContext): Promise<string | undefined> {
|
|
101
|
+
const commits = await getRecentCommits(ctx.cwd, 30);
|
|
102
|
+
if (commits.length === 0) {
|
|
103
|
+
ctx.ui.notify("No recent commits found", "warning");
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return selectFromList(ctx, {
|
|
107
|
+
items: commits.map((c) => ({
|
|
108
|
+
value: c.sha,
|
|
109
|
+
label: `${c.sha.slice(0, 7)} ${c.subject}`,
|
|
110
|
+
description: c.sha,
|
|
111
|
+
})),
|
|
112
|
+
title: "Select commit to review",
|
|
113
|
+
maxHeight: 15,
|
|
114
|
+
onSelect: (item) => item.value,
|
|
115
|
+
});
|
|
116
|
+
}
|