1148 lines
66 KiB
Markdown
1148 lines
66 KiB
Markdown
# Changelog
|
||
|
||
All notable changes to Chat Switchboard.
|
||
|
||
## [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 via `UI.toast()`
|
||
- `openPreview(html)` — load HTML into side panel preview iframe
|
||
- `isDark()` — theme detection without DOM sniffing
|
||
- `isMobile()` — viewport width check (≤768px)
|
||
- `isPanelOpen()` — side panel container visibility
|
||
- `confirm(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.ui` primitives exclusively.
|
||
Zero direct references to `UI.*`, `PanelRegistry.*`, or DOM class
|
||
sniffing for theme detection. Extension remains fully self-contained
|
||
in `extensions/builtin/mermaid-renderer/`.
|
||
- Mermaid source copy uses `ctx.ui.toast()` instead of inline button
|
||
text swap. Theme detection uses `ctx.ui.isDark()` instead of manual
|
||
`document.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), and `persona_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_review` memories.
|
||
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_review`
|
||
status. 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)
|
||
and `memory_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` — `memories`
|
||
table with composite unique index, full-text search index (GIN/keyword),
|
||
`memory_extraction_log` tracking 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 counts
|
||
- `PUT /memories/:id` — edit a memory's key/value/confidence
|
||
- `DELETE /memories/:id` — delete a memory
|
||
- `POST /memories/:id/approve` — approve a pending memory
|
||
- `POST /memories/:id/reject` — reject (archive) a pending memory
|
||
- `GET /admin/memories/pending` — admin pending review queue
|
||
- `POST /admin/memories/bulk-approve` — admin bulk approve
|
||
|
||
### Changed
|
||
- `CompletionHandler` now accepts an `*knowledge.Embedder` parameter 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_extraction` and
|
||
`memory_extraction_enabled` keys.
|
||
- `store.Stores` struct includes `Memories MemoryStore` field.
|
||
- 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 a `note_links` junction 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 by `GET /notes/search-titles?q=`. Results
|
||
are fetched via `ILIKE` title 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-DD` in 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_id` column 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_links` table.** New junction table with `source_note_id`,
|
||
`target_note_id` (nullable for dangling links), `target_title`,
|
||
`display_text`, `is_transclusion`, and `created_at`. Primary key on
|
||
`(source_note_id, target_title)`. Index on `target_note_id` for backlink
|
||
queries. Migrations for both Postgres and SQLite.
|
||
- **`source_message_id` column.** New nullable UUID column on `notes` table
|
||
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 CM6 `noteEditor` instance. The
|
||
container `#noteEditorContentContainer` is 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
|
||
calls `ResolveByTitle()` to fix dangling links from other notes.
|
||
- `showNotesList()` destroys the CM6 editor instance and hides graph view.
|
||
- `saveNote()` and `deleteNote()` call `invalidateNoteGraph()` 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.md` updated with `notelinks/` package, `note-graph.js`,
|
||
`CM.noteEditor()` factory, and expanded notes description covering the
|
||
linking model.
|
||
- `ROADMAP.md` updated with full v0.17.3 section including all checklist
|
||
items.
|
||
- CM6 bundle entrypoint (`index.mjs`) exports `noteEditor` alongside
|
||
existing `chatInput` and `codeEditor`.
|
||
- `theme.mjs` adds `noteEditorTheme` with 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.CM`
|
||
availability. 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 tracks `prefers-color-scheme` media query in real
|
||
time. Theme changes emit `theme.changed` on the EventBus; CM6 editors
|
||
toggle `oneDark` syntax 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.changed` event.
|
||
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-changes` job 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.frontend` and unified `Dockerfile`
|
||
now include a `cm6-build` stage (Node 20 Alpine → esbuild → IIFE bundle).
|
||
- `ChatInput` abstraction in `chat.js` replaces direct textarea access
|
||
across 7 callsites (`chat.js`, `tokens.js`, `attachments.js`).
|
||
- Extension editor in `admin-handlers.js` uses `CM.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.md` updated 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.sh` falls back to `npm install` if `package-lock.json`
|
||
is missing (belt-and-suspenders for local dev).
|
||
|
||
### Fixed
|
||
- **App initialization crash.** `Events.publish` (nonexistent) → `Events.emit`
|
||
(correct API). The unhandled exception during `initAppearance()` killed
|
||
`initListeners()`, 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 `--accent`
|
||
color (low contrast on both themes). Switched to `--text` for consistent
|
||
visibility. Added explicit `borderLeftWidth: 2px`.
|
||
- **Placeholder text offset.** Double padding between `.cm-editor` wrapper
|
||
(12px) and `.cm-content` (8px) pushed placeholder 20px below expected
|
||
position. Zeroed `.cm-content` padding — wrapper is the single source of
|
||
truth.
|
||
|
||
## [0.17.1] — 2026-02-27
|
||
|
||
### Added
|
||
- **SQLite backend.** Full dual-driver database layer — set `DB_DRIVER=sqlite`
|
||
to 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()` detects
|
||
`DB_DRIVER` and provisions either a Postgres test database or a SQLite
|
||
temp file. Exported `database.PH(n)` returns `$N` or `?` per dialect.
|
||
`database.TruncateAll()` uses `TRUNCATE CASCADE` on Postgres and
|
||
`DELETE FROM` with `PRAGMA foreign_keys` toggling on SQLite.
|
||
`dialectSQL()` helper in handler tests converts `$N` placeholders and
|
||
strips `::jsonb` casts at runtime.
|
||
- **SQLite CI pipeline.** New `test-sqlite` job runs the full handler
|
||
integration test suite and store tests against an embedded SQLite
|
||
database. Parallel with the existing Postgres test job. Build gate
|
||
verifies `CGO_ENABLED=0` compilation.
|
||
- **Generic provider test config.** Live provider integration tests now
|
||
read `PROVIDER`, `PROVIDER_KEY`, and `PROVIDER_URL` environment
|
||
variables instead of hardcoded `VENICE_API_KEY`. Legacy fallback
|
||
preserved. Model selection prefers non-reasoning models to avoid
|
||
thinking budget requirements with low `max_tokens`.
|
||
|
||
### Changed
|
||
- `kb_chunks` table in SQLite schema includes `embedding TEXT` column
|
||
for JSON-encoded float64 vectors (previously omitted as feature-gated).
|
||
- `SimilaritySearch` on SQLite loads candidate chunks, decodes JSON
|
||
embeddings, and computes cosine distance in Go — replacing the
|
||
previous "not available" error.
|
||
- `InsertChunks` on SQLite now stores embedding vectors as JSON text.
|
||
- Live test names genericized: `TestLive_Venice*` → `TestLive_*`.
|
||
- CI `build-and-deploy` depends on `[test, test-frontend, test-sqlite]`.
|
||
|
||
### Fixed
|
||
- **SQLite `RETURNING` + `time.Time` scan failure.** The modernc/sqlite
|
||
driver cannot scan `datetime('now')` TEXT columns into `time.Time` via
|
||
`RETURNING`. All 13 SQLite store `Create` methods rewritten to set
|
||
timestamps in Go (`time.Now().UTC()`) and use `ExecContext` instead of
|
||
`QueryRowContext(...).Scan()`. Format: `2006-01-02 15:04:05` (`timeFmt`
|
||
constant in `helpers.go`).
|
||
- **SQLite missing `id` in INSERTs.** Unlike Postgres (`DEFAULT
|
||
gen_random_uuid()`), SQLite `TEXT PRIMARY KEY` columns have no
|
||
auto-generation. Added `store.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`, `SeedGroupMember`
|
||
now branch on `IsSQLite()` to provide application-generated UUIDs
|
||
instead of relying on `RETURNING id`.
|
||
- **Postgres-isms in SQLite stores.** `extension.go` Update used `now()`
|
||
→ `datetime('now')`; `ListForUser` COALESCE used `true` → `1`.
|
||
- SQLite store bool/int type mismatches: `persona.go` auto-fetch flag,
|
||
`usage.go` exclude-BYOK filter, `user_settings.go` visibility map
|
||
values — all corrected from `0`/`1` to `false`/`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_search` tool 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 adds `persona_knowledge_bases` join table.
|
||
- **Enterprise KB mode.** New `discoverable` flag on knowledge bases
|
||
controls whether users can see and attach KBs directly. When
|
||
`kb_direct_access` platform policy is set to `true`, users cannot
|
||
attach KBs to channels directly — they access KBs exclusively through
|
||
persona bindings curated by admins. New `ListDiscoverableKBs` and
|
||
`SetDiscoverable` endpoints.
|
||
- **Role fallback alerts (issue #69).** When a role's primary provider
|
||
fails and the fallback activates, the resolver emits a `role.fallback`
|
||
event 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 skipped `resource_grants` checks from v0.16.0. Now uses
|
||
`PersonaStore.UserCanAccess()`.
|
||
- **KB create scope authorization.** Now enforces admin role for global
|
||
KBs and team admin role for team KBs.
|
||
- **Completion handler persona ID scoping.** `personaID` variable scoped
|
||
inside an `if` block but referenced downstream. Fixed by threading
|
||
through function signatures.
|
||
- **Nil slice JSON marshaling.** `ListDiscoverableKBs` returns `[]`
|
||
instead of `null` when no KBs match.
|
||
|
||
### Changed
|
||
- `UpdateDocumentStorageKey` moved from raw `ExecContext` to 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 in `attachments.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. `groups` and `group_members` tables. 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_grants` table
|
||
with `grant_scope` and `granted_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_grants` for
|
||
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_recall` tool.** Two operations: `list` returns filenames
|
||
and metadata for the current channel's attachments; `read` extracts
|
||
and returns content by attachment ID. Channel-scoped access control.
|
||
- **`conversation_search` tool.** Full-text search across the current
|
||
channel's message history using PostgreSQL `plainto_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_compact` channel setting.
|
||
- **Compaction service.** `compaction.Service` orchestrates 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_search` tool 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 as `kb_chunks`
|
||
with 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_search` tool.** 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_chunks` table includes `embedding vector(1536)` column
|
||
with IVFFlat index.
|
||
|
||
## [0.13.1] — 2026-02-26
|
||
|
||
### Added
|
||
- **`web_search` tool.** 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
|
||
via `SEARCH_PROVIDER` and `SEARXNG_URL` env vars.
|
||
- **`url_fetch` tool.** 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 `category` field (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.** `ObjectStore` interface 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 when
|
||
`STORAGE_BACKEND` is 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_STYLE` env
|
||
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 when `STORAGE_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 rekey` re-encrypts all
|
||
provider API keys when rotating the encryption key.
|
||
`switchboard vault status` shows encryption health. Admin UI
|
||
encryption status indicator in Settings tab.
|
||
- **Per-chat model persistence.** Server-side `channels.settings`
|
||
JSONB field stores `last_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 via `Extensions.register()` and receive isolated contexts during
|
||
`initAll()`. 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-scripts` only), 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 during `init()` and remove
|
||
them during `destroy()`. Global stylesheet only keeps `.ext-rendered`
|
||
wrapper. 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.** `extensions` and
|
||
`extension_user_settings` tables.
|
||
|
||
### Fixed
|
||
- **WebSocket token field mismatch.** API saved `{ accessToken: '...' }`
|
||
but EventBus read `tokens.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 old `ui-format.js` without the function. Added `typeof` guard.
|
||
- **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: Exact` doesn't
|
||
reliably win over `Prefix` rules in the same Ingress resource. Changed
|
||
`/ws` and `/health` to `Prefix` for longest-prefix-wins routing.
|
||
- **Mermaid SVG oversized.** Added `max-height: 600px` on diagram
|
||
container with overflow scroll, removed mermaid.js hardcoded `height`
|
||
attribute from SVGs so CSS constraints apply.
|
||
|
||
### Changed
|
||
- Extension CSS removed from global `styles.css`. Each extension owns its
|
||
styles via `_injectStyles()` in `init()` with `destroy()` 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 the `renderPresetForm()` pattern:
|
||
`(container, options) → control object` with `getValues/setValues/clear`.
|
||
- `Providers` registry — types, labels, default endpoints (was 5 places → 1)
|
||
- `Roles` registry — 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 implementations
|
||
- `renderProviderList(container, opts)` — replaces 3 list renderers
|
||
- `renderRoleConfig(container, opts)` — replaces 2 role UIs + 4 handlers
|
||
- `renderUsageDashboard(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)
|
||
and `settings-handlers.js` (1 dynamic build + 2 endpoint maps). All now
|
||
sourced from `Providers` registry in `ui-primitives.js`.
|
||
- Provider list rendering uses event delegation instead of inline `onclick`
|
||
handlers. Each list returns `{ refresh, getCache }` control handles.
|
||
- Role configuration uses `data-role-*` attributes for event delegation
|
||
instead of `id`-based selectors and global `onchange` handlers.
|
||
- Usage dashboards accept options for compact/full mode, custom API calls,
|
||
and extension-provided extra columns.
|
||
- All 16 `confirm()` calls replaced with `showConfirm()` — styled modal
|
||
dialog matching the app design instead of browser-native dialog. Supports
|
||
`danger` styling, 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-item` shared 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`, and `ui-settings.js`.
|
||
- Removed ~40 lines of static HTML form markup from `index.html`.
|
||
|
||
## [0.10.4] — 2026-02-24
|
||
|
||
### Added
|
||
- **`model_type` field 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/models` returns `type`
|
||
per 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` → frontend `model_type`
|
||
|
||
### Fixed
|
||
- **Admin role save didn't refresh UI.** `adminSaveRole()` showed "✓ Saved"
|
||
but never called `UI.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
|
||
|
||
### Changed
|
||
- Venice provider now reads the `type` field from each model in the API
|
||
response and normalizes it (`text` → `chat`, `embedding` → `embedding`,
|
||
`image` → `image`).
|
||
- OpenAI provider wire type extended with optional `type` field 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) and `app.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 panel |
|
||
| ui-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 in `app.js`
|
||
dispatches 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/summarize` calls the utility role to generate a
|
||
summary, inserts it as a tree node with `metadata.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.settings` JSONB under `model_roles` key, same shape as team overrides.
|
||
- **Utility Rate Limiting** — `utility_rate_limit` global setting (default: 20
|
||
calls/hour/user, 0 = unlimited). Org-funded utility calls check against
|
||
`usage_log` before executing. BYOK calls exempt (user pays their own way).
|
||
Returns 429 with clear message when exceeded.
|
||
- **Message metadata in path** — `PathMessage` now includes `metadata` JSONB
|
||
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 from
|
||
`ValidRoles` and 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 `userID` parameter 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.configs` not
|
||
`configs.data`) and model field mapping (`configId`/`baseModelId` not
|
||
`provider_config_id`/`model_id`).
|
||
- **`GetRole` endpoint integration test** — `GET /admin/roles/:role` now
|
||
exercised by CI via `TestIntegration_Roles_GetSingleRole`, catching the
|
||
`GetConfig` signature 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 --check` runs on all `src/js/*.js` files
|
||
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_personas` policy).
|
||
- **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 /usage` and the user Usage tab now
|
||
only show consumption against personal (BYOK) providers. Global provider
|
||
usage is the org's cost, not the user's. New `QueryByUserPersonal` store
|
||
method filters on `scope = 'personal' AND owner_id = user_id`. Admin usage
|
||
views remain unaffected. Usage tab hidden when admin disables user providers
|
||
(`allow_user_byok` policy).
|
||
- **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 `logUsage` that
|
||
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_VeniceChatCompletion` sent wrong
|
||
field names (`messages`/`config_id` instead of `content`/`provider_config_id`).
|
||
|
||
## [0.10.0] — 2026-02-24
|
||
|
||
### Added
|
||
- **Model Roles** — Named model slots (`utility`, `embedding`, `generation`)
|
||
with primary + fallback bindings. Stored in `global_settings['model_roles']`.
|
||
Team-level overrides via `teams.settings` JSONB. New `server/roles/` package
|
||
with `Resolver.Complete()` and `Resolver.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 returns `ErrNotSupported`.
|
||
New types: `EmbeddingRequest`, `EmbeddingResponse`.
|
||
- **Usage Tracking** — Every completion (streaming and non-streaming) logs
|
||
token counts and cost to `usage_log` table. Cost calculated at insert time
|
||
from `model_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_pricing` table with catalog sync and manual admin
|
||
overrides. Sync from provider APIs (Venice, OpenRouter) auto-populates
|
||
pricing with `source='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: parse `message_start` for
|
||
input/cache tokens, `message_delta` for output tokens. Cache token fields
|
||
(`CacheCreationTokens`, `CacheReadTokens`) on `CompletionResponse`,
|
||
`StreamEvent`, and `streamResult`.
|
||
- **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 to
|
||
`provider_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_KEY` env var.
|
||
Tests: non-streaming completion, streaming completion, usage logging for
|
||
both modes, pricing from catalog sync, and direct embeddings via BGE-M3.
|
||
Uses `qwen3-4b` (Venice Small) — cheapest at $0.05/$0.15 per 1M tokens.
|
||
- **Migration 004** — `usage_log` table, `model_pricing` table, `model_roles`
|
||
seed in `global_settings`.
|
||
|
||
### Fixed
|
||
- **UEK re-wrap on password change** — `ChangePassword` now 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** — `ResetPassword` now 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** — `StreamEvent` lacked 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=true` on the finish_reason chunk (step 2) before the usage
|
||
chunk arrived (step 3, with `choices:[]`). The receiver returned
|
||
immediately on Done, so usage was captured but never delivered.
|
||
Fix: defer the Done event as `pendingFinish` until 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** — `streamResult` in
|
||
`stream_loop.go` now 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 on
|
||
`provider_configs` and filters `scope != 'personal'`. Admin `UpsertPricing`
|
||
also validates provider scope, rejecting manual pricing on personal
|
||
providers.
|
||
- **Team role handlers used wrong param name** — `ListTeamRoles`,
|
||
`UpdateTeamRole`, `DeleteTeamRole` read `c.Param("id")` but routes use
|
||
`:teamId`. Every team role operation silently got an empty team ID.
|
||
- **Usage logging suppressed for zero-token streams** — `logUsage` had an
|
||
early exit when `inputTokens == 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_VeniceChatCompletion`
|
||
sent `messages` and `config_id` but the handler expects `content` and
|
||
`provider_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.go` with 11 round-trip tests.
|
||
- **UEK lifecycle** — UEK generated on registration, unwrapped on login
|
||
(Argon2id → AES-GCM), cached in `sync.Map` for 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_set` to users table. Adds `api_key_enc` (BYTEA), `key_nonce`,
|
||
`key_scope` to provider_configs. Backfills `key_scope` from existing scope.
|
||
- **Startup backfill** — `BackfillEncryptedKeys()` encrypts plaintext API keys
|
||
on first startup with `ENCRYPTION_KEY` set. `EnforceEncryptionKey()` refuses
|
||
startup if encrypted keys exist but env var is missing.
|
||
- **CI/CD: encryption secret** — `ENCRYPTION_KEY` Gitea secret synced to k8s
|
||
`switchboard-encryption` secret. Backend manifest references it as optional
|
||
`secretKeyRef`.
|
||
|
||
### 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 permissive
|
||
`ADD_TAGS` (default allows canvas, style, form, etc.) to strict
|
||
`ALLOWED_TAGS` allowlist of only markdown-produced elements; (2) think-block
|
||
placeholders padded with `\n\n` to ensure adjacent fences start on fresh
|
||
lines; (3) unclosed code fences auto-closed before `marked.parse` for
|
||
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 at
|
||
>15 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 `showThinking` setting. 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_model` policy 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_content` in 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: none` toggle, 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: the
|
||
`getActivePath` query now includes `tool_calls` column, and `PathMessage`
|
||
carries the data through to the frontend renderer.
|
||
- **Tool result "undefined results" text** — Operator precedence bug in tool
|
||
result summary parser caused `undefined results` to 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()` into `stream_loop.go` as the
|
||
single canonical streaming implementation. Both `streamCompletion` (normal
|
||
chat) and `Regenerate` now 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 json` on catalog sync. Nil pricing now
|
||
routes through `ToJSON()` producing `"{}"` instead of nil `[]byte`.
|
||
- **API key appears unsaved** — `ListGlobalConfigs` returned raw
|
||
`ProviderConfig` structs where `APIKeyEnc` is `json:"-"` (never serialized),
|
||
so `has_key` was always `undefined`. Now returns computed `has_key` field.
|
||
- **Case-insensitive usernames** — Login, registration, and all user lookups
|
||
use `LOWER()` in SQL. All user creation paths normalize to lowercase.
|
||
New migration `002_ci_username.sql` adds `LOWER()` 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`, no `allow-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. Typed `proxyBlocked` errors
|
||
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:PROXY` log 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/audit` and
|
||
`/audit/actions` endpoints 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 secret
|
||
`switchboard-seed-users` wired in backend manifest.
|
||
|
||
## [0.9.1] — 2026-02-23
|
||
|
||
### Removed
|
||
- **Static known model table**: Deleted `knownModels` map from backend and
|
||
`KNOWN_MODELS` from 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.
|
||
- [x] EXTENSIONS.md recovered into repo, updated with Appendix A (Custom
|
||
Renderers) and Appendix B (Model Roles with utility/embedding/generation slots)
|
||
- [x] 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 via `GetByModelIDAny` catalog fallback.
|
||
- **Venice `optimizedForCode`**: Added mapping to `CodeOptimized` capability.
|
||
- **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/fetch` endpoint + UI button
|
||
- **Composite model IDs**: `configId:modelId` format 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 on `ProviderConfig.APIKeyEnc` silently dropped
|
||
keys during admin create/update. Fixed with wrapper structs that bypass the tag.
|
||
- **NULL model_default scan**: `scanProviders()` crashed on NULL `model_default` column,
|
||
silently hiding all team and BYOK models. Fixed with `sql.NullString`.
|
||
- **Nil slice serialization**: Go nil slices serialized as JSON `null` instead of `[]`,
|
||
breaking frontend fallback chains. Fixed with `make([]T, 0)`.
|
||
- **Frontend error swallowing**: API responses with `errors` field were silently ignored.
|
||
|
||
### Changed
|
||
- Backward-compatible API routes with v0.8 field name aliases
|
||
- User model preferences table (`user_model_settings`) |