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";...@@ -88,6 +88,7 @@ import { resolvePathLinkTarget } from "~/terminal-links";
88import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";88import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
89import { readLocalApi } from "~/localApi";89import { readLocalApi } from "~/localApi";
90import { getSourceControlPresentation } from "~/sourceControlPresentation";90import { getSourceControlPresentation } from "~/sourceControlPresentation";
91import { openPullRequestLink } from "~/lib/openPullRequestLink";
9192
92interface GitActionsControlProps {93interface GitActionsControlProps {
93 gitCwd: string | null;94 gitCwd: string | null;
...@@ -1229,7 +1230,8 @@ export default function GitActionsControl({...@@ -1229,7 +1230,8 @@ export default function GitActionsControl({
1229 });1230 });
1230 return;1231 return;
1231 }1232 }
1232 void api.shell.openExternal(prUrl).catch((err: unknown) => {1233 void openPullRequestLink(api.shell, prUrl).catch((err: unknown) => {
1234 console.error(err);
1233 toastManager.add(1235 toastManager.add(
1234 stackedThreadToast({1236 stackedThreadToast({
1235 type: "error",1237 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 @@...@@ -1,8 +1,45 @@
1import type { LocalApi } from "@t3tools/contracts";
2import * as Schema from "effect/Schema";
1import { type MouseEvent, useCallback } from "react";3import { type MouseEvent, useCallback } from "react";
24
3import { stackedThreadToast, toastManager } from "../components/ui/toast";5import { stackedThreadToast, toastManager } from "../components/ui/toast";
4import { readLocalApi } from "../localApi";6import { 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
6/**43/**
7 * Returns a click handler that opens a pull request URL in the system browser.44 * Returns a click handler that opens a pull request URL in the system browser.
8 *45 *
...@@ -24,7 +61,8 @@ export function useOpenPrLink() {...@@ -24,7 +61,8 @@ export function useOpenPrLink() {
24 return;61 return;
25 }62 }
2663
27 void api.shell.openExternal(prUrl).catch((error) => {64 void openPullRequestLink(api.shell, prUrl).catch((error) => {
65 console.error(error);
28 toastManager.add(66 toastManager.add(
29 stackedThreadToast({67 stackedThreadToast({
30 type: "error",68 type: "error",