summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/adapter/http.ts
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/workspaces/adapter/http.ts
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/workspaces/adapter/http.ts')
-rw-r--r--src/features/workspaces/adapter/http.ts134
1 files changed, 134 insertions, 0 deletions
diff --git a/src/features/workspaces/adapter/http.ts b/src/features/workspaces/adapter/http.ts
new file mode 100644
index 0000000..97fc2e2
--- /dev/null
+++ b/src/features/workspaces/adapter/http.ts
@@ -0,0 +1,134 @@
+import type {
+ DeleteWorkspaceResponse,
+ EnsureWorkspaceRequest,
+ SetWorkspaceDefaultCwdRequest,
+ SetWorkspaceTitleRequest,
+ Workspace,
+ WorkspaceEntry,
+ WorkspaceListResponse,
+ WorkspaceResponse,
+} from "@dispatch/transport-contract";
+
+/**
+ * Workspace HTTP effects — the injected edge that talks to the backend's
+ * workspace endpoints. Mirrors the store's fetch pattern: `httpBase` + an
+ * injected `fetchImpl` (so it is testable without the network). Returns typed
+ * `WorkspaceResult<T>` (`{ok,value}` | `{ok:false,error}`) for mutating ops so a
+ * caller can surface the backend's `{ error }` reason; reads return data or a
+ * safe empty/null on failure (non-fatal — the UI falls back gracefully).
+ *
+ * Endpoints ([email protected]):
+ * - `GET /workspaces` → list
+ * - `PUT /workspaces/:id` (create-on-miss, idempotent) → ensure
+ * - `GET /workspaces/:id` (404 → null) → get
+ * - `PUT /workspaces/:id/title` → rename
+ * - `PUT /workspaces/:id/default-cwd` → set/clear default cwd
+ * - `DELETE /workspaces/:id` (409 for "default") → delete
+ */
+export type WorkspaceResult<T> =
+ | { readonly ok: true; readonly value: T }
+ | { readonly ok: false; readonly error: string };
+
+export interface WorkspaceHttp {
+ list(): Promise<readonly WorkspaceEntry[]>;
+ ensure(id: string, body?: EnsureWorkspaceRequest): Promise<WorkspaceResult<Workspace>>;
+ get(id: string): Promise<Workspace | null>;
+ setTitle(id: string, title: string): Promise<WorkspaceResult<Workspace>>;
+ setDefaultCwd(id: string, defaultCwd: string | null): Promise<WorkspaceResult<Workspace>>;
+ delete(id: string): Promise<WorkspaceResult<{ closedCount: number }>>;
+}
+
+async function errText(res: Response): Promise<string> {
+ try {
+ const body = (await res.json()) as { error?: string };
+ return body.error ?? `HTTP ${res.status}`;
+ } catch {
+ return `HTTP ${res.status}`;
+ }
+}
+
+export function createWorkspaceHttp(httpBase: string, fetchImpl: typeof fetch): WorkspaceHttp {
+ return {
+ async list(): Promise<readonly WorkspaceEntry[]> {
+ try {
+ const res = await fetchImpl(`${httpBase}/workspaces`);
+ if (!res.ok) return [];
+ const data = (await res.json()) as WorkspaceListResponse;
+ return data.workspaces;
+ } catch {
+ return [];
+ }
+ },
+
+ async ensure(id, body): Promise<WorkspaceResult<Workspace>> {
+ try {
+ const res = await fetchImpl(`${httpBase}/workspaces/${encodeURIComponent(id)}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body ?? {}),
+ });
+ if (!res.ok) return { ok: false, error: await errText(res) };
+ return { ok: true, value: (await res.json()) as WorkspaceResponse };
+ } catch (err) {
+ return {
+ ok: false,
+ error: err instanceof Error ? err.message : "Workspace request failed",
+ };
+ }
+ },
+
+ async get(id): Promise<Workspace | null> {
+ try {
+ const res = await fetchImpl(`${httpBase}/workspaces/${encodeURIComponent(id)}`);
+ if (res.status === 404 || !res.ok) return null;
+ return (await res.json()) as WorkspaceResponse;
+ } catch {
+ return null;
+ }
+ },
+
+ async setTitle(id, title): Promise<WorkspaceResult<Workspace>> {
+ try {
+ const res = await fetchImpl(`${httpBase}/workspaces/${encodeURIComponent(id)}/title`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ title } satisfies SetWorkspaceTitleRequest),
+ });
+ if (!res.ok) return { ok: false, error: await errText(res) };
+ return { ok: true, value: (await res.json()) as WorkspaceResponse };
+ } catch (err) {
+ return { ok: false, error: err instanceof Error ? err.message : "Rename failed" };
+ }
+ },
+
+ async setDefaultCwd(id, defaultCwd): Promise<WorkspaceResult<Workspace>> {
+ try {
+ const res = await fetchImpl(
+ `${httpBase}/workspaces/${encodeURIComponent(id)}/default-cwd`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ defaultCwd } satisfies SetWorkspaceDefaultCwdRequest),
+ },
+ );
+ if (!res.ok) return { ok: false, error: await errText(res) };
+ return { ok: true, value: (await res.json()) as WorkspaceResponse };
+ } catch (err) {
+ return { ok: false, error: err instanceof Error ? err.message : "Set default cwd failed" };
+ }
+ },
+
+ async delete(id): Promise<WorkspaceResult<{ closedCount: number }>> {
+ try {
+ const res = await fetchImpl(`${httpBase}/workspaces/${encodeURIComponent(id)}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) return { ok: false, error: await errText(res) };
+ const data = (await res.json()) as DeleteWorkspaceResponse;
+ return { ok: true, value: { closedCount: data.closedCount } };
+ } catch (err) {
+ return { ok: false, error: err instanceof Error ? err.message : "Delete failed" };
+ }
+ },
+ };
+}