Skip to content

New desktop onboarding experience#1681

Merged
richiemcilroy merged 3 commits intomainfrom
desktop-onboarding
Mar 25, 2026
Merged

New desktop onboarding experience#1681
richiemcilroy merged 3 commits intomainfrom
desktop-onboarding

Conversation

@richiemcilroy
Copy link
Copy Markdown
Member

@richiemcilroy richiemcilroy commented Mar 25, 2026

  • Adds a new multi-step onboarding window that walks first-time users through Cap's recording modes (Instant, Studio, Screenshot), required OS permissions, keyboard shortcuts, and FAQs
  • Adds a "Help & Tour" button in the main window header so existing users can re-open the onboarding at any time

Greptile Summary

This PR replaces the old Setup window with a polished multi-step Onboarding window that walks first-time users through an animated welcome overlay, OS permissions, recording-mode demos (Instant, Studio, Screenshot), a live mode-switcher, a settings overview, and a FAQ accordion. It also adds a persistent "Help & Tour" button in the main window header so existing users can re-open the tour at any time.

Key changes:

  • New onboarding.tsx (2 314 lines): full SolidJS onboarding flow with startup audio, cloud animations, looping animated mockups, and a permissions polling loop.
  • windows.rs: Setup variant replaced by Onboarding with responsive sizing; ShowCapWindow::Main is now guarded by a permission re-check that redirects to Onboarding if necessary permissions are revoked.
  • general_settings.rs: adds has_completed_onboarding with serde(default = "default_true") so existing users are never re-prompted, while new installs default to false via Default::default().
  • lib.rs: startup logic extended to check both has_completed_startup and has_completed_onboarding, and reset_microphone_permissions is now correctly gated to macOS.
  • new-main/index.tsx: "Help & Tour" icon button added to the main header.
  • All remaining setup references in tray.rs, debug.tsx, and App.tsx are renamed to onboarding.

Confidence Score: 4/5

  • Safe to merge; the one P1 concern (mode mutation during the tour demo) affects UX but not data integrity or stability.
  • The Setup → Onboarding migration is complete and consistent across all layers (Rust backend, TypeScript frontend, routing). Backward-compatibility for existing users is handled correctly via the serde default. The only noteworthy logic concern is that the interactive mode-switcher on step 5 makes real setRecordingMode IPC calls, which silently changes the user's active mode as a side effect of exploring the demo. The remaining findings (un-awaited close, println!, serde comment) are style-level cleanups.
  • apps/desktop/src/routes/(window-chrome)/onboarding.tsx (ToggleStep mode mutation), apps/desktop/src-tauri/src/general_settings.rs (serde/Default discrepancy worth a comment)

Important Files Changed

Filename Overview
apps/desktop/src/routes/(window-chrome)/onboarding.tsx New 2 314-line onboarding component with multi-step flow, animated startup overlay, permission grants, mode demos, shortcuts step, and FAQ accordion. Core logic is sound; minor issue: the interactive mode-switcher step makes real setRecordingMode IPC calls as a side effect of tour navigation.
apps/desktop/src-tauri/src/windows.rs Replaces Setup window variant with Onboarding throughout, adds Onboarding builder with responsive sizing, hides the Main window when onboarding opens, and guards ShowCapWindow::Main with a permission re-check that redirects to Onboarding if needed. Migration is clean and complete.
apps/desktop/src-tauri/src/general_settings.rs Adds has_completed_onboarding: bool with serde(default = "default_true") so existing users skip onboarding, while Default::default() correctly returns false for new installs. The intentional disagreement is a maintenance footgun without a comment.
apps/desktop/src-tauri/src/lib.rs Startup logic now reads both has_completed_startup and has_completed_onboarding, routing to Onboarding when either flag is false or permissions are missing. The reset_microphone_permissions function is made macOS-conditional, which is an improvement. A bare println! debug statement was added in the startup path.
apps/desktop/src-tauri/src/tray.rs Simple rename of is_setup_window_openis_onboarding_window_open and updating the tray's "Request Permissions" action to open the Onboarding window. No logic changes.
apps/desktop/src/App.tsx Removes the /setup lazy route and adds /onboarding route pointing to the new OnboardingPage component. Clean one-for-one substitution.
apps/desktop/src/routes/(window-chrome)/new-main/index.tsx Adds a "Help & Tour" icon button in the main window header that calls commands.showWindow("Onboarding"). Minimal and correct change.
apps/desktop/src/routes/debug.tsx Renames the debug button from "Show Setup Window" to "Show Onboarding Window" and updates the command accordingly. Trivial rename.
apps/desktop/src/utils/tauri.ts Auto-generated bindings file updated to include "Onboarding" as a ShowCapWindow variant and remove "Setup". No manual changes needed here.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App Launch] --> B{startup_completed\nAND onboarding_completed\nAND permissions.necessary_granted?}
    B -- No --> C[ShowCapWindow::Onboarding]
    B -- Yes --> D[ShowCapWindow::Main]

    C --> E[OnboardingPage mounts\nready = false]
    E --> F[doPermissionsCheck]
    F --> G{hasCompletedStartup?}
    G -- No --> H[Show StartupOverlay\nwith audio & animations]
    G -- Yes --> I[Skip overlay\nstep = 0]

    H --> J[User clicks Get Started]
    J --> K{macOS?}
    K -- Yes --> I
    K -- No --> L[goToStep 1\nskip permissions UI]

    I --> M[Step 0: PermissionsStep\ngrant screen rec + accessibility]
    M --> N{allRequired\npermissions granted?}
    N -- No --> M
    N -- Yes --> O[permsGranted = true\nNext button enabled]

    O --> P[Steps 1-7: Modes / Tour / FAQ]
    L --> P

    P --> Q[handleFinish\nset hasCompletedOnboarding = true]
    Q --> R[showWindow Main]
    R --> S{Main window exists\nAND permissions missing?}
    S -- Yes --> C
    S -- No --> T[Show Main Window\nClose Onboarding]

    D --> T

    subgraph Revisit via Help & Tour
    U[Main Window\nHelp & Tour button] --> V[showWindow Onboarding]
    V --> W{permissionsNeeded\nAND isRevisit?}
    W -- Yes --> X[permissionsOnly mode\n1-step flow]
    W -- No --> Y[Full 8-step tour\nisRevisit = true]
    X --> Q
    Y --> Q
    end
Loading

Comments Outside Diff (1)

  1. apps/desktop/src-tauri/src/lib.rs, line 106-107 (link)

    P2 Debug println! left in startup path

    A bare println!("Showing onboarding") was added to the app startup path. The surrounding context already removed a similar println for the main-window branch — the new one for onboarding should use the project's tracing/logging infrastructure (info! or tracing::info!) rather than println!, which bypasses the recording_logging_handle wired up at the start of run().

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src-tauri/src/lib.rs
    Line: 106-107
    
    Comment:
    **Debug `println!` left in startup path**
    
    A bare `println!("Showing onboarding")` was added to the app startup path. The surrounding context already removed a similar println for the main-window branch — the new one for onboarding should use the project's tracing/logging infrastructure (`info!` or `tracing::info!`) rather than `println!`, which bypasses the `recording_logging_handle` wired up at the start of `run()`.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/lib.rs
Line: 106-107

Comment:
**Debug `println!` left in startup path**

A bare `println!("Showing onboarding")` was added to the app startup path. The surrounding context already removed a similar println for the main-window branch — the new one for onboarding should use the project's tracing/logging infrastructure (`info!` or `tracing::info!`) rather than `println!`, which bypasses the `recording_logging_handle` wired up at the start of `run()`.

```suggestion
                        tracing::info!("Showing onboarding");
                        let _ = ShowCapWindow::Onboarding.show(&app).await;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/routes/(window-chrome)/onboarding.tsx
Line: 811-815

Comment:
**Recording mode is mutated during the onboarding tour**

`commands.setRecordingMode(modes[index].id)` is called whenever a user clicks an icon in the interactive mode-switcher demo on step 5. This permanently changes the user's active recording mode as a side effect of exploring the tutorial. A first-time user who clicks around will silently leave the tour with a different mode than they started with — likely not the intended UX.

Consider either:
- removing the `setRecordingMode` call so the step is purely visual, or
- restoring the original mode in a cleanup (`onCleanup`) if the user never explicitly committed to a choice.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src/routes/(window-chrome)/onboarding.tsx
Line: 355-356

Comment:
**`Window.close()` is not awaited in `handleFinish`**

`getCurrentWindow().close()` returns a `Promise<void>`, but it is called without `await`. In most cases this is harmless because the window teardown is async anyway, but it means any rejection is silently swallowed and the calling function technically completes before the close operation has been acknowledged by the runtime. 

```suggestion
	await getCurrentWindow().close();
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/general_settings.rs
Line: 152-153

