summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/workspaces/ui')
-rw-r--r--src/features/workspaces/ui/WorkspaceCard.svelte32
-rw-r--r--src/features/workspaces/ui/WorkspacesHome.svelte50
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}