7568a6bc1f
* fix(tui): keep welcome banner at top after /new PR #262 replaced scroll_end() with anchor() for free-scrolling. When /new clears a long anchored conversation, the anchor kept the viewport pinned to the (now empty) bottom, producing a negative scroll_y and pushing the welcome banner out of view. Reset the anchor and scroll to the top in clear_chat(), and restore the follow/new-content flags so the fresh session starts correctly. Closes #301 * fix(tui): suppress anchor when chat content fits viewport The previous fix for #301 only handled the /new path. din0s reported that the banner still dropped to the bottom after a normal short turn (user types 'hi', agent replies) — i.e. whenever the conversation fit in the viewport. Root cause is in Textual's compositor (textual._compositor): when a widget is anchored, scroll_y is recomputed via set_reactive, which bypasses the validator. If the anchored widget's content is shorter than the viewport, scroll_y goes negative on the next layout pass and the welcome banner is pushed below the visible region. PR #262 made _stream_with_widgets re-engage the anchor at the end of every turn via _anchor_chat, so the bug surfaced on any short reply that fit in the viewport. Markdown re-renders, status-bar updates, or any subsequent mount would then trip the compositor. Fix in three places: * _anchor_chat: only engage the anchor when max_scroll_y > 0; otherwise release and scroll_home so the banner stays at the top. * streaming anchor loop: if content shrinks below the viewport mid-stream (e.g. loading widget removed), release the anchor instead of leaving _anchored=True for the compositor to trip on. * clear_chat: keep the unconditional reset (children are removed asynchronously so a max_scroll_y check would be stale) but document why. Adds two regressions: * test_short_turn_keeps_banner_at_top_after_layout_refresh — the exact scenario din0s tested; fails with scroll_y=-10 on the previous code, passes with the fix. * test_long_turn_keeps_viewport_pinned_to_bottom — guards against regressing free-scrolling for overflowing conversations. Manually verified: 'hi' -> reply (banner stays at top) -> /new (banner at top) -> another turn (banner stays at top). * test(tui): address review feedback on banner-position regressions - extract `_release_anchor_and_pin_top` helper for the 3-line `anchor(False) + scroll_home(...)` pattern repeated in `clear_chat`, `_anchor_chat`, and the streaming loop - replace `pytest.skip` in `_capture_app` with a hard `RuntimeError` so a broken capture never silently passes - drop the redundant `load_agent` and `create_session_workspace` monkeypatches (the factory is given those as parameters, so the module-level symbols never run; added a comment explaining why) - add a defensive `_FakeChannelRuntime` patch for symmetry with the other module-level fakes - drop the local `_run` helper and use the `run_async` fixture from `conftest.py` (its teardown is better) * test(tui): replace _FakeChannelRuntime with _auto_start_channel no-op The _FakeChannelRuntime patch was ineffective because ChannelRuntime is just a dataclass — the real channel manager still started via _auto_start_channel, leaving pending tasks and non-hermetic test state. Per review feedback, stub _auto_start_channel directly instead.