pi-diff-review 0.1.19 → 0.1.20

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.
@@ -0,0 +1,171 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import type { PersistedAsk, ReviewComment } from "./types.ts";
6
+
7
+ const REVIEW_COMMENT_CACHE_ENTRY = "pi-diff-review-cache";
8
+ const REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
9
+ const REVIEW_ASK_CACHE_ENTRY = "pi-diff-review-ask-cache";
10
+
11
+ type ReviewCommentCacheEntry = {
12
+ cacheKey: string;
13
+ comments: ReviewComment[];
14
+ updatedAt: number;
15
+ };
16
+
17
+ type ReviewExplanationCacheEntry = {
18
+ cacheKey: string;
19
+ explanations: Record<string, string>;
20
+ updatedAt: number;
21
+ };
22
+
23
+ type ReviewAskCacheEntry = {
24
+ cacheKey: string;
25
+ ask?: PersistedAsk;
26
+ updatedAt: number;
27
+ };
28
+
29
+ export function getCachedComments(
30
+ ctx: ExtensionCommandContext,
31
+ cacheKey: string,
32
+ ): Map<string, ReviewComment> {
33
+ let latest: ReviewCommentCacheEntry | undefined;
34
+ for (const entry of ctx.sessionManager.getEntries()) {
35
+ if (
36
+ entry.type !== "custom" ||
37
+ entry.customType !== REVIEW_COMMENT_CACHE_ENTRY
38
+ ) {
39
+ continue;
40
+ }
41
+
42
+ const data = entry.data as Partial<ReviewCommentCacheEntry> | undefined;
43
+ if (data?.cacheKey !== cacheKey || !Array.isArray(data.comments)) continue;
44
+
45
+ if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
46
+ latest = {
47
+ cacheKey: data.cacheKey,
48
+ comments: data.comments,
49
+ updatedAt: data.updatedAt ?? 0,
50
+ };
51
+ }
52
+ }
53
+
54
+ return new Map(
55
+ (latest?.comments ?? []).map((comment) => [comment.id, comment]),
56
+ );
57
+ }
58
+
59
+ export function persistCachedComments(
60
+ pi: ExtensionAPI,
61
+ cacheKey: string,
62
+ comments: Iterable<ReviewComment>,
63
+ ): void {
64
+ pi.appendEntry(REVIEW_COMMENT_CACHE_ENTRY, {
65
+ cacheKey,
66
+ comments: [...comments],
67
+ updatedAt: Date.now(),
68
+ } satisfies ReviewCommentCacheEntry);
69
+ }
70
+
71
+ export function getCachedExplanations(
72
+ ctx: ExtensionCommandContext,
73
+ cacheKey: string,
74
+ ): Map<string, string> {
75
+ let latest: ReviewExplanationCacheEntry | undefined;
76
+ for (const entry of ctx.sessionManager.getEntries()) {
77
+ if (
78
+ entry.type !== "custom" ||
79
+ entry.customType !== REVIEW_EXPLANATION_CACHE_ENTRY
80
+ ) {
81
+ continue;
82
+ }
83
+
84
+ const data = entry.data as Partial<ReviewExplanationCacheEntry> | undefined;
85
+ if (
86
+ data?.cacheKey !== cacheKey ||
87
+ !data.explanations ||
88
+ typeof data.explanations !== "object" ||
89
+ Array.isArray(data.explanations)
90
+ ) {
91
+ continue;
92
+ }
93
+
94
+ if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
95
+ latest = {
96
+ cacheKey: data.cacheKey,
97
+ explanations: Object.fromEntries(
98
+ Object.entries(data.explanations).filter(
99
+ (entry): entry is [string, string] =>
100
+ typeof entry[0] === "string" && typeof entry[1] === "string",
101
+ ),
102
+ ),
103
+ updatedAt: data.updatedAt ?? 0,
104
+ };
105
+ }
106
+ }
107
+
108
+ return new Map(Object.entries(latest?.explanations ?? {}));
109
+ }
110
+
111
+ export function persistCachedExplanations(
112
+ pi: ExtensionAPI,
113
+ cacheKey: string,
114
+ explanations: Map<string, string>,
115
+ ): void {
116
+ pi.appendEntry(REVIEW_EXPLANATION_CACHE_ENTRY, {
117
+ cacheKey,
118
+ explanations: Object.fromEntries(explanations),
119
+ updatedAt: Date.now(),
120
+ } satisfies ReviewExplanationCacheEntry);
121
+ }
122
+
123
+ export function getCachedAsk(
124
+ ctx: ExtensionCommandContext,
125
+ cacheKey: string,
126
+ ): PersistedAsk | undefined {
127
+ let latest: ReviewAskCacheEntry | undefined;
128
+ for (const entry of ctx.sessionManager.getEntries()) {
129
+ if (
130
+ entry.type !== "custom" ||
131
+ entry.customType !== REVIEW_ASK_CACHE_ENTRY
132
+ ) {
133
+ continue;
134
+ }
135
+
136
+ const data = entry.data as Partial<ReviewAskCacheEntry> | undefined;
137
+ if (data?.cacheKey !== cacheKey) continue;
138
+
139
+ const ask = data.ask;
140
+ const validAsk =
141
+ ask &&
142
+ typeof ask === "object" &&
143
+ typeof ask.scopeKey === "string" &&
144
+ typeof ask.anchorLineId === "string" &&
145
+ typeof ask.text === "string"
146
+ ? ask
147
+ : undefined;
148
+
149
+ if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
150
+ latest = {
151
+ cacheKey: data.cacheKey,
152
+ ask: validAsk,
153
+ updatedAt: data.updatedAt ?? 0,
154
+ };
155
+ }
156
+ }
157
+
158
+ return latest?.ask;
159
+ }
160
+
161
+ export function persistCachedAsk(
162
+ pi: ExtensionAPI,
163
+ cacheKey: string,
164
+ ask?: PersistedAsk,
165
+ ): void {
166
+ pi.appendEntry(REVIEW_ASK_CACHE_ENTRY, {
167
+ cacheKey,
168
+ ask,
169
+ updatedAt: Date.now(),
170
+ } satisfies ReviewAskCacheEntry);
171
+ }
@@ -41,6 +41,7 @@ import type {
41
41
  SelectionBounds,
42
42
  SplitDiffCell,
43
43
  SplitDiffRow,
44
+ WorkspaceCommentSummary,
44
45
  } from "./types.ts";
