Skip to content

release: desktop v1.0.0-beta.1 - #9

Merged
slaveofcode merged 199 commits into
mainfrom
develop
Jul 25, 2026
Merged

release: desktop v1.0.0-beta.1#9
slaveofcode merged 199 commits into
mainfrom
develop

Conversation

@slaveofcode

Copy link
Copy Markdown
Owner

Desktop app first beta — v1.0.0-beta.1

Promotes develop to main for the first tagged desktop release.

Highlights since the last main:

  • GoodWebTools Desktop (Tauri 2) — service abstraction layer, all tools running on desktop.
  • System-wide screenshot — global hotkey (⌘⇧A), multi-display picker, region overlay, native server-side crop; pre-warmed window + raw-bytes IPC for near-instant capture.
  • Screen recording + audio — cross-platform capture (macOS core-graphics; Windows/Linux via xcap with a persistent stream), FFmpeg audio muxed correctly per container.
  • Truly global recording hotkey (⌘⇧R) — screen picker, 5s countdown, accurate timer, restorable window, start/stop from anywhere.
  • First-run wizard, settings, auto-updater, tray, /download page, GitHub Actions release pipeline.
  • Build fix: download.astro no longer breaks npm run build.

Version bumped to 1.0.0-beta.1. Tagging desktop-v1.0.0-beta.1 after merge triggers the cross-platform release build.

🤖 Generated with Claude Code

Kresna 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
Kresna and others added 29 commits July 20, 2026 06:13
…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
feat(desktop): GoodWebTools Desktop app (Tauri 2) — Phase 10
…to JS)

The in-tool region flow captured the frozen full-res screen, shipped the whole
PNG to JS (10-30MB on a 5K display), base64'd it, decoded it, and cropped in a
canvas — even for a tiny region.

Now: capture_hold captures the frozen full frame once (while the main window is
hidden) and keeps it in Rust; after selection, crop_held crops server-side with
HiDPI scaling and returns only the small region as raw PNG bytes. release_held
frees it on cancel. Memory is bounded to one held frame (cleared on next capture).

physical_crop_rect (logical→physical, clamped) is a pure fn with 6 unit tests.
Screenshot.tsx loads the crop via createImageBitmap — no base64, no full-res
decode. Region button is Tauri-only so there's no browser path to preserve.

cargo test: 19 passed; JS: 385 passed. Manual test: crop is pixel-accurate on
HiDPI and correct on a secondary display.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Windows/Linux had no capture backend — every capture command returned
"not yet implemented", so screen recording (which feeds on capture_screen_fast)
produced zero frames and the audio pipeline had nothing to attach to.

Adds xcap (0.9.4, wgc on Windows) as the Windows + Linux capture backend, wired
into list_displays, capture_screen, capture_screen_fast, and capture_hold.
macOS keeps its validated core-graphics path untouched. HeldCapture is unified
on image::RgbaImage so crop_held is one platform-agnostic path; crop_rgba is a
pure fn with unit tests.

Verification: macOS builds + 21 unit tests pass locally. Windows/Linux can't be
compiled on a macOS dev machine, so .github/workflows/desktop-check.yml
compile-checks all three targets on CI (with the PipeWire/Wayland/dbus deps xcap
needs on Linux). Runtime validation still needs real Windows/Linux hardware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
xcap's Linux backend pulls libspa/spa-sys, which need PipeWire 1.0+ headers
(spa_meta_region_is_valid, spa_video_info_raw.flags). ubuntu-22.04 ships an
older PipeWire and failed to compile libspa. Bumped both the compile-check and
release workflows to ubuntu-24.04 and added libpipewire-0.3-dev, libspa-0.2-dev,
wayland/xcb, and libclang (spa-sys uses bindgen). Windows already compiled clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Down from 12 warnings to 0:
- remove unnecessary parens in capture_screen_fast
- drop unused `std::path::PathBuf` import in recording.rs
- remove genuinely-dead overlay::Rectangle struct (+ its serde import)
- #[allow(dead_code)] on frontend-contract fields not yet read in Rust
  (CaptureOptions/RecordOptions bitrate + audio flags) and on RecordingState
  snapshot fields and the reference capture_screen_internal helper
- declare cfg(cargo-clippy) via [lints.rust] check-cfg so the objc msg_send!
  macro no longer trips unexpected_cfgs

Build is warning-clean; audio unit tests still pass. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…r-frame)

record_frames called capture_screen_fast per frame, which on Linux re-negotiates
the xdg-desktop-portal screencast on every grab — far too slow for real-time
recording (and it would re-prompt). macOS keeps its fast per-frame core-graphics
loop; Windows/Linux now open ONE xcap VideoRecorder stream, consume frames from
its channel, throttle to the target FPS, crop/encode each kept frame to JPEG, and
push into the same buffer the encoder drains on stop.

- record_frames is now macOS-only; record_frames_xcap handles Windows/Linux
- start_recording spawns the right loop per platform
- crop_rgba is pub(crate) so the recording loop can reuse the crop math
- even-dimension enforcement so H.264/mp4 encoding accepts the frames

