From d30cb88651668a2aaba2ef5d987db984a42c2e78 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:28:20 +0530 Subject: [PATCH] wait on screen recording permissions --- apps/x/apps/main/forge.config.cjs | 24 ++++++------- apps/x/apps/main/src/ipc.ts | 12 ++++++- apps/x/apps/renderer/src/App.tsx | 57 ++++++++++++++++++++----------- apps/x/packages/shared/src/ipc.ts | 10 ++++++ 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/apps/x/apps/main/forge.config.cjs b/apps/x/apps/main/forge.config.cjs index c79a8c43..120c96b1 100644 --- a/apps/x/apps/main/forge.config.cjs +++ b/apps/x/apps/main/forge.config.cjs @@ -11,18 +11,18 @@ module.exports = { icon: './icons/icon', // .icns extension added automatically appBundleId: 'com.rowboat.app', appCategoryType: 'public.app-category.productivity', - osxSign: { - batchCodesignCalls: true, - optionsForFile: () => ({ - entitlements: path.join(__dirname, 'entitlements.plist'), - 'entitlements-inherit': path.join(__dirname, 'entitlements.plist'), - }), - }, - osxNotarize: { - appleId: process.env.APPLE_ID, - appleIdPassword: process.env.APPLE_PASSWORD, - teamId: process.env.APPLE_TEAM_ID - }, + // osxSign: { + // batchCodesignCalls: true, + // optionsForFile: () => ({ + // entitlements: path.join(__dirname, 'entitlements.plist'), + // 'entitlements-inherit': path.join(__dirname, 'entitlements.plist'), + // }), + // }, + // osxNotarize: { + // appleId: process.env.APPLE_ID, + // appleIdPassword: process.env.APPLE_PASSWORD, + // teamId: process.env.APPLE_TEAM_ID + // }, // Since we bundle everything with esbuild, we don't need node_modules at all. // These settings prevent Forge's dependency walker (flora-colossus) from trying // to analyze/copy node_modules, which fails with pnpm's symlinked workspaces. diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b92e3fe9..2ab77c1e 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -1,4 +1,4 @@ -import { ipcMain, BrowserWindow, shell, dialog } from 'electron'; +import { ipcMain, BrowserWindow, shell, dialog, systemPreferences } from 'electron'; import { ipc } from '@x/shared'; import path from 'node:path'; import os from 'node:os'; @@ -719,6 +719,16 @@ export function setupIpcHandlers() { return { success: false, error: 'Unknown format' }; }, + 'meeting:checkScreenPermission': async () => { + if (process.platform !== 'darwin') return { granted: true }; + const status = systemPreferences.getMediaAccessStatus('screen'); + console.log('[meeting] Screen recording permission status:', status); + return { granted: status === 'granted' }; + }, + 'meeting:openScreenRecordingSettings': async () => { + await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture'); + return { success: true }; + }, 'meeting:summarize': async (_event, args) => { const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson); return { notes }; diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f83ea5cb..6cc127bd 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3417,9 +3417,9 @@ function App() { const [meetingSummarizing, setMeetingSummarizing] = useState(false) const [showMeetingPermissions, setShowMeetingPermissions] = useState(false) - const startMeetingAfterPermissions = useCallback(async () => { - setShowMeetingPermissions(false) - localStorage.setItem('meeting-permissions-acknowledged', '1') + const [checkingPermission, setCheckingPermission] = useState(false) + + const startMeetingNow = useCallback(async () => { const calEvent = pendingCalendarEventRef.current pendingCalendarEventRef.current = undefined const notePath = await meetingTranscription.start(calEvent) @@ -3429,6 +3429,23 @@ function App() { } }, [meetingTranscription, handleVoiceNoteCreated]) + const handleCheckPermissionAndRetry = useCallback(async () => { + setCheckingPermission(true) + try { + const { granted } = await window.ipc.invoke('meeting:checkScreenPermission', null) + if (granted) { + setShowMeetingPermissions(false) + await startMeetingNow() + } + } finally { + setCheckingPermission(false) + } + }, [startMeetingNow]) + + const handleOpenScreenRecordingSettings = useCallback(async () => { + await window.ipc.invoke('meeting:openScreenRecordingSettings', null) + }, []) + const handleToggleMeeting = useCallback(async () => { if (meetingTranscription.state === 'recording') { await meetingTranscription.stop() @@ -3477,20 +3494,18 @@ function App() { meetingNotePathRef.current = null } } else if (meetingTranscription.state === 'idle') { - // Show permissions modal on first use (macOS only — Windows works out of the box) - if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) { - setShowMeetingPermissions(true) - return - } - const calEvent = pendingCalendarEventRef.current - pendingCalendarEventRef.current = undefined - const notePath = await meetingTranscription.start(calEvent) - if (notePath) { - meetingNotePathRef.current = notePath - await handleVoiceNoteCreated(notePath) + // On macOS, check screen recording permission before starting + if (isMac) { + const result = await window.ipc.invoke('meeting:checkScreenPermission', null) + console.log('[meeting] Permission check result:', result) + if (!result.granted) { + setShowMeetingPermissions(true) + return + } } + await startMeetingNow() } - }, [meetingTranscription, handleVoiceNoteCreated]) + }, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow]) handleToggleMeetingRef.current = handleToggleMeeting // Listen for calendar block "join meeting & take notes" events @@ -4421,23 +4436,25 @@ function App() { - Meeting transcription setup + Screen recording permission required - Rowboat needs Screen Recording permission to capture meeting audio from other apps (Zoom, Meet, etc.). + Rowboat needs Screen Recording permission to capture meeting audio from other apps (Zoom, Meet, etc.). This feature won't work without it.

To enable this:

    -
  1. Open System SettingsPrivacy & Security
  2. -
  3. Click Screen Recording
  4. +
  5. Open System SettingsPrivacy & SecurityScreen Recording
  6. Toggle on Rowboat
  7. You may need to restart the app after granting permission
- + +
diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 5f4988f4..28718db3 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -501,6 +501,16 @@ const ipcSchemas = { mimeType: z.string(), }), }, + 'meeting:checkScreenPermission': { + req: z.null(), + res: z.object({ + granted: z.boolean(), + }), + }, + 'meeting:openScreenRecordingSettings': { + req: z.null(), + res: z.object({ success: z.boolean() }), + }, 'meeting:summarize': { req: z.object({ transcript: z.string(),