feat(api): session lists carry the pin flag and never drop a pinned row

The three list endpoints (`/api/sessions`, `/api/profiles/sessions`, and
the batched sidebar route) now request the pinned back-fill and expose
`pinned` as a real JSON boolean alongside `archived`.

Both merge paths re-window rows after sorting, which would have thrown
away exactly what the back-fill fetched, so pinned rows survive the cap.
The sidebar's "load more" signal discounts them — they arrive past the
LIMIT by design and would otherwise fake a full page on a short list.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
Co-authored-by: eason2026 <209090628+eason2026@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-07-29 12:00:31 -05:00
parent 1b317d23fb
commit 3290807e60

View file

@ -4974,6 +4974,7 @@ def get_sessions(
# rows, skip the system_prompt blob inside SQLite too (pairs
# with the API-level _strip_session_list_rows below).
compact_rows=not full,
include_pinned=True,
)
total = db.session_count(
source=source or None,
@ -4996,6 +4997,7 @@ def get_sessions(
s["is_default_profile"] = profile_name == "default"
# SQLite stores the flag as 0/1; expose a real JSON boolean.
s["archived"] = bool(s.get("archived"))
s["pinned"] = bool(s.get("pinned"))
if not full:
_strip_session_list_rows(sessions)
return {"sessions": sessions, "total": total, "limit": limit, "offset": offset}
@ -5098,6 +5100,7 @@ def get_profiles_sessions(
order_by_last_active=order == "recent",
# Same SQL-level blob skip as /api/sessions (see above).
compact_rows=not full,
include_pinned=True,
)
profile_total = db.session_count(
source=source_filter,
@ -5118,6 +5121,7 @@ def get_profiles_sessions(
and (now - s.get("last_active", s.get("started_at", 0))) < 300
)
s["archived"] = bool(s.get("archived"))
s["pinned"] = bool(s.get("pinned"))
merged.append(s)
except Exception as exc:
errors.append({"profile": name, "error": str(exc)})
@ -5126,7 +5130,12 @@ def get_profiles_sessions(
sort_key = "last_active" if order == "recent" else "started_at"
merged.sort(key=lambda s: s.get(sort_key) or s.get("started_at") or 0, reverse=True)
# Pinned rows are back-filled past each profile's LIMIT on purpose; keep
# them in the merged window instead of re-dropping them on recency.
window = merged[offset:offset + limit]
if len(merged) > offset + limit:
seen = {id(s) for s in window}
window.extend(s for s in merged[offset + limit:] if s.get("pinned") and id(s) not in seen)
if not full:
_strip_session_list_rows(window)
return {
@ -5203,6 +5212,9 @@ def get_profiles_sessions_sidebar(
and (now - s.get("last_active", s.get("started_at", 0))) < 300
)
s["archived"] = bool(s.get("archived"))
# SQLite stores the pin as 0/1; the sidebar needs a real boolean to
# render the Pinned section from server state.
s["pinned"] = bool(s.get("pinned"))
return rows
def _slice(db, *, source=None, exclude=None, cap):
@ -5216,6 +5228,9 @@ def get_profiles_sessions_sidebar(
archived_only=False,
order_by_last_active=True,
compact_rows=True,
# A pinned conversation must reach the sidebar even when it has
# aged past the window — otherwise its Pinned row renders empty.
include_pinned=True,
)
for name, home in targets:
@ -5233,8 +5248,10 @@ def get_profiles_sessions_sidebar(
# A full window means more rows remain on disk. That is all the
# sidebar's "load more" needs, and unlike an exact COUNT(*) per
# profile per refresh it costs nothing beyond the rows already
# read.
recents_truncated[name] = len(profile_rows) >= recents_cap
# read. Discount pinned back-fills — they arrive past the LIMIT
# and would otherwise fake a full page on a short list.
unpinned_count = sum(1 for s in profile_rows if not s.get("pinned"))
recents_truncated[name] = unpinned_count >= recents_cap
recents_rows.extend(_tag(profile_rows, name))
cron_rows.extend(_tag(_slice(db, source="cron", cap=cron_cap), name))
messaging_rows.extend(
@ -5247,7 +5264,13 @@ def get_profiles_sessions_sidebar(
def _window(rows: List[Dict[str, Any]], cap: int) -> List[Dict[str, Any]]:
rows.sort(key=lambda s: s.get("last_active") or s.get("started_at") or 0, reverse=True)
# Pinned rows survive the cap. The per-profile queries deliberately
# back-fill them past the LIMIT, so truncating the merged window on
# recency alone would throw away exactly what the back-fill fetched.
win = rows[:cap]
if len(rows) > cap:
seen = {id(s) for s in win}
win.extend(s for s in rows[cap:] if s.get("pinned") and id(s) not in seen)
_strip_session_list_rows(win)
return win