Table of contents
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
papote
Getting started
Using it
Under the hood