fix(session-history): keep scan notices from burying the session list (#29392)

Every session the scanner read only partly (for example a Codex transcript on an
SSH host with a record over the 10 MiB cap) adds its own notice row above the
list, outside the list's scroll area. With dozens of them the panel filled with
notices and the sessions could not be reached. Notices now sit in one bounded
area that scrolls on its own.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-15 23:16:09 -07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 1faa210d4c
commit b908d929f7
2 changed files with 79 additions and 25 deletions
@@ -0,0 +1,42 @@
// @vitest-environment happy-dom
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, expect, it } from 'vitest'
import type { AiVaultListResult, AiVaultSession } from '../../../../shared/ai-vault-types'
import { AiVaultScanIssueBanners } from './AiVaultScanIssueBanners'
afterEach(() => {
cleanup()
})
it('keeps many partial-read notices in a bounded scroll area above the session list', () => {
const notices = Array.from({ length: 40 }, (_, index) => ({
executionHostId: 'ssh:box' as const,
agent: 'codex' as const,
kind: 'notice' as const,
path: `/home/u/.codex/sessions/rollout-${index}.jsonl`,
message: `Skipped 1 oversized transcript record over the 10.0 MiB limit (12.0 MiB at byte ${index}). The rest of the session was read.`
}))
const scanResult: AiVaultListResult = {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: banners only read the session count.
sessions: [{} as AiVaultSession],
issues: [...notices, { agent: 'codex', path: '/broken.jsonl', message: 'Unreadable' }],
scannedAt: '2026-10-15T00:00:00.000Z'
}
render(<AiVaultScanIssueBanners scanResult={scanResult} />)
const region = screen.getByTestId('ai-vault-scan-notices')
expect(region.className).toContain('max-h-24')
expect(region.className).toContain('overflow-y-auto')
expect(region.children).toHaveLength(42)
expect(region.textContent).toContain('1 transcript skipped')
})
it('renders no notice area for a clean scan', () => {
render(
<AiVaultScanIssueBanners
scanResult={{ sessions: [], issues: [], scannedAt: '2026-10-15T00:00:00.000Z' }}
/>
)
expect(screen.queryByTestId('ai-vault-scan-notices')).toBeNull()
})
@@ -18,6 +18,10 @@ export function AiVaultScanIssueBanners({
const blocking = blockingAiVaultScanIssue(scanResult)
const skippedTranscriptCount = skippedAiVaultTranscriptCount(scanResult)
const notices = aiVaultScanNoticeIssues(scanResult)
const reasons = skippedAiVaultTranscriptReasons(scanResult)
const hasNoticeRows = notices.length > 0 || skippedTranscriptCount > 0 || reasons.length > 0
return (
<>
{blocking ? (
@@ -25,35 +29,43 @@ export function AiVaultScanIssueBanners({
{blocking.message}
</div>
) : null}
{aiVaultScanNoticeIssues(scanResult).map((issue) => (
{hasNoticeRows ? (
// Why bounded: one notice per partly-read session can outgrow the panel and hide the list.
<div
// Message is part of the key: one host can report several distinct
// messages for the same path, and a colliding key drops those rows.
key={`${issue.executionHostId ?? 'local'}:${issue.kind}:${issue.agent}:${issue.path}:${issue.message}`}
className={`border-b border-sidebar-border px-3 py-1.5 text-[11px] ${
issue.kind === 'host' ? 'text-destructive' : 'text-muted-foreground'
}`}
data-testid="ai-vault-scan-notices"
className="scrollbar-sleek max-h-24 shrink-0 overflow-y-auto"
>
{issue.message}
</div>
))}
{skippedTranscriptCount > 0 ? (
<div className="border-b border-sidebar-border px-3 py-1.5 text-[11px] text-muted-foreground">
{translate(
'auto.components.right.sidebar.AiVaultPanel.transcriptsSkipped',
'{{count}} transcript skipped',
{ count: skippedTranscriptCount }
)}
{notices.map((issue) => (
<div
// Message is part of the key: one host can report several distinct
// messages for the same path, and a colliding key drops those rows.
key={`${issue.executionHostId ?? 'local'}:${issue.kind}:${issue.agent}:${issue.path}:${issue.message}`}
className={`border-b border-sidebar-border px-3 py-1.5 text-[11px] ${
issue.kind === 'host' ? 'text-destructive' : 'text-muted-foreground'
}`}
>
{issue.message}
</div>
))}
{skippedTranscriptCount > 0 ? (
<div className="border-b border-sidebar-border px-3 py-1.5 text-[11px] text-muted-foreground">
{translate(
'auto.components.right.sidebar.AiVaultPanel.transcriptsSkipped',
'{{count}} transcript skipped',
{ count: skippedTranscriptCount }
)}
</div>
) : null}
{reasons.map((reason) => (
<div
key={reason}
className="border-b border-sidebar-border px-3 py-1.5 text-[11px] text-muted-foreground"
>
{reason}
</div>
))}
</div>
) : null}
{skippedAiVaultTranscriptReasons(scanResult).map((reason) => (
<div
key={reason}
className="border-b border-sidebar-border px-3 py-1.5 text-[11px] text-muted-foreground"
>
{reason}
</div>
))}
</>
)
}