diff options
Diffstat (limited to 'frontend-workspaces-handoff.md')
| -rw-r--r-- | frontend-workspaces-handoff.md | 216 |
1 files changed, 0 insertions, 216 deletions
diff --git a/frontend-workspaces-handoff.md b/frontend-workspaces-handoff.md deleted file mode 100644 index 1fed1bf..0000000 --- a/frontend-workspaces-handoff.md +++ /dev/null @@ -1,216 +0,0 @@ -# Backend handoff — Workspaces (backend → FE) — courier doc - -> **From:** arch-rewrite orchestrator · **To:** frontend orchestrator · **Courier:** the user. -> Response to `backend-handoff-workspaces.md`. This doc finalizes the contract shapes -> the backend will implement. The FE should re-pin `@dispatch/wire` and -> `@dispatch/transport-contract` `file:` deps and re-mirror any `.dispatch/*.reference.md`. - -## Version bumps - -| Package | From | To | Notes | -|---|---|---|---| -| `@dispatch/wire` | `0.11.0` | `0.12.0` | Additive: `Workspace`, `WorkspaceEntry`, `ConversationMeta.workspaceId` | -| `@dispatch/transport-contract` | `0.15.0` | `0.16.0` | Additive: workspace endpoints + `workspaceId` on chat/queue ops | -| `@dispatch/ui-contract` | `0.2.0` | `0.2.0` | **Unchanged** | - ---- - -## 1. Final types — `@dispatch/[email protected]` - -```ts -/** - * A named, URL-driven grouping of conversations that owns a default cwd. - * Every conversation belongs to exactly one workspace; conversations that - * haven't set their own per-conversation cwd inherit `defaultCwd`. - */ -export interface Workspace { - /** The URL slug (immutable). Lowercase `[a-z0-9-]`, 1–40 chars. */ - readonly id: string; - /** Display title (editable). Defaults to `id` on creation. */ - readonly title: string; - /** The workspace's default cwd, or `null` (fall through to server default). */ - readonly defaultCwd: string | null; - /** Epoch-ms when the workspace was first created. */ - readonly createdAt: number; - /** Epoch-ms of the most recent conversation activity in this workspace. */ - readonly lastActivityAt: number; -} - -/** - * A workspace entry in the list response — a `Workspace` plus a conversation count. - */ -export interface WorkspaceEntry extends Workspace { - /** Number of conversations assigned to this workspace. */ - readonly conversationCount: number; -} -``` - -`ConversationMeta` gains a required `workspaceId`: - -```ts -export interface ConversationMeta { - readonly id: string; - readonly createdAt: number; - readonly lastActivityAt: number; - readonly title: string; - readonly status: ConversationStatus; - /** Always present; "default" for legacy/unspecified conversations. */ - readonly workspaceId: string; - readonly compactedFrom?: string; -} -``` - ---- - -## 2. Final types — `@dispatch/[email protected]` - -### Additive fields on existing request types - -```ts -export interface ChatRequest { - readonly conversationId?: string; - readonly message: string; - readonly model?: string; - readonly cwd?: string; - readonly reasoningEffort?: ReasoningEffort; - /** Workspace to assign the conversation to. Default "default". Auto-creates if missing. */ - readonly workspaceId?: string; -} - -export interface QueueRequest { - readonly text: string; - /** Default "default". Auto-creates if missing. */ - readonly workspaceId?: string; -} - -export interface ChatQueueMessage { - readonly type: "chat.queue"; - readonly conversationId: string; - readonly text: string; - /** Default "default". Auto-creates if missing. */ - readonly workspaceId?: string; -} -``` - -### Workspace endpoint types - -```ts -/** Body of `PUT /workspaces/:id` (all fields optional — the ensure/create call). */ -export interface EnsureWorkspaceRequest { - /** Display title. Default: the workspace id. Only used on create; ignored if workspace exists. */ - readonly title?: string; - /** Default cwd. Default: null (inherit server default). Only used on create. */ - readonly defaultCwd?: string | null; -} - -/** Response of GET/PUT /workspaces/:id — the workspace itself. */ -export interface WorkspaceResponse extends Workspace {} - -/** Response of `GET /workspaces` — all workspaces sorted by lastActivityAt desc. */ -export interface WorkspaceListResponse { - readonly workspaces: readonly WorkspaceEntry[]; -} - -/** Body of `PUT /workspaces/:id/title`. */ -export interface SetWorkspaceTitleRequest { - readonly title: string; -} - -/** Body of `PUT /workspaces/:id/default-cwd`. null/absent = clear to server default. */ -export interface SetWorkspaceDefaultCwdRequest { - readonly defaultCwd: string | null; -} - -/** Response of `DELETE /workspaces/:id`. */ -export interface DeleteWorkspaceResponse { - readonly workspaceId: string; - /** Conversations that were closed (status → "closed") by this delete. */ - readonly closedCount: number; -} -``` - ---- - -## 3. Final endpoint list - -| Method & Path | Body | Returns | Notes | -|---|---|---|---| -| `GET /workspaces` | — | `WorkspaceListResponse` | Sorted by `lastActivityAt` desc. Includes `conversationCount`. | -| `PUT /workspaces/:id` | `EnsureWorkspaceRequest?` | `WorkspaceResponse` | **Create-on-miss** (idempotent). Creates with `title=id`, `defaultCwd=null` if missing. Returns existing as-is if present. Slug validated. | -| `GET /workspaces/:id` | — | `WorkspaceResponse` | Pure read. 404 if missing. | -| `PUT /workspaces/:id/title` | `SetWorkspaceTitleRequest` | `WorkspaceResponse` | Rename (display only; id unchanged). | -| `PUT /workspaces/:id/default-cwd` | `SetWorkspaceDefaultCwdRequest` | `WorkspaceResponse` | Set/clear workspace default cwd. | -| `DELETE /workspaces/:id` | — | `DeleteWorkspaceResponse` | **Closes all conversations** (status → "closed"), reassigns them to "default", then deletes the workspace. 409 for `"default"`. | -| `GET /conversations` | `?workspaceId=`, `?status=`, `?q=` | `ConversationListResponse` | Additive `?workspaceId=` filter, composable with existing filters. | -| `DELETE /conversations/:id/cwd` | — | `CwdResponse` | Clears explicit conversation cwd (returns `cwd: null`). | - -### Existing endpoints (semantic note, no type change) - -- `GET /conversations/:id/cwd` — unchanged: returns the **explicit** conversation cwd (`null` = inheriting workspace default). -- `GET /conversations/:id/lsp` — now roots LSP at the **effective** cwd; `LspStatusResponse.cwd` returns the effective cwd. - ---- - -## 4. cwd resolution (backend-owned) - -``` -effectiveCwd = conversationStore.getCwd(conversationId) // explicit per-conversation -if (effectiveCwd == null) { - workspaceId = conversationStore.getWorkspaceId(conversationId) // "default" fallback - workspace = conversationStore.getWorkspace(workspaceId) - effectiveCwd = workspace?.defaultCwd ?? null -} -if (effectiveCwd == null) effectiveCwd = serverDefaultCwd // process.cwd() today -``` - -- `GET /conversations/:id/cwd` → explicit cwd only (`null` = inherit). -- `GET /conversations/:id/lsp` → effective cwd. -- Turn start (`runTurn` / `warm`) → effective cwd. - ---- - -## 5. `DELETE /workspaces/:id` semantics - -1. Close all conversations in that workspace (set `status = "closed"`). -2. Reassign their `workspaceId` to `"default"` (so no dangling reference). -3. Delete the workspace entity. -4. Return `{ workspaceId, closedCount }`. -5. `DELETE /workspaces/default` → HTTP 409. - -Closed conversations are hidden from tab-restore (`?status=active,idle` excludes `closed`). - ---- - -## 6. Workspace lifecycle / auto-creation - -- **Auto-create on turn start:** if `workspaceId` is provided and doesn't exist, the backend auto-creates it (`title = id`, `defaultCwd = null`). -- **`PUT /workspaces/:id` create-on-miss:** if absent, creates with optional `title`/`defaultCwd` from the body (defaults: `title = id`, `defaultCwd = null`). If present, returns existing as-is. -- **Slug validation:** `^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$` (1–40 chars, lowercase, digits, internal hyphens only). Reject invalid with 400. No normalization. `"default"` allowed but non-deletable. -- **`"default"` workspace:** always synthesized if not persisted; guaranteed in `GET /workspaces` list. -- **`lastActivityAt`:** updates when a conversation in the workspace appends, or on first creation. Does NOT update on title/default-cwd changes. -- **Compaction:** post-compaction conversations inherit the original's `workspaceId`. - ---- - -## 7. Answers to FE open questions (Q1–Q8) - -| # | Decision | -|---|---| -| Q1 | **Close all conversations** in the workspace (status → "closed"), reassign to "default", then delete the workspace. Return `closedCount`. | -| Q2 | **Add `DELETE /conversations/:id/cwd`** to clear explicit cwd (fall back to workspace default). `PUT` validation unchanged (empty string still 400). | -| Q3 | **Deferred to v1** — no WS lifecycle push. Fetch-on-mount + manual refresh sufficient. Can add `workspace.created/updated/deleted` later, additively. | -| Q4 | **`PUT /workspaces/:id`** is the create-on-miss entry point (idempotent, 200). `GET /workspaces/:id` is a pure read (404 if missing). | -| Q5 | Slug regex `^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$`. Reject, don't normalize. `"default"` non-deletable. | -| Q6 | `Workspace` in `@dispatch/wire`. Request/response bodies in `@dispatch/transport-contract`. | -| Q7 | Confirmed — backend does nothing beyond `workspaceId` on `ConversationMeta` + `?workspaceId=` filter. | -| Q8 | Yes — post-compaction conversations inherit `workspaceId`. `forkHistory` copies it. | - ---- - -## 8. Gaps resolved (from FE handoff §3) - -1. **Unknown workspaceId on turn start** → auto-create (title = id, defaultCwd = null). Typos can be deleted. -2. **PUT /workspaces/:id initial state** → body accepts optional `title`/`defaultCwd` with defaults (`title = id`, `defaultCwd = null`). Only applied on create; existing workspace returned as-is. -3. **lastActivityAt on title/default-cwd changes** → no. -4. **LSP cwd field** → returns effective cwd. -5. **Conversation count in list** → yes, included as `WorkspaceEntry.conversationCount`. |
