pi-diff-review 0.1.9 → 0.1.11
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 +1 -0
- package/package.json +1 -1
- package/src/index.ts +76 -1
- package/src/review-component.ts +64 -37
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ pi install https://github.com/cmpadden/pi-diff-review
|
|
|
36
36
|
- `C` to add or edit an overall diff comment
|
|
37
37
|
- `x` to delete a comment for the current line or selected range
|
|
38
38
|
- `Enter` to submit comments back to pi
|
|
39
|
+
- Comments are cached per session and restored when reopening the same diff
|
|
39
40
|
- `q` to exit
|
|
40
41
|
|
|
41
42
|
## Release
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import type {
|
|
2
3
|
ExtensionAPI,
|
|
3
4
|
ExtensionCommandContext,
|
|
@@ -9,6 +10,67 @@ import { buildReviewPrompt } from "./prompt.ts";
|
|
|
9
10
|
import { ReviewComponent } from "./review-component.ts";
|
|
10
11
|
import type { DiffSource, ReviewComment, ReviewResult } from "./types.ts";
|
|
11
12
|
|
|
13
|
+
const DIFF_REVIEW_CACHE_ENTRY = "pi-diff-review-cache";
|
|
14
|
+
|
|
15
|
+
type DiffReviewCacheEntry = {
|
|
16
|
+
cacheKey: string;
|
|
17
|
+
comments: ReviewComment[];
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getDiffCacheKey(
|
|
22
|
+
cwd: string,
|
|
23
|
+
source: DiffSource,
|
|
24
|
+
diffText: string,
|
|
25
|
+
): string {
|
|
26
|
+
const hash = createHash("sha256").update(diffText).digest("hex");
|
|
27
|
+
return `${cwd}\0${source.label}\0${hash}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getCachedComments(
|
|
31
|
+
ctx: ExtensionCommandContext,
|
|
32
|
+
cacheKey: string,
|
|
33
|
+
): Map<string, ReviewComment> {
|
|
34
|
+
let latest: DiffReviewCacheEntry | undefined;
|
|
35
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
36
|
+
if (
|
|
37
|
+
entry.type !== "custom" ||
|
|
38
|
+
entry.customType !== DIFF_REVIEW_CACHE_ENTRY
|
|
39
|
+
) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = entry.data as Partial<DiffReviewCacheEntry> | undefined;
|
|
44
|
+
if (data?.cacheKey !== cacheKey || !Array.isArray(data.comments)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
|
|
49
|
+
latest = {
|
|
50
|
+
cacheKey: data.cacheKey,
|
|
51
|
+
comments: data.comments,
|
|
52
|
+
updatedAt: data.updatedAt ?? 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Map(
|
|
58
|
+
(latest?.comments ?? []).map((comment) => [comment.id, comment]),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function persistCachedComments(
|
|
63
|
+
pi: ExtensionAPI,
|
|
64
|
+
cacheKey: string,
|
|
65
|
+
comments: Iterable<ReviewComment>,
|
|
66
|
+
): void {
|
|
67
|
+
pi.appendEntry(DIFF_REVIEW_CACHE_ENTRY, {
|
|
68
|
+
cacheKey,
|
|
69
|
+
comments: [...comments],
|
|
70
|
+
updatedAt: Date.now(),
|
|
71
|
+
} satisfies DiffReviewCacheEntry);
|
|
72
|
+
}
|
|
73
|
+
|
|
12
74
|
export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
13
75
|
pi.registerCommand("diff", {
|
|
14
76
|
description: "Review a git diff in a custom TUI (/diff [git diff args])",
|
|
@@ -30,9 +92,17 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
30
92
|
}
|
|
31
93
|
|
|
32
94
|
const reviewLines = parseDiff(diffText);
|
|
95
|
+
const cacheKey = getDiffCacheKey(ctx.cwd, source, diffText);
|
|
96
|
+
const comments = getCachedComments(ctx, cacheKey);
|
|
97
|
+
if (comments.size > 0) {
|
|
98
|
+
ctx.ui.notify(
|
|
99
|
+
`Restored ${comments.size} cached diff comment${comments.size === 1 ? "" : "s"}.`,
|
|
100
|
+
"info",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
33
104
|
const result = await ctx.ui.custom<ReviewResult>(
|
|
34
105
|
(tui, theme, _keybindings, done) => {
|
|
35
|
-
const comments = new Map<string, ReviewComment>();
|
|
36
106
|
return new ReviewComponent(
|
|
37
107
|
tui,
|
|
38
108
|
theme,
|
|
@@ -41,6 +111,9 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
41
111
|
comments,
|
|
42
112
|
done,
|
|
43
113
|
new PiModelDiffExplainer(ctx),
|
|
114
|
+
(updatedComments) => {
|
|
115
|
+
persistCachedComments(pi, cacheKey, updatedComments.values());
|
|
116
|
+
},
|
|
44
117
|
);
|
|
45
118
|
},
|
|
46
119
|
);
|
|
@@ -48,9 +121,11 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
|
|
|
48
121
|
if (!result || result.action !== "submit") return;
|
|
49
122
|
if (result.comments.length === 0) {
|
|
50
123
|
ctx.ui.notify("No review comments to send.", "info");
|
|
124
|
+
persistCachedComments(pi, cacheKey, []);
|
|
51
125
|
return;
|
|
52
126
|
}
|
|
53
127
|
|
|
128
|
+
persistCachedComments(pi, cacheKey, []);
|
|
54
129
|
pi.sendUserMessage(
|
|
55
130
|
buildReviewPrompt(result.comments, source.promptLabel),
|
|
56
131
|
);
|
package/src/review-component.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLanguageFromPath,
|
|
3
|
+
highlightCode,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
1
5
|
import {
|
|
2
6
|
Editor,
|
|
3
7
|
matchesKey,
|
|
@@ -58,6 +62,7 @@ export class ReviewComponent {
|
|
|
58
62
|
private commentLineKeys = new Map<number, string[]>();
|
|
59
63
|
private commentsRevision = 0;
|
|
60
64
|
private commentLineKeysRevision = -1;
|
|
65
|
+
private highlightedLineCache = new Map<string, string>();
|
|
61
66
|
|
|
62
67
|
constructor(
|
|
63
68
|
private tui: ReviewTui,
|
|
@@ -67,6 +72,7 @@ export class ReviewComponent {
|
|
|
67
72
|
private comments: Map<string, ReviewComment>,
|
|
68
73
|
private done: (result: ReviewResult) => void,
|
|
69
74
|
private explainer?: DiffExplainer,
|
|
75
|
+
private onCommentsChanged?: (comments: Map<string, ReviewComment>) => void,
|
|
70
76
|
) {
|
|
71
77
|
const firstCommentable = this.lines.findIndex((line) => line.commentable);
|
|
72
78
|
this.selected = firstCommentable >= 0 ? firstCommentable : 0;
|
|
@@ -388,7 +394,9 @@ export class ReviewComponent {
|
|
|
388
394
|
return { diffHeight, commentsHeight };
|
|
389
395
|
}
|
|
390
396
|
|
|
391
|
-
invalidate(): void {
|
|
397
|
+
invalidate(): void {
|
|
398
|
+
this.highlightedLineCache.clear();
|
|
399
|
+
}
|
|
392
400
|
|
|
393
401
|
dispose(): void {
|
|
394
402
|
this.explanationAbort?.abort();
|
|
@@ -508,6 +516,7 @@ export class ReviewComponent {
|
|
|
508
516
|
|
|
509
517
|
private markCommentsChanged(): void {
|
|
510
518
|
this.commentsRevision++;
|
|
519
|
+
this.onCommentsChanged?.(this.comments);
|
|
511
520
|
}
|
|
512
521
|
|
|
513
522
|
private ensureCommentLineKeys(): void {
|
|
@@ -726,22 +735,8 @@ export class ReviewComponent {
|
|
|
726
735
|
const commentMark = hasComment ? this.theme.fg("warning", "●") : " ";
|
|
727
736
|
const lineNumber =
|
|
728
737
|
side === "left" ? line.oldLineNumber : line.newLineNumber;
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
let styled: string;
|
|
732
|
-
switch (line.kind) {
|
|
733
|
-
case "add":
|
|
734
|
-
styled = this.theme.fg("toolDiffAdded", raw);
|
|
735
|
-
break;
|
|
736
|
-
case "remove":
|
|
737
|
-
styled = this.theme.fg("toolDiffRemoved", raw);
|
|
738
|
-
break;
|
|
739
|
-
case "context":
|
|
740
|
-
styled = this.theme.fg("toolDiffContext", raw);
|
|
741
|
-
break;
|
|
742
|
-
default:
|
|
743
|
-
styled = this.theme.fg("muted", raw);
|
|
744
|
-
}
|
|
738
|
+
const prefix = `${commentMark} ${lineNumberCell(lineNumber)} `;
|
|
739
|
+
let styled = this.renderDiffRowContent(line, prefix);
|
|
745
740
|
|
|
746
741
|
styled = truncateToWidth(styled, width);
|
|
747
742
|
const selection = this.getSelectionBounds();
|
|
@@ -750,7 +745,7 @@ export class ReviewComponent {
|
|
|
750
745
|
if (index === this.selected || inSelection) {
|
|
751
746
|
return this.theme.bg("selectedBg", padToWidth(styled, width));
|
|
752
747
|
}
|
|
753
|
-
return styled;
|
|
748
|
+
return this.applyDiffBackground(line, styled, width);
|
|
754
749
|
}
|
|
755
750
|
|
|
756
751
|
private ensureScroll(viewportHeight: number): void {
|
|
@@ -777,43 +772,75 @@ export class ReviewComponent {
|
|
|
777
772
|
: line.text;
|
|
778
773
|
}
|
|
779
774
|
|
|
780
|
-
private
|
|
775
|
+
private applyDiffBackground(
|
|
781
776
|
line: ReviewLine,
|
|
782
|
-
|
|
777
|
+
styled: string,
|
|
783
778
|
width: number,
|
|
784
|
-
selected: boolean,
|
|
785
|
-
selection?: SelectionBounds,
|
|
786
779
|
): string {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
780
|
+
if (line.kind === "add") {
|
|
781
|
+
return this.theme.bg("toolSuccessBg", padToWidth(styled, width));
|
|
782
|
+
}
|
|
783
|
+
if (line.kind === "remove") {
|
|
784
|
+
return this.theme.bg("toolErrorBg", padToWidth(styled, width));
|
|
785
|
+
}
|
|
786
|
+
return styled;
|
|
787
|
+
}
|
|
791
788
|
|
|
792
|
-
|
|
789
|
+
private renderDiffRowContent(line: ReviewLine, prefix: string): string {
|
|
793
790
|
switch (line.kind) {
|
|
794
791
|
case "add":
|
|
795
|
-
|
|
796
|
-
break;
|
|
792
|
+
return `${this.theme.fg("toolDiffAdded", prefix)}${this.getHighlightedDisplayText(line)}`;
|
|
797
793
|
case "remove":
|
|
798
|
-
|
|
799
|
-
break;
|
|
794
|
+
return `${this.theme.fg("toolDiffRemoved", prefix)}${this.getHighlightedDisplayText(line)}`;
|
|
800
795
|
case "context":
|
|
801
|
-
|
|
802
|
-
break;
|
|
796
|
+
return `${this.theme.fg("toolDiffContext", prefix)}${this.getHighlightedDisplayText(line)}`;
|
|
803
797
|
case "hunk":
|
|
804
|
-
|
|
805
|
-
break;
|
|
798
|
+
return this.theme.fg("accent", `${prefix}${this.getDisplayText(line)}`);
|
|
806
799
|
default:
|
|
807
|
-
|
|
800
|
+
return this.theme.fg("muted", `${prefix}${this.getDisplayText(line)}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private getHighlightedDisplayText(line: ReviewLine): string {
|
|
805
|
+
const code = this.getDisplayText(line);
|
|
806
|
+
if (!code) return code;
|
|
807
|
+
|
|
808
|
+
const lang = line.filePath ? getLanguageFromPath(line.filePath) : undefined;
|
|
809
|
+
const cacheKey = `${line.id}\0${lang ?? ""}\0${code}`;
|
|
810
|
+
const cached = this.highlightedLineCache.get(cacheKey);
|
|
811
|
+
if (cached != null) return cached;
|
|
812
|
+
|
|
813
|
+
let highlighted = code;
|
|
814
|
+
try {
|
|
815
|
+
highlighted = highlightCode(code, lang)[0] ?? code;
|
|
816
|
+
} catch {
|
|
817
|
+
highlighted = code;
|
|
808
818
|
}
|
|
809
819
|
|
|
820
|
+
this.highlightedLineCache.set(cacheKey, highlighted);
|
|
821
|
+
return highlighted;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private renderDiffLine(
|
|
825
|
+
line: ReviewLine,
|
|
826
|
+
index: number,
|
|
827
|
+
width: number,
|
|
828
|
+
selected: boolean,
|
|
829
|
+
selection?: SelectionBounds,
|
|
830
|
+
): string {
|
|
831
|
+
const hasComment = this.getCommentKeysForLine(index).length > 0;
|
|
832
|
+
const commentMark = hasComment ? this.theme.fg("warning", "●") : " ";
|
|
833
|
+
const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
|
|
834
|
+
const prefix = `${commentMark} ${numbers} `;
|
|
835
|
+
let styled = this.renderDiffRowContent(line, prefix);
|
|
836
|
+
|
|
810
837
|
styled = truncateToWidth(styled, width);
|
|
811
838
|
const inSelection =
|
|
812
839
|
selection != null && index >= selection.start && index <= selection.end;
|
|
813
840
|
if (selected || inSelection) {
|
|
814
841
|
return this.theme.bg("selectedBg", padToWidth(styled, width));
|
|
815
842
|
}
|
|
816
|
-
return styled;
|
|
843
|
+
return this.applyDiffBackground(line, styled, width);
|
|
817
844
|
}
|
|
818
845
|
|
|
819
846
|
private renderRightPane(
|