Skip to content

Tracking: split App.tsx (3782 → ≤500 lines) — staged extraction #565

@esengine

Description

@esengine

Why

src/cli/ui/App.tsx is 3782 lines — 4.7× the project's 800-line
hard ceiling and the single largest architectural debt in the
codebase. It's dominated by one AppInner component (line 323)
holding 164 hook calls and ~20 pieces of top-level state.

This file blocks several other improvements:

Strategy

Staged extraction, every stage independently shippable. No big
bang. Each stage:

  1. Pulls one cohesive concern out of AppInner (a custom hook or
    a sub-component).
  2. Lands as one PR with green tests and no behavior change.
  3. Reduces App.tsx line count measurably; we update the tracking
    table here as we go.

Per the project's autonomous-refactor pattern, once stages are
agreed we ship them back-to-back without pausing between PRs.

Stages

Each item below becomes a sub-PR (and may get its own issue if
discussion-worthy). Order is approximate — earlier ones unblock
later ones, but adjacent stages can land in either order.

Phase 1 — extract custom hooks (low risk)

State piles that already have a clear domain. These are the
cheapest wins.

  • useTerminalSetup() — bracketed paste / modifyOtherKeys
    / DECSET 1007 / cleanup on unmount (currently App.tsx:398-426).
  • useToolProgressDisplay()ongoingTool + toolProgress
    + statusLine + the spinner-row state machine.
  • useEditGate()pendingEdits ref + pendingCount
    mirror + syncPendingCount + interceptor wiring + editMode
    + persistence.
  • useWorkspaceRoot()currentRootDir + /cwd handler
    + propagation effects to memory / hooks / shell allowlist.
  • useHookList()hookList + /hooks reload + cwd
    rescan + loop sync.
  • usePresetMode()preset + persistence + thinking
    mode + escalation arming.
  • useLanguageReload()languageVersion + i18n reload
    effect (currently a 2-line useState + useEffect that can
    live next to its consumer).

Target after Phase 1: ~2500 lines.

Phase 2 — extract sub-components

The render output is a long JSX tree that mostly returns one big
column. Decompose by visual region.

  • <TerminalDressing> — DECSET-driven terminal escape
    bracketing (uses Phase 1's useTerminalSetup).
  • <HeaderBar> / <StatusBar> — already partly split as
    StatusRow; consolidate any remaining inline-rendered
    header bits.
  • <MessageStream> (already exists as CardStream) — confirm
    no inline overrides leak from App.tsx.
  • <ComposerArea> — composer + hint + slash overlay (uses
    existing Composer + PromptInput; consolidate App.tsx-level
    glue into the area).
  • <LiveActivityRow> — subagent + tool spinners + status
    line, all the "what's happening right now" rows.
  • <UndoBanner> / <EditQueueBadge> — small, self-contained.

Target after Phase 2: ~1500 lines.

Phase 3 — extract the slash-command surface

Slash command handlers are currently a wall of inline closures in
AppInner. Move them to a typed registry.

  • Define a SlashHandler type + a registry module.
  • Each handler gets its own file or grouping (mode commands /
    session commands / mcp commands / etc.).
  • App.tsx imports the registry, invokes by name. Closure capture
    moves to thin adapters that accept an AppContext object.
  • Existing src/cli/ui/slash/commands.ts (389 lines) is the
    probable home — extend it rather than create a parallel
    structure.

Target after Phase 3: ~800 lines.

Phase 4 — final polish

Non-goals

  • Behavior changes. Each stage is mechanical refactor. Bug fixes
    ride separate PRs.
  • Renaming / restyling things. If a hook is called useFoo today
    the extracted hook stays useFoo. Stylistic swings expand the
    diff and mask real changes.
  • Test coverage growth as a goal. Add a test if extraction surfaces
    a clean seam where one pays back; don't add tests just to bump
    numbers (per CLAUDE.md).
  • Cross-file API changes. We may expose hooks from new files,
    but no caller outside cli/ui/ should need to learn new
    imports.

Acceptance

Tracking

Phase PR Status App.tsx LOC after
Baseline 3782
Phase 1 (hooks) TBD not started ~2500 (target)
Phase 2 (components) TBD not started ~1500 (target)
Phase 3 (slash) TBD not started ~800 (target)
Phase 4 (polish) TBD not started ≤500 (target)

Tracking lives here. Update the table on every merge.

Relates to

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttrackingTracking issue / umbrella for a multi-PR effort

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions