Phase 13 — Conversation Session Navigator

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.

What Already Exists

What Needs Building

Design Decisions

Floating ConversationsWindow, not a sidebar expansion. Consistent with existing MemoryWindow / CriticWindow / DocumentsWindow pattern. Gets its own tile slot (col 2, row 1 — currently empty in the grid). Can be opened from a new sidebar button.
Session title = first 60 chars of first user message. Auto-derived; no manual naming needed. Stored in session metadata so it doesn't require re-scanning messages on every list refresh.
Rejoin clears the conversation log but keeps the session. The log widget is visual-only — clearing it doesn't affect ConversationService. A "── rejoined session ──" marker is appended so the user knows the context is loaded.
Schema migration on load. Current file format is {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.

UI Layout

conversations Refresh ──────────────────────────────────────────────────────── Title Msgs Age ──────────────────────────────────────────────────────── ● here is the URL that gives an outl… 24 13h 🔁 🗑 how about client-server architectu… 8 9m 🔁 🗑 what is Alexander the Great known … 4 49s 🔁 🗑 ──────────────────────────────────────────────────────── ● = current session 🔁 = rejoin 🗑 = delete

Current session highlighted (●). Clicking 🔁 on any row rejoins that session. Clicking 🗑 deletes the session (with confirmation). Sessions ordered newest first by last_active.

Implementation Plan

13a · ConversationService

Schema migration + session metadata

ChangeDetail
New storage format{session_id: {messages, started_at, last_active, title}}
Migration on loadDetect 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() unchangedStill 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.

13b · ConversationsWindow

Floating session list panel

13c · MainWindow wiring

Rejoin, sidebar button, tile slot

ChangeDetail
rejoin_session(session_id)Sets self._session_id, clears log widget, appends "── rejoined ──" marker, raises ConversationsWindow to refresh
ConversationsWindow constructionCreated at startup alongside MemoryWindow; injected with shared_conv, session_id_getter, and self.rejoin_session callback
Sidebar button💬 icon; opens/raises ConversationsWindow
Tile slotcol 2, row 1 (currently empty) in _tile_windows()
New session (⟳) buttonAlready exists; no change needed — it sets a new UUID which naturally creates a new session on next append_messages

Files Changed

FileChange
src/local/services/conversation_service.pySchema migration, metadata fields, list_sessions(), delete_session()
src/local/ui/conversations_window.pyNew file — sessions table, rejoin/delete actions
src/local/ui/main_window.pyConstruct ConversationsWindow, rejoin_session(), sidebar button, tile slot
tests/test_conversation_service.pyTests for list_sessions, delete_session, schema migration

Out of Scope