rowboat/apps/x/apps/preload/src/preload.ts
Arjun 781b16d881 feat(x): browser screen-share picker, web notifications, Chrome extensions
Three capability upgrades to the embedded browser pane:

- Screen share: getDisplayMedia() in the browser partition now shows a
  Chrome-style source picker (screens/windows with thumbnails, optional
  system audio for screens) instead of silently capturing the primary
  screen. Requests are denied when the pane is hidden. The app's own
  session keeps its auto-loopback handler for meeting transcription.

- Notifications: the browser partition grants the notifications
  permission so sites (WhatsApp Web, Gmail, ...) can post native OS
  notifications while loaded. Background Web Push remains unavailable.

- Extensions: electron-chrome-extensions wired to the browser session.
  Unpacked extensions load from ~/.rowboat/extensions/<name>/; tab
  lifecycle is mirrored into chrome.tabs, and a <browser-action-list>
  toolbar hosts action icons/popups next to the address bar. NOTE: the
  library is GPL-3.0/Patron dual-licensed — licensing decision required
  before shipping in a release build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 09:18:40 +05:30

72 lines
2.3 KiB
TypeScript

import { contextBridge, ipcRenderer, webFrame, webUtils } from 'electron';
import { injectBrowserAction } from 'electron-chrome-extensions/browser-action';
import { ipc as ipcShared } from '@x/shared';
// Expose the <browser-action-list> custom element (extension action icons +
// popups for the embedded browser pane). App documents only — this preload
// is attached solely to the app window, but guard against it ever being
// reused for remote content.
if (location.protocol === 'app:' || location.origin === 'http://localhost:5173') {
try {
injectBrowserAction();
} catch (error) {
console.error('[preload] injectBrowserAction failed:', error);
}
}
type InvokeChannels = ipcShared.InvokeChannels;
type IPCChannels = ipcShared.IPCChannels;
type SendChannels = ipcShared.SendChannels;
const { validateRequest } = ipcShared;
const ipc = {
/**
* Invoke a channel that expects a response (request/response pattern)
* Only channels with non-null responses can be invoked
*/
invoke<K extends InvokeChannels>(
channel: K,
args: IPCChannels[K]['req']
): Promise<IPCChannels[K]['res']> {
// Runtime validation of request payload
const validatedArgs = validateRequest(channel, args);
return ipcRenderer.invoke(channel, validatedArgs);
},
/**
* Send a message to a channel without expecting a response (fire-and-forget)
* Only channels with null responses can be sent
*/
send<K extends SendChannels>(
channel: K,
args: IPCChannels[K]['req']
): void {
// Runtime validation of request payload
const validatedArgs = validateRequest(channel, args);
ipcRenderer.send(channel, validatedArgs);
},
/**
* Listen to a send channel event
* Returns a cleanup function to remove the listener
*/
on<K extends SendChannels>(
channel: K,
handler: (event: IPCChannels[K]['req']) => void
): () => void {
const listener = (_event: unknown, data: IPCChannels[K]['req']) => {
handler(data);
};
ipcRenderer.on(channel, listener);
return () => {
ipcRenderer.removeListener(channel, listener);
};
},
};
contextBridge.exposeInMainWorld('ipc', ipc);
contextBridge.exposeInMainWorld('electronUtils', {
getPathForFile: (file: File) => webUtils.getPathForFile(file),
getZoomFactor: () => webFrame.getZoomFactor(),
});