Skip to content

feat(ui): migrate ToolActivity card shell onto @maka/ui chat primitives (#332 PR3b) - #350

Merged
Astro-Han merged 7 commits into
mainfrom
claude/chat-tool-card-primitives
Jun 28, 2026
Merged

feat(ui): migrate ToolActivity card shell onto @maka/ui chat primitives (#332 PR3b)#350
Astro-Han merged 7 commits into
mainfrom
claude/chat-tool-card-primitives

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Moves the ToolActivity card shell — the inline section + count pill, the <details> card, its <summary> header row + status dot, the body / intent, and the args <pre> override — onto the @maka/ui chat substrate, retiring the bespoke .maka-tool* / .toolInline / .toolItem / .toolArgs CSS with zero visual change. This is PR3b of the #332 conversation-flow governance pass, following PR1 (#334, bubble/row shell), PR2 (#337, turn markers), and PR3a (#348, tool live-output stream).

The static shell literalizes onto a new internal toolVariants cva exactly as PR2/PR3a did. Three pieces escape the leaf-literal proof and stay a small named residue re-keyed off the retired .maka-tool class onto the governed [data-slot="tool"] hook: the card mount entrance (transition + @starting-style, which only paints on the first frame), the native <summary> marker reset (pseudo-elements), and the running dot's @keyframes maka-tool-pulse ring (a keyframe is a global rule, not an element property). These are pinned by a cascade contract + the diff harness instead.

Why

Closes part of #332 (PR3b). The conversation-flow display is being migrated onto one @maka/ui substrate + a test net, one island at a time, with zero visual change. The tool card was the one remaining bespoke island between already-governed neighbours (bubble / marker / stream); lifting it now keeps the chat surface coherent and location-independent (status is a data-* vocabulary, not a descendant-selector coupling). The native <details> card and the .maka-turn-thinking block can later converge onto one Base UI Disclosure primitive — PR3b deliberately leaves both in the same shape (native details + named residue) so that structural pass is a clean follow-up, not a prerequisite.

Scope

Changed (three atomic commits):

  • feat(ui): add internal toolVariants chat primitivepackages/ui/src/primitives/chat.tsx (the cva table; every value a LITERAL arbitrary utility compiling 1:1 to the retired declaration), packages/ui/src/index.ts (kept OFF the barrel like markerVariants / streamVariants — single consumer, relative import).
  • refactor(ui): migrate ToolActivity onto toolVariants and retire .maka-tool* CSSpackages/ui/src/components.tsx (rewire + data-slot="tool"), apps/desktop/src/renderer/maka-tokens.css + styles/tool-output.css (delete the migrated rules; keep the [data-slot="tool"] entrance/marker residue + the @keyframes maka-tool-pulse ring). Consumer + CSS removal land together so the renderer orphan-selector contract stays green.
  • test(ui): lock the migration with cascade contract + diff harness — see Verification.

Not included:

  • ToolErrorBanner + .maka-tool-error* — already on the @maka/ui Alert primitive; its dead grid/icon/body/title classes and live copy-feedback tints are coupled to visible-copy-hygiene-contract, a different review lens. Separate PR (PR3c).
  • The result-preview renderers (.maka-tool-diff* / .maka-tool-terminal* / .maka-office-document* / .maka-explore-agent* / .maka-load-tool-* / .maka-overlay-*) — separate components rendered inside the tool body, a different concern.
  • The shared .maka-code inline-code base (Markdown / artifact previews use it too) — untouched; the args <pre> keeps the class and only its .toolArgs override moves.
  • Base UI Disclosure unification of the card + .maka-turn-thinking — a later structural pass.

Verification

This repo has no CI; everything below was run locally.

  • npm run -w @maka/ui test — 13/13 (adds a toolVariants literal/scale-drift + single-backslash waiting_permission runtime guard).
  • npm run -w @maka/desktop test — 1613/1613 (adds chat-tool-card-cascade-contract.test.ts: retired selectors absent without touching .maka-tool-error* / .maka-tool-diff* / .maka-tool-terminal* / .maka-code; @keyframes maka-tool-pulse ring frames pinned; the [data-slot="tool"] entrance + marker residue pinned; the [open]>summary divider + running-dot literals + String.raw escape pinned; and a guard deriving the status set from STATUS_LABEL so every production status keeps a diffed row).
  • npm run typecheck — clean across all workspaces.
  • npm run check:chat-visual -- <pre-PR2 baseline.css> <head.css> (the computed-style diff, extended with a tool-card tree): against a pre-PR2 renderer CSS baseline (e033a8c4~1, which still carries the bespoke marker + stream + tool CSS), TOTAL DIFFS: 0 across 61 element/state rows + 2 ::before middots — including all 25 tool-card rows (every one of the six production statuses renders a card — pending included, proving the base border + gray-dot fallback it has no data-[status] branch for). Each card is rendered at its real isOpenByDefault state (pending/waiting/running/errored open, completed/interrupted collapsed), so the most common historical card — a settled, collapsed completed tool — is covered too: the collapsed summary's divider reads 0px vs the open card's 1px (the non-vacuous proof the collapsed branch is exercised), identical across main/head. Non-vacuous: the baseline and head bundles differ in source, yet the migrated chrome reads byte-identical computed style on both sides.
  • The running dot is the one part the diff can't cover (animated → phase-dependent getComputedStyle); pinned by the @keyframes maka-tool-pulse frame contract + the chat.tsx literal.

User-facing impact

None — zero visual change by design, locked by the contract + diff above.

Reviewer notes

  • The diff harness earned its keep: it caught a real literalization bug a source-only test would have missed. waiting_permission carries a literal underscore, and Tailwind turns a bare _ in an arbitrary value into a SPACE — so data-[status=waiting_permission] compiled to [data-status="waiting permission"] and the card/dot tint silently fell back to the base color (3 diff rows). Fixed with a String.raw \_ escape so the scanned source text and cva's runtime class agree on a single backslash; both contract tests now lock it (the four underscore-free statuses never had the problem).
  • toolVariants is deliberately off the barrel (like markerVariants / streamVariants): one consumer, applied by relative import, so it stays an internal, freely-removable styling detail rather than public API.
  • The status dot stays a toolVariants part, not a shared primitive like LiveIndicator: it is tool-specific (six states, a box-shadow ring pulse distinct from the maka-pulse opacity/scale breath), so there is no second consumer to justify promotion.
  • The tests intentionally do NOT re-assert the static shell's per-part literals: that shell is proven by the computed-style diff, so re-stating the strings would only mirror the implementation. Source-string assertions are kept only for the escapes the diff cannot observe.
  • Reduced-motion / visual-smoke suppression both ride GLOBAL * rules (maka-tokens.css / base.css), so — unlike LiveIndicator, a reusable primitive carrying its own motion-reduce: guards — the dot and card need no per-element motion utilities; the same global rules cover them as before.

…ity card (#332 PR3b)

Add a `toolVariants` cva literalize table to packages/ui/src/primitives/chat.tsx
covering the `ToolActivity` card shell parts (container / count / item / header /
dot / name / meta / duration / status-label / body / intent / args). Every value
is a literal arbitrary utility compiling 1:1 to the bespoke `.maka-tool*` it will
replace, mirroring `markerVariants` / `streamVariants`. Kept OFF the package
barrel (relative import only) so it stays an internal, removable styling detail.

The running dot's `[animation:maka-tool-pulse…]` rides in the `dot` part; the
mount entrance + `<summary>` marker reset are left for the CSS residue (next
commit) since they escape the leaf-literal proof. `waiting_permission` uses a
`String.raw` `\_` escape so the scanned source and cva's runtime class agree on a
single backslash — a bare `_` would be read by Tailwind as a space and never match.

No consumer yet; the table is wired up in the following commit.
…-tool* CSS (#332 PR3b)

Rewire `ToolActivity` to apply the `toolVariants` parts (+ `data-slot="tool"` on
the card), and delete the bespoke shell it replaces: `.maka-tool` /
`.maka-tool-header` / `-name` / `-meta` / `-duration` / `-status-label` /
`-status-dot` / `-body` / `-intent` / `-count` and the `[data-status]` border
swaps (maka-tokens.css), plus `.toolInline` / `.toolItem` / `.toolArgs`
(tool-output.css). The args `<pre>` keeps the shared `.maka-code` base.

What stays is the irreducible residue that escapes the computed-style proof,
re-keyed off the retired `.maka-tool` class onto the governed `[data-slot="tool"]`
hook: the mount entrance (transition + `@starting-style`) and the native
`<summary>` marker reset. The running dot's `@keyframes maka-tool-pulse` ring is
kept too (a keyframe is a global rule, not an element property).

Consumer + CSS removal land together so the renderer orphan-selector contract
stays green. Out of scope: `ToolErrorBanner` + `.maka-tool-error*` (on `Alert`;
PR3c) and the result-preview renderers (`.maka-tool-diff*` / `.maka-tool-terminal*`).
…f harness (#332 PR3b)

Pin the two halves of "zero visual change" the way PR2 / PR3a did:

- Static shell -> computed-style diff: extend check-chat-marker-computed-style.mjs
  with a tool-card tree (section + count, all five `[data-status]` cards, the
  summary header grid, four static dot colors, name/meta/duration/status-label/
  body/intent/args). 20 new rows, 0 delta (running dot excluded — animated).
- Escapes -> source / keyframe contract: add chat-tool-card-cascade-contract.test.ts
  asserting the retired selectors are gone (without touching `.maka-tool-error*`,
  `.maka-tool-diff*`, `.maka-tool-terminal*`, `.maka-code`), the
  `@keyframes maka-tool-pulse` ring frames stay, the `[data-slot="tool"]` entrance
  + marker residue stays, and the running-dot literals hold. Plus a runtime guard
  in chat-primitives.test.ts that cva preserves the single-backslash
  `waiting_permission` escape and stays literal.

The diff harness caught a real bug while building this: `waiting_permission`'s
underscore compiled to a Tailwind space and the tint silently fell back to base —
fixed in the primitive (String.raw `\_`), now locked by both tests.
…oof (#332 PR3b)

Review follow-up (P2-B). The computed-style diff forced every status card `open`,
so it never exercised the real `isOpenByDefault` default — and the most common
historical card is a settled, COLLAPSED `completed` tool. Rebuild the fixture to
render each card at its true state (pending/waiting/running/errored open,
completed/interrupted collapsed), move the rich inner parts onto the open
`errored` card, and add a collapsed `completed` card. The non-vacuous collapsed
signal is the summary divider: 1px (open) vs 0px (collapsed, the `[open]` gate) —
verified distinct, identical across main/head. 23 tool-card rows, 0 delta (59
rows total). Also pin the `[open]>summary` divider literal in the cascade
contract as the automated guard (the diff harness is manual / no CI).

(The body is hidden via Chromium's `::details-content`, so its child `display`
stays `block`; the collapsed body row still diffs box / typography parity. The
earlier comment claiming `display:none` is corrected.)
… why (#332 PR3b)

Review follow-up (P3-A). Shed the PR-narrative lines from the `WP_*` escape
comment (12 -> 5), keeping the Tailwind `_`-as-space / `String.raw` rationale.
Density now matches the sibling `streamVariants` / `markerVariants` docstrings.
…#332 PR3b)

The computed-style fixture rendered five of the six production tool statuses
but omitted `pending` — a default-expanded state (isOpenByDefault) whose card
therefore had no diffed row, so a future pending-specific tint drift or
mis-literalization (the class of bug `waiting_permission`'s `_`-escape hit)
would pass unseen. The script's own comment + openByDefault already claimed
pending was covered, so the fixture was internally inconsistent.

- render a `pending` card (STAT) + diff its item/dot (IDS); it has no
  `data-[status=pending]` branch, so it proves the base border + gray-dot
  fallback. Computed-diff: 61 rows, tool-item/dot-pending 49/49 identical, 0 delta.
- guard the gap shut: the cascade contract now derives the status set from
  components.tsx's `STATUS_LABEL` (the `ToolActivityItem['status']` Record) and
  asserts the harness STAT renders every one — a new status can no longer escape
  the proof silently.
The fixture's header count was hardcoded `'5'` while STAT had grown to six
statuses — a self-description that no longer matched the rendered cards. The
count never entered the computed diff (it reads CSS props, not textContent), so
this was a credibility gap, not a missed regression — but the cards, count, and
diffed IDS were three hand-kept lists that could drift apart.

Collapse them onto the single module-level STAT: cards `STAT.map`, count
`String(STAT.length)`, and the IDS tool rows derive `tool-item-${s}` for all and
`tool-dot-${s}` for all but running (its animated ring stays keyframe-pinned).
This also closes a residual hole — a new status added to STAT now auto-gets a
diffed row instead of rendering an un-measured card. Diff unchanged: 61 rows, 0 delta.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant