Skip to content

fix(tui): make all TUI list views scrollable (#1351)#1363

Merged
Aaronontheweb merged 8 commits into
netclaw-dev:devfrom
Aaronontheweb:claude-wt-mcp-permissions-scrollable
Jun 9, 2026
Merged

fix(tui): make all TUI list views scrollable (#1351)#1363
Aaronontheweb merged 8 commits into
netclaw-dev:devfrom
Aaronontheweb:claude-wt-mcp-permissions-scrollable

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes MCP permissions TUI is not scrollable when there are many tools #1351netclaw mcp permissions tool grid was not scrollable when a server had many tools; same gap existed on five other TUI screens
  • All SelectionList-based pages (ApprovalsManagerPage, ModelManagerPage, ProviderManagerPage, ProviderStepView) now call .WithFillHeight() — the native Termina 0.11 fill mode, removing the need for the custom FillSelectionListNode<T> workaround
  • McpToolPermissionsPage wraps its tool rows in a ScrollableContainerNode with a dedicated DynamicLayoutNode; cursor navigation invalidates only the tool rows node so the scroll offset survives between keystrokes; EnsureToolCursorVisible() keeps the cursor row in the viewport
  • SessionsPage replaces the raw TextNode for-loop with SelectionListNode.WithFillHeight() + .WithHighlightedIndex(), delegating scroll to Termina
  • ProviderStepView drops the 30-item model truncation and "... and N more" sentinel — no longer needed now that the list scrolls natively

Smoke coverage

Adds four VHS tapes for TUI surfaces that previously had no interactive smoke coverage:

  • mcp-permissions.tape — opens netclaw mcp permissions, anchors on header, exits cleanly
  • approvals.tape — opens netclaw approvals, anchors on panel title, exits cleanly
  • model-manager.tape — opens netclaw model, anchors on panel title, exits cleanly
  • sessions-tui.tape — opens netclaw sessions, anchors on panel title, exits cleanly

All four run without a live daemon (exercise empty/loading-state rendering). All four added to LIGHT_TAPES.

Test plan

  • dotnet build passes (0 errors)
  • dotnet slopwatch analyze passes (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

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
Aaronontheweb enabled auto-merge (squash) June 9, 2026 01:39
@Aaronontheweb Aaronontheweb added the tui Terminal UI (Termina) issues label Jun 9, 2026
@Aaronontheweb
Aaronontheweb disabled auto-merge June 9, 2026 01:39

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

private DynamicLayoutNode? _contentNode;
private DynamicLayoutNode? _footerNode;
private DynamicLayoutNode? _toolRowsNode;
private ScrollableContainerNode? _toolScrollNode;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) June 9, 2026 13:45
@Aaronontheweb
Aaronontheweb merged commit 7ddd3df into netclaw-dev:dev Jun 9, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the claude-wt-mcp-permissions-scrollable branch June 9, 2026 23:26
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tui Terminal UI (Termina) issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP permissions TUI is not scrollable when there are many tools

1 participant