This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/ROADMAP.md
2026-03-07 19:58:43 +00:00

1627 lines
87 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.22.4 Provider Health UX + Project Files + Export ✅
v0.22.5 Surfaces + Go Templates + Admin Bug Fixes ✅
v0.22.6 Split Deployment Fix + CI Timeout + Code Pruning ✅
v0.22.7 Surface Prototypes (Theme + ChatPane + Splash) ✅
v0.22.8 Surface Integration (Wire into existing JS) ✅
v0.23.0 @mention Routing + Persona Handles + Proxy ✅
v0.23.1 Multi-User Navigation + Conversation Taxonomy ✅
v0.23.2 Multi-User Polish + Channel Lifecycle ✅
v0.24.0 Auth Abstraction + User Identity ← active
├── v0.24.1 mTLS + OIDC Providers (parallel)
│ │
│ └── v0.24.3 Anonymous Sessions
└── v0.24.2 Fine-Grained Permissions (parallel)
┌───────┴──────────────┐
│ │
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.154.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.0v0.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.0v0.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
- [x] _(shipped v0.22.4)_ PDF/DOCX export via pandoc in article mode
- [x] _(shipped v0.22.4)_ Project-specific file uploads (full project-level upload architecture)
---
## v0.22.4 — Provider Health UX + Project Files + Export ✅
Closes remaining provider infrastructure gaps and delivers UX features
deferred from earlier releases.
**Provider Infrastructure:**
- [x] Rate limit tracking per provider config (HTTP 429 classified separately)
- [x] Tool health tracking (web_search, url_fetch → `tool_health` table)
- [x] Auto-disable policy (N consecutive "down" windows → deactivate provider)
- [x] Search provider health tracking via tool health pipeline
- [x] SQLite health store parity (rate_limit_count, tool health, auto-disable)
- [x] Config: `PROVIDER_AUTO_DISABLE_THRESHOLD` env var (default: 3)
**UX:**
- [x] Workspace settings UI: git config section in project panel (remote URL, branch, credential)
- [x] Project-specific file uploads (POST/GET endpoints, project panel UI)
- [x] PDF/DOCX export via pandoc (POST /api/v1/export, editor menu integration)
- [x] Admin health dashboard: rate limit count display with warning color
**Frontend:**
- [x] 5 new API client methods (projectUploadFile, projectListFiles, exportDocument, updateWorkspace, listGitCredentials)
- [x] Health cards show rate limit counts
- [x] Git settings section appears when workspace is bound
---
## v0.22.5 — Surfaces + Go Templates + Admin Bug Fixes ✅
Replaces monolithic client-side rendering with server-rendered Go
templates organized as composable surfaces. Each page route renders a
dedicated surface that owns its full layout below the classification
banner. Reusable template components (`model-select`, `team-select`,
`file-upload`) eliminate duplicated form-building logic and fix data
population bugs by construction.
**Template Engine:**
- [x] `html/template` with `//go:embed` for compiled-in templates
- [x] Custom `FuncMap` (roleFilterType, toJSON, hasPrefix, dict)
- [x] CSP nonce generation per request
- [x] Data loaders per surface (pre-fetch exactly what templates need)
- [x] Base template with banner system (top + bottom, CSS custom properties)
**Surfaces:**
- [x] Chat surface (`/`, `/chat/:chatID`) — bridge to existing JS
- [x] Editor surface (`/editor/:wsId`) — server-rendered layout shell, fixes bugs #4 + #5
- [x] Notes surface (`/notes/:noteId`) — standalone with assist pane
- [x] Settings surface (`/settings/:section`) — full-page replaces modal
- [x] Admin surface (`/admin/:section`) — 5 categories, 10+ sections
- [x] Login page (`/login`) — standalone, cookie-based auth
**Reusable Components:**
- [x] `model-select` — type-filtered model dropdown (fixes bug #1)
- [x] `team-select` — pre-populated team picker (fixes bug #2)
- [x] `file-upload` — drop zone + file picker
**Auth:**
- [x] `AuthOrRedirect` middleware — cookie JWT, redirect to `/login`
- [x] `RequireAdminPage()` — role gate for admin pages
- [x] `sb_token` cookie sync on every token save/refresh
**Editor File Upload (bug #3):**
- [x] Upload button in toolbar + drag-and-drop on file tree
- [x] `uploadWorkspaceFile()` API method (binary-safe, respects quota)
- [x] Auto-open single uploaded text files
**Cleanup:**
- [x] Removed `/legacy` SPA fallback route
- [x] nginx.conf: page route proxy blocks before SPA fallback
---
## v0.22.6 — Split Deployment Fix + CI Timeout + Code Pruning ✅
Hotfix for FE/BE split k8s deployment broken by v0.22.5 surfaces
architecture, plus aggressive pruning of OBE client-side code now
replaced by server-rendered surfaces.
- [x] `BACKEND_URL` env var on FE container for page route proxying
- [x] Entrypoint generates `proxy_pass` blocks for all surface routes
- [x] Works with both `BASE_PATH=""` and `BASE_PATH="/prefix"`
- [x] CI: `-timeout 8m` on all `go test` invocations
- [x] Removed `router.js` (322 lines) — hash routing replaced by server routes
- [x] Removed `surfaces.js` (368 lines) — client registry replaced by Go templates
- [x] Replaced `index.html` SPA shell (1,296 → 16 lines) with redirect stub
- [x] Stripped old Surfaces paths from `editor-mode.js` (~260 lines)
- [x] Simplified `docker-entrypoint-fe.sh` — requires `BACKEND_URL`, removed SPA-only path
- [x] Consolidated `nginx.conf` proxy blocks for unified container
- [x] **Net: ~2,430 lines removed**
---
---
## v0.22.7 — Surface Prototypes (Theme + ChatPane + Splash) ✅
Foundation CSS/JS for the v0.22.5 surface architecture: theme system,
reusable chat pane component, splash/login redesign, and UI primitive
extensions. All files target the correct tree locations established in
v0.22.5. Settings surface gains BYOK and User Personas feature gates.
Depends on: surfaces + Go templates (v0.22.5), code pruning (v0.22.6).
**Theme System:**
- [x] `theme.css`: CSS custom properties for light/dark themes via `[data-theme]` attribute
- [x] System preference auto-detection (`prefers-color-scheme` media query)
- [x] Variable bridge: new vars (`--bg`, `--text`) alias old vars (`--bg-primary`, `--text-primary`)
- [x] Shared component styles: toast stack, badges, buttons, icon buttons, form fields, confirm dialog, theme toggle, avatar upload, section headers
- [x] Surface layout styles: splash, chat-pane, admin, settings, editor
- [x] Google Fonts: DM Sans (UI) + JetBrains Mono (code)
- [x] `base.html`: `data-theme` on `<html>`, `theme.css` before `styles.css`
**ChatPane Component:**
- [x] `chat-pane.js`: factory pattern — `ChatPane.create(opts)` returns self-contained instance
- [x] Instance API: renderMessages, appendChunk, finalizeStream, appendTyping, removeTyping, scrollToBottom, showWelcome, clear, getInputValue, setInputValue, focusInput, destroy
- [x] Lookup: `ChatPane.get(id)`, `ChatPane.forChannel(channelId)`, `ChatPane.active()`
- [x] `components/chat-pane.html`: Go template partial for server-rendered mount points
- [x] Editor surface: `chat-pane` template in assist pane with `ChatPane.create()` boot
**Splash / Login:**
- [x] `login.html` rewritten: split hero + auth card layout
- [x] Animated grid canvas (40px grid, scrolling offset, `--border` color)
- [x] Auth tabs (Login / Register) with server-gated registration
- [x] Register form: username, display name, email, password, avatar upload
- [x] `pages-splash.js`: `Pages.initSplash()`, grid animation, POST login, POST register + PUT avatar
- [x] `PageData` gains `InstanceName`, `LogoURL`, `Tagline`, `RegistrationOpen`
- [x] `loadBranding()` + `isRegistrationOpen()` from GlobalConfig
**UI Primitives Additions:**
- [x] `Toast`: show/success/error/warning/info with auto-dismiss, stacking
- [x] `renderBadge()`: 6 color variants (accent, success, danger, warning, purple, muted)
- [x] `renderIcon()`: 20+ SVG icon set
- [x] `renderIconBtn()`: icon button factory with active state
- [x] `Theme`: init/set/get/resolved/renderToggle with localStorage
- [x] `showConfirmDialog()`: enhanced with variant, onConfirm/onCancel
- [x] `mountAvatarUpload()`: file input with preview, onUpload callback
**Settings Surface Gates:**
- [x] BYOK gate: My Providers, Model Roles, Usage tabs shown when `user_providers.enabled`
- [x] User Personas gate: My Personas tab shown when `user_presets.enabled`
- [x] Models tab added to base navigation
- [x] `SettingsPageData` gains `BYOKEnabled`, `UserPersonasEnabled` from GlobalConfig
- [x] BYOK status indicator in nav footer
**Go Changes:**
- [x] `PageData`: Theme, SurfaceSettings, InstanceName, LogoURL, Tagline, RegistrationOpen
- [x] `Render()` defaults Theme to "dark"
- [x] `RenderLogin()` populates splash fields from GlobalConfig
- [x] `settingsLoader()` reads feature gates
---
## ✅ v0.22.8 — Surface Integration (Wire into Existing JS)
Connect v0.22.7 prototypes to the existing frontend JS modules.
ChatPane.primary replaces singleton UI.* calls. Theme system wired
into appearance settings. Admin surface hybrid loader completed.
Naming cleanup: preset → persona, APIConfigID → ProviderConfigID.
Depends on: surface prototypes (v0.22.7).
**ChatPane Bridge:**
- [x] `ChatPane.primary` creation in `startApp()` from server-rendered mount points
- [x] `ui-core.js` delegation: `UI.renderMessages``ChatPane.primary.renderMessages`, etc.
- [x] Branding init from `window.__SETTINGS__` (instanceName, logoURL)
**Theme Integration:**
- [x] Wire `Theme` into appearance settings save/load cycle
- [x] CM6 theme sync on theme change
- [x] Persist theme preference to user settings API
**Admin Surface:**
- [x] Complete hybrid section loaders for all JS-loaded sections
**Settings Surface:**
- [x] Models visibility toggles (API.saveModelPreferences)
- [x] User Personas CRUD (add/edit/delete with confirm dialogs)
- [x] BYOK provider CRUD in settings context
- [x] Teams tab content
**Naming Cleanup:**
- [x] preset → persona: routes, JSON keys, Go structs, JS, CSS, DOM IDs, templates
- [x] APIConfigID → ProviderConfigID: all Go structs and JSON fields
- [x] Shared `app-state.js` extracted from `app.js` — available on all surfaces
- [x] Auth tokens loaded for all surfaces via `base.html`
---
## ✅ v0.23.0 — @mention Routing + Persona Handles + Proxy
@mention routing works in any chat against the full model catalog and persona
registry. No roster, groups, or participant setup required — just type `@handle`
or `@model-id` and send.
**@mention Resolution**
- [x] `resolveMention()`: direct DB lookup — persona handle (exact/prefix) → model catalog (exact/prefix)
- [x] Autocomplete popup on `@` in any chat — all enabled models + personas
- [x] Handle field on personas: auto-generated from name, unique, editable
- [x] @mention pill rendering in message content (accent-colored, code-aware)
- [x] Context boundaries: persona→plain and persona→persona style isolation
- [x] Participant list injection: system prompt tells LLM who it can @mention
**AI-to-AI Chaining**
- [x] `chainIfMentioned()`: uses `resolveMention()` on assistant response content
- [x] Self-mention blocked, depth capped at 5
- [x] WebSocket delivery (`message.created` + typing indicators)
- [x] Same resolution path for user→LLM and LLM→LLM routing
**Provider Proxy**
- [x] `proxy_mode` (system/direct/custom) + `proxy_url` on `provider_configs`
- [x] All four providers use `cfg.Client()` with mode-aware HTTP transport
- [x] Bad custom URL falls back to system (not direct) — safe for mandatory-proxy environments
**Channel Infrastructure**
- [x] `channel_participants` table with polymorphic types and roles
- [x] `channel_models` partial indexes: persona entries keyed per-persona, raw models per-provider
- [x] Per-provider model preferences (composite keys in `user_model_settings`)
- [x] `persona_groups` + `persona_group_members` tables (schema ready, CRUD not yet wired)
---
## ✅ v0.23.1 — Multi-User Navigation + Conversation Taxonomy
Conversation type taxonomy (direct/dm/group/channel/workflow), three-section
sidebar (Projects → Channels → Chats), folder system for chats, DM plumbing,
channel configuration with `ai_mode`, presence heartbeat, and the unified
active conversation model.
Depends on: @mention routing (v0.23.0), WebSocket infrastructure (exists).
See [DESIGN-0_23_1.md](DESIGN-0_23_1.md) for the full spec.
**Conversation Types:**
- [x] Five-type taxonomy: direct (1:1 AI), dm (human-to-human), group (multi-participant), channel (named persistent), workflow (staged, deferred)
- [x] Channel type constraint extended: `dm`, `channel` added to `type` CHECK
- [x] `ai_mode` column on channels: `auto` | `mention_only` | `off`
- [x] `ai_mode` guard in completion handler — `off` returns 403, `mention_only` without @mention delivers without AI
- [x] `topic` column on channels for header display
**Sidebar Architecture:**
- [x] Three collapsible sections: Projects → Channels → Chats
- [x] `renderChannelsSection()` with # / person icons, unread badges, online dots
- [x] Channel items: context menu (rename, set topic, delete), ⋯ hover button
- [x] Section collapse/expand state in localStorage
**Folder System:**
- [x] `chat_folders` table, CRUD handler (`handlers/folders.go`)
- [x] Drag chats into/out of folders; folder context menu (rename, delete)
- [x] Folder delete modal: "Keep chats" / "Delete chats too" / Cancel
- [x] Unfiled drop zone when folders exist
**DM Plumbing:**
- [x] `resolveMention()` extended to resolve users (exact + prefix on username)
- [x] User @mention skips AI, delivers `user.mentioned` WebSocket notification
- [x] DM creation flow with user search modal
- [x] DM dedup guard — one channel per participant pair
**Presence:**
- [x] `user_presence` table with heartbeat upsert
- [x] `POST /presence/heartbeat` (30s interval), `GET /presence?users=...` (90s threshold)
- [x] Presence WebSocket events: `presence.changed`
**Channel Participants:**
- [x] `channel_participants` CRUD handler, auto-created on channel creation
- [x] Persona group CRUD endpoints wired (`persona_groups`, `persona_group_members`)
- [x] DM participant auto-creation from `req.Participants`
**Migration:**
- [x] 016_v023_multiuser: `ai_mode`, `topic`, `user_presence`, channel type extension
---
## ✅ v0.23.2 — Multi-User Polish + Channel Lifecycle
Bug fixes, unified active conversation refactor, channel lifecycle (archive/delete),
group chat leader routing, `@all` fan-out, message attribution, and deferred
surface integration from v0.22.8.
Depends on: v0.23.1 sidebar and conversation taxonomy.
**Unified Active Conversation:**
- [x] `App.activeConversation = { id, type }` replaces `currentChatId` + `currentChannelId`
- [x] `setActive(id, type)`, `activeId` getter, `getActiveChat()` helper
- [x] Send path, model bar, streaming, session restore all keyed off `activeConversation.id`
- [x] Sidebar highlight works across both chat and channel sections
**Group Chat + Persona Groups:**
- [x] Group leader default response: no @mention in group → leader persona responds
- [x] `@all` fan-out: routes to every persona participant, depth-1 only
- [x] Participant mutation guards: DMs keep ≥2 humans, groups keep ≥1 persona
- [x] "New Group Chat" flow with ad-hoc persona picker
**Message Attribution:**
- [x] Human sender display: other participants show avatar + display name (not "You")
- [x] Persona sender display verified for loaded history in channel context
**Channel Lifecycle:**
- [x] Archive action via context menu (`is_archived`, `archived_at`)
- [x] Delete gated by `channel_retention.mode` (flexible/retain)
- [x] Admin purge with `purge_after_days` floor
- [x] Retention config keys in `global_config`
**Bug Fixes:**
- [x] Channel persistence through refresh (resp.channels → resp.data)
- [x] Folder drag-and-drop (folderId mapping, drop handlers, unfiled zone)
- [x] DM creation 404 → added `GET /users/search` endpoint
- [x] Channel ⋯ menu consistency (replaced ✕ with hover ⋯ pattern)
- [x] Folder ⋯ button reflow (visibility:hidden instead of display:none)
- [x] Unread subquery using `last_read_at` (not migration-017-dependent column)
- [x] Settings Models section template wiring
- [x] `u.avatar``u.avatar_url` in ListMessages JOIN and treepath
**Surface Integration (deferred v0.22.8):**
- [x] `ChatPane.primary` bridge from server-rendered mount points
- [x] Theme save/load cycle wired to settings appearance
- [x] Admin hybrid section loaders completed
- [x] Settings surface: models toggles, persona CRUD, BYOK, teams
**@mention UX:**
- [x] Autocomplete includes users alongside personas and models
- [x] User mention pill styling (distinct color from persona pills)
- [x] `user.mentioned` WebSocket notification with toast
**Migration:**
- [x] 017_unread: `last_read_message_id` on `channel_participants`
---
## v0.24.0 — Auth Abstraction + User Identity ✅
Auth provider interface decoupling builtin auth from the handler/middleware
layer. User model extended with `auth_source`, `external_id`, `handle`.
@mention resolution upgraded to use handles. Zero behavior change for
existing users — builtin mode goes through the new abstraction identically.
See [DESIGN-0.24.0.md](DESIGN-0.24.0.md) for the full spec including
v0.24.1v0.24.3 subversions.
Depends on: v0.23.0 (channel_participants), v0.16.0 (groups), v0.9.4 (vault).
**Auth Provider Interface (`server/auth/`):**
- [x] `Provider` interface: `Mode()`, `Authenticate()`, `SupportsRegistration()`, `Register()`
- [x] `BuiltinProvider`: extracts login/register from `handlers/auth.go`
- [x] `UniqueHandle()`: collision-safe handle generation with `-2`, `-3` suffixes
- [x] `ErrorToHTTPStatus()`: maps auth errors to HTTP codes
- [x] `ParseMode()`: validates `AUTH_MODE` string with error return
**Auth Handler Refactor:**
- [x] `AuthHandler` gains `provider auth.Provider` field
- [x] `Login()` delegates to `provider.Authenticate()`, then vault unlock + JWT
- [x] `Register()` delegates to `provider.Register()`, then vault init + JWT
- [x] `generateTokens()` response includes `handle`, `auth_source`
- [x] `BootstrapAdmin()` / `SeedUsers()` backfill handles for existing users
**User Model Extension:**
- [x] `AuthSource` (builtin/mtls/oidc), `ExternalID` (nullable), `Handle` (unique)
- [x] All user store queries (PG + SQLite) include three new columns
- [x] `GetByHandle()`, `GetByExternalID()` on both store implementations
- [x] Unified `scanOneUser` / `scanOne` helpers replace per-method scan blocks
**Config + Middleware:**
- [x] `AUTH_MODE` env var (default `builtin`)
- [x] `main.go` dispatch: builtin wired, mTLS/OIDC fail-fast at startup
- [x] Middleware unchanged — JWT validation is auth-mode-agnostic
**@mention Resolution:**
- [x] `resolveMention()` uses `LOWER(handle)` instead of `LOWER(username)`
- [x] `SearchUsers` returns `handle`, filters on handle
- [x] Frontend autocomplete matches on handle, uses handle as @mention token
**Migration:**
- [x] 018_auth_abstraction: `auth_source`, `external_id`, `handle` columns + unique indexes + backfill
---
## v0.24.1 — mTLS + OIDC Providers ✅
Two external auth implementations plugging into v0.24.0's provider interface.
Keycloak integration test environment via docker-compose overlay.
Depends on: auth abstraction (v0.24.0).
See [DESIGN-0.24.0.md](DESIGN-0.24.0.md) §v0.24.1 for full spec.
- [x] mTLS provider: trusted header extraction, cert DN parsing, auto-provision
- [x] OIDC provider: well-known discovery, JWKS caching, token validation, claim extraction
- [x] OIDC authorization code flow: login redirect, code exchange, token handoff via fragment
- [x] Split-horizon issuer support (`OIDC_EXTERNAL_ISSUER_URL` for Docker/K8s)
- [x] OIDC claim → group mapping (external groups sync to internal groups with `source=oidc`)
- [x] Login page adapts by `AUTH_MODE` (form / cert status / SSO button)
- [x] Keycloak docker-compose overlay with pre-configured realm (`ci/keycloak-realm.json`)
- [x] `QArgs()` dialect adapter: handles Postgres `$N` reuse for SQLite arg expansion
- [x] `database.QueryRow/Query/Exec` wrappers with automatic arg expansion
- [x] Migration 019: `oidc_auth_state` table, `groups.source` column
- [x] Unit tests: mTLS (ParseDN, config), OIDC (discovery, auth URL, mode), QArgs (5 tests)
- [x] Regression test: `TestIntegration_ChannelListWithTypeFilter`
- [x] SQLite local dev fixes: `_time_format` DSN, store wiring, nginx resolver, extension assets
---
## v0.24.2 — Fine-Grained Permissions
Groups become capability carriers. Permission model on top of existing
group infrastructure.
Depends on: auth abstraction (v0.24.0). Parallel to v0.24.1.
See [DESIGN-0.24.0.md](DESIGN-0.24.0.md) §v0.24.2 for full spec.
- [x] Permission constants (`domain.action` convention, code-level enum)
- [x] `permissions` JSONB column on groups
- [x] `ResolvePermissions()`: union of group permissions + Everyone group
- [x] `RequirePermission()` middleware
- [x] Token budgets: per-group daily/monthly ceilings, enforced in completion handler
- [x] Model access control: per-group model allowlists
- [x] Admin UI: permission checklist, budget fields, model picker on group edit
- [x] Everyone group supersedes `DefaultUserPerms` (seeded in migration, editable in admin)
- [x] Migration 020: permissions, budgets, allowed_models on groups
---
## v0.24.3 — Anonymous / Session Participants
Unauthenticated visitors for workflow intake channels. Direct
prerequisite for v0.25.0 (Workflow Engine).
Depends on: mTLS provider (v0.24.1) for cert-based anonymous identity.
See [DESIGN-0.24.0.md](DESIGN-0.24.0.md) §v0.24.3 for full spec.
- [ ] `session_participants` table (channel-scoped, ephemeral token)
- [ ] Session JWT: `SessionClaims{SessionID, ChannelID}` — distinct from user JWT
- [ ] `SessionOrAuth()` middleware: accepts either JWT type
- [ ] Capability scoping: session participants limited to their bound channel
- [ ] mTLS anonymous path: cert fingerprint as stable session identity
- [ ] `channels.allow_anonymous` flag
- [ ] Session display in participant list (team members see visitor identity)
- [ ] Migration 021: `session_participants` table, `allow_anonymous` column
---
## 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.3).
_(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
- **Helm chart.** The k8s/ raw manifests with `${VAR}` substitution have served well but are friction for external adopters and make values management manual. A Helm chart wraps the same backend + frontend deployments with a `values.yaml` (replicas, image tags, ingress host, storage class, secret refs, resource limits, feature flags). Target: `helm install switchboard ./chart` for a fresh cluster, `helm upgrade` for rolling deploys. Subcharts for optional Postgres (for dev/test — prod uses external). Candidate for v0.27+ or a parallel track once the core feature set stabilizes.
**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