pi-diff-review 0.1.18 → 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.
package/README.md CHANGED
@@ -49,6 +49,14 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
49
49
 
50
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
51
 
52
+ Open one or more files or folders with `/view`:
53
+
54
+ ```text
55
+ /view src/index.ts src/review
56
+ ```
57
+
58
+ `/view` expands folders into text files, renders them in the same review UI, and lets you annotate actual code lines instead of diff hunks.
59
+
52
60
  ### Staged vs. unstaged changes
53
61
 
54
62
  - `/diff` shows unstaged working-tree changes only.
@@ -59,6 +67,7 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
59
67
  ## Features
60
68
 
61
69
  - `/diff` reviews the current unstaged `git diff`
70
+ - `/view <files-or-folders>` reviews source files directly
62
71
  - `/diff --cached` reviews staged changes
63
72
  - `/diff main...HEAD` reviews changes on the current branch relative to `main`
64
73
  - `/diff <git-diff-args>` passes arguments through to `git diff`
@@ -77,7 +86,9 @@ Review a branch or commit range by passing any `git diff` arguments after `/diff
77
86
  - `C` to add or edit an overall diff comment
78
87
  - `x` to delete a comment for the current line or selected range
79
88
  - `Enter` to submit comments back to pi
80
- - Comments are cached per session and restored when reopening the same diff
89
+ - Comments are cached per session and restored when reopening the same diff or view
90
+ - File comments are also persisted in a repo-local workspace store and shown again in `/view` or on matching lines in `/diff`
91
+ - The UI indicates persisted comments that are hidden in the current files, elsewhere in the workspace, stale, or orphaned
81
92
  - `q` to exit
82
93
 
83
94
  ## Contributing
@@ -1,6 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerDiffReviewCommand } from "../src/index.ts";
2
+ import {
3
+ registerDiffReviewCommand,
4
+ registerViewCommand,
5
+ } from "../src/index.ts";
3
6
 
4
7
  export default function (pi: ExtensionAPI) {
5
8
  registerDiffReviewCommand(pi);
9
+ registerViewCommand(pi);
6
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-diff-review",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Local diff review TUI extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import type { DiffSource } from "../review/types.ts";
3
+ import { tokenizeShellArgs } from "../shared/args.ts";
3
4
 
4
5
  export function parseDiffSource(args: string): DiffSource {
5
6
  const trimmed = args.trim();
@@ -20,48 +21,12 @@ export function parseDiffSource(args: string): DiffSource {
20
21
  }
21
22
 
22
23
  function tokenizeDiffArgs(input: string): string[] {
23
- const args: string[] = [];
24
- let current = "";
25
- let quote: '"' | "'" | undefined;
26
- let escaping = false;
27
-
28
- for (const char of input) {
29
- if (escaping) {
30
- current += char;
31
- escaping = false;
32
- continue;
33
- }
34
-
35
- if (char === "\\" && quote !== "'") {
36
- escaping = true;
37
- continue;
38
- }
39
-
40
- if ((char === '"' || char === "'") && !quote) {
41
- quote = char;
42
- continue;
43
- }
44
-
45
- if (char === quote) {
46
- quote = undefined;
47
- continue;
48
- }
49
-
50
- if (/\s/.test(char) && !quote) {
51
- if (current) {
52
- args.push(current);
53
- current = "";
54
- }
55
- continue;
56
- }
57
-
58
- current += char;
24
+ try {
25
+ return tokenizeShellArgs(input);
26
+ } catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ throw new Error(message.replace(/in arguments$/, "in git diff args"));
59
29
  }
60
-
61
- if (escaping) current += "\\";
62
- if (quote) throw new Error(`Unterminated ${quote} quote in git diff args`);
63
- if (current) args.push(current);
64
- return args;
65
30
  }
66
31
 
67
32
  export const DIFF_MAX_BUFFER_BYTES = 128 * 1024 * 1024;
@@ -12,7 +12,40 @@ export function getCurrentHunkScope(
12
12
  selected: number,
13
13
  ): ExplanationScope | undefined {
14
14
  const selectedLine = lines[selected];
15
- if (!selectedLine?.filePath || !selectedLine.hunkLabel) return undefined;
15
+ if (!selectedLine?.filePath) return undefined;
16
+ if (!selectedLine.commentable && !selectedLine.hunkLabel) return undefined;
17
+
18
+ if (!selectedLine.hunkLabel) {
19
+ let start = selected;
20
+ while (
21
+ start > 0 &&
22
+ lines[start - 1]?.filePath === selectedLine.filePath &&
23
+ lines[start - 1]?.commentable
24
+ ) {
25
+ start--;
26
+ }
27
+
28
+ let end = selected;
29
+ while (
30
+ end + 1 < lines.length &&
31
+ lines[end + 1]?.filePath === selectedLine.filePath &&
32
+ lines[end + 1]?.commentable
33
+ ) {
34
+ end++;
35
+ }
36
+
37
+ const diffText = lines
38
+ .slice(start, end + 1)
39
+ .map((line) => line.text.replace(/^ /, ""))
40
+ .join("\n");
41
+ return {
42
+ key: `file:${selectedLine.filePath}:${start}:${end}`,
43
+ kind: "file",
44
+ title: selectedLine.filePath,
45
+ filePath: selectedLine.filePath,
46
+ diffText,
47
+ };
48
+ }
16
49
 
17
50
  let start = selected;
18
51
  while (
@@ -62,12 +95,19 @@ export class ExplanationController {
62
95
  private readonly onExplanationsChanged?: (
63
96
  explanations: Map<string, string>,
64
97
  ) => void,
98
+ cachedAskText?: string,
99
+ private readonly onAskChanged?: (state?: ExplanationState) => void,
65
100
  ) {
66
101
  for (const [key, text] of cachedExplanations) {
67
102
  const trimmed = text.trim();
68
103
  if (trimmed)
69
104
  this.explanations.set(key, { status: "ready", text: trimmed });
70
105
  }
106
+
107
+ const trimmedAskText = cachedAskText?.trim();
108
+ if (trimmedAskText) {
109
+ this.askState = { status: "ready", text: trimmedAskText };
110
+ }
71
111
  }
72
112
 
73
113
  get isAvailable(): boolean {
@@ -90,6 +130,7 @@ export class ExplanationController {
90
130
  const requestId = ++this.askRequestId;
91
131
  let text = "";
92
132
  this.askState = { status: "loading", text };
133
+ this.onAskChanged?.(this.askState);
93
134
  this.startLoadingTimer();
94
135
 
95
136
  void this.explainer
@@ -99,6 +140,7 @@ export class ExplanationController {
99
140
  if (requestId !== this.askRequestId) return;
100
141
  text += delta;
101
142
  this.askState = { status: "loading", text };
143
+ this.onAskChanged?.(this.askState);
102
144
  this.tui.requestRender();
103
145
  },
104
146
  })
@@ -108,6 +150,7 @@ export class ExplanationController {
108
150
  status: "ready",
109
151
  text: finalText.trim() || text.trim() || "No answer returned.",
110
152
  };
153
+ this.onAskChanged?.(this.askState);
111
154
  })
112
155
  .catch((error) => {
113
156
  if (requestId !== this.askRequestId) return;
@@ -116,6 +159,7 @@ export class ExplanationController {
116
159
  status: "error",
117
160
  message: error instanceof Error ? error.message : String(error),
118
161
  };
162
+ this.onAskChanged?.(this.askState);
119
163
  })
120
164
  .finally(() => {
121
165
  if (requestId !== this.askRequestId) return;
@@ -134,6 +178,7 @@ export class ExplanationController {
134
178
  this.askAbortController?.abort();
135
179
  this.askAbortController = undefined;
136
180
  this.askState = undefined;
181
+ this.onAskChanged?.(undefined);
137
182
  }
138
183
 
139
184
  ensure(scope: ExplanationScope | undefined): void {
@@ -4,7 +4,7 @@ import type { AssistantMessage, Context } from "@earendil-works/pi-ai";
4
4
 
5
5
  export type ExplanationScope = {
6
6
  key: string;
7
- kind: "hunk";
7
+ kind: "hunk" | "file";
8
8
  title: string;
9
9
  filePath?: string;
10
10
  diffText: string;
@@ -30,10 +30,30 @@ export function buildAskPrompt(
30
30
  scope: ExplanationScope,
31
31
  question: string,
32
32
  ): string {
33
+ if (scope.kind === "file") {
34
+ return `Given this code excerpt:\n\`\`\`${scope.filePath ? ` ${scope.filePath}` : ""}\n${scope.diffText}\n\`\`\`\n\n${question}`;
35
+ }
36
+
33
37
  return `Given this git diff hunk:\n\`\`\`diff\n${scope.diffText}\n\`\`\`\n\n${question}`;
