feat(desktop): session hygiene, archive, media streaming + connecting overlay (#37099)
* feat(desktop): session hygiene, archive, media streaming + connecting overlay
Address a batch of desktop feedback:
- Stop leaking empty "Untitled" sessions: the TUI gateway pre-created a DB
row on every session.create (i.e. every launch/draft). Persist the row
lazily on first prompt instead, and hide message-less rows in the sidebar.
- Archive/hide sessions: new `archived` column + set_session_archived, web
API (`?archived=` + PATCH archived), Ctrl/⌘-click and a context-menu item
in the sidebar, and an "Archived Chats" settings panel to restore/delete.
- Videos load via a streaming `hermes-media://` protocol instead of capped,
in-memory data URLs (16 MB limit) — bypasses the cap and supports seeking.
- Background-process completions route to the session that launched them:
the completion event now carries session_key and each poller only consumes
its own.
- Sidebar: "Group by workspace" toggle is always visible; each workspace
group gets a "+" to start a session in that directory; "New agent"/"Agents"
relabeled to "New session"/"Sessions".
- New gateway connecting overlay (ascii decode → fade out) replacing the bare
skeleton/"starting gateway" state.
* fix(desktop): bail connecting overlay on boot error
The shownRef latch kept the connecting overlay mounted behind
BootFailureOverlay after a hard boot failure. Return null on boot.error
so the failure recovery surface fully owns the screen.
* fix(desktop): address Copilot review
- /api/sessions: validate `archived` (400 on unknown) and return `archived`
as a JSON boolean instead of SQLite's 0/1.
- PATCH /api/sessions/{id}: 400 (not a misleading 404) when the body has no
updatable fields; stop conflating a no-op with "not found".
- hermes-media protocol: drop `bypassCSP` — streaming only needs
secure/standard/stream/supportFetchAPI.
- Sidebar workspace header: split the toggle and the "+" into sibling buttons
so we no longer nest interactive elements inside a <button>.
* fix(desktop): address Copilot re-review
- hermes-media protocol: restrict streaming to an audio/video extension
allowlist (415 otherwise) so it can't be used to read arbitrary local files.
- Connecting overlay: use z-[1200] instead of the non-standard z-1200 utility.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -264,6 +264,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
handoff_platform TEXT,
|
||||
handoff_error TEXT,
|
||||
rewind_count INTEGER NOT NULL DEFAULT 0,
|
||||
archived INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (parent_session_id) REFERENCES sessions(id)
|
||||
);
|
||||
|
||||
@ -1430,6 +1431,22 @@ class SessionDB:
|
||||
row = cursor.fetchone()
|
||||
return row["title"] if row else None
|
||||
|
||||
def set_session_archived(self, session_id: str, archived: bool) -> bool:
|
||||
"""Archive or unarchive a session.
|
||||
|
||||
Archived sessions are hidden from the default session list but keep all
|
||||
their messages — this is a soft hide, not a delete. Returns True when a
|
||||
row was updated.
|
||||
"""
|
||||
def _do(conn):
|
||||
cursor = conn.execute(
|
||||
"UPDATE sessions SET archived = ? WHERE id = ?",
|
||||
(1 if archived else 0, session_id),
|
||||
)
|
||||
return cursor.rowcount
|
||||
rowcount = self._execute_write(_do)
|
||||
return rowcount > 0
|
||||
|
||||
def get_session_by_title(self, title: str) -> Optional[Dict[str, Any]]:
|
||||
"""Look up a session by exact title. Returns session dict or None."""
|
||||
with self._lock:
|
||||
@ -1549,6 +1566,8 @@ class SessionDB:
|
||||
min_message_count: int = 0,
|
||||
project_compression_tips: bool = True,
|
||||
order_by_last_active: bool = False,
|
||||
include_archived: bool = False,
|
||||
archived_only: bool = False,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""List sessions with preview (first user message) and last active timestamp.
|
||||
|
||||
@ -1604,6 +1623,10 @@ class SessionDB:
|
||||
if min_message_count > 0:
|
||||
where_clauses.append("s.message_count >= ?")
|
||||
params.append(min_message_count)
|
||||
if archived_only:
|
||||
where_clauses.append("s.archived = 1")
|
||||
elif not include_archived:
|
||||
where_clauses.append("s.archived = 0")
|
||||
|
||||
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
||||
if order_by_last_active:
|
||||
@ -3027,7 +3050,13 @@ class SessionDB:
|
||||
# Utility
|
||||
# =========================================================================
|
||||
|
||||
def session_count(self, source: str = None, min_message_count: int = 0) -> int:
|
||||
def session_count(
|
||||
self,
|
||||
source: str = None,
|
||||
min_message_count: int = 0,
|
||||
include_archived: bool = False,
|
||||
archived_only: bool = False,
|
||||
) -> int:
|
||||
"""Count sessions, optionally filtered by source."""
|
||||
where_clauses = []
|
||||
params = []
|
||||
@ -3038,6 +3067,10 @@ class SessionDB:
|
||||
if min_message_count > 0:
|
||||
where_clauses.append("message_count >= ?")
|
||||
params.append(min_message_count)
|
||||
if archived_only:
|
||||
where_clauses.append("archived = 1")
|
||||
elif not include_archived:
|
||||
where_clauses.append("archived = 0")
|
||||
|
||||
where_sql = f" WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user