The Control Panel Is Drawn With a Neutral Shared Shell
Why the panel body promoted out of entities/block into shared/ui/molecules/work-panel/v1 as WorkPanel, and how the three tiers supply their own words
Superseded — 2026-09-28
The per-card Control Panel and its shell WorkPanel are retired: Goldie is
the side panel's (Builder Goldie Lives in the Side Panel).
NoticeRow and the transcript row types moved to shared/ui/molecules/panel-notice/v1;
PanelBox, BlockSourceBox and the panel files named below were removed. The
record below is history.
Decision
The Control Panel's body is one domain-neutral component in Shared —
WorkPanel, formerly at src/shared/ui/molecules/work-panel/v1 (removed 2026-09-28) —
which Block, Module and Document each mount and supply their own vocabulary
over. It holds no product copy: every user-visible string it renders is
either neutral English (Reply…, Send, Responding…) or arrives as a prop.
In plain terms: the panel is a printed form with blank lines on it. The same form is used for a clause, a section and a whole agreement; whoever hands it out fills in the headings. Nobody keeps three separately typed versions of the form.
"Control Panel" remains the product's word for the surface. ControlPanel,
ModuleControlPanel and DocumentControlPanel keep their names; WorkPanel is
what those three are built out of, and it is a neighbour of the existing shared
WorkBar.
Landed with it, and part of the same rule:
| Piece | Home | Why |
|---|---|---|
WorkPanel, NoticeRow, PanelBox | shared/ui/molecules/work-panel/v1 (removed 2026-09-28) | The shell, its system line, and the white payload plate. Neutral. |
SourceDiff (was BlockSourceDiff) | shared/ui/molecules/source-diff/v1 | Owner, 2026-08-24: "modules and document stacks will also require diffs." |
agentPanelCopy(kind) | entities/agent/lib/panel-copy.ts | The three strings each tier hands the shell. |
BlockSourceBox, the block payload branches | entities/block/ui/block-card/v4 | Genuinely Block vocabulary: facets, About source, proposals. |
The shell's Interface is the seam. Its invariants — fixed row order (box → bar → notice → transcript → reply), turn ids as the scroll key, prepend preserving position, the starter showing only on an empty transcript, the Enter/Shift+Enter/Escape grammar, and the shell stopping click and keydown propagation because it always mounts inside a clickable card — are stated in the file header and guarded by its own Live story.
Context
The panel body was BlockSourceComposer, a 541-line file in
entities/block/ui/block-card/v4 still opening with "PROTOTYPE. Storybook
only, no wiring, no agent, no mutation." It had been wired at three tiers
since 2026-08-23 and was reached by two cross-Entity @x doors. There was
never a second copy — FSD's @x mechanism was doing its job — but the owner
ruled on 2026-08-24 that it "should be promoted to shared", and the promotion
forced the Interface to be settled rather than inherited from the prototype.
Two layer violations blocked a plain move. biome.json's src/shared/**
override bans this folder from importing #/entities/** and
@goldie-blocks/domain, the latter with the message "Shared UI is
domain-neutral: it cannot import the domain. Product meaning enters through
props from Entities and Features." The composer did both: it imported
agentMessageRole from the Agent Entity, and AGENT_DISPLAY_NAME from the
domain. Both bans were deliberately broken once during the migration and
observed red before the work was called done.
The domain import was the more interesting one, because it was user-visible copy, not a projection. The shell hard-coded "Ask about or describe a change to this block" as its input's accessible name and "…explain this block" as its first-turn greeting, at all three tiers — so on the running canvas the Document panel's prompt field announced itself as a block's. Promoting the file is what made that a compile error rather than a small ugliness.
Why not
Why not leave it in entities/block behind two doors. It passed steiger
and it was one definition. But the doors existed for one import each, the
Document's door for a single component, and neither door could stop the shell
learning block vocabulary — which is exactly what had happened to the copy. A
fourth consumer would have needed a third door. Promotion deletes the doors:
entities/block/@x/document.ts is gone entirely, and @x/module.ts is down to
one story-only export.
Why not merge it into PromptComposer. Verified rather than assumed
(GB-128): BlockSourceComposer never composed
shared/ui/molecules/prompt-composer and never had. They are different
objects — PromptComposer is a neutral send form (FormData, submit, status),
WorkPanel is a whole panel body of five ordered rows. Folding one into the
other is its own design question and was not smuggled in here.
Why agentMessageRole does not move to packages/domain. The rule was
stated three times: the function in entities/agent/lib, an inline ternary in
the drafting editor's panel model, and a dead call in the composer whose
input was already narrowed. Moving the function to the domain would have made a
fourth home for a projection onto a shared-UI type. Instead the direction
inverted: the widget's ternary now calls agentMessageRole, the dead call and
its door export are deleted, and PanelTurn.role is already MessageRole by
the time the shell sees it. The fail-safe is unchanged — an unrecognised
speaker still reads as the agent rather than as the user, decided once at the
point the wide string enters the frontend.
Why there is no tier or kind prop on the shell. That is the
flag-configured shell
The Folio Tiers Share Parts, Not a Recipe
rules out by name. The shell receives three strings from agentPanelCopy(kind)
and never learns which tier called it; a new tier adds a row to that table, not
a branch inside Shared. Candidate 25 (the Control Panel assembly) is not
foreclosed by this — what remains for it is a shared wrapper or a thin
PanelFrame part, never a FolioControlPanel with a tier axis.
Why the diff is in Shared too. The first draft of this decision kept it in
entities/block, on the ground that neither Module nor Document diffs
anything. The owner ruled that this is true today and not by design —
"modules and document stacks will also require diffs" — so it re-homed now
rather than after the second tier forced a door. Nothing is wired for Module or
Document by this change: the mechanism moved, the feature did not.
Amendment — 2026-09-22: two faces, one reply row (GB-310)
The shell draws two tabs where the About fold-out sat: the caller's conversation tab and its About tab, one shown at a time in one fixed-height panel, with the reply row beneath either. Owner rulings 2026-09-22, judged on the Panel views prototype: tabs, not filter buttons; the reply box stays put under both; both faces take the transcript's 288px ceiling as their one height, so the reply row never jumps on a switch; sending from About stays on About, and a reply that lands while About is up marks the conversation tab with a gold dot until it is chosen; each tab draws its own 2px rail, gold on the active one, one weight in both states; the close control sits on the tabs row inside the apron in Goldie's colourway.
In plain terms: the fold-out drawer on the back of every folder is now two tabs, one for talking to Goldie and one for the folder's label, guide and tags. The same two tabs serve a clause, a section and a whole agreement.
The shell still holds no copy: the tab words, the dot's spoken text and the
About placeholder arrive through faces (PanelFaces), from
agentPanelCopy(kind) on the three tiers and inline on the list screens' rows.
The About face itself is the Folio Entity's FolioAboutFace
(entities/folio/ui/folio-metadata/): name and guide as bare fields with a
gold spine beside the guide while it is edited, tags as chips a person can
remove but only Goldie adds, a tip at the foot, and a memo's proposed rename
or rewrite standing in for the row it changes. The name written there is the
header's name, through the same write. FolioAboutCard and the disclosure it
drew are gone. The railed list variant and the goldie tone on Tabs v4 are
the atom's, so no entity carries class overrides. A selected content block's
rail takes gold-500 with the same ruling.
The row order is now box → bar → notice → faces (tabs row, then the one
face panel holding the transcript or About) → reply; without faces the
transcript stands where it did.
Amendment — 2026-09-23: the review gutter, and the panel in a popover (GB-305)
Every folio card — workspace, project, document, module and block — loses
the G. Its trailing corner is a shared ReviewGutter
(shared/ui/molecules/review-gutter/v1): a ButtonGroup of 24px outline icon
buttons, Accept (tick), Reject (cross) and Comment (speech bubble), in an 88px
trailing column — 8px clear of the text, the 72px group, 8px to the card's
edge. Comment always shows, the icon alone at rest and the group on white with
borders on hover or focus of the card; Accept and Reject exist only while the
folio has a pending proposal. Owner rulings 2026-09-23, judged on the Proto
story Widgets/DraftingEditor/Review gutter.
In plain terms: the tick-cross-comment trio replaces the gold G on every card, and the chat panel moves off the bottom of the card into a movable pop-up.
- The gutter is the one place a proposal is decided. The tick and cross
call the same handlers the panel's
ReviewActionspair did: a whole-folio review first (a proposed insertion or removal — a removal applies on the tick), then the folio's own proposal, then a memo anchored on it. The Control Panel no longer draws a decision pair;ModuleControlPanelandDocumentControlPaneltake their slot without the decision members, which the cards read for the gutter. The whole-folio review row keeps only a refusal and Open. - Comment opens the folio's live Control Panel in a
PanelPopover, on the Base UI Popover atom, replacing theStackFootershelf. It opens below the folio (below a container's header row), right-aligned 88px inside the folio's edge so the review column of every card beneath stays uncovered (GB-306: a proposed card lands right under its anchor, and closing the panel to reach its tick would sign the memo), tucked 8px over its foot and never flipped above; white, 16px padding left, right and top, with no words-column inset or control gutter; the tabs' rail under the tabs only; a header of grip (drag to move), the caption (the folio's kind and name) and an X, the panel's own X not drawn; a resize corner at the bottom left (width 320–720px, face 160–640px). Each opening starts beside its folio at 420 × 288px; while open it scrolls with the folio. - It closes only on Comment, its X, Escape, or the selection moving — not on a click or focus move elsewhere, as the shelf never did, because closing a panel can sign an open memo on the canvas. Focus stays on Comment when it opens, as it stayed on the G, so one Escape still closes the panel and puts the card down.
- A proposal washes the whole card: an insertion green, a deletion red. A
whole-clause deletion is drawn as the clause struck, with no empty added
line (
ProseDiffdraws a one-sided change as one line).
Consequences the owner should expect to see
- The Module and Document panels now name themselves. Their prompt field's accessible name and their first-turn greeting say "this module" and "this document"; the Block tier is unchanged.
availableFieldsis dropped. It was a reserved prop for an@-mention picker that was never built, accepted and immediately discarded by the panel, and a domain-shaped noun that would have leaked into Shared.
A conditional note on facet-box height (rescued 2026-09-12)
The facet box the shell can render is hidden today (hidePayload is always
on), so no height-locking observer exists. If a future consumer renders the
panel with the payload shown, the original constraint returns: do not copy the
inline box observer for data blocks (Fields rows render outside the box);
measure the wrapper region that swaps between Fields rows and the facet box,
freeze the last measurement on a tab switch, and use a sensible minimum when
the panel opens without a baseline.
Work Bar Shows Where You Stand
Compact singular parent and current levels in the Work Bar, with named parent context above the card and paperless container screens
The Workspace Is Home; the Project Is the Sharing Grain
Owner ruling 2026-09-13 — one workspace is minted for every person and is their home screen; the Project is stored beneath it and is what gets shared; the organisation is the access record and capability rules, never a Folio kind; and no read may infer the current workspace or project from the person. Amended 2026-10-04 — the app calls that screen Home, one landing screen at /app