45
46
 
46
47
  type InlineBoxPart = "top" | "body" | "bottom";
@@ -122,6 +123,9 @@ export class ReviewComponent {
122
123
  private onExplanationsChanged?: (explanations: Map<string, string>) => void,
123
124
  cachedAsk?: PersistedAsk,
124
125
  private onAskChanged?: (ask?: PersistedAsk) => void,
126
+ private getWorkspaceCommentSummary?: (
127
+ comments: Map<string, ReviewComment>,
128
+ ) => WorkspaceCommentSummary | undefined,
125
129
  ) {
126
130
  const firstCommentable = this.lines.findIndex((line) => line.commentable);
127
131
  this.navigation = new ReviewNavigationState(
@@ -394,11 +398,12 @@ export class ReviewComponent {
394
398
  render(width: number): string[] {
395
399
  const viewportHeight = this.getContentHeight();
396
400
  const selectedLine = this.lines[this.selected];
401
+ const workspaceSummary = this.getWorkspaceCommentSummary?.(this.comments);
397
402
  const output: string[] = [];
398
403
 
399
404
  output.push(
400
405
  this.renderStatusLine(
401
- this.getHeaderText(selectedLine),
406
+ this.getHeaderText(selectedLine, workspaceSummary),
402
407
  "Press h for help",
403
408
  width,
404
409
  ),
@@ -409,15 +414,21 @@ export class ReviewComponent {
409
414
 
410
415
  output.push(
411
416
  truncateToWidth(
412
- this.theme.fg("muted", this.getFooterText(selectedLine)),
417
+ this.theme.fg(
418
+ "muted",
419
+ this.getFooterText(selectedLine, workspaceSummary),
420
+ ),
413
421
  width,
414
422
  ),
415
423
  );
416
424
  return this.helpVisible ? this.renderHelpModal(output, width) : output;
417
425
  }
418
426
 
419
- private getHeaderText(selectedLine?: ReviewLine): string {
420
- const base = `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments`;
427
+ private getHeaderText(
428
+ selectedLine?: ReviewLine,
429
+ workspaceSummary?: WorkspaceCommentSummary,
430
+ ): string {
431
+ const base = `${this.title} • ${this.lines.length} lines • ${this.comments.size} comments${this.formatWorkspaceSummary(workspaceSummary)}`;
421
432
 
422
433
  if (this.editMode) {
423
434
  return `${base} • editing ${this.editingCommentKey === GLOBAL_COMMENT_KEY ? "overall comment" : "inline comment"}`;
@@ -1209,7 +1220,10 @@ export class ReviewComponent {
1209
1220
  : position;
1210
1221
  }
1211
1222
 
1212
- private getFooterText(selectedLine?: ReviewLine): string {
1223
+ private getFooterText(
1224
+ selectedLine?: ReviewLine,
1225
+ workspaceSummary?: WorkspaceCommentSummary,
1226
+ ): string {
1213
1227
  if (this.search.mode) {
1214
1228
  return `Search: /${this.search.draftQuery} • Enter jump • Esc cancel`;
1215
1229
  }
@@ -1224,7 +1238,28 @@ export class ReviewComponent {
1224
1238
  const endLine = this.lines[selection.end]!;
1225
1239
  return `Selected ${count} lines: ${formatLocation(startLine)} -> ${formatLocation(endLine)}`;
1226
1240
  }
1227
- return `Selected: ${selectedLine ? formatLocation(selectedLine) : "(no selection)"}`;
1241
+
1242
+ const selectedText = `Selected: ${selectedLine ? formatLocation(selectedLine) : "(no selection)"}`;
1243
+ const workspaceText = this.formatWorkspaceSummary(workspaceSummary, false);
1244
+ return workspaceText ? `${selectedText} • ${workspaceText}` : selectedText;
1245
+ }
1246
+
1247
+ private formatWorkspaceSummary(
1248
+ summary?: WorkspaceCommentSummary,
1249
+ includeVisible = true,
1250
+ ): string {
1251
+ if (!summary) return "";
1252
+
1253
+ const parts: string[] = [];
1254
+ if (includeVisible && summary.visible > 0)
1255
+ parts.push(`${summary.visible} visible persisted`);
1256
+ if (summary.hiddenInCurrentFiles > 0) {
1257
+ parts.push(`${summary.hiddenInCurrentFiles} hidden in current files`);
1258
+ }
1259
+ if (summary.elsewhere > 0) parts.push(`${summary.elsewhere} elsewhere`);
1260
+ if (summary.stale > 0) parts.push(`${summary.stale} stale`);
1261
+ if (summary.orphaned > 0) parts.push(`${summary.orphaned} orphaned`);
1262
+ return parts.length > 0 ? ` • ${parts.join(" • ")}` : "";
1228
1263
  }
1229
1264
 
1230
1265
  private jumpHunk(direction: 1 | -1): void {
@@ -36,16 +36,39 @@ export function formatCommentLocation(comment: ReviewComment): string {
36
36
  export function buildReviewPrompt(
37
37
  comments: ReviewComment[],
38
38
  promptLabel: string,
39
+ ): string {
40
+ return buildCommentPrompt(
41
+ comments,
42
+ `Address this local code review feedback for ${promptLabel}.`,
43
+ "diff",
44
+ );
45
+ }
46
+
47
+ export function buildViewReviewPrompt(
48
+ comments: ReviewComment[],
49
+ promptLabel: string,
50
+ ): string {
51
+ return buildCommentPrompt(
52
+ comments,
53
+ `Address this code review feedback for ${promptLabel}.`,
54
+ "ts",
55
+ );
56
+ }
57
+
58
+ function buildCommentPrompt(
59
+ comments: ReviewComment[],
60
+ intro: string,
61
+ codeFenceLanguage: string,
39
62
  ): string {
40
63
  const body = comments
41
64
  .map((comment) => {
42
65
  const location = formatCommentLocation(comment);
43
66
  const excerpt = comment.lineText.trim()
44
- ? `\n Excerpt:\n\n\`\`\`diff\n${comment.lineText}\n\`\`\``
67
+ ? `\n Excerpt:\n\n\`\`\`${codeFenceLanguage}\n${comment.lineText}\n\`\`\``
45
68
  : "";
46
69
  return `- \`${location}\` — ${comment.text}${excerpt}`;
47
70
  })
48
71
  .join("\n");
49
72
 
50
- return `Address this local code review feedback for ${promptLabel}.\n\n## Review comments\n${body}\n\nPlease apply the feedback and summarize what changed.`;
73
+ return `${intro}\n\n## Review comments\n${body}\n\nPlease apply the feedback and summarize what changed.`;
51
74
  }
@@ -48,6 +48,14 @@ export type PersistedAsk = {
48
48
  text: string;
49
49
  };
50
50
 
51
+ export type WorkspaceCommentSummary = {
52
+ visible: number;
53
+ hiddenInCurrentFiles: number;
54
+ elsewhere: number;
55
+ stale: number;
56
+ orphaned: number;
57
+ };
58
+
51
59
  export type DiffRenderMode = "unified" | "split";
52
60
 
53
61
  export type SplitDiffCell = {