feat: load plugin frontends at runtime and remove plugin ids from the shell

This commit is contained in:
LukeGus committed 2026-09-22 20:15:52 -05:00
1 parent 49f0286a14
commit e88d5a52e3
721 files changed
+54265 -44416

No files matched your search

+23 -3
View File
@@ -15,7 +15,10 @@ import { usePermissions } from "@/hooks/use-permissions";
* dropped entirely rather than disabled, which is how admin-gated UI already
* behaves elsewhere in the app.
*/
export function useActionSlot(slotId: string): SlotContribution[] {
export function useActionSlot(
slotId: string,
context?: Record<string, unknown>,
): SlotContribution[] {
const contributions = useSyncExternalStore(
subscribeToActionRegistry,
() => getSlotContributions(slotId),
@@ -31,7 +34,24 @@ export function useActionSlot(slotId: string): SlotContribution[] {
return contributions.filter((contribution) => {
const permission =
getActionPermission(contribution.actionId) ?? undefined;
return !permission || has(permission);
if (permission && !has(permission)) return false;
if (!contribution.when) return true;
try {
return contribution.when(context ?? {});
} catch {
return false;
}
});
}, [contributions, has, loaded]);
// context is compared by value: slot owners build it inline.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [contributions, has, loaded, contextKey(context)]);
}
function contextKey(context: Record<string, unknown> | undefined): string {
if (!context) return "";
try {
return JSON.stringify(context);
} catch {
return String(Object.keys(context));
}
}
-68
View File
@@ -1,68 +0,0 @@
import { useEffect, useState } from "react";
export interface AiAvailability {
/** Admin kill switch. When false the feature does not exist for anyone. */
globallyEnabled: boolean;
/** Global switch and the user's own opt-in, which is what the rail needs. */
userEnabled: boolean;
/** False until the status call answers, so nothing flashes in or out. */
loaded: boolean;
}
/** Fired when the AI status may have changed, so every surface re-reads it. */
export const AI_STATUS_CHANGED_EVENT = "aiStatusChanged";
export function notifyAiStatusChanged(): void {
window.dispatchEvent(new Event(AI_STATUS_CHANGED_EVENT));
}
/**
* One source of truth for whether any AI surface should render.
*
* The admin global is a hard kill switch: when it is off the assistant is
* hidden everywhere for everyone, including the tab bar, command palette,
* mobile bar, and the navigation visibility toggles, so nobody can reach or
* even see an entry the server would refuse anyway.
*/
export function useAiAvailability(): AiAvailability {
const [state, setState] = useState<AiAvailability>({
globallyEnabled: false,
userEnabled: false,
loaded: false,
});
useEffect(() => {
let cancelled = false;
const refresh = () => {
import("@/api/ai-api")
.then(({ getAiStatus }) => getAiStatus())
.then((status) => {
if (cancelled) return;
setState({
globallyEnabled: status.globallyEnabled,
userEnabled: status.globallyEnabled && status.enabled,
loaded: true,
});
})
.catch(() => {
if (!cancelled)
setState({
globallyEnabled: false,
userEnabled: false,
loaded: true,
});
});
};
refresh();
window.addEventListener(AI_STATUS_CHANGED_EVENT, refresh);
// The profile toggle and onboarding still fire this older event.
window.addEventListener("hiddenRailTabsChanged", refresh);
return () => {
cancelled = true;
window.removeEventListener(AI_STATUS_CHANGED_EVENT, refresh);
window.removeEventListener("hiddenRailTabsChanged", refresh);
};
}, []);
return state;
}
+10
View File
@@ -102,3 +102,13 @@ export function resetPermissionsCache(): void {
cache = null;
inFlight = null;
}
/** Test seam: answers every hook with these grants, no request made. */
export function setPermissionsForTesting(
permissions: string[],
isAdmin = false,
): void {
cache = { permissions, isAdmin };
inFlight = null;
window.dispatchEvent(new Event(PERMISSIONS_CHANGED_EVENT));
}