8.5 KiB
status, system, requirements
| status | system | requirements | |||
|---|---|---|---|---|---|
| draft | ui |
|
Task Surface Render Isolation System Design
Purpose and boundaries
The UI system owns render isolation for task surfaces with many repeated rows. This design covers the file tree, sidebar task rows, pull-request indicators, and task-top-bar plugin contributions.
The task, integration, and plugin systems still own their data and actions. This design does not change their contracts, persistence, or user-visible behavior.
The browser trace from 2026-08-31 showed three full render waves in 5.74 seconds. Each wave rendered approximately 602 file rows and 48 sidebar rows. Layout, paint, and garbage collection were minor costs, so this design targets React work and mounted component count.
Requirement mapping
| Requirement | Design sections |
|---|---|
REQ-UI-FILE-TREE-CHAT-CONTEXT-001 |
File-tree rows, File-tree virtualization, Responsive behavior |
REQ-UI-SIDEBAR-TASK-ROW-PRESENTATION-001 |
Sidebar rows, Derived contribution props, Responsive behavior |
REQ-UI-MOBILE-TASK-NAVIGATION-001 |
Responsive behavior, Failure and recovery |
Components and responsibilities
useFileBrowserTreeowns stable file-tree state and action identities.FileTreeViewowns row-window calculation and the single file-tree scroll owner.TreeNodeItemowns one file or directory row and skips semantically unchanged renders.FileContextMenupreserves the established pointer, keyboard, and touch actions for mounted rows.useSidebarSelectionowns stable selection actions for the task switcher.TaskSwitcherandTaskRowskip work when their rendered inputs do not change.TaskTopBarPluginActionsderives stable session identifiers for plugin slot props.useTaskPRTooltipHydrationreturns a stable value while its status and actions do not change.PRTaskIconreuses stable empty and unchanged pull-request inputs.
Render identity contract
A parent render must not change a row callback when the callback behavior is unchanged. A hook must not return a new aggregate object when all fields retain the same identity and value.
List rows use memoized component boundaries. Each row receives the smallest required state. A row rerenders when its node, selection, expansion, active-file, drag, or action state changes.
Render-count tests prove identity behavior. The tests use controlled rerenders and do not use elapsed-time thresholds.
File-tree rows
File-tree actions read changing tree data from stable refs or functional state transitions. The
callbacks do not depend on the complete treeState aggregate object.
TreeNodeItem uses a memoized boundary with explicit, stable inputs. Selection or expansion of one
node does not rerender unrelated visible rows. File opening, drag and drop, rename, download,
deletion, multi-selection, and chat-context actions keep their current behavior.
Each mounted row keeps its own context-menu trigger. A shared menu is not required for this change. Virtualization limits the number of mounted menu providers.
File-tree virtualization
FileTreeView uses the existing @tanstack/react-virtual dependency. The flattened visibleRows
array remains the source for order, nesting depth, expansion, and filtering.
The existing file-tree viewport remains the only vertical scroll owner. The virtualizer positions only the current row window and a small overscan window inside that viewport.
Rows use measured sizes because editing controls and touch layouts can change row height. The virtualizer uses an estimated size before measurement. It updates positions after a measured size changes.
Only positive row measurements can replace cached sizes. A hidden panel can
produce zero-height observer entries even though its rows remain connected.
Those entries represent unavailable geometry, not a row with no content.
The measurement callback retains the last positive size for the row key, or
uses the current estimate when no positive measurement exists.
When the panel becomes visible, normal measurement accepts its actual row sizes.
This applies to node rows and inline creation controls in both pointer modes.
It implements AC-UI-FILE-TREE-CHAT-CONTEXT-001.10 without another scroll owner
or changes to Dockview panel lifetime.
Active-file reveal and restored scroll operations resolve a row index from visibleRows. They use
the virtualizer scroll API before they query a row element. This sequence makes an offscreen row
available before focus or selection work uses its element.
Sidebar rows
useSidebarSelection derives onBulkMove from stable action functions and current selection data.
It does not depend on a newly allocated selection aggregate.
TaskSwitcher receives stable callbacks during unrelated parent renders. A memoized task row can
then isolate row work when task data and row state do not change.
The task row remains the primary mobile tap target. The visible mobile action and fine-pointer hover behavior remain unchanged.
Derived contribution props
Plugin slot props use a stable session-identifier array while the ordered identifiers do not change. Session status updates cannot rerender the plugin contribution when its slot input stays equal.
Pull-request tooltip hydration returns one stable aggregate while its status, hydration action, and automation options stay equal. Pull-request indicators also reuse one empty array constant.
These changes do not alter plugin host APIs or integration data. They only stabilize values at UI component boundaries.
Control flow
- A store or local state update renders a task-surface owner.
- Stable selectors and callbacks preserve identities for unchanged inputs.
- Memoized boundaries skip unchanged rows and contributions.
- The file-tree virtualizer calculates the required row window from the scroll position.
- React renders only changed components inside the active row window.
Responsive behavior
Desktop and mobile use the same flattened file-tree model and virtual row window. The layout does not create a second vertical or horizontal scroll owner.
Each mounted mobile file row keeps its visible action trigger and 44 CSS pixel touch target. The responsive menu stays inside the viewport and keeps its safe-area behavior.
Each mobile sidebar row remains the primary navigation action. Its explicit task menu stays visible and touch-reachable. Virtualization does not change the sidebar composition.
Failure and recovery
If row measurement is unavailable, the virtualizer uses the estimated row size. If a target row is offscreen, reveal logic scrolls to its index before it requests the element.
An empty tree renders the current empty state. A changed filter or expansion state recalculates the flat row list and virtual range from the current scroll owner.
Identity optimizations must fail toward an extra render, not stale UI. Equality logic must include every input that changes visible output or interaction behavior.
Persistence
This design adds no persisted state. Existing expansion, selection, file, sidebar, plugin, and pull-request state keeps its current owner and lifetime.
Security
This design adds no trust boundary or data access. Virtual rows use the same authorized file and task data as the current complete list.
Observability
Unit tests record render counts for controlled updates. Browser E2E tests record the number of mounted rows for a large generated file tree.
Measurement tests cover positive sizes, unavailable sizes with a cached value, and unavailable sizes before the first measurement. Browser regressions hide and restore the Files panel, then compare visible row bounds with the viewport. Coverage includes compact desktop rows, touch rows, and scrolled large trees.
Final performance evaluation uses a warmed production build without React DevTools or CPU profiler startup in the measured window. The report includes main-thread work, long tasks, and mounted-row counts. Development traces remain diagnostic evidence only.
Related decisions
No ADR is required. This design applies the repository's existing virtual-list dependency and UI ownership model.