Efren Yevale Varela пре 1 месец
родитељ
комит
c272437470

+ 6 - 5
docs/ASSISTANT.md

@@ -55,10 +55,11 @@ Set `[llm].enabled = false` to run without an LLM (commands then report
 
 Command and chat modes are mutually exclusive.
 
-While in chat mode, spoken transcripts are captured automatically. The TUI's
-`send transcripts` checkbox (or `{"type":"chat_capture","on":false}`) disables that
-automatic capture without leaving chat: typed `chat-send` turns still reach the model,
-and triage/brief keep consuming transcripts independently.
+In chat mode, spoken transcripts can be captured automatically, but both `send transcripts`
+and `send to triage` default to **off**. The `send transcripts` checkbox
+(`{"type":"chat_capture","on":true}`) enables automatic capture of spoken turns; until it is on,
+only typed `chat-send` turns reach the model. Triage/brief still consume transcripts
+independently of this toggle.
 
 With the triage panel enabled, a second `send to triage` checkbox
 (`{"type":"triage_chat","on":true}`) mirrors chat turns into the triage window and brief.
@@ -232,7 +233,7 @@ daemon, remove the id from `speakers/speakers.json` and `prototypes.npz`, then r
 
 `t` cycles the lower panes through command output, conversation, and hidden. Selecting the conversation turns chat mode on; cycling away turns it off. The conversation is kept across those transitions — only `c` discards it. Arming or executing a command (by key or typed input) leaves chat mode if it was on and reveals the command-output pane; this never happens from within chat itself, since a command always pre-empts it. Transcripts are not affected.
 
-The row below the transcript holds two inputs, split 50/50: the command buffer on the left and a **chat** box on the right. Typing a message into the chat box and pressing Enter turns chat mode on (if it was off) and shows the conversation pane, sends the message immediately (no turn-end wait), clears the box, and keeps it focused for the next message. Chat mode does not need to be entered first. To the right of the conversation pane sits a **send transcripts** checkbox: turning it off stops spoken transcripts from being fed to the model while staying in chat (typed messages still send, and triage/brief are unaffected); it is clickable, or focusable with `Tab` outside the maximized view and toggled with Space. When the triage panel is enabled, a second **send to triage** checkbox mirrors chat turns into triage (assistant reply always, plus your message when `send transcripts` is off). Maximizing the conversation pane (`m`) hides the command buffer, keeps the chat box docked at the bottom of the screen, and docks the checkboxes beneath the pane; maximizing the command-output pane keeps the command buffer docked there instead. While maximized, `Tab` / `Shift+Tab` cycle focus between the pane and its docked input.
+The row below the transcript holds two inputs, split 50/50: the command buffer on the left and a **chat** box on the right. Typing a message into the chat box and pressing Enter turns chat mode on (if it was off) and shows the conversation pane, sends the message immediately (no turn-end wait), clears the box, and keeps it focused for the next message. Chat mode does not need to be entered first. To the right of the conversation pane sits a **send transcripts** checkbox, off by default: turning it on feeds spoken transcripts to the model while in chat (typed messages send regardless, and triage/brief are unaffected); it is clickable, or focusable with `Tab` outside the maximized view and toggled with Space. When the triage panel is enabled, a second **send to triage** checkbox mirrors chat turns into triage (assistant reply always, plus your message when `send transcripts` is off). Maximizing the conversation pane (`m`) hides the command buffer, keeps the chat box docked at the bottom of the screen, and docks the checkboxes beneath the pane; maximizing the command-output pane keeps the command buffer docked there instead. While maximized, `Tab` / `Shift+Tab` cycle focus between the pane and its docked input.
 
 `c` asks for confirmation, then wipes the conversation and command-output panes, hides whichever of them was showing, and resets the assistant's in-memory chat history (the LLM session), while staying in the current mode. Transcripts are not affected.
 

+ 1 - 1
src/stt_daemon/assistant/service.py

@@ -124,7 +124,7 @@ class AssistantService:
         self.chat_history: list[str] = []
         self._pending_chat: list[str] = []
         self.triage_history: list[str] = []
-        self.chat_capture = True
+        self.chat_capture = False
         self.triage_chat = False
 
         # Speaker roster cache (display-name join only; the daemon owns identity).

+ 3 - 1
src/stt_daemon/tui/app.py

@@ -119,7 +119,7 @@ class AssistantTUI(App):
         yield Footer()
 
     def _chat_controls(self):
-        yield Checkbox("send transcripts", value=True, id="chat-capture")
+        yield Checkbox("send transcripts", value=False, id="chat-capture")
         if self.triage_enabled:
             yield Checkbox("send to triage", value=False, id="triage-chat")
 
@@ -180,6 +180,8 @@ class AssistantTUI(App):
             except queue.Empty:
                 break
             self.state.ingest(event)
+        if not self.query("#chat-input"):
+            return
         if self.screen.maximized is None:
             self._restore_panes()
         self._render()

+ 1 - 1
src/stt_daemon/tui/state.py

@@ -90,7 +90,7 @@ class ViewState:
     command_text: str = ""
     command_edited: bool = False
     in_flight: bool = False
-    chat_capture: bool = True
+    chat_capture: bool = False
     triage_chat: bool = False
     transcripts: list[dict] = field(default_factory=list)
     transcripts_total: int = 0

+ 3 - 0
tests/test_assistant_integration.py

@@ -634,6 +634,7 @@ def test_two_engines_emit_signals_then_brief_and_chat_works(tmp_path):
     h = _engine_service(tmp_path, client, chat_slot=7, brief_slot=5)
 
     assert h.service.toggle_chat() is True
+    h.service.set_chat_capture(True)
     _settle(h.service, h.clock, "first thought")
     h.service.tick()  # dispatches the chat turn through the real queue
 
@@ -673,6 +674,7 @@ def test_slotless_engines_omit_id_slot(tmp_path):
     h = _engine_service(tmp_path, client, chat_slot=None, brief_slot=None)
 
     assert h.service.toggle_chat() is True
+    h.service.set_chat_capture(True)
     _settle(h.service, h.clock, "hello")
     h.service.tick()
     _deliver(h.service, _laya_payload())
@@ -774,6 +776,7 @@ def test_clear_resets_both_engines_and_stale_job_cannot_clobber(tmp_path):
     service = h.service
 
     assert service.toggle_chat() is True
+    service.set_chat_capture(True)
     _settle(service, h.clock, "hello")
     service.tick()
     _run_queued(service, _chat_jobs(service)[0])

+ 28 - 5
tests/test_assistant_service.py

@@ -231,6 +231,7 @@ def test_chat_idle_timeout_exits_chat():
 def test_chat_exit_phrase_exits_chat():
     service, _, clock = _service()
     assert service.toggle_chat() is True
+    service.set_chat_capture(True)
 
     service.handle_event(_transcript(clock, "That's all."))
 
@@ -240,6 +241,7 @@ def test_chat_exit_phrase_exits_chat():
 def test_exit_phrase_preserves_chat_history():
     service, _, clock = _service()
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
     service.handle_event(_transcript(clock, "That's all."))
 
@@ -250,6 +252,7 @@ def test_exit_phrase_preserves_chat_history():
 def test_viewer_detached_exits_chat_and_keeps_history():
     service, _, clock = _service()
     assert service.toggle_chat() is True
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
 
     assert service.viewer_detached() is True
@@ -270,6 +273,7 @@ def test_exit_chat_preserves_history_and_does_not_reset():
         cfg, FakeSubscriber(), time_fn=Clock(), chat_reset=lambda: resets.append(1)
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(Clock(), "hello"))
 
     service.viewer_detached()
