Pages and windows
Choose a page for a top-level app surface and a slot view to extend the host workbench or composer. This guide covers registration, Desktop windows, Cloud panels, and the lifecycle a view must handle.
Three surfaces
Section titled “Three surfaces”Window behavior in this table is specific to Desktop. The Cloud mapping follows below.
| Surface | Declared as | Where it renders |
|---|---|---|
| Docked page | contributes.pages[], presentation omitted or 'docked' |
The main content pane, inside the shell — sidebar, dock, and chrome all present. Can also be popped into extra windows |
| App page | contributes.pages[], presentation: 'app' |
Its own window, one per page id. No host chrome at all. Never rendered in the main window |
| Slot view | contributes.views[] |
A specific spot in WAMP’s UI — a dock tab, a chat-input strip |
Registering a page
Section titled “Registering a page”The page id in the manifest and the key in your views export must agree.
{ "contributes": { "pages": [ { "id": "notes", "title": "Notes", "icon": "NotebookPen", "context": "both" } ] }}export const views = { notes: NotesPage, // key === page id};That is the whole registration. The page’s surface is wired up for you: each
contributes.pages[] entry auto-registers a workspace.main view whose view
id and type are both the page id, resolved from views[page.id]. You do
not add a contributes.views[] entry for a page’s own surface — and if you
do, with slot workspace.main and the same id, it is silently ignored in
favor of the page’s own registration.
One convenience worth knowing: a single-page extension whose bundle has only a
default export is wired up from that default. As soon as you have two pages
you must key each one explicitly in views, because a default export cannot
say which page it is.
| Page field | Values | Default |
|---|---|---|
id |
string | required |
title |
string | required |
icon |
lucide-react name |
none |
context |
'project', 'both' |
'both' |
presentation |
'docked', 'app' |
'docked' |
placement |
'top', 'footer' |
'top' |
context says whether the page needs a project: 'project' pages are offered
only while a workspace is open, 'both' pages everywhere.
Docked pages
Section titled “Docked pages”A docked page renders in the main content pane with everything else still around it. Its sidebar row is reorderable by the user.
placement: 'footer' pins the row to the fixed group at the bottom of the
sidebar instead — where WAMP puts Docs. It has one side effect that is easy to
trip over: declaring a footer page means that extension’s other pages are
dropped from the sidebar, on the assumption that the footer row is the
extension’s front door. If you want several rows visible, do not use footer.
Your component is mounted inside a full-height host element, so a docked page should size itself to its container rather than to the viewport:
function NotesPage() { return ( <div className="h-full flex flex-col bg-background text-foreground"> <header className="flex items-center justify-between p-3 border-b border-border"> <h1 className="text-sm font-semibold">Notes</h1> </header> <div className="flex-1 overflow-y-auto p-4">{/* body */}</div> </div> );}The pane’s width is fluid — it shrinks when the user opens the dock and grows when they collapse the sidebar. Never assume a fixed width.
App pages own a window
Section titled “App pages own a window”presentation: 'app' is not “a docked page with the chrome hidden”. An app
page is a window:
- It opens in its own window. There is exactly one window per app page id: asking for it again focuses the existing window (restoring it first if it was minimized) rather than creating a second.
- The main window never renders it. If navigation lands on an app page in the main window, the host opens the app’s window and steps the main window back to what it was showing.
- Its sidebar entry lives in the Apps group, and
placementis ignored. - That window has no sidebar, no dock, and no host title bar. Your component is the entire contents.
Because you own the whole surface, you own the title bar — which is what
AppShell from @wamp/ui provides, matched to host styling, including the
inset the macOS traffic lights need:
import { AppShell } from '@wamp/ui';
function TasksApp() { return ( <AppShell> <AppShell.TitleBar> <AppShell.Title> <span className="text-sm font-semibold">Tasks</span> </AppShell.Title> </AppShell.TitleBar> <AppShell.Content className="overflow-y-auto"> {/* body */} </AppShell.Content> </AppShell> );}
export const views = { tasks: TasksApp };AppShell has exactly three subcomponents — TitleBar, Title, and Content.
There is no Footer, Sidebar, or Header.
On relaunch, app windows the user had open are reopened. Docked pages opened in extra windows are not.
Extra windows for a docked page
Section titled “Extra windows for a docked page”Any docked page can also be opened in its own window, and this is where the mental model matters: opening a page in a window copies it, it does not move it.
- The docked instance in the main window is untouched and keeps rendering.
- Each request mints a new window, so the same page can be live in three windows at once — that is the design, not a bug. Three chats in three windows is the motivating case.
- Closing one of those windows does nothing else. Nothing re-docks, nothing navigates, and the main window never notices.
- A detached window is a peer, not a child. It loads the same bundle and mounts the same component; it is not a screenshot of the docked one, and the two do not share React state. They share everything that lives outside React — typed data, storage, cloud records — because those are per-extension, not per-window.
Windows are identified by a window id, not by the page they host, precisely because several can host the same page. The default size for a programmatically opened window is 1200×800.
Views in slots
Section titled “Views in slots”A view is for contributing to a surface WAMP already owns. Declare the slot:
{ "contributes": { "views": [ { "id": "notes.dock", "slot": "session.dock", "title": "Notes", "icon": "NotebookPen" } ] }}export const views = { 'notes.dock': NotesDockPanel, // key === view id};Five slots exist in the vocabulary, and each has a Desktop host:
| Slot | What the user sees | Props your component receives |
|---|---|---|
workspace.main |
The main content pane. This is what a page uses | { paneId, type } |
session.dock |
A workbench view; the shell manages tabs, groups, splits, and instances | SessionDockContext — authority, session, instance, visibility, and resource tab |
chat.newSessionControls |
A compact control beside the local new-chat destination pill | { workspace, activeWorkspacePath, draftGeneration, setSubmissionPending } |
chat.composerStatus |
Status and the next action above the active chat composer | { chatId, visible, workspace, activeWorkspacePath, isStreaming, onInsert } |
chatInput.attachments |
A strip between the chat text area and its toolbar | { draftText, onInsert, onAttachmentRemoved } |
A dock view can mount more than once. instanceId distinguishes a rendered
instance from a hidden provider controller:
- A controller has no
instanceId. It publishes resource tabs and renders nothing. Hosts without a controller mount can register a provider from the bundle’sactivate(host)instead. - An instance has
instanceIdand renders its ownactiveTabId. A null tab means render the default surface, not an empty panel. Do not republish the provider’s tab catalog from each instance. visible: falsemeans keep state and idle. Moving, splitting, or hiding a view does not imply disconnecting its resource.restoredTabIdsnames saved references. Extension providers should restore those references without implicitly starting processes or connections.
The core Desktop Terminal has explicit startup restoration behavior: it starts fresh local shells for saved terminal tabs whose processes no longer exist. Previous process output is not reconstructed. This is specific to the core Terminal; a restored tab id is not a general instruction to recreate resources.
Use workspace.authority before accessing files. engine carries a directory
reachable by this host’s engine. cloud means Desktop is brokering a Cloud
Session and the sandbox root is private; do not treat it as a local path. The
Cloud browser uses engine, since its attached engine owns the sandbox.
A minimal dock component that supports engine workspaces:
import type { SlotProps } from '@wamp/extension-sdk';
function WorkspaceInfo(props: SlotProps<'session.dock'>) { if (props.instanceId === undefined) return null; const { workspace } = props; if (workspace.authority === 'cloud') { return <p className="p-4">This view requires direct engine workspace access.</p>; } if (!workspace.context) return <p className="p-4">Choose a project.</p>; if (!workspace.hydrated) return <p className="p-4">Loading workspace…</p>; return ( <div className="h-full p-4 bg-background text-foreground"> <p>{workspace.context.path}</p> <p>{props.sessionId ? 'Session workspace' : 'New session workspace'}</p> </div> );}
export const views = { 'notes.dock': WorkspaceInfo };The manifest above supplies the matching view declaration. See the slot reference for every prop.
Cloud web panels
Section titled “Cloud web panels”The same renderer bundle can run in a Cloud workspace when installed in that session’s engine:
| Declaration | Cloud web result |
|---|---|
requiresElectron: true |
UI omitted, including when a server half exists |
Page with context: 'project' or 'both' |
Workspace panel; presentation: 'app' does not create a native window |
session.dock view |
Workspace panel with a rendered instanceId and engine authority |
| Other view slots | No matching surface |
Bundles and their CSS load on first open. Renderer activate(host) runs then
and can publish dock tabs and resource openers; a workbench catalog request can
also activate dock providers. The Cloud host has no chat or tools
registration namespaces. Check optional host capabilities before using them,
and return a disposable for registrations you own. Details:
renderer activation host.
Compose portable panels from the shared UI primitives. Desktop composites such
as ModelPicker are not available in this host; see
Interface kit.
Navigating between surfaces
Section titled “Navigating between surfaces”From the window:
import { pluginAPI } from '@wamp/plugin-api';
pluginAPI.navigation.go('notes', { noteId: id }); // navigate in this realmconst { noteId } = pluginAPI.navigation.params(); // read what go() passedpluginAPI.navigation.back(); // pop historygo routes through the Desktop host’s navigation store — the same path the sidebar
takes — so history and the back affordance come for free. back() restores the
previous page and its params. Behavior at the
edges, all verified:
- An unregistered page id logs a warning and does nothing. It is not an error
and not a crash; if a
gocall appears to be ignored, check the id against the page you actually registered. back()on empty history is a no-op. History is capped at ten entries.params()is session-scoped and not persisted — a relaunch loses them, so never make a page’s correctness depend on params being present.- Navigating in the main window does not change what a detached window shows. A detached window renders the page it was opened with, permanently.
pluginAPI.ui.showExtension(id) surfaces another extension’s primary page,
picking the right mechanism for you — an app page opens its window, a docked
page navigates.
Where your styles apply
Section titled “Where your styles apply”This is the part that wastes the most time, so here are the rules that hold.
Rules on body and html do not work. Your component is mounted inside
host elements that paint their own background from the theme token, so a rule
on the document root compiles cleanly and is occluded. A “successful” style
edit with no visible change is almost always this.
Style your own root element instead. Everything your component renders is
yours. Give your outermost element the classes you want, and for an app page
pass className to AppShell — its root accepts every standard div
attribute. To retint a whole app surface, override the design token on your own
root rather than on the document, and set it inline so nothing has to compile
it:
<AppShell style={{ '--background': '220 20% 8%' } as React.CSSProperties}>Token values are HSL channels without the hsl() wrapper — that is the format
the theme uses, and every surface inside your app that reads the token follows.
Use theme tokens, not literal colors. bg-background, text-foreground,
bg-card, bg-muted, text-muted-foreground, border-border, bg-primary,
text-primary-foreground, bg-secondary, bg-accent. A hardcoded
bg-slate-900 or #1e191a looks right in one theme and wrong in the other,
and the user can switch at any time.
Host-built apps receive their own generated utility stylesheet. The host
scans the extension’s TS/JS source, applies the WAMP Tailwind preset, and writes
dist/plugin.css on each build. Literal arbitrary classes such as w-[137px]
work. Dynamically assembled class names are not discoverable; use complete
strings or inline styles. A utility-generation failure appears as a build
warning and can leave only the host’s global classes available. Extensions that
own their build must also produce their needed CSS.
Your CSS file is not scoped. A stylesheet imported by your bundle is
injected into the window’s document head as-is. It is not wrapped, prefixed, or
sandboxed, so a bare button { … } rule reaches WAMP’s own buttons and every
other extension’s. Scope your selectors yourself — a class prefix or a root
class you control. Tailwind utility classes in your JSX avoid the problem
entirely, which is why they are the recommended path.
Assume a fluid box. Host wrappers are full-height, the pane width changes
with the user’s layout, and a detached window can be resized to anything. Use
h-full and flexbox; do not use viewport units for layout.
When nothing appears
Section titled “When nothing appears”| Symptom | Cause |
|---|---|
| Sidebar row exists, pane is blank | The views key does not match the page id exactly |
| Extension flips to faulted with a mount error | No component registered for the page within five seconds of mount — usually the same key mismatch, or a bundle that failed to build |
| Extension will not load, error names a page id | Another extension or WAMP itself already registered that id |
| A contributed view never appears | Check the loader warning for a retired slot or a missing renderer registration |
navigation.go seems ignored |
The page id is not registered; check the console warning |
| A style change compiles but nothing looks different | The rule is on body, html, or an invented root id. Move it to your own root element |
- Interface kit — the components and hooks available in a page.
- The manifest — where
pagesandviewssit among the other contributions. - The plugin API —
navigation,ui, and everything else the window can call.