| 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,6 +88,7 @@ import { resolvePathLinkTarget } from "~/terminal-links"; |
| 88 | import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; | 88 | import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; |
| 89 | import { readLocalApi } from "~/localApi"; | 89 | import { readLocalApi } from "~/localApi"; |
| 90 | import { getSourceControlPresentation } from "~/sourceControlPresentation"; | 90 | import { getSourceControlPresentation } from "~/sourceControlPresentation"; |
| 91 | import { openPullRequestLink } from "~/lib/openPullRequestLink"; | ||
| 91 | 92 | ||
| 92 | interface GitActionsControlProps { | 93 | interface 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 @@ | ||
| 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,8 +1,45 @@ |
| 1 | import type { LocalApi } from "@t3tools/contracts"; | ||
| 2 | import * as Schema from "effect/Schema"; | ||
| 1 | import { type MouseEvent, useCallback } from "react"; | 3 | import { type MouseEvent, useCallback } from "react"; |
| 2 | 4 | ||
| 3 | import { stackedThreadToast, toastManager } from "../components/ui/toast"; | 5 | import { stackedThreadToast, toastManager } from "../components/ui/toast"; |
| 4 | import { readLocalApi } from "../localApi"; | 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 | * 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 | } |
| 26 | 63 | ||
| 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", |