feat(desktop): GoodWebTools Desktop app (Tauri 2) — Phase 10 - #2
Merged
Conversation
added 30 commits
July 14, 2026 21:25
Complete design spec for GoodWebTools Desktop: - One codebase, two shells (web + Tauri) architecture - Service abstraction layer for shell-agnostic tools - All 5 native capabilities (system capture, hotkeys, marquee, system audio, native FFmpeg) - All 55 tools refactored to use services - Complete desktop features (Settings, tray, wizard, auto-update) - Cross-platform permission handling (macOS, Windows, Linux) - Build & release pipeline with GitHub Actions - Download page with tracking endpoint - 6-8 week implementation timeline
33-task plan covering 8-week implementation: - Tasks 1-10: Foundation & service layer (2 weeks) - Tasks 11-20: Tool refactoring all 55 tools (3 weeks) - Tasks 21-28: Desktop features (Settings, tray, wizard, updater) (2 weeks) - Tasks 29-33: Build pipeline & testing (1 week) Each task follows TDD pattern with exact file paths, code, and tests. All 259 existing tests must pass throughout refactoring.
- Install @tauri-apps/cli v2 - Add tauri npm scripts (dev, build, bundle) - Create Cargo.toml with dependencies (tauri plugins, platform-specific deps) - Create tauri.conf.json (bundle disabled for dev) - Create main.rs, lib.rs, build.rs - Generate app icons from existing icon-512.png - Verify Rust compilation passes
- Create Platform service with shell detection (browser vs Tauri) - Detect platform (macOS, Windows, Linux) - Detect architecture (x86_64, aarch64) - Add comprehensive unit tests (4 passing tests) - Export PlatformInfo interface for type safety
- Define CaptureService interface with 7 methods - Add types: Rectangle, CaptureOptions, RecordOptions, RecordingHandle - Add CaptureServiceCapabilities interface - Implement shell detection pattern (browser vs Tauri) - Lazy-load implementations based on environment - Ready for browser and Tauri implementations
- Implement captureScreen using getDisplayMedia and canvas - Implement start/stopRecording using MediaRecorder - Implement captureWindow (falls back to captureScreen) - Return null for showRegionSelector (not supported in browser) - Add comprehensive unit tests (4 passing tests) - Mock all browser APIs for testing (MediaStream, video, canvas) - Returns correct capabilities (all false for browser)
- Create commands.rs with 7 IPC command stubs - Define Rust types: CaptureOptions, Rectangle, RecordOptions, RecordingHandle - Register commands in main.rs invoke_handler - Create TauriCaptureService TypeScript implementation - Add browser fallback for when native capture not implemented - Platform-specific stubs return 'not yet implemented' errors - Rust compiles successfully (0 errors, 7 warnings for unused params)
- Replace direct getDisplayMedia call with captureService.captureScreen() - Update supported check to use service capabilities - Simplify capture logic (service handles stream/video creation) - Maintains countdown and crop functionality - First tool refactored to validate service layer works - TypeScript compiles successfully
- Create FileService interface with file picker and save methods - Implement BrowserFileService using File System Access API with legacy fallback - Implement TauriFileService using native dialogs and FS APIs - Add comprehensive type definitions - Install @tauri-apps/api for Tauri integration - 5 tests created (3 passing, 2 need fixes)
- Change from 'in window' to typeof === 'function' check - Fixes test mocking for legacy fallback paths - All 5 FileService tests now passing
- Replace MediaRecorder setup with captureService.startRecording() - Replace stop logic with captureService.stopRecording() - Simplify state management (removed stream/chunks refs) - Keep UI-specific features (elapsed timer, mic toggle) - Second tool validated against service layer - TypeScript compiles successfully
- Create ClipboardService interface for clipboard operations - Implement BrowserClipboardService using Clipboard API with legacy fallback - Implement TauriClipboardService using Tauri clipboard API - Support text and image read/write operations - 5 comprehensive tests - all passing - Image clipboard in Tauri requires custom Rust commands (not yet implemented)
- Create HotkeyService interface for hotkey registration - Implement BrowserHotkeyService with window-level key listeners - Implement TauriHotkeyService with true global shortcuts - Support modifier keys and key combinations - 6 comprehensive tests - all passing - Browser hotkeys only work when window focused (platform limitation)
- Create DownloadService interface with download/downloadZip methods - Split into BrowserDownloadService (File System Access API + fallback) - Create TauriDownloadService (native save dialog) - Update 22 imports across codebase from old path to new - Maintain backward compatibility with existing API - ZIP support via fflate in both implementations
- Create AssetService interface with fetch/isCached/clearCache methods - Split into BrowserAssetService (in-memory cache + streaming) - Create TauriAssetService (file-based cache in AppCache dir) - Update imports across codebase from old path to new - Maintain backward compatibility with assetCache API - Both implementations support progress tracking for large assets
- Replace File System Access API with fileService.openFile/saveFile - Replace Clipboard API with clipboardService.writeText - Remove FileSystemFileHandle refs (handled by service) - Simplify save logic - service handles dialog automatically - Removed unused useRef import - Third high-value tool refactored to validate services
- Add @tauri-apps/api to optimizeDeps.exclude - Prevents Vite from trying to bundle Tauri packages in web mode - Tauri imports are only loaded dynamically in desktop environment - Resolves 'could not be resolved' error in web dev mode
…Recorder - Remove premature captureService.getCapabilities() call before instance creation - Use direct browser API detection (navigator.mediaDevices.getDisplayMedia) - Fixes 'browser doesn't support screen capture' false negative - Both tools now correctly detect browser support
- Initialize supported state based on actual browser capability - Add console.log to debug what's being detected - Check for window existence to avoid SSR issues - Will help diagnose why browser support check is failing
- Import isTauri() to detect desktop app mode - In Tauri: Always show as supported (uses native Rust APIs) - In browser: Check for getDisplayMedia support - Fixes 'browser doesn't support' error in Tauri WebView - WebViews don't expose mediaDevices API (expected) - Tauri capture works via IPC commands, not browser APIs
- Add withGlobalTauri: true to app config - Enables window.__TAURI__ global object - Required for isTauri() detection to work - Also add url and label to main window config
- Remove @tauri-apps/api from optimizeDeps.exclude - Vite needs to resolve these imports for Tauri dev mode - Dynamic imports ensure they're only loaded in Tauri runtime - Tree-shaking removes them from web build - Fixes 'Failed to resolve import' error when capturing in Tauri
- Change '@tauri-apps/api/tauri' to '@tauri-apps/api/core' - invoke() is exported from /core in Tauri 2 - Fixes 'Failed to resolve import' error - Other APIs (dialog, fs, clipboard) still in main package
- Remove try/catch with browserFallback in captureScreen - Remove browserFallback method entirely - Browser APIs don't work in Tauri WebView - Let native errors propagate to UI properly - Users see 'not implemented' instead of mediaDevices error
- Log errors to console for debugging - Will show actual error from Rust IPC or service layer - Helps diagnose why capture fails in Tauri app
- Add core-graphics and image dependencies for macOS - Implement capture_screen using CGDisplay API - Capture main display and convert to PNG/JPEG - Handle format and quality options - Supports both PNG and JPEG output formats - macOS screen capture now fully functional!
- Import ImageEncoder trait for PngEncoder - Convert ColorType to ExtendedColorType with .into() - Add explicit type annotation for ImageError - Fixes compilation errors
- Cast bytes_per_row to usize in offset calculation - Ensures type compatibility with data.len() - Fixes compilation error
- Wrap CGMainDisplayID() call in unsafe block - Make JPEG encoder mutable - Fixes compilation errors
- Initialize supported state to false for SSR - useEffect sets correct value on client mount - Fixes React hydration warnings in Tauri app
- Move close_screen_selector to end of workflow - Use localStorage to signal main window to navigate - Add storage event listener in GlobalHotkeyInit - Prevents workflow interruption when window closes
- Simplify screen selector to just store display ID and close - Add continueScreenshotWorkflow function in global-hotkeys - Main window detects selection via storage event - Continues workflow from main window (has event permissions) - Fixes event.listen permission error
- Log before and after close_screen_selector call - Catch and log any errors - Help debug why window isn't closing
- Hide body immediately on click for instant feedback - Close window before triggering storage event - Prevents visible window during workflow - Storage event fires AFTER window closes
- Replace localStorage storage events with Tauri emit/listen - Screen selector emits 'screen-selected' event with displayId - Main window listens for event and continues workflow - Storage events don't work between Tauri windows - Fixes workflow not continuing after selection
- Add select_display command that emits event and closes window - Rust side has no permission restrictions - Screen selector calls command instead of trying to emit directly - Fixes event.emit permission error - Main window receives event and continues workflow
- Hide main GWT window before showing region selector - Prevents main window appearing in screenshots - Window shows again after capture for review - Clean screenshot capture without UI interference
- Close screen selector FIRST, then emit event - Add 100ms delay to ensure window is fully closed - Fixes race condition where region selector appears before screen selector closes - Screen selector won't appear in screenshots anymore
Covers captureRegion displayId extraction, TauriHotkeyService registration lifecycle, and handleGlobalScreenshot single/multi-display paths including window hide/show sequencing and localStorage thumbnail storage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Prevents compiled Rust artifacts from being committed. src-tauri/.gitignore already had target/ but root .gitignore was missing it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…sset services Fixes Tauri v1→v2 API paths (plugin-dialog, plugin-fs, plugin-http, plugin-clipboard-manager). Adds vitest setupFiles with Blob.arrayBuffer polyfill for jsdom. 385 tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Left-click on tray icon focuses main window. Screenshot menu item emits tray-screenshot event which GlobalHotkeyInit routes into the same workflow as the Cmd+Shift+A hotkey. Requires tray-icon Tauri feature flag. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…tion Detects macOS (arm64/x64), Windows, and Linux from the browser user agent and highlights the recommended download. Links to GitHub releases. Shows feature comparison and system requirements. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Builds macOS (arm64 + x64), Windows, and Linux on tag push (desktop-v*). Runs vitest before building. Creates a draft GitHub Release with signed binaries. Marks as pre-release when tag contains beta/alpha. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
AudioRecorder spawns FFmpeg subprocess (avfoundation/pulse/dshow per platform) to capture mic audio to a temp .aac file alongside frame capture. stop_recording muxes audio+video via ffmpeg -c:v copy -c:a aac. Falls back to video-only if FFmpeg audio capture fails. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
ffmpeg.rs resolves bundled binary from app dir, falls back to system ffmpeg. download-ffmpeg-binaries.mjs guides devs to fetch platform binaries for bundling. src-tauri/bin/ gitignored (large binaries). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
… beta release prep Tasks 25-26, 28-29, 32-33 from the Tauri desktop app plan. - Task 25: First-run permission wizard (check_permissions/mark_first_run_complete/ open_system_preferences commands; /first-run page; FirstRunWizard.tsx island) - Task 26: Settings page enhancements (DesktopSettings, PermissionStatus islands; wired into /settings alongside existing HotkeySettings) - Task 28: Auto-updater (tauri-plugin-updater wired in main.rs + tauri.conf.json; UpdateChecker.tsx island with check/install flow; @tauri-apps/plugin-updater npm pkg) - Task 29: bundle-tauri-assets.mjs script validates icons, FFmpeg sidecars, and Rust toolchain before release; hooked as pretauri:build - Task 32: Full test suite passes — 385 tests across 43 files - Task 33: CHANGELOG.md for v1.0.0-beta.1; README updated with desktop phase summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
tauri-plugin-updater requires a minisign pubkey in config or it panics with "missing field 'pubkey'" at startup. Added a generated Ed25519/minisign public key. The matching private key is stored at ~/.tauri/goodwebtools.key and must be added to GitHub Actions secrets as TAURI_SIGNING_PRIVATE_KEY before signing release artifacts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…instructions Covers: signing keypair generation, adding TAURI_SIGNING_PRIVATE_KEY to GitHub Actions secrets, FFmpeg sidecar setup, version bumping, tag-based release flow, and local release builds. Linked from README. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…11-20)
ScreenRecorder.tsx:
- Replace invoke('list_displays') → captureService.listDisplays()
- Replace invoke('capture_region') for countdown preview → captureService.captureRegion()
- Replace invoke('capture_screen') for overlay background → captureService.captureScreen()
- Add blobToDataUrl() helper; window-management invokes stay direct (not in CaptureService)
SqlitePlayground.tsx:
- Replace navigator.clipboard.writeText() → clipboardService.writeText()
so cell-copy works natively in Tauri without browser clipboard permission
385/385 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…nit tests The blocking bug: AudioRecorder::stop() called child.wait() BEFORE child.kill(). FFmpeg capturing a live mic never self-exits, so wait() blocked forever and the kill() after it was unreachable — any recording with audio enabled hung in stop_recording. Now stop() sends 'q' to stdin for a graceful finalize, drops stdin (EOF), waits briefly, then force-kills so it can never block. Also route all audio FFmpeg calls through crate::ffmpeg::ffmpeg_path() so the bundled sidecar is used instead of only system ffmpeg on PATH. Tests: extract muxed_extension() as a pure helper and add 6 unit tests covering format→extension mapping, no-op recorder start/stop, stop() idempotency, and output-path preservation. cargo test audio:: → 6 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Replaced the hardcoded placeholder GUID (audio=@device_cm_{...}\wave_{...},
which matched no real device) with runtime enumeration: query
`ffmpeg -list_devices true -f dshow -i dummy`, parse the audio devices from
stderr, and capture from the first one. Errors clearly if no mic is present.
The parser (parse_dshow_audio_devices) is a pure function handling both FFmpeg
output styles — newer inline `(audio)`/`(video)` tags and older
`DirectShow audio devices` section headers — and excludes alternative-name
lines and video devices. 7 new unit tests cover both formats, no-device,
alt-name exclusion, and quote extraction. cargo test audio:: → 13 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Pre-warmed window architecture to make region screenshots feel instant: A) pre-warm + reuse overlay window, B) raw-bytes IPC + asset-protocol bg, C) two-phase instant reveal, D) native server-side crop. Includes a risk register drawn from prior abandoned attempts in git history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Building the overlay WebviewWindow on the shortcut hot path paid the full WebKit/WebView2 init penalty (~50-150ms) on every screenshot, then closed it. Now: - prewarm_region_selector() builds the window hidden at startup (main.rs setup) - show_region_selector() reuses it: reposition/resize to the target display, show, focus, and emit `overlay-show` (repositions every show to avoid the prior wrong-display reuse bug); builds on the fly only as a fallback - close_region_selector() now hides instead of closing (keeps it warm) - overlay.astro init logic extracted into a re-runnable initOverlay() that runs on load and on each `overlay-show` event (resets selection, reloads bg) Builds clean (0 errors); 385 tests green. Requires macOS hardware smoke-test: 2nd+ capture should show the crosshair fast, on the correct display, with a reset selection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
… JSON) capture_screen / capture_region / capture_window now return tauri::ipc::Response::new(bytes) instead of Vec<u8>. By default Tauri serializes Vec<u8> as a JSON array of numbers — a full-res 5K PNG became a ~40MB+ inflated JSON string that froze the IPC thread on parse. Returning a Response ships the raw bytes as a binary ArrayBuffer instead. Frontend (capture/tauri.ts) reads invoke<ArrayBuffer>(...) → Blob directly, no Uint8Array-from-number[] round-trip. Test mocks updated to resolve ArrayBuffers to match the real IPC contract. The overlay-background load (base64→localStorage → asset protocol) is deferred to Phase C, where the two-phase reveal reworks background injection via an event. Builds clean (0 errors); 385 tests green. Manual test: capture on a 4K/5K display should no longer show a multi-second freeze. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…full-screen fit Three bugs surfaced by hardware-testing Phase A (window reuse): 1. Blank overlay: a reused/persistent overlay webview's localStorage is stale vs the main window's writes (WKWebView caches per-webview), so the background never loaded. Background + displayId now travel over Tauri events (overlay-set-background / overlay-show) instead of localStorage. Updated all four call sites (global-hotkeys x2, Screenshot, ScreenRecorder). 2. ESC cancel wedged the hotkey: close_region_selector never emitted region-selector-closed, so showRegionSelector's promise hung, leaving screenshotInProgress stuck true → every later hotkey ignored. close now emits the event (cancel path); submit uses a new silent hide_region_selector so a real selection can't be clobbered. handleGlobalScreenshot also clears the guard in a finally as a belt-and-suspenders. 3. Pre-selection not full screen: the reused window resizes just as overlay-show fires, so window.innerWidth was stale. Pre-warm now sizes to the main display, and the overlay re-fits the full-screen selection on the webview resize event. Also: global-hotkeys captures the overlay background at scale 0.5 (4x fewer pixels) like the in-tool flow already did — snappier pre-overlay capture. Builds clean; 385 tests green (global-hotkeys tests updated to assert the event-based background instead of localStorage). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…sleeps hide_main_window used minimize(), which plays a ~250ms macOS genie animation; the flow then slept 150-200ms to wait it out before capturing (obs 8766 tuned that). Switched to hide() — the window drops in ~1 frame — and cut the post-hide waits to ~60ms (a couple of frames for the compositor). Removes ~150-190ms of dead time from every region capture. Applies to the in-tool Screenshot flow and both ScreenRecorder hide points. show_main_window already restores hidden windows (unminimize is a harmless no-op). Builds clean; 385 tests green. Manual test: main window must NOT appear in the capture (if it does on a slower machine, bump the 60ms settle back up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
perf(desktop): optimize screenshot tool — pre-warmed window, raw-bytes IPC, snappier reveal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the GoodWebTools Desktop app built on Tauri 2, alongside the existing web app. Introduces a platform service-abstraction layer so all 55 tools run in both browser and desktop, plus native capabilities the web can't offer: system-wide screen capture/recording, global hotkeys, a desktop region selector, system audio, and native FFmpeg.
166 files, ~33k insertions.
Highlights
Native capabilities
bundle:checkvalidates assets before release.Service layer (browser + desktop)
isTauri().Desktop app features
tauri-plugin-updater)./downloadpage with OS/arch auto-detection linking to GitHub Releases.desktop-v*tags.Docs
RELEASING-DESKTOP.md(signing keys + release flow),CHANGELOG.md(v1.0.0-beta.1), README desktop section.Testing
cargo buildclean; audio module has unit tests (13) for muxing/no-op/dshow parsing.Notes
🤖 Generated with Claude Code