pi-diff-review 0.1.12 → 0.1.14
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 +36 -4
- package/package.json +1 -1
- package/src/comment-manager.ts +73 -0
- package/src/diff-source.ts +36 -10
- package/src/explanation-controller.ts +167 -0
- package/src/index.ts +70 -0
- package/src/prompt.ts +1 -1
- package/src/render-utils.ts +11 -0
- package/src/review-component.ts +539 -708
- package/src/review-navigation.ts +94 -0
- package/src/split-diff.ts +61 -0
- package/src/types.ts +0 -3
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# pi-diff-review
|
|
4
4
|
|
|
5
|
-
Embedded code reviews
|
|
5
|
+
Embedded code reviews and AI summaries directly within [pi](https://pi.dev/).
|
|
6
6
|
|
|
7
|
-
<img width="1986" height="1556" alt="pi-diff-review-screenshot
|
|
7
|
+
<img width="1986" height="1556" alt="pi-diff-review-screenshot" src="https://github.com/user-attachments/assets/5ddd7226-28d4-4617-8c5c-35b4b6af68dc" />
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -27,14 +27,46 @@ git clone https://github.com/cmpadden/pi-diff-review
|
|
|
27
27
|
pi install ./pi-diff-review
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Start a review with `/diff`. By default, this reviews your current unstaged changes:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
/diff
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Review staged changes with `--cached`:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
/diff --cached
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Review a branch or commit range by passing any `git diff` arguments after `/diff`:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/diff main...HEAD
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`/diff <git-diff-args>` is passed through to `git diff`, so these examples are equivalent to running `git diff`, `git diff --cached`, and `git diff main...HEAD` locally before opening the review UI.
|
|
51
|
+
|
|
52
|
+
### Staged vs. unstaged changes
|
|
53
|
+
|
|
54
|
+
- `/diff` shows unstaged working-tree changes only.
|
|
55
|
+
- `/diff --cached` shows staged changes only.
|
|
56
|
+
- If you have both staged and unstaged edits, run both commands separately to review each set.
|
|
57
|
+
- To review everything relative to a base branch, use a range such as `/diff main...HEAD`.
|
|
58
|
+
|
|
30
59
|
## Features
|
|
31
60
|
|
|
32
61
|
- `/diff` reviews the current unstaged `git diff`
|
|
33
|
-
- `/diff
|
|
62
|
+
- `/diff --cached` reviews staged changes
|
|
63
|
+
- `/diff main...HEAD` reviews changes on the current branch relative to `main`
|
|
64
|
+
- `/diff <git-diff-args>` passes arguments through to `git diff`
|
|
34
65
|
- `j/k` or arrow keys to move
|
|
35
66
|
- `g/G` to jump to the top or bottom of the diff
|
|
36
67
|
- `ctrl-u` / `ctrl-d` to move up/down by half a page
|
|
37
|
-
- `t` toggles
|
|
68
|
+
- `t` toggles inline comments/explanations
|
|
69
|
+
- `v` toggles the diff between unified and side-by-side split rendering
|
|
38
70
|
- `?` toggles an AI-generated explanation for the current hunk
|
|
39
71
|
- `J/K` to extend a highlighted selection into a comment range
|
|
40
72
|
- `esc` clears the active selection, or exits review when no selection is active
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ReviewComment, ReviewLine, SelectionBounds } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const GLOBAL_COMMENT_KEY = "__global_diff_comment__";
|
|
4
|
+
|
|
5
|
+
export function getSelectionKey(
|
|
6
|
+
lines: ReviewLine[],
|
|
7
|
+
start: number,
|
|
8
|
+
end: number,
|
|
9
|
+
): string {
|
|
10
|
+
return `${lines[start]?.id ?? start}:${lines[end]?.id ?? end}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildGlobalComment(text: string): ReviewComment {
|
|
14
|
+
return {
|
|
15
|
+
id: GLOBAL_COMMENT_KEY,
|
|
16
|
+
filePath: "Overall diff",
|
|
17
|
+
text,
|
|
18
|
+
global: true,
|
|
19
|
+
startLineId: GLOBAL_COMMENT_KEY,
|
|
20
|
+
endLineId: GLOBAL_COMMENT_KEY,
|
|
21
|
+
lineText: "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildCommentFromSelection(
|
|
26
|
+
lines: ReviewLine[],
|
|
27
|
+
selection: SelectionBounds,
|
|
28
|
+
text: string,
|
|
29
|
+
): ReviewComment {
|
|
30
|
+
const startLine = lines[selection.start]!;
|
|
31
|
+
const endLine = lines[selection.end]!;
|
|
32
|
+
const excerpt = lines
|
|
33
|
+
.slice(selection.start, selection.end + 1)
|
|
34
|
+
.map((line) => line.text)
|
|
35
|
+
.join("\n");
|
|
36
|
+
return {
|
|
37
|
+
id: getSelectionKey(lines, selection.start, selection.end),
|
|
38
|
+
filePath: startLine.filePath ?? endLine.filePath ?? "(unknown file)",
|
|
39
|
+
text,
|
|
40
|
+
startLineId: startLine.id,
|
|
41
|
+
endLineId: endLine.id,
|
|
42
|
+
startOldLineNumber: startLine.oldLineNumber,
|
|
43
|
+
startNewLineNumber: startLine.newLineNumber,
|
|
44
|
+
endOldLineNumber: endLine.oldLineNumber,
|
|
45
|
+
endNewLineNumber: endLine.newLineNumber,
|
|
46
|
+
lineText: excerpt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildCommentLineKeys(
|
|
51
|
+
comments: Map<string, ReviewComment>,
|
|
52
|
+
lineIndexById: Map<string, number>,
|
|
53
|
+
): Map<number, string[]> {
|
|
54
|
+
const commentLineKeys = new Map<number, string[]>();
|
|
55
|
+
|
|
56
|
+
for (const [key, comment] of comments) {
|
|
57
|
+
const start = lineIndexById.get(comment.startLineId);
|
|
58
|
+
const end = lineIndexById.get(comment.endLineId);
|
|
59
|
+
if (start == null || end == null) continue;
|
|
60
|
+
const from = Math.min(start, end);
|
|
61
|
+
const to = Math.max(start, end);
|
|
62
|
+
for (let index = from; index <= to; index++) {
|
|
63
|
+
const keys = commentLineKeys.get(index);
|
|
64
|
+
if (keys) {
|
|
65
|
+
keys.push(key);
|
|
66
|
+
} else {
|
|
67
|
+
commentLineKeys.set(index, [key]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return commentLineKeys;
|
|
73
|
+
}
|
package/src/diff-source.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
2
|
import type { DiffSource } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
export function parseDiffSource(args: string): DiffSource {
|
|
@@ -64,14 +64,40 @@ function tokenizeDiffArgs(input: string): string[] {
|
|
|
64
64
|
return args;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export const DIFF_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
|
|
68
|
+
|
|
67
69
|
export function getDiff(cwd: string, source: DiffSource): string {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
70
|
+
const args = ["diff", "--no-color", "--unified=3", ...source.args];
|
|
71
|
+
const result = spawnSync("git", args, {
|
|
72
|
+
cwd,
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
maxBuffer: DIFF_MAX_BUFFER_BYTES,
|
|
75
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (result.error) {
|
|
79
|
+
throw new Error(formatSpawnError(result.error));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (result.status !== 0) {
|
|
83
|
+
const stderr = result.stderr.trim();
|
|
84
|
+
throw new Error(
|
|
85
|
+
stderr || `git ${args.join(" ")} exited with status ${result.status}.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result.stdout;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatSpawnError(error: Error & { code?: string }): string {
|
|
93
|
+
if (error.code === "ENOBUFS") {
|
|
94
|
+
return `Diff output exceeded the ${formatBytes(DIFF_MAX_BUFFER_BYTES)} safety limit. Try reviewing a smaller diff or narrowing with git diff pathspecs.`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return error.message;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatBytes(bytes: number): string {
|
|
101
|
+
const mib = bytes / 1024 / 1024;
|
|
102
|
+
return `${Number.isInteger(mib) ? mib : mib.toFixed(1)} MiB`;
|
|
77
103
|
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DiffExplainer,
|
|
3
|
+
ExplanationScope,
|
|
4
|
+
ExplanationState,
|
|
5
|
+
} from "./explain.ts";
|
|
6
|
+
import type { ReviewLine, ReviewTui } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
|
+
|
|
10
|
+
export function getCurrentHunkScope(
|
|
11
|
+
lines: ReviewLine[],
|
|
12
|
+
selected: number,
|
|
13
|
+
): ExplanationScope | undefined {
|
|
14
|
+
const selectedLine = lines[selected];
|
|
15
|
+
if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
|
|
16
|
+
|
|
17
|
+
let start = selected;
|
|
18
|
+
while (
|
|
19
|
+
start > 0 &&
|
|
20
|
+
lines[start - 1]?.filePath === selectedLine.filePath &&
|
|
21
|
+
lines[start - 1]?.hunkLabel === selectedLine.hunkLabel
|
|
22
|
+
) {
|
|
23
|
+
start--;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let end = selected;
|
|
27
|
+
while (
|
|
28
|
+
end + 1 < lines.length &&
|
|
29
|
+
lines[end + 1]?.filePath === selectedLine.filePath &&
|
|
30
|
+
lines[end + 1]?.hunkLabel === selectedLine.hunkLabel
|
|
31
|
+
) {
|
|
32
|
+
end++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const diffText = lines
|
|
36
|
+
.slice(start, end + 1)
|
|
37
|
+
.map((line) => line.text)
|
|
38
|
+
.join("\n");
|
|
39
|
+
return {
|
|
40
|
+
key: `hunk:${selectedLine.filePath}:${selectedLine.hunkLabel}:${start}:${end}`,
|
|
41
|
+
kind: "hunk",
|
|
42
|
+
title: `${selectedLine.filePath} ${selectedLine.hunkLabel}`,
|
|
43
|
+
filePath: selectedLine.filePath,
|
|
44
|
+
diffText,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class ExplanationController {
|
|
49
|
+
readonly explanations = new Map<string, ExplanationState>();
|
|
50
|
+
private abortController?: AbortController;
|
|
51
|
+
private requestId = 0;
|
|
52
|
+
private loadingFrame = 0;
|
|
53
|
+
private loadingTimer?: ReturnType<typeof setInterval>;
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly tui: ReviewTui,
|
|
57
|
+
private readonly explainer?: DiffExplainer,
|
|
58
|
+
cachedExplanations: Map<string, string> = new Map(),
|
|
59
|
+
private readonly onExplanationsChanged?: (
|
|
60
|
+
explanations: Map<string, string>,
|
|
61
|
+
) => void,
|
|
62
|
+
) {
|
|
63
|
+
for (const [key, text] of cachedExplanations) {
|
|
64
|
+
const trimmed = text.trim();
|
|
65
|
+
if (trimmed)
|
|
66
|
+
this.explanations.set(key, { status: "ready", text: trimmed });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get isAvailable(): boolean {
|
|
71
|
+
return this.explainer != null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getState(scope: ExplanationScope | undefined): ExplanationState | undefined {
|
|
75
|
+
return scope ? this.explanations.get(scope.key) : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getLoadingFrame(): string {
|
|
79
|
+
return LOADING_FRAMES[this.loadingFrame % LOADING_FRAMES.length] ?? "⠋";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ensure(scope: ExplanationScope | undefined): void {
|
|
83
|
+
if (!scope || !this.explainer) return;
|
|
84
|
+
if (this.explanations.has(scope.key)) return;
|
|
85
|
+
|
|
86
|
+
this.abortController?.abort();
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
this.abortController = controller;
|
|
89
|
+
const requestId = ++this.requestId;
|
|
90
|
+
let text = "";
|
|
91
|
+
|
|
92
|
+
this.explanations.set(scope.key, { status: "loading", text });
|
|
93
|
+
this.startLoadingTimer();
|
|
94
|
+
|
|
95
|
+
void this.explainer
|
|
96
|
+
.explain(scope, {
|
|
97
|
+
signal: controller.signal,
|
|
98
|
+
onDelta: (delta) => {
|
|
99
|
+
if (requestId !== this.requestId) return;
|
|
100
|
+
text += delta;
|
|
101
|
+
this.explanations.set(scope.key, { status: "loading", text });
|
|
102
|
+
this.tui.requestRender();
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
.then((finalText) => {
|
|
106
|
+
if (requestId !== this.requestId) return;
|
|
107
|
+
this.explanations.set(scope.key, {
|
|
108
|
+
status: "ready",
|
|
109
|
+
text: finalText.trim() || text.trim() || "No explanation returned.",
|
|
110
|
+
});
|
|
111
|
+
this.emitExplanationsChanged();
|
|
112
|
+
})
|
|
113
|
+
.catch((error) => {
|
|
114
|
+
if (requestId !== this.requestId) return;
|
|
115
|
+
if (controller.signal.aborted) return;
|
|
116
|
+
this.explanations.set(scope.key, {
|
|
117
|
+
status: "error",
|
|
118
|
+
message: error instanceof Error ? error.message : String(error),
|
|
119
|
+
});
|
|
120
|
+
})
|
|
121
|
+
.finally(() => {
|
|
122
|
+
if (requestId !== this.requestId) return;
|
|
123
|
+
this.stopLoadingTimerIfIdle();
|
|
124
|
+
this.tui.requestRender();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.tui.requestRender();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
dispose(): void {
|
|
131
|
+
this.abortController?.abort();
|
|
132
|
+
this.stopLoadingTimer();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private emitExplanationsChanged(): void {
|
|
136
|
+
if (!this.onExplanationsChanged) return;
|
|
137
|
+
|
|
138
|
+
const readyExplanations = new Map<string, string>();
|
|
139
|
+
for (const [key, explanation] of this.explanations) {
|
|
140
|
+
if (explanation.status === "ready") {
|
|
141
|
+
readyExplanations.set(key, explanation.text);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.onExplanationsChanged(readyExplanations);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private startLoadingTimer(): void {
|
|
148
|
+
if (this.loadingTimer) return;
|
|
149
|
+
this.loadingTimer = setInterval(() => {
|
|
150
|
+
this.loadingFrame++;
|
|
151
|
+
this.tui.requestRender();
|
|
152
|
+
}, 120);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private stopLoadingTimerIfIdle(): void {
|
|
156
|
+
const hasLoading = [...this.explanations.values()].some(
|
|
157
|
+
(explanation) => explanation.status === "loading",
|
|
158
|
+
);
|
|
159
|
+
if (!hasLoading) this.stopLoadingTimer();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private stopLoadingTimer(): void {
|
|
163
|
+
if (!this.loadingTimer) return;
|
|
164
|
+
clearInterval(this.loadingTimer);
|
|
165
|
+
this.loadingTimer = undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { ReviewComponent } from "./review-component.ts";
|
|
|
11
11
|
import type { DiffSource, ReviewComment, ReviewResult } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
const DIFF_REVIEW_CACHE_ENTRY = "pi-diff-review-cache";
|
|
14
|
+
const DIFF_REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
|
|
14
15
|
|
|
15
16
|
type DiffReviewCacheEntry = {
|
|
16
17
|
cacheKey: string;
|
|
@@ -18,6 +19,12 @@ type DiffReviewCacheEntry = {
|
|
|
18
19
|
updatedAt: number;
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
type DiffExplanationCacheEntry = {
|
|
23
|
+
cacheKey: string;
|
|
24
|
+
explanations: Record<string, string>;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
function getDiffCacheKey(
|
|
22
29
|
cwd: string,
|
|
23
30
|
source: DiffSource,
|
|
@@ -71,6 +78,58 @@ function persistCachedComments(
|
|
|
71
78
|
} satisfies DiffReviewCacheEntry);
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
function getCachedExplanations(
|
|
82
|
+
ctx: ExtensionCommandContext,
|
|
83
|
+
cacheKey: string,
|
|
84
|
+
): Map<string, string> {
|
|
85
|
+
let latest: DiffExplanationCacheEntry | undefined;
|
|
86
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
87
|
+
if (
|
|
88
|
+
entry.type !== "custom" ||
|
|
89
|
+
entry.customType !== DIFF_REVIEW_EXPLANATION_CACHE_ENTRY
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = entry.data as Partial<DiffExplanationCacheEntry> | undefined;
|
|
95
|
+
if (
|
|
96
|
+
data?.cacheKey !== cacheKey ||
|
|
97
|
+
!data.explanations ||
|
|
98
|
+
typeof data.explanations !== "object" ||
|
|
99
|
+
Array.isArray(data.explanations)
|
|
100
|
+
) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
105
|
+
latest = {
|
|
106
|
+
cacheKey: data.cacheKey,
|
|
107
|
+
explanations: Object.fromEntries(
|
|
108
|
+
Object.entries(data.explanations).filter(
|
|
109
|
+
(entry): entry is [string, string] =>
|
|
110
|
+
typeof entry[0] === "string" && typeof entry[1] === "string",
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
updatedAt: data.updatedAt ?? 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new Map(Object.entries(latest?.explanations ?? {}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function persistCachedExplanations(
|
|
122
|
+
pi: ExtensionAPI,
|
|
123
|
+
cacheKey: string,
|
|
124
|
+
explanations: Map<string, string>,
|
|
125
|
+
): void {
|
|
126
|
+
pi.appendEntry(DIFF_REVIEW_EXPLANATION_CACHE_ENTRY, {
|
|
127
|
+
cacheKey,
|
|
128
|
+
explanations: Object.fromEntries(explanations),
|
|
129
|
+
updatedAt: Date.now(),
|
|
130
|
+
} satisfies DiffExplanationCacheEntry);
|
|
131
|
+
}
|
|
132
|
+
|
|
74
133
|
export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
75
134
|
pi.registerCommand("diff", {
|
|
76
135
|
description: "Review a git diff in a custom TUI (/diff [git diff args])",
|
|
@@ -94,12 +153,19 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
94
153
|
const reviewLines = parseDiff(diffText);
|
|
95
154
|
const cacheKey = getDiffCacheKey(ctx.cwd, source, diffText);
|
|
96
155
|
const comments = getCachedComments(ctx, cacheKey);
|
|
156
|
+
const explanations = getCachedExplanations(ctx, cacheKey);
|
|
97
157
|
if (comments.size > 0) {
|
|
98
158
|
ctx.ui.notify(
|
|
99
159
|
`Restored ${comments.size} cached diff comment${comments.size === 1 ? "" : "s"}.`,
|
|
100
160
|
"info",
|
|
101
161
|
);
|
|
102
162
|
}
|
|
163
|
+
if (explanations.size > 0) {
|
|
164
|
+
ctx.ui.notify(
|
|
165
|
+
`Restored ${explanations.size} cached hunk explanation${explanations.size === 1 ? "" : "s"}.`,
|
|
166
|
+
"info",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
103
169
|
|
|
104
170
|
const result = await ctx.ui.custom<ReviewResult>(
|
|
105
171
|
(tui, theme, _keybindings, done) => {
|
|
@@ -114,6 +180,10 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
114
180
|
(updatedComments) => {
|
|
115
181
|
persistCachedComments(pi, cacheKey, updatedComments.values());
|
|
116
182
|
},
|
|
183
|
+
explanations,
|
|
184
|
+
(updatedExplanations) => {
|
|
185
|
+
persistCachedExplanations(pi, cacheKey, updatedExplanations);
|
|
186
|
+
},
|
|
117
187
|
);
|
|
118
188
|
},
|
|
119
189
|
);
|
package/src/prompt.ts
CHANGED
|
@@ -17,7 +17,7 @@ export function formatLocation(line: {
|
|
|
17
17
|
return file;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function formatCommentLocation(comment: ReviewComment): string {
|
|
20
|
+
export function formatCommentLocation(comment: ReviewComment): string {
|
|
21
21
|
if (comment.global) return "Overall diff";
|
|
22
22
|
|
|
23
23
|
const start = formatLocation({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export function padToWidth(text: string, width: number): string {
|
|
4
|
+
const visible = visibleWidth(text);
|
|
5
|
+
if (visible >= width) return truncateToWidth(text, width);
|
|
6
|
+
return text + " ".repeat(width - visible);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function lineNumberCell(value?: number): string {
|
|
10
|
+
return value == null ? " " : String(value).padStart(4, " ");
|
|
11
|
+
}
|