diff --git a/ui/src/components/github-link-target.ts b/ui/src/components/github-link-target.ts index d5666340eaf7..63b95984abf3 100644 --- a/ui/src/components/github-link-target.ts +++ b/ui/src/components/github-link-target.ts @@ -13,7 +13,7 @@ export type GitHubLinkTarget = GitHubItemTarget & { href: string; }; -function decodePathSegment(value: string): string | null { +export function decodeGitHubPathSegment(value: string): string | null { try { const decoded = decodeURIComponent(value).trim(); return decoded && decoded !== "." && decoded !== ".." ? decoded : null; @@ -22,10 +22,10 @@ function decodePathSegment(value: string): string | null { } } -function parseGitHubItemPath(url: URL): GitHubItemTarget | null { +export function parseGitHubItemPath(url: URL): GitHubItemTarget | null { const segments = url.pathname.split("/").filter(Boolean); - const owner = decodePathSegment(segments[0] ?? ""); - const repo = decodePathSegment(segments[1] ?? ""); + const owner = decodeGitHubPathSegment(segments[0] ?? ""); + const repo = decodeGitHubPathSegment(segments[1] ?? ""); const surface = segments[2]; const numberText = segments[3] ?? ""; if (!owner || !repo || !/^[1-9]\d{0,9}$/.test(numberText)) { @@ -52,35 +52,12 @@ export function parseGitHubLinkTarget(href: string): GitHubLinkTarget | null { return target ? { ...target, href: url.href } : null; } -export function formatGitHubLinkLabel(url: URL): string { - const segments = url.pathname.split("/").filter(Boolean); - const item = parseGitHubItemPath(url); - if (item && segments.length === 4 && !url.search && !url.hash) { - return `#${item.number}`; - } - if (item) { - return url.href; - } - if (segments.length === 2) { - return segments.map((segment) => decodePathSegment(segment) ?? segment).join("/"); - } - if (segments[2] === "blob" && segments.length > 4) { - const filename = decodePathSegment(segments.at(-1) ?? ""); - if (filename) { - return filename; - } - } - const fallbackSegments = segments.length > 2 ? segments.slice(2) : segments; - const path = fallbackSegments.map((segment) => decodePathSegment(segment) ?? segment); - return ["github.com", ...path].join("/"); -} - export function gitHubProfileUrl(login: string): string { return `https://${GITHUB_HOST}/${encodeURIComponent(login)}`; } -// Built from the parsed parts rather than the source href, which isGitHubItemRootPath -// shows may already carry its own sub-path, query, or comment fragment. +// Build from parsed parts because the source href may already carry its own +// sub-path, query, or comment fragment. export function gitHubFilesChangedUrl(target: GitHubItemTarget): string { const repoPath = `${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}`; return `https://${GITHUB_HOST}/${repoPath}/pull/${target.number}/files`; diff --git a/ui/src/components/markdown-parser.ts b/ui/src/components/markdown-parser.ts index 790c82d77211..875b2edfb505 100644 --- a/ui/src/components/markdown-parser.ts +++ b/ui/src/components/markdown-parser.ts @@ -3,7 +3,7 @@ import markdownItTaskLists from "markdown-it-task-lists"; import type Token from "markdown-it/lib/token.mjs"; import { t } from "../i18n/index.ts"; import { fileKindForPath, shortestFileLabels } from "./file-kind.ts"; -import { formatGitHubLinkLabel } from "./github-link-target.ts"; +import { decodeGitHubPathSegment, parseGitHubItemPath } from "./github-link-target.ts"; import { installAssistantTranscriptRoleImageRenderer, installAssistantTranscriptRoleMarkdown, @@ -108,6 +108,26 @@ function parseWebLinkHref(href: string): URL | null { return url.protocol === "https:" || url.protocol === "http:" ? url : null; } +function formatGitHubLinkLabel(url: URL): string { + const segments = url.pathname.split("/").filter(Boolean); + const item = parseGitHubItemPath(url); + if (item) { + return segments.length === 4 && !url.search && !url.hash ? `#${item.number}` : url.href; + } + if (segments.length === 2) { + return segments.map((segment) => decodeGitHubPathSegment(segment) ?? segment).join("/"); + } + if (segments[2] === "blob" && segments.length > 4) { + const filename = decodeGitHubPathSegment(segments.at(-1) ?? ""); + if (filename) { + return filename; + } + } + const fallbackSegments = segments.length > 2 ? segments.slice(2) : segments; + const path = fallbackSegments.map((segment) => decodeGitHubPathSegment(segment) ?? segment); + return ["github.com", ...path].join("/"); +} + function isFileLinkBoundaryBefore(value: string, index: number): boolean { const char = value[index - 1]; return char === undefined || /\s/.test(char) || "([{<\"'`".includes(char); diff --git a/ui/src/components/markdown-tables.test.ts b/ui/src/components/markdown-tables.test.ts index af9952afc522..225e05e89184 100644 --- a/ui/src/components/markdown-tables.test.ts +++ b/ui/src/components/markdown-tables.test.ts @@ -77,16 +77,11 @@ function interactiveOwner(): { scrollLeft: { configurable: true, value: 0, writable: true }, scrollWidth: { configurable: true, value: 300 }, }); + owner.addEventListener("click", handleMarkdownTableInteraction); enhanceMarkdownTables(owner); return { owner, shell, viewport }; } -function markdownTableInteractionEvent(target: Element): Event { - const event = new MouseEvent("click", { bubbles: true }); - Object.defineProperty(event, "target", { value: target }); - return event; -} - describe("Markdown table interactions", () => { beforeEach(() => { TestMutationObserver.instances = []; @@ -174,8 +169,7 @@ describe("Markdown table interactions", () => { vi.useFakeTimers(); const { owner } = interactiveOwner(); const copy = owner.querySelector(".markdown-table__copy")!; - - handleMarkdownTableInteraction(markdownTableInteractionEvent(copy)); + copy.click(); expect(writeText).toHaveBeenCalledWith("Name\tValue\nAlpha\tOne"); await vi.advanceTimersByTimeAsync(0); @@ -190,8 +184,7 @@ describe("Markdown table interactions", () => { const { owner } = interactiveOwner(); const expand = owner.querySelector(".markdown-table__expand")!; expand.focus(); - - handleMarkdownTableInteraction(markdownTableInteractionEvent(expand)); + expand.click(); const dialog = document.querySelector(".markdown-table-dialog")!; expect(dialog.hasAttribute("open")).toBe(true); @@ -212,10 +205,7 @@ describe("Markdown table interactions", () => { expect(document.querySelector(".markdown-table-dialog")).toBeNull(); expect(document.activeElement).toBe(expand); - // Reopen through the handler like every other interaction here: the file - // runs in the isolated lane, so no shared-graph document listener exists - // to service a raw click(). - handleMarkdownTableInteraction(markdownTableInteractionEvent(expand)); + expand.click(); const reopenedDialog = document.querySelector(".markdown-table-dialog")!; reopenedDialog.querySelector(".markdown-table-dialog__close")!.click();