Skip to content

Decompose App.tsx phase 1: extract the leaf hooks #2128

Description

@cliffhall

Phase 1 of #2126 — decomposing clients/web/src/App.tsx (5,270 lines).

Extract the clusters that have no participation in the connection/OAuth/command cycle described in the parent issue. Each becomes a hook in src/hooks/ returning a single plain object. No useSessionRef needed yet — that seam is Phase 2's prerequisite, and nothing here requires it.

Estimated 600–700 lines out of the component body. Low risk: these are state declarations and mechanical wiring, not control flow.

The hooks

useTabUiState() — ~15 useStates

Lines 1033–1048: toolsUi, promptsUi, resourcesUi, appsUi, tasksUi, logsUi, protocolUi, networkUi, consoleUi, pinnedProtocolIds, activeTab.

Return them bundled rather than as 22 loose values:

const { ui, setUi, activeTab, setActiveTab, pinnedProtocolIds, togglePin } = useTabUiState();

This one has an outsized effect on the JSX: it is currently ~20 separate props on InspectorView (toolsUi, onToolsUiChange, promptsUi, onPromptsUiChange, …). Bundling the state here does not by itself change the prop wall — that is Phase 3 — but it is what makes Phase 3 a rename rather than a redesign.

useInspectorStores(inspectorClient) — ~200 lines

Lines 971–1000 (the 13 Managed*State / Paged*State / MessageLogState / FetchRequestLogState / StderrLogState instances and their setters) plus 1174–1335 (the matching useManagedTools / usePagedTools / useMessageLog / … calls and the pagination hooks).

This is the most mechanical block in the file and the single biggest line count in this phase. It is pure wiring: instantiate a store, hand it to its core/react hook, return the result. The risk is transcription error, not design.

Watch for: fetchLogRef (2002) and listedResourcesRef (819) are read by code that stays behind in Phase 1 — leave them in App.tsx for now, or accept them as arguments, rather than pulling their consumers along early.

useExportActions(stores) — ~148 lines

Lines 4128–4276: onClearLogs, onClearProtocol, onClearNetwork, onClearConsole, onExportLogs, onExportProtocol, onExportNetwork, onExportConsole, onClearProtocolSection, onExportProtocolSection, onTogglePinProtocol, onReplayProtocol.

Depends only on the log states and lib/downloadFile (plus lib/protocolReplay once Phase 0 lands). Genuinely a leaf.

useThemeToggle() — ~16 lines

Lines 671–686. Trivial, included because it is free and it establishes the return-an-object convention.

The toast effects — ~105 lines

Lines 1336–1433, plus the progressToastIdsRef / taskToastIdsRef / activeToolCallTaskIdRef refs at 1136–1173. Split into useProgressToasts and useTaskToasts, consuming the lib/toasts/* modules Phase 0 created.

These are the only hooks in this phase with real effect semantics — subscribe, fire notifications, clean up the ids they own. Treat them as the careful ones.

Deliberately out of scope

useServerListState (the modal/highlight/CRUD block at 699–760 + 4277–4470) looks like a leaf but is not: onConfirmRemove (4287) reaches into the connection lifecycle. It moves in Phase 2 or in its own follow-up, not here.

Coverage

All of these land under the ≥90% gate. Cost estimate, honestly:

  • useTabUiState, useThemeToggle, useInspectorStores — cheap. State and wiring, few branches.
  • useExportActions — moderate. Twelve callbacks, each with a "nothing to export" branch and a filename/format path.
  • The toast hooks — the expensive ones. Effects, notification side effects, and id-set bookkeeping. Note the leaked-timer safety net in src/test/setup.ts and the renderWithMantine rules in AGENTS.md before writing these; notifications.show will need to be observed rather than rendered.

Test these with renderHook where the hook is state-only, and through a host component where effects matter.

Done when

  • Each hook lives in src/hooks/ and returns one object — five of six; useProgressToasts is side-effect-only and returns nothing, recorded as a deliberate exception
  • App.tsx's component body is down by ~600–700 lines — 4,891 → 4,267 (−624)
  • Every new hook at ≥90% on all four dimensions — all six at 100%, no v8 ignore
  • No behavior change; npm run ci green — all 5,150 pre-existing tests pass untouched; ci green (incl. both browser engines)

Activity

  1. added this to the v2.5.0 milestone on Aug 25, 2026
  2. added
    v2Issues and PRs for v2
    choreMaintenance: deps, build tooling, CI, cleanup — no user-facing behavior change
    on Aug 25, 2026
  3. kbsoso9999-maker commented on Aug 26, 2026

    @kbsoso9999-maker
  4. self-assigned this
    on Aug 26, 2026
  5. linked a pull request that will close this issuechore(web): extract App.tsx's leaf hooks #2150on Aug 27, 2026
  6. cliffhall commented on Aug 27, 2026

    @cliffhall
    MemberAuthor

    Recording one deliberate exception to this issue's "Each hook lives in src/hooks/ and returns one object" criterion, so the criteria and the implementation in #2150 agree rather than silently diverging.

    useProgressToasts returns nothing.

    It subscribes to progressNotification, shows/updates/hides toasts, and unsubscribes. The only state it owns is the live-toast-id set, which is bookkeeping the caller must never touch — exposing it would invite exactly the coupling the ref exists to avoid. Returning {} to satisfy the letter of the criterion would put an empty object at the call site and advertise an API that is not there.

    The criterion's stated purpose here is to stop a hook handing back a long list of loose values — useTabUiState's "22 loose values" is the case it was written against, and that one does return a bundled object. A side-effect-only hook is not that case.

    The other five all return one object as specified. The exception is now noted in the hook's own doc block too, so it reads as a decision rather than an oversight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

choreMaintenance: deps, build tooling, CI, cleanup — no user-facing behavior changerefactorCode refactoringv2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions