mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
use space across mcp surfaces
This commit is contained in:
parent
d6e34d2131
commit
8a9fcd52dc
34 changed files with 170 additions and 162 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# Supermemory MCP Server
|
||||
|
||||
The Supermemory MCP server gives authenticated AI clients access to a user's
|
||||
memories, profile, workspaces, and interactive MCP Apps.
|
||||
memories, profile, spaces, and interactive MCP Apps.
|
||||
|
||||
## Runtime Model
|
||||
|
||||
|
|
@ -9,17 +9,17 @@ memories, profile, workspaces, and interactive MCP Apps.
|
|||
- Modern MCP `2026-07-28` plus stateless compatibility for 2025 clients
|
||||
- OAuth token validation on every request
|
||||
- No MCP protocol session or protocol Durable Object
|
||||
- Active workspace stored as application state in a dedicated Durable Object
|
||||
- Workspace state keyed by authenticated `organizationId + userId`
|
||||
- Active space stored as application state in a dedicated Durable Object
|
||||
- Space state keyed by authenticated `organizationId + userId`
|
||||
|
||||
The workspace used by an operation resolves in this order:
|
||||
The space used by an operation resolves in this order:
|
||||
|
||||
1. An explicit `containerTag` tool or prompt argument
|
||||
2. The account's durable active workspace
|
||||
2. The account's durable active space
|
||||
3. The Supermemory client default, `sm_project_default`
|
||||
|
||||
An explicit override applies only to that call. It does not mutate the active
|
||||
workspace.
|
||||
space.
|
||||
|
||||
## Server URL
|
||||
|
||||
|
|
@ -49,18 +49,18 @@ The client discovers the OAuth authorization server through
|
|||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `search_memory` | Search memories and optionally include profile context |
|
||||
| `listDocuments` | List document metadata and summaries in a workspace |
|
||||
| `listDocuments` | List document metadata and summaries in a space |
|
||||
| `getDocument` | Read one document's available content by ID |
|
||||
| `listMemories` | List extracted memory entries and their source document IDs |
|
||||
| `listSpaces` | List workspaces visible to the authenticated account |
|
||||
| `whoAmI` | Return identity, access, and active-workspace context |
|
||||
| `listSpaces` | List spaces visible to the authenticated account |
|
||||
| `whoAmI` | Return identity, access, and active-space context |
|
||||
| `add_memory` | Save or forget a memory |
|
||||
|
||||
### MCP App launchers
|
||||
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `select-workspace` | Open the interactive workspace picker |
|
||||
| `select-space` | Open the interactive space picker |
|
||||
| `memory-graph` | Open the interactive memory graph |
|
||||
| `guided-save` | Open the guided memory form |
|
||||
| `upload-file` | Open the file upload form |
|
||||
|
|
@ -71,7 +71,7 @@ These tools are available to the embedded MCP App and hidden from the model.
|
|||
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `set-active-tag` | Persist the selected active workspace |
|
||||
| `set-active-tag` | Persist the selected active space |
|
||||
| `save-memory` | Submit the guided save form |
|
||||
| `upload-file-submit` | Submit an encoded file upload |
|
||||
| `fetch-graph-data` | Fetch graph documents for the app |
|
||||
|
|
@ -80,10 +80,10 @@ These tools are available to the embedded MCP App and hidden from the model.
|
|||
|
||||
| Kind | Name or URI | Purpose |
|
||||
| --- | --- | --- |
|
||||
| Resource | `supermemory://profile` | Profile facts in the effective workspace |
|
||||
| Resource | `supermemory://container-tags` | Visible workspaces |
|
||||
| Resource | `supermemory://profile` | Profile facts in the effective space |
|
||||
| Resource | `supermemory://spaces` | Visible spaces |
|
||||
| Resource | `ui://supermemory/app-v3.html` | Embedded MCP App bundle |
|
||||
| Prompt | `context` | Profile and recent context for an optional workspace |
|
||||
| Prompt | `context` | Profile and recent context for an optional space |
|
||||
|
||||
The App resource and tool metadata include both current nested `ui` metadata and
|
||||
the legacy flat resource URI key while MCP Apps completes its SDK v2 migration.
|
||||
|
|
@ -131,10 +131,12 @@ discovery and rejection tests still run.
|
|||
| `API_URL` | Supermemory API and OAuth issuer | `https://api.supermemory.ai` |
|
||||
| `MCP_RESOURCE` | Expected OAuth audience | `https://mcp.supermemory.ai/mcp` |
|
||||
| `ALLOWED_MCP_ORIGIN_HOSTNAMES` | Additional comma-separated browser origins | Built-in host allowlist |
|
||||
| `POSTHOG_API_KEY` | Server-side MCP tool analytics project key | Disabled |
|
||||
| `POSTHOG_HOST` | PostHog ingestion host | `https://us.i.posthog.com` |
|
||||
|
||||
## Storage And Rollout
|
||||
|
||||
`WorkspaceState` stores only the active container tag. It never stores bearer
|
||||
`WorkspaceState` stores only the active space's container tag. It never stores bearer
|
||||
tokens, MCP client identity, or protocol messages.
|
||||
|
||||
The old `SupermemoryMCP` class and binding remain inert for one rollout. This
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const EXPECTED_TOOLS = [
|
|||
"memory-graph",
|
||||
"save-memory",
|
||||
"search_memory",
|
||||
"select-workspace",
|
||||
"select-space",
|
||||
"set-active-tag",
|
||||
"upload-file",
|
||||
"upload-file-submit",
|
||||
|
|
@ -80,11 +80,22 @@ describeWithAuth("MCP — discovery & identity", () => {
|
|||
expect(memory?.annotations).toMatchObject(MEMORY_TOOL_ANNOTATIONS)
|
||||
})
|
||||
|
||||
it("lists profile and container-tag resources", async () => {
|
||||
it("lists profile and space resources", async () => {
|
||||
const { resources } = await s.client.listResources()
|
||||
const uris = resources.map((r) => r.uri)
|
||||
expect(uris).toContain("supermemory://profile")
|
||||
expect(uris).toContain("supermemory://container-tags")
|
||||
expect(uris).toContain("supermemory://spaces")
|
||||
})
|
||||
|
||||
it("uses space terminology across exposed MCP metadata", async () => {
|
||||
const [{ tools }, { resources }, { prompts }] = await Promise.all([
|
||||
s.client.listTools(),
|
||||
s.client.listResources(),
|
||||
s.client.listPrompts(),
|
||||
])
|
||||
expect(JSON.stringify({ tools, resources, prompts })).not.toMatch(
|
||||
/\bworkspaces?\b/i,
|
||||
)
|
||||
})
|
||||
|
||||
it("lists the context prompt", async () => {
|
||||
|
|
@ -97,6 +108,8 @@ describeWithAuth("MCP — discovery & identity", () => {
|
|||
expect(res.isError).toBeFalsy()
|
||||
const parsed = JSON.parse(textOf(res))
|
||||
expect(parsed.userId).toBeTruthy()
|
||||
expect(parsed).toHaveProperty("activeSpace")
|
||||
expect(parsed).not.toHaveProperty("activeWorkspace")
|
||||
})
|
||||
|
||||
it("listSpaces returns content", async () => {
|
||||
|
|
|
|||
|
|
@ -49,25 +49,25 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
|
|||
const res = await s.client.readResource({ uri: "supermemory://profile" })
|
||||
expect(res.contents.length).toBeGreaterThan(0)
|
||||
expect(res.contents[0].mimeType).toBe("text/plain")
|
||||
expect(res.contents[0].text).toMatch(/# Active Workspace Profile/)
|
||||
expect(res.contents[0].text).toMatch(/Workspace:/)
|
||||
expect(res.contents[0].text).toMatch(/# Active Space Profile/)
|
||||
expect(res.contents[0].text).toMatch(/Space:/)
|
||||
expect(res.contents[0].text).toMatch(
|
||||
/Use `listSpaces` to find the relevant workspace key/,
|
||||
/Use `listSpaces` to find the relevant space key/,
|
||||
)
|
||||
})
|
||||
|
||||
it("reads all workspaces in a compact human-readable format", async () => {
|
||||
it("reads all spaces in a compact human-readable format", async () => {
|
||||
const res = await s.client.readResource({
|
||||
uri: "supermemory://container-tags",
|
||||
uri: "supermemory://spaces",
|
||||
})
|
||||
const text = res.contents[0].text as string
|
||||
expect(res.contents[0].mimeType).toBe("text/plain")
|
||||
expect(text).toMatch(/# My Workspaces/)
|
||||
expect(text).toMatch(/# My Spaces/)
|
||||
expect(text).toMatch(/Active:/)
|
||||
expect(text).not.toMatch(/"containerTags":/)
|
||||
})
|
||||
|
||||
it("gets compact active-workspace context without prompt arguments", async () => {
|
||||
it("gets compact active-space context without prompt arguments", async () => {
|
||||
const prompts = await s.client.listPrompts()
|
||||
const contextPrompt = prompts.prompts.find(
|
||||
(prompt) => prompt.name === "context",
|
||||
|
|
@ -78,6 +78,6 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
|
|||
expect(res.messages.length).toBeGreaterThan(0)
|
||||
const text = res.messages[0].content.text as string
|
||||
expect(text).toMatch(/# Supermemory Context/)
|
||||
expect(text).toMatch(/Active workspace:/)
|
||||
expect(text).toMatch(/Active space:/)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
|
|||
await session?.close()
|
||||
})
|
||||
|
||||
it("loads visible workspaces and effective permissions on demand", async () => {
|
||||
const result = await callTool(session.client, "select-workspace")
|
||||
it("loads visible spaces and effective permissions on demand", async () => {
|
||||
const result = await callTool(session.client, "select-space")
|
||||
expect(result.isError).toBeFalsy()
|
||||
const content = result.structuredContent as {
|
||||
view?: string
|
||||
|
|
@ -43,8 +43,8 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
|
|||
).toBe(true)
|
||||
})
|
||||
|
||||
it("shares the selected workspace across MCP transport sessions", async () => {
|
||||
const picker = await callTool(session.client, "select-workspace")
|
||||
it("shares the selected space across MCP transport sessions", async () => {
|
||||
const picker = await callTool(session.client, "select-space")
|
||||
const pickerContent = picker.structuredContent as {
|
||||
containerTags?: Array<{ containerTag: string }>
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
|
|||
const identity = await callTool(separateSession.client, "whoAmI")
|
||||
expect(identity.isError).toBeFalsy()
|
||||
expect(JSON.parse(textOf(identity))).toMatchObject({
|
||||
activeWorkspace: firstTag,
|
||||
activeSpace: firstTag,
|
||||
})
|
||||
} finally {
|
||||
await separateSession.close()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const propsOf = (tools: ToolLike[], name: string): Record<string, unknown> =>
|
|||
|
||||
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
|
||||
|
||||
describeWithAuth("MCP - workspace scoping", () => {
|
||||
describeWithAuth("MCP - space scoping", () => {
|
||||
it("keeps per-call containerTag overrides when an obsolete header is sent", async () => {
|
||||
const scoped = await connect({
|
||||
headers: { "x-sm-project": "obsolete-root-scope" },
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ describe("MCP tool analytics", () => {
|
|||
)
|
||||
|
||||
await harness.invoke(
|
||||
{ query: "private query", containerTag: "private-workspace" },
|
||||
{ query: "private query", containerTag: "private-space" },
|
||||
context,
|
||||
)
|
||||
|
||||
|
|
@ -64,14 +64,12 @@ describe("MCP tool analytics", () => {
|
|||
toolName: "search_memory",
|
||||
surface: "model_tool",
|
||||
outcome: "success",
|
||||
workspaceExplicit: true,
|
||||
spaceExplicit: true,
|
||||
client: { name: "claude", version: "1.2.3" },
|
||||
}),
|
||||
)
|
||||
expect(JSON.stringify(record.mock.calls[0])).not.toContain("private query")
|
||||
expect(JSON.stringify(record.mock.calls[0])).not.toContain(
|
||||
"private-workspace",
|
||||
)
|
||||
expect(JSON.stringify(record.mock.calls[0])).not.toContain("private-space")
|
||||
expect(JSON.stringify(record.mock.calls[0])).not.toContain("secret result")
|
||||
})
|
||||
|
||||
|
|
@ -148,7 +146,7 @@ describe("MCP tool analytics", () => {
|
|||
surface: "app_launcher",
|
||||
outcome: "success",
|
||||
durationMs: 42,
|
||||
workspaceExplicit: false,
|
||||
spaceExplicit: false,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -163,7 +161,7 @@ describe("MCP tool analytics", () => {
|
|||
duration_ms: 42,
|
||||
mcp_runtime: "stateless",
|
||||
mcp_surface: "app_launcher",
|
||||
workspace_explicit: false,
|
||||
space_explicit: false,
|
||||
oauth_client_id: "client_123",
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export interface McpToolExecution {
|
|||
surface: McpToolSurface
|
||||
outcome: McpToolOutcome
|
||||
durationMs: number
|
||||
workspaceExplicit: boolean
|
||||
spaceExplicit: boolean
|
||||
client?: { name: string; version?: string }
|
||||
errorType?: string
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ const TOOL_SURFACES: Record<string, McpToolSurface> = {
|
|||
listSpaces: "model_tool",
|
||||
whoAmI: "model_tool",
|
||||
add_memory: "model_tool",
|
||||
"select-workspace": "app_launcher",
|
||||
"select-space": "app_launcher",
|
||||
"memory-graph": "app_launcher",
|
||||
"guided-save": "app_launcher",
|
||||
"upload-file": "app_launcher",
|
||||
|
|
@ -87,7 +87,7 @@ export function posthogEventForToolExecution(
|
|||
duration_ms: execution.durationMs,
|
||||
mcp_runtime: "stateless",
|
||||
mcp_surface: execution.surface,
|
||||
workspace_explicit: execution.workspaceExplicit,
|
||||
space_explicit: execution.spaceExplicit,
|
||||
...(execution.client
|
||||
? {
|
||||
mcp_client_name: execution.client.name,
|
||||
|
|
@ -126,7 +126,7 @@ export function createPosthogAnalytics(
|
|||
}
|
||||
}
|
||||
|
||||
function workspaceWasExplicit(value: unknown): boolean {
|
||||
function spaceWasExplicit(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object") return false
|
||||
const containerTag = Reflect.get(value, "containerTag")
|
||||
return typeof containerTag === "string" && containerTag.trim().length > 0
|
||||
|
|
@ -185,7 +185,7 @@ export function createTrackedToolServer(
|
|||
surface: TOOL_SURFACES[name] ?? "model_tool",
|
||||
outcome,
|
||||
durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
||||
workspaceExplicit: workspaceWasExplicit(input),
|
||||
spaceExplicit: spaceWasExplicit(input),
|
||||
...(client ? { client } : {}),
|
||||
...(errorType ? { errorType } : {}),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ describe("SupermemoryClient memory listing", () => {
|
|||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it("calls the canonical memory-list endpoint with the selected workspace", async () => {
|
||||
it("calls the canonical memory-list endpoint with the selected space", async () => {
|
||||
const responseBody = {
|
||||
memoryEntries: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ export const containerTagSchema = z
|
|||
.string()
|
||||
.min(1, "Container tag is required")
|
||||
.max(128, "Container tag exceeds maximum length")
|
||||
.describe("Workspace key returned by listSpaces")
|
||||
.describe("Space key returned by listSpaces")
|
||||
|
||||
export const optionalContainerTagSchema = containerTagSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"Workspace key to use for this call. If the user names a workspace, call listSpaces to resolve its key and pass it here. Omit only when the user means the active workspace.",
|
||||
"Space key to use for this call. If the user names a space, call listSpaces to resolve its key and pass it here. Omit only when the user means the active space.",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function registerContextPrompt(
|
|||
server.registerPrompt(
|
||||
"context",
|
||||
{
|
||||
description: "Attach compact context for the active workspace",
|
||||
description: "Attach compact context for the active space",
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
|
|
@ -37,7 +37,7 @@ export function registerContextPrompt(
|
|||
const fallback = selectedTag ? "" : " (default)"
|
||||
const parts: string[] = [
|
||||
"# Supermemory Context",
|
||||
`Active workspace: ${activeLabel} [${activeKey}]${fallback}`,
|
||||
`Active space: ${activeLabel} [${activeKey}]${fallback}`,
|
||||
]
|
||||
|
||||
if (activeWorkspace) {
|
||||
|
|
@ -65,7 +65,7 @@ export function registerContextPrompt(
|
|||
profileResult.profile.static.length === 0 &&
|
||||
profileResult.profile.dynamic.length === 0
|
||||
) {
|
||||
parts.push("No profile facts are available for this workspace yet.")
|
||||
parts.push("No profile facts are available for this space yet.")
|
||||
}
|
||||
|
||||
const recentWorkspaces = sortWorkspaces(workspaces, activeKey)
|
||||
|
|
@ -74,7 +74,7 @@ export function registerContextPrompt(
|
|||
if (recentWorkspaces.length > 0) {
|
||||
parts.push(
|
||||
"",
|
||||
"## Recently Active Workspaces",
|
||||
"## Recently Active Spaces",
|
||||
...recentWorkspaces.map((workspace) =>
|
||||
formatWorkspaceRow(workspace, activeKey, 100),
|
||||
),
|
||||
|
|
@ -83,7 +83,7 @@ export function registerContextPrompt(
|
|||
|
||||
parts.push(
|
||||
"",
|
||||
"Use a workspace key with workspace-aware tools when the user asks about another workspace. Keep workspace contexts separate unless the user asks to combine them.",
|
||||
"Use a space key with space-aware tools when the user asks about another space. Keep space contexts separate unless the user asks to combine them.",
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -11,41 +11,36 @@ export function registerContainerTagsResource(
|
|||
getClient: () => SupermemoryClient,
|
||||
resolveContainerTag: () => Promise<string | undefined>,
|
||||
) {
|
||||
server.registerResource(
|
||||
"My Workspaces",
|
||||
"supermemory://container-tags",
|
||||
{},
|
||||
async () => {
|
||||
const client = getClient()
|
||||
const [containerTags, selectedTag] = await Promise.all([
|
||||
client.listContainerTags(),
|
||||
resolveContainerTag(),
|
||||
])
|
||||
const activeKey = selectedTag ?? DEFAULT_PROJECT_ID
|
||||
const activeWorkspace = containerTags.find(
|
||||
(workspace) => workspace.containerTag === activeKey,
|
||||
)
|
||||
const rows = sortWorkspaces(containerTags, activeKey).map((workspace) =>
|
||||
formatWorkspaceRow(workspace, activeKey),
|
||||
)
|
||||
const activeLabel = workspaceDisplayName(activeWorkspace, activeKey)
|
||||
const fallback = selectedTag ? "" : " (default)"
|
||||
const text = [
|
||||
"# My Workspaces",
|
||||
`${containerTags.length} available · Active: ${activeLabel} [${activeKey}]${fallback}`,
|
||||
"",
|
||||
...rows,
|
||||
].join("\n")
|
||||
server.registerResource("My Spaces", "supermemory://spaces", {}, async () => {
|
||||
const client = getClient()
|
||||
const [containerTags, selectedTag] = await Promise.all([
|
||||
client.listContainerTags(),
|
||||
resolveContainerTag(),
|
||||
])
|
||||
const activeKey = selectedTag ?? DEFAULT_PROJECT_ID
|
||||
const activeWorkspace = containerTags.find(
|
||||
(workspace) => workspace.containerTag === activeKey,
|
||||
)
|
||||
const rows = sortWorkspaces(containerTags, activeKey).map((workspace) =>
|
||||
formatWorkspaceRow(workspace, activeKey),
|
||||
)
|
||||
const activeLabel = workspaceDisplayName(activeWorkspace, activeKey)
|
||||
const fallback = selectedTag ? "" : " (default)"
|
||||
const text = [
|
||||
"# My Spaces",
|
||||
`${containerTags.length} available · Active: ${activeLabel} [${activeKey}]${fallback}`,
|
||||
"",
|
||||
...rows,
|
||||
].join("\n")
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: "supermemory://container-tags",
|
||||
mimeType: "text/plain",
|
||||
text,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: "supermemory://spaces",
|
||||
mimeType: "text/plain",
|
||||
text,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function registerProfileResource(
|
|||
resolveContainerTag: () => Promise<string | undefined>,
|
||||
) {
|
||||
server.registerResource(
|
||||
"Active Workspace Profile",
|
||||
"Active Space Profile",
|
||||
"supermemory://profile",
|
||||
{},
|
||||
async () => {
|
||||
|
|
@ -31,8 +31,8 @@ export function registerProfileResource(
|
|||
const activeLabel = workspaceDisplayName(activeWorkspace, activeKey)
|
||||
const fallback = selectedTag ? "" : " (default)"
|
||||
const parts: string[] = [
|
||||
"# Active Workspace Profile",
|
||||
`Workspace: ${activeLabel} [${activeKey}]${fallback}`,
|
||||
"# Active Space Profile",
|
||||
`Space: ${activeLabel} [${activeKey}]${fallback}`,
|
||||
]
|
||||
|
||||
if (activeWorkspace) {
|
||||
|
|
@ -60,12 +60,12 @@ export function registerProfileResource(
|
|||
profileResult.profile.static.length === 0 &&
|
||||
profileResult.profile.dynamic.length === 0
|
||||
) {
|
||||
parts.push("No profile facts are available for this workspace yet.")
|
||||
parts.push("No profile facts are available for this space yet.")
|
||||
}
|
||||
|
||||
parts.push(
|
||||
"",
|
||||
"Other workspaces are available. Use `listSpaces` to find the relevant workspace key, then use that key with workspace-aware tools when the user asks about another workspace. Keep workspace contexts separate unless the user asks to combine them.",
|
||||
"Other spaces are available. Use `listSpaces` to find the relevant space key, then use that key with space-aware tools when the user asks about another space. Keep space contexts separate unless the user asks to combine them.",
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function register(deps: ToolDeps) {
|
|||
"add_memory",
|
||||
{
|
||||
description:
|
||||
"Add (save) or forget a memory in the user's ACTIVE workspace. Defaults to 'save'. The target workspace is the one the user selected via select-workspace; pass containerTag only to override it. Use 'forget' when information is outdated or the user asks to remove it.",
|
||||
"Add (save) or forget a memory in the user's ACTIVE space. Defaults to 'save'. The target space is the one the user selected via select-space; pass containerTag only to override it. Use 'forget' when information is outdated or the user asks to remove it.",
|
||||
inputSchema,
|
||||
annotations: MEMORY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
@ -38,7 +38,7 @@ export function register(deps: ToolDeps) {
|
|||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Memory saved (ID: ${result.id}, workspace: ${result.containerTag})`,
|
||||
text: `Memory saved (ID: ${result.id}, space: ${result.containerTag})`,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function register(deps: ToolDeps) {
|
|||
{
|
||||
title: "Get Document",
|
||||
description:
|
||||
"Read one stored document by ID, including its summary and available content. Use listDocuments in the intended workspace to discover document IDs.",
|
||||
"Read one stored document by ID, including its summary and available content. Use listDocuments in the intended space to discover document IDs.",
|
||||
inputSchema,
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import * as listMemories from "./list-memories"
|
|||
import * as memoryGraph from "./memory-graph"
|
||||
import * as saveMemory from "./save-memory"
|
||||
import * as searchMemory from "./search-memory"
|
||||
import * as selectWorkspace from "./select-workspace"
|
||||
import * as selectSpace from "./select-space"
|
||||
import * as setActiveTag from "./set-active-tag"
|
||||
import type { ToolDeps } from "./types"
|
||||
import * as uploadFile from "./upload-file"
|
||||
|
|
@ -22,7 +22,7 @@ export function registerAllTools(deps: ToolDeps) {
|
|||
listMemories.register(deps)
|
||||
listContainerTags.register(deps)
|
||||
whoAmI.register(deps)
|
||||
selectWorkspace.register(deps)
|
||||
selectSpace.register(deps)
|
||||
setActiveTag.register(deps)
|
||||
memoryGraph.register(deps)
|
||||
fetchGraphData.register(deps)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export function register(deps: ToolDeps) {
|
|||
"listSpaces",
|
||||
{
|
||||
description:
|
||||
"List the workspaces available to the user. Returns each workspace's name, key, emoji, document/memory counts, and last activity. Use this first to resolve a named workspace before calling a workspace-aware tool, or when the user asks which workspace may contain something. The list is auto-filtered to workspaces the user can access.",
|
||||
"List the spaces available to the user. Returns each space's name, key, emoji, document/memory counts, and last activity. Use this first to resolve a named space before calling a space-aware tool, or when the user asks which space may contain something. The list is auto-filtered to spaces the user can access.",
|
||||
inputSchema: z.object({}),
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function register(deps: ToolDeps) {
|
|||
{
|
||||
title: "List Documents",
|
||||
description:
|
||||
"List documents in one workspace with their IDs, titles, types, processing status, dates, and summaries. This does not return full document content; use getDocument with an ID from this result to read one document. When the user names a workspace, resolve it with listSpaces and pass containerTag; otherwise use the active workspace.",
|
||||
"List documents in one space with their IDs, titles, types, processing status, dates, and summaries. This does not return full document content; use getDocument with an ID from this result to read one document. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space.",
|
||||
inputSchema,
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function register(deps: ToolDeps) {
|
|||
{
|
||||
title: "List Memories",
|
||||
description:
|
||||
"List the latest extracted memory entries in one workspace, including stable memory IDs, version information, and source document IDs. This lists memories directly, not documents. When the user names a workspace, resolve it with listSpaces and pass containerTag; otherwise use the active workspace. Use search_memory instead for semantic recall.",
|
||||
"List the latest extracted memory entries in one space, including stable memory IDs, version information, and source document IDs. This lists memories directly, not documents. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space. Use search_memory instead for semantic recall.",
|
||||
inputSchema,
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function register(deps: ToolDeps) {
|
|||
{
|
||||
title: "Memory Graph",
|
||||
description:
|
||||
"Render the workspace's memory graph directly as an interactive MCP App. This tool is the final visualization; do not create another graph, file, or artifact unless the user explicitly asks for one. When the user names a workspace, resolve it with listSpaces and pass containerTag.",
|
||||
"Render the space's memory graph directly as an interactive MCP App. This tool is the final visualization; do not create another graph, file, or artifact unless the user explicitly asks for one. When the user names a space, resolve it with listSpaces and pass containerTag.",
|
||||
inputSchema,
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
_meta: appToolMeta(),
|
||||
|
|
@ -44,7 +44,7 @@ export function register(deps: ToolDeps) {
|
|||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Rendered the interactive Memory Graph MCP App: ${result.documents.length} documents, ${memoryCount} memories${effectiveTag ? `. Workspace: ${effectiveTag}` : ""}. Do not create a duplicate graph or artifact unless the user explicitly requests one.`,
|
||||
text: `Rendered the interactive Memory Graph MCP App: ${result.documents.length} documents, ${memoryCount} memories${effectiveTag ? `. Space: ${effectiveTag}` : ""}. Do not create a duplicate graph or artifact unless the user explicitly requests one.`,
|
||||
},
|
||||
],
|
||||
structuredContent: sc,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export function register(deps: ToolDeps) {
|
|||
"search_memory",
|
||||
{
|
||||
description:
|
||||
"Search memories in one workspace with a natural-language query. Returns relevant memories plus that workspace's profile summary. When the user names a workspace, resolve it with listSpaces and pass containerTag; otherwise use the active workspace.",
|
||||
"Search memories in one space with a natural-language query. Returns relevant memories plus that space's profile summary. When the user names a space, resolve it with listSpaces and pass containerTag; otherwise use the active space.",
|
||||
inputSchema,
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import type { ToolDeps } from "./types"
|
|||
|
||||
export function register(deps: ToolDeps) {
|
||||
deps.server.registerTool(
|
||||
"select-workspace",
|
||||
"select-space",
|
||||
{
|
||||
title: "Select Workspace",
|
||||
title: "Select Space",
|
||||
description:
|
||||
"Choose which container tag to work in. Shows available container tags as interactive cards.",
|
||||
"Choose the active Supermemory space. Shows available spaces as interactive cards.",
|
||||
inputSchema: z.object({}),
|
||||
_meta: appToolMeta(),
|
||||
},
|
||||
|
|
@ -40,7 +40,7 @@ export function register(deps: ToolDeps) {
|
|||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `${tags.length} container tags available. Select one to set your active context.`,
|
||||
text: `${tags.length} spaces available. Select one to set your active context.`,
|
||||
},
|
||||
],
|
||||
structuredContent: sc,
|
||||
|
|
@ -8,7 +8,7 @@ export function register(deps: ToolDeps) {
|
|||
deps.server.registerTool(
|
||||
"set-active-tag",
|
||||
{
|
||||
description: "Set the active container tag for this account",
|
||||
description: "Set the active Supermemory space for this account",
|
||||
inputSchema: z.object({
|
||||
containerTag: containerTagSchema,
|
||||
viewId: z.string().uuid().optional(),
|
||||
|
|
@ -35,7 +35,7 @@ export function register(deps: ToolDeps) {
|
|||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Active workspace set to ${containerTag}`,
|
||||
text: `Active space set to ${containerTag}`,
|
||||
},
|
||||
],
|
||||
structuredContent: sc,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export function register(deps: ToolDeps) {
|
|||
deps.server.registerTool(
|
||||
"whoAmI",
|
||||
{
|
||||
description: "Get current user info, role, and workspace context",
|
||||
description: "Get current user info, role, and space context",
|
||||
inputSchema: z.object({}),
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
},
|
||||
|
|
@ -28,8 +28,8 @@ export function register(deps: ToolDeps) {
|
|||
name: session.user.name,
|
||||
role: session.role ?? "unknown",
|
||||
accessType: session.accessType ?? "full",
|
||||
activeWorkspace: activeTag ?? null,
|
||||
assignedTags:
|
||||
activeSpace: activeTag ?? null,
|
||||
assignedSpaces:
|
||||
session.accessType === "restricted"
|
||||
? session.containerTags
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import {
|
|||
sortWorkspaces,
|
||||
} from "./workspace-presentation"
|
||||
|
||||
const workspace = (
|
||||
const space = (
|
||||
containerTag: string,
|
||||
lastActivityAt: string | null,
|
||||
): ContainerTag => ({
|
||||
id: containerTag,
|
||||
name: `Workspace ${containerTag}`,
|
||||
name: `Space ${containerTag}`,
|
||||
containerTag,
|
||||
description: "A compact workspace description.",
|
||||
description: "A compact space description.",
|
||||
visibility: "private",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
|
|
@ -25,13 +25,13 @@ const workspace = (
|
|||
lastActivityAt,
|
||||
})
|
||||
|
||||
describe("workspace presentation", () => {
|
||||
it("keeps the active workspace first, then sorts by activity", () => {
|
||||
describe("space presentation", () => {
|
||||
it("keeps the active space first, then sorts by activity", () => {
|
||||
const sorted = sortWorkspaces(
|
||||
[
|
||||
workspace("older", "2026-01-01T00:00:00.000Z"),
|
||||
workspace("active", "2025-01-01T00:00:00.000Z"),
|
||||
workspace("newer", "2026-02-01T00:00:00.000Z"),
|
||||
space("older", "2026-01-01T00:00:00.000Z"),
|
||||
space("active", "2025-01-01T00:00:00.000Z"),
|
||||
space("newer", "2026-02-01T00:00:00.000Z"),
|
||||
],
|
||||
"active",
|
||||
)
|
||||
|
|
@ -45,7 +45,7 @@ describe("workspace presentation", () => {
|
|||
|
||||
it("formats compact rows without internal database IDs", () => {
|
||||
const row = formatWorkspaceRow(
|
||||
workspace("project-key", "2026-07-29T19:44:28.177Z"),
|
||||
space("project-key", "2026-07-29T19:44:28.177Z"),
|
||||
"project-key",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ describe("workspace application state", () => {
|
|||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it("tells the model how to route explicit workspace requests", () => {
|
||||
it("tells the model how to route explicit space requests", () => {
|
||||
expect(optionalContainerTagSchema.description).toContain(
|
||||
"If the user names a workspace",
|
||||
"If the user names a space",
|
||||
)
|
||||
expect(optionalContainerTagSchema.description).toContain("listSpaces")
|
||||
expect(optionalContainerTagSchema.description).toContain("active workspace")
|
||||
expect(optionalContainerTagSchema.description).toContain("active space")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ interface WorkspaceSelectProps {
|
|||
}
|
||||
|
||||
// Ports console-v2's ChipSelect/ContainerTagSelect: a Popover-anchored,
|
||||
// searchable list. Scales to hundreds of workspaces where chips can't.
|
||||
// searchable list. Scales to hundreds of spaces where chips can't.
|
||||
export function WorkspaceSelect({
|
||||
value,
|
||||
onValueChange,
|
||||
options,
|
||||
placeholder = "Select workspace",
|
||||
emptyText = "No workspaces",
|
||||
searchPlaceholder = "Search workspaces…",
|
||||
placeholder = "Select space",
|
||||
emptyText = "No spaces",
|
||||
searchPlaceholder = "Search spaces…",
|
||||
disabled = false,
|
||||
className,
|
||||
}: WorkspaceSelectProps) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* "sm_project_marketing" → "Marketing"
|
||||
* "sm_project_eng_rfcs" → "Eng Rfcs"
|
||||
* "my_custom_workspace" → "My Custom Workspace"
|
||||
* "my_custom_space" → "My Custom Space"
|
||||
*/
|
||||
export function formatTagLabel(raw: string): string {
|
||||
const slug = raw.replace(/^sm_project_/i, "").replace(/^sm_/i, "")
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ describe("view checkpoints", () => {
|
|||
saveViewCheckpoint(view)
|
||||
|
||||
expect(setWidgetState).toHaveBeenCalledWith({
|
||||
modelContent: 'Supermemory active workspace is now "model_test".',
|
||||
modelContent: 'Supermemory active space is now "model_test".',
|
||||
privateContent: {
|
||||
existing: true,
|
||||
supermemoryView: view,
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@ function getStorage(): Storage | undefined {
|
|||
function modelContentForView(view: ViewMessage): string {
|
||||
switch (view.view) {
|
||||
case "confirmation":
|
||||
return `Supermemory active workspace is now "${view.containerTag}".`
|
||||
return `Supermemory active space is now "${view.containerTag}".`
|
||||
case "save-success":
|
||||
return `A memory was saved to Supermemory workspace "${view.containerTag}" with memory ID "${view.id}".`
|
||||
return `A memory was saved to Supermemory space "${view.containerTag}" with memory ID "${view.id}".`
|
||||
case "upload-success":
|
||||
return `"${view.fileName}" was uploaded to Supermemory workspace "${view.containerTag}" with document ID "${view.id}".`
|
||||
return `"${view.fileName}" was uploaded to Supermemory space "${view.containerTag}" with document ID "${view.id}".`
|
||||
default:
|
||||
return "Supermemory widget state updated."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,8 +331,8 @@ export function Studio() {
|
|||
</div>
|
||||
<Card className="max-w-sm">
|
||||
<Field
|
||||
hint="Searchable Popover — scales to hundreds of workspaces."
|
||||
label="Workspace select"
|
||||
hint="Searchable Popover — scales to hundreds of spaces."
|
||||
label="Space select"
|
||||
>
|
||||
<WorkspaceSelect
|
||||
onValueChange={setSelectValue}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ export function Confirmation({ containerTag }: Props) {
|
|||
</span>
|
||||
<Stack align="center" gap="xs">
|
||||
<div className="text-(length:--text-sm) font-semibold text-text-primary">
|
||||
Active workspace set
|
||||
Active space set
|
||||
</div>
|
||||
<WorkspaceChip containerTag={containerTag} />
|
||||
</Stack>
|
||||
<p className="max-w-xs text-(length:--text-xs) leading-relaxed text-text-muted">
|
||||
Saves and recalls will use this workspace until you change it.
|
||||
Saves and recalls will use this space until you change it.
|
||||
</p>
|
||||
</Stack>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,17 +56,17 @@ export function Picker({
|
|||
setPending(null)
|
||||
if (!result.ok || !result.data) {
|
||||
log("error", `[picker] set-active-tag failed: ${result.error}`)
|
||||
onError(result.error ?? "Failed to set active workspace")
|
||||
onError(result.error ?? "Failed to set active space")
|
||||
return
|
||||
}
|
||||
onAdvance(result.data)
|
||||
const handoff = await handoffToModel({
|
||||
context: `Supermemory workspace selection changed. Active workspace: "${containerTag}". Use it for future Supermemory actions until another workspace is selected.`,
|
||||
message: `I selected "${containerTag}" as my active Supermemory workspace. Use this workspace for future Supermemory actions until I select another one.`,
|
||||
context: `Supermemory space selection changed. Active space: "${containerTag}". Use it for future Supermemory actions until another space is selected.`,
|
||||
message: `I selected "${containerTag}" as my active Supermemory space. Use this space for future Supermemory actions until I select another one.`,
|
||||
structuredContent: {
|
||||
supermemory: {
|
||||
action: "workspace-selected",
|
||||
activeWorkspace: containerTag,
|
||||
action: "space-selected",
|
||||
activeSpace: containerTag,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -87,22 +87,22 @@ export function Picker({
|
|||
const count = containerTags.length
|
||||
const description =
|
||||
count === 0
|
||||
? "Create a workspace in Supermemory to get started."
|
||||
: "Pick the workspace to save and recall from."
|
||||
? "Create a space in Supermemory to get started."
|
||||
: "Pick the space to save and recall from."
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<PageHeader description={description} title="Workspaces" />
|
||||
<PageHeader description={description} title="Spaces" />
|
||||
<div className="flex flex-col gap-(--space-3) px-(--page-header-px) pb-(--space-6)">
|
||||
{count === 0 ? (
|
||||
<div className="flex flex-col items-center gap-(--space-2) rounded-xl border border-border bg-[var(--card-bg)] px-(--space-6) py-(--space-10) text-center">
|
||||
<Package className="size-7 text-text-muted" />
|
||||
<p className="text-(length:--text-sm) font-medium text-text-primary">
|
||||
No workspaces yet
|
||||
No spaces yet
|
||||
</p>
|
||||
<p className="max-w-xs text-(length:--text-xs) leading-relaxed text-text-muted">
|
||||
Workspaces you create in Supermemory show up here, ready to save
|
||||
and recall from.
|
||||
Spaces you create in Supermemory show up here, ready to save and
|
||||
recall from.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -113,7 +113,7 @@ export function Picker({
|
|||
<Input
|
||||
className="pl-(--space-8)"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search workspaces…"
|
||||
placeholder="Search spaces…"
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -122,7 +122,7 @@ export function Picker({
|
|||
{filtered.length === 0 ? (
|
||||
<div className="workspace-picker-grid items-center justify-center py-(--space-8)">
|
||||
<p className="px-(--space-4) text-center text-(length:--text-sm) text-text-muted">
|
||||
No workspaces match “{query}”.
|
||||
No spaces match “{query}”.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -148,7 +148,7 @@ export function Picker({
|
|||
|
||||
{pending ? (
|
||||
<p className="text-(length:--text-xs) text-text-muted">
|
||||
Setting workspace to {formatTagLabel(pending)}…
|
||||
Setting space to {formatTagLabel(pending)}…
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -76,12 +76,12 @@ export function Save({
|
|||
result.data.view === "save-success" ? result.data.id : undefined
|
||||
onAdvance(result.data)
|
||||
const handoff = await handoffToModel({
|
||||
context: `Supermemory widget action completed. A memory was saved to workspace "${selectedTag}"${memoryId ? ` with memory ID "${memoryId}"` : ""}. Saved content:\n\n${trimmed}\n\nIt is already saved; do not save it again.`,
|
||||
message: `I used the Supermemory widget to save a memory to workspace "${selectedTag}"${memoryId ? ` (memory ID: ${memoryId})` : ""}. The memory is already saved; do not save it again.`,
|
||||
context: `Supermemory widget action completed. A memory was saved to space "${selectedTag}"${memoryId ? ` with memory ID "${memoryId}"` : ""}. Saved content:\n\n${trimmed}\n\nIt is already saved; do not save it again.`,
|
||||
message: `I used the Supermemory widget to save a memory to space "${selectedTag}"${memoryId ? ` (memory ID: ${memoryId})` : ""}. The memory is already saved; do not save it again.`,
|
||||
structuredContent: {
|
||||
supermemory: {
|
||||
action: "memory-saved",
|
||||
activeWorkspace: selectedTag,
|
||||
activeSpace: selectedTag,
|
||||
memoryId,
|
||||
content: trimmed,
|
||||
},
|
||||
|
|
@ -104,7 +104,7 @@ export function Save({
|
|||
return (
|
||||
<div className="flex flex-col">
|
||||
<PageHeader
|
||||
description="Capture a thought to a workspace your team can search later."
|
||||
description="Capture a thought to a space your team can search later."
|
||||
title="Add Memory"
|
||||
/>
|
||||
<div className="px-(--page-header-px) pb-(--space-6)">
|
||||
|
|
@ -120,7 +120,7 @@ export function Save({
|
|||
</Field>
|
||||
|
||||
{writableTags.length > 0 ? (
|
||||
<Field label="Workspace">
|
||||
<Field label="Space">
|
||||
<WorkspaceSelect
|
||||
onValueChange={setSelectedTag}
|
||||
options={options}
|
||||
|
|
|
|||
|
|
@ -80,12 +80,12 @@ export function Upload({
|
|||
result.data.view === "upload-success" ? result.data.id : undefined
|
||||
onAdvance(result.data)
|
||||
const handoff = await handoffToModel({
|
||||
context: `Supermemory widget action completed. "${file.name}" was uploaded to workspace "${selectedTag}"${documentId ? ` with document ID "${documentId}"` : ""}. It is already uploaded; do not upload it again.`,
|
||||
message: `I used the Supermemory widget to upload "${file.name}" to workspace "${selectedTag}"${documentId ? ` (document ID: ${documentId})` : ""}. The file is already uploaded; do not upload it again.`,
|
||||
context: `Supermemory widget action completed. "${file.name}" was uploaded to space "${selectedTag}"${documentId ? ` with document ID "${documentId}"` : ""}. It is already uploaded; do not upload it again.`,
|
||||
message: `I used the Supermemory widget to upload "${file.name}" to space "${selectedTag}"${documentId ? ` (document ID: ${documentId})` : ""}. The file is already uploaded; do not upload it again.`,
|
||||
structuredContent: {
|
||||
supermemory: {
|
||||
action: "file-uploaded",
|
||||
activeWorkspace: selectedTag,
|
||||
activeSpace: selectedTag,
|
||||
documentId,
|
||||
fileName: file.name,
|
||||
},
|
||||
|
|
@ -114,7 +114,7 @@ export function Upload({
|
|||
return (
|
||||
<div className="flex flex-col">
|
||||
<PageHeader
|
||||
description="Send a file (text, PDF, image, video) into a workspace."
|
||||
description="Send a file (text, PDF, image, video) into a space."
|
||||
title="Upload File"
|
||||
/>
|
||||
<div className="px-(--page-header-px) pb-(--space-6)">
|
||||
|
|
@ -150,7 +150,7 @@ export function Upload({
|
|||
)}
|
||||
|
||||
{writableTags.length > 0 ? (
|
||||
<Field label="Workspace">
|
||||
<Field label="Space">
|
||||
<WorkspaceSelect
|
||||
onValueChange={setSelectedTag}
|
||||
options={options}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue