Temporary chats

A clock in the top-right starts one. It is never listed in the sidebar and
is swept a day after the last thing said in it.

A real row rather than something held in the browser, because a reload, a
crash or a background tab all look identical from here -- "delete when you
navigate away" would lose conversations people meant to keep. The flag rides
in the URL (/chat?temporary=1) rather than in JavaScript, so it survives a
reload and can be bookmarked, and the composer carries it as a hidden field
beside model_id.

Keep clears the flag. Without a way out, a conversation that turns out to
matter is destroyed a day later with no recourse, and people would find that
out exactly once.

archived was filtered in three places and temporary mirrors all three, plus
Folder.visible_chats. It also skips the unread flag in _persist: there is no
sidebar row for the dot to land on, and the toast would name a chat nobody
can navigate to.

The sweep measures age from the newest message, not from the chat row.
created_at would destroy a conversation still in use at hour 23, and
updated_at does not move when a message is inserted -- onupdate fires on an
UPDATE of the chat, and adding a message is not one. It runs at startup
beside the existing upload sweep.

Deleting a chat cascades its rows but leaves the files on disk; only the
orphan sweep unlinks anything, and it looks only at uploads that were never
attached. files.remove_files_for_chats() closes that for the new sweep. The
same hole in delete_chat is pre-existing and left for its own change, which
can now call the same helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-01 00:45:03 +02:00
parent e185edc9e1
commit 09eecbdd9a
12 changed files with 387 additions and 10 deletions
+1
View File
@@ -345,6 +345,7 @@ button, input, textarea, select {
.badge--leaf { background: var(--leaf-soft); color: var(--leaf); }
.badge--success { background: var(--success-soft); color: var(--success); }
.badge--danger { background: var(--danger-soft); color: var(--danger); }
.badge--warning { background: var(--warning-soft); color: var(--warning); }
/* --- Application shell ----------------------------------------------------- */
.shell { display: flex; height: 100dvh; overflow: hidden; }
@@ -50,6 +50,9 @@
{% if not chat and current_model %}
<input type="hidden" name="model_id" value="{{ current_model.model_id }}">
{% endif %}
{% if not chat and starting_temporary %}
<input type="hidden" name="temporary" value="true">
{% endif %}
<div class="composer__row">
{% if can.get("files.upload") %}
+26
View File
@@ -25,6 +25,32 @@
</h1>
<div class="topbar__actions">
{#
A link, not a script: the flag lives in the URL, so it survives a
reload and can be bookmarked. On an existing temporary chat the same
corner explains what temporary means and offers the way out -- without
one, a conversation that turns out to matter is destroyed a day later
with no recourse.
#}
{% if chat and chat.temporary %}
<span class="badge badge--warning"
title="Not listed in the sidebar, and removed 24 hours after the last message.">
Temporary
</span>
<button class="btn btn--sm" type="button"
hx-post="/api/chats/{{ chat.id }}/keep" hx-swap="none"
title="Keep this chat and list it in the sidebar">
{{ icon("pin", "icon--sm") }} Keep
</button>
{% elif not chat and can.get("chat.create") %}
<a class="btn btn--icon {{ 'is-active' if starting_temporary }}"
href="{{ '/chat' if starting_temporary else '/chat?temporary=1' }}"
aria-label="Temporary chat"
title="{% if starting_temporary %}Starting a temporary chat. Click to go back to a normal one.{% else %}Start a temporary chat: not listed in the sidebar, and removed after a day.{% endif %}">
{{ icon("clock") }}
</a>
{% endif %}
{% if models %}
{% if can.get("chat.model_select") or not chat %}
{% include "chat/_model_picker.html" %}
@@ -114,6 +114,10 @@
<rect x="3.5" y="4.5" width="17" height="4" rx="1.2"/>
<path d="M5 8.5v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9M10 12.5h4"/>
</symbol>
<symbol id="i-clock" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="8.5"/>
<path d="M12 7.2V12l3.2 2"/>
</symbol>
<symbol id="i-warning" viewBox="0 0 24 24">
<path d="M12 4.2 21 19.5H3Z"/>
<path d="M12 10v4M12 16.8h.01"/>