feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down

This commit is contained in:
2026-08-29 17:27:04 -04:00
parent 6bf7f456d4
commit 1a60ecbd8b
186 changed files with 1738 additions and 7796 deletions
+14 -4
View File
@@ -165,9 +165,16 @@ def brain_bubble_longer_than(n: int, min_bubbles: int = 1) -> str:
def wait_settled(page: Page) -> None:
"""The turn is over: the never-stale contract re-enabled the button."""
"""The turn is over: the label is back to "Send" (phase 48: the
button is never disabled — the in-flight state is the enabled Stop
control, so the label is the settle marker). Phase 48 note: the old
contract let ``to_be_enabled`` block until the turn settled (the
in-flight button was disabled); now the label assertion carries the
wait — with an explicit timeout, since Playwright expect's default
(5s) is shorter than the mock's long turn (~9.5s) and does not
inherit the page default."""
expect(page.locator("#send-btn")).to_be_enabled(timeout=30_000)
expect(page.locator("#send-label")).to_have_text("Send")
expect(page.locator("#send-label")).to_have_text("Send", timeout=30_000)
def wait_scroll_still(page: Page, timeout: float = 10.0) -> float:
@@ -259,15 +266,18 @@ def test_no_autoscroll_during_long_answer(
assert abs(scroll_state(page)["y"] - target) <= STABLE_PX
# Sample the viewport across the rest of the stream ...
# Phase 48: in-flight detection rides the Stop label (the button is
# the enabled Stop control while a turn runs — it is no longer
# disabled, so the disabled-state proxy is gone).
samples: list[float] = []
mid_stream = 0
deadline = time.monotonic() + 40
while time.monotonic() < deadline:
time.sleep(0.25)
samples.append(scroll_state(page)["y"])
if not page.locator("#send-btn").is_enabled():
if page.locator("#send-label").inner_text() == "Stop":
mid_stream += 1
if len(samples) >= 10 and page.locator("#send-btn").is_enabled():
if len(samples) >= 10 and page.locator("#send-label").inner_text() == "Send":
# ... and a few more AFTER `done` (the turn is over; nothing
# queued behind the stream may move the page either).
for _ in range(3):