From 4f929acc1037caebd7ce1ba1d51827f7bc9b62b7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 21:50:47 -0700 Subject: [PATCH] fix(dashboard): stop over-aggressive component unmounts (keep-alive for planning, terminals, popups) (#2420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md: every confirmed source of unnecessary unmount/remount churn in the dashboard, plus a keep-alive layer for conversation- and terminal-bearing surfaces. ## What changed **Keying / component identity (U1–U3)** - Streaming chat segment key no longer embeds `entries.length` — an expanded thinking block stays expanded while entries stream into it (R1). - Dock task list keys `TaskCard` rows by `task.id` (occurrence suffix only for the duplicate-id anomaly) instead of `id-index` — no remount on reorder/filter/status change (R2). - `ProviderStatusBadge` / `GitHubStatusBadge` hoisted out of ModelOnboardingModal's render body (R3); MCP server rows key by `server.name` alone (R4). **Keep-alive layer (U4–U6)** - New shared `KeepAliveView` wrapper: visible = in-flow flex child; hidden = out-of-flow `position:absolute; inset:0` with `visibility:hidden; pointer-events:none` + `aria-hidden` (never `display:none`, so xterm geometry never collapses). - Planning Mode renders as a kept-alive sibling of the MainContent switch after first open (per-project latch mirroring Quick Chat). While hidden, the session-list SSE, recovery poll, and elapsed ticker suspend via a new `active` prop; reveal re-subscribes and refreshes the sessions list once. Payload-carrying entry points (initial-plan handoff, resume) and project switches remount via a new `modalManager.planningEntryGeneration` key, preserving pre-keep-alive fresh-open semantics. `recordResumeEvent` instrumentation records `remount` on first activation and `route-active` on reveal. - Task-detail Terminal / Worktree-terminal / Planner-chat tabs stay mounted-but-hidden after first open (per-task latches; task switch/close still disposes fully). `SessionTerminal` gains `active`: reveal refits + forces a font remeasure, and if the WS died while hidden it re-runs the full attach lifecycle (dead-socket recovery). - Popped-out task windows hide via FloatingWindow `hidden` instead of leaving the render array; `TaskDetailContent` gains `active` so hidden popups close their SSE/EventSource channels while the terminal WS stays open. `visiblePoppedOutTaskEntries` remains the Escape-shortcut consumer. **Planning Mode internal-transition audit (U7)** - Audit findings: session-list mode and mobile list/detail flips are CSS-class transitions over one always-mounted detail pane (no state-discarding unmounts); re-selecting the active session is an early-return visibility restore; session switching intentionally reloads from the session row (stream re-attach for generating sessions); remaining index keys are on stateless lists. No product-code defects found; regression tests now lock the always-mounted invariant on desktop + mobile. **Cheap-view state (U8)** - CommandCenter (active sub-tab + date range) and DevServerView (selected script/task + typed-but-unsent command) persist per project via `modalPersistence` and restore after their (intentional) unmount round-trips. Also fixed the candidate auto-fill effect clobbering a customized non-empty command. ## Symptom Verification - **Original symptom:** streaming thinking blocks collapsed mid-stream; terminals reconnected and lost scroll/input on tab flips; Planning Mode lost in-flight interviews on navigation; popped-out windows vanished off-view; dock cards remounted on reorder. - **Exact reproduction:** (1) expand a thinking block during a stream; (2) run a command in the Terminal tab, flip to Plan and back; (3) start a planning interview, navigate Board and back; (4) pop out a task with board/list-only scoping and switch views; (5) change a dock task's status. - **Assertion it is gone:** component-identity/instrumentation tests in TaskChatTab, SessionTerminal, TaskDetailModal (worktree/planner-chat/tabs), PlanningModeModal keep-alive + internal-transitions, App keep-alive round-trip, and App.taskPopupViewGating assert no remount and preserved state for each repro, across desktop and mobile breakpoints. ## Verification - File-scoped vitest: 23 files / 1091 tests green (all touched suites plus FloatingWindow, TerminalModal, TaskPlannerChatTab, lazy-loaded-views guard, App suites). - `pnpm verify:fast`: PASS (13 steps — scoped typecheck/build, CLI build, boot smoke). - `pnpm check:changesets`: passes; changeset `fix-dashboard-remount-churn` (`@runfusion/fusion` patch, labeled format). - Known pre-existing failures NOT caused by this branch (verified failing at base a224c1111 in a clean worktree): 7 tests in `TaskDetailModal.oversight-controls/oversight-mobile/models-progress-workflow`. - jsdom cannot prove rendered-grid correctness for xterm reveal; per the plan's risk note, manual browser verification of terminal reveal remains recommended. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Bug Fixes** * Switching views or tabs no longer resets Planning Mode, task details, terminals, planner chats, or popped-out task windows. * Streaming content remains expanded and stable as new entries arrive. * Hidden views suspend background activity and resume correctly when shown. * Terminal sessions reconnect automatically when needed. * **Improvements** * Command Center and Dev Server selections persist per project. * Custom Dev Server commands are preserved while browsing suggestions. * Improved stability when reordering task lists and updating server states. * **Documentation** * Updated dashboard guidance for hidden, retained task pop-ups and view transitions. --------- Co-authored-by: Claude Fable 5 --- .changeset/fix-dashboard-remount-churn.md | 7 + docs/dashboard-guide.md | 3 +- ...22-001-fix-dashboard-remount-churn-plan.md | 292 ++++++++++++++++++ packages/dashboard/app/App.tsx | 56 +++- .../App.taskPopupViewGating.test.tsx | 51 ++- .../app/components/DevServerView.tsx | 30 +- .../dashboard/app/components/DockTaskList.tsx | 22 +- .../app/components/KeepAliveView.css | 18 ++ .../app/components/KeepAliveView.tsx | 30 ++ .../app/components/PlanningModeModal.tsx | 58 +++- .../app/components/ProjectSelector.css | 5 + .../app/components/SessionTerminal.tsx | 36 ++- .../dashboard/app/components/TaskChatTab.tsx | 8 +- .../app/components/TaskDetailModal.css | 5 + .../app/components/TaskDetailModal.tsx | 187 +++++++---- .../app/components/TerminalModal.tsx | 53 +++- .../app/components/__tests__/App.test.tsx | 42 ++- .../__tests__/DevServerView.test.tsx | 29 ++ .../__tests__/DockTaskList.test.tsx | 75 ++++- .../__tests__/KeepAliveView.test.tsx | 65 ++++ ...ingModeModal.internal-transitions.test.tsx | 98 ++++++ .../PlanningModeModal.keep-alive.test.tsx | 177 +++++++++++ .../__tests__/SessionTerminal.test.tsx | 44 +++ .../components/__tests__/TaskChatTab.test.tsx | 53 ++++ ...kDetailModal.attachments-and-tabs.test.tsx | 32 ++ ...skDetailModal.popup-hidden-gating.test.tsx | 120 +++++++ .../__tests__/TaskDetailModal.test.tsx | 7 +- ...TaskDetailModal.worktree-terminal.test.tsx | 102 +++++- .../__tests__/TerminalModal.test.tsx | 23 ++ .../command-center/CommandCenter.tsx | 25 +- .../__tests__/CommandCenter.test.tsx | 31 ++ .../app/components/dashboard/MainContent.tsx | 65 +--- .../dashboard/PlanningKeepAlive.tsx | 84 +++++ .../app/components/dashboard/types.ts | 3 - .../settings/sections/McpServersCard.tsx | 6 +- .../dashboard/app/hooks/modalPersistence.ts | 80 +++++ .../dashboard/app/hooks/useModalManager.ts | 12 + 37 files changed, 1849 insertions(+), 185 deletions(-) create mode 100644 .changeset/fix-dashboard-remount-churn.md create mode 100644 docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md create mode 100644 packages/dashboard/app/components/KeepAliveView.css create mode 100644 packages/dashboard/app/components/KeepAliveView.tsx create mode 100644 packages/dashboard/app/components/__tests__/KeepAliveView.test.tsx create mode 100644 packages/dashboard/app/components/__tests__/PlanningModeModal.internal-transitions.test.tsx create mode 100644 packages/dashboard/app/components/__tests__/PlanningModeModal.keep-alive.test.tsx create mode 100644 packages/dashboard/app/components/__tests__/TaskDetailModal.popup-hidden-gating.test.tsx create mode 100644 packages/dashboard/app/components/dashboard/PlanningKeepAlive.tsx diff --git a/.changeset/fix-dashboard-remount-churn.md b/.changeset/fix-dashboard-remount-churn.md new file mode 100644 index 0000000000..21c5aef2e4 --- /dev/null +++ b/.changeset/fix-dashboard-remount-churn.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Terminals, planning sessions, and popped-out task windows no longer reset when switching views or tabs. +category: fix +dev: Keep-alive layer (KeepAliveView, visibility-based out-of-flow hiding) for Planning Mode, task-detail terminal/planner-chat tabs, and popped-out task FloatingWindows; hidden surfaces suspend SSE/EventSource work via `active` props. Stable keys for streaming chat segments, dock task cards, and MCP server rows. CommandCenter/DevServerView selections persist per project via modalPersistence. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index de443ef050..fb49559db5 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -196,7 +196,8 @@ If **Settings → Appearance → Open tasks in the right sidebar** is enabled, b FNXC:TaskPopupGeometry 2026-07-03-00:00: Desktop/tablet task popups share a persisted geometry key across task IDs so operators can size and place the popup once, then open other tasks without repeating that setup. Mobile remains a full-screen sheet regardless of saved desktop geometry. FNXC:RightDockTaskPopup 2026-07-03-00:00: The same task-popup preference also applies to ordinary clicks in the right-dock Tasks list so that opted-in operators never lose the list to embedded detail by clicking a dock task row. FNXC:TaskPopupLayer 2026-07-17-15:55: Task popups and Quick Chat share the board/task-detail interaction stack, so the most recently clicked or focused overlapping surface appears on top. Terminal, Files, New Task, workflow editor, and other utility windows retain their separate higher utility stacking. -FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 makes popup view scoping default-on for every dashboard view. Hidden popups are render-only hidden, not closed, so snapshots and shared geometry survive navigation; same-task popups remain independently addressable by origin view. --> +FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 makes popup view scoping default-on for every dashboard view. Same-task popups remain independently addressable by origin view. +FNXC:TaskPopupViewGating 2026-07-22-13:20: FN remount-churn fix R7 — off-origin-view popups are now render-only hidden (FloatingWindow `hidden`: visibility-based, aria-hidden, effects suspended), never unmounted. The embedded task detail, including an open terminal WebSocket, stays live while hidden; the detail's SSE/EventSource channels close via `active={false}` and reopen on reveal. Returning to the origin view is an instant reveal, not a remount. --> **Settings → Appearance → Open tasks as popups** changes ordinary board task-card clicks, List row/card opens, and right-dock Tasks-list clicks across desktop, tablet, and mobile viewports. When enabled, those clicks use the existing task popup/FloatingWindow surface on the board/task-detail layer instead of the full-panel task detail, List split-detail/docked detail, or right-dock task detail, keeping the board, List view, or dock list visible in the background. Overlapping Quick Chat and task popups interleave by the most-recent pointer or focus interaction; other utility windows retain their higher global stacking. On desktop and tablet, task popups restore the last saved popup size and position between tasks; on mobile, task popups stay full-screen sheets. Board task-card `changes`/`retries`/`workflow` chips open the popup with the requested tab. List context-menu/refine actions, task-detail links, plugin/graph opens, and explicit pop-out actions keep their existing paths. **Settings → Appearance → Keep task popups on the view where they were opened** is enabled by default. Every task-detail popup is attached to its exact originating view, including Planning, Agents, Command Center, Documents, Missions, and plugin views: navigating elsewhere hides it without closing it, and returning re-shows it in the same saved position. You can open the same task independently in more than one view; closing or pressing Escape on one popup does not affect the other. Disable this setting only to restore legacy globally shared popups. Legacy saved popups without an origin remain visible everywhere for compatibility. diff --git a/docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md b/docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md new file mode 100644 index 0000000000..542e798643 --- /dev/null +++ b/docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md @@ -0,0 +1,292 @@ +--- +title: "fix: Stop over-aggressive component unmounts across the dashboard" +type: fix +status: completed +date: 2026-07-22 +--- + +# fix: Stop over-aggressive component unmounts across the dashboard + +## Summary + +Fix every confirmed source of unnecessary unmount/remount churn in the dashboard: two volatile-key bugs, two mechanical anti-patterns, and — the substantive part — a keep-alive layer so conversation- and terminal-bearing surfaces (Planning Mode, task-detail terminal and planner-chat tabs, popped-out task windows) survive navigation and tab switches mounted-but-hidden, mirroring the existing Quick Chat hidden-FloatingWindow pattern. Includes a deep audit of Planning Mode's internal session/view transitions and lightweight state preservation for the cheaper views. + +--- + +## Problem Frame + +A remount audit (2026-07-22) found the dashboard unmounts components far more often than intended. The main-content area is a pure `if (taskView === ...) return` switch (`packages/dashboard/app/components/dashboard/MainContent.tsx`), so every navigation destroys the previous view entirely; TaskDetail's tab body is one mutually-exclusive ternary, so every tab switch tears down terminals (WebSocket close + xterm dispose) and the planner chat; popped-out task windows are filtered out of the render array when the user leaves their origin view. On top of the architectural cause, two keys are more volatile than the identity they represent: the streaming chat segment key embeds `entries.length` (remounts the actively streaming thinking block on every entry), and the dock task list keys `TaskCard` by `id-index` (remounts on any reorder). + +User-visible symptoms: an expanded thinking block snapping shut on every streamed entry, terminals reconnecting and losing scroll/unsent input on every tab flip, Planning Mode losing in-flight interview UI (view state, streaming output, retry counters, unsaved summary edits) on any sidebar navigation, and floating task windows vanishing and remounting when switching views. + +The codebase already contains the correct pattern twice: Quick Chat stays mounted via `