wait on screen recording permissions

This commit is contained in:
Arjun 2026-03-28 11:28:20 +05:30
parent 30e1785fe2
commit d30cb88651
4 changed files with 70 additions and 33 deletions

View file

@ -11,18 +11,18 @@ module.exports = {
icon: './icons/icon', // .icns extension added automatically icon: './icons/icon', // .icns extension added automatically
appBundleId: 'com.rowboat.app', appBundleId: 'com.rowboat.app',
appCategoryType: 'public.app-category.productivity', appCategoryType: 'public.app-category.productivity',
osxSign: { // osxSign: {
batchCodesignCalls: true, // batchCodesignCalls: true,
optionsForFile: () => ({ // optionsForFile: () => ({
entitlements: path.join(__dirname, 'entitlements.plist'), // entitlements: path.join(__dirname, 'entitlements.plist'),
'entitlements-inherit': path.join(__dirname, 'entitlements.plist'), // 'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
}), // }),
}, // },
osxNotarize: { // osxNotarize: {
appleId: process.env.APPLE_ID, // appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD, // appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID // teamId: process.env.APPLE_TEAM_ID
}, // },
// Since we bundle everything with esbuild, we don't need node_modules at all. // 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 // These settings prevent Forge's dependency walker (flora-colossus) from trying
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces. // to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.

View file

@ -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 { ipc } from '@x/shared';
import path from 'node:path'; import path from 'node:path';
import os from 'node:os'; import os from 'node:os';
@ -719,6 +719,16 @@ export function setupIpcHandlers() {
return { success: false, error: 'Unknown format' }; 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) => { 'meeting:summarize': async (_event, args) => {
const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson); const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson);
return { notes }; return { notes };

View file

@ -3417,9 +3417,9 @@ function App() {
const [meetingSummarizing, setMeetingSummarizing] = useState(false) const [meetingSummarizing, setMeetingSummarizing] = useState(false)
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false) const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
const startMeetingAfterPermissions = useCallback(async () => { const [checkingPermission, setCheckingPermission] = useState(false)
setShowMeetingPermissions(false)
localStorage.setItem('meeting-permissions-acknowledged', '1') const startMeetingNow = useCallback(async () => {
const calEvent = pendingCalendarEventRef.current const calEvent = pendingCalendarEventRef.current
pendingCalendarEventRef.current = undefined pendingCalendarEventRef.current = undefined
const notePath = await meetingTranscription.start(calEvent) const notePath = await meetingTranscription.start(calEvent)
@ -3429,6 +3429,23 @@ function App() {
} }
}, [meetingTranscription, handleVoiceNoteCreated]) }, [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 () => { const handleToggleMeeting = useCallback(async () => {
if (meetingTranscription.state === 'recording') { if (meetingTranscription.state === 'recording') {
await meetingTranscription.stop() await meetingTranscription.stop()
@ -3477,20 +3494,18 @@ function App() {
meetingNotePathRef.current = null meetingNotePathRef.current = null
} }
} else if (meetingTranscription.state === 'idle') { } else if (meetingTranscription.state === 'idle') {
// Show permissions modal on first use (macOS only — Windows works out of the box) // On macOS, check screen recording permission before starting
if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) { if (isMac) {
setShowMeetingPermissions(true) const result = await window.ipc.invoke('meeting:checkScreenPermission', null)
return console.log('[meeting] Permission check result:', result)
} if (!result.granted) {
const calEvent = pendingCalendarEventRef.current setShowMeetingPermissions(true)
pendingCalendarEventRef.current = undefined return
const notePath = await meetingTranscription.start(calEvent) }
if (notePath) {
meetingNotePathRef.current = notePath
await handleVoiceNoteCreated(notePath)
} }
await startMeetingNow()
} }
}, [meetingTranscription, handleVoiceNoteCreated]) }, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow])
handleToggleMeetingRef.current = handleToggleMeeting handleToggleMeetingRef.current = handleToggleMeeting
// Listen for calendar block "join meeting & take notes" events // Listen for calendar block "join meeting & take notes" events
@ -4421,23 +4436,25 @@ function App() {
<Dialog open={showMeetingPermissions} onOpenChange={setShowMeetingPermissions}> <Dialog open={showMeetingPermissions} onOpenChange={setShowMeetingPermissions}>
<DialogContent showCloseButton={false}> <DialogContent showCloseButton={false}>
<DialogHeader> <DialogHeader>
<DialogTitle>Meeting transcription setup</DialogTitle> <DialogTitle>Screen recording permission required</DialogTitle>
<DialogDescription> <DialogDescription>
Rowboat needs <strong>Screen Recording</strong> permission to capture meeting audio from other apps (Zoom, Meet, etc.). Rowboat needs <strong>Screen Recording</strong> permission to capture meeting audio from other apps (Zoom, Meet, etc.). This feature won't work without it.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm text-muted-foreground"> <div className="space-y-3 text-sm text-muted-foreground">
<p>To enable this:</p> <p>To enable this:</p>
<ol className="list-decimal list-inside space-y-1.5"> <ol className="list-decimal list-inside space-y-1.5">
<li>Open <strong>System Settings</strong> <strong>Privacy & Security</strong></li> <li>Open <strong>System Settings</strong> <strong>Privacy & Security</strong> <strong>Screen Recording</strong></li>
<li>Click <strong>Screen Recording</strong></li>
<li>Toggle on <strong>Rowboat</strong></li> <li>Toggle on <strong>Rowboat</strong></li>
<li>You may need to restart the app after granting permission</li> <li>You may need to restart the app after granting permission</li>
</ol> </ol>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setShowMeetingPermissions(false)}>Cancel</Button> <Button variant="outline" onClick={() => setShowMeetingPermissions(false)}>Cancel</Button>
<Button onClick={() => { void startMeetingAfterPermissions() }}>Continue</Button> <Button variant="outline" onClick={() => { void handleOpenScreenRecordingSettings() }}>Open System Settings</Button>
<Button onClick={() => { void handleCheckPermissionAndRetry() }} disabled={checkingPermission}>
{checkingPermission ? 'Checking...' : 'Check Again'}
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View file

@ -501,6 +501,16 @@ const ipcSchemas = {
mimeType: z.string(), 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': { 'meeting:summarize': {
req: z.object({ req: z.object({
transcript: z.string(), transcript: z.string(),