@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.0
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/CHANGELOG.md +82 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +1 -1
- package/src/cli/args.ts +2 -2
- package/src/cli.ts +1 -0
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/config/model-resolver.ts +30 -0
- package/src/config/settings-schema.ts +31 -0
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +10 -0
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/shared/rewrite-imports.ts +120 -48
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/extensibility/extensions/runner.ts +54 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/skills.ts +33 -1
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +13 -9
- package/src/modes/acp/acp-agent.ts +361 -54
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/controllers/command-controller.ts +14 -6
- package/src/modes/controllers/event-controller.ts +24 -11
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +72 -39
- package/src/modes/interactive-mode.ts +71 -7
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/ui-helpers.ts +15 -3
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +22 -21
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/exit-plan-mode.md +2 -2
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/hashline.md +6 -6
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +7 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +69 -12
- package/src/session/agent-session.ts +231 -22
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +699 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/task/executor.ts +10 -3
- package/src/task/index.ts +17 -1
- package/src/task/render.ts +6 -3
- package/src/tools/bash.ts +176 -2
- package/src/tools/conflict-detect.ts +6 -6
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +19 -1
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +682 -176
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/read.ts +110 -27
- package/src/tools/write.ts +23 -1
- package/src/tui/code-cell.ts +70 -2
package/src/tools/gh.ts
CHANGED
|
@@ -5,10 +5,12 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
5
5
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { abortableSleep, getWorktreesDir, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
+
import type { Settings } from "../config/settings";
|
|
8
9
|
import githubDescription from "../prompts/tools/github.md" with { type: "text" };
|
|
9
10
|
import * as git from "../utils/git";
|
|
10
11
|
import type { ToolSession } from ".";
|
|
11
12
|
import { formatShortSha } from "./gh-format";
|
|
13
|
+
import { type CacheStatus, getOrFetchView, resolveGithubCacheAuthKey } from "./github-cache";
|
|
12
14
|
import type { OutputMeta } from "./output-meta";
|
|
13
15
|
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
14
16
|
import { toolResult } from "./tool-result";
|
|
@@ -103,35 +105,81 @@ const GH_PR_CHECKOUT_FIELDS = [
|
|
|
103
105
|
"title",
|
|
104
106
|
"url",
|
|
105
107
|
];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
108
|
+
// /search/<endpoint> API response shapes (subset). Used when projecting raw
|
|
109
|
+
// REST results into the normalized `GhSearch*Result` shapes the formatters
|
|
110
|
+
// consume. We talk to the API directly because `gh search prs`/`issues`
|
|
111
|
+
// quotes multi-token positional queries (`is:"merged is:pr"`) and returns 0
|
|
112
|
+
// hits — see https://github.com/cli/cli for the upstream regression.
|
|
113
|
+
interface GhApiSearchResponse<T> {
|
|
114
|
+
total_count?: number;
|
|
115
|
+
incomplete_results?: boolean;
|
|
116
|
+
items?: T[];
|
|
117
|
+
}
|
|
118
|
+
interface GhApiUser {
|
|
119
|
+
login?: string;
|
|
120
|
+
name?: string | null;
|
|
121
|
+
}
|
|
122
|
+
interface GhApiLabel {
|
|
123
|
+
name?: string;
|
|
124
|
+
}
|
|
125
|
+
interface GhApiPullRequestRef {
|
|
126
|
+
merged_at?: string | null;
|
|
127
|
+
}
|
|
128
|
+
interface GhApiSearchIssueItem {
|
|
129
|
+
number?: number;
|
|
130
|
+
title?: string;
|
|
131
|
+
state?: string;
|
|
132
|
+
state_reason?: string | null;
|
|
133
|
+
user?: GhApiUser | null;
|
|
134
|
+
labels?: GhApiLabel[];
|
|
135
|
+
created_at?: string;
|
|
136
|
+
updated_at?: string;
|
|
137
|
+
html_url?: string;
|
|
138
|
+
repository_url?: string;
|
|
139
|
+
pull_request?: GhApiPullRequestRef | null;
|
|
140
|
+
}
|
|
141
|
+
interface GhApiSearchCodeItem {
|
|
142
|
+
name?: string;
|
|
143
|
+
path?: string;
|
|
144
|
+
sha?: string;
|
|
145
|
+
html_url?: string;
|
|
146
|
+
repository?: { full_name?: string } | null;
|
|
147
|
+
text_matches?: Array<{ fragment?: string; property?: string }>;
|
|
148
|
+
}
|
|
149
|
+
interface GhApiSearchCommitGitActor {
|
|
150
|
+
name?: string;
|
|
151
|
+
email?: string;
|
|
152
|
+
date?: string;
|
|
153
|
+
}
|
|
154
|
+
interface GhApiSearchCommitItem {
|
|
155
|
+
sha?: string;
|
|
156
|
+
node_id?: string;
|
|
157
|
+
html_url?: string;
|
|
158
|
+
author?: GhApiUser | null;
|
|
159
|
+
committer?: GhApiUser | null;
|
|
160
|
+
commit?: {
|
|
161
|
+
author?: GhApiSearchCommitGitActor | null;
|
|
162
|
+
committer?: GhApiSearchCommitGitActor | null;
|
|
163
|
+
message?: string;
|
|
164
|
+
} | null;
|
|
165
|
+
repository?: { full_name?: string } | null;
|
|
166
|
+
}
|
|
167
|
+
interface GhApiSearchRepoItem {
|
|
168
|
+
full_name?: string;
|
|
169
|
+
description?: string | null;
|
|
170
|
+
language?: string | null;
|
|
171
|
+
stargazers_count?: number;
|
|
172
|
+
forks_count?: number;
|
|
173
|
+
open_issues_count?: number;
|
|
174
|
+
archived?: boolean;
|
|
175
|
+
fork?: boolean;
|
|
176
|
+
private?: boolean;
|
|
177
|
+
visibility?: string | null;
|
|
178
|
+
updated_at?: string;
|
|
179
|
+
created_at?: string;
|
|
180
|
+
html_url?: string;
|
|
181
|
+
owner?: GhApiUser | null;
|
|
182
|
+
}
|
|
135
183
|
const SEARCH_LIMIT_DEFAULT = 10;
|
|
136
184
|
const SEARCH_LIMIT_MAX = 50;
|
|
137
185
|
const FILE_PREVIEW_LIMIT = 50;
|
|
@@ -142,6 +190,7 @@ const RUN_WATCH_TAIL_MAX = 200;
|
|
|
142
190
|
const REVIEW_COMMENTS_PAGE_SIZE = 100;
|
|
143
191
|
const RUN_JOBS_PAGE_SIZE = 100;
|
|
144
192
|
const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/.*)?$/;
|
|
193
|
+
const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)(?:\/.*)?$/;
|
|
145
194
|
const RUN_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/actions\/runs\/(\d+)(?:\/.*)?$/;
|
|
146
195
|
const RUN_SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
147
196
|
const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required", "startup_failure"]);
|
|
@@ -151,10 +200,7 @@ const githubSchema = Type.Object({
|
|
|
151
200
|
op: StringEnum(
|
|
152
201
|
[
|
|
153
202
|
"repo_view",
|
|
154
|
-
"issue_view",
|
|
155
203
|
"pr_create",
|
|
156
|
-
"pr_view",
|
|
157
|
-
"pr_diff",
|
|
158
204
|
"pr_checkout",
|
|
159
205
|
"pr_push",
|
|
160
206
|
"search_issues",
|
|
@@ -178,12 +224,6 @@ const githubSchema = Type.Object({
|
|
|
178
224
|
examples: ["main", "develop"],
|
|
179
225
|
}),
|
|
180
226
|
),
|
|
181
|
-
issue: Type.Optional(
|
|
182
|
-
Type.String({
|
|
183
|
-
description: "issue number or url (issue_view)",
|
|
184
|
-
examples: ["123", "https://github.com/owner/repo/issues/123"],
|
|
185
|
-
}),
|
|
186
|
-
),
|
|
187
227
|
pr: Type.Optional(
|
|
188
228
|
Type.Union(
|
|
189
229
|
[
|
|
@@ -194,17 +234,10 @@ const githubSchema = Type.Object({
|
|
|
194
234
|
],
|
|
195
235
|
{
|
|
196
236
|
description:
|
|
197
|
-
"pr number, url, or branch (
|
|
237
|
+
"pr number, url, or branch (pr_checkout); pass an array to batch-process multiple pull requests in one call",
|
|
198
238
|
},
|
|
199
239
|
),
|
|
200
240
|
),
|
|
201
|
-
comments: Type.Optional(Type.Boolean({ description: "include comments (issue_view, pr_view)", default: true })),
|
|
202
|
-
nameOnly: Type.Optional(Type.Boolean({ description: "return file names only (pr_diff)" })),
|
|
203
|
-
exclude: Type.Optional(
|
|
204
|
-
Type.Array(Type.String({ description: "glob to exclude" }), {
|
|
205
|
-
description: "file globs to exclude (pr_diff)",
|
|
206
|
-
}),
|
|
207
|
-
),
|
|
208
241
|
force: Type.Optional(Type.Boolean({ description: "reset existing local branch (pr_checkout)" })),
|
|
209
242
|
forceWithLease: Type.Optional(Type.Boolean({ description: "force-with-lease push (pr_push)" })),
|
|
210
243
|
title: Type.Optional(
|
|
@@ -699,13 +732,7 @@ function appendRepoFlag(args: string[], repo: string | undefined, identifier?: s
|
|
|
699
732
|
args.push("--repo", repo);
|
|
700
733
|
}
|
|
701
734
|
|
|
702
|
-
const
|
|
703
|
-
issues: GH_SEARCH_FIELDS,
|
|
704
|
-
prs: GH_SEARCH_FIELDS,
|
|
705
|
-
code: GH_SEARCH_CODE_FIELDS,
|
|
706
|
-
commits: GH_SEARCH_COMMITS_FIELDS,
|
|
707
|
-
repos: GH_SEARCH_REPOS_FIELDS,
|
|
708
|
-
};
|
|
735
|
+
const REPO_API_URL_PREFIX = "https://api.github.com/repos/";
|
|
709
736
|
|
|
710
737
|
const RELATIVE_DURATION_PATTERN = /^(\d+)\s*(m|h|d|w|mo|y)$/i;
|
|
711
738
|
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -811,21 +838,98 @@ function composeSearchQuery(parts: ReadonlyArray<string | undefined>): string {
|
|
|
811
838
|
return cleaned.join(" ");
|
|
812
839
|
}
|
|
813
840
|
|
|
814
|
-
function
|
|
815
|
-
|
|
841
|
+
function buildGhApiSearchArgs(
|
|
842
|
+
endpoint: "issues" | "code" | "commits" | "repositories",
|
|
816
843
|
query: string,
|
|
817
844
|
limit: number,
|
|
818
|
-
|
|
845
|
+
extraHeaders?: ReadonlyArray<string>,
|
|
819
846
|
): string[] {
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
appendRepoFlag(args, repo);
|
|
847
|
+
const args = ["api", "-X", "GET", `/search/${endpoint}`, "-f", `q=${query}`, "-F", `per_page=${limit}`];
|
|
848
|
+
for (const header of extraHeaders ?? []) {
|
|
849
|
+
args.push("-H", header);
|
|
824
850
|
}
|
|
825
|
-
args.push("--", query);
|
|
826
851
|
return args;
|
|
827
852
|
}
|
|
828
853
|
|
|
854
|
+
function repoFromRepositoryUrl(value: string | undefined): string | undefined {
|
|
855
|
+
if (!value?.startsWith(REPO_API_URL_PREFIX)) return undefined;
|
|
856
|
+
return value.slice(REPO_API_URL_PREFIX.length);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function apiUserToGhUser(user: GhApiUser | null | undefined): GhUser | undefined {
|
|
860
|
+
if (!user) return undefined;
|
|
861
|
+
const login = user.login ?? undefined;
|
|
862
|
+
const name = user.name ?? undefined;
|
|
863
|
+
if (login === undefined && name === undefined) return undefined;
|
|
864
|
+
return { login, name };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function apiLabelsToGhLabels(labels: GhApiLabel[] | undefined): GhLabel[] {
|
|
868
|
+
return labels?.map(label => ({ name: label.name })) ?? [];
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function apiIssueToSearchResult(item: GhApiSearchIssueItem): GhSearchResult {
|
|
872
|
+
const merged = Boolean(item.pull_request?.merged_at);
|
|
873
|
+
return {
|
|
874
|
+
author: apiUserToGhUser(item.user) ?? null,
|
|
875
|
+
createdAt: item.created_at,
|
|
876
|
+
labels: apiLabelsToGhLabels(item.labels),
|
|
877
|
+
number: item.number,
|
|
878
|
+
repository: { nameWithOwner: repoFromRepositoryUrl(item.repository_url) },
|
|
879
|
+
state: merged ? "merged" : item.state,
|
|
880
|
+
title: item.title,
|
|
881
|
+
updatedAt: item.updated_at,
|
|
882
|
+
url: item.html_url,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function apiCodeToSearchResult(item: GhApiSearchCodeItem): GhSearchCodeResult {
|
|
887
|
+
return {
|
|
888
|
+
path: item.path,
|
|
889
|
+
repository: { nameWithOwner: item.repository?.full_name },
|
|
890
|
+
sha: item.sha,
|
|
891
|
+
textMatches: item.text_matches?.map(match => ({ fragment: match.fragment, property: match.property })),
|
|
892
|
+
url: item.html_url,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function apiCommitToSearchResult(item: GhApiSearchCommitItem): GhSearchCommitResult {
|
|
897
|
+
return {
|
|
898
|
+
author: apiUserToGhUser(item.author) ?? null,
|
|
899
|
+
commit: item.commit
|
|
900
|
+
? {
|
|
901
|
+
author: item.commit.author ?? null,
|
|
902
|
+
committer: item.commit.committer ?? null,
|
|
903
|
+
message: item.commit.message,
|
|
904
|
+
}
|
|
905
|
+
: null,
|
|
906
|
+
committer: apiUserToGhUser(item.committer) ?? null,
|
|
907
|
+
id: item.node_id,
|
|
908
|
+
repository: { nameWithOwner: item.repository?.full_name },
|
|
909
|
+
sha: item.sha,
|
|
910
|
+
url: item.html_url,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function apiRepoToSearchResult(item: GhApiSearchRepoItem): GhSearchRepoResult {
|
|
915
|
+
return {
|
|
916
|
+
createdAt: item.created_at,
|
|
917
|
+
description: item.description,
|
|
918
|
+
forksCount: item.forks_count,
|
|
919
|
+
fullName: item.full_name,
|
|
920
|
+
isArchived: item.archived,
|
|
921
|
+
isFork: item.fork,
|
|
922
|
+
isPrivate: item.private,
|
|
923
|
+
language: item.language,
|
|
924
|
+
openIssuesCount: item.open_issues_count,
|
|
925
|
+
owner: apiUserToGhUser(item.owner) ?? null,
|
|
926
|
+
stargazersCount: item.stargazers_count,
|
|
927
|
+
updatedAt: item.updated_at,
|
|
928
|
+
url: item.html_url,
|
|
929
|
+
visibility: item.visibility ?? null,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
829
933
|
function sanitizeRemoteName(value: string): string {
|
|
830
934
|
const sanitized = value
|
|
831
935
|
.toLowerCase()
|
|
@@ -1086,6 +1190,29 @@ function parsePullRequestUrl(value: string | undefined): { repo?: string; prNumb
|
|
|
1086
1190
|
};
|
|
1087
1191
|
}
|
|
1088
1192
|
|
|
1193
|
+
/**
|
|
1194
|
+
* Parse a digit-only decimal positive integer or return undefined. Rejects
|
|
1195
|
+
* `1e2`, `0x10`, `12.0`, leading +/-, or any other shape `Number()` would
|
|
1196
|
+
* accept — those would otherwise key the cache against the wrong row.
|
|
1197
|
+
*/
|
|
1198
|
+
export function parsePositiveDecimalInt(value: string | undefined): number | undefined {
|
|
1199
|
+
if (!value || !/^\d+$/.test(value)) return undefined;
|
|
1200
|
+
const num = Number(value);
|
|
1201
|
+
if (!Number.isSafeInteger(num) || num <= 0) return undefined;
|
|
1202
|
+
return num;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function parseIssueUrl(value: string | undefined): { repo?: string; issueNumber?: number } {
|
|
1206
|
+
const normalized = normalizeOptionalString(value);
|
|
1207
|
+
if (!normalized) return {};
|
|
1208
|
+
const match = normalized.match(ISSUE_URL_PATTERN);
|
|
1209
|
+
if (!match) return {};
|
|
1210
|
+
return {
|
|
1211
|
+
repo: match[1],
|
|
1212
|
+
issueNumber: Number(match[2]),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1089
1216
|
function normalizePrReviewComment(comment: GhPrReviewCommentApi): GhPrReviewComment | null {
|
|
1090
1217
|
if (typeof comment.id !== "number") {
|
|
1091
1218
|
return null;
|
|
@@ -1600,6 +1727,52 @@ async function resolveGitHubRepo(
|
|
|
1600
1727
|
return requireNonEmpty(resolved, "repo");
|
|
1601
1728
|
}
|
|
1602
1729
|
|
|
1730
|
+
/**
|
|
1731
|
+
* Process-lifetime cache of `gh repo view --json nameWithOwner` lookups keyed
|
|
1732
|
+
* by absolute cwd. Avoids repeated `gh` chatter when the same protocol handler
|
|
1733
|
+
* or tool call resolves the default repo many times in a row.
|
|
1734
|
+
*
|
|
1735
|
+
* The shared lookup is intentionally **not** bound to any caller's
|
|
1736
|
+
* AbortSignal. Cancelling one caller would otherwise kill the underlying
|
|
1737
|
+
* `gh repo view` for every concurrent waiter on the same cwd. Each caller's
|
|
1738
|
+
* signal is honored at the wait point via `untilAborted` instead, so an abort
|
|
1739
|
+
* unwinds only that caller.
|
|
1740
|
+
*/
|
|
1741
|
+
const DEFAULT_REPO_RESOLVED = new Map<string, string>();
|
|
1742
|
+
const DEFAULT_REPO_INFLIGHT = new Map<string, Promise<string>>();
|
|
1743
|
+
|
|
1744
|
+
export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSignal): Promise<string> {
|
|
1745
|
+
const key = path.resolve(cwd);
|
|
1746
|
+
const ready = DEFAULT_REPO_RESOLVED.get(key);
|
|
1747
|
+
if (ready) return ready;
|
|
1748
|
+
let pending = DEFAULT_REPO_INFLIGHT.get(key);
|
|
1749
|
+
if (!pending) {
|
|
1750
|
+
pending = (async () => {
|
|
1751
|
+
// No caller signal: this lookup is shared across every concurrent
|
|
1752
|
+
// waiter on the same cwd.
|
|
1753
|
+
const resolved = await git.github.text(cwd, [
|
|
1754
|
+
"repo",
|
|
1755
|
+
"view",
|
|
1756
|
+
"--json",
|
|
1757
|
+
"nameWithOwner",
|
|
1758
|
+
"-q",
|
|
1759
|
+
".nameWithOwner",
|
|
1760
|
+
]);
|
|
1761
|
+
const value = requireNonEmpty(resolved, "repo");
|
|
1762
|
+
DEFAULT_REPO_RESOLVED.set(key, value);
|
|
1763
|
+
return value;
|
|
1764
|
+
})();
|
|
1765
|
+
// Drop the in-flight slot on settle so failures don't poison the cache
|
|
1766
|
+
// and so a successful resolution survives only in `DEFAULT_REPO_RESOLVED`.
|
|
1767
|
+
void pending.then(
|
|
1768
|
+
() => DEFAULT_REPO_INFLIGHT.delete(key),
|
|
1769
|
+
() => DEFAULT_REPO_INFLIGHT.delete(key),
|
|
1770
|
+
);
|
|
1771
|
+
DEFAULT_REPO_INFLIGHT.set(key, pending);
|
|
1772
|
+
}
|
|
1773
|
+
return untilAborted(signal, pending);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1603
1776
|
async function resolveGitHubBranchHead(
|
|
1604
1777
|
cwd: string,
|
|
1605
1778
|
repo: string,
|
|
@@ -2237,14 +2410,8 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
2237
2410
|
switch (params.op) {
|
|
2238
2411
|
case "repo_view":
|
|
2239
2412
|
return executeRepoView(this.session, params, signal);
|
|
2240
|
-
case "issue_view":
|
|
2241
|
-
return executeIssueView(this.session, params, signal);
|
|
2242
2413
|
case "pr_create":
|
|
2243
2414
|
return executePrCreate(this.session, params, signal);
|
|
2244
|
-
case "pr_view":
|
|
2245
|
-
return executePrView(this.session, params, signal);
|
|
2246
|
-
case "pr_diff":
|
|
2247
|
-
return executePrDiff(this.session, params, signal);
|
|
2248
2415
|
case "pr_checkout":
|
|
2249
2416
|
return executePrCheckout(this.session, params, signal);
|
|
2250
2417
|
case "pr_push":
|
|
@@ -2288,111 +2455,449 @@ async function executeRepoView(
|
|
|
2288
2455
|
return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
|
|
2289
2456
|
}
|
|
2290
2457
|
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2458
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2459
|
+
// Cached issue/PR view fetchers
|
|
2460
|
+
//
|
|
2461
|
+
// Used by `executeIssueView`/`executePrView` and by the `issue://` / `pr://`
|
|
2462
|
+
// internal-URL protocol handlers. The cache wrapper lives in `./github-cache`;
|
|
2463
|
+
// the fresh fetchers stay here to share the existing formatter helpers.
|
|
2464
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2465
|
+
|
|
2466
|
+
export interface IssueViewLookupOptions {
|
|
2467
|
+
cwd: string;
|
|
2468
|
+
repo?: string;
|
|
2469
|
+
/** Issue number or GitHub issue URL. */
|
|
2470
|
+
issue: string;
|
|
2471
|
+
includeComments?: boolean;
|
|
2472
|
+
signal?: AbortSignal;
|
|
2473
|
+
settings?: Settings;
|
|
2474
|
+
cacheAuthKey?: string | null;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
export interface PrViewLookupOptions {
|
|
2478
|
+
cwd: string;
|
|
2479
|
+
repo: string;
|
|
2480
|
+
number: number;
|
|
2481
|
+
includeComments?: boolean;
|
|
2482
|
+
signal?: AbortSignal;
|
|
2483
|
+
settings?: Settings;
|
|
2484
|
+
cacheAuthKey?: string | null;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
export interface ViewLookupResult<T> {
|
|
2488
|
+
rendered: string;
|
|
2489
|
+
sourceUrl: string | undefined;
|
|
2490
|
+
payload: T;
|
|
2491
|
+
status: CacheStatus;
|
|
2492
|
+
fetchedAt: number;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
async function fetchIssueViewFresh(
|
|
2496
|
+
cwd: string,
|
|
2497
|
+
repo: string | undefined,
|
|
2498
|
+
identifier: string,
|
|
2499
|
+
includeComments: boolean,
|
|
2294
2500
|
signal: AbortSignal | undefined,
|
|
2295
|
-
): Promise<
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
const includeComments = params.comments ?? true;
|
|
2299
|
-
const args = ["issue", "view", issue];
|
|
2300
|
-
appendRepoFlag(args, repo, issue);
|
|
2501
|
+
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: GhIssueViewData }> {
|
|
2502
|
+
const args = ["issue", "view", identifier];
|
|
2503
|
+
appendRepoFlag(args, repo, identifier);
|
|
2301
2504
|
args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
|
|
2302
|
-
|
|
2303
|
-
const data = await git.github.json<GhIssueViewData>(session.cwd, args, signal, {
|
|
2505
|
+
const data = await git.github.json<GhIssueViewData>(cwd, args, signal, {
|
|
2304
2506
|
repoProvided: Boolean(repo),
|
|
2305
2507
|
});
|
|
2306
|
-
|
|
2508
|
+
const rendered = formatIssueView(data, { issue: identifier, repo, comments: includeComments });
|
|
2509
|
+
return { rendered, sourceUrl: data.url, payload: data };
|
|
2307
2510
|
}
|
|
2308
2511
|
|
|
2309
|
-
async function
|
|
2310
|
-
|
|
2311
|
-
|
|
2512
|
+
async function fetchPrViewFresh(
|
|
2513
|
+
cwd: string,
|
|
2514
|
+
repo: string,
|
|
2515
|
+
number: number,
|
|
2516
|
+
includeComments: boolean,
|
|
2312
2517
|
signal: AbortSignal | undefined,
|
|
2313
|
-
): Promise<
|
|
2314
|
-
const
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
const
|
|
2518
|
+
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: GhPrViewData }> {
|
|
2519
|
+
const args = ["pr", "view", String(number)];
|
|
2520
|
+
appendRepoFlag(args, repo, String(number));
|
|
2521
|
+
args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
|
|
2522
|
+
const data = await git.github.json<GhPrViewData>(cwd, args, signal, { repoProvided: true });
|
|
2523
|
+
if (includeComments && typeof data.number === "number") {
|
|
2524
|
+
data.reviewComments = await fetchPrReviewComments(cwd, repo, data.number, signal);
|
|
2525
|
+
}
|
|
2526
|
+
const rendered = formatPrView(data, { pr: String(number), repo, comments: includeComments });
|
|
2527
|
+
return { rendered, sourceUrl: data.url, payload: data };
|
|
2528
|
+
}
|
|
2318
2529
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2530
|
+
/**
|
|
2531
|
+
* Cache-aware issue/view fetcher. Used by both the `github` tool op and the
|
|
2532
|
+
* `issue://` protocol handler so a single shared row services both surfaces.
|
|
2533
|
+
*/
|
|
2534
|
+
export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<ViewLookupResult<GhIssueViewData>> {
|
|
2535
|
+
const identifier = requireNonEmpty(options.issue, "issue");
|
|
2536
|
+
const includeComments = options.includeComments ?? true;
|
|
2537
|
+
const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
|
|
2538
|
+
const urlParse = parseIssueUrl(identifier);
|
|
2539
|
+
// Prefer the URL's repo when the identifier is a full URL; fall back to the
|
|
2540
|
+
// explicit `repo` option, then to the cwd's default repo.
|
|
2541
|
+
let repo = urlParse.repo ?? normalizeOptionalString(options.repo);
|
|
2542
|
+
let cacheNumber = urlParse.issueNumber;
|
|
2543
|
+
if (cacheNumber === undefined) {
|
|
2544
|
+
cacheNumber = parsePositiveDecimalInt(identifier);
|
|
2545
|
+
}
|
|
2546
|
+
if (cacheNumber !== undefined && !repo) {
|
|
2547
|
+
try {
|
|
2548
|
+
repo = await resolveDefaultRepoMemoized(options.cwd, options.signal);
|
|
2549
|
+
} catch {
|
|
2550
|
+
// Resolution failure leaves `repo` undefined: we'll fall through to a
|
|
2551
|
+
// direct fetch below so gh produces its own error message instead of
|
|
2552
|
+
// us masking it with a friendlier one.
|
|
2553
|
+
repo = undefined;
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2325
2556
|
|
|
2326
|
-
|
|
2327
|
-
repoProvided: Boolean(repo),
|
|
2328
|
-
});
|
|
2329
|
-
const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
|
|
2330
|
-
if (includeComments && resolvedRepo && typeof data.number === "number") {
|
|
2331
|
-
data.reviewComments = await fetchPrReviewComments(session.cwd, resolvedRepo, data.number, signal);
|
|
2332
|
-
}
|
|
2333
|
-
return { prRef, data };
|
|
2334
|
-
}),
|
|
2335
|
-
);
|
|
2557
|
+
const doFetch = () => fetchIssueViewFresh(options.cwd, repo, identifier, includeComments, options.signal);
|
|
2336
2558
|
|
|
2337
|
-
if (
|
|
2338
|
-
const
|
|
2339
|
-
return
|
|
2340
|
-
formatPrView(view.data, { pr: view.prRef, repo, comments: includeComments }),
|
|
2341
|
-
view.data.url,
|
|
2342
|
-
);
|
|
2559
|
+
if (!repo || cacheNumber === undefined) {
|
|
2560
|
+
const fresh = await doFetch();
|
|
2561
|
+
return { ...fresh, status: "miss", fetchedAt: Date.now() };
|
|
2343
2562
|
}
|
|
2344
2563
|
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2564
|
+
const lookup = await getOrFetchView<GhIssueViewData>({
|
|
2565
|
+
repo,
|
|
2566
|
+
kind: "issue",
|
|
2567
|
+
number: cacheNumber,
|
|
2568
|
+
includeComments,
|
|
2569
|
+
settings: options.settings,
|
|
2570
|
+
authKey,
|
|
2571
|
+
fetchFresh: doFetch,
|
|
2572
|
+
});
|
|
2573
|
+
return {
|
|
2574
|
+
rendered: lookup.rendered,
|
|
2575
|
+
sourceUrl: lookup.sourceUrl,
|
|
2576
|
+
payload: lookup.payload,
|
|
2577
|
+
status: lookup.status,
|
|
2578
|
+
fetchedAt: lookup.fetchedAt,
|
|
2579
|
+
};
|
|
2348
2580
|
}
|
|
2349
2581
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2582
|
+
/**
|
|
2583
|
+
* Cache-aware PR view fetcher. Caller must supply a numeric PR number;
|
|
2584
|
+
* branch-name / current-branch lookups bypass the cache entirely upstream
|
|
2585
|
+
* (see `executePrView`).
|
|
2586
|
+
*/
|
|
2587
|
+
export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLookupResult<GhPrViewData>> {
|
|
2588
|
+
const includeComments = options.includeComments ?? true;
|
|
2589
|
+
const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
|
|
2590
|
+
const doFetch = () => fetchPrViewFresh(options.cwd, options.repo, options.number, includeComments, options.signal);
|
|
2591
|
+
const lookup = await getOrFetchView<GhPrViewData>({
|
|
2592
|
+
repo: options.repo,
|
|
2593
|
+
kind: "pr",
|
|
2594
|
+
number: options.number,
|
|
2595
|
+
includeComments,
|
|
2596
|
+
settings: options.settings,
|
|
2597
|
+
authKey,
|
|
2598
|
+
fetchFresh: doFetch,
|
|
2599
|
+
});
|
|
2600
|
+
return {
|
|
2601
|
+
rendered: lookup.rendered,
|
|
2602
|
+
sourceUrl: lookup.sourceUrl,
|
|
2603
|
+
payload: lookup.payload,
|
|
2604
|
+
status: lookup.status,
|
|
2605
|
+
fetchedAt: lookup.fetchedAt,
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2610
|
+
// PR diff fetcher
|
|
2611
|
+
//
|
|
2612
|
+
// Used by the `pr://<n>/diff[/…]` internal-URL family. Stores the verbatim
|
|
2613
|
+
// `gh pr diff` text plus a parsed file index so the listing, full-diff, and
|
|
2614
|
+
// per-file slice variants all share one cache row.
|
|
2615
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2616
|
+
|
|
2617
|
+
export interface PrDiffFile {
|
|
2618
|
+
/** Display path. Prefers the post-image (`b/<path>`) when present. */
|
|
2619
|
+
path: string;
|
|
2620
|
+
additions: number;
|
|
2621
|
+
deletions: number;
|
|
2622
|
+
changeType: "modified" | "added" | "deleted" | "renamed" | "binary";
|
|
2623
|
+
/** Pre-image path for renames/deletes; same as `path` otherwise. */
|
|
2624
|
+
oldPath?: string;
|
|
2625
|
+
/** Byte offset of the section's `diff --git` line in the unified diff. */
|
|
2626
|
+
startOffset: number;
|
|
2627
|
+
/** Byte offset of the next section (or end-of-text). */
|
|
2628
|
+
endOffset: number;
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
export interface PrDiffPayload {
|
|
2632
|
+
/** Full unified diff text as returned by `gh pr diff --color never`. */
|
|
2633
|
+
unified: string;
|
|
2634
|
+
files: PrDiffFile[];
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
export interface PrDiffLookupOptions {
|
|
2638
|
+
cwd: string;
|
|
2639
|
+
repo: string;
|
|
2640
|
+
number: number;
|
|
2641
|
+
signal?: AbortSignal;
|
|
2642
|
+
settings?: Settings;
|
|
2643
|
+
cacheAuthKey?: string | null;
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Split `gh pr diff` output on `^diff --git ` boundaries and parse per-file
|
|
2647
|
+
* metadata. The unified diff is preserved verbatim so callers can slice it by
|
|
2648
|
+
* byte offsets without re-running gh.
|
|
2649
|
+
*/
|
|
2650
|
+
export function parsePrUnifiedDiff(text: string): PrDiffPayload {
|
|
2651
|
+
const files: PrDiffFile[] = [];
|
|
2652
|
+
if (text.length === 0) {
|
|
2653
|
+
return { unified: text, files };
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// Walk match positions manually so we capture each section's byte range.
|
|
2657
|
+
const sectionStarts: number[] = [];
|
|
2658
|
+
const re = /^diff --git /gm;
|
|
2659
|
+
let m: RegExpExecArray | null = re.exec(text);
|
|
2660
|
+
while (m !== null) {
|
|
2661
|
+
sectionStarts.push(m.index);
|
|
2662
|
+
// Avoid zero-length match infinite loop (regex has fixed prefix, but
|
|
2663
|
+
// be explicit).
|
|
2664
|
+
if (re.lastIndex === m.index) re.lastIndex += 1;
|
|
2665
|
+
m = re.exec(text);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
for (let i = 0; i < sectionStarts.length; i += 1) {
|
|
2669
|
+
const startOffset = sectionStarts[i] ?? 0;
|
|
2670
|
+
const endOffset = sectionStarts[i + 1] ?? text.length;
|
|
2671
|
+
const section = text.slice(startOffset, endOffset);
|
|
2672
|
+
files.push(parsePrDiffSection(section, startOffset, endOffset));
|
|
2673
|
+
}
|
|
2674
|
+
return { unified: text, files };
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
interface ParsedDiffHeaderToken {
|
|
2678
|
+
value: string;
|
|
2679
|
+
nextIndex: number;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
function skipDiffHeaderSpaces(text: string, index: number): number {
|
|
2683
|
+
let i = index;
|
|
2684
|
+
while (text.charAt(i) === " ") i += 1;
|
|
2685
|
+
return i;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffHeaderToken {
|
|
2689
|
+
const next = text.charAt(slashIndex + 1);
|
|
2690
|
+
if (next === "") return { value: "\\", nextIndex: slashIndex + 1 };
|
|
2691
|
+
|
|
2692
|
+
if (next >= "0" && next <= "7") {
|
|
2693
|
+
let end = slashIndex + 1;
|
|
2694
|
+
while (end < text.length && end < slashIndex + 4) {
|
|
2695
|
+
const digit = text.charAt(end);
|
|
2696
|
+
if (digit < "0" || digit > "7") break;
|
|
2697
|
+
end += 1;
|
|
2698
|
+
}
|
|
2699
|
+
return {
|
|
2700
|
+
value: String.fromCharCode(Number.parseInt(text.slice(slashIndex + 1, end), 8)),
|
|
2701
|
+
nextIndex: end,
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
switch (next) {
|
|
2706
|
+
case "a":
|
|
2707
|
+
return { value: "\x07", nextIndex: slashIndex + 2 };
|
|
2708
|
+
case "b":
|
|
2709
|
+
return { value: "\b", nextIndex: slashIndex + 2 };
|
|
2710
|
+
case "f":
|
|
2711
|
+
return { value: "\f", nextIndex: slashIndex + 2 };
|
|
2712
|
+
case "n":
|
|
2713
|
+
return { value: "\n", nextIndex: slashIndex + 2 };
|
|
2714
|
+
case "r":
|
|
2715
|
+
return { value: "\r", nextIndex: slashIndex + 2 };
|
|
2716
|
+
case "t":
|
|
2717
|
+
return { value: "\t", nextIndex: slashIndex + 2 };
|
|
2718
|
+
case "v":
|
|
2719
|
+
return { value: "\v", nextIndex: slashIndex + 2 };
|
|
2720
|
+
case "\\":
|
|
2721
|
+
case '"':
|
|
2722
|
+
return { value: next, nextIndex: slashIndex + 2 };
|
|
2723
|
+
default:
|
|
2724
|
+
return { value: next, nextIndex: slashIndex + 2 };
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
function parseDiffQuotedToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
|
|
2729
|
+
if (text.charAt(startIndex) !== '"') return undefined;
|
|
2730
|
+
let value = "";
|
|
2731
|
+
for (let i = startIndex + 1; i < text.length; i += 1) {
|
|
2732
|
+
const ch = text.charAt(i);
|
|
2733
|
+
if (ch === '"') return { value, nextIndex: i + 1 };
|
|
2734
|
+
if (ch !== "\\") {
|
|
2735
|
+
value += ch;
|
|
2736
|
+
continue;
|
|
2737
|
+
}
|
|
2738
|
+
const escaped = parseDiffQuotedEscape(text, i);
|
|
2739
|
+
value += escaped.value;
|
|
2740
|
+
i = escaped.nextIndex - 1;
|
|
2741
|
+
}
|
|
2742
|
+
return undefined;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function parseDiffHeaderToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
|
|
2746
|
+
const start = skipDiffHeaderSpaces(text, startIndex);
|
|
2747
|
+
if (start >= text.length) return undefined;
|
|
2748
|
+
const quoted = parseDiffQuotedToken(text, start);
|
|
2749
|
+
if (quoted) return quoted;
|
|
2750
|
+
const end = text.indexOf(" ", start);
|
|
2751
|
+
if (end === -1) return { value: text.slice(start), nextIndex: text.length };
|
|
2752
|
+
return { value: text.slice(start, end), nextIndex: end };
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function stripPrDiffPathPrefix(value: string, prefix: "a/" | "b/"): string | undefined {
|
|
2756
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: string } {
|
|
2760
|
+
const trail = header.slice("diff --git ".length);
|
|
2761
|
+
if (trail.startsWith('"')) {
|
|
2762
|
+
const oldToken = parseDiffQuotedToken(trail, 0);
|
|
2763
|
+
if (!oldToken) return {};
|
|
2764
|
+
const newToken = parseDiffHeaderToken(trail, oldToken.nextIndex);
|
|
2765
|
+
if (!newToken) return {};
|
|
2766
|
+
return {
|
|
2767
|
+
oldPath: stripPrDiffPathPrefix(oldToken.value, "a/"),
|
|
2768
|
+
newPath: stripPrDiffPathPrefix(newToken.value, "b/"),
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const bIdx = trail.indexOf(" b/");
|
|
2773
|
+
if (trail.startsWith("a/") && bIdx > 0) {
|
|
2774
|
+
return {
|
|
2775
|
+
oldPath: trail.slice(2, bIdx),
|
|
2776
|
+
newPath: trail.slice(bIdx + 3),
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
return {};
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function isPrDiffFileHeaderLine(line: string): boolean {
|
|
2783
|
+
return (
|
|
2784
|
+
line === "--- /dev/null" ||
|
|
2785
|
+
line === "+++ /dev/null" ||
|
|
2786
|
+
line.startsWith("--- a/") ||
|
|
2787
|
+
line.startsWith("+++ b/") ||
|
|
2788
|
+
line.startsWith('--- "a/') ||
|
|
2789
|
+
line.startsWith('+++ "b/')
|
|
2375
2790
|
);
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function parsePrDiffSection(section: string, startOffset: number, endOffset: number): PrDiffFile {
|
|
2794
|
+
const lines = section.split("\n");
|
|
2795
|
+
const header = lines[0] ?? "";
|
|
2796
|
+
const headerPaths = parsePrDiffHeaderPaths(header);
|
|
2797
|
+
let oldPath = headerPaths.oldPath;
|
|
2798
|
+
let newPath = headerPaths.newPath;
|
|
2799
|
+
|
|
2800
|
+
let changeType: PrDiffFile["changeType"] = "modified";
|
|
2801
|
+
let isBinary = false;
|
|
2802
|
+
let additions = 0;
|
|
2803
|
+
let deletions = 0;
|
|
2804
|
+
|
|
2805
|
+
let inHunk = false;
|
|
2806
|
+
for (let li = 1; li < lines.length; li += 1) {
|
|
2807
|
+
const line = lines[li] ?? "";
|
|
2808
|
+
if (line.startsWith("new file mode")) {
|
|
2809
|
+
changeType = "added";
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (line.startsWith("deleted file mode")) {
|
|
2813
|
+
changeType = "deleted";
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
if (line.startsWith("rename from ")) {
|
|
2817
|
+
changeType = "renamed";
|
|
2818
|
+
oldPath = line.slice("rename from ".length);
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
if (line.startsWith("rename to ")) {
|
|
2822
|
+
newPath = line.slice("rename to ".length);
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2825
|
+
if (line.startsWith("Binary files ") && line.endsWith(" differ")) {
|
|
2826
|
+
isBinary = true;
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
if (line.startsWith("@@ ")) {
|
|
2830
|
+
inHunk = true;
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (!inHunk && isPrDiffFileHeaderLine(line)) continue;
|
|
2834
|
+
if (line.startsWith("+")) {
|
|
2835
|
+
additions += 1;
|
|
2836
|
+
} else if (line.startsWith("-")) {
|
|
2837
|
+
deletions += 1;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2376
2840
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2841
|
+
if (isBinary) {
|
|
2842
|
+
if (changeType === "modified") changeType = "binary";
|
|
2843
|
+
additions = 0;
|
|
2844
|
+
deletions = 0;
|
|
2845
|
+
}
|
|
2379
2846
|
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2847
|
+
const displayPath =
|
|
2848
|
+
changeType === "deleted" ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
|
|
2849
|
+
const file: PrDiffFile = {
|
|
2850
|
+
path: displayPath,
|
|
2851
|
+
additions,
|
|
2852
|
+
deletions,
|
|
2853
|
+
changeType,
|
|
2854
|
+
startOffset,
|
|
2855
|
+
endOffset,
|
|
2856
|
+
};
|
|
2857
|
+
if (oldPath && oldPath !== displayPath) {
|
|
2858
|
+
file.oldPath = oldPath;
|
|
2384
2859
|
}
|
|
2860
|
+
return file;
|
|
2861
|
+
}
|
|
2385
2862
|
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2863
|
+
async function fetchPrDiffFresh(
|
|
2864
|
+
cwd: string,
|
|
2865
|
+
repo: string,
|
|
2866
|
+
number: number,
|
|
2867
|
+
signal: AbortSignal | undefined,
|
|
2868
|
+
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: PrDiffPayload }> {
|
|
2869
|
+
const args = ["pr", "diff", String(number), "--color", "never"];
|
|
2870
|
+
appendRepoFlag(args, repo, String(number));
|
|
2871
|
+
const text = await git.github.text(cwd, args, signal, { repoProvided: true, trimOutput: false });
|
|
2872
|
+
const payload = parsePrUnifiedDiff(text);
|
|
2873
|
+
return { rendered: text, sourceUrl: undefined, payload };
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
/**
|
|
2877
|
+
* Cache-aware PR diff fetcher. Stores the full unified diff plus a parsed
|
|
2878
|
+
* file index in a single `pr-diff` cache row so the listing, full-diff, and
|
|
2879
|
+
* per-file slice variants of `pr://<n>/diff` share one `gh pr diff`
|
|
2880
|
+
* invocation.
|
|
2881
|
+
*/
|
|
2882
|
+
export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<ViewLookupResult<PrDiffPayload>> {
|
|
2883
|
+
const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
|
|
2884
|
+
const doFetch = () => fetchPrDiffFresh(options.cwd, options.repo, options.number, options.signal);
|
|
2885
|
+
const lookup = await getOrFetchView<PrDiffPayload>({
|
|
2886
|
+
repo: options.repo,
|
|
2887
|
+
kind: "pr-diff",
|
|
2888
|
+
number: options.number,
|
|
2889
|
+
includeComments: false,
|
|
2890
|
+
settings: options.settings,
|
|
2891
|
+
authKey,
|
|
2892
|
+
fetchFresh: doFetch,
|
|
2393
2893
|
});
|
|
2394
|
-
|
|
2395
|
-
|
|
2894
|
+
return {
|
|
2895
|
+
rendered: lookup.rendered,
|
|
2896
|
+
sourceUrl: lookup.sourceUrl,
|
|
2897
|
+
payload: lookup.payload,
|
|
2898
|
+
status: lookup.status,
|
|
2899
|
+
fetchedAt: lookup.fetchedAt,
|
|
2900
|
+
};
|
|
2396
2901
|
}
|
|
2397
2902
|
|
|
2398
2903
|
function joinSections(sections: string[]): string[] {
|
|
@@ -2765,13 +3270,13 @@ async function executeSearchIssues(
|
|
|
2765
3270
|
const limit = resolveSearchLimit(params.limit);
|
|
2766
3271
|
const dateField = resolveSearchDateField("issues", params.dateField);
|
|
2767
3272
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
3273
|
+
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3274
|
+
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:issue"]);
|
|
3275
|
+
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
|
2770
3276
|
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
return buildTextResult(formatSearchResults("issues", query, repo, items));
|
|
3277
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchIssueItem>>(session.cwd, args, signal);
|
|
3278
|
+
const items = (response.items ?? []).map(apiIssueToSearchResult);
|
|
3279
|
+
return buildTextResult(formatSearchResults("issues", displayQuery, repo, items));
|
|
2775
3280
|
}
|
|
2776
3281
|
|
|
2777
3282
|
async function executeSearchPrs(
|
|
@@ -2783,13 +3288,13 @@ async function executeSearchPrs(
|
|
|
2783
3288
|
const limit = resolveSearchLimit(params.limit);
|
|
2784
3289
|
const dateField = resolveSearchDateField("prs", params.dateField);
|
|
2785
3290
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2786
|
-
const
|
|
2787
|
-
const
|
|
3291
|
+
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3292
|
+
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:pr"]);
|
|
3293
|
+
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
|
2788
3294
|
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
return buildTextResult(formatSearchResults("pull requests", query, repo, items));
|
|
3295
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchIssueItem>>(session.cwd, args, signal);
|
|
3296
|
+
const items = (response.items ?? []).map(apiIssueToSearchResult);
|
|
3297
|
+
return buildTextResult(formatSearchResults("pull requests", displayQuery, repo, items));
|
|
2793
3298
|
}
|
|
2794
3299
|
|
|
2795
3300
|
async function executeSearchCode(
|
|
@@ -2803,11 +3308,11 @@ async function executeSearchCode(
|
|
|
2803
3308
|
}
|
|
2804
3309
|
const repo = normalizeOptionalString(params.repo);
|
|
2805
3310
|
const limit = resolveSearchLimit(params.limit);
|
|
2806
|
-
const
|
|
3311
|
+
const apiQuery = composeSearchQuery([query, repo ? `repo:${repo}` : undefined]);
|
|
3312
|
+
const args = buildGhApiSearchArgs("code", apiQuery, limit, ["Accept: application/vnd.github.text-match+json"]);
|
|
2807
3313
|
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
});
|
|
3314
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchCodeItem>>(session.cwd, args, signal);
|
|
3315
|
+
const items = (response.items ?? []).map(apiCodeToSearchResult);
|
|
2811
3316
|
return buildTextResult(formatSearchCodeResults(query, repo, items));
|
|
2812
3317
|
}
|
|
2813
3318
|
|
|
@@ -2820,13 +3325,13 @@ async function executeSearchCommits(
|
|
|
2820
3325
|
const limit = resolveSearchLimit(params.limit);
|
|
2821
3326
|
const dateField = resolveSearchDateField("commits", params.dateField);
|
|
2822
3327
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2823
|
-
const
|
|
2824
|
-
const
|
|
3328
|
+
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3329
|
+
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined]);
|
|
3330
|
+
const args = buildGhApiSearchArgs("commits", apiQuery, limit);
|
|
2825
3331
|
|
|
2826
|
-
const
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
return buildTextResult(formatSearchCommitsResults(query, repo, items));
|
|
3332
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchCommitItem>>(session.cwd, args, signal);
|
|
3333
|
+
const items = (response.items ?? []).map(apiCommitToSearchResult);
|
|
3334
|
+
return buildTextResult(formatSearchCommitsResults(displayQuery, repo, items));
|
|
2830
3335
|
}
|
|
2831
3336
|
|
|
2832
3337
|
async function executeSearchRepos(
|
|
@@ -2838,9 +3343,10 @@ async function executeSearchRepos(
|
|
|
2838
3343
|
const dateField = resolveSearchDateField("repos", params.dateField);
|
|
2839
3344
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2840
3345
|
const query = composeSearchQuery([params.query, dateQualifier]);
|
|
2841
|
-
const args =
|
|
3346
|
+
const args = buildGhApiSearchArgs("repositories", query, limit);
|
|
2842
3347
|
|
|
2843
|
-
const
|
|
3348
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchRepoItem>>(session.cwd, args, signal);
|
|
3349
|
+
const items = (response.items ?? []).map(apiRepoToSearchResult);
|
|
2844
3350
|
return buildTextResult(formatSearchReposResults(query, items));
|
|
2845
3351
|
}
|
|
2846
3352
|
|