Appearance
Finding widget shell
How a finding widget is built, and the steps to add one.
Every finding widget is made by one function, defineFindingWidget (src/view/finding-widget.tsx). It takes a per-widget row model and a Row component. The model decides what the widget shows. The Row draws it. The shell owns everything else.
What each part owns
- The row model,
<Widget>.rows.ts, owns the facts. A row decides, as data, whether its finding is stated by the line above an evidence panel and what the density shows (for example ashowStatusflag). Therowsfunction is pure. - The
Rowcomponent,<Widget>.tsx, renders only what its row says. It makes no decision of its own and reads no store, context or density. - The shell owns the
WidgetCard, the title (the finding's core name fromfindingName, unless the model gives a pluraltitle), the ALL-CAPS tag, the sort toggle, the paging, the route index and the row anchors. See List sort mode, Row/expand contract and Investigation routing. - Mounting follows the rows. The widget renders when
rows(...).length > 0and renders nothing otherwise. The samerowsfunction feeds the evidence panel,computeActiveWidgetsand the Clean checks list, so the card, its evidence control and its panel exist together or not at all. A model setsalwaysMountedonly for a run-wide reference tile.
Row model
FindingWidgetModel (src/view/finding-widget.tsx) has these fields. Only type, rows and stageOf are required.
type: the finding type.rows: aRowsOffunction. It takesPanelInputand returns the rows in base order.PanelInputhas the findings, the stated ids, the density, theappModeland the run's catalog. Every row lists the findings it stands for infindings.stageOf: the stage number of a row, for the stage sort. It returns null for a row with no stage.summary: the collapsed card's lead value and context.title,tagClassName,tagDocAnchor: a plural card title, tag colour classes, and a doc anchor taken from the rows.findings: the findings the widget is for, when it is not the type's findings in the catalog (Config Audit readsconfigFindings).sections,jumpLabel,rowElement,rowClassName,separated,listClassName: sections with their own paging, the jump control, and the row layout.
The view passed beside the model has a required Row and the optional Lead, Summary, Frame and Status slots.
Adding a finding widget
- Add the detector side first: the folder
packages/core/src/detectors/<type>/with itsdetect.tsand its entry indetectors/index.ts, as listed in Detector contract. The compiler reports a missing piece. - Add the presentation row in core, in the folder's
presentation.ts(FINDING_PRESENTATIONassembles it), its entry indetectors/presentations.tsand its path onALLOWED_EXPORT_CORE_MODULESinscripts/export-analysis-guard.mjs. It holds the finding's name, tag, family, one-sentence pitch and summary. The card title comes from the name. - Add
src/view/widgets/<Widget>.rows.ts. It exports the row type (extendingWidgetRow<'type'>), therowsfunction and the model. Put every decision in the row: whether it is stated (isStated(finding, stated)), what the density shows, which extension text it has. Return no row for a finding the widget has nothing to show for.RetryWaste.rows.tsis the smallest example. - Add
src/view/widgets/<Widget>.tsx. It has aRowcomponent andexport const <Widget> = defineFindingWidget(<widget>Model, { Row }).RetryWaste.tsxis the smallest example. - In
src/view/detector-registry.tsx, add the lazy import for the widget and theREGISTRYentry with itscomponent,widgetId,routeableandmodel:. The entry has nocontentand no hand-writtenregion; the region comes from the presentation row. - Test the
rowsfunction in the widget's own test file: the rows for a finding that is stated and one that is not, at Basic and at Advanced. Do not test title, tag, sort, paging, route index or anchors there.tests/view/finding-widget.test.tsxtests them once for every widget. - Add the tag to
AGENTS.md's tag list when it is new, and add the.changeset/*.md.
Rules
- A row decides the stated and density facts as data. The
Rowrenders only that. - The shell owns the card, the title, the tag, the sort, the paging, the route index and the anchors. A widget file does not use
WidgetCard,TagBadge,RowPagination,SortModeToggleorAdvancedOnly, and does not call the sort, paging, anchor, route, density or inline-evidence hooks (finding-widget-uses-the-shell). .rows.tsfiles are pure. They may not import React, the store, a context or a hook module, call a hook, or read a global, the clock or randomness (rows-are-pure,rows-are-pure-inline). A value a row needs goes intoPanelInput.- A table cell never compares numbers, and a chip comes from a finding (
no-numeric-severity-in-cell). StageTable shows a chip on a Retried or Spill cell only when theretryWasteorspilldetector flagged the stage, in that finding's impact band. Fetch Wait and I/O Ratio show raw numbers. - Hiding after render is banned, including
empty:hidden. An element with nothing to show is not a row.
See Structure rules for the rules and Evidence placement for where a finding's evidence shows.