diff --git a/src/App.jsx b/src/App.jsx
index 7bc72af4..556b24db 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -114,6 +114,9 @@ export default function App() {
});
const unsubscribeAccessibility = window.electronAPI?.onAccessibilityMissing?.(() => {
+ if (localStorage.getItem("accessibilitySkipped") === "true") {
+ return;
+ }
toast({
title: t("app.toasts.accessibilityMissing.title"),
description: t("app.toasts.accessibilityMissing.description"),
diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx
index 7825b70c..c5d059eb 100644
--- a/src/components/ControlPanel.tsx
+++ b/src/components/ControlPanel.tsx
@@ -217,6 +217,9 @@ export default function ControlPanel() {
// When accessibility is missing on macOS, open the permissions settings page
useEffect(() => {
const cleanup = window.electronAPI?.onAccessibilityMissing?.(() => {
+ if (localStorage.getItem("accessibilitySkipped") === "true") {
+ return;
+ }
setSettingsSection("privacyData");
setShowSettings(true);
toast({
diff --git a/src/components/OnboardingFlow.tsx b/src/components/OnboardingFlow.tsx
index c250c003..d1931a81 100644
--- a/src/components/OnboardingFlow.tsx
+++ b/src/components/OnboardingFlow.tsx
@@ -71,6 +71,14 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
},
}
);
+ const [accessibilitySkipped, setAccessibilitySkipped] = useLocalStorage(
+ "accessibilitySkipped",
+ false,
+ {
+ serialize: String,
+ deserialize: (value) => value === "true",
+ }
+ );
const {
useLocalWhisper,
@@ -128,6 +136,16 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
const screenRecording = useScreenRecordingPermission();
+ useEffect(() => {
+ if (permissionsHook.accessibilityPermissionGranted && accessibilitySkipped) {
+ setAccessibilitySkipped(false);
+ }
+ }, [
+ permissionsHook.accessibilityPermissionGranted,
+ accessibilitySkipped,
+ setAccessibilitySkipped,
+ ]);
+
// For signed-in users, merge setup and permissions into one step
const steps =
isSignedIn && !skipAuth
@@ -296,6 +314,13 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
return;
}
+ const isMacOS = getPlatform() === "darwin";
+ const isPermissionsStep =
+ isSignedIn && !skipAuth ? currentStep === 1 : currentStep === 2;
+ if (isMacOS && isPermissionsStep && !permissionsHook.accessibilityPermissionGranted) {
+ setAccessibilitySkipped(true);
+ }
+
const newStep = currentStep + 1;
setCurrentStep(newStep);
@@ -305,7 +330,16 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
window.electronAPI.showDictationPanel();
}
}
- }, [currentStep, setCurrentStep, steps.length, activationStepIndex]);
+ }, [
+ currentStep,
+ setCurrentStep,
+ steps.length,
+ activationStepIndex,
+ isSignedIn,
+ skipAuth,
+ permissionsHook.accessibilityPermissionGranted,
+ setAccessibilitySkipped,
+ ]);
const prevStep = useCallback(() => {
if (currentStep > 0) {
@@ -409,9 +443,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
description={t("onboarding.permissions.accessibilityDescription")}
granted={permissionsHook.accessibilityPermissionGranted}
onRequest={permissionsHook.testAccessibilityPermission}
- buttonText={t("onboarding.permissions.testAndGrant")}
+ buttonText={t("onboarding.permissions.grant")}
onOpenSettings={permissionsHook.openAccessibilitySettings}
- openSettingsText={t("onboarding.permissions.openSystemSettings")}
+ badge={t("onboarding.permissions.optional")}
/>
{});
+ this.checkAccessibilityPermissions(true).catch(() => {});
this.resolveFastPasteBinary();
}
diff --git a/src/hooks/useAudioRecording.js b/src/hooks/useAudioRecording.js
index 774589b1..7334f1a4 100644
--- a/src/hooks/useAudioRecording.js
+++ b/src/hooks/useAudioRecording.js
@@ -113,10 +113,14 @@ export const useAudioRecording = (toast, options = {}) => {
const isStreaming = result.source?.includes("streaming");
const { keepTranscriptionInClipboard } = getSettings();
+ const accessibilitySkipped =
+ typeof window !== "undefined" &&
+ window.localStorage.getItem("accessibilitySkipped") === "true";
const pasteStart = performance.now();
await audioManagerRef.current.safePaste(result.text, {
...(isStreaming ? { fromStreaming: true } : {}),
restoreClipboard: !keepTranscriptionInClipboard,
+ allowClipboardFallback: accessibilitySkipped,
});
logger.info(
"Paste timing",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 9cfa4475..5e2b9bd2 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -157,7 +157,7 @@
"accessibilityTitle": "Accessibility",
"accessibilityDescription": "Enable in System Settings by toggling it on",
"testAndGrant": "Test & Grant",
- "requiredForApp": "Required for OpenWhispr to work",
+ "requiredForApp": "Microphone required. Accessibility enables auto-paste.",
"microphoneRequired": "Microphone access required",
"openSystemSettings": "Open System Settings",
"screenRecordingTitle": "System Audio",
diff --git a/src/main.jsx b/src/main.jsx
index 777fa35d..340d0b45 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -314,6 +314,7 @@ function MainApp() {
const authSkipped =
localStorage.getItem("authenticationSkipped") === "true" ||
localStorage.getItem("skipAuth") === "true";
+ const accessibilitySkipped = localStorage.getItem("accessibilitySkipped") === "true";
// Valid session proves prior onboarding — restore flag if localStorage was wiped
const isReturningUser = !onboardingCompleted && isSignedIn;
@@ -332,7 +333,7 @@ function MainApp() {
// Returning users who skipped onboarding may lack accessibility permissions.
// Trigger an immediate check so the main process sends accessibility-missing.
- if (isReturningUser) {
+ if (isReturningUser && !accessibilitySkipped) {
window.electronAPI?.checkAccessibilityTrusted?.();
}
}
diff --git a/src/types/electron.ts b/src/types/electron.ts
index 4acdfd90..e5e01529 100644
--- a/src/types/electron.ts
+++ b/src/types/electron.ts
@@ -275,7 +275,14 @@ declare global {
interface Window {
electronAPI: {
// Basic window operations
- pasteText: (text: string, options?: { fromStreaming?: boolean }) => Promise;
+ pasteText: (
+ text: string,
+ options?: {
+ fromStreaming?: boolean;
+ restoreClipboard?: boolean;
+ allowClipboardFallback?: boolean;
+ }
+ ) => Promise;
hideWindow: () => Promise;
showDictationPanel: () => Promise;
onToggleDictation: (callback: () => void) => () => void;