Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions web/src/components/SessionList.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { SessionSummary } from '@/types/api'
import { deduplicateSessionsByAgentId, getVisibleSessionPreview, normalizeSearch, sessionMatchesQuery } from './SessionList'
import { deduplicateSessionsByAgentId, expandSelectedSessionCollapseOverrides, getVisibleSessionPreview, normalizeSearch, sessionMatchesQuery } from './SessionList'

function makeSession(overrides: Partial<SessionSummary> & { id: string }): SessionSummary {
return {
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('session list search helpers', () => {
})

describe('getVisibleSessionPreview', () => {
it('keeps selected and active sessions inside the collapsed preview', () => {
it('does not promote the selected session in collapsed previews', () => {
const sessions = Array.from({ length: 6 }, (_, index) => makeSession({
id: `s-${index + 1}`,
active: index === 4,
Expand All @@ -111,11 +111,10 @@ describe('getVisibleSessionPreview', () => {
}))

const preview = getVisibleSessionPreview(sessions, {
selectedSessionId: 's-6',
limit: 3
})

expect(preview.map(session => session.id)).toEqual(['s-6', 's-5', 's-1'])
expect(preview.map(session => session.id)).toEqual(['s-5', 's-1', 's-2'])
})

it('returns all sessions when expanded', () => {
Expand All @@ -127,3 +126,34 @@ describe('getVisibleSessionPreview', () => {
expect(getVisibleSessionPreview(sessions, { expanded: true, limit: 2 })).toHaveLength(4)
})
})


describe('expandSelectedSessionCollapseOverrides', () => {
it('expands collapsed project, machine, and session preview overrides for selected sessions', () => {
const overrides = new Map<string, boolean>([
['machine-1::/work/hapi', true],
['sessions::machine-1::/work/hapi', true],
['machine::machine-1', true]
])

const result = expandSelectedSessionCollapseOverrides(overrides, {
key: 'machine-1::/work/hapi',
machineId: 'machine-1'
})

expect(result.has('machine-1::/work/hapi')).toBe(false)
expect(result.get('sessions::machine-1::/work/hapi')).toBe(false)
expect(result.has('machine::machine-1')).toBe(false)
})

it('sets missing session preview override to expanded', () => {
const overrides = new Map<string, boolean>()

const result = expandSelectedSessionCollapseOverrides(overrides, {
key: 'machine-1::/work/hapi',
machineId: 'machine-1'
})

expect(result.get('sessions::machine-1::/work/hapi')).toBe(false)
})
})
62 changes: 37 additions & 25 deletions web/src/components/SessionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,36 @@ function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] {
})
}


export function expandSelectedSessionCollapseOverrides(
overrides: Map<string, boolean>,
group: { key: string; machineId: string | null }
): Map<string, boolean> {
const next = new Map(overrides)
let changed = false

// Expand project group if collapsed. Project and machine keys use true = collapsed.
if (overrides.has(group.key) && overrides.get(group.key)) {
next.delete(group.key)
changed = true
}

// Session preview keys use inverted semantics: false = expanded, true/missing = collapsed.
const sessionPreviewKey = `sessions::${group.key}`
if (overrides.get(sessionPreviewKey) !== false) {
next.set(sessionPreviewKey, false)
changed = true
}

const machineKey = `machine::${group.machineId ?? UNKNOWN_MACHINE_ID}`
if (overrides.has(machineKey) && overrides.get(machineKey)) {
next.delete(machineKey)
changed = true
}

return changed ? next : overrides
}

function groupByMachine(
groups: SessionGroup[],
resolveMachineLabel: (id: string | null) => string
Expand Down Expand Up @@ -398,7 +428,6 @@ export function getVisibleSessionPreview(
sessions: SessionSummary[],
options: {
expanded?: boolean
selectedSessionId?: string | null
limit?: number
} = {}
): SessionSummary[] {
Expand All @@ -413,11 +442,6 @@ export function getVisibleSessionPreview(
visible.push(session)
}

const selectedSession = options.selectedSessionId
? sessions.find(session => session.id === options.selectedSessionId)
: undefined
if (selectedSession) addSession(selectedSession)

for (const session of sessions) {
if (visible.length >= limit) break
if (session.active) addSession(session)
Expand All @@ -438,19 +462,21 @@ function SessionListSearch(props: {
const { t } = useTranslation()
return (
<div className="relative px-3 pb-2">
<SearchIcon className="pointer-events-none absolute left-5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--app-hint)]" />
<div className="pointer-events-none absolute inset-y-0 left-5 flex items-center pb-2 text-[var(--app-hint)]">
<SearchIcon className="h-3.5 w-3.5" />
</div>
<input
type="search"
value={props.value}
onChange={(event) => props.onChange(event.target.value)}
placeholder={t('sessions.search.placeholder')}
className="w-full rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] py-1.5 pl-8 pr-8 text-sm text-[var(--app-fg)] outline-none transition-colors placeholder:text-[var(--app-hint)] focus:border-[var(--app-link)]"
className="w-full appearance-none rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] py-1.5 pl-8 pr-8 text-sm text-[var(--app-fg)] outline-none transition-colors placeholder:text-[var(--app-hint)] focus:border-[var(--app-link)] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
/>
{props.value ? (
<button
type="button"
onClick={() => props.onChange('')}
className="absolute right-5 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
className="absolute inset-y-0 right-5 flex items-center pb-2 rounded p-0.5 text-[var(--app-hint)] hover:text-[var(--app-fg)]"
title={t('sessions.search.clear')}
>
<XIcon className="h-3.5 w-3.5" />
Expand Down Expand Up @@ -749,8 +775,7 @@ export function SessionList(props: {
return getVisibleSessionPreview(
group.sessions,
{
expanded: isSessionGroupExpanded(group),
selectedSessionId
expanded: isSessionGroupExpanded(group)
}
)
}
Expand Down Expand Up @@ -789,20 +814,7 @@ export function SessionList(props: {
g.sessions.some(s => s.id === selectedSessionId)
)
if (!group) return prev
const next = new Map(prev)
let changed = false
// Expand project group if collapsed
if (prev.has(group.key) && prev.get(group.key)) {
next.delete(group.key)
changed = true
}
// Expand machine group if collapsed
const machineKey = `machine::${group.machineId ?? UNKNOWN_MACHINE_ID}`
if (prev.has(machineKey) && prev.get(machineKey)) {
next.delete(machineKey)
changed = true
}
return changed ? next : prev
return expandSelectedSessionCollapseOverrides(prev, group)
})
}, [selectedSessionId, allGroups])

Expand Down
Loading