diff --git a/surfsense_desktop/src/modules/autocomplete/index.ts b/surfsense_desktop/src/modules/autocomplete/index.ts deleted file mode 100644 index d4eb727fd..000000000 --- a/surfsense_desktop/src/modules/autocomplete/index.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { clipboard, globalShortcut, ipcMain, screen } from 'electron'; -import { IPC_CHANNELS } from '../../ipc/channels'; -import { getFrontmostApp, getWindowTitle, hasAccessibilityPermission, simulatePaste } from '../platform'; -import { hasScreenRecordingPermission, requestAccessibility, requestScreenRecording } from '../permissions'; -import { captureScreen } from './screenshot'; -import { createSuggestionWindow, destroySuggestion, getSuggestionWindow } from './suggestion-window'; -import { getShortcuts } from '../shortcuts'; -import { getActiveSearchSpaceId } from '../active-search-space'; -import { trackEvent } from '../analytics'; - -let currentShortcut = ''; -let autocompleteEnabled = true; -let savedClipboard = ''; -let sourceApp = ''; - -function isSurfSenseWindow(): boolean { - const app = getFrontmostApp(); - return app === 'Electron' || app === 'SurfSense' || app === 'surfsense-desktop'; -} - -async function triggerAutocomplete(): Promise { - if (!autocompleteEnabled) return; - if (isSurfSenseWindow()) return; - - if (!hasScreenRecordingPermission()) { - requestScreenRecording(); - return; - } - - sourceApp = getFrontmostApp(); - const windowTitle = getWindowTitle(); - savedClipboard = clipboard.readText(); - - const screenshot = await captureScreen(); - if (!screenshot) { - console.error('[autocomplete] Screenshot capture failed'); - return; - } - - const searchSpaceId = await getActiveSearchSpaceId(); - if (!searchSpaceId) { - console.warn('[autocomplete] No active search space. Select a search space first.'); - return; - } - trackEvent('desktop_autocomplete_triggered', { search_space_id: searchSpaceId }); - const cursor = screen.getCursorScreenPoint(); - const win = createSuggestionWindow(cursor.x, cursor.y); - - win.webContents.once('did-finish-load', () => { - const sw = getSuggestionWindow(); - setTimeout(() => { - if (sw && !sw.isDestroyed()) { - sw.webContents.send(IPC_CHANNELS.AUTOCOMPLETE_CONTEXT, { - screenshot, - searchSpaceId, - appName: sourceApp, - windowTitle, - }); - } - }, 300); - }); -} - -async function acceptAndInject(text: string): Promise { - if (!sourceApp) return; - - if (!hasAccessibilityPermission()) { - requestAccessibility(); - return; - } - - clipboard.writeText(text); - destroySuggestion(); - - try { - await new Promise((r) => setTimeout(r, 50)); - simulatePaste(); - await new Promise((r) => setTimeout(r, 100)); - clipboard.writeText(savedClipboard); - } catch { - clipboard.writeText(savedClipboard); - } -} - -let ipcRegistered = false; - -function registerIpcHandlers(): void { - if (ipcRegistered) return; - ipcRegistered = true; - - ipcMain.handle(IPC_CHANNELS.ACCEPT_SUGGESTION, async (_event, text: string) => { - trackEvent('desktop_autocomplete_accepted'); - await acceptAndInject(text); - }); - ipcMain.handle(IPC_CHANNELS.DISMISS_SUGGESTION, () => { - trackEvent('desktop_autocomplete_dismissed'); - destroySuggestion(); - }); - ipcMain.handle(IPC_CHANNELS.SET_AUTOCOMPLETE_ENABLED, (_event, enabled: boolean) => { - autocompleteEnabled = enabled; - if (!enabled) { - destroySuggestion(); - } - }); - ipcMain.handle(IPC_CHANNELS.GET_AUTOCOMPLETE_ENABLED, () => autocompleteEnabled); -} - -function autocompleteHandler(): void { - const sw = getSuggestionWindow(); - if (sw && !sw.isDestroyed()) { - destroySuggestion(); - return; - } - triggerAutocomplete(); -} - -async function registerShortcut(): Promise { - const shortcuts = await getShortcuts(); - currentShortcut = shortcuts.autocomplete; - - const ok = globalShortcut.register(currentShortcut, autocompleteHandler); - - if (!ok) { - console.error(`[autocomplete] Failed to register shortcut ${currentShortcut}`); - } else { - console.log(`[autocomplete] Registered shortcut ${currentShortcut}`); - } -} - -export async function registerAutocomplete(): Promise { - registerIpcHandlers(); - await registerShortcut(); -} - -export function unregisterAutocomplete(): void { - if (currentShortcut) globalShortcut.unregister(currentShortcut); - destroySuggestion(); -} - -export async function reregisterAutocomplete(): Promise { - unregisterAutocomplete(); - await registerShortcut(); -} diff --git a/surfsense_desktop/src/modules/autocomplete/screenshot.ts b/surfsense_desktop/src/modules/autocomplete/screenshot.ts deleted file mode 100644 index 22b7c1b14..000000000 --- a/surfsense_desktop/src/modules/autocomplete/screenshot.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { desktopCapturer, screen } from 'electron'; - -/** - * Captures the primary display as a base64-encoded PNG data URL. - * Uses the display's actual size for full-resolution capture. - */ -export async function captureScreen(): Promise { - try { - const primaryDisplay = screen.getPrimaryDisplay(); - const { width, height } = primaryDisplay.size; - - const sources = await desktopCapturer.getSources({ - types: ['screen'], - thumbnailSize: { width, height }, - }); - - if (!sources.length) { - console.error('[screenshot] No screen sources found'); - return null; - } - - return sources[0].thumbnail.toDataURL(); - } catch (err) { - console.error('[screenshot] Failed to capture screen:', err); - return null; - } -} diff --git a/surfsense_desktop/src/modules/autocomplete/suggestion-window.ts b/surfsense_desktop/src/modules/autocomplete/suggestion-window.ts deleted file mode 100644 index 8f61b2901..000000000 --- a/surfsense_desktop/src/modules/autocomplete/suggestion-window.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { BrowserWindow, screen, shell } from 'electron'; -import path from 'path'; -import { getServerPort } from '../server'; - -const TOOLTIP_WIDTH = 420; -const TOOLTIP_HEIGHT = 38; -const MAX_HEIGHT = 400; - -let suggestionWindow: BrowserWindow | null = null; -let resizeTimer: ReturnType | null = null; -let cursorOrigin = { x: 0, y: 0 }; - -const CURSOR_GAP = 20; - -function positionOnScreen(cursorX: number, cursorY: number, w: number, h: number): { x: number; y: number } { - const display = screen.getDisplayNearestPoint({ x: cursorX, y: cursorY }); - const { x: dx, y: dy, width: dw, height: dh } = display.workArea; - - const x = Math.max(dx, Math.min(cursorX, dx + dw - w)); - - const spaceBelow = (dy + dh) - (cursorY + CURSOR_GAP); - const y = spaceBelow >= h - ? cursorY + CURSOR_GAP - : cursorY - h - CURSOR_GAP; - - return { x, y: Math.max(dy, y) }; -} - -function stopResizePolling(): void { - if (resizeTimer) { clearInterval(resizeTimer); resizeTimer = null; } -} - -function startResizePolling(win: BrowserWindow): void { - stopResizePolling(); - let lastH = 0; - resizeTimer = setInterval(async () => { - if (!win || win.isDestroyed()) { stopResizePolling(); return; } - try { - const h: number = await win.webContents.executeJavaScript( - `document.body.scrollHeight` - ); - if (h > 0 && h !== lastH) { - lastH = h; - const clamped = Math.min(h, MAX_HEIGHT); - const pos = positionOnScreen(cursorOrigin.x, cursorOrigin.y, TOOLTIP_WIDTH, clamped); - win.setBounds({ x: pos.x, y: pos.y, width: TOOLTIP_WIDTH, height: clamped }); - } - } catch {} - }, 150); -} - -export function getSuggestionWindow(): BrowserWindow | null { - return suggestionWindow; -} - -export function destroySuggestion(): void { - stopResizePolling(); - if (suggestionWindow && !suggestionWindow.isDestroyed()) { - suggestionWindow.close(); - } - suggestionWindow = null; -} - -export function createSuggestionWindow(x: number, y: number): BrowserWindow { - destroySuggestion(); - cursorOrigin = { x, y }; - - const pos = positionOnScreen(x, y, TOOLTIP_WIDTH, TOOLTIP_HEIGHT); - - suggestionWindow = new BrowserWindow({ - width: TOOLTIP_WIDTH, - height: TOOLTIP_HEIGHT, - x: pos.x, - y: pos.y, - frame: false, - transparent: true, - focusable: false, - alwaysOnTop: true, - skipTaskbar: true, - hasShadow: true, - type: 'panel', - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - }, - show: false, - }); - - suggestionWindow.loadURL(`http://localhost:${getServerPort()}/desktop/suggestion?t=${Date.now()}`); - - suggestionWindow.once('ready-to-show', () => { - suggestionWindow?.showInactive(); - if (suggestionWindow) startResizePolling(suggestionWindow); - }); - - suggestionWindow.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('http://localhost')) { - return { action: 'allow' }; - } - shell.openExternal(url); - return { action: 'deny' }; - }); - - suggestionWindow.on('closed', () => { - stopResizePolling(); - suggestionWindow = null; - }); - - return suggestionWindow; -}