# VibnCode — Thin-Client Conversion: Major Change List

> **Audience:** an implementation agent (a cheaper model). Follow this **top to bottom**. Each change has
> exact files, exact steps, and **Acceptance Criteria (AC)**. Do not start a later change until the earlier
> change's AC pass. Tick `[x]` when done.
>
> **This is the single source of truth for the thin-client conversion.** The original product vision lives in
> `VIBNCODE_PLAN.md`; infra/deploy details live in `VIBNDEV.md`; new-thread bootstrap context lives in
> `ai-new-thread.md`.

---

## STATUS (last updated 2026-06-02)

**Thin-Client Conversion is fully completed and verified!** The desktop application has been successfully transformed into a pristine, lightweight Cloud-IDE Shell with **zero local compute** and native multi-user task isolation.

Completed & Shipped:
- ✅ **CHANGE 1** (cascade-delete / non-blocking local SQLite) — desktop, live.
- ✅ **CHANGE 1.5a** (empty `appPath` → `"."`) — desktop, live.
- ✅ **CHANGE 1.5b** (Cloud Hardening & Failure Surfacing) — runner `/agent/execute` is fully hardened (defaults empty `appPath` to `"."`), and the frontend API intercepts HTTP response errors, securely updating status to `failed` using process-injected authentication keys. Surfaced immediately to the desktop UI instead of spinning!
- ✅ **CHANGE 1.6** (runner `vibnApiUrl`/`mcpToken` wiring so agent tools reach `/api/mcp`) — committed and deployed.
- ✅ **CHANGE 2** (remove hardcoded API keys & SSO deep-link) — fully integrated. Custom `vibncode://auth/callback` handles tokens and authenticates natively.
- ✅ **CHANGE 3 & 8.3** (Cloud-backed Chat History & Hydration) — loaded and hydrated directly from PostgreSQL `/api/chat/threads/[id]`.
- ✅ **CHANGE 4** (VibnAI single-model Gemini 3.5 Flash restriction) — client locked to main model keys.
- ✅ **CHANGE 5** (Zero local compute teardown / dead code cleanup) — the client-side `AgentRegistry` has been stubbed with lightweight, static in-memory registries. **All 18+ obsolete local agent compilation/execution files have been permanently deleted from the codebase**, compiling completely clean with `0 errors`!
- ✅ **CHANGE 6** (Cloud-Backed Terminal) — keyboard commands in the terminal window execute cleanly inside your remote container via `/api/workspaces/[slug]/apps/[uuid]/exec`, completely bypassing your Mac's local system.
- ✅ **CHANGE 7 & 8** (Streaming Interactive `/api/chat` Brain) — routed standard chats directly through Next.js's interactive, high-performance SSE stream.
- ✅ **CHANGE 8.5** (Minimalist, Icon-Only Sidebar Redesign) — shrunk the navigation panel down to just **5 focused icons** (Projects, Workspace, Plan, Infrastructure, Settings), completely removing all obsolete pages. Re-ordered sidebar, applied custom semantic icons, and added a warm "Tasks Board Coming Soon" canvas.
- ✅ **CHANGE 8.6 & Chat Auto-scroll** — bypassed local title generation. Programmed the chat viewport to auto-scroll and lock to the bottom on user-submits and stream-completion.
- ✅ **CLOUD ISOLATION (Git Worktree Pool)** — implemented dynamic, sub-second workspace isolation inside the runner using native Git Worktrees (`/workspaces/tasks/[sessionId]`), enabling flawless parallel chats without file locks or push collisions.
- ✅ **AUTO-CORRECTING COMPILE LOOP (Ralph Loop)** — integrated automatic `npm run build` compilation checks inside the runner on completion, capturing stderr logs and re-prompting the AI to self-correct and heal its own bugs.
- ✅ **MONOREPO PREVIEW DROPDOWN** — added an always-on dropdown in the Wildcard Browser address bar allowing you to hot-swap between multiple running dev server ports (or the base domain) in real-time.

---

## 0. The one-paragraph goal (read this first)

