|
|
@@ -977,12 +977,14 @@ def test_triage_enabled_composes_split(server):
|
|
|
transcript = app.query_one("#transcript", RichLog)
|
|
|
triage = app.query_one("#triage", RichLog)
|
|
|
chat = app.query_one("#chat", RichLog)
|
|
|
+ row = app.query_one("#chat-row", Horizontal)
|
|
|
assert triage.parent is main
|
|
|
- assert chat.parent is main
|
|
|
+ assert row.parent is main
|
|
|
+ assert chat.parent is row
|
|
|
assert transcript.parent is not main
|
|
|
assert triage.border_title == "triage \\[inf 0]" # markup: '[' is escaped
|
|
|
- # triage sits LEFT of chat inside #main.
|
|
|
- assert main.children.index(triage) < main.children.index(chat)
|
|
|
+ # triage sits LEFT of the chat row inside #main.
|
|
|
+ assert main.children.index(triage) < main.children.index(row)
|
|
|
# Outside widgets stay outside #main.
|
|
|
assert app.query_one("#status").parent is not main
|
|
|
assert transcript.parent is not main
|
|
|
@@ -1016,11 +1018,11 @@ def test_triage_enabled_layout_proportions(server):
|
|
|
main = app.query_one("#main", Horizontal)
|
|
|
transcript = app.query_one("#transcript", RichLog)
|
|
|
triage = app.query_one("#triage", RichLog)
|
|
|
- chat = app.query_one("#chat", RichLog)
|
|
|
+ chat_row = app.query_one("#chat-row", Horizontal)
|
|
|
return (
|
|
|
main.size.width,
|
|
|
triage.size.width,
|
|
|
- chat.size.width,
|
|
|
+ chat_row.size.width,
|
|
|
app.screen.size.height,
|
|
|
transcript.region.height,
|
|
|
)
|
|
|
@@ -1030,8 +1032,9 @@ def test_triage_enabled_layout_proportions(server):
|
|
|
f"measured: main_w={main_w} triage_w={triage_w} chat_w={chat_w} "
|
|
|
f"screen_h={screen_h} transcript_h={transcript_h}"
|
|
|
)
|
|
|
- # Content boxes exclude the 1-cell border on each side; allow ±2 px for
|
|
|
- # borders/rounding on the fractional split.
|
|
|
+ # The chat row (chat plus its capture controls) keeps the 2/3 share; the
|
|
|
+ # controls column is carved out of it. Content boxes exclude the 1-cell
|
|
|
+ # border on each side; allow ±2 px for borders/rounding.
|
|
|
assert abs(triage_w / main_w - 1 / 3) < 0.05, (main_w, triage_w, chat_w)
|
|
|
assert abs(chat_w / main_w - 2 / 3) < 0.05, (main_w, triage_w, chat_w)
|
|
|
# region.height includes the transcript's 2 border rows; at the small
|
|
|
@@ -1353,7 +1356,7 @@ def test_escape_while_maximized_restores_panes(server):
|
|
|
|
|
|
|
|
|
def test_maximize_chat_when_triage_disabled(server):
|
|
|
- """triage_enabled=False: focus #chat, press m maximizes #main without crashing."""
|
|
|
+ """triage_enabled=False: focus #chat, press m maximizes the chat row (with controls)."""
|
|
|
pytest.importorskip("textual")
|
|
|
from textual.widgets import RichLog
|
|
|
|
|
|
@@ -1379,7 +1382,7 @@ def test_maximize_chat_when_triage_disabled(server):
|
|
|
await pilot.press("m")
|
|
|
await pilot.pause()
|
|
|
observed["maximized"] = app.screen.maximized
|
|
|
- observed["chat"] = app.query_one("#chat", RichLog)
|
|
|
+ observed["chat_row"] = app.query_one("#chat-row")
|
|
|
observed["region"] = app.query_one("#chat", RichLog).region
|
|
|
observed["screen_size"] = app.screen.size
|
|
|
await pilot.press("m")
|
|
|
@@ -1389,7 +1392,7 @@ def test_maximize_chat_when_triage_disabled(server):
|
|
|
asyncio.run(scenario())
|
|
|
print("chat maximized (disabled): region=%s screen=%s"
|
|
|
% (observed["region"], observed["screen_size"]))
|
|
|
- assert observed["maximized"] is observed["chat"]
|
|
|
+ assert observed["maximized"] is observed["chat_row"]
|
|
|
assert observed["restored"] is None
|
|
|
|
|
|
|
|
|
@@ -1908,7 +1911,7 @@ def test_chat_mode_shows_chat_and_restores_split(server):
|
|
|
observed["chat_display"] = app.query_one("#chat", RichLog).display
|
|
|
observed["main_region"] = app.query_one("#main", Horizontal).region
|
|
|
observed["triage_region"] = app.query_one("#triage", RichLog).region
|
|
|
- observed["chat_region"] = app.query_one("#chat", RichLog).region
|
|
|
+ observed["chat_region"] = app.query_one("#chat-row", Horizontal).region
|
|
|
|
|
|
asyncio.run(scenario())
|
|
|
print(
|
|
|
@@ -2557,3 +2560,104 @@ def test_status_event_questions_populate_questions_view(server):
|
|
|
assert observed["questions"] == _questions_payload()
|
|
|
assert observed["epoch"] == observed["before_epoch"] + 1
|
|
|
assert any("topic [choice]" in line for line in observed["lines"])
|
|
|
+
|
|
|
+
|
|
|
+def test_chat_capture_checkbox_toggles_and_sends_control(server):
|
|
|
+ """Toggling #chat-capture off sends a chat_capture control message."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import Checkbox
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ sent: list[tuple] = []
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app._timer.stop() # no background drain: server status must not clobber state
|
|
|
+ app._drain()
|
|
|
+ app.client.send = lambda type_, **fields: sent.append((type_, fields))
|
|
|
+ capture = app.query_one("#chat-capture", Checkbox)
|
|
|
+ observed["initial"] = capture.value
|
|
|
+ capture.value = False
|
|
|
+ await pilot.pause()
|
|
|
+ observed["after_off"] = capture.value
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["initial"] is True
|
|
|
+ assert observed["after_off"] is False
|
|
|
+ assert ("chat_capture", {"on": False}) in sent
|
|
|
+
|
|
|
+
|
|
|
+def test_chat_capture_checkbox_follows_incoming_event(server):
|
|
|
+ """A chat_capture event updates the checkbox without bouncing a send back."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.widgets import Checkbox
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test() as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app._timer.stop() # no background drain: the raw event is the only source
|
|
|
+ app._drain()
|
|
|
+ app.state.ingest({"type": "chat_capture", "on": False})
|
|
|
+ app._render()
|
|
|
+ await pilot.pause()
|
|
|
+ observed["value"] = app.query_one("#chat-capture", Checkbox).value
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ assert observed["value"] is False
|
|
|
+
|
|
|
+
|
|
|
+def test_maximize_chat_docks_capture_controls_below(server):
|
|
|
+ """Maximizing chat keeps the capture controls visible, docked under the pane."""
|
|
|
+ pytest.importorskip("textual")
|
|
|
+ from textual.containers import Horizontal, Vertical
|
|
|
+ from textual.widgets import RichLog
|
|
|
+
|
|
|
+ from stt_daemon.tui.app import AssistantTUI
|
|
|
+
|
|
|
+ path, _service = server
|
|
|
+ app = AssistantTUI(path, triage_enabled=True)
|
|
|
+ observed: dict = {}
|
|
|
+
|
|
|
+ async def scenario():
|
|
|
+ async with app.run_test(size=(80, 40)) as pilot:
|
|
|
+ await pilot.pause()
|
|
|
+ app._timer.stop()
|
|
|
+ app._drain()
|
|
|
+ app._lower_view = "chat"
|
|
|
+ app.state.ingest({"type": "mode", "mode": "chat"})
|
|
|
+ app.state.ingest({"type": "chat_message", "role": "user", "text": "hi"})
|
|
|
+ app._render()
|
|
|
+ await pilot.pause()
|
|
|
+ chat = app.query_one("#chat", RichLog)
|
|
|
+ controls = app.query_one("#chat-controls", Vertical)
|
|
|
+ observed["normal_right_of_chat"] = controls.region.x > chat.region.x
|
|
|
+ observed["normal_same_top"] = controls.region.y == chat.region.y
|
|
|
+ app.query_one("#chat", RichLog).focus()
|
|
|
+ await pilot.pause()
|
|
|
+ await pilot.press("m")
|
|
|
+ await pilot.pause()
|
|
|
+ row = app.query_one("#chat-row", Horizontal)
|
|
|
+ observed["maximized_is_main"] = app.screen.maximized is app.query_one("#main")
|
|
|
+ observed["controls_display"] = controls.display
|
|
|
+ observed["controls_below_chat"] = controls.region.y > chat.region.y
|
|
|
+ observed["controls_span_row"] = controls.region.width == row.region.width
|
|
|
+
|
|
|
+ asyncio.run(scenario())
|
|
|
+ print("capture controls:", observed)
|
|
|
+ assert observed["normal_right_of_chat"] is True
|
|
|
+ assert observed["normal_same_top"] is True
|
|
|
+ assert observed["maximized_is_main"] is True
|
|
|
+ assert observed["controls_display"] is True
|
|
|
+ assert observed["controls_below_chat"] is True
|
|
|
+ assert observed["controls_span_row"] is True
|