This document records when a larger frontend refactor pays off, a lightweight map of apps/desktop/src/App.tsx, and three incremental extraction candidates. It implements the team decision from the refactor plan: prefer small PRs aligned with architecture.md (components/, features/).
Treat refactoring as a tool, not a goal. Start or schedule work when at least one of these is true:
| Signal | What it means here |
|---|---|
| Reviews | PRs touching App.tsx are hard to review because unrelated concerns (sidebar + settings + splits) change together. |
| Bugs | Regressions recur in the same area (e.g. drag/drop, workspace restore, settings modal) because state and UI are tightly coupled in one file. |
| Feature tempo | New UI takes disproportionate effort: finding the right useEffect / handler among thousands of lines, or fear of breaking unrelated flows. |
| Onboarding | New contributors cannot form a mental model of “where host list lives” vs “where terminal layout lives” within a reasonable time. |
If none of these apply and releases are stable, defer large splits; use the map below only when you pick up the next refactor PR.
Already in good shape: Rust modules in src-tauri, shared helpers under features/, and IPC in tauri-api.ts — no parallel backend refactor is required for frontend cleanup.
Line ranges are approximate (~4048 lines in App.tsx after sidebar bridge + settings split). Use them as navigation hints, not rigid contracts.
| Region (lines) | Content |
|---|---|
| ~1–~220 | Imports; lazy LayoutCommandCenter; re-exports from features/ (split-tree, workspace snapshot, pane DnD, preferences, session model, bootstrap, …). |
| ~220–~3610 | export function App(): state, refs, effects (many persisted in hooks/ — workspace bootstrap/persist, ref sync, session-output trust listener), handlers (connect, backup, layout, DnD, …). |
| ~3610–~3668 | createSplitPaneRenderer → renderSplitNode bridge; host list rows live in HostSidebar via HostListRow + hostListRowBridge. |
| ~3669–end | Root JSX: app-shell, HostSidebar, TerminalWorkspaceDock (workspace tabs + DnD, terminal grid / mobile pager, footer), context menus, settings, modals (AddHostModal, QuickConnectModal, TrustHostModal), mobile shell. Split panes use renderSplitNode from SplitWorkspace.tsx. |
Pure logic (no React) now also lives in: split-tree.ts, workspace-snapshot.ts, pane-dnd.ts, app-preferences.ts, app-bootstrap.ts, session-model.ts, app-id.ts, tauri-runtime.ts (with Vitest where noted in repo).
Hooks: useAppRefSync, useSessionOutputTrustListener, useWorkspaceLocalStorage (useWorkspaceBootstrapFromStorage, useWorkspacePersistToStorage).
Each step should be one PR, with npm test (and npm run build in apps/desktop) green before merge.
Target: apps/desktop/src/features/view-profile-filters.ts.
Status: Done — logic + ViewFilterHostRow live in that module; Vitest in view-profile-filters.test.ts; App.tsx wires evaluateGroup, createDefaultViewProfile, and createEmptyViewFilterRule.
Target: apps/desktop/src/components/AppSettingsPanel.tsx (shell + tab wiring) and apps/desktop/src/components/settings/ (app-settings-types, app-settings-panel-props, app-settings-constants, per-tab modules under settings/tabs/).
Status: Done — settings UI split by main tab; App still passes the full prop surface into AppSettingsPanel.
Target: apps/desktop/src/components/HostSidebar.tsx.
Status: Done — sidebar chrome, filters, and host list call back into App via props; rows render inside the sidebar via hostListRowBridge + HostListRow.
Target: apps/desktop/src/hooks/useSettingsModalDrag.ts.
Status: Done — encapsulates the auto-center effect, the pointer-move/up listener loop, the close-while-dragging cleanup, and the header pointer-down handler. App.tsx now reads settingsModalRef, settingsModalPosition, isSettingsDragging, and handleSettingsHeaderPointerDown from a single hook call (~70 lines removed from App).
Target: apps/desktop/src/styles.css (~9k lines).
Status: Planned — not yet executed because per-section splits would touch every component import. Suggested order for incremental extraction (one section per PR):
- Tokens (
:rootcustom properties + font-faces) →styles/tokens.css. - App shell + sidebar (sidebar, host rows, host slide, add-host modal) →
styles/sidebar.css. - Workspace (split panes, drop overlays, terminal chrome, NSS-Commander dock) →
styles/workspace.css. - Settings + modals (settings shell, tabs, dialogs, popovers) →
styles/settings.css. - File panes (file table, breadcrumbs, semantic colors, transfer progress) →
styles/file-pane.css.
Each split should stay zero-diff visually and pass the existing Playwright screenshot suite (npm run screenshots) before merging.
Target: apps/desktop/src/components/TerminalPane.tsx.
Status: Done — TerminalPane is wrapped in React.memo so unrelated parent re-renders (sidebar, settings, drag, host list refresh) do not cascade into the xterm subtree. Both callback props (onUserInput, onSessionWorkingDirectoryChange) are already useCallback-stable in App.tsx, so the default shallow comparison is correct. Not yet memoized: RemoteFilePane and LocalFilePane — they receive a spec/handler bag that today is reconstructed each render inside SplitWorkspace.createSplitPaneRenderer; stabilizing those references is the prerequisite. Memoizing createSplitPaneRenderer directly is not useful: its bridge captures ~80 closure fields that change on most re-renders, so memoization would either be a no-op or risk stale-closure bugs.
- Component smoke tests: shared bridge fixtures in
host-list-row-fixtures.ts;HostListRow.test.tsx(row UI),HostSidebar.test.tsx(empty list, one row, settings click viaminimalHostSidebarProps()). useReducer/ context for workspace or split state: defer until interaction bugs or review pain justify a single owner for that subgraph; current hooks + props remain easier to follow for most changes.- Further shrink
App:HostSidebarrendersHostListRowvia a typedhostListRowBridge(norenderHostRowcallback inApp). Right-dock workspace UI lives inTerminalWorkspaceDock. Remaining cohesive slices for follow-up extractions include the settings sub-tab routing (6 sub-tabuseStates + cross-tab navigation calls) and the Identity-Store form drafts (8 user / key form fields inApp).
Compiler output next to sources (duplicate .js files) is avoided by "noEmit": true in apps/desktop/tsconfig.json. npm run build remains tsc && vite build: typecheck only from tsc, bundling from Vite.