Skip to content

fix(tui): prevent crash when /rewind picker triggers session remount#466

Open
Kailigithub wants to merge 1 commit into
lsdefine:mainfrom
Kailigithub:fix/issue-421-rewind-picker-crash
Open

fix(tui): prevent crash when /rewind picker triggers session remount#466
Kailigithub wants to merge 1 commit into
lsdefine:mainfrom
Kailigithub:fix/issue-421-rewind-picker-crash

Conversation

@Kailigithub
Copy link
Copy Markdown
Contributor

When /rewind is invoked via the picker in the TUI, the callback
(_do_rewind) truncates session messages and calls
_remount_current_session(), which removes all widgets from the
#messages container. Back in _collapse_choice(), the anchor widget
reference (msg._hint_widget / msg._body_widget) still exists but is
now detached, causing a crash when trying to container.mount(after=anchor).

Fix: Add a guard after the on_select callback that checks
anchor.is_mounted. If the callback already rebuilt the UI (anchor is
detached), return early to avoid operating on detached widgets.

For normal callbacks that don't remount, the anchor widgets remain
mounted and the guard is a no-op — behavior is unchanged.

Closes #421

When /rewind is invoked via the picker, the callback (_do_rewind)
truncates session messages and calls _remount_current_session(), which
removes all widgets from the container. Back in _collapse_choice(), the
anchor widget reference still exists but is now detached, causing a
crash on mount(after=anchor).

Add a guard that checks is_mounted on the anchor widget after the
on_select callback. If the callback already rebuilt the UI, return
early to avoid operating on detached widgets.

Closes lsdefine#421
lsdefine pushed a commit that referenced this pull request May 28, 2026
v2 Ctrl+S freeze: defer reset() + cleanup via call_after_refresh so the
keystroke handler returns immediately even when streaming has the
reactive queue saturated. Added _skip_change_next flag to short-circuit
on_input_area_changed during the deferred clear.

v3 terminal title: route _set_term_title via os.write(1,...) — sys.stdout
gets redirected to sb_agent.log during PTK app run, so OSC 0 escapes
were silently ending up in the log file. Default _session_name = 'session'
for v2 parity, and repaint title on running→idle transition.

