Skip to content

Commit 0a4e8f7

Browse files
authored
fix(tasks): infer repository from pull request prompts (#3748)
1 parent 5bbe70c commit 0a4e8f7

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

packages/core/src/deep-links/newTaskLinkResolver.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,54 @@ describe("NewTaskLinkResolver", () => {
4949
expect(result.analytics.properties.has_prompt).toBe(true);
5050
});
5151

52+
it.each([
53+
{
54+
name: "one pull request",
55+
prompt:
56+
'<github_pr number="12" title="Fix it" url="https://github.com/acme/web/pull/12" />',
57+
expected: "acme/web",
58+
},
59+
{
60+
name: "several pull requests from one repository",
61+
prompt:
62+
'<github_pr number="12" url="https://github.com/acme/web/pull/12" />\n<github_pr number="13" url="https://github.com/acme/web/pull/13" />',
63+
expected: "acme/web",
64+
},
65+
{
66+
name: "pull requests from different repositories",
67+
prompt:
68+
'<github_pr number="12" url="https://github.com/acme/web/pull/12" />\n<github_pr number="4" url="https://github.com/acme/api/pull/4" />',
69+
expected: undefined,
70+
},
71+
{
72+
name: "an ordinary prompt",
73+
prompt: "fix the tests",
74+
expected: undefined,
75+
},
76+
])("infers the repository for $name", async ({ prompt, expected }) => {
77+
const resolver = makeResolver(vi.fn());
78+
79+
const result = await resolver.resolve({ action: "new", prompt });
80+
81+
if (result.kind !== "navigate") throw new Error("expected navigate");
82+
expect(result.navigation.initialCloudRepository).toBe(expected);
83+
});
84+
85+
it("prefers an explicit repository over the pull request prompt", async () => {
86+
const resolver = makeResolver(vi.fn());
87+
const prompt =
88+
'<github_pr number="12" url="https://github.com/acme/web/pull/12" />';
89+
90+
const result = await resolver.resolve({
91+
action: "new",
92+
prompt,
93+
repo: "acme/api",
94+
});
95+
96+
if (result.kind !== "navigate") throw new Error("expected navigate");
97+
expect(result.navigation.initialCloudRepository).toBe("acme/api");
98+
});
99+
52100
it("uses the decoded plan as the prompt for a plan-action payload", async () => {
53101
const resolver = makeResolver(vi.fn());
54102
const payload: NewTaskLinkPayload = { action: "plan", plan: "step one" };

packages/core/src/deep-links/newTaskLinkResolver.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@ import {
1010

1111
export { NEW_TASK_LINK_RESOLVER };
1212

13+
const GITHUB_PR_TAG_REGEX = /<github_pr\b[^>]*\burl="([^"]+)"[^>]*\/>/g;
14+
15+
function inferRepositoryFromPullRequests(
16+
prompt: string | undefined,
17+
): string | undefined {
18+
if (!prompt) return undefined;
19+
20+
const repositories = new Set<string>();
21+
for (const match of prompt.matchAll(GITHUB_PR_TAG_REGEX)) {
22+
try {
23+
const url = new URL(match[1]);
24+
const pathParts = url.pathname.split("/").filter(Boolean);
25+
if (
26+
url.hostname !== "github.com" ||
27+
pathParts.length !== 4 ||
28+
pathParts[2] !== "pull"
29+
) {
30+
continue;
31+
}
32+
repositories.add(`${pathParts[0]}/${pathParts[1]}`);
33+
} catch {}
34+
}
35+
36+
return repositories.size === 1
37+
? repositories.values().next().value
38+
: undefined;
39+
}
40+
1341
@injectable()
1442
export class NewTaskLinkResolver {
1543
constructor(
@@ -35,7 +63,8 @@ export class NewTaskLinkResolver {
3563
kind: "navigate",
3664
navigation: {
3765
initialPrompt: payload.prompt,
38-
initialCloudRepository: payload.repo,
66+
initialCloudRepository:
67+
payload.repo ?? inferRepositoryFromPullRequests(payload.prompt),
3968
initialModel: payload.model,
4069
initialMode: payload.mode,
4170
},

0 commit comments

Comments
 (0)