diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/system_prompt_desktop.md b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/system_prompt_desktop.md index 4e5465aaf..b0f2dacb2 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/system_prompt_desktop.md +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/system_prompt_desktop.md @@ -33,7 +33,7 @@ Map outcomes to your `status`: - Any other `"Error: …"` → `status=error` and relay the tool's message verbatim as `next_step`. - HITL rejection → `status=blocked` with `next_step="User declined this filesystem action. Do not retry."`. -You construct the structured `evidence` fields from your own knowledge of what you called and what you observed — the tools do not return them. `chunk_ids` apply only to `` hits; for local-file operations leave them `null`. Never report values you did not actually see. +You construct the structured `evidence` fields from your own knowledge of what you called and what you observed — the tools do not return them. Never report values you did not actually see. (`chunk_ids` is always `null` in desktop mode — see "Chunk citations in your prose" below.) ## Chunk citations in your prose diff --git a/surfsense_desktop/.env.example b/surfsense_desktop/.env.example index e127b99e0..2d9de7561 100644 --- a/surfsense_desktop/.env.example +++ b/surfsense_desktop/.env.example @@ -5,6 +5,11 @@ # inside the desktop app. Set to your production frontend domain. HOSTED_FRONTEND_URL=https://surfsense.net +# Runtime override for the above (read at app start, no rebuild required). +# Useful for self-hosters whose backend NEXT_FRONTEND_URL differs from the +# value baked into the official desktop builds. Leave empty to use HOSTED_FRONTEND_URL. +# SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE= + # PostHog analytics (leave empty to disable) POSTHOG_KEY= POSTHOG_HOST=https://assets.surfsense.com diff --git a/surfsense_desktop/assets/icons/1024x1024.png b/surfsense_desktop/assets/icons/1024x1024.png new file mode 100644 index 000000000..853201c5e Binary files /dev/null and b/surfsense_desktop/assets/icons/1024x1024.png differ diff --git a/surfsense_desktop/assets/icons/128x128.png b/surfsense_desktop/assets/icons/128x128.png new file mode 100644 index 000000000..97286c8b6 Binary files /dev/null and b/surfsense_desktop/assets/icons/128x128.png differ diff --git a/surfsense_desktop/assets/icons/16x16.png b/surfsense_desktop/assets/icons/16x16.png new file mode 100644 index 000000000..860f9fef1 Binary files /dev/null and b/surfsense_desktop/assets/icons/16x16.png differ diff --git a/surfsense_desktop/assets/icons/256x256.png b/surfsense_desktop/assets/icons/256x256.png new file mode 100644 index 000000000..edb7aa512 Binary files /dev/null and b/surfsense_desktop/assets/icons/256x256.png differ diff --git a/surfsense_desktop/assets/icons/32x32.png b/surfsense_desktop/assets/icons/32x32.png new file mode 100644 index 000000000..2c1ef1222 Binary files /dev/null and b/surfsense_desktop/assets/icons/32x32.png differ diff --git a/surfsense_desktop/assets/icons/48x48.png b/surfsense_desktop/assets/icons/48x48.png new file mode 100644 index 000000000..2d765024d Binary files /dev/null and b/surfsense_desktop/assets/icons/48x48.png differ diff --git a/surfsense_desktop/assets/icons/512x512.png b/surfsense_desktop/assets/icons/512x512.png new file mode 100644 index 000000000..3fc480dd7 Binary files /dev/null and b/surfsense_desktop/assets/icons/512x512.png differ diff --git a/surfsense_desktop/assets/icons/64x64.png b/surfsense_desktop/assets/icons/64x64.png new file mode 100644 index 000000000..a218a4ee2 Binary files /dev/null and b/surfsense_desktop/assets/icons/64x64.png differ diff --git a/surfsense_desktop/electron-builder.yml b/surfsense_desktop/electron-builder.yml index e4e7670ec..0a7c48203 100644 --- a/surfsense_desktop/electron-builder.yml +++ b/surfsense_desktop/electron-builder.yml @@ -55,6 +55,11 @@ mac: NSAccessibilityUsageDescription: "SurfSense uses accessibility features to bring the app to the foreground and interact with the active application when you use desktop assists." NSScreenCaptureUsageDescription: "SurfSense uses screen capture so you can attach a selected region to chat (Screenshot Assist) or capture the full screen from the composer." NSAppleEventsUsageDescription: "SurfSense uses Apple Events to interact with the active application." + # `surfsense://` scheme — install-time registration for LaunchServices. + CFBundleURLTypes: + - CFBundleURLName: com.surfsense.desktop + CFBundleURLSchemes: + - surfsense target: - target: dmg arch: [x64, arm64] @@ -72,7 +77,7 @@ nsis: createDesktopShortcut: true createStartMenuShortcut: true linux: - icon: assets/icon.png + icon: assets/icons/ category: Utility artifactName: "${productName}-${version}-${arch}.${ext}" mimeTypes: diff --git a/surfsense_desktop/src/modules/deep-links.ts b/surfsense_desktop/src/modules/deep-links.ts index 11b7bfcff..7d5e429bd 100644 --- a/surfsense_desktop/src/modules/deep-links.ts +++ b/surfsense_desktop/src/modules/deep-links.ts @@ -60,6 +60,11 @@ export function setupDeepLinks(): boolean { app.setAsDefaultProtocolClient(PROTOCOL); } + // Cold-start on Windows/Linux: protocol URL arrives via argv of the + // first instance, not via `second-instance` or `open-url`. + const cold = process.argv.find((arg) => arg.startsWith(`${PROTOCOL}://`)); + if (cold) handleDeepLink(cold); + return true; } diff --git a/surfsense_desktop/src/modules/server.ts b/surfsense_desktop/src/modules/server.ts index e2f078a8c..17fcfb445 100644 --- a/surfsense_desktop/src/modules/server.ts +++ b/surfsense_desktop/src/modules/server.ts @@ -39,7 +39,8 @@ export async function startNextServer(): Promise { const serverScript = path.join(standalonePath, 'server.js'); process.env.PORT = String(serverPort); - process.env.HOSTNAME = '0.0.0.0'; + // Loopback bind: 0.0.0.0 leaks into request.url and flips window origin via NextResponse.redirect. + process.env.HOSTNAME = 'localhost'; process.env.NODE_ENV = 'production'; process.chdir(standalonePath); diff --git a/surfsense_desktop/src/modules/window.ts b/surfsense_desktop/src/modules/window.ts index 5317005d5..003241ef3 100644 --- a/surfsense_desktop/src/modules/window.ts +++ b/surfsense_desktop/src/modules/window.ts @@ -6,9 +6,26 @@ import { getServerPort } from './server'; 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'; +function getHostedFrontendUrl(): string { + return ( + process.env.SURFSENSE_HOSTED_FRONTEND_URL_OVERRIDE || + process.env.HOSTED_FRONTEND_URL || + 'https://surfsense.net' + ); +} + +function getHostedFrontendHosts(): string[] { + try { + const host = new URL(getHostedFrontendUrl()).host; + const sibling = host.startsWith('www.') ? host.slice(4) : `www.${host}`; + return Array.from(new Set([host, sibling])); + } catch { + return []; + } +} + let mainWindow: BrowserWindow | null = null; let isQuitting = false; @@ -58,11 +75,47 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow { return { action: 'deny' }; }); - const filter = { urls: [`${HOSTED_FRONTEND_URL}/*`] }; - session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => { - const rewritten = details.url.replace(HOSTED_FRONTEND_URL, `http://localhost:${getServerPort()}`); - callback({ redirectURL: rewritten }); - }); + const hostedHosts = getHostedFrontendHosts(); + const rewriteFilter = { + urls: hostedHosts.flatMap((h) => [`http://${h}/*`, `https://${h}/*`]), + }; + if (rewriteFilter.urls.length > 0) { + session.defaultSession.webRequest.onBeforeRequest(rewriteFilter, (details, callback) => { + try { + const u = new URL(details.url); + const originalHost = u.host; + u.protocol = 'http:'; + u.host = `localhost:${getServerPort()}`; + trackEvent('desktop_oauth_redirect_intercepted', { + host: originalHost, + path: u.pathname, + rewritten_to_port: getServerPort(), + }); + callback({ redirectURL: u.toString() }); + } catch { + callback({}); + } + }); + } + + // Diagnostic: connector callback landing somewhere other than localhost + // means the rewrite missed and the user is stranded off-app. + session.defaultSession.webRequest.onCompleted( + { urls: ['*://*/dashboard/*/connectors/callback*'] }, + (details) => { + try { + const u = new URL(details.url); + if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') return; + trackEvent('desktop_oauth_redirect_missed', { + host: u.host, + path: u.pathname, + status_code: details.statusCode, + }); + } catch { + // ignore malformed URLs + } + } + ); mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { console.error(`Failed to load ${validatedURL}: ${errorDescription} (${errorCode})`);