From b6a9b1ea7b6eadb4650b109d6eecb67f6d8fbff9 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sat, 16 May 2026 23:55:34 -0700 Subject: [PATCH 1/6] Update chat empty state and Auto space mode (#954) --- apps/mcp/src/client.ts | 2 +- apps/mcp/src/server.ts | 4 +- apps/web/app/(app)/page.tsx | 10 +- .../chat/chat-graph-context-rail.tsx | 8 +- .../components/chat/home-chat-composer.tsx | 42 ++--- apps/web/components/chat/index.tsx | 178 +++++++++++------- apps/web/components/select-spaces-modal.tsx | 80 +++++++- apps/web/components/space-selector.tsx | 29 ++- apps/web/lib/chat-auto-space.ts | 1 + 9 files changed, 250 insertions(+), 104 deletions(-) create mode 100644 apps/web/lib/chat-auto-space.ts diff --git a/apps/mcp/src/client.ts b/apps/mcp/src/client.ts index ace65912f..8fdb6748d 100644 --- a/apps/mcp/src/client.ts +++ b/apps/mcp/src/client.ts @@ -329,7 +329,7 @@ export class SupermemoryClient { async getDocuments( containerTags?: string[], page = 1, - limit = 200, + limit = 10, ): Promise { try { const response = await fetch(`${this.apiUrl}/v3/documents/documents`, { diff --git a/apps/mcp/src/server.ts b/apps/mcp/src/server.ts index 682e75c14..510c1481a 100644 --- a/apps/mcp/src/server.ts +++ b/apps/mcp/src/server.ts @@ -318,7 +318,7 @@ export class SupermemoryMCP extends McpAgent { ? [effectiveContainerTag] : undefined - const result = await client.getDocuments(containerTags, 1, 200) + const result = await client.getDocuments(containerTags, 1, 10) const memoryCount = result.documents.reduce( (sum, d) => sum + d.memoryEntries.length, @@ -366,7 +366,7 @@ export class SupermemoryMCP extends McpAgent { inputSchema: z.object({ containerTag: z.string().optional(), page: z.number().optional().default(1), - limit: z.number().optional().default(200), + limit: z.number().optional().default(10), }), _meta: { ui: { diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index da76a70bf..f5bb9aeb6 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -159,6 +159,9 @@ export default function NewPage() { const [fullscreenInitialContent, setFullscreenInitialContent] = useState("") const [queuedChatSeed, setQueuedChatSeed] = useState(null) const [queuedChatModel, setQueuedChatModel] = useState(null) + const [queuedChatProject, setQueuedChatProject] = useState( + null, + ) const [queuedHighlightContent, setQueuedHighlightContent] = useState< string | null >(null) @@ -473,6 +476,7 @@ export default function NewPage() { setQueuedHighlightContent(highlightContent) setQueuedChatSeed(userReply) setQueuedChatModel(null) + setQueuedChatProject(null) setQueuedMessageSource("highlight") void setViewMode("chat") }, @@ -480,10 +484,11 @@ export default function NewPage() { ) const handleHomeChatStart = useCallback( - (message: string, model: ModelId) => { + (message: string, model: ModelId, projectId: string) => { setQueuedHighlightContent(null) setQueuedChatSeed(message) setQueuedChatModel(model) + setQueuedChatProject(projectId) setQueuedMessageSource("home") void setViewMode("chat") }, @@ -493,6 +498,7 @@ export default function NewPage() { const consumeQueuedChat = useCallback(() => { setQueuedChatSeed(null) setQueuedChatModel(null) + setQueuedChatProject(null) setQueuedHighlightContent(null) setQueuedMessageSource("highlight") }, []) @@ -608,7 +614,7 @@ export default function NewPage() { onConsumeQueuedMessage={consumeQueuedChat} queuedMessageSource={queuedMessageSource} initialSelectedModel={queuedChatModel} - emptyStateSuggestions={highlightsData?.questions} + initialChatProject={queuedChatProject} /> ) : viewMode === "integrations" ? ( diff --git a/apps/web/components/chat/chat-graph-context-rail.tsx b/apps/web/components/chat/chat-graph-context-rail.tsx index 5e712cf73..cb83141bd 100644 --- a/apps/web/components/chat/chat-graph-context-rail.tsx +++ b/apps/web/components/chat/chat-graph-context-rail.tsx @@ -11,12 +11,18 @@ import { dmSansClassName } from "@/lib/fonts" export function ChatGraphContextRail({ messages, + containerTags, className, }: { messages: UIMessage[] + containerTags?: string[] | null className?: string }) { const { effectiveContainerTags } = useProject() + const graphContainerTags = + containerTags === undefined + ? effectiveContainerTags + : (containerTags ?? undefined) const highlightIds = useMemo( () => extractHighlightDocumentIdsFromMessages(messages), [messages], @@ -45,7 +51,7 @@ export function ChatGraphContextRail({
0} diff --git a/apps/web/components/chat/home-chat-composer.tsx b/apps/web/components/chat/home-chat-composer.tsx index 00179658c..98d75eed1 100644 --- a/apps/web/components/chat/home-chat-composer.tsx +++ b/apps/web/components/chat/home-chat-composer.tsx @@ -1,41 +1,33 @@ "use client" -import { useCallback, useMemo, useState } from "react" +import { useCallback, useState } from "react" import ChatInput from "./input" import ChatModelSelector from "./model-selector" -import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" import { useProject } from "@/stores" -import { useContainerTags } from "@/hooks/use-container-tags" -import { dmSansClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import type { ModelId } from "@/lib/models" +import { SpaceSelector } from "@/components/space-selector" export function HomeChatComposer({ onStartChat, className, }: { - onStartChat: (message: string, model: ModelId) => void + onStartChat: (message: string, model: ModelId, projectId: string) => void className?: string }) { const [input, setInput] = useState("") const [selectedModel, setSelectedModel] = useState("gemini-2.5-pro") const { selectedProject } = useProject() - const { allProjects } = useContainerTags() - const chatSpaceLabel = useMemo( - () => - getChatSpaceDisplayLabel({ - selectedProject, - allProjects, - }), - [selectedProject, allProjects], - ) + const [chatSpaceProjects, setChatSpaceProjects] = useState([ + selectedProject, + ]) const send = useCallback(() => { const t = input.trim() if (!t) return - onStartChat(t, selectedModel) + onStartChat(t, selectedModel, chatSpaceProjects[0] ?? selectedProject) setInput("") - }, [input, onStartChat, selectedModel]) + }, [chatSpaceProjects, input, onStartChat, selectedModel, selectedProject]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { @@ -62,17 +54,13 @@ export function HomeChatComposer({ onModelChange={setSelectedModel} minimal /> -
- - {chatSpaceLabel} - -
+ } /> diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index cc0ea763c..adfeabf2a 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -22,7 +22,6 @@ import { ChevronDownIcon, HistoryIcon, Plus, - SearchIcon, SquarePenIcon, Trash2, XIcon, @@ -33,11 +32,11 @@ import { dmSansClassName } from "@/lib/fonts" import ChatInput from "./input" import ChatModelSelector from "./model-selector" import { getNovaChatErrorCopy } from "@/lib/chat-stream-error" -import { GradientLogo, LogoBgGradient } from "@ui/assets/Logo" import { useProject } from "@/stores" import { useContainerTags } from "@/hooks/use-container-tags" import { getChatSpaceDisplayLabel } from "@/lib/chat-space-label" import { modelNames, type ModelId } from "@/lib/models" +import { SpaceSelector } from "@/components/space-selector" import { SuperLoader } from "../superloader" import { UserMessage } from "./message/user-message" import { AgentMessage } from "./message/agent-message" @@ -49,50 +48,78 @@ import { analytics } from "@/lib/analytics" import { generateId } from "@lib/generate-id" import { useViewMode } from "@/lib/view-mode-context" import { threadParam } from "@/lib/search-params" +import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" -const DEFAULT_SUGGESTIONS = [ - "Show me all content related to Supermemory.", - "Summarize the key ideas from My Gita.", - "Which memories connect design and AI?", - "What are the main themes across my memories?", +const DEFAULT_CHAT_PROMPTS = [ + "What do you know about me?", + "What have I been working on lately?", + "What themes keep showing up in my memories?", ] +const chatEmptyCardClass = cn( + "flex min-h-[76px] flex-col justify-between rounded-lg border border-[#2B3038] bg-[#14161A]/95 p-3 text-left md:min-h-[88px]", + "shadow-[0_18px_50px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.04)]", + "transition-colors hover:border-[#3374FF]/55 hover:bg-[#1A1F26] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3374FF]/70", +) + function ChatEmptyStatePlaceholder({ onSuggestionClick, - suggestions = DEFAULT_SUGGESTIONS, + suggestions = DEFAULT_CHAT_PROMPTS, }: { onSuggestionClick: (suggestion: string) => void suggestions?: string[] }) { + const promptCards = suggestions.slice(0, 3) + return (
-
- - -
-
-

Ask me anything about your memories…

-
+
+
+ +

- {suggestions.map((suggestion) => ( - + ))}

@@ -150,6 +177,7 @@ export function ChatSidebar({ onConsumeQueuedMessage, queuedMessageSource = "highlight", initialSelectedModel = null, + initialChatProject = null, emptyStateSuggestions, layout = "sidebar", }: { @@ -160,6 +188,7 @@ export function ChatSidebar({ onConsumeQueuedMessage?: () => void queuedMessageSource?: "highlight" | "home" initialSelectedModel?: ModelId | null + initialChatProject?: string | null emptyStateSuggestions?: string[] layout?: "sidebar" | "page" }) { @@ -196,16 +225,22 @@ export function ChatSidebar({ const pendingHighlightMessageRef = useRef(null) const targetHighlightChatIdRef = useRef(null) const { selectedProject } = useProject() + const [chatSpaceProjects, setChatSpaceProjects] = useState([ + initialChatProject ?? selectedProject, + ]) + const chatProject = chatSpaceProjects[0] ?? selectedProject const { allProjects } = useContainerTags() - const selectedProjectRef = useRef(selectedProject) - selectedProjectRef.current = selectedProject + const selectedProjectRef = useRef(chatProject) + selectedProjectRef.current = chatProject const chatSpaceLabel = useMemo( () => - getChatSpaceDisplayLabel({ - selectedProject, - allProjects, - }), - [selectedProject, allProjects], + chatProject === AUTO_CHAT_SPACE_ID + ? "Auto" + : getChatSpaceDisplayLabel({ + selectedProject: chatProject, + allProjects, + }), + [chatProject, allProjects], ) const { viewMode } = useViewMode() const { user: _user } = useAuth() @@ -232,6 +267,12 @@ export function ChatSidebar({ metadata: { chatId: chatIdRef.current, projectId: selectedProjectRef.current, + spaceMode: + selectedProjectRef.current === AUTO_CHAT_SPACE_ID + ? "auto" + : "manual", + enableSpaceDiscovery: + selectedProjectRef.current === AUTO_CHAT_SPACE_ID, model: selectedModelRef.current, }, }, @@ -326,6 +367,25 @@ export function ChatSidebar({ scrollToBottom() } + const handleSuggestedQuestion = useCallback( + (suggestion: string) => { + if (status === "submitted" || status === "streaming") return + if (!threadId) setThreadId(fallbackChatId) + analytics.chatSuggestedQuestionClicked() + analytics.chatMessageSent({ source: "suggested" }) + sendMessage({ text: suggestion }) + scrollToBottom() + }, + [ + fallbackChatId, + sendMessage, + setThreadId, + status, + threadId, + scrollToBottom, + ], + ) + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() @@ -394,7 +454,7 @@ export function ChatSidebar({ setIsLoadingThreads(true) try { const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads?projectId=${selectedProject}`, + `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/threads?projectId=${chatProject}`, { credentials: "include" }, ) if (response.ok) { @@ -406,7 +466,7 @@ export function ChatSidebar({ } finally { setIsLoadingThreads(false) } - }, [selectedProject]) + }, [chatProject]) useEffect(() => { if (!isHistoryOpen) return @@ -908,20 +968,13 @@ export function ChatSidebar({ selectedModel={selectedModel} onModelChange={handleModelChange} /> -
- - {chatSpaceLabel} - -
+ )}
@@ -948,11 +1001,7 @@ export function ChatSidebar({ )} {messages.length === 0 && ( { - analytics.chatSuggestedQuestionClicked() - analytics.chatMessageSent({ source: "suggested" }) - sendMessage({ text: suggestion }) - }} + onSuggestionClick={handleSuggestedQuestion} suggestions={emptyStateSuggestions} /> )} @@ -1105,17 +1154,13 @@ export function ChatSidebar({ onModelChange={handleModelChange} minimal /> -
- - {chatSpaceLabel} - -
+ ) : undefined } @@ -1165,7 +1210,12 @@ export function ChatSidebar({ {chatHistorySheet} {isPageDesktop ? (
- +
{pageDesktopToolbarRow}
diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index 4162dd571..55fdaf9fc 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -19,6 +19,7 @@ import { Loader, Pencil, Check, + Sparkles, } from "lucide-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { toast } from "sonner" @@ -43,6 +44,7 @@ import { } from "@/lib/plugin-catalog" import { InstallSteps, PillButton } from "./integrations/install-steps" import { useProjectMutations } from "@/hooks/use-project-mutations" +import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" interface SelectSpacesModalProps { isOpen: boolean @@ -52,6 +54,7 @@ interface SelectSpacesModalProps { projects: ContainerTagListType[] recents?: string[] showNewSpace?: boolean + includeAuto?: boolean onNewSpace?: () => void enableDelete?: boolean onDeleteRequest?: (project: { @@ -90,6 +93,7 @@ export function SelectSpacesModal({ projects, recents, showNewSpace = false, + includeAuto = false, onNewSpace, enableDelete = false, onDeleteRequest, @@ -198,6 +202,7 @@ export function SelectSpacesModal({ const defaultCategory = useMemo(() => { if (!currentSelection) return "all" + if (currentSelection === AUTO_CHAT_SPACE_ID) return "all" const plugin = detectPluginSpace(currentSelection) if (plugin) return `plugin:${plugin.pluginId}` return "my" @@ -380,6 +385,15 @@ export function SelectSpacesModal({ [onApply], ) + const handleSelectAuto = useCallback(() => { + setEditingProject(null) + setIsBulkDeleteMode(false) + setBulkDeleteTags(new Set()) + setLastBulkDeleteTag(null) + onApply([AUTO_CHAT_SPACE_ID]) + setSearchQuery("") + }, [onApply]) + const handleBulkModeToggle = useCallback(() => { setEditingProject(null) setBulkDeleteTags(new Set()) @@ -478,6 +492,19 @@ export function SelectSpacesModal({ [filteredProjects, recentSet], ) + const showAutoRow = useMemo(() => { + if (!includeAuto) return false + if (isBulkDeleteMode) return false + if (activeCategory !== "all" && activeCategory !== "my") return false + const query = searchQuery.trim().toLowerCase() + if (!query) return true + return ( + "auto".includes(query) || + "let nova choose the right spaces".includes(query) || + "discover spaces".includes(query) + ) + }, [includeAuto, isBulkDeleteMode, activeCategory, searchQuery]) + const visibleBulkDeleteTags = useMemo( () => [...recentProjects, ...mainList] @@ -742,6 +769,48 @@ export function SelectSpacesModal({ ], ) + const renderAutoRow = useCallback(() => { + const isSelected = currentSelection === AUTO_CHAT_SPACE_ID + return ( +
+
+ {isSelected &&
} +
+ +
+ ) + }, [currentSelection, handleSelectAuto]) + return (
- {filteredProjects.length === 0 ? ( + {filteredProjects.length === 0 && !showAutoRow ? (

No spaces found

) : (
+ {showAutoRow && ( + <> +
+ Mode +
+ {renderAutoRow()} +
+ + )} {recentProjects.length > 0 && ( <>
diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index 3b0daa718..689219b18 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -7,8 +7,9 @@ import { cn } from "@lib/utils" import { $fetch } from "@lib/api" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { DEFAULT_PROJECT_ID } from "@lib/constants" -import { XIcon, Loader2, Trash2 } from "lucide-react" +import { ChevronDownIcon, Sparkles, XIcon, Loader2, Trash2 } from "lucide-react" import type { ContainerTagListType } from "@lib/types" +import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" import { AddSpaceModal } from "./add-space-modal" import { SelectSpacesModal } from "./select-spaces-modal" import { useProjectMutations } from "@/hooks/use-project-mutations" @@ -46,6 +47,7 @@ export interface SpaceSelectorProps { showNewSpace?: boolean enableDelete?: boolean compact?: boolean + includeAuto?: boolean } const triggerVariants = { @@ -104,6 +106,7 @@ export function SpaceSelector({ showNewSpace = true, enableDelete = false, compact = false, + includeAuto = false, }: SpaceSelectorProps) { const [showCreateDialog, setShowCreateDialog] = useState(false) const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false) @@ -158,7 +161,7 @@ export function SpaceSelector({ return data?.pagination?.totalItems ?? 0 }, staleTime: 30 * 1000, - enabled: !!activeTag, + enabled: !!activeTag && activeTag !== AUTO_CHAT_SPACE_ID, }) const pluginTags = useMemo( @@ -176,10 +179,14 @@ export function SpaceSelector({ name: string emoji: string | null plugin: ReturnType + isAuto: boolean }>(() => { const containerTag = selectedProjects[0] ?? "" + if (includeAuto && containerTag === AUTO_CHAT_SPACE_ID) { + return { name: "Auto", emoji: null, plugin: null, isAuto: true } + } if (!containerTag || containerTag === DEFAULT_PROJECT_ID) { - return { name: "My Space", emoji: "📁", plugin: null } + return { name: "My Space", emoji: "📁", plugin: null, isAuto: false } } const found = allProjects.find( (p: ContainerTagListType) => p.containerTag === containerTag, @@ -195,8 +202,9 @@ export function SpaceSelector({ : spaceSelectorDisplayName(found, containerTag), emoji: found?.emoji || "📁", plugin, + isAuto: false, } - }, [allProjects, selectedProjects, pluginMetaMap]) + }, [allProjects, selectedProjects, pluginMetaMap, includeAuto]) const pushRecent = useCallback((tag: string) => { setRecents((prev) => { @@ -212,7 +220,7 @@ export function SpaceSelector({ const selectedTag = next[0] setShowSelectSpacesModal(false) onValueChange(next) - if (selectedTag) { + if (selectedTag && selectedTag !== AUTO_CHAT_SPACE_ID) { queueMicrotask(() => { analytics.spaceSwitched({ space_id: selectedTag }) pushRecent(selectedTag) @@ -356,7 +364,9 @@ export function SpaceSelector({ triggerClassName, )} > - {displayInfo.plugin ? ( + {displayInfo.isAuto ? ( + + ) : displayInfo.plugin ? ( displayInfo.plugin.iconSrc ? ( )} + {!compact && ( + + )} {compact && ( {isLoading ? "Loading" : displayInfo.name} @@ -433,6 +449,7 @@ export function SpaceSelector({ projects={allProjects} recents={recents} showNewSpace={showNewSpace} + includeAuto={includeAuto} onNewSpace={handleNewSpace} enableDelete={enableDelete} onDeleteRequest={handleDeleteRequest} diff --git a/apps/web/lib/chat-auto-space.ts b/apps/web/lib/chat-auto-space.ts new file mode 100644 index 000000000..8f7662486 --- /dev/null +++ b/apps/web/lib/chat-auto-space.ts @@ -0,0 +1 @@ +export const AUTO_CHAT_SPACE_ID = "__supermemory_auto_space__" From 6df9c8f462533e07be7e36401ec4d33f0acf627e Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 17 May 2026 00:02:46 -0700 Subject: [PATCH 2/6] Fix space selector overflow (#955) --- apps/web/components/select-spaces-modal.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index 55fdaf9fc..1a2ca2fb7 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -815,10 +815,11 @@ export function SelectSpacesModal({
-
+
{categories.map((category) => { const isActive = activeCategory === category.id @@ -984,7 +985,7 @@ export function SelectSpacesModal({
-
+
{activeCategory.startsWith("discover:") ? (
-
- {filteredProjects.length === 0 && !showAutoRow ? ( +
+ {filteredProjects.length === 0 ? (

No spaces found

From ba8eb52b94bce998f78ae41a20e21200d4a962c4 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 17 May 2026 00:15:37 -0700 Subject: [PATCH 3/6] Show own space as Your conversations (#956) --- apps/web/components/dashboard-view.tsx | 10 +++++-- apps/web/components/select-spaces-modal.tsx | 32 +++++++++++++++------ apps/web/components/space-selector.tsx | 16 +++++++++-- apps/web/hooks/use-personalization.ts | 18 +++++++++--- apps/web/lib/ingest-auto-space.ts | 14 +++++++++ 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index 2632446d5..411f0b89c 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -723,10 +723,16 @@ export function DashboardView({ firstName, ) + const [tipIndex, setTipIndex] = useState(0) + + useEffect(() => { + setTipIndex(Math.floor(Math.random() * TIPS[profession].length)) + }, [profession]) + const tip = useMemo(() => { const tips = TIPS[profession] - return tips[Math.floor(Math.random() * tips.length)] - }, [profession]) + return tips[tipIndex % tips.length] ?? tips[0] + }, [profession, tipIndex]) return (
( null, @@ -455,15 +456,19 @@ export function SelectSpacesModal({ return byCategory.filter((p) => { const plugin = detectPluginSpace(p.containerTag) const projectName = pluginMetaMap.get(p.containerTag)?.projectName + const displayName = spaceSelectorDisplayName(p, p.containerTag, { + currentUserId: user?.id, + }) return ( p.containerTag.toLowerCase().includes(query) || (p.name ?? "").toLowerCase().includes(query) || + displayName.toLowerCase().includes(query) || (plugin?.label.toLowerCase().includes(query) ?? false) || (plugin?.projectId?.toLowerCase().includes(query) ?? false) || (projectName?.toLowerCase().includes(query) ?? false) ) }) - }, [allSpaces, activeCategory, searchQuery, pluginMetaMap]) + }, [allSpaces, activeCategory, searchQuery, pluginMetaMap, user?.id]) const recentProjects = useMemo(() => { if (!recents?.length) return [] @@ -551,10 +556,12 @@ export function SelectSpacesModal({ ) .map((project) => ({ id: project.id, - name: project.name ?? project.containerTag, + name: spaceSelectorDisplayName(project, project.containerTag, { + currentUserId: user?.id, + }), containerTag: project.containerTag, })), - [allSpaces, bulkDeleteTags], + [allSpaces, bulkDeleteTags, user?.id], ) const bulkDeleteCount = bulkDeleteProjects.length @@ -567,8 +574,16 @@ export function SelectSpacesModal({ project.containerTag, )?.projectName const pluginIdLabel = pluginProjectName || plugin?.projectId + const displayName = spaceSelectorDisplayName( + project, + project.containerTag, + { + currentUserId: user?.id, + }, + ) const isDefault = project.containerTag === DEFAULT_PROJECT_ID - const canEdit = !isDefault && !plugin + const isOwnSpace = isOwnConversationSpace(project, user?.id) + const canEdit = !isDefault && !plugin && !isOwnSpace const canBulkDelete = enableDelete && !isDefault const isEditing = editingProject?.containerTag === project.containerTag const isBulkDeleteSelected = bulkDeleteTags.has(project.containerTag) @@ -697,7 +712,7 @@ export function SelectSpacesModal({ )} {plugin ? ( <> @@ -709,7 +724,7 @@ export function SelectSpacesModal({ )} ) : ( - spaceSelectorDisplayName(project, project.containerTag) + displayName )} @@ -738,7 +753,7 @@ export function SelectSpacesModal({ e.stopPropagation() onDeleteRequest({ id: project.id, - name: project.name ?? project.containerTag, + name: displayName, containerTag: project.containerTag, }) }} @@ -766,6 +781,7 @@ export function SelectSpacesModal({ startEditing, toggleBulkDeleteTag, updateProjectMutation.isPending, + user?.id, ], ) diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index 689219b18..40d5947ea 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -31,6 +31,7 @@ import { } from "@repo/ui/components/select" import { Button } from "@repo/ui/components/button" import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip" +import { useAuth } from "@lib/auth-context" import { analytics } from "@/lib/analytics" import { compareSpacesUserFirst, @@ -135,6 +136,7 @@ export function SpaceSelector({ const { deleteProjectMutation, deleteProjectsMutation } = useProjectMutations() const { allProjects, isLoading } = useContainerTags() + const { user } = useAuth() useEffect(() => { setRecents(readRecents()) @@ -199,12 +201,14 @@ export function SpaceSelector({ ? idForLabel ? `${plugin.label} · ${idForLabel}` : plugin.label - : spaceSelectorDisplayName(found, containerTag), + : spaceSelectorDisplayName(found, containerTag, { + currentUserId: user?.id, + }), emoji: found?.emoji || "📁", plugin, isAuto: false, } - }, [allProjects, selectedProjects, pluginMetaMap, includeAuto]) + }, [allProjects, selectedProjects, pluginMetaMap, includeAuto, user?.id]) const pushRecent = useCallback((tag: string) => { setRecents((prev) => { @@ -615,7 +619,13 @@ export function SpaceSelector({ )} ) : ( - spaceSelectorDisplayName(p, p.containerTag) + spaceSelectorDisplayName( + p, + p.containerTag, + { + currentUserId: user?.id, + }, + ) )} diff --git a/apps/web/hooks/use-personalization.ts b/apps/web/hooks/use-personalization.ts index 256fab3c2..619b3ac2f 100644 --- a/apps/web/hooks/use-personalization.ts +++ b/apps/web/hooks/use-personalization.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from "react" import { $fetch } from "@lib/api" -import type { SearchResult } from "@repo/lib/api" +import type { SearchResult } from "@repo/validation/api" const CACHE_KEY = "sm_profession_v1" const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000 @@ -235,6 +235,15 @@ function pickCopy(p: Profession): PersonalizedCopy { } } +function defaultCopy(p: Profession): PersonalizedCopy { + const pool = COPY_POOLS[p] + return { + saveLink: pool.saveLink[0] ?? "", + writeNote: pool.writeNote[0] ?? "", + chatPlaceholder: pool.chatPlaceholder[0] ?? "", + } +} + const sessionCopyCache: Partial> = {} function getSessionCopy(p: Profession): PersonalizedCopy { @@ -398,7 +407,7 @@ export function usePersonalization(): { setProfession: (p: Profession) => void } { const [copy, setCopy] = useState(() => - getSessionCopy("default"), + defaultCopy("default"), ) const [profession, setProfessionState] = useState("default") @@ -410,8 +419,9 @@ export function usePersonalization(): { ) } catch {} // Re-pick on explicit change so the user sees fresh copy for the new identity - sessionCopyCache[p] = pickCopy(p) - setCopy(sessionCopyCache[p]!) + const freshCopy = pickCopy(p) + sessionCopyCache[p] = freshCopy + setCopy(freshCopy) setProfessionState(p) }, []) diff --git a/apps/web/lib/ingest-auto-space.ts b/apps/web/lib/ingest-auto-space.ts index 9768a27e7..3f38a85a0 100644 --- a/apps/web/lib/ingest-auto-space.ts +++ b/apps/web/lib/ingest-auto-space.ts @@ -1,6 +1,8 @@ import { DEFAULT_PROJECT_ID } from "@lib/constants" import type { ContainerTagListType } from "@lib/types" +export const OWN_CONVERSATIONS_SPACE_NAME = "Your conversations" + /** * Spaces auto-created on first ingest use `name === \`Space ${containerTag}\`` * (mono `apps/api/src/routes/memories/handler-effect.ts`). Those are noisy in the @@ -24,10 +26,22 @@ export function compareSpacesUserFirst( ) } +export function isOwnConversationSpace( + p: Pick | undefined, + currentUserId?: string | null, +): boolean { + return !!currentUserId && p?.containerTag === currentUserId +} + export function spaceSelectorDisplayName( p: Pick | undefined, fallback: string, + options?: { currentUserId?: string | null }, ): string { + const containerTag = p?.containerTag ?? fallback + if (containerTag === options?.currentUserId) { + return OWN_CONVERSATIONS_SPACE_NAME + } if (!p) return fallback const name = p.name ?? p.containerTag const long = name.length > 44 From fd78633fcfb4d0a37b41d2f3f9b7509b07a6b0cd Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 17 May 2026 00:25:22 -0700 Subject: [PATCH 4/6] Brand own space as Nova chats (#957) --- apps/web/components/select-spaces-modal.tsx | 3 ++ apps/web/components/space-selector.tsx | 32 +++++++++++++++++++-- apps/web/lib/ingest-auto-space.ts | 4 +-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx index 7fdfaefd9..7529d5215 100644 --- a/apps/web/components/select-spaces-modal.tsx +++ b/apps/web/components/select-spaces-modal.tsx @@ -46,6 +46,7 @@ import { import { InstallSteps, PillButton } from "./integrations/install-steps" import { useProjectMutations } from "@/hooks/use-project-mutations" import { AUTO_CHAT_SPACE_ID } from "@/lib/chat-auto-space" +import NovaOrb from "@/components/nova/nova-orb" interface SelectSpacesModalProps { isOpen: boolean @@ -705,6 +706,8 @@ export function SelectSpacesModal({ {pluginInitial(plugin.label)} ) + ) : isOwnSpace ? ( + ) : ( {project.emoji || "📁"} diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx index 40d5947ea..73d42e2a5 100644 --- a/apps/web/components/space-selector.tsx +++ b/apps/web/components/space-selector.tsx @@ -35,10 +35,12 @@ import { useAuth } from "@lib/auth-context" import { analytics } from "@/lib/analytics" import { compareSpacesUserFirst, + isOwnConversationSpace, spaceSelectorDisplayName, } from "@/lib/ingest-auto-space" import { detectPluginSpace, pluginInitial } from "@/lib/plugin-space" import { usePluginSpaceMeta } from "@/hooks/use-plugin-space-meta" +import NovaOrb from "@/components/nova/nova-orb" export interface SpaceSelectorProps { selectedProjects: string[] @@ -182,18 +184,32 @@ export function SpaceSelector({ emoji: string | null plugin: ReturnType isAuto: boolean + isOwnSpace: boolean }>(() => { const containerTag = selectedProjects[0] ?? "" if (includeAuto && containerTag === AUTO_CHAT_SPACE_ID) { - return { name: "Auto", emoji: null, plugin: null, isAuto: true } + return { + name: "Auto", + emoji: null, + plugin: null, + isAuto: true, + isOwnSpace: false, + } } if (!containerTag || containerTag === DEFAULT_PROJECT_ID) { - return { name: "My Space", emoji: "📁", plugin: null, isAuto: false } + return { + name: "My Space", + emoji: "📁", + plugin: null, + isAuto: false, + isOwnSpace: false, + } } const found = allProjects.find( (p: ContainerTagListType) => p.containerTag === containerTag, ) const plugin = detectPluginSpace(containerTag) + const isOwnSpace = isOwnConversationSpace({ containerTag }, user?.id) const projectName = pluginMetaMap.get(containerTag)?.projectName const idForLabel = projectName || plugin?.projectId return { @@ -207,6 +223,7 @@ export function SpaceSelector({ emoji: found?.emoji || "📁", plugin, isAuto: false, + isOwnSpace, } }, [allProjects, selectedProjects, pluginMetaMap, includeAuto, user?.id]) @@ -370,6 +387,11 @@ export function SpaceSelector({ > {displayInfo.isAuto ? ( + ) : displayInfo.isOwnSpace ? ( + ) : displayInfo.plugin ? ( displayInfo.plugin.iconSrc ? ( { const plugin = detectPluginSpace(p.containerTag) + const isOwnSpace = isOwnConversationSpace(p, user?.id) return ( ) + ) : isOwnSpace ? ( + ) : ( {p.emoji || "📁"} )} diff --git a/apps/web/lib/ingest-auto-space.ts b/apps/web/lib/ingest-auto-space.ts index 3f38a85a0..a4d2f8dc5 100644 --- a/apps/web/lib/ingest-auto-space.ts +++ b/apps/web/lib/ingest-auto-space.ts @@ -1,7 +1,7 @@ import { DEFAULT_PROJECT_ID } from "@lib/constants" import type { ContainerTagListType } from "@lib/types" -export const OWN_CONVERSATIONS_SPACE_NAME = "Your conversations" +export const OWN_CHAT_SPACE_NAME = "Nova chats" /** * Spaces auto-created on first ingest use `name === \`Space ${containerTag}\`` @@ -40,7 +40,7 @@ export function spaceSelectorDisplayName( ): string { const containerTag = p?.containerTag ?? fallback if (containerTag === options?.currentUserId) { - return OWN_CONVERSATIONS_SPACE_NAME + return OWN_CHAT_SPACE_NAME } if (!p) return fallback const name = p.name ?? p.containerTag From 28a60f56203e04896f38f938b1eaa59f7756f929 Mon Sep 17 00:00:00 2001 From: Dhravya Shah Date: Sun, 17 May 2026 00:43:52 -0700 Subject: [PATCH 5/6] Fix Nova chat composer spacing (#958) --- apps/web/components/chat/home-chat-composer.tsx | 2 +- apps/web/components/chat/index.tsx | 14 ++++++++++++-- apps/web/components/chat/input/index.tsx | 9 ++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/web/components/chat/home-chat-composer.tsx b/apps/web/components/chat/home-chat-composer.tsx index 98d75eed1..9e50fe337 100644 --- a/apps/web/components/chat/home-chat-composer.tsx +++ b/apps/web/components/chat/home-chat-composer.tsx @@ -38,7 +38,7 @@ export function HomeChatComposer({ return (
-
+
setInput(e.target.value)} diff --git a/apps/web/components/chat/index.tsx b/apps/web/components/chat/index.tsx index adfeabf2a..5780bc61d 100644 --- a/apps/web/components/chat/index.tsx +++ b/apps/web/components/chat/index.tsx @@ -776,6 +776,9 @@ export function ChatSidebar({ const isStackedInput = layout === "page" const showHeaderRow = !isPageDesktop || isMobile || !isStackedInput + const isResponding = status === "submitted" || status === "streaming" + const showInputStatusStrip = + !isStackedInput || isResponding || messages.length > 0 const chatHistorySheet = ( )} -
+
setInput(e.target.value)} onSend={handleSend} onStop={stop} onKeyDown={handleKeyDown} - isResponding={status === "submitted" || status === "streaming"} + isResponding={isResponding} activeStatus={ status === "submitted" ? "Thinking…" @@ -1142,6 +1151,7 @@ export function ChatSidebar({ ? "Structuring response…" : "Waiting for input…" } + showStatusStrip={showInputStatusStrip} onExpandedChange={setIsInputExpanded} chainOfThoughtComponent={ messages.length > 0 ? : null diff --git a/apps/web/components/chat/input/index.tsx b/apps/web/components/chat/input/index.tsx index b1ee11092..1c66e7aa2 100644 --- a/apps/web/components/chat/input/index.tsx +++ b/apps/web/components/chat/input/index.tsx @@ -4,7 +4,7 @@ import { ChevronUpIcon } from "lucide-react" import NovaOrb from "@/components/nova/nova-orb" import { cn } from "@lib/utils" import { dmSansClassName } from "@/lib/fonts" -import { type ReactNode, useRef, useState } from "react" +import { type ReactNode, useEffect, useRef, useState } from "react" import { motion } from "motion/react" import { SendButton, StopButton } from "./actions" @@ -41,6 +41,13 @@ export default function ChatInput({ const [isExpanded, setIsExpanded] = useState(false) const textareaRef = useRef(null) + useEffect(() => { + if (!showStatusStrip && isExpanded) { + setIsExpanded(false) + onExpandedChange?.(false) + } + }, [isExpanded, onExpandedChange, showStatusStrip]) + const handleChange = (e: React.ChangeEvent) => { onChange(e) From 36ecf471107e7612d32dcedccaba0671105a18eb Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Sun, 17 May 2026 13:25:12 +0530 Subject: [PATCH 6/6] Fix modal memory graph scoping by document and space (#948) --- .../document-modal/graph-list-memories.tsx | 5 ++ .../memory-graph/hooks/use-graph-api.ts | 65 ++++++++++++++----- .../memory-graph/memory-graph-wrapper.tsx | 2 + 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/apps/web/components/document-modal/graph-list-memories.tsx b/apps/web/components/document-modal/graph-list-memories.tsx index dc62cfbaa..c8f4ae8dd 100644 --- a/apps/web/components/document-modal/graph-list-memories.tsx +++ b/apps/web/components/document-modal/graph-list-memories.tsx @@ -2,6 +2,7 @@ import { useState } from "react" import { cn } from "@lib/utils" import { Tabs, TabsList, TabsTrigger, TabsContent } from "@ui/components/tabs" import { MemoryGraph } from "../memory-graph" +import { useProject } from "@/stores" export interface MemoryEntry { id: string @@ -172,6 +173,7 @@ export function GraphListMemories({ memoryEntries: MemoryEntry[] documentId?: string }) { + const { effectiveContainerTags } = useProject() const [expandedMemories, setExpandedMemories] = useState>( new Set(), ) @@ -263,7 +265,10 @@ export function GraphListMemories({
diff --git a/apps/web/components/memory-graph/hooks/use-graph-api.ts b/apps/web/components/memory-graph/hooks/use-graph-api.ts index 94c08e331..7991f600b 100644 --- a/apps/web/components/memory-graph/hooks/use-graph-api.ts +++ b/apps/web/components/memory-graph/hooks/use-graph-api.ts @@ -13,6 +13,7 @@ const PAGE_SIZE = 100 interface UseGraphApiOptions { containerTags?: string[] + documentIds?: string[] enabled?: boolean } @@ -81,7 +82,20 @@ function toGraphMemory(mem: ApiMemoryEntry): GraphApiMemory { } } -function toGraphDocument(doc: ApiDocument): GraphApiDocument { +function toGraphDocument( + doc: ApiDocument, + containerTags?: string[], +): GraphApiDocument { + const allowedContainerTags = new Set(containerTags?.filter(Boolean) ?? []) + const memoryEntries = + allowedContainerTags.size > 0 + ? doc.memoryEntries.filter( + (mem) => + mem.spaceContainerTag != null && + allowedContainerTags.has(mem.spaceContainerTag), + ) + : doc.memoryEntries + return { id: doc.id, title: doc.title, @@ -89,12 +103,15 @@ function toGraphDocument(doc: ApiDocument): GraphApiDocument { documentType: doc.type, createdAt: doc.createdAt, updatedAt: doc.updatedAt, - memories: doc.memoryEntries.map(toGraphMemory), + memories: memoryEntries.map(toGraphMemory), } } export function useGraphApi(options: UseGraphApiOptions = {}) { - const { containerTags, enabled = true } = options + const { containerTags, documentIds, enabled = true } = options + const filteredDocumentIds = documentIds?.filter(Boolean) + const hasDocumentIds = + filteredDocumentIds != null && filteredDocumentIds.length > 0 const { data, @@ -104,19 +121,33 @@ export function useGraphApi(options: UseGraphApiOptions = {}) { hasNextPage, fetchNextPage, } = useInfiniteQuery({ - queryKey: ["documents-with-memories", containerTags, []], + queryKey: [ + "documents-with-memories", + containerTags, + [], + filteredDocumentIds, + ], initialPageParam: 1, queryFn: async ({ pageParam }) => { - const response = await $fetch("@post/documents/documents", { - body: { - page: pageParam as number, - limit: PAGE_SIZE, - sort: "createdAt", - order: "desc", - containerTags, - }, - disableValidation: true, - }) + const response = hasDocumentIds + ? await $fetch("@post/documents/documents/by-ids", { + body: { + ids: filteredDocumentIds, + by: "id", + containerTags, + }, + disableValidation: true, + }) + : await $fetch("@post/documents/documents", { + body: { + page: pageParam as number, + limit: PAGE_SIZE, + sort: "createdAt", + order: "desc", + containerTags, + }, + disableValidation: true, + }) if (response.error) { throw new Error(response.error?.message || "Failed to fetch documents") @@ -134,8 +165,10 @@ export function useGraphApi(options: UseGraphApiOptions = {}) { const documents = useMemo(() => { if (!data?.pages) return [] - return data.pages.flatMap((page) => page.documents.map(toGraphDocument)) - }, [data]) + return data.pages.flatMap((page) => + page.documents.map((doc) => toGraphDocument(doc, containerTags)), + ) + }, [data, containerTags]) const totalCount = data?.pages[0]?.pagination.totalItems ?? 0 diff --git a/apps/web/components/memory-graph/memory-graph-wrapper.tsx b/apps/web/components/memory-graph/memory-graph-wrapper.tsx index ae2bf3271..0b2ca8269 100644 --- a/apps/web/components/memory-graph/memory-graph-wrapper.tsx +++ b/apps/web/components/memory-graph/memory-graph-wrapper.tsx @@ -29,6 +29,7 @@ export function MemoryGraph({ error: externalError = null, variant = "console", containerTags, + documentIds, maxNodes, canvasRef, ...rest @@ -57,6 +58,7 @@ export function MemoryGraph({ totalCount, } = useGraphApi({ containerTags, + documentIds, enabled: containerSize.width > 0 && containerSize.height > 0, })