feat: keep the chat tabs you left open, and keep them with you
Nightly Build / build (push) Successful in 7m39s

Reopening the app closed every project tab: the copilot's tab bar lived in
RAM, so a reload dropped it and each conversation had to be found again from
its project board.

The set of open tabs is now a column on the session row, `chat_sessions.is_open`
(additive, `ensure_column`), restored by `GET /api/sessions/open` and written by
`PUT /api/sessions/{id}/open`. Not localStorage: that store is per-origin, so on
a shared laptop one member's tabs would greet the next, whereas the owner table
sits in their own encrypted file and follows them to another device. Which tab
is *selected* stays in sessionStorage — that one is genuinely per window, and a
shared value would have two windows fighting over it.

`is_open` defaults to 0 and `chat_sessions::create` never sets it: every `/new`
leaves its predecessor behind and every system-agent pass mints a row, so the
opposite default would restore a bar full of conversations nobody opened. Only
the copilot writes the column, at the moment it opens the tab. A reset moves the
flag rather than copying it — `POST /api/sessions` now returns the new id and
`new_session` carries it, and the old row is closed as the new one opens, or the
source would restore twice and a later close would clear the stale row.

Closing a tab clears the flag and nothing else: the conversation is kept and
comes back with its history when the project is reopened.
This commit is contained in:
2026-08-04 21:50:10 +01:00
parent 01b8a187b5
commit 8f5c5382c8
10 changed files with 309 additions and 15 deletions
+10
View File
@@ -541,12 +541,20 @@ export class ChatSession extends InboxCardsMixin(LightElement) {
try {
const res = await fetch(`/api/sessions?source=${this._source}`, { method: 'POST' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { session_id } = await res.json();
if (session_id) this._onSessionReplaced(this._source, session_id);
} catch (e) {
this._pushError('Could not clear session: ' + e.message);
}
this._connectWS();
}
/**
* A reset replaced this source's session with a fresh one. Anything anchored to
* the session id (the copilot's tab bar) has to follow it across. No-op here.
*/
_onSessionReplaced(_source, _sessionId) {}
// ── Message handling ──────────────────────────────────────────────────────────
_handleServerMsg(msg) {
@@ -822,6 +830,8 @@ export class ChatSession extends InboxCardsMixin(LightElement) {
this._cancelStreamFlush();
this._messages = [];
this._waiting = false;
// A reset from another client of this source: follow the new session id.
if (msg.session_id) this._onSessionReplaced(this._source, msg.session_id);
break;
case 'client_selected':