34
38
  }
35
39
 
36
40
  export function buildExplanationPrompt(scope: ExplanationScope): string {
41
+ if (scope.kind === "file") {
42
+ return `Explain this code excerpt for a reviewer.
43
+
44
+ Focus on:
45
+ - what this code is doing
46
+ - why it matters in context
47
+ - behavioral, API, or test implications
48
+ - notable risks or edge cases
49
+
50
+ Keep it concise and practical.
51
+
52
+ \`\`\`${scope.filePath ? ` ${scope.filePath}` : ""}
53
+ ${scope.diffText}
54
+ \`\`\``;
55
+ }
56
+
37
57
  return `Explain this git diff hunk for a code reviewer.
38
58
 
39
59
  Focus on:
@@ -72,7 +92,7 @@ export class PiModelDiffExplainer implements DiffExplainer {
72
92
 
73
93
  const context: Context = {
74
94
  systemPrompt:
75
- "You explain code diffs clearly and concisely for code review. Focus on intent, behavior, and risk. Avoid restating every changed line.",
95
+ "You explain code clearly and concisely for review. Focus on intent, behavior, and risk. Avoid restating every line.",
76
96
  messages: [
77
97
  {
78
98
  role: "user",
package/src/index.ts CHANGED
@@ -6,139 +6,35 @@ import type {
6
6
  import { getDiff, parseDiffSource } from "./diff/source.ts";
7
7
  import { parseDiff } from "./diff/parser.ts";
8
8
  import { PiModelDiffExplainer } from "./explanation/explainer.ts";
9
- import { buildReviewPrompt } from "./review/prompt.ts";
9
+ import {
10
+ getCachedAsk,
11
+ getCachedComments,
12
+ getCachedExplanations,
13
+ persistCachedAsk,
14
+ persistCachedComments,
15
+ persistCachedExplanations,
16
+ } from "./review/cache.ts";
10
17
  import { ReviewComponent } from "./review/component.ts";
18
+ import { buildReviewPrompt, buildViewReviewPrompt } from "./review/prompt.ts";
11
19
  import type {
12
- DiffSource,
13
20
  ReviewComment,
21
+ ReviewLine,
14
22
  ReviewResult,
15
23
  } from "./review/types.ts";
24
+ import { WorkspaceCommentStore } from "./review/workspace-comments.ts";
25
+ import { parseViewFiles } from "./view/parser.ts";
26
+ import { parseViewSource, resolveViewFiles } from "./view/source.ts";
16
27
 
17
- const DIFF_REVIEW_CACHE_ENTRY = "pi-diff-review-cache";
18
- const DIFF_REVIEW_EXPLANATION_CACHE_ENTRY = "pi-diff-review-explanation-cache";
19
-
20
- type DiffReviewCacheEntry = {
21
- cacheKey: string;
22
- comments: ReviewComment[];
23
- updatedAt: number;
24
- };
25
-
26
- type DiffExplanationCacheEntry = {
27
- cacheKey: string;
28
- explanations: Record<string, string>;
29
- updatedAt: number;
30
- };
31
-
32
- function getDiffCacheKey(
33
- cwd: string,
34
- source: DiffSource,
35
- diffText: string,
36
- ): string {
37
- const hash = createHash("sha256").update(diffText).digest("hex");
38
- return `${cwd}\0${source.label}\0${hash}`;
39
- }
40
-
41
- function getCachedComments(
42
- ctx: ExtensionCommandContext,
43
- cacheKey: string,
44
- ): Map<string, ReviewComment> {
45
- let latest: DiffReviewCacheEntry | undefined;
46
- for (const entry of ctx.sessionManager.getEntries()) {
47
- if (
48
- entry.type !== "custom" ||
49
- entry.customType !== DIFF_REVIEW_CACHE_ENTRY
50
- ) {
51
- continue;
52
- }
53
-
54
- const data = entry.data as Partial<DiffReviewCacheEntry> | undefined;
55
- if (data?.cacheKey !== cacheKey || !Array.isArray(data.comments)) {
56
- continue;
57
- }
58
-
59
- if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
60
- latest = {
61
- cacheKey: data.cacheKey,
62
- comments: data.comments,
63
- updatedAt: data.updatedAt ?? 0,
64
- };
65
- }
66
- }
67
-
68
- return new Map(
69
- (latest?.comments ?? []).map((comment) => [comment.id, comment]),
70
- );
71
- }
72
-
73
- function persistCachedComments(
74
- pi: ExtensionAPI,
75
- cacheKey: string,
76
- comments: Iterable<ReviewComment>,
77
- ): void {
78
- pi.appendEntry(DIFF_REVIEW_CACHE_ENTRY, {
79
- cacheKey,
80
- comments: [...comments],
81
- updatedAt: Date.now(),
82
- } satisfies DiffReviewCacheEntry);
83
- }
84
-
85
- function getCachedExplanations(
86
- ctx: ExtensionCommandContext,
87
- cacheKey: string,
88
- ): Map<string, string> {
89
- let latest: DiffExplanationCacheEntry | undefined;
90
- for (const entry of ctx.sessionManager.getEntries()) {
91
- if (
92
- entry.type !== "custom" ||
93
- entry.customType !== DIFF_REVIEW_EXPLANATION_CACHE_ENTRY
94
- ) {
95
- continue;
96
- }
97
-
98
- const data = entry.data as Partial<DiffExplanationCacheEntry> | undefined;
99
- if (
100
- data?.cacheKey !== cacheKey ||
101
- !data.explanations ||
102
- typeof data.explanations !== "object" ||
103
- Array.isArray(data.explanations)
104
- ) {
105
- continue;
106
- }
107
-
108
- if (!latest || (data.updatedAt ?? 0) >= latest.updatedAt) {
109
- latest = {
110
- cacheKey: data.cacheKey,
111
- explanations: Object.fromEntries(
112
- Object.entries(data.explanations).filter(
113
- (entry): entry is [string, string] =>
114
- typeof entry[0] === "string" && typeof entry[1] === "string",
115
- ),
116
- ),
117
- updatedAt: data.updatedAt ?? 0,
118
- };
119
- }
120
- }
121
-
122
- return new Map(Object.entries(latest?.explanations ?? {}));
123
- }
124
-
125
- function persistCachedExplanations(
126
- pi: ExtensionAPI,
127
- cacheKey: string,
128
- explanations: Map<string, string>,
129
- ): void {
130
- pi.appendEntry(DIFF_REVIEW_EXPLANATION_CACHE_ENTRY, {
131
- cacheKey,
132
- explanations: Object.fromEntries(explanations),
133
- updatedAt: Date.now(),
134
- } satisfies DiffExplanationCacheEntry);
28
+ function getReviewCacheKey(cwd: string, label: string, text: string): string {
29
+ const hash = createHash("sha256").update(text).digest("hex");
30
+ return `${cwd}\0${label}\0${hash}`;
135
31
  }
136
32
 
137
33
  export function registerDiffReviewCommand(pi: ExtensionAPI): void {
138
34
  pi.registerCommand("diff", {
139
35
  description: "Review a git diff in a custom TUI (/diff [git diff args])",
140
36
  handler: async (args: string, ctx: ExtensionCommandContext) => {
141
- let source: DiffSource;
37
+ let source;
142
38
  let diffText: string;
143
39
  try {
144
40
  source = parseDiffSource(args);
@@ -155,54 +51,168 @@ export function registerDiffReviewCommand(pi: ExtensionAPI): void {
155
51
  }
156
52
 
157
53
  const reviewLines = parseDiff(diffText);
158
- const cacheKey = getDiffCacheKey(ctx.cwd, source, diffText);
159
- const comments = getCachedComments(ctx, cacheKey);
160
- const explanations = getCachedExplanations(ctx, cacheKey);
161
- if (comments.size > 0) {
162
- ctx.ui.notify(
163
- `Restored ${comments.size} cached diff comment${comments.size === 1 ? "" : "s"}.`,
164
- "info",
165
- );
54
+ await openReview(pi, ctx, {
55
+ title: source.label,
56
+ promptLabel: source.promptLabel,
57
+ cacheKey: getReviewCacheKey(ctx.cwd, source.label, diffText),
58
+ reviewLines,
59
+ buildPrompt: (comments) =>
60
+ buildReviewPrompt(comments, source.promptLabel),
61
+ });
62
+ },
63
+ });
64
+ }
65
+
66
+ export function registerViewCommand(pi: ExtensionAPI): void {
67
+ pi.registerCommand("view", {
68
+ description:
69
+ "Review one or more files or folders in a custom TUI (/view <paths...>)",
70
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
71
+ let source;
72
+ let files: string[];
73
+ try {
74
+ source = parseViewSource(args);
75
+ files = resolveViewFiles(ctx.cwd, source);
76
+ } catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ ctx.ui.notify(`Unable to open view: ${message}`, "error");
79
+ return;
166
80
  }
167
- if (explanations.size > 0) {
81
+
82
+ if (files.length === 0) {
168
83
  ctx.ui.notify(
169
- `Restored ${explanations.size} cached hunk explanation${explanations.size === 1 ? "" : "s"}.`,
84
+ "No viewable text files matched the requested paths.",
170
85
  "info",
171
86
  );
87
+ return;
172
88
  }
173
89
 
174
- const result = await ctx.ui.custom<ReviewResult>(
175
- (tui, theme, _keybindings, done) => {
176
- return new ReviewComponent(
177
- tui,
178
- theme,
179
- source.label,
180
- reviewLines,
181
- comments,
182
- done,
183
- new PiModelDiffExplainer(ctx),
184
- (updatedComments) => {
185
- persistCachedComments(pi, cacheKey, updatedComments.values());
186
- },
187
- explanations,
188
- (updatedExplanations) => {
189
- persistCachedExplanations(pi, cacheKey, updatedExplanations);
190
- },
90
+ const workspaceStore = new WorkspaceCommentStore(ctx.cwd);
91
+ const reviewLines = parseViewFiles(workspaceStore.rootPath, files);
92
+ const contentKey = reviewLines.map((line) => line.text).join("\n");
93
+
94
+ await openReview(pi, ctx, {
95
+ title: source.label,
96
+ promptLabel: source.promptLabel,
97
+ cacheKey: getReviewCacheKey(ctx.cwd, source.label, contentKey),
98
+ reviewLines,
99
+ workspaceStore,
100
+ buildPrompt: (comments) =>
101
+ buildViewReviewPrompt(comments, source.promptLabel),
102
+ });
103
+ },
104
+ });
105
+ }
106
+
107
+ async function openReview(
108
+ pi: ExtensionAPI,
109
+ ctx: ExtensionCommandContext,
110
+ options: {
111
+ title: string;
112
+ promptLabel: string;
113
+ cacheKey: string;
114
+ reviewLines: ReviewLine[];
115
+ buildPrompt: (comments: ReviewComment[]) => string;
116
+ workspaceStore?: WorkspaceCommentStore;
117
+ },
118
+ ): Promise<void> {
119
+ const workspaceStore =
120
+ options.workspaceStore ?? new WorkspaceCommentStore(ctx.cwd);
121
+ const cachedComments = getCachedComments(ctx, options.cacheKey);
122
+ const comments = workspaceStore.getVisibleComments(options.reviewLines);
123
+ for (const [id, comment] of cachedComments) {
124
+ comments.set(id, comment);
125
+ }
126
+
127
+ const explanations = getCachedExplanations(ctx, options.cacheKey);
128
+ const ask = getCachedAsk(ctx, options.cacheKey);
129
+ const summary = () => workspaceStore.summarize(options.reviewLines);
130
+
131
+ if (cachedComments.size > 0) {
132
+ ctx.ui.notify(
133
+ `Restored ${cachedComments.size} cached review comment${cachedComments.size === 1 ? "" : "s"}.`,
134
+ "info",
135
+ );
136
+ }
137
+ if (explanations.size > 0) {
138
+ ctx.ui.notify(
139
+ `Restored ${explanations.size} cached explanation${explanations.size === 1 ? "" : "s"}.`,
140
+ "info",
141
+ );
142
+ }
143
+ if (ask) {
144
+ ctx.ui.notify("Restored cached ask answer.", "info");
145
+ }
146
+
147
+ const initialSummary = summary();
148
+ if (initialSummary.hiddenInCurrentFiles > 0 || initialSummary.elsewhere > 0) {
149
+ ctx.ui.notify(
150
+ [
151
+ initialSummary.hiddenInCurrentFiles > 0
152
+ ? `${initialSummary.hiddenInCurrentFiles} comments hidden in current files`
153
+ : undefined,
154
+ initialSummary.elsewhere > 0
155
+ ? `${initialSummary.elsewhere} comments elsewhere in the workspace`
156
+ : undefined,
157
+ ]
158
+ .filter(Boolean)
159
+ .join(" • "),
160
+ "info",
161
+ );
162
+ }
163
+ if (initialSummary.stale > 0 || initialSummary.orphaned > 0) {
164
+ ctx.ui.notify(
165
+ [
166
+ initialSummary.stale > 0
167
+ ? `${initialSummary.stale} stale comment${initialSummary.stale === 1 ? "" : "s"}`
168
+ : undefined,
169
+ initialSummary.orphaned > 0
170
+ ? `${initialSummary.orphaned} orphaned comment${initialSummary.orphaned === 1 ? "" : "s"}`
171
+ : undefined,
172
+ ]
173
+ .filter(Boolean)
174
+ .join(" • "),
175
+ "warning",
176
+ );
177
+ }
178
+
179
+ const result = await ctx.ui.custom<ReviewResult>(
180
+ (tui, theme, _keybindings, done) => {
181
+ return new ReviewComponent(
182
+ tui,
183
+ theme,
184
+ options.title,
185
+ options.reviewLines,
186
+ comments,
187
+ done,
188
+ new PiModelDiffExplainer(ctx),
189
+ (updatedComments) => {
190
+ persistCachedComments(pi, options.cacheKey, updatedComments.values());
191
+ workspaceStore.syncFromComments(
192
+ options.reviewLines,
193
+ updatedComments.values(),
191
194
  );
192
195
  },
196
+ explanations,
197
+ (updatedExplanations) => {
198
+ persistCachedExplanations(pi, options.cacheKey, updatedExplanations);
199
+ },
200
+ ask,
201
+ (updatedAsk) => {
202
+ persistCachedAsk(pi, options.cacheKey, updatedAsk);
203
+ },
204
+ () => summary(),
193
205
  );
206
+ },
207
+ );
194
208
 
195
- if (!result || result.action !== "submit") return;
196
- if (result.comments.length === 0) {
197
- ctx.ui.notify("No review comments to send.", "info");
198
- persistCachedComments(pi, cacheKey, []);
199
- return;
200
- }
209
+ if (!result || result.action !== "submit") return;
210
+ if (result.comments.length === 0) {
211
+ ctx.ui.notify("No review comments to send.", "info");
212
+ persistCachedComments(pi, options.cacheKey, []);
213
+ return;
214
+ }
201
215
 
202
- persistCachedComments(pi, cacheKey, []);
203
- pi.sendUserMessage(
204
- buildReviewPrompt(result.comments, source.promptLabel),
205
- );
206
- },
207
- });
216
+ persistCachedComments(pi, options.cacheKey, []);
217
+ pi.sendUserMessage(options.buildPrompt(result.comments));
208
218
  }