From 9f8285095992ff93b00d93458e43217543e310af Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Fri, 17 Jul 2026 17:01:24 -0700 Subject: [PATCH] fix(desktop): route text copies through native clipboard (#2054) Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> --- desktop/src-tauri/src/commands/clipboard.rs | 36 ++++++++++++++++ .../src-tauri/src/commands/media_download.rs | 41 ++++++++----------- desktop/src-tauri/src/commands/mod.rs | 2 + desktop/src-tauri/src/lib.rs | 6 +-- .../channels/ui/ChannelManagementSheet.tsx | 7 ++-- .../ui/ChannelManagementSheetRows.tsx | 3 +- desktop/src/features/chat/ui/ChatHeader.tsx | 3 +- .../features/communities/ui/WelcomeSetup.tsx | 3 +- .../ui/InviteLinkSection.tsx | 3 +- .../onboarding/ui/MembershipDenied.tsx | 3 +- .../onboarding/ui/NsecMaskedDisplay.tsx | 3 +- .../profile/ui/NostrBindConsentDialog.tsx | 3 +- .../projects/ui/ProjectCommitCopyButton.tsx | 3 +- .../src/features/pulse/lib/useNoteActions.ts | 3 +- .../settings/ui/MobilePairingCard.tsx | 3 +- .../settings/ui/ProfileSettingsCard.tsx | 3 +- .../src/features/sidebar/ui/CommunityRail.tsx | 3 +- desktop/src/shared/lib/clipboard.ts | 16 ++++---- desktop/src/shared/lib/codeBlockClipboard.ts | 9 ++-- desktop/tests/e2e/inbox-live-update.spec.ts | 2 +- desktop/tests/e2e/typing-latency.perf.ts | 9 ---- 21 files changed, 100 insertions(+), 64 deletions(-) create mode 100644 desktop/src-tauri/src/commands/clipboard.rs diff --git a/desktop/src-tauri/src/commands/clipboard.rs b/desktop/src-tauri/src/commands/clipboard.rs new file mode 100644 index 000000000..b4fe072ef --- /dev/null +++ b/desktop/src-tauri/src/commands/clipboard.rs @@ -0,0 +1,36 @@ +use std::sync::Mutex; + +use tauri::Manager; + +/// App-lifetime clipboard ownership keeps copied data available on Linux and +/// serializes access on Windows. All operations still run on Tauri's main +/// thread for macOS/AppKit safety. +pub struct ClipboardState(Mutex>); + +impl ClipboardState { + pub fn new() -> Self { + Self(Mutex::new(None)) + } + + pub fn release(&self) { + if let Ok(mut clipboard) = self.0.lock() { + clipboard.take(); + } + } +} + +pub fn with_clipboard( + app: &tauri::AppHandle, + operation: impl FnOnce(&mut arboard::Clipboard) -> Result, +) -> Result { + let state = app.state::(); + let mut stored = state + .0 + .lock() + .map_err(|_| "clipboard state lock poisoned".to_string())?; + if stored.is_none() { + *stored = Some(arboard::Clipboard::new().map_err(|e| format!("clipboard error: {e}"))?); + } + operation(stored.as_mut().expect("clipboard initialized")) + .map_err(|e| format!("clipboard error: {e}")) +} diff --git a/desktop/src-tauri/src/commands/media_download.rs b/desktop/src-tauri/src/commands/media_download.rs index 1ba691597..c182986cb 100644 --- a/desktop/src-tauri/src/commands/media_download.rs +++ b/desktop/src-tauri/src/commands/media_download.rs @@ -3,6 +3,7 @@ use sha2::{Digest, Sha256}; use tauri::State; use crate::app_state::AppState; +use crate::commands::clipboard::with_clipboard; use crate::commands::export_util::save_bytes_with_dialog; use crate::commands::media::{detect_and_validate_mime, mint_media_get_auth, sanitize_filename}; use crate::commands::{ @@ -201,18 +202,15 @@ pub async fn copy_image_to_clipboard( // arboard requires main-thread access on macOS. Use a sync channel so the // async command can await the result. let (tx, rx) = std::sync::mpsc::sync_channel::>(1); + let clipboard_app = app.clone(); app.run_on_main_thread(move || { - let result = arboard::Clipboard::new() - .map_err(|e| format!("clipboard error: {e}")) - .and_then(|mut clipboard| { - clipboard - .set_image(arboard::ImageData { - width, - height, - bytes: std::borrow::Cow::Owned(raw), - }) - .map_err(|e| format!("clipboard error: {e}")) - }); + let result = with_clipboard(&clipboard_app, |clipboard| { + clipboard.set_image(arboard::ImageData { + width, + height, + bytes: std::borrow::Cow::Owned(raw), + }) + }); // Ignore send errors — the receiver dropped only if the command was // cancelled, in which case nobody is waiting for the result. let _ = tx.send(result); @@ -235,20 +233,15 @@ pub async fn copy_text_to_clipboard( app: tauri::AppHandle, ) -> Result<(), String> { let (tx, rx) = std::sync::mpsc::sync_channel::>(1); + let clipboard_app = app.clone(); app.run_on_main_thread(move || { - let result = arboard::Clipboard::new() - .map_err(|e| format!("clipboard error: {e}")) - .and_then(|mut clipboard| { - if let Some(html) = html { - clipboard - .set_html(html, Some(text)) - .map_err(|e| format!("clipboard error: {e}")) - } else { - clipboard - .set_text(text) - .map_err(|e| format!("clipboard error: {e}")) - } - }); + let result = with_clipboard(&clipboard_app, |clipboard| { + if let Some(html) = html { + clipboard.set_html(html, Some(text)) + } else { + clipboard.set_text(text) + } + }); let _ = tx.send(result); }) .map_err(|e| format!("main thread dispatch failed: {e}"))?; diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 3e39efb99..eaa608c33 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -12,6 +12,7 @@ mod canvas; mod channel_templates; mod channel_window; mod channels; +mod clipboard; mod dms; mod engrams; mod export_util; @@ -61,6 +62,7 @@ pub use canvas::*; pub use channel_templates::*; pub use channel_window::*; pub use channels::*; +pub use clipboard::*; pub use dms::*; pub use engrams::*; pub use global_agent_config::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index dadd2198b..da1ea50f7 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,7 +1,5 @@ -// Deep async call chains (mesh ensure→download→start under Tauri command -// futures) exceed the default query depth when computing layouts. +// Deep async call chains under Tauri command futures exceed the default query depth when computing layouts. #![recursion_limit = "256"] - mod app_state; mod archive; mod commands; @@ -450,6 +448,7 @@ pub fn run() { }); }) .manage(build_app_state()) + .manage(ClipboardState::new()) .manage(PendingCommunityDeepLinks::default()) .manage(commands::pairing::PairingHandle::new()) .setup(move |app| { @@ -980,6 +979,7 @@ pub fn run() { } RunEvent::Exit => { shut_down_app(app_handle, &run_shutdown_done); + app_handle.state::().release(); #[cfg(all(feature = "mesh-llm", target_os = "macos"))] if restart_requested.load(Ordering::SeqCst) { diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx index 3feed2842..1bf08a110 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx @@ -84,6 +84,7 @@ import { ToggleRow, } from "./ChannelManagementSheetRows"; import { ChannelManagementModerationActions } from "./ChannelManagementModerationActions"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type ChannelManagementSheetProps = { channel: Channel | null; @@ -788,9 +789,9 @@ function ChannelManagementPanelContent({ icon={Copy} label="Copy ID" onClick={() => { - void navigator.clipboard - .writeText(resolvedChannel.id) - .then(() => toast.success("Copied channel ID")); + void writeTextToClipboard(resolvedChannel.id).then(() => + toast.success("Copied channel ID"), + ); }} testId="channel-management-copy-id-action" /> diff --git a/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx b/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx index 825bad974..f666398f1 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheetRows.tsx @@ -12,6 +12,7 @@ import { toast } from "sonner"; import type { Channel } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { Switch } from "@/shared/ui/switch"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; function getChannelIcon(channelType: Channel["channelType"]): LucideIcon { if (channelType === "forum") { @@ -126,7 +127,7 @@ export function CopyFieldRow({ testId?: string; }) { async function handleCopy() { - await navigator.clipboard.writeText(value); + await writeTextToClipboard(value); toast.success(`Copied ${label.toLowerCase()}`); } diff --git a/desktop/src/features/chat/ui/ChatHeader.tsx b/desktop/src/features/chat/ui/ChatHeader.tsx index b20a45188..9ced50675 100644 --- a/desktop/src/features/chat/ui/ChatHeader.tsx +++ b/desktop/src/features/chat/ui/ChatHeader.tsx @@ -18,6 +18,7 @@ import { UpdateIndicator } from "@/features/settings/UpdateIndicator"; import { cn } from "@/shared/lib/cn"; import { channelChrome } from "@/shared/layout/chromeLayout"; import { Button } from "@/shared/ui/button"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type ChatHeaderProps = { actions?: React.ReactNode; @@ -104,7 +105,7 @@ export function ChatHeader({ if (!value) return; try { - await navigator.clipboard.writeText(value); + await writeTextToClipboard(value); toast.success("Channel name copied"); } catch { toast.error("Failed to copy channel name"); diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index a7f7218d9..c4412bd2d 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -23,6 +23,7 @@ import { Button } from "@/shared/ui/button"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type WelcomeSetupPage = "welcome" | "join" | "invite"; type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection; @@ -190,7 +191,7 @@ export function WelcomeSetup({ className="h-10 w-10 shrink-0 text-muted-foreground hover:text-foreground" disabled={!npub} onClick={() => { - void navigator.clipboard.writeText(npub).then(() => { + void writeTextToClipboard(npub).then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 1500); }); diff --git a/desktop/src/features/community-members/ui/InviteLinkSection.tsx b/desktop/src/features/community-members/ui/InviteLinkSection.tsx index 9852b6881..19d73b6d2 100644 --- a/desktop/src/features/community-members/ui/InviteLinkSection.tsx +++ b/desktop/src/features/community-members/ui/InviteLinkSection.tsx @@ -14,6 +14,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/shared/ui/dropdown-menu"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; const TTL_OPTIONS: { label: string; value: number }[] = [ { label: "1 day", value: 24 * 60 * 60 }, @@ -72,7 +73,7 @@ export function InviteLinkSection() { async function handleCopy() { if (!invite) return; try { - await navigator.clipboard.writeText(invite.url); + await writeTextToClipboard(invite.url); setCopied(true); toast.success("Invite link copied"); setTimeout(() => setCopied(false), 2000); diff --git a/desktop/src/features/onboarding/ui/MembershipDenied.tsx b/desktop/src/features/onboarding/ui/MembershipDenied.tsx index ce7df5425..3de6857d3 100644 --- a/desktop/src/features/onboarding/ui/MembershipDenied.tsx +++ b/desktop/src/features/onboarding/ui/MembershipDenied.tsx @@ -9,6 +9,7 @@ import { Input } from "@/shared/ui/input"; import { Spinner } from "@/shared/ui/spinner"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; import { InviteRedeemForm } from "./InviteRedeemForm"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type MembershipDeniedProps = { /** The relay that denied membership — used as the target for bare-code invites. */ @@ -53,7 +54,7 @@ export function MembershipDenied({ const handleCopy = React.useCallback(async () => { try { - await navigator.clipboard.writeText(npub); + await writeTextToClipboard(npub); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { diff --git a/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx b/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx index 148ead60c..8092eeb0c 100644 --- a/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx +++ b/desktop/src/features/onboarding/ui/NsecMaskedDisplay.tsx @@ -1,6 +1,7 @@ import { Check, Copy, Eye, EyeOff } from "lucide-react"; import * as React from "react"; import { Button } from "@/shared/ui/button"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type NsecMaskedDisplayProps = { nsec: string; @@ -43,7 +44,7 @@ export function NsecMaskedDisplay({ } async function handleCopy() { - await navigator.clipboard.writeText(nsec); + await writeTextToClipboard(nsec); setIsCopied(true); if (copyTimerRef.current) clearTimeout(copyTimerRef.current); copyTimerRef.current = setTimeout(() => setIsCopied(false), 2000); diff --git a/desktop/src/features/profile/ui/NostrBindConsentDialog.tsx b/desktop/src/features/profile/ui/NostrBindConsentDialog.tsx index 3d32c756f..976d51bfb 100644 --- a/desktop/src/features/profile/ui/NostrBindConsentDialog.tsx +++ b/desktop/src/features/profile/ui/NostrBindConsentDialog.tsx @@ -16,6 +16,7 @@ import { cn } from "@/shared/lib/cn"; import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; import { Button } from "@/shared/ui/button"; import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; const COPY_SUCCESS_MESSAGE = "Signed response copied. Paste it into the Buzz admin console."; @@ -103,7 +104,7 @@ function formatError(error: unknown): string { async function copyToClipboard(text: string): Promise { try { - await navigator.clipboard.writeText(text); + await writeTextToClipboard(text); return true; } catch (error) { console.warn("copy signed nostr binding response failed:", error); diff --git a/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx b/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx index 70e7d2a20..0930146e2 100644 --- a/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx +++ b/desktop/src/features/projects/ui/ProjectCommitCopyButton.tsx @@ -2,6 +2,7 @@ import { Check, Copy } from "lucide-react"; import * as React from "react"; import { cn } from "@/shared/lib/cn"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; /** Icon button that copies arbitrary text with a brief check feedback. */ export function CopyTextButton({ @@ -15,7 +16,7 @@ export function CopyTextButton({ }) { const [copied, setCopied] = React.useState(false); const handleCopy = React.useCallback(() => { - void navigator.clipboard.writeText(text).then(() => { + void writeTextToClipboard(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2_000); }); diff --git a/desktop/src/features/pulse/lib/useNoteActions.ts b/desktop/src/features/pulse/lib/useNoteActions.ts index 329d31d74..8178cdc53 100644 --- a/desktop/src/features/pulse/lib/useNoteActions.ts +++ b/desktop/src/features/pulse/lib/useNoteActions.ts @@ -17,6 +17,7 @@ import { toggleNoteIdInSet, } from "@/features/pulse/lib/noteActions"; import type { UserNote } from "@/shared/api/socialTypes"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; export type PulseNoteActions = { isReplySending: boolean; @@ -144,7 +145,7 @@ export function usePulseNoteActions({ const share = React.useCallback(async (note: UserNote) => { try { - await navigator.clipboard.writeText(buildNoteShareUri(note)); + await writeTextToClipboard(buildNoteShareUri(note)); toast.success("Copied note link"); } catch { toast.error("Failed to copy note link"); diff --git a/desktop/src/features/settings/ui/MobilePairingCard.tsx b/desktop/src/features/settings/ui/MobilePairingCard.tsx index fab4aa158..4f53f4161 100644 --- a/desktop/src/features/settings/ui/MobilePairingCard.tsx +++ b/desktop/src/features/settings/ui/MobilePairingCard.tsx @@ -28,6 +28,7 @@ import { } from "@/shared/ui/dialog"; import { SettingsOptionGroup, SettingsOptionRow } from "./SettingsOptionGroup"; import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type PairingStep = | "generating" @@ -168,7 +169,7 @@ function PairingDialog({ async function handleCopy() { if (!qrUri) return; - await navigator.clipboard.writeText(qrUri); + await writeTextToClipboard(qrUri); toast.success("Copied to clipboard"); } diff --git a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx index b7122d7b7..d09a4fed6 100644 --- a/desktop/src/features/settings/ui/ProfileSettingsCard.tsx +++ b/desktop/src/features/settings/ui/ProfileSettingsCard.tsx @@ -36,6 +36,7 @@ import { Input } from "@/shared/ui/input"; import { Spinner } from "@/shared/ui/spinner"; import { Textarea } from "@/shared/ui/textarea"; import { SettingsSectionHeader } from "./SettingsSectionHeader"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type ProfileSettingsCardProps = { currentPubkey?: string; @@ -85,7 +86,7 @@ function IdentityRow({ className="inline-flex shrink-0 items-center gap-1.5 rounded-full bg-muted px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" data-testid={`copy-${testId}`} onClick={async () => { - await navigator.clipboard.writeText(copyValue); + await writeTextToClipboard(copyValue); toast.success("Copied to clipboard"); }} title={`Copy ${label}`} diff --git a/desktop/src/features/sidebar/ui/CommunityRail.tsx b/desktop/src/features/sidebar/ui/CommunityRail.tsx index f54de4dd3..a234d6cf2 100644 --- a/desktop/src/features/sidebar/ui/CommunityRail.tsx +++ b/desktop/src/features/sidebar/ui/CommunityRail.tsx @@ -21,6 +21,7 @@ import { cn } from "@/shared/lib/cn"; import { getInitials } from "@/shared/lib/initials"; import { isMacPlatform } from "@/shared/lib/platform"; import { useIsFullscreen } from "@/shared/lib/useIsFullscreen"; +import { writeTextToClipboard } from "@/shared/lib/clipboard"; type CommunityRailProps = { communities: Community[]; @@ -223,7 +224,7 @@ export function CommunityRail({ { - void navigator.clipboard.writeText(community.relayUrl); + void writeTextToClipboard(community.relayUrl); }} > diff --git a/desktop/src/shared/lib/clipboard.ts b/desktop/src/shared/lib/clipboard.ts index cd246bca1..93e2bed44 100644 --- a/desktop/src/shared/lib/clipboard.ts +++ b/desktop/src/shared/lib/clipboard.ts @@ -1,16 +1,18 @@ import { toast } from "sonner"; -/** - * Copy plain text to the clipboard with a success toast, surfacing - * `writeText` rejections (permissions, unfocused document) as an error - * toast instead of an unhandled rejection. - */ +import { copyTextToSystemClipboard } from "@/shared/api/tauriMedia"; + +/** Write plain text through the native clipboard integration. */ +export async function writeTextToClipboard(text: string): Promise { + await copyTextToSystemClipboard(text); +} + +/** Copy plain text and show standard success/error feedback. */ export function copyTextToClipboard( text: string, successMessage = "Copied to clipboard", ) { - void navigator.clipboard - .writeText(text) + void writeTextToClipboard(text) .then(() => { toast.success(successMessage); }) diff --git a/desktop/src/shared/lib/codeBlockClipboard.ts b/desktop/src/shared/lib/codeBlockClipboard.ts index bc6d50063..00f0067f6 100644 --- a/desktop/src/shared/lib/codeBlockClipboard.ts +++ b/desktop/src/shared/lib/codeBlockClipboard.ts @@ -1,3 +1,5 @@ +import { copyTextToSystemClipboard } from "@/shared/api/tauriMedia"; + const BUZZ_CODE_BLOCK_ATTRIBUTE = "data-buzz-code-block"; function escapeHtml(value: string) { @@ -15,13 +17,10 @@ function createBuzzCodeBlockHtml(code: string) { export async function copyCodeBlockToClipboard(code: string) { const clipboard = navigator.clipboard; - if (!clipboard) { - throw new Error("Clipboard API is unavailable"); - } if ( typeof ClipboardItem !== "undefined" && - typeof clipboard.write === "function" + typeof clipboard?.write === "function" ) { try { await clipboard.write([ @@ -38,7 +37,7 @@ export async function copyCodeBlockToClipboard(code: string) { } } - await clipboard.writeText(code); + await copyTextToSystemClipboard(code); } export function getBuzzCodeBlockClipboardText( diff --git a/desktop/tests/e2e/inbox-live-update.spec.ts b/desktop/tests/e2e/inbox-live-update.spec.ts index a27dd4915..94e0b7b54 100644 --- a/desktop/tests/e2e/inbox-live-update.spec.ts +++ b/desktop/tests/e2e/inbox-live-update.spec.ts @@ -1388,7 +1388,7 @@ test.describe("inbox stable-conversation regressions", () => { // be truly centered rather than clamped at the scroll container floor. for (let i = 1; i <= 5; i++) { - const reply = emit({ + emit({ channelName: "general", content: `Reaction-drift later reply ${i} — provides content below the selected message for a non-clamped center.`, parentEventId: fetchRoot.id, diff --git a/desktop/tests/e2e/typing-latency.perf.ts b/desktop/tests/e2e/typing-latency.perf.ts index 715594050..881a42e28 100644 --- a/desktop/tests/e2e/typing-latency.perf.ts +++ b/desktop/tests/e2e/typing-latency.perf.ts @@ -46,15 +46,6 @@ type LatencyReport = { longtaskTotal: number; }; -function quantile(sorted: number[], q: number): number { - if (sorted.length === 0) return 0; - const index = Math.min( - sorted.length - 1, - Math.floor(q * (sorted.length - 1)), - ); - return sorted[index]; -} - async function resetWindowMetrics(page: import("@playwright/test").Page) { await page.evaluate(() => { const store = window as unknown as {