macOS builds clean. Windows/Linux compilation verified via desktop-check CI.
Runtime still needs real hardware (xcap Linux path needs the portal grant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
…-crop

perf(screenshot): Phase D — native server-side region crop
feat(desktop): Windows + Linux screen capture & recording (xcap)
chore(desktop): clean up all Rust compiler warnings (12 → 0)
Two bugs made every recording come out silent:
1. TauriCaptureService.startRecording dropped includeAudio/systemAudio before
   the IPC call, so Rust never started the AudioRecorder. Now forwarded.
2. The mux always used -c:a aac, but WebM (the default output) can't hold AAC —
   it needs Opus, so the mux failed and left a video-only file. Pick the audio
   codec by container: mp4 → aac, webm → libopus.

Also fixes a build break: the [lints.rust] check-cfg form added earlier is
invalid on current rustc (errored with "invalid --check-cfg argument"); switched
to `unexpected_cfgs = "allow"`.

macOS builds clean (0 warnings); 385 JS tests pass. Needs a mic-enabled recording
on hardware to confirm audio lands in the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
fix(recording): silent recordings — capture + mux audio correctly
… not hide)

Phase C switched hide_main_window to window.hide() for snappier screenshots, but
recording keeps the window out of view for the whole session — and a hidden
window can't be brought back from the dock, so users got locked out of the app
mid-recording (most obvious when recording an extended display, where GWT sits on
the still-visible main screen).

Add a minimize_main_window command and use it for the recording-start hide so the
window can be restored from the dock. Screenshots keep the instant hide() (they
auto-restore a moment later). Region-selection hide is short-lived and unchanged.

macOS builds clean; 385 JS tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
The recording hotkey was registered in an effect keyed on [recording, stopping],
so it unregistered + re-registered on every start/stop. That async churn raced —
the re-register could hit "already registered" and silently fail, leaving ⌘⇧R
dead so it no longer toggled recording.

Register it ONCE (keyed on [inTauriApp]) and route the callback through a ref
that always holds the current toggle logic, so state stays fresh without
re-registering.

385 tests pass; file lints clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
fix(recording): restorable window + reliable ⌘⇧R hotkey
Recording lived inside the ScreenRecorder component, so ⌘⇧R only worked while
that tool page was open. Extracted a global recording manager
(services/global-recording.ts) that is the single source of truth for recording
state, the window (minimize/restore), and the produced blob.

- ⌘⇧R is now registered globally at startup (global-hotkeys.ts) → toggles a
  full-screen recording from ANY page using the last-configured settings.
- Stop from a non-recorder page auto-downloads the file (no UI to show it);
  from the recorder page the mounted component shows it via a live event.
- ScreenRecorder is now a thin UI over the manager: it persists settings, drives
  start/stop through it, and mirrors state/results via gwt:recording-* events.
  Removed its component-scoped hotkey registration.
- Dropped the pre-record countdown to unify the hotkey and in-page flows
  (recording starts immediately on trigger).

Also fixes two pre-existing bugs found while verifying:
- download.astro had a top-level `return` in its module <script>, which broke
  the entire `npm run build` (web deploy + Tauri release). Guarded instead.
- ScreenRecorder.selectRegion referenced `invoke` out of scope in its catch.

Frontend builds; 386 JS tests pass (added a ⌘⇧R registration test); lints clean.
No NEW type errors introduced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Two fixes from hardware testing the global recording:

1. Timer stuck at 00:00. The manager tracked no start time, and mounting the
   recorder page mid-recording reflected recording=true without starting the
   ticker. The manager now records `startedAt` (after any countdown) and exposes
   it; the UI ticks elapsed from it — accurate even when navigating to the page
   mid-recording, and immune to background throttling.

2. Restored the pre-record countdown. A 5s countdown now shows on the target
   display before capture begins (for both the ⌘⇧R hotkey and the in-page
   button), which also signals which screen is being recorded. The countdown
   window blends in with a screenshot of the area behind it and self-closes.

Frontend builds; 386 tests pass; lints clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
⌘⇧R already toggles (stops when recording), but the window only came back if the
manager had minimized it. Stopping now always brings the main window to front so
a single ⌘⇧R reliably stops the recording AND reveals the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
Pressing the recording hotkey with more than one display now opens the same
multi-display picker the screenshot flow uses, so the user chooses which screen
to record. Recording (with the 5s countdown) begins after they pick.

The screen-selected event is shared with the screenshot flow, so a pending-intent
flag (consumePendingRecordingSelect) routes the pick to recording vs screenshot.
Single-display stays a direct start. A guard ignores repeat presses while the
picker is open.

Frontend builds; 386 tests pass; lints clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
feat(recording): truly global ⌘⇧R recording hotkey
Match the desktop-v1.0.0-beta.1 release tag so the built app and the
auto-updater manifest report the correct version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dqouzFP8vy9jaKhTDFj5H
@slaveofcode
slaveofcode merged commit 53c96fc into main Jul 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant