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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 17:26:07 -07:00
parent ac028cbe04
commit 8d4e1fd0ff
2 changed files with 41 additions and 30 deletions

View File

@ -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<SpaceRoleString, string> = {
@ -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 });
}
});

View File

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