diff --git a/packages/core/src/deep-links/newTaskLinkResolver.test.ts b/packages/core/src/deep-links/newTaskLinkResolver.test.ts index b417fde5ff..2434fb28e5 100644 --- a/packages/core/src/deep-links/newTaskLinkResolver.test.ts +++ b/packages/core/src/deep-links/newTaskLinkResolver.test.ts @@ -49,6 +49,54 @@ describe("NewTaskLinkResolver", () => { expect(result.analytics.properties.has_prompt).toBe(true); }); + it.each([ + { + name: "one pull request", + prompt: + '', + expected: "acme/web", + }, + { + name: "several pull requests from one repository", + prompt: + '\n', + expected: "acme/web", + }, + { + name: "pull requests from different repositories", + prompt: + '\n', + expected: undefined, + }, + { + name: "an ordinary prompt", + prompt: "fix the tests", + expected: undefined, + }, + ])("infers the repository for $name", async ({ prompt, expected }) => { + const resolver = makeResolver(vi.fn()); + + const result = await resolver.resolve({ action: "new", prompt }); + + if (result.kind !== "navigate") throw new Error("expected navigate"); + expect(result.navigation.initialCloudRepository).toBe(expected); + }); + + it("prefers an explicit repository over the pull request prompt", async () => { + const resolver = makeResolver(vi.fn()); + const prompt = + ''; + + const result = await resolver.resolve({ + action: "new", + prompt, + repo: "acme/api", + }); + + if (result.kind !== "navigate") throw new Error("expected navigate"); + expect(result.navigation.initialCloudRepository).toBe("acme/api"); + }); + it("uses the decoded plan as the prompt for a plan-action payload", async () => { const resolver = makeResolver(vi.fn()); const payload: NewTaskLinkPayload = { action: "plan", plan: "step one" }; diff --git a/packages/core/src/deep-links/newTaskLinkResolver.ts b/packages/core/src/deep-links/newTaskLinkResolver.ts index 25d0c7b7e8..7af6907de1 100644 --- a/packages/core/src/deep-links/newTaskLinkResolver.ts +++ b/packages/core/src/deep-links/newTaskLinkResolver.ts @@ -10,6 +10,34 @@ import { export { NEW_TASK_LINK_RESOLVER }; +const GITHUB_PR_TAG_REGEX = /]*\burl="([^"]+)"[^>]*\/>/g; + +function inferRepositoryFromPullRequests( + prompt: string | undefined, +): string | undefined { + if (!prompt) return undefined; + + const repositories = new Set(); + for (const match of prompt.matchAll(GITHUB_PR_TAG_REGEX)) { + try { + const url = new URL(match[1]); + const pathParts = url.pathname.split("/").filter(Boolean); + if ( + url.hostname !== "github.com" || + pathParts.length !== 4 || + pathParts[2] !== "pull" + ) { + continue; + } + repositories.add(`${pathParts[0]}/${pathParts[1]}`); + } catch {} + } + + return repositories.size === 1 + ? repositories.values().next().value + : undefined; +} + @injectable() export class NewTaskLinkResolver { constructor( @@ -35,7 +63,8 @@ export class NewTaskLinkResolver { kind: "navigate", navigation: { initialPrompt: payload.prompt, - initialCloudRepository: payload.repo, + initialCloudRepository: + payload.repo ?? inferRepositoryFromPullRequests(payload.prompt), initialModel: payload.model, initialMode: payload.mode, },