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

@ -543,7 +543,7 @@ def record_kb_search_duration(
_record( _record(
_kb_search_duration(), _kb_search_duration(),
duration_ms, duration_ms,
{"search_space.id": workspace_id, "search.surface": surface}, {"workspace.id": workspace_id, "search.surface": surface},
) )

View file

@ -261,7 +261,7 @@ def kb_search_span(
"""Span around knowledge-base search routines.""" """Span around knowledge-base search routines."""
attrs: dict[str, Any] = {} attrs: dict[str, Any] = {}
if workspace_id is not None: if workspace_id is not None:
attrs["search_space.id"] = int(workspace_id) attrs["workspace.id"] = int(workspace_id)
if query_chars is not None: if query_chars is not None:
attrs["query.chars"] = int(query_chars) attrs["query.chars"] = int(query_chars)
if extra: if extra:
@ -303,7 +303,7 @@ def chat_request_span(
if chat_id is not None: if chat_id is not None:
attrs["chat.id"] = int(chat_id) attrs["chat.id"] = int(chat_id)
if workspace_id is not None: if workspace_id is not None:
attrs["search_space.id"] = int(workspace_id) attrs["workspace.id"] = int(workspace_id)
if flow: if flow:
attrs["chat.flow"] = flow attrs["chat.flow"] = flow
if request_id: if request_id:

View file

@ -112,12 +112,12 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
})); }));
const token = await storage.get("token"); const token = await storage.get("token");
const search_space_id = parseInt(await storage.get("search_space_id"), 10); const workspace_id = parseInt(await storage.get("workspace_id"), 10);
const toSend = { const toSend = {
document_type: "EXTENSION", document_type: "EXTENSION",
content: content, content: content,
search_space_id: search_space_id, workspace_id: workspace_id,
}; };
console.log("toSend", toSend); console.log("toSend", toSend);

View file

@ -106,12 +106,12 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
})); }));
const token = await storage.get("token"); const token = await storage.get("token");
const search_space_id = parseInt(await storage.get("search_space_id"), 10); const workspace_id = parseInt(await storage.get("workspace_id"), 10);
const toSend = { const toSend = {
document_type: "EXTENSION", document_type: "EXTENSION",
content: content, content: content,
search_space_id: search_space_id, workspace_id: workspace_id,
}; };
const requestOptions = { const requestOptions = {

View file

@ -0,0 +1,7 @@
allowBuilds:
'@parcel/watcher': set this to true or false
'@swc/core': set this to true or false
esbuild: set this to true or false
lmdb: set this to true or false
msgpackr-extract: set this to true or false
sharp: set this to true or false

View file

@ -33,6 +33,20 @@ import { getRenderedHtml } from "~utils/commons";
import type { WebHistory } from "~utils/interfaces"; import type { WebHistory } from "~utils/interfaces";
import Loading from "./Loading"; import Loading from "./Loading";
// One-time migration: legacy persisted keys were `search_space` / `search_space_id`.
// Copy them to the new `workspace` / `workspace_id` keys on first read so the
// user's selected workspace survives the rename.
async function migrateLegacyWorkspaceKeys(storage: Storage): Promise<void> {
const legacyName = await storage.get("search_space");
if (legacyName !== undefined && (await storage.get("workspace")) === undefined) {
await storage.set("workspace", legacyName);
}
const legacyId = await storage.get("search_space_id");
if (legacyId !== undefined && (await storage.get("workspace_id")) === undefined) {
await storage.set("workspace_id", legacyId);
}
}
const HomePage = () => { const HomePage = () => {
const { toast } = useToast(); const { toast } = useToast();
const navigation = useNavigate(); const navigation = useNavigate();
@ -40,11 +54,11 @@ const HomePage = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<string>(""); const [value, setValue] = React.useState<string>("");
const [searchspaces, setSearchSpaces] = useState([]); const [workspaces, setWorkspaces] = useState([]);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
useEffect(() => { useEffect(() => {
const checkSearchSpaces = async () => { const checkWorkspaces = async () => {
const storage = new Storage({ area: "local" }); const storage = new Storage({ area: "local" });
const token = await storage.get("token"); const token = await storage.get("token");
@ -55,7 +69,7 @@ const HomePage = () => {
} }
try { try {
const response = await fetch(await buildBackendUrl("/api/v1/searchspaces"), { const response = await fetch(await buildBackendUrl("/api/v1/workspaces"), {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
} }
@ -66,7 +80,7 @@ const HomePage = () => {
} else { } else {
const res = await response.json(); const res = await response.json();
console.log(res); console.log(res);
setSearchSpaces(res); setWorkspaces(res);
} }
} catch (error) { } catch (error) {
await storage.remove("token"); await storage.remove("token");
@ -77,7 +91,7 @@ const HomePage = () => {
} }
}; };
checkSearchSpaces(); checkWorkspaces();
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -98,10 +112,11 @@ const HomePage = () => {
}); });
const storage = new Storage({ area: "local" }); const storage = new Storage({ area: "local" });
const searchspace = await storage.get("search_space"); await migrateLegacyWorkspaceKeys(storage);
const workspace = await storage.get("workspace");
if (searchspace) { if (workspace) {
setValue(searchspace); setValue(workspace);
} }
await storage.set("showShadowDom", true); await storage.set("showShadowDom", true);
@ -262,17 +277,17 @@ const HomePage = () => {
const saveDatamessage = async () => { const saveDatamessage = async () => {
if (value === "") { if (value === "") {
toast({ toast({
title: "Select a SearchSpace !", title: "Select a Workspace !",
}); });
return; return;
} }
const storage = new Storage({ area: "local" }); const storage = new Storage({ area: "local" });
const search_space_id = await storage.get("search_space_id"); const workspace_id = await storage.get("workspace_id");
if (!search_space_id) { if (!workspace_id) {
toast({ toast({
title: "Invalid SearchSpace selected!", title: "Invalid Workspace selected!",
variant: "destructive", variant: "destructive",
}); });
return; return;
@ -319,15 +334,15 @@ const HomePage = () => {
const storage = new Storage({ area: "local" }); const storage = new Storage({ area: "local" });
await storage.remove("token"); await storage.remove("token");
await storage.remove("showShadowDom"); await storage.remove("showShadowDom");
await storage.remove("search_space"); await storage.remove("workspace");
await storage.remove("search_space_id"); await storage.remove("workspace_id");
navigation("/login"); navigation("/login");
} }
if (loading) { if (loading) {
return <Loading />; return <Loading />;
} else { } else {
return searchspaces.length === 0 ? ( return workspaces.length === 0 ? (
<div className="flex min-h-screen flex-col bg-gradient-to-br from-gray-900 to-gray-800"> <div className="flex min-h-screen flex-col bg-gradient-to-br from-gray-900 to-gray-800">
<div className="flex flex-1 items-center justify-center p-4"> <div className="flex flex-1 items-center justify-center p-4">
<div className="w-full max-w-md space-y-8"> <div className="w-full max-w-md space-y-8">
@ -337,7 +352,7 @@ const HomePage = () => {
</div> </div>
<h1 className="mt-4 text-3xl font-semibold tracking-tight text-white">SurfSense</h1> <h1 className="mt-4 text-3xl font-semibold tracking-tight text-white">SurfSense</h1>
<div className="mt-4 rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-4 text-yellow-300"> <div className="mt-4 rounded-lg border border-yellow-500/20 bg-yellow-500/10 p-4 text-yellow-300">
<p className="text-sm">Please create a Search Space to continue</p> <p className="text-sm">Please create a Workspace to continue</p>
</div> </div>
</div> </div>
@ -390,7 +405,7 @@ const HomePage = () => {
</div> </div>
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-4 backdrop-blur-sm"> <div className="rounded-lg border border-gray-700 bg-gray-800/50 p-4 backdrop-blur-sm">
<Label className="mb-2 block text-sm font-medium text-gray-300">Search Space</Label> <Label className="mb-2 block text-sm font-medium text-gray-300">Workspace</Label>
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -399,8 +414,8 @@ const HomePage = () => {
className="w-full justify-between border-gray-700 bg-gray-900 text-white hover:bg-gray-700" className="w-full justify-between border-gray-700 bg-gray-900 text-white hover:bg-gray-700"
> >
{value {value
? searchspaces.find((space) => space.name === value)?.name ? workspaces.find((space) => space.name === value)?.name
: "Select Search Space..."} : "Select Workspace..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@ -411,23 +426,23 @@ const HomePage = () => {
className="border-gray-700 bg-gray-900 text-gray-200" className="border-gray-700 bg-gray-900 text-gray-200"
/> />
<CommandList> <CommandList>
<CommandEmpty>No search spaces found.</CommandEmpty> <CommandEmpty>No workspaces found.</CommandEmpty>
<CommandGroup> <CommandGroup>
{searchspaces.map((space) => ( {workspaces.map((space) => (
<CommandItem <CommandItem
key={space.name} key={space.name}
value={space.name} value={space.name}
onSelect={async (currentValue) => { onSelect={async (currentValue) => {
const storage = new Storage({ area: "local" }); const storage = new Storage({ area: "local" });
if (currentValue === value) { if (currentValue === value) {
await storage.set("search_space", ""); await storage.set("workspace", "");
await storage.set("search_space_id", 0); await storage.set("workspace_id", 0);
} else { } else {
const selectedSpace = searchspaces.find( const selectedSpace = workspaces.find(
(space) => space.name === currentValue (space) => space.name === currentValue
); );
await storage.set("search_space", currentValue); await storage.set("workspace", currentValue);
await storage.set("search_space_id", selectedSpace.id); await storage.set("workspace_id", selectedSpace.id);
} }
setValue(currentValue === value ? "" : currentValue); setValue(currentValue === value ? "" : currentValue);
setOpen(false); setOpen(false);

File diff suppressed because one or more lines are too long

View file

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

View file

@ -27,7 +27,7 @@ import {
} from '../modules/folder-watcher'; } from '../modules/folder-watcher';
import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts'; import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts';
import { getAutoLaunchState, setAutoLaunch } from '../modules/auto-launch'; 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 { reregisterQuickAsk } from '../modules/quick-ask';
import { reregisterGeneralAssist, reregisterScreenshotAssist } from '../modules/tray'; import { reregisterGeneralAssist, reregisterScreenshotAssist } from '../modules/tray';
import { import {
@ -205,9 +205,9 @@ export function registerIpcHandlers(): void {
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT, IPC_CHANNELS.READ_AGENT_LOCAL_FILE_TEXT,
async (_event, virtualPath: string, searchSpaceId?: number | null) => { async (_event, virtualPath: string, workspaceId?: number | null) => {
try { try {
const result = await readAgentLocalFileText(virtualPath, searchSpaceId); const result = await readAgentLocalFileText(virtualPath, workspaceId);
return { ok: true, path: result.path, content: result.content }; return { ok: true, path: result.path, content: result.content };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to read local file'; const message = error instanceof Error ? error.message : 'Failed to read local file';
@ -218,9 +218,9 @@ export function registerIpcHandlers(): void {
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.WRITE_AGENT_LOCAL_FILE_TEXT, 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 { try {
const result = await writeAgentLocalFileText(virtualPath, content, searchSpaceId); const result = await writeAgentLocalFileText(virtualPath, content, workspaceId);
return { ok: true, path: result.path }; return { ok: true, path: result.path };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Failed to write local file'; 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) => ipcMain.handle(IPC_CHANNELS.SET_ACTIVE_WORKSPACE, (_event, id: string) =>
setActiveSearchSpaceId(id) setActiveWorkspaceId(id)
); );
ipcMain.handle(IPC_CHANNELS.SET_SHORTCUTS, async (_event, config: Partial<ShortcutConfig>) => { 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) => ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_SETTINGS, (_event, workspaceId?: number | null) =>
getAgentFilesystemSettings(searchSpaceId) getAgentFilesystemSettings(workspaceId)
); );
ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, searchSpaceId?: number | null) => ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_GET_MOUNTS, (_event, workspaceId?: number | null) =>
getAgentFilesystemMounts(searchSpaceId) getAgentFilesystemMounts(workspaceId)
); );
ipcMain.handle( ipcMain.handle(
@ -384,7 +384,7 @@ export function registerIpcHandlers(): void {
_event, _event,
options: { options: {
rootPath: string; rootPath: string;
searchSpaceId?: number | null; workspaceId?: number | null;
excludePatterns?: string[] | null; excludePatterns?: string[] | null;
fileExtensions?: string[] | null; fileExtensions?: string[] | null;
} }
@ -397,10 +397,10 @@ export function registerIpcHandlers(): void {
( (
_event, _event,
payload: { payload: {
searchSpaceId?: number | null; workspaceId?: number | null;
settings: { mode?: 'cloud' | 'desktop_local_folder'; localRootPaths?: string[] | 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, () => ipcMain.handle(IPC_CHANNELS.AGENT_FILESYSTEM_PICK_ROOT, () =>
@ -415,7 +415,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP, IPC_CHANNELS.AGENT_FILESYSTEM_TREE_WATCH_STOP,
(_event, searchSpaceId?: number | null) => (_event, workspaceId?: number | null) =>
stopAgentFilesystemTreeWatch(searchSpaceId) 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; const EVENT_DEBOUNCE_MS = 700;
export type AgentFilesystemTreeWatchOptions = { export type AgentFilesystemTreeWatchOptions = {
searchSpaceId?: number | null; workspaceId?: number | null;
rootPaths: string[]; rootPaths: string[];
excludePatterns?: string[] | null; excludePatterns?: string[] | null;
fileExtensions?: string[] | null; fileExtensions?: string[] | null;
@ -17,7 +17,7 @@ export type AgentFilesystemTreeWatchOptions = {
type TreeDirtyReason = 'watcher_event' | 'safety_poll'; type TreeDirtyReason = 'watcher_event' | 'safety_poll';
type TreeDirtyEvent = { type TreeDirtyEvent = {
searchSpaceId: number | null; workspaceId: number | null;
reason: TreeDirtyReason; reason: TreeDirtyReason;
rootPath: string; rootPath: string;
changedPath: string | null; changedPath: string | null;
@ -25,7 +25,7 @@ type TreeDirtyEvent = {
}; };
type WatchSession = { type WatchSession = {
searchSpaceId: number | null; workspaceId: number | null;
optionsSignature: string; optionsSignature: string;
rootPaths: string[]; rootPaths: string[];
excludePatterns: string[]; excludePatterns: string[];
@ -40,15 +40,15 @@ type WatchSession = {
const sessions = new Map<string, WatchSession>(); const sessions = new Map<string, WatchSession>();
function normalizeSearchSpaceId(searchSpaceId?: number | null): number | null { function normalizeWorkspaceId(workspaceId?: number | null): number | null {
if (typeof searchSpaceId === 'number' && Number.isFinite(searchSpaceId) && searchSpaceId > 0) { if (typeof workspaceId === 'number' && Number.isFinite(workspaceId) && workspaceId > 0) {
return searchSpaceId; return workspaceId;
} }
return null; return null;
} }
function getSessionKey(searchSpaceId?: number | null): string { function getSessionKey(workspaceId?: number | null): string {
const normalized = normalizeSearchSpaceId(searchSpaceId); const normalized = normalizeWorkspaceId(workspaceId);
return normalized === null ? 'default' : String(normalized); return normalized === null ? 'default' : String(normalized);
} }
@ -71,13 +71,13 @@ function normalizeExtensions(value: string[] | null | undefined): string[] | nul
} }
function buildOptionsSignature( function buildOptionsSignature(
searchSpaceId: number | null, workspaceId: number | null,
rootPaths: string[], rootPaths: string[],
excludePatterns: string[], excludePatterns: string[],
fileExtensions: string[] | null fileExtensions: string[] | null
): string { ): string {
return JSON.stringify({ return JSON.stringify({
searchSpaceId, workspaceId,
rootPaths: [...rootPaths].sort(), rootPaths: [...rootPaths].sort(),
excludePatterns: [...excludePatterns].sort(), excludePatterns: [...excludePatterns].sort(),
fileExtensions: fileExtensions ? [...fileExtensions].sort() : null, fileExtensions: fileExtensions ? [...fileExtensions].sort() : null,
@ -99,10 +99,10 @@ async function buildRootSnapshotSignature(
rootPath: string rootPath: string
): Promise<string> { ): Promise<string> {
let hash = 2166136261; 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({ const files = await listAgentFilesystemFiles({
rootPath, rootPath,
searchSpaceId: session.searchSpaceId, workspaceId: session.workspaceId,
excludePatterns: session.excludePatterns, excludePatterns: session.excludePatterns,
fileExtensions: session.fileExtensions, fileExtensions: session.fileExtensions,
}); });
@ -118,13 +118,13 @@ async function buildRootSnapshotSignature(
} }
function sendTreeDirtyEvent( function sendTreeDirtyEvent(
searchSpaceId: number | null, workspaceId: number | null,
reason: TreeDirtyReason, reason: TreeDirtyReason,
rootPath: string, rootPath: string,
changedPath: string | null changedPath: string | null
): void { ): void {
const payload: TreeDirtyEvent = { const payload: TreeDirtyEvent = {
searchSpaceId, workspaceId,
reason, reason,
rootPath, rootPath,
changedPath, changedPath,
@ -158,7 +158,7 @@ function scheduleDirtyEmit(
session.pendingDirtyByRoot.clear(); session.pendingDirtyByRoot.clear();
for (const [pendingRootPath, payload] of pending) { for (const [pendingRootPath, payload] of pending) {
sendTreeDirtyEvent( sendTreeDirtyEvent(
session.searchSpaceId, session.workspaceId,
payload.reason, payload.reason,
pendingRootPath, pendingRootPath,
payload.changedPath payload.changedPath
@ -183,21 +183,21 @@ async function closeSession(session: WatchSession): Promise<void> {
export async function startAgentFilesystemTreeWatch( export async function startAgentFilesystemTreeWatch(
options: AgentFilesystemTreeWatchOptions options: AgentFilesystemTreeWatchOptions
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
const searchSpaceId = normalizeSearchSpaceId(options.searchSpaceId); const workspaceId = normalizeWorkspaceId(options.workspaceId);
const rootPaths = Array.from( const rootPaths = Array.from(
new Set(normalizeList(options.rootPaths).map((rootPath) => normalizeRootPath(rootPath))) new Set(normalizeList(options.rootPaths).map((rootPath) => normalizeRootPath(rootPath)))
); );
const excludePatterns = Array.from(new Set(normalizeList(options.excludePatterns))); const excludePatterns = Array.from(new Set(normalizeList(options.excludePatterns)));
const fileExtensions = normalizeExtensions(options.fileExtensions); const fileExtensions = normalizeExtensions(options.fileExtensions);
const sessionKey = getSessionKey(searchSpaceId); const sessionKey = getSessionKey(workspaceId);
if (rootPaths.length === 0) { if (rootPaths.length === 0) {
await stopAgentFilesystemTreeWatch(searchSpaceId); await stopAgentFilesystemTreeWatch(workspaceId);
return { ok: true }; return { ok: true };
} }
const optionsSignature = buildOptionsSignature( const optionsSignature = buildOptionsSignature(
searchSpaceId, workspaceId,
rootPaths, rootPaths,
excludePatterns, excludePatterns,
fileExtensions fileExtensions
@ -228,7 +228,7 @@ export async function startAgentFilesystemTreeWatch(
); );
const session: WatchSession = { const session: WatchSession = {
searchSpaceId, workspaceId,
optionsSignature, optionsSignature,
rootPaths, rootPaths,
excludePatterns, excludePatterns,
@ -291,9 +291,9 @@ export async function startAgentFilesystemTreeWatch(
} }
export async function stopAgentFilesystemTreeWatch( export async function stopAgentFilesystemTreeWatch(
searchSpaceId?: number | null workspaceId?: number | null
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
const sessionKey = getSessionKey(searchSpaceId); const sessionKey = getSessionKey(workspaceId);
const session = sessions.get(sessionKey); const session = sessions.get(sessionKey);
if (!session) return { ok: true }; if (!session) return { ok: true };
sessions.delete(sessionKey); sessions.delete(sessionKey);

View file

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

View file

@ -5,6 +5,7 @@ import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import { IPC_CHANNELS } from '../ipc/channels'; import { IPC_CHANNELS } from '../ipc/channels';
import { trackEvent } from './analytics'; import { trackEvent } from './analytics';
import { migrateWatchedFolderConfigs } from './migrate-watched-folders';
export interface WatchedFolderConfig { export interface WatchedFolderConfig {
path: string; path: string;
@ -12,7 +13,7 @@ export interface WatchedFolderConfig {
excludePatterns: string[]; excludePatterns: string[];
fileExtensions: string[] | null; fileExtensions: string[] | null;
rootFolderId: number | null; rootFolderId: number | null;
searchSpaceId: number; workspaceId: number;
active: boolean; active: boolean;
} }
@ -27,7 +28,7 @@ type FolderSyncAction = 'add' | 'change' | 'unlink';
export interface FolderSyncFileChangedEvent { export interface FolderSyncFileChangedEvent {
id: string; id: string;
rootFolderId: number | null; rootFolderId: number | null;
searchSpaceId: number; workspaceId: number;
folderPath: string; folderPath: string;
folderName: string; folderName: string;
relativePath: string; relativePath: string;
@ -68,6 +69,12 @@ async function getStore() {
[STORE_KEY]: [] as WatchedFolderConfig[], [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; return store;
} }
@ -267,7 +274,7 @@ async function startWatcher(config: WatchedFolderConfig) {
if (storedMtime === undefined) { if (storedMtime === undefined) {
sendFileChangedEvent({ sendFileChangedEvent({
rootFolderId: config.rootFolderId, rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId, workspaceId: config.workspaceId,
folderPath: config.path, folderPath: config.path,
folderName: config.name, folderName: config.name,
relativePath: rel, relativePath: rel,
@ -278,7 +285,7 @@ async function startWatcher(config: WatchedFolderConfig) {
} else if (Math.abs(currentMtime - storedMtime) >= MTIME_TOLERANCE_S * 1000) { } else if (Math.abs(currentMtime - storedMtime) >= MTIME_TOLERANCE_S * 1000) {
sendFileChangedEvent({ sendFileChangedEvent({
rootFolderId: config.rootFolderId, rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId, workspaceId: config.workspaceId,
folderPath: config.path, folderPath: config.path,
folderName: config.name, folderName: config.name,
relativePath: rel, relativePath: rel,
@ -295,7 +302,7 @@ async function startWatcher(config: WatchedFolderConfig) {
if (!(rel in currentMap)) { if (!(rel in currentMap)) {
sendFileChangedEvent({ sendFileChangedEvent({
rootFolderId: config.rootFolderId, rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId, workspaceId: config.workspaceId,
folderPath: config.path, folderPath: config.path,
folderName: config.name, folderName: config.name,
relativePath: rel, relativePath: rel,
@ -346,7 +353,7 @@ async function startWatcher(config: WatchedFolderConfig) {
sendFileChangedEvent({ sendFileChangedEvent({
rootFolderId: config.rootFolderId, rootFolderId: config.rootFolderId,
searchSpaceId: config.searchSpaceId, workspaceId: config.workspaceId,
folderPath: config.path, folderPath: config.path,
folderName: config.name, folderName: config.name,
relativePath, relativePath,
@ -403,7 +410,7 @@ export async function addWatchedFolder(
} }
trackEvent('desktop_folder_watch_added', { trackEvent('desktop_folder_watch_added', {
search_space_id: config.searchSpaceId, workspace_id: config.workspaceId,
root_folder_id: config.rootFolderId, root_folder_id: config.rootFolderId,
active: config.active, active: config.active,
has_exclude_patterns: (config.excludePatterns?.length ?? 0) > 0, has_exclude_patterns: (config.excludePatterns?.length ?? 0) > 0,
@ -431,7 +438,7 @@ export async function removeWatchedFolder(
if (removed) { if (removed) {
trackEvent('desktop_folder_watch_removed', { trackEvent('desktop_folder_watch_removed', {
search_space_id: removed.searchSpaceId, workspace_id: removed.workspaceId,
root_folder_id: removed.rootFolderId, 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 { checkAccessibilityPermission, getFrontmostApp, simulateCopy, simulatePaste } from './platform';
import { getServerOrigin } from './server'; import { getServerOrigin } from './server';
import { getShortcuts } from './shortcuts'; import { getShortcuts } from './shortcuts';
import { getActiveSearchSpaceId } from './active-search-space'; import { getActiveWorkspaceId } from './active-workspace';
import { trackEvent } from './analytics'; import { trackEvent } from './analytics';
let currentShortcut = ''; let currentShortcut = '';
let quickAskWindow: BrowserWindow | null = null; let quickAskWindow: BrowserWindow | null = null;
let pendingText = ''; let pendingText = '';
let pendingMode = ''; let pendingMode = '';
let pendingSearchSpaceId: string | null = null; let pendingWorkspaceId: string | null = null;
let sourceApp = ''; let sourceApp = '';
let savedClipboard = ''; let savedClipboard = '';
@ -57,7 +57,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
skipTaskbar: true, skipTaskbar: true,
}); });
const spaceId = pendingSearchSpaceId; const spaceId = pendingWorkspaceId;
const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard'; const route = spaceId ? `/dashboard/${spaceId}/new-chat` : '/dashboard';
quickAskWindow.loadURL(`${getServerOrigin()}${route}?quickAssist=true`); quickAskWindow.loadURL(`${getServerOrigin()}${route}?quickAssist=true`);
@ -87,7 +87,7 @@ function createQuickAskWindow(x: number, y: number): BrowserWindow {
async function openQuickAsk(text: string): Promise<void> { async function openQuickAsk(text: string): Promise<void> {
pendingText = text; pendingText = text;
pendingMode = 'quick-assist'; pendingMode = 'quick-assist';
pendingSearchSpaceId = await getActiveSearchSpaceId(); pendingWorkspaceId = await getActiveWorkspaceId();
const cursor = screen.getCursorScreenPoint(); const cursor = screen.getCursorScreenPoint();
const pos = clampToScreen(cursor.x, cursor.y, 450, 750); const pos = clampToScreen(cursor.x, cursor.y, 450, 750);
createQuickAskWindow(pos.x, pos.y); createQuickAskWindow(pos.x, pos.y);

View file

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

View file

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

View file

@ -12,5 +12,5 @@
"noEmit": true "noEmit": true
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "scripts"] "exclude": ["node_modules", "dist", "scripts", "src/**/*.test.ts"]
} }

View file

@ -52,7 +52,7 @@ Open **Settings → SurfSense** in Obsidian and fill in:
| --- | --- | | --- | --- |
| Server URL | `https://surfsense.com` for SurfSense Cloud, or your self-hosted URL | | Server URL | `https://surfsense.com` for SurfSense Cloud, or your self-hosted URL |
| API token | Create a personal access token from the *Connectors → Obsidian* dialog or *User settings → API access* in the SurfSense web app | | API token | Create a personal access token from the *Connectors → Obsidian* dialog or *User settings → API access* in the SurfSense web app |
| Search space | Pick the search space this vault should sync into | | Search space | Pick the workspace this vault should sync into |
| Vault name | Defaults to your Obsidian vault name; rename if you have multiple vaults | | Vault name | Defaults to your Obsidian vault name; rename if you have multiple vaults |
| Sync mode | *Auto* (recommended) or *Manual* | | Sync mode | *Auto* (recommended) or *Manual* |
| Exclude patterns | Glob patterns of folders/files to skip (e.g. `.trash`, `_attachments`, `templates/**`) | | Exclude patterns | Glob patterns of folders/files to skip (e.g. `.trash`, `_attachments`, `templates/**`) |
@ -82,7 +82,7 @@ addendum.
- `vault_id`: a random UUID minted in the plugin's `data.json` on first run - `vault_id`: a random UUID minted in the plugin's `data.json` on first run
- `vault_name`: the Obsidian vault folder name - `vault_name`: the Obsidian vault folder name
- `search_space_id`: the SurfSense search space you picked - `workspace_id`: the SurfSense workspace you picked
**Sent per note on `/sync`, `/rename`, `/delete`:** **Sent per note on `/sync`, `/rename`, `/delete`:**

3153
surfsense_obsidian/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
allowBuilds:
esbuild: set this to true or false

View file

@ -7,7 +7,7 @@ import type {
NotePayload, NotePayload,
RenameAck, RenameAck,
RenameItem, RenameItem,
SearchSpace, Workspace,
SyncAck, SyncAck,
} from "./types"; } from "./types";
@ -94,14 +94,14 @@ export class SurfSenseApiClient {
return await this.request<HealthResponse>("GET", "/api/v1/obsidian/health"); return await this.request<HealthResponse>("GET", "/api/v1/obsidian/health");
} }
async listSearchSpaces(): Promise<SearchSpace[]> { async listWorkspaces(): Promise<Workspace[]> {
const resp = await this.request<SearchSpace[] | { items: SearchSpace[] }>( const resp = await this.request<Workspace[] | { items: Workspace[] }>(
"GET", "GET",
"/api/v1/searchspaces/" "/api/v1/workspaces/"
); );
if (Array.isArray(resp)) return resp; if (Array.isArray(resp)) return resp;
if (resp && Array.isArray((resp as { items?: SearchSpace[] }).items)) { if (resp && Array.isArray((resp as { items?: Workspace[] }).items)) {
return (resp as { items: SearchSpace[] }).items; return (resp as { items: Workspace[] }).items;
} }
return []; return [];
} }
@ -114,7 +114,7 @@ export class SurfSenseApiClient {
} }
async connect(input: { async connect(input: {
searchSpaceId: number; workspaceId: number;
vaultId: string; vaultId: string;
vaultName: string; vaultName: string;
vaultFingerprint: string; vaultFingerprint: string;
@ -125,7 +125,7 @@ export class SurfSenseApiClient {
{ {
vault_id: input.vaultId, vault_id: input.vaultId,
vault_name: input.vaultName, vault_name: input.vaultName,
search_space_id: input.searchSpaceId, workspace_id: input.workspaceId,
vault_fingerprint: input.vaultFingerprint, vault_fingerprint: input.vaultFingerprint,
} }
); );

View file

@ -119,7 +119,7 @@ export default class SurfSensePlugin extends Plugin {
name: "Sync current note", name: "Sync current note",
checkCallback: (checking) => { checkCallback: (checking) => {
const file = this.app.workspace.getActiveFile(); const file = this.app.workspace.getActiveFile();
if (!file || file.extension.toLowerCase() !== "md") return false; if (file?.extension.toLowerCase() !== "md") return false;
if (checking) return true; if (checking) return true;
this.queue.enqueueUpsert(file.path); this.queue.enqueueUpsert(file.path);
void this.engine.flushQueue(); void this.engine.flushQueue();
@ -186,7 +186,7 @@ export default class SurfSensePlugin extends Plugin {
if (!changed) return; if (!changed) return;
this.engine?.refreshStatus(); this.engine?.refreshStatus();
this.notifyStatusChange(); this.notifyStatusChange();
if (this.settings.searchSpaceId !== null) { if (this.settings.workspaceId !== null) {
void this.engine.ensureConnected(); void this.engine.ensureConnected();
} }
} }
@ -252,16 +252,24 @@ export default class SurfSensePlugin extends Plugin {
} }
async loadSettings() { async loadSettings() {
const data = (await this.loadData()) as Partial<SurfsensePluginSettings> | null; // One-time migration: the workspace was previously persisted as `searchSpaceId`.
// Destructure it out so the migrated value moves into `workspaceId` and the
// dead key is not spread back in / re-persisted.
const data = (await this.loadData()) as
| (Partial<SurfsensePluginSettings> & { searchSpaceId?: number | null })
| null;
const { searchSpaceId: legacyWorkspaceId, ...persisted } = data ?? {};
this.settings = { this.settings = {
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
...(data ?? {}), ...persisted,
queue: (data?.queue ?? []).map((i: QueueItem) => ({ ...i })), workspaceId:
tombstones: { ...(data?.tombstones ?? {}) }, persisted.workspaceId ?? legacyWorkspaceId ?? DEFAULT_SETTINGS.workspaceId,
includeFolders: [...(data?.includeFolders ?? [])], queue: (persisted.queue ?? []).map((i: QueueItem) => ({ ...i })),
excludeFolders: [...(data?.excludeFolders ?? [])], tombstones: { ...(persisted.tombstones ?? {}) },
excludePatterns: data?.excludePatterns?.length includeFolders: [...(persisted.includeFolders ?? [])],
? [...data.excludePatterns] excludeFolders: [...(persisted.excludeFolders ?? [])],
excludePatterns: persisted.excludePatterns?.length
? [...persisted.excludePatterns]
: [...DEFAULT_SETTINGS.excludePatterns], : [...DEFAULT_SETTINGS.excludePatterns],
}; };
} }

View file

@ -13,13 +13,13 @@ import { normalizeFolder, parseExcludePatterns } from "./excludes";
import { FolderSuggestModal } from "./folder-suggest-modal"; import { FolderSuggestModal } from "./folder-suggest-modal";
import type SurfSensePlugin from "./main"; import type SurfSensePlugin from "./main";
import { STATUS_VISUALS } from "./status-visuals"; import { STATUS_VISUALS } from "./status-visuals";
import type { SearchSpace } from "./types"; import type { Workspace } from "./types";
/** Plugin settings tab. */ /** Plugin settings tab. */
export class SurfSenseSettingTab extends PluginSettingTab { export class SurfSenseSettingTab extends PluginSettingTab {
private readonly plugin: SurfSensePlugin; private readonly plugin: SurfSensePlugin;
private searchSpaces: SearchSpace[] = []; private workspaces: Workspace[] = [];
private loadingSpaces = false; private loadingSpaces = false;
private connectionIndicator: HTMLElement | null = null; private connectionIndicator: HTMLElement | null = null;
private readonly onStatusChange = (): void => this.updateConnectionIndicator(); private readonly onStatusChange = (): void => this.updateConnectionIndicator();
@ -51,7 +51,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
const next = value.trim(); const next = value.trim();
const previous = this.plugin.settings.serverUrl; const previous = this.plugin.settings.serverUrl;
if (previous !== "" && next !== previous) { if (previous !== "" && next !== previous) {
this.plugin.settings.searchSpaceId = null; this.plugin.settings.workspaceId = null;
this.plugin.settings.connectorId = null; this.plugin.settings.connectorId = null;
} }
this.plugin.settings.serverUrl = next; this.plugin.settings.serverUrl = next;
@ -80,7 +80,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
const next = value.trim(); const next = value.trim();
const previous = this.plugin.settings.apiToken; const previous = this.plugin.settings.apiToken;
if (previous !== "" && next !== previous) { if (previous !== "" && next !== previous) {
this.plugin.settings.searchSpaceId = null; this.plugin.settings.workspaceId = null;
this.plugin.settings.connectorId = null; this.plugin.settings.connectorId = null;
} }
this.plugin.settings.apiToken = next; this.plugin.settings.apiToken = next;
@ -102,7 +102,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
await this.plugin.api.verifyToken(); await this.plugin.api.verifyToken();
new Notice("Surfsense: token verified."); new Notice("Surfsense: token verified.");
this.plugin.engine.refreshStatus({ force: true }); this.plugin.engine.refreshStatus({ force: true });
await this.refreshSearchSpaces(); await this.refreshWorkspaces();
this.display(); this.display();
} catch (err) { } catch (err) {
this.handleApiError(err); this.handleApiError(err);
@ -115,21 +115,21 @@ export class SurfSenseSettingTab extends PluginSettingTab {
new Setting(containerEl) new Setting(containerEl)
.setName("Search space") .setName("Search space")
.setDesc( .setDesc(
"Which Surfsense search space this vault syncs into. Reload after changing your token.", "Which Surfsense workspace this vault syncs into. Reload after changing your token.",
) )
.addDropdown((drop) => { .addDropdown((drop) => {
drop.addOption("", this.loadingSpaces ? "Loading…" : "Select a search space"); drop.addOption("", this.loadingSpaces ? "Loading…" : "Select a workspace");
for (const space of this.searchSpaces) { for (const space of this.workspaces) {
drop.addOption(String(space.id), space.name); drop.addOption(String(space.id), space.name);
} }
if (settings.searchSpaceId !== null) { if (settings.workspaceId !== null) {
drop.setValue(String(settings.searchSpaceId)); drop.setValue(String(settings.workspaceId));
} }
drop.onChange(async (value) => { drop.onChange(async (value) => {
this.plugin.settings.searchSpaceId = value ? Number(value) : null; this.plugin.settings.workspaceId = value ? Number(value) : null;
this.plugin.settings.connectorId = null; this.plugin.settings.connectorId = null;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
if (this.plugin.settings.searchSpaceId !== null) { if (this.plugin.settings.workspaceId !== null) {
try { try {
await this.plugin.engine.ensureConnected(); await this.plugin.engine.ensureConnected();
await this.plugin.engine.maybeReconcile(true); await this.plugin.engine.maybeReconcile(true);
@ -144,9 +144,9 @@ export class SurfSenseSettingTab extends PluginSettingTab {
.addExtraButton((btn) => .addExtraButton((btn) =>
btn btn
.setIcon("refresh-ccw") .setIcon("refresh-ccw")
.setTooltip("Reload search spaces") .setTooltip("Reload workspaces")
.onClick(async () => { .onClick(async () => {
await this.refreshSearchSpaces(); await this.refreshWorkspaces();
this.display(); this.display();
}), }),
); );
@ -319,13 +319,13 @@ export class SurfSenseSettingTab extends PluginSettingTab {
indicator.setAttr("title", visual.label); indicator.setAttr("title", visual.label);
} }
private async refreshSearchSpaces(): Promise<void> { private async refreshWorkspaces(): Promise<void> {
this.loadingSpaces = true; this.loadingSpaces = true;
try { try {
this.searchSpaces = await this.plugin.api.listSearchSpaces(); this.workspaces = await this.plugin.api.listWorkspaces();
} catch (err) { } catch (err) {
this.handleApiError(err); this.handleApiError(err);
this.searchSpaces = []; this.workspaces = [];
} finally { } finally {
this.loadingSpaces = false; this.loadingSpaces = false;
} }

View file

@ -48,7 +48,7 @@ export interface SyncEngineSettings {
vaultId: string; vaultId: string;
apiToken: string; apiToken: string;
connectorId: number | null; connectorId: number | null;
searchSpaceId: number | null; workspaceId: number | null;
includeFolders: string[]; includeFolders: string[];
excludeFolders: string[]; excludeFolders: string[];
excludePatterns: string[]; excludePatterns: string[];
@ -96,7 +96,7 @@ export class SyncEngine {
this.setStatus("syncing", "Connecting to SurfSense…"); this.setStatus("syncing", "Connecting to SurfSense…");
const settings = this.deps.getSettings(); const settings = this.deps.getSettings();
if (!settings.searchSpaceId) { if (!settings.workspaceId) {
// No target yet — /health still surfaces auth/network errors. // No target yet — /health still surfaces auth/network errors.
try { try {
const health = await this.deps.apiClient.health(); const health = await this.deps.apiClient.health();
@ -124,7 +124,7 @@ export class SyncEngine {
*/ */
async ensureConnected(): Promise<boolean> { async ensureConnected(): Promise<boolean> {
const settings = this.deps.getSettings(); const settings = this.deps.getSettings();
if (!settings.searchSpaceId) { if (!settings.workspaceId) {
this.setStatus("idle"); this.setStatus("idle");
return false; return false;
} }
@ -132,7 +132,7 @@ export class SyncEngine {
try { try {
const fingerprint = await computeVaultFingerprint(this.deps.app); const fingerprint = await computeVaultFingerprint(this.deps.app);
const resp = await this.deps.apiClient.connect({ const resp = await this.deps.apiClient.connect({
searchSpaceId: settings.searchSpaceId, workspaceId: settings.workspaceId,
vaultId: settings.vaultId, vaultId: settings.vaultId,
vaultName: this.deps.app.vault.getName(), vaultName: this.deps.app.vault.getName(),
vaultFingerprint: fingerprint, vaultFingerprint: fingerprint,
@ -272,7 +272,7 @@ export class SyncEngine {
this.refreshStatus({ force: true }); this.refreshStatus({ force: true });
return; return;
} }
if (!settings.searchSpaceId) { if (!settings.workspaceId) {
try { try {
const health = await this.deps.apiClient.health(); const health = await this.deps.apiClient.health();
this.applyHealth(health); this.applyHealth(health);
@ -543,7 +543,7 @@ export class SyncEngine {
const isError = const isError =
last === "auth-error" || last === "offline" || last === "error"; last === "auth-error" || last === "offline" || last === "error";
const s = this.deps.getSettings(); const s = this.deps.getSettings();
const setupComplete = !!(s.apiToken && s.searchSpaceId && s.connectorId); const setupComplete = !!(s.apiToken && s.workspaceId && s.connectorId);
if (isError && setupComplete) return; if (isError && setupComplete) return;
} }
this.setStatus(this.queueStatusKind(), this.statusDetail()); this.setStatus(this.queueStatusKind(), this.statusDetail());
@ -571,7 +571,7 @@ export class SyncEngine {
kind = "needs-setup"; kind = "needs-setup";
detail = this.setupHint(s); detail = this.setupHint(s);
} else if (kind !== "auth-error" && kind !== "offline" && kind !== "error") { } else if (kind !== "auth-error" && kind !== "offline" && kind !== "error") {
if (!s.searchSpaceId || !s.connectorId) { if (!s.workspaceId || !s.connectorId) {
kind = "needs-setup"; kind = "needs-setup";
detail = this.setupHint(s); detail = this.setupHint(s);
} }
@ -582,7 +582,7 @@ export class SyncEngine {
private setupHint(s: SyncEngineSettings): string { private setupHint(s: SyncEngineSettings): string {
if (!s.apiToken) return "Paste your API token in settings."; if (!s.apiToken) return "Paste your API token in settings.";
if (!s.searchSpaceId) return "Pick a search space in settings."; if (!s.workspaceId) return "Pick a workspace in settings.";
return "Connecting…"; return "Connecting…";
} }

View file

@ -3,7 +3,7 @@
export interface SurfsensePluginSettings { export interface SurfsensePluginSettings {
serverUrl: string; serverUrl: string;
apiToken: string; apiToken: string;
searchSpaceId: number | null; workspaceId: number | null;
connectorId: number | null; connectorId: number | null;
/** UUID for the vault — lives here so Obsidian Sync replicates it across devices. */ /** UUID for the vault — lives here so Obsidian Sync replicates it across devices. */
vaultId: string; vaultId: string;
@ -25,7 +25,7 @@ export interface SurfsensePluginSettings {
export const DEFAULT_SETTINGS: SurfsensePluginSettings = { export const DEFAULT_SETTINGS: SurfsensePluginSettings = {
serverUrl: "https://surfsense.com", serverUrl: "https://surfsense.com",
apiToken: "", apiToken: "",
searchSpaceId: null, workspaceId: null,
connectorId: null, connectorId: null,
vaultId: "", vaultId: "",
syncIntervalMinutes: 10, syncIntervalMinutes: 10,
@ -107,7 +107,7 @@ export interface HeadingRef {
level: number; level: number;
} }
export interface SearchSpace { export interface Workspace {
id: number; id: number;
name: string; name: string;
description?: string; description?: string;
@ -117,7 +117,7 @@ export interface SearchSpace {
export interface ConnectResponse { export interface ConnectResponse {
connector_id: number; connector_id: number;
vault_id: string; vault_id: string;
search_space_id: number; workspace_id: number;
capabilities: string[]; capabilities: string[];
server_time_utc: string; server_time_utc: string;
[key: string]: unknown; [key: string]: unknown;

View file

@ -45,7 +45,7 @@ export function AutomationDetailContent({
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden /> <ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2> <h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto"> <p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
You don't have permission to view automations in this search space. You don't have permission to view automations in this workspace.
</p> </p>
</div> </div>
); );

View file

@ -32,7 +32,7 @@ export function AutomationEditContent({ workspaceId, automationId }: AutomationE
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden /> <ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2> <h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto"> <p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
You don't have permission to edit automations in this search space. You don't have permission to edit automations in this workspace.
</p> </p>
</div> </div>
); );

View file

@ -13,7 +13,7 @@ interface AutomationsContentProps {
/** /**
* Client orchestrator for the automations list page. Pulls the active * Client orchestrator for the automations list page. Pulls the active
* search space's first page (via ``useAutomations`` ``automationsListAtom``) * workspace's first page (via ``useAutomations`` ``automationsListAtom``)
* and the user's permissions, then decides between empty / loading / table. * and the user's permissions, then decides between empty / loading / table.
* *
* Read access is mandatory; anything else is hidden behind RBAC. The * Read access is mandatory; anything else is hidden behind RBAC. The
@ -46,7 +46,7 @@ export function AutomationsContent({ workspaceId }: AutomationsContentProps) {
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden /> <ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2> <h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto"> <p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
You don't have permission to view automations in this search space. You don't have permission to view automations in this workspace.
</p> </p>
</div> </div>
); );

View file

@ -40,7 +40,7 @@ export function AutomationsEmptyState({ workspaceId, canCreate }: AutomationsEmp
</div> </div>
) : ( ) : (
<p className="mt-6 text-xs text-muted-foreground"> <p className="mt-6 text-xs text-muted-foreground">
You don't have permission to create automations in this search space. You don't have permission to create automations in this workspace.
</p> </p>
)} )}
</div> </div>

View file

@ -120,7 +120,7 @@ export function AutomationBuilderForm({
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
// Eligible models + the search-space-seeded defaults. Models are chosen per // Eligible models + the workspace-seeded defaults. Models are chosen per
// automation on create; in edit mode the backend preserves the captured // automation on create; in edit mode the backend preserves the captured
// snapshot, so the picker is create-only. // snapshot, so the picker is create-only.
const eligibleModels = useAutomationEligibleModels(); const eligibleModels = useAutomationEligibleModels();

View file

@ -235,7 +235,7 @@ export function MentionTaskInput({
<ComposerSuggestionPopoverContent side="bottom"> <ComposerSuggestionPopoverContent side="bottom">
<DocumentMentionPicker <DocumentMentionPicker
ref={pickerRef} ref={pickerRef}
searchSpaceId={workspaceId} workspaceId={workspaceId}
onSelectionChange={handleSelection} onSelectionChange={handleSelection}
onDone={closePopover} onDone={closePopover}
initialSelectedDocuments={mentions} initialSelectedDocuments={mentions}

View file

@ -47,7 +47,7 @@ export function DeleteAutomationDialog({
async function handleConfirm() { async function handleConfirm() {
setSubmitting(true); setSubmitting(true);
try { try {
await deleteAutomation({ automationId, searchSpaceId: workspaceId }); await deleteAutomation({ automationId, workspaceId: workspaceId });
onDeleted?.(); onDeleted?.();
onOpenChange(false); onOpenChange(false);
} finally { } finally {

View file

@ -31,7 +31,7 @@ export function AutomationNewContent({ workspaceId }: AutomationNewContentProps)
<ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden /> <ShieldAlert className="mx-auto h-10 w-10 text-muted-foreground" aria-hidden />
<h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2> <h2 className="mt-3 text-base font-semibold text-foreground">Access denied</h2>
<p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto"> <p className="mt-1 text-sm text-muted-foreground max-w-md mx-auto">
You don't have permission to create automations in this search space. You don't have permission to create automations in this workspace.
</p> </p>
</div> </div>
); );

View file

@ -5,7 +5,7 @@ export default async function ChatsPage({ params }: { params: Promise<{ workspac
return ( return (
<div className="w-full select-none"> <div className="w-full select-none">
<AllChatsWorkspaceContent searchSpaceId={workspace_id} /> <AllChatsWorkspaceContent workspaceId={workspace_id} />
</div> </div>
); );
} }

View file

@ -52,12 +52,12 @@ export function DashboardClientLayout({
const isOnboardingPage = pathname?.includes("/onboard"); const isOnboardingPage = pathname?.includes("/onboard");
const isOwner = access?.is_owner ?? false; const isOwner = access?.is_owner ?? false;
const isSearchSpaceReady = activeWorkspaceId === workspaceId; const isWorkspaceReady = activeWorkspaceId === workspaceId;
useEffect(() => { useEffect(() => {
if (isSearchSpaceReady) return; if (isWorkspaceReady) return;
setHasCheckedOnboarding(false); setHasCheckedOnboarding(false);
}, [isSearchSpaceReady]); }, [isWorkspaceReady]);
useEffect(() => { useEffect(() => {
if (isOnboardingPage) { if (isOnboardingPage) {
@ -66,7 +66,7 @@ export function DashboardClientLayout({
} }
if ( if (
isSearchSpaceReady && isWorkspaceReady &&
!loading && !loading &&
!accessLoading && !accessLoading &&
!globalConfigsLoading && !globalConfigsLoading &&
@ -75,7 +75,7 @@ export function DashboardClientLayout({
!hasCheckedOnboarding !hasCheckedOnboarding
) { ) {
// Onboarding is only relevant when no operator-provided // Onboarding is only relevant when no operator-provided
// global_llm_config.yaml exists. When it does, search spaces inherit // global_llm_config.yaml exists. When it does, workspaces inherit
// the global config and should never be forced into onboarding. // the global config and should never be forced into onboarding.
if (globalConfigStatus?.exists) { if (globalConfigStatus?.exists) {
setHasCheckedOnboarding(true); setHasCheckedOnboarding(true);
@ -102,7 +102,7 @@ export function DashboardClientLayout({
setHasCheckedOnboarding(true); setHasCheckedOnboarding(true);
} }
}, [ }, [
isSearchSpaceReady, isWorkspaceReady,
loading, loading,
accessLoading, accessLoading,
globalConfigsLoading, globalConfigsLoading,
@ -153,13 +153,13 @@ export function DashboardClientLayout({
setActiveWorkspaceIdState(activeSeacrhSpaceId); setActiveWorkspaceIdState(activeSeacrhSpaceId);
// Sync to Electron store if stored value is null (first navigation) // Sync to Electron store if stored value is null (first navigation)
if (electronAPI?.getActiveSearchSpace && electronAPI.setActiveSearchSpace) { if (electronAPI?.getActiveWorkspace && electronAPI.setActiveWorkspace) {
const setActiveSearchSpace = electronAPI.setActiveSearchSpace; const setActiveWorkspace = electronAPI.setActiveWorkspace;
electronAPI electronAPI
.getActiveSearchSpace() .getActiveWorkspace()
.then((stored: string | null) => { .then((stored: string | null) => {
if (!stored) { if (!stored) {
setActiveSearchSpace(activeSeacrhSpaceId); setActiveWorkspace(activeSeacrhSpaceId);
} }
}) })
.catch(() => {}); .catch(() => {});
@ -169,7 +169,7 @@ export function DashboardClientLayout({
// Determine if we should show loading // Determine if we should show loading
const shouldShowLoading = const shouldShowLoading =
!hasCheckedOnboarding && !hasCheckedOnboarding &&
(!isSearchSpaceReady || (!isWorkspaceReady ||
loading || loading ||
accessLoading || accessLoading ||
globalConfigsLoading || globalConfigsLoading ||
@ -214,7 +214,7 @@ export function DashboardClientLayout({
return ( return (
<DocumentUploadDialogProvider> <DocumentUploadDialogProvider>
<OnboardingTour /> <OnboardingTour />
<LayoutDataProvider searchSpaceId={workspaceId}>{children}</LayoutDataProvider> <LayoutDataProvider workspaceId={workspaceId}>{children}</LayoutDataProvider>
</DocumentUploadDialogProvider> </DocumentUploadDialogProvider>
); );
} }

View file

@ -579,7 +579,7 @@ export default function NewChatPage() {
error, error,
flow, flow,
context: { context: {
searchSpaceId: workspaceId, workspaceId: workspaceId,
threadId, threadId,
}, },
}); });
@ -717,7 +717,7 @@ export default function NewChatPage() {
data: typeof threadMessagesQuery.data; data: typeof threadMessagesQuery.data;
}>({ threadId: null, data: undefined }); }>({ threadId: null, data: undefined });
// Reset thread-local runtime state on route/search-space changes. Data fetching // Reset thread-local runtime state on route/workspace changes. Data fetching
// is handled by React Query below so the chat shell can render immediately. // is handled by React Query below so the chat shell can render immediately.
useEffect(() => { useEffect(() => {
const nextThreadId = urlChatId > 0 ? urlChatId : null; const nextThreadId = urlChatId > 0 ? urlChatId : null;
@ -755,7 +755,7 @@ export default function NewChatPage() {
chatId: thread.id, chatId: thread.id,
title: thread.title, title: thread.title,
chatUrl: `/dashboard/${thread.workspace_id ?? workspaceId}/new-chat/${thread.id}`, chatUrl: `/dashboard/${thread.workspace_id ?? workspaceId}/new-chat/${thread.id}`,
searchSpaceId: thread.workspace_id ?? workspaceId, workspaceId: thread.workspace_id ?? workspaceId,
visibility: thread.visibility, visibility: thread.visibility,
hasComments: thread.has_comments ?? false, hasComments: thread.has_comments ?? false,
}); });
@ -892,7 +892,7 @@ export default function NewChatPage() {
} }
setCurrentThreadMetadata({ setCurrentThreadMetadata({
id: null, id: null,
searchSpaceId: null, workspaceId: null,
visibility: null, visibility: null,
hasComments: false, hasComments: false,
}); });
@ -906,7 +906,7 @@ export default function NewChatPage() {
setCurrentThreadMetadata({ setCurrentThreadMetadata({
id: currentThread.id, id: currentThread.id,
searchSpaceId: currentThread.workspace_id ?? workspaceId, workspaceId: currentThread.workspace_id ?? workspaceId,
visibility, visibility,
hasComments: currentThread.has_comments ?? false, hasComments: currentThread.has_comments ?? false,
}); });

View file

@ -74,7 +74,7 @@ export default function OnboardPage() {
</p> </p>
</div> </div>
<ModelProviderConnectionsPanel <ModelProviderConnectionsPanel
searchSpaceId={workspaceId} workspaceId={workspaceId}
connections={connections} connections={connections}
className="flex flex-col gap-6 text-left" className="flex flex-col gap-6 text-left"
footerAction={ footerAction={

View file

@ -1,6 +1,6 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function SearchSpaceDashboardPage({ export default async function WorkspaceDashboardPage({
params, params,
}: { }: {
params: Promise<{ workspace_id: string }>; params: Promise<{ workspace_id: string }>;

View file

@ -96,7 +96,7 @@ import type { Role } from "@/contracts/types/roles.types";
import { invitesApiService } from "@/lib/apis/invites-api.service"; import { invitesApiService } from "@/lib/apis/invites-api.service";
import { rolesApiService } from "@/lib/apis/roles-api.service"; import { rolesApiService } from "@/lib/apis/roles-api.service";
import { formatRelativeDate } from "@/lib/format-date"; import { formatRelativeDate } from "@/lib/format-date";
import { trackSearchSpaceInviteSent, trackSearchSpaceUsersViewed } from "@/lib/posthog/events"; import { trackWorkspaceInviteSent, trackWorkspaceUsersViewed } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -229,7 +229,7 @@ export function TeamContent({ workspaceId }: TeamContentProps) {
useEffect(() => { useEffect(() => {
if (members.length > 0 && !membersLoading) { if (members.length > 0 && !membersLoading) {
const ownerCount = members.filter((m) => m.is_owner).length; const ownerCount = members.filter((m) => m.is_owner).length;
trackSearchSpaceUsersViewed(workspaceId, members.length, ownerCount); trackWorkspaceUsersViewed(workspaceId, members.length, ownerCount);
} }
}, [members, membersLoading, workspaceId]); }, [members, membersLoading, workspaceId]);
@ -654,7 +654,7 @@ function CreateInviteDialog({
setCreatedInvite(invite); setCreatedInvite(invite);
const roleName = roleId ? roles.find((r) => r.id.toString() === roleId)?.name : undefined; const roleName = roleId ? roles.find((r) => r.id.toString() === roleId)?.name : undefined;
trackSearchSpaceInviteSent(workspaceId, { trackWorkspaceInviteSent(workspaceId, {
roleName, roleName,
hasExpiry: !!expiresAt, hasExpiry: !!expiresAt,
hasMaxUses: !!maxUses, hasMaxUses: !!maxUses,

View file

@ -226,9 +226,7 @@ export function AgentPermissionsContent() {
} }
if (!workspaceId) { if (!workspaceId) {
return ( return <p className="text-sm text-muted-foreground">Open a workspace to manage agent rules.</p>;
<p className="text-sm text-muted-foreground">Open a search space to manage agent rules.</p>
);
} }
return ( return (

View file

@ -13,15 +13,15 @@ import {
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import type { SearchSpace } from "@/contracts/types/workspace.types"; import type { Workspace } from "@/contracts/types/workspace.types";
import { useElectronAPI } from "@/hooks/use-platform"; import { useElectronAPI } from "@/hooks/use-platform";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service"; import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
export function DesktopContent() { export function DesktopContent() {
const api = useElectronAPI(); const api = useElectronAPI();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]); const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null); const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
const [autoLaunchEnabled, setAutoLaunchEnabled] = useState(false); const [autoLaunchEnabled, setAutoLaunchEnabled] = useState(false);
@ -40,14 +40,14 @@ export function DesktopContent() {
setAutoLaunchSupported(hasAutoLaunchApi); setAutoLaunchSupported(hasAutoLaunchApi);
Promise.all([ Promise.all([
api.getActiveSearchSpace?.() ?? Promise.resolve(null), api.getActiveWorkspace?.() ?? Promise.resolve(null),
searchSpacesApiService.getSearchSpaces(), workspacesApiService.getWorkspaces(),
hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null), hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null),
]) ])
.then(([spaceId, spaces, autoLaunch]) => { .then(([spaceId, spaces, autoLaunch]) => {
if (!mounted) return; if (!mounted) return;
setActiveSpaceId(spaceId); setActiveSpaceId(spaceId);
if (spaces) setSearchSpaces(spaces); if (spaces) setWorkspaces(spaces);
if (autoLaunch) { if (autoLaunch) {
setAutoLaunchEnabled(autoLaunch.enabled); setAutoLaunchEnabled(autoLaunch.enabled);
setAutoLaunchHidden(autoLaunch.openAsHidden); setAutoLaunchHidden(autoLaunch.openAsHidden);
@ -136,30 +136,30 @@ export function DesktopContent() {
} }
}; };
const handleSearchSpaceChange = (value: string) => { const handleWorkspaceChange = (value: string) => {
setActiveSpaceId(value); setActiveSpaceId(value);
api.setActiveSearchSpace?.(value); api.setActiveWorkspace?.(value);
toast.success("Default search space updated"); toast.success("Default workspace updated");
}; };
return ( return (
<div className="flex flex-col gap-4 md:gap-6"> <div className="flex flex-col gap-4 md:gap-6">
<section> <section>
<div className="pb-2 md:pb-3"> <div className="pb-2 md:pb-3">
<h2 className="text-base md:text-lg font-semibold">Default Search Space</h2> <h2 className="text-base md:text-lg font-semibold">Default Workspace</h2>
<p className="text-xs md:text-sm text-muted-foreground"> <p className="text-xs md:text-sm text-muted-foreground">
Choose which search space General Assist, Screenshot Assist, and Quick Assist use by Choose which workspace General Assist, Screenshot Assist, and Quick Assist use by
default. default.
</p> </p>
</div> </div>
<div> <div>
{searchSpaces.length > 0 ? ( {workspaces.length > 0 ? (
<Select value={activeSpaceId ?? undefined} onValueChange={handleSearchSpaceChange}> <Select value={activeSpaceId ?? undefined} onValueChange={handleWorkspaceChange}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Select a search space" /> <SelectValue placeholder="Select a workspace" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{searchSpaces.map((space) => ( {workspaces.map((space) => (
<SelectItem key={space.id} value={String(space.id)}> <SelectItem key={space.id} value={String(space.id)}>
{space.name} {space.name}
</SelectItem> </SelectItem>
@ -167,9 +167,7 @@ export function DesktopContent() {
</SelectContent> </SelectContent>
</Select> </Select>
) : ( ) : (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">No workspaces found. Create one first.</p>
No search spaces found. Create one first.
</p>
)} )}
</div> </div>
</section> </section>

View file

@ -17,8 +17,8 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import type { SearchSpace } from "@/contracts/types/workspace.types"; import type { Workspace } from "@/contracts/types/workspace.types";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service"; import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { authenticatedFetch } from "@/lib/auth-fetch"; import { authenticatedFetch } from "@/lib/auth-fetch";
import { buildBackendUrl } from "@/lib/env-config"; import { buildBackendUrl } from "@/lib/env-config";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -79,7 +79,7 @@ export function MessagingChannelsContent() {
const workspaceId = Number(params.workspace_id); const workspaceId = Number(params.workspace_id);
const [gatewayConfig, setGatewayConfig] = useState<GatewayConfigState>(null); const [gatewayConfig, setGatewayConfig] = useState<GatewayConfigState>(null);
const [connections, setConnections] = useState<GatewayConnection[]>([]); const [connections, setConnections] = useState<GatewayConnection[]>([]);
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]); const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [pairing, setPairing] = useState<Pairing | null>(null); const [pairing, setPairing] = useState<Pairing | null>(null);
const [pairingPlatform, setPairingPlatform] = useState<PairingPlatform | null>(null); const [pairingPlatform, setPairingPlatform] = useState<PairingPlatform | null>(null);
const [baileysHealth, setBaileysHealth] = useState<BaileysHealth | null>(null); const [baileysHealth, setBaileysHealth] = useState<BaileysHealth | null>(null);
@ -114,11 +114,11 @@ export function MessagingChannelsContent() {
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const [nextConnections, spaces, nextGatewayConfig] = await Promise.all([ const [nextConnections, spaces, nextGatewayConfig] = await Promise.all([
fetchConnections(), fetchConnections(),
searchSpacesApiService.getSearchSpaces(), workspacesApiService.getWorkspaces(),
fetchGatewayConfig(), fetchGatewayConfig(),
]); ]);
setConnections(nextConnections); setConnections(nextConnections);
setSearchSpaces(spaces); setWorkspaces(spaces);
setGatewayConfig(nextGatewayConfig); setGatewayConfig(nextGatewayConfig);
}, [fetchConnections, fetchGatewayConfig]); }, [fetchConnections, fetchGatewayConfig]);
@ -210,10 +210,7 @@ export function MessagingChannelsContent() {
await refreshPlatform(connection.platform as GatewayPlatform); await refreshPlatform(connection.platform as GatewayPlatform);
} }
async function updateConnectionSearchSpace( async function updateConnectionWorkspace(connection: GatewayConnection, nextWorkspaceId: string) {
connection: GatewayConnection,
nextWorkspaceId: string
) {
const previousConnections = connections; const previousConnections = connections;
const parsedWorkspaceId = Number(nextWorkspaceId); const parsedWorkspaceId = Number(nextWorkspaceId);
const targetKey = connectionKey(connection); const targetKey = connectionKey(connection);
@ -324,14 +321,14 @@ export function MessagingChannelsContent() {
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Select <Select
value={String(connection.workspace_id)} value={String(connection.workspace_id)}
onValueChange={(value) => updateConnectionSearchSpace(connection, value)} onValueChange={(value) => updateConnectionWorkspace(connection, value)}
disabled={searchSpaces.length === 0} disabled={workspaces.length === 0}
> >
<SelectTrigger className="h-8 min-w-[180px] flex-1 text-xs"> <SelectTrigger className="h-8 min-w-[180px] flex-1 text-xs">
<SelectValue placeholder="Select search space" /> <SelectValue placeholder="Select workspace" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{searchSpaces.map((space) => ( {workspaces.map((space) => (
<SelectItem key={space.id} value={String(space.id)}> <SelectItem key={space.id} value={String(space.id)}>
{space.name} {space.name}
</SelectItem> </SelectItem>
@ -409,7 +406,7 @@ export function MessagingChannelsContent() {
<AlertDescription> <AlertDescription>
<p> <p>
Soon you'll be able to connect WhatsApp, Telegram, Slack, and Discord to your Soon you'll be able to connect WhatsApp, Telegram, Slack, and Discord to your
SurfSense agent so you can ask questions, route messages to search spaces, and get SurfSense agent so you can ask questions, route messages to workspaces, and get
answers from your knowledge base without leaving your chat app. answers from your knowledge base without leaving your chat app.
</p> </p>
</AlertDescription> </AlertDescription>

View file

@ -9,25 +9,20 @@ import { useCallback, useMemo, useState } from "react";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export type SearchSpaceSettingsTab = export type WorkspaceSettingsTab = "general" | "models" | "team-roles" | "prompts" | "public-links";
| "general"
| "models"
| "team-roles"
| "prompts"
| "public-links";
const DEFAULT_TAB: SearchSpaceSettingsTab = "general"; const DEFAULT_TAB: WorkspaceSettingsTab = "general";
interface SearchSpaceSettingsLayoutShellProps { interface WorkspaceSettingsLayoutShellProps {
workspaceId: string; workspaceId: string;
children: React.ReactNode; children: React.ReactNode;
} }
export function SearchSpaceSettingsLayoutShell({ export function WorkspaceSettingsLayoutShell({
workspaceId, workspaceId,
children, children,
}: SearchSpaceSettingsLayoutShellProps) { }: WorkspaceSettingsLayoutShellProps) {
const t = useTranslations("searchSpaceSettings"); const t = useTranslations("workspaceSettings");
const segment = useSelectedLayoutSegment(); const segment = useSelectedLayoutSegment();
const [tabScrollPos, setTabScrollPos] = useState<"start" | "middle" | "end">("start"); const [tabScrollPos, setTabScrollPos] = useState<"start" | "middle" | "end">("start");
@ -69,13 +64,13 @@ export function SearchSpaceSettingsLayoutShell({
[t] [t]
); );
const activeTab: SearchSpaceSettingsTab = const activeTab: WorkspaceSettingsTab =
segment && navItems.some((item) => item.value === segment) segment && navItems.some((item) => item.value === segment)
? (segment as SearchSpaceSettingsTab) ? (segment as WorkspaceSettingsTab)
: DEFAULT_TAB; : DEFAULT_TAB;
const selectedLabel = navItems.find((item) => item.value === activeTab)?.label ?? t("title"); const selectedLabel = navItems.find((item) => item.value === activeTab)?.label ?? t("title");
const hrefFor = (tab: SearchSpaceSettingsTab) => const hrefFor = (tab: WorkspaceSettingsTab) =>
`/dashboard/${workspaceId}/workspace-settings/${tab}`; `/dashboard/${workspaceId}/workspace-settings/${tab}`;
return ( return (

View file

@ -1,8 +1,8 @@
import type React from "react"; import type React from "react";
import { use } from "react"; import { use } from "react";
import { SearchSpaceSettingsLayoutShell } from "./layout-shell"; import { WorkspaceSettingsLayoutShell } from "./layout-shell";
export default function SearchSpaceSettingsLayout({ export default function WorkspaceSettingsLayout({
params, params,
children, children,
}: { }: {
@ -12,8 +12,8 @@ export default function SearchSpaceSettingsLayout({
const { workspace_id } = use(params); const { workspace_id } = use(params);
return ( return (
<SearchSpaceSettingsLayoutShell workspaceId={workspace_id}> <WorkspaceSettingsLayoutShell workspaceId={workspace_id}>
{children} {children}
</SearchSpaceSettingsLayoutShell> </WorkspaceSettingsLayoutShell>
); );
} }

View file

@ -1,6 +1,6 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default async function SearchSpaceSettingsPage({ export default async function WorkspaceSettingsPage({
params, params,
}: { }: {
params: Promise<{ workspace_id: string }>; params: Promise<{ workspace_id: string }>;

View file

@ -6,8 +6,8 @@ import { motion } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { searchSpacesAtom } from "@/atoms/workspaces/workspace-query.atoms"; import { workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
import { CreateSearchSpaceDialog } from "@/components/layout"; import { CreateWorkspaceDialog } from "@/components/layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
@ -42,7 +42,7 @@ function ErrorScreen({ message }: { message: string }) {
} }
function EmptyState({ onCreateClick }: { onCreateClick: () => void }) { function EmptyState({ onCreateClick }: { onCreateClick: () => void }) {
const t = useTranslations("searchSpace"); const t = useTranslations("workspace");
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4"> <div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4">
@ -74,26 +74,26 @@ export default function DashboardPage() {
const router = useRouter(); const router = useRouter();
const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false);
const { data: searchSpaces = [], isLoading, error } = useAtomValue(searchSpacesAtom); const { data: workspaces = [], isLoading, error } = useAtomValue(workspacesAtom);
useEffect(() => { useEffect(() => {
if (isLoading) return; if (isLoading) return;
if (searchSpaces.length > 0) { if (workspaces.length > 0) {
// Read the query string at the time of redirect — no subscription needed. // Read the query string at the time of redirect — no subscription needed.
// (Vercel Best Practice: rerender-defer-reads 5.2) // (Vercel Best Practice: rerender-defer-reads 5.2)
const query = window.location.search; const query = window.location.search;
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat${query}`); router.replace(`/dashboard/${workspaces[0].id}/new-chat${query}`);
} }
}, [isLoading, searchSpaces, router]); }, [isLoading, workspaces, router]);
// Show loading while fetching or while we have spaces and are about to redirect // Show loading while fetching or while we have spaces and are about to redirect
const shouldShowLoading = isLoading || searchSpaces.length > 0; const shouldShowLoading = isLoading || workspaces.length > 0;
// Use global loading screen - spinner animation won't reset // Use global loading screen - spinner animation won't reset
useGlobalLoadingEffect(shouldShowLoading); useGlobalLoadingEffect(shouldShowLoading);
if (error) return <ErrorScreen message={error?.message || "Failed to load search spaces"} />; if (error) return <ErrorScreen message={error?.message || "Failed to load workspaces"} />;
if (shouldShowLoading) { if (shouldShowLoading) {
return null; return null;
@ -102,7 +102,7 @@ export default function DashboardPage() {
return ( return (
<> <>
<EmptyState onCreateClick={() => setShowCreateDialog(true)} /> <EmptyState onCreateClick={() => setShowCreateDialog(true)} />
<CreateSearchSpaceDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} /> <CreateWorkspaceDialog open={showCreateDialog} onOpenChange={setShowCreateDialog} />
</> </>
); );
} }

View file

@ -14,7 +14,7 @@ import { Separator } from "@/components/ui/separator";
import { ShortcutKbd } from "@/components/ui/shortcut-kbd"; import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { useElectronAPI } from "@/hooks/use-platform"; import { useElectronAPI } from "@/hooks/use-platform";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service"; import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { getPostLoginRedirectPath } from "@/lib/auth-utils"; import { getPostLoginRedirectPath } from "@/lib/auth-utils";
type ShortcutKey = "generalAssist" | "quickAsk" | "screenshotAssist"; type ShortcutKey = "generalAssist" | "quickAsk" | "screenshotAssist";
@ -239,7 +239,7 @@ export default function DesktopLoginPage() {
setIsGoogleRedirecting(true); setIsGoogleRedirecting(true);
try { try {
await api?.startGoogleOAuth?.(); await api?.startGoogleOAuth?.();
await autoSetSearchSpace(); await autoSetWorkspace();
router.push(getPostLoginRedirectPath()); router.push(getPostLoginRedirectPath());
} catch (error) { } catch (error) {
setIsGoogleRedirecting(false); setIsGoogleRedirecting(false);
@ -247,13 +247,13 @@ export default function DesktopLoginPage() {
} }
}; };
const autoSetSearchSpace = async () => { const autoSetWorkspace = async () => {
try { try {
const stored = await api?.getActiveSearchSpace?.(); const stored = await api?.getActiveWorkspace?.();
if (stored) return; if (stored) return;
const spaces = await searchSpacesApiService.getSearchSpaces(); const spaces = await workspacesApiService.getWorkspaces();
if (spaces?.length) { if (spaces?.length) {
await api?.setActiveSearchSpace?.(String(spaces[0].id)); await api?.setActiveWorkspace?.(String(spaces[0].id));
} }
} catch { } catch {
// non-critical — dashboard-sync will catch it later // non-critical — dashboard-sync will catch it later
@ -272,7 +272,7 @@ export default function DesktopLoginPage() {
} }
await api.loginPassword(email, password); await api.loginPassword(email, password);
await autoSetSearchSpace(); await autoSetWorkspace();
setTimeout(() => { setTimeout(() => {
router.push(getPostLoginRedirectPath()); router.push(getPostLoginRedirectPath());

View file

@ -34,9 +34,9 @@ import { useSession } from "@/hooks/use-session";
import { invitesApiService } from "@/lib/apis/invites-api.service"; import { invitesApiService } from "@/lib/apis/invites-api.service";
import { setRedirectPath } from "@/lib/auth-utils"; import { setRedirectPath } from "@/lib/auth-utils";
import { import {
trackSearchSpaceInviteAccepted, trackWorkspaceInviteAccepted,
trackSearchSpaceInviteDeclined, trackWorkspaceInviteDeclined,
trackSearchSpaceUserAdded, trackWorkspaceUserAdded,
} from "@/lib/posthog/events"; } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
@ -97,12 +97,8 @@ export default function InviteAcceptPage() {
setAcceptedData(result); setAcceptedData(result);
// Track invite accepted and user added events // Track invite accepted and user added events
trackSearchSpaceInviteAccepted( trackWorkspaceInviteAccepted(result.workspace_id, result.workspace_name, result.role_name);
result.workspace_id, trackWorkspaceUserAdded(result.workspace_id, result.workspace_name, result.role_name);
result.workspace_name,
result.role_name
);
trackSearchSpaceUserAdded(result.workspace_id, result.workspace_name, result.role_name);
} }
} catch (err: any) { } catch (err: any) {
setError(err.message || "Failed to accept invite"); setError(err.message || "Failed to accept invite");
@ -113,7 +109,7 @@ export default function InviteAcceptPage() {
const handleDecline = () => { const handleDecline = () => {
// Track invite declined event // Track invite declined event
trackSearchSpaceInviteDeclined(inviteInfo?.workspace_name); trackWorkspaceInviteDeclined(inviteInfo?.workspace_name);
router.push("/dashboard"); router.push("/dashboard");
}; };
@ -187,7 +183,7 @@ export default function InviteAcceptPage() {
</div> </div>
<div> <div>
<p className="font-medium">{acceptedData.workspace_name}</p> <p className="font-medium">{acceptedData.workspace_name}</p>
<p className="text-sm text-muted-foreground">Search Space</p> <p className="text-sm text-muted-foreground">Workspace</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -206,7 +202,7 @@ export default function InviteAcceptPage() {
className="w-full gap-2" className="w-full gap-2"
onClick={() => router.push(`/dashboard/${acceptedData.workspace_id}`)} onClick={() => router.push(`/dashboard/${acceptedData.workspace_id}`)}
> >
Go to Search Space Go to Workspace
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
</Button> </Button>
</CardFooter> </CardFooter>
@ -256,7 +252,7 @@ export default function InviteAcceptPage() {
</motion.div> </motion.div>
<CardTitle className="text-2xl">You're Invited!</CardTitle> <CardTitle className="text-2xl">You're Invited!</CardTitle>
<CardDescription> <CardDescription>
Sign in to join {inviteInfo?.workspace_name || "this search space"} Sign in to join {inviteInfo?.workspace_name || "this workspace"}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -267,7 +263,7 @@ export default function InviteAcceptPage() {
</div> </div>
<div> <div>
<p className="font-medium">{inviteInfo?.workspace_name}</p> <p className="font-medium">{inviteInfo?.workspace_name}</p>
<p className="text-sm text-muted-foreground">Search Space</p> <p className="text-sm text-muted-foreground">Workspace</p>
</div> </div>
</div> </div>
{inviteInfo?.role_name && ( {inviteInfo?.role_name && (
@ -303,7 +299,7 @@ export default function InviteAcceptPage() {
</motion.div> </motion.div>
<CardTitle className="text-2xl">You're Invited!</CardTitle> <CardTitle className="text-2xl">You're Invited!</CardTitle>
<CardDescription> <CardDescription>
Accept this invite to join {inviteInfo?.workspace_name || "this search space"} Accept this invite to join {inviteInfo?.workspace_name || "this workspace"}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@ -314,7 +310,7 @@ export default function InviteAcceptPage() {
</div> </div>
<div> <div>
<p className="font-medium">{inviteInfo?.workspace_name}</p> <p className="font-medium">{inviteInfo?.workspace_name}</p>
<p className="text-sm text-muted-foreground">Search Space</p> <p className="text-sm text-muted-foreground">Workspace</p>
</div> </div>
</div> </div>
{inviteInfo?.role_name && ( {inviteInfo?.role_name && (

View file

@ -12,78 +12,78 @@ export const agentToolsAtom = atomWithQuery((_get) => ({
const STORAGE_PREFIX = "surfsense-disabled-tools-"; const STORAGE_PREFIX = "surfsense-disabled-tools-";
function loadDisabledTools(searchSpaceId: string): string[] { function loadDisabledTools(workspaceId: string): string[] {
if (typeof window === "undefined") return []; if (typeof window === "undefined") return [];
try { try {
const raw = localStorage.getItem(`${STORAGE_PREFIX}${searchSpaceId}`); const raw = localStorage.getItem(`${STORAGE_PREFIX}${workspaceId}`);
return raw ? (JSON.parse(raw) as string[]) : []; return raw ? (JSON.parse(raw) as string[]) : [];
} catch { } catch {
return []; return [];
} }
} }
function saveDisabledTools(searchSpaceId: string, tools: string[]) { function saveDisabledTools(workspaceId: string, tools: string[]) {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
if (tools.length === 0) { if (tools.length === 0) {
localStorage.removeItem(`${STORAGE_PREFIX}${searchSpaceId}`); localStorage.removeItem(`${STORAGE_PREFIX}${workspaceId}`);
} else { } else {
localStorage.setItem(`${STORAGE_PREFIX}${searchSpaceId}`, JSON.stringify(tools)); localStorage.setItem(`${STORAGE_PREFIX}${workspaceId}`, JSON.stringify(tools));
} }
} }
const disabledToolsBaseAtom = atom<string[]>([]); const disabledToolsBaseAtom = atom<string[]>([]);
/** Tracks whether the atom has been hydrated from localStorage for the current search space */ /** Tracks whether the atom has been hydrated from localStorage for the current workspace */
const hydratedForAtom = atom<string | null>(null); const hydratedForAtom = atom<string | null>(null);
/** /**
* Read/write atom for the set of disabled tool names. * Read/write atom for the set of disabled tool names.
* Persists to localStorage keyed by search space ID. * Persists to localStorage keyed by workspace ID.
*/ */
export const disabledToolsAtom = atom( export const disabledToolsAtom = atom(
(get) => { (get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const hydratedFor = get(hydratedForAtom); const hydratedFor = get(hydratedForAtom);
if (searchSpaceId && hydratedFor !== searchSpaceId) { if (workspaceId && hydratedFor !== workspaceId) {
return loadDisabledTools(searchSpaceId); return loadDisabledTools(workspaceId);
} }
return get(disabledToolsBaseAtom); return get(disabledToolsBaseAtom);
}, },
(get, set, update: string[] | ((prev: string[]) => string[])) => { (get, set, update: string[] | ((prev: string[]) => string[])) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const prev = get(disabledToolsBaseAtom); const prev = get(disabledToolsBaseAtom);
const next = typeof update === "function" ? update(prev) : update; const next = typeof update === "function" ? update(prev) : update;
set(disabledToolsBaseAtom, next); set(disabledToolsBaseAtom, next);
set(hydratedForAtom, searchSpaceId); set(hydratedForAtom, workspaceId);
if (searchSpaceId) { if (workspaceId) {
saveDisabledTools(searchSpaceId, next); saveDisabledTools(workspaceId, next);
} }
} }
); );
/** /**
* Hydrate disabled tools from localStorage when search space changes. * Hydrate disabled tools from localStorage when workspace changes.
* Call this from a useEffect in a component that has access to the search space. * Call this from a useEffect in a component that has access to the workspace.
*/ */
export const hydrateDisabledToolsAtom = atom(null, (get, set) => { export const hydrateDisabledToolsAtom = atom(null, (get, set) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
if (!searchSpaceId) return; if (!workspaceId) return;
const stored = loadDisabledTools(searchSpaceId); const stored = loadDisabledTools(workspaceId);
set(disabledToolsBaseAtom, stored); set(disabledToolsBaseAtom, stored);
set(hydratedForAtom, searchSpaceId); set(hydratedForAtom, workspaceId);
}); });
/** Toggle a single tool's enabled/disabled state */ /** Toggle a single tool's enabled/disabled state */
export const toggleToolAtom = atom(null, (get, set, toolName: string) => { export const toggleToolAtom = atom(null, (get, set, toolName: string) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const current = get(disabledToolsBaseAtom); const current = get(disabledToolsBaseAtom);
const next = current.includes(toolName) const next = current.includes(toolName)
? current.filter((t) => t !== toolName) ? current.filter((t) => t !== toolName)
: [...current, toolName]; : [...current, toolName];
set(disabledToolsBaseAtom, next); set(disabledToolsBaseAtom, next);
set(hydratedForAtom, searchSpaceId); set(hydratedForAtom, workspaceId);
if (searchSpaceId) { if (workspaceId) {
saveDisabledTools(searchSpaceId, next); saveDisabledTools(workspaceId, next);
} }
}); });

View file

@ -26,15 +26,15 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queryClient } from "@/lib/query-client/client"; import { queryClient } from "@/lib/query-client/client";
// Cache invalidation strategy: // Cache invalidation strategy:
// - Automation writes invalidate the search-space list + the touched detail. // - Automation writes invalidate the workspace list + the touched detail.
// - Trigger writes only invalidate the parent automation detail (triggers // - Trigger writes only invalidate the parent automation detail (triggers
// come back inline in AutomationDetail). // come back inline in AutomationDetail).
// We deliberately invalidate the whole "automations" prefix on the list side // We deliberately invalidate the whole "automations" prefix on the list side
// because list is keyed by (searchSpaceId, limit, offset) and we don't track // because list is keyed by (workspaceId, limit, offset) and we don't track
// the active pagination in this layer. // the active pagination in this layer.
function invalidateList(searchSpaceId: number) { function invalidateList(workspaceId: number) {
queryClient.invalidateQueries({ queryKey: ["automations", "list", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["automations", "list", workspaceId] });
} }
function invalidateDetail(automationId: number) { function invalidateDetail(automationId: number) {
@ -113,17 +113,17 @@ export const updateAutomationMutationAtom = atomWithMutation(() => ({
export const deleteAutomationMutationAtom = atomWithMutation(() => ({ export const deleteAutomationMutationAtom = atomWithMutation(() => ({
meta: { suppressGlobalErrorToast: true }, meta: { suppressGlobalErrorToast: true },
mutationFn: async (vars: { automationId: number; searchSpaceId: number }) => { mutationFn: async (vars: { automationId: number; workspaceId: number }) => {
await automationsApiService.deleteAutomation(vars.automationId); await automationsApiService.deleteAutomation(vars.automationId);
return vars; return vars;
}, },
onSuccess: (vars) => { onSuccess: (vars) => {
invalidateList(vars.searchSpaceId); invalidateList(vars.workspaceId);
invalidateDetail(vars.automationId); invalidateDetail(vars.automationId);
toast.success("Automation deleted"); toast.success("Automation deleted");
trackAutomationDeleted({ trackAutomationDeleted({
automation_id: vars.automationId, automation_id: vars.automationId,
workspace_id: vars.searchSpaceId, workspace_id: vars.workspaceId,
}); });
}, },
onError: (error: Error, vars) => { onError: (error: Error, vars) => {

View file

@ -3,7 +3,7 @@ import { activeWorkspaceIdAtom } from "@/atoms/workspaces/workspace-query.atoms"
import { automationsApiService } from "@/lib/apis/automations-api.service"; import { automationsApiService } from "@/lib/apis/automations-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
// First page of the active search space's automations. // First page of the active workspace's automations.
// Detail + paginated/parameterized reads live in hooks (see use-automation.ts, // Detail + paginated/parameterized reads live in hooks (see use-automation.ts,
// use-automation-runs.ts) so atoms stay tied to "current scope" and don't // use-automation-runs.ts) so atoms stay tied to "current scope" and don't
// proliferate atom families for every (id, limit, offset) tuple. // proliferate atom families for every (id, limit, offset) tuple.
@ -11,18 +11,18 @@ const DEFAULT_LIMIT = 50;
const DEFAULT_OFFSET = 0; const DEFAULT_OFFSET = 0;
export const automationsListAtom = atomWithQuery((get) => { export const automationsListAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
queryKey: cacheKeys.automations.list(Number(searchSpaceId ?? 0), DEFAULT_LIMIT, DEFAULT_OFFSET), queryKey: cacheKeys.automations.list(Number(workspaceId ?? 0), DEFAULT_LIMIT, DEFAULT_OFFSET),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 60 * 1000, staleTime: 60 * 1000,
queryFn: async () => { queryFn: async () => {
if (!searchSpaceId) { if (!workspaceId) {
return { items: [], total: 0 }; return { items: [], total: 0 };
} }
return automationsApiService.listAutomations({ return automationsApiService.listAutomations({
workspace_id: Number(searchSpaceId), workspace_id: Number(workspaceId),
limit: DEFAULT_LIMIT, limit: DEFAULT_LIMIT,
offset: DEFAULT_OFFSET, offset: DEFAULT_OFFSET,
}); });

View file

@ -4,14 +4,14 @@ import { reportPanelAtom } from "./report-panel.atom";
interface CurrentThreadState { interface CurrentThreadState {
id: number | null; id: number | null;
searchSpaceId: number | null; workspaceId: number | null;
visibility: ChatVisibility | null; visibility: ChatVisibility | null;
hasComments: boolean; hasComments: boolean;
} }
interface CurrentThreadMetadataPatch { interface CurrentThreadMetadataPatch {
id: number | null; id: number | null;
searchSpaceId?: number | null; workspaceId?: number | null;
visibility?: ChatVisibility | null; visibility?: ChatVisibility | null;
hasComments?: boolean; hasComments?: boolean;
} }
@ -24,7 +24,7 @@ interface CurrentThreadMetadataUpdate {
const initialState: CurrentThreadState = { const initialState: CurrentThreadState = {
id: null, id: null,
searchSpaceId: null, workspaceId: null,
visibility: null, visibility: null,
hasComments: false, hasComments: false,
}; };
@ -44,11 +44,11 @@ export const setCurrentThreadMetadataAtom = atom(
set(currentThreadAtom, { set(currentThreadAtom, {
...current, ...current,
id: metadata.id, id: metadata.id,
searchSpaceId: workspaceId:
"searchSpaceId" in metadata "workspaceId" in metadata
? (metadata.searchSpaceId ?? null) ? (metadata.workspaceId ?? null)
: isSameThread : isSameThread
? current.searchSpaceId ? current.workspaceId
: null, : null,
visibility: visibility:
"visibility" in metadata "visibility" in metadata

View file

@ -13,38 +13,38 @@ import { queryClient } from "@/lib/query-client/client";
import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms"; import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms";
export const createConnectorMutationAtom = atomWithMutation((get) => { export const createConnectorMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.connectors.all(searchSpaceId ?? ""), mutationKey: cacheKeys.connectors.all(workspaceId ?? ""),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: CreateConnectorRequest) => { mutationFn: async (request: CreateConnectorRequest) => {
return connectorsApiService.createConnector(request); return connectorsApiService.createConnector(request);
}, },
onSuccess: () => { onSuccess: () => {
if (!searchSpaceId) return; if (!workspaceId) return;
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.connectors.all(searchSpaceId), queryKey: cacheKeys.connectors.all(workspaceId),
}); });
}, },
}; };
}); });
export const updateConnectorMutationAtom = atomWithMutation((get) => { export const updateConnectorMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.connectors.all(searchSpaceId ?? ""), mutationKey: cacheKeys.connectors.all(workspaceId ?? ""),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: UpdateConnectorRequest) => { mutationFn: async (request: UpdateConnectorRequest) => {
return connectorsApiService.updateConnector(request); return connectorsApiService.updateConnector(request);
}, },
onSuccess: (_, request: UpdateConnectorRequest) => { onSuccess: (_, request: UpdateConnectorRequest) => {
if (!searchSpaceId) return; if (!workspaceId) return;
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.connectors.all(searchSpaceId), queryKey: cacheKeys.connectors.all(workspaceId),
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.connectors.byId(String(request.id)), queryKey: cacheKeys.connectors.byId(String(request.id)),
@ -54,19 +54,19 @@ export const updateConnectorMutationAtom = atomWithMutation((get) => {
}); });
export const deleteConnectorMutationAtom = atomWithMutation((get) => { export const deleteConnectorMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.connectors.all(searchSpaceId ?? ""), mutationKey: cacheKeys.connectors.all(workspaceId ?? ""),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: DeleteConnectorRequest) => { mutationFn: async (request: DeleteConnectorRequest) => {
return connectorsApiService.deleteConnector(request); return connectorsApiService.deleteConnector(request);
}, },
onSuccess: (_, request: DeleteConnectorRequest) => { onSuccess: (_, request: DeleteConnectorRequest) => {
if (!searchSpaceId) return; if (!workspaceId) return;
queryClient.setQueryData( queryClient.setQueryData(
cacheKeys.connectors.all(searchSpaceId), cacheKeys.connectors.all(workspaceId),
(oldData: GetConnectorsResponse | undefined) => { (oldData: GetConnectorsResponse | undefined) => {
if (!oldData) return oldData; if (!oldData) return oldData;
return oldData.filter((connector) => connector.id !== request.id); return oldData.filter((connector) => connector.id !== request.id);
@ -80,19 +80,19 @@ export const deleteConnectorMutationAtom = atomWithMutation((get) => {
}); });
export const indexConnectorMutationAtom = atomWithMutation((get) => { export const indexConnectorMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.connectors.index(), mutationKey: cacheKeys.connectors.index(),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: IndexConnectorRequest) => { mutationFn: async (request: IndexConnectorRequest) => {
return connectorsApiService.indexConnector(request); return connectorsApiService.indexConnector(request);
}, },
onSuccess: (response: IndexConnectorResponse) => { onSuccess: (response: IndexConnectorResponse) => {
if (!searchSpaceId) return; if (!workspaceId) return;
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.connectors.all(searchSpaceId), queryKey: cacheKeys.connectors.all(workspaceId),
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.connectors.byId(String(response.connector_id)), queryKey: cacheKeys.connectors.byId(String(response.connector_id)),

View file

@ -4,16 +4,16 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms"; import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms";
export const connectorsAtom = atomWithQuery((get) => { export const connectorsAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
queryKey: cacheKeys.connectors.all(searchSpaceId!), queryKey: cacheKeys.connectors.all(workspaceId!),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5 minutes
queryFn: async () => { queryFn: async () => {
return connectorsApiService.getConnectors({ return connectorsApiService.getConnectors({
queryParams: { queryParams: {
workspace_id: searchSpaceId!, workspace_id: workspaceId!,
}, },
}); });
}, },

View file

@ -14,12 +14,12 @@ import { queryClient } from "@/lib/query-client/client";
import { globalDocumentsQueryParamsAtom } from "./ui.atoms"; import { globalDocumentsQueryParamsAtom } from "./ui.atoms";
export const createDocumentMutationAtom = atomWithMutation((get) => { export const createDocumentMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const documentsQueryParams = get(globalDocumentsQueryParamsAtom); const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
return { return {
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: CreateDocumentRequest) => { mutationFn: async (request: CreateDocumentRequest) => {
return documentsApiService.createDocument(request); return documentsApiService.createDocument(request);
}, },
@ -34,12 +34,12 @@ export const createDocumentMutationAtom = atomWithMutation((get) => {
}); });
export const uploadDocumentMutationAtom = atomWithMutation((get) => { export const uploadDocumentMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const documentsQueryParams = get(globalDocumentsQueryParamsAtom); const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
return { return {
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: UploadDocumentRequest) => { mutationFn: async (request: UploadDocumentRequest) => {
return documentsApiService.uploadDocument(request); return documentsApiService.uploadDocument(request);
}, },
@ -47,19 +47,19 @@ export const uploadDocumentMutationAtom = atomWithMutation((get) => {
onSuccess: () => { onSuccess: () => {
// Note: Toast notification is handled by the caller (DocumentUploadTab) to use i18n // Note: Toast notification is handled by the caller (DocumentUploadTab) to use i18n
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(searchSpaceId ?? undefined), queryKey: cacheKeys.logs.summary(workspaceId ?? undefined),
}); });
}, },
}; };
}); });
export const updateDocumentMutationAtom = atomWithMutation((get) => { export const updateDocumentMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const documentsQueryParams = get(globalDocumentsQueryParamsAtom); const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
return { return {
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: UpdateDocumentRequest) => { mutationFn: async (request: UpdateDocumentRequest) => {
return documentsApiService.updateDocument(request); return documentsApiService.updateDocument(request);
}, },
@ -77,12 +77,12 @@ export const updateDocumentMutationAtom = atomWithMutation((get) => {
}); });
export const deleteDocumentMutationAtom = atomWithMutation((get) => { export const deleteDocumentMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
const documentsQueryParams = get(globalDocumentsQueryParamsAtom); const documentsQueryParams = get(globalDocumentsQueryParamsAtom);
return { return {
mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: DeleteDocumentRequest) => { mutationFn: async (request: DeleteDocumentRequest) => {
return documentsApiService.deleteDocument(request); return documentsApiService.deleteDocument(request);
}, },

View file

@ -4,7 +4,7 @@ import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils"; import { atomWithStorage } from "jotai/utils";
/** /**
* Set of folder IDs that are currently expanded in the tree, keyed by search space ID. * Set of folder IDs that are currently expanded in the tree, keyed by workspace ID.
* Persisted to localStorage so expand/collapse state survives page refreshes. * Persisted to localStorage so expand/collapse state survives page refreshes.
*/ */
export const expandedFolderIdsAtom = atomWithStorage<Record<number, number[]>>( export const expandedFolderIdsAtom = atomWithStorage<Record<number, number[]>>(
@ -13,7 +13,7 @@ export const expandedFolderIdsAtom = atomWithStorage<Record<number, number[]>>(
); );
/** /**
* Expanded folder keys for Local filesystem tree, keyed by search space ID. * Expanded folder keys for Local filesystem tree, keyed by workspace ID.
* Persisted so local tree expansion survives remounts/reloads. * Persisted so local tree expansion survives remounts/reloads.
*/ */
export const localExpandedFolderKeysAtom = atomWithStorage<Record<number, string[]>>( export const localExpandedFolderKeysAtom = atomWithStorage<Record<number, string[]>>(

View file

@ -12,7 +12,7 @@ export interface AgentCreatedDocument {
id: number; id: number;
title: string; title: string;
documentType: string; documentType: string;
searchSpaceId: number; workspaceId: number;
folderId: number | null; folderId: number | null;
createdById: string | null; createdById: string | null;
} }

View file

@ -6,7 +6,7 @@ interface EditorPanelState {
kind: "document" | "local_file" | "memory"; kind: "document" | "local_file" | "memory";
documentId: number | null; documentId: number | null;
localFilePath: string | null; localFilePath: string | null;
searchSpaceId: number | null; workspaceId: number | null;
memoryScope: "user" | "team" | null; memoryScope: "user" | "team" | null;
title: string | null; title: string | null;
} }
@ -16,7 +16,7 @@ const initialState: EditorPanelState = {
kind: "document", kind: "document",
documentId: null, documentId: null,
localFilePath: null, localFilePath: null,
searchSpaceId: null, workspaceId: null,
memoryScope: null, memoryScope: null,
title: null, title: null,
}; };
@ -33,18 +33,18 @@ export const openEditorPanelAtom = atom(
get, get,
set, set,
payload: payload:
| { documentId: number; searchSpaceId: number; title?: string; kind?: "document" } | { documentId: number; workspaceId: number; title?: string; kind?: "document" }
| { | {
kind: "local_file"; kind: "local_file";
localFilePath: string; localFilePath: string;
title?: string; title?: string;
searchSpaceId?: number; workspaceId?: number;
} }
| { | {
kind: "memory"; kind: "memory";
memoryScope: "user" | "team"; memoryScope: "user" | "team";
title?: string; title?: string;
searchSpaceId?: number; workspaceId?: number;
} }
) => { ) => {
if (!get(editorPanelAtom).isOpen) { if (!get(editorPanelAtom).isOpen) {
@ -56,7 +56,7 @@ export const openEditorPanelAtom = atom(
kind: "local_file", kind: "local_file",
documentId: null, documentId: null,
localFilePath: payload.localFilePath, localFilePath: payload.localFilePath,
searchSpaceId: payload.searchSpaceId ?? null, workspaceId: payload.workspaceId ?? null,
memoryScope: null, memoryScope: null,
title: payload.title ?? null, title: payload.title ?? null,
}); });
@ -70,7 +70,7 @@ export const openEditorPanelAtom = atom(
kind: "memory", kind: "memory",
documentId: null, documentId: null,
localFilePath: null, localFilePath: null,
searchSpaceId: payload.searchSpaceId ?? null, workspaceId: payload.workspaceId ?? null,
memoryScope: payload.memoryScope, memoryScope: payload.memoryScope,
title: payload.title ?? null, title: payload.title ?? null,
}); });
@ -83,7 +83,7 @@ export const openEditorPanelAtom = atom(
kind: "document", kind: "document",
documentId: payload.documentId, documentId: payload.documentId,
localFilePath: null, localFilePath: null,
searchSpaceId: payload.searchSpaceId, workspaceId: payload.workspaceId,
memoryScope: null, memoryScope: null,
title: payload.title ?? null, title: payload.title ?? null,
}); });

View file

@ -79,7 +79,7 @@ export const acceptInviteMutationAtom = atomWithMutation(() => ({
return invitesApiService.acceptInvite(request); return invitesApiService.acceptInvite(request);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cacheKeys.searchSpaces.all }); queryClient.invalidateQueries({ queryKey: cacheKeys.workspaces.all });
toast.success("Invite accepted successfully"); toast.success("Invite accepted successfully");
}, },
onError: (error: Error) => { onError: (error: Error) => {

View file

@ -4,18 +4,18 @@ import { invitesApiService } from "@/lib/apis/invites-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
export const invitesAtom = atomWithQuery((get) => { export const invitesAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
queryKey: cacheKeys.invites.all(searchSpaceId?.toString() ?? ""), queryKey: cacheKeys.invites.all(workspaceId?.toString() ?? ""),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5 minutes
queryFn: async () => { queryFn: async () => {
if (!searchSpaceId) { if (!workspaceId) {
return []; return [];
} }
return invitesApiService.getInvites({ return invitesApiService.getInvites({
workspace_id: Number(searchSpaceId), workspace_id: Number(workspaceId),
}); });
}, },
}; };

View file

@ -13,10 +13,10 @@ import { queryClient } from "@/lib/query-client/client";
* Create Log Mutation * Create Log Mutation
*/ */
export const createLogMutationAtom = atomWithMutation((get) => { export const createLogMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined), mutationKey: cacheKeys.logs.list(workspaceId ?? undefined),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: CreateLogRequest) => logsApiService.createLog(request), mutationFn: async (request: CreateLogRequest) => logsApiService.createLog(request),
onSuccess: () => { onSuccess: () => {
// Invalidate all log-related queries (list, summary, detail, withQueryParams) // Invalidate all log-related queries (list, summary, detail, withQueryParams)
@ -29,10 +29,10 @@ export const createLogMutationAtom = atomWithMutation((get) => {
* Update Log Mutation * Update Log Mutation
*/ */
export const updateLogMutationAtom = atomWithMutation((get) => { export const updateLogMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined), mutationKey: cacheKeys.logs.list(workspaceId ?? undefined),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async ({ logId, data }: { logId: number; data: UpdateLogRequest }) => mutationFn: async ({ logId, data }: { logId: number; data: UpdateLogRequest }) =>
logsApiService.updateLog(logId, data), logsApiService.updateLog(logId, data),
onSuccess: (_data, variables) => { onSuccess: (_data, variables) => {
@ -45,10 +45,10 @@ export const updateLogMutationAtom = atomWithMutation((get) => {
* Delete Log Mutation * Delete Log Mutation
*/ */
export const deleteLogMutationAtom = atomWithMutation((get) => { export const deleteLogMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: cacheKeys.logs.list(searchSpaceId ?? undefined), mutationKey: cacheKeys.logs.list(workspaceId ?? undefined),
enabled: !!searchSpaceId, enabled: !!workspaceId,
mutationFn: async (request: DeleteLogRequest) => logsApiService.deleteLog(request), mutationFn: async (request: DeleteLogRequest) => logsApiService.deleteLog(request),
onSuccess: (_data, request) => { onSuccess: (_data, request) => {
queryClient.invalidateQueries({ queryKey: ["logs"] }); queryClient.invalidateQueries({ queryKey: ["logs"] });

View file

@ -3,8 +3,8 @@ import { toast } from "sonner";
import type { import type {
DeleteMembershipRequest, DeleteMembershipRequest,
DeleteMembershipResponse, DeleteMembershipResponse,
LeaveSearchSpaceRequest, LeaveWorkspaceRequest,
LeaveSearchSpaceResponse, LeaveWorkspaceResponse,
UpdateMembershipRequest, UpdateMembershipRequest,
UpdateMembershipResponse, UpdateMembershipResponse,
} from "@/contracts/types/members.types"; } from "@/contracts/types/members.types";
@ -48,20 +48,20 @@ export const deleteMemberMutationAtom = atomWithMutation(() => {
}; };
}); });
export const leaveSearchSpaceMutationAtom = atomWithMutation(() => { export const leaveWorkspaceMutationAtom = atomWithMutation(() => {
return { return {
meta: { suppressGlobalErrorToast: true }, meta: { suppressGlobalErrorToast: true },
mutationFn: async (request: LeaveSearchSpaceRequest) => { mutationFn: async (request: LeaveWorkspaceRequest) => {
return membersApiService.leaveSearchSpace(request); return membersApiService.leaveWorkspace(request);
}, },
onSuccess: (_: LeaveSearchSpaceResponse, request: LeaveSearchSpaceRequest) => { onSuccess: (_: LeaveWorkspaceResponse, request: LeaveWorkspaceRequest) => {
toast.success("Successfully left the search space"); toast.success("Successfully left the workspace");
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.members.all(request.workspace_id.toString()), queryKey: cacheKeys.members.all(request.workspace_id.toString()),
}); });
}, },
onError: () => { onError: () => {
toast.error("Failed to leave search space"); toast.error("Failed to leave workspace");
}, },
}; };
}); });

View file

@ -5,37 +5,37 @@ import { membersApiService } from "@/lib/apis/members-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
export const membersAtom = atomWithQuery((get) => { export const membersAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
queryKey: cacheKeys.members.all(searchSpaceId?.toString() ?? ""), queryKey: cacheKeys.members.all(workspaceId?.toString() ?? ""),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 3 * 1000, // 3 seconds - short staleness for live collaboration staleTime: 3 * 1000, // 3 seconds - short staleness for live collaboration
refetchInterval: 2 * 60 * 1000, // 2 minutes refetchInterval: 2 * 60 * 1000, // 2 minutes
queryFn: async () => { queryFn: async () => {
if (!searchSpaceId) { if (!workspaceId) {
return []; return [];
} }
return membersApiService.getMembers({ return membersApiService.getMembers({
workspace_id: Number(searchSpaceId), workspace_id: Number(workspaceId),
}); });
}, },
}; };
}); });
export const myAccessAtom = atomWithQuery((get) => { export const myAccessAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
queryKey: cacheKeys.members.myAccess(searchSpaceId?.toString() ?? ""), queryKey: cacheKeys.members.myAccess(workspaceId?.toString() ?? ""),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5 minutes
queryFn: async () => { queryFn: async () => {
if (!searchSpaceId) { if (!workspaceId) {
return null; return null;
} }
return membersApiService.getMyAccess({ return membersApiService.getMyAccess({
workspace_id: Number(searchSpaceId), workspace_id: Number(workspaceId),
}); });
}, },
}; };

View file

@ -18,18 +18,18 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queryClient } from "@/lib/query-client/client"; import { queryClient } from "@/lib/query-client/client";
import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms"; import { activeWorkspaceIdAtom } from "../workspaces/workspace-query.atoms";
function invalidateModelConnections(searchSpaceId: number) { function invalidateModelConnections(workspaceId: number) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.modelConnections.all(searchSpaceId), queryKey: cacheKeys.modelConnections.all(workspaceId),
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.modelConnections.roles(searchSpaceId), queryKey: cacheKeys.modelConnections.roles(workspaceId),
}); });
} }
function upsertModelConnection(searchSpaceId: number, connection: ConnectionRead) { function upsertModelConnection(workspaceId: number, connection: ConnectionRead) {
queryClient.setQueryData<ConnectionRead[]>( queryClient.setQueryData<ConnectionRead[]>(
cacheKeys.modelConnections.all(searchSpaceId), cacheKeys.modelConnections.all(workspaceId),
(current = []) => { (current = []) => {
if (current.some((item) => item.id === connection.id)) { if (current.some((item) => item.id === connection.id)) {
return current.map((item) => (item.id === connection.id ? connection : item)); return current.map((item) => (item.id === connection.id ? connection : item));
@ -40,19 +40,19 @@ function upsertModelConnection(searchSpaceId: number, connection: ConnectionRead
} }
export const createModelConnectionMutationAtom = atomWithMutation((get) => { export const createModelConnectionMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["model-connections", "create"], mutationKey: ["model-connections", "create"],
mutationFn: (request: ConnectionCreateRequest) => mutationFn: (request: ConnectionCreateRequest) =>
modelConnectionsApiService.createConnection(request), modelConnectionsApiService.createConnection(request),
onSuccess: (connection: ConnectionRead, request: ConnectionCreateRequest) => { onSuccess: (connection: ConnectionRead, request: ConnectionCreateRequest) => {
const resolvedSearchSpaceId = Number( const resolvedWorkspaceId = Number(
request.workspace_id ?? connection.workspace_id ?? searchSpaceId request.workspace_id ?? connection.workspace_id ?? workspaceId
); );
toast.success("Connection created"); toast.success("Connection created");
if (resolvedSearchSpaceId > 0) { if (resolvedWorkspaceId > 0) {
upsertModelConnection(resolvedSearchSpaceId, connection); upsertModelConnection(resolvedWorkspaceId, connection);
invalidateModelConnections(resolvedSearchSpaceId); invalidateModelConnections(resolvedWorkspaceId);
} }
}, },
onError: (error: Error) => toast.error(error.message || "Failed to create connection"), onError: (error: Error) => toast.error(error.message || "Failed to create connection"),
@ -60,34 +60,34 @@ export const createModelConnectionMutationAtom = atomWithMutation((get) => {
}); });
export const updateModelConnectionMutationAtom = atomWithMutation((get) => { export const updateModelConnectionMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["model-connections", "update"], mutationKey: ["model-connections", "update"],
mutationFn: ({ id, data }: { id: number; data: ConnectionUpdateRequest }) => mutationFn: ({ id, data }: { id: number; data: ConnectionUpdateRequest }) =>
modelConnectionsApiService.updateConnection(id, data), modelConnectionsApiService.updateConnection(id, data),
onSuccess: () => { onSuccess: () => {
toast.success("Connection updated"); toast.success("Connection updated");
invalidateModelConnections(searchSpaceId); invalidateModelConnections(workspaceId);
}, },
onError: (error: Error) => toast.error(error.message || "Failed to update connection"), onError: (error: Error) => toast.error(error.message || "Failed to update connection"),
}; };
}); });
export const deleteModelConnectionMutationAtom = atomWithMutation((get) => { export const deleteModelConnectionMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["model-connections", "delete"], mutationKey: ["model-connections", "delete"],
mutationFn: (id: number) => modelConnectionsApiService.deleteConnection(id), mutationFn: (id: number) => modelConnectionsApiService.deleteConnection(id),
onSuccess: () => { onSuccess: () => {
toast.success("Connection deleted"); toast.success("Connection deleted");
invalidateModelConnections(searchSpaceId); invalidateModelConnections(workspaceId);
}, },
onError: (error: Error) => toast.error(error.message || "Failed to delete connection"), onError: (error: Error) => toast.error(error.message || "Failed to delete connection"),
}; };
}); });
export const verifyModelConnectionMutationAtom = atomWithMutation((get) => { export const verifyModelConnectionMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["model-connections", "verify"], mutationKey: ["model-connections", "verify"],
mutationFn: (id: number) => modelConnectionsApiService.verifyConnection(id), mutationFn: (id: number) => modelConnectionsApiService.verifyConnection(id),
@ -103,14 +103,14 @@ export const verifyModelConnectionMutationAtom = atomWithMutation((get) => {
: "Couldn't list models. Chat may still work — add model IDs manually." : "Couldn't list models. Chat may still work — add model IDs manually."
); );
} }
invalidateModelConnections(searchSpaceId); invalidateModelConnections(workspaceId);
}, },
onError: (error: Error) => toast.error(error.message || "Failed to verify connection"), onError: (error: Error) => toast.error(error.message || "Failed to verify connection"),
}; };
}); });
export const discoverConnectionModelsMutationAtom = atomWithMutation((get) => { export const discoverConnectionModelsMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["model-connections", "discover"], mutationKey: ["model-connections", "discover"],
mutationFn: (id: number) => modelConnectionsApiService.discoverModels(id), mutationFn: (id: number) => modelConnectionsApiService.discoverModels(id),
@ -118,7 +118,7 @@ export const discoverConnectionModelsMutationAtom = atomWithMutation((get) => {
toast.success( toast.success(
models.length ? `${models.length} models discovered` : "No models found for this connection" models.length ? `${models.length} models discovered` : "No models found for this connection"
); );
invalidateModelConnections(searchSpaceId); invalidateModelConnections(workspaceId);
}, },
onError: (error: Error) => toast.error(error.message || "Failed to discover models"), onError: (error: Error) => toast.error(error.message || "Failed to discover models"),
}; };
@ -149,64 +149,64 @@ export const testPreviewModelMutationAtom = atomWithMutation(() => {
}); });
export const addManualModelMutationAtom = atomWithMutation((get) => { export const addManualModelMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["models", "add-manual"], mutationKey: ["models", "add-manual"],
mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelCreateRequest }) => mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelCreateRequest }) =>
modelConnectionsApiService.addManualModel(connectionId, data), modelConnectionsApiService.addManualModel(connectionId, data),
onSuccess: () => { onSuccess: () => {
toast.success("Model added"); toast.success("Model added");
invalidateModelConnections(searchSpaceId); invalidateModelConnections(workspaceId);
}, },
onError: (error: Error) => toast.error(error.message || "Failed to add model"), onError: (error: Error) => toast.error(error.message || "Failed to add model"),
}; };
}); });
export const updateModelMutationAtom = atomWithMutation((get) => { export const updateModelMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["models", "update"], mutationKey: ["models", "update"],
mutationFn: ({ id, data }: { id: number; data: ModelUpdateRequest }) => mutationFn: ({ id, data }: { id: number; data: ModelUpdateRequest }) =>
modelConnectionsApiService.updateModel(id, data), modelConnectionsApiService.updateModel(id, data),
onSuccess: () => invalidateModelConnections(searchSpaceId), onSuccess: () => invalidateModelConnections(workspaceId),
onError: (error: Error) => toast.error(error.message || "Failed to update model"), onError: (error: Error) => toast.error(error.message || "Failed to update model"),
}; };
}); });
export const bulkUpdateModelsMutationAtom = atomWithMutation((get) => { export const bulkUpdateModelsMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["models", "bulk-update"], mutationKey: ["models", "bulk-update"],
mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelsBulkUpdateRequest }) => mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelsBulkUpdateRequest }) =>
modelConnectionsApiService.bulkUpdateModels(connectionId, data), modelConnectionsApiService.bulkUpdateModels(connectionId, data),
onSuccess: () => invalidateModelConnections(searchSpaceId), onSuccess: () => invalidateModelConnections(workspaceId),
onError: (error: Error) => toast.error(error.message || "Failed to update models"), onError: (error: Error) => toast.error(error.message || "Failed to update models"),
}; };
}); });
export const testModelMutationAtom = atomWithMutation((get) => { export const testModelMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["models", "test"], mutationKey: ["models", "test"],
mutationFn: (id: number) => modelConnectionsApiService.testModel(id), mutationFn: (id: number) => modelConnectionsApiService.testModel(id),
onSuccess: (result: VerifyConnectionResponse) => { onSuccess: (result: VerifyConnectionResponse) => {
if (result.ok) toast.success("Model test succeeded"); if (result.ok) toast.success("Model test succeeded");
else toast.error(result.message || "Model test failed"); else toast.error(result.message || "Model test failed");
invalidateModelConnections(searchSpaceId); invalidateModelConnections(workspaceId);
}, },
onError: (error: Error) => toast.error(error.message || "Failed to test model"), onError: (error: Error) => toast.error(error.message || "Failed to test model"),
}; };
}); });
export const updateModelRolesMutationAtom = atomWithMutation((get) => { export const updateModelRolesMutationAtom = atomWithMutation((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
mutationKey: ["model-roles", "update"], mutationKey: ["model-roles", "update"],
mutationFn: (roles: ModelRoles) => mutationFn: (roles: ModelRoles) =>
modelConnectionsApiService.updateModelRoles(searchSpaceId, roles), modelConnectionsApiService.updateModelRoles(workspaceId, roles),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.modelConnections.roles(searchSpaceId), queryKey: cacheKeys.modelConnections.roles(workspaceId),
}); });
}, },
onError: (error: Error) => toast.error(error.message || "Failed to update model roles"), onError: (error: Error) => toast.error(error.message || "Failed to update model roles"),

View file

@ -26,21 +26,21 @@ export const modelProvidersAtom = atomWithQuery(() => ({
})); }));
export const modelConnectionsAtom = atomWithQuery((get) => { export const modelConnectionsAtom = atomWithQuery((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
queryKey: cacheKeys.modelConnections.all(searchSpaceId), queryKey: cacheKeys.modelConnections.all(workspaceId),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
queryFn: () => modelConnectionsApiService.getConnections(searchSpaceId), queryFn: () => modelConnectionsApiService.getConnections(workspaceId),
}; };
}); });
export const modelRolesAtom = atomWithQuery((get) => { export const modelRolesAtom = atomWithQuery((get) => {
const searchSpaceId = Number(get(activeWorkspaceIdAtom)); const workspaceId = Number(get(activeWorkspaceIdAtom));
return { return {
queryKey: cacheKeys.modelConnections.roles(searchSpaceId), queryKey: cacheKeys.modelConnections.roles(workspaceId),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
queryFn: () => modelConnectionsApiService.getModelRoles(searchSpaceId), queryFn: () => modelConnectionsApiService.getModelRoles(workspaceId),
}; };
}); });

View file

@ -4,18 +4,18 @@ import { chatThreadsApiService } from "@/lib/apis/chat-threads-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
export const publicChatSnapshotsAtom = atomWithQuery((get) => { export const publicChatSnapshotsAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeWorkspaceIdAtom); const workspaceId = get(activeWorkspaceIdAtom);
return { return {
queryKey: cacheKeys.publicChatSnapshots.bySearchSpace(Number(searchSpaceId) || 0), queryKey: cacheKeys.publicChatSnapshots.byWorkspace(Number(workspaceId) || 0),
enabled: !!searchSpaceId, enabled: !!workspaceId,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
queryFn: async () => { queryFn: async () => {
if (!searchSpaceId) { if (!workspaceId) {
return { snapshots: [] }; return { snapshots: [] };
} }
return chatThreadsApiService.listPublicChatSnapshotsForSearchSpace({ return chatThreadsApiService.listPublicChatSnapshotsForWorkspace({
workspace_id: Number(searchSpaceId), workspace_id: Number(workspaceId),
}); });
}, },
}; };

View file

@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { migrateLegacyTabs } from "./migrate-tabs";
// Run with: pnpm exec tsx --test atoms/tabs/migrate-tabs.test.ts
test("maps legacy searchSpaceId to workspaceId on read", () => {
const migrated = migrateLegacyTabs({
tabs: [{ id: "chat-new", type: "chat", searchSpaceId: 7 } as never],
activeTabId: "chat-new",
});
const tab = migrated.tabs[0] as { workspaceId?: number };
assert.equal(tab.workspaceId, 7);
});
test("leaves an already-migrated workspaceId untouched", () => {
const migrated = migrateLegacyTabs({
tabs: [{ id: "d1", type: "document", workspaceId: 3, searchSpaceId: 9 } as never],
});
const tab = migrated.tabs[0] as { workspaceId?: number };
assert.equal(tab.workspaceId, 3);
});

View file

@ -0,0 +1,19 @@
/**
* One-time read-migration for persisted tabs: legacy state stored the workspace
* as `searchSpaceId`. Map it to `workspaceId` on read so already-open tabs keep
* their workspace association after the rename. Pure + dependency-free so it can
* be unit-checked without loading the atom module.
*/
export function migrateLegacyTabs<T extends { tabs: Array<{ workspaceId?: number }> }>(
state: T
): T {
return {
...state,
tabs: state.tabs.map((t) => {
const legacy = t as { workspaceId?: number; searchSpaceId?: number };
return legacy.workspaceId === undefined && legacy.searchSpaceId !== undefined
? { ...t, workspaceId: legacy.searchSpaceId }
: t;
}),
};
}

View file

@ -1,6 +1,7 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { atomWithStorage, createJSONStorage } from "jotai/utils"; import { atomWithStorage, createJSONStorage } from "jotai/utils";
import type { ChatVisibility } from "@/lib/chat/thread-persistence"; import type { ChatVisibility } from "@/lib/chat/thread-persistence";
import { migrateLegacyTabs } from "./migrate-tabs";
export type TabType = "chat" | "document"; export type TabType = "chat" | "document";
@ -15,7 +16,7 @@ export interface Tab {
hasComments?: boolean; hasComments?: boolean;
/** For document tabs */ /** For document tabs */
documentId?: number; documentId?: number;
searchSpaceId?: number; workspaceId?: number;
} }
interface TabsState { interface TabsState {
@ -40,11 +41,17 @@ const initialState: TabsState = {
const deletedChatIdsAtom = atom<Set<number>>(new Set<number>()); const deletedChatIdsAtom = atom<Set<number>>(new Set<number>());
// Persist tabs in localStorage so they survive a hard refresh and let the user // Persist tabs in localStorage so they survive a hard refresh and let the user
// keep tabs open across multiple search spaces (browser-like behavior). // keep tabs open across multiple workspaces (browser-like behavior).
const localStorageAdapter = createJSONStorage<TabsState>( const localStorageAdapter = createJSONStorage<TabsState>(
() => (typeof window !== "undefined" ? localStorage : undefined) as Storage () => (typeof window !== "undefined" ? localStorage : undefined) as Storage
); );
// Wrap getItem in place so the adapter keeps its original (sync) type while
// migrating legacy persisted state on read.
const baseGetItem = localStorageAdapter.getItem.bind(localStorageAdapter);
localStorageAdapter.getItem = (key, initialValue) =>
migrateLegacyTabs(baseGetItem(key, initialValue));
export const tabsStateAtom = atomWithStorage<TabsState>( export const tabsStateAtom = atomWithStorage<TabsState>(
"surfsense:tabs", "surfsense:tabs",
initialState, initialState,
@ -81,14 +88,14 @@ export const syncChatTabAtom = atom(
chatId, chatId,
title, title,
chatUrl, chatUrl,
searchSpaceId, workspaceId,
visibility, visibility,
hasComments, hasComments,
}: { }: {
chatId: number | null; chatId: number | null;
title?: string; title?: string;
chatUrl?: string; chatUrl?: string;
searchSpaceId: number; workspaceId: number;
visibility?: ChatVisibility; visibility?: ChatVisibility;
hasComments?: boolean; hasComments?: boolean;
} }
@ -111,7 +118,7 @@ export const syncChatTabAtom = atom(
...t, ...t,
title: title || t.title, title: title || t.title,
chatUrl: chatUrl || t.chatUrl, chatUrl: chatUrl || t.chatUrl,
searchSpaceId: searchSpaceId ?? t.searchSpaceId, workspaceId: workspaceId ?? t.workspaceId,
...(visibility !== undefined ? { visibility } : {}), ...(visibility !== undefined ? { visibility } : {}),
...(hasComments !== undefined ? { hasComments } : {}), ...(hasComments !== undefined ? { hasComments } : {}),
} }
@ -122,18 +129,18 @@ export const syncChatTabAtom = atom(
} }
// If navigating to a new chat (no chatId), ensure there's a "new chat" tab // If navigating to a new chat (no chatId), ensure there's a "new chat" tab
// scoped to the current search space. // scoped to the current workspace.
if (!chatId) { if (!chatId) {
const hasNewChatTab = state.tabs.some((t) => t.id === "chat-new"); const hasNewChatTab = state.tabs.some((t) => t.id === "chat-new");
if (hasNewChatTab) { if (hasNewChatTab) {
set(tabsStateAtom, { set(tabsStateAtom, {
...state, ...state,
activeTabId: "chat-new", activeTabId: "chat-new",
tabs: state.tabs.map((t) => (t.id === "chat-new" ? { ...t, searchSpaceId, chatUrl } : t)), tabs: state.tabs.map((t) => (t.id === "chat-new" ? { ...t, workspaceId, chatUrl } : t)),
}); });
} else { } else {
set(tabsStateAtom, { set(tabsStateAtom, {
tabs: [...state.tabs, { ...INITIAL_CHAT_TAB, searchSpaceId, chatUrl }], tabs: [...state.tabs, { ...INITIAL_CHAT_TAB, workspaceId, chatUrl }],
activeTabId: "chat-new", activeTabId: "chat-new",
}); });
} }
@ -148,7 +155,7 @@ export const syncChatTabAtom = atom(
title: title || "New Chat", title: title || "New Chat",
chatId, chatId,
chatUrl, chatUrl,
searchSpaceId, workspaceId,
...(visibility !== undefined ? { visibility } : {}), ...(visibility !== undefined ? { visibility } : {}),
...(hasComments !== undefined ? { hasComments } : {}), ...(hasComments !== undefined ? { hasComments } : {}),
}; };
@ -197,11 +204,7 @@ export const openDocumentTabAtom = atom(
( (
get, get,
set, set,
{ { documentId, workspaceId, title }: { documentId: number; workspaceId: number; title?: string }
documentId,
searchSpaceId,
title,
}: { documentId: number; searchSpaceId: number; title?: string }
) => { ) => {
const state = get(tabsStateAtom); const state = get(tabsStateAtom);
const tabId = makeDocumentTabId(documentId); const tabId = makeDocumentTabId(documentId);
@ -221,7 +224,7 @@ export const openDocumentTabAtom = atom(
type: "document", type: "document",
title: title || `Document ${documentId}`, title: title || `Document ${documentId}`,
documentId, documentId,
searchSpaceId, workspaceId,
}; };
set(tabsStateAtom, { set(tabsStateAtom, {
@ -300,7 +303,7 @@ export const removeChatTabAtom = atom(null, (get, set, chatId: number) => {
return remaining.find((t) => t.id === newActiveId) ?? null; return remaining.find((t) => t.id === newActiveId) ?? null;
}); });
/** Reset tabs when switching search spaces. */ /** Reset tabs when switching workspaces. */
export const resetTabsAtom = atom(null, (_get, set) => { export const resetTabsAtom = atom(null, (_get, set) => {
set(tabsStateAtom, { ...initialState }); set(tabsStateAtom, { ...initialState });
set(deletedChatIdsAtom, new Set<number>()); set(deletedChatIdsAtom, new Set<number>());

View file

@ -1,96 +1,96 @@
import { atomWithMutation } from "jotai-tanstack-query"; import { atomWithMutation } from "jotai-tanstack-query";
import { toast } from "sonner"; import { toast } from "sonner";
import type { import type {
CreateSearchSpaceRequest, CreateWorkspaceRequest,
DeleteSearchSpaceRequest, DeleteWorkspaceRequest,
UpdateSearchSpaceApiAccessRequest, UpdateWorkspaceApiAccessRequest,
UpdateSearchSpaceRequest, UpdateWorkspaceRequest,
} from "@/contracts/types/workspace.types"; } from "@/contracts/types/workspace.types";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service"; import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
import { queryClient } from "@/lib/query-client/client"; import { queryClient } from "@/lib/query-client/client";
import { activeWorkspaceIdAtom } from "./workspace-query.atoms"; import { activeWorkspaceIdAtom } from "./workspace-query.atoms";
export const createSearchSpaceMutationAtom = atomWithMutation(() => { export const createWorkspaceMutationAtom = atomWithMutation(() => {
return { return {
mutationKey: ["create-search-space"], mutationKey: ["create-workspace"],
mutationFn: async (request: CreateSearchSpaceRequest) => { mutationFn: async (request: CreateWorkspaceRequest) => {
return searchSpacesApiService.createSearchSpace(request); return workspacesApiService.createWorkspace(request);
}, },
onSuccess: () => { onSuccess: () => {
toast.success("Search space created successfully"); toast.success("Search space created successfully");
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.all, queryKey: cacheKeys.workspaces.all,
}); });
}, },
}; };
}); });
export const updateSearchSpaceMutationAtom = atomWithMutation((get) => { export const updateWorkspaceMutationAtom = atomWithMutation((get) => {
const activeSearchSpaceId = get(activeWorkspaceIdAtom); const activeWorkspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: ["update-search-space", activeSearchSpaceId], mutationKey: ["update-workspace", activeWorkspaceId],
enabled: !!activeSearchSpaceId, enabled: !!activeWorkspaceId,
mutationFn: async (request: UpdateSearchSpaceRequest) => { mutationFn: async (request: UpdateWorkspaceRequest) => {
return searchSpacesApiService.updateSearchSpace(request); return workspacesApiService.updateWorkspace(request);
}, },
onSuccess: (_, request: UpdateSearchSpaceRequest) => { onSuccess: (_, request: UpdateWorkspaceRequest) => {
toast.success("Search space updated successfully"); toast.success("Search space updated successfully");
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.all, queryKey: cacheKeys.workspaces.all,
}); });
if (request.id) { if (request.id) {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.detail(String(request.id)), queryKey: cacheKeys.workspaces.detail(String(request.id)),
}); });
} }
}, },
}; };
}); });
export const updateSearchSpaceApiAccessMutationAtom = atomWithMutation((get) => { export const updateWorkspaceApiAccessMutationAtom = atomWithMutation((get) => {
const activeSearchSpaceId = get(activeWorkspaceIdAtom); const activeWorkspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: ["update-search-space-api-access", activeSearchSpaceId], mutationKey: ["update-workspace-api-access", activeWorkspaceId],
enabled: !!activeSearchSpaceId, enabled: !!activeWorkspaceId,
mutationFn: async (request: UpdateSearchSpaceApiAccessRequest) => { mutationFn: async (request: UpdateWorkspaceApiAccessRequest) => {
return searchSpacesApiService.updateSearchSpaceApiAccess(request); return workspacesApiService.updateWorkspaceApiAccess(request);
}, },
onSuccess: (_, request: UpdateSearchSpaceApiAccessRequest) => { onSuccess: (_, request: UpdateWorkspaceApiAccessRequest) => {
toast.success("API access updated successfully"); toast.success("API access updated successfully");
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.all, queryKey: cacheKeys.workspaces.all,
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.detail(String(request.id)), queryKey: cacheKeys.workspaces.detail(String(request.id)),
}); });
}, },
}; };
}); });
export const deleteSearchSpaceMutationAtom = atomWithMutation((get) => { export const deleteWorkspaceMutationAtom = atomWithMutation((get) => {
const activeSearchSpaceId = get(activeWorkspaceIdAtom); const activeWorkspaceId = get(activeWorkspaceIdAtom);
return { return {
mutationKey: ["delete-search-space", activeSearchSpaceId], mutationKey: ["delete-workspace", activeWorkspaceId],
enabled: !!activeSearchSpaceId, enabled: !!activeWorkspaceId,
mutationFn: async (request: DeleteSearchSpaceRequest) => { mutationFn: async (request: DeleteWorkspaceRequest) => {
return searchSpacesApiService.deleteSearchSpace(request); return workspacesApiService.deleteWorkspace(request);
}, },
onSuccess: (_, request: DeleteSearchSpaceRequest) => { onSuccess: (_, request: DeleteWorkspaceRequest) => {
toast.success("Search space deleted successfully"); toast.success("Search space deleted successfully");
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.searchSpaces.all, queryKey: cacheKeys.workspaces.all,
}); });
if (request.id) { if (request.id) {
queryClient.removeQueries({ queryClient.removeQueries({
queryKey: cacheKeys.searchSpaces.detail(String(request.id)), queryKey: cacheKeys.workspaces.detail(String(request.id)),
}); });
} }
}, },

View file

@ -1,25 +1,25 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query"; import { atomWithQuery } from "jotai-tanstack-query";
import type { GetSearchSpacesRequest } from "@/contracts/types/workspace.types"; import type { GetWorkspacesRequest } from "@/contracts/types/workspace.types";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service"; import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cacheKeys } from "@/lib/query-client/cache-keys";
export const activeWorkspaceIdAtom = atom<string | null>(null); export const activeWorkspaceIdAtom = atom<string | null>(null);
export const searchSpacesQueryParamsAtom = atom<GetSearchSpacesRequest["queryParams"]>({ export const workspacesQueryParamsAtom = atom<GetWorkspacesRequest["queryParams"]>({
skip: 0, skip: 0,
limit: 10, limit: 10,
owned_only: false, owned_only: false,
}); });
export const searchSpacesAtom = atomWithQuery((get) => { export const workspacesAtom = atomWithQuery((get) => {
const queryParams = get(searchSpacesQueryParamsAtom); const queryParams = get(workspacesQueryParamsAtom);
return { return {
queryKey: cacheKeys.searchSpaces.withQueryParams(queryParams), queryKey: cacheKeys.workspaces.withQueryParams(queryParams),
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
queryFn: async () => { queryFn: async () => {
return searchSpacesApiService.getSearchSpaces({ return workspacesApiService.getWorkspaces({
queryParams, queryParams,
}); });
}, },

View file

@ -14,7 +14,7 @@ This release brings major improvements to **collaboration and user experience**.
#### New Chat-First Interface #### New Chat-First Interface
- **Dashboard Removed**: Users now land directly in a chat for faster access - **Dashboard Removed**: Users now land directly in a chat for faster access
- **Redesigned Search Spaces**: Moved to a left column with color-coded icons and hover tooltips - **Redesigned Workspaces**: Moved to a left column with color-coded icons and hover tooltips
- **Collapsible Sidebar**: New sidebar design with collapsible sections for private and group chats - **Collapsible Sidebar**: New sidebar design with collapsible sections for private and group chats
- **Streamlined Settings**: Accessible through intuitive dropdown menus - **Streamlined Settings**: Accessible through intuitive dropdown menus
- **Mobile-Responsive Design**: Better experience on all devices - **Mobile-Responsive Design**: Better experience on all devices

View file

@ -16,8 +16,8 @@ This update brings **public sharing, image generation**, a redesigned Documents
#### Public Sharing #### Public Sharing
- **Public Chats**: Share snapshots of chats via public links. - **Public Chats**: Share snapshots of chats via public links.
- **Sharing Permissions**: Search Space owners control who can create and manage public links. - **Sharing Permissions**: Workspace owners control who can create and manage public links.
- **Link Management Page**: View and revoke all public chats from Search Space Settings. - **Link Management Page**: View and revoke all public chats from Workspace Settings.
#### Auto (Load Balanced) Mode #### Auto (Load Balanced) Mode

View file

@ -80,7 +80,7 @@ SurfSense now treats every sensitive AI action as an explicit, reviewable step.
- **Mobile-First Polish**: Mobile citation drawer, long-press actions in chats and documents, a responsive documents sidebar, and a mobile-friendly onboarding tour. - **Mobile-First Polish**: Mobile citation drawer, long-press actions in chats and documents, a responsive documents sidebar, and a mobile-friendly onboarding tour.
- **Reworked Composer**: Tool actions are grouped into a cleaner menu with better icons, plus a helpful "connect tools" banner. - **Reworked Composer**: Tool actions are grouped into a cleaner menu with better icons, plus a helpful "connect tools" banner.
- **Settings & Team**: New tabbed user settings page (profile + API keys), team roles management with pagination, and a search space settings dialog. - **Settings & Team**: New tabbed user settings page (profile + API keys), team roles management with pagination, and a workspace settings dialog.
- **Right Panel & Docked Sidebar**: A tabbed Sources/Report panel with smooth transitions, plus an optional docked documents sidebar. - **Right Panel & Docked Sidebar**: A tabbed Sources/Report panel with smooth transitions, plus an optional docked documents sidebar.
- **Community Prompts**: Public prompt library with copy support, inline share toggles, and a see-more/less toggle for long prompts. - **Community Prompts**: Public prompt library with copy support, inline share toggles, and a see-more/less toggle for long prompts.
- **New Homepage**: Smooth scrolling, a use-cases grid, an updated walkthrough hero, a GitHub stars badge, and a new carousel for AI-generated video. - **New Homepage**: Smooth scrolling, a use-cases grid, an updated walkthrough hero, a GitHub stars badge, and a new carousel for AI-generated video.

View file

@ -44,7 +44,7 @@ The SurfSense desktop app becomes a serious always-on **AI like ChatGPT** that a
- **Multi-Suggestion UI**: The suggestion popup now offers up to **3 options** to pick from, with clean cards and quick-assist detection. - **Multi-Suggestion UI**: The suggestion popup now offers up to **3 options** to pick from, with clean cards and quick-assist detection.
- **Knowledge Base Grounding**: Suggestions are grounded in your connected knowledge base, not just generic model output. - **Knowledge Base Grounding**: Suggestions are grounded in your connected knowledge base, not just generic model output.
- **macOS Permission Onboarding**: A clearer onboarding page walks macOS users through the permissions the app needs, only when they're actually needed. - **macOS Permission Onboarding**: A clearer onboarding page walks macOS users through the permissions the app needs, only when they're actually needed.
- **Switch Search Spaces from the Overlay**: Change the active search space without opening the main app. - **Switch Workspaces from the Overlay**: Change the active workspace without opening the main app.
- **General Assist**: A new general-purpose assist mode with cleaner shortcut icons and descriptions. - **General Assist**: A new general-purpose assist mode with cleaner shortcut icons and descriptions.
- **Stay Signed In Everywhere**: Sign-in is now synchronized between the desktop app and the web app. - **Stay Signed In Everywhere**: Sign-in is now synchronized between the desktop app and the web app.
- **Vision Model Settings**: A dedicated Vision Models tab in Settings lets you pick and manage vision models, including a dynamic model list from OpenRouter. - **Vision Model Settings**: A dedicated Vision Models tab in Settings lets you pick and manage vision models, including a dynamic model list from OpenRouter.

View file

@ -21,7 +21,7 @@ Turn your knowledge and profile details into export-ready resume documents.
- **Cross-Site Anonymous Chat Cookies**: Anonymous chat cookies now adapt their SameSite and Secure settings based on deployment context, making hosted and cross-domain setups more reliable. - **Cross-Site Anonymous Chat Cookies**: Anonymous chat cookies now adapt their SameSite and Secure settings based on deployment context, making hosted and cross-domain setups more reliable.
- **Better Anonymous Chat History**: Message history handling in anonymous chat is more dependable, especially when users move between public chat states. - **Better Anonymous Chat History**: Message history handling in anonymous chat is more dependable, especially when users move between public chat states.
- **Safer Form Inputs**: Login, registration, profile, search space, and role forms now enforce sensible max-length limits directly in the UI. - **Safer Form Inputs**: Login, registration, profile, workspace, and role forms now enforce sensible max-length limits directly in the UI.
- **Cleaner Page Landmarks**: Home and free-chat pages no longer nest main landmarks, improving HTML semantics and screen-reader navigation. - **Cleaner Page Landmarks**: Home and free-chat pages no longer nest main landmarks, improving HTML semantics and screen-reader navigation.
- **SEO Metadata Refresh**: Titles and descriptions across key pages now better communicate SurfSense's open-source, privacy-focused positioning. - **SEO Metadata Refresh**: Titles and descriptions across key pages now better communicate SurfSense's open-source, privacy-focused positioning.

View file

@ -491,7 +491,7 @@ export const AssistantMessage: FC = () => {
const commentPanelRef = useRef<HTMLDivElement>(null); const commentPanelRef = useRef<HTMLDivElement>(null);
const commentTriggerRef = useRef<HTMLButtonElement>(null); const commentTriggerRef = useRef<HTMLButtonElement>(null);
const messageId = useAuiState(({ message }) => message?.id); const messageId = useAuiState(({ message }) => message?.id);
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom); const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const dbMessageId = parseMessageId(messageId); const dbMessageId = parseMessageId(messageId);
const commentsEnabled = useAtomValue(commentsEnabledAtom); const commentsEnabled = useAtomValue(commentsEnabledAtom);
@ -520,7 +520,7 @@ export const AssistantMessage: FC = () => {
const commentCount = commentsData?.total_count ?? 0; const commentCount = commentsData?.total_count ?? 0;
const hasComments = commentCount > 0; const hasComments = commentCount > 0;
const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId; const showCommentTrigger = workspaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
// Close floating panel when clicking outside (but not on portaled popover/dropdown content) // Close floating panel when clicking outside (but not on portaled popover/dropdown content)
useEffect(() => { useEffect(() => {

View file

@ -36,10 +36,10 @@ interface ConnectorIndicatorProps {
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>( export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
(_props, ref) => { (_props, ref) => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom); const workspaceId = useAtomValue(activeWorkspaceIdAtom);
// Real-time document type counts via Zero (updates instantly as docs are indexed) // Real-time document type counts via Zero (updates instantly as docs are indexed)
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId); const documentTypeCounts = useZeroDocumentTypeCounts(workspaceId);
// Read status inbox items from shared atom (populated by LayoutDataProvider) // Read status inbox items from shared atom (populated by LayoutDataProvider)
// instead of creating a duplicate useInbox("status") hook. // instead of creating a duplicate useInbox("status") hook.
const statusInboxItems = useAtomValue(statusInboxItemsAtom); const statusInboxItems = useAtomValue(statusInboxItemsAtom);
@ -124,7 +124,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
loading: connectorsLoading, loading: connectorsLoading,
error: connectorsError, error: connectorsError,
refreshConnectors: refreshConnectorsSync, refreshConnectors: refreshConnectorsSync,
} = useConnectorsSync(searchSpaceId); } = useConnectorsSync(workspaceId);
const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError); const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError);
const connectors = useSyncData ? connectorsFromSync : allConnectors || []; const connectors = useSyncData ? connectorsFromSync : allConnectors || [];
@ -142,7 +142,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
inboxItems inboxItems
); );
// Get document types that have documents in the search space // Get document types that have documents in the workspace
const activeDocumentTypes = documentTypeCounts const activeDocumentTypes = documentTypeCounts
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0) ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
: []; : [];
@ -163,7 +163,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
open: () => handleOpenChange(true), open: () => handleOpenChange(true),
})); }));
if (!searchSpaceId) return null; if (!workspaceId) return null;
return ( return (
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}> <Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
@ -191,8 +191,8 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
> >
<DialogTitle className="sr-only">Manage Connectors</DialogTitle> <DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */} {/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? ( {isYouTubeView && workspaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} /> <YouTubeCrawlerView workspaceId={workspaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? ( ) : viewingMCPList ? (
<ConnectorAccountsListView <ConnectorAccountsListView
connectorType="MCP_CONNECTOR" connectorType="MCP_CONNECTOR"
@ -253,7 +253,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
isSaving={isSaving} isSaving={isSaving}
isDisconnecting={isDisconnecting} isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)} isIndexing={indexingConnectorIds.has(editingConnector.id)}
searchSpaceId={searchSpaceId?.toString()} workspaceId={workspaceId?.toString()}
onStartDateChange={setStartDate} onStartDateChange={setStartDate}
onEndDateChange={setEndDate} onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled} onPeriodicEnabledChange={setPeriodicEnabled}
@ -346,7 +346,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
<TabsContent value="all" className="m-0"> <TabsContent value="all" className="m-0">
<AllConnectorsTab <AllConnectorsTab
searchQuery={searchQuery} searchQuery={searchQuery}
searchSpaceId={searchSpaceId} workspaceId={workspaceId}
connectedTypes={connectedTypes} connectedTypes={connectedTypes}
connectingId={connectingId} connectingId={connectingId}
allConnectors={connectors} allConnectors={connectors}

View file

@ -159,17 +159,17 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<div className="h-px bg-border/60" /> <div className="h-px bg-border/60" />
{/* Step 4 — Pick search space */} {/* Step 4 — Pick workspace */}
<article> <article>
<header className="mb-3 flex items-center gap-2"> <header className="mb-3 flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium"> <div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
4 4
</div> </div>
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3> <h3 className="text-sm font-medium sm:text-base">Pick this workspace</h3>
</header> </header>
<p className="text-[11px] text-muted-foreground sm:text-xs"> <p className="text-[11px] text-muted-foreground sm:text-xs">
In the plugin's <span className="font-medium">Search space</span> setting, choose the In the plugin's <span className="font-medium">Search space</span> setting, choose the
search space you want this vault to sync into. The connector will appear here workspace you want this vault to sync into. The connector will appear here
automatically once the plugin makes its first sync. automatically once the plugin makes its first sync.
</p> </p>
</article> </article>

View file

@ -7,7 +7,7 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
LINEAR_CONNECTOR: [ LINEAR_CONNECTOR: [
"Search through all your Linear issues and comments", "Search through all your Linear issues and comments",
"Access issue titles, descriptions, and full discussion threads", "Access issue titles, descriptions, and full discussion threads",
"Connect your team's project management directly to your search space", "Connect your team's project management directly to your workspace",
"Keep your search results up-to-date with latest Linear content", "Keep your search results up-to-date with latest Linear content",
"Index your Linear issues for enhanced search capabilities", "Index your Linear issues for enhanced search capabilities",
], ],
@ -36,63 +36,63 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
SLACK_CONNECTOR: [ SLACK_CONNECTOR: [
"Search through all your Slack messages and conversations", "Search through all your Slack messages and conversations",
"Access messages from public and private channels", "Access messages from public and private channels",
"Connect your team's communications directly to your search space", "Connect your team's communications directly to your workspace",
"Keep your search results up-to-date with latest Slack content", "Keep your search results up-to-date with latest Slack content",
"Index your Slack conversations for enhanced search capabilities", "Index your Slack conversations for enhanced search capabilities",
], ],
DISCORD_CONNECTOR: [ DISCORD_CONNECTOR: [
"Search through all your Discord messages and conversations", "Search through all your Discord messages and conversations",
"Access messages from all accessible channels", "Access messages from all accessible channels",
"Connect your community's communications directly to your search space", "Connect your community's communications directly to your workspace",
"Keep your search results up-to-date with latest Discord content", "Keep your search results up-to-date with latest Discord content",
"Index your Discord conversations for enhanced search capabilities", "Index your Discord conversations for enhanced search capabilities",
], ],
NOTION_CONNECTOR: [ NOTION_CONNECTOR: [
"Search through all your Notion pages and databases", "Search through all your Notion pages and databases",
"Access page content, properties, and metadata", "Access page content, properties, and metadata",
"Connect your knowledge base directly to your search space", "Connect your knowledge base directly to your workspace",
"Keep your search results up-to-date with latest Notion content", "Keep your search results up-to-date with latest Notion content",
"Index your Notion workspace for enhanced search capabilities", "Index your Notion workspace for enhanced search capabilities",
], ],
CONFLUENCE_CONNECTOR: [ CONFLUENCE_CONNECTOR: [
"Search through all your Confluence pages and spaces", "Search through all your Confluence pages and spaces",
"Access page content, comments, and attachments", "Access page content, comments, and attachments",
"Connect your team's documentation directly to your search space", "Connect your team's documentation directly to your workspace",
"Keep your search results up-to-date with latest Confluence content", "Keep your search results up-to-date with latest Confluence content",
"Index your Confluence workspace for enhanced search capabilities", "Index your Confluence workspace for enhanced search capabilities",
], ],
BOOKSTACK_CONNECTOR: [ BOOKSTACK_CONNECTOR: [
"Search through all your BookStack pages and books", "Search through all your BookStack pages and books",
"Access page content, chapters, and documentation", "Access page content, chapters, and documentation",
"Connect your documentation directly to your search space", "Connect your documentation directly to your workspace",
"Keep your search results up-to-date with latest BookStack content", "Keep your search results up-to-date with latest BookStack content",
"Index your BookStack instance for enhanced search capabilities", "Index your BookStack instance for enhanced search capabilities",
], ],
GITHUB_CONNECTOR: [ GITHUB_CONNECTOR: [
"Search through code, issues, and documentation from GitHub repositories", "Search through code, issues, and documentation from GitHub repositories",
"Access repository content, pull requests, and discussions", "Access repository content, pull requests, and discussions",
"Connect your codebase directly to your search space", "Connect your codebase directly to your workspace",
"Keep your search results up-to-date with latest GitHub content", "Keep your search results up-to-date with latest GitHub content",
"Index your GitHub repositories for enhanced search capabilities", "Index your GitHub repositories for enhanced search capabilities",
], ],
JIRA_CONNECTOR: [ JIRA_CONNECTOR: [
"Search through all your Jira issues and tickets", "Search through all your Jira issues and tickets",
"Access issue descriptions, comments, and project data", "Access issue descriptions, comments, and project data",
"Connect your project management directly to your search space", "Connect your project management directly to your workspace",
"Keep your search results up-to-date with latest Jira content", "Keep your search results up-to-date with latest Jira content",
"Index your Jira projects for enhanced search capabilities", "Index your Jira projects for enhanced search capabilities",
], ],
CLICKUP_CONNECTOR: [ CLICKUP_CONNECTOR: [
"Search through all your ClickUp tasks and projects", "Search through all your ClickUp tasks and projects",
"Access task descriptions, comments, and project data", "Access task descriptions, comments, and project data",
"Connect your task management directly to your search space", "Connect your task management directly to your workspace",
"Keep your search results up-to-date with latest ClickUp content", "Keep your search results up-to-date with latest ClickUp content",
"Index your ClickUp workspace for enhanced search capabilities", "Index your ClickUp workspace for enhanced search capabilities",
], ],
LUMA_CONNECTOR: [ LUMA_CONNECTOR: [
"Search through all your Luma events", "Search through all your Luma events",
"Access event details, descriptions, and attendee information", "Access event details, descriptions, and attendee information",
"Connect your events directly to your search space", "Connect your events directly to your workspace",
"Keep your search results up-to-date with latest Luma content", "Keep your search results up-to-date with latest Luma content",
"Index your Luma events for enhanced search capabilities", "Index your Luma events for enhanced search capabilities",
], ],

View file

@ -165,7 +165,7 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
<AlertDescription> <AlertDescription>
Configure this URL in Circleback Settings Automations Create automation Send Configure this URL in Circleback Settings Automations Create automation Send
webhook request. The webhook will automatically send meeting notes, transcripts, and webhook request. The webhook will automatically send meeting notes, transcripts, and
action items to this search space. action items to this workspace.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}

View file

@ -8,7 +8,7 @@ export interface ConnectorConfigProps {
connector: SearchSourceConnector; connector: SearchSourceConnector;
onConfigChange?: (config: Record<string, unknown>) => void; onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void; onNameChange?: (name: string) => void;
searchSpaceId?: string; workspaceId?: string;
} }
export type ConnectorConfigComponent = FC<ConnectorConfigProps>; export type ConnectorConfigComponent = FC<ConnectorConfigProps>;

View file

@ -41,7 +41,7 @@ interface ConnectorEditViewProps {
isSaving: boolean; isSaving: boolean;
isDisconnecting: boolean; isDisconnecting: boolean;
isIndexing?: boolean; isIndexing?: boolean;
searchSpaceId?: string; workspaceId?: string;
onStartDateChange: (date: Date | undefined) => void; onStartDateChange: (date: Date | undefined) => void;
onEndDateChange: (date: Date | undefined) => void; onEndDateChange: (date: Date | undefined) => void;
onPeriodicEnabledChange: (enabled: boolean) => void; onPeriodicEnabledChange: (enabled: boolean) => void;
@ -65,7 +65,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
isSaving, isSaving,
isDisconnecting, isDisconnecting,
isIndexing = false, isIndexing = false,
searchSpaceId, workspaceId,
onStartDateChange, onStartDateChange,
onEndDateChange, onEndDateChange,
onPeriodicEnabledChange, onPeriodicEnabledChange,
@ -78,7 +78,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
onConfigChange, onConfigChange,
onNameChange, onNameChange,
}) => { }) => {
const searchSpaceIdAtom = useAtomValue(activeWorkspaceIdAtom); const workspaceIdAtom = useAtomValue(activeWorkspaceIdAtom);
const isAuthExpired = connector.config?.auth_expired === true; const isAuthExpired = connector.config?.auth_expired === true;
const reauthEndpoint = getReauthEndpoint(connector); const reauthEndpoint = getReauthEndpoint(connector);
const [reauthing, setReauthing] = useState(false); const [reauthing, setReauthing] = useState(false);
@ -91,7 +91,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
(connector.is_indexable || connector.connector_type === EnumConnectorName.OBSIDIAN_CONNECTOR); (connector.is_indexable || connector.connector_type === EnumConnectorName.OBSIDIAN_CONNECTOR);
const handleReauth = useCallback(async () => { const handleReauth = useCallback(async () => {
const spaceId = searchSpaceId ?? searchSpaceIdAtom; const spaceId = workspaceId ?? workspaceIdAtom;
if (!spaceId || !reauthEndpoint) return; if (!spaceId || !reauthEndpoint) return;
setReauthing(true); setReauthing(true);
try { try {
@ -119,7 +119,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
} finally { } finally {
setReauthing(false); setReauthing(false);
} }
}, [searchSpaceId, searchSpaceIdAtom, reauthEndpoint, connector.id]); }, [workspaceId, workspaceIdAtom, reauthEndpoint, connector.id]);
// Get connector-specific config component (MCP-backed connectors use a generic view) // Get connector-specific config component (MCP-backed connectors use a generic view)
const ConnectorConfigComponent = useMemo(() => { const ConnectorConfigComponent = useMemo(() => {
@ -273,7 +273,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
connector={connector} connector={connector}
onConfigChange={onConfigChange} onConfigChange={onConfigChange}
onNameChange={onNameChange} onNameChange={onNameChange}
searchSpaceId={searchSpaceId} workspaceId={workspaceId}
/> />
)} )}

View file

@ -58,7 +58,7 @@ function clearOAuthResultCookie(): void {
} }
export const useConnectorDialog = () => { export const useConnectorDialog = () => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom); const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom); const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom);
const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom); const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom);
const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom); const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom);
@ -140,7 +140,7 @@ export const useConnectorDialog = () => {
const handleAutoIndex = useCallback( const handleAutoIndex = useCallback(
async (connector: SearchSourceConnector, connectorTitle: string, connectorType: string) => { async (connector: SearchSourceConnector, connectorTitle: string, connectorType: string) => {
if (!searchSpaceId || isAutoIndexingRef.current) return; if (!workspaceId || isAutoIndexingRef.current) return;
isAutoIndexingRef.current = true; isAutoIndexingRef.current = true;
const defaults = AUTO_INDEX_DEFAULTS[connectorType]; const defaults = AUTO_INDEX_DEFAULTS[connectorType];
@ -165,13 +165,13 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: connector.id, connector_id: connector.id,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
start_date: format(startDate, "yyyy-MM-dd"), start_date: format(startDate, "yyyy-MM-dd"),
end_date: format(endDate, "yyyy-MM-dd"), end_date: format(endDate, "yyyy-MM-dd"),
}, },
}); });
trackIndexWithDateRangeStarted(Number(searchSpaceId), connectorType, connector.id, { trackIndexWithDateRangeStarted(Number(workspaceId), connectorType, connector.id, {
hasStartDate: true, hasStartDate: true,
hasEndDate: true, hasEndDate: true,
}); });
@ -188,13 +188,13 @@ export const useConnectorDialog = () => {
}); });
} finally { } finally {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), queryKey: cacheKeys.logs.summary(Number(workspaceId)),
}); });
await refetchAllConnectors(); await refetchAllConnectors();
isAutoIndexingRef.current = false; isAutoIndexingRef.current = false;
} }
}, },
[searchSpaceId, indexConnector, updateConnector, refetchAllConnectors] [workspaceId, indexConnector, updateConnector, refetchAllConnectors]
); );
// YouTube view state // YouTube view state
@ -206,7 +206,7 @@ export const useConnectorDialog = () => {
// Consume OAuth result from cookie (set by /connectors/callback route handler) // Consume OAuth result from cookie (set by /connectors/callback route handler)
useEffect(() => { useEffect(() => {
const raw = readOAuthResultCookie(); const raw = readOAuthResultCookie();
if (!raw || !searchSpaceId) return; if (!raw || !workspaceId) return;
clearOAuthResultCookie(); clearOAuthResultCookie();
const result = parseOAuthCallbackResult(raw); const result = parseOAuthCallbackResult(raw);
@ -221,7 +221,7 @@ export const useConnectorDialog = () => {
if (oauthConnector) { if (oauthConnector) {
trackConnectorSetupFailure( trackConnectorSetupFailure(
Number(searchSpaceId), Number(workspaceId),
oauthConnector.connectorType, oauthConnector.connectorType,
result.error, result.error,
"oauth_callback" "oauth_callback"
@ -292,7 +292,7 @@ export const useConnectorDialog = () => {
const connectorValidation = searchSourceConnector.safeParse(newConnector); const connectorValidation = searchSourceConnector.safeParse(newConnector);
if (connectorValidation.success) { if (connectorValidation.success) {
trackConnectorConnected( trackConnectorConnected(
Number(searchSpaceId), Number(workspaceId),
oauthConnector.connectorType, oauthConnector.connectorType,
newConnector.id newConnector.id
); );
@ -338,20 +338,20 @@ export const useConnectorDialog = () => {
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchSpaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]); }, [workspaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]);
// Handle OAuth connection // Handle OAuth connection
const handleConnectOAuth = useCallback( const handleConnectOAuth = useCallback(
async (connector: (typeof OAUTH_CONNECTORS)[number] | (typeof COMPOSIO_CONNECTORS)[number]) => { async (connector: (typeof OAUTH_CONNECTORS)[number] | (typeof COMPOSIO_CONNECTORS)[number]) => {
if (!searchSpaceId || !connector.authEndpoint) return; if (!workspaceId || !connector.authEndpoint) return;
// Set connecting state immediately to disable button and show spinner // Set connecting state immediately to disable button and show spinner
setConnectingId(connector.id); setConnectingId(connector.id);
trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click"); trackConnectorSetupStarted(Number(workspaceId), connector.connectorType, "oauth_click");
try { try {
const url = buildBackendUrl(connector.authEndpoint, { space_id: searchSpaceId }); const url = buildBackendUrl(connector.authEndpoint, { space_id: workspaceId });
const response = await authenticatedFetch(url, { method: "GET" }); const response = await authenticatedFetch(url, { method: "GET" });
@ -370,7 +370,7 @@ export const useConnectorDialog = () => {
} catch (error) { } catch (error) {
console.error(`Error connecting to ${connector.title}:`, error); console.error(`Error connecting to ${connector.title}:`, error);
trackConnectorSetupFailure( trackConnectorSetupFailure(
Number(searchSpaceId), Number(workspaceId),
connector.connectorType, connector.connectorType,
error instanceof Error ? error.message : "oauth_initiation_failed", error instanceof Error ? error.message : "oauth_initiation_failed",
"oauth_init" "oauth_init"
@ -384,22 +384,22 @@ export const useConnectorDialog = () => {
setConnectingId(null); setConnectingId(null);
} }
}, },
[searchSpaceId] [workspaceId]
); );
// Handle creating YouTube crawler (not a connector, shows view in popup) // Handle creating YouTube crawler (not a connector, shows view in popup)
const handleCreateYouTubeCrawler = useCallback(() => { const handleCreateYouTubeCrawler = useCallback(() => {
if (!searchSpaceId) return; if (!workspaceId) return;
setIsYouTubeView(true); setIsYouTubeView(true);
}, [searchSpaceId]); }, [workspaceId]);
// Handle creating webcrawler connector // Handle creating webcrawler connector
const handleCreateWebcrawler = useCallback(async () => { const handleCreateWebcrawler = useCallback(async () => {
if (!searchSpaceId) return; if (!workspaceId) return;
setConnectingId("webcrawler-connector"); setConnectingId("webcrawler-connector");
trackConnectorSetupStarted( trackConnectorSetupStarted(
Number(searchSpaceId), Number(workspaceId),
EnumConnectorName.WEBCRAWLER_CONNECTOR, EnumConnectorName.WEBCRAWLER_CONNECTOR,
"webcrawler_quick_add" "webcrawler_quick_add"
); );
@ -418,7 +418,7 @@ export const useConnectorDialog = () => {
enable_vision_llm: false, enable_vision_llm: false,
}, },
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
}, },
}); });
@ -433,7 +433,7 @@ export const useConnectorDialog = () => {
if (connectorValidation.success) { if (connectorValidation.success) {
// Track webcrawler connector connected // Track webcrawler connector connected
trackConnectorConnected( trackConnectorConnected(
Number(searchSpaceId), Number(workspaceId),
EnumConnectorName.WEBCRAWLER_CONNECTOR, EnumConnectorName.WEBCRAWLER_CONNECTOR,
connector.id connector.id
); );
@ -453,7 +453,7 @@ export const useConnectorDialog = () => {
} catch (error) { } catch (error) {
console.error("Error creating webcrawler connector:", error); console.error("Error creating webcrawler connector:", error);
trackConnectorSetupFailure( trackConnectorSetupFailure(
Number(searchSpaceId), Number(workspaceId),
EnumConnectorName.WEBCRAWLER_CONNECTOR, EnumConnectorName.WEBCRAWLER_CONNECTOR,
error instanceof Error ? error.message : "webcrawler_create_failed", error instanceof Error ? error.message : "webcrawler_create_failed",
"webcrawler_quick_add" "webcrawler_quick_add"
@ -462,18 +462,18 @@ export const useConnectorDialog = () => {
} finally { } finally {
setConnectingId(null); setConnectingId(null);
} }
}, [searchSpaceId, createConnector, refetchAllConnectors, setIsOpen]); }, [workspaceId, createConnector, refetchAllConnectors, setIsOpen]);
// Handle connecting non-OAuth connectors (like Tavily API, Obsidian plugin, etc.) // Handle connecting non-OAuth connectors (like Tavily API, Obsidian plugin, etc.)
const handleConnectNonOAuth = useCallback( const handleConnectNonOAuth = useCallback(
(connectorType: string) => { (connectorType: string) => {
if (!searchSpaceId) return; if (!workspaceId) return;
trackConnectorSetupStarted(Number(searchSpaceId), connectorType, "non_oauth_click"); trackConnectorSetupStarted(Number(workspaceId), connectorType, "non_oauth_click");
setConnectingConnectorType(connectorType); setConnectingConnectorType(connectorType);
}, },
[searchSpaceId] [workspaceId]
); );
// Handle submitting connect form // Handle submitting connect form
@ -495,7 +495,7 @@ export const useConnectorDialog = () => {
}, },
onIndexingStart?: (connectorId: number) => void onIndexingStart?: (connectorId: number) => void
) => { ) => {
if (!searchSpaceId || !connectingConnectorType) { if (!workspaceId || !connectingConnectorType) {
return; return;
} }
@ -519,7 +519,7 @@ export const useConnectorDialog = () => {
enable_vision_llm: false, enable_vision_llm: false,
}, },
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
}, },
}); });
// Refetch connectors to get the new one // Refetch connectors to get the new one
@ -536,7 +536,7 @@ export const useConnectorDialog = () => {
const currentConnectorType = connectingConnectorType; const currentConnectorType = connectingConnectorType;
// Track connector connected event for non-OAuth connectors // Track connector connected event for non-OAuth connectors
trackConnectorConnected(Number(searchSpaceId), currentConnectorType, connector.id); trackConnectorConnected(Number(workspaceId), currentConnectorType, connector.id);
// Find connector title from constants // Find connector title from constants
const connectorInfo = OTHER_CONNECTORS.find( const connectorInfo = OTHER_CONNECTORS.find(
@ -612,7 +612,7 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: connector.id, connector_id: connector.id,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
start_date: startDateStr, start_date: startDateStr,
end_date: endDateStr, end_date: endDateStr,
}, },
@ -631,7 +631,7 @@ export const useConnectorDialog = () => {
setIndexingConnectorConfig(null); setIndexingConnectorConfig(null);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), queryKey: cacheKeys.logs.summary(Number(workspaceId)),
}); });
await refetchAllConnectors(); await refetchAllConnectors();
@ -684,7 +684,7 @@ export const useConnectorDialog = () => {
} catch (error) { } catch (error) {
console.error("Error creating connector:", error); console.error("Error creating connector:", error);
trackConnectorSetupFailure( trackConnectorSetupFailure(
Number(searchSpaceId), Number(workspaceId),
connectingConnectorType ?? formData.connector_type, connectingConnectorType ?? formData.connector_type,
error instanceof Error ? error.message : "connector_create_failed", error instanceof Error ? error.message : "connector_create_failed",
"non_oauth_form" "non_oauth_form"
@ -698,7 +698,7 @@ export const useConnectorDialog = () => {
}, },
[ [
connectingConnectorType, connectingConnectorType,
searchSpaceId, workspaceId,
createConnector, createConnector,
refetchAllConnectors, refetchAllConnectors,
updateConnector, updateConnector,
@ -724,7 +724,7 @@ export const useConnectorDialog = () => {
// Handle viewing accounts list for OAuth connector type // Handle viewing accounts list for OAuth connector type
const handleViewAccountsList = useCallback( const handleViewAccountsList = useCallback(
(connectorType: string, _connectorTitle?: string) => { (connectorType: string, _connectorTitle?: string) => {
if (!searchSpaceId) return; if (!workspaceId) return;
const oauthConnector = const oauthConnector =
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) || OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
@ -736,7 +736,7 @@ export const useConnectorDialog = () => {
}); });
} }
}, },
[searchSpaceId] [workspaceId]
); );
// Handle going back from accounts list view // Handle going back from accounts list view
@ -746,9 +746,9 @@ export const useConnectorDialog = () => {
// Handle viewing MCP list // Handle viewing MCP list
const handleViewMCPList = useCallback(() => { const handleViewMCPList = useCallback(() => {
if (!searchSpaceId) return; if (!workspaceId) return;
setViewingMCPList(true); setViewingMCPList(true);
}, [searchSpaceId]); }, [workspaceId]);
// Handle going back from MCP list view // Handle going back from MCP list view
const handleBackFromMCPList = useCallback(() => { const handleBackFromMCPList = useCallback(() => {
@ -765,7 +765,7 @@ export const useConnectorDialog = () => {
// Handle starting indexing // Handle starting indexing
const handleStartIndexing = useCallback( const handleStartIndexing = useCallback(
async (refreshConnectors: () => void) => { async (refreshConnectors: () => void) => {
if (!indexingConfig || !searchSpaceId) return; if (!indexingConfig || !workspaceId) return;
// Validate date range (skip for Google Drive, Composio Drive, OneDrive, Dropbox, and Webcrawler) // Validate date range (skip for Google Drive, Composio Drive, OneDrive, Dropbox, and Webcrawler)
if ( if (
@ -847,7 +847,7 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: indexingConfig.connectorId, connector_id: indexingConfig.connectorId,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
}, },
body: { body: {
folders: selectedFolders || [], folders: selectedFolders || [],
@ -870,14 +870,14 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: indexingConfig.connectorId, connector_id: indexingConfig.connectorId,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
}, },
}); });
} else { } else {
await indexConnector({ await indexConnector({
connector_id: indexingConfig.connectorId, connector_id: indexingConfig.connectorId,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
start_date: startDateStr, start_date: startDateStr,
end_date: endDateStr, end_date: endDateStr,
}, },
@ -886,7 +886,7 @@ export const useConnectorDialog = () => {
// Track index with date range started event // Track index with date range started event
trackIndexWithDateRangeStarted( trackIndexWithDateRangeStarted(
Number(searchSpaceId), Number(workspaceId),
indexingConfig.connectorType, indexingConfig.connectorType,
indexingConfig.connectorId, indexingConfig.connectorId,
{ {
@ -898,7 +898,7 @@ export const useConnectorDialog = () => {
// Track periodic indexing started if enabled // Track periodic indexing started if enabled
if (periodicEnabled) { if (periodicEnabled) {
trackPeriodicIndexingStarted( trackPeriodicIndexingStarted(
Number(searchSpaceId), Number(workspaceId),
indexingConfig.connectorType, indexingConfig.connectorType,
indexingConfig.connectorId, indexingConfig.connectorId,
parseInt(frequencyMinutes, 10) parseInt(frequencyMinutes, 10)
@ -915,7 +915,7 @@ export const useConnectorDialog = () => {
refreshConnectors(); refreshConnectors();
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), queryKey: cacheKeys.logs.summary(Number(workspaceId)),
}); });
} catch (error) { } catch (error) {
console.error("Error starting indexing:", error); console.error("Error starting indexing:", error);
@ -926,7 +926,7 @@ export const useConnectorDialog = () => {
}, },
[ [
indexingConfig, indexingConfig,
searchSpaceId, workspaceId,
startDate, startDate,
endDate, endDate,
indexConnector, indexConnector,
@ -951,7 +951,7 @@ export const useConnectorDialog = () => {
// Handle starting edit mode // Handle starting edit mode
const handleStartEdit = useCallback( const handleStartEdit = useCallback(
(connector: SearchSourceConnector) => { (connector: SearchSourceConnector) => {
if (!searchSpaceId) return; if (!workspaceId) return;
// For MCP connectors from "All Connectors" tab, show the list view instead of directly editing // For MCP connectors from "All Connectors" tab, show the list view instead of directly editing
// (unless we're already in the MCP list view or on the Active tab where individual MCPs are shown) // (unless we're already in the MCP list view or on the Active tab where individual MCPs are shown)
@ -986,11 +986,7 @@ export const useConnectorDialog = () => {
// Track index with date range opened event // Track index with date range opened event
if (connector.is_indexable) { if (connector.is_indexable) {
trackIndexWithDateRangeOpened( trackIndexWithDateRangeOpened(Number(workspaceId), connector.connector_type, connector.id);
Number(searchSpaceId),
connector.connector_type,
connector.id
);
} }
setEditingConnector(connector); setEditingConnector(connector);
@ -1001,13 +997,13 @@ export const useConnectorDialog = () => {
setStartDate(undefined); setStartDate(undefined);
setEndDate(undefined); setEndDate(undefined);
}, },
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab] [workspaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
); );
// Handle saving connector changes // Handle saving connector changes
const handleSaveConnector = useCallback( const handleSaveConnector = useCallback(
async (refreshConnectors: () => void) => { async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId || isSaving) return; if (!editingConnector || !workspaceId || isSaving) return;
// Validate date range (skip for Google Drive/OneDrive/Dropbox which uses folder selection, Webcrawler which uses config, and non-indexable connectors) // Validate date range (skip for Google Drive/OneDrive/Dropbox which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
if ( if (
@ -1114,7 +1110,7 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: editingConnector.id, connector_id: editingConnector.id,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
}, },
body: { body: {
folders: selectedFolders || [], folders: selectedFolders || [],
@ -1134,7 +1130,7 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: editingConnector.id, connector_id: editingConnector.id,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
}, },
}); });
indexingDescription = "Re-indexing started with updated configuration."; indexingDescription = "Re-indexing started with updated configuration.";
@ -1143,7 +1139,7 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: editingConnector.id, connector_id: editingConnector.id,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
start_date: startDateStr, start_date: startDateStr,
end_date: endDateStr, end_date: endDateStr,
}, },
@ -1157,7 +1153,7 @@ export const useConnectorDialog = () => {
(indexingDescription.includes("Re-indexing") || indexingDescription.includes("indexing")) (indexingDescription.includes("Re-indexing") || indexingDescription.includes("indexing"))
) { ) {
trackIndexWithDateRangeStarted( trackIndexWithDateRangeStarted(
Number(searchSpaceId), Number(workspaceId),
editingConnector.connector_type, editingConnector.connector_type,
editingConnector.id, editingConnector.id,
{ {
@ -1170,7 +1166,7 @@ export const useConnectorDialog = () => {
// Track periodic indexing if enabled // Track periodic indexing if enabled
if (periodicEnabled && editingConnector.is_indexable) { if (periodicEnabled && editingConnector.is_indexable) {
trackPeriodicIndexingStarted( trackPeriodicIndexingStarted(
Number(searchSpaceId), Number(workspaceId),
editingConnector.connector_type, editingConnector.connector_type,
editingConnector.id, editingConnector.id,
frequency || parseInt(frequencyMinutes, 10) frequency || parseInt(frequencyMinutes, 10)
@ -1190,7 +1186,7 @@ export const useConnectorDialog = () => {
refreshConnectors(); refreshConnectors();
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), queryKey: cacheKeys.logs.summary(Number(workspaceId)),
}); });
} catch (error) { } catch (error) {
console.error("Error saving connector:", error); console.error("Error saving connector:", error);
@ -1201,7 +1197,7 @@ export const useConnectorDialog = () => {
}, },
[ [
editingConnector, editingConnector,
searchSpaceId, workspaceId,
isSaving, isSaving,
startDate, startDate,
endDate, endDate,
@ -1220,7 +1216,7 @@ export const useConnectorDialog = () => {
// Handle disconnecting connector // Handle disconnecting connector
const handleDisconnectConnector = useCallback( const handleDisconnectConnector = useCallback(
async (refreshConnectors: () => void) => { async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId) return; if (!editingConnector || !workspaceId) return;
setIsDisconnecting(true); setIsDisconnecting(true);
try { try {
@ -1230,7 +1226,7 @@ export const useConnectorDialog = () => {
// Track connector deleted event // Track connector deleted event
trackConnectorDeleted( trackConnectorDeleted(
Number(searchSpaceId), Number(workspaceId),
editingConnector.connector_type, editingConnector.connector_type,
editingConnector.id editingConnector.id
); );
@ -1255,7 +1251,7 @@ export const useConnectorDialog = () => {
refreshConnectors(); refreshConnectors();
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), queryKey: cacheKeys.logs.summary(Number(workspaceId)),
}); });
} catch (error) { } catch (error) {
console.error("Error disconnecting connector:", error); console.error("Error disconnecting connector:", error);
@ -1264,7 +1260,7 @@ export const useConnectorDialog = () => {
setIsDisconnecting(false); setIsDisconnecting(false);
} }
}, },
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen] [editingConnector, workspaceId, deleteConnector, cameFromMCPList, setIsOpen]
); );
// Handle quick index (index with selected date range, or backend defaults if none selected) // Handle quick index (index with selected date range, or backend defaults if none selected)
@ -1276,7 +1272,7 @@ export const useConnectorDialog = () => {
startDate?: Date, startDate?: Date,
endDate?: Date endDate?: Date
) => { ) => {
if (!searchSpaceId) { if (!workspaceId) {
if (stopIndexing) { if (stopIndexing) {
stopIndexing(connectorId); stopIndexing(connectorId);
} }
@ -1285,7 +1281,7 @@ export const useConnectorDialog = () => {
// Track quick index clicked event // Track quick index clicked event
if (connectorType) { if (connectorType) {
trackQuickIndexClicked(Number(searchSpaceId), connectorType, connectorId); trackQuickIndexClicked(Number(workspaceId), connectorType, connectorId);
} }
try { try {
@ -1296,7 +1292,7 @@ export const useConnectorDialog = () => {
await indexConnector({ await indexConnector({
connector_id: connectorId, connector_id: connectorId,
queryParams: { queryParams: {
workspace_id: searchSpaceId, workspace_id: workspaceId,
start_date: startDateStr, start_date: startDateStr,
end_date: endDateStr, end_date: endDateStr,
}, },
@ -1305,7 +1301,7 @@ export const useConnectorDialog = () => {
// Invalidate queries to refresh data // Invalidate queries to refresh data
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)), queryKey: cacheKeys.logs.summary(Number(workspaceId)),
}); });
// Note: Don't call stopIndexing here - let useIndexingConnectors hook // Note: Don't call stopIndexing here - let useIndexingConnectors hook
// detect when last_indexed_at changes via real-time sync // detect when last_indexed_at changes via real-time sync
@ -1318,7 +1314,7 @@ export const useConnectorDialog = () => {
} }
} }
}, },
[searchSpaceId, indexConnector] [workspaceId, indexConnector]
); );
// Handle going back from edit view // Handle going back from edit view
@ -1406,7 +1402,7 @@ export const useConnectorDialog = () => {
periodicEnabled, periodicEnabled,
frequencyMinutes, frequencyMinutes,
enableVisionLlm, enableVisionLlm,
searchSpaceId, workspaceId,
allConnectors, allConnectors,
viewingAccountsType, viewingAccountsType,
viewingMCPList, viewingMCPList,

View file

@ -44,7 +44,7 @@ export function getConnectorDisplayName(fullName: string): string {
interface AllConnectorsTabProps { interface AllConnectorsTabProps {
searchQuery: string; searchQuery: string;
searchSpaceId: string; workspaceId: string;
connectedTypes: Set<string>; connectedTypes: Set<string>;
connectingId: string | null; connectingId: string | null;
allConnectors: SearchSourceConnector[] | undefined; allConnectors: SearchSourceConnector[] | undefined;

View file

@ -44,7 +44,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
isConnecting = false, isConnecting = false,
addButtonText, addButtonText,
}) => { }) => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom); const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const [reauthingId, setReauthingId] = useState<number | null>(null); const [reauthingId, setReauthingId] = useState<number | null>(null);
const [confirmDisconnectId, setConfirmDisconnectId] = useState<number | null>(null); const [confirmDisconnectId, setConfirmDisconnectId] = useState<number | null>(null);
const [disconnectingId, setDisconnectingId] = useState<number | null>(null); const [disconnectingId, setDisconnectingId] = useState<number | null>(null);
@ -58,13 +58,13 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
const handleReauth = useCallback( const handleReauth = useCallback(
async (connector: SearchSourceConnector) => { async (connector: SearchSourceConnector) => {
const endpoint = getReauthEndpoint(connector); const endpoint = getReauthEndpoint(connector);
if (!searchSpaceId || !endpoint) return; if (!workspaceId || !endpoint) return;
setReauthingId(connector.id); setReauthingId(connector.id);
try { try {
const response = await authenticatedFetch( const response = await authenticatedFetch(
buildBackendUrl(endpoint, { buildBackendUrl(endpoint, {
connector_id: connector.id, connector_id: connector.id,
space_id: searchSpaceId, space_id: workspaceId,
return_url: window.location.pathname, return_url: window.location.pathname,
}) })
); );
@ -86,7 +86,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
setReauthingId(null); setReauthingId(null);
} }
}, },
[searchSpaceId] [workspaceId]
); );
// Filter connectors to only show those of this type // Filter connectors to only show those of this type

View file

@ -38,11 +38,11 @@ function extractYoutubeUrls(text: string): string[] {
} }
interface YouTubeCrawlerViewProps { interface YouTubeCrawlerViewProps {
searchSpaceId: string; workspaceId: string;
onBack: () => void; onBack: () => void;
} }
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId, onBack }) => { export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ workspaceId, onBack }) => {
const t = useTranslations("add_youtube"); const t = useTranslations("add_youtube");
const [videoTags, setVideoTags] = useState<TagType[]>([]); const [videoTags, setVideoTags] = useState<TagType[]>([]);
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null); const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
@ -165,7 +165,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
{ {
document_type: "YOUTUBE_VIDEO", document_type: "YOUTUBE_VIDEO",
content: videoUrls, content: videoUrls,
workspace_id: parseInt(searchSpaceId, 10), workspace_id: parseInt(workspaceId, 10),
}, },
{ {
onSuccess: () => { onSuccess: () => {

View file

@ -90,9 +90,9 @@ const DocumentUploadPopupContent: FC<{
isOpen: boolean; isOpen: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}> = ({ isOpen, onOpenChange }) => { }> = ({ isOpen, onOpenChange }) => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom); const workspaceId = useAtomValue(activeWorkspaceIdAtom);
if (!searchSpaceId) return null; if (!workspaceId) return null;
const handleSuccess = () => { const handleSuccess = () => {
onOpenChange(false); onOpenChange(false);
@ -112,12 +112,12 @@ const DocumentUploadPopupContent: FC<{
Upload Documents Upload Documents
</DialogTitle> </DialogTitle>
<DialogDescription className="text-xs sm:text-base text-muted-foreground/80 line-clamp-1"> <DialogDescription className="text-xs sm:text-base text-muted-foreground/80 line-clamp-1">
Upload and sync your documents to your search space Upload and sync your documents to your workspace
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="px-4 sm:px-6 pb-4 sm:pb-6"> <div className="px-4 sm:px-6 pb-4 sm:pb-6">
<DocumentUploadTab searchSpaceId={searchSpaceId} onSuccess={handleSuccess} /> <DocumentUploadTab workspaceId={workspaceId} onSuccess={handleSuccess} />
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View file

@ -189,7 +189,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
const openEditorPanel = useSetAtom(openEditorPanelAtom); const openEditorPanel = useSetAtom(openEditorPanelAtom);
const params = useParams(); const params = useParams();
const electronAPI = useElectronAPI(); const electronAPI = useElectronAPI();
const resolvedSearchSpaceId = getWorkspaceIdNumber(params); const resolvedWorkspaceId = getWorkspaceIdNumber(params);
const { displayName, isFolder } = getVirtualPathDisplay(path); const { displayName, isFolder } = getVirtualPathDisplay(path);
const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />; const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />;
@ -204,7 +204,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
if (electronAPI.getAgentFilesystemMounts) { if (electronAPI.getAgentFilesystemMounts) {
try { try {
const mounts = (await electronAPI.getAgentFilesystemMounts( const mounts = (await electronAPI.getAgentFilesystemMounts(
resolvedSearchSpaceId resolvedWorkspaceId
)) as AgentFilesystemMount[]; )) as AgentFilesystemMount[];
resolvedLocalPath = normalizeLocalVirtualPathForEditor(path, mounts); resolvedLocalPath = normalizeLocalVirtualPathForEditor(path, mounts);
} catch { } catch {
@ -215,21 +215,21 @@ function FilePathLink({ path, className }: { path: string; className?: string })
kind: "local_file", kind: "local_file",
localFilePath: resolvedLocalPath, localFilePath: resolvedLocalPath,
title: resolvedLocalPath.split("/").pop() || resolvedLocalPath, title: resolvedLocalPath.split("/").pop() || resolvedLocalPath,
searchSpaceId: resolvedSearchSpaceId, workspaceId: resolvedWorkspaceId,
}); });
return; return;
} }
if (!resolvedSearchSpaceId || !path.startsWith("/documents/")) return; if (!resolvedWorkspaceId || !path.startsWith("/documents/")) return;
try { try {
const doc = await documentsApiService.getDocumentByVirtualPath({ const doc = await documentsApiService.getDocumentByVirtualPath({
workspace_id: resolvedSearchSpaceId, workspace_id: resolvedWorkspaceId,
virtual_path: path, virtual_path: path,
}); });
openEditorPanel({ openEditorPanel({
kind: "document", kind: "document",
documentId: doc.id, documentId: doc.id,
searchSpaceId: resolvedSearchSpaceId, workspaceId: resolvedWorkspaceId,
title: doc.title, title: doc.title,
}); });
} catch { } catch {
@ -237,7 +237,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
} }
})(); })();
}, },
[electronAPI, openEditorPanel, path, resolvedSearchSpaceId] [electronAPI, openEditorPanel, path, resolvedWorkspaceId]
); );
// Folders cannot open in the editor panel — keep them as visual chips. // Folders cannot open in the editor panel — keep them as visual chips.

View file

@ -895,7 +895,7 @@ const Composer: FC = () => {
<ComposerSuggestionPopoverContent side="top"> <ComposerSuggestionPopoverContent side="top">
<DocumentMentionPicker <DocumentMentionPicker
ref={documentPickerRef} ref={documentPickerRef}
searchSpaceId={workspaceId ?? 0} workspaceId={workspaceId ?? 0}
enableChatMentions enableChatMentions
currentChatId={threadId} currentChatId={threadId}
onSelectionChange={handleDocumentsMention} onSelectionChange={handleDocumentsMention}
@ -961,7 +961,7 @@ const Composer: FC = () => {
</div> </div>
<ComposerAction <ComposerAction
isBlockedByOtherUser={isBlockedByOtherUser} isBlockedByOtherUser={isBlockedByOtherUser}
searchSpaceId={workspaceId ?? 0} workspaceId={workspaceId ?? 0}
onChatModelSelected={handleChatModelSelected} onChatModelSelected={handleChatModelSelected}
/> />
<ConnectorIndicator showTrigger={false} /> <ConnectorIndicator showTrigger={false} />
@ -982,13 +982,13 @@ const Composer: FC = () => {
interface ComposerActionProps { interface ComposerActionProps {
isBlockedByOtherUser?: boolean; isBlockedByOtherUser?: boolean;
searchSpaceId: number; workspaceId: number;
onChatModelSelected?: () => void; onChatModelSelected?: () => void;
} }
const ComposerAction: FC<ComposerActionProps> = ({ const ComposerAction: FC<ComposerActionProps> = ({
isBlockedByOtherUser = false, isBlockedByOtherUser = false,
searchSpaceId, workspaceId,
onChatModelSelected, onChatModelSelected,
}) => { }) => {
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom); const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
@ -1564,7 +1564,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
)} )}
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-2"> <div className="ml-auto flex min-w-0 shrink-0 items-center gap-2">
<ChatHeader <ChatHeader
searchSpaceId={searchSpaceId} workspaceId={workspaceId}
className="h-9 max-w-[44vw] px-2 sm:max-w-[220px] sm:px-3" className="h-9 max-w-[44vw] px-2 sm:max-w-[220px] sm:px-3"
onChatModelSelected={onChatModelSelected} onChatModelSelected={onChatModelSelected}
/> />

View file

@ -76,33 +76,33 @@ const UserTextPart: FC = () => {
const openEditorPanel = useSetAtom(openEditorPanelAtom); const openEditorPanel = useSetAtom(openEditorPanelAtom);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const resolvedSearchSpaceId = getWorkspaceIdNumber(params); const resolvedWorkspaceId = getWorkspaceIdNumber(params);
const handleOpenDoc = useCallback( const handleOpenDoc = useCallback(
(docId: number, title: string) => { (docId: number, title: string) => {
if (!resolvedSearchSpaceId) { if (!resolvedWorkspaceId) {
toast.error("Cannot open document outside a search space."); toast.error("Cannot open document outside a workspace.");
return; return;
} }
openEditorPanel({ openEditorPanel({
kind: "document", kind: "document",
documentId: docId, documentId: docId,
searchSpaceId: resolvedSearchSpaceId, workspaceId: resolvedWorkspaceId,
title, title,
}); });
}, },
[openEditorPanel, resolvedSearchSpaceId] [openEditorPanel, resolvedWorkspaceId]
); );
const handleOpenThread = useCallback( const handleOpenThread = useCallback(
(threadId: number) => { (threadId: number) => {
if (!resolvedSearchSpaceId) { if (!resolvedWorkspaceId) {
toast.error("Cannot open chat outside a search space."); toast.error("Cannot open chat outside a workspace.");
return; return;
} }
router.push(`/dashboard/${resolvedSearchSpaceId}/new-chat/${threadId}`); router.push(`/dashboard/${resolvedWorkspaceId}/new-chat/${threadId}`);
}, },
[resolvedSearchSpaceId, router] [resolvedWorkspaceId, router]
); );
const segments = parseMentionSegments(text, mentionedDocs); const segments = parseMentionSegments(text, mentionedDocs);

View file

@ -77,7 +77,7 @@ export const CitationPanelContent: FC<CitationPanelContentProps> = ({
if (!data) return; if (!data) return;
openEditorPanel({ openEditorPanel({
documentId: data.id, documentId: data.id,
searchSpaceId: data.workspace_id, workspaceId: data.workspace_id,
title: data.title, title: data.title,
}); });
}; };

View file

@ -49,7 +49,7 @@ export interface FolderDisplay {
name: string; name: string;
position: string; position: string;
parentId: number | null; parentId: number | null;
searchSpaceId: number; workspaceId: number;
metadata?: Record<string, unknown> | null; metadata?: Record<string, unknown> | null;
} }

View file

@ -146,7 +146,7 @@ export function EditorPanelContent({
documentId, documentId,
localFilePath, localFilePath,
memoryScope, memoryScope,
searchSpaceId, workspaceId,
title, title,
onClose, onClose,
}: { }: {
@ -154,7 +154,7 @@ export function EditorPanelContent({
documentId?: number; documentId?: number;
localFilePath?: string; localFilePath?: string;
memoryScope?: "user" | "team"; memoryScope?: "user" | "team";
searchSpaceId?: number; workspaceId?: number;
title: string | null; title: string | null;
onClose?: () => void; onClose?: () => void;
}) { }) {
@ -186,14 +186,14 @@ export function EditorPanelContent({
} }
try { try {
const mounts = (await electronAPI.getAgentFilesystemMounts( const mounts = (await electronAPI.getAgentFilesystemMounts(
searchSpaceId workspaceId
)) as AgentFilesystemMount[]; )) as AgentFilesystemMount[];
return normalizeLocalVirtualPathForEditor(candidatePath, mounts); return normalizeLocalVirtualPathForEditor(candidatePath, mounts);
} catch { } catch {
return candidatePath; return candidatePath;
} }
}, },
[electronAPI, searchSpaceId] [electronAPI, workspaceId]
); );
const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD; const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD;
@ -234,7 +234,7 @@ export function EditorPanelContent({
const resolvedLocalPath = await resolveLocalVirtualPath(localFilePath); const resolvedLocalPath = await resolveLocalVirtualPath(localFilePath);
const readResult = await electronAPI.readAgentLocalFileText( const readResult = await electronAPI.readAgentLocalFileText(
resolvedLocalPath, resolvedLocalPath,
searchSpaceId workspaceId
); );
if (!readResult.ok) { if (!readResult.ok) {
throw new Error(readResult.error || "Failed to read local file"); throw new Error(readResult.error || "Failed to read local file");
@ -257,7 +257,7 @@ export function EditorPanelContent({
if (!memoryScope) throw new Error("Missing memory context"); if (!memoryScope) throw new Error("Missing memory context");
const { document, limits } = await fetchMemoryEditorDocument({ const { document, limits } = await fetchMemoryEditorDocument({
scope: memoryScope, scope: memoryScope,
searchSpaceId, workspaceId,
title, title,
signal: controller.signal, signal: controller.signal,
}); });
@ -271,12 +271,12 @@ export function EditorPanelContent({
return; return;
} }
if (!documentId || !searchSpaceId) { if (!documentId || !workspaceId) {
throw new Error("Missing document context"); throw new Error("Missing document context");
} }
const response = await authenticatedFetch( const response = await authenticatedFetch(
buildBackendUrl( buildBackendUrl(
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/editor-content` `/api/v1/workspaces/${workspaceId}/documents/${documentId}/editor-content`
), ),
{ method: "GET" } { method: "GET" }
); );
@ -323,7 +323,7 @@ export function EditorPanelContent({
localFilePath, localFilePath,
memoryScope, memoryScope,
resolveLocalVirtualPath, resolveLocalVirtualPath,
searchSpaceId, workspaceId,
title, title,
]); ]);
@ -382,7 +382,7 @@ export function EditorPanelContent({
const writeResult = await electronAPI.writeAgentLocalFileText( const writeResult = await electronAPI.writeAgentLocalFileText(
resolvedLocalPath, resolvedLocalPath,
contentToSave, contentToSave,
searchSpaceId workspaceId
); );
if (!writeResult.ok) { if (!writeResult.ok) {
throw new Error(writeResult.error || "Failed to save local file"); throw new Error(writeResult.error || "Failed to save local file");
@ -395,7 +395,7 @@ export function EditorPanelContent({
if (!memoryScope) throw new Error("Missing memory context"); if (!memoryScope) throw new Error("Missing memory context");
const { markdown: savedContent, limits } = await saveMemoryMarkdown({ const { markdown: savedContent, limits } = await saveMemoryMarkdown({
scope: memoryScope, scope: memoryScope,
searchSpaceId, workspaceId,
markdown: markdownRef.current, markdown: markdownRef.current,
}); });
markdownRef.current = savedContent; markdownRef.current = savedContent;
@ -408,11 +408,11 @@ export function EditorPanelContent({
return true; return true;
} }
if (!searchSpaceId || !documentId) { if (!workspaceId || !documentId) {
throw new Error("Missing document context"); throw new Error("Missing document context");
} }
const response = await authenticatedFetch( const response = await authenticatedFetch(
buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/save`), buildBackendUrl(`/api/v1/workspaces/${workspaceId}/documents/${documentId}/save`),
{ {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -460,7 +460,7 @@ export function EditorPanelContent({
plateMaxBytes, plateMaxBytes,
plateMaxLines, plateMaxLines,
resolveLocalVirtualPath, resolveLocalVirtualPath,
searchSpaceId, workspaceId,
] ]
); );
@ -515,12 +515,12 @@ export function EditorPanelContent({
}, [editorDoc?.source_markdown]); }, [editorDoc?.source_markdown]);
const handleDownloadMarkdown = useCallback(async () => { const handleDownloadMarkdown = useCallback(async () => {
if (!searchSpaceId || !documentId) return; if (!workspaceId || !documentId) return;
setDownloading(true); setDownloading(true);
try { try {
const response = await authenticatedFetch( const response = await authenticatedFetch(
buildBackendUrl( buildBackendUrl(
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/download-markdown` `/api/v1/workspaces/${workspaceId}/documents/${documentId}/download-markdown`
), ),
{ method: "GET" } { method: "GET" }
); );
@ -542,7 +542,7 @@ export function EditorPanelContent({
} finally { } finally {
setDownloading(false); setDownloading(false);
} }
}, [documentId, editorDoc?.title, searchSpaceId]); }, [documentId, editorDoc?.title, workspaceId]);
const largeDocAlert = viewerMode === "monaco" && !isLocalFileMode && editorDoc && ( const largeDocAlert = viewerMode === "monaco" && !isLocalFileMode && editorDoc && (
<Alert className="m-4 shrink-0"> <Alert className="m-4 shrink-0">
@ -890,7 +890,7 @@ function DesktopEditorPanel() {
const hasTarget = const hasTarget =
panelState.kind === "document" panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.workspaceId
: panelState.kind === "local_file" : panelState.kind === "local_file"
? !!panelState.localFilePath ? !!panelState.localFilePath
: !!panelState.memoryScope; : !!panelState.memoryScope;
@ -903,7 +903,7 @@ function DesktopEditorPanel() {
documentId={panelState.documentId ?? undefined} documentId={panelState.documentId ?? undefined}
localFilePath={panelState.localFilePath ?? undefined} localFilePath={panelState.localFilePath ?? undefined}
memoryScope={panelState.memoryScope ?? undefined} memoryScope={panelState.memoryScope ?? undefined}
searchSpaceId={panelState.searchSpaceId ?? undefined} workspaceId={panelState.workspaceId ?? undefined}
title={panelState.title} title={panelState.title}
onClose={closePanel} onClose={closePanel}
/> />
@ -919,7 +919,7 @@ function MobileEditorDrawer() {
const hasTarget = const hasTarget =
panelState.kind === "document" panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.workspaceId
: !!panelState.memoryScope; : !!panelState.memoryScope;
if (!hasTarget) return null; if (!hasTarget) return null;
@ -943,7 +943,7 @@ function MobileEditorDrawer() {
documentId={panelState.documentId ?? undefined} documentId={panelState.documentId ?? undefined}
localFilePath={panelState.localFilePath ?? undefined} localFilePath={panelState.localFilePath ?? undefined}
memoryScope={panelState.memoryScope ?? undefined} memoryScope={panelState.memoryScope ?? undefined}
searchSpaceId={panelState.searchSpaceId ?? undefined} workspaceId={panelState.workspaceId ?? undefined}
title={panelState.title} title={panelState.title}
/> />
</div> </div>
@ -957,7 +957,7 @@ export function EditorPanel() {
const isDesktop = useMediaQuery("(min-width: 1024px)"); const isDesktop = useMediaQuery("(min-width: 1024px)");
const hasTarget = const hasTarget =
panelState.kind === "document" panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.workspaceId
: panelState.kind === "local_file" : panelState.kind === "local_file"
? !!panelState.localFilePath ? !!panelState.localFilePath
: !!panelState.memoryScope; : !!panelState.memoryScope;
@ -977,7 +977,7 @@ export function MobileEditorPanel() {
const isDesktop = useMediaQuery("(min-width: 1024px)"); const isDesktop = useMediaQuery("(min-width: 1024px)");
const hasTarget = const hasTarget =
panelState.kind === "document" panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.workspaceId
: panelState.kind === "local_file" : panelState.kind === "local_file"
? !!panelState.localFilePath ? !!panelState.localFilePath
: !!panelState.memoryScope; : !!panelState.memoryScope;

View file

@ -24,10 +24,10 @@ interface MemoryReadResponse {
limits?: MemoryLimits; limits?: MemoryLimits;
} }
function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) { function getMemoryPath(scope: MemoryScope, workspaceId?: number | null) {
if (scope === "user") return "/api/v1/users/me/memory"; if (scope === "user") return "/api/v1/users/me/memory";
if (!searchSpaceId) throw new Error("Missing search space context"); if (!workspaceId) throw new Error("Missing workspace context");
return `/api/v1/workspaces/${searchSpaceId}/memory`; return `/api/v1/workspaces/${workspaceId}/memory`;
} }
export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) { export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {
@ -53,16 +53,16 @@ export function getMemoryLimitState(length: number, limits?: MemoryLimits | null
export async function fetchMemoryEditorDocument({ export async function fetchMemoryEditorDocument({
scope, scope,
searchSpaceId, workspaceId,
title, title,
signal, signal,
}: { }: {
scope: MemoryScope; scope: MemoryScope;
searchSpaceId?: number | null; workspaceId?: number | null;
title?: string | null; title?: string | null;
signal?: AbortSignal; signal?: AbortSignal;
}) { }) {
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), { const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, workspaceId)), {
method: "GET", method: "GET",
signal, signal,
}); });
@ -87,14 +87,14 @@ export async function fetchMemoryEditorDocument({
export async function saveMemoryMarkdown({ export async function saveMemoryMarkdown({
scope, scope,
searchSpaceId, workspaceId,
markdown, markdown,
}: { }: {
scope: MemoryScope; scope: MemoryScope;
searchSpaceId?: number | null; workspaceId?: number | null;
markdown: string; markdown: string;
}) { }) {
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), { const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, workspaceId)), {
method: "PUT", method: "PUT",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ memory_md: markdown }), body: JSON.stringify({ memory_md: markdown }),

View file

@ -5,13 +5,13 @@ export type {
IconRailProps, IconRailProps,
NavItem, NavItem,
PageUsage, PageUsage,
SearchSpace,
SidebarSectionProps, SidebarSectionProps,
User, User,
Workspace,
} from "./types/layout.types"; } from "./types/layout.types";
export { export {
ChatListItem, ChatListItem,
CreateSearchSpaceDialog, CreateWorkspaceDialog,
CreditBalanceDisplay, CreditBalanceDisplay,
Header, Header,
IconRail, IconRail,
@ -20,10 +20,10 @@ export {
MobileSidebarTrigger, MobileSidebarTrigger,
NavIcon, NavIcon,
NavSection, NavSection,
SearchSpaceAvatar,
Sidebar, Sidebar,
SidebarCollapseButton, SidebarCollapseButton,
SidebarHeader, SidebarHeader,
SidebarSection, SidebarSection,
SidebarUserProfile, SidebarUserProfile,
WorkspaceAvatar,
} from "./ui"; } from "./ui";

View file

@ -9,14 +9,14 @@ import { useLoginGate } from "@/contexts/login-gate";
import { useAnnouncements } from "@/hooks/use-announcements"; import { useAnnouncements } from "@/hooks/use-announcements";
import { useIsMobile } from "@/hooks/use-mobile"; import { useIsMobile } from "@/hooks/use-mobile";
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service"; import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
import type { ChatItem, NavItem, PageUsage, SearchSpace } from "../types/layout.types"; import type { ChatItem, NavItem, PageUsage, Workspace } from "../types/layout.types";
import { LayoutShell } from "../ui/shell"; import { LayoutShell } from "../ui/shell";
interface FreeLayoutDataProviderProps { interface FreeLayoutDataProviderProps {
children: ReactNode; children: ReactNode;
} }
const GUEST_SPACE: SearchSpace = { const GUEST_SPACE: Workspace = {
id: 0, id: 0,
name: "SurfSense Free", name: "SurfSense Free",
description: "Free AI chat without login", description: "Free AI chat without login",
@ -94,19 +94,16 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]); const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]);
const handleSearchSpaceSelect = useCallback( const handleWorkspaceSelect = useCallback((_id: number) => gate("switch workspaces"), [gate]);
(_id: number) => gate("switch search spaces"),
[gate]
);
return ( return (
<LayoutShell <LayoutShell
searchSpaces={[GUEST_SPACE]} workspaces={[GUEST_SPACE]}
activeSearchSpaceId={0} activeWorkspaceId={0}
onSearchSpaceSelect={handleSearchSpaceSelect} onWorkspaceSelect={handleWorkspaceSelect}
onSearchSpaceSettings={gatedAction("search space settings")} onWorkspaceSettings={gatedAction("workspace settings")}
onAddSearchSpace={gatedAction("create search spaces")} onAddWorkspace={gatedAction("create workspaces")}
searchSpace={GUEST_SPACE} workspace={GUEST_SPACE}
navItems={navItems} navItems={navItems}
onNavItemClick={handleNavItemClick} onNavItemClick={handleNavItemClick}
chats={[]} chats={[]}
@ -121,7 +118,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
email: "Guest", email: "Guest",
name: "Guest", name: "Guest",
}} }}
onSettings={gatedAction("search space settings")} onSettings={gatedAction("workspace settings")}
onManageMembers={gatedAction("team management")} onManageMembers={gatedAction("team management")}
onUserSettings={gatedAction("account settings")} onUserSettings={gatedAction("account settings")}
onAnnouncements={handleAnnouncements} onAnnouncements={handleAnnouncements}

Some files were not shown because too many files have changed in this diff Show more