Skip to content

refactor(desktop): make AppShell a composition root, R2 (migrate by call site) #4582

Description

@chihumyum

Why a new issue

#3439 was closed when the R1 foundation (#4088, corrected by #4249) merged, with 9 of its 12 checklist items still open. Four controller migrations (#4315, #4316, #4491, #4498) and the Usage slice (#4425 / #4440) currently reference a closed issue. This issue is the successor and carries the remaining work.

The definition of done changes, following the evidence Astro-Han posted on #3439 and #4109: extracting a feature into features/<name>/ does not change the scope of any state. A slice is done when

  1. none of its hooks is called in the render body of AppShell or AppShellContent (the entry disappears from scripts/check-app-shell-hooks.mjs, or shrinks to a single equality-selected read that the shell genuinely needs);
  2. its window.maka.* paths are gone from app-shell.tsx (the bridgePaths entry in renderer-architecture.json shrinks);
  3. it is registered as a controllerOwners entry once refactor(desktop): move Module Hub controller below AppShell #4315 lands;
  4. the same PR deletes the prop threading and helpers the move makes redundant.

Baseline (upstream/main 6c8e749d3, 2026-09-03)

app-shell.tsx 3,324 lines, 105 imports
hooks in AppShell + AppShellContent render bodies 42 hooks / 78 call sites (check:app-shell-hooks)
window.maka.* paths in app-shell.tsx 32 distinct / 33 references (ledger bridgePaths)
feature slices 9 (connection-settings, goals, module-hub, runtime-host-management, session-collaboration, session-navigation, session-settings, task-entry, workbar)

For reference, app-shell.tsx had 23 bridge references when #3439 opened; the five extraction PRs did not stop new product work (#3427, #3497, #3635, #3764, #3798) from landing bridge calls in the shell. The R1 ratchet has held the count since 2026-08-30.

Checklist

Each item names the hooks and bridge paths that must leave AppShell/AppShellContent. Claim an item by commenting; one slice per PR.

Controllers already extracted, call site still in the shell

Shell-owned state that has no slice yet

  • App Update — inline useEffect/useState; bridge app.installUpdate, app.retryUpdateDownload, app.subscribeUpdateStatus, app.updateStatusrefactor(desktop): move App Update controller below AppShell #4498 (open)
  • Usage settings — legacy settings/usage-settings-page.tsxrefactor(desktop): extract Usage settings into a renderer feature slice #4425 / refactor(desktop): move Settings → Usage into a renderer feature slice #4440 (open; extraction only, pricing editor separate)
  • Conversation host / transcript runtimeuseAppShellTurnPresentation, useShellLiveTurn, useActiveSessionEvents, useTurnActionRegistry, useActiveExecutionBoundary, useSessionEventHealthPolling, useShellRunUpdates, useShellResume, useShellMemoryPill, useShellChatModel; bridge transcripts (2), sessions.listActiveInteractions, sessions.subscribeActiveInteractions, sessions.listTurnLandmarks, sessions.compact, notifications.runEnded
  • Task submission / composeruseTaskSubmissionReadiness, useComposerAttachments, useAppShellComposerQuotes, useNewTaskChoice; bridge attachments, attachments.readBytes, sessions.updateQueueEntry, sessions.retractQueueEntry, sessions.promoteQueueEntry, sessions.reorderQueueEntries
  • Session controlsuseSessionSettingIntent, useSessionCollaborationDialog, usePlanModeState, useSettingsModal; bridge sessions.getPlanState, sessions.abandonPlanProposal, sessions.setCollaborationMode
  • Session catalog / workspace authorityuseAppShellSessionWorkspace, useAppShellSessionUiReads, useAppShellProjectContext, useAppShellNavRefSync; bridge sessions (whole namespace handed down)
  • WorkHub router — bridge workHub.resolveCoordinationSession, workHub.record, workHub.candidates, workHub.act, settings.getClient, settings.subscribeClientChanged, runtimeHostProfiles.subscribeChanges, connections.subscribeEvents (coordinate with tracking(desktop): evolve WorkHub into a per-Runtime-Host conversational coordinator #3492)
  • App lifecycle / bootstrapuseAppShellBootstrapSubscriptions, useAppShellHostEffects, useAppShellPersistenceEffects, useOnboardingSnapshot, useShellConnections (3), useShellAppearance, useSystemUiLocale; bridge diagnostics.copyReport, diagnostics.copyPreviousMainProcessInterruption, diagnostics.takePreviousMainProcessInterruption, onboarding.setMilestone
  • Commands / overlaysuseCommandPalette, useKeyboardHelp, useShellSearch, useToast
  • Raw shell stateuseState (15 + 2), useEffect (14), useLayoutEffect (2), useStableActions (6) converge to the region/provider that reads them; this item closes last

Shared infrastructure (do before the next three slices)

  • Collapse the per-slice join cost (Astro-Han, refactor(desktop): make AppShell a renderer composition root #3439). Provide one createServicesContext<S>(name) outside the renderer zones (checker treats packages/ui and src/shared as external) and reduce Desktop adapters to { ns: bridge.ns } or { ...bridge.ns, <real adaptation> }. Measured on 6c8e749d3 with tsc -p tsconfig.renderer.json: 81 of 119 adapter forwarders are identity. Replacing identity forwarders with ...bridge.<ns> and keeping the real adaptation verbatim produced zero new type errors for goals, module-hub (skills, scheduledTasks), session-navigation, task-entry, and workbar (terminal, todo, browser, attachments); together that is about 80 lines removed across 5 adapters. The blocks that mix identity forwarders with multi-line adaptation (workbar review/artifacts/inspector/sideChat, module-hub dailyReview, runtime-host-management peerMesh) need a hand pass; connection-settings, session-collaboration, and session-settings do real adaptation and stay as they are. Bridge namespaces are plain objects (no this in preload), so passing them whole is runtime-safe.
  • One gate for AppShell hooks (needs @Astro-Han's agreement — the hook gate's no---write friction is deliberate). Proposal: the ledger gains a component-scoped hookCalls for AppShell/AppShellContent with exact-match semantics, controllerOwners (refactor(desktop): move Module Hub controller below AppShell #4315) is the provenance authority, and scripts/check-app-shell-hooks.mjs retires. Until then both gates run in CI.
  • Ratchet hardening (follow-ups to fix(desktop): ratchet renderer debt against the base tree, not its ledger #4249): fail instead of warn when the base worktree cannot be materialized under CI; run the base commit's checker against the head tree as a cross-check so a PR cannot lower both sides by weakening a rule; add the two-commit git-fixture test fix(desktop): ratchet renderer debt against the base tree, not its ledger #4249 deferred.

Invariants (unchanged from #3439)

No product, visual, IPC, storage-schema, copy, or shortcut changes. Composer mount semantics, bootstrap behavior, streaming isolation, Session-switch races, and Workbar resource lifecycles stay compatible. Performance evidence, where given, is measured in one running Electron instance with alternating configurations and paired trials, and the claim is bounded to what was measured.

Non-goals

No new global state library or service locator; no IPC redesign; no packages/ui extraction; no mechanical moves into features/ that leave the hook call site in the shell.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions