mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
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:
parent
2a020629c5
commit
a8c1fb660d
259 changed files with 5480 additions and 2285 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
35
surfsense_desktop/src/modules/active-workspace.ts
Normal file
35
surfsense_desktop/src/modules/active-workspace.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
23
surfsense_desktop/src/modules/migrate-watched-folders.ts
Normal file
23
surfsense_desktop/src/modules/migrate-watched-folders.ts
Normal 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 };
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue