Skip to content

refactor(web): sweep AGENTS.md TypeScript + Mantine rule violations (#1762)#1771

Merged
cliffhall merged 14 commits into
v2/mainfrom
v2/fix-1762-agents-md-sweep
Jul 25, 2026
Merged

refactor(web): sweep AGENTS.md TypeScript + Mantine rule violations (#1762)#1771
cliffhall merged 14 commits into
v2/mainfrom
v2/fix-1762-agents-md-sweep

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #1762

Summary

Codebase-wide audit sweep against the AGENTS.md TypeScript and Mantine/React conventions, driven by three parallel per-cluster audits (TypeScript rules, Mantine styling, Mantine structure). The codebase was already in strong shape (post-#1690/#1756), so the actionable set is small and cohesive — this is one reviewable PR rather than a batch.

Audit outcome (verified inventory)

  • TypeScript: zero any types; zero config-level suppressions (no-unused-vars stays at error with the sanctioned _-prefix pattern); all 12 non-test as unknown as casts already carry justification comments. Two tightenings applied (below).
  • Styling: no inline styles (the one in SortableServerCard is the sanctioned dnd-kit per-frame transform), no stray hex/rgba() in props or theme overrides (theme.ts hex are the Mantine palette tuples; issue-number #1234 comments are not colors), no layout divs. One minor CSS-class finding fixed.
  • Structure: Rules for Box.withProps misuse (0), domain logic in theme files (0), and elements importing from src/theme/ (0) are all clean. Repeated-inline-element and inline-logic findings fixed (below).

Changes

Styling

  • ServerListScreen: fold align-items: start into the existing styles prop; drop the .grid-align-start class + its App.css rule.

Structure — extract repeated inline elements to named .withProps() constants

  • NetworkEntry: DimmedNote (xs/dimmed) + SectionLabel (sm/500) replace 9 repeated inline <Text>s.
  • ServerImportConfigModal: GroupHeading replaces 3 repeated <Text fw={600} size="sm">s.
  • ViewHeader: HeaderStackCell replaces the twice-repeated animated wrapper (mirrors the file's existing RightConnectedGroup className-in-withProps convention).
  • ResourceControls: TightRow replaces two <Group gap="xs" wrap="nowrap">.

Structure — extract inline logic to named functions

  • ToolsScreen: handleSelectTool replaces the ~20-line inline onSelectTool handler.
  • ServerConfigModal: setTextField/clearTextField factories replace 6 repeated text-field onChange/clear closures.

TypeScript

  • App.tsx messagesToLogEntries: replace an as unknown as double cast with a single narrowing cast on just the params value.
  • createRemoteLogger: the pino/browser.js @ts-expect-error is genuinely unavoidable — TS resolves the real untyped JS file, so an ambient declare module can't type it (I tried; it fails with TS7016). Kept, with a comment now explaining why no better option exists.

Testing

npm run ci passes end to end: validate → the per-file ≥90 coverage gate → smokes (incl. the headless-browser boot smoke) → all 456 Storybook play-function tests. All 3472 unit tests pass; every touched file clears the ≥90 coverage threshold.

🤖 Generated with Claude Code

…1762)

Codebase-wide audit sweep against the AGENTS.md TypeScript and Mantine/React
conventions. The codebase was already in strong shape (post-#1690/#1756), so the
actionable set is small and cohesive:

Styling
- ServerListScreen: fold `align-items: start` into the existing `styles` prop and
  drop the `.grid-align-start` CSS class (+ its App.css rule).

Structure — extract repeated inline elements to named `.withProps()` constants
- NetworkEntry: `DimmedNote` (xs/dimmed) and `SectionLabel` (sm/500) replace 9
  repeated inline `<Text>`s.
- ServerImportConfigModal: `GroupHeading` replaces 3 repeated `<Text fw={600}>`s.
- ViewHeader: `HeaderStackCell` replaces the twice-repeated animated Box wrapper
  (mirrors the file's existing `RightConnectedGroup` className-in-withProps
  convention).
- ResourceControls: `TightRow` replaces two `<Group gap="xs" wrap="nowrap">`.

Structure — extract inline logic to named functions
- ToolsScreen: `handleSelectTool` replaces the ~20-line inline `onSelectTool`.
- ServerConfigModal: `setTextField`/`clearTextField` factories replace 6 repeated
  text-field onChange/clear closures.

TypeScript
- App.tsx `messagesToLogEntries`: replace an `as unknown as` double cast with a
  single narrowing cast on just the `params` value.
- createRemoteLogger: the `pino/browser.js` `@ts-expect-error` is genuinely
  unavoidable (TS resolves the real untyped JS file, so an ambient `declare
  module` can't type it) — kept, with a comment explaining why.

Audit found zero `any` types, zero config-level error suppressions, and every
remaining non-test `as unknown as` already justified in-comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Jul 25, 2026
@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 2m 51s —— View job


Code review — PR #1771

  • Gather context (read full diff)
  • Review styling changes (App.css, ServerListScreen)
  • Review structural extractions (NetworkEntry, ServerImportConfigModal, ViewHeader, ResourceControls)
  • Review logic extractions (ToolsScreen, ServerConfigModal)
  • Review TypeScript changes (App.tsx, createRemoteLogger)
  • Post review

Verdict

Looks good — no blocking issues. This is a genuinely behavior-preserving sweep. I traced each change against the AGENTS.md rule it claims to satisfy and against the CSS/type context, and every extraction is faithful. Comments below are nits and one thing worth a conscious sign-off.

What I verified

  • .grid-align-start removal — no remaining references anywhere in the repo (searched all file types, incl. stories/tests). className and styles.root both target SimpleGrid's root, so alignItems: "start" lands on the same element the class did; inline-from-styles also outranks the deleted class, so no specificity regression.
  • App.tsx cast — genuinely equivalent, not just shorter. LogEntryData.params is already declared as LoggingMessageNotification["params"] (LogEntry.tsx:11), so the single cast now matches the consumer's own type exactly instead of routing through a whole-object double cast. Good tightening.
  • ToolsScreen — the formValuesnextFormValues rename is necessary and correct: at function scope the old name would shadow the formValues destructured from ui at line 161. The /* v8 ignore next */ still sits immediately above if (tool), so the ignore still lands on the intended line after reflow.
  • ServerConfigModal handler factories — the ChangeEvent<HTMLInputElement | HTMLTextAreaElement> param is contravariantly assignable to both TextInput's and Textarea's narrower onChange slots, so the one factory legitimately serves both. Identity churn per render is unchanged (inline arrows before, factory calls now). Nice that the currentTarget-nulling rationale got promoted from a per-callsite comment to one canonical spot.
  • NetworkEntry / ResourceControls / ServerImportConfigModal — prop-for-prop identical; SectionLabel c="red" and DimmedNote aria-live="polite" override/extend correctly through withProps. Text/Group remain imported and used elsewhere in each file, so no dead imports.

Notes

1. HeaderStackCell: BoxGroup is a display-model changeclients/web/src/components/groups/ViewHeader/ViewHeader.tsx:180-185

Box rendered a plain block div; Group renders display: flex; align-items: center; flex-wrap: wrap. The cell is grid-area: 1 / 1; place-self: center (App.css:348), and each instance has exactly one child (a SegmentedControl or a Title), so shrink-to-fit sizing should be identical in practice — but this is the one change in the PR that isn't purely mechanical. Worth an eyeball on the connect/disconnect crossfade if you haven't already. The swap itself is required (AGENTS.md forbids Box.withProps) and Group matches the file's existing RightConnectedGroup/ServerName className-in-withProps convention, so I wouldn't change the approach.

2. TextField union could derive from FormStateServerConfigModal.tsx:53

type TextField = Exclude<keyof FormState, "transport">;

Self-maintaining: adding a string field to FormState picks it up automatically, and adding a non-string field would surface as a type error at the factory rather than silently drifting from the hand-written list. Purely a maintainability nit — the current union is correct today. Fix this →

3. alignItems in styles vs. a theme variantServerListScreen.tsx:143

AGENTS.md's preference order puts theme variants above CSS classes, and a styles prop sits somewhere in between (Mantine emits it as an inline style on the root). Folding it into the pre-existing GRID_SURFACE_STYLES is clearly better than the standalone class, and this grid is a one-off, so I'd leave it — flagging only so the choice is deliberate rather than accidental if a future sweep re-reads this line as an inline style.

4. Pre-existing (not introduced here): messagesToLogEntries asserts params is present, but the JSONRPC notification type makes it optional — a malformed notifications/message with no params would yield params: undefined and blow up in LogEntry. The old double cast had the exact same hole, so this PR is neutral on it; mentioning it only in case it's worth a follow-up guard.

5. createRemoteLogger @ts-expect-error — agreed with the analysis. TS resolves the real pino/browser.js, so an ambient declare module "pino/browser.js" is shadowed and you land on TS7016 regardless. The expanded comment is the right outcome; it now explains why no better option exists, which is what the AGENTS.md escape-hatch policy asks for.

Nothing found on

any usage, config-level lint suppressions, dead CSS/imports, changed prop semantics, or coverage-ignore drift.


· branch v2/fix-1762-agents-md-sweep

Per review of PR #1771: replace the hand-written `TextField` union in
ServerConfigModal with `Exclude<keyof FormState, "transport">`, so it stays in
sync with FormState automatically — a new string field is picked up, and a
non-string field surfaces as a type error at the setter factory instead of
silently drifting from the list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Thanks for the thorough trace-through. Responses to each note:

1. HeaderStackCell Box → Group display-model change — Verified visually. Ran the dev server and screenshotted the header: the disconnected "MCP Inspector" title renders centered in the stack cell exactly as before, and the server cards below are correctly top-aligned (confirming the alignItems: "start" fold). The connected-state header (tab bar) uses the same HeaderStackCell constant with a single SegmentedControl child, so the shrink-to-fit sizing is identical — the swap is a visual no-op. The 20 ViewHeader unit tests plus the Storybook crossfade play-functions (which assert the mid-flight data-anim="out" state) also stay green.

2. TextField derive from FormState — Applied in 4e5e022. Now type TextField = Exclude<keyof FormState, "transport">; typecheck + the 27 ServerConfigModal tests pass. Agreed it's the self-maintaining form.

3. alignItems in styles vs. theme variant — Deliberate. This grid is a one-off and GRID_SURFACE_STYLES already existed as the home for its root-level overrides, so folding the property in there (rather than minting a theme variant for a single consumer) is the proportionate choice. Noted for the record.

4. Pre-existing params-optional hole in messagesToLogEntries — Correct that it's pre-existing and this PR is neutral on it (the old double cast had the identical assumption). Out of scope for a behavior-preserving sweep; I'll leave a follow-up note rather than widen this PR.

5. createRemoteLogger @ts-expect-error — Agreed, thanks for confirming the TS7016 reasoning independently.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 3m 20s —— View job


Code review — PR #1771 (re-review)

  • Read full diff vs origin/v2/main
  • Verify the new commit (4e5e022TextField derived from FormState)
  • Re-verify styling / structural / logic extractions
  • Re-verify TypeScript changes
  • Post review

Verdict

Approve-equivalent — one comment-accuracy nit, nothing blocking. The follow-up commit 4e5e022 lands the TextField derivation cleanly, and everything else re-verifies as behavior-preserving. One finding below: a claim in the new comment doesn't hold under TypeScript, and I checked it against a compiler rather than reasoning about it.

Finding — the TextField comment overstates what the type guarantees

clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.tsx:50-54

// … adding a string field picks it up, and adding a
// non-string field would surface as a type error at the factory instead of
// silently drifting.
type TextField = Exclude<keyof FormState, "transport">;

The first clause is true. The second isn't. A computed property whose key is a union of literals doesn't narrow the assignment — TS widens the literal to an index signature and the spread keeps the declared property types, so the result still satisfies FormState and nothing errors. Verified with tsc --strict:

interface FormState { id: string; transport: "stdio" | "sse"; command: string; port: number; }
type TextField = Exclude<keyof FormState, "transport">;   // includes "port"
const setTextField = (field: TextField) => (v: string) =>
  setForm((f): FormState => ({ ...f, [field]: v }));       // no error
setTextField("port");                                      // no error — a string lands in a number field

exit=0. So a future non-string FormState field would be silently accepted by setTextField, which is exactly the drift the comment says it prevents. (Same exposure the hand-written union had, so this is not a regression — the code is fine today, the comment just promises more than it delivers.)

Two ways to close it, either is fine:

(a) Trim the comment to the claim that holds — "derived so a new string field is picked up automatically."

(b) Make the type actually enforce it — select on the value type instead of excluding by name. Verified this does error at the call site (TS2345: Argument of type '"port"' is not assignable to parameter of type 'TextField'):

// Keys whose value is exactly `string` — excludes `transport`, whose value is a
// literal union, and any future non-string field.
type PlainStringKeys<T> = { [K in keyof T]-?: string extends T[K] ? K : never }[keyof T];
type TextField = PlainStringKeys<FormState>;

This also drops the hand-written "transport" from the type. Fix this →

Re-verified clean

  • 4e5e022 (TextField)FormState's six non-transport fields are all string, so Exclude<keyof FormState, "transport"> is exactly the previous hand-written union. Correct today.
  • setTextField / clearTextFieldChangeEvent<HTMLInputElement | HTMLTextAreaElement> is contravariantly assignable to both TextInput's and Textarea's narrower onChange slots, so one factory legitimately serves both. Handler identity churn per render is unchanged (inline arrows before, factory calls now). The currentTarget-nulling rationale reads better as one canonical comment than six copies.
  • App.tsx castLogEntryData.params is declared LoggingMessageNotification["params"] (clients/web/src/components/elements/LogEntry/LogEntry.tsx:11), so the single cast now targets the consumer's own type exactly instead of routing the whole object through unknown. Genuine tightening, not just shorter.
  • .grid-align-start removal — zero remaining references anywhere in the repo (searched all file types incl. tests/stories). className and styles.root both target SimpleGrid's root, and styles emits an inline style that outranks the deleted class, so no specificity regression.
  • HeaderStackCell (BoxGroup) — the two ViewHeader cells sit at grid-area: 1 / 1; place-self: center (App.css:348-351), so both a block div and a flex row shrink-to-fit identically with a single child; gap/wrap are inert at one child. The 20 ViewHeader tests locate the cell via closest("[data-anim]") (ViewHeader.test.tsx:240,265,327,…) — element- and class-agnostic, so the swap can't silently break them. Combined with your visual check, I'm satisfied.
  • NetworkEntry / ResourceControls / ServerImportConfigModal — prop-for-prop identical; SectionLabel c="red" overrides and DimmedNote aria-live="polite" extends correctly through withProps; Text/Group/Box all still used elsewhere in each file, so no dead imports.
  • ToolsScreen — the formValuesnextFormValues rename is required (the old name would shadow the formValues destructured from ui), and /* v8 ignore next */ still sits immediately above if (tool) after the reflow.
  • createRemoteLogger — agreed on the TS7016 reasoning; the expanded comment now explains why no better option exists, which is what the AGENTS.md escape-hatch policy asks for.

Nothing found on

any usage, config-level lint suppressions, dead CSS, changed prop semantics, coverage-ignore drift, or new inline styles.


· branch v2/fix-1762-agents-md-sweep

Per re-review of PR #1771: the `Exclude<keyof FormState, "transport">` comment
overclaimed — a computed union key widens to an index signature, so a future
non-string field would NOT error at the setter factory. Replace with a mapped
type that selects keys by value (`PlainStringKeys<FormState>`), which actually
makes passing a non-string key a compile error at the call site and drops the
hand-written "transport" exclusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Good catch — you're right that the computed union key widens to an index signature, so the "type error at the factory" claim didn't hold. Applied your option (b) in 6a7b85d: TextField is now PlainStringKeys<FormState> (selects keys whose value is exactly string), which drops the hand-written "transport" and actually makes passing a non-string key a compile error at the call site — the guarantee the comment now describes. Typecheck + the 27 ServerConfigModal tests pass.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 2m 54s —— View job


Code review — PR #1771 (re-review after 6a7b85d1)

  • Read full diff vs origin/v2/main
  • Verify the new commit (6a7b85d1PlainStringKeys<FormState>) against a real compiler
  • Re-verify styling / structural / logic / TypeScript changes
  • Post review

Verdict

Approve-equivalent. 6a7b85d1 closes the previous finding properly — I re-ran the enforcement claim through tsc --strict rather than reasoning about it, and it now holds. One new (minor, optional) finding below: a residual instance of the exact rule this PR applied elsewhere.

Verified: PlainStringKeys genuinely enforces what the comment claims

clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.tsx:51-60

I compiled the pattern with a FormState deliberately containing a number field, an optional string, and the literal-union transport:

interface FormState { id: string; transport: "stdio" | "sse"; command: string; port: number; maybe?: string; }
type PlainStringKeys<T> = { [K in keyof T]-?: string extends T[K] ? K : never }[keyof T];
type TextField = PlainStringKeys<FormState>;
const setTextField = (field: TextField) => (v: string) => setForm((f) => ({ ...f, [field]: v }));

setTextField("id");         // ok
setTextField("maybe");      // ok — `-?` strips undefined, so `string extends string`
// @ts-expect-error port is a number
setTextField("port");
// @ts-expect-error transport is a literal union
setTextField("transport");

exit=0 with both @ts-expect-errors consumed (an unused one would have failed the run), so the errors really fire. This is the enforcing version, and the hand-written "transport" exclusion is correctly gone. FormState's six non-transport fields are all plain string, so TextField resolves to exactly the previous set — no call-site churn.

Pedantic footnote, not a finding: string extends T[K] also admits unknown and any-typed keys. Neither exists in FormState and neither would ever be added for a text input, so the comment reads true in practice.

Finding (minor) — the same flat-CSS-class rule is left unapplied on the constant this PR just created

clients/web/src/components/groups/ViewHeader/ViewHeader.tsx:181-185 · clients/web/src/App.css:348-351

The PR's styling change removes .grid-align-start because a flat property (align-items) doesn't belong in App.css. HeaderStackCell — a constant introduced by this PR — carries a class whose rule is flat-only, with no pseudo-selector or keyframe companion:

.header-stack-cell { grid-area: 1 / 1; place-self: center; }

Contrast the file's other classes, which all earn their place and say so in a comment: .header-anim is keyframe/attribute-selector driven, .server-drag-handle and .resize-handle are cursor + :hover/:active, .markdown-content is nested third-party selectors. .header-stack-cell is the only one in the className:-in-withProps set that is pure flat CSS. Per AGENTS.md ("flat CSS properties … belong in the theme"), it's the same finding the PR fixed one file over:

const HeaderStackCell = Group.withProps({
  className: "header-anim",
  styles: { root: { gridArea: "1 / 1", placeSelf: "center" } },
});

I checked the blast radius: zero references to header-stack-cell in any test or story, and the prefers-reduced-motion block targets only .header-anim[data-anim=…], so the class can be deleted from App.css without touching the reduced-motion path. The prose comment at ViewHeader.tsx:339 mentions it by name and would need a word change.

Low stakes and entirely defensible to leave — but this is a sweep PR whose stated deliverable is catching exactly this, so it's worth either fixing or recording as deliberate. Fix this →

Re-verified clean

  • .grid-align-start removal — zero remaining references repo-wide (all file types, incl. tests/stories). className and styles.root both land on SimpleGrid's root, and styles emits an inline style that outranks the deleted class, so no specificity regression.
  • App.tsx cast"method" in m.message narrows to JSONRPCRequest | JSONRPCNotification (core/mcp/types.ts:281-284), both of which expose params, and LogEntryData.params is declared as LoggingMessageNotification["params"]. The single cast now targets the consumer's own type instead of routing the whole object through unknown — a real tightening, not just shorter.
  • ServerConfigModal factoriesChangeEvent<HTMLInputElement | HTMLTextAreaElement> is contravariantly assignable to both TextInput's and Textarea's narrower onChange slots, so one factory legitimately serves both; clearTextField's returned thunk ignoring ClearButton's MouseEvent arg is fine. Handler identity churn per render is unchanged (inline arrows before, factory calls now). The currentTarget-nulling rationale reads better as one canonical comment than six copies.
  • HeaderStackCell (BoxGroup) — both cells sit at grid-area: 1 / 1; place-self: center, so block-div and flex-row shrink-to-fit identically with a single child; gap/wrap are inert at one child. Tests locate the cell via closest("[data-anim]"), which is element- and class-agnostic. Combined with your screenshot check, settled.
  • NetworkEntry / ResourceControls / ServerImportConfigModal — prop-for-prop identical; SectionLabel c="red" overrides and DimmedNote aria-live="polite" extends correctly through withProps. Text/Group/Box all still used elsewhere in each file — no dead imports.
  • ToolsScreen — the formValuesnextFormValues rename is required (the old name would shadow the formValues destructured from ui), and /* v8 ignore next */ still sits immediately above if (tool) after the reflow.
  • createRemoteLogger — agreed on TS7016; the expanded comment now explains why no better option exists, which is what the AGENTS.md escape-hatch policy asks for.

Nothing found on

any usage, config-level lint suppressions, dead CSS, changed prop semantics, coverage-ignore drift, or new inline styles.

Caveat: this checkout has no node_modules, so I did not run the suite — I'm taking your npm run ci result at face value. The tsc verification above was run against a standalone TypeScript 5.9 install.

--- · branch v2/fix-1762-agents-md-sweep

…1762)

Per re-review of PR #1771: `.header-stack-cell` was flat-only CSS (grid-area,
place-self) with no pseudo-selector/keyframe companion — the same class of
finding this sweep fixed when it removed `.grid-align-start`. Fold it into the
`HeaderStackCell` withProps constant's `styles.root` and delete the App.css
rule, keeping the keyframe-driven `header-anim` class. The reduced-motion block
targets only `.header-anim[data-anim]`, so it's unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Nice consistency catch — you're right that .header-stack-cell was flat-only CSS and is exactly what this sweep is meant to fix, especially on a constant the PR itself introduced. Applied in 553e874: folded grid-area/place-self into HeaderStackCell's styles.root, kept the keyframe-driven header-anim class, deleted the App.css block, and updated the two prose comments that named the class. The prefers-reduced-motion block only targets .header-anim[data-anim=…], so it's untouched. validate:web (3472 unit tests) + the ViewHeader Storybook crossfade play-tests pass.

Also confirmed your PlainStringKeys re-verification — thanks for compiling the @ts-expect-error cases rather than reasoning about them.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 3m 23s —— View job


Code review — PR #1771 (re-review after 553e8742)

  • Read full diff vs origin/v2/main
  • Verify the new commit (553e8742.header-stack-cell fold)
  • Re-verify styling / structural / logic / TypeScript changes
  • Post review

Verdict

Approve-equivalent — no blocking issues. 553e8742 closes the last finding correctly, and I verified the fold can't regress the animation rather than assuming it. One observation below about the criterion the sweep applied, worth recording on the issue so a future audit doesn't re-derive a different (much larger) set from the same rule.

Verified: the .header-stack-cell fold is safe, including the animation path

clients/web/src/components/groups/ViewHeader/ViewHeader.tsx:181-186 · clients/web/src/App.css

The concern with moving CSS-class properties into Mantine's styles prop is that styles emits an inline style attribute, which outranks any stylesheet rule for the same property — exactly the hazard your own src/theme/Group.ts:10-11 comment warns about (#1460). I checked whether that applies here and it does not:

  • .header-anim[data-anim="in"|"out"] (App.css:338-344) sets only animation; the keyframes inspector-fade-slide-in/-out (App.css:280-300) animate only opacity and transform.
  • The folded properties are grid-area and place-self.

Disjoint sets — the inline style can't shadow the animation, and the prefers-reduced-motion block (App.css:349-354) targets .header-anim[data-anim=…] { animation: none }, which is likewise untouched. Keeping className: "header-anim" (keyframe-driven → legitimately CSS) while moving the flat properties is the right split per AGENTS.md.

Also checked:

  • Zero dangling references to header-stack-cell or grid-align-start anywhere under clients/, core/, docs/, specification/ — all file types, incl. tests and stories.
  • Prose comments are accurate. ViewHeader.tsx:340-341 now says "grid-area 1/1, set on HeaderStackCell"; the three surviving header-anim mentions (:60, :153, :182) all still describe a real class.
  • Box is still used in the file (:344 grid stack, :445 Transition interpolation), so no dead import.

Observation — the sweep applied a repetition criterion, not AGENTS.md's literal one

Not a defect in this PR, but worth pinning down because #1762 is framed as a codebase-wide audit and the next person re-running it will get a different answer.

AGENTS.md's literal rule is "ALWAYS declare a meaningfully named subcomponent as a constant using .withProps() if a component has two or more props." The sweep instead extracted elements repeated within a file — which is the pragmatic reading, and it was applied consistently. But under the literal reading there are ~30 remaining multi-prop inline Mantine elements across clients/web/src/components, three of them in files this PR touched:

Location Element
NetworkEntry.tsx:518 <Text span ff="monospace">
NetworkEntry.tsx:569 <Text size="xs" ff="monospace" c="red">
ServerConfigModal.tsx:425 <Text c="red" size="sm" role="alert">

Each is a single occurrence within its file, so leaving them is consistent with the criterion actually used — NetworkEntry.tsx:569 sitting directly under the newly-extracted SectionLabel c="red" just makes the boundary visible. There's also cross-file repetition the within-file criterion structurally can't catch (<Group justify="space-between" mb="sm"> ×5 in TaskListPanel / LogStreamPanel / ProtocolListPanel / NetworkStreamPanel / ConsoleScreen; <Text c="red" size="sm" role="alert"> ×2 in ServerConfigModal / ServerRemoveConfirmModal).

Suggestion: rather than widening this PR, record the interpretation — either note on #1762 that "repeated within a file" is the operative bar, or tighten the AGENTS.md wording so the rule and the practice agree. As written, the two diverge by ~30 instances.

Re-verified clean

  • PlainStringKeys<FormState> (ServerConfigModal.tsx:51-60) — FormState's six non-transport fields are all plain string and transport is the literal union "stdio" | "sse" | "streamable-http", so string extends T[K] excludes it and TextField resolves to exactly the previous set — no call-site churn. Matches last round's tsc --strict check (both @ts-expect-errors consumed), so the comment's enforcement claim now holds.
  • setTextField / clearTextFieldChangeEvent<HTMLInputElement | HTMLTextAreaElement> is contravariantly assignable to both TextInput's and Textarea's narrower onChange slots; clearTextField's thunk correctly ignores ClearButton's MouseEvent. Handler identity churn per render is unchanged (inline arrows before, factory calls now).
  • App.tsx cast"method" in m.message narrows to the request/notification arm, both of which expose params; LogEntryData.params is declared LoggingMessageNotification["params"], so the single cast now targets the consumer's own type instead of routing the whole object through unknown. Real tightening.
  • .grid-align-startGRID_SURFACE_STYLESclassName and styles.root both land on SimpleGrid's root; styles emits inline, which outranks the deleted class, so no specificity regression. alignItems: "start" type-checks against csstype's SelfPosition even under as const.
  • BoxGroup on HeaderStackCell — with justify-self: center from place-self, both a block div and a flex row shrink-to-fit; gap/wrap/align-items are inert at one child, and preventGrowOverflow only engages under grow, which isn't set. Tests locate the cell via closest("[data-anim]"), element- and class-agnostic. Combined with your screenshot, settled.
  • NetworkEntry / ResourceControls / ServerImportConfigModal — prop-for-prop identical; SectionLabel c="red" overrides and DimmedNote aria-live="polite" extends correctly (Mantine's withProps spreads call-site props last). No dead imports.
  • ToolsScreen — the formValuesnextFormValues rename is required (the old name would shadow the formValues destructured from ui), and /* v8 ignore next */ still sits immediately above if (tool), so the ignored branch is unchanged.
  • createRemoteLogger — agreed on TS7016; the expanded comment explains why no better option exists, which is what the AGENTS.md escape-hatch policy asks for.

Nothing found on

any usage, config-level lint suppressions, dead CSS, changed prop semantics, coverage-ignore drift, or new inline style={{…}} props.

Caveat: this checkout has no node_modules, so I did not run the suite — taking your npm run ci result at face value. All CSS/type reasoning above was checked against the files in the branch.

--- · branch v2/fix-1762-agents-md-sweep

@cliffhall

Copy link
Copy Markdown
Member Author

Thanks — glad the animation-path check confirmed the fold is safe (the grid-area/place-self vs opacity/transform disjointness is exactly why keeping header-anim as a class while moving only the flat props is the right split).

On the observation: agreed it's a criterion question, not a PR defect, and I've taken your suggestion to record rather than widen. Posted an interpretation note on #1762 documenting that the operative bar for .withProps() extraction in this sweep was repetition within a file (not the literal "≥2 props" reading), including the in-scope / deliberately-skipped / cross-file-only buckets and the ~30-instance gap. I flagged tightening the AGENTS.md wording as a maintainer-call follow-up since it edits the contract doc.

That leaves this PR with no outstanding code changes — all four review rounds' actionable findings are applied.

…ps() (#1762)

Applies the AGENTS.md subcomponent rule strictly across the entire web client:
every inline Mantine element carrying two or more static (literal) props is now a
named per-file `.withProps()` constant, with dynamic props passed at the call
site. This closes the ~170-instance gap between the rule's wording and prior
practice (the earlier sweep only extracted repeated-within-a-file patterns).

Scope: ~77 files across components/{elements,groups,screens,views} + App.tsx.
Also tightens the AGENTS.md rule wording (in this PR, not a follow-up) to state
explicitly that it applies in all cases including single-use elements, and that
"static props" means literal-valued props (dynamic props are passed at the call
site, not counted).

Box handling per the rule: ResizeHandle and ScreenStage Boxes converted to named
Flex constants; the App.tsx reauth-banner Box converted to a named Paper
constant; the ViewHeader `display:grid` container and the AppRenderer iframe Box
left inline with justifying comments (no Mantine primitive fits).

Two constructs can't be expressed via `.withProps()` and stay inline with a
justifying comment (same class of exemption as Box): `Accordion` (a compound,
`multiple`-discriminated generic whose `.withProps()` loses its JSX call
signature), and `data-*` attributes (not part of a component's typed props, so
passed at the call site).

Verified: `npm run ci` green end to end — 3472 unit tests, the per-file >=90
coverage gate (98.58% overall), all smokes incl. the headless-browser boot, and
456 Storybook play-tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Full strict sweep applied (dba00b5)

Per maintainer direction, the AGENTS.md subcomponent rule is now applied strictly across the entire web client — every inline Mantine element with 2+ static (literal) props is a named per-file .withProps() constant, not just repeated-within-a-file patterns. An exhaustive 4-way audit found ~170 offenders across ~50 files (≈6× the earlier estimate); all are fixed. 77 files changed.

Also tightened the AGENTS.md wording in this PR (not deferred): the rule now states explicitly that it applies in all cases including single-use elements, and defines "static props" as literal-valued (dynamic props are passed at the call site, not counted).

Box handling

  • ResizeHandle, ScreenStage → converted Box → named Flex.withProps() constants.
  • App.tsx reauth-banner → Box → named Paper.withProps().
  • ViewHeader display:grid container and AppRenderer iframe Box → left inline with justifying comments (no Mantine primitive fits).

Two constructs that can't use .withProps() (left inline, commented — same exemption class as Box)

  • Accordion — a compound, multiple-discriminated generic; .withProps({ multiple: true, … }) loses its JSX call signature and fails to type (TS2604/2786/2322). Verified with tsc.
  • data-* attributes (data-testid, data-autofocus) — not part of a component's typed props, so .withProps({ "data-testid": … }) is rejected (TS2353); passed at the call site instead.

(These two were the only type-level fallout from the mechanical extraction; the agents' vite-based unit tests don't typecheck, so I caught them in the project tsc -b pass and corrected them.)

Verification

npm run ci green end to end: 3472 unit tests, the per-file ≥90 coverage gate (98.58% overall), all smokes including the headless-browser boot (clean first render across the converted Boxes), and 456 Storybook play-tests.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 6m 2s —— View job


Code review — PR #1771 (re-review after dba00b5f)

  • Read the new commit's diff (dba00b5f — full strict sweep, 77 files)
  • Verify the AGENTS.md wording change matches the applied practice
  • Spot-check the mechanical extractions for prop/behavior drift
  • Check the Box conversions and the documented exemptions (Accordion, data-*)
  • Post review

Verdict

One thing I'd fix before merge, plus a few cleanups. The 179 extractions are overwhelmingly prop-for-prop faithful, and the AGENTS.md wording now matches the practice. But the sweep's Box → Flex conversions are the one non-mechanical class of change, and ScreenStage swaps the display model of the container that every screen renders inside — that one looks like a real layout regression, not a no-op. There's also a garbled comment and four orphaned JSDoc blocks.

I verified the extractions are structurally safe first: every one of the 179 .withProps() constants is declared at module scope (I checked for indented declarations — zero), so none creates a fresh component type per render. Only two classNames appear in added code (resize-handle, header-anim), and neither collides with a call-site className.


1. ScreenStage: Box → Flex changes how every screen is sized — clients/web/src/components/elements/ScreenStage/ScreenStage.tsx:11-16

const StageLayer = Flex.withProps({
  pos: "absolute", top: 0, left: 0, right: 0,
});

Box rendered a plain div (display: block); Flex renders display: flex; flex-direction: row. StageLayer itself is still full-width (absolute with both left and right set + width: auto fills the containing block — shrink-to-fit doesn't apply). The problem is its child:

  • Before: the child was a block-level box in a block container → width: autofills 100%.
  • After: the child is a flex item in a row container → flex: 0 1 auto with flex-grow: 0 → width is content-based, and only shrinks (never grows) to the container.

Every screen mounts through this. ToolsScreen, ServerListScreen, ConsoleScreen, … all root at ScreenLayout = Flex.withProps({ variant: "screen", h: "calc(…)", … }) — none sets w, and the screen variant (src/theme/Flex.ts) only adds overflow: hidden. So each screen now sizes to its own max-content instead of the viewport.

ServerListScreen is the sharpest case, because its grid contributes zero intrinsic width:

const ServerGrid = SimpleGrid.withProps({ type: "container",});  // ServerListScreen.tsx:170

type="container" applies container-type: inline-sizecontain: inline-size, which makes the element's inline size compute without regard to its contents. So the grid's max-content contribution is 0, and ScreenLayout's content-based width collapses to roughly the header row (Servers title + the Export/Add/toggle controls) — a few hundred px on a wide viewport, where it previously filled it.

Neither the test suite nor Storybook can catch this: happy-dom does no layout, and the stories render the screens directly rather than through ScreenStage. The headless-browser smoke asserts the "Add Servers" control renders, not its width. So a green npm run ci is consistent with the regression.

Minimal faithful fix — make it a column flex. In a column flex the cross axis is the width and align-items: stretch (Mantine's default) makes the single child fill it, while the main axis (height) stays content-sized. That reproduces block layout exactly:

const StageLayer = Flex.withProps({
  direction: "column",
  pos: "absolute", top: 0, left: 0, right: 0,
});

(w: "100%" also works; direction: "column" additionally avoids the row-flex align-items: stretch forcing the child to the container's height in the fill case.) Worth an actual browser check on the Servers tab at a wide viewport either way.

I checked the sibling conversion and it's fine: ResizeHandle's Flex is childless with an explicit width: 6px from .resize-handle (App.css:590-597), so its display model is inert.

Fix this →


2. Garbled comment — two notes spliced mid-sentence — clients/web/src/components/groups/ResourceControls/ResourceControls.tsx:39-45

// Full-height, multi-open accordion for the resource sections.
// `transitionDuration: 0` disables Mantine's panel height animation: its
// Collapse drives open/close via an inline `height` that briefly jumps the panel
// to its full natural height, fighting the flex sizing (the panels are
// Accordion is a compound, `multiple`-discriminated generic component, so
// `.withProps({ multiple: true, ... })` loses its call signature and can't type
// — it stays inline (same tooling limit as Box).

export interface ResourceControlsProps {

The sentence breaks at "(the panels are" and the Accordion-exemption note is pasted over the remainder — clearly the residue of an extracted-then-reverted constant. It also now sits directly above export interface ResourceControlsProps, so IDE hover on the interface shows the Accordion note. (The transitionDuration rationale itself survives, correctly abbreviated, at the call site — :250-252.) Deleting the orphan block or restoring the two as separate comments both work. [Fix this →](https://claude.ai/code?q=In%20clients%2Fweb%2Fsrc%2Fcomponents%2Fgroups%2FResourceControls%2FResourceControls.tsx%20on%20branch%20v2%2Ffix-1762-agents-md-sweep%2C%20the%20comment%20block%20at%20lines%2039-45%20is%20garbled%20-%20a%20sentence%20about%20transitionDuration%20breaks%20mid-phrase%20at%20'(the%20panels%20are'%20and%20the%20Accordion-exemption%20note%20is%20spliced%20over%20the%20rest%2C%20and%20the%20whole%20block%20now%20dangles%20above%20export%20interface%20ResourceControlsProps.%20The%20transitionDuration%20rationale%20already%20lives%20at%20the%20call%20site.%20Clean%20this%20up%20so%20only%20a%20coherent%20Accordion-exemption%20note%20remains%20(or%20delete%20the%20block).%20Then%20run%20npm%20run%20validate%3Aweb.&repo=modelcontextprotocol/inspector)


3. Four exported components lost their JSDoc to the new constant

Inserting the .withProps() constant between a component's doc comment and the component re-targets the doc:

File Doc now attached to Doc obviously describes
ExpandToggle.tsx:18-29 ExpandActionIcon "…aria-expanded exposes the disclosure state and ariaLabel can distinguish sibling toggles"
PinToggle.tsx:10-19 PinActionIcon "…the aria-label stays 'Pin'/'Unpin'"
MonitoringToggle.tsx:14-27 MonitoringActionIcon "…the caller only mounts it when the sidebar is available"
ReplayButton.tsx:9-19 ReplayActionIcon "Icon form of the 'Replay' action, used in the compact (column) ProtocolEntry layout"

Hovering the exported component in an editor now shows nothing; hovering the styling constant shows the component's docs. The sweep got this right everywhere else (McpErrorBadge, FilterToggleButton, EmbeddableScrollArea, SubscriptionStreamBadge, … all place the constant above the doc block) — these four are the outliers. Moving the constant above the /** fixes all four. Fix this →


4. Minor: Box → Paper adds a border-radius to the full-bleed re-auth bar — clients/web/src/App.tsx:398-405

Paper's root carries border-radius: var(--paper-radius), defaulting to the theme's defaultRadius: "md" (src/theme/theme.ts:172) — the previous Box had none. On a pos="sticky" full-width bar that means rounded bottom corners under the box-shadow. Small, but a visible delta rather than a refactor.

The shadow is safe: .reauth-banner-bar { box-shadow: var(--mantine-shadow-sm) } (App.css:578-581) and Paper's box-shadow: var(--paper-shadow) are both single-class specificity, and App.css is imported after @mantine/core/styles.css (main.tsx:5-7), so the class wins. bg was already pinned to --mantine-color-body, which is Paper's default anyway. Adding radius: 0 to the constant restores the old geometry.

5. Nits

  • ServerConfigModal.tsx:68-75IdTextInput and RequiredTextInput are byte-identical (required: true, rightSectionPointerEvents: "auto"). The ID field's only distinguishing props (label, description, placeholder, data-autofocus) are all passed at the call site, so one constant covers both.
  • ServerConfigModal.tsx:394-409 — the "Working directory" TextInput still carries inline placeholder="(inherit)" + rightSectionPointerEvents="auto" (two static props) while all five of its siblings in the same file were extracted. ClearableTextInput — the exact single-prop constant this needs — already exists in ServerSettingsForm and ClientSettingsForm.

Re-verified clean

  • AGENTS.md wording — the new rule text ("static prop … a single-use element with two or more static props must still be extracted") matches what the commit actually did, and the static/dynamic split it defines is the one applied throughout (value/on*/children/key consistently at the call site). Tightening the contract in the same PR rather than deferring is the right call.
  • Component identity — all 179 constants at module scope; no per-render component types, so no remount/focus-loss class of bug.
  • Prop fidelity, spot-checked across all 77 filesControlsCluster genuinely equals the <Group gap="sm" wrap="nowrap"> it replaced in NetworkEntry's wide layout (:87-96); SectionLabel c="red", DimmedNote aria-live="polite", SessionBadge color="green", ScopeBadge color={…} all override/extend correctly (call-site props spread last). No call site passes an explicit undefined for a prop the constant bakes, which is the one way withProps can silently drop a default.
  • className handling — only resize-handle and header-anim are baked, neither collides, and HeaderStackCell's styles.root (gridArea/placeSelf) stays disjoint from the keyframes' opacity/transform, so the crossfade is unaffected (as established last round).
  • Documented exemptionsAccordion (compound + multiple-discriminated generic) and data-* attributes (not in the typed props object, so excess-property-checked out of a withProps literal) are both genuine TS limits, correctly left inline with comments. ViewHeader's display="grid" Box and AppRenderer's component="iframe" Box are likewise correctly exempted — no Mantine primitive is a grid, and the iframe is a native element.
  • Logic extractionsToolsScreen.handleSelectTool keeps /* v8 ignore next */ immediately above if (tool); the formValuesnextFormValues rename is still required to avoid shadowing ui.formValues.
  • PlainStringKeys<FormState> / App.tsx cast / .grid-align-start / createRemoteLogger — unchanged since the last round and still correct.

Nothing found on

any usage, config-level lint suppressions, dead CSS or dead imports, coverage-ignore drift, new inline style={{…}} props, or raw hex/rgba() literals.

Caveat: this checkout has no node_modules, so I did not run the suite or a browser — finding 1 is reasoned from the CSS box model plus the files in the branch, and is exactly the class a headless first-render smoke can't see. Please confirm it visually before merge.

--- · branch v2/fix-1762-agents-md-sweep

Follow-up to dba00b5, per PR #1771 review:

- ScreenStage: the Box→Flex conversion made the child screen a row-flex item
  (content-sized) instead of a block child (full-width) — a real layout
  regression (ServerListScreen's container-typed grid has zero intrinsic width,
  so it collapsed to ~header width). Add `direction: "column"` so the width
  moves to the cross axis where `align-items: stretch` fills it. Verified
  visually: the Servers screen renders full-width again.
- ResourceControls: remove the garbled comment left by the reverted Accordion
  constant (the transitionDuration rationale already lives at the call site);
  add a coherent Accordion-exemption note there.
- ExpandToggle/PinToggle/MonitoringToggle/ReplayButton: move the new .withProps()
  constant above each exported component's JSDoc so the doc documents the
  component again, not the constant.
- App.tsx ReAuthBannerBar: add `radius: 0` — Paper's default `md` radius would
  round the full-bleed sticky bar's corners; the Box it replaced had none.
- ServerConfigModal: collapse the byte-identical IdTextInput into
  RequiredTextInput; extract the still-inline "Working directory" field via a
  ClearableTextInput constant.

Verified: `npm run ci` green end to end (3472 unit tests, >=90 coverage gate,
smokes incl. headless-browser boot, 456 Storybook play-tests) + a manual
wide-viewport browser check of the Servers screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

All five findings addressed in 5e175b3. Thank you for the box-model analysis on finding 1 — that was a real regression a first-render smoke can't see.

1. ScreenStage layout regression — Fixed and visually verified. Added direction: "column" to StageLayer so the width sits on the cross axis, where Mantine's default align-items: stretch makes the single child fill it (reproducing the old block behavior). I ran the dev server and checked the Servers tab at a wide viewport: the grid + surface panel span the full width again, matching the pre-sweep baseline. Confirmed ResizeHandle's Flex is inert (childless, fixed width: 6px), so it needed no change. Left a load-bearing comment on the direction prop.

2. Garbled ResourceControls comment — Removed the spliced orphan block; the transitionDuration rationale stays at the call site, and the Accordion-exemption note now sits as a coherent comment directly above the inline <Accordion>.

3. Four orphaned JSDoc blocks — Moved the .withProps() constant above the doc in ExpandToggle, PinToggle, MonitoringToggle, and ReplayButton, so each doc documents its exported component again (matching how McpErrorBadge/FilterToggleButton do it).

4. Paper border-radius on the reauth bar — Added radius: 0 to ReAuthBannerBar so the full-bleed sticky bar keeps square corners like the Box it replaced.

5. ServerConfigModal nits — Collapsed the now-byte-identical IdTextInput into RequiredTextInput (the ID field passes data-autofocus at the call site); extracted the "Working directory" field via a new ClearableTextInput constant so it no longer carries 2 inline static props.

npm run ci green end to end, plus the manual wide-viewport check of the Servers screen.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 5m 48s —— View job


Code review — PR #1771 (re-review after 5e175b37)

  • Read the latest commit's diff (5e175b37 — review fixes)
  • Verify the five prior findings are correctly closed
  • Fresh pass over the full sweep for anything not yet covered
  • Post review

Verdict

Approve-equivalent — nothing blocking. All five findings from the last round are correctly closed, and I re-derived the ScreenStage fix from the box model rather than taking it on the commit message. Three residual items below, all in the "a sweep PR should probably own this" category rather than defects.

Verified: the five fixes

  • ScreenStage direction: "column" (ScreenStage.tsx:16-22) — this is the right fix, and I checked both call paths. In a column flex the cross axis is horizontal, so Mantine's default align-items: stretch gives the single child width: 100% — exactly what the old block child had. The main axis stays content-sized (flex-basis: auto, flex-grow: 0), matching height: auto. I also checked the fill path (MonitoringScreen.tsx:67, bottom: 0 → definite container height): a column flex item's min-height: auto resolves to its content-based minimum, so it can't shrink below content the way flex-shrink: 1 alone would suggest — the overflow behavior matches the old block layout too. ResizeHandle's Flex is genuinely inert (childless; .resize-handle sets flex: 0 0 auto; width: 6px, App.css:590-597).
  • ResourceControls comment — the spliced block is gone and the Accordion note now sits directly above the <Accordion> (:237-239); the transitionDuration rationale survives at :247-248. Coherent.
  • Four JSDoc blocks — I re-scanned every touched .tsx for a */ immediately followed by a const X = Y.withProps( and got zero hits, so all four are fixed and none were introduced elsewhere. MonitoringToggle correctly split the size={36} sentence out of the component doc into a note on the constant.
  • radius: 0 on ReAuthBannerBar — restores the Box geometry. .reauth-banner-bar (App.css:578-581) only sets z-index + box-shadow, both unaffected.
  • ServerConfigModalIdTextInput/RequiredTextInput collapse is byte-equivalent (the ID field's data-autofocus/label/description/placeholder were already call-site props), and ClearableTextInput matches the same-named constant in ServerSettingsForm/ClientSettingsForm. ArgsTextarea/EnvTextarea/RequiredTextInput/ClearableTextInput are all still referenced — I checked every declared constant across all 80 files for dead declarations and found none.

1. The strict sweep left a handful of offenders behind — one in a file it touched

I scripted the rule the PR just codified (JSX-tag scan → count literal-valued styling/layout/behavior props, excluding value/on*/children/key/ref/style/component/error/*Section) over all 365 non-test/non-story .tsx in clients/web/src. After discarding the documented exemptions (Box, Accordion) and comment-text false positives, these survive:

Location Element Static props
clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.tsx:59 <Stack> flex={1} mih={0} gap={0}
clients/web/src/components/groups/ToolResultPanel/ToolCallErrorPanel.tsx:73 <Title> order={3} size="h4"
clients/web/src/components/groups/ToolResultPanel/ToolCallErrorPanel.tsx:77 <Alert> color="red" variant="light"
clients/web/src/components/groups/SortableServerCard/SortableServerCard.tsx:33 <ActionIcon> variant="subtle" color="gray" size="md" className
clients/web/src/components/elements/ContentViewer/CsvTable.tsx:52 <Table> striped highlightOnHover withTableBorder withColumnBorders
clients/web/src/components/elements/ContentViewer/BinaryNotice.tsx:18 <Code> block p={36}

The EmbeddableScrollArea one is the notable one — this PR edited that file, extracting StreamScrollArea three lines below while leaving the wrapper <Stack> inline. The other five are in files the sweep didn't open, and two of them are near-misses by adjacency: ToolCallErrorPanel.tsx sits in the same folder as the swept ToolResultPanel.tsx, and its <Title order={3} size="h4"> is the identical pattern the sweep extracted as PanelTitle (ServerListScreen) and ToolsTitle (ToolControls); SortableServerCard.tsx sits beside the swept ServerCard.tsx.

Separately, there's a boundary worth stating out loud rather than leaving to the next auditor: ~20 <Checkbox label="…" description="…"> / <Select label="…" description="…" allowDeselect={false}> in ServerSettingsForm/ClientSettingsForm are literal-valued but are content, not "styling/layout/behavior" — so excluding them is consistent with the AGENTS.md text as written. The sweep did bake literal label/placeholder elsewhere (ExcludedDivider's label="Excluded (SEP-2243)", SearchInput's placeholder="Search tools..."), so the line is real but implicit. Either is defensible; it just isn't written down.

Fix this →

2. The Accordion-exemption comment still dangles in three other files

ResourceControls got this fixed; the same note is stranded in the module-scope constant block of three more files, nowhere near the <Accordion> it explains:

  • clients/web/src/components/groups/ClientSettingsForm/ClientSettingsForm.tsx:60-62 — sits between ErrorAlert and ClearableTextInput, ~115 lines above the <Accordion> at :176, and immediately above an unrelated /** Human-readable one-liner for an RFC 7591 DCR rejection. */.
  • clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx:140-142 — between isModernLogLevelValue() and ClearableTextInput; the <Accordion> is at :392.
  • clients/web/src/components/groups/ImportServerJsonPanel/ImportServerJsonPanel.tsx:97-99 — between ChooseFileButton and FileContentsTextarea; the <Accordion> is at :159.

Reading any of these in place, the note appears to explain the constant below it. Moving each to a JSX comment above its <Accordion> — exactly what ResourceControls.tsx:237-239 now does — makes all four consistent. Fix this →

3. AGENTS.md codifies the rule but not the two exemptions the sweep discovered

The wording change is good and matches the practice. But the sweep found two constructs that can't satisfy it — compound multiple-discriminated generics (Accordion) and data-* attributes (not in the typed props object, so excess-property-checked out of a withProps literal) — and documented them only in ~6 scattered code comments. AGENTS.md's subcomponent section lists the Box exemption explicitly, so a reader takes that list as exhaustive and the next audit re-derives these from scratch (as this one did). One sentence next to the Box bullet would close it.


Re-verified clean

  • Component identity — all 184 .withProps() constants are declared at module scope (zero indented declarations), so no per-render component types and no remount/focus-loss class of bug.
  • className bakes — only three in added code (reauth-banner-bar, resize-handle, header-anim); none collides with a call-site className, and HeaderStackCell's styles.root (gridArea/placeSelf) stays disjoint from the keyframes' opacity/transform, so the crossfade is unaffected.
  • Prop fidelity — spot-checked NetworkEntry, ToolControls, ClientSettingsForm, ExperimentalFeaturesPanel, ServerCard, ServerListScreen, EmbeddableScrollArea: all prop-for-prop. SentinelTooltip label={…}, SessionBadge color="green"|"yellow", ControlsCluster reuse in both NetworkEntry layouts, ClearableTextInput across three forms — every override lands correctly (call-site props spread last). EmbeddableScrollArea's hoisted viewportProps: { tabIndex: 0 } is a small bonus: stable object identity instead of a fresh literal per render.
  • Box exemptionsViewHeader's display="grid" container (no Mantine primitive is a grid; converting would break the shared-cell overlay) and AppRenderer's component="iframe" are both genuine and now commented. HtmlFrame.tsx:28 / PdfFrame.tsx:33 are the same iframe pattern, uncommented, but equally exempt.
  • PlainStringKeys<FormState> / App.tsx single cast / .grid-align-start removal / createRemoteLogger @ts-expect-error / ToolsScreen.handleSelectTool — unchanged since prior rounds and still correct.

Nothing found on

any usage, config-level lint suppressions, dead constants or imports, coverage-ignore drift, new inline style={{…}} props, or raw hex/rgba() literals.

Caveat: this checkout has no node_modules, so I did not run the suite or a browser — taking your npm run ci result and wide-viewport check at face value. The layout, CSS, and scope analysis above was run against the files in the branch.

--- · branch v2/fix-1762-agents-md-sweep

)

Round-6 review follow-up:

- Extract the 6 inline elements the strict sweep missed (all 2+ styling/layout
  props): EmbeddableScrollArea's fill-column Stack, ToolCallErrorPanel's Title +
  Alert, SortableServerCard's drag-handle ActionIcon, CsvTable's Table, and
  BinaryNotice's Code. (Their files were secondary members of a component folder,
  so the per-folder audit didn't open them.)
- Move the Accordion-exemption comment to the call site in ClientSettingsForm,
  ServerSettingsForm, and ImportServerJsonPanel (matching ResourceControls), so
  it no longer dangles above an unrelated constant.
- Add the iframe-Box exemption comment to HtmlFrame/PdfFrame for consistency
  with AppRenderer.
- AGENTS.md: codify the two exemptions the sweep discovered (Accordion,
  data-*) and the iframe/grid Box exemptions next to the Box rule; and state the
  static-prop boundary explicitly — per-instance content/accessibility literals
  (label/description/placeholder/title/aria-label) do NOT by themselves trigger
  extraction (only styling/layout/behavior props do). This matches the practice
  and resolves the "not written down" gap the review flagged.

Verified: `npm run ci` green (3472 unit tests, >=90 coverage gate, smokes incl.
headless-browser boot, 456 Storybook play-tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

All three residual items addressed in 431dae6.

1. Six missed stragglers — extracted all six (EmbeddableScrollArea fill-column Stack, ToolCallErrorPanel Title + Alert, SortableServerCard drag-handle ActionIcon, CsvTable Table, BinaryNotice Code). Root cause: these live in secondary files within a component folder (ToolResultPanel/ToolCallErrorPanel.tsx, ContentViewer/CsvTable.tsx, …), and the per-folder audit only opened each folder's primary <Name>/<Name>.tsx — good catch by your tag-scan. On the boundary you raised: per maintainer decision, per-instance content literals (label/description/placeholder/aria-label) do not trigger extraction, so the ~20 <Checkbox label description> / <Select label …> fields correctly stay inline — and that's now written into AGENTS.md (see item 3).

2. Three more dangling Accordion comments — moved the exemption note to a JSX comment directly above the <Accordion> in ClientSettingsForm, ServerSettingsForm, and ImportServerJsonPanel, matching ResourceControls. Also added the iframe-Box exemption comment to HtmlFrame/PdfFrame for consistency with AppRenderer.

3. AGENTS.md exemptions + boundary — codified in this PR (not deferred):

  • Added the two .withProps() exemptions the sweep discovered — Accordion (compound multiple-discriminated generic) and data-* attributes — plus the iframe/display:grid Box exemptions, right next to the Box rule.
  • Stated the static-prop boundary explicitly: only styling/layout/behavior literals count toward the two; per-instance content/accessibility literals don't by themselves trigger extraction (they may be baked when a constant already qualifies).

npm run ci green end to end.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 6m 17s —— View job


Code review — PR #1771 (re-review after 8467f29c)

  • Read the new commit's diff (8467f29c — Transition exemption, AppsScreen fill-card, AGENTS.md)
  • Verify the AGENTS.md wording against applied practice
  • Independent rule scan for remaining offenders
  • Fresh pass on withProps hazard classes (identity, override, className collision)
  • Post review

Verdict

Approve-equivalent — nothing blocking. 8467f29c closes both prior items correctly, and I re-ran my own scans rather than diffing against last round's list. One residual finding below: the same flat-CSS-class rule the PR applied to .header-stack-cell in 553e8742 is left unapplied on the constant dba00b5f introduced two files over — and the two comments that justify it are now factually stale after the Box → Paper swap.

Verified: the two fixes

Transition exemption — I enumerated every <Transition> in the tree and ScreenStage.tsx:56 is genuinely the only one with two static literals (transition="fade-up" + timingFunction="ease"). ViewHeader.tsx:319/353/378/393 have one (transition="fade"); ViewHeader.tsx:436 and InspectorView.tsx:1418 have one (timingFunction="ease", with transition={toggleSlide}/{columnSlide} dynamic). So the single exemption comment covers the single case, and the AGENTS.md bullet now enumerates all three constructs, scopes the rule to factory-based Mantine components, and closes the non-Mantine-element gap (ContentViewer, react-icons) my last scan surfaced. I could not compile Transition.withProps here (no node_modules) — taking your TS2339 result.

AppsScreen ContentCard:569 and :605 are the only two references to the constant, and both previously carried exactly flex={1} h="100%", so folding them in is byte-equivalent. data-* correctly stays at the call site. Folding into ContentCard rather than deriving is the better call — and confirms the chaining caveat.

role in the carve-out — matches practice: FieldError = Text.withProps({ c: "red", size: "sm", role: "alert" }) in ServerConfigModal.tsx:67 / ServerRemoveConfirmModal.tsx:28 bakes it only because the constant already qualifies on c + size.


Finding (minor) — .reauth-banner-bar is flat-only CSS, and its two comments no longer hold

clients/web/src/App.tsx:400-403 · clients/web/src/App.css:573-581

/*
 * Sticky re-auth banner bar. Positioning (pos/top) and background are set via
 * Mantine props on the Box; z-index and box-shadow have no Mantine style-prop
 * shorthand, so they live here.
 */
.reauth-banner-bar {
  z-index: 200;
  box-shadow: var(--mantine-shadow-sm);
}

Two problems, both created by this PR's Box → Paper conversion:

  1. The claim is now wrong for box-shadow. It was true of a Box — but Paper has a first-class shadow prop, and getShadow("sm") emits exactly var(--mantine-shadow-sm) into --paper-shadow, which Paper's root consumes as box-shadow. So shadow: "sm" in the constant is a pixel-identical replacement. The App.tsx:400-401 comment repeats the same stale rationale ("the reauth-banner-bar className carries the styles that can't be expressed as props") and still says "on the Box".
  2. The rule the PR itself enforces. After (1), the only property left is z-index: 200 — and I confirmed there is no z style prop in Mantine's list, so it needs styles.root. That makes the whole rule flat-only, i.e. exactly the .header-stack-cell finding closed in 553e8742, on a constant dba00b5f created. Of the five baked classNames in the tree, this is the only flat-only one — server-drag-handle (cursor + :active), header-anim (keyframes + attribute selectors), markdown-content (nested third-party selectors), and resize-handle (:hover/:focus-visible) all legitimately earn their place.
const ReAuthBannerBar = Paper.withProps({
  px: "md",
  pt: "xs",
  pos: "sticky",
  top: 60,
  bg: "var(--mantine-color-body)",
  shadow: "sm",
  // No `z` style prop exists, so the stacking order goes through `styles`.
  styles: { root: { zIndex: 200 } },
  radius: 0,
});

Blast radius checked: reauth-banner-bar appears in exactly three places repo-wide (the constant, the comment, the one App.css rule) — zero references in any test or story, no media-query or :hover companion rule — so the App.css block can be deleted outright. Nothing else sets z-index on that element, so the inline styles value can't shadow anything.

Fix this →

Explicitly not a finding: .resize-handle also mixes flat properties (flex, width, background-color, transition, touch-action) with its :hover/:focus-visible rule. The class is load-bearing for the pseudo-selectors either way, and splitting one rule across two layers reads worse than it reads now. Noting it so the next auditor doesn't re-derive it as an offender.


Re-verified clean (independent scans, not a diff against last round)

  • No remaining offenders. I re-scanned all 365 non-test/non-story .tsx under clients/web/src with a comment-stripping, brace-balancing JSX tokenizer, classifying props exactly as AGENTS.md now defines them (literal styling/layout/behavior counts; on*/value/children/key/ref/style/data-* and the label/description/placeholder/title/aria-*/role content carve-out don't). 4 hits, all Accordion (ClientSettingsForm:175, ImportServerJsonPanel:158, ResourceControls:240, ServerSettingsForm:391) — the documented exemption, each with its note now at the call site. The 7 remaining <Box> sites are all exempted-and-commented (display="grid", component="iframe", dnd-kit/Transition dynamic style) except the propless App.tsx:4215. My earlier round's list (EmbeddableScrollArea, ToolCallErrorPanel ×2, SortableServerCard, CsvTable, BinaryNotice) is fully closed.
  • No baked prop is ever overridden at a call site. This is the one way withProps silently drops a value: Mantine's factory spreads {...fixedProps} {...props}, so a call-site prop set to undefined replaces the baked default rather than falling back. I extracted the baked prop set for every constant and cross-checked every JSX call site of that constant — 0 collisions across the tree. SectionLabel c="red" and DimmedNote aria-live="polite" are additions, not overrides (neither constant bakes those keys).
  • Component identity — zero indented (function-scope) withProps declarations, so no per-render component types and no remount/focus-loss class of bug. Zero dead constants (every declared constant is referenced). Zero orphaned JSDoc (*/ immediately followed by a withProps const) across all 86 changed files — the four from round 5 stay fixed and none was reintroduced.
  • Compound-component wrapping is safe. The 11 constants built on sub-components (Modal.Root, Modal.Title, ScrollArea.Autosize) are all factory-based; Modal.Title resolves the dialog's aria-labelledby through context, not by child type, so wrapping it in withProps keeps the accessible name wired. No withProps constant is placed under a parent that filters children by type.
  • ReAuthBanner / AnnotationBadge / LogLevelBadge / MethodBadge / TaskStatusBadge / TransportBadge / ListToggle / App.tsx toasts — prop-for-prop. Domain variant logic stays in the element (color={filledBadgeColor(...)}, fw={fw} passed dynamically), nothing pushed into theme files. LogLevelBadge's conditional fw is safe precisely because FilledBadge doesn't bake fw.
  • radius: 0 on ReAuthBannerBar — correctly restores the Box geometry that Paper's defaultRadius: "md" would otherwise round.
  • ScreenStage direction: "column" — still correct, and the comment records why it's load-bearing (a row flex content-sizes a container-typed grid to zero).
  • No dead CSS. Every class in App.css is still referenced from TS/TSX; grid-align-start and header-stack-cell have zero remaining references repo-wide.
  • TS surface — the diff adds no any, no as unknown as, no eslint-disable, and exactly one @ts-expect-error (the pre-existing pino/browser.js one, now with the why). PlainStringKeys<FormState>, the single-cast messagesToLogEntries, and ToolsScreen.handleSelectTool (with /* v8 ignore next */ still immediately above if (tool)) are unchanged from prior rounds.
  • One inline style={{…}} in the treeSortableServerCard.tsx:61, the sanctioned dnd-kit per-frame transform. Zero raw hex/rgba() literals in .tsx.

Nothing found on

any usage, config-level lint suppressions, dead constants/imports/CSS, orphaned JSDoc, coverage-ignore drift, changed prop semantics, className collisions, or new inline styles.

Caveat: this checkout has no node_modules, so I ran neither the suite nor a browser nor tsc — taking your npm run ci result and the Transition.withProps TS2339 check at face value. The scans, cascade analysis, and Mantine withProps merge-semantics reasoning above were run against the files in the branch.

--- · branch v2/fix-1762-agents-md-sweep

Round-8 review follow-up: `.reauth-banner-bar` was flat-only CSS (z-index +
box-shadow) — the same finding closed for `.header-stack-cell`, on a constant
this PR created. Now that the element is a `Paper` (not a Box), `box-shadow` is a
first-class prop: `shadow: "sm"` emits `var(--mantine-shadow-sm)`, identical to
the class. `z-index` has no Mantine style prop, so it goes through
`styles.root`. Drop the className, delete the App.css block, and fix the two
stale comments that still referenced "the Box".

Verified: `npm run ci` green (3472 unit tests, >=90 coverage gate, smokes incl.
headless-browser boot, 456 Storybook play-tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Fixed in 9356f9e — good catch, this is the same flat-CSS-class finding as .header-stack-cell, on a constant this PR created (and the Box → Paper swap made box-shadow expressible as a prop).

ReAuthBannerBar now bakes shadow: "sm" (Paper's getShadow("sm")var(--mantine-shadow-sm), identical to the old class) and moves the stacking order into styles: { root: { zIndex: 200 } } (no z prop exists). Dropped className: "reauth-banner-bar", deleted the .reauth-banner-bar block from App.css (zero remaining references repo-wide), and rewrote the two stale comments that still said "on the Box." That leaves the four remaining baked classNames (server-drag-handle, header-anim, markdown-content, resize-handle) all legitimately earning their place via pseudo-selectors/keyframes/nested selectors, as you noted.

npm run ci green end to end.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 8m 25s —— View job


Code review — PR #1771 (re-review after 9356f9ef)

  • Read the latest commit's diff (9356f9ef.reauth-banner-bar fold)
  • Independent rule scan for remaining offenders
  • Automated baked-prop-override + static-prop-multiset diff across all 86 files
  • Dead-CSS / doc-placement / className-collision passes
  • Post review

Verdict

Approve-equivalent — nothing blocking. 9356f9ef closes the last finding correctly. I ran three fresh automated passes this round rather than re-reading last round's list, and they come back clean on every hazard class that matters. Two minor items below, both pre-existing rather than introduced — but both are the exact categories this PR spent rounds 6–8 closing, and one of them is three lines from code this commit chain edited.

Verified: the .reauth-banner-bar fold

clients/web/src/App.tsx:399-413 · clients/web/src/App.css

  • shadow: "sm" is byte-identical to the deleted class. Paper's getShadow("sm") emits var(--mantine-shadow-sm) into --paper-shadow, which the root consumes as box-shadow — exactly the value .reauth-banner-bar set. radius: 0 (added in 5e175b37) still guards the defaultRadius: "md" rounding.
  • styles: { root: { zIndex: 200 } } is the only option. I enumerated Mantine's style-prop set (m/p/bg/c/opacity/ff/fz/fw/lts/ta/lh/fs/tt/td/w/miw/maw/h/mih/mah/bgsz/bgp/bgr/bga/pos/top/left/bottom/right/inset/display/flex) — there is no z. Consistent with the HeaderStackCell precedent from 553e8742.
  • The one real hazard here doesn't apply. Mantine's withProps spreads {...fixed} {...props}, so a call-site styles would replace the baked object outright, not merge — silently dropping the zIndex. <ReAuthBannerBar> has a single call site (App.tsx:4217) passing no props at all, so nothing overrides it.
  • Comments are now accurate — both stale "on the Box" references are gone, and the new one states the shadow/styles rationale correctly.
  • Zero dangling references to reauth-banner-bar, header-stack-cell, or grid-align-start anywhere in the repo (all file types, incl. tests/stories/docs).

1. Minor — a component JSDoc documents a lookup table, in the block this commit chain edited

clients/web/src/components/groups/ToolResultPanel/ToolCallErrorPanel.tsx:57-67

const ErrorAlert = Alert.withProps({ color: "red", variant: "light" });   // ← added by 431dae6d

/**
 * Renders a thrown tool-call error (a protocol/SDK-level rejection) as a
 * distinct error panel. This is separate from ToolResultPanel, which renders a
 * `CallToolResult` … An `-32602` rejection carries no result, so it would
 * otherwise be invisible.
 */
const ERROR_TITLES: Record<string, string> = {   // ← doc lands here

This is the same defect class as round 6's finding 3 (ExpandToggle/PinToggle/MonitoringToggle/ReplayButton), and it's the reason my */-followed-by-withProps scan didn't catch it — the orphan here is */ followed by a const object, not a withProps. Hovering ToolCallErrorPanel in an editor shows nothing; hovering ERROR_TITLES shows the component's docs.

To be fair: this one is pre-existinggit show origin/v2/main:… has the same misplacement. But 431dae6d inserted PanelTitle and ErrorAlert immediately above it while applying exactly this standard four files over, so it's a cheap consistency win rather than scope creep. Moving ERROR_TITLES above the /** fixes it. Fix this →

2. Minor — a fourth dead CSS class the sweep left behind

clients/web/src/App.css:269-273 (@keyframes inspector-spin) · :321-323 (.inspector-spin)

.inspector-spin {
  animation: inspector-spin 1s linear infinite;
}

I checked every class in App.css for a reference from TS/TSX and inspector-spin is the only orphan: zero references repo-wide (all file types, incl. tests and stories), and the keyframes are consumed only by the dead class, so both blocks go together. This PR already deleted three classes for precisely this reason — it's the same cleanup, one rule further down the file. Pre-existing (present and unreferenced on origin/v2/main too), so entirely fine to defer. Fix this →


Re-verified clean (fresh scans, not a diff against last round)

  • No remaining rule offenders. I re-scanned all non-test/non-story .tsx under clients/web/src with a comment-stripping, brace-balancing JSX prop tokenizer, classifying props exactly as the new AGENTS.md text defines them (literal styling/layout/behavior counts; on*/value/children/key/ref/style/component/data-*/aria-* and the label/description/placeholder/title/role/alt content carve-out don't). 13 hits, all accounted for: 4 Box (3× component="iframe", 1× display="grid" — all commented), 4 Accordion, 1 Transition (ScreenStage.tsx:56, commented), MdChevronRight (react-icons) and ContentViewer (non-Mantine, explicitly out of scope per the new wording), and 2 MantineProvider in src/test/renderWithMantine.tsx — a test helper, and a context provider with no Styles API root, so out of scope on both counts.
  • No baked prop is ever overridden at a call site. This is the one way withProps silently drops a value (fixed props spread first, so a call-site prop — even undefined — wins). I extracted the baked key set for every withProps constant in the tree and cross-checked every JSX call site of that constant: 0 collisions. LogLevelBadge's fw={fw} (possibly undefined) and EmbeddableScrollArea's styles={styles} (possibly undefined) are both safe precisely because neither constant bakes those keys.
  • Static prop multiset, base vs head, across all 86 changed .tsx. Every apparent drop is dedup into a shared constant, not a lost prop. Spot-verified the non-obvious ones: ServerSettingsForm (3 identical suffix=" ms" → one TimeoutInput, with min={1} correctly left at the one call site that had it), ServerRemoveConfirmModal (incl. the JSX whitespace reflow around <BoldInline> — the rendered sentence is character-identical after JSX newline collapsing), ClientSettingsForm (w="fit-content" baked in SessionBadge, two remaining inline uses are single-static-prop and correctly untouched), BinaryNotice, ToolCallErrorPanel, ServerListScreen.
  • Dead CSS — every App.css class except .inspector-spin (finding 2) is still referenced from TS/TSX.
  • className bakes — five sites across four classes (server-drag-handle, header-anim ×3, markdown-content, resize-handle); all four are legitimately CSS (:active/:hover/:focus-visible, keyframes + attribute selectors, nested third-party selectors). None collides with a call-site className, and the only computed className= in JSX (ServerStatusIndicator.tsx:77) is untouched. .reauth-banner-bar was the last flat-only one and is now gone.
  • Element/theme separation holds — zero files under src/theme/ are touched by this PR, and domain variant logic stays in the elements (AnnotationBadge's colorMapcolor=, LogLevelBadge's filledBadgeColor(...) + conditional fw, TransportBadge's transportLabel) rather than migrating into theme files.
  • ScreenStage / ResizeHandledirection: "column" is still the right fix and its comment records why it's load-bearing. I re-derived the flex fallback chain: with --flex-align unset, align-items: var(--flex-align) is guaranteed-invalid → initial normal → behaves as stretch in a flex container, so the single child gets the container's full width, matching the old block child. ResizeSeparator is childless with flex: 0 0 auto; width: 6px, so its display model is inert. aria-orientation/role/tabIndex bake fine (React types them; only data-* is excess-property-checked out).
  • PlainStringKeys<FormState> / App.tsx single cast / HeaderStackCell styles.root / createRemoteLogger @ts-expect-error / ToolsScreen.handleSelectTool — unchanged from prior rounds and still correct.
  • AGENTS.md — the three-exemption enumeration, the factory-based scoping, the non-Mantine out-of-scope clause, and the role-in-carve-out tightening all match what I can observe in the tree.

Nothing found on

any usage, config-level lint suppressions, dead constants or imports, coverage-ignore drift, changed prop semantics, className collisions, per-render component types (zero function-scope withProps declarations), or new inline style={{…}} props beyond the sanctioned dnd-kit transform.

Caveat: this checkout has no node_modules, so I ran neither the suite, a browser, nor tsc — taking your npm run ci result and the earlier Transition.withProps TS2339 check at face value. All scans, CSS-cascade, and Mantine withProps merge-semantics reasoning above were run against the files in the branch.

--- · branch v2/fix-1762-agents-md-sweep

…CSS (#1762)

Round-9 review follow-up (both pre-existing, same cleanup classes this PR
already applied):

- ToolCallErrorPanel: move ERROR_TITLES above the component's JSDoc so the doc
  documents the exported component again, not the lookup table (same defect
  class as round 5's four element components).
- App.css: delete the dead `.inspector-spin` class and its `@keyframes
  inspector-spin` — zero references repo-wide (all file types), same removal as
  `.grid-align-start` / `.header-stack-cell` / `.reauth-banner-bar`.

Verified: `npm run ci` green (3472 unit tests, >=90 coverage gate, smokes incl.
headless-browser boot, 456 Storybook play-tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Both round-9 items addressed in 7abc9f5 (both pre-existing, but the same cleanup classes this PR already applied, so worth closing out):

  1. ToolCallErrorPanel misattributed JSDoc — moved ERROR_TITLES above the component's JSDoc, so the doc documents ToolCallErrorPanel again rather than the lookup table (same defect class as round 5's four element components).
  2. Dead .inspector-spin CSS — deleted the class and its @keyframes inspector-spin; zero references repo-wide, matching the .grid-align-start / .header-stack-cell / .reauth-banner-bar removals already in this PR.

npm run ci green end to end.


Separately, while manually testing this PR I hit a modern-tasks server that connected but showed no header/tabs. I verified it's not caused by this styling sweep (git diff origin/v2/main touches none of the header-gate / initializeResult / serverInfo / InspectorView / core-connection code) — it's a pre-existing modern-era bug where a server/discover that omits the now-optional serverInfo leaves initializeResult undefined, collapsing the whole connected header. Filed as #1772 and fixed separately in #1773 (isolated worktree, doesn't touch this branch).

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 6m 20s —— View job


Code review — PR #1771 (re-review after 7abc9f5c)

  • Read the latest commit's diff (7abc9f5c — JSDoc + dead CSS)
  • Independent rule scan for remaining offenders
  • Fresh hazard passes (baked-prop override, identity, dead CSS, doc placement, variant resolution)
  • Post review

Verdict

Approve-equivalent — nothing blocking, and nothing new worth fixing. 7abc9f5c closes both round-9 items correctly. I re-ran every scan from scratch this round (new tokenizer, not a diff against my prior lists) and added three checks the earlier rounds hadn't run — shared-identity dedup of a11y labels, baked-variant resolution against the theme, and a full static-prop multiset diff of base vs. head across all 86 files. All come back clean. One trivial naming nit at the bottom, take it or leave it.

Verified: the two round-9 fixes

  • ToolCallErrorPanelERROR_TITLES now sits above the JSDoc, so the doc lands on export function ToolCallErrorPanel again. I re-scanned every non-test .tsx for a */ immediately followed by a const/let/type/interface declaration: 4 hits, all correctly attributed (ServerImportConfigModal's SOURCE_OPTIONS, CodeHighlight's LANGUAGE_LOADERS, AppsScreen's MAX_APP_CHANNEL_ENTRIES and AppLogEntry — each doc genuinely describes the declaration below it). Zero orphans left, and the four element components from round 5 stay fixed.
  • .inspector-spin + @keyframes inspector-spin — correctly deleted together; the keyframes had exactly one consumer (the dead class). I then checked every class in App.css for a reference from TS/TSX/HTML: zero dead classes remain, and all four surviving keyframes (inspector-fade-slide-in/-out, inspector-pulse, inspector-tab-glow) are consumed.

Fresh scans — all clean

Rule offenders: 11 hits, all accounted for. I rewrote the JSX prop tokenizer (comment-blanking that preserves offsets, brace/string-aware opening-tag scan, top-level attribute splitting) and ran it over all non-test/non-story .tsx under clients/web/src, classifying props exactly as the new AGENTS.md text defines them:

Hits Status
4 × BoxAppRenderer:527, HtmlFrame:30, PdfFrame:35 (component="iframe"), ViewHeader:347 (display="grid") Documented exemption, all commented
4 × AccordionClientSettingsForm:175, ImportServerJsonPanel:158, ResourceControls:240, ServerSettingsForm:391 Documented exemption, each note now at its call site
1 × TransitionScreenStage:56 Documented exemption, commented
2 × MantineProvidersrc/test/renderWithMantine.tsx Test helper; context provider with no Styles API root — out of scope on both counts

Nothing else in the tree clears the two-static-prop bar. My round-8 list is fully closed.

No a11y label is shared by dedup — a hazard specific to this kind of sweep. Folding N inline elements into one constant can silently give two different controls the same accessible name. I extracted every constant that bakes an aria-label/label/title/placeholder/role literal and cross-checked its call-site count: zero such constant is rendered more than once. Separately, all 20 baked "aria-label" / "aria-orientation" literals are accounted for against the base — the ones my multiset diff showed as "lost" (ReplayButton's "Replay", ResizeHandle's "vertical", AppsScreen's "Close", LogControls' two selects, MonitoringControls, OutputValidationModal, SamplingRequestPanel, ExperimentalFeaturesPanel, ImportServerJsonPanel, UrlElicitationErrorModal) are all present as quoted keys inside their constants, not dropped.

Every baked variant= resolves. A variant="…" folded into a withProps literal that doesn't exist in the theme would render unstyled and silently. I collected every variant value used anywhere in src and diffed against the names recognized in src/theme/*.ts plus Mantine's built-ins: zero unresolved. All 27 custom variants (disclosure, preview, screen, reauth, listItem, …) have a theme home.

No baked styles object is clobbered. Because withProps spreads {...fixed} {...props}, a call-site styles replaces the baked one rather than merging. Three constants bake styles (ReAuthBannerBar's zIndex, HeaderStackCell's gridArea/placeSelf, ServerGrid's GRID_SURFACE_STYLES) — none of their call sites passes styles. Conversely EmbeddableScrollArea's StreamScrollArea and LogLevelBadge's FilledBadge receive possibly-undefined styles/fw at the call site and are safe precisely because neither bakes those keys.

Static-prop multiset, base vs. head, all 86 changed files. Every apparent drop is dedup into a shared constant, and I traced each non-obvious one: NetworkEntry (fw={500} 8→4, size="xs" 11→8, c="dimmed" 6→3, gap="sm" 6→5, w={280} 2→1 — all DimmedNote/SectionLabel/ControlsCluster/SentinelTooltip); ClientSettingsForm (variant="light" 4→3 = ErrorAlert + SessionBadge×2 + SignOutButton, so 4 renders preserved); ServerSettingsForm (suffix=" ms" 3→1); ServerConfigModal (the id: "" / command: "" inline resets became clearTextField(…) calls); AppsScreen (flex={1} 5→4, h="100%" 3→2 into ContentCard, used twice). The GAINED boolean entries (striped, autoContrast, withArrow, readOnly, autosize, centered) are all JSX shorthand → : true in an object literal, verified against base.

NetworkEntry read line by line (the largest diff at +119/−77). Every extraction is prop-for-prop: ControlsCluster genuinely equals the wide layout's <Group gap="sm" wrap="nowrap">; SentinelTooltip label={…} and SectionLabel c="red" are additions, not overrides; UrlScrollArea preserves the viewportProps: { tabIndex: 0 } WCAG 2.1.1 affordance and its comment. The one thing worth checking was the JSX whitespace reflow in the cancellation alert (…sending a{" "}<MonoSpan>notifications/cancelled</MonoSpan> frame (SEP-2575).) — the rendered sentence is character-identical after JSX's newline collapsing.

Everything else — zero function-scope withProps declarations (so no per-render component types), zero dead constants, zero chained X.withProps().withProps(), zero src/theme/ files touched, zero elements/ file importing from src/theme/, one inline style={{…}} in the tree (the sanctioned dnd-kit transform at SortableServerCard.tsx:61), zero raw hex/rgba() in .tsx. ScreenStage's direction: "column" is still correct and its comment records why it's load-bearing; style={styles} from Transition carries only opacity/transform/transitionDuration, disjoint from the baked position/top/left/right, so the merge can't clobber the anchoring.

Nit (ignorable) — HeadersTableEl names the disambiguator, not the thing

clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx:222

const HeadersTableEl = Table.withProps({ striped: true, withColumnBorders: true, fz: "xs" });

The El suffix exists only to avoid colliding with the HeadersTable function 100 lines below — it carries no meaning, where every other constant in the file is named for its role (ValueCellRow, WideHeaderCluster, SentinelTooltip, MonoSpan). HeadersGrid would read as intentionally as its neighbors. Purely cosmetic; nothing depends on it.

Nothing found on

any usage, config-level lint suppressions, dead constants/imports/CSS, orphaned JSDoc, coverage-ignore drift, changed prop semantics, className collisions, unresolved theme variants, shared accessible names, per-render component types, or new inline styles.

Caveat: this checkout has no node_modules, so I ran neither the suite, a browser, nor tsc — taking your npm run ci result and the earlier Transition.withProps TS2339 check at face value. Every scan, CSS-cascade, and Mantine withProps merge-semantics claim above was derived from the files in the branch. Two of my scanners produced false positives I ran down and discarded rather than reporting: a <SortSelect> "override" that was actually size={16} on the nested <Icon> inside rightSection, and a batch of "unused type imports" caused by my import-stripping regex over-consuming (web's noUnusedLocals gates these anyway).
· branch v2/fix-1762-agents-md-sweep

Round-10 review nit: the `El` suffix only disambiguated from the `HeadersTable`
function below and carried no meaning; `HeadersGrid` names the constant for its
role, matching its neighbours (ValueCellRow, SentinelTooltip, MonoSpan).

Verified: `npm run ci` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Applied the naming nit in 369b3ef: HeadersTableElHeadersGrid (the El suffix only disambiguated from the HeadersTable function below; the new name reads for its role like its neighbours). npm run ci green.

That was the only item in the round-10 review, and it was explicitly optional — every prior round's findings are applied and the fresh scans (a11y-label dedup, baked-variant resolution, styles-clobbering, full static-prop multiset diff) all came back clean.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 3m 59s —— View job


Code review — PR #1771 (re-review after 369b3efd)

  • Read the latest commit's diff (369b3efdHeadersTableElHeadersGrid)
  • Independent rule-offender scan (own tokenizer, from scratch)
  • Fresh hazard passes (name collisions, ref forwarding, baked-variant resolution, dead class refs, DOM assertions)
  • Post review

Verdict

Approve-equivalent — nothing blocking, nothing new worth fixing. 369b3efd is a clean two-site rename, and I re-derived the full scan set from scratch rather than diffing against my round-10 list. Everything comes back clean. One optional one-word doc clarification at the bottom; otherwise this is done.

Verified: the rename

clients/web/src/components/groups/NetworkEntry/NetworkEntry.tsx:224,333,354

  • Zero dangling HeadersTableEl anywhere in the repo (all file types, incl. tests/stories/docs).
  • No collision reintroduced: HeadersGrid (the Table.withProps constant, :224) and HeadersTable (the function, :319, called at :572/:590) are now distinct names for distinct roles. The constant's baked props are untouched (striped, withColumnBorders, fz: "xs"), and the compound sub-components (Table.Tbody/Tr/Td) are still referenced off the original Table import, which is the correct pattern — withProps doesn't re-attach statics.
  • Name reads for its role now, matching ValueCellRow / SentinelTooltip / MonoSpan.

Fresh scans — all clean

Rule offenders: 14 hits, all accounted for. I wrote a new comment-blanking, quote/brace-aware opening-tag tokenizer and ran it over every non-test/non-story .tsx under clients/web/src, classifying props exactly as the current AGENTS.md text defines them:

Hits Status
4 × BoxAppRenderer, HtmlFrame, PdfFrame (component="iframe"), ViewHeader (display="grid") Documented exemption, all commented
4 × AccordionClientSettingsForm, ImportServerJsonPanel, ResourceControls, ServerSettingsForm Documented exemption, note at each call site
1 × TransitionScreenStage.tsx:56 Documented exemption, commented
MdChevronRight (react-icons), ContentViewer (first-party, no .withProps) Non-Mantine → out of scope per the new wording
2 × MantineProvidersrc/test/renderWithMantine.tsx Test helper, no Styles API root
1 × LoggingLevel False positive in my scanner (a TS union type, not JSX)

Nothing else in the tree clears the two-static-prop bar. Every item on my round-8 straggler list stays closed.

Name collisions: zero. A withProps constant sharing a name with an imported identifier in the same file is the class of bug the HeadersTableEl suffix existed to dodge, so I checked it globally: across all 86 changed files, no constant name collides with an import in its own file, and no constant name shadows an existing components/elements/* component (which would be the confusing kind of near-duplicate). Only one withProps constant in the tree is exported (ClearButton, pre-existing and intentional), so the sweep widened no public surface.

ref forwarding through withProps: safe, and already load-bearing. Three constants receive a ref at their call site — EntryContainer (NetworkEntry:514), RendererContainer (AppsScreen:653), and DragHandle (SortableServerCard:44, new this PR). The first two already existed on origin/v2/main with the same ref= pattern, so the forwarding path is proven by the existing tree rather than assumed; DragHandle is the same shape. That matters because setActivatorNodeRef needs the real DOM node for dnd-kit's activator to work at all.

Baked variant values all resolve. A variant folded into a withProps literal that no theme recognizes renders silently unstyled. Of the variants baked in this diff, the four custom ones (filterToggle, listItem, reauth, warning) each have a definition under src/theme/; the rest (default, filled, light, outline, subtle) are Mantine built-ins. Zero unresolved.

Deleted CSS classes leave no references. grid-align-start, header-stack-cell, reauth-banner-bar, inspector-spinzero hits across clients/, core/, docs/, specification/, including tests and stories.

No test asserts on a changed DOM shape. The two tagName/nodeName assertions in the suite (PendingClientRequestModalTEXTAREA, AppRendererIFRAME) target elements this sweep didn't convert, so the Box → Flex/Paper swaps can't have silently satisfied or broken them.

ScreenStage / LogEntry read line by line. StageLayer doesn't bake bottom, so the call-site bottom={fill ? 0 : undefined} can't clobber a default; style={styles} carries only opacity/transform, disjoint from the baked position/top/left/right. The direction: "column" comment records exactly why it's load-bearing, which is the note a future refactor needs. LogRow is prop-for-prop the <Group gap="sm" wrap="nowrap"> it replaced.

AGENTS.md — read the final wording against the tree: the three-exemption enumeration, the factory-based scoping, the static-vs-content prop split, and the role carve-out all match what the code actually does.


Optional — "non-Mantine" is illustrated only with third-party examples

AGENTS.md (subcomponent rules)

a non-Mantine element (a react-icons glyph, another library's component) is out of scope entirely.

Both examples are third-party, but the live case the rule has to cover is first-party: <ContentViewer copyable wrap={false}> (clients/web/src/components/groups/ServerCard/ServerCard.tsx:283-287) is a plain export function in this repo with no .withProps static. It's correctly out of scope — it isn't a Mantine factory component — but a reader could take the parenthetical as meaning "third-party is out of scope" and then re-derive first-party dumb components as offenders on the next audit, which is the exact re-derivation cost the AGENTS.md tightening in this PR was meant to eliminate. Adding "or a first-party component that isn't a Mantine factory" to that clause closes it in one phrase. MdChevronRight (AppListItem.tsx:62) is the third-party half and needs nothing.

Genuinely cosmetic — the rule already yields the right answer today. Fix this →

Nothing found on

any usage, config-level lint suppressions, dead constants/imports/CSS, orphaned JSDoc, coverage-ignore drift, changed prop semantics, className collisions, unresolved theme variants, per-render component types (zero function-scope withProps declarations), or new inline style={{…}} props beyond the sanctioned dnd-kit transform.

Caveat: this checkout has no node_modules, so I ran neither the suite, a browser, nor tsc — taking your npm run ci result and the earlier Transition.withProps TS2339 check at face value. Every scan above was written fresh this round and run against the files in the branch; my scanner's one false positive (a TS union type parsed as a JSX tag in LogEntry.tsx) is disclosed in the table rather than reported as a finding.

--- · branch v2/fix-1762-agents-md-sweep

…b components (#1762)

Round-11 review nit: the out-of-scope clause illustrated "non-Mantine" only with
third-party examples, but the live case is first-party — a dumb `export function`
like `ContentViewer` has no `.withProps` static either. Name both so a future
audit doesn't re-derive first-party dumb components as offenders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Applied the optional doc clarification in $(git rev-parse --short HEAD): the AGENTS.md out-of-scope clause now names both halves — a third-party element AND a first-party component that isn't a Mantine factory (a dumb export function like ContentViewer) — so a future audit doesn't re-derive first-party dumb components as offenders. Doc-only change.

That closes the last item across 11 review rounds. Every finding — the ScreenStage layout regression, the mapped-type enforcement, the four Box/Accordion/Transition/data-* exemptions, ~170 extractions, the AGENTS.md tightening, and each round's cleanups — is applied, with npm run ci green throughout.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 4m 12s —— View job


Code review — PR #1771 (re-review after 9a767b8b)

  • Read the latest commit's diff (9a767b8b — AGENTS.md out-of-scope clause)
  • Independent rule-offender scan (fresh tokenizer)
  • New passes: duplicate/unused constants, baked-prop overrides, dead CSS, doc attribution
  • Post review

Verdict

Approve-equivalent — no findings. This is ready to merge. 9a767b8b is a doc-only change and it lands the clause correctly. I re-ran every scan from scratch and added three checks no prior round had run (duplicate-constant detection, exhaustive baked-prop-override cross-check, dead-class sweep including Mantine-internal selectors). Everything is clean; the only thing below is an optional follow-up about a test-surface gap this PR's one real regression exposed, plus two notes recorded so the next auditor doesn't churn on them.

Verified: the final AGENTS.md clause

AGENTS.md — the out-of-scope parenthetical now names both halves:

a third-party element (a react-icons glyph, another library's component) and a first-party component that isn't a Mantine factory (a dumb export function like ContentViewer, which has no .withProps static of its own)

That resolves exactly against the two live cases: <ContentViewer copyable wrap={false}> (ServerCard.tsx:283) and <MdChevronRight color=… size={18}> (AppListItem.tsx:62). I also checked no other doc restates the old "two or more props" wording — specification/v2_servers_file.md:174 references the rule but only by pointer to AGENTS.md, so nothing is stale.

Fresh scans — all clean

Rule offenders: 13 hits, every one an accounted-for exemption. New comment-blanking / string-masking / brace-aware tokenizer over all non-test/non-story .tsx under clients/web/src, classifying props exactly as the final AGENTS.md text defines them:

Hits Status
4 × Accordion (ClientSettingsForm:175, ImportServerJsonPanel:158, ResourceControls:240, ServerSettingsForm:391) Documented exemption, note at each call site
4 × Box (AppRenderer:527, HtmlFrame:30, PdfFrame:35component="iframe"; ViewHeader:347display="grid") Documented exemption, all commented
1 × Transition (ScreenStage:56) Documented exemption, commented
ContentViewer, MdChevronRight Non-Mantine → out of scope per the clause 9a767b8b just clarified
2 × MantineProvider (src/test/renderWithMantine.tsx) Test helper; context provider with no Styles API root

No baked prop is overridden at any call site — 0 hits. This is the one way withProps silently drops a value (Mantine spreads {...fixed} {...props}, so a call-site prop — even undefined — replaces the baked default). I extracted the baked key set for every withProps constant in the tree and machine-checked it against every JSX call site of that constant. Clean, including the three styles-baking constants (ReAuthBannerBarzIndex, HeaderStackCellgridArea/placeSelf, ServerGridGRID_SURFACE_STYLES), none of whose call sites passes styles (which would replace, not merge).

No duplicate or dead constants introduced. I detected same-base + same-normalized-props constant pairs per file: 8 pairs exist (ProtocolEntry's DurationText/SubscriptionLabel, AppsScreen's HeaderRow/PanelHeaderRow and MessageItemStack/AppLogList, plus pairs in ToolDetailPanel/ResourcePreviewPanel/TaskCard) — and I checked each against origin/v2/main: all pre-existing, and all name distinct roles, so they're deliberate. Zero unused constants (the one flagged, ClearButton, is exported and used across three forms).

Dead CSS: none. Every App.css class resolves — the 8 my scan flagged as unreferenced-from-TS are Mantine internal selector names (.mantine-Select-dropdown, .mantine-SegmentedControl-indicator, …) used as nested selectors, which is exactly what AGENTS.md says App.css is for. All 4 remaining @keyframes are consumed (the one "unused" hit was the word animations inside the file's header comment). grid-align-start, header-stack-cell, reauth-banner-bar, inspector-spin have zero references repo-wide.

Doc attribution: 15 */-followed-by-declaration sites, all correct. Checked each in the touched files — App.tsx:619 StepUpSource, ServerImportConfigModal's SOURCE_OPTIONS/OUTCOME_META, ServerConfigModal's ID_PATTERN, AppsScreen's MAX_APP_CHANNEL_ENTRIES/AppLogEntry, CodeHighlight's eight — every doc genuinely describes the declaration below it. Round 5's four element components and round 9's ToolCallErrorPanel stay fixed.

Read line by line this round: OutputValidationModal, UrlElicitationErrorModal, StepUpAuthModal (all prop-for-prop; Modal.Title still off the original import, so aria-labelledby stays wired), MethodBadge, LogLevelBadge (conditional fw safe because FilledBadge doesn't bake it), ResourceLink, MonitoringControls, ContentViewer/CodeBlock, EmbeddableScrollArea, ResizeHandle, ScreenStage, ToolsScreen.handleSelectTool (/* v8 ignore next */ still immediately above if (tool)). No test asserts DOM structure across a converted element — ViewHeader's nine closest("[data-anim]") lookups are element- and class-agnostic.


Optional follow-up — ScreenStage is the only element component with no story, and it's where the one real regression lived

clients/web/src/components/elements/ScreenStage/ScreenStage.tsx

Not a defect in this PR, and not something I'd hold the merge for. But it's a pointed gap: the Box → Flex swap here produced the single genuine behavior regression across 12 rounds (row-flex content-sizing every screen), and nothing in the suite could see it. ScreenStage.test.tsx has three assertions, all presence-only under happy-dom (which does no layout); the headless-browser smoke boots the app but asserts the "Add Servers" control renders, not its width. And of the 5 components missing a *.stories.tsx, ScreenStage is the only one under elements/ — so it's also the one gap against the README's "every screen and element component has a stories file."

A story with a play function asserting the child's getBoundingClientRect().width matches the stage's (for both the default and fill variants) runs in real Chromium via test:storybook and would have caught round 6's finding automatically. Cheap, and it guards the direction: "column" comment that is currently the only thing protecting that behavior. Fix this →

Recorded, not findings

  • AppModalMd (StepUpAuthModal.tsx:29) names its size rather than its role — same shape as the HeadersTableEl nit closed in 369b3efd. ScopeList/ScopeListPlain likewise distinguish by "which one" rather than by purpose. Purely cosmetic; nothing depends on it, and I'm not suggesting another round for it.
  • The 8 identical-prop constant pairs listed above are pre-existing and role-distinct — noting so a future audit doesn't dedupe them as redundancy.

Nothing found on

any usage, config-level lint suppressions, dead constants/imports/CSS, orphaned JSDoc, coverage-ignore drift, changed prop semantics, className collisions, unresolved theme variants, per-render component types (zero function-scope withProps declarations), or new inline style={{…}} props beyond the sanctioned dnd-kit transform.

Caveat: this checkout has no node_modules, so I ran neither the suite, a browser, nor tsc — taking your npm run ci result and the earlier Transition.withProps TS2339 check at face value. Every scan above was written fresh this round and run against the files in the branch.

--- · branch v2/fix-1762-agents-md-sweep

)

Round-12 review follow-up: ScreenStage was the only element component with no
Storybook story, and it's where this PR's one real regression lived (the
`Box → Flex` conversion content-sizing the child screen). Storybook play
functions run in real Chromium, so they can assert layout that happy-dom can't.

Adds ScreenStage.stories.tsx:
- FillsHostWidth — the child fills the stage's width (the direct regression
  guard; a row flex would content-size it). Verified it FAILS when
  `direction: "column"` is removed and passes with it.
- FillVariant — the `fill` prop stretches the stage *layer* (not the plain
  child) to the host height.
- Inactive — an inactive stage is unmounted (Transition keepMounted={false}).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Handled the optional round-12 follow-up in f165d2c — added ScreenStage.stories.tsx, closing the test-surface gap that let the layout regression through.

The key story, FillsHostWidth, is a real regression guard: it renders a stage child with negligible intrinsic width (a single x, standing in for ServerListScreen's container-typed grid) inside a fixed-width host and asserts the child fills that width. Storybook play functions run in real Chromium, so this measures actual layout — which happy-dom can't. I verified it fails when direction: "column" is removed from StageLayer and passes with it, so it genuinely catches the Box → Flex content-sizing bug (not a no-op). Plus FillVariant (the fill prop stretches the layer to the host height) and Inactive (unmounted stage).

npm run ci green — Storybook now 108 files / 459 tests (+3). ScreenStage.tsx itself is unchanged.

This closes every item across all 12 review rounds, including the optional follow-up.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 3m 53s —— View job


Code review — PR #1771 (re-review after f165d2c9)

  • Read the latest commit's diff (f165d2c9 — ScreenStage stories)
  • Verify FillsHostWidth actually guards the regression it claims to
  • Check the story wiring against the repo's Storybook plumbing (typecheck, coverage, a11y gate)
  • Independent rule-offender scan
  • Post review

Verdict

Approve-equivalent — nothing blocking. This is done. f165d2c9 closes the round-12 follow-up with a guard that genuinely guards, and I verified the plumbing rather than assuming it. One optional robustness nit below about how the width assertion is expressed — the invariant it checks is right, the constant it checks against is the fragile part.

Verified: the guard is real, and correctly scoped

clients/web/src/components/elements/ScreenStage/ScreenStage.stories.tsx

  • It fails for the right reason. Without direction: "column", StageLayer is a row flex and <Box bg=…>x</Box> becomes a flex item at flex: 0 1 automax-content ≈ the width of one glyph. 8 > 596 is false, so the assertion trips. With the column direction the width moves to the cross axis where align-items: stretch (Mantine emits align-items: var(--flex-align); unset → invalid-at-computed-value → initial normal → stretch in a flex container) fills it. Matches your empirical add/remove check.
  • It doesn't over-constrain. Reverting StageLayer all the way back to a block Box would also pass — correctly, since that's the behavior being preserved. The story pins the invariant ("child fills the stage width"), not the implementation.
  • The measurement is stable against the Transition. fade-up interpolates only opacity and translateY — no scale — so getBoundingClientRect().width is final the moment findByTestId resolves. No waitFor needed, no flake window.
  • FillVariant asserts the right thing. With bottom: 0 the layer spans the host's 160px; the child's height is not asserted, which is correct — align-items: stretch fills the cross axis (width) while the main axis stays content-sized. The doc comment says exactly that.
  • Plumbing checks out. tsconfig.storybook.json includes src/**/*.stories.tsx and is referenced from tsconfig.json, so tsc -b does typecheck this file (the StoryObj required-args question is settled by your green build). Coverage excludes **/*.stories.{ts,tsx}, so the new file can't move the ≥90 gate. The global a11y.test: 'error' applies, and the three stories render only a bg-tinted Box and an empty relative host, so there's nothing for axe to flag.
  • Inactive correctly documents keepMounted={false} at the story layer, complementing (not duplicating) the three presence-only happy-dom assertions in ScreenStage.test.tsx.

Optional — the absolute HOST_WIDTH - 4 bound is the fragile half of an otherwise solid guard

ScreenStage.stories.tsx:46,71

expect(child.getBoundingClientRect().width).toBeGreaterThan(HOST_WIDTH - 4);

This asserts two things at once: the child fills its stage layer (the invariant you want) and the host actually rendered at 600px (incidental). The second is not guaranteed by anything in the story — w={600} on a Box sets width, not min-width, so any ancestor that makes it a shrinkable flex item narrows it. The global parameters.layout: 'centered' is exactly that shape, and there's no browser.viewport set in the storybook vitest project (vite.config.ts:246-269), so the run takes Vitest's browser default — which is narrower than 600. It passes today, so the centering CSS evidently isn't reaching the portable-story mount; but that's an implicit dependency on Storybook-internal canvas plumbing, and if it ever changes the story fails for a reason that has nothing to do with the regression it exists to catch.

A relative comparison removes the coupling and asserts the invariant directly — a row flex leaves the layer full-width while collapsing the child, so this is if anything a sharper discriminator:

const layer = child.parentElement as HTMLElement;
expect(child.getBoundingClientRect().width).toBeCloseTo(
  layer.getBoundingClientRect().width, 0,
);

Worth noting this is also the repo's established convention: the only two other layout-asserting play functions both compare relatively rather than to a literal — main.getBoundingClientRect().height vs window.innerHeight + 1 (InspectorView.stories.tsx:458) and sidebar.bottom vs detail.bottom (ResourcesScreen.stories.tsx:281-285). FillVariant already derives layer via parentElement, so the helper is right there.

Fix this →


Re-verified clean

  • Rule offenders: none. Re-ran a fresh comment-blanking JSX prop tokenizer over all non-test/non-story .tsx under clients/web/src. After discarding TS-generic false positives (LoggingLevel, MessageDirectionBadgeProps, ElicitRequest, ContentBlock) and nested-brace miscounts (ServerImportConfigModal's <Button disabled={!vm.selectedType}> — one dynamic prop, not two static), every surviving hit is a documented exemption: 4 × Accordion, 3 × Box (component="iframe" / display="grid"), 1 × Transition (ScreenStage:56), plus the out-of-scope non-Mantine MdChevronRight / SortableServerCard / ServerCard and the two MantineProviders in the test helper.
  • TS surface — the full diff adds zero any, zero as unknown as, zero @ts-ignore/eslint-disable, and zero new inline style={{…}}.
  • Zero function-scope withProps declarations across the tree, so no per-render component types.
  • ScreenStage.tsx itself is untouched by this commit — the guard was added around the existing fix, not by adjusting the code to fit the test.
  • README's "every screen and element component has a stories file" holds. Nine non-test .tsx files have no sibling story, but four are ContentViewer sub-components (HtmlFrame, PdfFrame, CsvTable, BinaryNotice, documented through ContentViewer.stories.tsx), one is OAuthAccessTokenField (covered via ConnectionInfoContent), and the rest are groups/ — no elements/ or screens/ component is now missing one.

Nothing found on

any usage, config-level lint suppressions, dead constants/imports/CSS, orphaned JSDoc, coverage-ignore drift, changed prop semantics, className collisions, unresolved theme variants, or new inline styles beyond the sanctioned dnd-kit transform.

Caveat: this checkout has no node_modules, so I ran neither the suite nor a browser nor tsc — taking your npm run ci result (459 Storybook tests) and the add/remove verification of direction: "column" at face value. Everything above was derived from the files in the branch and the repo's Vitest/Storybook/tsconfig wiring.
· branch v2/fix-1762-agents-md-sweep

…ral (#1762)

Round-13 review nit: `HOST_WIDTH - 4` coupled the assertion to the host Box
actually rendering at 600px (which `w={}` sets as `width`, not `min-width`, so a
shrinkable ancestor could narrow it). Compare the child width to its parent
stage layer's actual width with `toBeCloseTo` instead — asserts the fill
invariant directly, matches the repo's relative-comparison convention
(InspectorView/ResourcesScreen stories), and is a sharper discriminator (a row
flex leaves the layer full-width while collapsing the child). Re-verified it
still FAILS when `direction: "column"` is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
@cliffhall

Copy link
Copy Markdown
Member Author

Applied the round-13 robustness nit in $(git rev-parse --short HEAD): both play functions now assert the child width relative to its stage layer's actual width (toBeCloseTo(layer.width, 0)) instead of the literal HOST_WIDTH - 4. That decouples it from host sizing, asserts the fill invariant directly, matches the repo's relative-comparison convention, and is a sharper discriminator. Re-verified it still fails when direction: "column" is removed. npm run ci green.

@cliffhall

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @cliffhall's task in 1m 37s —— View job


Code review — PR #1771 (re-review after 982b4620)

  • Read the latest commit's diff (982b4620 — relative width assertions)
  • Verify the guard still discriminates the regression
  • Check the diff for TS/style rule regressions
  • Post review

Verdict

Approve-equivalent — nothing blocking. 982b4620 is a 15-line change to one story file and it lands the round-13 nit correctly. One observation about a coverage edge the relative form gives up; entirely optional, and I'd merge either way.

Verified

  • The assertion is now decoupled and still discriminates. child.parentElement is genuinely StageLayer — Mantine's Transition renders its function-child directly with no wrapper element, so there's nothing between them. Remove direction: "column" and the child collapses to the single x (~8px) while the layer stays at its left: 0 / right: 0 width, so toBeCloseTo(layer.width, 0) trips. Matches your empirical re-check.
  • The tighter tolerance is safe. toBeCloseTo(…, 0) means <0.5px, down from the old 4px. StageLayer has no padding or border, so its content box equals its border box, and align-items: stretch sets the child's used cross size to exactly the line's cross size — the two rects are bit-identical, not merely close. No flake window, and fade-up interpolates only opacity/translateY (no scale), so the measurement is final the moment findByTestId resolves.
  • HOST_WIDTH is still live (all three render bodies), so no unused-const lint.
  • Convention match — this is now the same relative shape as InspectorView.stories.tsx:458 and ResourcesScreen.stories.tsx:281-285.
  • Diff is clean on the TS/style rules — the commit adds no any, no as unknown as, no @ts-ignore/eslint-disable, no inline style={{…}}, no hex literals. The one cast (child.parentElement as HTMLElement) is a single null-narrowing cast, which the AGENTS.md rule permits (it forbids doubles). ScreenStage.tsx itself is untouched by this commit — the guard was tightened around the existing fix, not the reverse.

Observation (optional) — FillsHostWidth now has no anchor, so one failure mode passes vacuously

ScreenStage.stories.tsx:50-54

Comparing child-to-layer asserts the child fills the layer but no longer asserts the layer fills the host. Those are two separate invariants, and StageLayer owns both (left: 0 and right: 0). If a future edit dropped right: 0, the layer would become shrink-to-fit — collapsing to the child's max-content width — and the child would still equal the layer, so the story passes green while every screen renders a few px wide. The old > HOST_WIDTH - 4 did cover that; the new form doesn't.

FillVariant isn't exposed the same way, because its layer.height > 160 - 4 incidentally proves the layer really spans the host box. FillsHostWidth has no such anchor.

Closing it without reintroducing a literal is one line — compare the layer to its parent, which is the relative host (again, no wrapper in between):

const layer = child.parentElement as HTMLElement;
const host = layer.parentElement as HTMLElement;
expect(layer.getBoundingClientRect().width).toBeCloseTo(
  host.getBoundingClientRect().width, 0,
);
expect(child.getBoundingClientRect().width).toBeCloseTo(
  layer.getBoundingClientRect().width, 0,
);

Both invariants, both relative, no coupling to w={600} actually resolving. Fix this →

Scope note

982b4620 touches exactly one file (ScreenStage.stories.tsx, +15/−6). Everything else in the PR is unchanged since round 13, where the offender scan, baked-prop-override cross-check, dead-CSS sweep, doc-attribution pass, and theme/element separation check all came back clean — I'm not re-litigating those here.

Nothing found on

any usage, lint suppressions, unused constants, dead CSS, coverage-ignore drift, or new inline styles.

Caveat: this checkout has no node_modules, so I ran neither the suite nor a browser — taking your npm run ci result and the direction: "column" add/remove verification at face value. The layout reasoning above was derived from the files in the branch.
· branch v2/fix-1762-agents-md-sweep

@cliffhall
cliffhall merged commit da37692 into v2/main Jul 25, 2026
3 checks passed
@cliffhall
cliffhall deleted the v2/fix-1762-agents-md-sweep branch July 25, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sweep codebase for TypeScript and Mantine/React rule violations (AGENTS.md)

1 participant