# Roadmap — Chat Switchboard

**See also:**
- [ARCHITECTURE.md](ARCHITECTURE.md) — Core services design, store layer, scope model
- [EXTENSIONS.md](EXTENSIONS.md) — Extension system spec (Browser/Starlark/Sidecar tiers,
  manifests, browser tool bridge, surfaces/modes, model roles)
- [CHANGELOG.md](../CHANGELOG.md) — Detailed release notes for all completed versions

**Versioning (pre-1.0):** `0.<major>.<minor>` — hotfixes use quad: `0.x.y.z`
No compatibility guarantees before 1.0.

---

## Dependency Graph

Features have real dependencies. This ordering respects them.

```
v0.9.x  Stability + Quick UX Wins              ✅
          │
v0.9.3  Content Visibility & Block Controls     ✅
          │
v0.9.4  API Key Encryption + Vault              ✅
          │
v0.10.0 Model Roles (utility + embedding)       ✅
        + Usage Tracking
          │
v0.10.2 Summarize & Continue                    ✅
          │
v0.10.3 Frontend Refactor                       ✅
          │
v0.10.4 Model Type Pipeline + Role Fixes        ✅
          │
v0.10.5 UI Primitives + Extension Surfaces      ✅
          │
v0.11.0 Extension Foundation (browser tier)     ✅
          │
  ┌───────┴──────────────┐
  │                      │
v0.12.0 File Handling  v0.13.0 Admin Panel
  + Vision ✅            Refactor (fullscreen) ✅
  │                      │
  │                    v0.13.1 Web Search
  │                      + url_fetch ✅
  │                      │
  └───────┬──────────────┘
          │
v0.14.0 Knowledge Bases ✅ (embedding role + file storage + pgvector)
          │
v0.15.0 Compaction ✅ (utility role + background job)
          │
v0.15.1 Context Recall Tools ✅ (attachment_recall, conversation_search)
          │
  ┌───────┴──────────────┐
  │                      │
v0.16.0 User Groups    v0.17.0 Persona-KB Binding
  + Resource Grants ✅   + Enterprise KB Mode
  │                      + QOL / UX Wins
  └───────┬──────────────┘
          │
  ┌───────┴──────────────┐
  │                      │
v0.17.1 SQLite Backend ✅ v0.17.2 CodeMirror 6 ✅
v0.17.3 Notes Graph + Wikilinks ✅
  (dual DB)              (editor bundle, chat
  │                       input, ext editor)
  └───────┬──────────────┘
          │
v0.18.0 Memory ✅ (user + persona scopes, review pipeline)
          │
v0.18.1 Side Panel Architecture ✅ (single-slot, ctx.ui primitives, mermaid refactor)
          │
v0.19.0 Projects / Workspaces ✅ (project groups, KB resolution, drag-drop sidebar)
          │
v0.19.1 Active Project + System Prompt + Detail Panel ✅
          │
v0.19.2 Project Persona Default + Archive + Reorder ✅
          │
v0.20.0 Notifications + @mention Routing + Multi-model   ✅
          │
v0.21.0 Workspace Storage Primitive                      ✅
          │
  ┌───────┴──────────────┐
  │                      │
v0.21.1 Workspace ✅   v0.21.3 Surface Infra ✅
  Tools + Bindings       + REPL (parallel)
  │                      │
v0.21.2 Workspace ✅   v0.21.5 Editor Surface ✅
  Indexing + Search      (Development Mode)
  │                      │
v0.21.4 Git ✅         v0.21.6 Editor Surface ✅
  Integration            + Document Output
  └───────┬──────────────┘
          │
v0.21.7 Bugfix: scanJSON driver buffer aliasing   ✅
          │
v0.22.0 Provider Health + Capability Overrides     ✅
  + Workspace Pane Refactor (FE)
          │
v0.22.1 Provider Extensions (declarative config)   ✅
          │
v0.22.2 Routing Policies + Fallback Chains          ✅
          │
v0.22.3 Provider Admin UI + Deferred Polish          ✅
          │
v0.23.0 Multi-Participant Channels + Presence
          │
v0.24.0 Auth Strategy (mTLS/OIDC) + Full RBAC + Group permissions
          │
  ┌───────┴──────────────┐
  │                      │
v0.25.0 Workflow       v0.26.0 Tasks /
  Engine                 Autonomous Agents
  (team-owned,           (service channels,
   staged processes,      scheduler, unattended
   human + AI collab)     execution)
```

---

## Completed Releases

> Full details for all completed versions are in [CHANGELOG.md](../CHANGELOG.md).

### ✅ v0.9.0 — Schema Consolidation + BYOK
21 migrations → single schema, store layer abstraction, persona-as-trust-boundary,
BYOK with auto-fetch, composite model IDs, journey integration tests.

### ✅ v0.9.1 — Capability Architecture + Docs
Removed static known model table. Resolution chain: catalog → heuristic only.
Frontend relies solely on backend for capabilities. Docs rewrite.

### ✅ v0.9.2 — Quick UX Wins + Hardening
Collapsible code blocks, HTML preview, token counter, context warning, proxy
interception detection, environment injection, team admin audit scoping.

### ✅ v0.9.3 — Content Visibility & Block Controls
Button-driven code collapse, always-rendered thinking blocks, tool call
persistence in history, Notes panel → unified side panel, streaming refactor
(`streamWithToolLoop`), regenerate fix.

### ✅ v0.9.4 — API Key Encryption + Vault
Two-tier AES-256-GCM: env-var key for global/team, per-user UEK (Argon2id)
for personal BYOK. `server/crypto/` package. Migration 003. Startup backfill.
DOMPurify strict allowlist (XSS fix).

### ✅ v0.10.0 — Model Roles + Usage Tracking + Vault Debt
Named role slots (utility, embedding) with fallback. `Provider.Embed()` interface.
`usage_log` + `model_pricing` tables. Streaming token capture. Vault debt:
UEK re-wrap on password change, destruction on admin reset.

### ✅ v0.10.1 — Polish + UX
Admin system prompt injection, Personas tab, Team Management modal extraction,
preview pane enhancements, code download, personal usage scoped to BYOK.

### ✅ v0.10.2 — Summarize & Continue + Role Cleanup
User-triggered conversation compaction via utility role. Summary as tree node
with boundary metadata. BYOK role overrides (personal → team → global).
Utility rate limiting. Generation role removed.

### ✅ v0.10.3 — Frontend Refactor
2 monolith files → 13 domain-scoped files (~544 lines avg). Zero features,
no function renames. Vanilla JS, no build step.
See [REFACTOR-0.10.3.md](REFACTOR-0.10.3.md).

### ✅ v0.10.4 — Model Type Pipeline + Role Fixes
`model_type` (chat/embedding/image) captured end-to-end from provider API
through catalog to frontend. Role dropdowns filter by type.

### ✅ v0.10.5 — UI Primitives + Extension Surfaces
Shared `Providers`/`Roles` registries, 6 consolidated render primitives,
`showConfirm()` replacing native dialogs, `.popup-menu` CSS primitive.
Removed ~400 lines of duplication.

### ✅ v0.11.0 — Extension Foundation (Browser Tier)
Full extension lifecycle: manifest, loader, scoped `ctx.*` API, browser tool
bridge via WebSocket. Custom renderer pipeline. 2 server tools (calculator,
datetime), 6 built-in browser extensions (Mermaid, KaTeX, CSV, Diff, JS
Sandbox, Regex). See [EXTENSIONS.md](EXTENSIONS.md).

### ✅ v0.12.0 — File Handling + Vision
S3/PVC storage backend, image/file upload (📎, drag-drop, paste), multimodal
message assembly, text extraction pipeline (PDF/DOCX/XLSX/PPTX/ODT/RTF),
vault CLI (`rekey`/`status`), per-chat model persistence.

### ✅ v0.13.0 — Admin Panel Refactor
12-tab modal → fullscreen admin panel. 4 categories (People, AI, System,
Monitoring) × section sidebar. URL-based routing, responsive, banner-aware.
CSS design token cleanup.

### ✅ v0.13.1 — Web Search + URL Fetch
`web_search` + `url_fetch` tools. Search provider abstraction (DuckDuckGo,
SearXNG). Tool categories with per-tool toggle UI in chat bar.

### ✅ v0.14.0 — Knowledge Bases
RAG: upload → chunk → embed (pgvector) → `kb_search` tool. Channel KB toggle.
Team/personal KB scopes. Notes semantic search. Admin panel KB management.
See [DESIGN-0.14.0.md](DESIGN-0.14.0.md).

### ✅ v0.15.0 — Compaction
Background scanner with configurable thresholds. Automatic summarization via
utility role. Per-channel opt-in/out. Context budget guard rail (80% ceiling).

