本文へ移動
cccskills
無料GitHub で公開

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 fails to restore after reload. NOT for WindowPanel drag/resize/tray internals (use window-panels).

インストール方法を見る

含まれるファイル(1)

  • SKILL.md13.2 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Window Panel Authoring

Quick Orientation

Every floating window is a WindowPanel shell rendered by ONE hand-maintained controller, features/overlays/OverlayController.tsx. No registry renders windows — windowRegistry.ts is deleted; any doc that says "add a registry entry to render a window" is stale. Persistence is a local-first, tab-scoped workspace (localStorage mirror + IndexedDB, features/window-panels/persistence/, coordinated by WindowPersistenceManager); nothing window-related is stored server-side (the window_sessions table was dropped 2026-08-12). Slots / drag / tray internals → window-panels skill. Openers / controller / catalogue rules → overlay-system skill.

If this window is an agent-aware surface, invoke surface-authoring and its overlay-surface reference for the manifest, scoped registration receipt, and nested runtime provider. A window resident never inherits ambient surface values; an outside helper may receive an approved task-appropriate snapshot under its documented integration contract.

Key files:

FileWhen you touch it
features/overlays/catalogue.tsEvery new overlay: its key IS the id (OverlayId = keyof typeof OVERLAY_CATALOGUE) + { label, instanceMode, isWindow }
features/window-panels/registry/windowRegistryMetadata.tsEvery new window; any defaultData / preservation change
features/window-panels/windows/[<feature>/]MyFeatureWindow.tsxNew window component
features/overlays/OverlayController.tsxMount: lazyOverlay import + selectors + gated block
features/overlays/openers/<overlayId>.tsxTyped useOpenX hook + XController
features/window-panels/tools-grid/toolsGridTiles.tsOptional Tools-grid tile
features/window-panels/url-sync/initUrlHydration.tsOptional ?panels= deep-link

Step-by-Step: Creating a New Window

A window is registered at every boundary, by hand (no codegen): catalogue entry (the id) → metadata → component → controller → opener, then the optional tile and hydrator.

1. Add the catalogue entry — it defines the id

In features/overlays/catalogue.ts: myFeatureWindow: { label: "My Feature", instanceMode: "singleton", isWindow: true },. The key IS the overlay id — the OverlayId union is keyof typeof OVERLAY_CATALOGUE, so every opener and openOverlay call narrows at compile time. There is no second id list.

2. Declare static metadata in windowRegistryMetadata.ts

// features/window-panels/registry/windowRegistryMetadata.ts
{
  slug: "my-feature-window",       // kebab-case; URL / diagnostic identity
  overlayId: "myFeatureWindow",    // camelCase; the exact catalogue key (step 1)
  kind: "window",
  label: "My Feature",
  defaultData: {
    selectedId: null,
    search: "",
  },
  mobilePresentation: "drawer",    // required for kind "window": fullscreen | drawer | card | hidden
  // ephemeral: true,                            // never enters the local workspace
  // preservation: { dataKeys: ["selectedId"] }, // opt in to refresh restore (default-deny)
  // instanceMode: "multi",
  // urlSync: { key: "my_feature" },             // needs a hydrator (step 7)
},

Rules:

  • slug and overlayId are each unique across the registry.
  • overlayId is the key every opener and openOverlay({ overlayId }) uses.
  • defaultData documents every key onCollectData() will ever return.
  • Component-free — never import component code here; this file is safe in boot code only because it has none.

3. Create windows/[<feature>/]MyFeatureWindow.tsx

"use client";
import { useState } from "react";
import { WindowPanel } from "@/features/window-panels/WindowPanel";
import { MyFeatureBody } from "@/features/my-feature/components/MyFeatureBody";

interface MyFeatureWindowProps {
  isOpen: boolean;
  onClose: () => void;
  initialSelectedId?: string | null;   // from overlay data, wired by the controller block
}

export default function MyFeatureWindow({ isOpen, onClose, initialSelectedId }: MyFeatureWindowProps) {
  if (!isOpen) return null;
  return <MyFeatureWindowInner onClose={onClose} initialSelectedId={initialSelectedId} />;
}

function MyFeatureWindowInner({ onClose, initialSelectedId }: Omit<MyFeatureWindowProps, "isOpen">) {
  const [selectedId, setSelectedId] = useState<string | null>(initialSelectedId ?? null);

  const collectData = (): Record<string, unknown> => ({ selectedId, search: "" });

  return (
    <WindowPanel
      title="My Feature"
      id="my-feature-window"          // stable id — key in windowManagerSlice
      minWidth={380}
      minHeight={280}
      width={560}
      height={440}
      position="center"
      onClose={onClose}
      overlayId="myFeatureWindow"     // identical to the catalogue key + metadata
      onCollectData={collectData}
      bodyClassName="flex min-h-0 flex-1 flex-col overflow-hidden p-0"
    >
      <MyFeatureBody selectedId={selectedId} onSelect={setSelectedId} />
    </WindowPanel>
  );
}