v3 input row bg fill: _tile() now pads explicit bg-active spaces to
width — PTK's cell renderer doesn't honour \x1b[K (erase-to-EOL), so
the user bubble's charcoal band used to stop at text length.

v3 /emoji picker: arrow-key menu (mirrors /llm). Pet styles cleaned to
bear (default) + cat + dot + unicode. Bear calm tier restored to ʕ•ᴥ•ʔ.
• used for the calm tier, o for focused — matches user-requested mood
progression. Pet frame rate decoupled from spinner via self._spin // 5.

v3 ask_user "coderun freeze" fix: _tool_status no longer marks an
in-flight ask_user chip as ✓ ok when the stream contains the
"Waiting for your answer ..." marker. Bumped DoneEvent grace from 2 s
to 10 s when the marker is in the stream so a slow ask_user_queue.put
doesn't cause the picker to fall through to _finalize.

v3 /scheduler picker: added pre_checked kwarg to _show_menu so the
running set lands atomically. slash_cmds._match_service relaxed to
path-only matching so directly-launched (`python reflect/foo.py`)
reflect tasks are detected. Running rows highlighted in functional
green. Confirm card accepts ← as cancel (parity with Esc) — same on
v2 ChoiceList.

v3 keybindings: Esc Esc (within 800 ms) → /rewind (handled in
_handle_key so PTK's Esc interception path actually fires).

v3 pending input queue: agent.task_dir wired so ga.turn_end_callback
can consume `<task_dir>/_intervene`. New AgentBridge.inject_intervene
appends to the file (atomic vs read-modify-write), and _drain_pending
routes the combined message either through that hook (mid-turn) or via
put_task (idle). Cooldown 5 s, resets per new pending. ↑ recalls the
last queued message, Esc clears the queue. v2 has equivalent shape:
AgentSession.pending + per-session agent.task_dir + _inject_intervene
+ _poll_pending timer.

! shell magic: prefix `!` enters shell mode — pink border, prompt mark
swaps `❯` → `!`, body strips the leading `!` so the glyph appears
once. Enter runs the rest as `subprocess.run(shell=True, timeout=30)`,
echoes `! cmd` + `└ output` into scrollback, and appends to LLM
history (via _intervene when agent is running, direct backend.history
append when idle — sidesteps the iterator race).

/resume: forwarded as a literal to the agent so GA's _handle_slash_cmd
(agentmain.py:124) can expand it. Added to v3 _cmds() + v2 COMMANDS +
slash_cmds.COMMANDS + i18n + /help + Tips.

Turn marker regex fix: agent_loop.py:52 switches `**LLM Running
(Turn N) ...**` to the short `**Turn N ...**` when task_dir is set.
Extended all v3/v2 turn-marker regexes to match either form.

Cherry-picked upstream fixes:
- PR #466 (8ae3645): _collapse_choice early-return when the
  on_select callback (e.g. /rewind) detaches the anchor widget.
- PR #461 (08f21e8): SelectableStatic.has_valid_selection_parent +
  app-level _is_stale_selectable_mouse_event filter that swallows
  mouse events on detached widgets.
FeiNiaoBF added a commit to FeiNiaoBF/GenericAgent that referenced this pull request May 28, 2026
v2 Ctrl+S freeze: defer reset() + cleanup via call_after_refresh so the
keystroke handler returns immediately even when streaming has the
reactive queue saturated. Added _skip_change_next flag to short-circuit
on_input_area_changed during the deferred clear.

v3 terminal title: route _set_term_title via os.write(1,...) — sys.stdout
gets redirected to sb_agent.log during PTK app run, so OSC 0 escapes
were silently ending up in the log file. Default _session_name = 'session'
for v2 parity, and repaint title on running→idle transition.

v3 input row bg fill: _tile() now pads explicit bg-active spaces to
width — PTK's cell renderer doesn't honour \x1b[K (erase-to-EOL), so
the user bubble's charcoal band used to stop at text length.

v3 /emoji picker: arrow-key menu (mirrors /llm). Pet styles cleaned to
bear (default) + cat + dot + unicode. Bear calm tier restored to ʕ•ᴥ•ʔ.
• used for the calm tier, o for focused — matches user-requested mood
progression. Pet frame rate decoupled from spinner via self._spin // 5.

v3 ask_user "coderun freeze" fix: _tool_status no longer marks an
in-flight ask_user chip as ✓ ok when the stream contains the
"Waiting for your answer ..." marker. Bumped DoneEvent grace from 2 s
to 10 s when the marker is in the stream so a slow ask_user_queue.put
doesn't cause the picker to fall through to _finalize.

v3 /scheduler picker: added pre_checked kwarg to _show_menu so the
running set lands atomically. slash_cmds._match_service relaxed to
path-only matching so directly-launched (`python reflect/foo.py`)
reflect tasks are detected. Running rows highlighted in functional
green. Confirm card accepts ← as cancel (parity with Esc) — same on
v2 ChoiceList.

v3 keybindings: Esc Esc (within 800 ms) → /rewind (handled in
_handle_key so PTK's Esc interception path actually fires).

v3 pending input queue: agent.task_dir wired so ga.turn_end_callback
can consume `<task_dir>/_intervene`. New AgentBridge.inject_intervene
appends to the file (atomic vs read-modify-write), and _drain_pending
routes the combined message either through that hook (mid-turn) or via
put_task (idle). Cooldown 5 s, resets per new pending. ↑ recalls the
last queued message, Esc clears the queue. v2 has equivalent shape:
AgentSession.pending + per-session agent.task_dir + _inject_intervene
+ _poll_pending timer.

! shell magic: prefix `!` enters shell mode — pink border, prompt mark
swaps `❯` → `!`, body strips the leading `!` so the glyph appears
once. Enter runs the rest as `subprocess.run(shell=True, timeout=30)`,
echoes `! cmd` + `└ output` into scrollback, and appends to LLM
history (via _intervene when agent is running, direct backend.history
append when idle — sidesteps the iterator race).

/resume: forwarded as a literal to the agent so GA's _handle_slash_cmd
(agentmain.py:124) can expand it. Added to v3 _cmds() + v2 COMMANDS +
slash_cmds.COMMANDS + i18n + /help + Tips.

Turn marker regex fix: agent_loop.py:52 switches `**LLM Running
(Turn N) ...**` to the short `**Turn N ...**` when task_dir is set.
Extended all v3/v2 turn-marker regexes to match either form.

Cherry-picked upstream fixes:
- PR lsdefine#466 (8ae3645): _collapse_choice early-return when the
  on_select callback (e.g. /rewind) detaches the anchor widget.
- PR lsdefine#461 (08f21e8): SelectableStatic.has_valid_selection_parent +
  app-level _is_stale_selectable_mouse_event filter that swallows
  mouse events on detached widgets.

# Conflicts:
#	frontends/tui_v3.py
#	frontends/tuiapp_v2.py
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.

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

1 participant