From b3e729bdea64b68fd51ed418d63511dcd9abf213 Mon Sep 17 00:00:00 2001 From: cocolate Date: Thu, 30 Apr 2026 01:12:26 +0800 Subject: [PATCH] fix(web): polish session search and ordering Align session search controls, hide the native search clear button, and keep collapsed session previews ordered by activity while still expanding previews for the selected session. --- web/src/components/SessionList.test.ts | 38 ++++++++++++++-- web/src/components/SessionList.tsx | 62 +++++++++++++++----------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/web/src/components/SessionList.test.ts b/web/src/components/SessionList.test.ts index df1dc60c1..598c0bbea 100644 --- a/web/src/components/SessionList.test.ts +++ b/web/src/components/SessionList.test.ts @@ -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 & { id: string }): SessionSummary { return { @@ -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, @@ -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', () => { @@ -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([ + ['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() + + const result = expandSelectedSessionCollapseOverrides(overrides, { + key: 'machine-1::/work/hapi', + machineId: 'machine-1' + }) + + expect(result.get('sessions::machine-1::/work/hapi')).toBe(false) + }) +}) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index c7a6356f5..8d3ad16f0 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -176,6 +176,36 @@ function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] { }) } + +export function expandSelectedSessionCollapseOverrides( + overrides: Map, + group: { key: string; machineId: string | null } +): Map { + 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 @@ -398,7 +428,6 @@ export function getVisibleSessionPreview( sessions: SessionSummary[], options: { expanded?: boolean - selectedSessionId?: string | null limit?: number } = {} ): SessionSummary[] { @@ -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) @@ -438,19 +462,21 @@ function SessionListSearch(props: { const { t } = useTranslation() return (
- +
+ +
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 ? (