From 7be4231ad4855e2a76b9c80675cc4f71cd069d20 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 17:45:27 +0530 Subject: [PATCH 01/12] feat(app): update product name to 'SurfSense' and implement server shutdown on app quit --- surfsense_desktop/package.json | 1 + surfsense_desktop/src/main.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/surfsense_desktop/package.json b/surfsense_desktop/package.json index 0ad279ece..4ee6ea3c4 100644 --- a/surfsense_desktop/package.json +++ b/surfsense_desktop/package.json @@ -1,5 +1,6 @@ { "name": "surfsense-desktop", + "productName": "SurfSense", "version": "0.0.25", "description": "SurfSense Desktop App", "main": "dist/main.js", diff --git a/surfsense_desktop/src/main.ts b/surfsense_desktop/src/main.ts index 492c61f17..632758ba8 100644 --- a/surfsense_desktop/src/main.ts +++ b/surfsense_desktop/src/main.ts @@ -1,7 +1,7 @@ import { app } from 'electron'; import { registerGlobalErrorHandlers, showErrorDialog } from './modules/errors'; -import { startNextServer } from './modules/server'; +import { startNextServer, stopNextServer } from './modules/server'; import { createMainWindow, getMainWindow, markQuitting } from './modules/window'; import { setupDeepLinks, handlePendingDeepLink, hasPendingDeepLink } from './modules/deep-links'; import { setupAutoUpdater } from './modules/auto-updater'; @@ -19,6 +19,7 @@ import { } from './modules/auto-launch'; registerGlobalErrorHandlers(); +app.setName('SurfSense'); if (!setupDeepLinks()) { app.quit(); @@ -93,6 +94,7 @@ app.on('will-quit', async (e) => { e.preventDefault(); unregisterQuickAsk(); unregisterFolderWatcher(); + stopNextServer(); destroyTray(); await shutdownAnalytics(); app.exit(); From a2847664c8112d04bb43fdadb30e1185943e6a2b Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 17:52:10 +0530 Subject: [PATCH 02/12] feat(server): enhance server management with process forking and implement server origin retrieval --- surfsense_desktop/src/modules/server.ts | 53 +++++++++++++++++++++---- surfsense_desktop/src/modules/window.ts | 10 +++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/surfsense_desktop/src/modules/server.ts b/surfsense_desktop/src/modules/server.ts index e2f078a8c..daec9bed0 100644 --- a/surfsense_desktop/src/modules/server.ts +++ b/surfsense_desktop/src/modules/server.ts @@ -1,9 +1,10 @@ import path from 'path'; -import { app } from 'electron'; +import { app, utilityProcess } from 'electron'; import { getPort } from 'get-port-please'; const isDev = !app.isPackaged; let serverPort = 3000; +let nextServerProcess: ReturnType | null = null; export function getServerPort(): number { return serverPort; @@ -38,16 +39,54 @@ export async function startNextServer(): Promise { const standalonePath = getStandalonePath(); const serverScript = path.join(standalonePath, 'server.js'); - process.env.PORT = String(serverPort); - process.env.HOSTNAME = '0.0.0.0'; - process.env.NODE_ENV = 'production'; - process.chdir(standalonePath); + const child = utilityProcess.fork(serverScript, [], { + cwd: standalonePath, + env: { + ...process.env, + PORT: String(serverPort), + HOSTNAME: '127.0.0.1', + NODE_ENV: 'production', + }, + serviceName: 'SurfSense Next Server', + stdio: 'pipe', + }); + nextServerProcess = child; - require(serverScript); + child.stdout?.on('data', (chunk) => { + process.stdout.write(chunk); + }); + child.stderr?.on('data', (chunk) => { + process.stderr.write(chunk); + }); - const ready = await waitForServer(`http://localhost:${serverPort}`); + const handleExit = (code: number) => { + if (nextServerProcess === child) { + nextServerProcess = null; + } + console.error(`Next.js server exited with code ${code}`); + }; + child.on('exit', handleExit); + + let startupExitHandler: ((code: number) => void) | null = null; + const exited = new Promise((_resolve, reject) => { + startupExitHandler = (code: number) => { + reject(new Error(`Next.js server exited before startup completed with code ${code}`)); + }; + child.once('exit', startupExitHandler); + }); + + const ready = await Promise.race([waitForServer(`http://localhost:${serverPort}`), exited]); + if (startupExitHandler) { + child.removeListener('exit', startupExitHandler); + } if (!ready) { + stopNextServer(); throw new Error('Next.js server failed to start within 30 s'); } console.log(`Next.js server ready on port ${serverPort}`); } + +export function stopNextServer(): void { + nextServerProcess?.kill(); + nextServerProcess = null; +} diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index 5317005d5..7ee7eb8db 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -8,6 +8,7 @@ import { setActiveSearchSpaceId } from './active-search-space'; const isDev = !app.isPackaged; const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string; const isMac = process.platform === 'darwin'; +const WINDOW_TITLE = 'SurfSense'; let mainWindow: BrowserWindow | null = null; let isQuitting = false; @@ -24,6 +25,7 @@ export function markQuitting(): void { export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { mainWindow = new BrowserWindow({ + title: WINDOW_TITLE, width: 1280, height: 800, minWidth: 800, @@ -48,6 +50,14 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { mainWindow?.show(); }); + mainWindow.webContents.on('page-title-updated', (event) => { + event.preventDefault(); + mainWindow?.setTitle(WINDOW_TITLE); + }); + mainWindow.webContents.on('did-finish-load', () => { + mainWindow?.setTitle(WINDOW_TITLE); + }); + mainWindow.loadURL(`http://localhost:${getServerPort()}${initialPath}`); mainWindow.webContents.setWindowOpenHandler(({ url }) => { From fe797e65d6def309817b0a3c8ef2714fd30fd2de Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 17:55:03 +0530 Subject: [PATCH 03/12] refactor(deep-links, quick-ask, window): replace localhost references with dynamic server origin retrieval --- surfsense_desktop/src/modules/deep-links.ts | 4 ++-- surfsense_desktop/src/modules/quick-ask.ts | 6 +++--- surfsense_desktop/src/modules/server.ts | 9 +++++++-- surfsense_desktop/src/modules/window.ts | 8 ++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/surfsense_desktop/src/modules/deep-links.ts b/surfsense_desktop/src/modules/deep-links.ts index 11b7bfcff..635e242fe 100644 --- a/surfsense_desktop/src/modules/deep-links.ts +++ b/surfsense_desktop/src/modules/deep-links.ts @@ -1,7 +1,7 @@ import { app } from 'electron'; import path from 'path'; import { getMainWindow } from './window'; -import { getServerPort } from './server'; +import { getServerOrigin } from './server'; import { trackEvent } from './analytics'; const PROTOCOL = 'surfsense'; @@ -23,7 +23,7 @@ function handleDeepLink(url: string) { }); if (parsed.hostname === 'auth' && parsed.pathname === '/callback') { const params = parsed.searchParams.toString(); - win.loadURL(`http://localhost:${getServerPort()}/auth/callback?${params}`); + win.loadURL(`${getServerOrigin()}/auth/callback?${params}`); } win.show(); diff --git a/surfsense_desktop/src/modules/quick-ask.ts b/surfsense_desktop/src/modules/quick-ask.ts index b31ae1bcd..d7602f470 100644 --- a/surfsense_desktop/src/modules/quick-ask.ts +++ b/surfsense_desktop/src/modules/quick-ask.ts @@ -2,7 +2,7 @@ import { BrowserWindow, clipboard, globalShortcut, ipcMain, screen, shell } from import path from 'path'; import { IPC_CHANNELS } from '../ipc/channels'; import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform'; -import { getServerPort } from './server'; +import { getServerOrigin } from './server'; import { getShortcuts } from './shortcuts'; import { getActiveSearchSpaceId } from './active-search-space'; import { trackEvent } from './analytics'; @@ -58,7 +58,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow { const spaceId = pendingSearchSpaceId; const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard'; - quickAskWindow.loadURL(`http://localhost:${getServerPort()}${route}?quickAssist=true`); + quickAskWindow.loadURL(`${getServerOrigin()}${route}?quickAssist=true`); quickAskWindow.once('ready-to-show', () => { quickAskWindow?.show(); @@ -69,7 +69,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow { }); quickAskWindow.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('http://localhost')) { + if (url.startsWith(getServerOrigin())) { return { action: 'allow' }; } shell.openExternal(url); diff --git a/surfsense_desktop/src/modules/server.ts b/surfsense_desktop/src/modules/server.ts index daec9bed0..55753d8fb 100644 --- a/surfsense_desktop/src/modules/server.ts +++ b/surfsense_desktop/src/modules/server.ts @@ -3,6 +3,7 @@ import { app, utilityProcess } from 'electron'; import { getPort } from 'get-port-please'; const isDev = !app.isPackaged; +const SERVER_HOST = '127.0.0.1'; let serverPort = 3000; let nextServerProcess: ReturnType | null = null; @@ -10,6 +11,10 @@ export function getServerPort(): number { return serverPort; } +export function getServerOrigin(): string { + return `http://${SERVER_HOST}:${serverPort}`; +} + function getStandalonePath(): string { if (isDev) { return path.join(__dirname, '..', '..', 'surfsense_web', '.next', 'standalone', 'surfsense_web'); @@ -44,7 +49,7 @@ export async function startNextServer(): Promise { env: { ...process.env, PORT: String(serverPort), - HOSTNAME: '127.0.0.1', + HOSTNAME: SERVER_HOST, NODE_ENV: 'production', }, serviceName: 'SurfSense Next Server', @@ -75,7 +80,7 @@ export async function startNextServer(): Promise { child.once('exit', startupExitHandler); }); - const ready = await Promise.race([waitForServer(`http://localhost:${serverPort}`), exited]); + const ready = await Promise.race([waitForServer(getServerOrigin()), exited]); if (startupExitHandler) { child.removeListener('exit', startupExitHandler); } diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index 7ee7eb8db..153fa0879 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -2,7 +2,7 @@ import { app, BrowserWindow, shell, session } from 'electron'; import path from 'path'; import { trackEvent } from './analytics'; import { showErrorDialog } from './errors'; -import { getServerPort } from './server'; +import { getServerOrigin } from './server'; import { setActiveSearchSpaceId } from './active-search-space'; const isDev = !app.isPackaged; @@ -58,10 +58,10 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { mainWindow?.setTitle(WINDOW_TITLE); }); - mainWindow.loadURL(`http://localhost:${getServerPort()}${initialPath}`); + mainWindow.loadURL(`${getServerOrigin()}${initialPath}`); mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('http://localhost')) { + if (url.startsWith(getServerOrigin())) { return { action: 'allow' }; } shell.openExternal(url); @@ -70,7 +70,7 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { const filter = { urls: [`${HOSTED_FRONTEND_URL}/*`] }; session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => { - const rewritten = details.url.replace(HOSTED_FRONTEND_URL, `http://localhost:${getServerPort()}`); + const rewritten = details.url.replace(HOSTED_FRONTEND_URL, getServerOrigin()); callback({ redirectURL: rewritten }); }); From 26fe4d74934f7026b0eaa5fba291dc039a914ebd Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 21:44:31 +0530 Subject: [PATCH 04/12] feat(auto-updater, menu): enhance update management and add menu options for updates and policies --- surfsense_desktop/.env.example | 2 +- surfsense_desktop/src/modules/auto-updater.ts | 119 ++++++++++++++++-- surfsense_desktop/src/modules/menu.ts | 36 +++++- surfsense_desktop/src/modules/window.ts | 2 +- 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/surfsense_desktop/.env.example b/surfsense_desktop/.env.example index 2d9de7561..f4e797250 100644 --- a/surfsense_desktop/.env.example +++ b/surfsense_desktop/.env.example @@ -3,7 +3,7 @@ # The hosted web frontend URL. Used to intercept OAuth redirects and keep them # inside the desktop app. Set to your production frontend domain. -HOSTED_FRONTEND_URL=https://surfsense.net +HOSTED_FRONTEND_URL=https://surfsense.com # Runtime override for the above (read at app start, no rebuild required). # Useful for self-hosters whose backend NEXT_FRONTEND_URL differs from the diff --git a/surfsense_desktop/src/modules/auto-updater.ts b/surfsense_desktop/src/modules/auto-updater.ts index e323abe53..c0fea6634 100644 --- a/surfsense_desktop/src/modules/auto-updater.ts +++ b/surfsense_desktop/src/modules/auto-updater.ts @@ -3,20 +3,35 @@ import { trackEvent } from './analytics'; const SEMVER_RE = /^\d+\.\d+\.\d+/; -export function setupAutoUpdater(): void { - if (!app.isPackaged) return; +type AutoUpdater = { + autoDownload: boolean; + on(event: string, listener: (...args: any[]) => void): void; + once(event: string, listener: (...args: any[]) => void): void; + removeListener(event: string, listener: (...args: any[]) => void): void; + checkForUpdates(): Promise; + quitAndInstall(): void; +}; - const version = app.getVersion(); - if (!SEMVER_RE.test(version)) { - console.log(`Auto-updater: skipping — "${version}" is not valid semver`); - return; - } +type UpdateInfo = { + version: string; +}; +let listenersRegistered = false; + +function getAutoUpdater(): AutoUpdater { const { autoUpdater } = require('electron-updater'); + return autoUpdater as AutoUpdater; +} +function configureAutoUpdater(autoUpdater: AutoUpdater): void { autoUpdater.autoDownload = true; - autoUpdater.on('update-available', (info: { version: string }) => { + if (listenersRegistered) return; + listenersRegistered = true; + + const version = app.getVersion(); + + autoUpdater.on('update-available', (info: UpdateInfo) => { console.log(`Update available: ${info.version}`); trackEvent('desktop_update_available', { current_version: version, @@ -24,7 +39,7 @@ export function setupAutoUpdater(): void { }); }); - autoUpdater.on('update-downloaded', (info: { version: string }) => { + autoUpdater.on('update-downloaded', (info: UpdateInfo) => { console.log(`Update downloaded: ${info.version}`); trackEvent('desktop_update_downloaded', { current_version: version, @@ -52,6 +67,92 @@ export function setupAutoUpdater(): void { message: err.message?.split('\n')[0], }); }); +} + +export function setupAutoUpdater(): void { + if (!app.isPackaged) return; + + const version = app.getVersion(); + if (!SEMVER_RE.test(version)) { + console.log(`Auto-updater: skipping - "${version}" is not valid semver`); + return; + } + + const autoUpdater = getAutoUpdater(); + configureAutoUpdater(autoUpdater); autoUpdater.checkForUpdates().catch(() => {}); } + +export async function checkForUpdatesManually(): Promise { + if (!app.isPackaged) { + await dialog.showMessageBox({ + type: 'info', + title: 'Updates Unavailable', + message: 'Updates are only available in packaged builds.', + }); + return; + } + + const version = app.getVersion(); + if (!SEMVER_RE.test(version)) { + await dialog.showMessageBox({ + type: 'info', + title: 'Updates Unavailable', + message: `Version "${version}" is not a valid release version, so updates cannot be checked.`, + }); + return; + } + + const autoUpdater = getAutoUpdater(); + configureAutoUpdater(autoUpdater); + + try { + const result = await new Promise<'available' | 'not-available'>((resolve, reject) => { + const cleanup = () => { + autoUpdater.removeListener('update-available', onAvailable); + autoUpdater.removeListener('update-not-available', onNotAvailable); + autoUpdater.removeListener('error', onError); + }; + const onAvailable = (info: UpdateInfo) => { + cleanup(); + void dialog.showMessageBox({ + type: 'info', + title: 'Update Available', + message: `Version ${info.version} is available and will download in the background.`, + }); + resolve('available'); + }; + const onNotAvailable = () => { + cleanup(); + resolve('not-available'); + }; + const onError = (err: Error) => { + cleanup(); + reject(err); + }; + + autoUpdater.once('update-available', onAvailable); + autoUpdater.once('update-not-available', onNotAvailable); + autoUpdater.once('error', onError); + autoUpdater.checkForUpdates().catch((err: Error) => { + cleanup(); + reject(err); + }); + }); + + if (result === 'not-available') { + await dialog.showMessageBox({ + type: 'info', + title: 'No Updates Available', + message: "You're up to date.", + }); + } + } catch (err) { + await dialog.showMessageBox({ + type: 'error', + title: 'Update Check Failed', + message: err instanceof Error ? err.message : String(err), + }); + } +} diff --git a/surfsense_desktop/src/modules/menu.ts b/surfsense_desktop/src/modules/menu.ts index 128a73a21..067a6e461 100644 --- a/surfsense_desktop/src/modules/menu.ts +++ b/surfsense_desktop/src/modules/menu.ts @@ -1,13 +1,45 @@ -import { Menu } from 'electron'; +import { app, Menu } from 'electron'; +import { checkForUpdatesManually } from './auto-updater'; + +const checkForUpdatesItem: Electron.MenuItemConstructorOptions = { + label: 'Check for Updates...', + click: () => { + void checkForUpdatesManually(); + }, +}; export function setupMenu(): void { const isMac = process.platform === 'darwin'; const template: Electron.MenuItemConstructorOptions[] = [ - ...(isMac ? [{ role: 'appMenu' as const }] : []), + ...(isMac + ? [{ + label: app.name, + submenu: [ + { role: 'about' as const }, + checkForUpdatesItem, + { type: 'separator' as const }, + { role: 'services' as const }, + { type: 'separator' as const }, + { role: 'hide' as const }, + { role: 'hideOthers' as const }, + { role: 'unhide' as const }, + { type: 'separator' as const }, + { role: 'quit' as const }, + ], + }] + : []), { role: 'fileMenu' as const }, { role: 'editMenu' as const }, { role: 'viewMenu' as const }, { role: 'windowMenu' as const }, + ...(!isMac + ? [{ + role: 'help' as const, + submenu: [ + checkForUpdatesItem, + ], + }] + : []), ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index e80bf7879..bcae1799c 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -13,7 +13,7 @@ function getHostedFrontendUrl(): string { return ( process.env.SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE || process.env.HOSTED_FRONTEND_URL || - 'https://surfsense.net' + 'https://surfsense.com' ); } From 79378db7c8195fcddd3860776b3532c455577a69 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 22:21:34 +0530 Subject: [PATCH 05/12] refactor(connector): streamline connector telemetry imports and clean up unused constants --- .../views/connector-edit-view.tsx | 5 +++-- .../constants/connector-constants.ts | 21 ------------------- .../views/connector-accounts-list-view.tsx | 5 +++-- surfsense_web/lib/connector-telemetry.ts | 2 +- 4 files changed, 7 insertions(+), 26 deletions(-) diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx index 90b28dd1a..3e9e9bb27 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-edit-view.tsx @@ -12,16 +12,17 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { getReauthEndpoint } from "@/lib/connector-telemetry"; +import { BACKEND_URL } from "@/lib/env-config"; import { cn } from "@/lib/utils"; import { DateRangeSelector } from "../../components/date-range-selector"; import { PeriodicSyncConfig } from "../../components/periodic-sync-config"; import { SummaryConfig } from "../../components/summary-config"; import { VisionLLMConfig } from "../../components/vision-llm-config"; -import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants"; +import { LIVE_CONNECTOR_TYPES } from "../../constants/connector-constants"; import { getConnectorDisplayName } from "../../tabs/all-connectors-tab"; import { MCPServiceConfig } from "../components/mcp-service-config"; import { getConnectorConfigComponent } from "../index"; -import { BACKEND_URL } from "@/lib/env-config"; const VISION_LLM_CONNECTOR_TYPES = new Set([ EnumConnectorName.GOOGLE_DRIVE_CONNECTOR, EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR, diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index 01a911d70..e62b9546a 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -1,5 +1,4 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; -import type { SearchSourceConnector } from "@/contracts/types/connector.types"; /** * Connectors that operate in real time (no background indexing). @@ -294,25 +293,5 @@ export const AUTO_INDEX_DEFAULTS: Record = { export const AUTO_INDEX_CONNECTOR_TYPES = new Set(Object.keys(AUTO_INDEX_DEFAULTS)); -// ============================================================================ -// CONNECTOR TELEMETRY REGISTRY -// ---------------------------------------------------------------------------- -// Single source of truth for "what does this connector_type look like in -// analytics?". Any connector added to the lists above is automatically -// picked up here, so adding a new integration does NOT require touching -// `lib/posthog/events.ts` or per-connector tracking code. -// ============================================================================ - -// Telemetry types & helpers are now defined in `@/lib/connector-telemetry`. -// Re-exported here for backward compatibility with existing imports. -export type { - ConnectorTelemetryGroup, - ConnectorTelemetryMeta, -} from "@/lib/connector-telemetry"; -export { - getConnectorTelemetryMeta, - getReauthEndpoint, -} from "@/lib/connector-telemetry"; - // Re-export IndexingConfigState from schemas for backward compatibility export type { IndexingConfigState } from "./connector-popup.schemas"; diff --git a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx index 41dae221e..27e102d7e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/views/connector-accounts-list-view.tsx @@ -11,12 +11,13 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { getReauthEndpoint } from "@/lib/connector-telemetry"; +import { BACKEND_URL } from "@/lib/env-config"; import { formatRelativeDate } from "@/lib/format-date"; import { cn } from "@/lib/utils"; -import { getReauthEndpoint, LIVE_CONNECTOR_TYPES } from "../constants/connector-constants"; +import { LIVE_CONNECTOR_TYPES } from "../constants/connector-constants"; import { useConnectorStatus } from "../hooks/use-connector-status"; import { getConnectorDisplayName } from "../tabs/all-connectors-tab"; -import { BACKEND_URL } from "@/lib/env-config"; interface ConnectorAccountsListViewProps { connectorType: string; connectorTitle: string; diff --git a/surfsense_web/lib/connector-telemetry.ts b/surfsense_web/lib/connector-telemetry.ts index ef1b3de32..396097445 100644 --- a/surfsense_web/lib/connector-telemetry.ts +++ b/surfsense_web/lib/connector-telemetry.ts @@ -1,9 +1,9 @@ import { EnumConnectorName } from "@/contracts/enums/connector"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { - OAUTH_CONNECTORS, COMPOSIO_CONNECTORS, CRAWLERS, + OAUTH_CONNECTORS, OTHER_CONNECTORS, } from "@/components/assistant-ui/connector-popup/constants/connector-constants"; From 74fff647792e04749415e9b2f2fa281a0b7df384 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 22:26:14 +0530 Subject: [PATCH 06/12] feat(menu): add Privacy Policy and Terms of Service options to the application menu --- surfsense_desktop/src/modules/menu.ts | 37 ++++++++++++++----- .../components/AgentPermissionsContent.tsx | 4 +- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/surfsense_desktop/src/modules/menu.ts b/surfsense_desktop/src/modules/menu.ts index 067a6e461..40e19398c 100644 --- a/surfsense_desktop/src/modules/menu.ts +++ b/surfsense_desktop/src/modules/menu.ts @@ -1,4 +1,4 @@ -import { app, Menu } from 'electron'; +import { app, Menu, shell } from 'electron'; import { checkForUpdatesManually } from './auto-updater'; const checkForUpdatesItem: Electron.MenuItemConstructorOptions = { @@ -8,6 +8,20 @@ const checkForUpdatesItem: Electron.MenuItemConstructorOptions = { }, }; +const privacyPolicyItem: Electron.MenuItemConstructorOptions = { + label: 'Privacy Policy', + click: () => { + void shell.openExternal('https://www.surfsense.com/privacy'); + }, +}; + +const termsOfServiceItem: Electron.MenuItemConstructorOptions = { + label: 'Terms of Service', + click: () => { + void shell.openExternal('https://www.surfsense.com/terms'); + }, +}; + export function setupMenu(): void { const isMac = process.platform === 'darwin'; const template: Electron.MenuItemConstructorOptions[] = [ @@ -32,14 +46,19 @@ export function setupMenu(): void { { role: 'editMenu' as const }, { role: 'viewMenu' as const }, { role: 'windowMenu' as const }, - ...(!isMac - ? [{ - role: 'help' as const, - submenu: [ - checkForUpdatesItem, - ], - }] - : []), + { + role: 'help' as const, + submenu: [ + ...(!isMac + ? [ + checkForUpdatesItem, + { type: 'separator' as const }, + ] + : []), + privacyPolicyItem, + termsOfServiceItem, + ], + }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentPermissionsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentPermissionsContent.tsx index 5af94f7e3..5919abcd6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentPermissionsContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/AgentPermissionsContent.tsx @@ -474,8 +474,10 @@ export function AgentPermissionsContent() { handleConfirmDelete(); }} disabled={deleteMutation.isPending} + className="relative min-w-[88px]" > - {deleteMutation.isPending ? "Deleting…" : "Delete"} + Delete + {deleteMutation.isPending && } From c0fefa4db1a67387bc3411852a8b590f4471e41e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 25 May 2026 23:24:26 +0530 Subject: [PATCH 07/12] feat(auto-updater, ui): implement update notification and installation prompt in desktop application --- surfsense_desktop/src/ipc/channels.ts | 2 + surfsense_desktop/src/ipc/handlers.ts | 5 ++ surfsense_desktop/src/modules/auto-updater.ts | 68 +++++++++++---- surfsense_desktop/src/preload.ts | 8 ++ surfsense_web/app/layout.tsx | 2 + .../desktop/DesktopUpdatePrompt.tsx | 82 +++++++++++++++++++ surfsense_web/types/window.d.ts | 6 ++ 7 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 surfsense_web/components/desktop/DesktopUpdatePrompt.tsx diff --git a/surfsense_desktop/src/ipc/channels.ts b/surfsense_desktop/src/ipc/channels.ts index 8d2af5107..17daab9a6 100644 --- a/surfsense_desktop/src/ipc/channels.ts +++ b/surfsense_desktop/src/ipc/channels.ts @@ -2,6 +2,8 @@ export const IPC_CHANNELS = { OPEN_EXTERNAL: 'open-external', GET_APP_VERSION: 'get-app-version', DEEP_LINK: 'deep-link', + UPDATE_DOWNLOADED: 'update:downloaded', + UPDATE_INSTALL_NOW: 'update:install-now', QUICK_ASK_TEXT: 'quick-ask-text', SET_QUICK_ASK_MODE: 'set-quick-ask-mode', GET_QUICK_ASK_MODE: 'get-quick-ask-mode', diff --git a/surfsense_desktop/src/ipc/handlers.ts b/surfsense_desktop/src/ipc/handlers.ts index d918fd90d..ed7eaac66 100644 --- a/surfsense_desktop/src/ipc/handlers.ts +++ b/surfsense_desktop/src/ipc/handlers.ts @@ -51,6 +51,7 @@ import { stopAgentFilesystemTreeWatch, type AgentFilesystemTreeWatchOptions, } from '../modules/agent-filesystem-tree-watcher'; +import { installDownloadedUpdate } from '../modules/auto-updater'; let authTokens: { bearer: string; refresh: string } | null = null; @@ -70,6 +71,10 @@ export function registerIpcHandlers(): void { return app.getVersion(); }); + ipcMain.handle(IPC_CHANNELS.UPDATE_INSTALL_NOW, () => { + installDownloadedUpdate(); + }); + ipcMain.handle(IPC_CHANNELS.GET_PERMISSIONS_STATUS, () => { return getPermissionsStatus(); }); diff --git a/surfsense_desktop/src/modules/auto-updater.ts b/surfsense_desktop/src/modules/auto-updater.ts index c0fea6634..4745fa24b 100644 --- a/surfsense_desktop/src/modules/auto-updater.ts +++ b/surfsense_desktop/src/modules/auto-updater.ts @@ -1,4 +1,5 @@ -import { app, dialog } from 'electron'; +import { app, BrowserWindow, dialog } from 'electron'; +import { IPC_CHANNELS } from '../ipc/channels'; import { trackEvent } from './analytics'; const SEMVER_RE = /^\d+\.\d+\.\d+/; @@ -17,6 +18,7 @@ type UpdateInfo = { }; let listenersRegistered = false; +let manualUpdateCheckInProgress = false; function getAutoUpdater(): AutoUpdater { const { autoUpdater } = require('electron-updater'); @@ -45,20 +47,9 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void { current_version: version, new_version: info.version, }); - dialog.showMessageBox({ - type: 'info', - buttons: ['Restart', 'Later'], - defaultId: 0, - title: 'Update Ready', - message: `Version ${info.version} has been downloaded. Restart to apply the update.`, - }).then(({ response }: { response: number }) => { - if (response === 0) { - trackEvent('desktop_update_install_accepted', { new_version: info.version }); - autoUpdater.quitAndInstall(); - } else { - trackEvent('desktop_update_install_deferred', { new_version: info.version }); - } - }); + if (!manualUpdateCheckInProgress) { + notifyRenderersUpdateDownloaded(info); + } }); autoUpdater.on('error', (err: Error) => { @@ -69,6 +60,39 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void { }); } +function notifyRenderersUpdateDownloaded(info: UpdateInfo): void { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(IPC_CHANNELS.UPDATE_DOWNLOADED, { + version: info.version, + }); + } + } +} + +async function showNativeInstallDialog(autoUpdater: AutoUpdater, info: UpdateInfo): Promise { + const { response } = await dialog.showMessageBox({ + type: 'info', + buttons: ['Restart', 'Later'], + defaultId: 0, + title: 'Update Ready', + message: `Version ${info.version} has been downloaded. Restart to apply the update.`, + }); + + if (response === 0) { + trackEvent('desktop_update_install_accepted', { new_version: info.version }); + autoUpdater.quitAndInstall(); + } else { + trackEvent('desktop_update_install_deferred', { new_version: info.version }); + } +} + +export function installDownloadedUpdate(): void { + const autoUpdater = getAutoUpdater(); + trackEvent('desktop_update_install_accepted', { source: 'renderer_prompt' }); + autoUpdater.quitAndInstall(); +} + export function setupAutoUpdater(): void { if (!app.isPackaged) return; @@ -108,25 +132,31 @@ export async function checkForUpdatesManually(): Promise { configureAutoUpdater(autoUpdater); try { - const result = await new Promise<'available' | 'not-available'>((resolve, reject) => { + manualUpdateCheckInProgress = true; + const result = await new Promise<'not-available' | 'downloaded'>((resolve, reject) => { const cleanup = () => { + manualUpdateCheckInProgress = false; autoUpdater.removeListener('update-available', onAvailable); autoUpdater.removeListener('update-not-available', onNotAvailable); + autoUpdater.removeListener('update-downloaded', onDownloaded); autoUpdater.removeListener('error', onError); }; const onAvailable = (info: UpdateInfo) => { - cleanup(); void dialog.showMessageBox({ type: 'info', title: 'Update Available', message: `Version ${info.version} is available and will download in the background.`, }); - resolve('available'); }; const onNotAvailable = () => { cleanup(); resolve('not-available'); }; + const onDownloaded = (info: UpdateInfo) => { + cleanup(); + void showNativeInstallDialog(autoUpdater, info); + resolve('downloaded'); + }; const onError = (err: Error) => { cleanup(); reject(err); @@ -134,6 +164,7 @@ export async function checkForUpdatesManually(): Promise { autoUpdater.once('update-available', onAvailable); autoUpdater.once('update-not-available', onNotAvailable); + autoUpdater.once('update-downloaded', onDownloaded); autoUpdater.once('error', onError); autoUpdater.checkForUpdates().catch((err: Error) => { cleanup(); @@ -149,6 +180,7 @@ export async function checkForUpdatesManually(): Promise { }); } } catch (err) { + manualUpdateCheckInProgress = false; await dialog.showMessageBox({ type: 'error', title: 'Update Check Failed', diff --git a/surfsense_desktop/src/preload.ts b/surfsense_desktop/src/preload.ts index 7d72e9da5..97232179c 100644 --- a/surfsense_desktop/src/preload.ts +++ b/surfsense_desktop/src/preload.ts @@ -10,6 +10,14 @@ contextBridge.exposeInMainWorld('electronAPI', { }, openExternal: (url: string) => ipcRenderer.send(IPC_CHANNELS.OPEN_EXTERNAL, url), getAppVersion: () => ipcRenderer.invoke(IPC_CHANNELS.GET_APP_VERSION), + onUpdateDownloaded: (callback: (data: { version: string }) => void) => { + const listener = (_event: unknown, data: { version: string }) => callback(data); + ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, listener); + return () => { + ipcRenderer.removeListener(IPC_CHANNELS.UPDATE_DOWNLOADED, listener); + }; + }, + installUpdateNow: () => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_INSTALL_NOW), onDeepLink: (callback: (url: string) => void) => { const listener = (_event: unknown, url: string) => callback(url); ipcRenderer.on(IPC_CHANNELS.DEEP_LINK, listener); diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 4e88709e9..4bb15c607 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -3,6 +3,7 @@ import "./globals.css"; import { RootProvider } from "fumadocs-ui/provider/next"; import { Roboto } from "next/font/google"; import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider"; +import { DesktopUpdatePrompt } from "@/components/desktop/DesktopUpdatePrompt"; import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider"; import { I18nProvider } from "@/components/providers/I18nProvider"; import { PostHogProvider } from "@/components/providers/PostHogProvider"; @@ -154,6 +155,7 @@ export default function RootLayout({ {children} + diff --git a/surfsense_web/components/desktop/DesktopUpdatePrompt.tsx b/surfsense_web/components/desktop/DesktopUpdatePrompt.tsx new file mode 100644 index 000000000..091121141 --- /dev/null +++ b/surfsense_web/components/desktop/DesktopUpdatePrompt.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { Download, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type UpdatePromptState = { + version: string; +}; + +export function DesktopUpdatePrompt() { + const [update, setUpdate] = useState(null); + + useEffect(() => { + const api = window.electronAPI; + if (!api?.onUpdateDownloaded) return; + + return api.onUpdateDownloaded(({ version }) => { + setUpdate({ version }); + }); + }, []); + + if (!update) return null; + + const installAndRestart = () => { + void window.electronAPI?.installUpdateNow(); + }; + + return ( +
+
+
+ +
+ +
+
Update available
+

+ A new version of SurfSense ({update.version}) is now available to install. +

+ +
+ + +
+
+ + +
+
+ ); +} diff --git a/surfsense_web/types/window.d.ts b/surfsense_web/types/window.d.ts index f25d43f5e..2d12169b1 100644 --- a/surfsense_web/types/window.d.ts +++ b/surfsense_web/types/window.d.ts @@ -83,6 +83,10 @@ interface LocalTextFileResult { error?: string; } +interface UpdateDownloadedEvent { + version: string; +} + interface ElectronAPI { versions: { electron: string; @@ -92,6 +96,8 @@ interface ElectronAPI { }; openExternal: (url: string) => void; getAppVersion: () => Promise; + onUpdateDownloaded: (callback: (data: UpdateDownloadedEvent) => void) => () => void; + installUpdateNow: () => Promise; onDeepLink: (callback: (url: string) => void) => () => void; onChatScreenCapture: (callback: (dataUrl: string) => void) => () => void; getQuickAskText: () => Promise; From 6cf0f0736674ad5a0c69cc7029ec2056d32dbae0 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 26 May 2026 02:38:23 +0530 Subject: [PATCH 08/12] refactor(auto-updater): remove native install dialog and streamline update notification handling --- surfsense_desktop/src/modules/auto-updater.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/surfsense_desktop/src/modules/auto-updater.ts b/surfsense_desktop/src/modules/auto-updater.ts index 4745fa24b..d140a1a2a 100644 --- a/surfsense_desktop/src/modules/auto-updater.ts +++ b/surfsense_desktop/src/modules/auto-updater.ts @@ -70,23 +70,6 @@ function notifyRenderersUpdateDownloaded(info: UpdateInfo): void { } } -async function showNativeInstallDialog(autoUpdater: AutoUpdater, info: UpdateInfo): Promise { - const { response } = await dialog.showMessageBox({ - type: 'info', - buttons: ['Restart', 'Later'], - defaultId: 0, - title: 'Update Ready', - message: `Version ${info.version} has been downloaded. Restart to apply the update.`, - }); - - if (response === 0) { - trackEvent('desktop_update_install_accepted', { new_version: info.version }); - autoUpdater.quitAndInstall(); - } else { - trackEvent('desktop_update_install_deferred', { new_version: info.version }); - } -} - export function installDownloadedUpdate(): void { const autoUpdater = getAutoUpdater(); trackEvent('desktop_update_install_accepted', { source: 'renderer_prompt' }); @@ -154,7 +137,7 @@ export async function checkForUpdatesManually(): Promise { }; const onDownloaded = (info: UpdateInfo) => { cleanup(); - void showNativeInstallDialog(autoUpdater, info); + notifyRenderersUpdateDownloaded(info); resolve('downloaded'); }; const onError = (err: Error) => { From 98697f1dd6e9a4f534c860ede20e4b5f118a740c Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 26 May 2026 03:11:06 +0530 Subject: [PATCH 09/12] feat(auto-updater, menu): implement update state management and enhance menu options for updates --- surfsense_desktop/src/modules/auto-updater.ts | 60 ++++++++++++++----- surfsense_desktop/src/modules/menu.ts | 53 +++++++++++++--- 2 files changed, 88 insertions(+), 25 deletions(-) diff --git a/surfsense_desktop/src/modules/auto-updater.ts b/surfsense_desktop/src/modules/auto-updater.ts index d140a1a2a..b318b737d 100644 --- a/surfsense_desktop/src/modules/auto-updater.ts +++ b/surfsense_desktop/src/modules/auto-updater.ts @@ -17,8 +17,32 @@ type UpdateInfo = { version: string; }; +type UpdateMenuState = + | { status: 'idle' } + | { status: 'downloading'; version: string } + | { status: 'ready'; version: string }; + let listenersRegistered = false; -let manualUpdateCheckInProgress = false; +let updateMenuState: UpdateMenuState = { status: 'idle' }; +const updateMenuStateListeners = new Set<(state: UpdateMenuState) => void>(); + +export function getUpdateMenuState(): UpdateMenuState { + return updateMenuState; +} + +export function onUpdateMenuStateChange(listener: (state: UpdateMenuState) => void): () => void { + updateMenuStateListeners.add(listener); + return () => { + updateMenuStateListeners.delete(listener); + }; +} + +function setUpdateMenuState(state: UpdateMenuState): void { + updateMenuState = state; + for (const listener of updateMenuStateListeners) { + listener(state); + } +} function getAutoUpdater(): AutoUpdater { const { autoUpdater } = require('electron-updater'); @@ -35,6 +59,7 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void { autoUpdater.on('update-available', (info: UpdateInfo) => { console.log(`Update available: ${info.version}`); + setUpdateMenuState({ status: 'downloading', version: info.version }); trackEvent('desktop_update_available', { current_version: version, new_version: info.version, @@ -43,16 +68,20 @@ function configureAutoUpdater(autoUpdater: AutoUpdater): void { autoUpdater.on('update-downloaded', (info: UpdateInfo) => { console.log(`Update downloaded: ${info.version}`); + setUpdateMenuState({ status: 'ready', version: info.version }); trackEvent('desktop_update_downloaded', { current_version: version, new_version: info.version, }); - if (!manualUpdateCheckInProgress) { - notifyRenderersUpdateDownloaded(info); - } + notifyRenderersUpdateDownloaded(info); + }); + + autoUpdater.on('update-not-available', () => { + setUpdateMenuState({ status: 'idle' }); }); autoUpdater.on('error', (err: Error) => { + setUpdateMenuState({ status: 'idle' }); console.log('Auto-updater: update check skipped —', err.message?.split('\n')[0]); trackEvent('desktop_update_error', { message: err.message?.split('\n')[0], @@ -92,6 +121,13 @@ export function setupAutoUpdater(): void { } export async function checkForUpdatesManually(): Promise { + const currentState = getUpdateMenuState(); + if (currentState.status === 'ready') { + installDownloadedUpdate(); + return; + } + if (currentState.status === 'downloading') return; + if (!app.isPackaged) { await dialog.showMessageBox({ type: 'info', @@ -115,29 +151,20 @@ export async function checkForUpdatesManually(): Promise { configureAutoUpdater(autoUpdater); try { - manualUpdateCheckInProgress = true; const result = await new Promise<'not-available' | 'downloaded'>((resolve, reject) => { const cleanup = () => { - manualUpdateCheckInProgress = false; autoUpdater.removeListener('update-available', onAvailable); autoUpdater.removeListener('update-not-available', onNotAvailable); autoUpdater.removeListener('update-downloaded', onDownloaded); autoUpdater.removeListener('error', onError); }; - const onAvailable = (info: UpdateInfo) => { - void dialog.showMessageBox({ - type: 'info', - title: 'Update Available', - message: `Version ${info.version} is available and will download in the background.`, - }); - }; + const onAvailable = () => {}; const onNotAvailable = () => { cleanup(); resolve('not-available'); }; - const onDownloaded = (info: UpdateInfo) => { + const onDownloaded = () => { cleanup(); - notifyRenderersUpdateDownloaded(info); resolve('downloaded'); }; const onError = (err: Error) => { @@ -151,6 +178,7 @@ export async function checkForUpdatesManually(): Promise { autoUpdater.once('error', onError); autoUpdater.checkForUpdates().catch((err: Error) => { cleanup(); + setUpdateMenuState({ status: 'idle' }); reject(err); }); }); @@ -163,7 +191,7 @@ export async function checkForUpdatesManually(): Promise { }); } } catch (err) { - manualUpdateCheckInProgress = false; + setUpdateMenuState({ status: 'idle' }); await dialog.showMessageBox({ type: 'error', title: 'Update Check Failed', diff --git a/surfsense_desktop/src/modules/menu.ts b/surfsense_desktop/src/modules/menu.ts index 40e19398c..2753aaacd 100644 --- a/surfsense_desktop/src/modules/menu.ts +++ b/surfsense_desktop/src/modules/menu.ts @@ -1,12 +1,39 @@ import { app, Menu, shell } from 'electron'; -import { checkForUpdatesManually } from './auto-updater'; +import { + checkForUpdatesManually, + getUpdateMenuState, + installDownloadedUpdate, + onUpdateMenuStateChange, +} from './auto-updater'; -const checkForUpdatesItem: Electron.MenuItemConstructorOptions = { - label: 'Check for Updates...', - click: () => { - void checkForUpdatesManually(); - }, -}; +let updateMenuListenerRegistered = false; + +function getUpdateMenuItem(): Electron.MenuItemConstructorOptions { + const state = getUpdateMenuState(); + + if (state.status === 'downloading') { + return { + label: 'Downloading...', + enabled: false, + }; + } + + if (state.status === 'ready') { + return { + label: 'Install and Restart', + click: () => { + installDownloadedUpdate(); + }, + }; + } + + return { + label: 'Check for Updates...', + click: () => { + void checkForUpdatesManually(); + }, + }; +} const privacyPolicyItem: Electron.MenuItemConstructorOptions = { label: 'Privacy Policy', @@ -23,14 +50,22 @@ const termsOfServiceItem: Electron.MenuItemConstructorOptions = { }; export function setupMenu(): void { + if (!updateMenuListenerRegistered) { + updateMenuListenerRegistered = true; + onUpdateMenuStateChange(() => { + setupMenu(); + }); + } + const isMac = process.platform === 'darwin'; + const updateMenuItem = getUpdateMenuItem(); const template: Electron.MenuItemConstructorOptions[] = [ ...(isMac ? [{ label: app.name, submenu: [ { role: 'about' as const }, - checkForUpdatesItem, + updateMenuItem, { type: 'separator' as const }, { role: 'services' as const }, { type: 'separator' as const }, @@ -51,7 +86,7 @@ export function setupMenu(): void { submenu: [ ...(!isMac ? [ - checkForUpdatesItem, + updateMenuItem, { type: 'separator' as const }, ] : []), From bf4e60d22467d66477acae9257c4bfc8bca83f65 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 26 May 2026 03:55:16 +0530 Subject: [PATCH 10/12] feat(menu, dev-tools): enhance menu with view options and enable dev tools in development mode --- surfsense_desktop/src/modules/menu.ts | 21 ++++++++++++++++++- surfsense_desktop/src/modules/quick-ask.ts | 3 ++- .../screen-capture/screen-region-picker.ts | 3 ++- .../modules/screen-capture/window-picker.ts | 3 ++- surfsense_desktop/src/modules/window.ts | 1 + 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/surfsense_desktop/src/modules/menu.ts b/surfsense_desktop/src/modules/menu.ts index 2753aaacd..629d88a04 100644 --- a/surfsense_desktop/src/modules/menu.ts +++ b/surfsense_desktop/src/modules/menu.ts @@ -58,7 +58,23 @@ export function setupMenu(): void { } const isMac = process.platform === 'darwin'; + const isDev = !app.isPackaged; const updateMenuItem = getUpdateMenuItem(); + const viewSubmenu: Electron.MenuItemConstructorOptions[] = [ + { role: 'reload' as const }, + { role: 'forceReload' as const }, + ...(isDev + ? [ + { role: 'toggleDevTools' as const }, + ] + : []), + { type: 'separator' as const }, + { role: 'resetZoom' as const }, + { role: 'zoomIn' as const }, + { role: 'zoomOut' as const }, + { type: 'separator' as const }, + { role: 'togglefullscreen' as const }, + ]; const template: Electron.MenuItemConstructorOptions[] = [ ...(isMac ? [{ @@ -79,7 +95,10 @@ export function setupMenu(): void { : []), { role: 'fileMenu' as const }, { role: 'editMenu' as const }, - { role: 'viewMenu' as const }, + { + label: 'View', + submenu: viewSubmenu, + }, { role: 'windowMenu' as const }, { role: 'help' as const, diff --git a/surfsense_desktop/src/modules/quick-ask.ts b/surfsense_desktop/src/modules/quick-ask.ts index d7602f470..0807e2e08 100644 --- a/surfsense_desktop/src/modules/quick-ask.ts +++ b/surfsense_desktop/src/modules/quick-ask.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, clipboard, globalShortcut, ipcMain, screen, shell } from 'electron'; +import { app, BrowserWindow, clipboard, globalShortcut, ipcMain, screen, shell } from 'electron'; import path from 'path'; import { IPC_CHANNELS } from '../ipc/channels'; import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform'; @@ -51,6 +51,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow { contextIsolation: true, nodeIntegration: false, sandbox: true, + devTools: !app.isPackaged, }, show: false, skipTaskbar: true, diff --git a/surfsense_desktop/src/modules/screen-capture/screen-region-picker.ts b/surfsense_desktop/src/modules/screen-capture/screen-region-picker.ts index fd771b0f7..0cfc92297 100644 --- a/surfsense_desktop/src/modules/screen-capture/screen-region-picker.ts +++ b/surfsense_desktop/src/modules/screen-capture/screen-region-picker.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, desktopCapturer, nativeImage, screen } from 'electron'; +import { app, BrowserWindow, desktopCapturer, nativeImage, screen } from 'electron'; import path from 'path'; import { IPC_CHANNELS } from '../../ipc/channels'; function fitNativeImageToWorkArea(img: Electron.NativeImage, display: Electron.Display): Electron.NativeImage { @@ -261,6 +261,7 @@ export function pickScreenRegion(opts?: { windowDataUrl?: string }): Promise { contextIsolation: true, nodeIntegration: false, sandbox: true, + devTools: !app.isPackaged, }, }); diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index bcae1799c..42011d089 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -53,6 +53,7 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { nodeIntegration: false, sandbox: true, webviewTag: false, + devTools: !app.isPackaged, }, show: false, ...(isMac From 7276210403653f62aa52e5c9c006b6fb3dedf48e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 26 May 2026 11:57:50 +0530 Subject: [PATCH 11/12] feat(auto-updater, ui): rename DesktopUpdatePrompt with DesktopUpdateToast --- surfsense_web/app/layout.tsx | 4 ++-- .../{DesktopUpdatePrompt.tsx => desktop-update-toast.tsx} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename surfsense_web/components/desktop/{DesktopUpdatePrompt.tsx => desktop-update-toast.tsx} (92%) diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 4bb15c607..eef03d463 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -3,7 +3,7 @@ import "./globals.css"; import { RootProvider } from "fumadocs-ui/provider/next"; import { Roboto } from "next/font/google"; import { AnnouncementToastProvider } from "@/components/announcements/AnnouncementToastProvider"; -import { DesktopUpdatePrompt } from "@/components/desktop/DesktopUpdatePrompt"; +import { DesktopUpdateToast } from "@/components/desktop/desktop-update-toast"; import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider"; import { I18nProvider } from "@/components/providers/I18nProvider"; import { PostHogProvider } from "@/components/providers/PostHogProvider"; @@ -155,7 +155,7 @@ export default function RootLayout({ {children} - + diff --git a/surfsense_web/components/desktop/DesktopUpdatePrompt.tsx b/surfsense_web/components/desktop/desktop-update-toast.tsx similarity index 92% rename from surfsense_web/components/desktop/DesktopUpdatePrompt.tsx rename to surfsense_web/components/desktop/desktop-update-toast.tsx index 091121141..367190709 100644 --- a/surfsense_web/components/desktop/DesktopUpdatePrompt.tsx +++ b/surfsense_web/components/desktop/desktop-update-toast.tsx @@ -5,12 +5,12 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -type UpdatePromptState = { +type UpdateToastState = { version: string; }; -export function DesktopUpdatePrompt() { - const [update, setUpdate] = useState(null); +export function DesktopUpdateToast() { + const [update, setUpdate] = useState(null); useEffect(() => { const api = window.electronAPI; @@ -71,7 +71,7 @@ export function DesktopUpdatePrompt() { variant="ghost" size="icon" className="absolute right-2 top-2 size-7 text-muted-foreground hover:bg-transparent hover:text-foreground" - aria-label="Dismiss update prompt" + aria-label="Dismiss update toast" onClick={() => setUpdate(null)} > From baba31ab43107b4071537acd576606dd7523279c Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 26 May 2026 12:23:41 +0530 Subject: [PATCH 12/12] feat(tray): refactor context menu creation for improved screenshot functionality and quit option --- surfsense_desktop/src/modules/tray.ts | 41 ++++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/surfsense_desktop/src/modules/tray.ts b/surfsense_desktop/src/modules/tray.ts index f0221fe53..e71168f6e 100644 --- a/surfsense_desktop/src/modules/tray.ts +++ b/surfsense_desktop/src/modules/tray.ts @@ -10,6 +10,30 @@ let tray: Tray | null = null; let registeredGeneralAssist: string | null = null; let registeredScreenshotAssist: string | null = null; +function buildContextMenu(screenshotAccelerator: string): Menu { + return Menu.buildFromTemplate([ + { label: 'Open SurfSense', click: () => showMainWindow('tray_menu') }, + { + label: 'Take Screenshot\u2026', + accelerator: screenshotAccelerator || undefined, + click: () => { + trackEvent('desktop_tray_screenshot_clicked'); + void Promise.resolve(runScreenshotAssistShortcut()).catch((err) => { + console.error('[tray] Screenshot Assist failed:', err); + }); + }, + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + trackEvent('desktop_tray_quit_clicked'); + app.exit(0); + }, + }, + ]); +} + function getTrayIcon(): NativeImage { const iconName = process.platform === 'darwin' @@ -59,22 +83,10 @@ export async function createTray(): Promise { tray = new Tray(getTrayIcon()); tray.setToolTip('SurfSense'); - const contextMenu = Menu.buildFromTemplate([ - { label: 'Open SurfSense', click: () => showMainWindow('tray_menu') }, - { type: 'separator' }, - { - label: 'Quit', - click: () => { - trackEvent('desktop_tray_quit_clicked'); - app.exit(0); - }, - }, - ]); - - tray.setContextMenu(contextMenu); + const shortcuts = await getShortcuts(); + tray.setContextMenu(buildContextMenu(shortcuts.screenshotAssist)); tray.on('double-click', () => showMainWindow('tray_click')); - const shortcuts = await getShortcuts(); registeredGeneralAssist = registerOne( null, shortcuts.generalAssist, @@ -107,6 +119,7 @@ export async function reregisterScreenshotAssist(): Promise { runScreenshotAssistShortcut, 'Screenshot Assist' ); + tray?.setContextMenu(buildContextMenu(shortcuts.screenshotAssist)); } export function destroyTray(): void {