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 <noreply@anthropic.com>
This commit is contained in:
Vedant Mahajan 2026-07-24 21:21:19 +05:30 committed by GitHub
parent ea2cf33fd3
commit e685762012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 87 additions and 34 deletions

View file

@ -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) => (
<ToggleGroupItem
key={filter.value}
value={filter.value}
aria-label={`Show ${filter.label} memories`}
className={cn(
dmSansClassName(),
"h-auto min-w-0 flex-none shrink-0 rounded-full! border border-[#161F2C]! bg-[#0D121A] px-2.5 py-1 text-xs hover:border-[#2261CA33]! hover:bg-[#00173C] data-[state=on]:border-[#2261CA33]! data-[state=on]:bg-[#00173C]",
)}
>
{filter.label}
<span className="ml-1 text-[#737373]">
({agentSourceCounts?.[filter.value] ?? 0})
</span>
</ToggleGroupItem>
))}
{AGENT_SOURCE_FILTERS.map((filter) => {
const count = agentSourceCounts?.[filter.value]
if (!count) return null
return (
<ToggleGroupItem
key={filter.value}
value={filter.value}
aria-label={`Show ${filter.label} memories`}
className={cn(
dmSansClassName(),
"h-auto min-w-0 flex-none shrink-0 rounded-full! border border-[#161F2C]! bg-[#0D121A] px-2.5 py-1 text-xs hover:border-[#2261CA33]! hover:bg-[#00173C] data-[state=on]:border-[#2261CA33]! data-[state=on]:bg-[#00173C]",
)}
>
{filter.label}
<span className="ml-1 text-[#737373]">({count})</span>
</ToggleGroupItem>
)
})}
</ToggleGroup>
)}
</div>

View file

@ -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 (
<p className="py-8 text-center text-sm text-[#737373]">
Claude Code and Codex are connected.
Claude Code, Codex, and OpenCode are connected.
</p>
)
}
@ -1682,7 +1682,7 @@ function AgentsDiscoverPanel({
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-[#FAFAFA]">Agents</p>
<p className="text-[11px] text-[#737373]">
Claude Code and Codex share project memory
Claude Code, Codex, and OpenCode share project memory
</p>
</div>
{catalogIds.map((catalogId) => {

View file

@ -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())

View file

@ -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<T extends { containerTag: string }>(
}
/**
* 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<T extends { containerTag: string }>(
projects: T[],
@ -256,9 +270,12 @@ export function groupAgentSpaces<T extends { containerTag: string }>(
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,

View file

@ -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",

View file

@ -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([])