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

17 KiB
Raw Permalink Blame History

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 37 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-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_idsw.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)

FrontendNotificationBell 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: 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.