Skip to content

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.

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 CDN

That’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.

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_pdf etc.)

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.

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: en

That’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.

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.html

Each 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.

NeedTab
Conversational interaction with your appchat
Tenant edits app configsettings
Browse files the user has generatedfiles
Compact list view next to the main bundle (e.g. drafts, inbox, saved items)custom tab
Side-by-side comparison viewscustom tab
Wizard / step UI separate from the main editorcustom 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.

  • 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 same init + theme.changed postMessage stream.
ConcernPlatform handlesYou 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)

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

  • Max 8 tabs per panel (4 built-in + 4 custom is plenty)
  • Custom tab path must 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
  • chat requires agent:; without an agent the tab shows a fallback message
  • settings requires install_settings:; without settings the tab shows “no configurable settings”