Comment:
**`serde` default and `Default` impl disagree on `has_completed_onboarding`**

The field uses `#[serde(default = "default_true")]` (deserialises missing field as `true`) but `Default::default()` returns `false`. The intent is clear: existing users whose settings files pre-date this field should be treated as having completed onboarding, while a truly fresh install (no file → `Default::default()`) must go through the flow. However, the mismatch is a maintenance footgun — any code that constructs a `GeneralSettingsStore` via `Default::default()` (e.g., in tests or fallback paths) silently gets `false`, which would force unexpected onboarding. A comment on the field explaining the deliberate discrepancy would help future contributors avoid breakage.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Rename Setup window to Onboarding" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Introduce an onboarding window that guides new users through initial setup. Adds the Onboarding window variant, route, settings flag (has_completed_onboarding), startup logic to show onboarding when needed, and a Help & Tour button in the main window header.

Made-with: Cursor
<button
type="button"
onClick={() => {
commands.showWindow("Onboarding");
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns a promise; consider handling it to avoid unhandled rejections in the click handler.

Suggested change
commands.showWindow("Onboarding");
void commands.showWindow("Onboarding").catch((err) => console.error("Failed to show onboarding window", err));

<button
class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
onClick={() => commands.showWindow("Setup")}
onClick={() => commands.showWindow("Onboarding")}
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here: showWindow is async, so it’s safer to handle the promise.

Suggested change
onClick={() => commands.showWindow("Onboarding")}
onClick={() => void commands.showWindow("Onboarding").catch((err) => console.error("Failed to show onboarding window", err))}

out our{" "}
<button
type="button"
onClick={() => shell.open("https://cap.so/pricing")}
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell.open is async; handling the promise avoids unhandled rejections.

Suggested change
onClick={() => shell.open("https://cap.so/pricing")}
onClick={() => void shell.open("https://cap.so/pricing").catch((err) => console.error("Failed to open pricing", err))}


<button
type="button"
onClick={() => shell.open("https://cap.so/pricing")}
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: this is async so it’s worth handling errors.

Suggested change
onClick={() => shell.open("https://cap.so/pricing")}
onClick={() => void shell.open("https://cap.so/pricing").catch((err) => console.error("Failed to open pricing", err))}

}
});

