Merge pull request #1162 from CREDO23/feat/vision-autocomplete

[Feat] Multi-suggestion autocomplete, Vision LLM config & Desktop analytics
This commit is contained in:
Rohan Verma 2026-04-07 14:01:44 -07:00 committed by GitHub
commit e827a3906d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 3263 additions and 153 deletions

View file

@ -4,3 +4,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
# PostHog analytics (leave empty to disable)
POSTHOG_KEY=
POSTHOG_HOST=https://assets.surfsense.com

View file

@ -34,6 +34,8 @@
"electron-store": "^11.0.2",
"electron-updater": "^6.8.3",
"get-port-please": "^3.2.0",
"node-mac-permissions": "^2.5.0"
"node-mac-permissions": "^2.5.0",
"node-machine-id": "^1.1.12",
"posthog-node": "^5.29.0"
}
}

View file

@ -26,6 +26,12 @@ importers:
node-mac-permissions:
specifier: ^2.5.0
version: 2.5.0
node-machine-id:
specifier: ^1.1.12
version: 1.1.12
posthog-node:
specifier: ^5.29.0
version: 5.29.0(rxjs@7.8.2)
devDependencies:
'@electron/rebuild':
specifier: ^4.0.3
@ -308,6 +314,9 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@posthog/core@1.25.0':
resolution: {integrity: sha512-XKaHvRFIIN7Dw84r1eKimV1rl9DS+9XMCPPZ7P3+l8fE+rDsmumebiTFsY+q40bVXflcGW9wB+57LH0lvcGmhw==}
'@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
@ -1194,6 +1203,9 @@ packages:
resolution: {integrity: sha512-zR8SVCaN3WqV1xwWd04XVAdzm3UTdjbxciLrZtB0Cc7F2Kd34AJfhPD4hm1HU0YH3oGUZO4X9OBLY5ijSTHsGw==}
os: [darwin]
node-machine-id@1.1.12:
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
nopt@8.1.0:
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
engines: {node: ^18.17.0 || >=20.5.0}
@ -1263,6 +1275,15 @@ packages:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
posthog-node@5.29.0:
resolution: {integrity: sha512-po7N55haSKxV8VOulkBZJja938yILShl6+fFjoUV3iQgOBCg4Muu615/xRg8mpNiz+UASvL0EEiGvIxdhXfj6Q==}
engines: {node: ^20.20.0 || >=22.22.0}
peerDependencies:
rxjs: ^7.0.0
peerDependenciesMeta:
rxjs:
optional: true
postject@1.0.0-alpha.6:
resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==}
engines: {node: '>=14.0.0'}
@ -1876,6 +1897,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@posthog/core@1.25.0': {}
'@sindresorhus/is@4.6.0': {}
'@standard-schema/spec@1.1.0': {}
@ -2940,6 +2963,8 @@ snapshots:
bindings: 1.5.0
node-addon-api: 7.1.1
node-machine-id@1.1.12: {}
nopt@8.1.0:
dependencies:
abbrev: 3.0.1
@ -3002,6 +3027,12 @@ snapshots:
base64-js: 1.5.1
xmlbuilder: 15.1.1
posthog-node@5.29.0(rxjs@7.8.2):
dependencies:
'@posthog/core': 1.25.0
optionalDependencies:
rxjs: 7.8.2
postject@1.0.0-alpha.6:
dependencies:
commander: 9.5.0

View file

@ -111,6 +111,12 @@ async function buildElectron() {
'process.env.HOSTED_FRONTEND_URL': JSON.stringify(
process.env.HOSTED_FRONTEND_URL || desktopEnv.HOSTED_FRONTEND_URL || 'https://surfsense.net'
),
'process.env.POSTHOG_KEY': JSON.stringify(
process.env.POSTHOG_KEY || desktopEnv.POSTHOG_KEY || ''
),
'process.env.POSTHOG_HOST': JSON.stringify(
process.env.POSTHOG_HOST || desktopEnv.POSTHOG_HOST || 'https://assets.surfsense.com'
),
},
};

View file

@ -12,6 +12,7 @@ import { registerAutocomplete, unregisterAutocomplete } from './modules/autocomp
import { registerFolderWatcher, unregisterFolderWatcher } from './modules/folder-watcher';
import { registerIpcHandlers } from './ipc/handlers';
import { createTray, destroyTray } from './modules/tray';
import { initAnalytics, shutdownAnalytics, trackEvent } from './modules/analytics';
registerGlobalErrorHandlers();
@ -22,6 +23,8 @@ if (!setupDeepLinks()) {
registerIpcHandlers();
app.whenReady().then(async () => {
initAnalytics();
trackEvent('desktop_app_launched');
setupMenu();
try {
await startNextServer();
@ -70,9 +73,15 @@ app.on('before-quit', () => {
isQuitting = true;
});
app.on('will-quit', () => {
let didCleanup = false;
app.on('will-quit', async (e) => {
if (didCleanup) return;
didCleanup = true;
e.preventDefault();
unregisterQuickAsk();
unregisterAutocomplete();
unregisterFolderWatcher();
destroyTray();
await shutdownAnalytics();
app.exit();
});

View file

@ -0,0 +1,50 @@
import { PostHog } from 'posthog-node';
import { machineIdSync } from 'node-machine-id';
import { app } from 'electron';
let client: PostHog | null = null;
let distinctId = '';
export function initAnalytics(): void {
const key = process.env.POSTHOG_KEY;
if (!key) return;
try {
distinctId = machineIdSync(true);
} catch {
return;
}
client = new PostHog(key, {
host: process.env.POSTHOG_HOST || 'https://assets.surfsense.com',
flushAt: 20,
flushInterval: 10000,
});
}
export function trackEvent(event: string, properties?: Record<string, unknown>): void {
if (!client) return;
try {
client.capture({
distinctId,
event,
properties: {
platform: 'desktop',
app_version: app.getVersion(),
os: process.platform,
...properties,
},
});
} catch {
// Analytics should never break the app
}
}
export async function shutdownAnalytics(): Promise<void> {
if (!client) return;
const timeout = new Promise<void>((resolve) => setTimeout(resolve, 3000));
await Promise.race([client.shutdown(), timeout]);
client = null;
}

View file

@ -6,6 +6,7 @@ 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;
@ -41,6 +42,7 @@ async function triggerAutocomplete(): Promise<void> {
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);
@ -87,9 +89,11 @@ function registerIpcHandlers(): void {
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) => {

View file

@ -5,6 +5,7 @@ import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePa
import { getServerPort } from './server';
import { getShortcuts } from './shortcuts';
import { getActiveSearchSpaceId } from './active-search-space';
import { trackEvent } from './analytics';
let currentShortcut = '';
let quickAskWindow: BrowserWindow | null = null;
@ -121,6 +122,7 @@ async function quickAskHandler(): Promise<void> {
sourceApp = getFrontmostApp();
console.log('[quick-ask] Source app:', sourceApp, '| Opening Quick Assist with', text.length, 'chars', selected ? '(selected)' : text ? '(clipboard fallback)' : '(empty)');
trackEvent('desktop_quick_ask_opened', { has_selected_text: !!selected });
openQuickAsk(text);
}
@ -152,6 +154,7 @@ function registerIpcHandlers(): void {
if (!checkAccessibilityPermission()) return;
trackEvent('desktop_quick_ask_replaced');
clipboard.writeText(text);
destroyQuickAsk();