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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user