diff --git a/apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx b/apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx
new file mode 100644
index 0000000000..52b3d69ad5
--- /dev/null
+++ b/apps/app/src/components/plugin/PluginSidebarFooterItems.persistence.test.tsx
@@ -0,0 +1,103 @@
+// @vitest-environment jsdom
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, expect, it } from "vitest";
+import { MemoryRouter } from "react-router-dom";
+import { SidebarMenu, SidebarProvider } from "@/components/ui/sidebar.js";
+import {
+ resetPluginSlotStoreForTest,
+ setPluginSlotRegistrations,
+} from "@/lib/plugin-slots";
+import {
+ collectPluginAppRegistrations,
+ definePluginApp,
+} from "@/lib/plugin-app-definition";
+import {
+ PluginSidebarFooterDisclosure,
+ PluginSidebarFooterItems,
+ usePluginSidebarFooterDisclosure,
+} from "./PluginSidebarFooterItems";
+
+function DisclosureHarness() {
+ const disclosure = usePluginSidebarFooterDisclosure();
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+function renderDisclosure() {
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+afterEach(() => {
+ cleanup();
+ resetPluginSlotStoreForTest();
+});
+
+it("keeps the active footer disclosure across a host remount", () => {
+ const definition = definePluginApp((app) => {
+ app.experimental_sidebarFooter.register({
+ kind: "disclosure",
+ id: "status",
+ label: "Provider status",
+ icon: "ChartColumn",
+ component: ({ dismiss }) => (
+
+
Provider status content
+
+
+ ),
+ });
+ });
+ setPluginSlotRegistrations(
+ "status-plugin",
+ collectPluginAppRegistrations(definition),
+ );
+
+ const firstHost = renderDisclosure();
+ fireEvent.click(screen.getByRole("button", { name: "Provider status" }));
+ expect(screen.getByText("Provider status content")).toBeTruthy();
+ firstHost.unmount();
+
+ const secondHost = renderDisclosure();
+ expect(screen.getByText("Provider status content")).toBeTruthy();
+ expect(
+ screen
+ .getByRole("button", { name: "Provider status" })
+ .getAttribute("aria-expanded"),
+ ).toBe("true");
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "Close provider status" }),
+ );
+ expect(screen.queryByText("Provider status content")).toBeNull();
+ secondHost.unmount();
+
+ renderDisclosure();
+ expect(
+ screen
+ .getByRole("button", { name: "Provider status" })
+ .getAttribute("aria-expanded"),
+ ).toBe("false");
+ expect(screen.queryByText("Provider status content")).toBeNull();
+});
diff --git a/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx b/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx
index be817c8950..16ff8e3302 100644
--- a/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx
+++ b/apps/app/src/components/plugin/PluginSidebarFooterItems.tsx
@@ -25,11 +25,22 @@ const SIDEBAR_FOOTER_ACTION_CLASS = cn(
COARSE_POINTER_CHILD_ICON_BUTTON_CLASS,
"text-muted-foreground hover:text-sidebar-foreground [&>svg]:opacity-80",
);
+let rememberedActiveDisclosureKey: string | null = null;
function footerItemKey(item: PluginSidebarFooterItemSlot): string {
return `${item.pluginId}/${item.id}/${item.generation}`;
}
+function nextActiveDisclosureKey(
+ current: string | null,
+ itemKey: string,
+ command: ExperimentalSidebarFooterCommandKind,
+): string | null {
+ if (command === "open") return itemKey;
+ if (command === "close") return current === itemKey ? null : current;
+ return current === itemKey ? null : itemKey;
+}
+
function footerDisclosureId(item: PluginSidebarFooterItemSlot): string {
return `plugin-sidebar-footer-disclosure-${item.pluginId}-${item.id}-${item.generation}`;
}
@@ -44,7 +55,9 @@ export function usePluginSidebarFooterDisclosure() {
() => sidebarFooterItems.filter((item) => item.kind === "disclosure"),
[sidebarFooterItems],
);
- const [activeKey, setActiveKey] = useState(null);
+ const [activeKey, setActiveKey] = useState(
+ rememberedActiveDisclosureKey,
+ );
const [suppressedTooltipKey, setSuppressedTooltipKey] = useState<
string | null
>(null);
@@ -76,9 +89,9 @@ export function usePluginSidebarFooterDisclosure() {
(command === "toggle" && activeKey === itemKey);
setSuppressedTooltipKey(isClosing ? itemKey : null);
setActiveKey((current) => {
- if (command === "open") return itemKey;
- if (command === "close") return current === itemKey ? null : current;
- return current === itemKey ? null : itemKey;
+ const next = nextActiveDisclosureKey(current, itemKey, command);
+ rememberedActiveDisclosureKey = next;
+ return next;
});
},
[activeKey],
@@ -88,6 +101,7 @@ export function usePluginSidebarFooterDisclosure() {
if (activeItem !== null) {
setSuppressedTooltipKey(footerItemKey(activeItem));
}
+ rememberedActiveDisclosureKey = null;
setActiveKey(null);
}, [activeItem]);