"""The seam between a turn running on a worker thread and the window. `Session` and `Tools` were written against a terminal-shaped UI (tool_call, tool_result, confirm, assistant_chunk, …). `Bridge` is that same shape, but every call becomes a Qt signal — delivered to the window on the GUI thread through a queued connection — so the whole turn logic is shared, unchanged, between the terminal and the window. The one call that has to *return* something is confirm(): the worker blocks on an Event until the user clicks Allow/Deny in the window, which calls answer() from the GUI thread. close() releases a pending wait so the app can quit mid approval without hanging the worker. """ from __future__ import annotations import threading from PySide6.QtCore import QObject, Signal class Bridge(QObject): busy = Signal(bool) chunk = Signal(str) # one streamed sentence reply_ended = Signal() # the streamed block is complete reply = Signal(str) # a whole (non-streamed) reply holding_line = Signal(str) # "give me a sec" while a tool runs tool_started = Signal(int, str, str) # id, name, argument tool_output = Signal(int, str, bool) # id, text, is_error tool_diff = Signal(int, str, str) # id, old, new approval_needed = Signal(int, str) # id, question notice = Signal(str, str) # level (info/warn/error), text def __init__(self, parent=None): super().__init__(parent) self._tool_id = 0 self._answer = "no" self._answered = threading.Event() self._closed = False # ── the terminal-shaped surface Session and Tools call ─────────────── def start_spinner(self, label: str = "Thinking") -> None: self.busy.emit(True) def stop_spinner(self) -> None: self.busy.emit(False) def assistant_chunk(self, sentence: str) -> None: if sentence.strip(): self.chunk.emit(sentence.strip()) def end_reply(self) -> None: self.reply_ended.emit() def assistant(self, text: str) -> None: if text.strip(): self.reply.emit(text.strip()) def holding(self, text: str) -> None: self.holding_line.emit(text.strip()) def tool_call(self, name: str, argument: str) -> None: self._tool_id += 1 self.tool_started.emit(self._tool_id, name, argument) def tool_result(self, text: str, error: bool = False) -> None: self.tool_output.emit(self._tool_id, text, error) def diff(self, old: str, new: str) -> None: self.tool_diff.emit(self._tool_id, old, new) def confirm(self, question: str) -> str: if self._closed: return "no" self._answered.clear() self._answer = "no" self.approval_needed.emit(self._tool_id, question) self._answered.wait() return self._answer def info(self, text: str) -> None: self.notice.emit("info", text) def warn(self, text: str) -> None: self.notice.emit("warn", text) def error(self, text: str) -> None: self.notice.emit("error", text) def line(self, text: str = "") -> None: if text.strip(): self.notice.emit("info", text.strip()) def style(self, text: str, *_names: str) -> str: return text # ── called from the GUI thread ─────────────────────────────────────── def answer(self, value: str) -> None: self._answer = value if value in ("yes", "no", "always") else "no" self._answered.set() def close(self) -> None: self._closed = True self.answer("no")