diff --git a/apps/web/components/memories-grid.tsx b/apps/web/components/memories-grid.tsx
index 5794cd19..beaee36c 100644
--- a/apps/web/components/memories-grid.tsx
+++ b/apps/web/components/memories-grid.tsx
@@ -388,6 +388,13 @@ export function MemoriesGrid({
enabled: !!user && showAgentFilters,
})
+ useEffect(() => {
+ if (!selectedAgentSource || !agentSourceCounts) return
+ if ((agentSourceCounts[selectedAgentSource] ?? 0) === 0) {
+ void setSelectedAgentSource(null)
+ }
+ }, [agentSourceCounts, selectedAgentSource, setSelectedAgentSource])
+
const {
data,
error,
@@ -700,22 +707,25 @@ export function MemoriesGrid({
aria-label="Filter memories by agent"
className="gap-1.5"
>
- {AGENT_SOURCE_FILTERS.map((filter) => (
-
- {filter.label}
-
- ({agentSourceCounts?.[filter.value] ?? 0})
-
-
- ))}
+ {AGENT_SOURCE_FILTERS.map((filter) => {
+ const count = agentSourceCounts?.[filter.value]
+ if (!count) return null
+
+ return (
+
+ {filter.label}
+ ({count})
+
+ )
+ })}
)}
diff --git a/apps/web/components/select-spaces-modal.tsx b/apps/web/components/select-spaces-modal.tsx
index 27db8ea0..f95321b0 100644
--- a/apps/web/components/select-spaces-modal.tsx
+++ b/apps/web/components/select-spaces-modal.tsx
@@ -95,7 +95,7 @@ type Category = {
count: number
}
-const AGENT_CATALOG_IDS = ["claude_code", "codex"] as const
+const AGENT_CATALOG_IDS = ["claude_code", "codex", "opencode"] as const
export function SelectSpacesModal({
isOpen,
@@ -1668,7 +1668,7 @@ function AgentsDiscoverPanel({
if (catalogIds.length === 0) {
return (
- Claude Code and Codex are connected.
+ Claude Code, Codex, and OpenCode are connected.
)
}
@@ -1682,7 +1682,7 @@ function AgentsDiscoverPanel({
Agents
- Claude Code and Codex share project memory
+ Claude Code, Codex, and OpenCode share project memory
{catalogIds.map((catalogId) => {
diff --git a/apps/web/lib/agent-space.test.ts b/apps/web/lib/agent-space.test.ts
index f413fa81..9082c21c 100644
--- a/apps/web/lib/agent-space.test.ts
+++ b/apps/web/lib/agent-space.test.ts
@@ -7,7 +7,7 @@ import {
} from "./agent-space"
describe("Agents spaces", () => {
- it("recognizes only Claude and Codex shared and legacy tags", () => {
+ it("recognizes Claude, Codex, and OpenCode shared and legacy tags", () => {
expect(isAgentContainerTag("repo_supermemory__0123456789abcdef")).toBe(true)
expect(isAgentContainerTag("user_project_0123456789abcdef")).toBe(true)
expect(isAgentContainerTag("repo_supermemory")).toBe(true)
@@ -16,7 +16,8 @@ describe("Agents spaces", () => {
)
expect(isAgentContainerTag("codex_project_0123456789abcdef")).toBe(true)
expect(isAgentContainerTag("codex_user_0123456789abcdef")).toBe(true)
- expect(isAgentContainerTag("opencode_project_0123456789abcdef")).toBe(false)
+ expect(isAgentContainerTag("opencode_project_0123456789abcdef")).toBe(true)
+ expect(isAgentContainerTag("opencode_user_0123456789abcdef")).toBe(true)
})
it("shows agent filters only for an Agents selection", () => {
@@ -38,6 +39,7 @@ describe("Agents spaces", () => {
"claude-code-plugin",
])
expect(agentSourceValues("codex")).toEqual(["codex"])
+ expect(agentSourceValues("opencode")).toEqual(["opencode"])
expect(agentSourceValues(null)).toBeUndefined()
})
@@ -46,6 +48,7 @@ describe("Agents spaces", () => {
{ containerTag: "repo_supermemory__fedcba9876543210" },
{ containerTag: "repo_supermemory" },
{ containerTag: "codex_project_0123456789abcdef" },
+ { containerTag: "opencode_project_0123456789abcdef" },
{ containerTag: "claudecode_project_0123456789abcdef" },
{ containerTag: "user_project_0123456789abcdef" },
]
@@ -69,6 +72,7 @@ describe("Agents spaces", () => {
"claudecode_project_0123456789abcdef",
"repo_supermemory",
"codex_project_0123456789abcdef",
+ "opencode_project_0123456789abcdef",
])
})
@@ -110,10 +114,31 @@ describe("Agents spaces", () => {
expect(groups[1]?.kind).toBe("legacy-personal")
})
+ it("keeps the old global OpenCode personal container separate", () => {
+ const projects = [
+ { containerTag: "user_project_0123456789abcdef" },
+ { containerTag: "opencode_user_fedcba9876543210" },
+ ]
+ const metadata = new Map(
+ projects.map((project) => [
+ project.containerTag,
+ { projectName: "supermemory" },
+ ]),
+ )
+
+ const groups = groupAgentSpaces(projects, metadata)
+
+ expect(groups).toHaveLength(2)
+ expect(groups[0]?.label).toBe("supermemory")
+ expect(groups[1]?.label).toBe("Legacy OpenCode personal")
+ expect(groups[1]?.kind).toBe("legacy-personal")
+ })
+
it("groups path-scoped legacy tags even before metadata loads", () => {
const projects = [
{ containerTag: "claudecode_project_0123456789abcdef" },
{ containerTag: "codex_project_0123456789abcdef" },
+ { containerTag: "opencode_project_0123456789abcdef" },
]
const groups = groupAgentSpaces(projects, new Map())
diff --git a/apps/web/lib/agent-space.ts b/apps/web/lib/agent-space.ts
index 74693af9..7f78f7bd 100644
--- a/apps/web/lib/agent-space.ts
+++ b/apps/web/lib/agent-space.ts
@@ -5,7 +5,7 @@ export type AgentContainerKind =
| "legacy-personal"
| "legacy-project"
-export type AgentSourceFilter = "claude-code" | "codex"
+export type AgentSourceFilter = "claude-code" | "codex" | "opencode"
export const AGENT_SOURCE_FILTERS: ReadonlyArray<{
value: AgentSourceFilter
@@ -18,6 +18,7 @@ export const AGENT_SOURCE_FILTERS: ReadonlyArray<{
sources: ["claude-code", "claude-code-plugin"],
},
{ value: "codex", label: "Codex", sources: ["codex"] },
+ { value: "opencode", label: "OpenCode", sources: ["opencode"] },
]
export type AgentSpaceMetadata = {
@@ -54,6 +55,14 @@ const TAG_PATTERNS: Array<{
kind: "legacy-project",
pattern: /^codex_project_([0-9a-f]{6,64})$/i,
},
+ {
+ kind: "legacy-personal",
+ pattern: /^opencode_user_([0-9a-f]{6,64})$/i,
+ },
+ {
+ kind: "legacy-project",
+ pattern: /^opencode_project_([0-9a-f]{6,64})$/i,
+ },
]
function matchAgentTag(containerTag: string): {
@@ -137,13 +146,17 @@ function legacyGroupIdentity(
return { key: `tag:${containerTag}`, label: containerTag, kind: "project" }
}
- // Old Codex personal memory was intentionally global. Even if its newest
- // document contains a project name, assigning the whole container to that
- // project would leak memories from its other historical projects.
- if (containerTag.startsWith("codex_user_")) {
+ // Old Codex and OpenCode personal containers were intentionally global.
+ // Even if the newest document has a project name, assigning the whole
+ // container to that project would mix memories from historical projects.
+ if (
+ containerTag.startsWith("codex_user_") ||
+ containerTag.startsWith("opencode_user_")
+ ) {
+ const agent = containerTag.startsWith("codex_user_") ? "Codex" : "OpenCode"
return {
key: `legacy-personal:${containerTag}`,
- label: "Legacy Codex personal",
+ label: `Legacy ${agent} personal`,
kind: "legacy-personal",
}
}
@@ -159,7 +172,8 @@ function legacyGroupIdentity(
if (
containerTag.startsWith("user_project_") ||
containerTag.startsWith("claudecode_project_") ||
- containerTag.startsWith("codex_project_")
+ containerTag.startsWith("codex_project_") ||
+ containerTag.startsWith("opencode_project_")
) {
return {
key: `path:${match.id.toLocaleLowerCase()}`,
@@ -211,9 +225,9 @@ function addProjectToGroup(
}
/**
- * Collapse the physical Claude/Codex containers into one selectable Agents row
- * per project. Every returned container tag remains real; the UI never writes
- * to a synthetic "agents" tag.
+ * Collapse the physical Claude/Codex/OpenCode containers into one selectable
+ * Agents row per project. Every returned container tag remains real; the UI
+ * never writes to a synthetic "agents" tag.
*/
export function groupAgentSpaces(
projects: T[],
@@ -256,9 +270,12 @@ export function groupAgentSpaces(
const canonicalMatches = projectName
? (canonicalKeysByName.get(projectName.toLocaleLowerCase()) ?? [])
: []
+ const firstCanonical = canonicalMatches[0]
const key =
- identity.kind === "project" && canonicalMatches.length === 1
- ? canonicalMatches[0]!
+ identity.kind === "project" &&
+ canonicalMatches.length === 1 &&
+ firstCanonical !== undefined
+ ? firstCanonical
: identity.key
addProjectToGroup(
grouped,
diff --git a/apps/web/lib/plugin-space.ts b/apps/web/lib/plugin-space.ts
index af04eea6..ba667273 100644
--- a/apps/web/lib/plugin-space.ts
+++ b/apps/web/lib/plugin-space.ts
@@ -27,7 +27,7 @@ const PLUGINS: PluginDef[] = [
id: "agents",
label: "Agents",
iconSrc: null,
- prefixes: ["user_project", "repo", "claudecode", "codex"],
+ prefixes: ["user_project", "repo", "claudecode", "codex", "opencode"],
},
{
id: "openclaw",
diff --git a/apps/web/lib/search-params.ts b/apps/web/lib/search-params.ts
index 661a5ed4..b900352b 100644
--- a/apps/web/lib/search-params.ts
+++ b/apps/web/lib/search-params.ts
@@ -62,5 +62,6 @@ export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault(
export const agentSourceParam = parseAsStringLiteral([
"claude-code",
"codex",
+ "opencode",
] as const)
export const projectParam = parseAsArrayOf(parseAsString, ",").withDefault([])