Repository navigation
Use non-virtualized scrolling for entity Views with fewer than 100 objects #1618
Description
Summary
Scrolling entity-card trees in view-model-backed Views is reported as jumpy for small populations (for example, fewer than 100 objects). Use ordinary, non-virtualized scrolling for fewer than 100 object nodes while retaining virtualization for 100 or more; scrolling should remain smooth without sacrificing the large-view performance restored by #1177.
Root Cause (verified scope)
features/Phantom.Workspaces.Gui.Shared/Styles/SharedStyles.axaml:303-327 assigns a VirtualizingStackPanel unconditionally as the ItemsPanel for both TreeView.entity-card-tree and TreeView.entity-card-tree-view:
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"
HorizontalAlignment="Stretch"
MinWidth="160"
MaxWidth="{Binding $parent[ScrollViewer].Viewport.Width}" />
</ItemsPanelTemplate>
</Setter>There is no small-population alternative. EntityBrowserWorkspaceTabView.axaml:31-38 binds entity-card-tree to EntityList.Items; MainWindow.axaml:224-228 binds the same style to CurrentViewPopulation.RootEntities. The former collection includes hierarchical items (EntityListViewModel.cs:8,88-130); the latter is only the roots of a population whose Entities collection includes the nodes (ViewPopulationViewModel.cs:24-26). Thus testing only RootEntities.Count would miss a large nested view. The virtualization-to-jump mechanism has not been reproduced or measured here; the confirmed defect is that small trees cannot opt out of the virtualizing panel. Reproduce/measure the reported jump before attributing its precise cause to viewport estimation, variable card heights, or recycling.
Affected Files
| File | Role |
|---|---|
features/Phantom.Workspaces.Gui.Shared/Styles/SharedStyles.axaml:303-327 |
Shared tree panel and horizontal width/inner-scroll constraints; introduce both panel regimes without losing these constraints. |
features/Phantom.Workspaces/Templates/EntityBrowserWorkspaceTabView.axaml:31-38 / features/Phantom.Workspaces/ViewModels/EntityListViewModel.cs:8,88-130 |
Browser tree and complete hierarchical Items count. |
features/Phantom.Workspaces/MainWindow.axaml:224-228 / features/Phantom.Workspaces/ViewModels/ViewPopulationViewModel.cs:24-26 |
Primary View tree: root binding and complete object-node count (Entities). |
features/Phantom.Workspaces.Agent.Gui/Controls/AgentChatEditorControl.axaml:287-295 / features/Phantom.Workspaces.Agent.Gui/Controls/AgentChatToolsDetailControl.axaml:17-22 |
Other consumers of these shared styles; check that changing the shared selector does not silently change their navigation-tree behavior. |
features/Phantom.Workspaces.Tests/EntityListViewModelTests.cs:418-447 |
Existing 1,000-item bounded-realization test to preserve. |
Design / Fix
- Expose a cheap, collection-change-aware complete object-node count for each affected entity View (browser:
EntityList.Items.Count; primary View:CurrentViewPopulation.Entities.Count, notRootEntities.Count). Define the boundary precisely:count < 100uses a non-virtualizing verticalStackPanel;count >= 100usesVirtualizingStackPanel. Count nested nodes even when a parent is collapsed; avoid enumerating/constructing field editors to count. For views that hide/filter items, explicitly decide and test whether the threshold counts all population nodes or currently displayed nodes, rather than accidentally counting roots. - Drive a named small/large class (or equivalent explicit tree state) from that count and apply mutually exclusive
ItemsPanelstyles. Sketch:TreeView.entity-card-tree.small-><StackPanel Orientation="Vertical" HorizontalAlignment="Stretch" MinWidth="160" MaxWidth="{Binding $parent[ScrollViewer].Viewport.Width}" />;TreeView.entity-card-tree.large-> the currentVirtualizingStackPanelwith the same constraints. Do the same forentity-card-tree-viewonly where its consumer should participate; preserve its background, shared item template, sticky behavior, and a single inner vertical/horizontal scroller. Do not simply replace virtualization globally: View becomes unresponsive with thousands of entity cards (no UI virtualization + eager per-card field-editor build) #1177 fixed a freeze with thousands of cards. - Handle count crossing 99 ↔ 100 during live updates, filtering or a View change without losing expansion, selected object/focus, or the visible scroll anchor/offset. Avoid wholesale tree recreation where possible. Confirm the small-tree scroll is smoother in the reported scenario and investigate any remaining jump separately rather than assuming the panel switch solves all causes. Do not eagerly create field editors for offscreen cards.
Expected Tests
| Test Name | Class | What It Verifies |
|---|---|---|
EntityCardTreeView_WhenBoundToFewerThanHundredItems_UsesNonVirtualizingPanel |
EntityListViewModelTests |
For 0, 1 and 99 nodes, the styled tree uses StackPanel and its contents scroll normally. |
EntityCardTreeView_WhenBoundToHundredItems_UsesVirtualizingPanel |
EntityListViewModelTests |
Boundary at exactly 100 uses VirtualizingStackPanel; existing EntityCardTreeView_WhenBoundToThousandsOfItems_RealizesBoundedNumberOfContainers still passes. |
EntityCardTreeView_WhenNestedViewHasHundredNodes_UsesVirtualizingPanel |
EntityListViewModelTests (or primary View GUI test class) |
A few root nodes with 100 total descendants are not misclassified as small. |
EntityCardTreeView_WhenPopulationCrossesThreshold_PreservesSelectionAndScroll |
EntityListViewModelTests (or GUI test class) |
Add/remove across 99/100 switches panels without losing expansion, selection/focus, scroll anchor and horizontal width behavior. |
Related: #1177 (closed, large-view virtualization and deferred card-editor construction). No changes to unrelated logs/diff list virtualization are requested here.
Activity
JoshuaRowePhantom commentedon Oct 7, 2026
Design
The unconditional shared
ItemsPanelvirtualizes even a small tree, and counting primary View roots would miss collapsed descendants. I will retain the existing virtualizing style as the default for agent-chat and other consumers, and add an opt-in small-tree class for the browser and primary View only. Bind an attached total-node-count property toEntityList.Items.CountandCurrentViewPopulation.Entities.Countrespectively (including hidden and collapsed nodes). Its transition handler will preserve the inner scroller offset and restore focus after the panel swap; it will not replace the TreeView or ItemsSource. Shared styles will keep the existing horizontal constraints on both panels. Tests will exercise real styled headless trees at 0/1/99/100/1000 and nested-root boundaries, plus live threshold transitions, selection, expansion and scrolling. Measurement will be reported only if reproducible. Files: shared styles, browser and main-window XAML, an attached behavior in the app, and targeted GUI tests.JoshuaRowePhantom commentedon Oct 8, 2026
Implementation notes
Commit: b20448f
Deviations from proposed design
The two affected trees opt in through an attached total-node-count property; unrelated consumers keep the existing virtualizing default. Switching panels retains the same TreeView/ItemsSource and restores realized expansion, focus and the scroll offset after layout. The threshold counts all population nodes, including hidden and collapsed descendants, not just displayed/visible roots. No desktop scrolling-jump measurement was available in headless tests; smoothness is not claimed and needs manual GUI acceptance.
Scope changes
None. Shared small-panel style is opt-in; other shared-style consumers remain virtualized.
Files changed
Phantom.Workspaces.Gui.Shared/Styles/SharedStyles.axamlPhantom.Workspaces/Controls/EntityTreePanel.csPhantom.Workspaces/MainWindow.axamlPhantom.Workspaces/Templates/EntityBrowserWorkspaceTabView.axamlPhantom.Workspaces.Tests/EntityTreePanelTests.csValidation
Full solution
dotnet build --no-incremental --no-restoresucceeded. TargetedEntityTreePanelTests,EntityListViewModelTestspassed (22/22) on 10 consecutive stability runs using the existing scripts scoped to the GUI test project; script restored afterward. Headless styled trees cover 0, 1, 99, 100, 1000, collapsed nested boundary, narrow viewport, default shared-style consumers and bidirectional live transitions.JoshuaRowePhantom commentedon Oct 8, 2026
Verification result
Status: ❌ 4 issue(s) found
Commits checked: b20448f Fix #1618: switch entity Views to nonvirtualized panel below 100 nodes
Outstanding gaps
EntityTreePanel.UpdatePanelsaves/restores onlyScrollViewer.Offset; inserting/removing a card ahead of the viewport changes which object occupies that pixel offset. The issue requires preserving the visible object/anchor across 99 ↔ 100, including dynamic collection changes. Add an item-identity/relative-position anchor or equivalent, and test inserts/removals before a scrolled-to card.TreeViewIteminstead. Focus on an editor/button is lost; an offscreen focused item may have no realized replacement at all. Test and retain focused element/semantic focus through both transitions, including realistic entity cards.TreeViews and synthetic collections; none instantiate the actual browser template or primary View binding (CurrentViewPopulation.Entities.Countversus roots), change selected Views, filter/hide items, or assert their styled panels at 0/1/99/100/thousands. The nested-100 test supplies a synthetic independent node count, not an actual nested primary View population. Existing tests do cover a shared styled tree, threshold changes, narrow width, and default virtualization, but not the affected production bindings/view changes.Tests
./scripts/run-tests.ps1 -Mode fast -TestNames EntityTreePanelTests,EntityListViewModelTests(worktree 13):scripts/test-results.logreports Executed: 22; Passed: 22; Failed: 0; Skipped: 0. Existing thousands-of-items bounded-realization test passes. No filesystem or Git repository layers are changed;needs-slow-testsdoes not apply. No new tests were skipped; no new TODO, sleep/polling or synchronous task blocking detected.JoshuaRowePhantom commentedon Oct 8, 2026
Verification follow-up
Commit:
50ba65505e2a6cff32e11e3a8d3eaa27688c22f1(afterb20448fcd6b8464fb3d06fa2c2474afeeb77f365)Items.Count; the browser now raises an explicitEntityList.NodeCountnotification on collection changes.Phantom.Workspaces/Controls/EntityTreePanel.cs,Phantom.Workspaces/ViewModels/EntityListViewModel.cs,Phantom.Workspaces/Templates/EntityBrowserWorkspaceTabView.axaml, and tests inEntityTreePanelTests.cs,EntityBrowserWorkspaceTabViewModelTests.cs,MainWindowIntegrationTests.cs.Full solution non-incremental build passed. Focused GUI tests passed 26/26 on each of 10 consecutive runs through the existing test/stability scripts, scoped to the GUI test project. Headless tests verify panel selection and object-anchor position, not subjective scrolling smoothness. The existing headless/process-acceptance infrastructure does not provide a measured desktop wheel-scroll trace for this entity-card view; the reported jump has not been reproduced or quantified before/after. Desktop GUI smoothness remains a genuine manual acceptance check; no smoothness improvement is claimed as measured.
JoshuaRowePhantom commentedon Oct 8, 2026
Verification result
Status: ❌ 4 issue(s) found
Commits checked:
50ba6550(follow-up),b20448fcagainstfeatures.Confirmed improvements
The instantiated browser now binds an explicitly notified
EntityList.NodeCount; the instantiated primary View bindsCurrentViewPopulation.Entities.Count(not roots). Headless integration exercises collapsed 100-node populations, hide-filter, View changes, and browser 0/1/99/100/1000 with bounded realization. A variable-height insert/remove-before-viewport test verifies the same visible object's screen position through both panel transitions. A nested button-focus test passes. Shared-style trees without opt-in remain virtualized; narrow horizontal width and the single scroller are tested. Noscripts/run-tests.ps1change is in the branch.Outstanding gaps
EntityTreePanel.UpdatePanelregisters oneLayoutUpdatedclosure per class change. A closure whose expected panel no longer matches returns without unsubscribing; after a subsequent crossing it can run with its old anchor, offset, expansion and focus and overwrite the current state. There is no cancellation/version guard or test for threshold crossings before layout completes. BrowserSetItemscan perform multiple collection changes in one update.EntityTreePanel.Restore, the replacement focused container is looked up among visual descendants, butScrollIntoView(focusedItem)is only attempted whenanchorItem is null. When a different visible anchor exists and the previously focused item was recycled/offscreen, the container stays null and no focus is restored. Cover this branch and preserve the focused object/control.NestedEditorFocus_SurvivesBothPanelTransitionssubstitutes a syntheticButtonas the item template; the browser integration test substitutesTextBlock. No test exercises the realEntityCardControlfield-editor template across a panel transition, where an index-based visual-child path can no longer be assumed to identify the same editor after rematerialization. This is an unverified required focus case, not a claim that the tested synthetic button fails.Tests
Approved
scripts/run-tests.ps1 -Mode fast -TestNames EntityTreePanelTests,EntityListViewModelTests,BrowserTemplate_ActualItemsCountBinding_SwitchesStyledPanelAtThreshold,PrimaryViewTree_ActualPopulationBinding_CountsCollapsedDescendantsAndViewSwitchfrom worktree 13; inspectedscripts/test-results.log: Executed: 26; Passed: 26; Failed: 0; Skipped: 0. The existing thousands-of-roots bounded-realization test passed. No repository/filesystem layers changed;needs-slow-testsis not applicable. No new disabled tests, unresolved TODOs, timing sleeps, dispatcher-pass pumping, or synchronous task blocking found in the changed code/tests.JoshuaRowePhantom commentedon Oct 8, 2026
Verification follow-up (rapid transitions and editor focus)
Commit:
5ba0c429cad3a85b7065eb336533d9be5d89503b(after50ba6550,b20448fc).LayoutUpdatedhandler before a new threshold crossing; deferred restoration checks the generation again, so queued callbacks cannot apply stale scroll, expansion or focus snapshots. A 99→100→99 before layout, followed by a different anchor/focus and another crossing, is covered.EntityCardControlwith aStringFieldEditorViewModelin edit mode is tested across 99↔100 while another editor is inserted ahead of the focused editor. Restoration matches the editor view-model identity and control type before falling back to the visual path; the actual field TextBox retains focus.Files:
Phantom.Workspaces/Controls/EntityTreePanel.cs,Phantom.Workspaces.Tests/EntityTreePanelTests.cs.Validation: full solution non-incremental build passed. Targeted GUI tests 29/29 passed on each of 10 consecutive stability runs, via the existing scripts scoped to the GUI test project; the script was restored and the worktree is clean. Desktop wheel-scroll jump/smoothness remains unmeasured; no smoothness claim is made, and real-desktop manual acceptance remains outstanding.
JoshuaRowePhantom commentedon Oct 8, 2026
Verification result
Status: ❌ 1 acceptance item outstanding; 0 shippable code/test gaps found.
Commits checked:
5ba0c429,50ba6550,b20448fcagainstfeatures(worktree 13, clean).Code and test verification
LayoutUpdatedhandlers and rejects stale deferred callbacks; the rapid 99→100→99→100 regression checks the later anchor/focus. An unrealized focused item is scrolled into view even with a different visible anchor; focus takes precedence when the two positions conflict. A realEntityCardControlwith aStringFieldEditorViewModelis exercised while another editor is inserted ahead of the focused field; focus remains on the same field identity across both transitions.EntityList.NodeCountwith collection-change notification;CurrentViewPopulation.Entities.Count, not roots). Styled GUI tests cover 0/1/99/100/1000, collapsed descendants at 100, hidden filtering and View changes. Variable-height insertion/removal ahead of the viewport preserves the visible object's position. Thousands of root items retain bounded realization; default shared-style consumers remain virtualized, with one scroller and the horizontal minimum width preserved. No eager offscreen editor construction was introduced.scripts/run-tests.ps1is unchanged. No new skipped/quarantined, timing-dependent, fixed-dispatcher-pass, unbacked TODO or synchronously blocking test/code found. No Git/filesystem layers changed, soneeds-slow-testsdoes not apply.Outstanding acceptance item (not a code/test defect)
verified-locallyis not applied.Tests
Approved
scripts/run-tests.ps1 -Mode fast -TestNames EntityTreePanelTests,EntityListViewModelTests,BrowserTemplate_ActualItemsCountBinding_SwitchesStyledPanelAtThreshold,PrimaryViewTree_ActualPopulationBinding_CountsCollapsedDescendantsAndViewSwitchrun from worktree 13; inspected freshscripts/test-results.log: Executed: 29; Passed: 29; Failed: 0; Skipped: 0. No verification label applied while desktop acceptance is outstanding.Fix #1618: switch entity Views to nonvirtualized panel below 100 nodes
Fix #1618: retain visible tree anchors and focused card controls
Fix #1618: cancel stale tree transitions and restore editor focus
Integrate #1618 entity-tree panel fix into release candidate
Integrate #1618 entity-tree panel transition fix
JoshuaRowePhantom commentedon Oct 9, 2026
The <100-node tree panel and the desktop 100→99 anchor/focus transition fix are now on features via #1626 (merge 1b86a98). A red/green test covers the immediate 100↔99 panel transition; targeted GUI checks and hosted fast (8,019/8,019) and full Windows/Linux CI passed. A Windows desktop harness observed 50px offset steps from injected WM_MOUSEWHEEL messages for 99/100 nodes and bounded realization at 1,100 nodes, but physical wheel input did not reach its test window and the original perceived jump was not reproduced. Leaving open until real physical-wheel scrolling in browser and primary View is accepted.