Files
orca/src/renderer/src/app-shell/AppRootSurfaces.tsx
T
Neil a3b472d050 refactor(renderer): decompose App.tsx into an app-shell module (#14607)
* refactor(renderer): decompose App.tsx into an app-shell module

App.tsx was 2831 lines behind an `eslint-disable max-lines` and a
grandfathered entry in the max-lines ratchet baseline. It is now 92 lines:
a root element, the shared providers, and three children.

The body is split by concern into src/renderer/src/app-shell/:

- use-app-chrome-layout — titlebar/sidebar/workbench layout derivations
- use-floating-workspace-panel — overlay open state, persistence, return focus
- use-app-startup-hydration — the boot chain (order preserved verbatim)
- use-app-session-persistence — session writer + shutdown checkpoint
- use-persisted-ui-writer / use-document-appearance /
  use-runtime-graph-sync / use-window-visibility-effects
- use-onboarding-and-feature-tips — first-run education gating
- use-global-keybindings + app-command-handlers — window shortcut dispatch
- use-app-shell-services — app-level subscriptions that outlive any surface
- AppWorkspaceShell / AppRootSurfaces / AppBackgroundServices / titlebar parts

Two startup branches that no longer needed to sit inline moved to
src/renderer/src/startup/: startup-ssh-connection-restore and
startup-degraded-recovery.

No behavior change. Sibling order of root overlays and modals is preserved
so stacking is unchanged; the sidebar's virtualized scroll refs still live
above the sidebar's remount boundary.

The source-assertion tests in app-startup-routing.test.ts follow the code to
its new files. The one that claimed to check "first-window startup services
before terminal reconnect" was matching the degraded-recovery block, not the
success path; it now asserts the success path, and the degraded ordering
keeps its own dedicated test.

Removes the disable comment and drops App.tsx from
config/max-lines-baseline.txt.

* refactor(renderer): move app-shell ref writes out of render

React Doctor's changed-lines purity gate flags three render-phase ref
writes in the new app-shell files. The patterns predate the split, but
moving them into new files brings them into the gate.

- use-app-chrome-layout: the terminal-workbench latch becomes state set
  during render (the pattern use-lazy-modal-mounts already uses). The
  `canMountTerminalWorkbenchNow ||` term keeps the current render correct,
  so the latch only has to be visible to the next one.
- use-app-startup-hydration: the onboarding callback ref syncs in an effect
  declared before the boot chain, so it lands first on mount. The callback
  is a `useCallback([])`, so its identity never actually changes.
- use-global-keybindings: the shortcut-state mirror syncs in useLayoutEffect,
  which commits before any key event can read it. Key events are discrete,
  so handlers always observe committed state.

All three now hold committed state only, so nothing can leak from a render
React discards — the behavior the gate is protecting.

pnpm run check:react-doctor:changed: 0 errors (was 3).
2026-08-14 17:46:44 -07:00

369 lines
15 KiB
TypeScript

import { Suspense } from 'react'
import { lazyWithRetry as lazy } from '@/lib/lazy-with-retry'
import { translate } from '@/i18n/i18n'
import { RecoverableRenderErrorBoundary } from '../components/error-boundaries/RecoverableRenderErrorBoundary'
import NewWorkspaceComposerModal from '../components/NewWorkspaceComposerModal'
import { CrashReportDialog } from '../components/crash-report/CrashReportDialog'
import { MarkdownTemplatePicker } from '../components/editor/MarkdownTemplatePicker'
import RecentTabSwitcher from '../components/tab-bar/RecentTabSwitcher'
import { SkillFreshnessUpdateDialog } from '../components/skills/SkillFreshnessUpdateDialog'
import { StarNagCard } from '../components/StarNagCard'
import { StarNagAgentValueMomentObserver } from '../components/star-nag/StarNagAgentValueMomentObserver'
import { StarNagToastHost } from '../components/star-nag/StarNagToastHost'
import { TelemetryFirstLaunchSurface } from '../components/TelemetryFirstLaunchSurface'
import { ZoomOverlay } from '../components/ZoomOverlay'
import { shouldRenderPetOverlay } from '../components/pet/pet-overlay-visibility'
import { useAppStore } from '../store'
import type { UpdateStatus } from '../../../shared/update-status-types'
import { useLazyModalMounts } from './use-lazy-modal-mounts'
import type { FloatingWorkspacePanelState } from './use-floating-workspace-panel'
import type { OnboardingGate } from './use-onboarding-and-feature-tips'
const QuickOpen = lazy(() => import('../components/QuickOpen'))
const WorktreeJumpPalette = lazy(() => import('../components/WorktreeJumpPalette'))
const WorkspaceCleanupDialog = lazy(
() => import('../components/workspace-cleanup/WorkspaceCleanupDialog')
)
const StatusBar = lazy(() =>
import('../components/status-bar/StatusBar').then((module) => ({ default: module.StatusBar }))
)
const SetupGuideModal = lazy(() => import('../components/setup-guide/SetupGuideModal'))
const FeatureWallModal = lazy(() => import('../components/feature-wall/FeatureWallModal'))
const FeatureTipsModal = lazy(() => import('../components/feature-tips/FeatureTipsModal'))
const AddRepoDialog = lazy(() => import('../components/sidebar/AddRepoDialog'))
const NonGitFolderDialog = lazy(() => import('../components/sidebar/NonGitFolderDialog'))
const AddProjectFromFolderDialog = lazy(
() => import('../components/sidebar/AddProjectFromFolderDialog')
)
const ProjectAddedDialog = lazy(() => import('../components/sidebar/ProjectAddedDialog'))
const DeleteWorktreeDialog = lazy(() => import('../components/sidebar/DeleteWorktreeDialog'))
const PreservedBranchBatchReviewModal = lazy(
() => import('../components/sidebar/PreservedBranchBatchReviewModal')
)
const DictationController = lazy(() =>
import('../components/dictation/DictationController').then((module) => ({
default: module.DictationController
}))
)
const SshPassphraseDialog = lazy(() =>
import('../components/settings/SshPassphraseDialog').then((module) => ({
default: module.SshPassphraseDialog
}))
)
const UpdateCard = lazy(() =>
import('../components/UpdateCard').then((module) => ({ default: module.UpdateCard }))
)
const RemoteServerUpdateDialog = lazy(
() => import('../components/settings/RemoteServerUpdateDialog')
)
const ContextualTourOverlay = lazy(() =>
import('../components/contextual-tours/ContextualTourOverlay').then((module) => ({
default: module.ContextualTourOverlay
}))
)
const SetupGuideTelemetryObserver = lazy(() =>
import('../components/setup-guide/SetupGuideTelemetryObserver').then((module) => ({
default: module.SetupGuideTelemetryObserver
}))
)
const FloatingTerminalPanel = lazy(() =>
import('../components/floating-terminal/FloatingTerminalPanel').then((module) => ({
default: module.FloatingTerminalPanel
}))
)
// Why: lazy so the WebP asset + overlay module aren't fetched unless the experimental flag is on.
const PetOverlay = lazy(() => import('../components/pet/PetOverlay'))
// Why: lazy so onboarding's step modules + assets aren't fetched for users past first-launch.
const OnboardingFlow = lazy(() => import('../components/onboarding/OnboardingFlow'))
type BoundaryProps = {
boundaryId: string
resetKey?: string | number | boolean | null
title?: string
description?: string
children: React.ReactNode
}
function ModalBoundary({ children, ...props }: BoundaryProps): React.JSX.Element {
return (
<RecoverableRenderErrorBoundary surface="modal" compact {...props}>
{children}
</RecoverableRenderErrorBoundary>
)
}
function OverlayBoundary({ children, ...props }: BoundaryProps): React.JSX.Element {
return (
<RecoverableRenderErrorBoundary surface="overlay" compact {...props}>
{children}
</RecoverableRenderErrorBoundary>
)
}
function shouldMountUpdateCardForStatus(status: UpdateStatus): boolean {
if (status.state === 'idle') {
return false
}
if (status.state === 'checking' || status.state === 'not-available') {
return status.userInitiated === true
}
return true
}
/**
* Every overlay and modal hosted at the App root, in a fixed sibling order so stacking stays
* stable. Each is gated so its chunk is only fetched once the surface can actually appear.
*/
export function AppRootSurfaces(props: {
floatingWorkspace: FloatingWorkspacePanelState
onboardingGate: OnboardingGate
}): React.JSX.Element {
const { floatingWorkspace, onboardingGate } = props
const { mountedLazyModalIds, shouldMountAddRepoDialog } = useLazyModalMounts()
const activeView = useAppStore((s) => s.activeView)
const activeModal = useAppStore((s) => s.activeModal)
const settings = useAppStore((s) => s.settings)
const statusBarVisible = useAppStore((s) => s.statusBarVisible)
const persistedUIReady = useAppStore((s) => s.persistedUIReady)
const petVisible = useAppStore((s) => s.petVisible)
const petEnabled = useAppStore((s) => s.settings?.experimentalPet === true)
const dictationState = useAppStore((s) => s.dictationState)
const updateStatus = useAppStore((s) => s.updateStatus)
const activeContextualTourId = useAppStore((s) => s.activeContextualTourId)
const hasSshCredentialRequest = useAppStore((s) => s.sshCredentialQueue.length > 0)
const shouldMountSetupGuideTelemetryObserver = persistedUIReady
const shouldMountUpdateCard = shouldMountUpdateCardForStatus(updateStatus)
const shouldMountDictationController =
settings?.voice?.enabled === true || dictationState !== 'idle'
const renderPetOverlay = shouldRenderPetOverlay({ persistedUIReady, petEnabled, petVisible })
return (
<>
{floatingWorkspace.shouldMountPanel ? (
<Suspense fallback={null}>
<OverlayBoundary
boundaryId="overlay.floating-workspace"
resetKey={floatingWorkspace.open}
title={translate('auto.App.1b3024bcd6', 'The floating workspace hit an error.')}
description={translate(
'auto.App.7cbfbf622f',
'Retry the floating workspace or close and reopen it.'
)}
>
<FloatingTerminalPanel
open={floatingWorkspace.open}
onOpenChange={floatingWorkspace.setOpenWithFocus}
tourInteractionSnapshot={floatingWorkspace.tourInteractionSnapshotRef.current}
/>
</OverlayBoundary>
</Suspense>
) : null}
{statusBarVisible ? (
<Suspense
fallback={
<div className="h-6 min-h-[24px] shrink-0 border-t border-border bg-[var(--bg-titlebar,var(--card))]" />
}
>
<OverlayBoundary
boundaryId="overlay.status-bar"
resetKey={activeView}
title={translate('auto.App.2e8ff36f94', 'The status bar hit an error.')}
description={translate(
'auto.App.8a023cea1f',
'Retry the status bar to remount its controls.'
)}
>
<StatusBar floatingTerminalOpen={floatingWorkspace.open} />
</OverlayBoundary>
</Suspense>
) : null}
{/* Why: keep in the entry bundle so a stale/corrupt lazy chunk can't strand users at Create. */}
{activeModal === 'new-workspace-composer' ? (
<ModalBoundary boundaryId="modal.new-workspace-composer" resetKey>
<NewWorkspaceComposerModal />
</ModalBoundary>
) : null}
<Suspense fallback={null}>
{shouldMountAddRepoDialog ? (
<ModalBoundary boundaryId="modal.add-repo" resetKey={activeModal === 'add-repo'}>
<AddRepoDialog />
</ModalBoundary>
) : null}
{/* Why: Settings can start Add Project without Sidebar, so its handoff dialogs must share the root host. */}
{activeModal === 'confirm-non-git-folder' ? (
<ModalBoundary boundaryId="modal.confirm-non-git-folder" resetKey>
<NonGitFolderDialog />
</ModalBoundary>
) : null}
{activeModal === 'confirm-add-project-from-folder' ? (
<ModalBoundary boundaryId="modal.confirm-add-project-from-folder" resetKey>
<AddProjectFromFolderDialog />
</ModalBoundary>
) : null}
{activeModal === 'project-added' ? (
<ModalBoundary boundaryId="modal.project-added" resetKey>
<ProjectAddedDialog />
</ModalBoundary>
) : null}
</Suspense>
{/* Why: root overlays can render Radix <Tooltip>s; keep inside the shared provider so lazy surfaces mount from any entry point. */}
<Suspense fallback={null}>
{mountedLazyModalIds.has('workspace-cleanup') ? (
<ModalBoundary
boundaryId="modal.workspace-cleanup"
resetKey={activeModal === 'workspace-cleanup'}
>
<WorkspaceCleanupDialog />
</ModalBoundary>
) : null}
</Suspense>
<Suspense fallback={null}>
{mountedLazyModalIds.has('quick-open') ? (
<ModalBoundary boundaryId="modal.quick-open" resetKey={activeModal === 'quick-open'}>
<QuickOpen />
</ModalBoundary>
) : null}
{mountedLazyModalIds.has('worktree-palette') ? (
<ModalBoundary
boundaryId="modal.worktree-palette"
resetKey={activeModal === 'worktree-palette'}
>
<WorktreeJumpPalette />
</ModalBoundary>
) : null}
{mountedLazyModalIds.has('setup-guide') ? (
<ModalBoundary boundaryId="modal.setup-guide" resetKey={activeModal === 'setup-guide'}>
<SetupGuideModal />
</ModalBoundary>
) : null}
{mountedLazyModalIds.has('feature-wall') ? (
<ModalBoundary boundaryId="modal.feature-wall" resetKey={activeModal === 'feature-wall'}>
<FeatureWallModal />
</ModalBoundary>
) : null}
{mountedLazyModalIds.has('feature-tips') ? (
<ModalBoundary boundaryId="modal.feature-tips" resetKey={activeModal === 'feature-tips'}>
<FeatureTipsModal />
</ModalBoundary>
) : null}
</Suspense>
{shouldMountSetupGuideTelemetryObserver ? (
<Suspense fallback={null}>
<SetupGuideTelemetryObserver />
</Suspense>
) : null}
{activeContextualTourId !== null ? (
<Suspense fallback={null}>
<ContextualTourOverlay />
</Suspense>
) : null}
{/* Why: mount only after UI hydration, else a hidden pet flashes while the store still holds default visibility. */}
{renderPetOverlay ? (
<Suspense fallback={null}>
<OverlayBoundary boundaryId="overlay.pet" resetKey={petVisible}>
<PetOverlay />
</OverlayBoundary>
</Suspense>
) : null}
{shouldMountUpdateCard ? (
<Suspense fallback={null}>
<OverlayBoundary boundaryId="overlay.update-card" resetKey={activeView}>
<UpdateCard />
</OverlayBoundary>
</Suspense>
) : null}
<OverlayBoundary boundaryId="overlay.star-nag" resetKey={activeView}>
<StarNagCard />
</OverlayBoundary>
<OverlayBoundary boundaryId="overlay.star-nag-toast" resetKey={activeView}>
<StarNagToastHost />
</OverlayBoundary>
<StarNagAgentValueMomentObserver />
{/* Why: mount at App root to render once per session; internal cohort gate limits it to pre-telemetry users — see telemetry-plan.md §First-launch experience. */}
<OverlayBoundary
boundaryId="overlay.telemetry-first-launch"
resetKey={settings?.telemetry?.optedIn ?? 'unknown'}
>
<TelemetryFirstLaunchSurface />
</OverlayBoundary>
<OverlayBoundary boundaryId="overlay.zoom" resetKey={activeView}>
<ZoomOverlay />
</OverlayBoundary>
<Suspense fallback={null}>
{activeModal === 'delete-worktree' ? (
<ModalBoundary boundaryId="modal.delete-worktree" resetKey>
<DeleteWorktreeDialog />
</ModalBoundary>
) : null}
{activeModal === 'preserved-branch-review' ? (
<ModalBoundary boundaryId="modal.preserved-branch-review" resetKey>
<PreservedBranchBatchReviewModal />
</ModalBoundary>
) : null}
</Suspense>
{hasSshCredentialRequest ? (
<Suspense fallback={null}>
<ModalBoundary boundaryId="modal.ssh-passphrase" resetKey={activeModal}>
<SshPassphraseDialog />
</ModalBoundary>
</Suspense>
) : null}
<ModalBoundary boundaryId="modal.markdown-template-picker" resetKey={activeModal}>
<MarkdownTemplatePicker />
</ModalBoundary>
<RecoverableRenderErrorBoundary
boundaryId="modal.crash-report"
surface="modal"
reportAsCrash={false}
resetKey={activeModal}
compact
title={translate('auto.App.722d03aa62', 'The crash report dialog hit an error.')}
description={translate(
'auto.App.acd66311dc',
'Use the Help menu after retrying if you still need diagnostics.'
)}
>
<CrashReportDialog />
</RecoverableRenderErrorBoundary>
{onboardingGate.onboarding && onboardingGate.shouldRender ? (
<Suspense fallback={null}>
<RecoverableRenderErrorBoundary
boundaryId="modal.onboarding"
surface="modal"
title={translate('auto.App.f02d37278a', 'Onboarding hit an error.')}
description={translate(
'auto.App.221a95ba38',
'Retry onboarding or close it and continue in the app.'
)}
>
<OnboardingFlow
onboarding={onboardingGate.onboarding}
onOnboardingChange={onboardingGate.setOnboarding}
/>
</RecoverableRenderErrorBoundary>
</Suspense>
) : null}
{shouldMountDictationController ? (
<Suspense fallback={null}>
<OverlayBoundary boundaryId="overlay.dictation" resetKey={activeView}>
<DictationController />
</OverlayBoundary>
</Suspense>
) : null}
<OverlayBoundary boundaryId="overlay.recent-tab-switcher" resetKey={activeView}>
<RecentTabSwitcher />
</OverlayBoundary>
{/* Why: hosts a live terminal pane needing the link-routing preference context; mounting outside crashes it. */}
<OverlayBoundary boundaryId="overlay.skill-freshness-update-dialog">
<SkillFreshnessUpdateDialog />
</OverlayBoundary>
<Suspense fallback={null}>
<OverlayBoundary boundaryId="overlay.remote-server-update-dialog">
<RemoteServerUpdateDialog />
</OverlayBoundary>
</Suspense>
</>
)
}