### ✅ v0.15.1 — Context Recall Tools
`attachment_recall` (list + read, channel-scoped), `conversation_search`
(full-text via `plainto_tsquery`). Token estimator attachment awareness.

### ✅ v0.16.0 — User Groups + Resource Grants
Groups (global/team-scoped ACLs) decoupled from team membership. Three-way
resource grants (team_only/global/groups) for Personas and KBs. Grant picker
UI. Schema consolidation: 9 migrations → single `001_v016_schema.sql`.

---

## v0.17.0 — Persona-KB Binding + Enterprise KB Mode + QOL ✅

Personas become **gateways** to knowledge. In enterprise deployments,
users don't interact with KBs directly — they talk to Personas that
have KBs attached. Plus quality-of-life improvements: chat rename,
token display, state persistence, utility model auto-naming.

Depends on: knowledge bases (v0.14.0), user groups (v0.16.0).

**Persona-KB Binding**
- [x] `persona_knowledge_bases` table: `persona_id`, `kb_id`, `auto_search` (bool — always search vs tool-only)
- [x] When a channel uses a Persona with bound KBs, `kb_search` is auto-scoped to those KBs
- [x] No user toggle needed — Persona carries its own KB context
- [x] Channel KB toggle becomes "additional KBs" on top of Persona-provided ones
- [x] Persona system prompt auto-includes KB listing hint (reuses `BuildKBHint` pattern)
- [x] Admin/team admin UI: KB picker on Persona create/edit form

