From 990cccfad76d4d2edfb77c572eed81787bdcbc51 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 21:26:23 +0000 Subject: [PATCH 01/11] test(client-runtime): reproduce #32667 pasted repository page URLs clone as is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These tests reproduce #32667: a GitHub /tree/ or /blob/ page URL or a GitLab /-/tree/ page URL pasted into Add project → Git URL reaches git clone unchanged and names the folder after the branch or file. Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/operations/projects.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 5b55a40f4ebb2..133dea565b7d1 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -65,6 +65,18 @@ describe("add project shared logic", () => { expect(normalizePastedCloneUrl("/srv/git/repo.git")).toBe("/srv/git/repo.git"); }); + it("clones the repository when given a page URL copied from the browser", () => { + for (const [pageUrl, pageRoute, repository] of [ + ["https://github.com/pingdotgg/t3code/tree/main", "/tree/", "t3code"], + ["https://github.com/pingdotgg/t3code/blob/main/README.md", "/blob/", "t3code"], + ["https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main", "/-/", "gitlab-runner"], + ] as const) { + const cloneUrl = normalizePastedCloneUrl(pageUrl); + expect(cloneUrl).not.toContain(pageRoute); + expect(getCloneDirectoryName(cloneUrl)).toBe(repository); + } + }); + it("uses HTTPS for repositories selected through a provider", () => { expect( getDefaultCloneUrl({ From 8a12aba7d896c3bc66a3460294155fceb9d7102f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 21:32:15 +0000 Subject: [PATCH 02/11] fix(client-runtime): clone the repository when a browser page URL is pasted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pasting a repository page URL such as `github.com/owner/repo/tree/main` into Add project → Git URL passed it to `git clone` unchanged, which failed, and suggested the branch or file name as the folder. Each source control definition can now map its own repository page URLs back to the repository (GitHub `/tree/` and `/blob/`, GitLab `/-/`, Forgejo `/src/branch|tag|commit/`, Bitbucket `/src/`), and `normalizePastedCloneUrl` asks the registry for http(s) input. The web command palette derives the default destination folder from the normalized URL instead of the raw input; mobile already did. Fixes pingdotgg/t3code#32667 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.tsx | 2 +- .../client-runtime/src/operations/projects.ts | 3 ++- .../src/shared/definition.test.ts | 11 +++++++++++ .../src/shared/definition.ts | 4 ++++ .../src/shared/definition.ts | 19 +++++++++++++++++++ .../src/shared/definition.test.ts | 16 ++++++++++++++++ .../src/shared/definition.ts | 3 +++ .../src/shared/definition.ts | 4 ++++ .../src/shared/definition.ts | 4 ++++ 9 files changed, 64 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 7c0821678e512..83f7b9530f7ae 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -2021,7 +2021,7 @@ function OpenCommandPaletteDialog(props: { const destinationPath = getDefaultCloneDestination( addProjectCloneFlow.environmentId, remoteUrl, - rawRepository, + remoteUrl, ); setAddProjectCloneFlow({ step: "confirm", diff --git a/packages/client-runtime/src/operations/projects.ts b/packages/client-runtime/src/operations/projects.ts index aab4f0f284290..f0d7e489423fd 100644 --- a/packages/client-runtime/src/operations/projects.ts +++ b/packages/client-runtime/src/operations/projects.ts @@ -128,7 +128,8 @@ export function githubIncompleteOwnerRepositoryMessage( export function normalizePastedCloneUrl(input: string): string { const trimmed = input.trim(); - return REPOSITORY_SHORTHAND.test(trimmed) ? (defaultHostCloneUrl(trimmed) ?? trimmed) : trimmed; + if (REPOSITORY_SHORTHAND.test(trimmed)) return defaultHostCloneUrl(trimmed) ?? trimmed; + return sourceControlClients.repositoryUrlFromPage(trimmed) ?? trimmed; } export function getDefaultCloneUrl( diff --git a/packages/source-control-bitbucket/src/shared/definition.test.ts b/packages/source-control-bitbucket/src/shared/definition.test.ts index f10b9c7e6bdf0..da844c2b2a2ab 100644 --- a/packages/source-control-bitbucket/src/shared/definition.test.ts +++ b/packages/source-control-bitbucket/src/shared/definition.test.ts @@ -51,6 +51,17 @@ describe("Bitbucket change request URLs", () => { }); }); +describe("Bitbucket repository pages", () => { + it("reads the repository from a source view on Bitbucket hosts only", () => { + const repositoryUrl = (url: string) => definition.repositoryUrlFromPage?.(new URL(url)); + expect(repositoryUrl("https://bitbucket.org/workspace/repo/src/main/")).toBe( + "https://bitbucket.org/workspace/repo", + ); + expect(repositoryUrl("https://bitbucket.org/workspace/repo")).toBeNull(); + expect(repositoryUrl("https://scm.example.test/workspace/repo/src/main/")).toBeNull(); + }); +}); + describe("Bitbucket remotes", () => { const recognize = (url: string) => recognizeRemoteWith(definition, url); diff --git a/packages/source-control-bitbucket/src/shared/definition.ts b/packages/source-control-bitbucket/src/shared/definition.ts index 2f643751c599a..edb1b103081cb 100644 --- a/packages/source-control-bitbucket/src/shared/definition.ts +++ b/packages/source-control-bitbucket/src/shared/definition.ts @@ -14,6 +14,7 @@ import { isChangeRequestOnProjectHost, parseChangeRequestPath, parseCommitPath, + repositoryUrlFromPagePath, projectChangeRequestHost, } from "@t3tools/source-control-core/shared/definition"; import { icon } from "./icon.ts"; @@ -23,6 +24,7 @@ const repositoryName = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; const CHANGE_REQUEST_PATH = /^\/([^/]+\/[^/]+)\/pull-requests\/(\d+)(?:\/|$)/u; const COMMIT_PATH = /^\/([^/]+\/[^/]+)\/commits\/([0-9a-f]+)\/?$/iu; +const REPOSITORY_PAGE_PATH = /^\/([^/]+\/[^/]+)\/src\/[^/]/u; const KIND = SourceControlProviderKind.make("bitbucket"); @@ -96,6 +98,8 @@ export const definition = defineSourceControl({ commitUrl: (input) => buildCommitUrl(input, "commits"), parseCommitUrl: (url) => isBitbucketHost(url.hostname) ? parseCommitPath(url, COMMIT_PATH) : null, + repositoryUrlFromPage: (url) => + isBitbucketHost(url.hostname) ? repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH) : null, changeRequestUrlFromReferenceAutolink: () => null, // No endpoint reopens a declined pull request, and nothing documented moves one in or out of // draft, so neither is offered rather than failing when pressed. diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 8f71e1cc73468..780bcb9dad1bd 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -258,6 +258,8 @@ export interface SourceControlDefinition { readonly parseCommitUrl?: (url: URL, known: boolean) => CommitLink | null; /** Null when the URL is not one of this host's change requests. */ readonly parseChangeRequestUrl: (url: URL) => ParsedChangeRequestUrl | null; + /** The repository URL when `url` is a page inside one of this host's repositories. */ + readonly repositoryUrlFromPage?: (url: URL) => string | null; /** * The change request URL a `reference` autolink might name, such as a GitHub `#123` written * through the issue route. Only a candidate: the caller must read it as a change request @@ -410,6 +412,12 @@ export function parseChangeRequestPath(url: URL, route: RegExp): ParsedChangeReq : null; } +/** Reads a repository page from `url` with `route`, whose first group is the repository path. */ +export function repositoryUrlFromPagePath(url: URL, route: RegExp): string | null { + const repository = route.exec(url.pathname)?.[1]; + return repository ? `${url.origin}/${repository}` : null; +} + export function changeRequestUrlBelow(webUrl: string, route: string): string { return `${webUrl.replace(/\/+$/u, "")}/${route}`; } @@ -615,6 +623,8 @@ export interface SourceControlDefinitionRegistry { readonly hostLabelForChangeRequestUrl: (url: string) => string; readonly isSameChangeRequest: (left: string, right: string) => boolean; readonly changeRequestUrlFromReferenceAutolink: (url: string) => string | null; + /** The repository a page URL copied from a browser is in, such as a branch or file view. */ + readonly repositoryUrlFromPage: (url: string) => string | null; /** * The host a remote is on, from the first definition in recognition order that claims it, or an * unknown host. Null when the remote names no host. @@ -744,6 +754,15 @@ export function makeSourceControlDefinitionRegistry( } return null; }, + repositoryUrlFromPage: (value) => { + const url = httpUrl(value); + if (url === null) return null; + for (const definition of claimOrder) { + const repositoryUrl = definition.repositoryUrlFromPage?.(url); + if (repositoryUrl != null) return repositoryUrl; + } + return null; + }, recognizeRemote: (remoteUrl) => { const remote = parseGitRemoteHost(remoteUrl); if (remote === null) return null; diff --git a/packages/source-control-forgejo/src/shared/definition.test.ts b/packages/source-control-forgejo/src/shared/definition.test.ts index 139d21c37f5ca..107a43a6d4cd2 100644 --- a/packages/source-control-forgejo/src/shared/definition.test.ts +++ b/packages/source-control-forgejo/src/shared/definition.test.ts @@ -104,6 +104,22 @@ describe("Forgejo change request URLs", () => { }); }); +describe("Forgejo repository pages", () => { + it("reads the repository from branch, tag and commit views", () => { + const repositoryUrl = (url: string) => definition.repositoryUrlFromPage?.(new URL(url)); + expect(repositoryUrl("https://codeberg.org/forgejo/forgejo/src/branch/forgejo/README.md")).toBe( + "https://codeberg.org/forgejo/forgejo", + ); + expect(repositoryUrl("http://git.example.test:3000/git/team/repo/src/tag/v1.0")).toBe( + "http://git.example.test:3000/git/team/repo", + ); + expect(repositoryUrl("https://codeberg.org/forgejo/forgejo/src/commit/abc123")).toBe( + "https://codeberg.org/forgejo/forgejo", + ); + expect(repositoryUrl("https://codeberg.org/forgejo/forgejo")).toBeNull(); + }); +}); + describe("Forgejo remotes", () => { it("recognises Codeberg and Forgejo or Gitea hosts, keeping an HTTP remote's origin", () => { for (const host of ["codeberg.org", "forgejo.example.test", "gitea.example.test"]) { diff --git a/packages/source-control-forgejo/src/shared/definition.ts b/packages/source-control-forgejo/src/shared/definition.ts index f78805fee1c60..44b4b5bcb238e 100644 --- a/packages/source-control-forgejo/src/shared/definition.ts +++ b/packages/source-control-forgejo/src/shared/definition.ts @@ -10,6 +10,7 @@ import { isChangeRequestOnProjectHost, parseChangeRequestPath, parseCommitPath, + repositoryUrlFromPagePath, projectChangeRequestHost, remoteWebOrigin, } from "@t3tools/source-control-core/shared/definition"; @@ -22,6 +23,7 @@ const CHANGE_REQUEST_REFERENCE = // Forgejo and Gitea run on any hostname, possibly mounted below a path. const CHANGE_REQUEST_PATH = /^\/([^/]+(?:\/[^/]+)+)\/pulls\/(\d+)(?:\/|$)/u; const COMMIT_PATH = /^\/([^/]+(?:\/[^/]+)+)\/commit\/([0-9a-f]+)\/?$/iu; +const REPOSITORY_PAGE_PATH = /^\/([^/]+(?:\/[^/]+)+?)\/src\/(?:branch|tag|commit)\/[^/]/u; const KIND = SourceControlProviderKind.make("forgejo"); @@ -87,6 +89,7 @@ export const definition = defineSourceControl({ changeRequestUrlFromReferenceAutolink: () => null, commitUrl: (input) => buildCommitUrl(input, "commit"), parseCommitUrl: (url) => parseCommitPath(url, COMMIT_PATH), + repositoryUrlFromPage: (url) => repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH), changeRequestActions: new Set(["merge", "close", "reopen", "update-branch"] as const), // Neither `fj` nor `tea` checks out by number. checkoutCommand: ({ number, repositoryUrl }) => diff --git a/packages/source-control-github/src/shared/definition.ts b/packages/source-control-github/src/shared/definition.ts index 3c33cfb3a3f64..b08547ae12c1b 100644 --- a/packages/source-control-github/src/shared/definition.ts +++ b/packages/source-control-github/src/shared/definition.ts @@ -8,6 +8,7 @@ import { isChangeRequestOnProjectHost, parseChangeRequestPath, parseCommitPath, + repositoryUrlFromPagePath, projectChangeRequestHost, remoteWebOrigin, type SourceControlAppSignIn, @@ -26,6 +27,7 @@ const CHANGE_REQUEST_REFERENCE = const CHANGE_REQUEST_PATH = /^\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/|$)/u; const COMMIT_PATH = /^\/([^/]+\/[^/]+)\/(?:commit|pull\/\d+\/commits)\/([0-9a-f]+)\/?$/iu; const ISSUE_PATH = /^\/([^/]+\/[^/]+)\/issues\/(\d+)(?:\/|$)/u; +const REPOSITORY_PAGE_PATH = /^\/([^/]+\/[^/]+)\/(?:tree|blob)\/[^/]/u; const KIND = SourceControlProviderKind.make("github"); @@ -98,6 +100,8 @@ export const definition = defineSourceControl({ parseCommitUrl: (url, known) => known || isGitHubHost(url.hostname) ? parseCommitPath(url, COMMIT_PATH) : null, parseChangeRequestUrl: (url) => parseChangeRequestPath(url, CHANGE_REQUEST_PATH), + repositoryUrlFromPage: (url) => + isGitHubHost(url.hostname) ? repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH) : null, // GitHub writes every bare reference through `/issues/`, including pull requests. changeRequestUrlFromReferenceAutolink: (url) => { const match = isGitHubHost(url.hostname) ? ISSUE_PATH.exec(url.pathname) : null; diff --git a/packages/source-control-gitlab/src/shared/definition.ts b/packages/source-control-gitlab/src/shared/definition.ts index 16d5bb8cf0efa..96d6f7be7200f 100644 --- a/packages/source-control-gitlab/src/shared/definition.ts +++ b/packages/source-control-gitlab/src/shared/definition.ts @@ -7,6 +7,7 @@ import { isChangeRequestOnProjectHost, parseChangeRequestPath, parseCommitPath, + repositoryUrlFromPagePath, projectChangeRequestHost, remoteWebOrigin, } from "@t3tools/source-control-core/shared/definition"; @@ -20,6 +21,8 @@ const CHANGE_REQUEST_REFERENCE = // Self-managed GitLab runs on any hostname and nests projects in groups. const CHANGE_REQUEST_PATH = /^\/([^/]+(?:\/[^/]+)+)\/-\/merge_requests\/(\d+)(?:\/|$)/u; const COMMIT_PATH = /^\/([^/]+(?:\/[^/]+)+)\/-\/commit\/([0-9a-f]+)\/?$/iu; +// Every GitLab page inside a project sits below its first `/-/`. +const REPOSITORY_PAGE_PATH = /^\/([^/]+(?:\/[^/]+)+?)\/-\//u; const KIND = SourceControlProviderKind.make("gitlab"); @@ -47,6 +50,7 @@ export const definition = defineSourceControl({ parseChangeRequestUrl: (url) => parseChangeRequestPath(url, CHANGE_REQUEST_PATH), commitUrl: (input) => buildCommitUrl(input, "-/commit"), parseCommitUrl: (url) => parseCommitPath(url, COMMIT_PATH), + repositoryUrlFromPage: (url) => repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH), // `!12` already links the merge request itself; `#12` is always an issue. changeRequestUrlFromReferenceAutolink: (url) => CHANGE_REQUEST_PATH.test(url.pathname) ? url.toString() : null, From a046c640dc19df9f2f8228bb200a45d3d81d1cbc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 21:43:36 +0000 Subject: [PATCH 03/11] fix(client-runtime): keep clone URLs and credentials when reading pasted repository pages A known public host's URLs are now read only by that host, so Forgejo's any-host page route no longer rewrites a gitlab.com nested-group clone URL. The repository URL keeps the userinfo and port a private clone relies on, and GitCafe tree and blob pages map back to the repository. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/CommandPalette.tsx | 3 +- .../src/operations/projects.test.ts | 10 +++++++ .../src/shared/definition.ts | 29 ++++++++++++------- .../src/shared/definition.ts | 6 ++++ .../src/shared/definition.test.ts | 8 +++++ 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 83f7b9530f7ae..65c69379a4ecc 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -1966,7 +1966,7 @@ function OpenCommandPaletteDialog(props: { function getDefaultCloneDestination( environmentId: EnvironmentId, remoteUrl: string, - repository: string, + repository?: string, ): string { const environment = environments.find((candidate) => candidate.environmentId === environmentId); return getCloneDestinationPath( @@ -2021,7 +2021,6 @@ function OpenCommandPaletteDialog(props: { const destinationPath = getDefaultCloneDestination( addProjectCloneFlow.environmentId, remoteUrl, - remoteUrl, ); setAddProjectCloneFlow({ step: "confirm", diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 133dea565b7d1..042cc58b97295 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -63,6 +63,12 @@ describe("add project shared logic", () => { ); expect(normalizePastedCloneUrl("group/subgroup/project")).toBe("group/subgroup/project"); expect(normalizePastedCloneUrl("/srv/git/repo.git")).toBe("/srv/git/repo.git"); + for (const gitlabProject of [ + "https://gitlab.com/acme/tools/src/branch/worker.git", + "https://gitlab.com/acme/tools/src/branch/worker", + ]) { + expect(normalizePastedCloneUrl(gitlabProject)).toBe(gitlabProject); + } }); it("clones the repository when given a page URL copied from the browser", () => { @@ -70,11 +76,15 @@ describe("add project shared logic", () => { ["https://github.com/pingdotgg/t3code/tree/main", "/tree/", "t3code"], ["https://github.com/pingdotgg/t3code/blob/main/README.md", "/blob/", "t3code"], ["https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main", "/-/", "gitlab-runner"], + ["https://git.cafe/pingdotgg/t3code/tree/main", "/tree/", "t3code"], ] as const) { const cloneUrl = normalizePastedCloneUrl(pageUrl); expect(cloneUrl).not.toContain(pageRoute); expect(getCloneDirectoryName(cloneUrl)).toBe(repository); } + expect( + normalizePastedCloneUrl("https://alice:example@bitbucket.org/workspace/repo/src/master/"), + ).toBe("https://alice:example@bitbucket.org/workspace/repo"); }); it("uses HTTPS for repositories selected through a provider", () => { diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 780bcb9dad1bd..04e7c5fcdea3f 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -415,7 +415,13 @@ export function parseChangeRequestPath(url: URL, route: RegExp): ParsedChangeReq /** Reads a repository page from `url` with `route`, whose first group is the repository path. */ export function repositoryUrlFromPagePath(url: URL, route: RegExp): string | null { const repository = route.exec(url.pathname)?.[1]; - return repository ? `${url.origin}/${repository}` : null; + if (!repository) return null; + // Keeps the credentials and port a private clone relies on. + const repositoryUrl = new URL(url); + repositoryUrl.pathname = `/${repository}`; + repositoryUrl.search = ""; + repositoryUrl.hash = ""; + return repositoryUrl.href; } export function changeRequestUrlBelow(webUrl: string, route: string): string { @@ -693,6 +699,14 @@ export function makeSourceControlDefinitionRegistry( } return undefined; }; + const findByPublicDomain = (hostname: string) => { + const host = hostname.toLowerCase(); + return definitions.find( + (definition) => + definition.publicHost !== null && + (host === definition.publicHost || host.endsWith(`.${definition.publicHost}`)), + ); + }; const parseChangeRequestUrl = (value: string): ParsedChangeRequest | null => { const url = httpUrl(value); if (url === null) return null; @@ -711,14 +725,7 @@ export function makeSourceControlDefinitionRegistry( const host = hostname.toLowerCase(); return definitions.find((definition) => definition.publicHost === host); }, - findByPublicDomain: (hostname) => { - const host = hostname.toLowerCase(); - return definitions.find( - (definition) => - definition.publicHost !== null && - (host === definition.publicHost || host.endsWith(`.${definition.publicHost}`)), - ); - }, + findByPublicDomain, parseChangeRequestUrl, parseCommitUrl: (value, kind) => { const url = httpUrl(value); @@ -757,7 +764,9 @@ export function makeSourceControlDefinitionRegistry( repositoryUrlFromPage: (value) => { const url = httpUrl(value); if (url === null) return null; - for (const definition of claimOrder) { + // A known public host's paths are its own, so an any-host route never rewrites them. + const owner = findByPublicDomain(url.hostname); + for (const definition of owner ? [owner] : claimOrder) { const repositoryUrl = definition.repositoryUrlFromPage?.(url); if (repositoryUrl != null) return repositoryUrl; } diff --git a/packages/source-control-gitcafe/src/shared/definition.ts b/packages/source-control-gitcafe/src/shared/definition.ts index 3749063ce7c1b..2abc258fbf955 100644 --- a/packages/source-control-gitcafe/src/shared/definition.ts +++ b/packages/source-control-gitcafe/src/shared/definition.ts @@ -14,6 +14,7 @@ import { parseChangeRequestPath, parseCommitPath, projectChangeRequestHost, + repositoryUrlFromPagePath, } from "@t3tools/source-control-core/shared/definition"; import { icon } from "./icon.ts"; @@ -23,6 +24,7 @@ const GITCAFE_HOSTS = new Set(["git.cafe", "staging.git.cafe"]); /** GitCafe serves pull requests at `/pull/` and redirects the older `/pulls/` there. */ const CHANGE_REQUEST_PATH = /^\/([^/]+\/[^/]+)\/pulls?\/(\d+)(?:\/|$)/u; const COMMIT_PATH = /^\/([^/]+\/[^/]+)\/commit\/([0-9a-f]+)\/?$/iu; +const REPOSITORY_PAGE_PATH = /^\/([^/]+\/[^/]+)\/(?:tree|blob)\/[^/]/u; function parseChangeRequestUrl(url: URL) { return GITCAFE_HOSTS.has(url.hostname.toLowerCase()) @@ -72,6 +74,10 @@ export const definition = defineSourceControl({ commitUrl: (input) => buildCommitUrl(input, "commit"), parseCommitUrl: (url) => GITCAFE_HOSTS.has(url.hostname.toLowerCase()) ? parseCommitPath(url, COMMIT_PATH) : null, + repositoryUrlFromPage: (url) => + GITCAFE_HOSTS.has(url.hostname.toLowerCase()) + ? repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH) + : null, changeRequestUrlFromReferenceAutolink: () => null, changeRequestActions: new Set([ "ready", diff --git a/packages/source-control-github/src/shared/definition.test.ts b/packages/source-control-github/src/shared/definition.test.ts index 924ab31716853..17ac17ad437b1 100644 --- a/packages/source-control-github/src/shared/definition.test.ts +++ b/packages/source-control-github/src/shared/definition.test.ts @@ -96,6 +96,14 @@ describe("GitHub change request URLs", () => { }); }); +describe("GitHub repository pages", () => { + it("leaves the tree route on other hosts to them", () => { + expect( + definition.repositoryUrlFromPage?.(new URL("https://scm.example.test/o/r/tree/main")), + ).toBeNull(); + }); +}); + describe("GitHub remotes", () => { const recognize = (url: string) => recognizeRemoteWith(definition, url); From fa974779c228283cbd415f667336c524ea6dbb52 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 21:52:36 +0000 Subject: [PATCH 04/11] fix(source-control-core): read a pasted page only with its recognized host's routes A self-hosted GitLab nested-group clone URL such as gitlab.example.test/acme/tools/src/branch/worker was cut at Forgejo's any-host page route. The registry now asks the definition that recognizes the URL's host, as remote recognition does, and falls back to claim order only for hosts no definition recognizes. Co-Authored-By: Claude Opus 5.5 --- .../src/operations/projects.test.ts | 3 +++ .../src/shared/definition.ts | 24 ++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 042cc58b97295..3b86b68d41b07 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -66,6 +66,8 @@ describe("add project shared logic", () => { for (const gitlabProject of [ "https://gitlab.com/acme/tools/src/branch/worker.git", "https://gitlab.com/acme/tools/src/branch/worker", + "https://gitlab.example.test/acme/tools/src/branch/worker.git", + "https://gitlab.example.test/acme/tools/src/branch/worker", ]) { expect(normalizePastedCloneUrl(gitlabProject)).toBe(gitlabProject); } @@ -77,6 +79,7 @@ describe("add project shared logic", () => { ["https://github.com/pingdotgg/t3code/blob/main/README.md", "/blob/", "t3code"], ["https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main", "/-/", "gitlab-runner"], ["https://git.cafe/pingdotgg/t3code/tree/main", "/tree/", "t3code"], + ["https://gitlab.example.test/g/p/-/blob/main/src/branch/x", "/-/", "p"], ] as const) { const cloneUrl = normalizePastedCloneUrl(pageUrl); expect(cloneUrl).not.toContain(pageRoute); diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 04e7c5fcdea3f..7c706ef917312 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -699,14 +699,8 @@ export function makeSourceControlDefinitionRegistry( } return undefined; }; - const findByPublicDomain = (hostname: string) => { - const host = hostname.toLowerCase(); - return definitions.find( - (definition) => - definition.publicHost !== null && - (host === definition.publicHost || host.endsWith(`.${definition.publicHost}`)), - ); - }; + const remoteOwner = (remote: GitRemoteHost) => + recognitionOrder.find((definition) => definition.recognizeRemote(remote) !== null); const parseChangeRequestUrl = (value: string): ParsedChangeRequest | null => { const url = httpUrl(value); if (url === null) return null; @@ -725,7 +719,14 @@ export function makeSourceControlDefinitionRegistry( const host = hostname.toLowerCase(); return definitions.find((definition) => definition.publicHost === host); }, - findByPublicDomain, + findByPublicDomain: (hostname) => { + const host = hostname.toLowerCase(); + return definitions.find( + (definition) => + definition.publicHost !== null && + (host === definition.publicHost || host.endsWith(`.${definition.publicHost}`)), + ); + }, parseChangeRequestUrl, parseCommitUrl: (value, kind) => { const url = httpUrl(value); @@ -764,8 +765,9 @@ export function makeSourceControlDefinitionRegistry( repositoryUrlFromPage: (value) => { const url = httpUrl(value); if (url === null) return null; - // A known public host's paths are its own, so an any-host route never rewrites them. - const owner = findByPublicDomain(url.hostname); + // A recognized host's paths are its own, so another host's any-host route never rewrites them. + const remote = parseGitRemoteHost(value); + const owner = remote === null ? undefined : remoteOwner(remote); for (const definition of owner ? [owner] : claimOrder) { const repositoryUrl = definition.repositoryUrlFromPage?.(url); if (repositoryUrl != null) return repositoryUrl; From adfedf5dc3521c4262e503ccab3a70c2a5096fcd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 7 Nov 2026 21:58:43 +0000 Subject: [PATCH 05/11] fix(source-control-core): leave clone URLs and ambiguous pages on unknown hosts alone A path ending in .git is a clone URL, so a GitLab nested-group repository on an unrecognized hostname is no longer cut at Forgejo's page route. On such hosts a page is rewritten only when every matching host route agrees, so a Forgejo file under a `-` folder is not read as a GitLab page. Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/operations/projects.test.ts | 4 ++++ .../source-control-core/src/shared/definition.ts | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 3b86b68d41b07..c08da0b2be2ee 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -68,6 +68,8 @@ describe("add project shared logic", () => { "https://gitlab.com/acme/tools/src/branch/worker", "https://gitlab.example.test/acme/tools/src/branch/worker.git", "https://gitlab.example.test/acme/tools/src/branch/worker", + "https://scm.example.test/acme/tools/src/branch/worker.git", + "https://scm.example.test/team/repo/src/branch/main/-/README.md", ]) { expect(normalizePastedCloneUrl(gitlabProject)).toBe(gitlabProject); } @@ -80,6 +82,8 @@ describe("add project shared logic", () => { ["https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main", "/-/", "gitlab-runner"], ["https://git.cafe/pingdotgg/t3code/tree/main", "/tree/", "t3code"], ["https://gitlab.example.test/g/p/-/blob/main/src/branch/x", "/-/", "p"], + ["https://code.company.test/g/p/-/tree/main", "/-/", "p"], + ["https://code.company.test/team/repo/src/branch/main/docs", "/src/", "repo"], ] as const) { const cloneUrl = normalizePastedCloneUrl(pageUrl); expect(cloneUrl).not.toContain(pageRoute); diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 7c706ef917312..079e7c6af8e26 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -764,15 +764,17 @@ export function makeSourceControlDefinitionRegistry( }, repositoryUrlFromPage: (value) => { const url = httpUrl(value); - if (url === null) return null; + // A path ending in `.git` is already a clone URL, whatever routes it happens to contain. + if (url === null || url.pathname.replace(/\/$/u, "").endsWith(".git")) return null; // A recognized host's paths are its own, so another host's any-host route never rewrites them. const remote = parseGitRemoteHost(value); const owner = remote === null ? undefined : remoteOwner(remote); - for (const definition of owner ? [owner] : claimOrder) { - const repositoryUrl = definition.repositoryUrlFromPage?.(url); - if (repositoryUrl != null) return repositoryUrl; - } - return null; + if (owner) return owner.repositoryUrlFromPage?.(url) ?? null; + // On an unrecognized host, only rewrite when every route that matches agrees. + const [repositoryUrl, ...others] = new Set( + claimOrder.flatMap((definition) => definition.repositoryUrlFromPage?.(url) ?? []), + ); + return repositoryUrl !== undefined && others.length === 0 ? repositoryUrl : null; }, recognizeRemote: (remoteUrl) => { const remote = parseGitRemoteHost(remoteUrl); From 210dd4cc4d65ca45022cb8b9dea04308891c6838 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 13:24:21 +0000 Subject: [PATCH 06/11] fix(source-control): leave ambiguous src/branch paths on unknown hosts unchanged On a host no definition recognizes, `//src/(branch|tag|commit)/` is equally an extensionless clone URL of a self-managed GitLab project nested under groups named `src` and `branch`, so pasting it must not rewrite it. The page hook now takes `known`, like `parseCommitUrl`. Off a known Forgejo host, Forgejo answers with the page itself: alone that means no rewrite, and next to GitLab's `/-/` answer it disagrees, so that stays unchanged too. AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- .../src/operations/projects.test.ts | 3 ++- .../src/shared/definition.ts | 18 ++++++++++++------ .../src/shared/definition.test.ts | 2 +- .../src/shared/definition.ts | 6 +++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index c08da0b2be2ee..6792a60ea4aef 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -69,7 +69,9 @@ describe("add project shared logic", () => { "https://gitlab.example.test/acme/tools/src/branch/worker.git", "https://gitlab.example.test/acme/tools/src/branch/worker", "https://scm.example.test/acme/tools/src/branch/worker.git", + "https://scm.example.test/acme/tools/src/branch/worker", "https://scm.example.test/team/repo/src/branch/main/-/README.md", + "https://code.company.test/team/repo/src/branch/main/docs", ]) { expect(normalizePastedCloneUrl(gitlabProject)).toBe(gitlabProject); } @@ -83,7 +85,6 @@ describe("add project shared logic", () => { ["https://git.cafe/pingdotgg/t3code/tree/main", "/tree/", "t3code"], ["https://gitlab.example.test/g/p/-/blob/main/src/branch/x", "/-/", "p"], ["https://code.company.test/g/p/-/tree/main", "/-/", "p"], - ["https://code.company.test/team/repo/src/branch/main/docs", "/src/", "repo"], ] as const) { const cloneUrl = normalizePastedCloneUrl(pageUrl); expect(cloneUrl).not.toContain(pageRoute); diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 079e7c6af8e26..38abc7dc4e424 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -258,8 +258,11 @@ export interface SourceControlDefinition { readonly parseCommitUrl?: (url: URL, known: boolean) => CommitLink | null; /** Null when the URL is not one of this host's change requests. */ readonly parseChangeRequestUrl: (url: URL) => ParsedChangeRequestUrl | null; - /** The repository URL when `url` is a page inside one of this host's repositories. */ - readonly repositoryUrlFromPage?: (url: URL) => string | null; + /** + * The repository URL when `url` is a page inside one of this host's repositories. `known` is set + * when the caller already knows this host serves the URL's repository. + */ + readonly repositoryUrlFromPage?: (url: URL, known: boolean) => string | null; /** * The change request URL a `reference` autolink might name, such as a GitHub `#123` written * through the issue route. Only a candidate: the caller must read it as a change request @@ -769,12 +772,15 @@ export function makeSourceControlDefinitionRegistry( // A recognized host's paths are its own, so another host's any-host route never rewrites them. const remote = parseGitRemoteHost(value); const owner = remote === null ? undefined : remoteOwner(remote); - if (owner) return owner.repositoryUrlFromPage?.(url) ?? null; - // On an unrecognized host, only rewrite when every route that matches agrees. + if (owner) return owner.repositoryUrlFromPage?.(url, true) ?? null; + // On an unrecognized host, only rewrite when every route that matches agrees. A route that + // answers with the page itself says it may already be the clone URL. const [repositoryUrl, ...others] = new Set( - claimOrder.flatMap((definition) => definition.repositoryUrlFromPage?.(url) ?? []), + claimOrder.flatMap((definition) => definition.repositoryUrlFromPage?.(url, false) ?? []), ); - return repositoryUrl !== undefined && others.length === 0 ? repositoryUrl : null; + return repositoryUrl !== undefined && repositoryUrl !== url.href && others.length === 0 + ? repositoryUrl + : null; }, recognizeRemote: (remoteUrl) => { const remote = parseGitRemoteHost(remoteUrl); diff --git a/packages/source-control-forgejo/src/shared/definition.test.ts b/packages/source-control-forgejo/src/shared/definition.test.ts index 107a43a6d4cd2..ccf88aa461def 100644 --- a/packages/source-control-forgejo/src/shared/definition.test.ts +++ b/packages/source-control-forgejo/src/shared/definition.test.ts @@ -106,7 +106,7 @@ describe("Forgejo change request URLs", () => { describe("Forgejo repository pages", () => { it("reads the repository from branch, tag and commit views", () => { - const repositoryUrl = (url: string) => definition.repositoryUrlFromPage?.(new URL(url)); + const repositoryUrl = (url: string) => definition.repositoryUrlFromPage?.(new URL(url), true); expect(repositoryUrl("https://codeberg.org/forgejo/forgejo/src/branch/forgejo/README.md")).toBe( "https://codeberg.org/forgejo/forgejo", ); diff --git a/packages/source-control-forgejo/src/shared/definition.ts b/packages/source-control-forgejo/src/shared/definition.ts index 44b4b5bcb238e..d953f2cb53180 100644 --- a/packages/source-control-forgejo/src/shared/definition.ts +++ b/packages/source-control-forgejo/src/shared/definition.ts @@ -89,7 +89,11 @@ export const definition = defineSourceControl({ changeRequestUrlFromReferenceAutolink: () => null, commitUrl: (input) => buildCommitUrl(input, "commit"), parseCommitUrl: (url) => parseCommitPath(url, COMMIT_PATH), - repositoryUrlFromPage: (url) => repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH), + repositoryUrlFromPage: (url, known) => { + const repositoryUrl = repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH); + // Elsewhere the same path is a GitLab project nested under groups named `src` and `branch`. + return repositoryUrl === null || known ? repositoryUrl : url.href; + }, changeRequestActions: new Set(["merge", "close", "reopen", "update-branch"] as const), // Neither `fj` nor `tea` checks out by number. checkoutCommand: ({ number, repositoryUrl }) => From 930287edd40da9ef0fe686ac3fe9605d2bba4bfa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 13:29:56 +0000 Subject: [PATCH 07/11] docs(source-control): note the page-as-answer convention for repositoryUrlFromPage AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- packages/source-control-core/src/shared/definition.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index 38abc7dc4e424..f3128e4aadeda 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -260,7 +260,8 @@ export interface SourceControlDefinition { readonly parseChangeRequestUrl: (url: URL) => ParsedChangeRequestUrl | null; /** * The repository URL when `url` is a page inside one of this host's repositories. `known` is set - * when the caller already knows this host serves the URL's repository. + * when the caller already knows this host serves the URL's repository. Off a known host, return + * `url.href` when the path may already be a clone URL, so no rewrite happens. */ readonly repositoryUrlFromPage?: (url: URL, known: boolean) => string | null; /** From 82e473297ea21f7ff2b2c12a746bd58de63f68e4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 13:32:54 +0000 Subject: [PATCH 08/11] fix(source-control): page URLs on known hosts map even when the ref ends in .git On a recognized host its own page route already tells a page from a clone URL, so the .git clone-URL guard only applies to unrecognized hosts. AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- packages/client-runtime/src/operations/projects.test.ts | 2 ++ packages/source-control-core/src/shared/definition.ts | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 6792a60ea4aef..855c183f359b9 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -85,6 +85,8 @@ describe("add project shared logic", () => { ["https://git.cafe/pingdotgg/t3code/tree/main", "/tree/", "t3code"], ["https://gitlab.example.test/g/p/-/blob/main/src/branch/x", "/-/", "p"], ["https://code.company.test/g/p/-/tree/main", "/-/", "p"], + ["https://github.com/pingdotgg/t3code/tree/release.git", "/tree/", "t3code"], + ["https://gitlab.com/g/p/-/blob/main/template.git", "/-/", "p"], ] as const) { const cloneUrl = normalizePastedCloneUrl(pageUrl); expect(cloneUrl).not.toContain(pageRoute); diff --git a/packages/source-control-core/src/shared/definition.ts b/packages/source-control-core/src/shared/definition.ts index f3128e4aadeda..a94ed6708e2d6 100644 --- a/packages/source-control-core/src/shared/definition.ts +++ b/packages/source-control-core/src/shared/definition.ts @@ -768,12 +768,13 @@ export function makeSourceControlDefinitionRegistry( }, repositoryUrlFromPage: (value) => { const url = httpUrl(value); - // A path ending in `.git` is already a clone URL, whatever routes it happens to contain. - if (url === null || url.pathname.replace(/\/$/u, "").endsWith(".git")) return null; + if (url === null) return null; // A recognized host's paths are its own, so another host's any-host route never rewrites them. const remote = parseGitRemoteHost(value); const owner = remote === null ? undefined : remoteOwner(remote); if (owner) return owner.repositoryUrlFromPage?.(url, true) ?? null; + // Elsewhere a path ending in `.git` is already a clone URL, whatever routes it contains. + if (url.pathname.replace(/\/$/u, "").endsWith(".git")) return null; // On an unrecognized host, only rewrite when every route that matches agrees. A route that // answers with the page itself says it may already be the clone URL. const [repositoryUrl, ...others] = new Set( From e3c800dd2cb73e1392680e157bbac942b7a1ca2b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 15:05:18 +0000 Subject: [PATCH 09/11] fix(source-control): Forgejo clone URLs under a src/branch mount stay unchanged Forgejo may be mounted below any path, including one containing src/branch/, so a path ending in .git is always its clone URL. Also tells users that a pasted branch or file page clones the default branch. AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- docs/user/source-control.md | 3 ++- packages/client-runtime/src/operations/projects.test.ts | 1 + packages/source-control-forgejo/src/shared/definition.ts | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/user/source-control.md b/docs/user/source-control.md index 39731cb3d35b6..c0543aa7baae7 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -155,7 +155,8 @@ publish it. If Git has no name or email on that machine, the project is created first commit. Use **Add Project** in the command palette (`Cmd/Ctrl+K`) to clone a repository. Choose a hosting -provider or paste a Git URL, then choose where to save it. The project opens right away while the +provider or paste a Git URL, then choose where to save it. A branch or file page copied from the +browser also works: T3 Code clones that repository's default branch. The project opens right away while the clone runs in the background: you can write your first prompt, and sending waits until the files are in place. A toast tracks progress and lets you cancel; if the clone fails, retry it from the toast or from the banner above the composer. When the host refuses the machine's SSH key, T3 Code diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 855c183f359b9..bdb5578673043 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -72,6 +72,7 @@ describe("add project shared logic", () => { "https://scm.example.test/acme/tools/src/branch/worker", "https://scm.example.test/team/repo/src/branch/main/-/README.md", "https://code.company.test/team/repo/src/branch/main/docs", + "https://forgejo.example.test/a/b/src/branch/mount/team/repo.git", ]) { expect(normalizePastedCloneUrl(gitlabProject)).toBe(gitlabProject); } diff --git a/packages/source-control-forgejo/src/shared/definition.ts b/packages/source-control-forgejo/src/shared/definition.ts index d953f2cb53180..65360496f28d1 100644 --- a/packages/source-control-forgejo/src/shared/definition.ts +++ b/packages/source-control-forgejo/src/shared/definition.ts @@ -90,6 +90,8 @@ export const definition = defineSourceControl({ commitUrl: (input) => buildCommitUrl(input, "commit"), parseCommitUrl: (url) => parseCommitPath(url, COMMIT_PATH), repositoryUrlFromPage: (url, known) => { + // A mount path may itself contain `src/branch/`, so a `.git` path is always the clone URL. + if (url.pathname.replace(/\/$/u, "").endsWith(".git")) return null; const repositoryUrl = repositoryUrlFromPagePath(url, REPOSITORY_PAGE_PATH); // Elsewhere the same path is a GitLab project nested under groups named `src` and `branch`. return repositoryUrl === null || known ? repositoryUrl : url.href; From aa5b807bdf166c723586f8f3522b40123e0f2f72 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 15:25:54 +0000 Subject: [PATCH 10/11] docs(source-control): name the hosts whose page URLs clone AI-assisted; verified with the tests and tools listed above. Co-Authored-By: Claude Opus 5.5 --- docs/user/source-control.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/user/source-control.md b/docs/user/source-control.md index c0543aa7baae7..15e125587cb6e 100644 --- a/docs/user/source-control.md +++ b/docs/user/source-control.md @@ -155,8 +155,9 @@ publish it. If Git has no name or email on that machine, the project is created first commit. Use **Add Project** in the command palette (`Cmd/Ctrl+K`) to clone a repository. Choose a hosting -provider or paste a Git URL, then choose where to save it. A branch or file page copied from the -browser also works: T3 Code clones that repository's default branch. The project opens right away while the +provider or paste a Git URL, then choose where to save it. A branch or file page copied from GitHub, +GitLab, GitCafe, Codeberg or Bitbucket also works: T3 Code clones that repository's default branch. +On other hosts, paste the repository's clone URL. The project opens right away while the clone runs in the background: you can write your first prompt, and sending waits until the files are in place. A toast tracks progress and lets you cancel; if the clone fails, retry it from the toast or from the banner above the composer. When the host refuses the machine's SSH key, T3 Code From 8a23cc7c3507047b92aa52c5460fce637c71ad5e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 19:39:58 +0000 Subject: [PATCH 11/11] test(client-runtime): keep one pasted page case per path through the registry AI-assisted; verified with the tests and tools listed above. --- packages/client-runtime/src/operations/projects.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index bdb5578673043..698754f069ccc 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -65,13 +65,9 @@ describe("add project shared logic", () => { expect(normalizePastedCloneUrl("/srv/git/repo.git")).toBe("/srv/git/repo.git"); for (const gitlabProject of [ "https://gitlab.com/acme/tools/src/branch/worker.git", - "https://gitlab.com/acme/tools/src/branch/worker", - "https://gitlab.example.test/acme/tools/src/branch/worker.git", - "https://gitlab.example.test/acme/tools/src/branch/worker", "https://scm.example.test/acme/tools/src/branch/worker.git", "https://scm.example.test/acme/tools/src/branch/worker", "https://scm.example.test/team/repo/src/branch/main/-/README.md", - "https://code.company.test/team/repo/src/branch/main/docs", "https://forgejo.example.test/a/b/src/branch/mount/team/repo.git", ]) { expect(normalizePastedCloneUrl(gitlabProject)).toBe(gitlabProject); @@ -81,13 +77,11 @@ describe("add project shared logic", () => { it("clones the repository when given a page URL copied from the browser", () => { for (const [pageUrl, pageRoute, repository] of [ ["https://github.com/pingdotgg/t3code/tree/main", "/tree/", "t3code"], - ["https://github.com/pingdotgg/t3code/blob/main/README.md", "/blob/", "t3code"], ["https://gitlab.com/gitlab-org/gitlab-runner/-/tree/main", "/-/", "gitlab-runner"], ["https://git.cafe/pingdotgg/t3code/tree/main", "/tree/", "t3code"], ["https://gitlab.example.test/g/p/-/blob/main/src/branch/x", "/-/", "p"], ["https://code.company.test/g/p/-/tree/main", "/-/", "p"], ["https://github.com/pingdotgg/t3code/tree/release.git", "/tree/", "t3code"], - ["https://gitlab.com/g/p/-/blob/main/template.git", "/-/", "p"], ] as const) { const cloneUrl = normalizePastedCloneUrl(pageUrl); expect(cloneUrl).not.toContain(pageRoute);