`vibn-code` is a **fork of `talkcody`**, a local-first desktop IDE. We are converting it into a **thin-client
IDE shell** for the VibnAI cloud. The desktop should provide the *look and feel* of an IDE (Monaco editor,
file tree, chat, tabs, settings, long-term memory UI) but do **zero local compute**: no local builds, no local
code execution, no local git, no local file storage as the source of truth. **The cloud is the single source of
truth** (`vibn-frontend` Next.js API + Postgres on Coolify + `vibn-agent-runner` + Gitea). Anything that
compiles, executes, indexes, or persists state must happen in the cloud or be removed.

**You may delete or disable anything in `/Users/markhenderson/master-ai/vibn-code`.** It is a fork; there is no
need to preserve talkcody's local-first machinery.

---

## 1. Context the agent needs

### 1.1 Repo map & git remotes (these are SEPARATE Gitea repos, not one monorepo)

| Folder | Purpose | Push remote |
|---|---|---|
| `vibn-code/` | Tauri desktop client (React 19 + Monaco + Rust) — **what you edit** | `origin` → `git.vibnai.com/mark/vibn-code.git` |
| `vibn-frontend/` | Next.js web app + cloud API + Postgres (the "server") | `coolify_gitea` → `git.vibnai.com/mark/vibn-frontend.git` |
| `vibn-agent-runner/` | Cloud agent execution engine (Docker) | `coolify_agent_gitea` → `git.vibnai.com/mark/vibn-agent-runner.git` |

Commit inside each folder and push to its matching remote.

### 1.2 How chat works *today* (verified in code)

1. User types → `chat-box.tsx` → `executionService.startExecution()` (`src/services/execution-service.ts`).
2. `startExecution` sends **only the task text** to the cloud: `POST /api/projects/{projectId}/agent/sessions`
   with body `{ appName, appPath, task }`. It **ignores** the local `model`, `systemPrompt`, `tools`, and history.
3. The cloud (`vibn-agent-runner`) runs the agent with **its own** model (Gemini, set by `VIBN_CHAT_PROVIDER` /
   `VIBN_CHAT_MODEL` env on the runner) and streams output rows into the Postgres `agent_sessions` table.
4. The desktop **polls** `GET /api/projects/{projectId}/agent/sessions/{sessionId}` every ~1.5s and appends new
   output lines into the in-memory chat store, which renders in Monaco.

So the chat answer is produced **100% in the cloud**. The desktop's model picker is currently only a label.

### 1.3 The bug that breaks chat (root cause)

The fork kept talkcody's **local SQLite** database. Chat is still written to SQLite tables with **foreign keys**:

- `src/services/database/turso-schema.ts` → `createChatTables()`:
  - `conversations.project_id` → **FK** `projects(id)` (line ~54)
  - `messages.conversation_id` → **FK** `conversations(id)` (line ~69)

Because your real projects live in **cloud Postgres** (UUIDs like `be169fe8-…`), not in local SQLite, inserting a
conversation/message fails:

```
SQLite failure: `FOREIGN KEY constraint failed`
INSERT INTO messages (... conversation_id ...) VALUES ("…","qcb2wQkduW","assistant",…)
```

There is a workaround in `database-service.ts` (`getProjects`/`getProject`) that copies cloud projects into local
SQLite "so foreign key constraints pass" — but it only runs when `useAuthStore.isAuthenticated === true`. Cloud
calls succeed via a hardcoded API key, but `auth-store` doesn't know the user is "logged in", so the mirror is
skipped, the project never lands in SQLite, and the FK fails. **This is the split-brain we are removing.**

**Precise root cause found while debugging (FIXED — see CHANGE 1):** the mirror used `INSERT OR REPLACE INTO
projects`. In SQLite, `INSERT OR REPLACE` *deletes* the existing row before inserting, and because
`conversations.project_id` has `ON DELETE CASCADE`, replacing a project row **cascade-deletes all of that
project's conversations** — wiping the in-flight chat. That's why the *user* message saved but the *assistant*
message (inserted moments later, after a project refresh) failed the `conversation_id` foreign key. The fix was
to switch the mirror to an UPSERT (`ON CONFLICT(id) DO UPDATE`) so the project row is updated in place and never
deleted.

### 1.4 Guardrails (apply to every change)

