Imported from SEMOSS/semoss-ui (
libs/panels/AGENTS.md). Install upstream withnpx skills add SEMOSS/semoss-ui --skill panels. Copyright stays with the author.
AGENTS.md - @semoss/panels
The SEMOSS file panels: eight editor/viewer blueprints, three explorer variants, the MCP toolbox
editor, and the resource-permission cache they read. Everything here is a WorkbenchPanelConfig
or something one of them needs.
Inherits from: ../../AGENTS.md (root). Load the applicable root skills, including the React standard, for general implementation rules.
Why this is its own package
The panels are both dock-aware and SEMOSS-aware, and neither existing lib can hold that:
@semoss/sharedwould have to depend on@semoss/workbench, dragging the dock into every consumer of shared — including ones that never mount one.@semoss/workbenchcannot take them at all. It exists to be domain-agnostic; these run pixels.
So the layering is one direction, three layers:
@semoss/ui ← @semoss/workbench ← @semoss/panels → @semoss/shared → @semoss/sdk
Nothing here may import from a host (packages/*). zustand is a peer dependency,
matching the dock. Hosts supply a compatible version and explicitly share the intended
store instances; a peer declaration alone does not guarantee shared state.
Layout
src/
├── components/ the panels, controls, explorer, and `components/mcp/` editor UI
├── contexts/ access provider context
├── stores/ access store factory
├── hooks/ access, file-panel, explorer, and MCP hooks
├── utility/ file-panel and MCP utility functions
├── types/ access, file-panel, and MCP types
├── constants/ file-panel runtime constants
├── styles/ the Tailwind `@source` every host imports
├── vite/ build-time bits a host cannot skip (see below)
└── index.ts curated public surface
src/vite/ is Node-side and is not in the barrel — it has its own ./vite export subpath.
Access is provided by the host
Panels call useAccess(type, id), which reads whatever store the nearest AccessStoreProvider
carries. The contract is structural (StoreApi<PermissionCache>) so a host can fold the cache into
a bigger store:
- The client folds
createPermissionCacheinto its session store, so permissions sit beside the user they belong to and are cleared on logout — without that, one user'sOWNERsurvives into the next user's session in the same tab. - A host with no session (the playground, a test) mounts
createAccessStore().
Worth knowing when reasoning about cost: an INSIGHT permission resolves to "EDIT" with no
network call at all.
file-explorer-control.tsx reads state.permissions[key] directly rather than through
useAccess. That is deliberate — a chrome control renders outside its panel's subtree, so it
cannot reuse the access the panel already resolved.
The MCP editor is duplicated on purpose
src/components/mcp/ is the canonical copy. packages/client/src/components/shared/mcp-json-editor/ is a
second one kept alive only for the legacy BLOCKS workspace, and is deleted when BLOCKS migrates
off FlexLayout. Do not re-point the panel at the client's copy to remove the duplication — that
would invert the dependency this package exists to establish.
Rules
src/index.tsis curated, notexport *. Of the sixty symbols the source tree used to leak, eight had an external consumer. Panel blueprints are not among them — a host registersFILE_PANEL_COMPONENTS. Add a symbol when a consumer needs it;export *also putgetImageMimeType(path)next to@semoss/shared's incompatiblegetImageMimeType(extension)in any barrel that re-exported both.useFilePanelanduseFileBufferare public, for a host whose editor needs chrome of its own.packages/terminalbuilds on them: its editor carries a Run toolbar and a scope guard, and the dock allows one control per panel, so it cannot inheritFILE_CODE_EDITOR_PANELwholesale. That is the supported way to reuse the access/read/save/dirty machinery.- Nothing a panel imports may import
file-panel.components.ts. It is an object literal over every blueprint, so a module still initializing when it is built lands in the map asundefined— the dock then renders "no component registered" andmatchessilently falls back to a shallow compare, with nothing thrown. That is exactly what homingisFilePanelTypethere did: it closed the cyclefile-panel.components→file-explorer-panel→use-workbench-file-panels→file-panel.components, and left the file explorer blueprint undefined in every host. The predicate lives infile-panel.constants.ts, which imports nothing.file-panel.components.test.tsstates the invariant, and the playground'suse-sidebar-panel-active.test.tsxis what actually reproduces a cycle — it enters through the package specifier, the way a host does. FILE_PANEL_TYPESstring values are a storage contract. Changing one breaks cached layouts that reference the old type; preserve compatibility when changing registrations. The client spreads these into its ownWORKBENCH_COMPONENTS.FILE_PANEL_COMPONENTSdefines what "a file panel" is at runtime.useWorkbenchFilePanelsdecides which open panels follow a rename by membership in it, never by the shape of a config — the Git panels carry the same{ type, id, name, path }fields and were being swept up.- Workbench event names live in
file-panel.constants.ts, beside the panel types. That file imports nothing, which is what keeps an event name safe to import from anywhere — the same cycle rule asisFilePanelType. The hook that subscribes is an ordinary hook inhooks/. - An event with no subscriber does not belong here.
useFilesChangedearns its place because four explorers and eight file panels consume it; an event added "for later" is surface with no consumer, and the dock's own panel-lifecycle events were deleted for exactly that reason. - Never dereference
a.mode.typein a blueprintmatches. It runs insideselectPanel, a store action outside any error boundary; a config it cannot read must return false, not throw. UsematchesFilePanel. - Keep PPTX viewer content lazy-loaded internally. The manifest exposes only the root,
globals.css, andviteentry points, not afile-pptx-viewer-contentpublic subpath. Do not turn the heavy viewer into an eager public-barrel import. - Monaco needs the
monaco-editoralias. Every host that renders a file panel aliases the bare specifier atlibs/shared/node_modules/...;vite.config.tshere does the same so the tests run. - Everything else about panels — blueprints, mount policy, chrome controls, the dirty
*marker — is in the dock's AGENTS.md and the client's.
What a host has to wire up
Three things, each of which fails quietly or late if it is missed:
| Why | |
|---|---|
@import "@semoss/panels/globals.css" (and the dock's) |
Tailwind only generates classes it has scanned. A package outside the host's @source globs contributes none, and a missing utility renders as an unstyled panel — never a build error. |
aiSdkStubAlias from @semoss/panels/vite |
pptx-react-viewer imports the ai package (its unused AI chat panel). It is an optional peer and is not installed, so without the stub the build fails outright. |
scopePptxViewerCssPlugin from @semoss/panels/vite |
The viewer ships its own Tailwind build emitting the same class and token names as the host's. Unscoped, whichever sheet loads last restyles the whole document. |
Plus the monaco-editor alias above. All of it lives here rather than in each host's
vite.config.ts, so adding a third host is an import rather than an archaeology exercise.
Validation
This package is source-only; it has no build script. From the repository root:
pnpm --filter @semoss/panels check-types
pnpm --filter @semoss/panels test
Also exercise an affected host when changing its panel or Vite integration.