26 KiB
Changelog
All notable changes to Chat Switchboard.
[0.10.3] — 2026-02-24
Changed
-
Frontend refactor: 2 monolith files → 13 domain-scoped files. Split
ui.js(2,582 lines) andapp.js(2,940 lines) into 13 focused files averaging ~544 lines each. No features added, no functions renamed, no architectural changes. Vanilla JS, no modules, no build step.New file structure:
File Lines Domain ui-format.js 353 Markdown rendering, esc(), code blocks, side panelui-core.js 974 UI object: sidebar, chat list, messages, streaming, model selector ui-settings.js 640 Settings tabs, teams, providers, user preferences ui-admin.js 645 Admin tabs, users, roles, usage, teams tokens.js 123 Context tracking, token estimation notes.js 364 Notes panel, editor, multi-select chat.js 584 Chat ops, send, regen, edit, branch, summarize settings-handlers.js 692 Settings save, provider CRUD, command palette admin-handlers.js 652 Admin actions, presets, team management app.js 567 State, init, boot, auth, listener dispatch Unchanged:
api.js(575),debug.js(580),events.js(327). -
initListeners()decomposed into domain-specific init functions:_initChatListeners(),_initSettingsListeners(),_initAdminListeners(),_initNotesListeners(),_initGlobalKeyboard(). The orchestrator inapp.jsdispatches to each. -
Side panel resize changed from self-invoking IIFE to
_initSidePanelResize()called during listener init, avoiding DOM timing issues. -
Service worker updated with new file list for pre-caching.
-
Policy-gating tests updated to read from the correct source files after the split. All 159 tests pass.
[0.10.2] — 2026-02-24
Added
- Summarize & Continue — User-triggered conversation compaction using the
utility model role. Button appears in context warning bar at ≥75% context
usage.
POST /channels/:id/summarizecalls the utility role to generate a summary, inserts it as a tree node withmetadata.type = "summary", and updates the cursor. Subsequent completions use the summary as a context boundary — messages before it are replaced by the summary as a system message. Multiple summaries stack. Fork-aware (summaries are tree nodes with their own branch position). "Show full history" toggle reveals collapsed earlier messages. - BYOK Role Overrides — Users with personal providers can override the org's
utility and embedding model roles. Resolution chain: personal → team → global.
New "Model Roles" tab in Settings (visible when BYOK is enabled). Stored in
user.settingsJSONB undermodel_roleskey, same shape as team overrides. - Utility Rate Limiting —
utility_rate_limitglobal setting (default: 20 calls/hour/user, 0 = unlimited). Org-funded utility calls check againstusage_logbefore executing. BYOK calls exempt (user pays their own way). Returns 429 with clear message when exceeded. - Message metadata in path —
PathMessagenow includesmetadataJSONB field, enabling summary detection and future message-type extensibility. - Per-chat model/preset restore — Switching between chats now restores the last-used model or preset in the selector. Stored in localStorage keyed by channel ID. Falls back to the channel's base model, then the global default, if the original selection is no longer available. Cleaned up on chat deletion.
Changed
- Generation role removed —
RoleGeneration("generation") removed fromValidRolesand admin Roles tab. Image/media generation will be extension-managed (v0.11.0) with its own provider config, not a role slot. The current completion/embedding abstraction doesn't fit image gen's fundamentally different API surface. - Resolver.Complete/Embed signatures — Now accept
userIDparameter for personal role override resolution. Empty string skips personal lookup. - Proactive token refresh — Access token is now refreshed at 80% of its lifetime (~12min for 15min tokens). On page reload, a conservative 60s refresh is scheduled since token age is unknown. Eliminates the race condition where profile succeeds on a near-expired token but subsequent calls fail.
- Auth guard in
startApp()— If token is invalidated during startup (e.g., 401 on loadChats after profile succeeded), app returns to login instead of rendering a broken UI with cascading 401 errors. - Per-chat model restore fallback — When the stored model/preset is removed, falls back to admin default → first visible model (was keeping stale selection). Also cleans up the stale localStorage entry.
- BYOK Role Overrides — Fixed response parsing (
configs.configsnotconfigs.data) and model field mapping (configId/baseModelIdnotprovider_config_id/model_id). GetRoleendpoint integration test —GET /admin/roles/:rolenow exercised by CI viaTestIntegration_Roles_GetSingleRole, catching theGetConfigsignature mismatch that caused the build failure.- Auth resilience frontend tests — 10 tests covering startup auth guard, token refresh lifecycle, and the profile-success-then-401 edge case.
- JS syntax lint in CI —
node --checkruns on allsrc/js/*.jsfiles before frontend tests, catching parse errors that tests alone can't detect.
[0.10.1] — 2026-02-24
Added
- Admin System Prompt — Global system prompt configured in Admin › Settings.
Injected as the first system message in every conversation, before user/preset
system prompts. Users cannot override or disable it. Stored in
global_settings['system_prompt']. - Personas tab — User presets moved from the Models tab to their own
"Personas" tab in Settings. Tab is hidden when admin disables user presets
(
allow_user_personaspolicy). - Team Management modal — Team admin functionality extracted from Settings into its own tabbed modal (Members, Providers, Presets, Usage, Activity). Accessed via "Team Management" flyout menu item, or "Manage →" on team cards in Settings. Multi-team admins see a picker first; single-team admins go straight to the tabbed view. Back arrow in header returns to picker.
- Preview pane: clear button — Trash icon in the preview header resets the iframe and shows the empty hint. Also auto-clears when deleting a chat that had active preview content.
- Preview/Notes pane: fullscreen — Expand button in the header toggles fullscreen mode (panel fills entire viewport width).
- Preview/Notes pane: resizable — Drag the left edge of the side panel to resize (280px–70vw). Width resets on close.
- Code block: download — "Download" button on every code block. Infers
file extension from language tag (e.g.
python→.py,go→.go).
Changed
- Personal Usage scoped to BYOK —
GET /usageand the user Usage tab now only show consumption against personal (BYOK) providers. Global provider usage is the org's cost, not the user's. NewQueryByUserPersonalstore method filters onscope = 'personal' AND owner_id = user_id. Admin usage views remain unaffected. Usage tab hidden when admin disables user providers (allow_user_byokpolicy). - OpenAI streaming usage race — Deferred the Done event until the usage
chunk arrives. Previously, finish_reason fired Done before the usage chunk
(which has
choices: []), so streaming token counts were always 0 for all OpenAI-compatible providers.
Fixed
- Usage logging zero-token guard — Removed early exit in
logUsagethat suppressed rows when tokens were 0. Combined with the streaming race above, this meant no streaming usage was ever recorded. - Live chat completion test —
TestLive_VeniceChatCompletionsent wrong field names (messages/config_idinstead ofcontent/provider_config_id).
[0.10.0] — 2026-02-24
Added
- Model Roles — Named model slots (
utility,embedding,generation) with primary + fallback bindings. Stored inglobal_settings['model_roles']. Team-level overrides viateams.settingsJSONB. Newserver/roles/package withResolver.Complete()andResolver.Embed()for automatic failover. Admin API:GET/PUT /admin/roles/:role,POST /admin/roles/:role/test. Team API:GET/PUT/DELETE /teams/:id/roles/:role. - Provider Embed() interface — All providers implement
Embed(). OpenAI, Venice, OpenRouter support/v1/embeddings; Anthropic returnsErrNotSupported. New types:EmbeddingRequest,EmbeddingResponse. - Usage Tracking — Every completion (streaming and non-streaming) logs
token counts and cost to
usage_logtable. Cost calculated at insert time frommodel_pricing(no retroactive recalculation). Provider scope denormalized for efficient admin filtering. Admin views exclude BYOK. New stores:UsageStore,PricingStore. Admin API:GET /admin/usage,GET /admin/usage/users/:id,GET /admin/usage/teams/:id. User API:GET /usage(personal summary). - Model Pricing —
model_pricingtable with catalog sync and manual admin overrides. Sync from provider APIs (Venice, OpenRouter) auto-populates pricing withsource='catalog'; manual entries never overwritten. Admin API:GET/PUT/DELETE /admin/pricing. - Streaming token capture — OpenAI:
stream_options.include_usage+ parse usage/cache from final chunk. Anthropic: parsemessage_startfor input/cache tokens,message_deltafor output tokens. Cache token fields (CacheCreationTokens,CacheReadTokens) onCompletionResponse,StreamEvent, andstreamResult. - Admin Roles tab — Configure primary/fallback provider+model per role, test-fire from UI.
- Admin Usage dashboard — Period selector (7d/30d/90d), group-by (model/user/day/provider), totals cards, breakdown table, pricing table.
- Team Usage dashboard — Team admins can view usage against their
team-owned providers via
GET /teams/:teamId/usage. Filters toprovider_configs WHERE scope='team' AND owner_id=teamId. Integrated into team management panel with period/group-by selectors. - Admin Reset Password — Button in user list with vault destruction warning dialog (two-step confirmation).
- User Usage tab — Settings modal "Usage" tab shows personal token consumption and estimated costs across all conversations, including BYOK. Period selector (7d/30d/90d) and group-by (model/day).
- Live Venice integration tests — Gated behind
VENICE_API_KEYenv var. Tests: non-streaming completion, streaming completion, usage logging for both modes, pricing from catalog sync, and direct embeddings via BGE-M3. Usesqwen3-4b(Venice Small) — cheapest at $0.05/$0.15 per 1M tokens. - Migration 004 —
usage_logtable,model_pricingtable,model_rolesseed inglobal_settings.
Fixed
- UEK re-wrap on password change —
ChangePasswordnow decrypts UEK with old password and re-encrypts with new password + fresh salt. Previously, password changes silently broke all personal BYOK keys. - UEK destruction on admin password reset —
ResetPasswordnow nullifies vault columns, evicts UEK from cache, deletes personal provider configs, and logs an audit event. Previously, admin resets left orphaned encrypted UEK that silently broke personal keys. - Streaming completions logged zero tokens —
StreamEventlacked token fields and providers discarded usage data from final chunks. Both OpenAI and Anthropic streaming parsers now capture and propagate token counts. - OpenAI streaming usage race condition — The OpenAI protocol sends
chunks in order: content → finish_reason → usage → [DONE]. The parser
sent
Done=trueon the finish_reason chunk (step 2) before the usage chunk arrived (step 3, withchoices:[]). The receiver returned immediately on Done, so usage was captured but never delivered. Fix: defer the Done event aspendingFinishuntil the usage chunk arrives, then flush with tokens attached. Tool-call finishes flush immediately (tool loop needs the event). Affects all OpenAI-compatible providers (OpenAI, Venice, OpenRouter). - Token accumulation across tool iterations —
streamResultinstream_loop.gonow accumulates input/output/cache tokens across multi-tool-call iterations instead of only capturing the final iteration. - Admin pricing leaked BYOK entries —
PricingStore.List()returned all model_pricing rows including those from personal BYOK providers. Admin panel showed pricing entries for user-private providers they shouldn't see, with O(users × models) scale explosion. Now joins onprovider_configsand filtersscope != 'personal'. AdminUpsertPricingalso validates provider scope, rejecting manual pricing on personal providers. - Team role handlers used wrong param name —
ListTeamRoles,UpdateTeamRole,DeleteTeamRolereadc.Param("id")but routes use:teamId. Every team role operation silently got an empty team ID. - Usage logging suppressed for zero-token streams —
logUsagehad an early exit wheninputTokens == 0 && outputTokens == 0. Combined with the OpenAI streaming parser bug (below), this silently dropped all streaming usage rows. Removed the guard — requests are always recorded. - Live chat completion test used wrong field names —
TestLive_VeniceChatCompletionsentmessagesandconfig_idbut the handler expectscontentandprovider_config_id. Test silently skipped on the binding error.
[0.9.4] — 2026-02-24
Added
- API key encryption (vault) — Two-tier AES-256-GCM encryption for stored
API keys. Global/team keys encrypted with env-var-derived key (HKDF-SHA256);
personal BYOK keys encrypted with per-user encryption key (UEK) derived from
password via Argon2id. Admin cannot recover personal keys without user's
passphrase. New
server/crypto/package:vault.go,cache.go,resolver.go,backfill.gowith 11 round-trip tests. - UEK lifecycle — UEK generated on registration, unwrapped on login
(Argon2id → AES-GCM), cached in
sync.Mapfor session duration, evicted with memory zeroing on logout. Pre-migration users auto-initialize vault on first login. - Migration 003_vault.sql — Adds
encrypted_uek,uek_salt,uek_nonce,vault_setto users table. Addsapi_key_enc(BYTEA),key_nonce,key_scopeto provider_configs. Backfillskey_scopefrom existing scope. - Startup backfill —
BackfillEncryptedKeys()encrypts plaintext API keys on first startup withENCRYPTION_KEYset.EnforceEncryptionKey()refuses startup if encrypted keys exist but env var is missing. - CI/CD: encryption secret —
ENCRYPTION_KEYGitea secret synced to k8sswitchboard-encryptionsecret. Backend manifest references it as optionalsecretKeyRef.
Fixed
- HTML code blocks render live in chat (XSS) — When a model's
</think>tag directly abutted a code fence (no newline), the fence wasn't recognized by marked.js, causing raw HTML to render as live DOM elements (canvas games, styles, etc.). Three-layer fix: (1) DOMPurify switched from permissiveADD_TAGS(default allows canvas, style, form, etc.) to strictALLOWED_TAGSallowlist of only markdown-produced elements; (2) think-block placeholders padded with\n\nto ensure adjacent fences start on fresh lines; (3) unclosed code fences auto-closed beforemarked.parsefor streaming protection.
[0.9.3] — 2026-02-23
Changed
- Code blocks: button-driven collapse — Replaced
<details>wrapper with inline collapse/expand toggle button in the code toolbar. Auto-collapses at15 lines with a fade mask; toggle available on all blocks >5 lines. User can always expand/collapse regardless of threshold. Smooth CSS transition instead of native
<details>jump. - Thinking blocks: always visible — Thinking blocks are always rendered in
the DOM regardless of the
showThinkingsetting. The setting now controls whether blocks start expanded (<details open>) or collapsed. User can always click to toggle. Setting label updated to "Auto-expand thinking blocks".
Added
- Admin default model — New
default_modelpolicy in Admin → Settings. Dropdown populated from enabled models. When a user has no saved selection (fresh login, cleared browser) or their saved model is no longer available, the admin default is used before falling back to first visible. Resolution chain:localStorage → admin default → first visible. - Reasoning/thinking support for OpenAI-compatible providers — Models that
send
reasoning_contentin stream deltas (Grok, DeepSeek, etc.) now have thinking blocks streamed live and persisted as<think>tags. Renders as collapsible thinking blocks in both streaming and history views. - Favicon animation during generation — Browser tab favicon pulses with cascading dot opacity animation while a completion is in progress, restoring to the static favicon when done.
- Tool calls in message history — Tool call metadata (name, arguments,
result, error status) is now persisted alongside assistant messages in the
database. History view renders tool calls as collapsed
<details>blocks showing "🔧 tool_name → done" with expandable input/output JSON. - Notes tool: "View note" link — When a notes tool (
note_create,note_update, etc.) completes, a "📝 View note" button appears in both the live streaming tool indicator and the history tool call block. Clicking opens the Notes panel and navigates directly to the note. - Notes panel: Copy button — "Copy" button added to note read mode, copies title + content as markdown to clipboard.
- Brand hover: jitter-free crossfade — Sidebar brand logo→collapse icon
swap uses opacity crossfade instead of
display: nonetoggle, eliminating layout reflow jitter on hover.
Fixed
- Model selector shows unavailable model — Selector restoration searched all models including user-hidden ones. Saved selection (localStorage) for a hidden model like Claude Opus would re-select it on every page load even when only Grok was visible. Resolution now filters to visible models only.
- Refresh toast shows wrong count — "Loaded 33 models" counted all models including hidden; now shows only visible count ("Loaded 1 model").
- Tool calls vanish after completion — Live streaming tool indicators
disappeared when
reloadActivePath()rebuilt messages from DB. Fixed: thegetActivePathquery now includestool_callscolumn, andPathMessagecarries the data through to the frontend renderer. - Tool result "undefined results" text — Operator precedence bug in tool
result summary parser caused
undefined resultsto display for note_create. Fixed summary logic to properly branch on title vs count. - Regenerate streams at wrong position — Regen'd response appeared below the full conversation (appended at bottom) then snapped to correct position after completion. Fixed: display is now truncated to the parent message before streaming starts, so the new response streams in-place as a clean branch. Backend context was already correct (excludes old response).
- Regenerate loses tools, reasoning, and tool execution — Regen handler
was a stripped-down copy of the completion handler missing tool definitions,
tool execution loop, reasoning_content forwarding, and tool_calls persistence.
Model lost access to note tools on regen and fell back to generic capabilities.
Refactored: extracted
streamWithToolLoop()intostream_loop.goas the single canonical streaming implementation. BothstreamCompletion(normal chat) andRegeneratenow call the shared function — only persistence differs. Future streaming features (new event types, tool capabilities) automatically apply to all code paths. - OpenRouter free models crash — Free models with nil pricing caused
pq: invalid input syntax for type jsonon catalog sync. Nil pricing now routes throughToJSON()producing"{}"instead of nil[]byte. - API key appears unsaved —
ListGlobalConfigsreturned rawProviderConfigstructs whereAPIKeyEncisjson:"-"(never serialized), sohas_keywas alwaysundefined. Now returns computedhas_keyfield. - Case-insensitive usernames — Login, registration, and all user lookups
use
LOWER()in SQL. All user creation paths normalize to lowercase. New migration002_ci_username.sqladdsLOWER()unique indexes and normalizes existing rows.
[0.9.2] — 2026-02-23
Added
- Collapsible code blocks: Code blocks over 15 lines auto-collapse into
<details>with language and line count summary. Language label shown in top-left corner of all code blocks. - HTML preview: "Preview" button on HTML code blocks opens a sandboxed
iframe (
allow-scripts, noallow-same-origin). Auto-detected for untagged blocks via heuristic. - Token count estimate: Live token counter below input area showing
approximate tokens and context usage percentage when model has
max_context. - Context length warning: Dismissable banner above input at 75% (yellow) and 90% (red) context usage with guidance to start a new chat.
- Proxy interception detection:
_parseJSON()checks Content-Type before.json()on all API paths including streaming. TypedproxyBlockederrors with proxy page title extraction and actionable splash messages. - Environment injection:
window.__ENV__wired through entrypoint, k8s, and index.html for dev/test/production gating. - Enhanced diagnostics:
NET:PROXYlog type, Content-Type capture in fetch interceptor, environment info in export header and state snapshot. - Team admin audit scoping: New
GET /api/v1/teams/:teamId/auditand/audit/actionsendpoints scoped to team members. Activity Log section in team manage panel with filter dropdown and pagination. - New favicon: Switchboard panel design with provider-colored jacks. Animated SVG (rotating plugs), 32px PNG, 256px PNG, and ICO.
- Seed users (dev/test only):
SEED_USERS=user:pass:role,...env var pre-creates active users on startup. Ignored in production. K8s secretswitchboard-seed-userswired in backend manifest.
[0.9.1] — 2026-02-23
Removed
- Static known model table: Deleted
knownModelsmap from backend andKNOWN_MODELSfrom frontend. The same model ID can have different capabilities depending on the provider (e.g. DeepSeek has tool_calling on OpenRouter but not on Venice). A hardcoded table can't represent this. - Frontend
lookupKnownCaps(): Removed client-side capability guessing. Backend is the sole source of truth via catalog → heuristic chain.
Changed
- Resolution chain simplified: catalog (provider API sync) → heuristic inference. No intermediate known table. Providers that report capabilities via API are authoritative; heuristics are best-effort for unsynced models.
- All providers updated: OpenAI, OpenRouter, Anthropic, Venice now call
InferCapabilities()directly instead of the dead known table lookup. - Frontend
resolveCapabilities(): Now passes through backend caps as-is. No client-side merging with a static table. - EXTENSIONS.md recovered into repo, updated with Appendix A (Custom Renderers) and Appendix B (Model Roles with utility/embedding/generation slots)
- ROADMAP.md restructured: extension foundation pulled to v0.11.0, model roles to v0.10.0, dependency graph, TBD replaces post-1.0, removed v0.8→v0.9 migration (OBE — no public release, no test path)
Added
- Heuristic patterns: Updated to detect qwen3, gpt-5, grok, kimi, minimax, glm-5, gemma-3 model families. Vision expanded to claude-opus/sonnet (not just claude-3). Reasoning expanded for thinking, grok, glm patterns.
Fixed
- Preset capability pills: Presets with auto-resolve (no
provider_config_id) now inherit base model capabilities viaGetByModelIDAnycatalog fallback. - Venice
optimizedForCode: Added mapping toCodeOptimizedcapability. - CI test stability: BYOK journey tests use unreachable endpoints so auto-fetch doesn't race with simulated data injection.
[0.9.0] — 2026-02-22
Added
- Schema consolidation: 21 migrations collapsed to single
001_initial.sql - Store layer: All database access through typed interfaces (no raw SQL in handlers)
- Persona model: Trust-boundary model replacing old presets; scoped global/team/personal
- Capabilities resolver: Three-tier chain — catalog → known table → heuristic inference
- Three-state model visibility: enabled / disabled / team-only
- BYOK auto-fetch: Creating a personal provider triggers model discovery from provider API
- User model refresh:
POST /api-configs/:id/models/fetchendpoint + UI button - Composite model IDs:
configId:modelIdformat prevents cross-provider collisions - Audit log foundation: All admin operations logged with actor, action, resource
- Journey integration tests: API-driven test suite replacing fake-data tests
- Frontend test suite: 107 tests, 27 suites validating model processing pipeline
- Live Venice API test: Proves real BYOK → auto-fetch → models visible flow
Fixed
- API key storage:
json:"-"tag onProviderConfig.APIKeyEncsilently dropped keys during admin create/update. Fixed with wrapper structs that bypass the tag. - NULL model_default scan:
scanProviders()crashed on NULLmodel_defaultcolumn, silently hiding all team and BYOK models. Fixed withsql.NullString. - Nil slice serialization: Go nil slices serialized as JSON
nullinstead of[], breaking frontend fallback chains. Fixed withmake([]T, 0). - Frontend error swallowing: API responses with
errorsfield were silently ignored.
Changed
- Backward-compatible API routes with v0.8 field name aliases
- User model preferences table (
user_model_settings)