From d120aa72990c0886f104c65c218fee04112572a0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 16:36:39 -0700 Subject: [PATCH] Structure pull request link failures Co-authored-by: codex --- apps/web/src/components/GitActionsControl.tsx | 4 +- apps/web/src/lib/openPullRequestLink.test.ts | 30 ++++++++++++++ apps/web/src/lib/openPullRequestLink.ts | 40 ++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/openPullRequestLink.test.ts diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index af3f7b47286..c9816719452 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -88,6 +88,7 @@ import { resolvePathLinkTarget } from "~/terminal-links"; import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { readLocalApi } from "~/localApi"; import { getSourceControlPresentation } from "~/sourceControlPresentation"; +import { openPullRequestLink } from "~/lib/openPullRequestLink"; interface GitActionsControlProps { gitCwd: string | null; @@ -1229,7 +1230,8 @@ export default function GitActionsControl({ }); return; } - void api.shell.openExternal(prUrl).catch((err: unknown) => { + void openPullRequestLink(api.shell, prUrl).catch((err: unknown) => { + console.error(err); toastManager.add( stackedThreadToast({ type: "error", diff --git a/apps/web/src/lib/openPullRequestLink.test.ts b/apps/web/src/lib/openPullRequestLink.test.ts new file mode 100644 index 00000000000..756e1ed6ad9 --- /dev/null +++ b/apps/web/src/lib/openPullRequestLink.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +import { openPullRequestLink, PullRequestLinkOpenError } from "./openPullRequestLink"; + +describe("openPullRequestLink", () => { + it("opens the requested pull request URL", async () => { + const openExternal = vi.fn(async () => undefined); + const targetUrl = "https://github.com/pingdotgg/t3code/pull/123"; + + await openPullRequestLink({ openExternal }, targetUrl); + + expect(openExternal).toHaveBeenCalledExactlyOnceWith(targetUrl); + }); + + it("reports bridge failures with a safe target origin", async () => { + const cause = new Error("desktop shell unavailable"); + const targetUrl = "https://github.com/pingdotgg/t3code/pull/123?token=secret"; + const openExternal = vi.fn(async () => Promise.reject(cause)); + + const result = openPullRequestLink({ openExternal }, targetUrl); + + await expect(result).rejects.toEqual( + new PullRequestLinkOpenError({ + targetOrigin: "https://github.com", + cause, + }), + ); + await expect(result).rejects.not.toHaveProperty("message", expect.stringContaining("secret")); + }); +}); diff --git a/apps/web/src/lib/openPullRequestLink.ts b/apps/web/src/lib/openPullRequestLink.ts index 899e5c38c58..acd3c5a062b 100644 --- a/apps/web/src/lib/openPullRequestLink.ts +++ b/apps/web/src/lib/openPullRequestLink.ts @@ -1,8 +1,45 @@ +import type { LocalApi } from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; import { type MouseEvent, useCallback } from "react"; import { stackedThreadToast, toastManager } from "../components/ui/toast"; import { readLocalApi } from "../localApi"; +export class PullRequestLinkOpenError extends Schema.TaggedErrorClass()( + "PullRequestLinkOpenError", + { + targetOrigin: Schema.NullOr(Schema.String), + cause: Schema.Defect(), + }, +) { + static fromCause(targetUrl: string, cause: unknown): PullRequestLinkOpenError { + let targetOrigin: string | null = null; + try { + targetOrigin = new URL(targetUrl).origin; + } catch { + // Keep malformed URLs out of diagnostics while preserving the open failure below. + } + return new PullRequestLinkOpenError({ targetOrigin, cause }); + } + + override get message(): string { + return this.targetOrigin === null + ? "Unable to open pull request link." + : `Unable to open pull request link at ${this.targetOrigin}.`; + } +} + +export async function openPullRequestLink( + shell: Pick, + targetUrl: string, +): Promise { + try { + await shell.openExternal(targetUrl); + } catch (cause) { + throw PullRequestLinkOpenError.fromCause(targetUrl, cause); + } +} + /** * Returns a click handler that opens a pull request URL in the system browser. * @@ -24,7 +61,8 @@ export function useOpenPrLink() { return; } - void api.shell.openExternal(prUrl).catch((error) => { + void openPullRequestLink(api.shell, prUrl).catch((error) => { + console.error(error); toastManager.add( stackedThreadToast({ type: "error",