mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
* feat(web): opt-in needs-attention session list filter Add a default-off sidebar filter (and Settings → Display toggle) that keeps permission/input/unread rows while excluding background-only busy work, mirroring the active-only preference pattern from #903. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): make needs-attention filter a transient sidebar toggle Drop the Settings → Display preference and localStorage persistence. This is an inbox lens: toggle in the session list, rows fall away as they're seen, and a reload clears the filter. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): unread-only sidebar lens, not needs-attention Rename the transient toggle to Unread only and filter solely on updatedAt > lastSeenAt. Permission/input attention stays on row dots; this is not Overseer review inbox. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): use unread-dot shape for unread filter toggle Match the session-row unread indicator instead of an inbox tray so Overseer can own inbox chrome later without competing metaphors. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): apply unread filter after machine scope Keep machineFilters derived from the unfiltered sidebar set so a persisted machine selection is not cleared when that machine has no unread rows. Unread intersects with the selected machine instead. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): snapshot last-seen once for unread filter pass Avoid N localStorage read/parse calls while the unread lens is on by loading the store once per memoized filter pass. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { describe, expect, it, beforeEach, vi } from 'vitest'
|
|
import {
|
|
getSessionLastSeenAt,
|
|
getSessionLastSeenSnapshot,
|
|
initializeSessionLastSeen,
|
|
markSessionSeen,
|
|
} from './sessionLastSeen'
|
|
|
|
describe('sessionLastSeen', () => {
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
})
|
|
|
|
it('stores the latest seen timestamp for a session', () => {
|
|
markSessionSeen('session-a', 1000)
|
|
markSessionSeen('session-a', 2500)
|
|
expect(getSessionLastSeenAt('session-a')).toBe(2500)
|
|
})
|
|
|
|
it('snapshots the store once for bulk unread filtering', () => {
|
|
markSessionSeen('session-a', 1000)
|
|
markSessionSeen('session-b', 2500)
|
|
expect(getSessionLastSeenSnapshot()).toEqual({
|
|
'session-a': 1000,
|
|
'session-b': 2500,
|
|
})
|
|
})
|
|
|
|
it('does not move the watermark backwards', () => {
|
|
markSessionSeen('session-a', 5000)
|
|
markSessionSeen('session-a', 2000)
|
|
expect(getSessionLastSeenAt('session-a')).toBe(5000)
|
|
})
|
|
|
|
it('uses the first session list as the unread baseline', () => {
|
|
initializeSessionLastSeen('hub-a', [
|
|
{ id: 'session-a', updatedAt: 1000 },
|
|
{ id: 'session-b', updatedAt: 2500 },
|
|
])
|
|
|
|
expect(getSessionLastSeenAt('session-a')).toBe(1000)
|
|
expect(getSessionLastSeenAt('session-b')).toBe(2500)
|
|
})
|
|
|
|
it('preserves existing watermarks while completing a legacy partial baseline', () => {
|
|
markSessionSeen('session-a', 1000)
|
|
|
|
initializeSessionLastSeen('hub-a', [
|
|
{ id: 'session-a', updatedAt: 2500 },
|
|
{ id: 'session-b', updatedAt: 2500 },
|
|
])
|
|
|
|
expect(getSessionLastSeenAt('session-a')).toBe(1000)
|
|
expect(getSessionLastSeenAt('session-b')).toBe(2500)
|
|
|
|
initializeSessionLastSeen('hub-a', [{ id: 'session-c', updatedAt: 3000 }])
|
|
expect(getSessionLastSeenAt('session-c')).toBe(0)
|
|
})
|
|
|
|
it('initializes each hub independently', () => {
|
|
initializeSessionLastSeen('hub-a', [{ id: 'session-a', updatedAt: 1000 }])
|
|
initializeSessionLastSeen('hub-b', [{ id: 'session-b', updatedAt: 2000 }])
|
|
|
|
expect(getSessionLastSeenAt('session-b')).toBe(2000)
|
|
})
|
|
|
|
it('ignores localStorage write failures', () => {
|
|
const setItem = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw new Error('quota exceeded')
|
|
})
|
|
|
|
expect(() => markSessionSeen('session-a', 1000)).not.toThrow()
|
|
|
|
setItem.mockRestore()
|
|
})
|
|
|
|
it('returns zero when localStorage getter throws', () => {
|
|
const localStorageDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage')
|
|
Object.defineProperty(window, 'localStorage', {
|
|
configurable: true,
|
|
get() {
|
|
throw new Error('storage denied')
|
|
},
|
|
})
|
|
|
|
expect(getSessionLastSeenAt('session-a')).toBe(0)
|
|
expect(() => markSessionSeen('session-a', 1000)).not.toThrow()
|
|
|
|
if (localStorageDescriptor) {
|
|
Object.defineProperty(window, 'localStorage', localStorageDescriptor)
|
|
}
|
|
})
|
|
})
|