Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ tauri.windows.conf.json
# Cursor
.cursor
.env*.local
.docs/
16 changes: 16 additions & 0 deletions apps/desktop/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,21 @@ export default defineConfig({
define: {
"import.meta.vitest": "undefined",
},
optimizeDeps: {
include: [
"@tauri-apps/plugin-os",
"@tanstack/solid-query",
"@tauri-apps/api/webviewWindow",
"@tauri-apps/plugin-dialog",
"@tauri-apps/plugin-store",
"posthog-js",
"uuid",
"@tauri-apps/plugin-clipboard-manager",
"@tauri-apps/api/window",
"@tauri-apps/api/core",
"@tauri-apps/api/event",
"cva",
],
},
}),
});
8 changes: 6 additions & 2 deletions apps/desktop/src-tauri/src/general_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ fn default_enable_native_camera_preview() -> bool {
}

fn default_enable_new_recording_flow() -> bool {
true
false
}

fn no(_: &bool) -> bool {
Expand Down Expand Up @@ -259,10 +259,14 @@ pub fn init(app: &AppHandle) {
};

if !store.recording_picker_preference_set {
store.enable_new_recording_flow = true;
store.enable_new_recording_flow = false;
store.recording_picker_preference_set = true;
}

if store.enable_new_recording_flow {
store.enable_new_recording_flow = false;
}

if let Err(e) = store.save(app) {
error!("Failed to save general settings: {}", e);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,8 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
async move {
if !permissions.screen_recording.permitted()
|| !permissions.accessibility.permitted()
|| !permissions.microphone.permitted()
|| !permissions.camera.permitted()
Comment on lines 2700 to +2703
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read context around lines 2700-2703 to understand the permission flow
sed -n '2685,2730p' apps/desktop/src-tauri/src/lib.rs

Repository: CapSoftware/Cap

Length of output: 1862


🏁 Script executed:

# Find where the permissions check is implemented
rg "fn do_permissions_check" --max-count 5 -B 2 -A 20

Repository: CapSoftware/Cap

Length of output: 2006


🏁 Script executed:

# Find the OSPermissionsCheck struct and its permitted() method
rg "struct OSPermissionsCheck|fn permitted" --max-count 10 -B 2 -A 5

Repository: CapSoftware/Cap

Length of output: 1107


🏁 Script executed:

# Look for the Setup window implementation to see if it's skippable
rg "ShowCapWindow::Setup" --max-count 10 -B 5 -A 10

Repository: CapSoftware/Cap

Length of output: 2610


🏁 Script executed:

# Look for the Setup window file or implementation
fd "setup" --type f --extension tsx --extension ts --extension rs | head -20

Repository: CapSoftware/Cap

Length of output: 265


🏁 Script executed:

# Read the setup window implementation
cat -n apps/desktop/src/routes/\(window-chrome\)/setup.tsx | head -100

Repository: CapSoftware/Cap

Length of output: 3350


🏁 Script executed:

# Continue reading the setup window implementation
sed -n '100,250p' apps/desktop/src/routes/\(window-chrome\)/setup.tsx

Repository: CapSoftware/Cap

Length of output: 4244


Confirm that mandatory microphone and camera permissions at startup align with product goals.

The Setup window requires users to grant all four permissions (screen recording, accessibility, microphone, and camera) before accessing the main application. The "Continue" button is disabled until every permission shows as granted or not needed. Users cannot proceed without granting all permissions, even if they only intend to use screen recording features.

While the PR indicates this expansion is intentional, verify with product/design that forcing microphone and camera grants at initial onboarding is the desired UX pattern. This creates a hard gate on features that users may not need, potentially causing drop-off during onboarding. If these permissions are truly optional features, consider deferring their requests until users attempt to use those specific features.

🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/lib.rs around lines 2700-2703, the setup gating
logic requires microphone and camera permissions alongside screen recording and
accessibility before enabling Continue; confirm with product/design whether mic
and camera should be mandatory at onboarding, and if they are optional change
the gating logic to only require truly mandatory permissions (e.g., screen
recording and accessibility) and defer requesting mic/camera until the user
invokes features that need them; alternatively, if they must remain optional but
still shown, update the UI to indicate they are optional and allow Continue when
required permissions are granted, and implement lazy permission prompts where
mic/camera requests are triggered at feature use.

|| GeneralSettingsStore::get(&app)
.ok()
.flatten()
Expand Down
175 changes: 171 additions & 4 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Router, useCurrentMatches } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Route, Router, useCurrentMatches } from "@solidjs/router";
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
import {
getCurrentWebviewWindow,
type WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { message } from "@tauri-apps/plugin-dialog";
import { createEffect, onCleanup, onMount, Suspense } from "solid-js";
import { createEffect, lazy, onCleanup, onMount, Suspense } from "solid-js";
import { Toaster } from "solid-toast";

import "@cap/ui-solid/main.css";
Expand All @@ -19,6 +18,61 @@ import { initAnonymousUser } from "./utils/analytics";
import { type AppTheme, commands } from "./utils/tauri";
import titlebar from "./utils/titlebar-state";

const WindowChromeLayout = lazy(() => import("./routes/(window-chrome)"));
const MainPage = lazy(() => import("./routes/(window-chrome)/(main)"));
const NewMainPage = lazy(() => import("./routes/(window-chrome)/new-main"));
const SetupPage = lazy(() => import("./routes/(window-chrome)/setup"));
const SettingsLayout = lazy(() => import("./routes/(window-chrome)/settings"));
const SettingsGeneralPage = lazy(
() => import("./routes/(window-chrome)/settings/general"),
);
const SettingsRecordingsPage = lazy(
() => import("./routes/(window-chrome)/settings/recordings"),
);
const SettingsScreenshotsPage = lazy(
() => import("./routes/(window-chrome)/settings/screenshots"),
);
const SettingsHotkeysPage = lazy(
() => import("./routes/(window-chrome)/settings/hotkeys"),
);
const SettingsChangelogPage = lazy(
() => import("./routes/(window-chrome)/settings/changelog"),
);
const SettingsFeedbackPage = lazy(
() => import("./routes/(window-chrome)/settings/feedback"),
);
const SettingsExperimentalPage = lazy(
() => import("./routes/(window-chrome)/settings/experimental"),
);
const SettingsLicensePage = lazy(
() => import("./routes/(window-chrome)/settings/license"),
);
const SettingsIntegrationsPage = lazy(
() => import("./routes/(window-chrome)/settings/integrations"),
);
const SettingsS3ConfigPage = lazy(
() => import("./routes/(window-chrome)/settings/integrations/s3-config"),
);
const UpgradePage = lazy(() => import("./routes/(window-chrome)/upgrade"));
const UpdatePage = lazy(() => import("./routes/(window-chrome)/update"));
const CameraPage = lazy(() => import("./routes/camera"));
const CaptureAreaPage = lazy(() => import("./routes/capture-area"));
const DebugPage = lazy(() => import("./routes/debug"));
const EditorPage = lazy(() => import("./routes/editor"));
const InProgressRecordingPage = lazy(
() => import("./routes/in-progress-recording"),
);
const ModeSelectPage = lazy(() => import("./routes/mode-select"));
const NotificationsPage = lazy(() => import("./routes/notifications"));
const RecordingsOverlayPage = lazy(() => import("./routes/recordings-overlay"));
const ScreenshotEditorPage = lazy(() => import("./routes/screenshot-editor"));
const TargetSelectOverlayPage = lazy(
() => import("./routes/target-select-overlay"),
);
const WindowCaptureOccluderPage = lazy(
() => import("./routes/window-capture-occluder"),
);

const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand All @@ -35,6 +89,20 @@ const queryClient = new QueryClient({
});

export default function App() {
// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "App.tsx:App",
message: "App component rendering",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C",
}),
}).catch(() => {});
// #endregion
return (
<QueryClientProvider client={queryClient}>
<Suspense>
Expand All @@ -45,10 +113,38 @@ export default function App() {
}

function Inner() {
// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "App.tsx:Inner",
message: "Inner component rendering",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C",
}),
}).catch(() => {});
// #endregion
const currentWindow = getCurrentWebviewWindow();
createThemeListener(currentWindow);

