747 lines
25 KiB
Markdown
747 lines
25 KiB
Markdown
# 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?
|