diff options
| author | Adam Malczewski <[email protected]> | 2026-09-03 18:05:29 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-09-03 18:05:29 +0900 |
| commit | 3a4922a05930b0dc22740bf5621910803e8f275e (patch) | |
| tree | d5f831c3d3838cfff346e378f5370a9c58bc2ed3 /src/features/workspaces | |
| parent | 33c7245f18346553f660434bc3ac589cff3e80c2 (diff) | |
| download | dispatch-web-3a4922a05930b0dc22740bf5621910803e8f275e.tar.gz dispatch-web-3a4922a05930b0dc22740bf5621910803e8f275e.zip | |
feat(plugin-system): plan 07 frontend plugin manager ui
Diffstat (limited to 'src/features/workspaces')
| -rw-r--r-- | src/features/workspaces/ui/WorkspaceCard.svelte | 32 | ||||
| -rw-r--r-- | src/features/workspaces/ui/WorkspacesHome.svelte | 50 |
2 files changed, 72 insertions, 10 deletions
diff --git a/src/features/workspaces/ui/WorkspaceCard.svelte b/src/features/workspaces/ui/WorkspaceCard.svelte index 6de4109..70cfda2 100644 --- a/src/features/workspaces/ui/WorkspaceCard.svelte +++ b/src/features/workspaces/ui/WorkspaceCard.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import type { ComputerEntry, WorkspaceEntry } from "@dispatch/wire"; import { untrack } from "svelte"; + import { WorkspacePluginModal, type PluginsStore } from "../../plugins"; import type { WorkspaceStore } from "../store.svelte"; import { relativeTime } from "../logic/view-model"; import { workspacePath } from "../logic/route"; @@ -12,6 +13,7 @@ onNavigate, computers, hasActive, + plugins, }: { ws: WorkspaceEntry; store: WorkspaceStore; @@ -25,6 +27,12 @@ * `workspaceHasActiveConversations`. Absent → no indicator (e.g. tests). */ hasActive?: (workspaceId: string) => boolean; + /** + * Optional plugin store (plan 07): when present, the card renders a + * "Plugins" button opening THIS workspace's enablement modal. Absent + * (tests) → no button. + */ + plugins?: PluginsStore; } = $props(); // Whether at least one conversation in this workspace is currently active @@ -118,6 +126,11 @@ } } + // ── Per-workspace plugin modal (plan 07 §7.3) ───────────────────────────── + // Enable/disable what THIS workspace uses. Unloaded plugins are greyed out + // + unselectable there — only the global manager (page header) loads code. + let pluginsOpen = $state(false); + // ── Delete ───────────────────────────────────────────────────────────────── let deleting = $state(false); @@ -263,6 +276,16 @@ > Open </a> + {#if plugins} + <button + type="button" + class="btn btn-outline btn-sm ml-2" + onclick={() => (pluginsOpen = true)} + title="Choose which plugins this workspace uses" + > + Plugins + </button> + {/if} </div> {#if cwdError} @@ -277,3 +300,12 @@ <p class="text-xs opacity-50">No default computer — conversations run locally (no SSH).</p> {/if} </li> + +{#if pluginsOpen && plugins} + <WorkspacePluginModal + workspaceId={ws.id} + workspaceTitle={ws.title} + {plugins} + onClose={() => (pluginsOpen = false)} + /> +{/if} diff --git a/src/features/workspaces/ui/WorkspacesHome.svelte b/src/features/workspaces/ui/WorkspacesHome.svelte index d97eab7..548d733 100644 --- a/src/features/workspaces/ui/WorkspacesHome.svelte +++ b/src/features/workspaces/ui/WorkspacesHome.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { onMount } from "svelte"; import type { ComputerEntry } from "@dispatch/wire"; + import { PluginManager, type PluginsStore } from "../../plugins"; import type { WorkspaceStore } from "../store.svelte"; import { isValidSlug, workspacePath } from "../logic/route"; import WorkspaceCard from "./WorkspaceCard.svelte"; @@ -10,6 +11,7 @@ onNavigate, computers, hasActive, + plugins, }: { store: WorkspaceStore; onNavigate: (path: string) => void; @@ -20,6 +22,13 @@ * Wired by the composition root to the app store. Absent → no indicator. */ hasActive?: (workspaceId: string) => boolean; + /** + * The plugin manager store (plan 07): drives the page-header global + * "Plugins" button + the per-card workspace modals (forwarded to + * {@link WorkspaceCard}). Optional (absent in tests that don't exercise + * plugins) → the buttons don't render. + */ + plugins?: PluginsStore; } = $props(); onMount(() => { @@ -31,6 +40,10 @@ const slugValid = $derived(newSlug.length > 0 && isValidSlug(newSlug)); + // Global plugin manager modal (plan 07 §7.2): opened from the header + // button; the ONLY surface that loads/unloads code (strict layering). + let managerOpen = $state(false); + function createWorkspace(): void { const slug = newSlug.trim(); if (!isValidSlug(slug)) { @@ -46,16 +59,28 @@ <div class="mx-auto flex h-screen w-full max-w-3xl flex-col gap-4 p-6"> <header class="flex items-center justify-between"> <h1 class="text-2xl font-bold">Workspaces</h1> - <a - href="/default" - class="btn btn-ghost btn-sm" - onclick={(e) => { - e.preventDefault(); - onNavigate("/default"); - }} - > - Default - </a> + <div class="flex items-center gap-2"> + <a + href="/default" + class="btn btn-ghost btn-sm" + onclick={(e) => { + e.preventDefault(); + onNavigate("/default"); + }} + > + Default + </a> + {#if plugins} + <button + type="button" + class="btn btn-outline btn-sm" + onclick={() => (managerOpen = true)} + title="Open the plugin manager (load/unload plugins globally)" + > + Plugins + </button> + {/if} + </div> </header> <form @@ -102,9 +127,14 @@ {onNavigate} {computers} {...(hasActive ? { hasActive } : {})} + {...(plugins ? { plugins } : {})} /> {/each} </ul> {/if} </div> </div> + +{#if managerOpen && plugins} + <PluginManager {plugins} onClose={() => (managerOpen = false)} /> +{/if} |