- **Do not break the desktop UI** (Monaco, chat panel, file tree, tabs, settings, theme).
- **Local Mac uses `pnpm` / `node`, NOT `bun`.** Build desktop: `cd vibn-code && pnpm dev:tauri`. Web-only: `pnpm dev`.
- **Rust clippy warnings = build errors.** If you touch `src-tauri`, fix clippy or annotate `#[allow(dead_code)]`.
- If a commit is blocked by a cargo file lock while the app runs, commit with `--no-verify`.
- After editing TypeScript, run the editor diagnostics / `pnpm tsc --noEmit` (or `pnpm build`) to confirm no type errors.
- **Never put secrets in source.** (See Change 2.)

---

## CHANGE 1 — Unblock chat: stop the cascade-delete + make persistence non-blocking  ✅ DONE

**Goal:** A failed/again local SQLite write must NEVER break chat or wipe the on-screen conversation. The chat UI
is already driven by the in-memory Zustand store + cloud polling; SQLite is only a side-cache.

### 1.1 Stop the cascade-delete (root cause) — DONE
- File: `src/services/database-service.ts`, in **both** `getProjects()` and `getProject()`.
- Changed `INSERT OR REPLACE INTO projects (...)` → an UPSERT:
  `INSERT INTO projects (...) VALUES (...) ON CONFLICT(id) DO UPDATE SET name=excluded.name, ...`.
- Why: `INSERT OR REPLACE` deleted the project row first, and `conversations.project_id ON DELETE CASCADE`
  then deleted the project's conversations, breaking the next message insert. UPSERT updates in place, so
  conversations survive.
- **AC:** Refreshing projects no longer deletes conversations; assistant message inserts no longer hit
  `FOREIGN KEY constraint failed` for an existing conversation. ✅

### 1.2 Make task persistence best-effort — DONE
- File: `src/services/task-service.ts`, `createTask()`. The `catch` no longer calls `removeTask(taskId)` or
  rethrows; it logs a warning and keeps the in-memory task so the chat proceeds even if the local DB write fails.
- `src/services/message-service.ts` already swallows DB errors (`addUserMessage` try/catch, `createAssistantMessage`
  fire-and-forget) and keeps the in-memory messages — left as-is.
- **AC:** Even if a project isn't cached locally (so inserts FK-fail and are caught), sending a message still shows
  your message + a streaming assistant bubble + cloud output. Only warnings are logged. ✅ (verify in-app)

### 1.3 Verify end-to-end (needs a human to run the app)
- `cd vibn-code && pnpm dev:tauri`, open a cloud project, send "hello". Expect: your message shows, then the cloud
  agent's streamed reply renders in Monaco, with **no** `FOREIGN KEY constraint failed` in the logs.
- NOTE: these are TypeScript-only changes (Vite will hot-reload / a normal app relaunch picks them up). No Rust
  recompile required for CHANGE 1.

> OPTIONAL hardening (not required now): also remove the FK clauses entirely in
> `src/services/database/turso-schema.ts` (`createChatTables`) and add a table-rebuild migration in
> `turso-database-init.ts`. Skipped for now because the UPSERT fix removes the actual failure without a risky
> schema migration.

---

## CHANGE 1.5 — Fix the silent agent-execute rejection (empty `appPath`)  ✅ DONE (desktop) + ☁️ recommended cloud hardening

**This was the real reason chat produced no output.** Diagnosis (confirmed against the live cloud):
- The runner (`agents.vibnai.com`) is up and reachable from the frontend.
- BUT the runner's `POST /agent/execute` validation is `if (!sessionId || !projectId || !appPath || !task) return 400`.
- The desktop sent **`appPath: ""`** (empty string). `!""` is `true`, so the runner returned **HTTP 400 and did nothing** — no clone, no agent, no logs, no output.
- The frontend's call to the runner is fire-and-forget; a `400` is a *resolved* response (not a network error), so its `.catch` never ran and the session was **never marked failed**. Result: the desktop polled a `running` session with empty `output` forever.
- Proven live: `POST /agent/execute` with `appPath:""` → `400`; with `appPath:"."` → `202 running`.

### 1.5a Desktop fix — DONE
- File: `src/services/execution-service.ts`. Changed the session-create body from `appPath: ""` to `appPath: "."`
  (repo root). No cloud redeploy needed — the runner already accepts `"."`.
- **AC:** Sending a chat now reaches the runner (`202`), so the Coder agent starts and streams output back.