**Enterprise KB Mode**
- [x] `discoverable` flag on knowledge_bases: `true` (default — appears in user's KB popup) or `false` (hidden, only accessible through Persona binding)
- [x] Hidden KBs don't appear in `GET /api/v1/knowledge-bases-discoverable` for non-admins
- [x] Hidden KBs still searchable by `kb_search` tool when accessed through a Persona
- [x] Admin panel: toggle discoverability per KB
- [x] Platform policy: `kb_direct_access` — when `false`, disables channel KB popup entirely (strict enterprise mode)

**Access Control Integration**
- [x] Persona grants (v0.16.0 groups) control who can *use* a Persona
- [x] KB grants control who can *manage* a KB (upload/delete docs)
- [x] Persona-KB binding grants implicit *search* access through the Persona
- [x] Users who can use a Persona can search its KBs, even if they can't see those KBs directly

**Team Admin Workflow**
- [x] Team admin creates KB → uploads documents → marks non-discoverable
- [x] Team admin creates Persona → binds KBs → sets access to Team Only or specific Groups
- [x] Team members see the Persona in their preset list, use it, get KB-powered answers
- [x] Team members never see or manage the underlying KBs

**Technical Debt (carried forward)**
- [x] **Security**: KB create handler authorization — team/global scope verified
- [x] **Test hygiene**: stripped DIAG diagnostics from `TestGroupBasedPersonaAccess`
- [x] **Architecture**: `KnowledgeBaseStore.UpdateDocumentStorageKey()` — moved to store layer
- [x] **UX**: Embedding dropdown fix — tolerant type filter, manual model ID fallback, auto-switch on empty _(already implemented in `ui-primitives.js`)_
- [x] **Minor**: Paste-to-file character threshold — synced from backend `PublicSettings` (`storage.paste_to_file_chars`), admin-configurable, default 2000

**Role Fallback Alerts** ([#69](https://git.gobha.me/xcaliber/chat-switchboard/issues/69))
- [x] Audit log entry on fallback fire: `action = "role.fallback"` with primary/fallback model IDs and error string
- [x] `role.fallback` WebSocket event via EventBus (`PublishAsync`, zero latency impact on completion path)
- [x] In-memory cooldown on `Resolver` (per-role, 5-minute TTL) to deduplicate repeated fallback alerts
- [x] Admin UI: persistent alert banner on `role.fallback` event (persists until dismissed)
- [x] Wire `*events.Bus` into `roles.NewResolver` via `.WithBus()` builder

**QOL / UX Wins**
- [x] **Chat rename**: inline edit on sidebar item — dblclick title → input → blur/Enter saves via `API.updateChannel(id, { title })`
- [x] **Chat token count**: display `Tokens.estimateConversation()` in model bar, update on `selectChat` and after each message
- [x] **State restore on refresh**: persist `App.currentChatId` to `sessionStorage` on select, restore on load
- [x] **Utility model auto-naming**: after first assistant response, background utility role request → `POST /channels/:id/generate-title` → re-render sidebar. Fallback to truncation when no utility model configured. Debounced via `_autoNamePending` set
- [x] **SW extension cache fix**: exclude `/extensions/` from service worker fetch handler
- [x] **Mermaid renderer**: promoted enhanced version (v2.0 — pan/zoom, SVG/PNG export, source copy) to `extensions/builtin/mermaid-renderer/`

---

## v0.17.1 — SQLite Backend ✅

Dual-database support. Every subsequent schema change is developed
against both Postgres and SQLite from day one — no retrofit. SQLite
enables single-binary deployment for dev, demo, edge, and single-user.

Depends on: v0.17.0 DB debt cleanup (store layer is sole DB interface).

**Store Layer**
- [x] 19 SQLite store implementations covering all domain interfaces
- [x] `DB_DRIVER` env var: `postgres` (default) | `sqlite`
- [x] Separate migration files per driver (`migrations/sqlite/`)
- [x] WAL mode + single-writer connection pooling for SQLite concurrency
- [x] CI matrix: full handler integration tests against both Postgres and SQLite

**Vector Search**
- [x] App-level cosine similarity in Go (pure Go, no CGO/sqlite-vec required)
- [x] Embeddings stored as JSON text in `kb_chunks.embedding` column
- [x] Full feature parity with pgvector: `SimilaritySearch`, `InsertChunks`
- [x] Graceful: adequate performance for SQLite-scale deployments (single-user, not millions of chunks)

**Schema Compatibility**
- [x] UUID generation: application-side `store.NewID()` (Go `uuid.New()`)
- [x] JSONB → JSON text columns with `ToJSON()`/`ScanJSON()` helpers
- [x] `UUID[]` arrays → JSON arrays with `ToJSONArray()`/`ScanJSONArray()`
- [x] `TIMESTAMPTZ` → SQLite `TEXT` with `datetime('now')`
- [x] `ON CONFLICT excluded.*` pattern (works in both dialects)
- [x] `INSERT RETURNING id` → separate query pattern for SQLite

**Test Infrastructure**
- [x] `dialectSQL()` converts `$N` → `?` and strips `::jsonb` at runtime
- [x] `database.PH(n)` returns dialect-appropriate placeholder
- [x] `database.TruncateAll()` dialect-aware (DELETE + PRAGMA vs TRUNCATE CASCADE)
- [x] Generic provider config: `PROVIDER`/`PROVIDER_KEY`/`PROVIDER_URL` (replaces `VENICE_API_KEY`)
- [x] Model selection prefers non-reasoning models in live tests

**What SQLite mode skips** (feature-gated, not broken):
- [x] `LISTEN/NOTIFY` — WebSocket event fan-out uses in-process EventBus (already works)
- [x] Full-text search `tsvector` — conversation_search uses LIKE fallback

---

## v0.17.2 — CodeMirror 6 Integration ✅

Rich editor infrastructure for the frontend. CM6 is ESM-native; the build
step runs in Docker (esbuild → single IIFE bundle). No change to dev
experience for non-editor code — the rest of `src/js/` stays vanilla.

Depends on: nothing (frontend-only). Prerequisite for: extension surfaces
(v0.21.5 editor surface). See [DESIGN-CM6.md](DESIGN-CM6.md) for full spec.

**Build Pipeline**
- [x] `src/editor/` directory: `package.json`, `build.mjs`, `index.mjs`, language/theme modules
- [x] `scripts/build-editor.sh`: shared build script for both Dockerfiles and local dev
- [x] New Docker stage (`cm6-build`): `npm ci` + `node build.mjs` → `vendor/codemirror/`
- [x] Both `Dockerfile.frontend` and `Dockerfile` (unified) use the shared build script
- [x] Bundle output: `codemirror.bundle.js` (~295KB min, ~90KB gzip)
- [x] Graceful degradation: all integration points check `window.CM` — falls back to `<textarea>` if bundle unavailable

**Languages (bundled)**
- [x] Markdown, JavaScript, JSON, SQL, HTML, CSS, YAML, Go, Python, Rust
- [x] Additional modes: one-line import + rebuild

**Phase 1: Extension Editor** (admin panel)
- [x] `CM.codeEditor()` factory: line numbers, bracket matching, auto-indent, search/replace
- [x] Replace manifest `<textarea>` → CM6 JSON mode
- [x] Replace script `<textarea>` → CM6 JavaScript mode
- [x] Remove manual Tab-key handler in `admin-handlers.js` (CM6 handles it)
- [x] Update `saveAdminExtension()` to use `.getValue()`

**Phase 2: Chat Input** (markdown mode)
- [x] `CM.chatInput()` factory: markdown highlighting, minimal chrome (no line numbers, no gutter)
- [x] Enter=send, Shift+Enter=newline keybindings
- [x] Auto-growing height, placeholder text
- [x] Wire `onChange` → `updateInputTokens()`
- [x] Update `chat.js`, `attachments.js`, `tokens.js` to use CM API
- [x] WYSIWYG fenced code block decorations (visual container matching claude.ai UX)
- [x] Inline code shortcut (Ctrl/Cmd+E)

**Phase 3: Polish**
- [x] Theme integration: CSS variable overrides (`--bg`, `--border`, `--accent`, etc.)
- [x] Dark/light/system mode toggle in appearance settings
- [x] Vim/Emacs keybinding preference in appearance settings (`@replit/codemirror-vim`, `@replit/codemirror-emacs`)
- [x] Vim/Emacs applies to code editor + extension editor only (never chat input)
- [x] SW cache: exclude `vendor/codemirror/` with version bust
- [x] Update `debug.js` snapshot to include CM6 version
- [x] Documentation in ARCHITECTURE.md

**CI/CD**
- [x] Path-based change detection gating (FE-only → skip BE tests, docs-only → skip all)

---

## v0.17.3 — Notes Graph + Wikilinks ✅

Knowledge graph and bi-directional linking for the notes system. Transforms
notes from flat documents into an interconnected knowledge base with
Obsidian-style `[[wikilinks]]`, backlinks, graph visualization, and
transclusion.

Depends on: CodeMirror 6 (v0.17.2), Notes CRUD (v0.13.x). See
[DESIGN-0.17.3.md](DESIGN-0.17.3.md) for full spec.

**Backend: Note Links**
- [x] `note_links` table (Postgres + SQLite migrations)
- [x] `NoteLinkStore` interface + both implementations
- [x] `ExtractWikilinks()` regex parser + tests
- [x] Link extraction on note create/update
- [x] Dangling link resolution on note create
- [x] `notes.source_message_id` column for chat-to-note provenance

**API Endpoints**
- [x] `GET /notes/:id/backlinks` — notes linking to this note
- [x] `GET /notes/search-titles?q=` — lightweight fuzzy search for autocomplete
- [x] `GET /notes/graph` — full graph topology (nodes + edges + unresolved)

**CM6 Note Editor**
- [x] `note-editor.mjs` factory: markdown live preview (heading sizes, blockquotes, code blocks)
- [x] `wikilink.mjs` CM6 extension: parse, decorate as clickable chips, autocomplete on `[[`
- [x] Textarea → CM6 swap in `notes.js` with graceful fallback
- [x] `noteEditorTheme` in `theme.mjs`

**Graph Visualization**
- [x] Canvas-based force-directed graph (~200 lines, no d3 dependency)
- [x] O(n²) Coulomb repulsion + Hooke spring + center gravity
- [x] Pan, zoom (0.15–4.0x), drag nodes, hover highlight, click to open
- [x] Node sizing by link_count, folder-based coloring
- [x] Ghost nodes for unresolved `[[links]]` with toggle
- [x] Energy-based pause (stop rAF below KE threshold)
- [x] ResizeObserver for responsive canvas

**Note-from-Selection + Daily Notes**
- [x] "Note" button in message action bar
- [x] Selection-aware capture (`window.getSelection()` within message)
- [x] Provenance tracking (`source_channel_id` + `source_message_id`)
- [x] "Today" button → find-or-create daily note in `/daily/`

**Read Mode**
- [x] `[[link]]` rendered as clickable wikilink chips
- [x] `![[embed]]` transclusion with async content fetch, recursion guard
- [x] Backlinks panel (collapsible, with count badge)

---

## v0.18.0 — Memory (User + Persona Scopes) ✅

Long-term memory across conversations, with scope-aware isolation.
Unlike KB (static documents) or compaction (within-conversation), this
captures facts and preferences that persist across channels.

**Key differentiator: Persona-scoped memory.** Each Persona can build
its own memory independently of user memory, preventing cross-context
contamination and enabling specialized knowledge accumulation.

Depends on: compaction (v0.15.0), knowledge bases (v0.14.0), persona-KB binding (v0.17.0), SQLite backend (v0.17.1).

See [DESIGN-0.18.0.md](DESIGN-0.18.0.md) for full spec.

**Memory Scopes**
- [x] `user` — personal facts/preferences, persists across all conversations (current industry standard)
- [x] `persona` — shared across all users of that Persona, accumulated from conversations (differentiator)
- [x] `persona+user` — per-user within a Persona context (e.g. tutoring progress per student)
- [x] Configurable per Persona: which scopes are active, whether user memory is passed through

**Data Model**
- [x] `memories` table: `id`, `scope` (user, persona, persona_user), `owner_id` (user_id or persona_id), `user_id` (nullable — for persona+user scope), `key`, `value`, `source_channel_id`, `confidence` (float), `status` (active, pending_review, archived), `created_at`, `updated_at`
- [x] Index on `(scope, owner_id, user_id)` for fast lookup
- [x] Embedding column (`vector(3072)`) for semantic memory search

**Tools**
- [x] `memory_save` tool: LLM explicitly stores a fact
    - Scope-aware: saves to the active memory scope for the current Persona context
    - Structured: `key` (short label) + `value` (detail) + `confidence`
- [x] `memory_recall` tool: LLM queries stored facts relevant to current context
    - Merges results from applicable scopes (user + persona + persona+user)
    - Semantic search via embeddings + keyword fallback

**Automatic Extraction**
- [x] Background job: post-conversation analysis identifies memorable facts (opt-in)
- [x] Configurable extraction prompt per Persona (e.g. "extract FAQ-worthy Q&A pairs")
- [x] Extracted memories start in `pending_review` status

**Review Pipeline**
- [x] Admin/team admin review queue: pending memories with approve/reject/edit
- [x] Persona memory review: team admins see what Personas are "learning"
- [x] User memory review: users see their own memories in Settings

**Memory Injection**
- [x] At completion time: inject relevant memories into system prompt
- [x] Context budget aware: limit injected memory tokens
- [x] Scope priority: persona+user > persona > user (most specific wins on conflicts)

**Use Cases**
- [x] Helpdesk Persona: builds FAQ from repeated questions, human-reviewable
- [x] Roleplay Persona: isolated memory prevents tone/context bleeding from user's other conversations
- [x] Tutoring Persona: tracks per-student progress, common misconceptions
- [x] Sales Persona: accumulates objection responses from real conversations
- [x] Onboarding Persona: learns what new hires commonly struggle with

**User Controls**
- [x] Settings → Memory: view, edit, delete personal memories
- [x] Per-Persona memory toggle: "Allow this persona to remember things about me"
- [x] Admin: enable/disable, retention policies, per-team scoping
- [x] Privacy: user memories never cross team boundaries; persona memories respect group access

---

## v0.18.1 — Side Panel Architecture ✅

Refactored the side panel from a shared-tab layout to independent
single-slot panels. Any action fills the slot, replacing whatever was
there — no tabs, no association between panel types. Extension UI
primitives give browser extensions safe access to host app features.

Depends on: Notes graph + wikilinks (v0.17.3). Prerequisite for: live
collaboration (v0.23.0) where multiple panels need simultaneous visibility.

**Panel System**
- [x] Panel registry: named panels with independent open/close state
- [x] Single-slot model: action-driven, each open replaces the previous
- [x] Dual-view mode: two panels side-by-side (configurable split ratio via drag divider)
- [x] Panel-persistent state: each panel remembers scroll position, open note, etc.
- [x] Header label shows active panel name (replaces tab bar)
- [x] Keyboard shortcut: Ctrl/Cmd+\ cycles panels, Ctrl/Cmd+Shift+\ toggles dual

**Preview Panel**
- [x] Dedicated preview panel (independent of Notes)
- [x] HTML/document preview with iframe sandbox
- [x] Code output preview (extension-rendered blocks via pop-out)
- [x] Live-updating during streaming responses (500ms debounce)

**Extension UI Primitives**
- [x] `ctx.ui.toast()` — toast notifications
- [x] `ctx.ui.openPreview()` — load HTML into side panel preview
- [x] `ctx.ui.isDark()` — theme detection
- [x] `ctx.ui.isMobile()` — viewport width check
- [x] `ctx.ui.isPanelOpen()` — side panel visibility
- [x] `ctx.ui.confirm()` — modal confirm dialog
- [x] Mermaid extension refactored to use primitives exclusively (zero direct globals)
- [x] Context-aware expand: single button, fullscreen or side panel pop-out

**Mobile**
- [x] Swipe navigation between panels
- [x] Tap-to-close overlay
- [x] Graceful collapse to single-panel on narrow viewports
- [x] Enlarged touch targets, fullscreen close button (40px desktop, 48px mobile)

---

## ✅ v0.19.0 — Projects / Workspaces

Organizational containers that group related conversations, KBs, and
notes into a single workspace with shared access controls.

Depends on: user groups (v0.16.0), knowledge bases (v0.14.0).

**Data Model**
- [x] `projects` table: `id`, `name`, `description`, `color`, `icon`, `scope` (personal/team/global), `owner_id`, `team_id`, `is_archived`, `settings` (JSONB)
- [x] `project_channels` junction: ordered membership with UNIQUE `channel_id`, denormalized FK on `channels.project_id`
- [x] `project_knowledge_bases` junction: with `auto_search` flag
- [x] `project_notes` junction
- [x] `resource_grants` CHECK extended to include `'project'`

**Features**
- [x] Channels can belong to a project (optional `project_id` FK)
- [x] KBs can belong to a project → auto-linked to all channels in the project (BuildKBHint + kbsearch)
- [x] Notes can belong to a project → auto-associated when created from project channel
- [x] KB resolution chain: Persona → Project → Channel → Personal
- [x] Project sidebar section: collapsible groups above time-based Recent section

**UI**
- [x] Sidebar: projects as collapsible groups with color dots, counts, options menu
- [x] Right-click context menu: move chat to project / remove / create-and-assign
- [x] Drag-and-drop channels between project groups and Recent
- [x] New Project button in split dropdown
- [x] Channel list `project_id` filter (`?project_id=uuid` or `?project_id=none`)
- [x] Collapse state persisted in localStorage

**Deferred to later versions:**
- Project creation dialog (name, description, default Persona, default KBs) — using prompt() for now
- ~~Project detail panel (KB + note management within a project)~~ → v0.19.1
- ~~Project-level Persona default~~ → v0.19.2
- Folders within projects
- Project templates
- Admin-level team/global project management
- Project-specific files (full project-level upload — deferred for proper architecture)
- Notifications (moved to v0.20.0)

---

## ✅ v0.19.1 — Active Project + Project System Prompt + Detail Panel

Quality-of-life improvements making projects usable for daily workflow.
No migrations.

Depends on: v0.19.0 Projects/Workspaces.

**Active Project**
- [x] Pin a project as active — new chats auto-assign via `addChannelToProject` after channel creation
- [x] `App.activeProjectId` persisted in `localStorage('cs-active-project')`
- [x] Visual indicator: `.project-group.active` accent border + 📌 icon
- [x] Toggle from project ⋯ menu ("Pin as active" / "Unpin project")
- [x] Cleared on project delete

**Project System Prompt**
- [x] Per-project instructions in `projects.settings` JSONB (`system_prompt` key)
- [x] Injected in `loadConversation` between persona/channel prompt and KB hint
- [x] Merge semantics on update: reads existing JSONB, overlays patch keys, writes back
- [x] Both Postgres and SQLite stores updated

**Project Detail Panel**
- [x] Registered with `PanelRegistry` (same pattern as Notes, Preview)
- [x] System prompt textarea with Save button
- [x] KB management: list with names (LEFT JOIN), [+ Add] picker dropdown, [✕] remove
- [x] Notes list with titles (LEFT JOIN), [✕] unbind
- [x] Accessible from project ⋯ menu → "Project settings"

**Bug Fixes**
- [x] `renderChatList` early return prevented project groups from rendering when zero chats
- [x] Recent section drop target: "Drop chats here to unassign" hint with `min-height: 40px`

---

## ✅ v0.19.2 — Project Persona Default + Archive + Channel Reorder

Completes the project management feature set. No migrations.

Depends on: v0.19.1.

**Project Persona Default**
- [x] Bind a persona to a project via detail panel dropdown
- [x] Stored in `projects.settings.persona_id`
- [x] Resolution chain: explicit `req.PresetID` → project persona → none
- [x] Inherits model, provider config, temperature, max tokens, system prompt as defaults

**Project Archive Toggle**
- [x] Checkbox in project detail panel
- [x] Archived projects hidden from sidebar by default
- [x] "▸ Show archived (N)" toggle button below project list
- [x] Chats in hidden-archived projects spill into Recent section (never vanish)
- [x] Dimmed + italic visual treatment for archived groups

**Channel Reorder**
- [x] `loadProjectChannelPositions()` fetches server-persisted positions on startup
- [x] `_getReorderedProjectChats()` sorts project chats by stored position
- [x] Right-click context menu: ↑ Move up / ↓ Move down (optimistic swap + API call)
- [x] Order maintained across add/remove/move operations

---

## v0.20.0 — Notifications + @mention Routing + Multi-model ✅

Notification infrastructure that makes collaboration real-time, plus
multi-model routing for channels. Three-phase delivery: notifications
core (in-app), @mention parsing + fan-out completions, email transport
+ per-user notification preferences.

_(Notifications shifted from v0.19.0; @mention routing shifted from v0.17.0)_

Depends on: v0.19.2 (Projects complete), EventBus + WebSocket hub (v0.9.x),
`channel_models` table (v0.16.0 schema). See
[DESIGN-0.20.0.md](DESIGN-0.20.0.md) for full spec.

**Phase 1 — Notifications Core (in-app)**
- [x] `notifications` table: `id`, `user_id`, `type`, `title`, `body`, `resource_type`, `resource_id`, `is_read`, `created_at` (Postgres + SQLite migrations)
- [x] `NotificationStore` interface + both implementations: Create, ListByUser (paginated), MarkRead, MarkAllRead, Delete, UnreadCount
- [x] `notifications.Service` with `Notify()` / `NotifyMany()` — centralized creation + WebSocket dispatch
- [x] EventBus routes: `notification.new`, `notification.read` (badge sync across tabs)
- [x] API endpoints: list, unread-count, mark-read, mark-all-read, delete (5 endpoints, all user-scoped)
- [x] Initial sources: `role.fallback` (EventBus subscription), `kb.ready`/`kb.error`, `grant.changed`
- [x] Frontend: bell icon + unread badge (capped 9+), notification dropdown (latest 10, click-to-navigate), full panel via PanelRegistry
- [x] WebSocket handler: real-time push + toast for high-priority types
- [x] Background cleanup goroutine (configurable retention, default 90 days)

**Phase 2 — @mention Parsing + Multi-model Routing**
- [x] `mentions.Parse()`: extract @mentions, resolve against channel model roster (case-insensitive, longest-match-first, trailing punctuation tolerant)
- [x] Parser tests: no mentions, unresolved, multiple, overlapping names, punctuation edge cases
- [x] Channel model CRUD handlers (4 endpoints: list, add, remove, update)
- [x] Completion handler: mention extraction → target resolution → sequential fan-out → N assistant messages
- [x] SSE streaming: model attribution in delta events (`model_display` field)
- [x] Frontend: model pills in chat header with add/remove UI
- [x] Frontend: @mention autocomplete via CM6 `mentionCompletion` extension (roster-backed)
- [x] Frontend: model attribution labels on multi-model assistant messages
- [x] Backward compat: channels with no extra `channel_models` rows work exactly as before

**Phase 3 — Email Transport + Notification Preferences**
- [x] `notification_preferences` table (Postgres + SQLite): per-user, per-type in_app/email toggles
- [x] Preference resolution chain: specific type → user wildcard `*` → system default (in_app=true, email=false)
- [x] `EmailTransport`: SMTP with implicit TLS (port 465) and STARTTLS (port 587), multipart MIME (HTML + plaintext)
- [x] Email templates: branded HTML + plaintext, domain-prefixed subjects, Go `html/template`
- [x] Preference CRUD endpoints (3: list, set with partial patch, delete)
- [x] Admin SMTP configuration: enable toggle, host/port/user/password/from/TLS fields, test email button
- [x] User notification preferences UI (Settings → Notifications tab, per-type checkboxes)
- [x] Notification service: preference check before dispatch, async email delivery (goroutine, 30s timeout)
- [x] Tests: preference resolution chain (7 tests), email templates (4 tests)
- [x] Deferred: digest mode (batched email), SMTP password vault encryption

**Hotfix (carried forward from v0.19.2 investigation)**
- [x] `user_model_settings` NULL scan fix: COALESCE on `hidden`/`sort_order` in SELECT + INSERT for both dialects
- [x] Gin release mode: auto-set `gin.ReleaseMode` when `ENVIRONMENT=production`, suppress health check log spam

---

## v0.21.x — Workspace Platform + Extension Surfaces

Seven-release series decomposing workspace storage, tools, indexing, git,
surface infrastructure, and editor/article modes. Full specification in
[DESIGN-0.21.0.md](DESIGN-0.21.0.md).

Depends on: file storage (v0.12.0), extension foundation (v0.11.0),
CodeMirror 6 (v0.17.2), knowledge base embedding pipeline (v0.14.0).

### v0.21.0 — Workspace Storage Primitive ✅

Pure backend foundation. Workspaces as a polymorphic platform primitive
owned by users, projects, channels, or teams. Dual-layer storage: PVC
filesystem (source of truth) + DB metadata index (queryable cache).

- [x] `workspaces` table: polymorphic owner (user/project/channel/team), root_path, max_bytes quota, status
- [x] `workspace_files` table: metadata index (path, MIME type, size, sha256, is_directory)
- [x] `WorkspaceStore` interface: CRUD, file index (upsert/delete/list/prefix), ownership lookup, stats
- [x] Postgres + SQLite store implementations with upsert-on-conflict for file index
- [x] `workspace.FS` package: read, write (atomic via temp+rename), delete, mkdir, stat, list, reconcile (FS→DB sync)
- [x] `workspace.Archive`: zip/tar.gz extract + create with archive bomb protection (10K file limit, 100MB single file, quota enforcement), common-prefix stripping
- [x] Content type detection: extension-based (40+ source code types) with http.DetectContentType fallback
- [x] SHA256 computed during write via tee reader (enables content-addressed skip in v0.21.2)
- [x] Path traversal guards: cleanPath + absPath validation, symlink rejection
- [x] 15 API endpoints: workspace CRUD, file CRUD, archive upload/download, reconcile, stats
- [x] Owner-based authorization: user=self, channel=owner, project=member, team=member
- [x] Unit tests: path cleaning, traversal detection, MIME detection, unsafe path filtering, write/read round-trip, delete, mkdir

### v0.21.1 — Workspace Tools + Channel/Project Binding ✅

Make workspaces useful in chat mode via tool calls.

- [x] Workspace tools (category: workspace): `workspace_ls`, `workspace_read` (50KB soft limit), `workspace_write` (auto-mkdir), `workspace_rm` (recursive guard), `workspace_mv`, `workspace_patch` (find/replace)
- [x] `channels.workspace_id` FK, `projects.workspace_id` FK
- [x] Workspace resolution: channel workspace → project workspace (override pattern)
- [x] Tool injection in completion handler when workspace bound
- [x] Frontend: deferred (channel settings workspace toggle, project Files tab, chat bar workspace indicator)

### v0.21.2 — Workspace Indexing + Semantic Search ✅

Embed text files for semantic search. Reuses `knowledge.SplitText` + `knowledge.Embedder`.

- [x] `workspace_chunks` table (Postgres + SQLite migrations)
- [x] `workspace_files` additions: `index_status`, `chunk_count` columns
- [x] `workspaces.indexing_enabled` boolean (default true)
- [x] Store methods: `InsertChunks`, `DeleteChunksByFile`, `SimilaritySearch`, `UpdateFileIndexStatus`
- [x] Indexable types: text MIME types + 40+ code extensions + special filenames (Makefile, Dockerfile, etc.)
- [x] Code-aware chunking: 1500-char chunks with function/class/type/sub boundary separators
- [x] Content-addressed skip: sha256 comparison before re-chunking/re-embedding
- [x] `workspace.Indexer`: background goroutine with configurable concurrency semaphore
- [x] Single-file indexing via `FS.WriteFile` hook (`SetIndexer`)
- [x] Batch indexing via `ExtractArchive` (sequential within one semaphore slot)
- [x] `workspace_search` tool: semantic search with optional glob filtering, top-K capped at 20
- [x] `GET /api/v1/workspaces/:id/index-status` endpoint (per-status file counts, total chunks)
- [x] Configuration: `WORKSPACE_INDEXING_ENABLED`, `WORKSPACE_INDEX_CONCURRENCY` env vars
- [x] Graceful degradation when no embedding role configured
- [x] Mock store updated for FS unit tests

### v0.21.3 — Surface Infrastructure + REPL ✅

Pure UI architecture. No workspace dependency (parallel development).

- [x] `data-surface-region` attributes on index.html containers (surface-header, surface-main, surface-footer, sidebar-content)
- [x] `surfaces.js` — SurfaceRegistry: register, activate, deactivate, getCurrent, list
- [x] `ctx.ui.replace()` / `ctx.ui.restore()` with DOM preservation (DocumentFragment-based)
- [x] `ctx.surfaces.register()` API for extensions
- [x] Mode selector component in sidebar (`#modeSelectorWrap`, auto-shown when ≥2 surfaces)
- [x] Chat as default surface (implicit, always registered)
- [x] `surface.activated` / `surface.deactivated` / `surface.registered` / `surface.unregistered` EventBus events
- [x] REPL tab: AsyncFunction wrapper, global injection (API, Events, Extensions, Surfaces, DebugLog, Panels, UI, $, $$, sleep)
- [x] REPL tab: pretty-print results (collapsible JSON, type-colored primitives, DOM element summaries)
- [x] REPL tab: command history (sessionStorage, ↑/↓ navigation)
- [x] REPL tab: tab-completion on object graphs, event labels, globals
- [x] REPL tab: event label hints (Events.on/emit completion)
- [x] REPL tab: admin gate (admin role OR ?debug=1 URL param)
- [x] REPL tab: toolbar (clear, copy, help)
- [x] CSS: mode selector + REPL styles
- [x] Documentation in EXTENSIONS.md §6 updated with implementation details
- [ ] Mobile: mode selector collapses to hamburger/bottom nav — moved to v0.22+
- [ ] Integration with extension loader (surfaces from manifest — moved to v0.22+)

### v0.21.4 — Git Integration ✅

Layer git operations onto workspaces.

- [x] `workspaces` additions: git_remote_url, git_branch, git_credential_id, git_last_sync
- [x] `git_credentials` table: auth_type (https_pat/https_basic/ssh_key), encrypted_data (AES-256-GCM)
- [x] Postgres migration (012_v0214_git.sql) + SQLite migration (011_v0214_git.sql)
- [x] `models.GitCredential`, `GitCredentialSummary`, `GitStatus`, `GitFileStatus`, `GitLogEntry`
- [x] `GitCredentialStore` interface + Postgres/SQLite implementations (Create, GetByID, ListByUser, Delete)
- [x] `workspace.GitOps` with exec-based git operations (9 operations)
- [x] Credential injection: temp `GIT_ASKPASS` (PAT), `.git-credentials` (basic), `GIT_SSH_COMMAND` (SSH)
- [x] Security: reject file:// URLs, sanitize error output, GIT_TERMINAL_PROMPT=0
- [x] Clone, Pull, Push, Status, Diff, Commit, Log, BranchList, Checkout
- [x] `SetGitLastSync` on WorkspaceStore interface + both implementations
- [x] Post-operation reconcile + re-index hook (clone, pull, checkout)
- [x] 5 git tools: git_status, git_diff, git_commit, git_log, git_branch
- [x] Git tools filtered out when no workspace bound (completion handler)
- [x] 9 git API endpoints on /api/v1/workspaces/:id/git/*
- [x] 3 credential CRUD endpoints on /api/v1/git-credentials
- [x] Wired in main.go: GitOps creation, tool registration, route registration
- [ ] Workspace settings UI: git config section — moved to v0.22+
- [ ] User Settings: git credentials management UI — moved to v0.22+
- [ ] Integration tests: clone, commit, push/pull cycle (requires git binary in CI)
- [ ] `.gitignore` respect in workspace indexing — moved to v0.22+

### v0.21.5 — Editor Surface (Development Mode) ✅

IDE-like experience. Browser-tier surface consuming workspace primitives.

- [x] Layout: file tree (sidebar), code editor (CM6, main), AI chat panel (resizable split), status bar
- [x] File tree: workspace_ls backed, context menu (open, delete), nested directory expansion, file icons
- [x] Code editor: CM6 codeEditor() factory with textarea fallback, language auto-detection (20+ languages), tab bar, Ctrl+S save
- [x] AI chat panel: same channel context via Surfaces DOM borrowing, live update via workspace.file.changed event
- [x] Keyboard shortcuts: Ctrl+P quick open, Ctrl+S save, Ctrl+W close tab
- [x] Split pane layout with draggable resize handle
- [x] Status bar: file path, language mode, git branch
- [x] Workspace API methods on frontend (8 methods)
- [x] Surfaces.getSavedFragment/putSavedFragment for cross-surface DOM sharing
- [x] workspace.file.changed event emission from stream_loop.go after tool calls
- [x] Event route table: workspace.file. → DirToClient
- [x] editor-mode.css: dark theme, mobile responsive (stacked at ≤768px)
- [x] Auto-registration: surface registers when channel/project has workspace binding
- [x] Workspace management UI: create, list, bind from project settings + chat context menu
- [x] GET /workspaces list endpoint, workspace_id in channel API (update, get, list)
- [x] Git status indicators in file tree (M/A/U badges + colors) — closed in v0.21.6
- [ ] Drag-drop file reorder — moved to v0.22+
- [x] Ctrl+Shift+F workspace search (maps to quick open) — closed in v0.21.6
- [x] Auto-save on tab/mode switch — closed in v0.21.6

### v0.21.6 — Article Surface + Document Output Pipeline ✅

Writing-focused surface. Document is artifact, conversation is secondary.

- [x] Layout: outline (sidebar top), AI assistant (sidebar bottom), rich text editor (CM6 markdown, main)
- [x] Outline: auto-generated from headings, click-to-scroll (navigates CM6 cursor)
- [x] Rich text editor: CM6 codeEditor() markdown mode, line wrapping, centered content (720px max), textarea fallback
- [x] AI assistant: chat panel embedded in sidebar via Surfaces DOM borrowing
- [x] Export dropdown: Download Markdown, Download HTML (via marked.js + DOMPurify), Copy to clipboard
- [x] Document picker: lists .md/.mdx/.txt files from workspace, create new document inline
- [x] Focus mode: dims non-active lines in editor (CM6 opacity-based, toggle button)
- [x] Word count + reading time in status bar (~230 wpm)
- [x] Auto-save: debounced (2s) on content change, save on tab switch / surface deactivate
- [x] Live update on workspace.file.changed event (reloads if file unmodified)
- [x] Surface registration: registers as "Article" with file-text icon when workspace bound
- [x] article-mode.css: outline styling, export menu, focus mode, mobile responsive
- [x] (from v0.21.5 deferred) Git status indicators in editor file tree (M/A/U badges + colors)
- [x] (from v0.21.5 deferred) Auto-save on tab/mode switch in editor surface
- [x] (from v0.21.5 deferred) Ctrl+Shift+F workspace search (maps to quick open)
- [ ] Article-specific AI tools (suggest_outline, expand_section, check_citations) — moved to v0.22+
- [ ] Drag-to-reorder outline sections — moved to v0.22+
- [ ] PDF export via pandoc — moved to v0.22+

#### Bugfixes & DX (added post-initial)

- [x] Hash Router (`router.js`): URL-driven direct-to-surface navigation (#/chat, #/editor, #/article). Bookmarkable, back/forward works. Precursor to Go `html/template` server-rendered pages (v0.25.0) for public/workflow entry points.
- [x] `openDirect(wsId)` on EditorMode + ArticleMode: bypass check(), register directly with workspace ID
- [x] Workspace picker overlay: shown when navigating to surface without workspace, auto-selects if only one
- [x] `SEED_PROVIDERS` env var: auto-creates global providers on startup (dev/test only, idempotent)
- [x] Mode selector labels: icon + text, labels hide when sidebar collapsed
- [x] `chat.switched` / `chat.created` events: wired into selectChat(), newChat(), sendMessage()
- [x] `workspace_id` in channel API responses: added to list/get/create for both SQLite and Postgres
- [x] Frontend chat + project objects: now carry `workspace_id` for Router and surface check() optimization

---

### v0.21.7 — Bugfix: scanJSON Driver Buffer Aliasing ✅

Critical fix for intermittent 500 errors on paginated list endpoints.

- [x] `scanJSON` []byte path: copy driver buffer instead of aliasing (`json.RawMessage(v)` → `make+copy`)
- [x] `CreateChannel` Postgres RETURNING path: use `scanJSON(&ch.Settings)` instead of bare `&ch.Settings`
- [x] `UpdateChannel` settings write: validate `json.Valid()` before JSONB merge, reject with 400
- [x] New tests: `TestScanJSON_ByteSliceNotAliased`, `TestScanJSON_ByteSliceIsolation_MultiRow`

**Root cause:** `json.RawMessage(v)` is a type conversion, not a copy — it aliases
the `[]byte` buffer owned by `database/sql`. Per the `Scanner` interface docs, `[]byte`
values are only valid until the next `rows.Scan()` call. In paginated list endpoints,
row N's Settings slice header pointed into memory that row N+1's scan overwrote,
producing corrupt JSON that passed `json.Valid()` at scan time but failed at marshal
time when `SafeJSON` serialized the full response.

---

## v0.22.0 — Provider Health + Capability Overrides ✅

Smallest shippable unit that unblocks routing. Track provider error rates
and latency, add admin override layer for model capabilities.

Depends on: usage tracking (v0.10.0), capabilities resolver (v0.9.1).

**Provider Health Tracking**
- [x] `provider_health` table: `provider_config_id` FK, `window_start` (hourly buckets), `request_count`, `error_count`, `timeout_count`, `total_latency_ms`, `max_latency_ms`, `last_error`, `last_error_at`
- [x] In-memory health accumulator: goroutine-safe counters per provider, flushed to DB on interval (60s)
- [x] `RecordSuccess`/`RecordError`/`RecordTimeout` called from completion handler after every provider call (sync + streaming)
- [x] Provider status derivation: `healthy` / `degraded` / `down` based on error rate thresholds (5% = degraded, 25% = down)
- [x] `GET /api/v1/admin/providers/:id/health` endpoint: current status + hourly windows (configurable hours param)
- [x] `GET /api/v1/admin/providers/health` endpoint: summary status for all providers
- [x] _(shipped v0.22.3)_ Health status included in `GET /api/v1/models/enabled` response (new `provider_status` field)
- [ ] _(deferred → v0.22.2)_ Auto-disable: optional policy to mark provider inactive when `down` for N consecutive windows
- [x] Background cleanup: prune health rows older than 7 days (6-hour cycle)

**Capability Admin Overrides**
- [x] `capability_overrides` table: `provider_config_id` (nullable for global), `model_id`, `field`, `value`, `set_by`, `created_at`
- [x] Three-tier resolution in `ResolveIntrinsic()`: catalog → heuristic → **admin override** (highest priority)
- [x] `PUT /api/v1/admin/models/:id/capabilities` endpoint: set individual capability overrides
- [x] `DELETE /api/v1/admin/models/:id/capabilities/:overrideId` endpoint: remove override
- [x] `GET /api/v1/admin/models/:id/capabilities` endpoint: show resolved caps with source annotation (catalog/heuristic/override)
- [x] `GET /api/v1/admin/capability-overrides` endpoint: list all overrides
- [x] Postgres + SQLite migrations (013_v0220_health.sql / sqlite 012_v0220_health.sql)

**Search Provider Health** _(deferred → v0.22.1)_
- [ ] `RecordOutcome` for web_search and url_fetch tool calls (search provider tracking)
- [ ] Rate limit tracking per provider config (requests/minute counter in health accumulator)

**Workspace Pane Refactor** (frontend)
- [x] `.workspace` flex container replaces `.chat-area` + `.side-panel` layout
- [x] `.workspace-primary` / `.workspace-secondary` independent pane model
- [x] `.workspace-handle` drag-resize between panes (replaces old side-panel-resize)
- [x] Surface layout declarations: `primary`, `secondary`, `secondaryOpts` per surface
- [x] Dual-view mode removed (was `_dualMode`, `_splitRatio`, `_secondary` in PanelRegistry)
- [x] Zoom selector updated for new class names

**Bugfix (v0.21.7)**
- [x] `scanJSON` driver buffer aliasing — copy `[]byte` from database driver instead of aliasing slice header
- [x] `CreateChannel` RETURNING path uses `scanJSON(&ch.Settings)` instead of bare scan
- [x] `UpdateChannel` validates `json.Valid()` on settings before JSONB merge

---

## v0.22.1 — Provider Extensions (Declarative Config) ✅

Replace hardcoded provider-specific behavior with data-driven configuration.
Same model, different provider → different parameters.

Depends on: provider health (v0.22.0).

**Provider Profiles**
- [x] Profile schema per provider type: defines available settings, types, defaults, validation, dependency rules (`providers/profile.go`)
- [x] Built-in profile schemas for: openai, anthropic, venice, openrouter (unknown types fall back to openai-compatible)
- [x] System prompt injection: `system_prompt_prefix` setting prepended to first system message (all providers)
- [x] Thinking mode toggle: Anthropic `extended_thinking` + `thinking_budget` → injects `thinking.type=enabled`; Venice `enable_thinking` → disables Venice system prompt
- [ ] _(not needed)_ `provider_profiles` JSONB column — existing `provider_configs.settings` JSONB already serves this purpose

**Request/Response Transforms**
- [x] `Hooks.PreRequest(cfg, req)` — decorate request before dispatch (system prompt prefix, ExtraBody injection)
- [x] `Hooks.PostStreamEvent(cfg, event)` — normalize provider-specific response fields (Venice thinking extraction)
- [x] Hook implementations: Venice web search + thinking, Anthropic extended thinking + beta header, OpenRouter routing preferences + require_parameters, OpenAI frequency/presence penalty
- [x] Preset-level overrides: `MergePresetSettings()` merges persona overrides onto provider settings, respecting `ProviderOnly` fields
- [x] `ProviderConfig.Settings` fully utilized: hooks read from config, not hardcoded switch statements
- [x] `CompletionRequest.ExtraBody` field + `mergeExtraBody()` for provider-specific wire fields
- [x] Anthropic extended thinking stream support: `thinking_delta` content blocks → `Reasoning` field

**Provider Type Registry**
- [x] `providers.Init()` uses `RegisterType()` with full metadata (name, description, default endpoint, profile schema)
- [x] `ProviderTypeMeta` struct: ID, name, description, default_endpoint, profile_schema
- [x] `GET /api/v1/admin/provider-types` endpoint: list available provider types with their profile schemas
- [ ] _(deferred → v0.23)_ New provider types registrable via config file (openai-compatible endpoint + custom profile schema)

**Search Provider Health** (carried from v0.22.0)
- [ ] _(deferred → v0.22.2)_ `RecordOutcome` for web_search and url_fetch tool calls
- [ ] _(deferred → v0.22.2)_ Rate limit tracking per provider config

---

## v0.22.2 — Routing Policies + Fallback Chains ✅

Rules-based routing engine — policy, not ML. Evaluated after `resolveConfig()`
between "what model was requested" and "which provider serves it."

Depends on: provider health (v0.22.0), provider extensions (v0.22.1).

**Routing Policies**
- [x] `routing_policies` table: `id`, `name`, `scope` (global/team), `team_id`, `priority` (lower wins), `policy_type`, `config` (JSONB), `is_active`
- [x] Policy types: `provider_prefer` (ordered fallback list), `team_route` (restrict team to providers), `cost_limit` (heuristic cost cap), `model_alias` (alias → provider+model rewrite)
- [x] Policy evaluator: takes requested model + user context → returns ranked list of `(providerConfigID, modelID)` candidates
- [x] Integration point: `evaluateRouting()` called after resolveConfig, before provider dispatch — reloads winning config credentials on switch
- [ ] _(deferred → v0.23)_ `capability_match` policy type ("cheapest model with tool_calling")

**Fallback Chains**
- [x] Fallback runner: `RunWithFallback()` tries candidates in order with configurable max retries
- [x] Health-aware: skip providers with status `down`, prefer `healthy` over `degraded`
- [x] All-down graceful degradation: keeps candidates rather than failing
- [ ] _(deferred → v0.23)_ Latency-aware preference: track response time percentiles, prefer faster providers
- [ ] _(deferred → v0.23)_ Cost-aware preference with budget ceiling per request

**Routing Metadata**
- [x] `X-Switchboard-Provider` response header: `providerID/configID` on every completion
- [x] `X-Switchboard-Route` header: policy name when routing is active
- [x] `routing_decision` JSONB column on `usage_log`
- [x] `POST /api/v1/admin/routing/test` endpoint: dry-run policy evaluation with live health status
- [x] Admin CRUD: GET/POST/PUT/DELETE `/api/v1/admin/routing/policies`

---

## v0.22.3 — Provider Admin UI + Deferred Polish ✅

Admin interfaces for v0.22.0–v0.22.2 features, plus deferred items from v0.21.x.

Depends on: routing policies (v0.22.2).

**Admin UI**
- [x] Provider health dashboard: status badges, error rate/latency/timeout metrics per provider, refresh button (`loadAdminHealth`)
- [x] Capability override editor: table listing all overrides with model, config, field, value; delete button per row (`loadAdminCapabilities`)
- [x] Routing policy builder: CRUD with name/priority/type/scope/team/config(JSON)/active toggle, dry-run test panel with candidate ranking and health status (`loadAdminRouting`, `_showRoutingForm`, `_runRoutingTest`)
- [x] New "Routing" admin category with Health, Routing, and Capabilities sections
- [x] Health status included in `GET /api/v1/models/enabled` response (`provider_status` field on UserModel)
- [ ] _(deferred → v0.23)_ Provider profile editor: key-value config per provider type, preview of effective settings
- [ ] _(deferred → v0.23)_ Fallback chain visualizer: drag-to-reorder provider priority per model family

**Deferred from v0.22.0–v0.22.2**
- [ ] _(deferred → v0.23)_ `capability_match` policy type ("cheapest model with tool_calling")
- [ ] _(deferred → v0.23)_ Latency-aware preference: track response time percentiles, prefer faster providers
- [ ] _(deferred → v0.23)_ Cost-aware preference with budget ceiling per request
- [ ] _(deferred → v0.23)_ New provider types registrable via config file (openai-compatible endpoint + custom profile schema)

**Deferred from v0.21.x**
- [ ] _(deferred → v0.23)_ Mobile: mode selector collapses to hamburger/bottom nav
- [ ] _(deferred → v0.23)_ Integration with extension loader (surfaces from manifest.json)
- [ ] _(deferred → v0.23)_ Workspace settings UI: git config section in channel/project settings
- [ ] _(deferred → v0.23)_ User Settings: git credentials management UI
- [ ] _(deferred → v0.23)_ `.gitignore` respect in workspace indexing
- [ ] _(deferred → v0.23)_ Drag-drop file reorder in editor file tree
- [ ] _(deferred → v0.23)_ Article tools: suggest_outline, expand_section, check_citations (AI-powered)
- [ ] _(deferred → v0.23)_ Drag-to-reorder outline sections in article mode
- [ ] _(deferred → v0.23)_ PDF/DOCX export via pandoc in article mode
- [ ] _(deferred → v0.23)_ Project-specific file uploads (full project-level upload architecture)

---

## v0.23.0 — Multi-Participant Channels + Presence

The channel foundation for workflows and real-time collaboration. Today
channels are single-user direct chats. This release makes channels a
shared space that multiple actors can inhabit.

Depends on: extension surfaces (v0.21.3), WebSocket infrastructure (already exists).

_(Shifted from v0.20.0 — no content changes)_

**Channel Participants**
- [ ] `channel_participants` table: `channel_id`, `participant_type` (user, session, persona), `participant_id`, `role` (owner, member, observer), `joined_at`
- [ ] Channel access queries rewritten: `WHERE user_id = $1` → participant-based lookup
- [ ] Backward compatible: existing single-user channels auto-have one participant (owner)
- [ ] Channel `type` column: `direct` (current), `group` (multi-user), `workflow` (staged)

**Presence + Real-time**
- [ ] Typing indicators and presence (who's online, who's in this channel)
- [ ] Participant join/leave events via WebSocket
- [ ] Co-editing for extension surfaces (editor mode, article mode)
- [ ] Cursor sharing, selection highlighting

**Channel-Scoped Notes**
- [ ] Optional `channel_id` FK on notes (NULL = personal notebook, set = channel-attached)
- [ ] Notes created by tool calls within a channel auto-attach to that channel
- [ ] Channel notes visible to all participants (not just creator)

---

## v0.24.0 — Auth Strategy (mTLS/OIDC) + Full RBAC

Enterprise auth modes on top of the teams + groups foundation.
Depends on: vault passphrase support from v0.9.3 (conditional unlock for
personal providers under external auth), user groups (v0.16.0).

_(Shifted from v0.21.0 — enhanced with group-based permissions)_

- [ ] `AUTH_MODE` env var: `builtin` | `mtls` | `oidc`
- [ ] All three resolve to the same internal user model
- [ ] `auth_source` + `external_id` columns on users table
- [ ] mTLS: header trust, auto-provision from cert DN
- [ ] OIDC: Keycloak/Okta token validation, claim extraction, role mapping
- [ ] Per-source auto-activate policy: auto_activate, default_team, default_role
- [ ] Vault passphrase prompt for mTLS/OIDC users (v0.9.3 conditional unlock)
- [ ] Fine-grained permissions: model access, KB write, task create, token budgets
- [ ] Group-based permission sets: assign permissions to groups, users inherit from membership
- [ ] OIDC claim → group mapping: external IdP groups auto-sync to internal groups

**Anonymous / Session Participants**
- [ ] Session-based identity for unauthenticated visitors (workflow intake)
- [ ] mTLS: cert CN/fingerprint as stable session identity (no `users` row required)
- [ ] Non-mTLS: ephemeral session token (cookie or URL token)
- [ ] Session participants can send messages and trigger tool calls within workflow channels
- [ ] Session identity visible to assigned team members (cert DN, display name, or "Anonymous #N")

---

## v0.25.0 — Workflow Engine

Team-owned, stage-based process execution. The channel is the runtime,
personas drive each stage, and the existing tool/notes infrastructure
handles structured data collection. See [ARCHITECTURE.md — Workflow
Architecture](ARCHITECTURE.md#workflow-architecture-future--v0210) for
the conceptual model.

Depends on: multi-participant channels (v0.23.0), anonymous identity (v0.24.0).

_(Shifted from v0.22.0 — no content changes, dependency refs updated)_

**Workflow Definitions**
- [ ] `workflows` table: `team_id` (owner), `name`, `description`, `is_active`, `entry_mode` (public_link, team_internal, api)
- [ ] `workflow_stages` table: `workflow_id`, `ordinal`, `persona_id`, `assignment_team_id`, `form_template` (JSONB, structured note schema), `transition_rules` (JSONB)
- [ ] Team-admin CRUD: create/edit/delete workflows and stages
- [ ] Workflow versioning: edits create new version, active instances continue on their version

**Workflow Instances (Channels)**
- [ ] Workflow-typed channels: `type='workflow'`, `workflow_id`, `current_stage`, `stage_version`
- [ ] Entry point: public link generates workflow channel, adds anonymous participant, binds stage 1 persona
- [ ] Stage transitions: triggered by AI (tool call), team member (button), or rule (form complete)
- [ ] On transition: swap active persona, notify assignment team, update channel metadata

**Go Template Pages (server-rendered entry points)**

Server-rendered HTML via Go `html/template` for pages that need SEO, OG tags,
pre-auth rendering, or standalone access outside the SPA shell. Same binary,
same auth middleware, same deployment — no second language runtime.

- [ ] Template engine: Go `html/template` with shared base layout, partials for nav/footer
- [ ] Route layer: `/w/:slug` for public workflow pages, `/p/:id` for shared project views
- [ ] Workflow landing page: branded intake form, persona avatar, description — rendered server-side
- [ ] Shared/embedded surface pages: `/s/editor/:wsId`, `/s/article/:wsId/:path` — server renders minimal shell, JS hydrates surface directly (replaces hash routing for public links)
- [ ] Template data: workflow metadata, branding, environment banner — injected server-side
- [ ] Auth gating: public pages skip auth, team pages require session, admin pages require role
- [ ] Precursor: hash router (v0.21.6) handles in-app navigation; Go templates handle external/public entry points

**AI Intake**
- [ ] Stage persona drives structured collection via system prompt
- [ ] Form template → persona system prompt injection ("collect these fields: ...")
- [ ] Tool calls create channel-scoped notes as form responses
- [ ] AI determines readiness: "I have everything needed" → trigger transition

**Assignment + Queue**
- [ ] Assignment queue: unassigned workflow channels visible to team members
- [ ] Claim model: team member claims channel (becomes participant with `member` role)
- [ ] Round-robin / rule-based auto-assignment (optional, per-stage config)
- [ ] Assignment notifications via WebSocket + optional webhook

**Team Member Collaboration**
- [ ] Assigned member sees full history (AI intake + artifacts + notes)
- [ ] Persona remains active — assists both visitor and team member
- [ ] Member can trigger stage transitions, add notes, invoke tools
- [ ] Member can reassign to different team member or escalate to different team

**Frontend**
- [ ] Workflow builder UI in team admin panel (stage list, persona picker, form template editor)
- [ ] Queue view: sidebar section showing unassigned workflow channels for user's teams
- [ ] Channel header: workflow stage indicator, transition controls
- [ ] Anonymous visitor view: minimal UI, persona-driven conversation (public link entry)

---

## v0.26.0 — Tasks / Autonomous Agents

Unattended execution — workflows without a human in the loop.
Depends on: workflow engine (v0.25.0).

_(Shifted from v0.23.0 — no content changes, dependency refs updated)_

- [ ] Scheduler + task runner (cron-like triggers for workflow instantiation)
- [ ] `task_create` tool (AI can spawn sub-workflows)
- [ ] `type: 'service'` channels: workflow instances with no human participants
- [ ] Execution budgets: max tokens, max tool calls, max wall-clock per stage
- [ ] Admin controls for resource limits and kill switches
- [ ] Completion webhooks (notify external systems when workflow completes)

---

## TBD (unscheduled — real features, no immediate need)

Items that are real but don't yet have a version assignment. Pull left
based on need.

**Extension System — Additional Extensions**
- Image generation tool (browser or sidecar, provider-agnostic)
- STT/TTS (browser extension, Web Speech API — personal use case)
- Code execution sandbox (server-side, container isolation)

**Extension System — Server Tiers**
- Starlark runtime integration (Tier 1 — server sandbox)
- Sidecar HTTP tool protocol (Tier 2 — container isolation)
- Server-side tool execution in completion handler

**Desktop + Mobile**
- Desktop app (Tauri)
- Full PWA with offline capability
- Mobile-optimized layouts (beyond current responsive)

**Data + Portability**
- Bulk export/import (account data, conversations, settings)
- ChatGPT/other tool import
- GDPR-style "download my data"
- Backup/restore CronJob manifests

**UX / Multi-Seat**
- "Group" scope badge on model selector / KB list: show access-source annotation so users see _why_ they have access (global, team, group grant). Requires backend to include `access_source` in list responses.
- Per-provider model preferences: `user_model_settings` unique key is `(user_id, model_id)` — same model from different providers shares one visibility toggle. Needs `provider_config_id` dimension in DB constraint, store, API, and frontend `hiddenModels` keying. Frontend composite key (`configId:modelId`) already exists in `App.models[].id`.

**Projects — Future**
- Project-specific files: full project-level upload (own endpoint, storage path, UI surface — not just channel attachment re-linking)
- Project templates: create new projects from predefined configurations (persona, KBs, system prompt)
- Project creation dialog: replace `prompt()` with proper modal (name, description, persona, KB picker)
- Admin-level project management: cross-instance visibility, reassign ownership, enforce team policies (scope: enterprise only, BYOK personal projects stay private)
- Sub-projects / nested hierarchy: child inherits parent KBs, system prompt, persona (deferred until usage patterns clarify need vs tags/labels)

**Knowledge Bases — Future**
- KB auto-injection: top-K chunk prepend to system prompt, context budget aware, per-channel toggle (latency budgeting required)
- Hybrid search: combine vector similarity with full-text `tsvector`, re-rank
- Semantic chunking: embedding-based boundary detection for smarter splits
- HNSW index: better query performance than IVFFlat for large datasets
- Web scraping source: ingest URLs as KB documents (extends url_fetch)
- Scheduled re-indexing: periodic rebuild when source documents update
- ~~Store cleanup: add `UpdateDocumentStorageKey()` to `KnowledgeBaseStore` interface~~ _(done in v0.17.0)_
  (currently uses direct `database.DB.ExecContext` in handler — works but bypasses store layer)

**Memory — Future**
- Memory compaction: summarize old memories to save context tokens
- Memory confidence decay: reduce confidence over time, prune low-confidence entries
- Memory export/import: portable memory format across instances
- Cross-persona memory sharing (opt-in): e.g. "coding assistant" can read facts from "project manager"
- Memory analytics: dashboard showing what Personas are learning, memory growth trends

**Platform**
- Rate limiting per user/team/tier (token budgets)
- ~~Provider health monitoring~~ → v0.22.0 + key rotation
- Multi-tenant SaaS mode
- Plugin/extension marketplace
- Virtual scroll for long conversations
- ~~SQLite backend option (single-user / dev)~~ → v0.17.1

**Pane Architecture (Workspace Container)**

The main area to the right of the sidebar is a **workspace container** that holds 1+ panes.
Each pane is a self-contained unit (chat, editor, notes, preview, etc.) with its own lifecycle.
Panes compose side-by-side rather than replacing each other (current surface model).

- Default layout: Left nav + single Chat pane (what exists today, zero cognitive overhead)
- Power user: Left nav + Editor pane + Chat pane side by side. Or Editor + Notes. Or all three.
- Workflow mode: Left nav hidden. Team member gets Chat + History. Customer gets just Chat.
- User-resizable splits between panes, layout persists per-project or per-user preference
- The current surface system was a stepping stone — proved out the region/activate/deactivate lifecycle. Panes reuse that contract but drop the "only one active at a time" constraint.
- Sidebar tabs (Chats / Files) already decouple navigation from center content (v0.21.6)

**Surfaces as Extensions**
- Surfaces (future: panes) become first-class extension types alongside block renderers and tool bridges
- `Surfaces.register()` already defines the contract: name, regions, activate/deactivate callbacks — swap hardcoded JS modules for manifest + dynamically loaded bundles
- Admin-installable surfaces: upload or enable from extension registry, scoped to team or global
- Surface manifest in `manifest.json`: declares required regions, capabilities, dependencies
- Surface IDE: built-in surface for building surfaces — Go template editor for server-rendered shells, JS/CSS editor for client behavior, live preview in sandboxed region. Eats its own dog food.
- Surface marketplace: share custom surfaces across instances (presentation mode, kanban, form builder, dashboard, etc.)
- Project-bound surface/pane defaults: project config specifies which panes are available and default layout