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
- Start
tuiapp_v2.py with a conversation that has multiple turns
- Type
/rewind (no argument) to open the turn-selection picker
- Select any number of turns from the picker (e.g. "回退 1 轮 · ...")
- 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:
- User selects a rewind option →
_collapse_choice(msg, idx) is called
- Line 2628:
msg.on_select(value) calls _do_rewind(n) (inside a try/except)
- 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
- 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
Bug Description
Using
/rewindwithout 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
tuiapp_v2.pywith a conversation that has multiple turns/rewind(no argument) to open the turn-selection pickerRoot Cause
The crash occurs in
_collapse_choice()(line ~2608) after theon_selectcallback returns.The call chain:
_collapse_choice(msg, idx)is calledmsg.on_select(value)calls_do_rewind(n)(inside atry/except)_do_rewind:sess.messages = sess.messages[:real_user[-n]]— truncates messages, removing the choice message itself fromsess.messagesself._remount_current_session()→container.remove_children()— removes ALL widgets from the DOM_collapse_choice(lines 2631-2644, outside the try/except):The
try/exceptat lines 2627-2630 only wraps theon_selectcall itself. The subsequent widget manipulation at lines 2631-2644 is unprotected and raises an unhandled exception, crashing the TUI.Why
/rewind <n>works but/rewinddoesn't/rewind <n>(line 2769): callsself._system(self._do_rewind(n))directly — no widget replacement happens after/rewind(no arg, line 2780-2785): creates akind="choice"message withon_select=lambda v: self._do_rewind(v)— after_do_rewindreturns,_collapse_choicetries to replace widgets that were already destroyed by_remount_current_session()Suggested Fix
In
_collapse_choice, after callingon_select, check ifmsgwas removed fromsess.messages. If so, skip the widget replacement since_do_rewindalready handled the UI update:Or alternatively, wrap lines 2631-2651 in a
try/exceptto prevent the crash.Environment
frontends/tuiapp_v2.py,_collapse_choice()~line 2608,_do_rewind()~line 2803