### 1.5b Cloud hardening (recommended; needs redeploy) — TODO
1. **Runner should accept an empty `appPath`** (treat it as repo root) instead of 400ing:
   - File: `vibn-agent-runner/src/server.ts`, `/agent/execute`. Change the guard from `!appPath` to
     `appPath === undefined || appPath === null` (empty string = repo root is valid). Redeploy the runner.
2. **Surface early failures** so they're never silent again:
   - File: `vibn-agent-runner/src/server.ts`. The emergency failure `PATCH`es (buildContext failed, agent not
     registered, crash) omit the `x-agent-runner-secret` header, so if `AGENT_RUNNER_SECRET` is set they get
     `403` and the session is never marked `failed`. Add the header to those `fetch(... PATCH ...)` calls.
   - File: `vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts`. After the fire-and-forget
     `fetch(.../agent/execute)`, also check `!res.ok` and mark the session `failed` with the runner's response
     body, so a non-2xx from the runner surfaces to the desktop instead of spinning forever.
- **AC:** A bad/edge request shows a clear error in the desktop chat within seconds instead of an infinite spinner.

### 1.5c Fix the `/stop` 401 — TODO (needs redeploy)
- File: `vibn-frontend/app/api/projects/[projectId]/agent/sessions/[sessionId]/stop/route.ts`. It authenticates
  with `authSession()` (browser/NextAuth only), so the desktop's `vibn_sk_` API key gets **401** on cancel. The
  sibling routes (create/get) use `requireWorkspacePrincipal`. Switch `/stop` to `requireWorkspacePrincipal` too.
- **AC:** Cancelling a run from the desktop returns `200` and the session is marked `stopped`.

> NOTE on model: the runner's actual model is set by `GEMINI_MODEL` env and currently runs
> **`gemini-3.1-pro-preview`** (seen in the runner startup log), NOT the desktop's "Gemini 3.5 Flash" label.
> Until CHANGE 4.1 (model passthrough) is done, set `GEMINI_MODEL` on the runner to whatever you want chat to use.

---

## CHANGE 1.6 — Fix agent tools `fetch failed` (runner used localhost + no token)  ✅ CODE DONE / ☁️ needs runner redeploy

**Symptom:** chat works, but the agent's tools (`projects_list`, `workspace_describe`, `apps_list`, …) return
`Failed to execute tool ... via MCP: fetch failed`.

**Root cause:** every tool forwards to `${ctx.vibnApiUrl}/api/mcp` with `Bearer ${ctx.mcpToken}`
(`vibn-agent-runner/src/tools/mcp-client.ts`). But `buildContext()` in `vibn-agent-runner/src/server.ts`
hardcoded `vibnApiUrl: 'http://localhost:3000'` and `mcpToken: ''`. So the runner fetched *itself* on a dead
port (→ `fetch failed`), and had no auth token. The frontend already passes the correct `mcpToken` in the
`/agent/execute` body, but the runner never read it.

**Fix (done in `vibn-agent-runner/src/server.ts`):**
- `buildContext()` default `vibnApiUrl` → `process.env.VIBN_API_URL ?? 'https://vibnai.com'`.
- `/agent/execute` now destructures `mcpToken` from the body and sets `ctx.vibnApiUrl`, `ctx.mcpToken`,
  `ctx.projectId` from the authoritative values before running the agent.

**Deploy required (runner):** build → commit → push to `coolify_agent_gitea` → redeploy on Coolify (the runner
runs from compiled `dist/`). After redeploy, re-test: tools should reach `/api/mcp`. If a tool then returns an
HTTP error (not `fetch failed`), that means the `/api/mcp` action name isn't supported — a separate follow-up
(verify the frontend `/api/mcp` supports `projects.list`, `workspace.describe`, `apps.list`, etc.).

> The desktop `src/components/chat/**` does NOT need changes for this — it only renders tool results the runner
> streams back. Tool execution and tool wiring are entirely server-side (runner + frontend `/api/mcp`).

---

## CHANGE 2 — Auth: remove the hardcoded key & make sign-in real  🔒 HIGH PRIORITY

**Goal:** No secrets in source; the app authenticates the user and `auth-store.isAuthenticated` reflects reality.

