Skip to content

Bug: /rewind via picker crashes TUI (orphaned widget mount after _remount_current_session) #421

@Dz-hy

Description

@Dz-hy

Bug Description

Using /rewind without arguments (which shows a picker to select the number of turns) causes the TUI to flash errors and immediately exit. Using /rewind <n> with a numeric argument works fine.

Reproduction Steps

  1. Start tuiapp_v2.py with a conversation that has multiple turns
  2. Type /rewind (no argument) to open the turn-selection picker
  3. Select any number of turns from the picker (e.g. "回退 1 轮 · ...")
  4. TUI crashes with a flash of errors and exits

Root Cause

The crash occurs in _collapse_choice() (line ~2608) after the on_select callback returns.

The call chain:

  1. User selects a rewind option → _collapse_choice(msg, idx) is called
  2. Line 2628: msg.on_select(value) calls _do_rewind(n) (inside a try/except)
  3. Inside _do_rewind:
    • Line 2815: sess.messages = sess.messages[:real_user[-n]] — truncates messages, removing the choice message itself from sess.messages
    • Line 2818: self._remount_current_session()container.remove_children()removes ALL widgets from the DOM
  4. Back in _collapse_choice (lines 2631-2644, outside the try/except):
    anchor = msg._hint_widget or msg._body_widget          # points to a widget removed from DOM
    container.mount(new_widget, after=anchor)                # 💥 CRASH: anchor is no longer a child of container

The try/except at lines 2627-2630 only wraps the on_select call itself. The subsequent widget manipulation at lines 2631-2644 is unprotected and raises an unhandled exception, crashing the TUI.

Why /rewind <n> works but /rewind doesn't

  • /rewind <n> (line 2769): calls self._system(self._do_rewind(n)) directly — no widget replacement happens after
  • /rewind (no arg, line 2780-2785): creates a kind="choice" message with on_select=lambda v: self._do_rewind(v) — after _do_rewind returns, _collapse_choice tries to replace widgets that were already destroyed by _remount_current_session()

Suggested Fix

In _collapse_choice, after calling on_select, check if msg was removed from sess.messages. If so, skip the widget replacement since _do_rewind already handled the UI update:

# After line 2630, before line 2631, add:
sess = self.sessions.get(self.current_id)
if sess and msg not in sess.messages:
    # on_select callback removed this message (e.g. rewind) — skip widget replacement
    return

Or alternatively, wrap lines 2631-2651 in a try/except to prevent the crash.

Environment

  • OS: Windows
  • Python: 3.x
  • Textual: latest
  • File: frontends/tuiapp_v2.py, _collapse_choice() ~line 2608, _do_rewind() ~line 2803

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions