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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
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
  );
@@ -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 raw = `${commentMark} ${lineNumberCell(lineNumber)} ${this.getDisplayText(line)}`;
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 renderDiffLine(
775
+ private applyDiffBackground(
781
776
  line: ReviewLine,
782
- index: number,
777
+ styled: string,
783
778
  width: number,
784
- selected: boolean,
785
- selection?: SelectionBounds,
786
779
  ): string {
787
- const hasComment = this.getCommentKeysForLine(index).length > 0;
788
- const commentMark = hasComment ? this.theme.fg("warning", "●") : " ";
789
- const numbers = `${lineNumberCell(line.oldLineNumber)} ${lineNumberCell(line.newLineNumber)}`;
790
- const raw = `${commentMark} ${numbers} ${this.getDisplayText(line)}`;
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
- let styled: string;
789
+ private renderDiffRowContent(line: ReviewLine, prefix: string): string {
793
790
  switch (line.kind) {
794
791
  case "add":
795
- styled = this.theme.fg("toolDiffAdded", raw);
796
- break;
792
+ return `${this.theme.fg("toolDiffAdded", prefix)}${this.getHighlightedDisplayText(line)}`;
797
793
  case "remove":
798
- styled = this.theme.fg("toolDiffRemoved", raw);
799
- break;
794
+ return `${this.theme.fg("toolDiffRemoved", prefix)}${this.getHighlightedDisplayText(line)}`;
800
795
  case "context":
801
- styled = this.theme.fg("toolDiffContext", raw);
802
- break;
796
+ return `${this.theme.fg("toolDiffContext", prefix)}${this.getHighlightedDisplayText(line)}`;
803
797
  case "hunk":
804
- styled = this.theme.fg("accent", raw);
805
- break;
798
+ return this.theme.fg("accent", `${prefix}${this.getDisplayText(line)}`);
806
799
  default:
807
- styled = this.theme.fg("muted", raw);
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(