@@ -282,6 +286,7 @@ def test_exit_chat_preserves_history_and_does_not_reset():
 def test_chat_accumulates_turns():
     service, _, clock = _service()
     service.toggle_chat()
+    service.set_chat_capture(True)
 
     for text in ("hello there", "how are you"):
         service.handle_event(_transcript(clock, text))
@@ -316,16 +321,16 @@ def test_chat_capture_off_still_allows_typed_turns_and_triage():
 
 def test_set_chat_capture_emits_once_and_reports_status():
     service, emitted, _ = _service()
-    assert service.status()["chat_capture"] is True
+    assert service.status()["chat_capture"] is False
 
-    assert service.set_chat_capture(False) is True
+    assert service.set_chat_capture(True) is True
     assert [e for e in emitted if e["type"] == "chat_capture"] == [
-        {"type": "chat_capture", "on": False}
+        {"type": "chat_capture", "on": True}
     ]
-    assert service.status()["chat_capture"] is False
+    assert service.status()["chat_capture"] is True
 
     emitted.clear()
-    service.set_chat_capture(False)
+    service.set_chat_capture(True)
     assert emitted == []
 
 
@@ -341,6 +346,7 @@ def test_triage_chat_off_ignores_chat_turns():
 def test_triage_chat_sends_reply_only_when_capture_on():
     service, _, _, _, lane = _triage_service()
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.set_triage_chat(True)
 
     service._run_chat("spoken, already in triage")
@@ -513,6 +519,7 @@ def test_pending_chat_is_not_lost_across_threads():
 
     service = AssistantService(cfg, FakeSubscriber(), emit=emit, chat_fn=lambda text: "ok")
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.start()
     try:
         total = 200
@@ -630,6 +637,7 @@ def test_chat_notify_emits_truncated_error():
 def test_clear_empties_chat_history():
     service, emitted, clock = _service()
     service.toggle_chat()
+    service.set_chat_capture(True)
 
     for text in ("hello", "how are you"):
         service.handle_event(_transcript(clock, text))
@@ -1352,6 +1360,7 @@ def test_none_payload_dispatches_nothing_and_keeps_deltas():
         brief_fn=lambda prompt: calls.append(prompt) or "RAW"
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1371,6 +1380,7 @@ def test_none_payload_dispatches_nothing_and_keeps_deltas():
 def test_none_payload_without_brief_fn_enqueues_nothing():
     service, emitted, clock, _, lane = _triage_service()
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1406,6 +1416,7 @@ def test_substantive_turn_dispatches_one_brief_job():
         triage=FakeTriage(escalate=True), brief_fn=lambda prompt: "RAW"
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "we might churn"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1429,6 +1440,7 @@ def test_substantive_turn_dispatches_one_brief_job():
 def test_brief_dispatches_without_escalation():
     service, _, clock, _, lane = _triage_service(brief_fn=lambda prompt: "RAW")
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "all calm"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1451,6 +1463,7 @@ def test_gated_out_turn_skips_brief():
         brief_fn=lambda prompt: calls.append(prompt) or "RAW",
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "small talk"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1504,6 +1517,7 @@ def test_gated_out_turn_retains_brief_when_extract_disabled():
         triage=FakeTriage(gate=False), brief_fn=lambda prompt: "RAW"
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     signals = _escalated_payload()
     service._last_brief_signals = signals
     token = _seed_brief(service, "Unknown: first")
