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
+12
View File
@@ -776,12 +776,24 @@ pub async fn create_owner_tables(pool: &SqlitePool) -> Result<()> {
agent_id TEXT NOT NULL DEFAULT 'main',
is_interactive INTEGER NOT NULL DEFAULT 1,
is_ephemeral INTEGER NOT NULL DEFAULT 0,
is_open INTEGER NOT NULL DEFAULT 0,
run_context TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)",
)
.execute(pool)
.await?;
// Which conversations the copilot shows as tabs — persisted here rather than in
// the browser so the set follows the person (a shared laptop can't leak one
// member's tabs to another) and stays inside their encrypted file.
//
// The default is deliberately **0**, not 1: every `/new` leaves its previous
// session behind, and every system-agent pass creates one, so `DEFAULT 1` would
// turn every historical row on an existing box into a tab at the next login.
// For the same reason `chat_sessions::create` doesn't set it — it also serves
// cron, channels and system agents. Only the copilot writes this column, at the
// moment it opens the tab.
ensure_column(pool, "chat_sessions", "is_open", "INTEGER NOT NULL DEFAULT 0").await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS chat_sessions_stack (