Right-side panel (chrome)
The bundle is your app’s main canvas. The chrome panel is the platform-rendered right-side rail that ships with built-in tabs your app can reuse for free — chat with your specialist agent, edit install settings, browse generated files, plus any custom tabs you want to slot in.
Same idea as Office Assistant or Sachverständiger: those first- party apps each ship their own bespoke React for the right rail. The chrome panel hands the same surface to partner manifests so you don’t reimplement chat / settings UI from scratch.
Quick example
Section titled “Quick example”Add a chrome: block to your manifest:
chrome: panel: enabled: true default_tab: chat width: 400 # px, 280-720 tabs: - chat # built-in - settings # built-in - files # built-in - id: drafts # custom tab label: Drafts icon: file-text path: /panel/drafts.html # served from your bundle CDNThat’s it. The platform renders the tab bar, applies the tenant
theme, mounts the right component for built-in tabs, and loads
your panel/drafts.html in a sub-iframe (same sandbox + bridge
protocol as the main bundle) for custom tabs.
Built-in tabs
Section titled “Built-in tabs”Mounts the platform’s ChatInterface scoped to your specialist
agent. Conversations the user starts here go directly to the
agent provisioned for your app — no orchestrator hop, faster
responses, and the conversation feels like “talking to your app”
rather than “asking the platform to dispatch.”
The chat UI surfaces:
- Message history persisted per (user, agent)
- Tool calls your agent makes (
study_generate_practice, etc.) rendered as cards - File attachments (when agent calls
create_pdfetc.)
Requires the agent: block in your manifest. The platform
looks up the active linkworld_agents row keyed by
(tenant_id, app_id) and pins the chat to it. If the agent isn’t
yet provisioned (e.g. install hook hasn’t run), the tab shows a
fallback pointing the user at re-activation.
settings
Section titled “settings”Auto-renders an edit form from your install_settings: schema —
same field renderer the activation consent dialog uses, but in
edit mode. The user changes values, clicks Save, and the platform
PATCHes linkworld_tenant_apps.config.
chrome: panel: enabled: true tabs: [settings]install_settings: - id: digest_time type: string label: Daily digest time default: "08:30" - id: language type: select label: Language options: [de, en] default: enThat’s a settings UI for free — no React to ship, no API to wire.
project_picker fields are read-only post-install (changing the
project re-provisions the vision-loop and is a heavier operation)
— the user is directed to deactivate + reactivate to switch.
Lists the user’s recent agent-generated artifacts (PDFs from
create_pdf, etc.) with download links.
v1 limitation: doesn’t yet filter to the calling app’s files
specifically. Per-app filtering arrives once skills auto-tag
generated files with app:<slug> — at which point the same Files
tab will narrow to your app’s outputs without a manifest change.
Custom tabs — sub-iframe pattern
Section titled “Custom tabs — sub-iframe pattern”A custom tab is a tab whose content is another iframe loaded from your bundle CDN. Same origin gate, same theme tokens, same postMessage bridge. The bundle ships one HTML file per tab:
your-app/web/├── index.html # main bundle (the big iframe)├── app.js├── styles.css└── panel/ ├── drafts.html # /panel/drafts.html ├── inbox.html └── compose.htmlEach panel HTML is a tiny single-page app that calls your tools via the bridge:
<!-- panel/drafts.html (sketch) --><!doctype html><html><head><link rel="stylesheet" href="../styles.css"></head><body> <h2>Drafts</h2> <ul id="list"></ul> <script> function callTool(t, a) { const id = `t${Date.now()}`; return new Promise((res, rej) => { window.addEventListener('message', e => { if (e.data.id === id && e.data.type === 'tools.result') res(e.data.result); if (e.data.id === id && e.data.type === 'tools.error') rej(e.data.error); }); window.parent.postMessage({ id, type: 'tools.call', tool: t, args: a }, '*'); }); } callTool('study_list_topics').then(r => { document.getElementById('list').innerHTML = r.topics .map(t => `<li>${t.topic}</li>`).join(''); }); </script></body></html>The same theme tokens (--lw-bg, --lw-accent, etc.) flow into
the panel iframe via the init message. Your styles.css reused
across main bundle and panels means everything stays consistent.
When to use which tab type
Section titled “When to use which tab type”| Need | Tab |
|---|---|
| Conversational interaction with your app | chat |
| Tenant edits app config | settings |
| Browse files the user has generated | files |
| Compact list view next to the main bundle (e.g. drafts, inbox, saved items) | custom tab |
| Side-by-side comparison views | custom tab |
| Wizard / step UI separate from the main editor | custom tab |
Two custom-tab patterns to copy:
A) Sidebar list (study-buddy panel/topics.html): compact
list of items with quick-action buttons. Clicking a button calls
the same tool the main bundle would call — they’re the same
backend.
B) Side-channel editor: e.g. social-manager could put a “compose post” panel in a custom tab while the main bundle shows the inbox. User drags a comment from the inbox to the compose panel.
Layout & UX details
Section titled “Layout & UX details”- Default width: 400px. Range 280-720. Bundle iframe gets the rest of the main column.
- Collapsible: user clicks
›in the panel header to hide it; bundle re-flows to fill. “Panel” button in the route header re-opens. - Default tab: set via
default_tab(must match a tab id or built-in name); falls back to first tab if unset. - Tab order: rendered left-to-right in declaration order.
- Theme: all tabs inherit the tenant’s color tokens via the
same
--lw-*CSS variables your main bundle uses. Custom tabs get the sameinit+theme.changedpostMessage stream.
What you get for free vs. what you ship
Section titled “What you get for free vs. what you ship”| Concern | Platform handles | You handle |
|---|---|---|
| Tab bar rendering | ✓ | — |
| Theme forwarding to all tabs | ✓ | — |
| Origin gate / sandbox for custom tabs | ✓ | — |
| Built-in tab content (chat / settings / files) | ✓ | — |
| Custom tab HTML/CSS/JS | — | ✓ (in web/panel/<id>.html) |
| Tool-call wiring inside panels | — | ✓ (postMessage to parent) |
Backwards compat
Section titled “Backwards compat”Apps without a chrome: block render exactly as before — bundle
iframe takes the full content area, no panel. Adding chrome later
is purely additive; tenants who installed before the chrome update
keep working unchanged until you bump version + they re-activate
(or the install row is bumped, in which case they get the new
manifest’s chrome on next page load).
Limits
Section titled “Limits”- Max 8 tabs per panel (4 built-in + 4 custom is plenty)
- Custom tab
pathmust resolve under your bundle’s apps-cdn origin — same CSP/sandbox rules as the main bundle - No nested chrome panels — a custom tab can’t itself declare another panel
chatrequiresagent:; without an agent the tab shows a fallback messagesettingsrequiresinstall_settings:; without settings the tab shows “no configurable settings”