# Changelog All notable changes to Switchboard Core are documented here. ## v0.5.2 β€” Chat Surface ### Added - **`chat` surface package**: Full messaging UI built on `chat-core` library. Route `/s/chat`, icon `πŸ’¬`, depends on `chat-core`. - **Conversation list sidebar**: Conversations sorted by recent activity, last message preview, relative timestamps, unread count badges. "New" button for conversation creation. - **Message thread**: Paginated message history with cursor-based load-more on scroll-to-top. Participant avatars, display names, timestamps. Scroll-to- bottom on new messages. - **Compose bar**: Auto-resizing textarea, Enter-to-send, Shift+Enter for newline. Typing indicator broadcast on keypress (3s debounce). - **Participant sidebar**: Collapsible right panel with participant list, online status via presence hub, role badges. Add participant via `sw.ui.Dialog` + `sw.ui.UserPicker`. Remove participant with confirmation. - **Create conversation dialog**: Group or Direct Message type selector. Multi- user picker with chip display for group, single picker for DM. Auto-title from participant names. - **Typing indicators**: Realtime broadcast via `POST /typing/:id` Starlark endpoint. Frontend subscribes to `conversation:{id}` channel, shows "X is typing..." with 4s auto-expire. Handles multiple typers. - **Read receipts**: Auto mark-read on conversation focus and new incoming messages. Unread counts in conversation list, cleared on selection. - **Message editing**: Inline edit mode on own messages with textarea. Save on Enter, cancel on Escape. "(edited)" label on edited messages. Realtime propagation to other participants. - **Message deletion**: Confirm dialog before delete. "This message was deleted" placeholder. Admin can delete any message. Realtime propagation. ### Fixed - **Stale token login deadlock**: REST client's 401β†’refreshβ†’retry loop caused a deadlock when the refresh endpoint itself returned 401 (stale DB). Auth endpoints (`/auth/login`, `/auth/refresh`, `/auth/register`) are now excluded from the retry loop. Defensive 8s timeout on `auth.boot()` ensures login page always renders even on unexpected hangs. - **Bundled package permissions**: Bundled packages were installed with `pending_review` status and `granted=0` permissions, requiring manual admin approval. Bundled installer now auto-grants all declared permissions and sets status to `active` on install. - **Creator display name**: `chat-core.create()` now accepts `creator_display_name` so the conversation creator shows a readable name in the participant sidebar instead of a raw UUID. - **Chat dark mode theming**: Replaced non-existent CSS variable names with actual theme tokens from `variables.css`. Fixes invisible text and wrong backgrounds in dark mode. - **Dialog dropdown clipping**: UserPicker autocomplete dropdown in dialogs (New Conversation, Add Participant) no longer clipped by `overflow: auto` on `.sw-dialog__body`. - **User menu at scaled UI**: Menu primitive now divides anchor coordinates and viewport dimensions by `sw.shell.getScale()` to compensate for CSS `transform: scale()` on `#surfaceInner`. Fixes menu clipping at 110%+. ### Changed - **Named Docker volume**: `docker-compose.yml` switched from bind mount (`./data:/data`) to named volume (`sb_data:/data`). Fixes volume mount failures when Docker daemon runs on a remote host (DinD, k8s pod). Deterministic wipe via `docker compose down -v`. ## v0.5.1 β€” Chat Core Library ### Added - **`chat-core` library package**: Conversations, messages, participants, and read cursors as ext_data tables. Permissions: `db.write`, `realtime.publish`. Consumable by other packages via `lib.require("chat-core")`. - **4 ext_data tables**: `conversations` (title, type, created_by, updated_at), `participants` (conversation_id, participant_id, type, display_name, role, joined_at), `messages` (conversation_id, participant_id, content, content_type, edited_at), `read_cursors` (conversation_id, participant_id, last_read_message_id). - **6 exported Starlark functions**: `create()`, `send()`, `history()`, `add_participant()`, `remove_participant()`, `mark_read()`. - **15 REST endpoints**: Full CRUD for conversations, messages, participants. Cursor-based paginated message history. Per-conversation unread counts. - **Realtime events**: `message`, `message.edited`, `message.deleted`, `participant.added`, `participant.removed` published to `conversation:{id}` channels. - **`db.query()` range parameters**: New `before` and `after` kwargs accept dicts of `{col: val}` generating `col < ?` / `col > ?` WHERE clauses. Enables cursor-based pagination and efficient unread counting for all extension packages. 7 new Go tests. ### Security - **Invisible Unicode scanning gate** (GlassWorm / Trojan Source defense): `ScanSource()` detects variation selectors, bidi overrides, zero-width chars, tag characters, and other invisible Unicode across 10 ranges. `Verdict()` blocks GlassWorm payloads (>=10 variation selector / tag chars), Trojan Source (any bidi override), and excessive zero-width chars (>=6). Two enforcement points: extension install (422 rejection before files written) and Starlark execution (belt-and-suspenders). 23 new tests including GlassWorm payload simulation. ### Changed - **Admin packages type filter**: Added `Library` option alongside Surface/Extension/Full/Workflow. Filter dropdown replaced button group with themed `Dropdown` primitive (keyboard nav, CSS-var theming, consistent with rest of admin UI). - SDK version bumped from 0.5.0 to 0.5.1. ## v0.5.0 β€” Realtime Primitive + Dialog Audit + Permissions UI ### Added - **Realtime Starlark module**: `realtime.publish(channel, event, data)` lets extensions publish events to WebSocket channels. Gated by the new `realtime.publish` permission. Payloads limited to 7KB (pg_notify safe). Reserved event prefixes (`system.`, `workflow.`, etc.) are blocked. - **WS room protocol**: Clients send `room.subscribe` / `room.unsubscribe` messages to join/leave rooms. Intercepted in the WebSocket readPump before the bus publish gate (like ping). Per-connection cap of 100 rooms. - **SDK realtime module**: `sw.realtime.subscribe(channel, [event], callback)` manages room join/leave over WebSocket and filters incoming `realtime.*` events by room. Returns unsubscribe handle. Auto re-joins all rooms on WebSocket reconnect. `sw.realtime.channels()` debug helper. - **Admin permissions UI**: Packages page gains a "Permissions" button for packages with declared permissions. Inline drawer shows each permission with Grant/Revoke toggle, granted-by user ID, and a "Grant All" bulk action. - **Status badges**: `pending_review` (amber) and `suspended` (red) badges on package rows. Enable toggle disabled with hint when `pending_review`. - **SDK API client**: `permissions()`, `grantPerm()`, `revokePerm()`, `grantAllPerms()` methods added to `sw.api.admin.packages`. - **`starlarkToGoVal()` helper**: Converts Starlark values to Go `any` for JSON marshaling (reverse of existing `goValToStarlark`). - **8 new Go tests**: Route table cases for `realtime.*` and `room.*`, realtime module publish (happy path, empty data, missing args, reserved prefix, payload too large), `starlarkToGoVal` type conversion. ### Changed - **Dialog audit**: 5 bare `confirm()` calls migrated to `await sw.confirm()` with `{ destructive: true }` styling in tasks, schedules, notes (Γ—2), and editor packages. Package archives rebuilt. - SDK version bumped from 0.2.3 to 0.5.0. - Event route table: `realtime.` β†’ DirToClient, `room.subscribe` / `room.unsubscribe` β†’ DirFromClient. - Runner gains `bus` field and `SetBus()` method; wired in main.go. ## v0.4.7 β€” Note Graph ### Added - **Graph API**: `GET /graph` endpoint returns all non-archived notes as nodes and resolved wikilinks as edges in a single payload. Nodes include id, title, folder_id, and tags. Edges include source, target, and link text. - **Graph view**: Canvas-based force-directed graph visualization replaces the editor pane when "Graph" button is clicked in the topbar. Force simulation uses repulsion (all pairs), attraction (edges), and center gravity with velocity damping. Supports zoom (scroll wheel) and pan (mouse drag). - **Click focus**: Single-click a node to dim unconnected nodes/edges to 15% opacity; clicked node and direct neighbors stay at full brightness. Edges between focused nodes highlighted. Click empty space to reset. - **Shift+click chain**: Hold shift and click a second node to union both neighborhoods β€” trace thought paths across two hops. - **Double-click open**: Double-click a graph node to navigate to that note in the editor. Graph view closes, editor opens with the selected note. - **Hover tooltips**: Hovering a node shows title, tag pills, and connection count in an HTML overlay (no additional API calls). - **Folder coloring**: Nodes colored by folder using a 10-color palette. Unfiled notes shown in gray. - **Tag filter integration**: Active tag filter dims non-matching nodes to 30% opacity, stacking with click focus. - **Orphan highlighting**: Notes with zero connections shown with dashed stroke. "Hide orphans" checkbox in toolbar to toggle visibility. ### Changed - Notes package version bumped from 0.7.0 to 0.8.0. - Topbar gains a "Graph" toggle button alongside "New Note" and "Import .md". ## v0.4.6 β€” Sidebar Restructure ### Added - **Sidebar tabs**: Two-tab header ("Notes" / "Outline") replaces the static sidebar header. Notes tab contains folder tree, tag filter, search, and note list. Outline tab shows the document heading tree for the active note. Outline tab disabled (grayed) when no note is selected. - **Sidebar outline**: Document outline relocated from editor-side panel into the sidebar's Outline tab. Nested heading hierarchy (H1β†’H2β†’H3) with depth indentation via `padding-left`. Click heading scrolls to it in both preview and CM6 editor modes. - **Active heading tracking**: Scroll listener (RAF-throttled) highlights the current heading in the sidebar outline based on scroll position. Uses `getBoundingClientRect()` in preview/split mode and CM6 viewport line in edit mode. ### Changed - Notes package version bumped from 0.6.0 to 0.7.0. - `EditorPane` exposes headings, scroll function, and active heading index to parent via callback props (`onHeadingsChange`, `scrollToHeadingRef`, `onActiveHeadingChange`). - Sidebar auto-resets to Notes tab when active note is deleted. ### Removed - Old `DocumentOutline` component and `.doc-outline` CSS styles (replaced by sidebar outline). ## v0.4.5 β€” Editor Modes + Document Outline ### Added - **Tri-state view mode**: Notes open in rendered (read-only) mode by default. Toolbar button cycles through Edit (CM6) and Split (side-by-side) modes. Replaces the old binary preview toggle. - **Split view**: Side-by-side CM6 editor + rendered preview using CSS grid. CM6 instance preserved when toggling between edit and split modes. - **Synced scroll**: In split mode, scrolling the CM6 editor proportionally scrolls the preview pane via linear ratio mapping on `scrollDOM`. - **View mode persistence**: User's preferred mode saved to localStorage (`sw.storage.local('notes')`). New `editor_mode` manifest setting with admin-level default (rendered / edit / split). - **Document outline**: Collapsible TOC panel adjacent to the editor body. `parseHeadings()` extracts heading level, text, and line number (skips fenced code blocks). Click heading to scroll β€” uses CM6 `EditorView.scrollIntoView()` in edit/split mode, DOM `scrollIntoView()` in rendered mode. Updates on body change with 300ms debounce. ### Changed - Notes package version bumped from 0.5.0 to 0.6.0. - Editor body wrapped in `.notes-editor__content` flex container to accommodate the outline panel. - Wikilink click handling active in both rendered and split modes. - Mobile: split view collapses to single column, outline panel hidden. --- ## v0.4.4 β€” Rich Editor + Import/Export ### Added - **CodeMirror 6 integration**: Notes editor now uses the vendored CM6 bundle (`CM.noteEditor()`) for rich markdown editing with syntax highlighting, wikilink autocomplete (`[[` triggers note title completion), and inline preview decorations for headings, code blocks, and blockquotes. - **CM6 dynamic loading**: Bundle loaded via `