Skip to content

fix(tui): redo timezone loop + restore session browser selection#1518

Merged
Aaronontheweb merged 5 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/identity-redo-timezone-loop
Jun 29, 2026
Merged

fix(tui): redo timezone loop + restore session browser selection#1518
Aaronontheweb merged 5 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/identity-redo-timezone-loop

Conversation

@Aaronontheweb

@Aaronontheweb Aaronontheweb commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two TUI bug fixes, rebased onto current dev. Supersedes #1515, whose branch was
based on stale dev, bundled in the already-merged GitHub Enterprise Copilot auth feature
(#1509), and shipped a SessionsPageTests that referenced a non-existent
SessionsViewModel.ResumeSessionId (the compile error that failed every Test-* job).

1. Identity redo timezone loop

IdentityStepView is a singleton on IdentityRedoViewModel. Every content rebuild —
including Termina's cursor-blink-timer re-renders (the class of bug behind #792) — creates
fresh TextInputNodes and adds new Submitted subscriptions to _stepSubs, which were
only cleared on step transitions, not on rebuilds. Stale callbacks accumulated and re-fired
on the timezone submit, bouncing the user back to the timezone field.

Fix: clear _stepSubs at the top of the content DynamicLayoutNode rebuild — the
documented StepViewCallbacks.Subscriptions contract ("Cleared when the step content is
rebuilt") that the sibling InitWizardPage already honours.

2. Session browser selection — netclaw sessions

Real regression, root-caused from the Termina source. #1363 ("make all TUI list views
scrollable") rendered the session list as a focusable SelectionListNode (FocusPriority
10). On a populated list, ReactivePage.ApplyFocusPolicy hands it keyboard focus, and
Termina's input dispatch routes keys to the focus manager before the ViewModel —
SelectionListNode.HandleInput consumes UpArrow/DownArrow/Enter/Escape (returns
true). 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 couldn't be driven, SelectedIndex stayed 0, and Enter resumed
nothing — "you can't select anything."

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 is exactly
what HandlePageInput is documented for ("page-level input before focused components").

Note: an earlier revision of this PR wrongly concluded this bug "did not reproduce on
dev" — that was a false positive. The headless xUnit harness never hands the list focus
during input processing, so keys fell through to the ViewModel and passed. The empty-state
sessions-tui tape renders a TextNode, not a focusable list, so it never hit the path
either. Both blind spots are called out in the new test/header comments.

Tests

  • SessionsPageTests (rewritten): observes the shared ChatNavigationState, registers
    a stub /chat route, uses the real SessionId. Now drives input through the real
    HandlePageInput path — removing that override fails them — so they guard the page→VM
    wiring (the previous tests were false positives). They do not reproduce the focus-eating
    itself; that needs native/manual coverage (documented in the header).
  • IdentityRedoPageTests (new): first page-level coverage of the redo flow to the saved
    screen.
  • init-redo-identity native smoke tape + assertion: forward-progress coverage for the
    redo flow (menu → all four sub-steps → saved; identity files written, config preserved).

Notes

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.
…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).
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).

@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

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) June 29, 2026 19:05
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 Aaronontheweb changed the title fix(tui): prevent identity redo timezone loop + correct session browser regression test fix(tui): redo timezone loop + restore session browser selection Jun 29, 2026
@Aaronontheweb
Aaronontheweb force-pushed the fix/identity-redo-timezone-loop branch from e471da9 to 65365af Compare June 29, 2026 19:48
@Aaronontheweb
Aaronontheweb merged commit 54c19ce into netclaw-dev:dev Jun 29, 2026
36 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant