feat(workspace): refactor search space handling to workspace

This commit is contained in:
Anish Sarkar 2026-07-06 10:04:17 +05:30
parent e92b7a34ed
commit c379ab1155
53 changed files with 268 additions and 169 deletions

View file

@ -34,6 +34,7 @@ import { useElectronAPI } from "@/hooks/use-platform";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { getVirtualPathDisplay } from "@/lib/chat/virtual-path-display";
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
import { getWorkspaceIdNumber } from "@/lib/route-params";
import { tryGetHostname } from "@/lib/url";
import { cn } from "@/lib/utils";
@ -188,13 +189,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const params = useParams();
const electronAPI = useElectronAPI();
const searchSpaceIdParam = params?.search_space_id;
const parsedSearchSpaceId = Array.isArray(searchSpaceIdParam)
? Number(searchSpaceIdParam[0])
: Number(searchSpaceIdParam);
const resolvedSearchSpaceId = Number.isFinite(parsedSearchSpaceId)
? parsedSearchSpaceId
: undefined;
const resolvedSearchSpaceId = getWorkspaceIdNumber(params);
const { displayName, isFolder } = getVirtualPathDisplay(path);
const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />;

View file

@ -108,6 +108,7 @@ import { useElectronAPI } from "@/hooks/use-platform";
import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { slideoutOpenedTickAtom } from "@/lib/layout-events";
import { getWorkspaceIdNumber } from "@/lib/route-params";
import { cn } from "@/lib/utils";
import {
DocumentMentionPicker,
@ -458,7 +459,9 @@ const Composer: FC = () => {
const prevMentionedDocsRef = useRef<Map<string, MentionedDocumentInfo>>(new Map());
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
const promptPickerRef = useRef<PromptPickerRef>(null);
const { search_space_id, chat_id } = useParams();
const params = useParams();
const workspaceId = getWorkspaceIdNumber(params);
const chat_id = params.chat_id;
const aui = useAui();
// Desktop-only auto-focus; on mobile, programmatic focus would
// summon the soft keyboard on every picker close / thread switch.
@ -824,7 +827,6 @@ const Composer: FC = () => {
const handleDocumentsMention = useCallback(
(mentions: MentionedDocumentInfo[]) => {
const parsedSearchSpaceId = Number(search_space_id);
const editorMentionedDocs = editorRef.current?.getMentionedDocuments() ?? [];
const editorDocKeys = new Set(editorMentionedDocs.map((doc) => getMentionDocKey(doc)));
@ -832,8 +834,8 @@ const Composer: FC = () => {
const key = getMentionDocKey(mention);
if (editorDocKeys.has(key)) continue;
editorRef.current?.insertMentionChip(mention);
if (Number.isFinite(parsedSearchSpaceId)) {
promoteRecentMention(parsedSearchSpaceId, mention);
if (workspaceId) {
promoteRecentMention(workspaceId, mention);
}
// Track within the loop so a duplicate-in-batch can't double-insert.
editorDocKeys.add(key);
@ -844,7 +846,7 @@ const Composer: FC = () => {
setMentionQuery("");
setSuggestionAnchorPoint(null);
},
[search_space_id]
[workspaceId]
);
useEffect(() => {
@ -893,7 +895,7 @@ const Composer: FC = () => {
<ComposerSuggestionPopoverContent side="top">
<DocumentMentionPicker
ref={documentPickerRef}
searchSpaceId={Number(search_space_id)}
searchSpaceId={workspaceId ?? 0}
enableChatMentions
currentChatId={threadId}
onSelectionChange={handleDocumentsMention}
@ -959,7 +961,7 @@ const Composer: FC = () => {
</div>
<ComposerAction
isBlockedByOtherUser={isBlockedByOtherUser}
searchSpaceId={Number(search_space_id)}
searchSpaceId={workspaceId ?? 0}
onChatModelSelected={handleChatModelSelected}
/>
<ConnectorIndicator showTrigger={false} />

View file

@ -26,6 +26,7 @@ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { parseMentionSegments } from "@/lib/chat/parse-mention-segments";
import { getWorkspaceIdNumber } from "@/lib/route-params";
interface AuthorMetadata {
displayName: string | null;
@ -75,13 +76,7 @@ const UserTextPart: FC = () => {
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const router = useRouter();
const params = useParams();
const searchSpaceIdParam = params?.search_space_id;
const parsedSearchSpaceId = Array.isArray(searchSpaceIdParam)
? Number(searchSpaceIdParam[0])
: Number(searchSpaceIdParam);
const resolvedSearchSpaceId = Number.isFinite(parsedSearchSpaceId)
? parsedSearchSpaceId
: undefined;
const resolvedSearchSpaceId = getWorkspaceIdNumber(params);
const handleOpenDoc = useCallback(
(docId: number, title: string) => {

View file

@ -27,7 +27,7 @@ interface MemoryReadResponse {
function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
if (scope === "user") return "/api/v1/users/me/memory";
if (!searchSpaceId) throw new Error("Missing search space context");
return `/api/v1/searchspaces/${searchSpaceId}/memory`;
return `/api/v1/workspaces/${searchSpaceId}/memory`;
}
export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {

View file

@ -81,6 +81,7 @@ import { authenticatedFetch } from "@/lib/auth-fetch";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { buildBackendUrl } from "@/lib/env-config";
import { uploadFolderScan } from "@/lib/folder-sync-upload";
import { getWorkspaceIdNumber } from "@/lib/route-params";
import { getSupportedExtensionsSet } from "@/lib/supported-extensions";
import { queries } from "@/zero/queries/index";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
@ -228,7 +229,7 @@ function AuthenticatedDocumentsSidebarBase({
const platformElectronAPI = useElectronAPI();
const electronAPI = desktopFeaturesEnabled ? platformElectronAPI : null;
const { etlService } = useRuntimeConfig();
const searchSpaceId = Number(params.search_space_id);
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const { data: agentFlags } = useAtomValue(agentFlagsAtom);
@ -828,7 +829,7 @@ function AuthenticatedDocumentsSidebarBase({
const endpoint =
doc.document_type === "USER_MEMORY"
? buildBackendUrl("/api/v1/users/me/memory")
: buildBackendUrl(`/api/v1/searchspaces/${searchSpaceId}/memory`);
: buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/memory`);
const response = await authenticatedFetch(endpoint, { method: "GET" });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: "Export failed" }));
@ -1038,7 +1039,7 @@ function AuthenticatedDocumentsSidebarBase({
const endpoint =
doc.document_type === "USER_MEMORY"
? buildBackendUrl("/api/v1/users/me/memory/reset")
: buildBackendUrl(`/api/v1/searchspaces/${searchSpaceId}/memory/reset`);
: buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/memory/reset`);
try {
const response = await authenticatedFetch(endpoint, { method: "POST" });
if (!response.ok) {

View file

@ -58,6 +58,7 @@ import { notificationsApiService } from "@/lib/apis/notifications-api.service";
import { convertRenderedToDisplay } from "@/lib/comments/utils";
import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { getWorkspaceIdNumber } from "@/lib/route-params";
import { cn } from "@/lib/utils";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
@ -165,7 +166,7 @@ export function InboxSidebarContent({
const router = useRouter();
const params = useParams();
const isMobile = !useMediaQuery("(min-width: 640px)");
const searchSpaceId = params?.search_space_id ? Number(params.search_space_id) : null;
const searchSpaceId = getWorkspaceIdNumber(params) ?? null;
const [, setTargetCommentId] = useAtom(setTargetCommentIdAtom);

View file

@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { useIsAnonymous } from "@/contexts/anonymous-mode";
import { getWorkspaceIdParam } from "@/lib/route-params";
import { cn } from "@/lib/utils";
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
@ -377,7 +378,7 @@ function SidebarUsageFooter({
onNavigate?: () => void;
}) {
const params = useParams();
const searchSpaceId = params?.search_space_id ?? "";
const searchSpaceId = getWorkspaceIdParam(params) ?? "";
const isAnonymous = useIsAnonymous();
if (isCollapsed) return null;

View file

@ -24,6 +24,7 @@ import {
ComposerSuggestionSeparator,
ComposerSuggestionSkeleton,
} from "@/components/new-chat/composer-suggestion-popup";
import { getWorkspaceIdParam } from "@/lib/route-params";
export interface PromptPickerRef {
selectHighlighted: () => void;
@ -69,9 +70,7 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
const createPromptIndex = filtered.length;
const totalItems = filtered.length + 1;
const searchSpaceId = Array.isArray(params?.search_space_id)
? params.search_space_id[0]
: params?.search_space_id;
const searchSpaceId = getWorkspaceIdParam(params);
const handleSelect = useCallback(
(index: number) => {

View file

@ -11,7 +11,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) {
const { locale, messages } = useLocaleContext();
return (
<NextIntlClientProvider messages={messages} locale={locale}>
<NextIntlClientProvider messages={messages} locale={locale} timeZone="UTC">
{children}
</NextIntlClientProvider>
);

View file

@ -15,6 +15,7 @@ import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { stripeApiService } from "@/lib/apis/stripe-api.service";
import { AppError } from "@/lib/error";
import { getWorkspaceIdNumber } from "@/lib/route-params";
import { queries } from "@/zero/queries";
const microsToDollars = (micros: number | null | undefined): string => {
@ -38,7 +39,7 @@ export function AutoReloadSettings() {
const pathname = usePathname();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const searchSpaceId = Number(params?.search_space_id);
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const [enabled, setEnabled] = useState(false);
const [thresholdInput, setThresholdInput] = useState("");

View file

@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { stripeApiService } from "@/lib/apis/stripe-api.service";
import { AppError } from "@/lib/error";
import { getWorkspaceIdNumber } from "@/lib/route-params";
import { cn } from "@/lib/utils";
import { queries } from "@/zero/queries";
@ -37,7 +38,7 @@ const formatUsd = (micros: number) => {
export function BuyCreditsContent() {
const params = useParams();
const searchSpaceId = Number(params?.search_space_id);
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const [quantity, setQuantity] = useState(1);
// Raw text of the amount field so the user can clear it while typing;
// committed back to a clamped integer on blur.

View file

@ -20,6 +20,7 @@ import {
trackIncentiveTaskClicked,
trackIncentiveTaskCompleted,
} from "@/lib/posthog/events";
import { getWorkspaceIdParam } from "@/lib/route-params";
import { cn } from "@/lib/utils";
// Compact dollar label for a task's reward (e.g. "+$0.03").
@ -32,7 +33,7 @@ const formatRewardUsd = (micros: number) => {
export function EarnCreditsContent() {
const params = useParams();
const queryClient = useQueryClient();
const searchSpaceId = params?.search_space_id ?? "";
const searchSpaceId = getWorkspaceIdParam(params) ?? "";
useEffect(() => {
trackIncentivePageViewed();

View file

@ -126,6 +126,11 @@ export function ModelProviderConnectionsPanel({
// Each provider connect form builds its own credential payload; the backend
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
function handleCreate(draft: ConnectionDraft) {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
const models = connectionModelsForDraft(draft);
const testModel = representativeTestModel(models);
if (!testModel) {
@ -165,6 +170,11 @@ export function ModelProviderConnectionsPanel({
setProvider(providerId);
setIsAddProviderOpen(true);
if (providerId === "vertex_ai") {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
previewModels.mutate(
{
provider: providerId,
@ -184,6 +194,11 @@ export function ModelProviderConnectionsPanel({
}
function refreshConnectModels(draft: ConnectionDraft) {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
previewModels.mutate(
{
provider,