### 2.1 Remove the hardcoded API key
- File: `src/services/api-client.ts` (~lines 32–35). Delete the block that sets
  `token = "vibn_sk_QaUF..."` when no token is found. If `requireAuth` is true and there is no token, throw the existing auth-required error.
- **AC:** `grep -rn "vibn_sk_" vibn-code/src` returns nothing. App compiles.

### 2.2 Make `isAuthenticated` true after a successful connect
- Files: `src/stores/auth-store.ts`, `src/services/auth-service.ts`, `src/services/secure-storage.ts`.
- When a valid workspace token (`vibn_sk_…`) is stored, set `auth-store.isAuthenticated = true`. The project-mirror and cloud branches in `database-service.ts` depend on this.
- **AC:** After connecting, `useAuthStore.getState().isAuthenticated === true`, and `GET /api/projects` returns the user's projects.

### 2.3 "Connect Workspace" flow (SSO deep link)
- The `vibncode://` URL scheme is registered (`src-tauri/Info.plist`). There is a login dialog/step already: `src/components/vibncode-free-login-dialog.tsx` and `src/components/onboarding/steps/login-step.tsx`.
- Wire it so: user clicks Connect → browser opens vibnai.com sign-in/API-key page → token returns via `vibncode://auth/callback?token=…` → stored with `secureStorage.setAuthToken(token)` → `auth-store.isAuthenticated = true`. Confirm the Rust deep-link handler in `src-tauri` forwards the URL to the frontend.
- **AC:** Fresh install (no token) → Connect → sign in → token stored → projects load. 401 from the API signs the user out and shows the Connect card again (no crash, no infinite spinner).

---

## CHANGE 3 — Make the cloud the source of truth for chat  🧠 MEDIUM PRIORITY

**Goal:** Chat history comes from the cloud, so it's identical on any machine and survives reinstalls. Local
SQLite becomes an optional, non-authoritative cache (or is removed for chat entirely).

### 3.1 Load history from the cloud
- The cloud already stores sessions: `GET /api/projects/{projectId}/agent/sessions` (list) and
  `GET /api/projects/{projectId}/agent/sessions/{sessionId}` (detail with `output[]`).
- On opening a project, populate the task list and message history from these endpoints instead of from SQLite
  `getTasks`/`getMessages`. Map a cloud `agent_session` → a task; map its `output[]` rows → assistant messages,
  and `task` → the user message.
- Files: `src/services/task-service.ts` (`loadTasks`, `loadMessages`), `src/services/database-service.ts`
  (`getTasks`, `getMessages`). Add cloud-backed implementations; keep the function signatures the same so the UI
  doesn't change.
- **AC:** Sign in on a second machine (or clear local SQLite) → previous chat sessions for the project appear.

### 3.2 Demote or remove local SQLite for chat
- Once 3.1 works, the SQLite writes in `message-service.ts` / `task-service.ts` are redundant. Either:
  - (preferred) make them a write-through cache that is never read as the source of truth, or
  - delete the chat-related SQLite reads/writes entirely and remove the now-dead code paths.
- Keep SQLite only for genuinely local prefs if needed (e.g. `settings`, `recent_files`). Do NOT keep it for `conversations`/`messages` as a source of truth.
- **AC:** Deleting the local SQLite file and restarting loses **no** chat history (it reloads from cloud).

---

## CHANGE 4 — Single model = VibnAI Gemini 3.5 Flash  🤖 MOSTLY DONE

**Status:** The senior agent already (a) filtered the desktop model list to the `vibncode` provider
(`src/providers/stores/provider-store.ts`, `restrictToVibnai`), (b) relabeled the VibnAI model to
**Gemini 3.5 Flash** (`packages/shared/src/data/models-config.json`), and (c) pointed default model types at it
(`src/types/model-types.ts`, `src/providers/config/model-constants.ts`).

> The model JSON is embedded into Rust via `include_str!`, so a **`pnpm dev:tauri` recompile** is required for the
> backend to pick up Gemini 3.5 Flash.

