This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/CHANGELOG.md
2026-02-25 13:29:15 +00:00

596 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to Chat Switchboard.
## [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 (280px70vw). 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`)