| author | |
| committer | |
| log | a9460bb772a8e25d39213ae62e73934152d44495 |
| tree | f47ad46a75e74870aea3b054214820922fb9ac6c |
| parent | 5edf7c56bafeeaf4d5af1a63f010dde45e6652dd |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 88 | import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; |
| 89 | 89 | import { readLocalApi } from "~/localApi"; |
| 90 | 90 | import { getSourceControlPresentation } from "~/sourceControlPresentation"; |
| 91 | import { openPullRequestLink } from "~/lib/openPullRequestLink"; | |
| 91 | 92 | |
| 92 | 93 | interface GitActionsControlProps { |
| 93 | 94 | gitCwd: string | null; |
| ... | ... | @@ -1229,7 +1230,8 @@ export default function GitActionsControl({ |
| 1229 | 1230 | }); |
| 1230 | 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 | 1235 | toastManager.add( |
| 1234 | 1236 | stackedThreadToast({ |
| 1235 | 1237 | type: "error", |
apps/web/src/lib/openPullRequestLink.test.ts created+30| ... | ... | @@ -0,0 +1,30 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { openPullRequestLink, PullRequestLinkOpenError } from "./openPullRequestLink"; | |
| 4 | ||
| 5 | describe("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 | import type { LocalApi } from "@t3tools/contracts"; | |
| 2 | import * as Schema from "effect/Schema"; | |
| 1 | 3 | import { type MouseEvent, useCallback } from "react"; |
| 2 | 4 | |
| 3 | 5 | import { stackedThreadToast, toastManager } from "../components/ui/toast"; |
| 4 | 6 | import { readLocalApi } from "../localApi"; |
| 5 | 7 | |
| 8 | export 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 | ||
| 32 | export 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 | 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 | 61 | return; |
| 25 | 62 | } |
| 26 | 63 | |
| 27 | void api.shell.openExternal(prUrl).catch((error) => { | |
| 64 | void openPullRequestLink(api.shell, prUrl).catch((error) => { | |
| 65 | console.error(error); | |
| 28 | 66 | toastManager.add( |
| 29 | 67 | stackedThreadToast({ |
| 30 | 68 | type: "error", |