Upload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user