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.
- 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.
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.