Phase 3 of #2126 — decomposing clients/web/src/App.tsx (5,270 lines).
<InspectorView> takes roughly 130 props, which is ~180 of App.tsx's 324 JSX lines. Group them into per-domain bundles so the JSX shows the component tree instead of a prop wall.
This is last and deliberately separate. It touches InspectorView, everything below it, and its stories — so it must not ride along with the Phase 2 hook extractions, where an unrelated prop rename would make a 400-line relocation impossible to review.
It is also genuinely optional. If Phases 0–2 land and this never does, App.tsx is already at ~700 lines and the parent's goal is met. Treat it as the finishing pass, not a dependency.
The current wall
The props arrive in obvious domain runs. Tools, for example, is spread across nine separate props:
tools={tools}
excludedTools={excludedTools}
toolsListChanged={toolsListChanged}
toolsLoadError={toolsPagination.error}
toolsUi={toolsUi}
onToolsUiChange={onToolsUiChange}
onCallTool={…}
onRefreshTools={onRefreshTools}
toolsPagination={toolsPaginationControls}
Prompts, resources, tasks, logs, protocol, network, console, and apps follow the same shape. The server-list group (onServerAdd / onServerImportConfig / onServerImportJson / onServerEdit / onServerClone / onServerRemove / onServerReorder / onServerExport / onServerSettings) is another run, and several of those are inline arrow bodies in the JSX today.
The change
One prop per domain:
<InspectorView
connection={connection}
servers={serverList}
tools={toolsPanel}
prompts={promptsPanel}
resources={resourcesPanel}
apps={appsPanel}
tasks={tasksPanel}
logs={logsPanel}
protocol={protocolPanel}
network={networkPanel}
console={consolePanel}
/>
The payoff compounds with Phase 2: a hook can then return exactly a prop bundle, so App.tsx stops assembling props at all and just forwards what a hook handed it. That is the difference between "shorter JSX" and "App.tsx no longer knows what InspectorView needs".
Do this in the same pass
Lift the inline arrow bodies out of the JSX. Several props are multi-line closures declared at the call site — onServerReorder carries a ~15-line .catch with a notifications.show, and onServerAdd / onServerImportConfig / onServerClone each clear the highlight before opening a modal. Those belong in whichever hook owns that domain (useServerListState), not in the tree.
Note the void-wrapping arrows (onToggleConnection, onCallTool, onGetPrompt, onReadResource, onOpenApp, onCancelTask, onDisconnect) exist because @typescript-eslint/no-floating-promises is at error. They can move into the bundles, but each still needs its one-line justification comment per the AGENTS.md rule — do not drop them into a bundle unexplained.
Risks
- This is the phase most likely to cause churn below
InspectorView. Decide up front whether the bundles stop at InspectorView (it destructures and passes down as before) or continue downward. Stopping at InspectorView is the cheaper, more reviewable choice and is the recommendation.
- 96+ story files exist, and every screen and element component has one. A prop-shape change ripples into fixtures. Budget for it — and the Storybook play functions run headless in
npm run ci, so breakage is loud rather than silent.
- Keep the bundle types beside the components that consume them (
components/**/types.ts is already excluded from the coverage include, so type-only modules do not distort the gate).
Coverage
Mostly type-level and prop-plumbing, so the incremental coverage cost is small — but any inline closure lifted out of the JSX lands in a gated hook and needs a test, and the story fixtures must be updated rather than deleted to make the gate pass.
Done when
Phase 3 of #2126 — decomposing
clients/web/src/App.tsx(5,270 lines).<InspectorView>takes roughly 130 props, which is ~180 of App.tsx's 324 JSX lines. Group them into per-domain bundles so the JSX shows the component tree instead of a prop wall.This is last and deliberately separate. It touches
InspectorView, everything below it, and its stories — so it must not ride along with the Phase 2 hook extractions, where an unrelated prop rename would make a 400-line relocation impossible to review.It is also genuinely optional. If Phases 0–2 land and this never does, App.tsx is already at ~700 lines and the parent's goal is met. Treat it as the finishing pass, not a dependency.
The current wall
The props arrive in obvious domain runs. Tools, for example, is spread across nine separate props:
Prompts, resources, tasks, logs, protocol, network, console, and apps follow the same shape. The server-list group (
onServerAdd/onServerImportConfig/onServerImportJson/onServerEdit/onServerClone/onServerRemove/onServerReorder/onServerExport/onServerSettings) is another run, and several of those are inline arrow bodies in the JSX today.The change
One prop per domain:
The payoff compounds with Phase 2: a hook can then return exactly a prop bundle, so App.tsx stops assembling props at all and just forwards what a hook handed it. That is the difference between "shorter JSX" and "App.tsx no longer knows what
InspectorViewneeds".Do this in the same pass
Lift the inline arrow bodies out of the JSX. Several props are multi-line closures declared at the call site —
onServerReordercarries a ~15-line.catchwith anotifications.show, andonServerAdd/onServerImportConfig/onServerCloneeach clear the highlight before opening a modal. Those belong in whichever hook owns that domain (useServerListState), not in the tree.Note the
void-wrapping arrows (onToggleConnection,onCallTool,onGetPrompt,onReadResource,onOpenApp,onCancelTask,onDisconnect) exist because@typescript-eslint/no-floating-promisesis aterror. They can move into the bundles, but each still needs its one-line justification comment per the AGENTS.md rule — do not drop them into a bundle unexplained.Risks
InspectorView. Decide up front whether the bundles stop atInspectorView(it destructures and passes down as before) or continue downward. Stopping atInspectorViewis the cheaper, more reviewable choice and is the recommendation.npm run ci, so breakage is loud rather than silent.components/**/types.tsis already excluded from the coverageinclude, so type-only modules do not distort the gate).Coverage
Mostly type-level and prop-plumbing, so the incremental coverage cost is small — but any inline closure lifted out of the JSX lands in a gated hook and needs a test, and the story fixtures must be updated rather than deleted to make the gate pass.
Done when
InspectorViewtakes ~10 domain props instead of ~130test:storybookgreennpm run cigreen