diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b134f71bb..fddbb23e0 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -55,7 +55,6 @@ import { isArm64HostRunningIntelBuild, resolveDesktopRuntimeInfo } from "./runti syncShellEnvironment(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; -const CAPTURE_WINDOW_CHANNEL = "desktop:capture-window"; const CONFIRM_CHANNEL = "desktop:confirm"; const SET_THEME_CHANNEL = "desktop:set-theme"; const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity"; @@ -1178,19 +1177,6 @@ function registerIpcHandlers(): void { return result.filePaths[0] ?? null; }); - ipcMain.removeHandler(CAPTURE_WINDOW_CHANNEL); - ipcMain.handle(CAPTURE_WINDOW_CHANNEL, async (event) => { - try { - const image = await event.sender.capturePage(); - if (image.isEmpty()) { - return null; - } - return image.toDataURL(); - } catch { - return null; - } - }); - ipcMain.removeHandler(CONFIRM_CHANNEL); ipcMain.handle(CONFIRM_CHANNEL, async (_event, message: unknown) => { if (typeof message !== "string") { diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 1c2b64a9d..833624787 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -2,7 +2,6 @@ import { contextBridge, ipcRenderer } from "electron"; import type { DesktopBridge } from "@okcode/contracts"; const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; -const CAPTURE_WINDOW_CHANNEL = "desktop:capture-window"; const CONFIRM_CHANNEL = "desktop:confirm"; const SET_THEME_CHANNEL = "desktop:set-theme"; const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity"; @@ -36,7 +35,6 @@ const wsUrl = process.env.OKCODE_DESKTOP_WS_URL ?? null; contextBridge.exposeInMainWorld("desktopBridge", { getWsUrl: () => wsUrl, - captureWindow: () => ipcRenderer.invoke(CAPTURE_WINDOW_CHANNEL), pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL), confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message), setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme), diff --git a/apps/web/package.json b/apps/web/package.json index 2716bdbf9..d265b5d45 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -39,7 +39,6 @@ "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "effect": "catalog:", - "html-to-image": "^1.11.13", "lexical": "^0.41.0", "lucide-react": "^0.564.0", "oxfmt": "^0.44.0", diff --git a/apps/web/src/components/ScreenshotTool.tsx b/apps/web/src/components/ScreenshotTool.tsx deleted file mode 100644 index bbc4c7f07..000000000 --- a/apps/web/src/components/ScreenshotTool.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { toPng } from "html-to-image"; -import { CameraIcon, XIcon } from "lucide-react"; - -import { useScreenshotStore } from "~/screenshotStore"; -import { readDesktopPreviewBridge } from "~/desktopPreview"; -import { toastManager } from "~/components/ui/toast"; -import { Button } from "~/components/ui/button"; -import { Tooltip, TooltipTrigger, TooltipPopup } from "~/components/ui/tooltip"; -import { readDesktopBridge } from "~/lib/runtimeBridge"; -import { buildDomCaptureOptions, captureBaseScreenshotDataUrl } from "~/lib/screenshotCapture"; -import { cn, isMacPlatform } from "~/lib/utils"; - -// ── Types ─────────────────────────────────────────────────────────── - -interface SelectionRect { - startX: number; - startY: number; - endX: number; - endY: number; -} - -function normalizeRect(rect: SelectionRect) { - return { - x: Math.min(rect.startX, rect.endX), - y: Math.min(rect.startY, rect.endY), - width: Math.abs(rect.endX - rect.startX), - height: Math.abs(rect.endY - rect.startY), - }; -} - -// ── Capture Logic ─────────────────────────────────────────────────── - -/** - * Find the browser preview surface element's bounds (if visible). - */ -function getPreviewSurfaceBounds(): { x: number; y: number; width: number; height: number } | null { - const el = document.querySelector("[data-preview-surface='true']"); - if (!el) return null; - const rect = el.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0) return null; - return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; -} - -/** - * Check whether two rectangles overlap. - */ -function rectsOverlap( - a: { x: number; y: number; width: number; height: number }, - b: { x: number; y: number; width: number; height: number }, -): boolean { - return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; -} - -async function captureRegion(rect: { - x: number; - y: number; - width: number; - height: number; -}): Promise { - const dpr = window.devicePixelRatio || 1; - - // Capture the full page at device resolution (DOM only — native BrowserView is excluded) - const rootElement = document.documentElement; - const desktopBridge = readDesktopBridge(); - const dataUrl = await captureBaseScreenshotDataUrl({ - captureWindow: () => desktopBridge?.captureWindow() ?? Promise.resolve(null), - captureDom: () => - toPng( - rootElement, - buildDomCaptureOptions({ - rootElement, - pixelRatio: dpr, - }), - ), - }); - - // Load into an Image to crop - const img = await loadImage(dataUrl); - - // Crop to the selected region - const canvas = document.createElement("canvas"); - const cropX = rect.x * dpr; - const cropY = rect.y * dpr; - const cropW = rect.width * dpr; - const cropH = rect.height * dpr; - - canvas.width = cropW; - canvas.height = cropH; - - const ctx = canvas.getContext("2d"); - if (!ctx) throw new Error("Failed to get canvas 2D context"); - - ctx.drawImage(img, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH); - - // Composite the native browser preview if the selection overlaps it - const previewBridge = readDesktopPreviewBridge(); - const surfaceBounds = getPreviewSurfaceBounds(); - if (previewBridge && surfaceBounds && rectsOverlap(rect, surfaceBounds)) { - try { - const browserDataUrl = await previewBridge.captureActiveTab(); - if (browserDataUrl) { - const browserImg = await loadImage(browserDataUrl); - // Position the browser image relative to the crop area - const drawX = (surfaceBounds.x - rect.x) * dpr; - const drawY = (surfaceBounds.y - rect.y) * dpr; - const drawW = surfaceBounds.width * dpr; - const drawH = surfaceBounds.height * dpr; - ctx.drawImage(browserImg, drawX, drawY, drawW, drawH); - } - } catch { - // If browser capture fails, the DOM-only screenshot is still valid - } - } - - return new Promise((resolve, reject) => { - canvas.toBlob( - (blob) => { - if (blob) resolve(blob); - else reject(new Error("Canvas toBlob returned null")); - }, - "image/png", - 1.0, - ); - }); -} - -function loadImage(src: string): Promise { - return new Promise((resolve, reject) => { - const img = new Image(); - const cleanup = () => { - img.removeEventListener("load", onLoad); - img.removeEventListener("error", onError); - }; - const onLoad = () => { - cleanup(); - resolve(img); - }; - const onError = () => { - cleanup(); - reject(new Error("Failed to load image")); - }; - img.addEventListener("load", onLoad); - img.addEventListener("error", onError); - img.src = src; - }); -} - -async function copyBlobToClipboard(blob: Blob): Promise { - await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]); -} - -function downloadBlob(blob: Blob, filename: string) { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -} - -// ── Minimum selection threshold ───────────────────────────────────── - -const MIN_SELECTION_SIZE = 8; - -// ── Selection Overlay ─────────────────────────────────────────────── - -function ScreenshotOverlay() { - const deactivate = useScreenshotStore((s) => s.deactivate); - const [selection, setSelection] = useState(null); - const [isCapturing, setIsCapturing] = useState(false); - const isDragging = useRef(false); - const overlayRef = useRef(null); - - // Cancel on Escape - useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - e.stopPropagation(); - deactivate(); - } - }; - window.addEventListener("keydown", onKeyDown, true); - return () => window.removeEventListener("keydown", onKeyDown, true); - }, [deactivate]); - - const handleMouseDown = useCallback( - (e: React.MouseEvent) => { - if (isCapturing) return; - e.preventDefault(); - isDragging.current = true; - setSelection({ - startX: e.clientX, - startY: e.clientY, - endX: e.clientX, - endY: e.clientY, - }); - }, - [isCapturing], - ); - - const handleMouseMove = useCallback( - (e: React.MouseEvent) => { - if (!isDragging.current || isCapturing) return; - setSelection((prev) => (prev ? { ...prev, endX: e.clientX, endY: e.clientY } : null)); - }, - [isCapturing], - ); - - const handleMouseUp = useCallback(async () => { - if (!isDragging.current || isCapturing) return; - isDragging.current = false; - - if (!selection) { - deactivate(); - return; - } - - const rect = normalizeRect(selection); - - // If the selection is too small, treat as a cancelled click - if (rect.width < MIN_SELECTION_SIZE || rect.height < MIN_SELECTION_SIZE) { - setSelection(null); - return; - } - - setIsCapturing(true); - - try { - const blob = await captureRegion(rect); - - // Copy to clipboard - await copyBlobToClipboard(blob); - - toastManager.add({ - type: "success", - title: "Screenshot copied", - description: "Image copied to clipboard", - data: { dismissAfterVisibleMs: 3000 }, - actionProps: { - children: "Save file", - onClick: () => { - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); - downloadBlob(blob, `screenshot-${timestamp}.png`); - }, - }, - }); - } catch (error) { - console.error("Screenshot capture failed:", error); - toastManager.add({ - type: "error", - title: "Screenshot failed", - description: error instanceof Error ? error.message : "Could not capture screenshot", - data: { dismissAfterVisibleMs: 5000 }, - }); - } finally { - setIsCapturing(false); - deactivate(); - } - }, [selection, isCapturing, deactivate]); - - const normalized = selection ? normalizeRect(selection) : null; - const hasValidSelection = - normalized && normalized.width >= MIN_SELECTION_SIZE && normalized.height >= MIN_SELECTION_SIZE; - - return ( -
- {/* Dimmed backdrop - uses CSS clip-path to create a "hole" for the selection */} -
- - {/* Selection rectangle border */} - {hasValidSelection && ( -
- {/* Dimension badge */} -
- {Math.round(normalized.width)} x {Math.round(normalized.height)} -
-
- )} - - {/* Instructions banner */} - {!hasValidSelection && !isCapturing && ( -
-
- - Click and drag to select an area - | - Esc - to cancel -
-
- )} - - {/* Capturing indicator */} - {isCapturing && ( -
-
-
- Capturing... -
-
- )} -
- ); -} - -// ── Screenshot Button ─────────────────────────────────────────────── - -function ScreenshotButton() { - const active = useScreenshotStore((s) => s.active); - const toggle = useScreenshotStore((s) => s.toggle); - const isMac = isMacPlatform(navigator.platform); - const shortcutLabel = isMac ? "⌘⇧S" : "Ctrl+Shift+S"; - - return ( - - - } - > - {active ? : } - - - {active ? "Cancel screenshot" : "Take screenshot"} ({shortcutLabel}) - - - ); -} - -// ── Main Export ────────────────────────────────────────────────────── - -function ScreenshotTool() { - const active = useScreenshotStore((s) => s.active); - return active ? : null; -} - -export { ScreenshotTool, ScreenshotButton }; diff --git a/apps/web/src/components/settings/SettingsUi.tsx b/apps/web/src/components/settings/SettingsUi.tsx index e30150005..fea0711a1 100644 --- a/apps/web/src/components/settings/SettingsUi.tsx +++ b/apps/web/src/components/settings/SettingsUi.tsx @@ -5,10 +5,7 @@ import { Input } from "../ui/input"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "../../lib/utils"; import { Undo2Icon } from "lucide-react"; -import { - BACKGROUND_IMAGE_OPACITY_MAX, - BACKGROUND_IMAGE_OPACITY_MIN, -} from "../../appSettings"; +import { BACKGROUND_IMAGE_OPACITY_MAX, BACKGROUND_IMAGE_OPACITY_MIN } from "../../appSettings"; export function SettingsSection({ title, diff --git a/apps/web/src/components/widget/ChatWidgetShell.tsx b/apps/web/src/components/widget/ChatWidgetShell.tsx index b6c33717c..8285edf87 100644 --- a/apps/web/src/components/widget/ChatWidgetShell.tsx +++ b/apps/web/src/components/widget/ChatWidgetShell.tsx @@ -4,7 +4,6 @@ import { Outlet, useParams } from "@tanstack/react-router"; import { useChatWidgetStore } from "../../chatWidgetStore"; import { useAppSettings } from "../../appSettings"; import { CommandPalette } from "../CommandPalette"; -import { ScreenshotTool, ScreenshotButton } from "../ScreenshotTool"; import ThreadSidebar from "../Sidebar"; import { Sidebar, SidebarProvider, SidebarRail } from "../ui/sidebar"; import { ChatWidgetBubble } from "./ChatWidgetBubble"; @@ -58,12 +57,6 @@ export function ChatWidgetShell() { {/* Global utilities available in both modes */} - - {!expanded && ( -
- -
- )} {/* Expanded panel with the full chat layout */} diff --git a/apps/web/src/lib/screenshotCapture.test.ts b/apps/web/src/lib/screenshotCapture.test.ts deleted file mode 100644 index d8025bb76..000000000 --- a/apps/web/src/lib/screenshotCapture.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { buildDomCaptureOptions, captureBaseScreenshotDataUrl } from "./screenshotCapture"; - -describe("screenshotCapture", () => { - it("prefers native desktop window capture when available", async () => { - const captureWindow = vi.fn(async () => "data:image/png;base64,desktop"); - const captureDom = vi.fn(async () => "data:image/png;base64,dom"); - - const result = await captureBaseScreenshotDataUrl({ captureWindow, captureDom }); - - expect(result).toBe("data:image/png;base64,desktop"); - expect(captureWindow).toHaveBeenCalledOnce(); - expect(captureDom).not.toHaveBeenCalled(); - }); - - it("falls back to DOM capture when native window capture is unavailable", async () => { - const captureWindow = vi.fn(async () => null); - const captureDom = vi.fn(async () => "data:image/png;base64,dom"); - - const result = await captureBaseScreenshotDataUrl({ captureWindow, captureDom }); - - expect(result).toBe("data:image/png;base64,dom"); - expect(captureWindow).toHaveBeenCalledOnce(); - expect(captureDom).toHaveBeenCalledOnce(); - }); - - it("skips font embedding and excludes the screenshot overlay from DOM capture", () => { - const rootElement = { - scrollWidth: 1440, - scrollHeight: 900, - } as HTMLElement; - - const options = buildDomCaptureOptions({ - rootElement, - pixelRatio: 2, - }); - - const overlay = { dataset: { screenshotOverlay: "true" } } as unknown as HTMLElement; - const content = { dataset: {} } as unknown as HTMLElement; - - expect(options.width).toBe(1440); - expect(options.height).toBe(900); - expect(options.pixelRatio).toBe(2); - expect(options.skipFonts).toBe(true); - expect(options.filter?.(overlay as HTMLElement)).toBe(false); - expect(options.filter?.(content as HTMLElement)).toBe(true); - }); -}); diff --git a/apps/web/src/lib/screenshotCapture.ts b/apps/web/src/lib/screenshotCapture.ts deleted file mode 100644 index 765b41565..000000000 --- a/apps/web/src/lib/screenshotCapture.ts +++ /dev/null @@ -1,49 +0,0 @@ -const TRANSPARENT_IMAGE_PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA="; - -export interface ScreenshotCaptureRect { - readonly x: number; - readonly y: number; - readonly width: number; - readonly height: number; -} - -export interface DomCaptureOptions { - readonly width: number; - readonly height: number; - readonly pixelRatio: number; - readonly skipFonts: boolean; - readonly imagePlaceholder: string; - readonly onImageErrorHandler: () => undefined; - readonly filter: (node: HTMLElement) => boolean; -} - -export function buildDomCaptureOptions(input: { - readonly rootElement: HTMLElement; - readonly pixelRatio: number; -}): DomCaptureOptions { - return { - width: input.rootElement.scrollWidth, - height: input.rootElement.scrollHeight, - pixelRatio: input.pixelRatio, - skipFonts: true, - imagePlaceholder: TRANSPARENT_IMAGE_PLACEHOLDER, - onImageErrorHandler: () => undefined, - filter: (node: HTMLElement) => { - if ("dataset" in node && node.dataset?.screenshotOverlay === "true") { - return false; - } - return true; - }, - }; -} - -export async function captureBaseScreenshotDataUrl(input: { - readonly captureWindow?: (() => Promise) | null; - readonly captureDom: () => Promise; -}): Promise { - const nativeCapture = await input.captureWindow?.(); - if (typeof nativeCapture === "string" && nativeCapture.length > 0) { - return nativeCapture; - } - return input.captureDom(); -} diff --git a/apps/web/src/routes/-_chat.test.ts b/apps/web/src/routes/-_chat.test.ts index a0ffd7f4e..29adcfc9d 100644 --- a/apps/web/src/routes/-_chat.test.ts +++ b/apps/web/src/routes/-_chat.test.ts @@ -8,4 +8,22 @@ describe("chat route sidebar chrome", () => { expect(src).not.toContain("backdrop-blur-sm"); }); + + it("does not expose screenshot UI or shortcuts in the chat shell", () => { + const chatRouteSrc = readFileSync(resolve(import.meta.dirname, "./_chat.tsx"), "utf8"); + const chatWidgetShellSrc = readFileSync( + resolve(import.meta.dirname, "../components/widget/ChatWidgetShell.tsx"), + "utf8", + ); + + expect(chatRouteSrc).not.toContain("ScreenshotTool"); + expect(chatRouteSrc).not.toContain("ScreenshotButton"); + expect(chatRouteSrc).not.toContain("useScreenshotStore"); + expect(chatRouteSrc).not.toContain("toggleScreenshot"); + expect(chatRouteSrc).not.toContain("Cmd+Shift+S"); + expect(chatRouteSrc).not.toContain("Ctrl+Shift+S"); + + expect(chatWidgetShellSrc).not.toContain("ScreenshotTool"); + expect(chatWidgetShellSrc).not.toContain("ScreenshotButton"); + }); }); diff --git a/apps/web/src/routes/_chat.tsx b/apps/web/src/routes/_chat.tsx index 0a43baaf6..b53b90e22 100644 --- a/apps/web/src/routes/_chat.tsx +++ b/apps/web/src/routes/_chat.tsx @@ -5,7 +5,6 @@ import { type CSSProperties, useEffect } from "react"; import ThreadSidebar from "../components/Sidebar"; import { CommandPalette } from "../components/CommandPalette"; -import { ScreenshotTool, ScreenshotButton } from "../components/ScreenshotTool"; import { WorktreeCleanupDialog } from "../components/WorktreeCleanupDialog"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { ZOOM_STEP, clearZoom, getStoredZoom, setStoredZoom } from "../lib/customTheme"; @@ -17,7 +16,6 @@ import { resolveShortcutCommand } from "../keybindings"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { useThreadSelectionStore } from "../threadSelectionStore"; import { useCommandPaletteStore } from "../commandPaletteStore"; -import { useScreenshotStore } from "../screenshotStore"; import { useStore } from "../store"; import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic"; import { useAppSettings } from "~/appSettings"; @@ -78,7 +76,6 @@ function ChatRouteGlobalShortcuts() { const paletteOpen = useCommandPaletteStore((state) => state.open); const pushMruThread = useCommandPaletteStore((state) => state.pushMruThread); const pushMruProject = useCommandPaletteStore((state) => state.pushMruProject); - const toggleScreenshot = useScreenshotStore((state) => state.toggle); const storeProjects = useStore((state) => state.projects); const storeThreads = useStore((state) => state.threads); const navigate = useNavigate(); @@ -121,14 +118,6 @@ function ChatRouteGlobalShortcuts() { return; } - // ── Screenshot: Cmd+Shift+S (Mac) / Ctrl+Shift+S (non-Mac) ── - if (key === "s" && modKey && event.shiftKey && !event.altKey && !isTerminalFocused()) { - event.preventDefault(); - event.stopPropagation(); - toggleScreenshot(); - return; - } - // ── Project switching: Cmd+1-9 (Mac) / Ctrl+1-9 (non-Mac) ─ if ( modKey && @@ -238,7 +227,6 @@ function ChatRouteGlobalShortcuts() { storeThreads, terminalOpen, togglePalette, - toggleScreenshot, appSettings.defaultThreadEnvMode, ]); @@ -350,10 +338,6 @@ function ChatRouteLayout() { - -
- -
void; - deactivate: () => void; - toggle: () => void; -} - -type ScreenshotStore = ScreenshotState & ScreenshotActions; - -// ── Store ─────────────────────────────────────────────────────────── - -export const useScreenshotStore = create((set, get) => ({ - active: false, - - activate: () => set({ active: true }), - deactivate: () => set({ active: false }), - toggle: () => set({ active: !get().active }), -})); diff --git a/bun.lock b/bun.lock index bcd68abd3..d1063fda4 100644 --- a/bun.lock +++ b/bun.lock @@ -183,7 +183,6 @@ "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "effect": "catalog:", - "html-to-image": "^1.11.13", "lexical": "^0.41.0", "lucide-react": "^0.564.0", "oxfmt": "^0.44.0", @@ -1604,8 +1603,6 @@ "hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="], - "html-to-image": ["html-to-image@1.11.13", "", {}, "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg=="], - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], diff --git a/packages/contracts/src/ipc.ts b/packages/contracts/src/ipc.ts index edfde759e..8de0db744 100644 --- a/packages/contracts/src/ipc.ts +++ b/packages/contracts/src/ipc.ts @@ -243,7 +243,6 @@ export interface PreviewNavigateResult { export interface DesktopBridge { getWsUrl: () => string | null; - captureWindow: () => Promise; pickFolder: () => Promise; confirm: (message: string) => Promise; setTheme: (theme: DesktopTheme) => Promise;