This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/DESIGN-shell-contract.md
Jeffrey Smith 0cae963480
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-sqlite (push) Has been skipped
CI/CD / test-frontend (push) Has been skipped
CI/CD / test-go-pg (push) Has been skipped
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / build-and-deploy (push) Successful in 30s
Chore roadmap v010x shift (#76)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-04-03 14:52:41 +00:00

449 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DESIGN: Shell Contract — v0.7.0
## Status: Proposed
## Problem
Extension surfaces render into `#extension-mount` with no shell chrome.
A full audit (v0.7.0 surface audit) found: 3/4 primary surfaces lack a
notification bell, 2/4 lack a user menu, 4 different topbar implementations,
user menu never updates on package/role changes, toast-and-forget error
handling, and empty states that explain nothing.
## Solution
### 1. Shell Topbar — Two-Slot Model
The kernel injects a topbar for all extension surfaces. Two named slots
(left + center) let surfaces customize without replacing the entire bar.
**Layout:**
```
┌──────────────────────────────────────────────────────────────┐
│ ← │ [left] │ [center: flex-1] │ 🔔 │ 👤 │
└──────────────────────────────────────────────────────────────┘
```
- **← (home):** Always visible. Navigates to `__BASE__/`. Simple `<a>`.
- **Left slot:** Defaults to manifest title. Surfaces override with `setLeft()`.
- **Center slot:** Empty by default. Surfaces inject tabs, search, pickers.
`flex: 1` — expands to fill available space.
- **Bell + User Menu:** Always visible. Kernel-managed.
**Template change** (`surfaces/extension.html`):
```html
{{define "surface-extension"}}
<div id="extension-surface" class="extension-surface"
data-surface-id="{{.Surface}}">
<div id="shell-topbar" class="sw-topbar sw-topbar--shell"></div>
<div id="extension-mount" class="extension-mount" data-ext="{{.Surface}}"></div>
</div>
{{end}}
```
**SDK API:**
```js
sw.shell.topbar.setLeft(vnode) // Override left slot (default: title)
sw.shell.topbar.setSlot(vnode) // Set center slot content
sw.shell.topbar.setTitle(str) // Shorthand: setLeft with plain text
sw.shell.topbar.hide() // Remove topbar entirely
sw.shell.topbar.show() // Restore after hiding
```
### 2. Three Navigation Patterns
The shell topbar provides the top bar. What happens below it is the
surface's business. Three patterns emerge naturally:
#### Pattern A — Default (simple extensions, Docs)
```
┌──────────────────────────────────────────┐
│ ← │ Surface Title │ 🔔 │ 👤 │
├──────────────────────────────────────────┤
│ │
│ Content (full width) │
│ │
└──────────────────────────────────────────┘
```
Title only, no tabs, no sidebar. Content gets everything.
Zero code required — kernel defaults handle it.
**Used by:** Docs, Notes, Chat, simple extensions.
#### Pattern B — Flat Tabs (no sidebar, full width)
```
┌──────────────────────────────────────────────────────────┐
│ ← │ Title │ Tab1 │ Tab2 │ Tab3 │ Tab4 │ │ 🔔 │ 👤 │
├──────────────────────────────────────────────────────────┤
│ │
│ Content (full width, no sidebar) │
│ │
└──────────────────────────────────────────────────────────┘
```
Tabs in the topbar center slot. No sidebar — content fills the full
viewport width. For surfaces with 37 sections that don't have sub-items.
More real estate for content than a sidebar layout.
**Surface code:**
```js
sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${sections.map(s => html`
<a class="sw-topbar__tab ${active === s.key ? 'active' : ''}"
href=${s.href} onClick=${navigate}>${s.label}</a>
`)}
</div>
`);
```
**Used by:** Team Admin (Members / Connections / Workflows / Settings / Activity),
Settings, Schedules.
#### Pattern C — Category Tabs + Sidebar (two-level)
```
┌─────────────────────────────────────────────────────────────┐
│ ← │ Title │ People │ Workflows │ System │ Mon │ 🔔 │ 👤 │
├──────────┬──────────────────────────────────────────────────┤
│ Users │ │
│ Teams │ Content │
│ Groups │ │
│ │ │
└──────────┴──────────────────────────────────────────────────┘
```
Major categories in the topbar (via center slot). Surface renders its own
sidebar inside the content area for sub-navigation within the active
category. The shell topbar doesn't know about the sidebar — it's a
surface-level div below `#extension-mount`.
**Surface code:**
```js
// Topbar: major categories
sw.shell.topbar.setLeft(html`
<img src="${BASE}/favicon.svg" width="18" height="18" style="vertical-align:-3px" />
<span style="margin-left:6px">Administration</span>
`);
sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${categories.map(c => html`
<a class="sw-topbar__tab ${activeCat === c.key ? 'active' : ''}"
href=${c.href} onClick=${navigate}>
<${CatIcon} paths=${c.icon} /> ${c.label}
</a>
`)}
</div>
`);
// Content area: sidebar is surface-owned
return html`
<div class="admin-body">
<div class="admin-nav">
${sidebarSections.map(s => html`...`)}
</div>
<div class="admin-content">
<${SectionComponent} />
</div>
</div>
`;
```
**Used by:** Admin.
### 3. Kernel-Provided Tab CSS
The kernel provides `.sw-topbar__tabs` and `.sw-topbar__tab` CSS so
surfaces get consistent tab styling. Not required — surfaces can style
their own slot content however they want.
```css
.sw-topbar__tabs {
display: flex;
align-items: center;
gap: var(--sp-1);
height: 100%;
}
.sw-topbar__tab {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
font-size: 13px;
font-weight: 500;
color: var(--text-2);
text-decoration: none;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
white-space: nowrap;
}
.sw-topbar__tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.sw-topbar__tab.active {
color: var(--text);
background: var(--bg-2);
}
```
### 4. Primary Surface Migrations
#### Settings → Pattern B (flat tabs)
Currently: custom topbar (back + icon + "Settings"), sidebar nav.
After: shell topbar with flat tabs, no sidebar. Content full width.
Settings has 6 sections (General / Appearance / Profile / Teams /
Connections / Notifications) — perfect for flat tabs. The sidebar was
thin (~140px) and ate width from the content area for no good reason.
**Migration:**
- Delete `settings-topbar` div and CSS.
- Delete sidebar nav. Move section links into `sw.shell.topbar.setSlot()`.
- Remove `sb_settings_return` sessionStorage — shell home link handles it.
- Content area becomes full-width.
- Fix Teams section: add "Open Team Admin →" link, show role, add leave action.
Extension config sections (`__CONFIG_SECTIONS__`) render as additional
tabs after the divider. Same as today, just in the topbar instead of sidebar.
#### Admin → Pattern C (category tabs + sidebar)
Currently: custom topbar (favicon + "Administration" + category tabs + UserMenu).
After: shell topbar with category tabs in center slot, surface-owned sidebar.
**Migration:**
- Delete custom `admin-topbar` div and CSS.
- `sw.shell.topbar.setLeft()` with favicon + "Administration".
- `sw.shell.topbar.setSlot()` with category tabs (People / Workflows / System / Monitoring).
- Bell and user menu come from the shell — delete the explicit `<${UserMenu}>`.
- Admin sidebar and content area unchanged — they're below the topbar.
- Delete custom `CatIcon` renderer if category tab icons use standard SVG.
- Fix hardcoded `favicon.svg` — left slot can use theme-aware image.
**Result:** Admin looks identical to today but its topbar is kernel-managed.
Bell added for free. User menu reactive for free.
#### Team Admin → Pattern B (flat tabs)
Currently: custom topbar (back + "Team Admin: {name}"), sidebar nav.
After: shell topbar with flat tabs, no sidebar.
With Groups removed, Team Admin has 5 sections: Members / Connections /
Workflows / Settings / Activity. Perfect for flat tabs.
**Migration:**
- Delete `team-admin-topbar` div and CSS.
- `sw.shell.topbar.setTitle('Team Admin: ' + team.name)` after team fetch.
- Section tabs into `sw.shell.topbar.setSlot()`.
- Delete sidebar nav. Content full-width.
- Remove `sb_team_admin_return` sessionStorage.
- Remove Groups tab entirely (37-line dead-end).
- Fix signoff display: `user_id``sw.users.displayName()`.
#### Docs → Pattern A (default)
Currently: imports `shell/topbar.js` and renders it explicitly.
After: shell topbar auto-renders. No surface code needed.
**Migration:**
- Delete `import { Topbar }` and `<${Topbar}>` render.
- Shell topbar provides title + bell + user menu automatically.
- Docs sidebar (document list) is in the content area, unaffected.
### 5. User Menu Reactivity
**The single most impactful fix.**
**Backend — new WS events:**
```go
// After package install/uninstall/enable/disable:
h.hub.BroadcastToUser(userID, "package.changed", map[string]string{
"action": "installed", "id": packageID,
})
// After team role/membership change:
h.hub.BroadcastToUser(userID, "auth.changed", map[string]string{
"reason": "team_role",
})
```
**Frontend** (`user-menu.js`):
```js
useEffect(() => {
if (!sw?.api?.surfaces?.list) return;
function fetchSurfaces() {
sw.api.surfaces.list().then(data => {
const raw = Array.isArray(data) ? data : data?.data || [];
setAllSurfaces(raw);
}).catch(() => {});
}
fetchSurfaces();
const off1 = sw.on?.('package.changed', fetchSurfaces);
const off2 = sw.on?.('auth.changed', fetchSurfaces);
return () => {
if (typeof off1 === 'function') off1();
if (typeof off2 === 'function') off2();
};
}, [authenticated]);
```
**Event inventory:**
| Event | Trigger | Payload |
|-------|---------|---------|
| `package.changed` | Install, uninstall, enable, disable | `{ action, id }` |
| `auth.changed` | Team role change, group membership change | `{ reason }` |
| `notification.read` | Mark notification read | `{ id }` |
| `notification.all_read` | Mark all read | `{}` |
### 6. Notification Read Broadcast
**Backend** — after `MarkRead()` / `MarkAllRead()`:
```go
h.hub.BroadcastToUser(userID, "notification.read", map[string]string{"id": id})
h.hub.BroadcastToUser(userID, "notification.all_read", nil)
```
**Frontend**`NotificationBell` listens for `.created`, `.read`, `.all_read`:
```js
const onRead = (e) => {
setNotifications(prev => prev.map(n =>
n.id === e.id ? { ...n, read_at: new Date().toISOString() } : n
));
};
const onAllRead = () => {
setNotifications(prev => prev.map(n => ({
...n, read_at: n.read_at || new Date().toISOString()
})));
};
```
### 7. Shell Announcement Global Dismiss
On dismiss, write: `localStorage.setItem('armature_dismissed_' + hash(text), '1')`.
On mount, check. New announcements (changed text) show again.
Implementation: inline `<script>` in `base.html` — checks localStorage on
DOMContentLoaded. Dismiss button writes the key. Works on every surface
including login.
### 8. Error Handling Pass
**Pattern** — inline error + retry replaces toast-and-forget:
```js
const [error, setError] = useState(null);
async function load() {
setError(null);
try {
const data = await sw.api.whatever.list();
setItems(data || []);
} catch (e) { setError(e.message); }
}
// In render:
${error && html`
<div class="sw-inline-error">
<span>${error}</span>
<button class="sw-btn sw-btn--secondary sw-btn--sm"
onClick=${load}>Retry</button>
</div>
`}
```
**New CSS class** (`sw-primitives.css`):
```css
.sw-inline-error {
display: flex; align-items: center; gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
background: var(--bg-2); border: 1px solid var(--danger);
border-radius: var(--radius); font-size: 13px; color: var(--danger);
}
```
**Sections requiring this pass:**
| Surface | Section | Current | After |
|---------|---------|---------|-------|
| Admin | Workflows | Toast + empty | Inline error + retry |
| Admin | Packages | Toast + empty | Inline error + retry |
| Admin | Groups | Toast + empty | Inline error + retry |
| Team Admin | Workflows (adopt) | Toast + "No global workflows" | Inline error + retry |
| Team Admin | Members | Toast + empty | Inline error + retry |
| Settings | General | Console warn | Inline error + retry |
| Settings | Teams | Toast + empty | Inline error + retry |
| Workflow Demo (pkg) | Main | Silent swallow | Inline error + retry |
### 9. Empty State Guidance
Every "No X" empty state gets: one-line explanation, primary action or doc link.
| Surface | Section | Current | After |
|---------|---------|---------|-------|
| Admin | Groups | "No groups" | "Groups control access to surfaces and features via permissions." + Create button |
| Admin | Workflows | "No workflows" | "Workflows define multi-stage approval processes with team roles and SLA tracking." + Create button |
| Admin | Teams | "No teams" | "Teams group users for shared connections, workflows, and access control." + Create button |
| Team Admin | Workflows | "No workflows — create one or adopt" | Add: "Adopt copies a global workflow for this team to customize." |
| Settings | Notifications | "No notification preferences" | "Preferences appear when notification types are configured by an administrator." |
## Rebrand Asset Inventory
### Current → Target
| File | Current | Target |
|------|---------|--------|
| `favicon.svg` | Dark icon ✅ | Unchanged |
| `favicon-light.svg` | **MISNAMED** (520×80 wordmark) | Square icon, light mode **(NEW)** |
| `favicon-32.png` | Dark raster ✅ | Unchanged |
| `favicon-256.png` | Dark raster ✅ | Unchanged |
| `favicon-light-32.png` | Missing | Light raster **(NEW)** |
| `favicon-light-256.png` | Missing | Light raster **(NEW)** |
| `favicon.ico` | Dark ✅ | Unchanged |
| `wordmark.svg` | Missing | **RENAMED** from current `favicon-light.svg` |
| `wordmark-dark.svg` | Missing | Light text #E5E5E5 on transparent **(NEW)** |
| `manifest.json` | "Self-hosted AI chat..." | "Self-hosted extension platform..." |
## Bug Fixes (bundled)
- **evil-chat:** `finally` cleanup + tighten `409` assertion.
- **Workflow demo:** Silent `catch` → inline error + retry.
- **Hello dashboard:** Delete `packages/hello-dashboard/`.
## Changeset Plan
| CS | Scope | Description |
|----|-------|-------------|
| CS-1 | Backend (Go) | WS events: `notification.read`, `notification.all_read`, `package.changed`, `auth.changed`. Tests. |
| CS-2 | Frontend (JS + HTML) | Shell topbar component, SDK API (`setLeft`/`setSlot`/`setTitle`/`hide`), extension.html template, SDK boot auto-mount. |
| CS-3 | Frontend (JS) | User menu reactivity: listen for `package.changed` + `auth.changed`. Notification bell: listen for `.read` + `.all_read`. |
| CS-4 | Frontend (JS + HTML) | Surface migrations: Settings → Pattern B, Admin → Pattern C, Team Admin → Pattern B, Docs → Pattern A. Delete custom topbars. |
| CS-5 | Frontend (JS + CSS) | Error handling pass + empty state guidance. `sw-inline-error` CSS. All sections from inventory. |
| CS-6 | Frontend (JS) | Announcement global dismiss (localStorage). |
| CS-7 | Static + docs | Rebrand assets, manifest.json, REBRAND-SPEC.md. |
| CS-8 | Frontend (JS) | Bug fixes: evil-chat cleanup, workflow demo error, hello-dashboard deletion. |
Each changeset independently CI-green.