mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): hide sidebar fake sessions for Cursor resume/archive (#836)
* fix(web): dedupe sidebar sessions by flavor resume id Wire deduplicateSessionsByAgentId into SessionList and resolve cursor threads via cursorSessionId so resume/archive no longer shows duplicate inactive rows for the same ACP session. Fixes #833 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): use SessionSummary.agentSessionId for sidebar dedup SessionList only receives SessionSummary from the API; native ids like cursorSessionId are already mapped into metadata.agentSessionId by toSessionSummary. Drop resolveAgentSessionIdFromMetadata to fix typecheck. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): hide inactive empty session stubs in sidebar Filter inactive rows with no agentSessionId and no title signal before grouping sessions, and expose lifecycleState on SessionSummary for future sidebar rules. Completes the #833 P0 follow-up alongside agent-id dedup. Fixes #833 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): scope sidebar dedup key by flavor Prevent cross-flavor collisions when flattened agentSessionId retains a stale native id. Add regression test and relax claudeRemote CI timeout. Fixes #833 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SessionSummary } from '@/types/api'
|
||||
import { deduplicateSessionsByAgentId, expandSelectedSessionCollapseOverrides, getVisibleSessionPreview, normalizeSearch, sessionMatchesQuery } from './SessionList'
|
||||
import {
|
||||
deduplicateSessionsByAgentId,
|
||||
expandSelectedSessionCollapseOverrides,
|
||||
getSessionDedupKey,
|
||||
getVisibleSessionPreview,
|
||||
isSidebarEmptySessionStub,
|
||||
normalizeSearch,
|
||||
prepareSidebarSessions,
|
||||
sessionMatchesQuery,
|
||||
shouldShowSessionInSidebar
|
||||
} from './SessionList'
|
||||
|
||||
function makeSession(overrides: Partial<SessionSummary> & { id: string }): SessionSummary {
|
||||
return {
|
||||
@@ -71,6 +81,25 @@ describe('deduplicateSessionsByAgentId', () => {
|
||||
expect(result).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('deduplicates cursor sessions by summary agentSessionId', () => {
|
||||
const sessions = [
|
||||
makeSession({
|
||||
id: 'a',
|
||||
active: true,
|
||||
metadata: { path: '/p', flavor: 'cursor', agentSessionId: 'acp-thread-1' },
|
||||
updatedAt: 100
|
||||
}),
|
||||
makeSession({
|
||||
id: 'b',
|
||||
metadata: { path: '/p', flavor: 'cursor', agentSessionId: 'acp-thread-1' },
|
||||
updatedAt: 200
|
||||
})
|
||||
]
|
||||
const result = deduplicateSessionsByAgentId(sessions)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe('a')
|
||||
})
|
||||
|
||||
it('deduplicates independently across different agentSessionIds', () => {
|
||||
const sessions = [
|
||||
makeSession({ id: 'a', metadata: { path: '/p', agentSessionId: 'thread-1' }, updatedAt: 100 }),
|
||||
@@ -82,9 +111,116 @@ describe('deduplicateSessionsByAgentId', () => {
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map(s => s.id).sort()).toEqual(['b', 'd'])
|
||||
})
|
||||
|
||||
it('does not dedupe across flavors sharing the same flattened agentSessionId', () => {
|
||||
const sessions = [
|
||||
makeSession({
|
||||
id: 'codex',
|
||||
metadata: { path: '/p', flavor: 'codex', agentSessionId: 'stale-shared-id' },
|
||||
updatedAt: 100
|
||||
}),
|
||||
makeSession({
|
||||
id: 'cursor',
|
||||
metadata: { path: '/p', flavor: 'cursor', agentSessionId: 'stale-shared-id' },
|
||||
updatedAt: 200
|
||||
})
|
||||
]
|
||||
|
||||
expect(getSessionDedupKey(sessions[0])).toBe('codex:stale-shared-id')
|
||||
expect(getSessionDedupKey(sessions[1])).toBe('cursor:stale-shared-id')
|
||||
expect(deduplicateSessionsByAgentId(sessions).map(session => session.id).sort()).toEqual(['codex', 'cursor'])
|
||||
expect(prepareSidebarSessions(sessions).map(session => session.id).sort()).toEqual(['codex', 'cursor'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('isSidebarEmptySessionStub', () => {
|
||||
it('treats inactive sessions without agent id or title as stubs', () => {
|
||||
expect(isSidebarEmptySessionStub(makeSession({
|
||||
id: 'stub',
|
||||
metadata: { path: '/work/hapi' }
|
||||
}))).toBe(true)
|
||||
})
|
||||
|
||||
it('does not treat active sessions as stubs', () => {
|
||||
expect(isSidebarEmptySessionStub(makeSession({
|
||||
id: 'live',
|
||||
active: true,
|
||||
metadata: { path: '/work/hapi' }
|
||||
}))).toBe(false)
|
||||
})
|
||||
|
||||
it('does not treat sessions with agentSessionId as stubs', () => {
|
||||
expect(isSidebarEmptySessionStub(makeSession({
|
||||
id: 'resume',
|
||||
metadata: { path: '/work/hapi', agentSessionId: 'thread-1' }
|
||||
}))).toBe(false)
|
||||
})
|
||||
|
||||
it('does not treat sessions with summary text as stubs', () => {
|
||||
expect(isSidebarEmptySessionStub(makeSession({
|
||||
id: 'titled',
|
||||
metadata: { path: '/work/hapi', summary: { text: 'Fix sidebar' } }
|
||||
}))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepareSidebarSessions', () => {
|
||||
it('hides inactive empty stubs but keeps real sessions', () => {
|
||||
const sessions = [
|
||||
makeSession({ id: 'stub', metadata: { path: '/work/hapi' } }),
|
||||
makeSession({
|
||||
id: 'real',
|
||||
metadata: { path: '/work/hapi', agentSessionId: 'thread-1', summary: { text: 'Real chat' } }
|
||||
})
|
||||
]
|
||||
|
||||
const result = prepareSidebarSessions(sessions)
|
||||
expect(result.map(session => session.id)).toEqual(['real'])
|
||||
})
|
||||
|
||||
it('keeps the selected inactive stub visible', () => {
|
||||
const sessions = [
|
||||
makeSession({ id: 'stub', metadata: { path: '/work/hapi' } }),
|
||||
makeSession({
|
||||
id: 'real',
|
||||
metadata: { path: '/work/hapi', agentSessionId: 'thread-1' }
|
||||
})
|
||||
]
|
||||
|
||||
const result = prepareSidebarSessions(sessions, 'stub')
|
||||
expect(result.map(session => session.id).sort()).toEqual(['real', 'stub'])
|
||||
})
|
||||
|
||||
it('deduplicates before filtering stubs', () => {
|
||||
const sessions = [
|
||||
makeSession({ id: 'stub', metadata: { path: '/work/hapi' } }),
|
||||
makeSession({
|
||||
id: 'older',
|
||||
metadata: { path: '/work/hapi', agentSessionId: 'thread-1' },
|
||||
updatedAt: 100
|
||||
}),
|
||||
makeSession({
|
||||
id: 'newer',
|
||||
metadata: { path: '/work/hapi', agentSessionId: 'thread-1' },
|
||||
updatedAt: 200
|
||||
})
|
||||
]
|
||||
|
||||
const result = prepareSidebarSessions(sessions)
|
||||
expect(result.map(session => session.id)).toEqual(['newer'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldShowSessionInSidebar', () => {
|
||||
it('always shows active and selected sessions', () => {
|
||||
const stub = makeSession({ id: 'stub', metadata: { path: '/work/hapi' } })
|
||||
expect(shouldShowSessionInSidebar(stub)).toBe(false)
|
||||
expect(shouldShowSessionInSidebar(stub, 'stub')).toBe(true)
|
||||
expect(shouldShowSessionInSidebar({ ...stub, active: true })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('session list search helpers', () => {
|
||||
it('normalizes whitespace and case before filtering', () => {
|
||||
const session = makeSession({
|
||||
|
||||
Reference in New Issue
Block a user