window-panel-authoring
Recipe for creating or changing a floating window panel. Use when adding a window, editing a window component, adding persistence, onCollectData, or ephemeral mode, wiring the Tools grid, or a window
SKILL.md.claude/skills/window-panel-authoring/SKILL.md
GitHubWindow Panel Authoring
<!-- rules:stamp -->Rules from
common-docs/systems/platform/ui-shell/FEATURE.md— edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).
- Delete means archive, everywhere. A delete that does not archive is a defect.
- A menu regroup ships as a side-by-side demo page with a what-goes-where list, never as a direct change.
- Every surface mounts the canonical menu. Use
NonEditableContextMenuorEditableContextMenufromfeatures/context-menu-v3/. Never use a bespokeDropdownMenuor a rawcontextmenuhandler, and never keep both. - A menu without a source is a defect. A menu with no
contentSource,entityorsurfaceNameopens with every verb dark. Fix these before unwired surfaces. - Lists built on
EntityListPageinherit the row menu throughItemContextMenu. They need their own wiring only for surface-specific sections. - A window or overlay mounts its own menu. It never delegates to the page under it.
- Reuse the domain's action registry. Never create a second one.
- Secret-bearing surfaces follow
VaultContextMenu. PassgetApplicationScope, forceselection: "", put names and field keys in the content (never values or notes), and pass noentity. - Done means the menu opens clean. The console shows no
INERT MENUorVALUE MAPPING GAPscream. - Chat is a universal agent host. Never bind a page-agent roster to it.
- Anything with an id shows it through the one canonical id component (display plus one-click copy).
- Flagship and first-run screens get a bespoke elevated stage, not the 28px app-chrome controls.
- Before a link goes to Arman, every screen on its path passes the Steve Jobs bar (walked and fixed by an Opus reviewer).
- Admin tables filter, search and sort on the server, and people are picked by name or email, never by id.
- Wasted vertical space is the worst sin; every status shown is backed by real state.
- A new output style or menu layout is a NEW function used only by the new path; never thin a shared builder.
- A page fetches only what it shows, each thing once; every removed fetch ships its on-engagement load.
- Floating things never move the page. Pills, assists, badges, alarms, notices, toasts and the
chat dock float over it; when one would cover something, it moves itself (lifts, rests above
the bar, yields). No page pads, shrinks or re-centres for one. Only the empty scroll room after a
page's last line may grow. Guard:
pnpm check:overlays-never-move-page(matrx-frontend). - Every feature has the same pages (front door,
/all,/new, record, sub-feature) and derived breadcrumbs: feature pages.<!-- rules:stamp-end -->
A window is an overlay whose render module returns a WindowPanel. The engine (WindowPanel, window manager, persistence, links) is the package @ai-matrx/summon; a defect in that engine is fixed in ../aidream/apps/shared/summon in the same session, never worked around in the window. Slots, drag, tray and phone presentation: window-panels skill. Definition, render module, registry and opener rules: overlay-system skill (read it first, the file list below assumes it).
If the window is an agent-aware surface, invoke surface-authoring and its overlay-surface reference. A window resident never inherits ambient surface values; an outside helper may receive an approved snapshot under its documented integration contract.
Files you touch
| File | When |
|---|---|
features/window-panels/windows/[<feature>/]MyFeatureWindow.tsx |
The window component. |
features/overlays/definitions/<id>.overlay.tsx |
Definition: id, label, instance, and the window block (windowId, mobile, preservation, restoreDefaults, ephemeral, deprecated), optional link. Holds the opener (useOpenX, XController). |
features/overlays/definitions/<id>.render.tsx |
Lazy chunk: narrows data, wires props by name (no spread). |
definitions/registry.ts |
The one hand line, d.<id>Overlay,; then pnpm gen:overlays (overlay-system "Add an overlay"). |
features/window-panels/tools-grid/toolsGridTiles.ts |
Optional Tools grid tile. |
definitions/<id>.link.ts |
Optional ?panels= hydrator. |
Steps
1. The window component
"use client";
import { useState } from "react";
import { WindowPanel } from "@ai-matrx/summon/window";
import { MyFeatureBody } from "@/features/my-feature/components/MyFeatureBody";
interface MyFeatureWindowProps {
onClose: () => void;
initialSelectedId?: string | null; // seeded from overlay data by the render module
}
export default function MyFeatureWindow({ onClose, initialSelectedId }: MyFeatureWindowProps) {
const [selectedId, setSelectedId] = useState<string | null>(initialSelectedId ?? null);
const collectData = (): Record<string, unknown> => ({ selectedId });
return (
<WindowPanel
title="My Feature"
id="my-feature-window" // stable, never random: geometry is keyed by it
overlayId="myFeatureWindow" // identical to the definition id; this is the close binding
minWidth={380} minHeight={280} width={560} height={440} position="center"
onClose={onClose}
onCollectData={collectData}
bodyClassName="flex min-h-0 flex-1 flex-col overflow-hidden p-0"
>
<MyFeatureBody selectedId={selectedId} onSelect={setSelectedId} />
</WindowPanel>
);
}
Rules:
- No
isOpenprop and noif (!isOpen) return null: a closed overlay is never rendered, so the render module only mounts an open window. idis stable.overlayIdequals the definitionid. Close binding is exactly one ofoverlayId,boundToOverlay,onClose(type-enforced).instance: "multi"windows are keyed by the render module'skey={instanceId}; pass the instance id into the component so each instance gets distinct state (windowInstanceIdpattern inNotesWindow).onCollectDatareturns all keys the window preserves, read from live state; the React Compiler memoizes it, so no manualuseCallback.- Body is content only; chrome is slots (
window-panelsskill). A window wraps the canonical component (FEATURE.md law), never a hand-rolled copy. - A window component that other code props-extends uses
Omit<WindowPanelBaseProps, ...>from@ai-matrx/summon/window.
2. The definition and render module
Create <id>.overlay.tsx with a window block and <id>.render.tsx that renders MyFeatureWindow with onClose={() => close()} and initialSelectedId narrowed from data; add d.myFeatureWindowOverlay, to registry.ts and run pnpm gen:overlays (overlay-system). Do not import MyFeatureWindow from anywhere except its render module (and its own tests).
window: {
windowId: "my-feature-window",
mobile: "drawer", // fullscreen | drawer | card | hidden
preservation: { dataKeys: ["selectedId"] }, // opt in to refresh restore (default-deny)
restoreDefaults: { selectedId: null },
// ephemeral: true, // never saves anything
},
3. (Optional) Tools grid tile
Tiles are declarative in tools-grid/toolsGridTiles.ts; ToolsGrid.tsx and activateToolsGridTile.ts read the definition via appOverlayDefinition(overlayId) (instance strategy comes from instance). Never add a tile in features/window-panels/components/SidebarWindowToggle.tsx.
{ id: "tile.my-feature", label: "My Feature", icon: MyFeatureIcon, category: "general",
overlayId: "myFeatureWindow" /* seedData: (ctx) => ({ selectedId: null }) */ },
4. (Optional) ?panels= address
Three edits, all required, or the link opens an empty frame: (a) link: { key: "my_feature", hydrate } on the definition (heavy hydrator in myFeatureWindow.link.ts, loaded lazily; the hydrator must call open({ data })), (b) urlSyncKey="my_feature" on the WindowPanel when it differs from the default, (c) urlSyncId={subjectId} on the WindowPanel. Two windows may not claim one key (the package throws on a duplicate link key). features/window-panels/__tests__/everyWindowHasAnAddress.test.ts refuses a window with a durable subject (...Id, Slug, Token data) and no address, unless listed in registry/window-address-baseline.json (which only shrinks).
Persistence contract (default-deny)
A window survives a refresh only when its window.preservation.dataKeys lists what to keep and it is not ephemeral. Geometry (rect, windowState, sidebarOpen, zIndex) is captured by the package; the window supplies content via onCollectData, filtered to dataKeys, JSON only, at most maxDataBytes (32 KiB default). Saves are debounced 250 ms after any window-manager or state change and flushed on pagehide; close removes the window from the saved workspace. Storage is users.user_surface_state (details: window-panels persistence branch). Callback-group windows (callbackGroupId in data) are ephemeral: true and listed addressless with the reason.
Ephemeral windows
ephemeral: true on the definition's window block for debug panels, one-shot dialogs and callback-group windows. It governs restore only, never reachability: an ephemeral window can and should still have a link.
Updating a window
- Change
restoreDefaultsandpreservation.dataKeystogether (a new key missing fromdataKeysis silently dropped from storage). - Update
onCollectDatato return all current keys. - Props changed: update the render module's by-name wiring and the opener's
Open…Optionsin the.overlayfile. Then runpnpm gen:overlaysif label, instance or window identity moved (pnpm check:overlaysfails until you do).
Sidebar layout
<div className="flex flex-col min-h-0 h-full">
<div className="px-2 py-1 border-b type-title shrink-0">Header</div>
<div className="flex-1 min-h-0 p-1.5 space-y-1">{items.map((item) => <Item key={item.id} />)}</div>
</div>
h-full min-h-0 flex flex-col on the root; no overflow-y-auto on the root (WindowPanel handles it); no shrink-0 on the root.
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
overlayId in the component differs from the definition id |
Type error, or the window reads another overlay's metadata | Make them identical |
Definition missing from registry.ts |
Window never renders; pnpm check:overlays names it |
Add d.<id>Overlay, to APP_OVERLAYS |
| Generated index stale | pnpm check:overlays fails |
Run pnpm gen:overlays and commit the result |
Definition file lacks "use client" |
Production build fails collecting page data for a server page | Add it |
No window.preservation |
Window reopens empty after refresh; Save does nothing | Declare dataKeys |
| Multi-instance window without a per-instance id | Windows share state, or restore finds no instance | Use key={instanceId} and pass instanceId through |
Random id on WindowPanel |
Geometry lost on re-render | Stable string |
ephemeral: true but the window needs restore |
Window does not reopen | Remove ephemeral, declare preservation |
Link edits skip urlSyncId |
Every instance writes the same token | Pass the subject id |
| Fixing package behavior in the window | Diverges from every other host | Fix ../aidream/apps/shared/summon |
Resources
- Package guides:
../aidream/apps/shared/summon/README.md(WindowPanel, links, persistence, handles) andFEATURE.md(invariants). - App window facts and laws:
features/window-panels/FEATURE.md. Definitions:features/overlays/definitions/. Host:features/overlays/host/. - Openers, registry, mounting:
overlay-systemskill. Slots, drag, tray:window-panelsskill. Removal:removing.mdin this skill.
Removing a window panel
Deleting a panel means removing it from every place its registration spreads, then proving zero residue: no shims, aliases, forwarding openers or tombstone comments. The full no-trace recipe (keystone, ten-row checklist, repoint-before-delete greps, shared infrastructure not to delete, verification) is in removing.md. Read it in full before any deletion.
Relocated
| Old skill and trigger | Now |
|---|---|
remove-window-panel: deleting, killing or retiring a window/overlay |
this skill, removing.md |
remove-window-panel: consolidating X into Y and deleting X |
this skill, removing.md |
remove-window-panel: "kill this panel", "remove the X window" |
this skill, removing.md |
remove-window-panel: NOT for adding or opening an overlay |
overlay-system skill |