Skip to content

Commit 7078692

Browse files
committed
fix(tui): refresh shell-mode update-notice frame snapshot
The per-frame _prompt_frame_update_notice snapshot was refreshed only by the agent render path and never cleared with the frame, so shell frames suppressed a live update notice (initial None) or replayed a stale agent-mode notice after a mode switch. Refresh the snapshot in the shell render like the agent path and reset it in _clear_prompt_frame.
1 parent d15d807 commit 7078692

3 files changed

Lines changed: 48 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ GitHub Releases page; `0.8.0` is the new starting line.
2525
- Fix queued follow-up input showing a bordered ghost; pressing Enter during an active turn now shows one intentional queued row.
2626
- Harden provider OAuth credential handling: scope token refresh to the active/selected provider, serialize persistence under a lock with atomic config replacement, roll back replaced credentials when a save fails, fail closed on credential migration, and validate OAuth token and implicit-state responses.
2727
- Fix an intermittent doubled/"ghost" copy of the running-prompt block (agent tree, spinner, and tip) on long streaming turns: after a scrollback handoff the suppressed live body now waits for the terminal's re-requested absolute cursor position to settle before it re-expands, so it repaints against a correct cursor model instead of the mis-anchored frame left behind by `run_in_terminal`.
28+
- Fix the "update available" footer notice being suppressed on shell-mode frames (and a stale agent-mode notice replaying after a mode switch): the shell prompt render now refreshes the per-frame update-notice snapshot like the agent path, and the snapshot is cleared with the frame.
2829

2930
## 0.60.0 (2026-07-18)
3031

src/pythinker_code/ui/shell/prompt.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2844,6 +2844,9 @@ def _capture_prompt_frame(app: Application[str]) -> None:
28442844

28452845
def _clear_prompt_frame(_app: Application[str]) -> None:
28462846
self._current_prompt_frame = None
2847+
# Drop the per-frame notice snapshot with the frame so a later render
2848+
# can never read a value captured for a stale frame/mode.
2849+
self._prompt_frame_update_notice = None
28472850

28482851
self._session.app.before_render.add_handler(_capture_prompt_frame)
28492852
self._session.app.after_render.add_handler(_clear_prompt_frame)
@@ -3132,6 +3135,15 @@ def _render_shell_prompt_message(self) -> FormattedText:
31323135
# (not just on the agent path) so a mode switch or resize cannot leave
31333136
# _fit_toolbar_to_terminal clipping against a stale agent-mode value.
31343137
self._prompt_footer_row_budget = frame.terminal_rows
3138+
# Snapshot the update notice for this frame too. The agent path caches it
3139+
# in _render_agent_prompt_message; without the same refresh here,
3140+
# _append_update_notice would replay a stale agent-mode notice (or the
3141+
# initial None, suppressing a live notice) once a frame is captured.
3142+
provider = cast(
3143+
Callable[[], str | None] | None,
3144+
getattr(self, "_update_notice_provider", None),
3145+
)
3146+
self._prompt_frame_update_notice = provider() if callable(provider) else None
31353147
fragments: FormattedText = FormattedText()
31363148

31373149
if getattr(self, "_shortcut_help_open", False):

tests/ui_and_conv/test_prompt_height_budget.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,41 @@ def test_one_row_scene_keeps_modal_then_input_before_footer(
145145
assert prompt._prompt_footer_row_budget == 0
146146

147147

148+
def test_shell_render_refreshes_update_notice_snapshot(
149+
monkeypatch: pytest.MonkeyPatch,
150+
) -> None:
151+
# _append_update_notice trusts the per-frame snapshot _prompt_frame_update_notice
152+
# whenever a frame is captured. Only the agent render path refreshed it, so a
153+
# shell frame would replay a stale agent-mode notice — or the initial None,
154+
# suppressing a live notice. The shell render must refresh the snapshot too.
155+
session = _session_for_scene(
156+
"body", width=80, height=10, card_style=False, monkeypatch=monkeypatch
157+
)
158+
session._mode = PromptMode.SHELL
159+
160+
# A live notice must overwrite a stale agent-mode value, and — because the
161+
# captured frame makes _append_update_notice read the snapshot rather than the
162+
# provider — actually reach the rendered footer.
163+
session._prompt_frame_update_notice = "STALE agent-mode notice"
164+
session._update_notice_provider = lambda: "↑ Update available"
165+
session._render_shell_prompt_message()
166+
assert session._prompt_frame_update_notice == "↑ Update available"
167+
fragments: list[tuple[str, str]] = []
168+
session._append_update_notice(fragments, 80)
169+
assert any("Update available" in text for _, text in fragments)
170+
assert not any("STALE" in text for _, text in fragments)
171+
172+
# No pending notice must clear the snapshot, never leave it stale — and the
173+
# footer stays empty instead of replaying the old text.
174+
session._prompt_frame_update_notice = "STALE agent-mode notice"
175+
session._update_notice_provider = lambda: None
176+
session._render_shell_prompt_message()
177+
assert session._prompt_frame_update_notice is None
178+
fragments = []
179+
session._append_update_notice(fragments, 80)
180+
assert fragments == []
181+
182+
148183
def test_two_row_modal_uses_hint_then_tail(monkeypatch: pytest.MonkeyPatch) -> None:
149184
session = _session_for_scene(
150185
"modal",

0 commit comments

Comments
 (0)