fix(desktop): keep pinned + recent sessions visible across compression
Long-running sessions auto-compress: the gateway ends the original session and surfaces the live continuation under a new id (list_sessions_rich projects the root forward to its tip). Two symptoms fell out of the id rotation: - A pinned session "vanished" — the pin is stored as the pre-compression root id, but the sidebar only matched on the live id, so it was filtered out. Pins now resolve on the durable lineage-root id (`_lineage_root_id`, already surfaced by the projection): the sidebar indexes sessions by both ids, pin/ unpin and reorder operate on the durable id, and `sessionPinId()` is shared with the Cmd+P toggle. Existing pins keep working with no migration. - A freshly-continued session was missing from the list until you ungrouped + "load 50 more" — the list paginated by original start time, so an old-but- active conversation sat past the first page. The desktop now requests `order=recent` (GET /api/sessions gains an `order` param backed by the existing recency CTE), surfacing live continuations on the first page.
This commit is contained in:
@@ -54,7 +54,8 @@ import {
|
||||
$sessions,
|
||||
$sessionsLoading,
|
||||
$sessionsTotal,
|
||||
$workingSessionIds
|
||||
$workingSessionIds,
|
||||
sessionPinId
|
||||
} from '@/store/session'
|
||||
|
||||
import { type AppView, ARTIFACTS_ROUTE, MESSAGING_ROUTE, SKILLS_ROUTE } from '../../routes'
|
||||
@@ -73,7 +74,12 @@ const SIDEBAR_NAV: SidebarNavItem[] = [
|
||||
icon: props => <Codicon name="robot" {...props} />,
|
||||
action: 'new-session'
|
||||
},
|
||||
{ id: 'skills', label: 'Skills & Tools', icon: props => <Codicon name="symbol-misc" {...props} />, route: SKILLS_ROUTE },
|
||||
{
|
||||
id: 'skills',
|
||||
label: 'Skills & Tools',
|
||||
icon: props => <Codicon name="symbol-misc" {...props} />,
|
||||
route: SKILLS_ROUTE
|
||||
},
|
||||
{ id: 'messaging', label: 'Messaging', icon: props => <Codicon name="comment" {...props} />, route: MESSAGING_ROUTE },
|
||||
{ id: 'artifacts', label: 'Artifacts', icon: props => <Codicon name="files" {...props} />, route: ARTIFACTS_ROUTE }
|
||||
]
|
||||
@@ -189,24 +195,45 @@ export function ChatSidebar({
|
||||
|
||||
const sortedSessions = useMemo(() => [...sessions].sort((a, b) => sessionTime(b) - sessionTime(a)), [sessions])
|
||||
|
||||
const sessionsById = useMemo(() => new Map(sessions.map(s => [s.id, s])), [sessions])
|
||||
const workingSessionIdSet = useMemo(() => new Set(workingSessionIds), [workingSessionIds])
|
||||
|
||||
const visiblePinnedIds = useMemo(
|
||||
() => pinnedSessionIds.filter(id => sessionsById.has(id)),
|
||||
[pinnedSessionIds, sessionsById]
|
||||
)
|
||||
// Index sessions by both their live id and their lineage-root id so a pin
|
||||
// stored as the pre-compression root resolves to the live continuation tip.
|
||||
const sessionByAnyId = useMemo(() => {
|
||||
const map = new Map<string, SessionInfo>()
|
||||
|
||||
const visiblePinnedIdSet = useMemo(() => new Set(visiblePinnedIds), [visiblePinnedIds])
|
||||
for (const s of sessions) {
|
||||
map.set(s.id, s)
|
||||
|
||||
const pinnedSessions = useMemo(
|
||||
() => visiblePinnedIds.map(id => sessionsById.get(id)!).filter(Boolean),
|
||||
[visiblePinnedIds, sessionsById]
|
||||
)
|
||||
if (s._lineage_root_id && !map.has(s._lineage_root_id)) {
|
||||
map.set(s._lineage_root_id, s)
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}, [sessions])
|
||||
|
||||
const pinnedSessions = useMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
const out: SessionInfo[] = []
|
||||
|
||||
for (const pinId of pinnedSessionIds) {
|
||||
const session = sessionByAnyId.get(pinId)
|
||||
|
||||
if (session && !seen.has(session.id)) {
|
||||
seen.add(session.id)
|
||||
out.push(session)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}, [pinnedSessionIds, sessionByAnyId])
|
||||
|
||||
const pinnedRealIdSet = useMemo(() => new Set(pinnedSessions.map(s => s.id)), [pinnedSessions])
|
||||
|
||||
const unpinnedAgentSessions = useMemo(
|
||||
() => sortedSessions.filter(s => !visiblePinnedIdSet.has(s.id)),
|
||||
[sortedSessions, visiblePinnedIdSet]
|
||||
() => sortedSessions.filter(s => !pinnedRealIdSet.has(s.id)),
|
||||
[sortedSessions, pinnedRealIdSet]
|
||||
)
|
||||
|
||||
const agentSessions = useMemo(
|
||||
@@ -236,7 +263,10 @@ export function ChatSidebar({
|
||||
return
|
||||
}
|
||||
|
||||
reorderPinnedSession(String(active.id), newIndex)
|
||||
// Sortable ids are live session ids; the pinned store is keyed by durable
|
||||
// (lineage-root) ids, so translate before reordering.
|
||||
const dragged = sessionByAnyId.get(String(active.id))
|
||||
reorderPinnedSession(dragged ? sessionPinId(dragged) : String(active.id), newIndex)
|
||||
}
|
||||
|
||||
const handleAgentDragEnd = ({ active, over }: DragEndEvent) => {
|
||||
@@ -536,7 +566,7 @@ function SidebarSessionsSection({
|
||||
isWorking: workingSessionIdSet.has(session.id),
|
||||
onArchive: () => onArchiveSession(session.id),
|
||||
onDelete: () => onDeleteSession(session.id),
|
||||
onPin: () => onTogglePin(session.id),
|
||||
onPin: () => onTogglePin(sessionPinId(session)),
|
||||
onResume: () => onResumeSession(session.id),
|
||||
session
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type FC, useCallback, useMemo, useRef } from 'react'
|
||||
|
||||
import type { SessionInfo } from '@/hermes'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { sessionPinId } from '@/store/session'
|
||||
|
||||
import { SidebarSessionRow } from './session-row'
|
||||
|
||||
@@ -77,7 +78,7 @@ export const VirtualSessionList: FC<VirtualSessionListProps> = ({
|
||||
isWorking: workingSessionIdSet.has(session.id),
|
||||
onArchive: () => onArchiveSession(session.id),
|
||||
onDelete: () => onDeleteSession(session.id),
|
||||
onPin: () => onTogglePin(session.id),
|
||||
onPin: () => onTogglePin(sessionPinId(session)),
|
||||
onResume: () => onResumeSession(session.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
$freshDraftReady,
|
||||
$gatewayState,
|
||||
$selectedStoredSessionId,
|
||||
$sessions,
|
||||
sessionPinId,
|
||||
setAwaitingResponse,
|
||||
setBusy,
|
||||
setCurrentBranch,
|
||||
@@ -224,10 +226,14 @@ export function DesktopController() {
|
||||
return
|
||||
}
|
||||
|
||||
if ($pinnedSessionIds.get().includes(sessionId)) {
|
||||
unpinSession(sessionId)
|
||||
// Pin on the durable lineage-root id so the pin survives auto-compression.
|
||||
const session = $sessions.get().find(s => s.id === sessionId || s._lineage_root_id === sessionId)
|
||||
const pinId = session ? sessionPinId(session) : sessionId
|
||||
|
||||
if ($pinnedSessionIds.get().includes(pinId)) {
|
||||
unpinSession(pinId)
|
||||
} else {
|
||||
pinSession(sessionId)
|
||||
pinSession(pinId)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user