onMount(() => {
// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "App.tsx:Inner:onMount",
message: "Inner onMount",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C",
}),
}).catch(() => {});
// #endregion
initAnonymousUser();
});

Expand Down Expand Up @@ -78,6 +174,29 @@ function Inner() {
const matches = useCurrentMatches();

onMount(() => {
// #region agent log
fetch(
"http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "App.tsx:Router:root:onMount",
message: "Router root onMount",
data: {
pathname: location.pathname,
matchCount: matches().length,
autoShowFlags: matches().map(
(m) => m.route.info?.AUTO_SHOW_WINDOW,
),
},
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C,D",
}),
},
).catch(() => {});
// #endregion
for (const match of matches()) {
if (match.route.info?.AUTO_SHOW_WINDOW === false) return;
}
Expand All @@ -98,7 +217,55 @@ function Inner() {
);
}}
>
<FileRoutes />
<Route path="/" component={WindowChromeLayout}>
<Route path="/" component={MainPage} />
<Route path="/new-main" component={NewMainPage} />
<Route path="/setup" component={SetupPage} />
<Route path="/settings" component={SettingsLayout}>
<Route path="/" component={SettingsGeneralPage} />
<Route path="/general" component={SettingsGeneralPage} />
<Route path="/recordings" component={SettingsRecordingsPage} />
<Route path="/screenshots" component={SettingsScreenshotsPage} />
<Route path="/hotkeys" component={SettingsHotkeysPage} />
<Route path="/changelog" component={SettingsChangelogPage} />
<Route path="/feedback" component={SettingsFeedbackPage} />
<Route
path="/experimental"
component={SettingsExperimentalPage}
/>
<Route path="/license" component={SettingsLicensePage} />
<Route
path="/integrations"
component={SettingsIntegrationsPage}
/>
<Route
path="/integrations/s3-config"
component={SettingsS3ConfigPage}
/>
</Route>
<Route path="/upgrade" component={UpgradePage} />
<Route path="/update" component={UpdatePage} />
</Route>
<Route path="/camera" component={CameraPage} />
<Route path="/capture-area" component={CaptureAreaPage} />
<Route path="/debug" component={DebugPage} />
<Route path="/editor" component={EditorPage} />
<Route
path="/in-progress-recording"
component={InProgressRecordingPage}
/>
<Route path="/mode-select" component={ModeSelectPage} />
<Route path="/notifications" component={NotificationsPage} />
<Route path="/recordings-overlay" component={RecordingsOverlayPage} />
<Route path="/screenshot-editor" component={ScreenshotEditorPage} />
<Route
path="/target-select-overlay"
component={TargetSelectOverlayPage}
/>
<Route
path="/window-capture-occluder"
component={WindowCaptureOccluderPage}
/>
</Router>
</CapErrorBoundary>
</>
Expand Down
16 changes: 13 additions & 3 deletions apps/desktop/src/entry-client.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// @refresh reload
import { mount, StartClient } from "@solidjs/start/client";
import { type } from "@tauri-apps/plugin-os";

document.documentElement.classList.add(`platform-${type()}`);
mount(() => <StartClient />, document.getElementById("app")!);
async function initApp() {
try {
const { type } = await import("@tauri-apps/plugin-os");
const osType = type();
document.documentElement.classList.add(`platform-${osType}`);
} catch (error) {
console.error("Failed to get OS type:", error);
}

mount(() => <StartClient />, document.getElementById("app")!);
}

initApp();
63 changes: 57 additions & 6 deletions apps/desktop/src/routes/(window-chrome).tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,39 @@ import {
WindowChromeContext,
} from "./(window-chrome)/Context";

export const route = {
info: {
AUTO_SHOW_WINDOW: false,
},
};

export default function (props: RouteSectionProps) {
let unlistenResize: UnlistenFn | undefined;

// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "(window-chrome).tsx:render",
message: "WindowChrome component rendering",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C",
}),
}).catch(() => {});
// #endregion

onMount(async () => {
// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "(window-chrome).tsx:onMount",
message: "WindowChrome onMount",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C",
}),
}).catch(() => {});
// #endregion
console.log("window chrome mounted");
unlistenResize = await initializeTitlebar();
if (location.pathname === "/") getCurrentWindow().show();
Expand Down Expand Up @@ -92,7 +115,35 @@ function Header() {
}

function Inner(props: ParentProps) {
// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "(window-chrome).tsx:Inner:render",
message: "Inner component rendering",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "C",
}),
}).catch(() => {});
// #endregion
onMount(() => {
// #region agent log
fetch("http://127.0.0.1:7243/ingest/1cff95e2-fcb2-4b1f-a666-2aa2ac4f0e23", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "(window-chrome).tsx:Inner:onMount",
message: "Inner onMount, about to show window",
data: { pathname: location.pathname },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "B,C",
}),
}).catch(() => {});
// #endregion
if (location.pathname !== "/") getCurrentWindow().show();
});

Expand Down
Loading