perf(dashboard-popout): park the kanban clock when hidden or timestamp-free (#9881)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
95ae346b87
commit
39b124c75b
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import type { DashboardCard, DashboardSnapshot } from '../../../../shared/dashboard-snapshot'
|
||||
import { AgentKanbanBoard } from './AgentKanbanBoard'
|
||||
|
||||
|
|
@ -11,15 +11,18 @@ import { AgentKanbanBoard } from './AgentKanbanBoard'
|
|||
vi.mock('./AgentKanbanCard', () => ({
|
||||
AgentKanbanCard: ({
|
||||
card,
|
||||
now,
|
||||
onOpenTerminal
|
||||
}: {
|
||||
card: DashboardCard
|
||||
now: number
|
||||
onOpenTerminal: (card: DashboardCard) => void
|
||||
}) => (
|
||||
<div
|
||||
data-testid="card"
|
||||
data-bucket={card.bucket}
|
||||
data-unseen={card.unseen}
|
||||
data-now={now}
|
||||
onClick={() => onOpenTerminal(card)}
|
||||
>
|
||||
{card.worktreeName}
|
||||
|
|
@ -81,7 +84,9 @@ describe('AgentKanbanBoard', () => {
|
|||
})
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders the three fixed columns in order', () => {
|
||||
|
|
@ -119,6 +124,47 @@ describe('AgentKanbanBoard', () => {
|
|||
expect(names).toEqual(['new-move', 'mid-move', 'old-move'])
|
||||
})
|
||||
|
||||
it('does not start the clock when no card renders a relative timestamp', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(100_000)
|
||||
|
||||
const { rerender } = render(<AgentKanbanBoard snapshot={{ generatedAt: 1, cards: [] }} />)
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
|
||||
rerender(
|
||||
<AgentKanbanBoard
|
||||
snapshot={{ generatedAt: 2, cards: [card({ startedAt: 0, finishedAt: null })] }}
|
||||
/>
|
||||
)
|
||||
const initialNow = screen.getByTestId('card').dataset.now
|
||||
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
act(() => vi.advanceTimersByTime(30_000))
|
||||
expect(screen.getByTestId('card').dataset.now).toBe(initialNow)
|
||||
})
|
||||
|
||||
it('parks the clock while hidden, catches up on reveal, and ticks while visible', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(100_000)
|
||||
let visibilityState: DocumentVisibilityState = 'hidden'
|
||||
vi.spyOn(document, 'visibilityState', 'get').mockImplementation(() => visibilityState)
|
||||
|
||||
renderBoard([card({ startedAt: 1 })])
|
||||
expect(screen.getByTestId('card').dataset.now).toBe('100000')
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
|
||||
act(() => vi.advanceTimersByTime(60_000))
|
||||
expect(screen.getByTestId('card').dataset.now).toBe('100000')
|
||||
|
||||
visibilityState = 'visible'
|
||||
act(() => document.dispatchEvent(new Event('visibilitychange')))
|
||||
expect(screen.getByTestId('card').dataset.now).toBe('160000')
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
|
||||
act(() => vi.advanceTimersByTime(30_000))
|
||||
expect(screen.getByTestId('card').dataset.now).toBe('190000')
|
||||
})
|
||||
|
||||
it('keeps the terminal dialog open across bucket moves and card removal', () => {
|
||||
const agent = card({ paneKey: 'pk-1', bucket: 'idle', worktreeName: 'wt1' })
|
||||
const { rerender } = render(<AgentKanbanBoard snapshot={{ generatedAt: 1, cards: [agent] }} />)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
type DashboardSnapshot
|
||||
} from '../../../../shared/dashboard-snapshot'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
|
||||
import { AgentKanbanCard } from './AgentKanbanCard'
|
||||
import { AgentTerminalDialog } from './AgentTerminalDialog'
|
||||
import './agent-board-transitions.css'
|
||||
|
|
@ -89,12 +90,21 @@ function KanbanColumn({
|
|||
/** The pop-out agent board: status columns fed by the relayed snapshot. */
|
||||
export function AgentKanbanBoard({ snapshot }: { snapshot: DashboardSnapshot }): React.JSX.Element {
|
||||
const grouped = useMemo(() => groupByBucket(snapshot.cards), [snapshot.cards])
|
||||
const hasRelativeTimestamps = useMemo(
|
||||
() => snapshot.cards.some((card) => (card.finishedAt ?? card.startedAt) > 0),
|
||||
[snapshot.cards]
|
||||
)
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(Date.now()), 30_000)
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
if (!hasRelativeTimestamps) {
|
||||
return
|
||||
}
|
||||
return installWindowVisibilityInterval({
|
||||
run: () => setNow(Date.now()),
|
||||
intervalMs: 30_000
|
||||
})
|
||||
}, [hasRelativeTimestamps])
|
||||
|
||||
// The open terminal dialog survives bucket moves: only the paneKey is
|
||||
// remembered, and the card data is re-resolved from each fresh snapshot.
|
||||
|
|
|
|||
Loading…
Reference in New Issue