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:
2026-09-25 19:50:10 -06:00
co-authored by Claude Opus 5.5
parent 54030a7b31
commit 3977804ae3
144 changed files with 5477 additions and 14736 deletions
+18 -4
View File
@@ -4,7 +4,7 @@ read/write/edit local files instead of a raw shell heredoc.
filectl {"op": ...} is a single-line JSON envelope (not a multi-line
marker block) because it rides the "command" tool marker, which the main
repo's tool-call extractor only captures up to the next newline — see the
module docstring in bolt_pet/file_ops.py."""
module docstring in bolt_console/file_ops.py."""
import json
import sys
@@ -14,7 +14,7 @@ import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import file_ops
from bolt_console import file_ops
def _cmd(payload: dict) -> str:
@@ -305,7 +305,7 @@ def test_a_repair_is_reported_in_the_output_never_hidden(tmp_path):
tomorrow — the model has to be told while it still has the turn."""
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
action = file_ops.parse(
'filectl {"op":"list","path":"%s","recursive":false"}' % tmp_path
'filectl {"op":"list","path":%s,"recursive":false"}' % json.dumps(str(tmp_path))
)
output = file_ops.execute(action)
assert "a.txt" in output
@@ -315,7 +315,7 @@ def test_a_repair_is_reported_in_the_output_never_hidden(tmp_path):
def test_valid_json_gets_no_repair_note(tmp_path):
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
action = file_ops.parse('filectl {"op": "list", "path": "%s"}' % tmp_path)
action = file_ops.parse('filectl {"op": "list", "path": %s}' % json.dumps(str(tmp_path)))
assert "_repairs" not in action
assert "malformed" not in file_ops.execute(action)
@@ -327,3 +327,17 @@ def test_genuinely_unparseable_json_points_at_the_character():
message = str(excinfo.value)
assert "^" in message # caret under the offending character
assert '"extra"' in message # ...and the fragment around it
def test_a_raw_windows_path_is_repaired_and_reported():
r"""Models type Windows paths with single backslashes. `\U` isn't a JSON
escape, so strict parsing would reject the whole call."""
action = file_ops.parse(r'filectl {"op": "read", "path": "C:\Users\sam\data.txt"}')
assert action["path"] == r"C:\Users\sam\data.txt"
assert any("backslash" in repair for repair in action["_repairs"])
def test_the_backslash_repair_leaves_valid_escapes_alone():
# The escaped pair and the valid \n survive; only the lone \U is doubled.
action = file_ops.parse(r'filectl {"op": "read", "path": "C:\\ok\Users\n.txt"}')
assert action["path"] == "C:\\ok\\Users\n.txt"