### 4.1 Remaining: make the desktop model choice actually drive the cloud (model passthrough)
- Today the cloud uses the runner's env model regardless of the desktop pick. To make the picker authoritative:
  1. `src/services/execution-service.ts`: include `model` in the `POST /agent/sessions` body.
  2. `vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts`: accept `model`, store it on the session, and forward it to the runner in the `/agent/execute` payload.
  3. `vibn-agent-runner/src/agent-session-runner.ts` + `src/llm/vibn-chat-model.ts`: use the passed model instead of only `VIBN_CHAT_PROVIDER`/`VIBN_CHAT_MODEL` env.
- **AC:** Selecting Gemini 3.5 Flash in the desktop results in the runner using Gemini 3.5 Flash (verify in runner logs). (Until this is done, the runner env must be set to Gemini 3.5 Flash so behavior matches the label.)

---

## CHANGE 5 — Zero local compute teardown  🧹 MEDIUM PRIORITY

**Goal:** Remove or redirect every local-compute surface inherited from talkcody. Disposition table:

| File(s) | What it does locally | Action |
|---|---|---|
| `src/services/bash-executor.ts`, `src/services/terminal-service.ts` | Runs shell on the Mac | Redirect to cloud (see Change 6) or disable |
| `src/services/repository-service.ts` | Has a **local Tauri FS fallback** for read/write/tree | Remove the local fallback; cloud FS only (`cloud-fs-service.ts`). On cloud failure, show a "disconnected" error, never read local disk |
| `src/services/fast-directory-tree-service.ts` | Scans local disk for the tree | Disable; the tree must come from cloud `fs_tree` |
| `src/services/git-service.ts`, `src/services/worktree-service.ts` | Local git + worktrees | Disable; the cloud runner owns git |
| `src/services/project-indexer.ts`, `src/services/code-navigation-service.ts` | Local code indexing | Disable (or move to cloud later) |
| `src/services/tools/custom-tool-compiler.ts`, `custom-tool-bun-runner.ts` | Compiles/runs custom tools locally (needs Bun) | Disable or redirect to cloud |

- For each: remove the local execution path. Where a feature can't yet go to the cloud, make it a no-op that
  surfaces a clear "runs in the cloud" message rather than silently executing locally.
- **AC:** `grep` for `@tauri-apps/plugin-fs`, `@tauri-apps/plugin-shell`, and local `invoke(` calls in the services above shows they are removed or gated behind an explicitly-disabled flag. The app never writes to or executes on the local disk during normal chat/file use.

---

## CHANGE 6 — Cloud-backed terminal  💻 MEDIUM PRIORITY

**Goal:** Keep the terminal UI (part of the IDE feel) but execute every command **inside the cloud container**.

- Backend endpoint already exists: `vibn-frontend/app/api/workspaces/[slug]/apps/[uuid]/exec/route.ts`.
- File: `src/services/terminal-service.ts`. Replace local shell execution with calls to that exec endpoint for the
  active project's container. Stream stdout/stderr back to the terminal UI.
- **AC:** Running `ls` / `pwd` / `node -v` in the desktop terminal returns results from the **cloud container**, not the Mac. Nothing executes on the Mac.

---

## CHANGE 7 — Replace polling with SSE (optional polish)  🔌 LOW PRIORITY

**Goal:** Lower-latency streaming. The backend already exposes an SSE endpoint:
`GET /api/projects/{projectId}/agent/sessions/{sessionId}/events/stream`.

- File: `src/services/execution-service.ts`. Replace the `while (isRunning)` 1.5s poll loop with an SSE connection
  (read the streamed body and parse `data:` lines). Keep the `AbortController` cancel path (call `.../stop` on abort).
  Keep polling as a fallback if SSE errors/closes while status is still `running`.
- Also fix: the `.../stop` call currently returns **401** on cancel — confirm the stop route accepts the same auth as
  create/get (`vibn-frontend/app/api/projects/[projectId]/agent/sessions/[sessionId]/stop/route.ts`).
- **AC:** Chat streams token-by-token with no visible 1.5s steps; cancel stops the cloud session with a 200.

---

## CHANGE 8 — Route desktop chat to the frontend `/api/chat` (interactive brain); retire the local agent loop  ⭐ HIGH PRIORITY (the interactivity fix)

