mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: load plugin frontends at runtime and remove plugin ids from the shell
This commit is contained in:
1 parent
49f0286a14
commit
e88d5a52e3
721 files changed
+54265
-44416
No files matched your search
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user