Critical rules:

  • Outer guard if (!isOpen) return null — inner component does the real work.
  • id must be stable (not a random UUID; omitted → useId()) — it is the Redux key for geometry.
  • overlayId must exactly match the catalogue key and the metadata overlayId. It is also the close binding: overlayId or onClose is required (type-enforced).
  • instanceMode: "multi" → also pass overlayInstanceId, or close and restore cannot find the exact instance.
  • onCollectData must return all keys that appear in defaultData, read from live state. No manual useCallback — the React Compiler memoizes it.
  • Body is content only; header / footer / sidebar are WindowPanel slots → window-panels skill.

4. Mount in features/overlays/OverlayController.tsx

Three hand edits. Props are wired by name — a {...spread} is an ESLint error.

// 1 — lazy import near the other window imports
const MyFeatureWindow = lazyOverlay(
  () => import("@/features/window-panels/windows/my-feature/MyFeatureWindow"),
  { ssr: false },
);
// 2 — one entry in `isOpenById`, one in `dataById`
myFeatureWindow: useAppSelector((s) => selectIsOverlayOpen(s, "myFeatureWindow")),
// …
myFeatureWindow: useAppSelector((s) => selectOverlayData(s, "myFeatureWindow")),
{/* 3 — gated render block; NEVER render ungated */}
{/* myFeatureWindow */}
{(() => {
  const isOpen = isOpenById.myFeatureWindow;
  const data = dataById.myFeatureWindow as Record<string, unknown> | null | undefined;
  if (!isOpen) return null;
  return (
    <MyFeatureWindow
      isOpen
      onClose={() => dispatch(closeOverlay({ overlayId: "myFeatureWindow" }))}
      initialSelectedId={typeof data?.selectedId === "string" ? data.selectedId : null}
    />
  );
})()}

5. Add the opener

  • features/overlays/openers/myFeatureWindow.tsx — copy an existing opener (feedbackDialog.tsx): useOpenMyFeatureWindow() + <MyFeatureWindowController />. New code opens the window through the opener, never a raw dispatch(openOverlay(...)).

6. (Optional) Tools grid — tools-grid/toolsGridTiles.ts

Tiles are declarative; ToolsGrid.tsx reads them and opens the overlay. Never add a tile inside components/SidebarWindowToggle.tsx.

{
  id: "tile.my-feature",
  label: "My Feature",
  icon: MyFeatureIcon,             // Lucide icon component
  category: "general",
  overlayId: "myFeatureWindow",
  // seedData: (ctx) => ({ selectedId: null }),
},

7. (Optional) URL hydrator — url-sync/initUrlHydration.ts

Only needed if you want ?panels=my_feature to reopen the window. The hydrator and the metadata urlSync.key (step 2) come as a pair: a hydrator without the key opens from the URL but never writes back.

registerPanelHydrator("my_feature", (dispatch, id) => {
  dispatch(openOverlay({ overlayId: "myFeatureWindow" }));   // multi-instance: add instanceId: id
});

Modifying an Existing Window

When you add/remove keys from a window's content state:

  1. Update defaultData in windowRegistryMetadata.ts — add new keys with null/empty defaults; remove stale ones. A preserved window: update preservation.dataKeys too, or the new key is silently dropped from storage.
  2. Update onCollectData in the window component to return all current keys.
  3. Update the inner component to read new keys from initialXxx props.
  4. Props changed → update the gated block in OverlayController.tsx AND the opener's Open…Options interface.

Persistence Contract

Default-deny. A window survives a refresh only when its metadata declares preservation.dataKeys and is not ephemeral (multi-instance: plus overlayInstanceId). Every other window simply closes on reload. The opted-in list and full lifecycle: features/window-panels/FEATURE.md § Persistence.

For a preserved window, WindowPanel captures chrome automatically when overlayId is set. The child only supplies content via onCollectData.

What panelState captures (automatic):

  • rect: { x, y, width, height }
  • windowState: "windowed" | "maximized" | "minimized"
  • sidebarOpen: boolean
  • zIndex: number