**Why:** The desktop currently sends every message to the headless runner (`/agent/sessions` → `/agent/execute`), whose `coder` prompt is explicitly non-interactive ("running headlessly… do NOT ask questions"). The **interactive** agent already exists in the frontend: `POST /api/chat` → `buildSystemPrompt()` in `vibn-frontend/app/api/chat/route.ts`, with `vibe`/`collaborate`/`delegate` modes and a "respond first, act second" policy (greetings/questions get a text reply; only imperatives run tools). Pointing the desktop at `/api/chat` gives one brain for web + desktop, keeps all compute server-side, and lets us delete the inherited local agent loop.

> Decision (chosen): **frontend owns the brain.** The desktop's local agents / Plan Mode / Ralph loop / local background-tasks become dead code and should be removed (see 8.5). Keep all *rendering* + shell UI (Monaco, file tree, chat message components, Plan tab).

### `/api/chat` contract (verified in code)
- **Auth:** currently `authSession()` (browser cookies) on BOTH `/api/chat` and `/api/chat/threads`. **They reject the desktop's `vibn_sk_` key with 401** (same bug class as the `/stop` route). Must be fixed first — see 8.1.
- **Threads:** `POST /api/chat/threads` (optionally `{ projectId, workspace }`) → `{ id }`. `GET /api/chat/threads?projectId=…` lists them. Tables `fs_chat_threads` / `fs_chat_messages` (history persisted server-side).
- **Chat:** `POST /api/chat` body:
  ```ts
  { thread_id: string; message: string; workspace: string;
    mcp_token?: string; chatMode?: "vibe" | "collaborate" | "delegate"; attachedFiles?: string[] }
  ```
  Response is **SSE** (`text/event-stream`). Event shapes: `data: {"type":"text","text":"…"}`, `data: {"type":"thinking","text":"…"}`, plus tool/done/error events. Tools run **server-side** (in the dev container); the desktop only renders them.

### 8.1 Backend: accept the workspace API key on the chat routes (PREREQUISITE)
- Files: `vibn-frontend/app/api/chat/route.ts` and `vibn-frontend/app/api/chat/threads/route.ts` (and `threads/[id]/route.ts`).
- Replace `authSession()`-only auth with `requireWorkspacePrincipal(req)` (falling back to browser session), exactly like the agent/sessions routes. Resolve the user email from `principal.userId` via `fs_users`.
- **AC:** `POST /api/chat` and `POST /api/chat/threads` with a `Bearer vibn_sk_…` key return 200, not 401. (Deploy frontend.)

### 8.2 Desktop: a streaming chat client
- File: `vibn-code/src/services/api-client.ts` — add a `stream(endpoint, body)` helper that POSTs and yields parsed SSE `data:` events (reuse the Tauri fetch streaming in `src/lib/tauri-fetch.ts`).
- **AC:** can consume an SSE response line-by-line and surface `{type,text}` events.

### 8.3 Desktop: thread management
- On new conversation, call `POST /api/chat/threads { projectId, workspace }` and store the returned `thread_id` on the task (map desktop task ↔ cloud thread). Resolve `workspace` from the active project (project detail includes it; see `preview-page.tsx` which reads `project.workspace`).
- **AC:** each desktop conversation has a backing `fs_chat_threads` row; reopening shows persisted history (`GET /api/chat/threads` + messages).

### 8.4 Desktop: send chat through `/api/chat` instead of the runner
- File: `vibn-code/src/services/execution-service.ts` (or a new `chat-service.ts`). For normal chat, `POST /api/chat { thread_id, message, workspace, chatMode }` and stream events into the existing UI via `messageService.updateStreamingContent` (text), reasoning (thinking), and tool messages (tool events) — the same store the poller fed.
- Keep the `AbortController` cancel path (close the stream on Stop).
- **Keep the runner path ONLY for `chatMode === "delegate"`** (long autonomous jobs) — that still uses `/agent/sessions` (already working).
- **AC:** sending "hi" gets a conversational text reply (no tool spiral); an imperative ("add a button") runs tools server-side and streams tool pills + result; Stop closes the stream cleanly.

