window-panels
The WindowPanel component and Window Manager. Use when a window won't drag/resize/minimize, the tray is missing, window state doesn't persist, rendering `<WindowPanel>` on a page, or editing features/
SKILL.md.claude/skills/window-panels/SKILL.md
GitHubWindow Panels: WindowPanel from @ai-matrx/summon/window
<!-- 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 -->
WindowPanel, the window manager (windowManager slice), the tray, pop-out, the three phone presentations and window persistence are the npm package @ai-matrx/summon (source ../aidream/apps/shared/summon/src/window, src/core, src/persistence; guides in the package README.md and FEATURE.md). This repo no longer contains that engine. What stays here: the window components (features/window-panels/windows/**), the Tools grid, tray previews, the Detail primitive host and the app's wiring. Deep reference for what stays: features/window-panels/FEATURE.md.
A package bug is fixed in the package, in the same session (edit ../aidream/apps/shared/summon, run its gates, publish, take latest). Never patch around it in a window component and never copy engine code back.
Opening, registering and closing an overlay is the overlay-system skill. This skill is what a window is once something renders it.
Branches: read only the one your task reaches
- Drag, resize, traffic lights, snap, full screen, pop-out → drag-resize-popout.md
- Tray, minimized cards,
windowManagerstate, reveal, silent-render guard → tray-and-window-manager.md - Save/restore,
window.preservation,onCollectData,ephemeral,?panels=links → persistence.md - Phone presentation (
window.mobile) → mobile-presentation.md - Creating or changing a window →
window-panel-authoringskill; deleting one →window-panel-authoringskill (removing.md).
Mental model
WindowPanelis a leaf component. Rendered by a*.render.tsx(the overlay path) or dropped on a page with its ownonClose. Either way, on mount it joins the window manager (z-order, minimize-all, tray, arrange, persistence, pop-out) because the manager lives in the app's single Redux store.- No registry renders windows.
SummonHostrenders definitions; nothing iterates a list to mount a window. A window's static facts (id, mobile presentation, ephemeral, preservation, link, deprecated) are thewindow/linkblocks on its definition infeatures/overlays/definitions/<id>.overlay.ts(x).windowRegistryMetadata.ts,windowRegistryTypes.ts,initUrlHydration.ts,WindowPersistenceManager,WindowTrayandlazy-bundle-guardare deleted. - The close binding is a type. Exactly one of
overlayId(close that overlay),boundToOverlay(close the enclosingSummonHostinstance) oronClose(page-local). None compiles to an error; do not relax it. Window components in this repo extendOmit<WindowPanelBaseProps, ...>(exported from@ai-matrx/summon/window) and neverOmit<WindowPanelProps, ...>. - Providers at the root, not per window.
AppSummonProvidermountsSummonProvider,WindowsProvider(selection surfaceAppWindowSelectionSurface, Escape yields to[data-organization-gate], [data-overlay-escape-yield]) andWindowTraySyncfrom@ai-matrx/summon/window-host. A page-localWindowPaneltherefore gets the same options as an overlay window. Do not mount another.
The SLOTS contract: the heart of building a window
The body is content only. Header, footer, sidebar and secondary panel are WindowPanel prop slots, never body JSX. Reference consumers: windows/notes/NotesWindow.tsx, windows/FeedbackWindow.tsx. Canonical body class: bodyClassName="flex min-h-0 flex-1 flex-col overflow-hidden p-0".
| Slot | Props | Contract |
|---|---|---|
| Header | title / titleNode (wins), actionsLeft, actionsRight |
Title is centered across the header; keep action clusters compact or they overlap it. Traffic lights and the sidebar toggle render automatically. actions is deprecated (= actionsRight). |
| Footer | footer or footerLeft / footerCenter / footerRight; footerVariant "bar" (default) / "rich" |
"bar" applies compact metadata-bar styling that crushes a composer. Pass footerVariant="rich" for a multi-row input (SmartAgentInput); reference windows/multi-file-smart-code-editor. |
| Sidebar | sidebar, sidebarDefaultSize, sidebarMinSize, defaultSidebarOpen, sidebarClassName |
Leave sidebarExpandsWindow false: it resizes the window on every toggle and fights drag/snap. |
| Secondary panel | secondaryPanel, secondaryPanelOpen, secondaryPanelDefaultSize, secondaryPanelMinSize |
History / inspector pane that belongs to the window. Desktop only; handle phones yourself. |
Composition-root pattern
A window component hoists shared state into one use<Feature> hook at its root (slots are siblings of the body, so state buried in the body cannot feed them), each unit takes an id and reads Redux, and the root maps units onto slots:
"use client";
import { WindowPanel, type WindowPanelBaseProps } from "@ai-matrx/summon/window";
export function MyFeatureWindow({ id = "my-feature-window", onClose }: Props) {
const state = useMyFeature();
return (
<WindowPanel id={id} overlayId="myFeatureWindow" title="My Feature" width={640} height={480}
minWidth={380} minHeight={280} bodyClassName="flex min-h-0 flex-1 flex-col overflow-hidden p-0"
actionsRight={<MyFeatureControls id={id} />} sidebar={<MyFeatureSidebar id={id} />}
footer={<MyFeatureFooter state={state} />} onClose={onClose}>
<MyFeatureBody id={id} />
</WindowPanel>
);
}
Units shared with plain pages drop INTO a slot as content and import no WindowPanel, so a route that renders them never pulls the window stack into its bundle.
Bundle invariant
@ai-matrx/summon/window (the whole WindowPanel module) and every windows/**/*Window.tsx stay behind a lazy boundary: reached only from a *.render.tsx (loaded by its definition) or a dynamic(..., { ssr: false }). The app root and boot code import only the light entries: @ai-matrx/summon, /react, /next, /window-host, /url, /persistence. The old [WINDOW-PANELS BUNDLE LEAK] runtime guard is gone; the guards now are the ESLint windowPanelsImportRestriction, scripts/check-bundle-size.ts (+2 KB per route) and app/__tests__/nothing-loads-until-engaged.test.tsx. Read the code-splitting skill before importing a window-panel file. Never nest ssr:false boundaries down one render path.
Common pitfalls
- Hand-rolled chrome in the body. Use the slot.
- State buried in the body that a footer or sidebar needs. Hoist it.
- Static-importing a
*Window.tsxor@ai-matrx/summon/windowfrom a route, layout or provider. Bundle leak. - No close binding. Intentional compile error.
- Mounting a second
WindowsProviderorWindowTraySync. The root already has one. - A random
idonWindowPanel. Geometry is keyed by it; use a stable string. - "Registry entry to render a window". Gone. Add a definition (overlay-system).
- Fixing a package behavior inside the app. Fix
../aidream/apps/shared/summon.
Key files
| Path | Role |
|---|---|
@ai-matrx/summon/window (package src/window/WindowPanel.tsx, useWindowPanel.ts) |
The frame, body portal, presentations. |
features/overlays/host/AppSummonProvider.tsx, AppWindowSelectionSurface.tsx |
Root providers and the right-click selection surface. |
features/overlays/definitions/trayPreviews.ts, features/window-panels/registry/tray-previews.tsx |
App tray previews registered with registerTrayPreview. |
features/window-panels/windows/** |
The window components. |
features/window-panels/tools-grid/ |
Tools grid tiles (toolsGridTiles.ts, ToolsGrid.tsx, activateToolsGridTile.ts). |
features/window-panels/detail/ |
Detail primitive host (DetailHost.tsx, shells/, canvas/). |
features/window-panels/__tests__/everyWindowHasAnAddress.test.ts + registry/window-address-baseline.json |
The address census. |
Checklist
- Body is content only; chrome is slots;
bodyClassNameis the canonical class. - Shared state hoisted into a
use<Feature>hook at the window root. - Close binding declared;
sidebarExpandsWindowleftfalse. - No static import of
@ai-matrx/summon/windowor a*Window.tsxoutside a lazy boundary. -
onCollectData, if set, returns stable keys (the React Compiler memoizes it; no manualuseCallback). -
pnpm type-checkclean forfeatures/window-panels/**.