Skip to content

refactor(desktop): break up the app-shell god component and stabilize action factories #1043

Description

@Astro-Han

Context

apps/desktop/src/renderer/app-shell.tsx is a 1490-line god component: ~40 hook/state
calls, ~20 inline helpers, 2 inline effects, and a 430-line JSX tree (1188-1614)
tangling session-workspace state, live-turn projection, action wiring, onboarding
routing, layout/resize, nav, overlays, and command assembly.

The app-shell-*.ts "split" reduced line count but not wiring: 9 createAppShell*Actions
factories (779/803/439/412/595/827/836/759/288) run unmemoized in render body, each
taking 10-20 deps and re-allocating every render. Only sessionRowActionHandlers is
ref-stabilized (:422). Consequences:

  • app-shell.tsx:875 — the settle-fallback effect depends on settleAssistantStreaming,
    a fresh closure each render, so its 1s timer tears down/re-arms every render (many
    times/sec while streaming). Same churn class the codebase already fixed for search.
  • app-shell.tsx:857 — reconcilePersistedMessages runs in a messages-triggered effect
    with an object dep (activeLiveTurn); should be event-driven, not effect-reconciled.

Proposed

  • Memoize the factories (or convert to hooks using internal refs) so the split becomes
    a real boundary and identities are stable.
  • Carve the detail-panel JSX (1247-1577) into , and extract a settings-
    controller and onboarding/seed controller, each owning its own state.
  • Colocate self-contained slices: expertTeams (609) → Composer-only hook; memoryActive
    (199) + refreshMemoryActive (1039) → memory-pill hook.

Acceptance

app-shell orchestrates rather than owns; factories have stable identity; the settle
timer no longer re-arms per render.

Plan (2026-07-17)

Investigation findings:

  • Factory closures are not stale (deps are mostly refs/setState); the concrete defect class is identity churn. The only observable harm today: the settle-fallback timer (app-shell.tsx:920) tears down/re-arms every render because settleAssistantStreaming is a fresh closure. The reconcile effect is already fixed via useEffectEvent.
  • The JSX seams already exist as cohesive hook results (useShellChatModel, useAppShellProjectContext, useAppShellComposerAttachments); the JSX flattens them into ~60 props. <ChatWorkspace> must take hook-result objects by owner — flattening would just move the god component one level down as god props.
  • Composer stays mounted across sections (hidden when not on sessions) to preserve drafts, so ChatWorkspace must be always-mounted and receive active/hidden flags. This is the one non-mechanical point of the extraction.

Delivery — three separately revertable PRs, in dependency order:

  1. Stabilize action factories. Shared useStableActions(factory, deps) (latest-ref) across all 9 createAppShell*Actions; remove the manual sessionRowActionHandlers ref-mirror special case (superseded path). Settle timer stops re-arming by construction. Contract tests: handler identity stable across renders; fallback timer arms once under render churn.
  2. Colocate self-contained slices.expertTeams → Composer-owned hook; memoryActive + refreshMemoryActive → memory-pill hook.
  3. Extract <ChatWorkspace> (ChatView + health notice + interaction slot + Composer + SessionWorkbar) taking hook results by owner, and <OnboardingEmptyState>. Pure code motion, no behavior change.

(2+3 may ship as one PR with two commits to avoid rebase churn on the same JSX region.)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions