@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
  3. package/dist/cli.js +10795 -10791
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/config/custom-models.d.ts +31 -0
  6. package/dist/types/config/model-config-values.d.ts +19 -0
  7. package/dist/types/config/model-patch.d.ts +93 -0
  8. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  9. package/dist/types/config/model-registry.d.ts +5 -52
  10. package/dist/types/config/settings.d.ts +5 -3
  11. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  12. package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
  13. package/dist/types/discovery/contained-path.d.ts +2 -3
  14. package/dist/types/eval/executor-base.d.ts +8 -2
  15. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  16. package/dist/types/export/share.d.ts +1 -1
  17. package/dist/types/lsp/diagnostics.d.ts +96 -0
  18. package/dist/types/lsp/index.d.ts +7 -128
  19. package/dist/types/lsp/servers.d.ts +72 -0
  20. package/dist/types/lsp/tool.d.ts +42 -0
  21. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  22. package/dist/types/lsp/writethrough.d.ts +33 -0
  23. package/dist/types/mcp/transports/header-policy.d.ts +2 -1
  24. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  25. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  26. package/dist/types/modes/theme/color.d.ts +19 -0
  27. package/dist/types/modes/theme/loader.d.ts +19 -0
  28. package/dist/types/modes/theme/schema.d.ts +175 -0
  29. package/dist/types/modes/theme/symbols.d.ts +28 -0
  30. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  31. package/dist/types/modes/theme/theme.d.ts +9 -280
  32. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  33. package/dist/types/registry/agent-registry.d.ts +1 -3
  34. package/dist/types/secrets/index.d.ts +3 -1
  35. package/dist/types/secrets/message-transform.d.ts +40 -0
  36. package/dist/types/secrets/obfuscator.d.ts +0 -108
  37. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  38. package/dist/types/secrets/placeholder.d.ts +94 -0
  39. package/dist/types/secrets/replacement.d.ts +82 -0
  40. package/dist/types/session/agent-session-types.d.ts +6 -0
  41. package/dist/types/session/agent-session.d.ts +1 -1
  42. package/dist/types/session/session-handoff.d.ts +3 -3
  43. package/dist/types/session/session-manager.d.ts +32 -0
  44. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  45. package/dist/types/session/turn-recovery.d.ts +2 -2
  46. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  47. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  48. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  49. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  50. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  51. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  52. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  53. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  54. package/dist/types/tools/gh-common.d.ts +69 -0
  55. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  56. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  57. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  58. package/dist/types/tools/gh-search.d.ts +61 -0
  59. package/dist/types/tools/gh-types.d.ts +340 -0
  60. package/dist/types/tools/gh-view.d.ts +88 -0
  61. package/dist/types/tools/gh.d.ts +4 -201
  62. package/dist/types/tools/read-archive.d.ts +16 -0
  63. package/dist/types/tools/read-format.d.ts +104 -0
  64. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  65. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  66. package/dist/types/tools/read-renderer.d.ts +24 -0
  67. package/dist/types/tools/read-selector.d.ts +27 -0
  68. package/dist/types/tools/read-sqlite.d.ts +16 -0
  69. package/dist/types/tools/read-summary.d.ts +19 -0
  70. package/dist/types/tools/read.d.ts +1 -23
  71. package/dist/types/tools/shell-tokenize.d.ts +2 -1
  72. package/dist/types/utils/changelog.d.ts +0 -5
  73. package/package.json +13 -13
  74. package/src/advisor/runtime.ts +12 -7
  75. package/src/config/custom-models.ts +188 -0
  76. package/src/config/model-config-values.ts +128 -0
  77. package/src/config/model-patch.ts +252 -0
  78. package/src/config/model-provider-discovery.ts +132 -0
  79. package/src/config/model-registry.ts +64 -704
  80. package/src/config/settings.ts +7 -3
  81. package/src/debug/raw-sse-buffer.ts +20 -0
  82. package/src/discovery/agent-plugin-format.ts +7 -7
  83. package/src/discovery/contained-path.ts +2 -5
  84. package/src/edit/hashline/diff.ts +5 -1
  85. package/src/eval/executor-base.ts +39 -6
  86. package/src/eval/jl/executor.ts +82 -371
  87. package/src/eval/kernel-session-registry.ts +398 -0
  88. package/src/eval/py/executor.ts +53 -261
  89. package/src/eval/rb/executor.ts +36 -279
  90. package/src/export/share.ts +2 -1
  91. package/src/lsp/diagnostics.ts +516 -0
  92. package/src/lsp/index.ts +20 -2819
  93. package/src/lsp/servers.ts +296 -0
  94. package/src/lsp/tool.ts +1352 -0
  95. package/src/lsp/workspace-diagnostics.ts +170 -0
  96. package/src/lsp/writethrough.ts +561 -0
  97. package/src/mcp/transports/header-policy.ts +1 -1
  98. package/src/modes/components/agent-hub-projection.ts +2 -2
  99. package/src/modes/components/agent-hub-renderer.ts +3 -7
  100. package/src/modes/controllers/command-controller.ts +4 -1
  101. package/src/modes/theme/color.ts +133 -0
  102. package/src/modes/theme/loader.ts +178 -0
  103. package/src/modes/theme/schema.ts +263 -0
  104. package/src/modes/theme/symbols.ts +1000 -0
  105. package/src/modes/theme/theme-class.ts +611 -0
  106. package/src/modes/theme/theme.ts +27 -2453
  107. package/src/modes/theme/tui-adapters.ts +279 -0
  108. package/src/registry/agent-registry.ts +2 -2
  109. package/src/secrets/index.ts +6 -12
  110. package/src/secrets/message-transform.ts +287 -0
  111. package/src/secrets/obfuscator.ts +39 -1303
  112. package/src/secrets/placeholder-scan.ts +506 -0
  113. package/src/secrets/placeholder.ts +309 -0
  114. package/src/secrets/replacement.ts +216 -0
  115. package/src/session/agent-session-types.ts +6 -0
  116. package/src/session/agent-session.ts +113 -7
  117. package/src/session/indexed-session-storage.ts +7 -2
  118. package/src/session/session-advisors.ts +32 -34
  119. package/src/session/session-handoff.ts +39 -20
  120. package/src/session/session-manager.ts +67 -3
  121. package/src/session/session-provider-boundary.ts +3 -6
  122. package/src/session/turn-recovery.ts +21 -12
  123. package/src/slash-commands/builtin-collaboration.ts +504 -0
  124. package/src/slash-commands/builtin-completions.ts +291 -0
  125. package/src/slash-commands/builtin-control.ts +78 -0
  126. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  127. package/src/slash-commands/builtin-marketplace.ts +567 -0
  128. package/src/slash-commands/builtin-modes.ts +518 -0
  129. package/src/slash-commands/builtin-registry.ts +21 -3000
  130. package/src/slash-commands/builtin-session.ts +592 -0
  131. package/src/task/executor.ts +17 -14
  132. package/src/tools/gh-common.ts +288 -0
  133. package/src/tools/gh-pr-checkout.ts +679 -0
  134. package/src/tools/gh-pr-diff.ts +473 -0
  135. package/src/tools/gh-run-watch.ts +1015 -0
  136. package/src/tools/gh-search.ts +500 -0
  137. package/src/tools/gh-types.ts +379 -0
  138. package/src/tools/gh-view.ts +612 -0
  139. package/src/tools/gh.ts +109 -3821
  140. package/src/tools/read-archive.ts +209 -0
  141. package/src/tools/read-format.ts +593 -0
  142. package/src/tools/read-path-resolution.ts +36 -0
  143. package/src/tools/read-pdf-images.ts +250 -0
  144. package/src/tools/read-renderer.ts +289 -0
  145. package/src/tools/read-selector.ts +84 -0
  146. package/src/tools/read-sqlite.ts +215 -0
  147. package/src/tools/read-summary.ts +199 -0
  148. package/src/tools/read.ts +77 -1820
  149. package/src/tools/shell-tokenize.ts +1 -1
  150. package/src/utils/changelog.ts +1 -1
  151. package/src/utils/title-generator.ts +3 -1
  152. package/src/web/search/providers/zai.ts +12 -3
@@ -0,0 +1,612 @@
1
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import type { Settings } from "../config/settings";
3
+ import * as git from "../utils/git";
4
+ import type { ToolSession } from ".";
5
+ import type { GhToolDetails } from "./gh";
6
+ import {
7
+ appendRepoFlag,
8
+ buildTextResult,
9
+ formatAuthor,
10
+ formatLabels,
11
+ normalizeOptionalString,
12
+ normalizeText,
13
+ parseIssueUrl,
14
+ parsePositiveDecimalInt,
15
+ pushLine,
16
+ requireNonEmpty,
17
+ resolveDefaultRepoMemoized,
18
+ } from "./gh-common";
19
+ import { formatShortSha } from "./gh-format";
20
+ import { FILE_PREVIEW_LIMIT } from "./gh-search";
21
+ import type {
22
+ GhComment,
23
+ GhIssueViewData,
24
+ GhPrFile,
25
+ GhPrReview,
26
+ GhPrReviewComment,
27
+ GhPrReviewCommentApi,
28
+ GhPrViewData,
29
+ GhRepoViewData,
30
+ GithubInput,
31
+ } from "./gh-types";
32
+ import { type CacheStatus, getOrFetchView, resolveGithubCacheAuthKey } from "./github-cache";
33
+ import { ToolError } from "./tool-errors";
34
+
35
+ export const GH_REPO_FIELDS = [
36
+ "nameWithOwner",
37
+ "description",
38
+ "url",
39
+ "defaultBranchRef",
40
+ "homepageUrl",
41
+ "forkCount",
42
+ "isArchived",
43
+ "isFork",
44
+ "primaryLanguage",
45
+ "repositoryTopics",
46
+ "stargazerCount",
47
+ "updatedAt",
48
+ "viewerPermission",
49
+ "visibility",
50
+ ];
51
+ export const GH_ISSUE_FIELDS = [
52
+ "author",
53
+ "body",
54
+ "comments",
55
+ "createdAt",
56
+ "labels",
57
+ "number",
58
+ "state",
59
+ "stateReason",
60
+ "title",
61
+ "updatedAt",
62
+ "url",
63
+ ];
64
+ export const GH_ISSUE_FIELDS_NO_COMMENTS = [
65
+ "author",
66
+ "body",
67
+ "createdAt",
68
+ "labels",
69
+ "number",
70
+ "state",
71
+ "stateReason",
72
+ "title",
73
+ "updatedAt",
74
+ "url",
75
+ ];
76
+
77
+ export const GH_ISSUE_STATE_REASON_FIELD = "stateReason";
78
+
79
+ export function ghJsonErrorNamesField(err: unknown, field: string): boolean {
80
+ if (!(err instanceof Error) || !err.message.includes("Unknown JSON field")) return false;
81
+ return err.message.includes(`"${field}"`) || err.message.includes(`'${field}'`) || err.message.includes(field);
82
+ }
83
+
84
+ export function dropJsonField(args: readonly string[], field: string): string[] | undefined {
85
+ const next = [...args];
86
+ const jsonIndex = next.indexOf("--json");
87
+ if (jsonIndex < 0) return undefined;
88
+ const fields = next[jsonIndex + 1];
89
+ if (!fields) return undefined;
90
+ const splitFields = fields.split(",");
91
+ const kept = splitFields.filter(candidate => candidate !== field);
92
+ if (kept.length === splitFields.length) return undefined;
93
+ next[jsonIndex + 1] = kept.join(",");
94
+ return next;
95
+ }
96
+
97
+ /** Runs `gh --json` for issue data, retrying without optional stateReason on older gh releases. */
98
+ export async function githubIssueJsonWithStateReasonFallback<T>(
99
+ cwd: string,
100
+ args: readonly string[],
101
+ signal: AbortSignal | undefined,
102
+ options?: git.GhCommandOptions,
103
+ ): Promise<T> {
104
+ try {
105
+ return await git.github.json<T>(cwd, [...args], signal, options);
106
+ } catch (err) {
107
+ if (!ghJsonErrorNamesField(err, GH_ISSUE_STATE_REASON_FIELD)) throw err;
108
+ const retryArgs = dropJsonField(args, GH_ISSUE_STATE_REASON_FIELD);
109
+ if (!retryArgs) throw err;
110
+ return await git.github.json<T>(cwd, retryArgs, signal, options);
111
+ }
112
+ }
113
+
114
+ export const GH_PR_FIELDS = [
115
+ "author",
116
+ "baseRefName",
117
+ "body",
118
+ "comments",
119
+ "createdAt",
120
+ "files",
121
+ "headRefName",
122
+ "isDraft",
123
+ "labels",
124
+ "mergeStateStatus",
125
+ "number",
126
+ "reviews",
127
+ "reviewDecision",
128
+ "state",
129
+ "title",
130
+ "updatedAt",
131
+ "url",
132
+ ];
133
+ export const GH_PR_FIELDS_NO_COMMENTS = [
134
+ "author",
135
+ "baseRefName",
136
+ "body",
137
+ "createdAt",
138
+ "files",
139
+ "headRefName",
140
+ "isDraft",
141
+ "labels",
142
+ "mergeStateStatus",
143
+ "number",
144
+ "reviews",
145
+ "reviewDecision",
146
+ "state",
147
+ "title",
148
+ "updatedAt",
149
+ "url",
150
+ ];
151
+ export const GH_REPO_CLONE_FIELDS = ["nameWithOwner", "sshUrl", "url"];
152
+
153
+ export const REVIEW_COMMENTS_PAGE_SIZE = 100;
154
+
155
+ export function normalizePrReviewComment(comment: GhPrReviewCommentApi): GhPrReviewComment | null {
156
+ if (typeof comment.id !== "number") {
157
+ return null;
158
+ }
159
+
160
+ return {
161
+ author: comment.user ?? null,
162
+ body: comment.body,
163
+ createdAt: normalizeOptionalString(comment.created_at),
164
+ id: comment.id,
165
+ inReplyToId: typeof comment.in_reply_to_id === "number" ? comment.in_reply_to_id : undefined,
166
+ line: typeof comment.line === "number" ? comment.line : undefined,
167
+ originalLine: typeof comment.original_line === "number" ? comment.original_line : undefined,
168
+ path: normalizeOptionalString(comment.path),
169
+ side: normalizeOptionalString(comment.side),
170
+ url: normalizeOptionalString(comment.html_url),
171
+ };
172
+ }
173
+
174
+ export async function fetchPrReviewComments(
175
+ cwd: string,
176
+ repo: string,
177
+ prNumber: number,
178
+ signal?: AbortSignal,
179
+ ): Promise<GhPrReviewComment[]> {
180
+ const reviewComments: GhPrReviewComment[] = [];
181
+ let page = 1;
182
+
183
+ while (true) {
184
+ const response = await git.github.json<GhPrReviewCommentApi[]>(
185
+ cwd,
186
+ [
187
+ "api",
188
+ "--method",
189
+ "GET",
190
+ `/repos/${repo}/pulls/${prNumber}/comments`,
191
+ "-F",
192
+ `per_page=${REVIEW_COMMENTS_PAGE_SIZE}`,
193
+ "-F",
194
+ `page=${page}`,
195
+ ],
196
+ signal,
197
+ { repoProvided: true },
198
+ );
199
+
200
+ const pageComments = response
201
+ .map(comment => normalizePrReviewComment(comment))
202
+ .filter((comment): comment is GhPrReviewComment => comment !== null);
203
+ reviewComments.push(...pageComments);
204
+
205
+ // Compare the raw page length: a dropped malformed item must not end
206
+ // pagination early and silently lose the remaining pages.
207
+ if (response.length < REVIEW_COMMENTS_PAGE_SIZE) {
208
+ break;
209
+ }
210
+
211
+ page += 1;
212
+ }
213
+
214
+ return reviewComments;
215
+ }
216
+
217
+ export function formatCommentsSection(comments: GhComment[] | undefined): string[] {
218
+ if (!comments || comments.length === 0) {
219
+ return [];
220
+ }
221
+
222
+ const visible = comments.filter(comment => !comment.isMinimized);
223
+ const hiddenCount = comments.length - visible.length;
224
+ const lines: string[] = ["## Comments", ""];
225
+
226
+ if (visible.length === 0) {
227
+ lines.push(`No visible comments. Minimized comments omitted: ${hiddenCount}.`);
228
+ return lines;
229
+ }
230
+
231
+ lines[0] = `## Comments (${visible.length})`;
232
+
233
+ for (const comment of visible) {
234
+ const author = formatAuthor(comment.author) ?? "unknown";
235
+ const createdAt = comment.createdAt ? ` · ${comment.createdAt}` : "";
236
+ lines.push(`### ${author}${createdAt}`);
237
+ lines.push("");
238
+ lines.push(normalizeText(comment.body) || "No comment body.");
239
+ if (comment.url) {
240
+ lines.push("");
241
+ lines.push(`URL: ${comment.url}`);
242
+ }
243
+ lines.push("");
244
+ }
245
+
246
+ if (hiddenCount > 0) {
247
+ lines.push(`Minimized comments omitted: ${hiddenCount}.`);
248
+ }
249
+
250
+ return lines;
251
+ }
252
+
253
+ export function formatReviewsSection(reviews: GhPrReview[] | undefined): string[] {
254
+ if (!reviews || reviews.length === 0) {
255
+ return [];
256
+ }
257
+
258
+ const lines: string[] = [`## Reviews (${reviews.length})`, ""];
259
+ for (const review of reviews) {
260
+ const author = formatAuthor(review.author) ?? "unknown";
261
+ const submittedAt = review.submittedAt ? ` - ${review.submittedAt}` : "";
262
+ const state = review.state ? ` [${review.state}]` : "";
263
+ lines.push(`### ${author}${submittedAt}${state}`);
264
+ if (review.commit?.oid) {
265
+ lines.push("");
266
+ lines.push(`Commit: ${formatShortSha(review.commit.oid)}`);
267
+ }
268
+ lines.push("");
269
+ lines.push(normalizeText(review.body) || "No review body.");
270
+ lines.push("");
271
+ }
272
+
273
+ return lines;
274
+ }
275
+
276
+ export function formatReviewCommentLocation(comment: GhPrReviewComment): string | undefined {
277
+ if (!comment.path) {
278
+ return undefined;
279
+ }
280
+
281
+ const line = comment.line ?? comment.originalLine;
282
+ return line === undefined ? comment.path : `${comment.path}:${line}`;
283
+ }
284
+
285
+ export function formatReviewCommentsSection(comments: GhPrReviewComment[] | undefined): string[] {
286
+ if (!comments || comments.length === 0) {
287
+ return [];
288
+ }
289
+
290
+ const lines: string[] = [`## Review Comments (${comments.length})`, ""];
291
+ for (const comment of comments) {
292
+ const author = formatAuthor(comment.author) ?? "unknown";
293
+ const createdAt = comment.createdAt ? ` · ${comment.createdAt}` : "";
294
+ lines.push(`### ${author}${createdAt}`);
295
+ lines.push("");
296
+ pushLine(lines, "Location", formatReviewCommentLocation(comment));
297
+ pushLine(lines, "Side", comment.side);
298
+ pushLine(lines, "Reply to", comment.inReplyToId);
299
+ pushLine(lines, "URL", comment.url);
300
+ lines.push("");
301
+ lines.push(normalizeText(comment.body) || "No review comment body.");
302
+ lines.push("");
303
+ }
304
+
305
+ return lines;
306
+ }
307
+
308
+ export function formatRepoView(data: GhRepoViewData, input: { repo?: string; branch?: string }): string {
309
+ const lines: string[] = [];
310
+ const name = data.nameWithOwner ?? input.repo ?? "GitHub Repository";
311
+ lines.push(`# ${name}`);
312
+ lines.push("");
313
+ lines.push(normalizeText(data.description) || "No description provided.");
314
+ lines.push("");
315
+ pushLine(lines, "URL", data.url);
316
+ pushLine(lines, "Default branch", data.defaultBranchRef?.name);
317
+ pushLine(lines, "Branch", normalizeOptionalString(input.branch));
318
+ pushLine(lines, "Visibility", data.visibility ?? undefined);
319
+ pushLine(lines, "Viewer permission", data.viewerPermission ?? undefined);
320
+ pushLine(lines, "Primary language", data.primaryLanguage?.name);
321
+ pushLine(lines, "Stars", data.stargazerCount);
322
+ pushLine(lines, "Forks", data.forkCount);
323
+ pushLine(lines, "Archived", data.isArchived);
324
+ pushLine(lines, "Fork", data.isFork);
325
+ pushLine(lines, "Updated", data.updatedAt);
326
+ pushLine(lines, "Homepage", data.homepageUrl ?? undefined);
327
+ const topics = data.repositoryTopics
328
+ ?.map(topic => topic.name ?? topic.topic?.name)
329
+ .filter((value): value is string => Boolean(value))
330
+ .join(", ");
331
+ pushLine(lines, "Topics", topics || undefined);
332
+ return lines.join("\n").trim();
333
+ }
334
+
335
+ export function formatIssueView(
336
+ data: GhIssueViewData,
337
+ input: { issue: string; repo?: string; comments?: boolean },
338
+ ): string {
339
+ const lines: string[] = [];
340
+ const issueNumber = data.number ?? input.issue;
341
+ lines.push(`# Issue #${issueNumber}: ${data.title ?? "Untitled"}`);
342
+ lines.push("");
343
+ pushLine(lines, "State", data.state);
344
+ pushLine(lines, "State reason", data.stateReason ?? undefined);
345
+ pushLine(lines, "Author", formatAuthor(data.author));
346
+ pushLine(lines, "Created", data.createdAt);
347
+ pushLine(lines, "Updated", data.updatedAt);
348
+ pushLine(lines, "Labels", formatLabels(data.labels));
349
+ pushLine(lines, "URL", data.url);
350
+ lines.push("");
351
+ lines.push("## Body");
352
+ lines.push("");
353
+ lines.push(normalizeText(data.body) || "No description provided.");
354
+
355
+ if ((input.comments ?? true) && data.comments) {
356
+ const commentSection = formatCommentsSection(data.comments);
357
+ if (commentSection.length > 0) {
358
+ lines.push("");
359
+ lines.push(...commentSection);
360
+ }
361
+ }
362
+
363
+ return lines.join("\n").trim();
364
+ }
365
+
366
+ export function formatPrFiles(files: GhPrFile[] | undefined): string[] {
367
+ if (!files || files.length === 0) return [];
368
+
369
+ const lines: string[] = [`## Files (${files.length})`, ""];
370
+ for (const file of files.slice(0, FILE_PREVIEW_LIMIT)) {
371
+ const changeType = file.changeType ?? "CHANGED";
372
+ const additions = file.additions ?? 0;
373
+ const deletions = file.deletions ?? 0;
374
+ lines.push(`- ${file.path ?? "(unknown file)"} [${changeType}] (+${additions} -${deletions})`);
375
+ }
376
+
377
+ if (files.length > FILE_PREVIEW_LIMIT) {
378
+ lines.push(`[…${files.length - FILE_PREVIEW_LIMIT} files elided…]`);
379
+ }
380
+
381
+ return lines;
382
+ }
383
+
384
+ export function formatPrView(data: GhPrViewData, input: { pr?: string; repo?: string; comments?: boolean }): string {
385
+ const lines: string[] = [];
386
+ const prIdentifier = data.number ?? input.pr ?? "current";
387
+ lines.push(`# Pull Request #${prIdentifier}: ${data.title ?? "Untitled"}`);
388
+ lines.push("");
389
+ pushLine(lines, "State", data.state);
390
+ pushLine(lines, "Draft", data.isDraft);
391
+ pushLine(lines, "Author", formatAuthor(data.author));
392
+ pushLine(lines, "Base", data.baseRefName);
393
+ pushLine(lines, "Head", data.headRefName);
394
+ pushLine(lines, "Review decision", data.reviewDecision ?? undefined);
395
+ pushLine(lines, "Merge state", data.mergeStateStatus);
396
+ pushLine(lines, "Created", data.createdAt);
397
+ pushLine(lines, "Updated", data.updatedAt);
398
+ pushLine(lines, "Labels", formatLabels(data.labels));
399
+ pushLine(lines, "URL", data.url);
400
+ lines.push("");
401
+ lines.push("## Body");
402
+ lines.push("");
403
+ lines.push(normalizeText(data.body) || "No description provided.");
404
+
405
+ const fileSection = formatPrFiles(data.files);
406
+ if (fileSection.length > 0) {
407
+ lines.push("");
408
+ lines.push(...fileSection);
409
+ }
410
+
411
+ if ((input.comments ?? true) && data.reviews) {
412
+ const reviewSection = formatReviewsSection(data.reviews);
413
+ if (reviewSection.length > 0) {
414
+ lines.push("");
415
+ lines.push(...reviewSection);
416
+ }
417
+ }
418
+
419
+ if ((input.comments ?? true) && data.reviewComments) {
420
+ const reviewCommentsSection = formatReviewCommentsSection(data.reviewComments);
421
+ if (reviewCommentsSection.length > 0) {
422
+ lines.push("");
423
+ lines.push(...reviewCommentsSection);
424
+ }
425
+ }
426
+
427
+ if ((input.comments ?? true) && data.comments) {
428
+ const commentSection = formatCommentsSection(data.comments);
429
+ if (commentSection.length > 0) {
430
+ lines.push("");
431
+ lines.push(...commentSection);
432
+ }
433
+ }
434
+
435
+ return lines.join("\n").trim();
436
+ }
437
+
438
+ export async function executeRepoView(
439
+ session: ToolSession,
440
+ params: GithubInput,
441
+ signal: AbortSignal | undefined,
442
+ ): Promise<AgentToolResult<GhToolDetails>> {
443
+ const repo = normalizeOptionalString(params.repo);
444
+ const branch = normalizeOptionalString(params.branch);
445
+ const args = ["repo", "view"];
446
+ if (repo) {
447
+ args.push(repo);
448
+ }
449
+ if (branch) {
450
+ args.push("--branch", branch);
451
+ }
452
+ args.push("--json", GH_REPO_FIELDS.join(","));
453
+
454
+ const data = await git.github.json<GhRepoViewData>(session.cwd, args, signal, {
455
+ repoProvided: Boolean(repo),
456
+ });
457
+ return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
458
+ }
459
+
460
+ // ────────────────────────────────────────────────────────────────────────────
461
+ // Cached issue/PR view fetchers
462
+ //
463
+ // Used by `executeIssueView`/`executePrView` and by the `issue://` / `pr://`
464
+ // internal-URL protocol handlers. The cache wrapper lives in `./github-cache`;
465
+ // the fresh fetchers stay here to share the existing formatter helpers.
466
+ // ────────────────────────────────────────────────────────────────────────────
467
+
468
+ export interface IssueViewLookupOptions {
469
+ cwd: string;
470
+ repo?: string;
471
+ /** Issue number or GitHub issue URL. */
472
+ issue: string;
473
+ includeComments?: boolean;
474
+ signal?: AbortSignal;
475
+ settings?: Settings;
476
+ cacheAuthKey?: string | null;
477
+ }
478
+
479
+ export interface PrViewLookupOptions {
480
+ cwd: string;
481
+ repo: string;
482
+ number: number;
483
+ includeComments?: boolean;
484
+ signal?: AbortSignal;
485
+ settings?: Settings;
486
+ cacheAuthKey?: string | null;
487
+ }
488
+
489
+ export interface ViewLookupResult<T> {
490
+ rendered: string;
491
+ sourceUrl: string | undefined;
492
+ payload: T;
493
+ status: CacheStatus;
494
+ fetchedAt: number;
495
+ }
496
+
497
+ export async function fetchIssueViewFresh(
498
+ cwd: string,
499
+ repo: string | undefined,
500
+ identifier: string,
501
+ includeComments: boolean,
502
+ signal: AbortSignal | undefined,
503
+ ): Promise<{ rendered: string; sourceUrl: string | undefined; payload: GhIssueViewData }> {
504
+ const args = ["issue", "view", identifier];
505
+ appendRepoFlag(args, repo, identifier);
506
+ args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
507
+ const data = await githubIssueJsonWithStateReasonFallback<GhIssueViewData>(cwd, args, signal, {
508
+ repoProvided: Boolean(repo),
509
+ });
510
+ const rendered = formatIssueView(data, { issue: identifier, repo, comments: includeComments });
511
+ return { rendered, sourceUrl: data.url, payload: data };
512
+ }
513
+
514
+ export async function fetchPrViewFresh(
515
+ cwd: string,
516
+ repo: string,
517
+ number: number,
518
+ includeComments: boolean,
519
+ signal: AbortSignal | undefined,
520
+ ): Promise<{ rendered: string; sourceUrl: string | undefined; payload: GhPrViewData }> {
521
+ const args = ["pr", "view", String(number)];
522
+ appendRepoFlag(args, repo, String(number));
523
+ args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
524
+ const data = await git.github.json<GhPrViewData>(cwd, args, signal, { repoProvided: true });
525
+ if (includeComments && typeof data.number === "number") {
526
+ data.reviewComments = await fetchPrReviewComments(cwd, repo, data.number, signal);
527
+ }
528
+ const rendered = formatPrView(data, { pr: String(number), repo, comments: includeComments });
529
+ return { rendered, sourceUrl: data.url, payload: data };
530
+ }
531
+
532
+ /**
533
+ * Cache-aware issue/view fetcher. Used by both the `github` tool op and the
534
+ * `issue://` protocol handler so a single shared row services both surfaces.
535
+ */
536
+ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<ViewLookupResult<GhIssueViewData>> {
537
+ const identifier = requireNonEmpty(options.issue, "issue");
538
+ if (identifier.startsWith("-")) {
539
+ throw new ToolError(`invalid issue identifier: ${identifier}. Pass an issue number or URL.`);
540
+ }
541
+ const includeComments = options.includeComments ?? true;
542
+ const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
543
+ const urlParse = parseIssueUrl(identifier);
544
+ // Prefer the URL's repo when the identifier is a full URL; fall back to the
545
+ // explicit `repo` option, then to the cwd's default repo.
546
+ let repo = urlParse.repo ?? normalizeOptionalString(options.repo);
547
+ let cacheNumber = urlParse.issueNumber;
548
+ if (cacheNumber === undefined) {
549
+ cacheNumber = parsePositiveDecimalInt(identifier);
550
+ }
551
+ if (cacheNumber !== undefined && !repo) {
552
+ try {
553
+ repo = await resolveDefaultRepoMemoized(options.cwd, options.signal);
554
+ } catch {
555
+ // Resolution failure leaves `repo` undefined: we'll fall through to a
556
+ // direct fetch below so gh produces its own error message instead of
557
+ // us masking it with a friendlier one.
558
+ repo = undefined;
559
+ }
560
+ }
561
+
562
+ const doFetch = () => fetchIssueViewFresh(options.cwd, repo, identifier, includeComments, options.signal);
563
+
564
+ if (!repo || cacheNumber === undefined) {
565
+ const fresh = await doFetch();
566
+ return { ...fresh, status: "miss", fetchedAt: Date.now() };
567
+ }
568
+
569
+ const lookup = await getOrFetchView<GhIssueViewData>({
570
+ repo,
571
+ kind: "issue",
572
+ number: cacheNumber,
573
+ includeComments,
574
+ settings: options.settings,
575
+ authKey,
576
+ fetchFresh: doFetch,
577
+ });
578
+ return {
579
+ rendered: lookup.rendered,
580
+ sourceUrl: lookup.sourceUrl,
581
+ payload: lookup.payload,
582
+ status: lookup.status,
583
+ fetchedAt: lookup.fetchedAt,
584
+ };
585
+ }
586
+
587
+ /**
588
+ * Cache-aware PR view fetcher. Caller must supply a numeric PR number;
589
+ * branch-name / current-branch lookups bypass the cache entirely upstream
590
+ * (see `executePrView`).
591
+ */
592
+ export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLookupResult<GhPrViewData>> {
593
+ const includeComments = options.includeComments ?? true;
594
+ const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
595
+ const doFetch = () => fetchPrViewFresh(options.cwd, options.repo, options.number, includeComments, options.signal);
596
+ const lookup = await getOrFetchView<GhPrViewData>({
597
+ repo: options.repo,
598
+ kind: "pr",
599
+ number: options.number,
600
+ includeComments,
601
+ settings: options.settings,
602
+ authKey,
603
+ fetchFresh: doFetch,
604
+ });
605
+ return {
606
+ rendered: lookup.rendered,
607
+ sourceUrl: lookup.sourceUrl,
608
+ payload: lookup.payload,
609
+ status: lookup.status,
610
+ fetchedAt: lookup.fetchedAt,
611
+ };
612
+ }