mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Merge pull request #74245 from NousResearch/bb/pinned-messaging-index
Pinning a messaging session removes it from the sidebar
This commit is contained in:
commit
cfa43f520a
3 changed files with 88 additions and 18 deletions
|
|
@ -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>()
|
||||
|
|
|
|||
48
apps/desktop/src/app/chat/sidebar/session-index.test.ts
Normal file
48
apps/desktop/src/app/chat/sidebar/session-index.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
33
apps/desktop/src/app/chat/sidebar/session-index.ts
Normal file
33
apps/desktop/src/app/chat/sidebar/session-index.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue