5 Conversations
Thomas Forgione edited this page 2026-09-11 17:29:53 +02:00

Saved conversations

Every conversation is written to ~/.local/share/papote/conversations/YYYY-MM-DD-<uuid>.json, under $XDG_DATA_HOME if you set it. Data, not configuration, so it does not live next to the settings.

The date is the day the conversation started, and it comes first so that the directory listed by name is the conversations in the order you had them — ls sorts them for you. The UUID is still on the end, since two conversations in a day is not unusual.

One file per conversation, holding the messages exactly as they went over the wire plus the model they were said to. That is what makes a resumed conversation byte-for-byte the history the model already saw — the system prompt included, since the API takes it as the first message like any other.

Each message also carries an at, the unix time it was finished: when you pressed Enter, when the model's last token landed, when a tool handed its answer back. That one is papote's own and is never sent — the endpoint gets the message and nothing else.

{ "role": "user",      "content": "Quelle heure est-il ?", "at": 1789140229 }
{ "role": "assistant", "tool_calls": [  ],                "at": 1789140232 }
{ "role": "tool",      "content": "{\"result\": …}",         "at": 1789140232 }
{ "role": "assistant", "content": "Il est 17:23.",         "at": 1789140233 }

A conversation saved before papote kept the time reads back with none rather than refusing to load, and is renamed to the dated form the next time it is written to — the new file first, the old one only once that has worked.

When one is created

By the first message. Starting papote and leaving writes nothing, and an answer that errored out leaves no trace. Under --private, none is ever created — see Private mode.

The file is rewritten after each completed exchange, so killing papote in the middle of an answer loses that answer and nothing else.

/clear starts a new conversation and leaves the old one on disk. The system prompt survives it; a conversation holding nothing but a system prompt is never written.

Names

A conversation can have a name. It shows on the top border of the input box, and /resume lists it by that name rather than by its first message.

┌───────────────────────────────── Les couleurs du ciel ┐
│❯                                                      │
└───────────────────────────────────────────────────────┘

/rename Les couleurs du ciel sets one. /rename with nothing after it asks the model to come up with a name for what has been said so far — a short, separate request that is never added to the conversation.

By default that happens on its own, once, after the first answer. Turn it off with auto_name = false under [ui] in config.toml; /rename still works on demand.

The instruction sent to the model is in English so as not to nudge it towards any one language, and asks it to answer in the language of the conversation. Models being models, the answer still arrives wrapped in quotes or prefixed with Title: often enough that papote strips all of that before using it.

Resuming

/resume lists the twenty most recent, newest first:

┌ Recent conversations ─────────────────────────────────────────────┐
│❯  1 min ago   gemma-4:26B-A4B:32K    Tu peux afficher une balise…  │
│  11 min ago   gemma-4:26B-A4B:32K    Salut !                       │
│     1 h ago   court                  Salut ! Ca va ?               │
│  2 days ago   gemma-4:26B-A4B:32K    Une conversation d'il y a…    │
└────────────────────────────────────────────────────────────────────┘

The title is the first thing you said, on one line. Picking one puts its whole history back in front of the model, so it can be questioned about what was said before.

Resuming stays on the model you are currently using rather than switching to the one the conversation was held with; the list shows which that was, and /model is one command away if you want to switch.

The directory is sorted by modification time before anything is parsed, so a long history costs no more to list than a short one. A file that no longer parses is skipped rather than making the whole list unusable.

Private mode

papote --private

Nothing about the conversation reaches the disk: no file is created, no name is asked of the model, and /resume has nothing to offer — it answers private mode — this conversation is not saved rather than listing what is already there. The input box says which mode you are in, in the corner where a name would otherwise sit:

┌────────────────────────────────────────────────── private ┐
│❯                                                          │
└───────────────────────────────────────────────────────────┘

Everything else is a normal papote: the model still receives the whole conversation, /clear still starts a fresh one, and /model still writes your pick to config.toml — that is configuration, not conversation.

Conversations saved by earlier runs are left exactly as they are. Private mode is about what this run writes, not about hiding what is already there.

Housekeeping

Nothing prunes the directory: conversations are small, and deleting one is rm. The format is plain JSON, so jq works on them.

# what did I ask about, and when?
jq -r '[(.updated|todate), .model, .messages[0].content] | @tsv' \
  ~/.local/share/papote/conversations/*.json | sort