Insert two new version series after v0.9.x: - v0.10.x Panels + Composable Layout (kernel primitive) - v0.11.x Notes Reference Extension (first-party extension) Renumber existing planned work: - v0.10.x Reference Extensions → v0.12.x - v0.11.x Sidecar Tier + Polish → v0.13.x Add new design docs for panels and notes v0.11.x series. Update v1.0.0 gate criteria and design decisions log. Remove stale docs (AUDIT-surfaces, USABILITY-SURVEY). Fix dangling reference in DESIGN-shell-contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
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):
{{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:
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 3–7 sections that don't have sub-items. More real estate for content than a sidebar layout.
Surface code:
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:
// 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.
.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-topbardiv and CSS. - Delete sidebar nav. Move section links into
sw.shell.topbar.setSlot(). - Remove
sb_settings_returnsessionStorage — 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-topbardiv 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
CatIconrenderer 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-topbardiv 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_returnsessionStorage. - 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:
// 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):
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():
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:
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:
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):
.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:
finallycleanup + tighten409assertion. - 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.