@@ -1578,6 +1592,7 @@ def test_no_brief_when_disabled_in_config():
     )
     service.cfg.triage.extract_enabled = False
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "churn risk"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1594,6 +1609,7 @@ def test_no_brief_when_disabled_in_config():
 def test_no_brief_without_brief_fn():
     service, _, clock, _, lane = _triage_service(triage=FakeTriage(escalate=True))
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "churn risk"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1613,6 +1629,7 @@ def test_brief_refused_when_queue_full():
     )
     service._inference = InferenceQueue(size=2)
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "churn risk"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1634,6 +1651,7 @@ def test_second_result_while_outstanding_is_refused():
     """At most one brief job is outstanding: the second turn only stores signals."""
     service, emitted, clock, _, lane = _triage_service(brief_fn=lambda prompt: "RAW")
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "first"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -1903,6 +1921,7 @@ def test_clear_resets_triage():
         chat_reset=lambda: chats.append(1),
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "churn"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -2044,6 +2063,7 @@ def test_stale_brief_after_clear_does_not_clear_newer_dispatch_flag():
         brief_fn=lambda prompt: "RAW", brief_file=brief_file
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -2101,6 +2121,7 @@ def test_triage_end_to_end_signals_content_cleared():
         brief_fn=lambda prompt: prompts.append(prompt) or "the brief"
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
 
     service.handle_event(_transcript(clock, "first thought"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
@@ -2315,6 +2336,7 @@ def _dump_brief(prompt, on_io=None, finalize=None):
 
 def _run_triage_flow(service, emitted, clock, lane, payload):
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
     clock.advance(service.cfg.assistant.turn_gap_s + 0.1)
     service.tick()
@@ -2683,6 +2705,7 @@ def test_live_session_change_resets_triage_and_keeps_chat():
         brief_file=brief_file, brief_reset=lambda: resets.append(1)
     )
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(_transcript(clock, "hello"))
     service.handle_event(_session_event(clock, "transcripts/a"))
     service.last_triage = {"signals": 1}

+ 1 - 0
tests/test_assistant_speaker.py

@@ -200,6 +200,7 @@ def test_speaker_name_inbox_is_private(tmp_path):
 def test_chat_context_uses_display_labels(tmp_path):
     service, _, clock = _service(tmp_path)
     service.toggle_chat()
+    service.set_chat_capture(True)
     service.handle_event(
         _transcript(
             clock, "hi", speaker_id="spk_0001", speaker_status="matched", speaker="Efren"

+ 5 - 4
tests/test_control.py

@@ -268,13 +268,13 @@ def test_chat_capture_command_sets_and_broadcasts(harness):
     client.send(type="subscribe", viewer=True)
     status = client.wait_for(lambda m: m["type"] == "status")
     assert status is not None
-    assert status["chat_capture"] is True
+    assert status["chat_capture"] is False
 
-    client.send(type="chat_capture", on=False)
+    client.send(type="chat_capture", on=True)
     assert client.wait_for(lambda m: m["type"] == "ack" and m["command"] == "chat_capture")
-    assert harness.service.chat_capture is False
+    assert harness.service.chat_capture is True
     assert client.wait_for(
-        lambda m: m["type"] == "chat_capture" and m["on"] is False
+        lambda m: m["type"] == "chat_capture" and m["on"] is True
     ) is not None
 
     client.close()
@@ -375,6 +375,7 @@ def test_clear_broadcasts_cleared_event(harness):
     assert client.wait_for(lambda m: m["type"] == "mode" and m["mode"] == "chat")
 
     service = harness.service
+    service.set_chat_capture(True)
     service.handle_event(_transcript("hello"))
 
     assert len(service.chat_history) == 1

+ 8 - 8
tests/test_tui_app.py

@@ -2563,7 +2563,7 @@ def test_status_event_questions_populate_questions_view(server):
 
 
 def test_chat_capture_checkbox_toggles_and_sends_control(server):
-    """Toggling #chat-capture off sends a chat_capture control message."""
+    """Toggling #chat-capture on sends a chat_capture control message."""
     pytest.importorskip("textual")
     from textual.widgets import Checkbox
 
@@ -2582,14 +2582,14 @@ def test_chat_capture_checkbox_toggles_and_sends_control(server):
             app.client.send = lambda type_, **fields: sent.append((type_, fields))
             capture = app.query_one("#chat-capture", Checkbox)
             observed["initial"] = capture.value
-            capture.value = False
+            capture.value = True
             await pilot.pause()
-            observed["after_off"] = capture.value
+            observed["after_on"] = capture.value
 
     asyncio.run(scenario())
-    assert observed["initial"] is True
-    assert observed["after_off"] is False
-    assert ("chat_capture", {"on": False}) in sent
+    assert observed["initial"] is False
+    assert observed["after_on"] is True
+    assert ("chat_capture", {"on": True}) in sent
 
 
 def test_chat_capture_checkbox_follows_incoming_event(server):
@@ -2608,13 +2608,13 @@ def test_chat_capture_checkbox_follows_incoming_event(server):
             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.state.ingest({"type": "chat_capture", "on": True})
             app._render()
             await pilot.pause()
             observed["value"] = app.query_one("#chat-capture", Checkbox).value
 
     asyncio.run(scenario())
-    assert observed["value"] is False
+    assert observed["value"] is True
 
 
 def test_maximize_chat_docks_capture_controls_below(server):

+ 5 - 5
tests/test_tui_state.py

@@ -116,16 +116,16 @@ def test_status_populates_fields():
     assert state.command_text == "x"
 
 
-def test_chat_capture_defaults_on_and_follows_event_and_status():
+def test_chat_capture_defaults_off_and_follows_event_and_status():
     state = ViewState()
-    assert state.chat_capture is True
-
-    state.ingest({"type": "chat_capture", "on": False})
     assert state.chat_capture is False
 
-    state.ingest({"type": "status", "chat_capture": True})
+    state.ingest({"type": "chat_capture", "on": True})
     assert state.chat_capture is True
 
+    state.ingest({"type": "status", "chat_capture": False})
+    assert state.chat_capture is False
+
 
 def test_triage_chat_defaults_off_and_follows_event_and_status():
     state = ViewState()