@@ -370,12 +1196,6 @@ function SourceCard({
))}
- {soft && !isDone && (
-
{footerLeft}
@@ -430,158 +1250,3 @@ function DriveScopePicker({
)
}
-
-function MoreDrawer({
- open,
- onClose,
- containerTag,
- connectRealProvider,
-}: {
- open: boolean
- onClose: () => void
- containerTag: string
- connectRealProvider: (
- provider: "google-drive" | "notion" | "onedrive",
- id: SourceId,
- ) => void
-}) {
- return (
-
!o && onClose()}>
-
-
-
- More integrations
-
-
- Add any of these alongside your spotlight sources. Everything routes
- to {containerTag}.
-
-
-
- }
- action="Request access"
- soft
- />
- }
- action="Request access"
- soft
- />
- }
- action="Connect"
- onAction={() => connectRealProvider("onedrive", "onedrive")}
- />
- }
- action="Coming soon"
- soft
- />
- }
- action="Connect"
- soft
- />
- }
- action="Connect"
- soft
- />
- }
- action="Install"
- soft
- />
- }
- action="Install"
- soft
- />
- }
- action="Install"
- soft
- />
- }
- action="Import"
- soft
- />
-
-
-
- )
-}
-
-function MoreItem({
- title,
- blurb,
- icon,
- action,
- soft,
- onAction,
-}: {
- title: string
- blurb: string
- icon: React.ReactNode
- action: string
- soft?: boolean
- onAction?: () => void
-}) {
- return (
-
-
- {icon}
-
-
-
- {title}
-
-
- {blurb}
-
-
-
-
- )
-}
From 81ae72224fcea448ebff4ae44dbacda7d69b35cb Mon Sep 17 00:00:00 2001
From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com>
Date: Fri, 12 Jun 2026 20:51:37 +0000
Subject: [PATCH 2/4] feat(spaces): per-space entity context + edit-space modal
(#1078)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- "What to remember" per-space context — set on create, on edit, and in the space profile; steers what gets extracted into memory
- Edit-space modal (name + context) from the space pill's hover edit, mirroring the create modal
- Manual saves respect a space's configured context instead of overwriting it with the generic default
---
apps/web/components/add-space-modal.tsx | 118 +++++++++--
apps/web/components/edit-space-modal.tsx | 206 ++++++++++++++++++++
apps/web/components/header.tsx | 1 +
apps/web/components/space-profile-panel.tsx | 117 +++++++++--
apps/web/components/space-selector.tsx | 198 ++++++++++++-------
apps/web/hooks/use-document-mutations.ts | 30 ++-
apps/web/hooks/use-space-context.ts | 115 +++++++++++
7 files changed, 681 insertions(+), 104 deletions(-)
create mode 100644 apps/web/components/edit-space-modal.tsx
create mode 100644 apps/web/hooks/use-space-context.ts
diff --git a/apps/web/components/add-space-modal.tsx b/apps/web/components/add-space-modal.tsx
index 7c9f3cf3d..9f09da7cf 100644
--- a/apps/web/components/add-space-modal.tsx
+++ b/apps/web/components/add-space-modal.tsx
@@ -2,7 +2,7 @@
import { useState } from "react"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
-import { Dialog, DialogContent } from "@repo/ui/components/dialog"
+import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import { cn } from "@lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon, Loader2 } from "lucide-react"
@@ -10,6 +10,7 @@ import { Button } from "@ui/components/button"
import { useProjectMutations } from "@/hooks/use-project-mutations"
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
import { analytics } from "@/lib/analytics"
+import { $fetch } from "@lib/api"
const EMOJI_LIST = [
"📁",
@@ -62,6 +63,25 @@ const EMOJI_LIST = [
"🤍",
]
+export const CONTEXT_PRESETS: { label: string; text: string }[] = [
+ {
+ label: "Work project",
+ text: "Tracks a work project — decisions, owners, deadlines, and current status.",
+ },
+ {
+ label: "Client",
+ text: "About a client — meetings, requirements, and account context.",
+ },
+ {
+ label: "Research",
+ text: "Research notes — sources, key findings, and open questions.",
+ },
+ {
+ label: "Personal",
+ text: "My personal space — notes, ideas, and things to remember.",
+ },
+]
+
export function AddSpaceModal({
isOpen,
onClose,
@@ -72,6 +92,8 @@ export function AddSpaceModal({
onCreated?: (containerTag: string) => void
}) {
const [spaceName, setSpaceName] = useState("")
+ const [spaceContext, setSpaceContext] = useState("")
+ const [showContext, setShowContext] = useState(false)
const [emoji, setEmoji] = useState("📁")
const [isEmojiOpen, setIsEmojiOpen] = useState(false)
const { createProjectMutation } = useProjectMutations()
@@ -79,6 +101,8 @@ export function AddSpaceModal({
const handleClose = () => {
onClose()
setSpaceName("")
+ setSpaceContext("")
+ setShowContext(false)
setEmoji("📁")
}
@@ -89,11 +113,18 @@ export function AddSpaceModal({
createProjectMutation.mutate(
{ name: trimmedName, emoji: emoji || undefined },
{
- onSuccess: (data) => {
+ onSuccess: async (data) => {
analytics.spaceCreated()
- if (data?.containerTag) {
- onCreated?.(data.containerTag)
+ const tag = data?.containerTag
+ const context = showContext ? spaceContext.trim() : ""
+ if (tag && context) {
+ try {
+ await $fetch(`@patch/container-tags/${tag}`, {
+ body: { entityContext: context },
+ })
+ } catch {}
}
+ if (tag) onCreated?.(tag)
handleClose()
},
},
@@ -132,21 +163,21 @@ export function AddSpaceModal({
-
- Create new space
-
+ New space
+
- Create spaces to organize your memories and documents and create
- a context rich environment
+ Group related memories and give Nova context for this space.
+ {!showContext ? (
+
+ ) : (
+
+
+
+ What to remember
+
+
+ Optional
+
+
+
+ )}
+
) : (
<>
-
+
+
+
Create Space
>
)}
diff --git a/apps/web/components/edit-space-modal.tsx b/apps/web/components/edit-space-modal.tsx
new file mode 100644
index 000000000..434c2c1a2
--- /dev/null
+++ b/apps/web/components/edit-space-modal.tsx
@@ -0,0 +1,206 @@
+"use client"
+
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { Loader2, XIcon } from "lucide-react"
+import { useEffect, useState } from "react"
+import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
+import { useSpaceContext, useUpdateSpace } from "@/hooks/use-space-context"
+import { CONTEXT_PRESETS } from "@/components/add-space-modal"
+import { DEFAULT_PROJECT_ID } from "@lib/constants"
+import { cn } from "@lib/utils"
+import { Button } from "@ui/components/button"
+import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
+
+const INPUT_SHADOW =
+ "0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)"
+
+type EditSpaceModalProps = {
+ containerTag: string
+ currentName: string
+ currentEmoji?: string
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function EditSpaceModal({
+ containerTag,
+ currentName,
+ currentEmoji = "📁",
+ open,
+ onOpenChange,
+}: EditSpaceModalProps) {
+ const isDefault = !containerTag || containerTag === DEFAULT_PROJECT_ID
+ const { data, isLoading } = useSpaceContext(containerTag, open)
+ const update = useUpdateSpace()
+ const [name, setName] = useState(currentName)
+ const [context, setContext] = useState("")
+
+ useEffect(() => {
+ if (!open) return
+ setName(data?.name ?? currentName ?? "")
+ setContext(data?.entityContext ?? "")
+ }, [open, data?.name, data?.entityContext, currentName])
+
+ const handleSave = () => {
+ update.mutate(
+ {
+ containerTag,
+ name: isDefault ? undefined : name.trim() || undefined,
+ entityContext: context.trim() ? context.trim() : null,
+ },
+ { onSuccess: () => onOpenChange(false) },
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx
index e47f56c26..8abe42ab3 100644
--- a/apps/web/components/header.tsx
+++ b/apps/web/components/header.tsx
@@ -207,6 +207,7 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
selectedProjects={selectedProjects}
onValueChange={setSelectedProjects}
enableDelete
+ enableEdit
/>
>
)}
diff --git a/apps/web/components/space-profile-panel.tsx b/apps/web/components/space-profile-panel.tsx
index 11d83a042..9e8ef1194 100644
--- a/apps/web/components/space-profile-panel.tsx
+++ b/apps/web/components/space-profile-panel.tsx
@@ -1,9 +1,14 @@
"use client"
-import { Brain, X } from "lucide-react"
+import { Brain, Loader, X } from "lucide-react"
import { motion } from "motion/react"
+import { useEffect, useState } from "react"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { useSpaceProfile } from "@/hooks/use-space-profile"
+import {
+ useSpaceContext,
+ useUpdateSpaceContext,
+} from "@/hooks/use-space-context"
import { cn } from "@lib/utils"
type SpaceProfilePanelProps = {
@@ -87,6 +92,85 @@ function ProfileSection({ label, items }: { label: string; items: string[] }) {
)
}
+function SpaceContextEditor({ containerTag }: { containerTag: string }) {
+ const { data, isLoading } = useSpaceContext(containerTag)
+ const update = useUpdateSpaceContext()
+ const [value, setValue] = useState("")
+ const saved = data?.entityContext ?? ""
+
+ useEffect(() => {
+ setValue(data?.entityContext ?? "")
+ }, [data?.entityContext])
+
+ const dirty = value.trim() !== saved.trim()
+
+ const handleSave = () => {
+ update.mutate({
+ containerTag,
+ entityContext: value.trim() ? value.trim() : null,
+ })
+ }
+
+ return (
+
+
+
+ What to remember
+
+
+ Tell Nova what matters in this space — it shapes which memories get
+ extracted.
+
+
+
+ )
+}
+
export function SpaceProfileContent({
containerTag,
onClose,
@@ -125,20 +209,23 @@ export function SpaceProfileContent({
- {isLoading ? (
-
- ) : error ? (
-
- Failed to load space profile.
-
- ) : totalCount === 0 ? (
-
- ) : (
-
- )}
+
+
+ {isLoading ? (
+
+ ) : error ? (
+
+ Failed to load space profile.
+
+ ) : totalCount === 0 ? (
+
+ ) : (
+
+ )}
+
)
diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx
index f74d57da7..7f89649e4 100644
--- a/apps/web/components/space-selector.tsx
+++ b/apps/web/components/space-selector.tsx
@@ -7,10 +7,11 @@ import { cn } from "@lib/utils"
import { $fetch } from "@lib/api"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
-import { ChevronDownIcon, XIcon, Loader2, Trash2 } from "lucide-react"
+import { ChevronDownIcon, Pencil, 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 { EditSpaceModal } from "./edit-space-modal"
import { SelectSpacesModal } from "./select-spaces-modal"
import { SpaceGlyph } from "./space-glyph"
import { useProjectMutations } from "@/hooks/use-project-mutations"
@@ -54,6 +55,7 @@ export interface SpaceSelectorProps {
compact?: boolean
includeAuto?: boolean
hideCount?: boolean
+ enableEdit?: boolean
}
const triggerVariants = {
@@ -114,9 +116,11 @@ export function SpaceSelector({
compact = false,
includeAuto = false,
hideCount = false,
+ enableEdit = false,
}: SpaceSelectorProps) {
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [showSelectSpacesModal, setShowSelectSpacesModal] = useState(false)
+ const [showEditDialog, setShowEditDialog] = useState(false)
const [recents, setRecents] = useState
([])
const [deleteDialog, setDeleteDialog] = useState<{
open: boolean
@@ -231,6 +235,12 @@ export function SpaceSelector({
}
}, [allProjects, selectedProjects, pluginMetaMap, includeAuto, user?.id])
+ const canEditCurrent =
+ enableEdit &&
+ !displayInfo.isAuto &&
+ !displayInfo.plugin &&
+ !displayInfo.isOwnSpace
+
const pushRecent = useCallback((tag: string) => {
setRecents((prev) => {
const next = [tag, ...prev.filter((t) => t !== tag)].slice(0, RECENTS_MAX)
@@ -371,85 +381,115 @@ export function SpaceSelector({
return (
<>
-
-
+
+
+
+
+
+
+ Switch space
+
+
+ {canEditCurrent && (
-
-
- Switch space
-
-
+ )}
+
+ {canEditCurrent && (
+
+ )}
+