Adds a ConversationsWindow that lists all past sessions with title and age,
and lets the user rejoin any session — loading its full message history back into
Gemma's context window. A new session starts fresh as today; rejoin continues
from where the conversation left off.
ConversationService persists sessions to .conversation_history.json on every append_messagesMainWindow._session_id is just a UUID; setting it to an old session ID makes _build_messages load that session's history automaticallyMemoryWindow Context tab already shows the message array for a session — confirms the data is therestarted_at, last_active, title — not currently storedlist_sessions() method on ConversationServiceConversationsWindow — floating panel showing sessions tableMainWindow.rejoin_session(session_id){session_id: [messages]}.
New format: {session_id: {messages: [...], started_at: float, last_active: float, title: str}}.
Old format detected on load (value is a list) and auto-migrated: title derived from
first message, timestamps set to current time. No manual migration step.
Current session highlighted (●). Clicking 🔁 on any row rejoins that session.
Clicking 🗑 deletes the session (with confirmation). Sessions ordered newest first by last_active.
| Change | Detail |
|---|---|
| New storage format | {session_id: {messages, started_at, last_active, title}} |
| Migration on load | Detect list value → wrap, derive title from first user message, timestamps = now |
| append_messages() | Update last_active on every write; set started_at + title on first write |
| list_sessions() | Returns [{session_id, title, message_count, started_at, last_active}] sorted by last_active desc |
| delete_session(session_id) | Remove from dict and save |
| get_history() unchanged | Still returns messages list — no callers need to change |
The schema change is internal to ConversationService. All callers use get_history() and append_messages() which keep the same signatures. Tests use :memory: sentinel so no disk interaction.
showEvent and Refresh buttonQMessageBox confirmation, then conv.delete_session() + refreshself._rejoin_callback(session_id) — callback injected by MainWindow at constructionConversationsWindow(conversation_service, session_id_getter, rejoin_callback)| Change | Detail |
|---|---|
| rejoin_session(session_id) | Sets self._session_id, clears log widget, appends "── rejoined ──" marker, raises ConversationsWindow to refresh |
| ConversationsWindow construction | Created at startup alongside MemoryWindow; injected with shared_conv, session_id_getter, and self.rejoin_session callback |
| Sidebar button | 💬 icon; opens/raises ConversationsWindow |
| Tile slot | col 2, row 1 (currently empty) in _tile_windows() |
| New session (⟳) button | Already exists; no change needed — it sets a new UUID which naturally creates a new session on next append_messages |
| File | Change |
|---|---|
| src/local/services/conversation_service.py | Schema migration, metadata fields, list_sessions(), delete_session() |
| src/local/ui/conversations_window.py | New file — sessions table, rejoin/delete actions |
| src/local/ui/main_window.py | Construct ConversationsWindow, rejoin_session(), sidebar button, tile slot |
| tests/test_conversation_service.py | Tests for list_sessions, delete_session, schema migration |