fix(tui): make all TUI list views scrollable (#1351)#1363
Merged
Aaronontheweb merged 8 commits intoJun 9, 2026
Merged
Conversation
All SelectionList-based pages now call .WithFillHeight() so the list expands to fill the terminal and scrolls natively via Termina 0.11. McpToolPermissionsPage gets a ScrollableContainerNode wrapping a dedicated DynamicLayoutNode for tool rows, with EnsureToolCursorVisible() keeping the cursor row in the viewport. Cursor navigation invalidates only the tool rows node (not the whole content node) so the scroll container's offset survives between keystrokes. SessionsPage replaces the raw TextNode for-loop with SelectionListNode using .WithHighlightedIndex(), delegating scroll entirely to Termina. ProviderStepView drops the 30-item model truncation and "... and N more" sentinel — now that the list scrolls natively those workarounds are gone. Adds four smoke tapes (mcp-permissions, approvals, model-manager, sessions-tui) for the TUI surfaces that had no interactive coverage.
Aaronontheweb
enabled auto-merge (squash)
June 9, 2026 01:39
Aaronontheweb
disabled auto-merge
June 9, 2026 01:39
Aaronontheweb
commented
Jun 9, 2026
| private DynamicLayoutNode? _contentNode; | ||
| private DynamicLayoutNode? _footerNode; | ||
| private DynamicLayoutNode? _toolRowsNode; | ||
| private ScrollableContainerNode? _toolScrollNode; |
Aaronontheweb
enabled auto-merge (squash)
June 9, 2026 13:45
Aaronontheweb
added a commit
to Aaronontheweb/netclaw
that referenced
this pull request
Jun 29, 2026
…coverage The PR's SessionsPageTests referenced a non-existent SessionsViewModel.ResumeSessionId (resume state lives on ChatNavigationState), which broke the build on every Test-* job. Rewrite it to observe the shared ChatNavigationState, register a stub /chat landing route so the Enter/N resume paths don't throw 'No route matches', use the real SessionId (strips the 'session-' prefix), and enqueue Ctrl+Q in the render-only cases so they stop burning the 10s cancellation timeout. The corrected tests pass against current dev: arrow keys reach the ViewModel, SelectedIndex tracks, and Enter resumes the highlighted session — the two-way selection binding netclaw-dev#1363 preserved is intact, so the described regression does not reproduce in dev. Add IdentityRedoPageTests: first page-level coverage of the identity-redo flow through Termina's real render + input pipeline, asserting the timezone submit reaches the saved screen (the user-visible symptom of the redo loop).
Aaronontheweb
added a commit
to Aaronontheweb/netclaw
that referenced
this pull request
Jun 29, 2026
The session list is rendered as a focusable SelectionListNode (FocusPriority 10, added by netclaw-dev#1363). On a populated list, ReactivePage.ApplyFocusPolicy hands it keyboard focus, and Termina's input dispatch routes keys to the focus manager (SelectionListNode.HandleInput consumes UpArrow/DownArrow/Enter/Escape, returning true) BEFORE they ever reach the ViewModel. SessionsPage had no HandlePageInput override and relied on a ViewModel.Input subscription, so on a real terminal the focused list swallowed every navigation/selection key: the highlight could not be driven, SelectedIndex stayed 0, and Enter resumed nothing — 'you can't select anything in netclaw sessions'. Fix: claim the keys in SessionsPage.HandlePageInput (dispatched before the focus manager) and forward them to a new public SessionsViewModel.HandleKey. The list stays a pure renderer driven one-way by .WithHighlightedIndex(SelectedIndex). This matches what HandlePageInput is documented for ('page-level input before focused components') and the established pattern in the other list pages. Why no test caught it: the empty-state tape (sessions-tui) renders a TextNode, not a focusable list, so it never hit the path; and the headless xUnit harness does not hand the list focus during input processing, so keys fell through to the ViewModel and the prior tests were false positives. They now drive the real HandlePageInput path — removing the override fails them — but faithful focus-eating coverage still needs native/manual validation.
Aaronontheweb
added a commit
that referenced
this pull request
Jun 29, 2026
…er regression test (#1518) * fix(tui): prevent identity redo loop on timezone submit The IdentityStepView is a singleton on IdentityRedoViewModel. Each layout rebuild creates new TextInputNode instances with new subscriptions added to _stepSubs, but old subscriptions were never cleared. After the user submitted the timezone field, stale callbacks from the previous render would re-fire and re-trigger GoNext(), creating an infinite loop back to the timezone input screen. Clear _stepSubs inside the DynamicLayoutNode callback before creating new subscriptions to prevent this accumulation. * test(tui): correct sessions regression test + add identity-redo page coverage The PR's SessionsPageTests referenced a non-existent SessionsViewModel.ResumeSessionId (resume state lives on ChatNavigationState), which broke the build on every Test-* job. Rewrite it to observe the shared ChatNavigationState, register a stub /chat landing route so the Enter/N resume paths don't throw 'No route matches', use the real SessionId (strips the 'session-' prefix), and enqueue Ctrl+Q in the render-only cases so they stop burning the 10s cancellation timeout. The corrected tests pass against current dev: arrow keys reach the ViewModel, SelectedIndex tracks, and Enter resumes the highlighted session — the two-way selection binding #1363 preserved is intact, so the described regression does not reproduce in dev. Add IdentityRedoPageTests: first page-level coverage of the identity-redo flow through Termina's real render + input pipeline, asserting the timezone submit reaches the saved screen (the user-visible symptom of the redo loop). * test(smoke): add native redo-identity flow tape Adds init-redo-identity.tape (+ sibling assertion) to the light smoke set: the existing-install menu had a redo path with no native coverage. The tape walks the menu -> 'Redo identity setup' -> all four identity sub-steps to the saved screen, and the assertion proves the redo wrote SOUL.md/TOOLING.md without clobbering netclaw.json (redo never calls WriteConfig). This is forward-progress coverage: a regression that blocks advancing past any redo sub-step (the symptom of the timezone loop) hangs the Wait+Screen anchor and fails the tape. It deliberately does NOT claim to reproduce the original loop, whose trigger is a non-step-transition re-render (cursor-blink/resize) that deterministic VHS input does not generate — verified by reverting the fix and watching the tape still pass. Runs sequentially within the already-parallel smoke-linux/smoke-macos jobs; ~40s added on the critical path (~3-4% of the ~20m smoke job).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
netclaw mcp permissionstool grid was not scrollable when a server had many tools; same gap existed on five other TUI screensSelectionList-based pages (ApprovalsManagerPage,ModelManagerPage,ProviderManagerPage,ProviderStepView) now call.WithFillHeight()— the native Termina 0.11 fill mode, removing the need for the customFillSelectionListNode<T>workaroundMcpToolPermissionsPagewraps its tool rows in aScrollableContainerNodewith a dedicatedDynamicLayoutNode; cursor navigation invalidates only the tool rows node so the scroll offset survives between keystrokes;EnsureToolCursorVisible()keeps the cursor row in the viewportSessionsPagereplaces the rawTextNodefor-loop withSelectionListNode.WithFillHeight()+.WithHighlightedIndex(), delegating scroll to TerminaProviderStepViewdrops the 30-item model truncation and"... and N more"sentinel — no longer needed now that the list scrolls nativelySmoke coverage
Adds four VHS tapes for TUI surfaces that previously had no interactive smoke coverage:
mcp-permissions.tape— opensnetclaw mcp permissions, anchors on header, exits cleanlyapprovals.tape— opensnetclaw approvals, anchors on panel title, exits cleanlymodel-manager.tape— opensnetclaw model, anchors on panel title, exits cleanlysessions-tui.tape— opensnetclaw sessions, anchors on panel title, exits cleanlyAll four run without a live daemon (exercise empty/loading-state rendering). All four added to
LIGHT_TAPES.Test plan
dotnet buildpasses (0 errors)dotnet slopwatch analyzepasses (0 new violations)./scripts/smoke/run-smoke.sh mcp-permissions— new tape./scripts/smoke/run-smoke.sh approvals— new tape./scripts/smoke/run-smoke.sh model-manager— new tape./scripts/smoke/run-smoke.sh sessions-tui— new tape./scripts/smoke/run-smoke.sh light— full light suite