From e6857620124a51f346e3b5fe838d9a4a04444380 Mon Sep 17 00:00:00 2001 From: Vedant Mahajan Date: Fri, 24 Jul 2026 21:21:19 +0530 Subject: [PATCH] Add OpenCode to Agents spaces (#1354) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 --- apps/web/components/memories-grid.tsx | 42 +++++++++++++-------- apps/web/components/select-spaces-modal.tsx | 6 +-- apps/web/lib/agent-space.test.ts | 29 +++++++++++++- apps/web/lib/agent-space.ts | 41 ++++++++++++++------ apps/web/lib/plugin-space.ts | 2 +- apps/web/lib/search-params.ts | 1 + 6 files changed, 87 insertions(+), 34 deletions(-) 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([])