feat: onboarding screen for macOS permission setup - #54
Merged
Conversation
Shows a Warm Ambient onboarding screen (dark base, orange radial glow, rounded panel) on first launch whenever Accessibility or Screen Recording permissions have not been granted. Permission flow: - Step 1: Accessibility - opens System Settings, polls every 500ms until granted, advances automatically (no restart needed) - Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register Thuki in TCC, then opens the Screen Recording settings pane directly, polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows Quit & Reopen only after grant is confirmed Key implementation details: - activator.rs: suppress native Accessibility popup at startup (prompt=false) - permissions.rs: new module with needs_onboarding pure logic + 7 Tauri commands (check/open/request for both permissions, tcc poll, quit+relaunch) - lib.rs: show_onboarding_window resizes/centers/shows panel at normal level (0) with native shadow re-enabled for tight window + professional shadow; notify_frontend_ready gates on permission check before showing overlay - OnboardingView.tsx: sequential two-step UI with polling state machines, drag region on logo/title area, transparent outer container for native rounded corners, CTAButton with brightness-only hover effect - 100% test coverage maintained (frontend + backend) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png) - Add KeyChip component for macOS-style key visualization (⌃ + ⌃) - Remove "Two quick steps" subtitle and Step 1/Step 2 badges - Show Granted badge only when accessibility is confirmed - Update Accessibility description to show double-tap hotkey with key chips - Update Screen Recording description to mention /screen and full screen capture - Increase title bottom margin for better spacing without subtitle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…nt updates - Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls when a poll tick fires before the previous one resolves - Add mountedRef to skip state updates from in-flight promises that resolve after the component unmounts - Fix typo: "response" -> "respond" in Accessibility description - Remove dead 'orange' and 'muted' Badge color variants (Step badges removed) - Add tests for unmount-during-polling cleanup on both permission flows Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
mountedRef was initialized once with useRef(true) but only ever set to false in the effect cleanup — never reset back to true. On any unmount followed by remount (conditional render, React StrictMode double-invoke), the ref stays false permanently and all polling state updates are silently dropped, leaving the component frozen. Fix: reset mountedRef.current = true at the top of the effect body so each mount starts with a clean guard regardless of prior lifecycle cycles. Also replace the weak not.toThrow() assertions in the unmount tests with console.error spies. The previous assertions would have passed even without the fix because React state-update-after-unmount warnings are console.error calls, not thrown exceptions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs (GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583) - Add 6 tests that exercise the defensive early-return guards: - mountedRef check in initial accessibility check (line 188) - axInFlightRef guard that blocks concurrent poll ticks (line 204) - mountedRef check inside ax poll callback (line 208) - mountedRef check before screen-recording poll setup (line 225) - screenInFlightRef guard that blocks concurrent TCC checks (line 228) - mountedRef check inside screen-recording poll callback (line 234) Each test uses a deferred promise to keep the relevant invoke call in-flight long enough to trigger the guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
vitest resolves its own copy of vite at 8.0.3 (in the vulnerable 8.0.0-8.0.4 range). Adding vite to the overrides map forces every transitive resolution to 8.0.5, clearing all high-severity CVEs reported by bun audit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node
added a commit
that referenced
this pull request
Apr 10, 2026
* feat: add premium onboarding screen for macOS permission setup
Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.
Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
Thuki in TCC, then opens the Screen Recording settings pane directly,
polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
Quit & Reopen only after grant is confirmed
Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
(0) with native shadow re-enabled for tight window + professional shadow;
notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
drag region on logo/title area, transparent outer container for native
rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* feat: polish onboarding UI with real logo and key chips
- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: guard polling intervals against in-flight overlap and post-unmount updates
- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: reset mountedRef on remount and strengthen unmount tests
mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.
Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.
Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix(onboarding): address all code review issues
- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: update vite to 8.0.5 and achieve 100% branch coverage
- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
(GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
- mountedRef check in initial accessibility check (line 188)
- axInFlightRef guard that blocks concurrent poll ticks (line 204)
- mountedRef check inside ax poll callback (line 208)
- mountedRef check before screen-recording poll setup (line 225)
- screenInFlightRef guard that blocks concurrent TCC checks (line 228)
- mountedRef check inside screen-recording poll callback (line 234)
Each test uses a deferred promise to keep the relevant invoke call
in-flight long enough to trigger the guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: override vite to >=8.0.5 for all transitive dependents
vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
---------
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node
added a commit
that referenced
this pull request
Apr 10, 2026
* feat: add premium onboarding screen for macOS permission setup
Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.
Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
Thuki in TCC, then opens the Screen Recording settings pane directly,
polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
Quit & Reopen only after grant is confirmed
Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
(0) with native shadow re-enabled for tight window + professional shadow;
notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
drag region on logo/title area, transparent outer container for native
rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* feat: polish onboarding UI with real logo and key chips
- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: guard polling intervals against in-flight overlap and post-unmount updates
- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: reset mountedRef on remount and strengthen unmount tests
mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.
Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.
Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix(onboarding): address all code review issues
- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: update vite to 8.0.5 and achieve 100% branch coverage
- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
(GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
- mountedRef check in initial accessibility check (line 188)
- axInFlightRef guard that blocks concurrent poll ticks (line 204)
- mountedRef check inside ax poll callback (line 208)
- mountedRef check before screen-recording poll setup (line 225)
- screenInFlightRef guard that blocks concurrent TCC checks (line 228)
- mountedRef check inside screen-recording poll callback (line 234)
Each test uses a deferred promise to keep the relevant invoke call
in-flight long enough to trigger the guard
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: override vite to >=8.0.5 for all transitive dependents
vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
---------
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node
added a commit
that referenced
this pull request
Apr 11, 2026
* feat: add premium onboarding screen for macOS permission setup
Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.
Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
Thuki in TCC, then opens the Screen Recording settings pane directly,
polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
Quit & Reopen only after grant is confirmed
Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
(0) with native shadow re-enabled for tight window + professional shadow;
notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
drag region on logo/title area, transparent outer container for native
rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* feat: polish onboarding UI with real logo and key chips
- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: guard polling intervals against in-flight overlap and post-unmount updates
- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: reset mountedRef on remount and strengthen unmount tests
mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.
Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.
Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix(onboarding): address all code review issues
- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: update vite to 8.0.5 and achieve 100% branch coverage
- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
(GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
- mountedRef check in initial accessibility check (line 188)
- axInFlightRef guard that blocks concurrent poll ticks (line 204)
- mountedRef check inside ax poll callback (line 208)
- mountedRef check before screen-recording poll setup (line 225)
- screenInFlightRef guard that blocks concurrent TCC checks (line 228)
- mountedRef check inside screen-recording poll callback (line 234)
Each test uses a deferred promise to keep the relevant invoke call
in-flight long enough to trigger the guard
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
* fix: override vite to >=8.0.5 for all transitive dependents
vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
---------
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
This was referenced Apr 11, 2026
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
CGRequestScreenCaptureAccess, polls for the TCC grant viaCGWindowListCopyWindowInfo, then prompts Quit & Reopen since macOS requires a process restart for Screen Recording to take effectneeds_onboardingand all permission predicates)What's new
Backend (
src-tauri/src/permissions.rs):needs_onboarding(accessibility, screen_recording) -> bool— pure predicate, fully unit testedcheck_accessibility_permission/open_accessibility_settingscheck_screen_recording_permission/open_screen_recording_settings/request_screen_recording_accesscheck_screen_recording_tcc_granted— polls viaCGWindowListCopyWindowInfo(works pre-restart, unlikeCGPreflightScreenCaptureAccess)quit_and_relaunch— usesapp_handle.restart()for proper bundle relaunchFrontend (
src/view/OnboardingView.tsx):StepCardcomponentsmountedRef+ in-flight guards prevent stale state updates after unmountTest plan
🤖 Generated with Claude Code