Skip to content

Commit be35a74

Browse files
committed
fix(tui): preserve simultaneous right-side toast and address review findings
- Carry left and right toasts separately in FooterViewModel so a right-side toast is no longer dropped when a left toast is active (regression from the footer unification). - Log clipboard read failures at debug before returning None. - Drop the unused is_clipboard_available re-export from prompt.py. - Split side-effecting append() calls out of assert statements in history tests.
1 parent 14534e4 commit be35a74

5 files changed

Lines changed: 60 additions & 17 deletions

File tree

src/pythinker_code/ui/shell/prompt.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@
9696
from pythinker_code.ui.shell.prompting.clipboard import (
9797
grab_media_from_clipboard as grab_media_from_clipboard,
9898
)
99-
from pythinker_code.ui.shell.prompting.clipboard import (
100-
is_clipboard_available as is_clipboard_available,
101-
)
10299
from pythinker_code.ui.shell.prompting.clipboard import (
103100
is_media_clipboard_available as is_media_clipboard_available,
104101
)
@@ -3960,8 +3957,6 @@ def _build_footer_view_model(self, columns: int) -> FooterViewModel:
39603957
):
39613958
command_line = runner.current_line
39623959

3963-
left_toast = self._prompt_toast("left")
3964-
toast_snapshot = left_toast if left_toast is not None else self._prompt_toast("right")
39653960
update_provider = cast(
39663961
Callable[[], str | None] | None,
39673962
getattr(self, "_update_notice_provider", None),
@@ -3978,8 +3973,9 @@ def _build_footer_view_model(self, columns: int) -> FooterViewModel:
39783973
bash=background_counts.bash,
39793974
agent=background_counts.agent,
39803975
),
3981-
toast=toast_snapshot,
3976+
toast=self._prompt_toast("left"),
39823977
update_notice=update_provider() if callable(update_provider) else None,
3978+
toast_right=self._prompt_toast("right"),
39833979
)
39843980

39853981
def _update_notice_for_render(self) -> str | None:
@@ -4091,14 +4087,14 @@ def _get_one_rotating_tip(self) -> str | None:
40914087
return self._tips[self._tip_rotation_index % len(self._tips)]
40924088

40934089
def _render_right_span(self, footer: FooterViewModel) -> str:
4094-
if footer.toast is None or footer.toast.position != "right":
4090+
if footer.toast_right is None:
40954091
status = footer.status
40964092
return format_context_status(
40974093
status.context_usage,
40984094
status.context_tokens,
40994095
status.max_context_tokens,
41004096
)
4101-
return footer.toast.message
4097+
return footer.toast_right.message
41024098

41034099

41044100
# Compatibility surface kept for tests that still import the legacy footer

src/pythinker_code/ui/shell/prompting/clipboard.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pythinker_code.utils.clipboard import (
2121
is_media_clipboard_available as utility_is_media_clipboard_available,
2222
)
23+
from pythinker_code.utils.logging import logger
2324

2425

2526
class ClipboardAdapter:
@@ -54,7 +55,8 @@ def paste_text(self, clipboard: Clipboard) -> str | None:
5455
"""Return pasted text, preserving the prompt's silent failure behavior."""
5556
try:
5657
data = clipboard.get_data()
57-
except Exception:
58+
except Exception as exc:
59+
logger.debug("Clipboard text read failed: error={!r}", exc)
5860
return None
5961
return data.text
6062

src/pythinker_code/ui/shell/prompting/footer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ class FooterViewModel:
2323
background_summary: str
2424
toast: ToastSnapshot | None
2525
update_notice: str | None
26+
# Left- and right-positioned toasts can be active at once; the right toast is
27+
# rendered by the right span, so it must be carried separately from ``toast``
28+
# (which feeds left-side content precedence) or it would be dropped.
29+
toast_right: ToastSnapshot | None = None
2630

2731

2832
@dataclass(frozen=True, slots=True)

tests/ui_and_conv/test_footer_view_model.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,39 @@ def test_legacy_and_card_adapters_share_left_policy_and_width_safety(
171171
assert "toast-only" not in _text(card)
172172

173173

174+
def test_left_and_right_toasts_both_render(monkeypatch: pytest.MonkeyPatch) -> None:
175+
"""Regression: a left toast must not drop a simultaneously active right toast."""
176+
monkeypatch.setenv("NO_COLOR", "1")
177+
session = _session()
178+
left = ToastSnapshot(
179+
message="left-toast",
180+
position="left",
181+
style="bold",
182+
topic=None,
183+
expires_at=float("inf"),
184+
)
185+
right = ToastSnapshot(
186+
message="right-toast",
187+
position="right",
188+
style="bold",
189+
topic=None,
190+
expires_at=float("inf"),
191+
)
192+
model = FooterViewModel(
193+
status=_status(120, ascii_only=True),
194+
command_line="",
195+
extension_statuses=(),
196+
background_summary="",
197+
toast=left,
198+
update_notice=None,
199+
toast_right=right,
200+
)
201+
202+
rendered = _text(session._render_legacy_bottom_toolbar(model))
203+
assert "left-toast" in rendered
204+
assert "right-toast" in rendered
205+
206+
174207
def test_footer_view_model_is_immutable_and_width_specific() -> None:
175208
model = _model(80, command="cached")
176209
narrower = replace(model, status=replace(model.status, columns=40))

tests/ui_and_conv/test_prompt_history.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,31 +148,38 @@ def test_append_history_entry_restricts_file_permissions(tmp_path) -> None:
148148
def test_prompt_history_store_loads_only_configured_tail(tmp_path) -> None:
149149
store = PromptHistoryStore(tmp_path / "history.jsonl", max_entries=2)
150150

151-
assert store.append("one")
152-
assert store.append("two")
153-
assert store.append("three")
151+
appended_one = store.append("one")
152+
appended_two = store.append("two")
153+
appended_three = store.append("three")
154+
assert appended_one
155+
assert appended_two
156+
assert appended_three
154157

155158
assert [entry.content for entry in store.load()] == ["two", "three"]
156159

157160

158161
def test_prompt_history_store_excludes_credential_commands(tmp_path) -> None:
159162
store = PromptHistoryStore(tmp_path / "history.jsonl")
160163

161-
assert store.append("/login api-key") is False
162-
assert store.append("logout openai") is False
164+
login_appended = store.append("/login api-key")
165+
logout_appended = store.append("logout openai")
166+
assert login_appended is False
167+
assert logout_appended is False
163168
assert not store.path.exists()
164169

165170

166171
def test_prompt_history_store_skips_oversized_records(tmp_path) -> None:
167172
store = PromptHistoryStore(tmp_path / "history.jsonl")
168173

169-
assert store.append("x" * (256 * 1024)) is False
174+
oversized_appended = store.append("x" * (256 * 1024))
175+
assert oversized_appended is False
170176
assert not store.path.exists()
171177

172178

173179
def test_prompt_history_store_clear_confirms_both_files_are_removed(tmp_path) -> None:
174180
store = PromptHistoryStore(tmp_path / "history.jsonl")
175-
assert store.append("kept")
181+
kept_appended = store.append("kept")
182+
assert kept_appended
176183
encoding = "utf-8"
177184
store.rotated_path.write_text('{"content":"older"}\n', encoding=encoding)
178185

@@ -196,7 +203,8 @@ def test_prompt_history_clear_reports_success_only_after_confirmed_removal(
196203
monkeypatch,
197204
) -> None:
198205
store = PromptHistoryStore(tmp_path / "history.jsonl")
199-
assert store.append("kept")
206+
kept_appended = store.append("kept")
207+
assert kept_appended
200208
prompt_session = object.__new__(shell_prompt.CustomPromptSession)
201209
cast(Any, prompt_session)._history_store = store
202210
app = SimpleNamespace(_prompt_session=prompt_session)

0 commit comments

Comments
 (0)