fix(web): write VAPID key only after hub registration; make running-section toggle keyboard-accessible with correct filtered state

This commit is contained in:
2026-08-02 22:37:00 +08:00
parent 087cb08e53
commit b4a8e89fe3
4 changed files with 54 additions and 9 deletions
+12 -7
View File
@@ -253,13 +253,18 @@ export function NewSession(props: {
if (!props.api) {
return
}
props.api.getClaudeCustomModels().then((result) => {
if (!cancelled) {
setClaudeCustomModels(Array.isArray(result.models) ? result.models : [])
}
}).catch(() => {
// Custom models are optional — fall back to the built-in presets.
})
try {
props.api.getClaudeCustomModels().then((result) => {
if (!cancelled) {
setClaudeCustomModels(Array.isArray(result.models) ? result.models : [])
}
}).catch(() => {
// Custom models are optional — fall back to the built-in presets.
})
} catch {
// Partial api clients (tests, older hub versions) without the
// method must not break the New Session form.
}
return () => {
cancelled = true
}
@@ -462,9 +462,11 @@ describe('SessionList collapse behavior', () => {
const runningPanel = () => screen.getByTitle('In progress').nextElementSibling
expect(runningPanel()?.getAttribute('data-open')).toBe('true')
expect(screen.getByTitle('In progress').getAttribute('aria-expanded')).toBe('true')
fireEvent.click(screen.getByTitle('In progress'))
expect(runningPanel()?.getAttribute('data-open')).toBeNull()
expect(screen.getByTitle('In progress').getAttribute('aria-expanded')).toBe('false')
fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
fireEvent.change(screen.getByPlaceholderText('Search sessions…'), {
@@ -472,6 +474,31 @@ describe('SessionList collapse behavior', () => {
})
expect(runningPanel()?.getAttribute('data-open')).toBe('true')
// The section stays reported open while searching even though the
// underlying collapsed state is still set.
expect(screen.getByTitle('In progress').getAttribute('aria-expanded')).toBe('true')
})
it('toggles the running section with the keyboard', () => {
const sessions = [
makeSession({
id: 'session-running',
active: true,
thinking: true,
updatedAt: 100,
metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' },
}),
]
render(renderSessionList(sessions))
const header = screen.getByRole('button', { name: /In progress/ })
expect(header.getAttribute('aria-expanded')).toBe('true')
fireEvent.keyDown(header, { key: 'Enter' })
expect(header.getAttribute('aria-expanded')).toBe('false')
fireEvent.keyDown(header, { key: ' ' })
expect(header.getAttribute('aria-expanded')).toBe('true')
})
it('keeps the previous selected path open when selection moves', async () => {
+10 -1
View File
@@ -1427,10 +1427,19 @@ export function SessionList(props: {
<div key="running-section">
<div
className="group/running flex min-w-0 w-full select-none cursor-pointer items-center gap-2 rounded-lg py-1.5 pl-2 pr-2 transition-colors hover:bg-[var(--app-secondary-bg)]"
role="button"
tabIndex={0}
aria-expanded={!runningSectionCollapsed || isFiltering}
onClick={() => setRunningSectionCollapsed((value) => !value)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
setRunningSectionCollapsed((value) => !value)
}
}}
title={t('sessions.runningSection')}
>
<ChevronIcon className="h-3.5 w-3.5 text-[var(--app-hint)] shrink-0" collapsed={runningSectionCollapsed} />
<ChevronIcon className="h-3.5 w-3.5 text-[var(--app-hint)] shrink-0" collapsed={runningSectionCollapsed && !isFiltering} />
<span className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center" aria-hidden="true">
<span className="h-1.5 w-1.5 rounded-full bg-[var(--app-badge-success-text)] animate-pulse" />
</span>
+5 -1
View File
@@ -143,7 +143,6 @@ export function usePushNotifications(api: ApiClient | null) {
applicationServerKey
})
}
writeStoredVapidKey(publicKey)
const json = subscription.toJSON()
const keys = json.keys
@@ -158,6 +157,11 @@ export function usePushNotifications(api: ApiClient | null) {
auth: keys.auth
}
})
// Only record the key after the hub registration succeeded. A
// failed registration must leave the previous key in place so the
// next load retries the replacement instead of reusing a
// subscription the hub never learned about.
writeStoredVapidKey(publicKey)
setIsSubscribed(true)
return true
} catch (error) {