mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
* feat(web): searchable session picker on share target (#980) Add search to the Android share-target picker so operators can attach shared content to inactive or older sessions, not just recent actives. Reuses sidebar search helpers; caps default active list with a search hint. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): defer share pending consume until session is active ShareSeedConsumer was consuming the sessionStorage transfer id on first mount even when the target session was inactive. Reopening into a new session id remounted the chat with the key already gone, dropping the shared payload. Consume and seed only after sessionActive is true. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): bind share pending transfer to target session id Deferring consume until active left a global pending slot that the next unrelated active SessionChat could claim. Bind the pending transfer to the picked session id, retarget on reopen/spawn id-swap, and only consume when the mounting session matches. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): wire SessionListSearch date-range props on share picker Upstream added required customStart/customEnd/onDateRangeChange to SessionListSearch; share /share must pass them and honor the range when filtering sessions after rebase onto v0.23.1. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): pass sessionActivityDates to share SessionListSearch Upstream SessionListSearch now requires activity dates for the date picker; without them CI typecheck fails and the PR chip stays needs_work. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): use cached machine labels in share picker search Share picker now uses useMachineLabels like SessionList so stale machine ids still match search by display name. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): align share picker with sidebar session prep and preview limit Snapshot via prepareSidebarSessions and pass useSessionPreviewLimit so search targets and fold cap match the main session list. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): wire SessionListSearch expanded props on share picker Upstream search now requires expanded/onExpandedChange; default open on the share picker so the field is available immediately. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): unblock typecheck after SessionSummary and RawSendError fields Fill new required SessionSummary watermarks in the share picker test helper, and set deliveryMode on abort-restore send errors. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): mock machines hooks in share page missing-state test SharePage now calls useMachines for picker search labels; the missing- share unit test needs those hooks stubbed without a QueryClient. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): retarget pending share on automatic session supersession When SessionDetailRoute follows supersededBySessionId A→B, rewrite the share pending target so ShareSeedConsumer on B can still claim it. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
109 lines
4.1 KiB
TypeScript
109 lines
4.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { SessionSummary } from '@/types/api'
|
|
import { DEFAULT_SESSION_PREVIEW_LIMIT } from '@/hooks/useSessionPreviewLimit'
|
|
import {
|
|
countHiddenActiveSharePickerSessions,
|
|
filterSharePickerSessions,
|
|
} from './sharePickerSessions'
|
|
|
|
function makeSession(overrides: Partial<SessionSummary> & { id: string }): SessionSummary {
|
|
return {
|
|
active: false,
|
|
thinking: false,
|
|
activeAt: 0,
|
|
updatedAt: 0,
|
|
metadata: null,
|
|
metadataVersion: 0,
|
|
agentStateVersion: 0,
|
|
todosUpdatedAt: 0,
|
|
todoProgress: null,
|
|
pendingRequestsCount: 0,
|
|
pendingRequestKinds: [],
|
|
pendingRequests: [],
|
|
backgroundTaskCount: 0,
|
|
futureScheduledMessageCount: 0,
|
|
nextScheduledAt: null,
|
|
model: null,
|
|
effort: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
const machineLabel = () => 'desktop'
|
|
|
|
describe('filterSharePickerSessions', () => {
|
|
it('returns active sessions sorted by updatedAt when query is empty', () => {
|
|
const sessions = [
|
|
makeSession({ id: 'old-active', active: true, updatedAt: 100 }),
|
|
makeSession({ id: 'inactive-recent', active: false, updatedAt: 300 }),
|
|
makeSession({ id: 'new-active', active: true, updatedAt: 200 }),
|
|
]
|
|
const result = filterSharePickerSessions(sessions, '', machineLabel)
|
|
expect(result.map((s) => s.id)).toEqual(['new-active', 'old-active'])
|
|
})
|
|
|
|
it('caps active sessions when query is empty', () => {
|
|
const previewLimit = DEFAULT_SESSION_PREVIEW_LIMIT
|
|
const sessions = Array.from({ length: previewLimit + 3 }, (_, index) =>
|
|
makeSession({ id: `s-${index}`, active: true, updatedAt: index }))
|
|
const result = filterSharePickerSessions(sessions, '', machineLabel, null, previewLimit)
|
|
expect(result).toHaveLength(previewLimit)
|
|
expect(result[0].id).toBe(`s-${previewLimit + 2}`)
|
|
})
|
|
|
|
it('searches all sessions including inactive when query is non-empty', () => {
|
|
const sessions = [
|
|
makeSession({
|
|
id: 'inactive-match',
|
|
active: false,
|
|
updatedAt: 50,
|
|
metadata: { path: '/proj/archive', summary: { text: 'Old bugfix' } },
|
|
}),
|
|
makeSession({ id: 'active-no-match', active: true, updatedAt: 200, metadata: { path: '/other' } }),
|
|
]
|
|
const result = filterSharePickerSessions(sessions, 'archive', machineLabel)
|
|
expect(result.map((s) => s.id)).toEqual(['inactive-match'])
|
|
})
|
|
|
|
it('matches machine label in search mode', () => {
|
|
const sessions = [
|
|
makeSession({
|
|
id: 'remote',
|
|
active: false,
|
|
updatedAt: 100,
|
|
metadata: { path: '/proj', machineId: 'machine-abc' },
|
|
}),
|
|
]
|
|
const result = filterSharePickerSessions(
|
|
sessions,
|
|
'laptop',
|
|
(machineId) => (machineId === 'machine-abc' ? 'dev-laptop' : 'unknown'),
|
|
)
|
|
expect(result.map((s) => s.id)).toEqual(['remote'])
|
|
})
|
|
})
|
|
|
|
describe('countHiddenActiveSharePickerSessions', () => {
|
|
it('returns zero when active count is within cap', () => {
|
|
const sessions = [
|
|
makeSession({ id: 'a', active: true }),
|
|
makeSession({ id: 'b', active: false }),
|
|
]
|
|
expect(countHiddenActiveSharePickerSessions(sessions)).toBe(0)
|
|
})
|
|
|
|
it('counts active sessions beyond the cap', () => {
|
|
const previewLimit = DEFAULT_SESSION_PREVIEW_LIMIT
|
|
const sessions = Array.from({ length: previewLimit + 2 }, (_, index) =>
|
|
makeSession({ id: `s-${index}`, active: true }))
|
|
expect(countHiddenActiveSharePickerSessions(sessions, previewLimit)).toBe(2)
|
|
})
|
|
|
|
it('honors a custom preview limit', () => {
|
|
const sessions = Array.from({ length: 5 }, (_, index) =>
|
|
makeSession({ id: `s-${index}`, active: true, updatedAt: index }))
|
|
expect(filterSharePickerSessions(sessions, '', machineLabel, null, 3)).toHaveLength(3)
|
|
expect(countHiddenActiveSharePickerSessions(sessions, 3)).toBe(2)
|
|
})
|
|
})
|