summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspace/ui
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
committerAdam Malczewski <[email protected]>2026-06-25 10:55:51 +0900
commit38db3827870960f466be89afbc49f91238d46144 (patch)
tree24cb1b896dfadc31e72552dbe67f00530881242e /src/features/workspace/ui
parent17ce47987e673b6618454d033885b17b2a01912e (diff)
downloaddispatch-web-38db3827870960f466be89afbc49f91238d46144.tar.gz
dispatch-web-38db3827870960f466be89afbc49f91238d46144.zip
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.
Diffstat (limited to 'src/features/workspace/ui')
-rw-r--r--src/features/workspace/ui/CwdField.svelte96
-rw-r--r--src/features/workspace/ui/LspStatusView.svelte127
2 files changed, 0 insertions, 223 deletions
diff --git a/src/features/workspace/ui/CwdField.svelte b/src/features/workspace/ui/CwdField.svelte
deleted file mode 100644
index bd8b870..0000000
--- a/src/features/workspace/ui/CwdField.svelte
+++ /dev/null
@@ -1,96 +0,0 @@
-<script lang="ts">
- import { untrack } from "svelte";
- import { cwdChanged, normalizeCwd, type SaveCwd } from "../logic/view-model";
-
- let {
- cwd,
- canEdit,
- save,
- }: {
- /** The active conversation's persisted cwd, or null when unset. */
- cwd: string | null;
- /** Whether a real conversation is focused (a draft can't persist a cwd yet). */
- canEdit: boolean;
- save: SaveCwd;
- } = $props();
-
- // Start empty; the $effect below seeds from the (async-loaded) cwd prop. (Reading
- // the prop directly into initial $state would only capture its first value.)
- let value = $state("");
- let lastSeed = $state("");
- let saving = $state(false);
- let error = $state<string | null>(null);
- let justSaved = $state(false);
-
- // Seed the input from the persisted cwd (it loads async). Only reseed while the
- // field is untouched, so an in-flight load can't clobber what the user typed.
- // Re-mounted per conversation, so there is no cross-tab bleed.
- $effect(() => {
- const incoming = cwd ?? "";
- untrack(() => {
- if (value === lastSeed) value = incoming;
- lastSeed = incoming;
- });
- });
-
- const dirty = $derived(cwdChanged(value, cwd));
-
- async function handleSave() {
- if (saving || !canEdit || !dirty) return;
- saving = true;
- error = null;
- justSaved = false;
- const result = await save(normalizeCwd(value));
- saving = false;
- if (result === null) return;
- if (result.ok) {
- justSaved = true;
- } else {
- error = result.error;
- }
- }
-
- function onInput() {
- justSaved = false;
- error = null;
- }
-</script>
-
-<div class="flex flex-col gap-1">
- <span class="text-xs font-semibold uppercase opacity-60">Working directory</span>
- <div class="flex items-center gap-2">
- <input
- type="text"
- class="input input-bordered input-sm w-full font-mono text-xs"
- placeholder={canEdit ? "/abs/path/to/project" : "Open a conversation first"}
- bind:value
- disabled={!canEdit || saving}
- oninput={onInput}
- onkeydown={(e) => {
- if (e.key === "Enter") handleSave();
- }}
- aria-label="Working directory"
- />
- <button
- type="button"
- class="btn btn-primary btn-sm"
- disabled={!canEdit || saving || !dirty}
- onclick={handleSave}
- >
- {#if saving}
- <span class="loading loading-spinner loading-xs"></span>
- {:else}
- Set
- {/if}
- </button>
- </div>
- {#if !canEdit}
- <p class="text-xs opacity-60">Start or open a conversation to set its working directory.</p>
- {:else if error}
- <p class="text-xs text-error">{error}</p>
- {:else if justSaved && !dirty}
- <p class="text-xs text-success">Saved.</p>
- {:else}
- <p class="text-xs opacity-50">Defaults each turn's cwd; drives the language servers below.</p>
- {/if}
-</div>
diff --git a/src/features/workspace/ui/LspStatusView.svelte b/src/features/workspace/ui/LspStatusView.svelte
deleted file mode 100644
index 77603a1..0000000
--- a/src/features/workspace/ui/LspStatusView.svelte
+++ /dev/null
@@ -1,127 +0,0 @@
-<script lang="ts">
- import { untrack } from "svelte";
- import {
- type Badge,
- type LoadLspStatus,
- type LspServerView,
- summarizeServers,
- viewLspServers,
- } from "../logic/view-model";
-
- let {
- cwd,
- canView,
- load,
- }: {
- /** The active conversation's cwd — the trigger to (re)load when it changes. */
- cwd: string | null;
- /** Whether a real conversation is focused. */
- canView: boolean;
- load: LoadLspStatus;
- } = $props();
-
- const badgeClass: Record<Badge, string> = {
- success: "badge-success",
- warning: "badge-warning",
- error: "badge-error",
- neutral: "badge-ghost",
- };
-
- let servers = $state<readonly LspServerView[]>([]);
- let loading = $state(false);
- let error = $state<string | null>(null);
- let loadedCwd = $state<string | null>(null);
- let hasLoaded = $state(false);
- let summary = $state("");
-
- async function refresh() {
- if (!canView) return;
- loading = true;
- error = null;
- const result = await load();
- loading = false;
- if (result === null) return;
- hasLoaded = true;
- if (result.ok) {
- servers = viewLspServers(result.servers);
- summary = summarizeServers(result.servers);
- loadedCwd = result.cwd;
- } else {
- error = result.error;
- }
- }
-
- // (Re)load on mount and whenever the conversation's cwd changes. The LSP GET
- // lazily spawns servers, so we avoid a redundant fetch when `cwd` resolves to
- // the value we already loaded for.
- $effect(() => {
- const target = cwd;
- const can = canView;
- untrack(() => {
- if (!can) return;
- if (!hasLoaded || target !== loadedCwd) void refresh();
- });
- });
-</script>
-
-<div class="flex flex-col gap-2">
- <div class="flex items-center justify-between gap-2">
- <span class="text-xs opacity-70">
- {#if loading}
- Resolving…
- {:else if hasLoaded && loadedCwd !== null}
- {summary}
- {:else}
- Language servers
- {/if}
- </span>
- <button
- type="button"
- class="btn btn-ghost btn-xs"
- disabled={!canView || loading}
- onclick={() => refresh()}
- aria-label="Refresh language server status"
- >
- {#if loading}
- <span class="loading loading-spinner loading-xs"></span>
- {:else}
- Refresh
- {/if}
- </button>
- </div>
-
- {#if !canView}
- <p class="text-xs opacity-60">Open or start a conversation to see its language servers.</p>
- {:else if error}
- <p class="text-xs text-error">{error}</p>
- {:else if hasLoaded && loadedCwd === null}
- <p class="text-xs opacity-60">
- Set a working directory in the Model panel to enable language servers.
- </p>
- {:else if hasLoaded && servers.length === 0 && !loading}
- <p class="text-xs opacity-60">No language servers configured for this directory.</p>
- {:else}
- <ul class="flex flex-col gap-2">
- {#each servers as server (server.id)}
- <li class="flex flex-col gap-1 rounded-box bg-base-200 p-2 text-sm">
- <div class="flex items-center justify-between gap-2">
- <span class="font-medium">{server.name}</span>
- <span class="badge badge-sm {badgeClass[server.badge]} gap-1">
- {#if server.busy}
- <span class="loading loading-spinner loading-xs"></span>
- {/if}
- {server.statusLabel}
- </span>
- </div>
- {#if server.extensionsLabel}
- <span class="font-mono text-xs opacity-60">{server.extensionsLabel}</span>
- {/if}
- <span class="truncate font-mono text-xs opacity-50" title={server.root}>{server.root}</span>
- {#if server.error}
- <span class="font-mono text-xs text-error">{server.error}</span>
- {/if}
- </li>
- {/each}
- </ul>
- {/if}
-</div>