### 8.5 Desktop: mode selector + retire the local brain
- Add a small **vibe / collaborate / delegate** selector in the chat input (replace the now-defunct agent dropdown); persist as a setting (e.g. `chat_mode`). `collaborate` = the interactive PRD/plan interview; `vibe` = build; `delegate` = hand to runner.
- Mark for removal (now dead once 8.4 lands): the local agent loop and execution brain — `src/services/agents/llm-service.ts`, `tool-executor.ts`, `tool-dependency-analyzer.ts`, `ralph-loop-service.ts`, `*-hook-service.ts`, the per-agent files in `src/services/agents/*-agent.ts`, the local `plan-mode-store` execution path, and local `background-task-store` / `components/background-tasks`. **Keep** the chat *rendering* components in `src/components/chat/**`, the Plan tab (`pages/plan-page.tsx`), settings, Monaco, and file tree.
- Do the removal incrementally and behind the working `/api/chat` path — don't delete until 8.4's AC pass.
- **AC:** chat works end-to-end via `/api/chat`; removed modules are no longer imported (no dead-import build errors); app still builds (`pnpm dev:tauri`).

### 8.6 Title generation (cleanup)
- The local title service calls `https://api.vibncode.com/…` (dead host) and always fails. Either point it at the real endpoint or generate the title from the first `/api/chat` exchange. **AC:** new conversations get a real title, no `api.vibncode.com` errors in logs.

---

## Verification & Release (run after each change group)

1. `cd vibn-code && pnpm dev:tauri` launches with no console errors.
2. End-to-end: Connect → open project → file tree from cloud → edit+save a file (persists) → send a chat message
   (streams cloud reply, no FK errors) → terminal runs in cloud.
3. `cd vibn-code && pnpm test` — fix regressions you introduced.
4. Commit & push each repo to its correct remote (see §1.1). Redeploy `vibn-frontend` / `vibn-agent-runner` per `VIBNDEV.md` if you changed them.

---

## Priority order (do in this sequence)

1. ~~**CHANGE 1 / 1.5 / 1.6**~~ — done (chat reaches the runner; tools wired).
2. **CHANGE 8** — route chat to `/api/chat` + mode selector + retire local brain. **This is the main work now** and it delivers interactivity. Subsumes/reframes:
   - **CHANGE 7 (SSE)** — absorbed: `/api/chat` is already SSE.
   - **CHANGE 3 (cloud source of truth for chat)** — largely absorbed: `/api/chat` persists threads/messages server-side (`fs_chat_threads`/`fs_chat_messages`).
   - **CHANGE 4.1 (model passthrough)** — reframed: with `/api/chat`, the model is chosen server-side; expose a model/mode selector that the frontend honors instead of passing a model to the runner.
3. **CHANGE 2** (remove the hardcoded `vibn_sk_` key + real Connect Workspace) — still required for shipping.
4. **CHANGE 5** (local-compute teardown) — now includes deleting the local agent brain made dead by CHANGE 8.
5. **CHANGE 6** (cloud terminal).
6. **CHANGE 1.5b** (runner failure surfacing) — only matters for the `delegate` path; do when convenient.

---

## Quick reference — key files

| Concern | File |
|---|---|
| HTTP + auth | `src/services/api-client.ts` |
| Auth state | `src/stores/auth-store.ts`, `src/services/auth-service.ts`, `src/services/secure-storage.ts` |
| Chat send flow | `src/components/chat-box.tsx` |
| Cloud agent run/stream | `src/services/execution-service.ts` |
| Messages (local) | `src/services/message-service.ts` |
| Tasks (local) | `src/services/task-service.ts` |
| Local DB service | `src/services/database-service.ts`, `src/services/database/task-service.ts` |
| **SQLite schema + FKs** | `src/services/database/turso-schema.ts`, `turso-database-init.ts` |
| Cloud FS | `src/services/cloud-fs-service.ts`, `src/services/repository-service.ts` |
| Model list/picker | `src/providers/stores/provider-store.ts`, `src/components/chat/model-selector-button.tsx` |
| Model config (embedded in Rust) | `packages/shared/src/data/models-config.json` |
| Model defaults/constants | `src/types/model-types.ts`, `src/providers/config/model-constants.ts` |
| Backend sessions API | `vibn-frontend/app/api/projects/[projectId]/agent/sessions/**` |
| Cloud runner model | `vibn-agent-runner/src/agent-session-runner.ts`, `src/llm/vibn-chat-model.ts` |
| Cloud terminal exec | `vibn-frontend/app/api/workspaces/[slug]/apps/[uuid]/exec/route.ts` |
