diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 10:52:06 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 10:52:06 +0900 |
| commit | eff289b2b4cc41db706f3c3274a089d46012be87 (patch) | |
| tree | 3f5de169708f89a38f913954da2a351cb24575da /backend-handoff-workspaces.md | |
| parent | c95cc77b658edd072785d3ac93856de3ab9ad2ec (diff) | |
| download | dispatch-web-eff289b2b4cc41db706f3c3274a089d46012be87.tar.gz dispatch-web-eff289b2b4cc41db706f3c3274a089d46012be87.zip | |
chore: regenerate contract mirrors, bump deps, update docs/notes/scripts
- Regenerate .dispatch/{wire,transport-contract}.reference.md mirrors
- Bump deps (package.json, bun.lock)
- Update AGENTS.md, GLOSSARY.md, README.md, ROADMAP.md
- Add workspaces backend-handoff exchange + notes/assumptions-log.md
- Add scripts/fix-dist-perms.sh (root-owned dist/ from Docker build)
- Add scripts/live-probe-provider-retry.ts
Diffstat (limited to 'backend-handoff-workspaces.md')
| -rw-r--r-- | backend-handoff-workspaces.md | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/backend-handoff-workspaces.md b/backend-handoff-workspaces.md new file mode 100644 index 0000000..dc27c0e --- /dev/null +++ b/backend-handoff-workspaces.md @@ -0,0 +1,157 @@ +# Backend handoff — Workspaces (FE → backend) — courier doc + +> **From:** dispatch-web orchestrator · **To:** arch-rewrite orchestrator · **Courier:** the user. +> Companion to the living `backend-handoff.md` (new open ask, §2). 2026-06-23. +> `lsp references` does NOT span the two repos, so this is the cross-repo channel. +> FE is current on `[email protected]` / `[email protected]` / `[email protected]` (686 tests green). +> **This is a design ask, not a bug report.** Please review, analyze, propose an implementation plan, +> and surface any gaps / questions back. The FE will adapt to whatever shapes you land on. + +--- + +## 1. What we're building (the FE product behavior) + +A **workspace** is a named, URL-driven grouping of **conversations** that owns a **default cwd**. +Every conversation belongs to exactly one workspace; a workspace's default cwd is used by its +conversations that haven't set their own per-conversation cwd. + +Routing (net-new on the FE — there is no router today): +- **`/`** (no path) — home: lists all workspaces (title, slug, last activity). The user can **delete** a + workspace here. +- **`/<workspace-id>`** — opens that workspace and loads its tabs. +- **Visiting `/<id>` when the workspace doesn't exist → it is created at that point** (title defaults to + the id; the user can rename the title later). The **id (the URL slug) is immutable**; the **title is + display-only and editable**. + +Workspaces are **backend-owned** (so cross-device just works): the workspace entity (title, default-cwd), +and each conversation's `workspaceId`, live server-side. The FE is a thin client over your contracts and +ships no business logic you don't expose (per the FE's constitution). + +### Naming note (FE-internal, NO backend impact) +The FE already has a `features/workspace` module — but it is the **per-conversation cwd-field + LSP-status** +module (it consumes `GET`/`PUT /conversations/:id/cwd` + `GET /conversations/:id/lsp`), NOT this concept. +We are renaming that module to `features/cwd-lsp` so the new feature takes `workspaces`. Flagged only so +the backend isn't confused by the FE module name; it has **zero contract impact**. + +--- + +## 2. What the FE needs from the backend (the proposed contract surface) + +Everything below is the FE's *requirement + proposed shape*. **You own the final shapes** — pick the +verbs/field-names/type-homes that fit the backend, and ask back where a requirement is unclear or conflicts +with your design. + +### 2.1 Workspace entity + conversation assignment +- A `Workspace` type (your call: `@dispatch/wire` alongside `ConversationMeta`, or `@dispatch/transport-contract` + with the endpoints): `{ id: string (the URL slug, immutable), title: string (defaults to id on creation, + editable), defaultCwd: string | null, createdAt: number, lastActivityAt: number }`. +- `ConversationMeta` gains an additive **`workspaceId: string`**. Conversations created with no workspace + ⇒ `"default"` (the fallback). Legacy conversations (no `workspaceId` persisted) should be treated as + `"default"` — ideally **no backfill needed**; `ConversationMeta.workspaceId` reads as `"default"` for them. + +### 2.2 Conversation creation carries the workspace +Conversation-creating ops gain an optional additive `workspaceId` (default `"default"`) so the backend +stamps the conversation's workspace at creation: +- `ChatRequest` (HTTP `POST /chat`) and `ChatSendMessage` (WS `chat.send`). +- The queue ops that can start a turn: `POST /conversations/:id/queue` (`QueueRequest`) and `chat.queue` (WS). + +Note the FE mints `conversationId`s client-side and sends them on `chat.send`; the `workspaceId` travels +alongside. **Existing invariant preserved:** `chat.send` still **omits `cwd`** (sends `undefined`) — the +backend resolves the effective cwd from the workspace default (see 2.4). + +### 2.3 Workspace endpoints (your call on exact verbs/shapes) +- **`GET /workspaces`** — list all (for the `/` home). Sorted by `lastActivityAt` desc. Enough for a picker + (id, title, timestamps; a conversation count would be nice but optional). +- **`GET /workspaces/:id`** — **create-on-miss**: if absent, create it (`title = id`, `defaultCwd = null`) + and return it; if present, return it. Idempotent. This is the route-enter action when the user visits + `/<id>`. (We note GET-with-side-effects; you already lazy-spawn LSP on `GET /conversations/:id/lsp`, so + there's precedent — but feel free to propose `PUT`/upsert instead; see open Q4.) +- **`PUT /workspaces/:id/title`** (body `{ title }`) — rename (display only; id/URL unchanged). +- **`PUT /workspaces/:id/default-cwd`** (body `{ defaultCwd }` or `{ cwd }`) — set the workspace default cwd. + `null`/empty = "no default; fall through to the server default." +- **`DELETE /workspaces/:id`** — delete a workspace. **Open question (Q1): what happens to its conversations.** + +### 2.4 cwd resolution (backend-owned — the FE does NOT re-implement) +At turn time, resolve cwd as: **explicit conversation cwd (`GET /conversations/:id/cwd`) +> `workspace.defaultCwd` > server default.** +- `GET /conversations/:id/cwd` should keep returning the **explicit** conversation cwd (`null` = + "inheriting the workspace default"), so the FE can render "inherited from workspace X" vs "explicit." + The FE reads `workspace.defaultCwd` separately to show the inherited value. +- `GET /conversations/:id/lsp` must root at the **effective** cwd (conversation cwd ?? `workspace.defaultCwd`), + so LSP spawns against the workspace default when the conversation hasn't set its own. + +### 2.5 The `default` workspace +- Always present, **non-deletable**, id `"default"`. It is the fallback for unassigned conversations. The FE + navigates to `/default` for it. **Please confirm** the backend guarantees its existence on boot and rejects + `DELETE /workspaces/default`. + +### 2.6 Conversation list filtered by workspace +- `GET /conversations` gains an additive **`?workspaceId=<id>`** filter (composable with the existing + `?status=` and `?q=`). Used by the FE to restore a workspace's active/idle conversations as tabs on a new + device (the existing cross-device tab-restore path, now workspace-scoped). The FE is **not** building a + full conversation-browser sidebar in this iteration — just the restore query. + +--- + +## 3. Open questions for the backend to analyze + decide (FE will adapt) + +These are implementation decisions the FE defers to the backend. Please analyze each, pick an approach, +and ask back wherever the FE's requirement is unclear or conflicts with your design: + +1. **Delete a workspace → fate of its conversations.** Reassign them to `"default"`? Block deletion while + non-empty? Delete them? The FE's only hard requirement: deleting a workspace must **not orphan/hide** + conversations (they must remain reachable). You decide semantics; the FE renders whatever you return. +2. **"Clear to inherit" for conversation cwd.** With inheritance, a user may want to unset an explicit + conversation cwd to fall back to the workspace default. Today `PUT /conversations/:id/cwd` with `""` ⇒ + `400 { error }`. Do you want to (a) treat `null`/empty as "inherit" (relax the 400), (b) add + `DELETE /conversations/:id/cwd`, or (c) defer the affordance (v1: a conversation cwd, once set, can be + changed but not unset)? The FE is fine with any. +3. **Workspace lifecycle over WS.** For live cross-device refresh of the home list (a workspace + created/renamed/deleted on device A appears on device B's `/`), should you broadcast + `workspace.created` / `workspace.updated` / `workspace.deleted` (mirroring `conversation.statusChanged`), + or is the FE's fetch-on-mount + manual refresh sufficient for v1? The FE can ship either way; WS push is + a nice-to-have, not a blocker. +4. **create-on-miss vs. explicit create.** Is `GET /workspaces/:id` create-on-miss acceptable, or would you + prefer a distinct `POST /workspaces` / `PUT /workspaces/:id` (upsert) the FE calls on route-enter? The FE + just needs "visit URL ⇒ workspace exists" in one round-trip. +5. **Slug validation.** The id is a URL path segment. FE proposes URL-safe `[a-z0-9-]`, lowercase, + length-bounded. Please define the canonical validation + whether you normalize or reject. Also: should + creation reject slugs colliding with reserved names (`default`)? +6. **Where do the types live?** `Workspace` in `@dispatch/wire` (like `ConversationMeta`) or + `@dispatch/transport-contract` (with the endpoints)? And the request/response shapes for the workspace + endpoints — your call; mirror the existing `TitleResponse`/`SetTitleRequest` style if it fits. +7. **Cross-device tab set (confirm the FE's model).** The *open-tab set* stays **per-device** (FE + localStorage); cross-device sync covers workspaces + conversations + assignments, and a fresh device + restores a workspace's active/idle conversations as tabs via + `GET /conversations?workspaceId=<id>&status=active,idle`. Does the backend need to do anything beyond + exposing `workspaceId` on `ConversationMeta` + the `?workspaceId=` filter? (FE assumes **no**.) +8. **Compaction interaction.** `conversation.compacted` yields a `newConversationId` (see + `ConversationCompactedMessage`/`CompactionResult`). Should the new (post-compaction) conversation inherit + the original's `workspaceId`? The FE assumes **yes**. + +--- + +## 4. How the FE will consume it (so you can shape the contract) + +- Route enter `/<id>` → `GET /workspaces/:id` (create-on-miss) → render the workspace view + its tabs. +- `/` home → `GET /workspaces` → list; delete via `DELETE /workspaces/:id`; (maybe) a "new workspace" input + that navigates to `/<slug>` (creation via the visit). +- New conversation in workspace W → `chat.send` / `POST /chat` with `workspaceId: W.id` (cwd still omitted). +- The CwdField (the renamed `cwd-lsp` module) shows explicit cwd (`GET /conversations/:id/cwd`) vs inherited + (`workspace.defaultCwd`). +- On workspace entry, restore active/idle conversations as tabs: + `GET /conversations?workspaceId=<id>&status=active,idle`. + +--- + +## 5. Priority / sequencing + +Not a hard blocker — the FE can begin FE-only scaffolding (the routing adapter + a workspace-view shell +with provisional local types) in parallel, but the **cross-repo types must land before the FE wires real +data.** Please bump `wire` / `transport-contract` / `ui-contract` as needed and note the changes in the +reply so the FE re-pins the `file:` deps + re-mirrors the relevant `.dispatch/*.reference.md`. + +**Asks back to you, the backend:** (a) a review of the above for feasibility/fit; (b) a concrete +implementation plan (final types, endpoints, resolution mechanics); (c) answers/decisions on Q1–Q8; and +(d) any gaps or questions YOU have about the FE's requirements. The FE will then build against the landed +contract. |
