(() => {})));
+ client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
+ container = document.createElement("div"); document.body.append(container); root = createRoot(container);
+ vi.spyOn(pluginsApi, "listUiContributions").mockRejectedValue(new TypeError("Failed to fetch"));
+});
+afterEach(async () => {
+ await act(async () => root.unmount()); container.remove(); client.clear(); _resetPluginModuleLoader();
+ vi.unstubAllGlobals(); vi.restoreAllMocks();
+});
+
+it("shows inline errors by default when contributions fail to load", async () => {
+ await render();
+ await vi.waitFor(() => expect(container.textContent).toContain("unavailable"));
+ expect(container.textContent).toContain("Plugin extensions unavailable: Failed to fetch");
+ expect(container.textContent).toContain("Plugin launchers unavailable: Failed to fetch");
+});
+
+it("hides errors and keeps the last loaded slots and launchers when errorBehavior is hidden", async () => {
+ registerPluginReactComponent("fixture.sidebar", "SidebarItem", () => Fixture sidebar item);
+ registerPluginReactComponent("fixture.sidebar", "SidebarPanel", () => Fixture sidebar panel);
+ client.setQueryData(queryKeys.plugins.uiContributions, [contribution]);
+ await render("hidden");
+ expect(container.textContent).toContain("Fixture sidebar item");
+ expect(container.textContent).toContain("Fixture sidebar panel");
+
+ await act(async () => {
+ await client.refetchQueries({ queryKey: queryKeys.plugins.uiContributions });
+ // React Query notifies observers on a timer; let the error render land.
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ });
+ expect(client.getQueryState(queryKeys.plugins.uiContributions)?.status).toBe("error");
+ expect(container.textContent).not.toContain("unavailable");
+ expect(container.textContent).toContain("Fixture sidebar item");
+ expect(container.textContent).toContain("Fixture sidebar panel");
+ expect(container.textContent).toContain("Fixture launcher");
+});
diff --git a/ui/src/plugins/slots.tsx b/ui/src/plugins/slots.tsx
index a86c5a7d722..5ea4d700b6f 100644
--- a/ui/src/plugins/slots.tsx
+++ b/ui/src/plugins/slots.tsx
@@ -905,6 +905,12 @@ type PluginSlotOutletProps = {
itemClassName?: string;
errorClassName?: string;
missingBehavior?: "hidden" | "placeholder";
+ /**
+ * `hidden` suppresses the inline error and keeps rendering the last loaded
+ * slots, so ambient chrome (the sidebar) stays quiet while the server is
+ * unreachable.
+ */
+ errorBehavior?: "inline" | "hidden";
};
export function PluginSlotOutlet({
@@ -915,6 +921,7 @@ export function PluginSlotOutlet({
itemClassName,
errorClassName,
missingBehavior = "hidden",
+ errorBehavior = "inline",
}: PluginSlotOutletProps) {
const { slots, errorMessage } = usePluginSlots({
slotTypes,
@@ -922,7 +929,7 @@ export function PluginSlotOutlet({
companyId: context.companyId,
});
- if (errorMessage) {
+ if (errorMessage && errorBehavior === "inline") {
return (
Plugin extensions unavailable: {errorMessage}