fix(web): address review findings — prune stale push endpoints, keep running section open while searching, clear auto-expand guard on section transitions; update tests

This commit is contained in:
2026-08-02 21:55:46 +08:00
parent fd457e0303
commit 240bf8affe
3 changed files with 69 additions and 10 deletions
@@ -388,9 +388,9 @@ describe('SessionList collapse behavior', () => {
]
const { rerender } = render(renderSessionList(baseSessions))
expect(getProjectPanel().getAttribute('data-open')).toBe('true')
fireEvent.click(screen.getByTitle('/work/hapi'))
// The running session is pinned in the "in progress" section; the
// directory group now only holds inactive sessions and starts
// collapsed.
expect(getProjectPanel().getAttribute('data-open')).toBeNull()
rerender(renderSessionList([
@@ -410,9 +410,7 @@ describe('SessionList collapse behavior', () => {
it('auto-expands the path again when the selected session changes', async () => {
const sessions = [
makeSession({
id: 'session-running',
active: true,
thinking: true,
id: 'session-first',
updatedAt: 100,
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
}),
@@ -424,16 +422,58 @@ describe('SessionList collapse behavior', () => {
]
const { rerender } = render(renderSessionList(sessions))
fireEvent.click(screen.getByTitle('/work/hapi'))
// Inactive-only groups start collapsed; selecting a session inside
// one auto-expands it.
expect(getProjectPanel().getAttribute('data-open')).toBeNull()
rerender(renderSessionList(sessions, 'session-next'))
rerender(renderSessionList([
...sessions,
makeSession({
id: 'session-running',
active: true,
thinking: true,
updatedAt: 110,
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
}),
], 'session-next'))
await waitFor(() => {
expect(getProjectPanel().getAttribute('data-open')).toBe('true')
})
})
it('keeps the running section open while searching even when collapsed', () => {
const sessions = [
makeSession({
id: 'session-running',
active: true,
thinking: true,
updatedAt: 100,
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
}),
makeSession({
id: 'session-idle',
updatedAt: 50,
metadata: { path: '/work/hapi', name: 'Idle task', flavor: 'codex' },
}),
]
render(renderSessionList(sessions))
const runningPanel = () => screen.getByTitle('In progress').nextElementSibling
expect(runningPanel()?.getAttribute('data-open')).toBe('true')
fireEvent.click(screen.getByTitle('In progress'))
expect(runningPanel()?.getAttribute('data-open')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
fireEvent.change(screen.getByPlaceholderText('Search sessions…'), {
target: { value: 'Running' },
})
expect(runningPanel()?.getAttribute('data-open')).toBe('true')
})
it('keeps the previous selected path open when selection moves', async () => {
const sessions = [
makeSession({
+9 -2
View File
@@ -1177,7 +1177,14 @@ export function SessionList(props: {
const group = groups.find(g =>
g.sessions.some(s => s.id === selectedSessionId)
)
if (!group) return
if (!group) {
// The selected session is not rendered inside any directory group
// (e.g. it moved to the pinned "in progress" section). Drop the
// guard so it auto-expands again when it transitions back into a
// group later.
autoExpandedSelectedSessionKeyRef.current = null
return
}
const autoExpandKey = `${selectedSessionId}::${group.key}`
if (autoExpandedSelectedSessionKeyRef.current === autoExpandKey) return
@@ -1432,7 +1439,7 @@ export function SessionList(props: {
({runningSessions.length})
</span>
</div>
<div className="collapsible-panel" data-open={!runningSectionCollapsed || undefined}>
<div className="collapsible-panel" data-open={(!runningSectionCollapsed || isFiltering) || undefined}>
<div className="collapsible-inner">
<div className="flex flex-col gap-0.5 ml-3 pl-1 py-1">
{runningSessions.map((s) => (
+12
View File
@@ -117,12 +117,24 @@ export function usePushNotifications(api: ApiClient | null) {
// mismatch via the key recorded at subscribe time and recreate it.
let subscription = existing
if (existing && readStoredVapidKey() !== publicKey) {
const staleEndpoint = existing.endpoint
try {
await existing.unsubscribe()
} catch {
// Ignore unsubscribe failures — subscribe() below still
// issues a fresh subscription with the current key.
}
// Prune the obsolete endpoint from the hub so it stops
// receiving failed sends (VapidPkHashMismatch) for a
// subscription that can no longer be reached.
if (staleEndpoint) {
try {
await api.unsubscribePushNotifications({ endpoint: staleEndpoint })
} catch {
// Best-effort cleanup — a stale hub registration is
// harmless beyond repeated failed sends until pruned.
}
}
subscription = null
}
if (!subscription) {