From bbbbe65bfaf14a909d9e258072e59c9fb5219849 Mon Sep 17 00:00:00 2001 From: xcaliber Date: Sun, 1 Mar 2026 18:20:01 +0000 Subject: [PATCH] Changeset 0.21.3 (#89) --- CHANGELOG.md | 17 ++ VERSION | 2 +- docs/EXTENSIONS.md | 60 ++++- docs/ROADMAP.md | 27 ++- src/css/styles.css | 80 +++++++ src/index.html | 26 +- src/js/app.js | 5 + src/js/extensions.js | 33 +++ src/js/repl.js | 552 +++++++++++++++++++++++++++++++++++++++++++ src/js/surfaces.js | 319 +++++++++++++++++++++++++ 10 files changed, 1098 insertions(+), 23 deletions(-) create mode 100644 src/js/repl.js create mode 100644 src/js/surfaces.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9682786..81658ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to Chat Switchboard. +## [0.21.3] — 2026-03-01 + +### Added +- **Surface Registry (`surfaces.js`).** Mode-switching system that allows extensions to register "surfaces" (UI modes) that take over named regions of the interface. DOM nodes are detached and preserved in `DocumentFragment`s during mode switches — not destroyed — ensuring CM6 editor instances and other stateful components survive transitions. +- **Surface region attributes.** Four `data-surface-region` attributes on `index.html` containers: `surface-header` (model bar), `surface-main` (chat messages), `surface-footer` (input area), `sidebar-content` (search + chat history). Extensions swap these regions via `ctx.ui.replace()` / `ctx.ui.restore()`. +- **Mode selector.** Appears in the sidebar (`#modeSelectorWrap`) when ≥1 extension surface is registered beyond the default chat. Shows icon buttons for each mode with active state highlighting. +- **Extension context API.** `ctx.surfaces.register()`, `ctx.surfaces.unregister()`, `ctx.surfaces.activate()`, `ctx.surfaces.getCurrent()` on the scoped extension context. `ctx.ui.replace()` and `ctx.ui.restore()` for region DOM management. +- **Surface events.** `surface.activated`, `surface.deactivated`, `surface.registered`, `surface.unregistered` on the EventBus (all `localOnly`). +- **REPL console (`repl.js`).** Fourth tab in the debug modal (Ctrl+Shift+L). `AsyncFunction` wrapper for top-level `await`. Injected globals: `API`, `Events`, `Extensions`, `Surfaces`, `DebugLog`, `Panels`, `UI`, plus `$()`, `$$()`, `sleep()` helpers. Features: collapsible JSON pretty-printing, red errors with expandable stack traces, command history via ↑/↓ (persisted to sessionStorage), tab-completion on live object graphs and EventBus labels, multi-line input (Shift+Enter), admin-gated (admin role OR `?debug=1` URL param). +- **CSS.** Mode selector styles (`.mode-selector`, `.mode-btn`, active/hover states). REPL styles (output formatting, value-type color coding, collapsible JSON, input prompt). + +### Changed +- `extensions.js` context builder: added `ctx.surfaces` namespace and `ctx.ui.replace()` / `ctx.ui.restore()` methods. +- `app.js` startup: initializes `Surfaces.init()` before `Extensions.loadAll()`, and `REPL.init()` after auth confirmation. +- `index.html`: added `data-surface-region` attributes, mode selector container, REPL tab in debug modal, script tags for `surfaces.js` and `repl.js`. +- `EXTENSIONS.md` §6: updated with implementation details, actual API signatures, and event catalog. + ## [0.21.2] — 2026-03-01 ### Added diff --git a/VERSION b/VERSION index 2d62101..804b3de 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.21.2 \ No newline at end of file +0.21.3 \ No newline at end of file diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md index 6fb2ced..4ec5466 100644 --- a/docs/EXTENSIONS.md +++ b/docs/EXTENSIONS.md @@ -300,28 +300,36 @@ tools. The editor extension defines `read_file`, `write_file`, ## 6. Surfaces (Modes) +*Implemented in v0.21.3.* + A "mode" is an extension that registers a **surface** — a UI region that -replaces or augments the default chat area. +replaces or augments the default chat area. The surface system preserves +DOM state across mode switches (critical for CM6 editor instances). ### 6.1 Surface Regions +The following `data-surface-region` attributes are present in `index.html`: + ``` ┌─────────────────────────────────────────────┐ │ sidebar-top │ surface-header │ +│ │ (model-bar) │ +│ mode-selector │ │ +│ (auto-shown │ surface-main │ +│ when ≥2 │ (chatMessages) │ +│ surfaces) │ │ │ │ │ -│ sidebar-nav │ │ -│ (mode selector) │ surface-main │ -│ │ (chat, editor, article, │ -│ sidebar-content │ cluster, ...) │ -│ (context panel) │ │ +│ sidebar-content │ surface-footer │ +│ (search+chats) │ (input-area) │ │ │ │ -│ sidebar-bottom │ surface-footer │ -│ │ (input area) │ +│ sidebar-bottom │ │ └─────────────────────────────────────────────┘ ``` ### 6.2 Surface Registration +**Manifest-based** (planned — extension loader integration): + ```json { "surfaces": [ @@ -336,11 +344,16 @@ replaces or augments the default chat area. } ``` +**Imperative** (implemented): + ```js Extensions.register({ id: 'editor-mode', init(ctx) { ctx.surfaces.register('editor', { + label: 'Editor', + icon: 'code', + regions: ['surface-main', 'surface-footer', 'sidebar-content'], activate() { ctx.ui.replace('surface-main', this.renderEditor()); ctx.ui.replace('surface-footer', this.renderEditorInput()); @@ -356,12 +369,35 @@ Extensions.register({ }); ``` +**Region management API** (on `ctx.ui`): + +- `ctx.ui.replace(regionId, element)` — saves current children to a + `DocumentFragment` (detached, not destroyed), inserts new element. +- `ctx.ui.restore(regionId)` — re-attaches saved children. + +This is critical for CM6 — editor instances survive mode switches because +their DOM nodes are detached (preserving internal state) rather than removed. + +**Surface management API** (on `ctx.surfaces`): + +- `ctx.surfaces.register(id, opts)` — register a new surface +- `ctx.surfaces.unregister(id)` — unregister (switches to chat if active) +- `ctx.surfaces.activate(id)` — switch to a surface +- `ctx.surfaces.getCurrent()` — returns active surface id + ### 6.3 Mode Selector -When extensions register surfaces, a mode selector appears in the sidebar. -Clicking a mode calls `activate()` on that surface and `deactivate()` on -the current one. The bus event `surface.activated` fires so other -extensions can react. +When extensions register surfaces, a mode selector appears in the sidebar +(`#modeSelectorWrap`) below the brand/new-chat area. Clicking a mode calls +`activate()` on that surface and `deactivate()` on the current one. + +Events fired: +```js +Events.on('surface.activated', ({ surface, previous }) => { ... }); +Events.on('surface.deactivated', ({ surface }) => { ... }); +Events.on('surface.registered', ({ surface }) => { ... }); +Events.on('surface.unregistered', ({ surface }) => { ... }); +``` ### 6.4 Core Surface diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 73c7cdc..9ddc392 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -82,7 +82,7 @@ v0.21.0 Workspace Storage Primitive ✅ │ ┌───────┴──────────────┐ │ │ -v0.21.1 Workspace ✅ v0.21.3 Surface Infra +v0.21.1 Workspace ✅ v0.21.3 Surface Infra ✅ Tools + Bindings + REPL (parallel) │ │ v0.21.2 Workspace ✅ v0.21.5 Editor Surface @@ -724,15 +724,28 @@ Embed text files for semantic search. Reuses `knowledge.SplitText` + `knowledge. - [x] Graceful degradation when no embedding role configured - [x] Mock store updated for FS unit tests -### v0.21.3 — Surface Infrastructure + REPL +### v0.21.3 — Surface Infrastructure + REPL ✅ Pure UI architecture. No workspace dependency (parallel development). -- [ ] Surface registration: `ctx.surfaces.register()` with label, icon, regions, activate/deactivate -- [ ] Region management: `ctx.ui.replace()` / `ctx.ui.restore()` for CM6 state preservation -- [ ] Mode selector in sidebar when ≥1 extension surface registered -- [ ] `surface.activated` / `surface.deactivated` events -- [ ] REPL console ([#70](https://git.gobha.me/xcaliber/chat-switchboard/issues/70)): fourth debug modal tab, AsyncFunction wrapper, injected globals, command history, tab-completion, admin-gated +- [x] `data-surface-region` attributes on index.html containers (surface-header, surface-main, surface-footer, sidebar-content) +- [x] `surfaces.js` — SurfaceRegistry: register, activate, deactivate, getCurrent, list +- [x] `ctx.ui.replace()` / `ctx.ui.restore()` with DOM preservation (DocumentFragment-based) +- [x] `ctx.surfaces.register()` API for extensions +- [x] Mode selector component in sidebar (`#modeSelectorWrap`, auto-shown when ≥2 surfaces) +- [x] Chat as default surface (implicit, always registered) +- [x] `surface.activated` / `surface.deactivated` / `surface.registered` / `surface.unregistered` EventBus events +- [x] REPL tab: AsyncFunction wrapper, global injection (API, Events, Extensions, Surfaces, DebugLog, Panels, UI, $, $$, sleep) +- [x] REPL tab: pretty-print results (collapsible JSON, type-colored primitives, DOM element summaries) +- [x] REPL tab: command history (sessionStorage, ↑/↓ navigation) +- [x] REPL tab: tab-completion on object graphs, event labels, globals +- [x] REPL tab: event label hints (Events.on/emit completion) +- [x] REPL tab: admin gate (admin role OR ?debug=1 URL param) +- [x] REPL tab: toolbar (clear, copy, help) +- [x] CSS: mode selector + REPL styles +- [x] Documentation in EXTENSIONS.md §6 updated with implementation details +- [ ] Mobile: mode selector collapses to hamburger/bottom nav (deferred — functions via sidebar on mobile) +- [ ] Integration with extension loader (surfaces from manifest — deferred to extension loader update) ### v0.21.4 — Git Integration diff --git a/src/css/styles.css b/src/css/styles.css index cbe7d3e..dd560be 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -1748,6 +1748,86 @@ select option { background: var(--bg-surface); color: var(--text); } .debug-state-pre { max-height: none; margin: 0; } .debug-footer { display: flex; align-items: center; } +/* ── Mode Selector (v0.21.3) ───────────── */ + +.mode-selector { + display: flex; gap: 2px; padding: 6px 8px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +.mode-btn { + display: flex; align-items: center; justify-content: center; + width: 32px; height: 32px; border: none; + border-radius: var(--radius); background: none; + color: var(--text-3); cursor: pointer; + transition: background var(--transition), color var(--transition); +} +.mode-btn:hover { background: var(--bg-hover); color: var(--text); } +.mode-btn.active { background: var(--bg-active); color: var(--accent); } +.sidebar.collapsed .mode-selector { justify-content: center; } + +/* ── REPL Console (v0.21.3) ────────────── */ + +.repl-toolbar { + display: flex; gap: 0.5rem; padding: 6px 12px; + border-bottom: 1px solid var(--border); font-size: 11px; +} +.repl-output { + flex: 1; overflow-y: auto; padding: 6px; + font-family: var(--mono); font-size: 12px; line-height: 1.6; +} +.repl-input-wrap { + display: flex; align-items: flex-start; gap: 6px; + padding: 8px 12px; border-top: 1px solid var(--border); + background: var(--bg-raised); +} +.repl-prompt { + font-family: var(--mono); font-size: 12px; line-height: 1.6; + color: var(--accent); font-weight: bold; + flex-shrink: 0; padding-top: 2px; +} +.repl-input { + flex: 1; background: none; border: none; outline: none; + font-family: var(--mono); font-size: 12px; line-height: 1.6; + color: var(--text); resize: none; overflow: hidden; + min-height: 20px; +} +.repl-entry { padding: 2px 6px; } +.repl-entry-input pre { color: var(--text-2); margin: 0; white-space: pre-wrap; word-break: break-all; } +.repl-entry-result { color: var(--text); } +.repl-entry-error { color: var(--danger, #e55); } +.repl-entry-info { color: var(--text-3); font-size: 11px; } + +.repl-error-msg { margin: 0; white-space: pre-wrap; font-weight: 500; } +.repl-error-stack { margin: 2px 0 0 0; color: var(--text-3); font-size: 11px; white-space: pre-wrap; } +.repl-stack-toggle { + background: none; border: none; color: var(--text-3); + cursor: pointer; font-size: 10px; font-family: var(--mono); + padding: 0; margin-top: 2px; +} +.repl-stack-toggle:hover { color: var(--text-2); } + +.repl-elapsed { color: var(--text-3); font-size: 10px; margin-left: 8px; } + +/* REPL value types */ +.repl-null, .repl-undefined { color: var(--text-3); margin: 0; } +.repl-string { color: #98c379; margin: 0; white-space: pre-wrap; word-break: break-all; } +.repl-number { color: #d19a66; margin: 0; } +.repl-boolean { color: #56b6c2; margin: 0; } +.repl-function { color: #c678dd; margin: 0; font-style: italic; } +.repl-symbol { color: #e5c07b; margin: 0; } +.repl-dom { color: #61afef; margin: 0; } +.repl-object { color: var(--text); margin: 0; } +.repl-json { margin: 0; white-space: pre-wrap; word-break: break-all; color: var(--text); font-size: 11px; } + +.repl-collapsible { display: inline; } +.repl-json-toggle { + background: none; border: none; color: var(--text-2); + cursor: pointer; font-family: var(--mono); font-size: 12px; + padding: 0; +} +.repl-json-toggle:hover { color: var(--accent); } + /* ── Sidebar Notes Button ───────────────── */ .sidebar-notes-btn { diff --git a/src/index.html b/src/index.html index 81909d1..abfba80 100644 --- a/src/index.html +++ b/src/index.html @@ -72,12 +72,17 @@ + + + +
+