From 38db3827870960f466be89afbc49f91238d46144 Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
Date: Thu, 25 Jun 2026 10:55:51 +0900
Subject: feat: workspaces shell + cwd-lsp rename + mcp/settings/system-prompt
 features + app wiring

- workspaces: URL-driven conversation grouping (home listing at /, routing,
  store, http adapter, WorkspaceCard) wired into the App.svelte shell
- rename features/workspace -> features/cwd-lsp (the cwd/lsp status feature)
- new features: mcp (status view), settings (chat-limit field), system-prompt
  (prompt builder), all rendered via the generic surface host
- chat: store + ChatView updates
- tabs: tabs-store updates
- app wiring: ErrorModal (full-screen error surface), app/App.svelte + store.svelte

This commit makes HEAD typecheck clean for the first time: the prior HEAD
(c95cc77) imported features/settings from app/App.svelte but never committed
the feature, so only the full working tree was green.
---
 src/features/workspaces/ui/WorkspacesHome.svelte | 86 ++++++++++++++++++++++++
 1 file changed, 86 insertions(+)
 create mode 100644 src/features/workspaces/ui/WorkspacesHome.svelte

(limited to 'src/features/workspaces/ui/WorkspacesHome.svelte')

diff --git a/src/features/workspaces/ui/WorkspacesHome.svelte b/src/features/workspaces/ui/WorkspacesHome.svelte
new file mode 100644
index 0000000..5894f0a
--- /dev/null
+++ b/src/features/workspaces/ui/WorkspacesHome.svelte
@@ -0,0 +1,86 @@
+<script lang="ts">
+	import { onMount } from "svelte";
+	import type { WorkspaceStore } from "../store.svelte";
+	import { isValidSlug, workspacePath } from "../logic/route";
+	import WorkspaceCard from "./WorkspaceCard.svelte";
+
+	let { store, onNavigate }: { store: WorkspaceStore; onNavigate: (path: string) => void } = $props();
+
+	onMount(() => {
+		void store.refresh();
+	});
+
+	let newSlug = $state("");
+	let slugError = $state<string | null>(null);
+
+	const slugValid = $derived(newSlug.length > 0 && isValidSlug(newSlug));
+
+	function createWorkspace(): void {
+		const slug = newSlug.trim();
+		if (!isValidSlug(slug)) {
+			slugError = "Lowercase letters, digits, and hyphens (1–40 chars).";
+			return;
+		}
+		slugError = null;
+		newSlug = "";
+		onNavigate(workspacePath(slug));
+	}
+</script>
+
+<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>
+	</header>
+
+	<form
+		class="flex items-end gap-2"
+		onsubmit={(e) => {
+			e.preventDefault();
+			createWorkspace();
+		}}
+	>
+		<div class="flex-1">
+			<label for="new-ws" class="mb-1 block text-xs font-semibold uppercase opacity-60">New workspace</label>
+			<input
+				id="new-ws"
+				class="input input-bordered w-full"
+				placeholder="my-workspace"
+				bind:value={newSlug}
+				autocomplete="off"
+				spellcheck="false"
+			/>
+		</div>
+		<button type="submit" class="btn btn-primary btn-sm" disabled={!slugValid}>Create</button>
+	</form>
+	{#if slugError}
+		<p class="text-xs text-error">{slugError}</p>
+	{/if}
+
+	<div class="flex-1 overflow-y-auto">
+		{#if store.loading && store.list.length === 0}
+			<div class="flex h-32 items-center justify-center">
+				<span class="loading loading-spinner loading-sm opacity-60"></span>
+			</div>
+		{:else if store.list.length === 0}
+			<p class="py-8 text-center text-sm opacity-60">
+				No workspaces yet. Create one above or visit <code>/your-name</code> in the URL.
+			</p>
+		{:else}
+			<ul class="flex flex-col gap-2">
+				{#each store.list as ws (ws.id)}
+					<WorkspaceCard {ws} {store} {onNavigate} />
+				{/each}
+			</ul>
+		{/if}
+	</div>
+</div>
-- 
cgit v1.2.3

