Merge pull request #74245 from NousResearch/bb/pinned-messaging-index

Pinning a messaging session removes it from the sidebar
This commit is contained in:
brooklyn! 2026-07-29 12:41:16 -05:00 committed by GitHub
commit cfa43f520a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 88 additions and 18 deletions

View file

@ -130,6 +130,7 @@ import {
useRepoWorktreeMap
} from './projects'
import { SidebarBlankState, SidebarPinnedEmptyState, SidebarSessionSkeletons } from './section-states'
import { buildSessionByAnyId } from './session-index'
import { SidebarSessionsSection, VIRTUALIZE_THRESHOLD } from './sessions-section'
import { CONTEXT_SPLIT_KIT, SplitSubmenu } from './split-submenu'
@ -393,24 +394,12 @@ export function ChatSidebar({
const workingSessionIdSet = useMemo(() => new Set(workingSessionIds), [workingSessionIds])
// 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>()
// Cron sessions are listed separately but can still be pinned, so index
// them too — otherwise a pinned cron job can't resolve into the Pinned
// section. Recents take precedence on id collisions (set last).
for (const s of [...cronSessions, ...visibleSessions]) {
map.set(s.id, s)
if (s._lineage_root_id && !map.has(s._lineage_root_id)) {
map.set(s._lineage_root_id, s)
}
}
return map
}, [visibleSessions, cronSessions])
// Index sessions by every id a pin might be stored under — recents, cron,
// AND messaging, since all three can be pinned (see session-index.ts).
const sessionByAnyId = useMemo(
() => buildSessionByAnyId(visibleSessions, cronSessions, messagingSessions),
[visibleSessions, cronSessions, messagingSessions]
)
const pinnedSessions = useMemo(() => {
const seen = new Set<string>()

View file

@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest'
import type { SessionInfo } from '@/types/hermes'
import { buildSessionByAnyId } from './session-index'
const row = (id: string, extra: Partial<SessionInfo> = {}): SessionInfo =>
({ id, message_count: 1, source: 'cli', started_at: 0, title: id, ...extra }) as SessionInfo
describe('buildSessionByAnyId', () => {
it('resolves a pin from every slice the sidebar fetches', () => {
// The contract that matters: a pin is looked up in this map no matter
// which slice owns the row. Messaging is the one that regressed — a
// pinned session is filtered out of its own section, so a miss here
// removes it from the sidebar entirely rather than just misplacing it.
const index = buildSessionByAnyId([row('recent')], [row('cron_job_1')], [row('telegram_42')])
for (const id of ['recent', 'cron_job_1', 'telegram_42']) {
expect(index.get(id)?.id).toBe(id)
}
})
it('resolves a pin stored on the pre-compression lineage root', () => {
const index = buildSessionByAnyId([], [], [row('tip', { _lineage_root_id: 'root' })])
// Both identities point at the one live row.
expect(index.get('root')?.id).toBe('tip')
expect(index.get('tip')?.id).toBe('tip')
})
it('lets a recents row win a direct id collision', () => {
const index = buildSessionByAnyId(
[row('dupe', { title: 'from recents' })],
[],
[row('dupe', { title: 'from messaging' })]
)
expect(index.get('dupe')?.title).toBe('from recents')
})
it('does not let a lineage alias clobber a real row under that id', () => {
// 'root' is a live session in its own right AND another row's lineage
// root; the real row must win so the pin opens the right conversation.
const index = buildSessionByAnyId([row('root')], [], [row('tip', { _lineage_root_id: 'root' })])
expect(index.get('root')?.id).toBe('root')
})
})

View file

@ -0,0 +1,33 @@
import type { SessionInfo } from '@/types/hermes'
/**
* Index sessions by every id a pin might be stored under.
*
* The sidebar fetches three independent slices recents, cron, and messaging
* and renders the latter two in self-managed sections. Any of them can be
* pinned, so all three must be indexed here or the Pinned section can't
* resolve the pin to a row. A pinned session is also filtered out of its own
* section, so failing to index it doesn't merely misplace the row: it removes
* the session from the sidebar entirely.
*
* Each session is keyed under both its live id and its lineage root, so a pin
* stored before an auto-compression still resolves to the live continuation
* tip. Recents are indexed last and win a direct id collision.
*/
export function buildSessionByAnyId(
visibleSessions: SessionInfo[],
cronSessions: SessionInfo[],
messagingSessions: SessionInfo[]
): Map<string, SessionInfo> {
const map = new Map<string, SessionInfo>()
for (const session of [...cronSessions, ...messagingSessions, ...visibleSessions]) {
map.set(session.id, session)
if (session._lineage_root_id && !map.has(session._lineage_root_id)) {
map.set(session._lineage_root_id, session)
}
}
return map
}