Changeset 0.22.7 (#149)
This commit is contained in:
746
docs/DESIGN-SURFACES.md
Normal file
746
docs/DESIGN-SURFACES.md
Normal file
@@ -0,0 +1,746 @@
|
||||
# DESIGN — Surface & Extension Architecture
|
||||
|
||||
**Status:** Accepted
|
||||
**Scope:** Primitives, Components, Surfaces, Extension hooks, Themes
|
||||
**Depends on:** v0.22.5 (Go template engine), v0.22.7 (ChatPane)
|
||||
**Informs:** v0.22.8+, EXTENSIONS.md rewrite, ARCHITECTURE.md update, ICD-API, ICD-SURFACE
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The UI is a four-layer architecture: Primitives, Components, Surfaces,
|
||||
and Banners. Each layer has a single responsibility and a strict
|
||||
dependency direction — primitives know nothing, components compose
|
||||
primitives, surfaces compose components, banners are platform chrome
|
||||
outside the surface boundary.
|
||||
|
||||
Extensions participate at every layer through six defined hooks.
|
||||
Admin-installed extensions are privileged (full DOM, direct API access,
|
||||
own routes). Themes are pure CSS custom property overrides that
|
||||
propagate through every layer uniformly.
|
||||
|
||||
This document defines the layer contracts, extension hook system,
|
||||
display content model, trust boundaries, and theme architecture.
|
||||
|
||||
---
|
||||
|
||||
## Definitions
|
||||
|
||||
A **surface** is a full-page layout — a composition of components with
|
||||
a route, a data loader, and a boot script. Chat is a surface. The
|
||||
editor is a surface. A custom triage intake form is a surface.
|
||||
|
||||
An **extension** is a package — a manifest plus code plus assets that
|
||||
participates in the platform through hooks. An extension can do one,
|
||||
some, or all of:
|
||||
|
||||
- Register tools the LLM can call (Hook 5: Tool Bridge)
|
||||
- Transform content during streaming (Hook 2: Stream Processing)
|
||||
- Enhance rendered messages (Hook 3: Post-Render)
|
||||
- Attach visual content to messages (Hook 4: Display Content)
|
||||
- Inject panels or sections into existing surfaces (Hook 6: Surface Injection)
|
||||
- Create entirely new surfaces with their own routes (Surface Registration)
|
||||
|
||||
The five **core surfaces** (Chat, Editor, Notes, Admin, Settings) exist
|
||||
without extensions. They are built from the same primitive and component
|
||||
layers that extensions use, but they are registered directly in Go
|
||||
rather than through a manifest.
|
||||
|
||||
An extension that creates a surface gets its own route (`/s/:slug`),
|
||||
its own data loader, and the full primitive/component library. An
|
||||
extension that only registers a post-render hook (like a block
|
||||
renderer) doesn't create any surfaces at all.
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Extension | Creates Surface? | Hooks Used |
|
||||
|-----------|-----------------|------------|
|
||||
| Mermaid renderer | No | Post-Render |
|
||||
| KaTeX renderer | No | Post-Render |
|
||||
| Calculator tool | No | Tool Bridge |
|
||||
| Image Generator | Yes (`/s/gallery`) | Tool Bridge, Stream Processing, Post-Render, Display Content, Surface |
|
||||
| Custom Dashboard | Yes (`/s/dashboard`) | Surface, Surface Injection (admin section) |
|
||||
|
||||
---
|
||||
|
||||
## Existing Extension Mapping
|
||||
|
||||
The current extension mechanisms map directly into this architecture:
|
||||
|
||||
| Current Mechanism | Architecture Equivalent |
|
||||
|-------------------|------------------------|
|
||||
| Block renderers (`ctx.renderers.register()`) | Hook 3: Post-Render |
|
||||
| Tool bridge (`ctx.tools.register()`) | Hook 5: Tool Bridge |
|
||||
| `ctx.ui.toast()`, `ctx.ui.openPreview()` | Primitive layer (unchanged API) |
|
||||
| `ctx.ui.isDark()`, `ctx.ui.isMobile()` | Theme layer queries |
|
||||
| `ctx.surfaces.getCurrent()` | Returns `window.__SURFACE__` |
|
||||
|
||||
The manifest schema gains optional new fields (`hooks`, `surfaces`,
|
||||
`surface_injections`, `theme`). Existing fields retain their meaning.
|
||||
|
||||
`Extensions.boot()` is the single entry point for extension
|
||||
initialization on every surface. It loads manifests, registers
|
||||
renderers, and initializes tool bridges — same sequence, available
|
||||
on every surface rather than only chat.
|
||||
|
||||
---
|
||||
|
||||
## Layer Model
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Banner (top) │ ← Platform config
|
||||
├──────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Surface │ ← Layout + lifecycle
|
||||
│ ┌─────────────┬──────────────────────────┐ │
|
||||
│ │ Component │ Component │ │ ← Domain-aware
|
||||
│ │ (ChatPane) │ (NoteEditor) │ │
|
||||
│ │ ┌─────────┐ │ ┌────────┐ ┌──────────┐ │ │
|
||||
│ │ │Primitive│ │ │Primitv.│ │Primitive │ │ │ ← Atomic UI
|
||||
│ │ │ (input) │ │ │ (menu) │ │ (toggle) │ │ │
|
||||
│ │ └─────────┘ │ └────────┘ └──────────┘ │ │
|
||||
│ └─────────────┴──────────────────────────┘ │
|
||||
│ │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ Banner (bottom) │ ← Platform config
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Four layers, strict dependency direction: Primitives know nothing.
|
||||
Components use Primitives. Surfaces compose Components. Banners are
|
||||
global chrome outside the surface boundary.
|
||||
|
||||
---
|
||||
|
||||
## Layer 1: Primitives
|
||||
|
||||
Atomic UI elements. No domain logic. A toggle doesn't know if it's
|
||||
toggling a KB or a theme — it takes a label, a state, and a callback.
|
||||
|
||||
Styled entirely via CSS custom properties (the theme contract). Every
|
||||
primitive reads from the same property namespace, so theme changes
|
||||
propagate instantly.
|
||||
|
||||
### Catalog
|
||||
|
||||
| Primitive | Description |
|
||||
|-----------|-------------|
|
||||
| `Input` | Text, textarea, number, password, with label + validation |
|
||||
| `Select` | Dropdown, single or multi |
|
||||
| `Toggle` | Boolean switch with label |
|
||||
| `Checkbox` | Checkbox with label |
|
||||
| `Button` | Text button, variants: primary, secondary, danger, ghost |
|
||||
| `IconButton` | Icon-only button with tooltip |
|
||||
| `ColorPicker` | Color input with hex text field |
|
||||
| `Menu` | Dropdown or context menu, item list with icons + shortcuts |
|
||||
| `Dialog` | Modal: confirm, prompt, or custom form content |
|
||||
| `Toast` | Transient notification: success, error, warning, info |
|
||||
| `Badge` | Inline label: accent, success, danger, warning, muted |
|
||||
| `Avatar` | Image circle with upload affordance |
|
||||
| `Tabs` | Tab bar with content switching |
|
||||
| `FormGroup` | Label + input + validation message + help text |
|
||||
| `SectionHeader` | Titled section divider |
|
||||
| `EmptyState` | Placeholder with icon + message + action |
|
||||
| `Spinner` | Loading indicator |
|
||||
| `Table` | Sortable, paginated data table |
|
||||
|
||||
### Implementation
|
||||
|
||||
Each primitive is a factory function that returns a DOM element (or
|
||||
attaches to an existing one). No classes, no inheritance — just
|
||||
functions.
|
||||
|
||||
```js
|
||||
// Example — not prescriptive API, just the pattern:
|
||||
Primitives.toggle({ label: 'Auto-search', value: true, onChange: fn })
|
||||
Primitives.menu({ anchor: el, items: [...], onSelect: fn })
|
||||
Primitives.dialog({ title: 'Confirm', body: el, onConfirm: fn })
|
||||
```
|
||||
|
||||
Current locations: `ui-primitives.js`, `ui-primitives-additions.js`,
|
||||
and Go template components (`model-select.html`, `team-select.html`,
|
||||
`file-upload.html`). These converge into one coherent set.
|
||||
|
||||
### Go Template Primitives
|
||||
|
||||
Some primitives need server-rendered initial state (e.g., model-select
|
||||
needs the model list, team-select needs the team list). These are Go
|
||||
template partials that render the HTML + initial data, then JS hydrates
|
||||
interactivity on load.
|
||||
|
||||
```html
|
||||
{{template "model-select" dict "ID" "channelModel" "Models" .Models "Type" "chat"}}
|
||||
```
|
||||
|
||||
The JS primitive attaches to the server-rendered DOM by ID, not by
|
||||
replacing it.
|
||||
|
||||
---
|
||||
|
||||
## Layer 2: Components
|
||||
|
||||
Domain-aware compositions of primitives. Each component owns its DOM
|
||||
subtree, manages its own state, and exposes a clean API for the surface
|
||||
to interact with.
|
||||
|
||||
### Catalog
|
||||
|
||||
| Component | Primitives Used | Domain |
|
||||
|-----------|----------------|--------|
|
||||
| `ChatPane` | Input, Button, Menu, Spinner, Toast | Channels, Messages, Streaming |
|
||||
| `NoteEditor` | Input (CM6), Menu, Tabs, Badge | Notes, Wikilinks, Folders |
|
||||
| `FileTree` | Menu (context), Button, Spinner | Workspaces, Files |
|
||||
| `ModelSelector` | Select, Badge, Spinner | Models, Capabilities, Health |
|
||||
| `PersonaPicker` | Select, Badge, Avatar | Personas, Scopes |
|
||||
| `KBPicker` | Toggle, Badge, Select | Knowledge Bases, Discoverability |
|
||||
| `ProjectSidebar` | Menu, Tabs, Badge, EmptyState | Projects, Channels, DnD |
|
||||
| `AuditLog` | Table, Select, Badge | Audit, Filtering, Pagination |
|
||||
| `CodeEditor` | Input (CM6), Select, Tabs | Workspaces, Languages |
|
||||
| `SettingsForm` | FormGroup, Toggle, Select, Button | User/Admin Settings |
|
||||
|
||||
### Instance Pattern
|
||||
|
||||
Components use the factory pattern established by ChatPane:
|
||||
|
||||
```js
|
||||
const pane = ChatPane.create({
|
||||
messagesEl: document.getElementById('editorChatMessages'),
|
||||
inputEl: document.getElementById('editorChatInput'),
|
||||
sendBtnEl: document.getElementById('editorSendBtn'),
|
||||
channelId: workspaceChatId,
|
||||
standalone: true,
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
pane.renderMessages(msgs);
|
||||
pane.streamResponse(resp, msgs);
|
||||
pane.destroy();
|
||||
```
|
||||
|
||||
Components can be instantiated multiple times on the same page (editor
|
||||
has a ChatPane, notes has a ChatPane — independent instances).
|
||||
|
||||
### Server-Rendered Shell + JS Hydration
|
||||
|
||||
Components have a Go template partial for the server-rendered scaffold:
|
||||
|
||||
```html
|
||||
{{template "chat-pane" dict "ID" "editor"}}
|
||||
```
|
||||
|
||||
This renders the DOM structure with predictable IDs. The JS component
|
||||
attaches to these IDs on DOMContentLoaded. No client-side DOM
|
||||
construction for the initial layout.
|
||||
|
||||
---
|
||||
|
||||
## Layer 3: Surfaces
|
||||
|
||||
A surface is a full-page layout that composes components. Each surface
|
||||
declares:
|
||||
|
||||
1. **What components it uses** (ChatPane, NoteEditor, FileTree, etc.)
|
||||
2. **How they're arranged** (CSS grid/flex layout)
|
||||
3. **What data it needs on load** (Go data loader)
|
||||
4. **What JS runs on boot** (script block or module)
|
||||
|
||||
### Current Surfaces
|
||||
|
||||
| Surface | Components | Data Loader |
|
||||
|---------|-----------|-------------|
|
||||
| Chat | ChatPane, ProjectSidebar, ModelSelector, PersonaPicker | channels, personas, models, projects |
|
||||
| Editor | FileTree, CodeEditor, ChatPane (assist) | workspace, files, models |
|
||||
| Notes | NoteEditor, NoteGraph, ChatPane (assist) | notes, folders, graph |
|
||||
| Admin | AuditLog, SettingsForm, Table (various) | users, configs, models, health |
|
||||
| Settings | SettingsForm, KBPicker, PersonaPicker | profile, preferences, policies |
|
||||
|
||||
### Surface Registration
|
||||
|
||||
Today: hardcoded in Go (`pageEngine.RenderSurface("chat")`).
|
||||
|
||||
Future: manifest-driven. An admin-installed extension declares a
|
||||
surface in its manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaces": [{
|
||||
"id": "triage-intake",
|
||||
"route": "/s/triage",
|
||||
"title": "Triage Intake",
|
||||
"components": ["chat-pane", "settings-form"],
|
||||
"data_requires": ["personas", "models"],
|
||||
"script": "surfaces/triage.js",
|
||||
"auth": "authenticated"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
The page engine reads registered surfaces, generates routes, assembles
|
||||
data loaders from the declared requirements, and renders a shell
|
||||
template that loads the surface's script.
|
||||
|
||||
**Route namespace:** Admin-created surfaces live under `/s/:slug` to
|
||||
avoid collision with core routes.
|
||||
|
||||
### Data Loaders
|
||||
|
||||
Each surface declares what data it needs. The page engine has a
|
||||
registry of data providers:
|
||||
|
||||
```
|
||||
"personas" → loads personas for the current user
|
||||
"models" → loads enabled models with health status
|
||||
"channels" → loads user's channels
|
||||
"workspace" → loads workspace by :wsId param
|
||||
"notes" → loads notes list
|
||||
...
|
||||
```
|
||||
|
||||
The surface's `data_requires` field pulls from this registry. Data is
|
||||
injected into the page as `window.__PAGE_DATA__` (existing pattern).
|
||||
The surface's JS reads from there on boot — no waterfall of API calls
|
||||
on page load.
|
||||
|
||||
---
|
||||
|
||||
## Layer 4: Banners
|
||||
|
||||
The banner is global chrome outside the surface boundary. It exists
|
||||
at the top, bottom, or both — configured by the platform admin via
|
||||
`PUT /admin/settings/banner`.
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"text": "DEVELOPMENT",
|
||||
"position": "both|top|bottom",
|
||||
"bg": "#007a33",
|
||||
"fg": "#ffffff"
|
||||
}
|
||||
```
|
||||
|
||||
The Go template base layout renders banners before and after the
|
||||
surface content area. CSS custom properties (`--banner-top-height`,
|
||||
`--banner-bottom-height`, `--banner-bg`, `--banner-fg`) allow surfaces
|
||||
to account for banner space without knowing banner state.
|
||||
|
||||
Banners are not configurable by extensions or themes. They are a
|
||||
platform-level trust signal.
|
||||
|
||||
---
|
||||
|
||||
## Themes
|
||||
|
||||
A theme is a set of CSS custom property overrides applied to `<html>`.
|
||||
|
||||
```css
|
||||
[data-theme="corporate"] {
|
||||
--bg: #f5f5f5;
|
||||
--text: #1a1a1a;
|
||||
--accent: #0066cc;
|
||||
--border: #d1d5db;
|
||||
--font-ui: 'Inter', sans-serif;
|
||||
--font-code: 'Fira Code', monospace;
|
||||
--radius: 4px;
|
||||
/* ... full property set */
|
||||
}
|
||||
```
|
||||
|
||||
### What a Theme Can Do
|
||||
|
||||
- Override any CSS custom property in the theme contract
|
||||
- Change colors, fonts, border radii, spacing scale
|
||||
- Switch between light and dark base palettes
|
||||
- Apply to every primitive, component, and surface uniformly
|
||||
|
||||
### What a Theme Cannot Do
|
||||
|
||||
- Add or remove DOM elements
|
||||
- Execute JavaScript
|
||||
- Modify component behavior or layout
|
||||
- Override banner appearance (platform chrome)
|
||||
- Access APIs or user data
|
||||
|
||||
### Theme Contract
|
||||
|
||||
The set of CSS custom properties that all primitives read from. This
|
||||
is the stable API between themes and the UI. Properties are namespaced:
|
||||
|
||||
```
|
||||
--bg, --bg-secondary, --bg-tertiary (backgrounds)
|
||||
--text, --text-secondary, --text-muted (typography)
|
||||
--accent, --accent-hover, --accent-muted (interactive)
|
||||
--border, --border-strong (edges)
|
||||
--success, --warning, --danger (semantic)
|
||||
--font-ui, --font-code (typefaces)
|
||||
--radius, --radius-lg (shapes)
|
||||
--shadow, --shadow-lg (elevation)
|
||||
--banner-bg, --banner-fg (read-only, set by platform)
|
||||
```
|
||||
|
||||
Themes are admin-installed. An admin uploads a CSS file that declares
|
||||
a `[data-theme="name"]` rule set. Users can select from installed
|
||||
themes in Settings → Appearance.
|
||||
|
||||
Built-in themes: `dark` (default), `light`. The system preference
|
||||
auto-detection (`prefers-color-scheme`) continues to work.
|
||||
|
||||
---
|
||||
|
||||
## Extension Hooks
|
||||
|
||||
Extensions interact with the application through a defined set of
|
||||
hooks. Each hook has a specific trigger point in the lifecycle and
|
||||
a clear contract for what the extension receives and returns.
|
||||
|
||||
### Hook 1: Pre-Completion
|
||||
|
||||
**When:** After the user sends a message, before the API request fires.
|
||||
**Receives:** The completion request object (model, messages, tools, etc.)
|
||||
**Returns:** Modified request object (or unmodified to pass through).
|
||||
**Use case:** Inject additional tools, modify system prompt, add context.
|
||||
|
||||
```js
|
||||
ctx.hooks.preCompletion(request => {
|
||||
request.tools.push(myCustomTool);
|
||||
return request;
|
||||
});
|
||||
```
|
||||
|
||||
### Hook 2: Stream Processing
|
||||
|
||||
**When:** On each SSE chunk during streaming.
|
||||
**Receives:** The chunk (content delta, tool_use, tool_result, etc.)
|
||||
**Returns:** Modified chunk (or null to suppress).
|
||||
**Use case:** Transform content, intercept tool calls, accumulate data.
|
||||
|
||||
```js
|
||||
ctx.hooks.streamChunk((chunk, context) => {
|
||||
if (chunk.type === 'tool_result' && chunk.name === 'image_gen') {
|
||||
context.displayContent.push({ type: 'image', src: chunk.data.url });
|
||||
return null; // suppress from text content
|
||||
}
|
||||
return chunk;
|
||||
});
|
||||
```
|
||||
|
||||
### Hook 3: Post-Render
|
||||
|
||||
**When:** After a message is rendered into the DOM.
|
||||
**Receives:** The message container element, the message object.
|
||||
**Returns:** Nothing (mutates DOM in place).
|
||||
**Use case:** Add action buttons, wrap elements, enhance display.
|
||||
|
||||
This is how extensions compose with each other's output. Extension A
|
||||
produces an image via display content. Extension B's post-render hook
|
||||
finds `<img>` elements and wraps them with action buttons. They don't
|
||||
know about each other — they agree on the DOM contract.
|
||||
|
||||
```js
|
||||
ctx.hooks.postRender((containerEl, message) => {
|
||||
containerEl.querySelectorAll('img[data-display-content]').forEach(img => {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'image-actions';
|
||||
actions.innerHTML = '<button data-action="upscale">Upscale</button>';
|
||||
img.parentElement.appendChild(actions);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Hook 4: Display Content
|
||||
|
||||
**When:** After a completion finishes (all chunks received).
|
||||
**Receives:** The assistant message object, accumulated display content.
|
||||
**Returns:** Display content items to attach to the message.
|
||||
|
||||
Display content is **message-scoped, rendered inline, but excluded from
|
||||
the LLM context window**. It's not in `messages.content` and not sent
|
||||
back on the next turn.
|
||||
|
||||
```
|
||||
Message
|
||||
├── content (text — goes to LLM)
|
||||
├── attachments (files — go to LLM via multimodal assembly)
|
||||
└── display_content[] ← rendered inline, NOT sent to LLM
|
||||
├── { type: "image", src: "...", extension_id: "img-gen" }
|
||||
└── { type: "html", content: "<div>...", extension_id: "editor" }
|
||||
```
|
||||
|
||||
**Storage:** Display content is persisted in a `display_content` JSONB
|
||||
column on the message (or a junction table). It survives page reload
|
||||
and is included in `GET /channels/:id/path` responses.
|
||||
|
||||
### Hook 5: Tool Bridge
|
||||
|
||||
**When:** The LLM invokes a tool registered by the extension.
|
||||
**Receives:** Tool name and input parameters.
|
||||
**Returns:** Tool result (string or structured).
|
||||
|
||||
Existing mechanism — the WebSocket tool bridge from EXTENSIONS.md.
|
||||
Tool is registered via manifest, exposed to the LLM through the
|
||||
tools list, and executed client-side (browser tier) or server-side
|
||||
(starlark/sidecar tiers).
|
||||
|
||||
### Hook 6: Surface Injection
|
||||
|
||||
**When:** Surface boot (DOMContentLoaded).
|
||||
**Receives:** The surface ID and available mount points.
|
||||
**Returns:** Nothing (attaches to mount points).
|
||||
|
||||
Extensions declare which surfaces they target and which mount points
|
||||
they use:
|
||||
|
||||
```json
|
||||
{
|
||||
"surface_injections": [{
|
||||
"surface": "chat",
|
||||
"mount_point": "side-panel",
|
||||
"component": "my-panel.js"
|
||||
}, {
|
||||
"surface": "admin",
|
||||
"mount_point": "section",
|
||||
"section_id": "my-admin-section",
|
||||
"label": "Image Gen Settings"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Mount points are declared by each surface template:
|
||||
|
||||
```html
|
||||
<div data-mount="side-panel"></div>
|
||||
<div data-mount="section" data-section-id="..."></div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Trust Model
|
||||
|
||||
Two tiers. The admin is the trust boundary.
|
||||
|
||||
### Privileged (Admin-Installed)
|
||||
|
||||
| Capability | Allowed |
|
||||
|-----------|---------|
|
||||
| Full DOM access | Yes |
|
||||
| Direct API calls (user's JWT) | Yes |
|
||||
| Modify other extensions' output (post-render) | Yes |
|
||||
| Create surfaces (own routes) | Yes |
|
||||
| Register tools | Yes |
|
||||
| Access all hook types | Yes |
|
||||
| Go template data loader | Yes |
|
||||
| Read `window.__PAGE_DATA__` | Yes |
|
||||
|
||||
The admin chose to install it. Same trust model as a VS Code extension
|
||||
or a WordPress plugin — you trust the publisher.
|
||||
|
||||
### Sandboxed (User-Installed, Future)
|
||||
|
||||
| Capability | Allowed |
|
||||
|-----------|---------|
|
||||
| Shadow DOM / iframe only | Yes |
|
||||
| API calls through message bridge | Yes (proxied, scoped) |
|
||||
| Modify other extensions' output | No |
|
||||
| Create surfaces | No |
|
||||
| Register tools | Limited (user-scoped) |
|
||||
| Access hooks | Post-render own output only |
|
||||
| Go template data loader | No |
|
||||
| Read `window.__PAGE_DATA__` | No |
|
||||
|
||||
Post-1.0 scope. Documented here to confirm the architecture
|
||||
accommodates it without redesign.
|
||||
|
||||
---
|
||||
|
||||
## Extension Lifecycle
|
||||
|
||||
### Boot Sequence
|
||||
|
||||
```
|
||||
1. Page loads (Go template renders surface shell + banner)
|
||||
2. Primitives initialize (CSS loaded, JS factories available)
|
||||
3. Components hydrate (attach to server-rendered DOM)
|
||||
4. Extensions.boot() — idempotent, runs on every surface
|
||||
a. Load extension manifests
|
||||
b. Register hooks (pre-completion, post-render, etc.)
|
||||
c. Register surface injections for current surface
|
||||
d. Initialize tool bridges
|
||||
5. Surface-specific JS runs (app.js for chat, editor-mode.js, etc.)
|
||||
```
|
||||
|
||||
`Extensions.boot()` replaces the current chat-specific
|
||||
`Extensions.loadAll()` / `Extensions.initAll()` pair and runs on
|
||||
every surface.
|
||||
|
||||
### Manifest Declaration
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "image-generator",
|
||||
"name": "AI Image Generator",
|
||||
"version": "1.0.0",
|
||||
"tier": "browser",
|
||||
"hooks": {
|
||||
"pre_completion": "hooks/pre-completion.js",
|
||||
"stream_chunk": "hooks/stream.js",
|
||||
"post_render": "hooks/post-render.js"
|
||||
},
|
||||
"tools": [{
|
||||
"name": "generate_image",
|
||||
"description": "Generate an image from a text description",
|
||||
"parameters": { ... }
|
||||
}],
|
||||
"surfaces": [{
|
||||
"id": "image-gallery",
|
||||
"route": "/s/gallery",
|
||||
"title": "Image Gallery",
|
||||
"components": ["chat-pane"],
|
||||
"data_requires": ["channels"],
|
||||
"script": "surfaces/gallery.js"
|
||||
}],
|
||||
"surface_injections": [{
|
||||
"surface": "chat",
|
||||
"mount_point": "message-actions",
|
||||
"script": "injections/image-actions.js"
|
||||
}],
|
||||
"theme": null,
|
||||
"settings_schema": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Display Content — Data Model
|
||||
|
||||
### Message Extension
|
||||
|
||||
```sql
|
||||
ALTER TABLE messages ADD COLUMN display_content JSONB DEFAULT '[]';
|
||||
```
|
||||
|
||||
Array of display items:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "image",
|
||||
"src": "data:image/png;base64,...",
|
||||
"alt": "A cat in a spacesuit",
|
||||
"extension_id": "image-generator",
|
||||
"metadata": { "model": "dall-e-3", "size": "1024x1024" }
|
||||
},
|
||||
{
|
||||
"type": "html",
|
||||
"content": "<div class='chart'>...</div>",
|
||||
"extension_id": "data-viz",
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### API Contract
|
||||
|
||||
Display content is included in message objects returned by
|
||||
`GET /channels/:id/path` and `GET /channels/:id/messages`.
|
||||
|
||||
It is **excluded** from the message array sent to the LLM provider
|
||||
in `POST /chat/completions`. The completion handler strips
|
||||
`display_content` when assembling the provider request.
|
||||
|
||||
### Rendering
|
||||
|
||||
During `renderMessages()`, after the message content is rendered,
|
||||
each `display_content` item is rendered below the text content:
|
||||
|
||||
```html
|
||||
<div class="message-content">
|
||||
<p>Here's the image you requested:</p>
|
||||
</div>
|
||||
<div class="message-display-content">
|
||||
<img src="..." alt="..." data-display-content data-extension="image-generator">
|
||||
</div>
|
||||
```
|
||||
|
||||
The `data-display-content` attribute is the DOM contract that
|
||||
post-render hooks use to discover display content from any extension.
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
| Current State | Target State |
|
||||
|---------------|-------------|
|
||||
| `ui-primitives.js` + `ui-primitives-additions.js` | Primitive catalog with stable factory API |
|
||||
| `ChatPane.create()` (v0.22.7) | Component catalog with shared instance pattern |
|
||||
| 5 Go template routes | Surface registry (hardcoded core, manifest-driven extensions) |
|
||||
| `ctx.ui.replace()` / `ctx.ui.inject()` | Hook 6: Surface Injection with declared mount points |
|
||||
| `ctx.surfaces.register()` | Manifest `surfaces` field → page engine route registration |
|
||||
| Block renderers in `Extensions.initAll()` | `Extensions.boot()` on every surface |
|
||||
| Message content only (text + attachments) | `display_content` column for render-only visual content |
|
||||
| Block renderer pipeline (mermaid, katex) | Post-render hook + DOM contract (`data-display-content`) |
|
||||
| `[data-theme]` with CSS variables | Theme contract: stable CSS custom property namespace |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Sequence
|
||||
|
||||
This does not prescribe version numbers. It describes dependency order.
|
||||
|
||||
**Phase 1: Primitive consolidation**
|
||||
- Audit `ui-primitives.js`, `ui-primitives-additions.js`, and Go
|
||||
template components
|
||||
- Define the primitive catalog and factory API
|
||||
- Define the CSS custom property contract (theme API)
|
||||
- Migrate existing callers to the consolidated API
|
||||
|
||||
**Phase 2: Component formalization**
|
||||
- ChatPane already exists (v0.22.7). Formalize the instance pattern.
|
||||
- Extract NoteEditor, FileTree, ModelSelector as components
|
||||
- Each component gets a Go template partial + JS hydration
|
||||
|
||||
**Phase 3: Extension hooks**
|
||||
- `Extensions.boot()` on every surface (v0.22.8)
|
||||
- Pre-completion and post-render hooks
|
||||
- Display content data model (message column + render pipeline)
|
||||
- Stream processing hook
|
||||
|
||||
**Phase 4: Surface registry**
|
||||
- Manifest-driven surface registration
|
||||
- Dynamic route generation in page engine
|
||||
- Data loader registry
|
||||
- Mount point declaration and injection
|
||||
|
||||
**Phase 5: Theme system**
|
||||
- Stable CSS custom property contract
|
||||
- Admin theme upload
|
||||
- User theme selection in Settings → Appearance
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Display content storage:** JSONB column on messages vs. separate
|
||||
`message_display_content` table. Column is simpler; table allows
|
||||
independent lifecycle (delete display content without touching
|
||||
message). Leaning column for KISS.
|
||||
|
||||
2. **Display content size limits:** Base64 images in JSONB could get
|
||||
large. May need blob storage for display content with URL references
|
||||
instead of inline data. Deferred until real usage patterns emerge.
|
||||
|
||||
3. **Extension ordering:** Post-render hooks from multiple extensions
|
||||
run in what order? Manifest priority field? Installation order?
|
||||
Alphabetical? Matters when Extension B adds buttons to Extension A's
|
||||
images — A must render first.
|
||||
|
||||
4. **Surface layout persistence:** If a surface supports resizable
|
||||
panes (editor: file tree width, chat pane width), where is that
|
||||
persisted? User settings? Per-workspace? Per-surface?
|
||||
|
||||
5. **Extension settings storage:** Extensions need per-user config
|
||||
(API keys for image gen, preferences for behavior). Current
|
||||
`UpdateUserExtensionSettings` endpoint exists. Is the schema
|
||||
sufficient or does it need structured validation from
|
||||
`settings_schema` in the manifest?
|
||||
Reference in New Issue
Block a user