77 KiB
Changelog
All notable changes to Chat Switchboard.
[0.21.0] — 2026-03-01
Added
- Workspace Storage Primitive. Platform-level file storage bound to users, projects, channels, or teams via polymorphic owner model. Dual-layer architecture: PVC filesystem (source of truth) with DB metadata index (queryable cache). Workspaces support configurable quotas, status lifecycle (active/archived/deleting), and owner-based authorization inheritance.
- Workspace data model. Two new tables:
workspaces(polymorphic owner_type/owner_id, root_path, max_bytes quota, status) andworkspace_files(path, MIME type, size, sha256, is_directory). Unique index on (workspace_id, path) enables upsert-on-conflict for file metadata sync. Migrations for both Postgres and SQLite. workspace.FSpackage. Filesystem operations layer with security-first design: atomic writes (temp file + rename with SHA256 computed via tee reader), path traversal guards (cleanPath normalization + absPath containment validation), symlink rejection on write targets. Operations: ReadFile, WriteFile, DeleteFile (with recursive guard), Mkdir, Stat, ListDir, Tree, Reconcile (filesystem→DB drift sync).- Archive operations. Extract zip and tar.gz archives into workspaces with bomb protection: 10K file limit, 100MB single file cap, workspace quota enforcement during extraction. Common-prefix stripping handles GitHub-style
project-name/wrapper directories. CreateArchive packages workspaces into downloadable zip or tar.gz. - Content type detection. Extension-based MIME detection covering 40+ source code types (Go, Rust, Python, TypeScript, etc.) with
http.DetectContentTypesniffing fallback for unknown extensions. WorkspaceStoreinterface. Full CRUD for workspaces, file index operations (upsert, delete, delete-by-prefix, get, list with recursive/non-recursive modes), ownership lookup (GetByOwner, ListByOwner), and aggregate stats. Postgres and SQLite implementations.- Workspace API. 15 new endpoints under
/api/v1/workspaces: workspace CRUD (create, get, update, delete), file operations (list, read, write, delete, mkdir), archive management (upload with extraction, download), reconcile (FS→DB sync), stats. Owner-based authorization: user workspaces require self, channel workspaces require channel owner, project workspaces require project member, team workspaces require team member. - Unit tests. Path cleaning (13 cases including traversal, dotfiles, whitespace), absPath traversal detection, MIME detection (14 extensions), unsafe path filtering (7 cases), common prefix detection (5 cases), write/read round-trip with mock store, delete verification, mkdir with index sync.
[0.20.0] — 2026-03-01
Added
- Notifications Core (Phase 1): Persistent, user-targeted notification infrastructure with real-time WebSocket delivery.
notificationstable (Postgres + SQLite),NotificationStorewith paginated queries,Service.Notify()/NotifyMany()for centralized creation and dispatch. Five API endpoints: list (paginated, filterable), unread count, mark read, mark all read, delete. Bell icon in header bar with unread count badge (capped at 9+). Notification dropdown (latest 10, grouped, click-to-navigate viaresource_type/resource_id). Full notification panel registered withPanelRegistry. WebSocket push vianotification.newevent with toast for high-priority types (kb.error,role.fallback). Background cleanup goroutine (configurable retention, default 90 days). Initial sources:role.fallback(via EventBus subscription),kb.ready/kb.error(knowledge base processing),grant.changed(group membership). - @mention Parsing + Multi-model Routing (Phase 2): Channels support multiple AI models with @mention-based routing.
mentions.Parse()extracts @mentions from message content, resolves against channel model roster (case-insensitive, longest-match-first, trailing punctuation tolerant). Completion handler fans out sequentially to mentioned model(s), producing one assistant response per target. Without @mention, default channel model responds (fully backward compatible). Channel model CRUD: 4 new endpoints for add/remove/update/list. Frontend: model pills in chat header, @mention autocomplete (CM6mentionCompletionextension with roster-backed suggestions), model attribution labels on multi-model responses. Messagemodel_displayfield for human-readable attribution. SSE streaming tagged with model info per response. - Email Transport + Notification Preferences (Phase 3): SMTP email delivery via
EmailTransportsupporting implicit TLS (port 465) and STARTTLS (port 587). Multipart MIME messages (HTML + plaintext) with branded templates using Gohtml/template.notification_preferencestable with three-tier resolution: specific type → user wildcard*→ system default (in_app=true, email=false). Three preference API endpoints (list, set with partial patch, delete). Admin SMTP configuration in settings (host, port, user, password, from address, TLS mode) with test email endpoint. User notification preferences UI in Settings → Notifications tab with per-type in-app/email checkboxes. Async email delivery (goroutine with 30s timeout, failures logged non-blocking). - Gin Release Mode: Backend now automatically sets
gin.SetMode(gin.ReleaseMode)whenENVIRONMENT=production, suppressing per-request access logs for health checks and reducing log noise.GIN_MODEenv var also added to K8s backend deployment as explicit override.
Fixed
- Model preferences 500 on every page load:
GET /api/v1/models/preferencesreturned HTTP 500 due toNULLvalues inuser_model_settings.hiddenandsort_ordercolumns failing Gosql.Scan()into non-pointer types. Root cause: theSetupsert passedNULLfor unset patch fields, bypassingDEFAULT false/DEFAULT 0on INSERT. Fixed withCOALESCEin both SELECT (read path) and INSERT VALUES (write path) for Postgres and SQLite. Includes data-fix SQL for existing NULL rows.
[0.19.2] — 2026-02-28
Added
- Project Persona Default: Bind a persona to a project via the detail panel. All chats in the project inherit the persona's model, parameters, and system prompt as a fallback when no explicit preset is selected. Resolution chain: explicit request → project persona → none.
- Project Archive Toggle: Archive/unarchive projects from the detail panel. Archived projects hidden from sidebar by default with "Show archived (N)" toggle. Dimmed visual treatment when shown.
- Channel Reorder: Right-click a chat within a project for "Move up" / "Move down" options. Server-persisted positions via
project_channels.position, loaded on startup, maintained across moves.
[0.19.1] — 2026-02-28
Added
- Active Project: Pin a project as active; new chats auto-assign to it. Persists across reloads via localStorage. Visual indicator (📌 + accent border) in sidebar.
- Project System Prompt: Per-project instructions stored in
projects.settingsJSONB. Injected between persona/channel prompt and KB hint during completion. Merge semantics on update (preserves other settings keys). - Project Detail Panel: Side panel (via PanelRegistry) for managing project system prompt, KB bindings, and notes. Accessible from project ⋯ menu → "Project settings".
- Enriched
ListKBsandListNotesresponses with JOIN-sourcedname/titlefields for display in the project panel.
Changed
ProjectPatchmodel now acceptssettingsfield for partial settings merge.- Project context menu expanded: "Pin as active", "Project settings", plus existing rename/color/delete.
[0.19.0] — 2026-02-28
Added
- Projects / Workspaces. Organizational containers that group related conversations, knowledge bases, and notes into a single workspace. Projects provide a scope-aware organizational layer above individual channels, with support for personal, team, and global visibility.
- Project data model. Four new tables:
projects(with scope, owner, team, color, icon, settings JSONB),project_channels(ordered membership with UNIQUE constraint),project_knowledge_bases(with auto_search flag), andproject_notes. Channels gain a denormalizedproject_idFK for efficient filtering. - Project API. 17 new endpoints under
/api/v1/projects: full CRUD, channel add/remove/list/reorder, KB add/remove/list, note add/remove/list. Access checks enforce owner-or-team-member visibility with owner-only delete. - Project KB resolution. Knowledge bases bound to a project are
automatically available to all channels within that project. Resolution
chain extended: Persona KBs → Project KBs → Channel KBs →
Personal KBs. Both
BuildKBHint(system prompt injection) andkbsearch(tool-time retrieval) updated. - Note auto-association. Notes created from a channel that belongs to a project are automatically added to that project's note collection.
- Sidebar project groups. Projects appear as collapsible groups in the sidebar above the existing time-based "Recent" section. Each group shows a color dot, chat count, and an options menu (⋯) for rename, color picker, and delete.
- Drag-and-drop. Drag chat items between project groups or back to Recent. Visual feedback with outline and background highlight on valid drop targets.
- Right-click context menu. Right-click any chat to move it to a project, remove it from its current project, or create a new project and assign in one step.
- New Project button. Added to the New Chat split-button dropdown for quick project creation.
- Channel project_id filter.
GET /api/v1/channelsaccepts?project_id=<uuid>to filter by project, or?project_id=nonefor unassigned channels. Response includesproject_idfield.
Changed
renderChatList()rewritten to support project grouping while preserving the original time-based layout when no projects exist. Chat items now includedraggable,oncontextmenu, and drag event handlers.- Channel response struct, SELECT queries, and scan calls updated across
ListChannels,GetChannel, andCreateChannel(both Postgres and SQLite paths) to includeproject_id. resource_grantsCHECK constraint extended to include'project'as a valid resource type.db-validate.shupdated: former "Dropped tables" assertions forprojectsandproject_channelsreplaced with positive checks for all four project tables pluschannels.project_idcolumn check.- Test helper truncation list updated with project junction tables.
Fixed
- JSON corruption defense (hotfix carry-forward from 0.18.2):
SafeJSONwrapper withscanJSONandscanTagshardened helpers that replace barejson.RawMessage/pq.Arrayscanning. Preventsencoding/json: invalid characterpanics from NULL or empty columns. - Service worker (hotfix carry-forward):
chrome-extension://URL filtering to avoid opaque response cache errors.
[0.18.1] — 2026-02-28
Added
- Side panel architecture. Complete rewrite of the side panel system from shared-tab layout to independent single-slot panels. Any action (preview, notes, diagram pop-out) fills the slot, replacing whatever was there — no tabs, no association between panel types.
- Panel registry (
PanelRegistry): named panels with independent open/close state, scroll/state preservation across switches, keyboard shortcuts (Ctrl+\ cycle, Ctrl+Shift+\ toggle dual-view). - Dual-view mode: two panels side-by-side via CSS grid with a drag-adjustable split ratio (0.2–0.8 range, 6px divider handle). Toggle button in header actions area alongside fullscreen and close.
- Live HTML preview: streaming responses update the preview iframe in real-time (500ms debounce). Extracts the last fenced HTML block from partial content and renders it as the response streams in.
- Extension pop-out:
⧉button on rendered extension blocks (mermaid, KaTeX, etc.) clones the content into the preview iframe and opens the side panel. - Extension UI primitives (
ctx.ui): seven methods exposed to browser extensions through the scoped extension context:toast(msg, type)— toast notifications viaUI.toast()openPreview(html)— load HTML into side panel preview iframeisDark()— theme detection without DOM sniffingisMobile()— viewport width check (≤768px)isPanelOpen()— side panel container visibilityconfirm(msg, opts)— modal confirm dialog (Promise<boolean>)createMenu(anchor, opts)— popup menu (unchanged from stub)
- Mermaid context-aware expand: single
⛶button replaces the previous two-button (pop-out + fullscreen) approach. When the side panel is open, expand pops the diagram into it; when closed, expand goes fullscreen. - Mermaid fullscreen close button: 40px circular close button overlaid top-right in fullscreen mode, 48px on mobile. Fixes the previous Escape-key-only exit which was unusable on touch devices.
- Mobile side panel: swipe navigation between panels (80px threshold, 1.5× horizontal-to-vertical ratio), tap-to-close overlay, responsive auto-collapse of dual mode on narrow viewports, enlarged touch targets for all panel controls.
- Side panel header label: simple text label showing the active panel name, replacing the previous tab bar UI.
Changed
- Side panel model changed from tabbed (Preview + Notes tabs visible simultaneously) to single-slot (one panel fills the space, actions replace it). No user-selectable tabs — content is entirely action-driven.
- Dual-view toggle button moved from tab bar (dynamically injected) to static header actions area next to fullscreen and close.
- Mermaid extension refactored to use
ctx.uiprimitives exclusively. Zero direct references toUI.*,PanelRegistry.*, or DOM class sniffing for theme detection. Extension remains fully self-contained inextensions/builtin/mermaid-renderer/. - Mermaid source copy uses
ctx.ui.toast()instead of inline button text swap. Theme detection usesctx.ui.isDark()instead of manualdocument.body.classList.contains('dark-theme')check. - Side panel outer resize minimum bumped to 480px in dual mode (vs 280px single).
Removed
- Tab bar UI (
.side-panel-tabs,.side-panel-tab, tab rendering logic). Panels no longer have user-selectable tabs. - Separate pop-out and fullscreen buttons in mermaid toolbar (collapsed into single context-aware expand button).
[0.18.0] — 2026-02-28
Added
- Memory system. Long-term memory across conversations with scope-aware
isolation. Three memory scopes:
user(personal facts/preferences),persona(shared across all users of a Persona), andpersona_user(per-user within a Persona context, e.g. tutoring progress per student). Memories persist across channels and are injected into the system prompt at completion time with scope priority (persona_user > persona > user). - Memory tools. Two new LLM-callable tools:
memory_save— LLM explicitly stores a fact with key/value/confidence. Scope-aware: saves to the active scope for the current Persona context.memory_recall— LLM queries stored facts relevant to current context. Merges results from applicable scopes with semantic search via embeddings and keyword fallback.
- Automatic memory extraction. Background scanner finds conversations
with sufficient new activity, sends them to the utility model role for
fact extraction, and stores results as
pending_reviewmemories. Configurable extraction prompt per Persona (e.g. "extract FAQ-worthy Q&A pairs"). Scanner runs on a configurable interval with concurrency control. Global kill switch via admin settings. - Memory review pipeline. Extracted memories start in
pending_reviewstatus. Admin panel shows pending review queue with bulk approve. Users can approve/reject/edit their own pending memories from the Settings → Memory tab. - User memory management. Settings → Memory tab shows active/pending memory counts, filterable/searchable memory list with inline edit and delete. Status filter (active, pending_review, archived). Approve All button for batch operations on pending memories.
- Admin memory controls. Admin panel → Memory section shows system-wide
pending review queue. Memory extraction toggle in admin Settings panel
(
memory_extraction_enabled). Bulk approve endpoint for batch review. - Hybrid semantic recall. Memory recall supports both keyword search
and vector similarity (cosine distance). Postgres uses
<=>operator with pgvector; SQLite computes cosine similarity in Go with app-level vector loading. Results are merged and deduplicated. - Memory injection at completion time.
BuildMemoryHint()loads relevant memories and formats them as a system prompt section, injected after the knowledge base hint. Context-budget aware with configurable character limit. Supports embedding-based relevance filtering when the user's latest message is available. - Persona memory configuration. Personas gain
memory_enabled(bool) andmemory_extraction_prompt(text) fields. When memory is enabled on a Persona, conversations with that Persona contribute to persona-scoped memory extraction. Custom extraction prompts allow specialization (e.g. helpdesk FAQ extraction vs. tutoring progress tracking). - Database migrations. Four new migration files (Postgres + SQLite):
004_v0180_memories.sql/sqlite/003_v0180_memories.sql—memoriestable with composite unique index, full-text search index (GIN/keyword),memory_extraction_logtracking table.005_v0180_memory_phase2.sql/sqlite/004_v0180_memory_phase2.sql— Persona memory columns, extraction log unique constraint.
- API endpoints. Eight new authenticated endpoints:
GET /memories— list user's memories (filterable by status/query)GET /memories/count— active + pending countsPUT /memories/:id— edit a memory's key/value/confidenceDELETE /memories/:id— delete a memoryPOST /memories/:id/approve— approve a pending memoryPOST /memories/:id/reject— reject (archive) a pending memoryGET /admin/memories/pending— admin pending review queuePOST /admin/memories/bulk-approve— admin bulk approve
Changed
CompletionHandlernow accepts an*knowledge.Embedderparameter for memory injection with semantic relevance filtering.- Persona create/update forms include memory configuration fields (enabled toggle, extraction prompt textarea).
- Admin settings save handler includes
memory_extractionandmemory_extraction_enabledkeys. store.Storesstruct includesMemories MemoryStorefield.- Admin panel sections include Memory with pending review loader.
Technical Notes
- Memory embeddings use
vector(3072)matching the existing KB/notes schema. HNSW index is not used (pgvector limits HNSW to 2000 dims); filtered sequential scans are performant for per-user memory tables. IVFFlat or dimension reduction available as future optimizations. - SQLite hybrid recall loads embeddings into Go and computes cosine
similarity at the application level, reusing the existing
cosineSimilarity()function from the knowledge base store. - Memory tools use late registration (like KB search and note tools) because they require stores and embedder dependencies initialized in main.go.
[0.17.3] — 2026-02-28
Added
- Wikilink bi-directional linking. Notes support
[[Title]]and[[Title|display text]]syntax. Links are extracted on save via regex, resolved to target note IDs by case-insensitive title match, and stored in anote_linksjunction table. Dangling links (references to notes that don't exist yet) are preserved and automatically resolved when a matching note is created later. - Transclusion embeds.
![[Title]]syntax renders embedded note content inline in read mode. Content is fetched asynchronously with a recursion guard (max depth 1 — nested transclusions render as plain text references). Transclusion links are visually distinct in both the editor (border-left, italic) and the graph (dashed edges). - Backlinks panel. Read mode shows a collapsible "Linked mentions" panel below the note content listing all notes that link to the current note. Each backlink is clickable to navigate directly. Count badge updates on every note open.
- Knowledge graph visualization. Canvas-based force-directed graph with
no external dependencies (~480 lines). Physics: O(n²) Coulomb repulsion,
Hooke spring attraction on edges, center gravity. Interaction: pan, zoom
(0.15–4.0x toward cursor), drag nodes, hover highlights node + neighbors.
Click opens note editor. Nodes sized by √(link_count), colored by folder
path (10-color palette). Ghost nodes for unresolved
[[links]]with toggle button. Energy-based pause (stops rAF when total kinetic energy falls below threshold). ResizeObserver for responsive canvas sizing. - CM6 note editor. New
CM.noteEditor()factory function with live markdown preview (heading sizes for h1–h3, blockquote styling, fenced code block decorations),[[wikilink]]autocomplete triggered by[[with async title search, and clickable wikilink chip rendering. Falls back to plain<textarea>if CM6 bundle is unavailable. - Wikilink autocomplete. Typing
[[in the note editor triggers an async completion popup backed byGET /notes/search-titles?q=. Results are fetched viaILIKEtitle match, limited to 8 suggestions. Selecting a result inserts the full[[Title]]syntax. - Daily notes. "Today" button in the notes toolbar creates or opens a
daily note titled
Daily — YYYY-MM-DDin the/daily/folder with a starter template (Tasks + Notes sections). Idempotent — repeated clicks navigate to the existing daily note. - Save-to-note from chat. "Note" button in message action bar captures
the full message content (or the current text selection within that message)
into a new note with pre-filled title extracted from the first line.
Provenance is tracked via
source_message_idcolumn on the notes table, enabling future jump-to-source navigation. - API endpoints. Three new authenticated endpoints:
GET /notes/search-titles?q=&limit=— lightweight title search for autocomplete (returns[{id, title}]).GET /notes/:id/backlinks— notes linking to the specified note, with display text and metadata.GET /notes/graph— full graph topology: nodes (with inbound/outbound link counts), resolved edges, and unresolved (dangling) link titles.
note_linkstable. New junction table withsource_note_id,target_note_id(nullable for dangling links),target_title,display_text,is_transclusion, andcreated_at. Primary key on(source_note_id, target_title). Index ontarget_note_idfor backlink queries. Migrations for both Postgres and SQLite.source_message_idcolumn. New nullable UUID column onnotestable for chat-to-note provenance tracking.- Wikilink extraction package. Standalone
notelinks/package with regex-based parser handling[[Title]],[[Title|Display]],![[Title]], and![[Title|Display]]. Deduplicates by lowercase title, preserves first occurrence. 10 unit tests covering edge cases.
Changed
- Notes editor replaces
<textarea>with CM6noteEditorinstance. The container#noteEditorContentContaineris lazily initialized on first edit; destroyed on panel close to avoid stale state. - Note create and update handlers now extract wikilinks from content and
call
ReplaceLinks()(transactional DELETE + batch INSERT). Create also callsResolveByTitle()to fix dangling links from other notes. showNotesList()destroys the CM6 editor instance and hides graph view.saveNote()anddeleteNote()callinvalidateNoteGraph()to clear the cached graph data.- Read mode renders
[[wikilinks]]as styled clickable chips via post- processing of the formatted HTML. Clicking navigates to the linked note or offers to create it if not found. ARCHITECTURE.mdupdated withnotelinks/package,note-graph.js,CM.noteEditor()factory, and expanded notes description covering the linking model.ROADMAP.mdupdated with full v0.17.3 section including all checklist items.- CM6 bundle entrypoint (
index.mjs) exportsnoteEditoralongside existingchatInputandcodeEditor. theme.mjsaddsnoteEditorThemewith full-height layout (min 200px, max 60vh), heading size decorations, and blockquote styling.
[0.17.2] — 2026-02-28
Added
- CodeMirror 6 integration. Rich editor infrastructure compiled at Docker
build time via esbuild (IIFE bundle, ~295KB min / ~90KB gzip). Two factory
functions exposed on
window.CM:CM.chatInput()— Markdown-mode editor for the chat input with auto-growing height, Enter=send / Shift+Enter=newline, spell check, and WYSIWYG fenced code block decorations (visual container with monospace font and accent border, matching claude.ai UX).CM.codeEditor()— Full-featured code editor for admin extension panel with line numbers, bracket matching, search/replace, fold gutter, and 10 bundled language modes (Markdown, JavaScript, JSON, SQL, HTML, CSS, YAML, Go, Python, Rust).
- Graceful degradation. All CM6 integration points check
window.CMavailability. If the bundle fails to load, the app falls back to native<textarea>with zero breakage. - Dark/Light/System theme toggle. New appearance setting with three
modes. Light theme overrides all CSS variables via
[data-theme="light"]selector. System mode tracksprefers-color-schememedia query in real time. Theme changes emittheme.changedon the EventBus; CM6 editors toggleoneDarksyntax theme via compartment reconfiguration. - Vim/Emacs keybinding preference. Editor keybinding mode (Standard /
Vim / Emacs) configurable in appearance settings. Applies to code editors
and extension editors only — chat input always uses standard keybindings.
Live-switchable on already-open editors via
keymap.changedevent. Bundled statically (~40KB for both modes). - Inline code shortcut. Ctrl/Cmd+E wraps selection in backticks or inserts an empty inline code pair with cursor between them.
- Code block shortcut. Typing
```at the start of a line expands to a fenced code block with cursor positioned inside. The decoration plugin renders code blocks with a styled visual container in the chat input. - CI path-based change detection. New
detect-changesjob classifies changed files into frontend/backend/infra/docs buckets. Test jobs skip when irrelevant (FE-only changes skip Go tests, docs-only changes skip all tests and deploy). Tags always run the full pipeline.
Changed
- Docker build pipeline: both
Dockerfile.frontendand unifiedDockerfilenow include acm6-buildstage (Node 20 Alpine → esbuild → IIFE bundle). ChatInputabstraction inchat.jsreplaces direct textarea access across 7 callsites (chat.js,tokens.js,attachments.js).- Extension editor in
admin-handlers.jsusesCM.codeEditor()with JSON and JavaScript modes, replacing bare<textarea>with manual Tab handler. - Service worker excludes
/vendor/codemirror/from cache (version-busted). - Debug state snapshot includes CM6 version and language list.
ARCHITECTURE.mdupdated to v0.17 reflecting CM6 integration, SQLite dual-driver, modular frontend file structure, and theme system.- CI pipeline header updated to v0.17.2 with path gating documentation.
build-editor.shfalls back tonpm installifpackage-lock.jsonis missing (belt-and-suspenders for local dev).
Fixed
- App initialization crash.
Events.publish(nonexistent) →Events.emit(correct API). The unhandled exception duringinitAppearance()killedinitListeners(), leaving the entire app half-initialized — settings modal unclosable, keyboard shortcuts unwired, paste handlers missing. - Cursor invisible in dark and light mode. CM6 cursor used
--accentcolor (low contrast on both themes). Switched to--textfor consistent visibility. Added explicitborderLeftWidth: 2px. - Placeholder text offset. Double padding between
.cm-editorwrapper (12px) and.cm-content(8px) pushed placeholder 20px below expected position. Zeroed.cm-contentpadding — wrapper is the single source of truth.
[0.17.1] — 2026-02-27
Added
- SQLite backend. Full dual-driver database layer — set
DB_DRIVER=sqliteto run with an embedded SQLite database. Pure Go (no CGO), zero external dependencies. 19 store files covering all domain stores: channels, messages, users, teams, personas, knowledge bases, notes, usage, audit, extensions, and more. Feature parity with Postgres including knowledge base vector search via app-level cosine similarity computed in Go. - Dialect-aware test infrastructure.
database.SetupTestDB()detectsDB_DRIVERand provisions either a Postgres test database or a SQLite temp file. Exporteddatabase.PH(n)returns$Nor?per dialect.database.TruncateAll()usesTRUNCATE CASCADEon Postgres andDELETE FROMwithPRAGMA foreign_keystoggling on SQLite.dialectSQL()helper in handler tests converts$Nplaceholders and strips::jsonbcasts at runtime. - SQLite CI pipeline. New
test-sqlitejob runs the full handler integration test suite and store tests against an embedded SQLite database. Parallel with the existing Postgres test job. Build gate verifiesCGO_ENABLED=0compilation. - Generic provider test config. Live provider integration tests now
read
PROVIDER,PROVIDER_KEY, andPROVIDER_URLenvironment variables instead of hardcodedVENICE_API_KEY. Legacy fallback preserved. Model selection prefers non-reasoning models to avoid thinking budget requirements with lowmax_tokens.
Changed
kb_chunkstable in SQLite schema includesembedding TEXTcolumn for JSON-encoded float64 vectors (previously omitted as feature-gated).SimilaritySearchon SQLite loads candidate chunks, decodes JSON embeddings, and computes cosine distance in Go — replacing the previous "not available" error.InsertChunkson SQLite now stores embedding vectors as JSON text.- Live test names genericized:
TestLive_Venice*→TestLive_*. - CI
build-and-deploydepends on[test, test-frontend, test-sqlite].
Fixed
- SQLite
RETURNING+time.Timescan failure. The modernc/sqlite driver cannot scandatetime('now')TEXT columns intotime.TimeviaRETURNING. All 13 SQLite storeCreatemethods rewritten to set timestamps in Go (time.Now().UTC()) and useExecContextinstead ofQueryRowContext(...).Scan(). Format:2006-01-02 15:04:05(timeFmtconstant inhelpers.go). - SQLite missing
idin INSERTs. Unlike Postgres (DEFAULT gen_random_uuid()), SQLiteTEXT PRIMARY KEYcolumns have no auto-generation. Addedstore.NewID()/uuid.New()to:usage_log,model_pricing(both upsert paths),team_members(AddMember),group_members(AddMember),refresh_tokens(CreateRefreshToken). - Test seed helpers SQLite-aware.
SeedTestUser,SeedTestChannel,SeedTestTeam,SeedTestTeamMember,SeedTestGroup,SeedGroupMembernow branch onIsSQLite()to provide application-generated UUIDs instead of relying onRETURNING id. - Postgres-isms in SQLite stores.
extension.goUpdate usednow()→datetime('now');ListForUserCOALESCE usedtrue→1. - SQLite store bool/int type mismatches:
persona.goauto-fetch flag,usage.goexclude-BYOK filter,user_settings.govisibility map values — all corrected from0/1tofalse/true.
[0.17.0] — 2026-02-27
Added
- Persona-KB binding. Personas can now have knowledge bases directly
bound to them. When a user selects a persona with bound KBs, the
kb_searchtool is automatically scoped to those KBs and the persona's system prompt includes a KB listing hint. Admin and team admin preset forms include a KB picker with per-KB auto-search toggles. Migration addspersona_knowledge_basesjoin table. - Enterprise KB mode. New
discoverableflag on knowledge bases controls whether users can see and attach KBs directly. Whenkb_direct_accessplatform policy is set totrue, users cannot attach KBs to channels directly — they access KBs exclusively through persona bindings curated by admins. NewListDiscoverableKBsandSetDiscoverableendpoints. - Role fallback alerts (issue #69). When a role's primary provider
fails and the fallback activates, the resolver emits a
role.fallbackevent on the EventBus with audit log entry. Admin users see a persistent dismissable banner. 5-minute per-role cooldown prevents flooding. - Chat rename. Double-click any chat title in the sidebar to edit inline. Enter saves, Escape cancels.
- Utility model auto-naming. After the first assistant response,
a background request to the utility role generates a concise title.
Falls back to truncation when no utility model is configured.
New endpoint:
POST /channels/:id/generate-title. - Chat token count. Conversation token estimate shown in the model bar, color-coded against context budget.
- State restore on refresh. Active chat ID persisted to
sessionStorage, auto-restored on page reload.
Fixed
- ResolvePreset group access bypass.
ResolvePreset()used raw SQL that skippedresource_grantschecks from v0.16.0. Now usesPersonaStore.UserCanAccess(). - KB create scope authorization. Now enforces admin role for global KBs and team admin role for team KBs.
- Completion handler persona ID scoping.
personaIDvariable scoped inside anifblock but referenced downstream. Fixed by threading through function signatures. - Nil slice JSON marshaling.
ListDiscoverableKBsreturns[]instead ofnullwhen no KBs match.
Changed
UpdateDocumentStorageKeymoved from rawExecContextto store method.- EventBus route table:
role.fallback→DirToClient. - Resolver gains
.WithBus(bus)builder (nil-safe, backwards compatible). - Service worker:
/extensions/excluded from fetch handler. - Mermaid renderer v2.0 (pan/zoom, export) promoted to builtin.
- Removed DIAG diagnostics from
TestGroupBasedPersonaAccess. - Paste-to-file threshold synced from backend
PublicSettings(storage.paste_to_file_chars, admin-configurable, default 2000). Resolves hardcoded constant inattachments.js. - Embedding dropdown already had tolerant type filter, manual model ID fallback, and auto-switch on empty — confirmed complete, checkbox updated.
[0.16.0] — 2026-02-27
Added
- User groups. Global and team-scoped groups decoupled from team
membership.
groupsandgroup_memberstables. Admin and team admin CRUD for group management. Groups serve as ACL targets for resources. - Resource grants. Three-way grant model (
team_only,global,groups) for Personas and Knowledge Bases.resource_grantstable withgrant_scopeandgranted_groups UUID[]columns. Grant picker UI on Persona and KB forms with group multi-select. - Schema consolidation. 9 incremental migrations collapsed into
single
001_v016_schema.sql. Fresh installs use the consolidated file; upgrade path preserved via migration version tracking.
Changed
- Persona and KB list queries now filter through
resource_grantsfor non-admin users. Team-only remains the default scope. - Admin panel shows group membership counts and grant summaries.
[0.15.1] — 2026-02-26
Added
attachment_recalltool. Two operations:listreturns filenames and metadata for the current channel's attachments;readextracts and returns content by attachment ID. Channel-scoped access control.conversation_searchtool. Full-text search across the current channel's message history using PostgreSQLplainto_tsquery. Returns matching messages with timestamps and role context.- Token estimator attachment awareness.
Tokens.estimateAttachments()accounts for staged file sizes in context budget calculations. Warning thresholds include attachment estimates.
[0.15.0] — 2026-02-26
Added
- Background compaction scanner. Periodic scan identifies channels
exceeding configurable context thresholds. Automatic summarization via
utility role compresses old messages into summary nodes. Per-channel
opt-in/out via
auto_compactchannel setting. - Compaction service.
compaction.Serviceorchestrates summary generation: estimates token usage, selects messages for compression, calls utility role, inserts summary as tree boundary node with metadata. - Context budget guard rail. 80% ceiling prevents compaction from triggering mid-generation. Cooldown timer prevents repeated compaction of the same channel.
- Summarize & Continue button. User-triggered compaction from the context warning bar. Reuses compaction service with immediate execution.
Changed
- Scanner configurable via
global_settings: threshold percentage, cooldown duration, enabled/disabled toggle. Channel-level overrides.
[0.14.0] — 2026-02-26
Added
- Knowledge bases. RAG pipeline: upload documents → chunk (recursive
text splitter) → embed via pgvector →
kb_searchtool for semantic retrieval. Team and personal KB scopes. Channel KB toggle enables per-conversation knowledge access. - Document ingestion.
knowledge.Ingest()pipeline: file upload → text extraction (reuses v0.12.0 pipeline) → chunking with configurable overlap → embedding via the embedding role → storage askb_chunkswith vector index. - KB admin panel. Knowledge Bases section under AI category. Create, delete, upload documents, view chunk counts and storage usage. Team admin scoped to team KBs.
- Notes semantic search. Note search upgraded from exact text match to pgvector cosine similarity when embeddings are available.
kb_searchtool. Registered when embedding role is configured. Accepts query text, returns top-K chunks with source document attribution. Channel KB bindings control which KBs are searched.
Changed
- pgvector extension enabled in schema (
CREATE EXTENSION IF NOT EXISTS vector).kb_chunkstable includesembedding vector(1536)column with IVFFlat index.
[0.13.1] — 2026-02-26
Added
web_searchtool. Search provider abstraction with two backends: DuckDuckGo (HTML scraping, zero config) and SearXNG (self-hosted, JSON API). Returns title, URL, and snippet for each result. Configured viaSEARCH_PROVIDERandSEARXNG_URLenv vars.url_fetchtool. Fetches and extracts text content from URLs. Respects robots.txt. Content-type detection with HTML-to-text conversion. Configurable timeout and size limits.- Tool categories. Tools now have a
categoryfield (builtin, search, knowledge, browser). Tools toggle UI in chat bar groups by category with per-tool enable/disable. - Tools toggle UI. Popup menu on chat input toolbar showing all
available tools. Per-tool checkbox state sent as
disabled_tools[]in completion requests. Browser extension tools included.
[0.13.0] — 2026-02-25
Changed
- Admin panel refactor. Replaced 12-tab modal with fullscreen admin
panel. Four categories (People, AI, System, Monitoring) with section
sidebar navigation. URL-based routing (
#admin/people/users). Responsive layout, classification banner-aware positioning. - CSS design token cleanup. Consolidated duplicate color/spacing variables. Admin panel uses shared token system with main UI.
[0.12.0] — 2026-02-25
Added
- File handling and vision support. Upload images, PDFs, and documents into chat via 📎 button, drag-and-drop, or paste. Multimodal message assembly injects base64 images for vision-capable models and extracted text for documents. Staged attachment strip shows upload progress and extraction status. Auth-aware blob rendering with Bearer tokens. Image lightbox viewer. Vision capability hints on image attachments.
- Storage backend abstraction.
ObjectStoreinterface with two implementations: PVC (local filesystem) for single-node/dev and S3 (minio-go) for multi-node production. S3 backend works with any S3-compatible API: MinIO, Ceph RGW, AWS S3. Auto-detection whenSTORAGE_BACKENDis not set (tries PVC, disables if not writable). Both backends share identical semantics — all handlers, attachment CRUD, multimodal assembly, and orphan cleanup work regardless of backend. - S3 configuration.
S3_ENDPOINT,S3_BUCKET,S3_ACCESS_KEY,S3_SECRET_KEY,S3_REGION,S3_PREFIX,S3_FORCE_PATH_STYLEenv vars. Endpoint auto-detects SSL from scheme. Path-style URLs default on (required for MinIO/Ceph). Optional key prefix for shared buckets. CI pipeline syncs S3 secrets to K8s whenSTORAGE_BACKEND=s3. - Text extraction pipeline. PDF, DOCX, XLSX, PPTX, ODT, RTF text extraction via filesystem-based queue. Inline and sidecar modes. Crash recovery for items stuck in processing state.
- Admin storage panel. Backend health, file count, total size, orphan detection and cleanup. Shows PVC path or S3 endpoint/bucket depending on active backend. Extraction queue status.
- Vault CLI commands.
switchboard vault rekeyre-encrypts all provider API keys when rotating the encryption key.switchboard vault statusshows encryption health. Admin UI encryption status indicator in Settings tab. - Per-chat model persistence. Server-side
channels.settingsJSONB field storeslast_selector_id. Roams across devices with localStorage write-through cache as fallback.
[0.11.0] — 2026-02-25
Added
- Browser extension system. Full lifecycle: manifest registration, script
injection, scoped context (
ctx.renderers,ctx.tools,ctx.events,ctx.storage,ctx.ui), permission-aware API proxying. Extensions self- register viaExtensions.register()and receive isolated contexts duringinitAll(). Admin CRUD endpoints, asset serving (public, no auth needed for<script>tag loading), auto-seeder for builtin extensions. - Custom renderer pipeline. Block renderers intercept code fences by
language tag, post renderers process the DOM after insertion. Case-
insensitive language matching handles LLM capitalization (
Mermaid,Diff,CSV). Nested```markdown ```fence unwrapping prevents the common LLM pattern of wrapping responses in markdown fences from breaking inner code blocks. - Browser tool bridge. Extensions register tool handlers via
ctx.tools.handle(). Server collects browser tool schemas alongside server tools, includes them in LLM completions. Tool calls route through WebSocket EventBus (tool.call.*→ browser →tool.result.*→ server). 30-second timeout with error recovery. - Server tools: calculator and datetime. Auto-register via
init(), zero wiring changes. Calculator: recursive-descent evaluator with arithmetic, 17 math functions, constants. Datetime: current date/time/ timezone/unix/ISO-week in any IANA timezone. - 6 built-in browser extensions (self-contained, own CSS via
_injectStyles()/destroy()lifecycle):- Mermaid: block + post renderer, SVG diagrams, dark mode detection, local vendor with CDN fallback, max-height constraint with scroll
- KaTeX: block renderer (
```latex/math/tex ```) + post renderer (inline$...and...$` in text nodes) - CSV Table: block renderer, RFC 4180 parser with quoted fields, sortable columns (click headers), numeric-aware sorting
- Diff Viewer: block renderer, red/green syntax highlighting for unified diffs, stats badge (+N/-N), hunk/file headers
- JS Sandbox: browser tool (
js_eval), sandboxed iframe (allow-scriptsonly), console capture, 10s timeout - Regex Tester: browser tool (
regex_test), multi-input matching, full match details with named groups and indices
- Admin extension editor. Edit button on each extension in Admin → Extensions. Inline editor shows name, description, manifest JSON, and script source with tab-key support. System extensions show overwrite warning.
- Enhanced diagnostics. Test 5: browser extensions (loaded, active, renderers, tool handlers). Test 6: Service Worker cache (registration, scope, state, cache names, entry counts). Purge Cache button in Debug Log modal footer.
- Extension-owned styles. All 4 rendering extensions (mermaid, katex,
csv, diff) inject their own
<style>tags duringinit()and remove them duringdestroy(). Global stylesheet only keeps.ext-renderedwrapper. User-created extensions can bring their own CSS without touching the global stylesheet. - Notes extension rendering.
runExtensionPostRender()called in both note read mode and preview mode. Mermaid diagrams and KaTeX math now render correctly in notes. - Database migration 006_extensions.sql.
extensionsandextension_user_settingstables.
Fixed
- WebSocket token field mismatch. API saved
{ accessToken: '...' }but EventBus readtokens.access. Tool bridge was dead on arrival. - Extension asset 401. Asset route was behind auth middleware, but
<script>tags don't send Authorization headers. Moved to public group. runExtensionPostRender is not defined. Stale Service Worker cache served oldui-format.jswithout the function. Addedtypeofguard.- Extension init order. Extensions loaded after
loadChats()— block renderers not registered when messages first rendered. Moved extension loading before chat loading. - K8s Ingress WebSocket routing. Traefik
pathType: Exactdoesn't reliably win overPrefixrules in the same Ingress resource. Changed/wsand/healthtoPrefixfor longest-prefix-wins routing. - Mermaid SVG oversized. Added
max-height: 600pxon diagram container with overflow scroll, removed mermaid.js hardcodedheightattribute from SVGs so CSS constraints apply.
Changed
- Extension CSS removed from global
styles.css. Each extension owns its styles via_injectStyles()ininit()withdestroy()cleanup. Idempotent injection guards prevent duplicates. - Markdown fence language extraction lowercased at the point of extraction
in
ui-format.js, making all block renderer pattern matches case-insensitive without per-extension workarounds. _unwrapMarkdownFence()pre-processor strips outer```markdown ```wrappers when they contain nested fences. Handles think-block placeholders and trailing explanation text. Only triggers when nested fences are present — plain markdown code blocks still render normally.
[0.10.5] — 2026-02-24
Added
ui-primitives.js— shared rendering primitives and registries. Single source of truth for provider types, role definitions, and reusable UI components. Extension-ready via registry pattern (Providers.add(),Roles.add()). Primitives follow therenderPresetForm()pattern:(container, options) → control objectwithgetValues/setValues/clear.Providersregistry — types, labels, default endpoints (was 5 places → 1)Rolesregistry — names, type filters, hints (was hardcoded in 2 places → 1)renderCapBadges(caps, opts)— consolidates 3 badge builders (compact + detailed)renderProviderForm(container, opts)— replaces 3 form implementationsrenderProviderList(container, opts)— replaces 3 list renderersrenderRoleConfig(container, opts)— replaces 2 role UIs + 4 handlersrenderUsageDashboard(container, opts)— replaces 3 usage renderers
Fixed
- Admin provider form now auto-fills endpoint on type change. Was missing from admin (worked in user BYOK and team forms). Now all three scopes use the same primitive with identical behavior.
- Team provider form consolidated. Was two separate HTML forms (create + edit) with separate listeners. Now a single dual-mode form matching the pattern used by admin and user BYOK scopes.
Changed
- Provider type definitions removed from
index.html(2 static<select>s) andsettings-handlers.js(1 dynamic build + 2 endpoint maps). All now sourced fromProvidersregistry inui-primitives.js. - Provider list rendering uses event delegation instead of inline
onclickhandlers. Each list returns{ refresh, getCache }control handles. - Role configuration uses
data-role-*attributes for event delegation instead ofid-based selectors and globalonchangehandlers. - Usage dashboards accept options for compact/full mode, custom API calls, and extension-provided extra columns.
- All 16
confirm()calls replaced withshowConfirm()— styled modal dialog matching the app design instead of browser-native dialog. Supportsdangerstyling, Escape/Enter keys, click-outside dismiss. - Sidebar collapse icon now always visible (dimmed) next to the logo, brightens on hover. Previously the icon replaced the logo on hover only. When sidebar is collapsed, only the collapse icon shows (logo hidden).
- New
.popup-menu+.popup-menu-itemshared CSS base for all dropdown and flyout menus.createPopupMenu(anchor, opts)primitive available for future menu creation with consistent behavior. - Removed ~360 lines of duplicated code across
admin-handlers.js,settings-handlers.js,ui-admin.js, andui-settings.js. - Removed ~40 lines of static HTML form markup from
index.html.
[0.10.4] — 2026-02-24
Added
model_typefield across the full pipeline. Models now carry a type classification (chat,embedding,image) sourced from provider APIs at sync time — no hardcoded lists. Venice's/v1/modelsreturnstypeper model; OpenAI-compatible APIs pass through the field when present.- New DB column:
model_catalog.model_type VARCHAR(20) DEFAULT 'chat' - Migration:
005_model_type.sql - Propagation:
providers.Model.Type→CatalogSyncEntry.ModelType→CatalogEntry.ModelType→UserModel.ModelType→ frontendmodel_type
- New DB column:
Fixed
- Admin role save didn't refresh UI.
adminSaveRole()showed "✓ Saved" but never calledUI.loadAdminRoles(), so dropdowns appeared stale after save. Now reloads the roles panel after a successful save. - Role model dropdowns showed all models regardless of type. Embedding
role showed chat models, utility role showed embedding models. Both admin
and user role UIs now filter the model dropdown by
model_type:- "embedding" role → only
model_type === 'embedding'models - "utility" role → only
model_type === 'chat'models
- "embedding" role → only
Changed
- Venice provider now reads the
typefield from each model in the API response and normalizes it (text→chat,embedding→embedding,image→image). - OpenAI provider wire type extended with optional
typefield for OpenAI-compatible APIs that include it.
[0.10.3] — 2026-02-24
Changed
-
Frontend refactor: 2 monolith files → 13 domain-scoped files. Split
ui.js(2,582 lines) andapp.js(2,940 lines) into 13 focused files averaging ~544 lines each. No features added, no functions renamed, no architectural changes. Vanilla JS, no modules, no build step.New file structure:
File Lines Domain ui-format.js 353 Markdown rendering, esc(), code blocks, side panelui-core.js 974 UI object: sidebar, chat list, messages, streaming, model selector ui-settings.js 640 Settings tabs, teams, providers, user preferences ui-admin.js 645 Admin tabs, users, roles, usage, teams tokens.js 123 Context tracking, token estimation notes.js 364 Notes panel, editor, multi-select chat.js 584 Chat ops, send, regen, edit, branch, summarize settings-handlers.js 692 Settings save, provider CRUD, command palette admin-handlers.js 652 Admin actions, presets, team management app.js 567 State, init, boot, auth, listener dispatch Unchanged:
api.js(575),debug.js(580),events.js(327). -
initListeners()decomposed into domain-specific init functions:_initChatListeners(),_initSettingsListeners(),_initAdminListeners(),_initNotesListeners(),_initGlobalKeyboard(). The orchestrator inapp.jsdispatches to each. -
Side panel resize changed from self-invoking IIFE to
_initSidePanelResize()called during listener init, avoiding DOM timing issues. -
Service worker updated with new file list for pre-caching.
-
Policy-gating tests updated to read from the correct source files after the split. All 159 tests pass.
[0.10.2] — 2026-02-24
Added
- Summarize & Continue — User-triggered conversation compaction using the
utility model role. Button appears in context warning bar at ≥75% context
usage.
POST /channels/:id/summarizecalls the utility role to generate a summary, inserts it as a tree node withmetadata.type = "summary", and updates the cursor. Subsequent completions use the summary as a context boundary — messages before it are replaced by the summary as a system message. Multiple summaries stack. Fork-aware (summaries are tree nodes with their own branch position). "Show full history" toggle reveals collapsed earlier messages. - BYOK Role Overrides — Users with personal providers can override the org's
utility and embedding model roles. Resolution chain: personal → team → global.
New "Model Roles" tab in Settings (visible when BYOK is enabled). Stored in
user.settingsJSONB undermodel_roleskey, same shape as team overrides. - Utility Rate Limiting —
utility_rate_limitglobal setting (default: 20 calls/hour/user, 0 = unlimited). Org-funded utility calls check againstusage_logbefore executing. BYOK calls exempt (user pays their own way). Returns 429 with clear message when exceeded. - Message metadata in path —
PathMessagenow includesmetadataJSONB field, enabling summary detection and future message-type extensibility. - Per-chat model/preset restore — Switching between chats now restores the last-used model or preset in the selector. Stored in localStorage keyed by channel ID. Falls back to the channel's base model, then the global default, if the original selection is no longer available. Cleaned up on chat deletion.
Changed
- Generation role removed —
RoleGeneration("generation") removed fromValidRolesand admin Roles tab. Image/media generation will be extension-managed (v0.11.0) with its own provider config, not a role slot. The current completion/embedding abstraction doesn't fit image gen's fundamentally different API surface. - Resolver.Complete/Embed signatures — Now accept
userIDparameter for personal role override resolution. Empty string skips personal lookup. - Proactive token refresh — Access token is now refreshed at 80% of its lifetime (~12min for 15min tokens). On page reload, a conservative 60s refresh is scheduled since token age is unknown. Eliminates the race condition where profile succeeds on a near-expired token but subsequent calls fail.
- Auth guard in
startApp()— If token is invalidated during startup (e.g., 401 on loadChats after profile succeeded), app returns to login instead of rendering a broken UI with cascading 401 errors. - Per-chat model restore fallback — When the stored model/preset is removed, falls back to admin default → first visible model (was keeping stale selection). Also cleans up the stale localStorage entry.
- BYOK Role Overrides — Fixed response parsing (
configs.configsnotconfigs.data) and model field mapping (configId/baseModelIdnotprovider_config_id/model_id). GetRoleendpoint integration test —GET /admin/roles/:rolenow exercised by CI viaTestIntegration_Roles_GetSingleRole, catching theGetConfigsignature mismatch that caused the build failure.- Auth resilience frontend tests — 10 tests covering startup auth guard, token refresh lifecycle, and the profile-success-then-401 edge case.
- JS syntax lint in CI —
node --checkruns on allsrc/js/*.jsfiles before frontend tests, catching parse errors that tests alone can't detect.
[0.10.1] — 2026-02-24
Added
- Admin System Prompt — Global system prompt configured in Admin › Settings.
Injected as the first system message in every conversation, before user/preset
system prompts. Users cannot override or disable it. Stored in
global_settings['system_prompt']. - Personas tab — User presets moved from the Models tab to their own
"Personas" tab in Settings. Tab is hidden when admin disables user presets
(
allow_user_personaspolicy). - Team Management modal — Team admin functionality extracted from Settings into its own tabbed modal (Members, Providers, Presets, Usage, Activity). Accessed via "Team Management" flyout menu item, or "Manage →" on team cards in Settings. Multi-team admins see a picker first; single-team admins go straight to the tabbed view. Back arrow in header returns to picker.
- Preview pane: clear button — Trash icon in the preview header resets the iframe and shows the empty hint. Also auto-clears when deleting a chat that had active preview content.
- Preview/Notes pane: fullscreen — Expand button in the header toggles fullscreen mode (panel fills entire viewport width).
- Preview/Notes pane: resizable — Drag the left edge of the side panel to resize (280px–70vw). Width resets on close.
- Code block: download — "Download" button on every code block. Infers
file extension from language tag (e.g.
python→.py,go→.go).
Changed
- Personal Usage scoped to BYOK —
GET /usageand the user Usage tab now only show consumption against personal (BYOK) providers. Global provider usage is the org's cost, not the user's. NewQueryByUserPersonalstore method filters onscope = 'personal' AND owner_id = user_id. Admin usage views remain unaffected. Usage tab hidden when admin disables user providers (allow_user_byokpolicy). - OpenAI streaming usage race — Deferred the Done event until the usage
chunk arrives. Previously, finish_reason fired Done before the usage chunk
(which has
choices: []), so streaming token counts were always 0 for all OpenAI-compatible providers.
Fixed
- Usage logging zero-token guard — Removed early exit in
logUsagethat suppressed rows when tokens were 0. Combined with the streaming race above, this meant no streaming usage was ever recorded. - Live chat completion test —
TestLive_VeniceChatCompletionsent wrong field names (messages/config_idinstead ofcontent/provider_config_id).
[0.10.0] — 2026-02-24
Added
- Model Roles — Named model slots (
utility,embedding,generation) with primary + fallback bindings. Stored inglobal_settings['model_roles']. Team-level overrides viateams.settingsJSONB. Newserver/roles/package withResolver.Complete()andResolver.Embed()for automatic failover. Admin API:GET/PUT /admin/roles/:role,POST /admin/roles/:role/test. Team API:GET/PUT/DELETE /teams/:id/roles/:role. - Provider Embed() interface — All providers implement
Embed(). OpenAI, Venice, OpenRouter support/v1/embeddings; Anthropic returnsErrNotSupported. New types:EmbeddingRequest,EmbeddingResponse. - Usage Tracking — Every completion (streaming and non-streaming) logs
token counts and cost to
usage_logtable. Cost calculated at insert time frommodel_pricing(no retroactive recalculation). Provider scope denormalized for efficient admin filtering. Admin views exclude BYOK. New stores:UsageStore,PricingStore. Admin API:GET /admin/usage,GET /admin/usage/users/:id,GET /admin/usage/teams/:id. User API:GET /usage(personal summary). - Model Pricing —
model_pricingtable with catalog sync and manual admin overrides. Sync from provider APIs (Venice, OpenRouter) auto-populates pricing withsource='catalog'; manual entries never overwritten. Admin API:GET/PUT/DELETE /admin/pricing. - Streaming token capture — OpenAI:
stream_options.include_usage+ parse usage/cache from final chunk. Anthropic: parsemessage_startfor input/cache tokens,message_deltafor output tokens. Cache token fields (CacheCreationTokens,CacheReadTokens) onCompletionResponse,StreamEvent, andstreamResult. - Admin Roles tab — Configure primary/fallback provider+model per role, test-fire from UI.
- Admin Usage dashboard — Period selector (7d/30d/90d), group-by (model/user/day/provider), totals cards, breakdown table, pricing table.
- Team Usage dashboard — Team admins can view usage against their
team-owned providers via
GET /teams/:teamId/usage. Filters toprovider_configs WHERE scope='team' AND owner_id=teamId. Integrated into team management panel with period/group-by selectors. - Admin Reset Password — Button in user list with vault destruction warning dialog (two-step confirmation).
- User Usage tab — Settings modal "Usage" tab shows personal token consumption and estimated costs across all conversations, including BYOK. Period selector (7d/30d/90d) and group-by (model/day).
- Live Venice integration tests — Gated behind
VENICE_API_KEYenv var. Tests: non-streaming completion, streaming completion, usage logging for both modes, pricing from catalog sync, and direct embeddings via BGE-M3. Usesqwen3-4b(Venice Small) — cheapest at $0.05/$0.15 per 1M tokens. - Migration 004 —
usage_logtable,model_pricingtable,model_rolesseed inglobal_settings.
Fixed
- UEK re-wrap on password change —
ChangePasswordnow decrypts UEK with old password and re-encrypts with new password + fresh salt. Previously, password changes silently broke all personal BYOK keys. - UEK destruction on admin password reset —
ResetPasswordnow nullifies vault columns, evicts UEK from cache, deletes personal provider configs, and logs an audit event. Previously, admin resets left orphaned encrypted UEK that silently broke personal keys. - Streaming completions logged zero tokens —
StreamEventlacked token fields and providers discarded usage data from final chunks. Both OpenAI and Anthropic streaming parsers now capture and propagate token counts. - OpenAI streaming usage race condition — The OpenAI protocol sends
chunks in order: content → finish_reason → usage → [DONE]. The parser
sent
Done=trueon the finish_reason chunk (step 2) before the usage chunk arrived (step 3, withchoices:[]). The receiver returned immediately on Done, so usage was captured but never delivered. Fix: defer the Done event aspendingFinishuntil the usage chunk arrives, then flush with tokens attached. Tool-call finishes flush immediately (tool loop needs the event). Affects all OpenAI-compatible providers (OpenAI, Venice, OpenRouter). - Token accumulation across tool iterations —
streamResultinstream_loop.gonow accumulates input/output/cache tokens across multi-tool-call iterations instead of only capturing the final iteration. - Admin pricing leaked BYOK entries —
PricingStore.List()returned all model_pricing rows including those from personal BYOK providers. Admin panel showed pricing entries for user-private providers they shouldn't see, with O(users × models) scale explosion. Now joins onprovider_configsand filtersscope != 'personal'. AdminUpsertPricingalso validates provider scope, rejecting manual pricing on personal providers. - Team role handlers used wrong param name —
ListTeamRoles,UpdateTeamRole,DeleteTeamRolereadc.Param("id")but routes use:teamId. Every team role operation silently got an empty team ID. - Usage logging suppressed for zero-token streams —
logUsagehad an early exit wheninputTokens == 0 && outputTokens == 0. Combined with the OpenAI streaming parser bug (below), this silently dropped all streaming usage rows. Removed the guard — requests are always recorded. - Live chat completion test used wrong field names —
TestLive_VeniceChatCompletionsentmessagesandconfig_idbut the handler expectscontentandprovider_config_id. Test silently skipped on the binding error.
[0.9.4] — 2026-02-24
Added
- API key encryption (vault) — Two-tier AES-256-GCM encryption for stored
API keys. Global/team keys encrypted with env-var-derived key (HKDF-SHA256);
personal BYOK keys encrypted with per-user encryption key (UEK) derived from
password via Argon2id. Admin cannot recover personal keys without user's
passphrase. New
server/crypto/package:vault.go,cache.go,resolver.go,backfill.gowith 11 round-trip tests. - UEK lifecycle — UEK generated on registration, unwrapped on login
(Argon2id → AES-GCM), cached in
sync.Mapfor session duration, evicted with memory zeroing on logout. Pre-migration users auto-initialize vault on first login. - Migration 003_vault.sql — Adds
encrypted_uek,uek_salt,uek_nonce,vault_setto users table. Addsapi_key_enc(BYTEA),key_nonce,key_scopeto provider_configs. Backfillskey_scopefrom existing scope. - Startup backfill —
BackfillEncryptedKeys()encrypts plaintext API keys on first startup withENCRYPTION_KEYset.EnforceEncryptionKey()refuses startup if encrypted keys exist but env var is missing. - CI/CD: encryption secret —
ENCRYPTION_KEYGitea secret synced to k8sswitchboard-encryptionsecret. Backend manifest references it as optionalsecretKeyRef.
Fixed
- HTML code blocks render live in chat (XSS) — When a model's
</think>tag directly abutted a code fence (no newline), the fence wasn't recognized by marked.js, causing raw HTML to render as live DOM elements (canvas games, styles, etc.). Three-layer fix: (1) DOMPurify switched from permissiveADD_TAGS(default allows canvas, style, form, etc.) to strictALLOWED_TAGSallowlist of only markdown-produced elements; (2) think-block placeholders padded with\n\nto ensure adjacent fences start on fresh lines; (3) unclosed code fences auto-closed beforemarked.parsefor streaming protection.
[0.9.3] — 2026-02-23
Changed
- Code blocks: button-driven collapse — Replaced
<details>wrapper with inline collapse/expand toggle button in the code toolbar. Auto-collapses at15 lines with a fade mask; toggle available on all blocks >5 lines. User can always expand/collapse regardless of threshold. Smooth CSS transition instead of native
<details>jump. - Thinking blocks: always visible — Thinking blocks are always rendered in
the DOM regardless of the
showThinkingsetting. The setting now controls whether blocks start expanded (<details open>) or collapsed. User can always click to toggle. Setting label updated to "Auto-expand thinking blocks".
Added
- Admin default model — New
default_modelpolicy in Admin → Settings. Dropdown populated from enabled models. When a user has no saved selection (fresh login, cleared browser) or their saved model is no longer available, the admin default is used before falling back to first visible. Resolution chain:localStorage → admin default → first visible. - Reasoning/thinking support for OpenAI-compatible providers — Models that
send
reasoning_contentin stream deltas (Grok, DeepSeek, etc.) now have thinking blocks streamed live and persisted as<think>tags. Renders as collapsible thinking blocks in both streaming and history views. - Favicon animation during generation — Browser tab favicon pulses with cascading dot opacity animation while a completion is in progress, restoring to the static favicon when done.
- Tool calls in message history — Tool call metadata (name, arguments,
result, error status) is now persisted alongside assistant messages in the
database. History view renders tool calls as collapsed
<details>blocks showing "🔧 tool_name → done" with expandable input/output JSON. - Notes tool: "View note" link — When a notes tool (
note_create,note_update, etc.) completes, a "📝 View note" button appears in both the live streaming tool indicator and the history tool call block. Clicking opens the Notes panel and navigates directly to the note. - Notes panel: Copy button — "Copy" button added to note read mode, copies title + content as markdown to clipboard.
- Brand hover: jitter-free crossfade — Sidebar brand logo→collapse icon
swap uses opacity crossfade instead of
display: nonetoggle, eliminating layout reflow jitter on hover.
Fixed
- Model selector shows unavailable model — Selector restoration searched all models including user-hidden ones. Saved selection (localStorage) for a hidden model like Claude Opus would re-select it on every page load even when only Grok was visible. Resolution now filters to visible models only.
- Refresh toast shows wrong count — "Loaded 33 models" counted all models including hidden; now shows only visible count ("Loaded 1 model").
- Tool calls vanish after completion — Live streaming tool indicators
disappeared when
reloadActivePath()rebuilt messages from DB. Fixed: thegetActivePathquery now includestool_callscolumn, andPathMessagecarries the data through to the frontend renderer. - Tool result "undefined results" text — Operator precedence bug in tool
result summary parser caused
undefined resultsto display for note_create. Fixed summary logic to properly branch on title vs count. - Regenerate streams at wrong position — Regen'd response appeared below the full conversation (appended at bottom) then snapped to correct position after completion. Fixed: display is now truncated to the parent message before streaming starts, so the new response streams in-place as a clean branch. Backend context was already correct (excludes old response).
- Regenerate loses tools, reasoning, and tool execution — Regen handler
was a stripped-down copy of the completion handler missing tool definitions,
tool execution loop, reasoning_content forwarding, and tool_calls persistence.
Model lost access to note tools on regen and fell back to generic capabilities.
Refactored: extracted
streamWithToolLoop()intostream_loop.goas the single canonical streaming implementation. BothstreamCompletion(normal chat) andRegeneratenow call the shared function — only persistence differs. Future streaming features (new event types, tool capabilities) automatically apply to all code paths. - OpenRouter free models crash — Free models with nil pricing caused
pq: invalid input syntax for type jsonon catalog sync. Nil pricing now routes throughToJSON()producing"{}"instead of nil[]byte. - API key appears unsaved —
ListGlobalConfigsreturned rawProviderConfigstructs whereAPIKeyEncisjson:"-"(never serialized), sohas_keywas alwaysundefined. Now returns computedhas_keyfield. - Case-insensitive usernames — Login, registration, and all user lookups
use
LOWER()in SQL. All user creation paths normalize to lowercase. New migration002_ci_username.sqladdsLOWER()unique indexes and normalizes existing rows.
[0.9.2] — 2026-02-23
Added
- Collapsible code blocks: Code blocks over 15 lines auto-collapse into
<details>with language and line count summary. Language label shown in top-left corner of all code blocks. - HTML preview: "Preview" button on HTML code blocks opens a sandboxed
iframe (
allow-scripts, noallow-same-origin). Auto-detected for untagged blocks via heuristic. - Token count estimate: Live token counter below input area showing
approximate tokens and context usage percentage when model has
max_context. - Context length warning: Dismissable banner above input at 75% (yellow) and 90% (red) context usage with guidance to start a new chat.
- Proxy interception detection:
_parseJSON()checks Content-Type before.json()on all API paths including streaming. TypedproxyBlockederrors with proxy page title extraction and actionable splash messages. - Environment injection:
window.__ENV__wired through entrypoint, k8s, and index.html for dev/test/production gating. - Enhanced diagnostics:
NET:PROXYlog type, Content-Type capture in fetch interceptor, environment info in export header and state snapshot. - Team admin audit scoping: New
GET /api/v1/teams/:teamId/auditand/audit/actionsendpoints scoped to team members. Activity Log section in team manage panel with filter dropdown and pagination. - New favicon: Switchboard panel design with provider-colored jacks. Animated SVG (rotating plugs), 32px PNG, 256px PNG, and ICO.
- Seed users (dev/test only):
SEED_USERS=user:pass:role,...env var pre-creates active users on startup. Ignored in production. K8s secretswitchboard-seed-userswired in backend manifest.
[0.9.1] — 2026-02-23
Removed
- Static known model table: Deleted
knownModelsmap from backend andKNOWN_MODELSfrom frontend. The same model ID can have different capabilities depending on the provider (e.g. DeepSeek has tool_calling on OpenRouter but not on Venice). A hardcoded table can't represent this. - Frontend
lookupKnownCaps(): Removed client-side capability guessing. Backend is the sole source of truth via catalog → heuristic chain.
Changed
- Resolution chain simplified: catalog (provider API sync) → heuristic inference. No intermediate known table. Providers that report capabilities via API are authoritative; heuristics are best-effort for unsynced models.
- All providers updated: OpenAI, OpenRouter, Anthropic, Venice now call
InferCapabilities()directly instead of the dead known table lookup. - Frontend
resolveCapabilities(): Now passes through backend caps as-is. No client-side merging with a static table. - EXTENSIONS.md recovered into repo, updated with Appendix A (Custom Renderers) and Appendix B (Model Roles with utility/embedding/generation slots)
- ROADMAP.md restructured: extension foundation pulled to v0.11.0, model roles to v0.10.0, dependency graph, TBD replaces post-1.0, removed v0.8→v0.9 migration (OBE — no public release, no test path)
Added
- Heuristic patterns: Updated to detect qwen3, gpt-5, grok, kimi, minimax, glm-5, gemma-3 model families. Vision expanded to claude-opus/sonnet (not just claude-3). Reasoning expanded for thinking, grok, glm patterns.
Fixed
- Preset capability pills: Presets with auto-resolve (no
provider_config_id) now inherit base model capabilities viaGetByModelIDAnycatalog fallback. - Venice
optimizedForCode: Added mapping toCodeOptimizedcapability. - CI test stability: BYOK journey tests use unreachable endpoints so auto-fetch doesn't race with simulated data injection.
[0.9.0] — 2026-02-22
Added
- Schema consolidation: 21 migrations collapsed to single
001_initial.sql - Store layer: All database access through typed interfaces (no raw SQL in handlers)
- Persona model: Trust-boundary model replacing old presets; scoped global/team/personal
- Capabilities resolver: Three-tier chain — catalog → known table → heuristic inference
- Three-state model visibility: enabled / disabled / team-only
- BYOK auto-fetch: Creating a personal provider triggers model discovery from provider API
- User model refresh:
POST /api-configs/:id/models/fetchendpoint + UI button - Composite model IDs:
configId:modelIdformat prevents cross-provider collisions - Audit log foundation: All admin operations logged with actor, action, resource
- Journey integration tests: API-driven test suite replacing fake-data tests
- Frontend test suite: 107 tests, 27 suites validating model processing pipeline
- Live Venice API test: Proves real BYOK → auto-fetch → models visible flow
Fixed
- API key storage:
json:"-"tag onProviderConfig.APIKeyEncsilently dropped keys during admin create/update. Fixed with wrapper structs that bypass the tag. - NULL model_default scan:
scanProviders()crashed on NULLmodel_defaultcolumn, silently hiding all team and BYOK models. Fixed withsql.NullString. - Nil slice serialization: Go nil slices serialized as JSON
nullinstead of[], breaking frontend fallback chains. Fixed withmake([]T, 0). - Frontend error swallowing: API responses with
errorsfield were silently ignored.
Changed
- Backward-compatible API routes with v0.8 field name aliases
- User model preferences table (
user_model_settings)