Goldie Blocks

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:

PieceHomeWhy
WorkPanel, NoticeRow, PanelBoxshared/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/v1Owner, 2026-08-24: "modules and document stacks will also require diffs."
agentPanelCopy(kind)entities/agent/lib/panel-copy.tsThe three strings each tier hands the shell.
BlockSourceBox, the block payload branchesentities/block/ui/block-card/v4Genuinely 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 ReviewActions pair 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; ModuleControlPanel and DocumentControlPanel take 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 the StackFooter shelf. 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 (ProseDiff draws 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.
  • availableFields is 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.

On this page