createEffect(() => {
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 250ms poll interval keeps running while this step is active, even after required permissions are granted. Stopping polling once everything required is permitted reduces background work.

Suggested change
createEffect(() => {
createEffect(() => {
if (!props.active || initialCheck()) return;
const c = check();
const allRequired = setupPermissions
.filter((p) => !p.optional)
.every((p) => isPermitted(c?.[p.key]));
if (allRequired) return;
const interval = setInterval(fetchPermissions, 250);
onCleanup(() => clearInterval(interval));
});

fetchPermissions();
};

const openSettings = async (permission: OSPermission) => {
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If openPermissionSettings/ask/relaunch fails, this can currently throw and leave the UI in a stale state. Wrapping in try/finally keeps the local check state consistent.

Suggested change
const openSettings = async (permission: OSPermission) => {
const openSettings = async (permission: OSPermission) => {
try {
await commands.openPermissionSettings(permission);
if (permission === "screenRecording") {
const shouldRestart = await ask(
"After adding Cap in System Settings, you'll need to restart the app for the permission to take effect.",
{
title: "Restart Required",
kind: "info",
okLabel: "Restart, I've granted permission",
cancelLabel: "No, I still need to add it",
},
);
if (shouldRestart) {
await relaunch();
}
}
} catch (err) {
console.error(`Error opening permission settings: ${err}`);
} finally {
setInitialCheck(false);
fetchPermissions();
}
};

setStep(target);
};

const handleFinish = async () => {
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but getCurrentWindow().close() returns a promise. Awaiting it (and optionally catching) avoids floating promises and makes sequencing deterministic.

Suggested change
const handleFinish = async () => {
const handleFinish = async () => {
try {
if (!isRevisit()) {
await generalSettingsStore.set({ hasCompletedOnboarding: true });
}
await commands.showWindow({ Main: { init_target_mode: null } });
await getCurrentWindow().close();
} catch (err) {
console.error(`Error finishing onboarding: ${err}`);
}
};

.arg(bundle_id)
.output()
.expect("Failed to reset microphone permissions");
Command::new("tccutil")
Copy link
Copy Markdown

@tembo tembo bot Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only errors on spawn failure; if tccutil exits non-zero, it’ll still return Ok(()). Capturing the output and checking status.success() makes failures actionable.

Suggested change
Command::new("tccutil")
let output = Command::new("tccutil")
.arg("reset")
.arg("Microphone")
.arg(bundle_id)
.output()
.map_err(|_| "Failed to reset microphone permissions".to_string())?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
return Err(if stderr.is_empty() {
"Failed to reset microphone permissions".to_string()
} else {
stderr
});
}

Comment on lines +811 to +815
const handleModeClick = (index: number) => {
setUserClicked(true);
setActiveMode(index);
commands.setRecordingMode(modes[index].id);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Recording mode is mutated during the onboarding tour

commands.setRecordingMode(modes[index].id) is called whenever a user clicks an icon in the interactive mode-switcher demo on step 5. This permanently changes the user's active recording mode as a side effect of exploring the tutorial. A first-time user who clicks around will silently leave the tour with a different mode than they started with — likely not the intended UX.

Consider either:

  • removing the setRecordingMode call so the step is purely visual, or
  • restoring the original mode in a cleanup (onCleanup) if the user never explicitly committed to a choice.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/(window-chrome)/onboarding.tsx
Line: 811-815

Comment:
**Recording mode is mutated during the onboarding tour**

`commands.setRecordingMode(modes[index].id)` is called whenever a user clicks an icon in the interactive mode-switcher demo on step 5. This permanently changes the user's active recording mode as a side effect of exploring the tutorial. A first-time user who clicks around will silently leave the tour with a different mode than they started with — likely not the intended UX.

Consider either:
- removing the `setRecordingMode` call so the step is purely visual, or
- restoring the original mode in a cleanup (`onCleanup`) if the user never explicitly committed to a choice.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +355 to +356
getCurrentWindow().close();
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Window.close() is not awaited in handleFinish

getCurrentWindow().close() returns a Promise<void>, but it is called without await. In most cases this is harmless because the window teardown is async anyway, but it means any rejection is silently swallowed and the calling function technically completes before the close operation has been acknowledged by the runtime.

Suggested change
getCurrentWindow().close();
};
await getCurrentWindow().close();
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/(window-chrome)/onboarding.tsx
Line: 355-356

Comment:
**`Window.close()` is not awaited in `handleFinish`**

`getCurrentWindow().close()` returns a `Promise<void>`, but it is called without `await`. In most cases this is harmless because the window teardown is async anyway, but it means any rejection is silently swallowed and the calling function technically completes before the close operation has been acknowledged by the runtime. 

```suggestion
	await getCurrentWindow().close();
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +152 to +153
#[serde(default = "default_true")]
pub has_completed_onboarding: bool,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 serde default and Default impl disagree on has_completed_onboarding

The field uses #[serde(default = "default_true")] (deserialises missing field as true) but Default::default() returns false. The intent is clear: existing users whose settings files pre-date this field should be treated as having completed onboarding, while a truly fresh install (no file → Default::default()) must go through the flow. However, the mismatch is a maintenance footgun — any code that constructs a GeneralSettingsStore via Default::default() (e.g., in tests or fallback paths) silently gets false, which would force unexpected onboarding. A comment on the field explaining the deliberate discrepancy would help future contributors avoid breakage.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/general_settings.rs
Line: 152-153

Comment:
**`serde` default and `Default` impl disagree on `has_completed_onboarding`**

The field uses `#[serde(default = "default_true")]` (deserialises missing field as `true`) but `Default::default()` returns `false`. The intent is clear: existing users whose settings files pre-date this field should be treated as having completed onboarding, while a truly fresh install (no file → `Default::default()`) must go through the flow. However, the mismatch is a maintenance footgun — any code that constructs a `GeneralSettingsStore` via `Default::default()` (e.g., in tests or fallback paths) silently gets `false`, which would force unexpected onboarding. A comment on the field explaining the deliberate discrepancy would help future contributors avoid breakage.

How can I resolve this? If you propose a fix, please make it concise.

@richiemcilroy richiemcilroy merged commit ccccdea into main Mar 25, 2026
18 checks passed
@paragon-review
Copy link
Copy Markdown

Paragon Summary

This pull request review identified 6 issues across 2 categories in 10 files. The review analyzed code changes, potential bugs, security vulnerabilities, performance issues, and code quality concerns using automated analysis tools.

This PR introduces a multi-step onboarding window for first-time desktop users that guides them through Cap's recording modes, permissions, shortcuts, and FAQs, while also adding a "Help & Tour" button in the main window header so existing users can revisit the onboarding experience at any time.Analysis submitted successfully. This onboarding PR adds a guided setup experience for new users and a persistent help button for existing users to revisit the tour.

Key changes:

  • New multi-step onboarding window for first-time users covering recording modes, OS permissions, shortcuts, and FAQs
  • "Help & Tour" button added to main window header for re-opening onboarding anytime
  • New onboarding.tsx route added, replacing removed setup.tsx route
  • Rust backend updated in windows.rs, tray.rs, and general_settings.rs to support onboarding window
  • Tauri utilities and App.tsx modified to wire onboarding flowI understand - I need to submit my analysis through the appropriate tool. Let me dothat now.Analysis submitted. Key findings:

Confidence score: 4/5

  • This PR has low-moderate risk with 3 medium-priority issues identified
  • Score reflects code quality concerns and maintainability issues
  • Consider addressing medium-priority findings to improve code quality

10 files reviewed, 6 comments

Severity breakdown: Medium: 3, Low: 3

Dashboard


const ringLeft = () => PAD + activeMode() * (CIRCLE + GAP);

const handleModeClick = (index: number) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: ToggleStep calls setRecordingMode during onboarding tour

ToggleStep calls setRecordingMode during onboarding tour. Revisiting users accidentally change their mode. Guard with an isRevisit check.

View Details

Location: apps/desktop/src/routes/(window-chrome)/onboarding.tsx (lines 811)

Analysis

ToggleStep calls setRecordingMode during onboarding tour

What fails Clicking mode icons in the ToggleStep during a revisit via 'Help & Tour' button changes the user's actual recording mode setting.
Result The user's actual recording mode is changed to the clicked mode via commands.setRecordingMode.
Expected During a revisit/tour, clicking mode icons should only animate the UI without changing the actual recording mode.
Impact Users who re-open onboarding as a help tour will unknowingly change their recording mode, disrupting their workflow.
How to reproduce
1. Complete onboarding once. 2. Click 'Help & Tour' button in main window header. 3. Navigate to the ToggleStep (step 5). 4. Click a different mode icon (e.g., Studio).
AI Fix Prompt
Fix this issue: ToggleStep calls setRecordingMode during onboarding tour. Revisiting users accidentally change their mode. Guard with an isRevisit check.

Location: apps/desktop/src/routes/(window-chrome)/onboarding.tsx (lines 811)
Problem: Clicking mode icons in the ToggleStep during a revisit via 'Help & Tour' button changes the user's actual recording mode setting.
Current behavior: The user's actual recording mode is changed to the clicked mode via commands.setRecordingMode.
Expected: During a revisit/tour, clicking mode icons should only animate the UI without changing the actual recording mode.
Steps to reproduce: 1. Complete onboarding once. 2. Click 'Help & Tour' button in main window header. 3. Navigate to the ToggleStep (step 5). 4. Click a different mode icon (e.g., Studio).

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

const handleStartupDone = async () => {
setIsExiting(true);
await generalSettingsStore.set({ hasCompletedStartup: true });
setTimeout(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Non-macOS startup skips permissions step entirely

Non-macOS startup skips permissions step entirely. Windows users may miss camera/mic prompts. Remove the platform-based step skip or show relevant permissions.

View Details

Location: apps/desktop/src/routes/(window-chrome)/onboarding.tsx (lines 361)

Analysis

Non-macOS startup skips permissions step entirely. Windows users may miss camera/mic prompts

What fails handleStartupDone jumps to step 1 on non-macOS, completely skipping the permissions step (step 0). If any permissions are needed on Windows, users never see the prompt.
Result Step 0 (permissions) is skipped on Windows. If camera or microphone permissions are needed, the user never sees the grant UI.
Expected Either show the permissions step on all platforms (filtering out notNeeded items) or verify no permissions are needed on Windows before skipping.
Impact Windows users may not grant needed permissions, and the Main window permission guard could redirect them back to onboarding in a loop.
How to reproduce
1. Run the app on Windows for the first time. 2. Click 'Get Started' on the startup overlay. 3. Observe the onboarding goes directly to step 1 (Modes Overview).
AI Fix Prompt
Fix this issue: Non-macOS startup skips permissions step entirely. Windows users may miss camera/mic prompts. Remove the platform-based step skip or show relevant permissions.

Location: apps/desktop/src/routes/(window-chrome)/onboarding.tsx (lines 361)
Problem: handleStartupDone jumps to step 1 on non-macOS, completely skipping the permissions step (step 0). If any permissions are needed on Windows, users never see the prompt.
Current behavior: Step 0 (permissions) is skipped on Windows. If camera or microphone permissions are needed, the user never sees the grant UI.
Expected: Either show the permissions step on all platforms (filtering out notNeeded items) or verify no permissions are needed on Windows before skipping.
Steps to reproduce: 1. Run the app on Windows for the first time. 2. Click 'Get Started' on the startup overlay. 3. Observe the onboarding goes directly to step 1 (Modes Overview).

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

.run(move |_handle, event| match event {
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen { .. } => {
if let Some(onboarding) = CapWindowId::Onboarding.get(_handle) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Dock Reopen handler always focuses onboarding over all windows

Dock Reopen handler always focuses onboarding over all windows. Users can't access other windows via dock click. Move onboarding check after other window checks.

View Details

Location: apps/desktop/src-tauri/src/lib.rs (lines 3885)

Analysis

Dock Reopen handler always focuses onboarding over all windows

What fails When onboarding is open (e.g., via Help & Tour), clicking the dock icon always focuses the onboarding window and returns early, preventing access to editor, settings, or other windows.
Result The onboarding window is focused. The editor or other windows cannot be reached via the dock icon.
Expected Dock click should show/focus the most relevant window, not always trap on onboarding during revisits.
Impact Users who open Help & Tour are effectively locked out of using the dock icon to navigate to other open windows until they close onboarding.
How to reproduce
1. Open the main window. 2. Click 'Help & Tour' to open onboarding. 3. Open an editor window. 4. Click the dock icon (macOS).
AI Fix Prompt
Fix this issue: Dock Reopen handler always focuses onboarding over all windows. Users can't access other windows via dock click. Move onboarding check after other window checks.

Location: apps/desktop/src-tauri/src/lib.rs (lines 3885)
Problem: When onboarding is open (e.g., via Help & Tour), clicking the dock icon always focuses the onboarding window and returns early, preventing access to editor, settings, or other windows.
Current behavior: The onboarding window is focused. The editor or other windows cannot be reached via the dock icon.
Expected: Dock click should show/focus the most relevant window, not always trap on onboarding during revisits.
Steps to reproduce: 1. Open the main window. 2. Click 'Help & Tour' to open onboarding. 3. Open an editor window. 4. Click the dock icon (macOS).

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

|| !permissions.necessary_granted()
{
let _ = ShowCapWindow::Setup.show(&app).await;
println!("Showing onboarding");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: println

println! used instead of tracing macros for startup logs. Structured logging is bypassed. Use info! or debug! macros instead.

View Details

Location: apps/desktop/src-tauri/src/lib.rs (lines 3530)

Analysis

println! used instead of tracing macros for startup logs. Structured logging is bypassed

What fails Startup decision logging uses println! instead of the tracing crate's structured logging macros, so these messages bypass log levels, filtering, and structured output.
Result Messages go to raw stdout, not captured by the tracing subscriber or log files.
Expected Messages should use info!() or debug!() so they appear in structured logs with timestamps and levels.
Impact Debugging startup flow is harder in production since these messages aren't in the app's log files.
How to reproduce
1. Build and run the desktop app. 2. Observe stdout for 'Showing onboarding' or 'Showing main window' messages. 3. Note these don't appear in structured logs.
Patch Details
-                        println!("Showing onboarding");
+                        info!("Showing onboarding");
AI Fix Prompt
Fix this issue: println! used instead of tracing macros for startup logs. Structured logging is bypassed. Use info! or debug! macros instead.

Location: apps/desktop/src-tauri/src/lib.rs (lines 3530)
Problem: Startup decision logging uses println! instead of the tracing crate's structured logging macros, so these messages bypass log levels, filtering, and structured output.
Current behavior: Messages go to raw stdout, not captured by the tracing subscriber or log files.
Expected: Messages should use info!() or debug!() so they appear in structured logs with timestamps and levels.
Steps to reproduce: 1. Build and run the desktop app. 2. Observe stdout for 'Showing onboarding' or 'Showing main window' messages. 3. Note these don't appear in structured logs.

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

): () => number {
const [phase, setPhase] = createSignal(0);

createEffect(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Permission polling runs every 250ms while active

Permission polling runs every 250ms while active. This is aggressive for a background check. Increase interval to 1000ms or use event-driven updates.

View Details

Location: apps/desktop/src/routes/(window-chrome)/onboarding.tsx (lines 162)

Analysis

Permission polling runs every 250ms while active. This is aggressive for a background check

What fails The permissions step polls for permission status every 250ms via setInterval, causing frequent IPC calls to the Rust backend.
Result High-frequency polling with 250ms intervals causes unnecessary IPC overhead and CPU usage.
Expected A 1000ms interval or event-driven notification when permissions change would reduce resource usage.
Impact Minor performance overhead on the permissions step, especially on lower-end machines.
How to reproduce
1. Open onboarding. 2. Stay on the permissions step. 3. Monitor IPC calls or CPU usage — doPermissionsCheck fires 4 times per second.
Patch Details
-			const interval = setInterval(fetchPermissions, 250);
+			const interval = setInterval(fetchPermissions, 1000);
AI Fix Prompt
Fix this issue: Permission polling runs every 250ms while active. This is aggressive for a background check. Increase interval to 1000ms or use event-driven updates.

Location: apps/desktop/src/routes/(window-chrome)/onboarding.tsx (lines 162)
Problem: The permissions step polls for permission status every 250ms via setInterval, causing frequent IPC calls to the Rust backend.
Current behavior: High-frequency polling with 250ms intervals causes unnecessary IPC overhead and CPU usage.
Expected: A 1000ms interval or event-driven notification when permissions change would reduce resource usage.
Steps to reproduce: 1. Open onboarding. 2. Stay on the permissions step. 3. Monitor IPC calls or CPU usage — doPermissionsCheck fires 4 times per second.

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

if !matches!(self, Self::Camera { .. } | Self::InProgressRecording { .. })
&& let Some(window) = self.id(app).get(app)
{
if matches!(self, Self::Main { .. })
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate permission guard for Main window in existing-window path

Duplicate permission guard for Main window in existing-window path. Same check exists in creation path. Remove the redundant early check or consolidate.

View Details

Location: apps/desktop/src-tauri/src/windows.rs (lines 925)

Analysis

Duplicate permission guard for Main window in existing-window path

What fails ShowCapWindow::show() checks permissions::do_permissions_check(false).necessary_granted() for the Main window in two separate locations — once when the window already exists (line 925) and again when creating a new window.
Result Two identical permission checks run in different branches. If the existing Main window is found, the first check runs and redirects to onboarding. If not found, the second check also redirects.
Expected A single permission check before branching on window existence, or a clear comment explaining why both are needed.
Impact Code maintainability — changes to permission logic must be duplicated in two places, risking divergence.
How to reproduce
1. Read windows.rs show() method. 2. Note the permission check at line 925-929 for existing Main window. 3. Note the same check in the Self::Main creation arm.
AI Fix Prompt
Fix this issue: Duplicate permission guard for Main window in existing-window path. Same check exists in creation path. Remove the redundant early check or consolidate.

Location: apps/desktop/src-tauri/src/windows.rs (lines 925)
Problem: ShowCapWindow::show() checks permissions::do_permissions_check(false).necessary_granted() for the Main window in two separate locations — once when the window already exists (line 925) and again when creating a new window.
Current behavior: Two identical permission checks run in different branches. If the existing Main window is found, the first check runs and redirects to onboarding. If not found, the second check also redirects.
Expected: A single permission check before branching on window existence, or a clear comment explaining why both are needed.
Steps to reproduce: 1. Read windows.rs show() method. 2. Note the permission check at line 925-929 for existing Main window. 3. Note the same check in the Self::Main creation arm.

Provide a code fix.


Tip: Reply with @paragon-run to automatically fix this issue

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