feat(rename): complete searchSpace to workspace transition across frontend and backend

- Introduced a comprehensive specification for renaming `searchSpace` to `workspace` in `surfsense_web` and `surfsense_desktop`, ensuring all TypeScript identifiers, React props, and local data structures are updated.
- Implemented migration shims for persisted local state to prevent data loss during the transition.
- Updated observability metrics and IPC channels to reflect the new naming convention.
- Removed legacy `active-search-space` module and replaced it with `active-workspace` to maintain consistency.
- Ensured no behavioral changes or data loss for users during the renaming process.
This commit is contained in:
Anish Sarkar 2026-07-06 15:12:40 +05:30
parent 2a020629c5
commit a8c1fb660d
259 changed files with 5480 additions and 2285 deletions

View file

@ -50,8 +50,8 @@ export const IPC_CHANNELS = {
GET_SHORTCUTS: 'shortcuts:get',
SET_SHORTCUTS: 'shortcuts:set',
// Active search space
GET_ACTIVE_SEARCH_SPACE: 'search-space:get-active',
SET_ACTIVE_SEARCH_SPACE: 'search-space:set-active',
GET_ACTIVE_WORKSPACE: 'workspace:get-active',
SET_ACTIVE_WORKSPACE: 'workspace:set-active',
// Launch on system startup
GET_AUTO_LAUNCH: 'auto-launch:get',
SET_AUTO_LAUNCH: 'auto-launch:set',

View file

@ -27,7 +27,7 @@ import {
} from '../modules/folder-watcher';
import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts';
import { getAutoLaunchState, setAutoLaunch } from '../modules/auto-launch';
import { getActiveSearchSpaceId, setActiveSearchSpaceId } from '../modules/active-search-space';
import { getActiveWorkspaceId, setActiveWorkspaceId } from '../modules/active-workspace';
import { reregisterQuickAsk } from '../modules/quick-ask';
import { reregisterGeneralAssist, reregisterScreenshotAssist } from '../modules/tray';
import {
@ -205,9 +205,9 @@ export function registerIpcHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT,
async (_event, virtualPath: string, searchSpaceId?: number | null) => {
async (_event, virtualPath: string, workspaceId?: number | null) => {
try {
const result = await readAgentLocalFileText(virtualPath, searchSpaceId);
const result = await readAgentLocalFileText(virtualPath, workspaceId);
return { ok: true, path: result.path, content: result.content };
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read local file';
@ -218,9 +218,9 @@ export function registerIpcHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT,
async (_event, virtualPath: string, content: string, searchSpaceId?: number | null) => {
async (_event, virtualPath: string, content: string, workspaceId?: number | null) => {
try {
const result = await writeAgentLocalFileText(virtualPath, content, searchSpaceId);
const result = await writeAgentLocalFileText(virtualPath, content, workspaceId);
return { ok: true, path: result.path };
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write local file';
@ -321,10 +321,10 @@ export function registerIpcHandlers(): void {
},
);
ipcMain.handle(IPC_CHANNELS.GET_ACTIVE_SEARCH_SPACE, () => getActiveSearchSpaceId());
ipcMain.handle(IPC_CHANNELS.GET_ACTIVE_WORKSPACE, () => getActiveWorkspaceId());
ipcMain.handle(IPC_CHANNELS.SET_ACTIVE_SEARCH_SPACE, (_event, id: string) =>
setActiveSearchSpaceId(id)
ipcMain.handle(IPC_CHANNELS.SET_ACTIVE_WORKSPACE, (_event, id: string) =>
setActiveWorkspaceId(id)
);
ipcMain.handle(IPC_CHANNELS.SET_SHORTCUTS, async (_event, config: Partial<ShortcutConfig>) => {
@ -370,12 +370,12 @@ export function registerIpcHandlers(): void {
};
});
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, (_event, searchSpaceId?: number | null) =>
getAgentFilesystemSettings(searchSpaceId)
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, (_event, workspaceId?: number | null) =>
getAgentFilesystemSettings(workspaceId)
);
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, searchSpaceId?: number | null) =>
getAgentFilesystemMounts(searchSpaceId)
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, workspaceId?: number | null) =>
getAgentFilesystemMounts(workspaceId)
);
ipcMain.handle(
@ -384,7 +384,7 @@ export function registerIpcHandlers(): void {
_event,
options: {
rootPath: string;
searchSpaceId?: number | null;
workspaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
}
@ -397,10 +397,10 @@ export function registerIpcHandlers(): void {
(
_event,
payload: {
searchSpaceId?: number | null;
workspaceId?: number | null;
settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | null };
}
) => setAgentFilesystemSettings(payload?.searchSpaceId, payload?.settings ?? {})
) => setAgentFilesystemSettings(payload?.workspaceId, payload?.settings ?? {})
);
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT, () =>
@ -415,7 +415,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP,
(_event, searchSpaceId?: number | null) =>
stopAgentFilesystemTreeWatch(searchSpaceId)
(_event, workspaceId?: number | null) =>
stopAgentFilesystemTreeWatch(workspaceId)
);
}

View file

@ -1,24 +0,0 @@
const STORE_KEY = 'activeSearchSpaceId';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let store: any = null;
async function getStore() {
if (!store) {
const { default: Store } = await import('electron-store');
store = new Store({
name: 'active-search-space',
defaults: { [STORE_KEY]: null as string | null },
});
}
return store;
}
export async function getActiveSearchSpaceId(): Promise<string | null> {
const s = await getStore();
return (s.get(STORE_KEY) as string | null) ?? null;
}
export async function setActiveSearchSpaceId(id: string): Promise<void> {
const s = await getStore();
s.set(STORE_KEY, id);
}

View file

@ -0,0 +1,35 @@
const STORE_KEY = 'activeWorkspaceId';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let store: any = null;
async function getStore() {
if (!store) {
const { default: Store } = await import('electron-store');
store = new Store({
name: 'active-workspace',
defaults: { [STORE_KEY]: null as string | null },
});
// One-time migration from the legacy `active-search-space` store so the
// user's last-selected workspace survives the rename.
if (store.get(STORE_KEY) == null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacy: any = new Store({
name: 'active-search-space',
defaults: { activeSearchSpaceId: null as string | null },
});
const prev = legacy.get('activeSearchSpaceId') as string | null;
if (prev != null) store.set(STORE_KEY, prev);
}
}
return store;
}
export async function getActiveWorkspaceId(): Promise<string | null> {
const s = await getStore();
return (s.get(STORE_KEY) as string | null) ?? null;
}
export async function setActiveWorkspaceId(id: string): Promise<void> {
const s = await getStore();
s.set(STORE_KEY, id);
}

View file

@ -8,7 +8,7 @@ const SAFETY_POLL_MS = 60_000;
const EVENT_DEBOUNCE_MS = 700;
export type AgentFilesystemTreeWatchOptions = {
searchSpaceId?: number | null;
workspaceId?: number | null;
rootPaths: string[];
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
@ -17,7 +17,7 @@ export type AgentFilesystemTreeWatchOptions = {
type TreeDirtyReason = 'watcher_event' | 'safety_poll';
type TreeDirtyEvent = {
searchSpaceId: number | null;
workspaceId: number | null;
reason: TreeDirtyReason;
rootPath: string;
changedPath: string | null;
@ -25,7 +25,7 @@ type TreeDirtyEvent = {
};
type WatchSession = {
searchSpaceId: number | null;
workspaceId: number | null;
optionsSignature: string;
rootPaths: string[];
excludePatterns: string[];
@ -40,15 +40,15 @@ type WatchSession = {
const sessions = new Map<string, WatchSession>();
function normalizeSearchSpaceId(searchSpaceId?: number | null): number | null {
if (typeof searchSpaceId === 'number' && Number.isFinite(searchSpaceId) && searchSpaceId > 0) {
return searchSpaceId;
function normalizeWorkspaceId(workspaceId?: number | null): number | null {
if (typeof workspaceId === 'number' && Number.isFinite(workspaceId) && workspaceId > 0) {
return workspaceId;
}
return null;
}
function getSessionKey(searchSpaceId?: number | null): string {
const normalized = normalizeSearchSpaceId(searchSpaceId);
function getSessionKey(workspaceId?: number | null): string {
const normalized = normalizeWorkspaceId(workspaceId);
return normalized === null ? 'default' : String(normalized);
}
@ -71,13 +71,13 @@ function normalizeExtensions(value: string[] | null | undefined): string[] | nul
}
function buildOptionsSignature(
searchSpaceId: number | null,
workspaceId: number | null,
rootPaths: string[],
excludePatterns: string[],
fileExtensions: string[] | null
): string {
return JSON.stringify({
searchSpaceId,
workspaceId,
rootPaths: [...rootPaths].sort(),
excludePatterns: [...excludePatterns].sort(),
fileExtensions: fileExtensions ? [...fileExtensions].sort() : null,
@ -99,10 +99,10 @@ async function buildRootSnapshotSignature(
rootPath: string
): Promise<string> {
let hash = 2166136261;
hash = hashText(`space:${session.searchSpaceId ?? 'default'}|root:${rootPath}`, hash);
hash = hashText(`space:${session.workspaceId ?? 'default'}|root:${rootPath}`, hash);
const files = await listAgentFilesystemFiles({
rootPath,
searchSpaceId: session.searchSpaceId,
workspaceId: session.workspaceId,
excludePatterns: session.excludePatterns,
fileExtensions: session.fileExtensions,
});
@ -118,13 +118,13 @@ async function buildRootSnapshotSignature(
}
function sendTreeDirtyEvent(
searchSpaceId: number | null,
workspaceId: number | null,
reason: TreeDirtyReason,
rootPath: string,
changedPath: string | null
): void {
const payload: TreeDirtyEvent = {
searchSpaceId,
workspaceId,
reason,
rootPath,
changedPath,
@ -158,7 +158,7 @@ function scheduleDirtyEmit(
session.pendingDirtyByRoot.clear();
for (const [pendingRootPath, payload] of pending) {
sendTreeDirtyEvent(
session.searchSpaceId,
session.workspaceId,
payload.reason,
pendingRootPath,
payload.changedPath
@ -183,21 +183,21 @@ async function closeSession(session: WatchSession): Promise<void> {
export async function startAgentFilesystemTreeWatch(
options: AgentFilesystemTreeWatchOptions
): Promise<{ ok: true }> {
const searchSpaceId = normalizeSearchSpaceId(options.searchSpaceId);
const workspaceId = normalizeWorkspaceId(options.workspaceId);
const rootPaths = Array.from(
new Set(normalizeList(options.rootPaths).map((rootPath) => normalizeRootPath(rootPath)))
);
const excludePatterns = Array.from(new Set(normalizeList(options.excludePatterns)));
const fileExtensions = normalizeExtensions(options.fileExtensions);
const sessionKey = getSessionKey(searchSpaceId);
const sessionKey = getSessionKey(workspaceId);
if (rootPaths.length === 0) {
await stopAgentFilesystemTreeWatch(searchSpaceId);
await stopAgentFilesystemTreeWatch(workspaceId);
return { ok: true };
}
const optionsSignature = buildOptionsSignature(
searchSpaceId,
workspaceId,
rootPaths,
excludePatterns,
fileExtensions
@ -228,7 +228,7 @@ export async function startAgentFilesystemTreeWatch(
);
const session: WatchSession = {
searchSpaceId,
workspaceId,
optionsSignature,
rootPaths,
excludePatterns,
@ -291,9 +291,9 @@ export async function startAgentFilesystemTreeWatch(
}
export async function stopAgentFilesystemTreeWatch(
searchSpaceId?: number | null
workspaceId?: number | null
): Promise<{ ok: true }> {
const sessionKey = getSessionKey(searchSpaceId);
const sessionKey = getSessionKey(workspaceId);
const session = sessions.get(sessionKey);
if (!session) return { ok: true };
sessions.delete(sessionKey);

View file

@ -119,9 +119,9 @@ async function normalizeLocalRootPathsCanonical(paths: unknown): Promise<string[
return [...uniquePaths];
}
function normalizeSearchSpaceKey(searchSpaceId?: number | null): string {
if (typeof searchSpaceId === "number" && Number.isFinite(searchSpaceId) && searchSpaceId > 0) {
return String(searchSpaceId);
function normalizeWorkspaceKey(workspaceId?: number | null): string {
if (typeof workspaceId === "number" && Number.isFinite(workspaceId) && workspaceId > 0) {
return String(workspaceId);
}
return DEFAULT_SPACE_KEY;
}
@ -147,9 +147,9 @@ function getDefaultStore(): AgentFilesystemSettingsStore {
function getSettingsFromStore(
store: AgentFilesystemSettingsStore,
searchSpaceId?: number | null
workspaceId?: number | null
): AgentFilesystemSettings {
const key = normalizeSearchSpaceKey(searchSpaceId);
const key = normalizeWorkspaceKey(workspaceId);
return store.spaces[key] ?? getDefaultSettings();
}
@ -194,22 +194,22 @@ async function loadAgentFilesystemSettingsStore(): Promise<AgentFilesystemSettin
}
export async function getAgentFilesystemSettings(
searchSpaceId?: number | null
workspaceId?: number | null
): Promise<AgentFilesystemSettings> {
const store = await loadAgentFilesystemSettingsStore();
return getSettingsFromStore(store, searchSpaceId);
return getSettingsFromStore(store, workspaceId);
}
export async function setAgentFilesystemSettings(
searchSpaceId: number | null | undefined,
workspaceId: number | null | undefined,
settings: {
mode?: AgentFilesystemMode;
localRootPaths?: string[] | null;
}
): Promise<AgentFilesystemSettings> {
const store = await loadAgentFilesystemSettingsStore();
const key = normalizeSearchSpaceKey(searchSpaceId);
const current = getSettingsFromStore(store, searchSpaceId);
const key = normalizeWorkspaceKey(workspaceId);
const current = getSettingsFromStore(store, workspaceId);
const nextMode =
settings.mode === "cloud" || settings.mode === "desktop_local_folder"
? settings.mode
@ -291,7 +291,7 @@ export type LocalRootMount = {
export type AgentFilesystemListOptions = {
rootPath: string;
searchSpaceId?: number | null;
workspaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
};
@ -332,9 +332,9 @@ function buildRootMounts(rootPaths: string[]): LocalRootMount[] {
}
export async function getAgentFilesystemMounts(
searchSpaceId?: number | null
workspaceId?: number | null
): Promise<LocalRootMount[]> {
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
const rootPaths = await resolveCurrentRootPaths(workspaceId);
return buildRootMounts(rootPaths);
}
@ -371,7 +371,7 @@ function normalizeExcludeSet(excludePatterns: string[] | null | undefined): Set<
export async function listAgentFilesystemFiles(
options: AgentFilesystemListOptions
): Promise<AgentFilesystemFileEntry[]> {
const allowedRootPaths = await resolveCurrentRootPaths(options.searchSpaceId);
const allowedRootPaths = await resolveCurrentRootPaths(options.workspaceId);
const requestedRootPath = await canonicalizeRootPath(options.rootPath);
const normalizedRequestedRoot = normalizeComparablePath(requestedRootPath);
const allowedRoots = new Set(
@ -474,8 +474,8 @@ function toMountedVirtualPath(mount: string, rootPath: string, absolutePath: str
return `/${mount}${relativePath}`;
}
async function resolveCurrentRootPaths(searchSpaceId?: number | null): Promise<string[]> {
const settings = await getAgentFilesystemSettings(searchSpaceId);
async function resolveCurrentRootPaths(workspaceId?: number | null): Promise<string[]> {
const settings = await getAgentFilesystemSettings(workspaceId);
if (settings.localRootPaths.length === 0) {
throw new Error("No local filesystem roots selected");
}
@ -484,9 +484,9 @@ async function resolveCurrentRootPaths(searchSpaceId?: number | null): Promise<s
export async function readAgentLocalFileText(
virtualPath: string,
searchSpaceId?: number | null
workspaceId?: number | null
): Promise<{ path: string; content: string }> {
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
const rootPaths = await resolveCurrentRootPaths(workspaceId);
const mounts = buildRootMounts(rootPaths);
const { mount, subPath } = parseMountedVirtualPath(virtualPath, mounts);
const rootMount = findMountByName(mounts, mount);
@ -507,9 +507,9 @@ export async function readAgentLocalFileText(
export async function writeAgentLocalFileText(
virtualPath: string,
content: string,
searchSpaceId?: number | null
workspaceId?: number | null
): Promise<{ path: string }> {
const rootPaths = await resolveCurrentRootPaths(searchSpaceId);
const rootPaths = await resolveCurrentRootPaths(workspaceId);
const mounts = buildRootMounts(rootPaths);
const { mount, subPath } = parseMountedVirtualPath(virtualPath, mounts);
const rootMount = findMountByName(mounts, mount);

View file

@ -5,6 +5,7 @@ import * as path from 'path';
import * as fs from 'fs';
import { IPC_CHANNELS } from '../ipc/channels';
import { trackEvent } from './analytics';
import { migrateWatchedFolderConfigs } from './migrate-watched-folders';
export interface WatchedFolderConfig {
path: string;
@ -12,7 +13,7 @@ export interface WatchedFolderConfig {
excludePatterns: string[];
fileExtensions: string[] | null;
rootFolderId: number | null;
searchSpaceId: number;
workspaceId: number;
active: boolean;
}
@ -27,7 +28,7 @@ type FolderSyncAction = 'add' | 'change' | 'unlink';
export interface FolderSyncFileChangedEvent {
id: string;
rootFolderId: number | null;
searchSpaceId: number;
workspaceId: number;
folderPath: string;
folderName: string;
relativePath: string;
@ -68,6 +69,12 @@ async function getStore() {
[STORE_KEY]: [] as WatchedFolderConfig[],
},
});
// One-time read-migration: legacy persisted configs stored the workspace as
// `searchSpaceId`. Map it to `workspaceId` and write back once so existing
// watched folders keep their sync target after the rename.
const raw = store.get(STORE_KEY, []) as Array<Record<string, unknown>>;
const { configs, migrated } = migrateWatchedFolderConfigs<WatchedFolderConfig>(raw);
if (migrated) store.set(STORE_KEY, configs);
}
return store;
}
@ -267,7 +274,7 @@ async function startWatcher(config: WatchedFolderConfig) {
if (storedMtime === undefined) {
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
workspaceId: config.workspaceId,
folderPath: config.path,
folderName: config.name,
relativePath: rel,
@ -278,7 +285,7 @@ async function startWatcher(config: WatchedFolderConfig) {
} else if (Math.abs(currentMtime - storedMtime) >= MTIME_TOLERANCE_S * 1000) {
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
workspaceId: config.workspaceId,
folderPath: config.path,
folderName: config.name,
relativePath: rel,
@ -295,7 +302,7 @@ async function startWatcher(config: WatchedFolderConfig) {
if (!(rel in currentMap)) {
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
workspaceId: config.workspaceId,
folderPath: config.path,
folderName: config.name,
relativePath: rel,
@ -346,7 +353,7 @@ async function startWatcher(config: WatchedFolderConfig) {
sendFileChangedEvent({
rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId,
workspaceId: config.workspaceId,
folderPath: config.path,
folderName: config.name,
relativePath,
@ -403,7 +410,7 @@ export async function addWatchedFolder(
}
trackEvent('desktop_folder_watch_added', {
search_space_id: config.searchSpaceId,
workspace_id: config.workspaceId,
root_folder_id: config.rootFolderId,
active: config.active,
has_exclude_patterns: (config.excludePatterns?.length ?? 0) > 0,
@ -431,7 +438,7 @@ export async function removeWatchedFolder(
if (removed) {
trackEvent('desktop_folder_watch_removed', {
search_space_id: removed.searchSpaceId,
workspace_id: removed.workspaceId,
root_folder_id: removed.rootFolderId,
});
}

View file

@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { migrateWatchedFolderConfigs } from "./migrate-watched-folders.ts";
// Run with: node --test src/modules/migrate-watched-folders.test.ts
test("maps legacy searchSpaceId to workspaceId and flags migration", () => {
const { configs, migrated } = migrateWatchedFolderConfigs<{ workspaceId?: number }>([
{ path: "/tmp/a", searchSpaceId: 42 },
]);
assert.equal(migrated, true);
assert.equal(configs[0].workspaceId, 42);
assert.equal("searchSpaceId" in (configs[0] as object), false);
});
test("leaves configs that already have workspaceId untouched", () => {
const { configs, migrated } = migrateWatchedFolderConfigs<{ workspaceId?: number }>([
{ path: "/tmp/b", workspaceId: 5 },
]);
assert.equal(migrated, false);
assert.equal(configs[0].workspaceId, 5);
});

View file

@ -0,0 +1,23 @@
/**
* One-time read-migration for persisted watched-folder configs: legacy configs
* stored the workspace as `searchSpaceId`. Map it to `workspaceId` so existing
* watched folders keep their sync target after the rename. Pure + dependency-free
* so it can be unit-checked without loading electron-store.
*
* Returns the migrated configs and whether anything changed (so callers can
* write back only when needed).
*/
export function migrateWatchedFolderConfigs<T>(
raw: Array<Record<string, unknown>>
): { configs: T[]; migrated: boolean } {
let migrated = false;
const configs = raw.map((c) => {
if (c.workspaceId === undefined && c.searchSpaceId !== undefined) {
migrated = true;
const { searchSpaceId, ...rest } = c;
return { ...rest, workspaceId: searchSpaceId } as unknown as T;
}
return c as unknown as T;
});
return { configs, migrated };
}

View file

@ -4,14 +4,14 @@ import { IPC_CHANNELS } from '../ipc/channels';
import { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
import { getServerOrigin } from './server';
import { getShortcuts } from './shortcuts';
import { getActiveSearchSpaceId } from './active-search-space';
import { getActiveWorkspaceId } from './active-workspace';
import { trackEvent } from './analytics';
let currentShortcut = '';
let quickAskWindow: BrowserWindow | null = null;
let pendingText = '';
let pendingMode = '';
let pendingSearchSpaceId: string | null = null;
let pendingWorkspaceId: string | null = null;
let sourceApp = '';
let savedClipboard = '';
@ -57,7 +57,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
skipTaskbar: true,
});
const spaceId = pendingSearchSpaceId;
const spaceId = pendingWorkspaceId;
const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard';
quickAskWindow.loadURL(`${getServerOrigin()}${route}?quickAssist=true`);
@ -87,7 +87,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
async function openQuickAsk(text: string): Promise<void> {
pendingText = text;
pendingMode = 'quick-assist';
pendingSearchSpaceId = await getActiveSearchSpaceId();
pendingWorkspaceId = await getActiveWorkspaceId();
const cursor = screen.getCursorScreenPoint();
const pos = clampToScreen(cursor.x, cursor.y, 450, 750);
createQuickAskWindow(pos.x, pos.y);

View file

@ -3,7 +3,7 @@ import path from 'path';
import { trackEvent } from './analytics';
import { showErrorDialog } from './errors';
import { getServerOrigin, getServerPort } from './server';
import { setActiveSearchSpaceId } from './active-search-space';
import { setActiveWorkspaceId } from './active-workspace';
const isDev = !app.isPackaged;
const isMac = process.platform === 'darwin';
@ -140,14 +140,14 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
});
// Auto-sync active search space from URL navigation
const syncSearchSpace = (url: string) => {
const syncWorkspace = (url: string) => {
const match = url.match(/\/dashboard\/(\d+)/);
if (match) {
setActiveSearchSpaceId(match[1]);
setActiveWorkspaceId(match[1]);
}
};
mainWindow.webContents.on('did-navigate', (_event, url) => syncSearchSpace(url));
mainWindow.webContents.on('did-navigate-in-page', (_event, url) => syncSearchSpace(url));
mainWindow.webContents.on('did-navigate', (_event, url) => syncWorkspace(url));
mainWindow.webContents.on('did-navigate-in-page', (_event, url) => syncWorkspace(url));
if (isDev) {
mainWindow.webContents.openDevTools();

View file

@ -74,10 +74,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
// Browse files via native dialog
browseFiles: () => ipcRenderer.invoke(IPC_CHANNELS.BROWSE_FILES),
readLocalFiles: (paths: string[]) => ipcRenderer.invoke(IPC_CHANNELS.READ_LOCAL_FILES, paths),
readAgentLocalFileText: (virtualPath: string, searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, virtualPath, searchSpaceId),
writeAgentLocalFileText: (virtualPath: string, content: string, searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content, searchSpaceId),
readAgentLocalFileText: (virtualPath: string, workspaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, virtualPath, workspaceId),
writeAgentLocalFileText: (virtualPath: string, content: string, workspaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, virtualPath, content, workspaceId),
// Auth token sync across windows
getAccessToken: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACCESS_TOKEN),
@ -104,9 +104,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke(IPC_CHANNELS.SET_AUTO_LAUNCH, { enabled, openAsHidden }),
// Active search space
getActiveSearchSpace: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACTIVE_SEARCH_SPACE),
setActiveSearchSpace: (id: string) =>
ipcRenderer.invoke(IPC_CHANNELS.SET_ACTIVE_SEARCH_SPACE, id),
getActiveWorkspace: () => ipcRenderer.invoke(IPC_CHANNELS.GET_ACTIVE_WORKSPACE),
setActiveWorkspace: (id: string) =>
ipcRenderer.invoke(IPC_CHANNELS.SET_ACTIVE_WORKSPACE, id),
// Analytics bridge — lets posthog-js running inside the Next.js renderer
// mirror identify/reset/capture into the Electron main-process PostHog
@ -118,27 +118,27 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.invoke(IPC_CHANNELS.ANALYTICS_CAPTURE, { event, properties }),
getAnalyticsContext: () => ipcRenderer.invoke(IPC_CHANNELS.ANALYTICS_GET_CONTEXT),
// Agent filesystem mode
getAgentFilesystemSettings: (searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, searchSpaceId),
getAgentFilesystemMounts: (searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, searchSpaceId),
getAgentFilesystemSettings: (workspaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, workspaceId),
getAgentFilesystemMounts: (workspaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, workspaceId),
listAgentFilesystemFiles: (options: {
rootPath: string;
searchSpaceId?: number | null;
workspaceId?: number | null;
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_LIST_FILES, options),
startAgentFilesystemTreeWatch: (options: {
searchSpaceId?: number | null;
workspaceId?: number | null;
rootPaths: string[];
excludePatterns?: string[] | null;
fileExtensions?: string[] | null;
}) => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_START, options),
stopAgentFilesystemTreeWatch: (searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP, searchSpaceId),
stopAgentFilesystemTreeWatch: (workspaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP, workspaceId),
onAgentFilesystemTreeDirty: (
callback: (data: {
searchSpaceId: number | null;
workspaceId: number | null;
reason: 'watcher_event' | 'safety_poll';
rootPath: string;
changedPath: string | null;
@ -148,7 +148,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
const listener = (
_event: unknown,
data: {
searchSpaceId: number | null;
workspaceId: number | null;
reason: 'watcher_event' | 'safety_poll';
rootPath: string;
changedPath: string | null;
@ -163,7 +163,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
setAgentFilesystemSettings: (settings: {
mode?: "cloud" | "desktop_local_folder";
localRootPaths?: string[] | null;
}, searchSpaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, { searchSpaceId, settings }),
}, workspaceId?: number | null) =>
ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_SET_SETTINGS, { workspaceId, settings }),
pickAgentFilesystemRoot: () => ipcRenderer.invoke(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT),
});