mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-17 12:33:57 +00:00
Group stable agent repository containers
This commit is contained in:
parent
03d1b63883
commit
8493d46a64
3 changed files with 131 additions and 29 deletions
|
|
@ -7,6 +7,7 @@ import { useAuth } from "@lib/auth-context"
|
|||
|
||||
export type PluginSpaceMeta = {
|
||||
projectName?: string
|
||||
projectId?: string
|
||||
source?: string
|
||||
lastUpdatedAt?: string
|
||||
}
|
||||
|
|
@ -27,8 +28,13 @@ function extractMeta(doc: RawDoc): PluginSpaceMeta {
|
|||
typeof md.sm_source === "string" && md.sm_source.trim()
|
||||
? md.sm_source.trim()
|
||||
: undefined
|
||||
const projectId =
|
||||
typeof md.sm_project_id === "string" && md.sm_project_id.trim()
|
||||
? md.sm_project_id.trim().toLowerCase()
|
||||
: undefined
|
||||
return {
|
||||
projectName: project,
|
||||
projectId,
|
||||
source,
|
||||
lastUpdatedAt: doc?.updatedAt ?? doc?.createdAt ?? undefined,
|
||||
}
|
||||
|
|
@ -36,7 +42,7 @@ function extractMeta(doc: RawDoc): PluginSpaceMeta {
|
|||
|
||||
/**
|
||||
* Fetches one recent doc per containerTag and pulls plugin metadata
|
||||
* (`metadata.project`, `metadata.sm_source`) so plugin-provisioned spaces
|
||||
* (`metadata.project`, `metadata.sm_project_id`, `metadata.sm_source`) so plugin-provisioned spaces
|
||||
* can show the real project name instead of the hash.
|
||||
*/
|
||||
export function usePluginSpaceMeta(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
|
||||
describe("Agents spaces", () => {
|
||||
it("recognizes only Claude and Codex shared and legacy tags", () => {
|
||||
expect(isAgentContainerTag("repo_supermemory__0123456789abcdef")).toBe(true)
|
||||
expect(isAgentContainerTag("user_project_0123456789abcdef")).toBe(true)
|
||||
expect(isAgentContainerTag("repo_supermemory")).toBe(true)
|
||||
expect(isAgentContainerTag("claudecode_project_0123456789abcdef")).toBe(
|
||||
|
|
@ -20,7 +21,10 @@ describe("Agents spaces", () => {
|
|||
|
||||
it("shows agent filters only for an Agents selection", () => {
|
||||
expect(
|
||||
isAgentsSelection(["user_project_0123456789abcdef", "repo_supermemory"]),
|
||||
isAgentsSelection([
|
||||
"repo_supermemory__0123456789abcdef",
|
||||
"repo_supermemory",
|
||||
]),
|
||||
).toBe(true)
|
||||
expect(isAgentsSelection(["repo_supermemory", "sm_project_default"])).toBe(
|
||||
false,
|
||||
|
|
@ -39,6 +43,7 @@ describe("Agents spaces", () => {
|
|||
|
||||
it("groups canonical and legacy project containers without synthetic tags", () => {
|
||||
const projects = [
|
||||
{ containerTag: "repo_supermemory__fedcba9876543210" },
|
||||
{ containerTag: "repo_supermemory" },
|
||||
{ containerTag: "codex_project_0123456789abcdef" },
|
||||
{ containerTag: "claudecode_project_0123456789abcdef" },
|
||||
|
|
@ -56,9 +61,10 @@ describe("Agents spaces", () => {
|
|||
expect(groups).toHaveLength(1)
|
||||
expect(groups[0]?.label).toBe("supermemory")
|
||||
expect(groups[0]?.representative.containerTag).toBe(
|
||||
"user_project_0123456789abcdef",
|
||||
"repo_supermemory__fedcba9876543210",
|
||||
)
|
||||
expect(groups[0]?.containerTags).toEqual([
|
||||
"repo_supermemory__fedcba9876543210",
|
||||
"user_project_0123456789abcdef",
|
||||
"claudecode_project_0123456789abcdef",
|
||||
"repo_supermemory",
|
||||
|
|
@ -66,6 +72,24 @@ describe("Agents spaces", () => {
|
|||
])
|
||||
})
|
||||
|
||||
it("keeps repositories with the same basename in separate agent spaces", () => {
|
||||
const projects = [
|
||||
{ containerTag: "repo_api__0123456789abcdef" },
|
||||
{ containerTag: "repo_api__fedcba9876543210" },
|
||||
{ containerTag: "repo_api" },
|
||||
]
|
||||
const metadata = new Map(
|
||||
projects.map((project) => [project.containerTag, { projectName: "api" }]),
|
||||
)
|
||||
|
||||
const groups = groupAgentSpaces(projects, metadata)
|
||||
|
||||
expect(groups).toHaveLength(3)
|
||||
expect(
|
||||
groups.filter((group) => group.key.startsWith("project-id:")),
|
||||
).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("keeps the old global Codex personal container separate", () => {
|
||||
const projects = [
|
||||
{ containerTag: "user_project_0123456789abcdef" },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type AgentContainerKind =
|
||||
| "canonical-project"
|
||||
| "personal"
|
||||
| "project"
|
||||
| "legacy-personal"
|
||||
|
|
@ -21,6 +22,7 @@ export const AGENT_SOURCE_FILTERS: ReadonlyArray<{
|
|||
|
||||
export type AgentSpaceMetadata = {
|
||||
projectName?: string
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
export type AgentSpaceGroup<T extends { containerTag: string }> = {
|
||||
|
|
@ -37,6 +39,7 @@ const TAG_PATTERNS: Array<{
|
|||
kind: AgentContainerKind
|
||||
pattern: RegExp
|
||||
}> = [
|
||||
{ kind: "canonical-project", pattern: /^repo_(.+)__([0-9a-f]{16})$/i },
|
||||
{ kind: "personal", pattern: /^user_project_([0-9a-f]{6,64})$/i },
|
||||
{ kind: "project", pattern: /^repo_(.+)$/i },
|
||||
{
|
||||
|
|
@ -56,10 +59,21 @@ const TAG_PATTERNS: Array<{
|
|||
function matchAgentTag(containerTag: string): {
|
||||
kind: AgentContainerKind
|
||||
id: string
|
||||
projectId?: string
|
||||
projectSlug?: string
|
||||
} | null {
|
||||
for (const definition of TAG_PATTERNS) {
|
||||
const match = containerTag.match(definition.pattern)
|
||||
if (match?.[1]) return { kind: definition.kind, id: match[1] }
|
||||
if (!match?.[1]) continue
|
||||
if (definition.kind === "canonical-project" && match[2]) {
|
||||
return {
|
||||
kind: definition.kind,
|
||||
id: match[2],
|
||||
projectId: match[2].toLowerCase(),
|
||||
projectSlug: match[1],
|
||||
}
|
||||
}
|
||||
return { kind: definition.kind, id: match[1] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -99,20 +113,22 @@ function humanizeProjectId(value: string): string {
|
|||
|
||||
function tagPriority(containerTag: string): number {
|
||||
switch (getAgentContainerKind(containerTag)) {
|
||||
case "personal":
|
||||
case "canonical-project":
|
||||
return 0
|
||||
case "personal":
|
||||
return 1
|
||||
case "legacy-personal":
|
||||
return containerTag.startsWith("claudecode_project_") ? 1 : 4
|
||||
return containerTag.startsWith("claudecode_project_") ? 2 : 5
|
||||
case "project":
|
||||
return 2
|
||||
case "legacy-project":
|
||||
return 3
|
||||
case "legacy-project":
|
||||
return 4
|
||||
default:
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
function groupIdentity(
|
||||
function legacyGroupIdentity(
|
||||
containerTag: string,
|
||||
projectName: string | undefined,
|
||||
): { key: string; label: string; kind: AgentSpaceGroup<never>["kind"] } {
|
||||
|
|
@ -159,6 +175,41 @@ function groupIdentity(
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeProjectId(value: string | undefined): string | undefined {
|
||||
const normalized = value?.trim().toLowerCase()
|
||||
return normalized || undefined
|
||||
}
|
||||
|
||||
function addProjectToGroup<T extends { containerTag: string }>(
|
||||
grouped: Map<string, AgentSpaceGroup<T>>,
|
||||
key: string,
|
||||
label: string,
|
||||
kind: AgentSpaceGroup<T>["kind"],
|
||||
project: T,
|
||||
projectName: string | undefined,
|
||||
) {
|
||||
const existing = grouped.get(key)
|
||||
if (existing) {
|
||||
existing.projects.push(project)
|
||||
existing.containerTags.push(project.containerTag)
|
||||
if (!existing.projectName && projectName) {
|
||||
existing.projectName = projectName
|
||||
existing.label = projectName
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
grouped.set(key, {
|
||||
key,
|
||||
label,
|
||||
projectName,
|
||||
kind,
|
||||
representative: project,
|
||||
projects: [project],
|
||||
containerTags: [project.containerTag],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse the physical Claude/Codex containers into one selectable Agents row
|
||||
* per project. Every returned container tag remains real; the UI never writes
|
||||
|
|
@ -169,33 +220,54 @@ export function groupAgentSpaces<T extends { containerTag: string }>(
|
|||
metadata: ReadonlyMap<string, AgentSpaceMetadata>,
|
||||
): AgentSpaceGroup<T>[] {
|
||||
const grouped = new Map<string, AgentSpaceGroup<T>>()
|
||||
const legacyProjects: Array<{
|
||||
project: T
|
||||
projectName: string | undefined
|
||||
}> = []
|
||||
const canonicalKeysByName = new Map<string, string[]>()
|
||||
|
||||
for (const project of projects) {
|
||||
if (!isAgentContainerTag(project.containerTag)) continue
|
||||
const projectName = normalizeProjectName(
|
||||
metadata.get(project.containerTag)?.projectName,
|
||||
const match = matchAgentTag(project.containerTag)
|
||||
if (!match) continue
|
||||
const spaceMetadata = metadata.get(project.containerTag)
|
||||
const projectName = normalizeProjectName(spaceMetadata?.projectName)
|
||||
const projectId = normalizeProjectId(
|
||||
spaceMetadata?.projectId ?? match.projectId,
|
||||
)
|
||||
const identity = groupIdentity(project.containerTag, projectName)
|
||||
const existing = grouped.get(identity.key)
|
||||
if (existing) {
|
||||
existing.projects.push(project)
|
||||
existing.containerTags.push(project.containerTag)
|
||||
if (!existing.projectName && projectName) {
|
||||
existing.projectName = projectName
|
||||
existing.label = projectName
|
||||
}
|
||||
if (!projectId) {
|
||||
legacyProjects.push({ project, projectName })
|
||||
continue
|
||||
}
|
||||
|
||||
grouped.set(identity.key, {
|
||||
key: identity.key,
|
||||
label: identity.label,
|
||||
const key = `project-id:${projectId}`
|
||||
const label =
|
||||
projectName || humanizeProjectId(match.projectSlug ?? "") || "Project"
|
||||
addProjectToGroup(grouped, key, label, "project", project, projectName)
|
||||
if (projectName) {
|
||||
const normalizedName = projectName.toLocaleLowerCase()
|
||||
const keys = canonicalKeysByName.get(normalizedName) ?? []
|
||||
if (!keys.includes(key)) keys.push(key)
|
||||
canonicalKeysByName.set(normalizedName, keys)
|
||||
}
|
||||
}
|
||||
|
||||
for (const { project, projectName } of legacyProjects) {
|
||||
const identity = legacyGroupIdentity(project.containerTag, projectName)
|
||||
const canonicalMatches = projectName
|
||||
? (canonicalKeysByName.get(projectName.toLocaleLowerCase()) ?? [])
|
||||
: []
|
||||
const key =
|
||||
identity.kind === "project" && canonicalMatches.length === 1
|
||||
? canonicalMatches[0]!
|
||||
: identity.key
|
||||
addProjectToGroup(
|
||||
grouped,
|
||||
key,
|
||||
projectName ?? identity.label,
|
||||
identity.kind,
|
||||
project,
|
||||
projectName,
|
||||
kind: identity.kind,
|
||||
representative: project,
|
||||
projects: [project],
|
||||
containerTags: [project.containerTag],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return [...grouped.values()]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue