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,163 @@
|
||||
"""`python -m bolt_console --doctor` — is this install actually going to work?
|
||||
|
||||
Carried over from Bolt-Pet, where a missing port publish, a wrong reverse-proxy
|
||||
header and a venv whose python was a zero-byte file all presented as "it's
|
||||
being weird" and each took a conversation to find.
|
||||
|
||||
Each check is independent and reports one of three things — ok, a warning
|
||||
(works, but degraded), or a failure (this will not do what you expect) — and
|
||||
says *what to do about it* rather than just what is wrong. Nothing here raises:
|
||||
a doctor that crashes on a broken install is diagnosing the wrong patient.
|
||||
|
||||
Deliberately does not touch the network by default — it's the first thing you
|
||||
reach for when the network is what's broken. `--deep` contacts the server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Optional
|
||||
|
||||
from . import config
|
||||
|
||||
OK, WARN, FAIL = "ok", "warn", "fail"
|
||||
|
||||
_MARKS = {OK: " ok ", WARN: " warn ", FAIL: " FAIL "}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Check:
|
||||
name: str
|
||||
status: str
|
||||
detail: str = ""
|
||||
fix: str = ""
|
||||
|
||||
def line(self) -> str:
|
||||
text = f"[{_MARKS[self.status]}] {self.name:22} {self.detail}"
|
||||
if self.fix and self.status != OK:
|
||||
text += f"\n{'':32}→ {self.fix}"
|
||||
return text
|
||||
|
||||
|
||||
# ── the checks ──────────────────────────────────────────────────────────────
|
||||
|
||||
def check_config() -> Check:
|
||||
missing = config.missing_config()
|
||||
if missing:
|
||||
return Check("server config", FAIL, f"missing {', '.join(missing)}",
|
||||
"set them in .env — without these the console exits at startup")
|
||||
return Check("server config", OK, f"{config.SERVER_URL} as {config.SESSION_ID}")
|
||||
|
||||
|
||||
def check_server(deep: bool = False) -> Check:
|
||||
if not config.SERVER_URL:
|
||||
return Check("server", FAIL, "no BOLT_SERVER_URL",
|
||||
"cp .env.example .env and set BOLT_SERVER_URL to your Bolt server")
|
||||
if not deep:
|
||||
return Check("server", OK, f"{config.SERVER_URL} (not contacted; --deep to try)")
|
||||
try:
|
||||
from . import server_client
|
||||
|
||||
health = server_client.check_health(timeout=8)
|
||||
return Check("server", OK, f"reachable — {health}")
|
||||
except Exception as exc:
|
||||
return Check("server", FAIL, f"unreachable: {exc}",
|
||||
"check the URL, the key, and that the container is up")
|
||||
|
||||
|
||||
def check_streaming_endpoint(deep: bool = False) -> Check:
|
||||
"""The streamed-reply endpoint is newer than some deployed servers."""
|
||||
if not config.STREAMING_REPLIES:
|
||||
return Check("streamed replies", WARN, "disabled (STREAMING_REPLIES=false)")
|
||||
if not deep:
|
||||
return Check("streamed replies", OK, "enabled (endpoint not probed)")
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
f"{config.SERVER_URL}/desk/converse_stream",
|
||||
json={"session_id": config.SESSION_ID, "text": ""},
|
||||
headers={"X-Desk-Api-Key": config.API_KEY}, timeout=8, stream=True,
|
||||
)
|
||||
if response.status_code == 404:
|
||||
return Check("streamed replies", WARN, "server has no /desk/converse_stream",
|
||||
"update the server, or set STREAMING_REPLIES=false to skip the probe")
|
||||
return Check("streamed replies", OK, f"endpoint answered {response.status_code}")
|
||||
except Exception as exc:
|
||||
return Check("streamed replies", WARN, f"probe failed: {exc}")
|
||||
|
||||
|
||||
def check_python() -> Check:
|
||||
version = ".".join(str(n) for n in sys.version_info[:3])
|
||||
if sys.version_info < (3, 9):
|
||||
return Check("python", FAIL, version, "Python 3.9 or newer is required")
|
||||
return Check("python", OK, f"{version} at {sys.executable}")
|
||||
|
||||
|
||||
def check_approval() -> Check:
|
||||
mode = config.APPROVAL_MODE
|
||||
if mode == "auto":
|
||||
return Check("approval", WARN, "auto — Bolt's commands, writes and edits run without asking",
|
||||
"APPROVAL_MODE=ask to confirm each one (or /ask for one session)")
|
||||
if mode != "ask":
|
||||
return Check("approval", WARN, f"unknown APPROVAL_MODE={mode!r}, treated as ask",
|
||||
"use ask or auto")
|
||||
return Check("approval", OK, "ask before commands, writes and edits")
|
||||
|
||||
|
||||
def check_deliveries() -> Check:
|
||||
if not config.RECEIVE_FILES:
|
||||
return Check("file delivery", WARN, "disabled (RECEIVE_FILES=false)")
|
||||
directory = config.DELIVERED_FILES_DIR
|
||||
probe = directory
|
||||
while not probe.exists() and probe.parent != probe:
|
||||
probe = probe.parent
|
||||
import os
|
||||
|
||||
if not os.access(str(probe), os.W_OK):
|
||||
return Check("file delivery", FAIL, f"can't write under {probe}",
|
||||
"set DELIVERED_FILES_DIR to a folder you can write to")
|
||||
return Check("file delivery", OK, str(directory))
|
||||
|
||||
|
||||
CHECKS = (
|
||||
("python", check_python),
|
||||
("config", check_config),
|
||||
("server", check_server),
|
||||
("stream", check_streaming_endpoint),
|
||||
("approval", check_approval),
|
||||
("deliveries", check_deliveries),
|
||||
)
|
||||
|
||||
|
||||
def run(deep: bool = False) -> list:
|
||||
results = []
|
||||
for _name, check in CHECKS:
|
||||
try:
|
||||
try:
|
||||
results.append(check(deep))
|
||||
except TypeError:
|
||||
results.append(check())
|
||||
except Exception as exc: # a broken check must not hide the others
|
||||
results.append(Check(_name, FAIL, f"the check itself failed: {exc}"))
|
||||
return results
|
||||
|
||||
|
||||
def main(argv: Optional[list] = None) -> int:
|
||||
argv = list(argv if argv is not None else sys.argv[1:])
|
||||
deep = "--deep" in argv
|
||||
print(f"Bolt Console preflight{' (deep: contacting the server)' if deep else ''}\n")
|
||||
results = run(deep=deep)
|
||||
for check in results:
|
||||
print(check.line())
|
||||
failures = [c for c in results if c.status == FAIL]
|
||||
warnings = [c for c in results if c.status == WARN]
|
||||
print()
|
||||
if failures:
|
||||
print(f"{len(failures)} problem(s) will stop this working. Fix those first.")
|
||||
elif warnings:
|
||||
print(f"Ready. {len(warnings)} thing(s) degraded but working.")
|
||||
else:
|
||||
print("Everything checks out.")
|
||||
return 1 if failures else 0
|
||||
Reference in New Issue
Block a user