authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 16:44:22-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 16:44:22-07:00
loga9460bb772a8e25d39213ae62e73934152d44495
treef47ad46a75e74870aea3b054214820922fb9ac6c
parent5edf7c56bafeeaf4d5af1a63f010dde45e6652dd
signature Signed by PGP key B5690EEEBB952194

[codex] Structure pull request link failures (#3445)

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 72 insertions(+), 2 deletions(-)

apps/web/src/components/GitActionsControl.tsx+3-1
......@@ -88,6 +88,7 @@ import { resolvePathLinkTarget } from "~/terminal-links";
8888import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
8989import { readLocalApi } from "~/localApi";
9090import { getSourceControlPresentation } from "~/sourceControlPresentation";
91import { openPullRequestLink } from "~/lib/openPullRequestLink";
9192
9293interface GitActionsControlProps {
9394 gitCwd: string | null;
......@@ -1229,7 +1230,8 @@ export default function GitActionsControl({
12291230 });
12301231 return;
12311232 }
1232 void api.shell.openExternal(prUrl).catch((err: unknown) => {
1233 void openPullRequestLink(api.shell, prUrl).catch((err: unknown) => {
1234 console.error(err);
12331235 toastManager.add(
12341236 stackedThreadToast({
12351237 type: "error",
apps/web/src/lib/openPullRequestLink.test.ts created+30
......@@ -0,0 +1,30 @@
1import { describe, expect, it, vi } from "vite-plus/test";
2
3import { openPullRequestLink, PullRequestLinkOpenError } from "./openPullRequestLink";
4
5describe("openPullRequestLink", () => {
6 it("opens the requested pull request URL", async () => {
7 const openExternal = vi.fn(async () => undefined);
8 const targetUrl = "https://github.com/pingdotgg/t3code/pull/123";
9
10 await openPullRequestLink({ openExternal }, targetUrl);
11
12 expect(openExternal).toHaveBeenCalledExactlyOnceWith(targetUrl);
13 });
14
15 it("reports bridge failures with a safe target origin", async () => {
16 const cause = new Error("desktop shell unavailable");
17 const targetUrl = "https://github.com/pingdotgg/t3code/pull/123?token=secret";
18 const openExternal = vi.fn(async () => Promise.reject(cause));
19
20 const result = openPullRequestLink({ openExternal }, targetUrl);
21
22 await expect(result).rejects.toEqual(
23 new PullRequestLinkOpenError({
24 targetOrigin: "https://github.com",
25 cause,
26 }),
27 );
28 await expect(result).rejects.not.toHaveProperty("message", expect.stringContaining("secret"));
29 });
30});
apps/web/src/lib/openPullRequestLink.ts+39-1
......@@ -1,8 +1,45 @@
1import type { LocalApi } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
13import { type MouseEvent, useCallback } from "react";
24
35import { stackedThreadToast, toastManager } from "../components/ui/toast";
46import { readLocalApi } from "../localApi";
57
8export class PullRequestLinkOpenError extends Schema.TaggedErrorClass<PullRequestLinkOpenError>()(
9 "PullRequestLinkOpenError",
10 {
11 targetOrigin: Schema.NullOr(Schema.String),
12 cause: Schema.Defect(),
13 },
14) {
15 static fromCause(targetUrl: string, cause: unknown): PullRequestLinkOpenError {
16 let targetOrigin: string | null = null;
17 try {
18 targetOrigin = new URL(targetUrl).origin;
19 } catch {
20 // Keep malformed URLs out of diagnostics while preserving the open failure below.
21 }
22 return new PullRequestLinkOpenError({ targetOrigin, cause });
23 }
24
25 override get message(): string {
26 return this.targetOrigin === null
27 ? "Unable to open pull request link."
28 : `Unable to open pull request link at ${this.targetOrigin}.`;
29 }
30}
31
32export async function openPullRequestLink(
33 shell: Pick<LocalApi["shell"], "openExternal">,
34 targetUrl: string,
35): Promise<void> {
36 try {
37 await shell.openExternal(targetUrl);
38 } catch (cause) {
39 throw PullRequestLinkOpenError.fromCause(targetUrl, cause);
40 }
41}
42
643/**
744 * Returns a click handler that opens a pull request URL in the system browser.
845 *
......@@ -24,7 +61,8 @@ export function useOpenPrLink() {
2461 return;
2562 }
2663
27 void api.shell.openExternal(prUrl).catch((error) => {
64 void openPullRequestLink(api.shell, prUrl).catch((error) => {
65 console.error(error);
2866 toastManager.add(
2967 stackedThreadToast({
3068 type: "error",