From 8d4e1fd0ff9cd3684063e8c721689e528f3ac2d9 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Mar 2026 17:26:07 -0700 Subject: [PATCH] fix(mi,canvas): filter disabled modules from MI assistant and eliminate app-switcher flash MI now loads space doc to filter module list, capabilities, and fallback by enabledModules. Canvas fetches /api/modules and space modules in parallel via Promise.all, calling setModules once with filtered list. Co-Authored-By: Claude Opus 4.6 --- server/mi-routes.ts | 23 ++++++++++++++++++---- website/canvas.html | 48 +++++++++++++++++++++------------------------ 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/server/mi-routes.ts b/server/mi-routes.ts index 5ab318d..2269fc5 100644 --- a/server/mi-routes.ts +++ b/server/mi-routes.ts @@ -13,6 +13,7 @@ import type { MiMessage } from "./mi-provider"; import { getModuleInfoList, getAllModules } from "../shared/module"; import { resolveCallerRole, roleAtLeast } from "./spaces"; import type { SpaceRoleString } from "./spaces"; +import { loadCommunity, getDocumentData } from "./community-store"; import { verifyToken, extractToken } from "./auth"; import type { EncryptIDClaims } from "./auth"; import { buildModuleCapabilities, MODULE_ROUTES } from "../lib/mi-module-routes"; @@ -58,6 +59,14 @@ mi.post("/ask", async (c) => { callerRole = "member"; } + // ── Resolve space's enabled modules ── + let enabledModuleIds: string[] | null = null; + if (space) { + await loadCommunity(space); + const spaceDoc = getDocumentData(space); + enabledModuleIds = spaceDoc?.meta?.enabledModules ?? null; + } + // ── Resolve model ── const modelId = requestedModel || miRegistry.getDefaultModel(); let providerInfo = miRegistry.resolveModel(modelId); @@ -75,7 +84,11 @@ mi.post("/ask", async (c) => { } // ── Build system prompt ── - const moduleList = getModuleInfoList() + const allModuleInfo = getModuleInfoList(); + const filteredModuleInfo = enabledModuleIds + ? allModuleInfo.filter(m => m.id === "rspace" || enabledModuleIds!.includes(m.id)) + : allModuleInfo; + const moduleList = filteredModuleInfo .map((m) => `- **${m.name}** (${m.id}): ${m.icon} ${m.description}`) .join("\n"); @@ -123,8 +136,10 @@ mi.post("/ask", async (c) => { } // Module capabilities for enabled modules - const enabledModuleIds = Object.keys(MODULE_ROUTES); - const moduleCapabilities = buildModuleCapabilities(enabledModuleIds); + const capabilityModuleIds = enabledModuleIds + ? Object.keys(MODULE_ROUTES).filter(id => enabledModuleIds!.includes(id)) + : Object.keys(MODULE_ROUTES); + const moduleCapabilities = buildModuleCapabilities(capabilityModuleIds); // Role-permission mapping const rolePermissions: Record = { @@ -285,7 +300,7 @@ Use requireConfirm:true for destructive batches.`; }); } catch (e: any) { console.error("mi: Provider error:", e.message); - const fallback = generateFallbackResponse(query, currentModule, space, getModuleInfoList()); + const fallback = generateFallbackResponse(query, currentModule, space, filteredModuleInfo); return c.json({ response: fallback }); } }); diff --git a/website/canvas.html b/website/canvas.html index 08dd91e..3a17634 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -2534,36 +2534,32 @@ }); // Load module list for app switcher and tab bar + menu + // Parallel fetch: modules + space-specific filter — setModules called once let moduleList = []; - fetch("/api/modules").then(r => r.json()).then(data => { + const spaceSlug = window.location.pathname.split("/").filter(Boolean)[0] || "demo"; + Promise.all([ + fetch("/api/modules").then(r => r.json()), + fetch(`/api/spaces/${encodeURIComponent(spaceSlug)}/modules`).then(r => r.ok ? r.json() : null), + ]).then(([data, spaceData]) => { moduleList = data.modules || []; window.__rspaceAllModules = moduleList; - document.querySelector("rstack-app-switcher")?.setModules(moduleList); - const tb = document.querySelector("rstack-tab-bar"); - if (tb) tb.setModules(moduleList); - // Fetch space-specific enabled modules and apply filtering - const spaceSlug = window.location.pathname.split("/").filter(Boolean)[0] || "demo"; - fetch(`/api/spaces/${encodeURIComponent(spaceSlug)}/modules`) - .then(r => r.ok ? r.json() : null) - .then(spaceData => { - if (!spaceData) return; - const enabledIds = spaceData.enabledModules; // null = all - window.__rspaceEnabledModules = enabledIds; - if (enabledIds) { - const enabledSet = new Set(enabledIds); - const filtered = moduleList.filter(m => m.id === "rspace" || enabledSet.has(m.id)); - document.querySelector("rstack-app-switcher")?.setModules(filtered); - const tb2 = document.querySelector("rstack-tab-bar"); - if (tb2) tb2.setModules(filtered); - } - // Initialize folk-rapp filtering - customElements.whenDefined("folk-rapp").then(() => { - const FolkRApp = customElements.get("folk-rapp"); - if (FolkRApp?.setEnabledModules) FolkRApp.setEnabledModules(enabledIds); - }); - }) - .catch(() => {}); + const enabledIds = spaceData?.enabledModules ?? null; + window.__rspaceEnabledModules = enabledIds; + + const visible = enabledIds + ? moduleList.filter(m => m.id === "rspace" || new Set(enabledIds).has(m.id)) + : moduleList; + + document.querySelector("rstack-app-switcher")?.setModules(visible); + const tb = document.querySelector("rstack-tab-bar"); + if (tb) tb.setModules(visible); + + // Initialize folk-rapp filtering + customElements.whenDefined("folk-rapp").then(() => { + const FolkRApp = customElements.get("folk-rapp"); + if (FolkRApp?.setEnabledModules) FolkRApp.setEnabledModules(enabledIds); + }); }).catch(() => {}); // React to runtime module toggling from app switcher