Skip to content

Bug: Slack/Discord bot token sub-step loops back instead of advancing #792

Description

@Aaronontheweb

Description

When configuring Slack or Discord through the channel picker sub-flow, entering a valid bot token and pressing Enter loops back to the same page instead of advancing to the next sub-step (app token for Slack, channel IDs for Discord).

Reproduction

  1. netclaw init
  2. Navigate forward to the Channel Picker step
  3. Select Slack (or Discord), press Enter to configure
  4. Enter a valid bot token (xoxb-... for Slack) and press Enter
  5. Expected: Advances to the next sub-step (App Token for Slack)
  6. Actual: Stays on the bot token page indefinitely

Bisection

Container Commit Works?
43cc4a36 (unified channel picker) 43cc4a36 Yes
66e232b2 (dev HEAD, v0.16.1) 66e232b2 No
bc896bf5 (wizard cleanup branch) bc896bf5 No

Introduced by: 95143415feat(tui): make AllowedUserIds an explicit allow/restrict choice (#760) (#778)

Root Cause

Subscription accumulation + cursor blink timer race in _stepSubs.

ChannelPickerStepView.ManagesOwnFocusState returns true, which prevents _stepSubs.Clear() from running in the DynamicLayoutNode factory between sub-step transitions. Each call to SlackStepView.BuildContent() adds new Submitted subscriptions to _stepSubs via .DisposeWith(callbacks.Subscriptions), but old subscriptions from previous sub-step renderings are never disposed.

TextInputBaseNode has a cursor blink timer that fires _invalidated.OnNext() on a real timer thread. If that timer fires between ProcessEvent completing and RenderCurrentPage starting, the old TextInputNode triggers a cascading invalidation that re-evaluates the DynamicLayoutNode factory. This creates duplicate subscriptions. Multiple AdvanceStep() calls fire per Enter, causing the wizard to jump erratically (appearing as a "loop").

Why headless tests pass: VirtualTerminal doesn't run real cursor blink timers — all events process synchronously with no real time passing. The interleaving condition never occurs.

Why #778 triggered it: Adding BuildUserAccessChoiceSubStep increased the subscription surface and likely changed timing enough for the blink timer race to manifest consistently.

Fix Direction

Clear adapter subscriptions in ChannelPickerStepView.BuildContent before delegating to the adapter view:

public ILayoutNode BuildContent(IWizardStepViewModel stepVm, StepViewCallbacks callbacks)
{
    _vm = (ChannelPickerStepViewModel)stepVm;
    _callbacks = callbacks;

    if (_vm.IsInSubFlow && _vm.ActiveAdapterVm is not null && _vm.ActiveAdapterView is not null)
    {
        callbacks.Subscriptions.Clear();  // <-- prevent subscription accumulation
        _vm.ActiveAdapterView.ClearFocusState();
        return _vm.ActiveAdapterView.BuildContent(_vm.ActiveAdapterVm, callbacks);
    }

    return BuildPickerChecklist();
}

This preserves picker cursor state (not in _stepSubs) while preventing subscription accumulation during sub-step transitions. The adapter view's focus state is also cleared, preventing stale TextInputNode instances from intercepting keys.

Environment

  • Netclaw v0.16.1 (dev branch)
  • Termina 0.8.0
  • .NET 10
  • Ubuntu 24.04 (Docker + bare metal)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions