diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx
index be3632740..5402685e4 100644
--- a/apps/web/components/memories-grid.tsx
+++ b/apps/web/components/memories-grid.tsx
@@ -35,6 +35,7 @@ import { QuickNoteCard } from "./quick-note-card"
import type { HighlightItem } from "./highlights-card"
import { Button } from "@ui/components/button"
import {
+ agentSourceParam,
categoriesParam,
type IntegrationParamValue,
} from "@/lib/search-params"
@@ -88,8 +89,17 @@ type DocumentFacet = {
label: string
}
+type AgentSource = "claude-code" | "codex"
+
+type SourceFacet = {
+ source: AgentSource
+ count: number
+ label: string
+}
+
type FacetsResponse = {
facets: DocumentFacet[]
+ sourceFacets?: SourceFacet[]
total: number
}
@@ -278,6 +288,10 @@ export function MemoriesGrid({
"categories",
categoriesParam,
)
+ const [selectedAgentSource, setSelectedAgentSource] = useQueryState(
+ "source",
+ agentSourceParam,
+ )
const selectedCategoriesSet = useMemo(
() => new Set(selectedCategories),
[selectedCategories],
@@ -315,6 +329,7 @@ export function MemoriesGrid({
"documents-with-memories",
effectiveContainerTags,
selectedCategories,
+ selectedAgentSource,
],
initialPageParam: 1,
queryFn: async ({ pageParam }) => {
@@ -327,6 +342,7 @@ export function MemoriesGrid({
containerTags: effectiveContainerTags,
categories:
selectedCategories.length > 0 ? selectedCategories : undefined,
+ sources: selectedAgentSource ? [selectedAgentSource] : undefined,
},
disableValidation: true,
})
@@ -381,9 +397,17 @@ export function MemoriesGrid({
[setSelectedCategories],
)
+ const handleAgentSourceToggle = useCallback(
+ (source: AgentSource) => {
+ setSelectedAgentSource((prev) => (prev === source ? null : source))
+ },
+ [setSelectedAgentSource],
+ )
+
const handleSelectAll = useCallback(() => {
setSelectedCategories(null)
- }, [setSelectedCategories])
+ setSelectedAgentSource(null)
+ }, [setSelectedCategories, setSelectedAgentSource])
const documents = useMemo(() => {
return (
@@ -559,6 +583,10 @@ export function MemoriesGrid({
const allVisibleSelected =
documents.length > 0 &&
documents.every((d) => d.id && selectedDocumentIds.has(d.id))
+ const agentSourceFacets =
+ facetsData?.sourceFacets?.filter(
+ (facet) => facet.source === "claude-code" || facet.source === "codex",
+ ) ?? []
return (
@@ -573,6 +601,7 @@ export function MemoriesGrid({
dmSansClassName(),
"shrink-0 whitespace-nowrap rounded-full border border-[#161F2C] bg-[#0D121A] px-2.5 py-1 text-xs h-auto hover:bg-[#00173C] hover:border-[#2261CA33]",
selectedCategories.length === 0 &&
+ !selectedAgentSource &&
"bg-[#00173C] border-[#2261CA33]",
)}
onClick={handleSelectAll}
@@ -599,6 +628,21 @@ export function MemoriesGrid({
({facet.count})
))}
+ {agentSourceFacets.map((facet) => (
+
+ ))}
{/* View mode toggle โ segmented control */}
diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx
index 7198d435d..c9436d956 100644
--- a/apps/web/components/select-spaces-modal.tsx
+++ b/apps/web/components/select-spaces-modal.tsx
@@ -34,7 +34,11 @@ import {
spaceSelectorDisplayName,
} from "@/lib/ingest-auto-space"
import {
+ AGENT_SPACE_CATEGORY_LABEL,
+ detectAgentSpace,
detectPluginSpace,
+ getAgentSpaceDisplayName,
+ getAgentSpaceSecondaryLabel,
pluginInitial,
type PluginSpaceInfo,
} from "@/lib/plugin-space"
@@ -79,6 +83,7 @@ interface SelectSpacesModalProps {
type CategoryId =
| "all"
| "my"
+ | "agents"
| `plugin:${PluginSpaceInfo["pluginId"]}`
| `discover:${string}`
@@ -90,6 +95,10 @@ type Category = {
count: number
}
+function normalizeAgentProjectKey(value: string | null | undefined) {
+ return value?.trim().replace(/_/g, "-").toLowerCase() || null
+}
+
export function SelectSpacesModal({
isOpen,
onClose,
@@ -148,6 +157,37 @@ export function SelectSpacesModal({
return [defaultSpace, ...rest]
}, [projects])
+ const visibleSpaces = useMemo(() => {
+ const sharedRepoProjects = new Set
()
+
+ for (const project of allSpaces) {
+ const agent = detectAgentSpace(project.containerTag)
+ if (!agent?.isSharedRepo) continue
+
+ const projectKey = normalizeAgentProjectKey(agent.projectId)
+ if (projectKey) sharedRepoProjects.add(projectKey)
+ }
+
+ if (sharedRepoProjects.size === 0) return allSpaces
+
+ return allSpaces.filter((project) => {
+ const agent = detectAgentSpace(project.containerTag)
+ if (!agent || agent.isSharedRepo) return true
+
+ // Claude Code already uses repo-based tags, so once the shared
+ // Agent Spaces row exists we should not show an extra source-specific row.
+ if (!agent.sourceIds.includes("claude-code")) return true
+
+ const metaProject = pluginMetaMap.get(project.containerTag)?.projectName
+ const projectKeys = [
+ normalizeAgentProjectKey(metaProject),
+ normalizeAgentProjectKey(agent.projectId),
+ ].filter((key): key is string => !!key)
+
+ return !projectKeys.some((key) => sharedRepoProjects.has(key))
+ })
+ }, [allSpaces, pluginMetaMap])
+
const { categories, connectedCatalogIds } = useMemo<{
categories: Category[]
connectedCatalogIds: Set
@@ -156,8 +196,14 @@ export function SelectSpacesModal({
PluginSpaceInfo["pluginId"],
{ label: string; iconSrc: string | null; count: number }
>()
+ let agentCount = 0
let myCount = 0
- for (const p of allSpaces) {
+ for (const p of visibleSpaces) {
+ const agent = detectAgentSpace(p.containerTag)
+ if (agent) {
+ agentCount += 1
+ continue
+ }
const plugin = detectPluginSpace(p.containerTag)
if (plugin) {
const prev = pluginCounts.get(plugin.pluginId)
@@ -184,6 +230,10 @@ export function SelectSpacesModal({
const catalogId = spacePluginIdToCatalogId(pluginId)
if (catalogId) connectedIds.add(catalogId)
}
+ if (agentCount > 0) {
+ connectedIds.add("claude_code")
+ connectedIds.add("codex")
+ }
return {
categories: [
{
@@ -191,7 +241,7 @@ export function SelectSpacesModal({
label: "All Spaces",
iconSrc: null,
emoji: null,
- count: allSpaces.length,
+ count: visibleSpaces.length,
},
{
id: "my",
@@ -200,15 +250,27 @@ export function SelectSpacesModal({
emoji: "๐",
count: myCount,
},
+ ...(agentCount > 0
+ ? [
+ {
+ id: "agents" as CategoryId,
+ label: AGENT_SPACE_CATEGORY_LABEL,
+ iconSrc: "/images/logo.png",
+ emoji: null,
+ count: agentCount,
+ },
+ ]
+ : []),
...pluginCats,
],
connectedCatalogIds: connectedIds,
}
- }, [allSpaces])
+ }, [visibleSpaces])
const defaultCategory = useMemo(() => {
if (!currentSelection) return "all"
if (currentSelection === AUTO_CHAT_SPACE_ID) return "all"
+ if (detectAgentSpace(currentSelection)) return "agents"
const plugin = detectPluginSpace(currentSelection)
if (plugin) return `plugin:${plugin.pluginId}`
return "my"
@@ -231,7 +293,14 @@ export function SelectSpacesModal({
setLastBulkDeleteTag(null)
}, [activeDiscoverId])
- const { org, user } = useAuth()
+ const modalSubtitle = useMemo(() => {
+ if (isBulkDeleteMode) return "Choose spaces to permanently delete"
+ if (activeCategory === "agents") {
+ return "Memories from Claude, Codex, and other agents"
+ }
+ return "Filter your memories by space"
+ }, [activeCategory, isBulkDeleteMode])
+
const queryClient = useQueryClient()
const [connectingPluginId, setConnectingPluginId] = useState(
null,
@@ -450,15 +519,18 @@ export function SelectSpacesModal({
)
const filteredProjects = useMemo(() => {
- const byCategory = allSpaces.filter((p) => {
+ const byCategory = visibleSpaces.filter((p) => {
if (activeCategory === "all") return true
+ const agent = detectAgentSpace(p.containerTag)
+ if (activeCategory === "agents") return !!agent
const plugin = detectPluginSpace(p.containerTag)
- if (activeCategory === "my") return !plugin
+ if (activeCategory === "my") return !plugin && !agent
return plugin && `plugin:${plugin.pluginId}` === activeCategory
})
if (!searchQuery.trim()) return byCategory
const query = searchQuery.trim().toLowerCase()
return byCategory.filter((p) => {
+ const agent = detectAgentSpace(p.containerTag)
const plugin = detectPluginSpace(p.containerTag)
const projectName = pluginMetaMap.get(p.containerTag)?.projectName
const displayName = spaceSelectorDisplayName(p, p.containerTag, {
@@ -468,18 +540,21 @@ export function SelectSpacesModal({
p.containerTag.toLowerCase().includes(query) ||
(p.name ?? "").toLowerCase().includes(query) ||
displayName.toLowerCase().includes(query) ||
+ (agent?.label.toLowerCase().includes(query) ?? false) ||
+ (agent?.isSharedRepo && "agent space".includes(query)) ||
+ (agent?.projectId?.toLowerCase().includes(query) ?? false) ||
(plugin?.label.toLowerCase().includes(query) ?? false) ||
(plugin?.projectId?.toLowerCase().includes(query) ?? false) ||
(projectName?.toLowerCase().includes(query) ?? false)
)
})
- }, [allSpaces, activeCategory, searchQuery, pluginMetaMap, user?.id])
+ }, [visibleSpaces, activeCategory, searchQuery, pluginMetaMap, user?.id])
const recentProjects = useMemo(() => {
if (!recents?.length) return []
if (searchQuery.trim()) return []
if (activeCategory !== "all") return []
- const byTag = new Map(allSpaces.map((p) => [p.containerTag, p]))
+ const byTag = new Map(visibleSpaces.map((p) => [p.containerTag, p]))
const out: ContainerTagListType[] = []
for (const tag of recents) {
const p = byTag.get(tag)
@@ -487,7 +562,7 @@ export function SelectSpacesModal({
if (out.length >= 5) break
}
return out
- }, [recents, searchQuery, activeCategory, allSpaces])
+ }, [recents, searchQuery, activeCategory, visibleSpaces])
const recentSet = useMemo(
() => new Set(recentProjects.map((p) => p.containerTag)),
@@ -574,11 +649,23 @@ export function SelectSpacesModal({
const renderRow = useCallback(
(project: ContainerTagListType) => {
const isSelected = currentSelection === project.containerTag
- const plugin = detectPluginSpace(project.containerTag)
+ const agent = detectAgentSpace(project.containerTag)
+ const plugin = agent ? null : detectPluginSpace(project.containerTag)
const pluginProjectName = pluginMetaMap.get(
project.containerTag,
)?.projectName
const pluginIdLabel = pluginProjectName || plugin?.projectId
+ const agentPrimaryLabel = agent
+ ? getAgentSpaceDisplayName(agent, {
+ projectName: pluginProjectName,
+ inAgentCategory: activeCategory === "agents",
+ })
+ : null
+ const agentSecondaryLabel = agent
+ ? getAgentSpaceSecondaryLabel(agent, {
+ inAllSpaces: activeCategory === "all",
+ })
+ : null
const displayName = spaceSelectorDisplayName(
project,
project.containerTag,
@@ -588,7 +675,7 @@ export function SelectSpacesModal({
)
const isDefault = project.containerTag === DEFAULT_PROJECT_ID
const isOwnSpace = isOwnConversationSpace(project, user?.id)
- const canEdit = !isDefault && !plugin && !isOwnSpace
+ const canEdit = !isDefault && !plugin && !agent && !isOwnSpace
const canBulkDelete = enableDelete && !isDefault
const isEditing = editingProject?.containerTag === project.containerTag
const isBulkDeleteSelected = bulkDeleteTags.has(project.containerTag)
@@ -696,7 +783,16 @@ export function SelectSpacesModal({
disabled={isBulkDeleteMode && !canBulkDelete}
className="flex min-w-0 flex-1 items-center gap-3 text-left cursor-pointer focus:outline-none focus:ring-0 disabled:cursor-not-allowed"
>
- {plugin ? (
+ {agent ? (
+
+ ) : plugin ? (
plugin.iconSrc ? (
- {plugin ? (
+ {agent ? (
+ <>
+ {agentPrimaryLabel}
+ {agentSecondaryLabel && (
+
+ ยท {agentSecondaryLabel}
+
+ )}
+ >
+ ) : plugin ? (
<>
{plugin.label}
{pluginIdLabel && (
@@ -780,6 +885,7 @@ export function SelectSpacesModal({
)
},
[
+ activeCategory,
cancelEditing,
bulkDeleteTags,
currentSelection,
@@ -837,11 +943,11 @@ export function SelectSpacesModal({
const renderCategoryChip = (
category: {
- id: string
+ id: CategoryId
label: string
count?: number
- iconSrc?: string
- emoji?: string
+ iconSrc?: string | null
+ emoji?: string | null
},
isDiscover: boolean,
) => {
@@ -1052,9 +1158,7 @@ export function SelectSpacesModal({
Select Space
- {isBulkDeleteMode
- ? "Choose spaces to permanently delete"
- : "Filter your memories by space"}
+ {modalSubtitle}
@@ -1137,9 +1241,7 @@ export function SelectSpacesModal({
Select Space
- {isBulkDeleteMode
- ? "Choose spaces to permanently delete"
- : "Filter your memories by space"}
+ {modalSubtitle}
diff --git a/apps/web/components/space-selector.tsx b/apps/web/components/space-selector.tsx
index 7f89649e4..61b982c53 100644
--- a/apps/web/components/space-selector.tsx
+++ b/apps/web/components/space-selector.tsx
@@ -40,7 +40,12 @@ import {
isOwnConversationSpace,
spaceSelectorDisplayName,
} from "@/lib/ingest-auto-space"
-import { detectPluginSpace, pluginInitial } from "@/lib/plugin-space"
+import {
+ detectAgentSpace,
+ detectPluginSpace,
+ getAgentSpaceDisplayName,
+ pluginInitial,
+} from "@/lib/plugin-space"
import { usePluginSpaceMeta } from "@/hooks/use-plugin-space-meta"
import NovaOrb from "@/components/nova/nova-orb"
import { AutoSpaceIcon } from "@/components/nova/auto-space-icon"
@@ -190,6 +195,7 @@ export function SpaceSelector({
const displayInfo = useMemo<{
name: string
emoji: string | null
+ agent: ReturnType
plugin: ReturnType
isAuto: boolean
isOwnSpace: boolean
@@ -199,6 +205,7 @@ export function SpaceSelector({
return {
name: "Auto",
emoji: null,
+ agent: null,
plugin: null,
isAuto: true,
isOwnSpace: false,
@@ -208,6 +215,7 @@ export function SpaceSelector({
return {
name: "My Space",
emoji: "๐",
+ agent: null,
plugin: null,
isAuto: false,
isOwnSpace: false,
@@ -216,19 +224,23 @@ export function SpaceSelector({
const found = allProjects.find(
(p: ContainerTagListType) => p.containerTag === containerTag,
)
- const plugin = detectPluginSpace(containerTag)
+ const agent = detectAgentSpace(containerTag)
+ const plugin = agent ? null : detectPluginSpace(containerTag)
const isOwnSpace = isOwnConversationSpace({ containerTag }, user?.id)
const projectName = pluginMetaMap.get(containerTag)?.projectName
- const idForLabel = projectName || plugin?.projectId
+ const idForLabel = projectName || agent?.projectId || plugin?.projectId
return {
- name: plugin
- ? idForLabel
- ? `${plugin.label} ยท ${idForLabel}`
- : plugin.label
- : spaceSelectorDisplayName(found, containerTag, {
- currentUserId: user?.id,
- }),
+ name: agent
+ ? getAgentSpaceDisplayName(agent, { projectName })
+ : plugin
+ ? idForLabel
+ ? `${plugin.label} ยท ${idForLabel}`
+ : plugin.label
+ : spaceSelectorDisplayName(found, containerTag, {
+ currentUserId: user?.id,
+ }),
emoji: found?.emoji || "๐",
+ agent,
plugin,
isAuto: false,
isOwnSpace,
@@ -238,6 +250,7 @@ export function SpaceSelector({
const canEditCurrent =
enableEdit &&
!displayInfo.isAuto &&
+ !displayInfo.agent &&
!displayInfo.plugin &&
!displayInfo.isOwnSpace
@@ -415,6 +428,18 @@ export function SpaceSelector({
size={compact ? 14 : 16}
className="shrink-0 blur-[0.45px]!"
/>
+ ) : displayInfo.agent ? (
+
) : displayInfo.plugin ? (
displayInfo.plugin.iconSrc ? (
+ isSharedRepo: boolean
+}
+
+export function getAgentSpacePlaceName(
+ agent: AgentSpaceInfo,
+ projectName?: string | null,
+): string {
+ return projectName?.trim() || agent.projectId?.trim() || agent.label
+}
+
+/** Primary row/trigger label for an agent-scoped space. */
+export function getAgentSpaceDisplayName(
+ agent: AgentSpaceInfo,
+ options?: {
+ projectName?: string | null
+ inAgentCategory?: boolean
+ },
+): string {
+ const placeName = getAgentSpacePlaceName(agent, options?.projectName)
+
+ if (agent.isSharedRepo) {
+ return placeName
+ }
+
+ const idLabel = options?.projectName || agent.projectId
+ if (idLabel) return `${agent.label} ยท ${idLabel}`
+ return agent.label
+}
+
+/** Secondary hint when agent spaces appear outside the Agent Spaces filter. */
+export function getAgentSpaceSecondaryLabel(
+ agent: AgentSpaceInfo,
+ options: { inAllSpaces?: boolean },
+): string | null {
+ if (agent.isSharedRepo && options.inAllSpaces) {
+ return "Agent space"
+ }
+ return null
+}
+
type PluginDef = {
id: PluginSpaceInfo["pluginId"]
label: string
@@ -53,6 +100,19 @@ const PLUGINS: PluginDef[] = [
},
]
+const AGENTS_ICON_SRC = "/images/logo.png"
+const AGENT_PLUGIN_IDS = new Set([
+ "claude-code",
+ "codex",
+])
+
+function parseRepoProjectId(containerTag: string): string | undefined {
+ if (!containerTag.startsWith("repo_")) return undefined
+ const repoName = containerTag.slice("repo_".length)
+ if (!repoName) return undefined
+ return repoName.replace(/_/g, "-").slice(0, 32)
+}
+
function parsePluginRest(rest: string): { projectId?: string } {
if (!rest || rest === "default" || rest === "global") {
return { projectId: "Global" }
@@ -216,3 +276,28 @@ export function detectPluginSpace(
return null
}
+
+export function detectAgentSpace(containerTag: string): AgentSpaceInfo | null {
+ if (!containerTag) return null
+
+ if (containerTag.startsWith("repo_")) {
+ return {
+ label: AGENT_SPACE_CATEGORY_LABEL,
+ iconSrc: AGENTS_ICON_SRC,
+ projectId: parseRepoProjectId(containerTag),
+ sourceIds: ["claude-code", "codex"],
+ isSharedRepo: true,
+ }
+ }
+
+ const plugin = detectPluginSpace(containerTag)
+ if (!plugin || !AGENT_PLUGIN_IDS.has(plugin.pluginId)) return null
+
+ return {
+ label: plugin.label as "Claude Code" | "Codex",
+ iconSrc: plugin.iconSrc ?? AGENTS_ICON_SRC,
+ projectId: plugin.projectId,
+ sourceIds: [plugin.pluginId as "claude-code" | "codex"],
+ isSharedRepo: false,
+ }
+}
diff --git a/apps/web/lib/search-params.ts b/apps/web/lib/search-params.ts
index 9afb6eab5..13b5f229e 100644
--- a/apps/web/lib/search-params.ts
+++ b/apps/web/lib/search-params.ts
@@ -58,4 +58,7 @@ export type IntegrationParamValue =
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault(
[],
)
+const agentSourceLiterals = ["claude-code", "codex"] as const
+export type AgentSourceParamValue = (typeof agentSourceLiterals)[number]
+export const agentSourceParam = parseAsStringLiteral(agentSourceLiterals)
export const projectParam = parseAsArrayOf(parseAsString, ",").withDefault([])