@@ -19,14 +19,16 @@ import {
1919 isDesktopZoomPercent ,
2020 isPendingDesktopScopeId ,
2121} from '@sim/desktop-bridge'
22+ import { createLogger } from '@sim/logger'
2223import {
2324 isTerminalOperation ,
2425 isTerminalToolName ,
2526 type TerminalToolArgs ,
2627} from '@sim/terminal-protocol'
28+ import { getErrorMessage } from '@sim/utils/errors'
2729import { isRecordLike } from '@sim/utils/object'
2830import type { BrowserWindow , IpcMainEvent , IpcMainInvokeEvent , WebContents } from 'electron'
29- import { clipboard , ipcMain } from 'electron'
31+ import { clipboard , ipcMain , shell } from 'electron'
3032import {
3133 type BrowserToolQueueBoundary ,
3234 cancelActiveTool ,
@@ -84,9 +86,35 @@ import type { ScopedEventRouter } from '@/main/scoped-event-router'
8486import type { TerminalRegistry } from '@/main/terminal/registry'
8587import { findCachedTerminalThemeProfile , listTerminalThemeProfiles } from '@/main/terminal-themes'
8688
89+ const logger = createLogger ( 'DesktopIpc' )
90+
8791/** Workspace/chat ids are opaque tokens; anything else never reaches a URL. */
8892const ID_PATTERN = / ^ [ A - Z a - z 0 - 9 _ - ] { 1 , 128 } $ /
8993
94+ const MICROPHONE_SETTINGS_URLS : Partial < Record < NodeJS . Platform , string > > = {
95+ darwin : 'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone' ,
96+ win32 : 'ms-settings:privacy-microphone' ,
97+ }
98+
99+ /** Opens the native microphone privacy pane without accepting a renderer-provided URL. */
100+ export async function openMicrophoneSettings (
101+ platform : NodeJS . Platform = process . platform
102+ ) : Promise < boolean > {
103+ const settingsUrl = MICROPHONE_SETTINGS_URLS [ platform ]
104+ if ( ! settingsUrl ) return false
105+
106+ try {
107+ await shell . openExternal ( settingsUrl )
108+ return true
109+ } catch ( error ) {
110+ logger . warn ( 'Could not open microphone privacy settings' , {
111+ error : getErrorMessage ( error ) ,
112+ platform,
113+ } )
114+ return false
115+ }
116+ }
117+
90118/**
91119 * Desktop state is partitioned by the existing chat id. A new-chat view uses
92120 * the composer’s existing provisional key until the server assigns that id.
@@ -627,6 +655,12 @@ export function registerIpcHandlers(deps: IpcDeps): void {
627655 handler : ( url ) =>
628656 typeof url === 'string' ? openExternalSafe ( url , deps . allowHttpLocalhost ( ) ) : false ,
629657 } ,
658+ 'desktop:open-microphone-settings' : {
659+ kind : 'invoke' ,
660+ gate : 'app-origin' ,
661+ denied : false ,
662+ handler : ( ) => openMicrophoneSettings ( ) ,
663+ } ,
630664 // OAuth connect handoff: the whole flow runs in the system browser (state
631665 // is cookie-bound to the initiating user agent), returning via loopback.
632666 'desktop:oauth-connect' : {
0 commit comments