Base int/m1-wave24 at 2a72cca, branch feat/M4-07-core-components in its own worktree; the branch is not merged locally and not pushed, the PR lane ports it. Text this ticket adds is English, also inside the German chapter docs/design/components.md. Names that differ on this base and are followed: the chapters are 01-produkt.md and 15-client-architektur.md; the trigger state widget is DsTriggerStateIcon, because DsTriggerState is the enum of the view models. No core rule or ruling from main was needed (R-PR-04, D-W-100 and D-W-114 are on the base).
Instructions of the launcher, which override the brief's landing step; merged names win (D-W-100, D-W-114).
D-M4-07-1
2026-10-15
Components take small immutable view models defined in design_system (view_models.dart: DsMessageData, DsRunData, DsStepData, DsThreadRowData, DsChannelRowData, DsSpaceRowData, DsComponentData, DsComponentHeader, DsSuggestion, DsComposerCommand and the enums DsMessageOrigin, DsMessageKind, DsTriggerState, DsRunState, DsStepKind, DsStepStatus, DsBillingMode, DsComponentType, DsComponentState, DsButtonStyle, DsApprover, DsChannelKind, DsFieldType, DsCategory). Enum values carry the codes of 03 and an unknown member (DsCategory falls back to granite); fromCode never throws. Callbacks are plain functions; no component performs I/O or imports a provider.
Brief note 1: testable with literal data, usable by the console and the golden harness; a newer server may send a new code (06 13).
D-M4-07-2
2026-10-15
DsMarkdown parses with package markdown (GitHub flavored without the inline HTML syntaxes) and builds its own widgets for the subset of the brief. HTML renders as literal text; an image renders as the text "Image: ()" and never loads; links are active only for https, http, mailto and the internal paths /t/ and /c/, show their domain (or the internal target) on hover and focus and reach onOpenLink only on activation; every other scheme renders as plain text; headings are at most type.heading; code blocks scroll horizontally and copy exactly the code. markdown 7.3.1 gets a checked PackageOverrides entry (BSD-3-Clause, from its LICENSE file) in osv-scanner.toml, because deps.dev knows no license for it, as D-M4-04-5 does for alchemist. Block quotes and lists nest as structure up to DsMarkdown.maxNesting (8) levels; a deeper element renders as its plain text, because the server accepts any nesting and thousands of levels froze the interface (red team of M4-07).
Brief note 2: a widget package would decide link and image handling and could load resources (SEC-OUT-1, SEC-OUT-2).
D-M4-07-3
2026-10-15
DsMessageComponents draws a framed header with security.step-up built only from DsComponentHeader (agent, run, required capability, approval note), then a divider labeled "Text from ", then the agent text as Markdown, then the components. DsMessage therefore does not draw the content of a kind = card message that has components. After the first activation the widget ignores every further one until the caller reports a new state of the card (another state, pendingCustomId, errorText or message); an equal view model rebuilt by the parent keeps the lock, and pendingCustomId from the caller locks the same way. For screen readers the group is labeled by the server header and the header is one node with the plain names; the agent's card title is read after the divider, and the message label of a card names only author and time. onActivate gets null for a button, the list of chosen values for a select and the field map for a form. Only Enter or Space on the focused button activates (the default ActivateIntent); no other key or shortcut does.
Brief note 5, SEC-OUT-4 and SEC-T14 at component level; 01 6.5 (a double click creates nothing twice); R-PR-04.
D-M4-07-4
2026-10-15
LiveAnnouncer ignores the first observation, announces a change of the state text or a new question ID, never a patch of steps or numbers, and merges announcements closer than minInterval (a11y.live_announce_min_interval) into the latest candidate, which flush sends once the interval has passed; a state candidate equal to the last sent text is dropped, a new question never is (all questions of an agent read the same). The live box arms the flush timer again when it fires early. The live box sends through SemanticsService.announce with polite assertiveness, in all three forms: the collapsed and the mobile line also take announceMinInterval, so the end of a run and questions on a phone are announced.
Brief note 4, 01 8 and components.md 2.4.
D-M4-07-5
2026-10-15
The golden matrix is one sheet per component (live box and components on messages split into an active and a final sheet), and one file per sheet, density and theme, each holding text scales 1 and 2 and the locales de and en: 9 sheets, 54 files for CI and 54 for macOS, the largest file below 700 KB, together about 22 MB (CI about 2.5 MB). dsGoldenMatrix passes the l10n delegates with the locale, because the alchemist app has none. guidelines_test.dart runs the four Flutter guidelines over the same sheets in light and dark with the touch density.
Brief note 8 allows a file per component, theme, density, text scale and locale; one file per component and density reached 2.5 MB and exceeds repo.max_file_kb, and no dimension may be dropped.
D-M4-07-7
2026-10-15
The message label stays ",
Code review of M4-07: links and the copy button were unreachable; 01 6.3 wants the state at every triggering message.
D-M4-07-8
2026-10-15
The composer leaves Enter, Tab, the arrows and Escape to the input method while a composition is active. The generated token files stay byte for byte as tokens.py build writes them; dart format runs only on hand-written folders.
Red team of M4-07: Enter during an IME composition sent half-typed text; a formatted copy made make tokens red.
D-M4-07-6
2026-10-15
Flutter 3.47.7 has no tree role in SemanticsRole, so space rows are expandable nodes with the expanded or collapsed state and channel rows carry their label; the arrow is action.expand (caret down) when expanded and action.collapse (caret right) when collapsed, as icon.tokens.json maps them. Two opacity values without a token are named constants with a reason: the icon tile of a space (0.16) and the lowest point of the run dot pulse (0.4).
components.md 7 asks for a tree; the semantics that Flutter offers are the expanded state. The token set has no tint or pulse opacity role.
D-M4-07-9
2026-10-17
The ticket was ported from its integration base onto main with only its own commits (the integration fixes of the wave concern other tickets); the English docs of main win, so the new paragraphs in docs/design/components.md sit under the English headings of main. The nine light goldens of badges, composer and active live box are rerendered on main, macOS and CI variant.
Main carries its own value of color.warning.subtle (palette.amber.100), light #FEE6D4 instead of #FEF3EA on the integration base; the warning chips of these sheets show it, nothing else changed in the images.
D-M4-07-10
2026-10-17
The component sheets are sheetWidth wide, wider than the longest scenario label of alchemist, and every golden is rerecorded; the text contrast guideline of guidelines_test.dart runs on macOS only, the tap target and label guidelines on every host. CI goldens compare with DsGoldenFileComparator (PL/5).
On the Linux runner the light comfortable sheets took the width of their label, a fraction of a pixel wider than a whole number, and the last pixel column stayed transparent there while macOS filled it. The contrast guideline measures the most frequent colors of the rendered glyphs: on Linux a blended edge shade of the link outnumbered color.text.link and measured 4.18 although the pair has 5.9 and passes the token gate, which checks every contrast pair on every host.
D-M4-07-11
2026-10-18
DsComposer clears its own text controller after onSend; a controller passed by the caller keeps its text, and the caller clears it. onChanged also reports the changes the composer makes itself (the new line of Shift+Enter, the clearing after a send), so a draft or typing state of the caller stays current.
CodeRabbit on the pull request, reproduced with a test: with the documented call without a controller the sent text stayed in the field, and the next Enter sent it again (a duplicate message).