Files
themajesticmagicianandClaude Opus 5.5 3977804ae3 Bolt Console: desktop app with chat list / new chat, dictation, file tools
Add a chat list and server-minted new chats on top of the desk API's
GET/POST /desk/chats:

- server_client.list_chats() / create_chat(), same shape and error
  handling as list_outbox_files().
- Switching is config.set_session(id): a plain reassignment of the
  SESSION_ID every call reads, persisted to session.json. The window
  refuses to switch mid-turn rather than locking.
- gui/chat_list_window.py: a Chats dialog (Ctrl+K) with a QListWidget,
  populated on open, with Refresh and New Chat. Network calls run as
  QRunnables on the global QThreadPool and come back through Signals,
  never from a slot. The header New chat (Ctrl+N) uses the same path.
- Switching clears the local transcript; there is no endpoint for a
  past chat's messages. Bolt's memory is per user, so it carries over.
- Terminal: /chats, /chat <n|id>, and /new (server-minted id, local
  fallback).

This commit also carries the uncommitted work since 54030a7 that turned
this Bolt-Pet fork into Bolt Console: the pet, wake word, TTS and screen
features removed; a Claude Code-style terminal client; the PySide6
window with tool cards, approvals and diffs; mic dictation through the
server's /desk/stt relay; filectl/JSON repairs; and detection of
invented tool results.

Build output (build/, dist/, Release/, *.spec) is now ignored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 19:50:10 -06:00

107 lines
3.7 KiB
Python

"""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")