diff options
| author | Adam Malczewski <[email protected]> | 2026-09-06 15:43:37 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-09-06 15:43:37 +0900 |
| commit | b117dba1cd1fa80e1b0c18bae487dc632f4005ae (patch) | |
| tree | 5c76f3133ce07595d28fc904133bf0b060b13daf /src/features/plugins | |
| parent | d39cce17765c809cef4555c9640d12ed171cf4af (diff) | |
| download | dispatch-web-b117dba1cd1fa80e1b0c18bae487dc632f4005ae.tar.gz dispatch-web-b117dba1cd1fa80e1b0c18bae487dc632f4005ae.zip | |
feat(layout): re-probe the layout when the workspace's plugin states change
The backend now activates layout providers PER WORKSPACE (the workspace's
per-workspace enablement state picks the ONE active layout plugin), so
toggling a layout plugin in the workspace plugins modal changes which tree
serves the workspace. The modal's toggle now refreshes the layout so the
newly-enabled provider's tree (or the no-provider degrade) lands without a
reload:
- WorkspaceLayoutPort gains refresh(workspaceId) — the modal calls it
after every state write (an attempted write re-probes either way: the
probe is the idempotent catch-up; a refused write changed nothing, so it
re-syncs the same tree).
- App.svelte wires the port to the layout store's new
onWorkspacePluginsChanged(workspaceId) — a guarded re-probe that only
fires for the ACTIVE workspace (another workspace's toggle is picked up
by its own probe on switch, exactly like the other §2.6 triggers).
The per-workspace tree resolution itself is backend-side; the marker
renderer, the probe triggers, and the WS push filtering are
unchanged.
Verification: typecheck (svelte-check 0 errors) / test (1528) / check
/ build all green.
Diffstat (limited to 'src/features/plugins')
| -rw-r--r-- | src/features/plugins/logic/types.ts | 9 | ||||
| -rw-r--r-- | src/features/plugins/ui/WorkspacePluginModal.svelte | 5 | ||||
| -rw-r--r-- | src/features/plugins/ui/WorkspacePluginModal.test.ts | 55 |
3 files changed, 68 insertions, 1 deletions
diff --git a/src/features/plugins/logic/types.ts b/src/features/plugins/logic/types.ts index ec80c9b..e64b553 100644 --- a/src/features/plugins/logic/types.ts +++ b/src/features/plugins/logic/types.ts @@ -70,6 +70,15 @@ export interface WorkspaceLayoutPort { * success or to the failure reason to display. */ readonly reset: (workspaceId: string) => Promise<string | null>; + /** + * The workspace's PER-WORKSPACE PLUGIN STATE changed (the modal just + * toggled a plugin's enablement): the layout plugin that serves THIS + * workspace may have changed (the per-workspace activation rule) — the + * composition root re-probes the layout so the newly-enabled provider's + * tree (or the no-provider degrade) lands without a reload. Only the + * ACTIVE workspace's toggle re-renders; others are ignored by the store. + */ + readonly refresh: (workspaceId: string) => void; } /** diff --git a/src/features/plugins/ui/WorkspacePluginModal.svelte b/src/features/plugins/ui/WorkspacePluginModal.svelte index d1bb27a..0ba7cd6 100644 --- a/src/features/plugins/ui/WorkspacePluginModal.svelte +++ b/src/features/plugins/ui/WorkspacePluginModal.svelte @@ -38,6 +38,11 @@ async function toggle(pluginId: string, checked: boolean): Promise<void> { await plugins.setWorkspaceState(workspaceId, pluginId, checked ? "enabled" : "disabled"); + // The per-workspace activation rule (layout): toggling a plugin may have + // changed which layout plugin serves THIS workspace — re-probe so the + // newly-enabled provider's tree (or the no-provider degrade) renders + // without a reload. The store ignores non-active workspaces. + layout?.refresh(workspaceId); } // ── The reset affordance (plan 04 §4.4) ───────────────────────────────── diff --git a/src/features/plugins/ui/WorkspacePluginModal.test.ts b/src/features/plugins/ui/WorkspacePluginModal.test.ts index 6bc7179..4c75ab3 100644 --- a/src/features/plugins/ui/WorkspacePluginModal.test.ts +++ b/src/features/plugins/ui/WorkspacePluginModal.test.ts @@ -51,8 +51,9 @@ async function renderModal(http: PluginsHttp = fakeHttp()) { function fakeLayoutPort( providerActive = true, reset: WorkspaceLayoutPort["reset"] = vi.fn(async () => null), + refresh: WorkspaceLayoutPort["refresh"] = vi.fn(), ): WorkspaceLayoutPort { - return { providerActive, reset }; + return { providerActive, reset, refresh }; } describe("WorkspacePluginModal", () => { @@ -117,6 +118,58 @@ describe("WorkspacePluginModal", () => { expect(fake.setWorkspaceState).toHaveBeenCalledWith("ws", "mcp", "disabled"); }); + it("a successful toggle calls the layout port's refresh (the per-workspace activation may change the served tree)", async () => { + const user = userEvent.setup(); + const refresh = vi.fn(); + const store = createPluginsStore(fakeHttp()); + await store.openWorkspace("ws"); + render(WorkspacePluginModal, { + props: { + workspaceId: "ws", + workspaceTitle: "My Workspace", + plugins: store, + layout: fakeLayoutPort( + true, + vi.fn(async () => null), + refresh, + ), + onClose: vi.fn(), + }, + }); + await user.click(screen.getByRole("checkbox", { name: /Enable mcp/i })); + expect(refresh).toHaveBeenCalledWith("ws"); + }); + + it("a REFUSED toggle (state write fails) still calls refresh — the probe is the idempotent catch-up", async () => { + const user = userEvent.setup(); + const refresh = vi.fn(); + const fake = fakeHttp({ + setWorkspaceState: async () => ({ + ok: false, + error: "Plugin is not loaded — load it globally first", + }), + }); + const store = createPluginsStore(fake); + await store.openWorkspace("ws"); + render(WorkspacePluginModal, { + props: { + workspaceId: "ws", + workspaceTitle: "My Workspace", + plugins: store, + layout: fakeLayoutPort( + true, + vi.fn(async () => null), + refresh, + ), + onClose: vi.fn(), + }, + }); + await user.click(screen.getByRole("checkbox", { name: /Enable mcp/i })); + // The attempted state change re-probes either way (a refused write changed + // nothing, so the probe re-syncs the same tree — harmless and consistent). + expect(refresh).toHaveBeenCalledWith("ws"); + }); + it("shows the template hint for the default workspace", async () => { const store = createPluginsStore(fakeHttp()); await store.openWorkspace("default"); |