What data captures (child's responsibility):

  • Whatever onCollectData() returns, merged over the launch data and filtered to preservation.dataKeys — JSON-serializable, ≤ maxDataBytes (32 KiB default). Functions, callbacks, and blobs never reach storage.

Save triggers:

  • Any windowManagerSlice change (move, resize, minimize, sidebar toggle) → workspace save, debounced 250 ms.
  • onCollectData or launch data changes → semantic data re-staged automatically; minimize collects and saves too.
  • Explicit: user clicks "Save window state" in the green traffic-light dropdown.
  • pagehide → synchronous localStorage flush.

On close: closeOverlay (and toggle-close / close-all) tombstones the session and the close middleware flushes localStorage synchronously, so the window doesn't reopen next load — even when closed mid-hydration.

On page load: WindowPersistenceManager reads the tab's identity-scoped workspace (localStorage mirror, then IndexedDB), clamps rects to the viewport, stages each session, and dispatches openOverlay; registerWindow consumes the staged state when the window mounts.


Ephemeral Windows

Use ephemeral: true in the metadata for windows that must NOT persist:

  • Debug/dev tool windows
  • One-shot dialogs (file upload, confirmation flows)
  • Windows whose state cannot be serialized (file blobs, live streams, callback-group windows)

Ephemeral windows never enter the workspace and never restore on reload. The "Save window state" item still renders for every overlayId window — without preservation it does nothing.


Sidebar Layout Rules

// Correct sidebar root
<div className="flex flex-col min-h-0 h-full">
  <div className="px-2 py-1 border-b text-xs font-medium 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>
  • DO use h-full min-h-0 flex flex-col on root.
  • DO NOT set overflow-y-auto on root — WindowPanel handles it.
  • DO NOT use shrink-0 on the root element.

Common Mistakes

MistakeSymptomFix
overlayId in component ≠ catalogue key / metadata overlayIdType error, or the window reads another overlay's mobile / restore metadataMake them identical
onCollectData returns stale valuesSaved data has old valuesRead live state in the collector, never a cached copy
Missing gated block in OverlayController.tsxWindow never rendersAdd lazyOverlay import + selectors + gated block
Missing metadata entry in windowRegistryMetadata.tsNo mobile presentation, URL sync, or restoreAdd the entry (kind, mobilePresentation)
No preservation on the metadata entryWindow doesn't reopen after refresh; "Save window state" does nothingDeclare preservation.dataKeys
Multi-instance window without overlayInstanceIdClose logs Cannot close multi-instance…; no restorePass overlayInstanceId
Random id passed to WindowPanelGeometry lost on re-renderUse stable string like "my-feature-window"
ephemeral: true but window needs restoreWindow doesn't reopenRemove ephemeral, declare preservation

Additional Resources

  • Deep reference (registry fields, persistence lifecycle, URL sync, Tools grid): features/window-panels/FEATURE.md
  • Static metadata: features/window-panels/registry/windowRegistryMetadata.ts (types: windowRegistryTypes.ts)
  • Local workspace store: features/window-panels/persistence/localWindowSessionStore.ts (+ windowSessionSerialization.ts, windowPersistenceCloseMiddleware.ts)
  • Persistence coordinator: features/window-panels/WindowPersistenceManager.tsx → WindowPersistenceCore.tsx
  • Openers, controller, catalogue: overlay-system skill · slots, drag, tray: window-panels skill

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Admin debug system wiring for the floating AdminIndicator's live debug data and Copy Full Context. Use when adding debug visibility to a route or feature, wiring useDebugContext or the debug panel, capturing console errors, or enabling the copy-context workflow for a page.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Compact two-icon Copy / Copy-for-AI controls (components/agent-copy). Use when adding copy buttons to a row, card, list, or record; merging duplicate Copy/JSON/AI controls; continuing the copy rollout; or writing a Copy-for-AI payload. NOT for markdown content actions (use rich-document-actions).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Disclosing a surface's existing fixed AI jobs in the shell's top Agents menu. Use when a page, panel, overlay, or window already runs a mandate behind a button, assist, automatic action, or mode; when the agent-disclosure guard names a file; or during a surface check.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

The watch-fix-rerun method for making an existing platform agent or automated process efficient and correct: baseline its ledger, run one unit yourself, fix the class behind every wasted call, rerun, record. Use when asked to improve, optimize, watch, or 'make efficient' an agent, a sandbox session, a sync job, or any recurring automated process.

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

Redux state for agent execution and firing agent shortcuts. Use when editing features/agents/redux/, building agent UI, creating a conversation, touching assembleRequest or NDJSON stream state, wiring agent-state selectors, adding a per-conversation capability, or triggering a shortcut from a button, menu, or code (useShortcutTrigger, launchAgentExecution).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

The provision for a call site: the exhaustive menu of values that place in the code can realistically produce. Use before creating or fixing a mandate or agent that reasons about more than its own input, when deciding whether an agent can answer at all with what it is sent, or when its output 'looks right' but nobody checked it could be. NOT for building the agent (use create-agent).

日本語の概要は準備中です。原文の説明を表示しています。

armanisadeghi/ai-matrx32026年10月12日 更新

armanisadeghi のスキルをすべて見る

このスキルの問題を報告する