Files
Bolt-Console/bolt_console/chats.py
T
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

54 lines
2.0 KiB
Python

"""Chat-list helpers with no Qt, shared by the Chats dialog and the
terminal's /chats: turning what GET /desk/chats returns into readable rows."""
from __future__ import annotations
import re
from datetime import datetime, timezone
from typing import Optional
# Context notes clients append to what you typed. They're in the stored last
# message, but they aren't what you said, so they stay out of the preview.
# The preview may be cut off mid-note, so an unclosed one runs to the end.
_CONTEXT_NOTES = re.compile(r"\s*\[(?:Bolt Console:|on screen right now:)[^\]]*(?:\]|$)", re.IGNORECASE)
def clean_preview(text: str, limit: int = 110) -> str:
text = " ".join(_CONTEXT_NOTES.sub("", str(text or "")).split())
return text if len(text) <= limit else text[: limit - 1].rstrip() + "…"
def relative_time(iso: str, now: Optional[datetime] = None) -> str:
"""'just now', '12 min ago', '3 h ago', 'yesterday', '4 days ago', or a
date. Unparseable input comes back as it was."""
try:
then = datetime.fromisoformat(str(iso).replace("Z", "+00:00"))
except (TypeError, ValueError):
return str(iso or "")
if then.tzinfo is None:
then = then.replace(tzinfo=timezone.utc)
now = now or datetime.now(timezone.utc)
seconds = (now - then).total_seconds()
if seconds < 60:
return "just now"
if seconds < 3600:
return f"{int(seconds // 60)} min ago"
if seconds < 86400:
return f"{int(seconds // 3600)} h ago"
days = int(seconds // 86400)
if days == 1:
return "yesterday"
if days < 7:
return f"{days} days ago"
return then.astimezone().strftime("%d %b %Y").lstrip("0")
def local_time(iso: str) -> str:
try:
then = datetime.fromisoformat(str(iso).replace("Z", "+00:00"))
if then.tzinfo is None:
then = then.replace(tzinfo=timezone.utc)
return then.astimezone().strftime("%A %d %B %Y, %H:%M")
except (TypeError, ValueError):
return str(iso or "")