diff --git a/packages/plugins/plugin-llm-wiki/src/ui/index.tsx b/packages/plugins/plugin-llm-wiki/src/ui/index.tsx index e73704e3e66..8110e827bc6 100644 --- a/packages/plugins/plugin-llm-wiki/src/ui/index.tsx +++ b/packages/plugins/plugin-llm-wiki/src/ui/index.tsx @@ -2829,7 +2829,7 @@ function SpaceRowMenu({ const handleArchive = async () => { if (!companyId || isDefault || busy) return; - if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages remain on disk; you can restore from settings.`)) { + if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages remain on disk; you can restore later through the plugin API or by un-archiving from the database.`)) { return; } setBusy(true); @@ -6768,7 +6768,7 @@ function SpaceEditCard({ async function archive() { if (!companyId || isDefault || isOnlySpace || archiveBusy) return; - if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages stay on disk; you can restore by un-archiving from the database.`)) { + if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages stay on disk; you can restore later through the plugin API or by un-archiving from the database.`)) { return; } setArchiveBusy(true); diff --git a/packages/plugins/plugin-llm-wiki/src/wiki.ts b/packages/plugins/plugin-llm-wiki/src/wiki.ts index bbdeb7bb3e5..d5550b7606f 100644 --- a/packages/plugins/plugin-llm-wiki/src/wiki.ts +++ b/packages/plugins/plugin-llm-wiki/src/wiki.ts @@ -1367,6 +1367,25 @@ export async function resolveSpace(ctx: PluginContext, input: SpaceInput): Promi return wikiSpaceFromRow(rows[0]); } +async function resolveSpaceAnyStatus(ctx: PluginContext, input: SpaceInput): Promise { + const wikiId = normalizeWikiId(input.wikiId); + const slug = normalizeSpaceSlug(input.spaceSlug); + if (slug === DEFAULT_SPACE_SLUG) { + return ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }); + } + const rows = await ctx.db.query( + `SELECT id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key, + path_prefix, configured_root_path, access_scope, owner_user_id, owner_agent_id, team_key, + settings, status, created_at::text AS created_at, updated_at::text AS updated_at + FROM ${spaceTable(ctx)} + WHERE company_id = $1 AND wiki_id = $2 AND slug = $3 + LIMIT 1`, + [input.companyId, wikiId, slug], + ); + if (!rows[0]) throw new Error(`LLM Wiki space not found: ${slug}`); + return wikiSpaceFromRow(rows[0]); +} + export async function listSpaces(ctx: PluginContext, input: { companyId: string; wikiId?: string | null }): Promise<{ spaces: WikiSpace[] }> { const wikiId = normalizeWikiId(input.wikiId); await ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId }); @@ -1440,12 +1459,12 @@ export async function createSpace(ctx: PluginContext, input: CreateSpaceInput): } export async function updateSpace(ctx: PluginContext, input: UpdateSpaceInput): Promise<{ status: "ok"; space: WikiSpace }> { - const space = await resolveSpace(ctx, input); - const nextDisplayName = stringField(input.displayName); const nextStatus = input.status ?? null; if (nextStatus !== null && nextStatus !== "active" && nextStatus !== "archived") { throw new Error("LLM Wiki space status must be active or archived."); } + const space = nextStatus === "active" ? await resolveSpaceAnyStatus(ctx, input) : await resolveSpace(ctx, input); + const nextDisplayName = stringField(input.displayName); if (space.slug === DEFAULT_SPACE_SLUG && nextStatus === "archived") { throw new Error("The default LLM Wiki space cannot be archived."); } diff --git a/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts b/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts index 22b26937f02..75592d76395 100644 --- a/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts +++ b/packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts @@ -2867,6 +2867,52 @@ Duplicate headings receive stable suffixes. expect(archived).toBe(true); }); + it("restores an archived non-default space through update-space", async () => { + const harness = createTestHarness({ manifest }); + const spaceRow = wikiSpaceRow({ + id: "77777777-7777-4777-8777-7777777777b2", + slug: "qa-space", + displayName: "QA Space", + pathPrefix: "spaces/qa-space", + status: "archived", + }); + let restored = false; + const originalQuery = harness.ctx.db.query.bind(harness.ctx.db); + harness.ctx.db.query = async (sql: string, params?: unknown[]) => { + harness.dbQueries.push({ sql, params }); + if (sql.includes("wiki_spaces") && params?.[2] === "qa-space") { + if (!restored && sql.includes("status <> 'archived'")) return [] as T[]; + return [{ + ...spaceRow, + status: restored ? "active" : "archived", + }] as T[]; + } + return originalQuery(sql, params); + }; + const originalExecute = harness.ctx.db.execute.bind(harness.ctx.db); + harness.ctx.db.execute = async (sql: string, params?: unknown[]) => { + if (sql.includes("UPDATE") && sql.includes("wiki_spaces") && params?.includes("active")) restored = true; + return originalExecute(sql, params); + }; + + await plugin.definition.setup(harness.ctx); + const updated = await harness.performAction<{ + status: string; + space: { slug: string; status: string }; + }>("update-space", { + companyId: COMPANY_ID, + spaceSlug: "qa-space", + status: "active", + }); + + expect(updated.status).toBe("ok"); + expect(updated.space).toMatchObject({ + slug: "qa-space", + status: "active", + }); + expect(restored).toBe(true); + }); + it("rejects unknown space statuses through update-space", async () => { const harness = createTestHarness({ manifest }); await plugin.definition.setup(harness.ctx);