From bc7502a9896d7463999aac92ead6fa48456b1a34 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 06:11:08 -0700 Subject: [PATCH 1/3] [codex] structure cross-client clipboard failures Co-authored-by: codex --- apps/mobile/src/app/settings/environments.tsx | 4 +- .../connection/ConnectionEnvironmentRow.tsx | 2 +- .../EnvironmentConnectionNotice.tsx | 6 +- .../src/features/threads/ThreadFeed.tsx | 8 +- .../mobile/src/lib/copyTextWithHaptic.test.ts | 62 ++++++++++++++- apps/mobile/src/lib/copyTextWithHaptic.ts | 73 +++++++++++++++++- apps/web/src/components/PlanSidebar.tsx | 2 +- .../src/components/chat/ProposedPlanCard.tsx | 1 + apps/web/src/components/ui/toast.tsx | 2 +- apps/web/src/hooks/useCopyToClipboard.test.ts | 60 +++++++++++++++ apps/web/src/hooks/useCopyToClipboard.ts | 75 +++++++++++++++---- 11 files changed, 267 insertions(+), 28 deletions(-) create mode 100644 apps/web/src/hooks/useCopyToClipboard.test.ts diff --git a/apps/mobile/src/app/settings/environments.tsx b/apps/mobile/src/app/settings/environments.tsx index c09bb3cebe6..8f65c630a54 100644 --- a/apps/mobile/src/app/settings/environments.tsx +++ b/apps/mobile/src/app/settings/environments.tsx @@ -397,7 +397,7 @@ function CloudEnvironmentRowShell(props: { className={cn("text-xs leading-[16px] underline", statusClassName)} onLongPress={(event) => { event.stopPropagation(); - copyTextWithHaptic(errorTraceId); + copyTextWithHaptic(errorTraceId, { target: "connection-trace-id" }); }} onPress={(event) => { event.stopPropagation(); @@ -441,7 +441,7 @@ function CopyTraceIdButton(props: { readonly traceId: string }) { { - copyTextWithHaptic(props.traceId); + copyTextWithHaptic(props.traceId, { target: "connection-trace-id" }); }} className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70" > diff --git a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx index f5aa26be960..7b901ec4c66 100644 --- a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx +++ b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx @@ -101,7 +101,7 @@ export function ConnectionEnvironmentRow(props: { className="underline" onLongPress={(event) => { event.stopPropagation(); - copyTextWithHaptic(statusTraceId); + copyTextWithHaptic(statusTraceId, { target: "connection-trace-id" }); }} onPress={(event) => { event.stopPropagation(); diff --git a/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx b/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx index 9b8c96d25ea..373b0d3ef03 100644 --- a/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx +++ b/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx @@ -85,7 +85,11 @@ export function EnvironmentConnectionNotice(props: { accessibilityHint="Copies the trace ID" accessibilityRole="button" className="underline decoration-dotted" - onPress={() => copyTextWithHaptic(props.connection.traceId!)} + onPress={() => + copyTextWithHaptic(props.connection.traceId!, { + target: "connection-trace-id", + }) + } > {props.connection.traceId} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 7424d856368..4f8ca9c747d 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1,4 +1,3 @@ -import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { KeyboardAvoidingLegendList } from "@legendapp/list/keyboard"; import { type LegendListRef } from "@legendapp/list/react-native"; @@ -31,6 +30,7 @@ import { TouchableOpacity } from "react-native-gesture-handler"; import ImageViewing from "react-native-image-viewing"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; +import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { hasNativeSelectableMarkdownText, SelectableMarkdownText, @@ -1321,8 +1321,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { }, []); const onCopyWorkRow = useCallback((rowId: string, value: string) => { - void Clipboard.setStringAsync(value); - void Haptics.selectionAsync(); + copyTextWithHaptic(value, { + target: "thread-work-row", + feedback: "selection", + }); setInteractionState((current) => ({ ...current, copiedRowId: rowId })); if (copyFeedbackTimeoutRef.current) { clearTimeout(copyFeedbackTimeoutRef.current); diff --git a/apps/mobile/src/lib/copyTextWithHaptic.test.ts b/apps/mobile/src/lib/copyTextWithHaptic.test.ts index d15a3a1a59b..1bac3351127 100644 --- a/apps/mobile/src/lib/copyTextWithHaptic.test.ts +++ b/apps/mobile/src/lib/copyTextWithHaptic.test.ts @@ -1,7 +1,8 @@ -import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; const mocks = vi.hoisted(() => ({ impactAsync: vi.fn(), + selectionAsync: vi.fn(), setStringAsync: vi.fn(), })); @@ -14,15 +15,25 @@ vi.mock("expo-haptics", () => ({ Light: "light", }, impactAsync: mocks.impactAsync, + selectionAsync: mocks.selectionAsync, })); -import { copyTextWithHaptic } from "./copyTextWithHaptic"; +import { + CopyTextClipboardWriteError, + CopyTextHapticFeedbackError, + copyTextWithHaptic, +} from "./copyTextWithHaptic"; describe("copyTextWithHaptic", () => { beforeEach(() => { vi.clearAllMocks(); mocks.setStringAsync.mockReturnValue(new Promise(() => undefined)); mocks.impactAsync.mockResolvedValue(undefined); + mocks.selectionAsync.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); }); it("triggers haptic feedback without waiting for the clipboard promise", () => { @@ -31,4 +42,51 @@ describe("copyTextWithHaptic", () => { expect(mocks.setStringAsync).toHaveBeenCalledWith("trace-123"); expect(mocks.impactAsync).toHaveBeenCalledWith("light"); }); + + it("preserves selection feedback for thread work rows", () => { + copyTextWithHaptic("work output", { + target: "thread-work-row", + feedback: "selection", + }); + + expect(mocks.setStringAsync).toHaveBeenCalledWith("work output"); + expect(mocks.selectionAsync).toHaveBeenCalledOnce(); + expect(mocks.impactAsync).not.toHaveBeenCalled(); + }); + + it("reports structured failures without including clipboard contents", async () => { + const clipboardCause = new Error("native clipboard failure"); + const hapticCause = new Error("native haptic failure"); + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); + mocks.setStringAsync.mockRejectedValueOnce(clipboardCause); + mocks.impactAsync.mockRejectedValueOnce(hapticCause); + + copyTextWithHaptic("secret clipboard contents", { target: "connection-trace-id" }); + + await vi.waitFor(() => { + expect(consoleError).toHaveBeenCalledTimes(2); + }); + + const failures = consoleError.mock.calls.map(([failure]) => failure); + const clipboardError = failures.find( + (failure) => failure instanceof CopyTextClipboardWriteError, + ); + expect(clipboardError).toBeInstanceOf(CopyTextClipboardWriteError); + expect(clipboardError).toMatchObject({ + operation: "write-clipboard", + target: "connection-trace-id", + cause: clipboardCause, + }); + expect((clipboardError as Error).message).not.toContain("secret clipboard contents"); + + const hapticError = failures.find((failure) => failure instanceof CopyTextHapticFeedbackError); + expect(hapticError).toBeInstanceOf(CopyTextHapticFeedbackError); + expect(hapticError).toMatchObject({ + operation: "trigger-haptic-feedback", + target: "connection-trace-id", + feedback: "light-impact", + cause: hapticCause, + }); + expect((hapticError as Error).message).not.toContain("secret clipboard contents"); + }); }); diff --git a/apps/mobile/src/lib/copyTextWithHaptic.ts b/apps/mobile/src/lib/copyTextWithHaptic.ts index 80f725f5b00..f9c353a2163 100644 --- a/apps/mobile/src/lib/copyTextWithHaptic.ts +++ b/apps/mobile/src/lib/copyTextWithHaptic.ts @@ -1,7 +1,74 @@ +import * as Schema from "effect/Schema"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; -export function copyTextWithHaptic(value: string): void { - void Clipboard.setStringAsync(value); - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); +export class CopyTextClipboardWriteError extends Schema.TaggedErrorClass()( + "CopyTextClipboardWriteError", + { + operation: Schema.Literal("write-clipboard"), + target: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to copy ${this.target} to the clipboard.`; + } +} + +export class CopyTextHapticFeedbackError extends Schema.TaggedErrorClass()( + "CopyTextHapticFeedbackError", + { + operation: Schema.Literal("trigger-haptic-feedback"), + target: Schema.String, + feedback: Schema.Literals(["light-impact", "selection"]), + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to trigger ${this.feedback} haptic feedback after copying ${this.target}.`; + } +} + +export function copyTextWithHaptic( + value: string, + options: { + readonly target?: string; + readonly feedback?: "light-impact" | "selection"; + } = {}, +): void { + const target = options.target ?? "text"; + const feedback = options.feedback ?? "light-impact"; + + void (async () => { + try { + await Clipboard.setStringAsync(value); + } catch (cause) { + console.error( + new CopyTextClipboardWriteError({ + operation: "write-clipboard", + target, + cause, + }), + ); + } + })(); + + void (async () => { + try { + if (feedback === "selection") { + await Haptics.selectionAsync(); + } else { + await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + } catch (cause) { + console.error( + new CopyTextHapticFeedbackError({ + operation: "trigger-haptic-feedback", + target, + feedback, + cause, + }), + ); + } + })(); } diff --git a/apps/web/src/components/PlanSidebar.tsx b/apps/web/src/components/PlanSidebar.tsx index fec255355a5..abc0db79b6c 100644 --- a/apps/web/src/components/PlanSidebar.tsx +++ b/apps/web/src/components/PlanSidebar.tsx @@ -83,7 +83,7 @@ const PlanSidebar = memo(function PlanSidebar({ const writeProjectFile = useAtomCommand(projectEnvironment.writeFile, { reportFailure: false, }); - const { copyToClipboard, isCopied } = useCopyToClipboard(); + const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "plan" }); const planMarkdown = activeProposedPlan?.planMarkdown ?? null; const displayedPlanMarkdown = planMarkdown ? stripDisplayedPlanMarkdown(planMarkdown) : null; diff --git a/apps/web/src/components/chat/ProposedPlanCard.tsx b/apps/web/src/components/chat/ProposedPlanCard.tsx index e507a2f7709..9746857a8ca 100644 --- a/apps/web/src/components/chat/ProposedPlanCard.tsx +++ b/apps/web/src/components/chat/ProposedPlanCard.tsx @@ -54,6 +54,7 @@ export const ProposedPlanCard = memo(function ProposedPlanCard({ reportFailure: false, }); const { copyToClipboard, isCopied } = useCopyToClipboard({ + target: "plan", onError: (error) => { toastManager.add( stackedThreadToast({ diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx index 2c2a554871a..d48cda453b8 100644 --- a/apps/web/src/components/ui/toast.tsx +++ b/apps/web/src/components/ui/toast.tsx @@ -117,7 +117,7 @@ function handleToastDismissClick( } function CopyErrorButton({ text }: { text: string }) { - const { copyToClipboard, isCopied } = useCopyToClipboard(); + const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "error-message" }); const label = isCopied ? "Copied error" : "Copy error"; return ( diff --git a/apps/web/src/hooks/useCopyToClipboard.test.ts b/apps/web/src/hooks/useCopyToClipboard.test.ts new file mode 100644 index 00000000000..ba428f78eb5 --- /dev/null +++ b/apps/web/src/hooks/useCopyToClipboard.test.ts @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; + +import { + ClipboardApiUnavailableError, + ClipboardWriteError, + writeTextToClipboard, +} from "./useCopyToClipboard"; + +describe("writeTextToClipboard", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("reports unavailable clipboard support with structural context", async () => { + vi.stubGlobal("window", {}); + vi.stubGlobal("navigator", {}); + + const error = await writeTextToClipboard("plan contents", "plan").then( + () => undefined, + (cause: unknown) => cause, + ); + + expect(error).toBeInstanceOf(ClipboardApiUnavailableError); + expect(error).toMatchObject({ + operation: "resolve-clipboard-api", + target: "plan", + }); + expect((error as Error).message).not.toContain("plan contents"); + }); + + it("preserves the exact clipboard failure without exposing copied contents", async () => { + const cause = new Error("browser clipboard failure"); + const writeText = vi.fn().mockRejectedValue(cause); + vi.stubGlobal("window", {}); + vi.stubGlobal("navigator", { clipboard: { writeText } }); + + const error = await writeTextToClipboard("secret clipboard contents", "error-message").then( + () => undefined, + (failure: unknown) => failure, + ); + + expect(writeText).toHaveBeenCalledWith("secret clipboard contents"); + expect(error).toBeInstanceOf(ClipboardWriteError); + expect(error).toMatchObject({ + operation: "write-clipboard", + target: "error-message", + cause, + }); + expect((error as Error).message).not.toContain("secret clipboard contents"); + }); + + it("keeps empty values as a no-op when clipboard support is available", async () => { + const writeText = vi.fn(); + vi.stubGlobal("window", {}); + vi.stubGlobal("navigator", { clipboard: { writeText } }); + + await expect(writeTextToClipboard("", "plan")).resolves.toBe(false); + expect(writeText).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/hooks/useCopyToClipboard.ts b/apps/web/src/hooks/useCopyToClipboard.ts index d1feb621159..a73d3f07f91 100644 --- a/apps/web/src/hooks/useCopyToClipboard.ts +++ b/apps/web/src/hooks/useCopyToClipboard.ts @@ -1,11 +1,65 @@ import * as React from "react"; +import * as Schema from "effect/Schema"; + +export class ClipboardApiUnavailableError extends Schema.TaggedErrorClass()( + "ClipboardApiUnavailableError", + { + operation: Schema.Literal("resolve-clipboard-api"), + target: Schema.String, + }, +) { + override get message(): string { + return `Clipboard API is unavailable while copying ${this.target}.`; + } +} + +export class ClipboardWriteError extends Schema.TaggedErrorClass()( + "ClipboardWriteError", + { + operation: Schema.Literal("write-clipboard"), + target: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to copy ${this.target} to the clipboard.`; + } +} + +export async function writeTextToClipboard(value: string, target = "text") { + if ( + typeof window === "undefined" || + typeof navigator === "undefined" || + !navigator.clipboard?.writeText + ) { + throw new ClipboardApiUnavailableError({ + operation: "resolve-clipboard-api", + target, + }); + } + + if (!value) return false; + + try { + await navigator.clipboard.writeText(value); + return true; + } catch (cause) { + throw new ClipboardWriteError({ + operation: "write-clipboard", + target, + cause, + }); + } +} export function useCopyToClipboard({ timeout = 2000, + target = "text", onCopy, onError, }: { timeout?: number; + target?: string; onCopy?: (ctx: TContext) => void; onError?: (error: Error, ctx: TContext) => void; } = {}): { copyToClipboard: (value: string, ctx: TContext) => void; isCopied: boolean } { @@ -13,22 +67,18 @@ export function useCopyToClipboard({ const timeoutIdRef = React.useRef(null); const onCopyRef = React.useRef(onCopy); const onErrorRef = React.useRef(onError); + const targetRef = React.useRef(target); const timeoutRef = React.useRef(timeout); onCopyRef.current = onCopy; onErrorRef.current = onError; + targetRef.current = target; timeoutRef.current = timeout; const copyToClipboard = React.useCallback((value: string, ctx: TContext): void => { - if (typeof window === "undefined" || !navigator.clipboard?.writeText) { - onErrorRef.current?.(new Error("Clipboard API unavailable."), ctx); - return; - } - - if (!value) return; - - navigator.clipboard.writeText(value).then( - () => { + void writeTextToClipboard(value, targetRef.current).then( + (didCopy) => { + if (!didCopy) return; if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } @@ -44,11 +94,8 @@ export function useCopyToClipboard({ } }, (error) => { - if (onErrorRef.current) { - onErrorRef.current(error, ctx); - } else { - console.error(error); - } + console.error(error); + onErrorRef.current?.(error, ctx); }, ); }, []); From ba7a9cecd4d2091c9cb95dad78b0dadac6aad54d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 06:20:17 -0700 Subject: [PATCH 2/3] refactor(web): route trace ID copies through clipboard helper Co-authored-by: codex --- .../components/settings/DiagnosticsSettings.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/settings/DiagnosticsSettings.tsx b/apps/web/src/components/settings/DiagnosticsSettings.tsx index 6df3367c642..92d8d3f6827 100644 --- a/apps/web/src/components/settings/DiagnosticsSettings.tsx +++ b/apps/web/src/components/settings/DiagnosticsSettings.tsx @@ -32,6 +32,7 @@ import { } from "../../state/server"; import { shellEnvironment } from "../../state/shell"; import { usePrimaryEnvironment } from "../../state/environments"; +import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { Button } from "../ui/button"; import { ScrollArea } from "../ui/scroll-area"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; @@ -246,16 +247,10 @@ function DiagnosticsTable({ } function TraceIdCell({ traceId }: { traceId: string }) { - const [copied, setCopied] = useState(false); - const copyTraceId = useCallback(() => { - void navigator.clipboard - ?.writeText(traceId) - .then(() => { - setCopied(true); - window.setTimeout(() => setCopied(false), 1_200); - }) - .catch(() => undefined); - }, [traceId]); + const { copyToClipboard, isCopied: copied } = useCopyToClipboard({ + target: "trace ID", + timeout: 1_200, + }); return (
@@ -281,7 +276,7 @@ function TraceIdCell({ traceId }: { traceId: string }) { type="button" className="inline-flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground" aria-label={copied ? "Copied trace ID" : "Copy trace ID"} - onClick={copyTraceId} + onClick={() => copyToClipboard(traceId)} > From fa91438adf1c785ad09f737fb701488a48732415 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 08:03:48 -0700 Subject: [PATCH 3/3] Remove redundant clipboard error fields Co-authored-by: codex --- apps/mobile/src/lib/copyTextWithHaptic.test.ts | 2 -- apps/mobile/src/lib/copyTextWithHaptic.ts | 4 ---- apps/web/src/hooks/useCopyToClipboard.test.ts | 2 -- apps/web/src/hooks/useCopyToClipboard.ts | 4 ---- 4 files changed, 12 deletions(-) diff --git a/apps/mobile/src/lib/copyTextWithHaptic.test.ts b/apps/mobile/src/lib/copyTextWithHaptic.test.ts index 1bac3351127..236fb44cd6b 100644 --- a/apps/mobile/src/lib/copyTextWithHaptic.test.ts +++ b/apps/mobile/src/lib/copyTextWithHaptic.test.ts @@ -73,7 +73,6 @@ describe("copyTextWithHaptic", () => { ); expect(clipboardError).toBeInstanceOf(CopyTextClipboardWriteError); expect(clipboardError).toMatchObject({ - operation: "write-clipboard", target: "connection-trace-id", cause: clipboardCause, }); @@ -82,7 +81,6 @@ describe("copyTextWithHaptic", () => { const hapticError = failures.find((failure) => failure instanceof CopyTextHapticFeedbackError); expect(hapticError).toBeInstanceOf(CopyTextHapticFeedbackError); expect(hapticError).toMatchObject({ - operation: "trigger-haptic-feedback", target: "connection-trace-id", feedback: "light-impact", cause: hapticCause, diff --git a/apps/mobile/src/lib/copyTextWithHaptic.ts b/apps/mobile/src/lib/copyTextWithHaptic.ts index f9c353a2163..1cc8c94eef7 100644 --- a/apps/mobile/src/lib/copyTextWithHaptic.ts +++ b/apps/mobile/src/lib/copyTextWithHaptic.ts @@ -5,7 +5,6 @@ import * as Haptics from "expo-haptics"; export class CopyTextClipboardWriteError extends Schema.TaggedErrorClass()( "CopyTextClipboardWriteError", { - operation: Schema.Literal("write-clipboard"), target: Schema.String, cause: Schema.Defect(), }, @@ -18,7 +17,6 @@ export class CopyTextClipboardWriteError extends Schema.TaggedErrorClass()( "CopyTextHapticFeedbackError", { - operation: Schema.Literal("trigger-haptic-feedback"), target: Schema.String, feedback: Schema.Literals(["light-impact", "selection"]), cause: Schema.Defect(), @@ -45,7 +43,6 @@ export function copyTextWithHaptic( } catch (cause) { console.error( new CopyTextClipboardWriteError({ - operation: "write-clipboard", target, cause, }), @@ -63,7 +60,6 @@ export function copyTextWithHaptic( } catch (cause) { console.error( new CopyTextHapticFeedbackError({ - operation: "trigger-haptic-feedback", target, feedback, cause, diff --git a/apps/web/src/hooks/useCopyToClipboard.test.ts b/apps/web/src/hooks/useCopyToClipboard.test.ts index ba428f78eb5..ccac333cd48 100644 --- a/apps/web/src/hooks/useCopyToClipboard.test.ts +++ b/apps/web/src/hooks/useCopyToClipboard.test.ts @@ -22,7 +22,6 @@ describe("writeTextToClipboard", () => { expect(error).toBeInstanceOf(ClipboardApiUnavailableError); expect(error).toMatchObject({ - operation: "resolve-clipboard-api", target: "plan", }); expect((error as Error).message).not.toContain("plan contents"); @@ -42,7 +41,6 @@ describe("writeTextToClipboard", () => { expect(writeText).toHaveBeenCalledWith("secret clipboard contents"); expect(error).toBeInstanceOf(ClipboardWriteError); expect(error).toMatchObject({ - operation: "write-clipboard", target: "error-message", cause, }); diff --git a/apps/web/src/hooks/useCopyToClipboard.ts b/apps/web/src/hooks/useCopyToClipboard.ts index a73d3f07f91..0129f2d6593 100644 --- a/apps/web/src/hooks/useCopyToClipboard.ts +++ b/apps/web/src/hooks/useCopyToClipboard.ts @@ -4,7 +4,6 @@ import * as Schema from "effect/Schema"; export class ClipboardApiUnavailableError extends Schema.TaggedErrorClass()( "ClipboardApiUnavailableError", { - operation: Schema.Literal("resolve-clipboard-api"), target: Schema.String, }, ) { @@ -16,7 +15,6 @@ export class ClipboardApiUnavailableError extends Schema.TaggedErrorClass()( "ClipboardWriteError", { - operation: Schema.Literal("write-clipboard"), target: Schema.String, cause: Schema.Defect(), }, @@ -33,7 +31,6 @@ export async function writeTextToClipboard(value: string, target = "text") { !navigator.clipboard?.writeText ) { throw new ClipboardApiUnavailableError({ - operation: "resolve-clipboard-api", target, }); } @@ -45,7 +42,6 @@ export async function writeTextToClipboard(value: string, target = "text") { return true; } catch (cause) { throw new ClipboardWriteError({ - operation: "write-clipboard", target, cause, });