80bef6f524
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
2.0 KiB
Python
63 lines
2.0 KiB
Python
"""Rolling transcript of the conversation.
|
|
|
|
The speech bubble hides itself after a few seconds, which is fine for chat
|
|
but bad for anything you actually needed to read (a command's output, a
|
|
number, a URL). This keeps the last HISTORY_LIMIT turns so the tray's
|
|
History window — and click-to-copy on the bubble — have something to show.
|
|
|
|
Pure logic, no Qt: the UI half is ui/history_window.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import deque
|
|
from dataclasses import dataclass
|
|
from typing import Iterable, Optional
|
|
|
|
USER = "you"
|
|
PET = "bolt"
|
|
SYSTEM = "system"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Entry:
|
|
role: str
|
|
text: str
|
|
timestamp: Optional[float] = None # time.time(); None when not recorded
|
|
|
|
def formatted(self, clock=None) -> str:
|
|
label = {USER: "You", PET: "Bolt", SYSTEM: "—"}.get(self.role, self.role)
|
|
stamp = clock(self.timestamp) if (clock and self.timestamp) else None
|
|
return f"[{stamp}] {label}: {self.text}" if stamp else f"{label}: {self.text}"
|
|
|
|
|
|
class ConversationHistory:
|
|
def __init__(self, limit: int = 100):
|
|
self._entries: deque[Entry] = deque(maxlen=max(1, limit))
|
|
|
|
def add(self, role: str, text: str, timestamp: Optional[float] = None) -> Optional[Entry]:
|
|
text = (text or "").strip()
|
|
if not text:
|
|
return None
|
|
entry = Entry(role=role, text=text, timestamp=timestamp)
|
|
self._entries.append(entry)
|
|
return entry
|
|
|
|
def entries(self) -> list[Entry]:
|
|
return list(self._entries)
|
|
|
|
def last(self, role: Optional[str] = None) -> Optional[Entry]:
|
|
for entry in reversed(self._entries):
|
|
if role is None or entry.role == role:
|
|
return entry
|
|
return None
|
|
|
|
def clear(self) -> None:
|
|
self._entries.clear()
|
|
|
|
def as_text(self, clock=None, entries: Optional[Iterable[Entry]] = None) -> str:
|
|
return "\n".join(e.formatted(clock) for e in (entries if entries is not None else self._entries))
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._entries)
|