fix(experimental): tighten paging guards and event payload typing

This commit is contained in:
mektpoy committed 2026-10-26 13:26:11 +08:00
1 parent 65b7e61b76
commit 120c717035
4 files changed
+32 -36

No files matched your search

@@ -1734,7 +1734,7 @@ function stepRows(options: StepRowsOptions): ReactNode[] {
const gap = gaps.get(step.step)
const visibleGap = gap !== undefined && gap >= STEP_GAP_MIN_MS ? gap : undefined
const expanded = open[step.step] === true
const hasAnswer = step.answerBody !== null && step.answerBody !== undefined
const hasAnswer = step.answerBody !== null
const hasDetail = step.calls.length > 0 || hasAnswer
const rowClass: (string | undefined)[] = []
if (hasDetail) rowClass.push(css.stepRowClickable)
@@ -102,23 +102,17 @@ export function apply(ctx: Context): void {
if (pending !== undefined) return pending
const task = (async (): Promise<LoadOlderResult> => {
const before = perf.getSnapshot()
let changed = false
const stop = perf.subscribe(() => { changed = true })
try {
// A view can mount before the Session finishes opening; paging before
// that is a no-op, so wait for open unless the view is gone.
await whenOpened(session, lifecycle.signal)
if (lifecycle.signal.aborted) return 'retry'
if (session.getSnapshot().loadingOlder) return 'retry'
await session.loadOlder()
if (changed || perf.getSnapshot() !== before) return 'grew'
const after = session.getSnapshot()
// A page can grow the Session window without adding a perf node. Only
// an exhausted Session may hide the trigger; otherwise keep it retryable.
return after.hasMore ? 'retry' : 'exhausted'
} finally {
stop()
}
// A view can mount before the Session finishes opening; paging before
// that is a no-op, so wait for open unless the view is gone.
await whenOpened(session, lifecycle.signal)
if (lifecycle.signal.aborted) return 'retry'
if (session.getSnapshot().loadingOlder) return 'retry'
await session.loadOlder()
if (perf.getSnapshot() !== before) return 'grew'
const after = session.getSnapshot()
// A page can grow the Session window without adding a perf node. Only
// an exhausted Session may hide the trigger; otherwise keep it retryable.
return after.hasMore ? 'retry' : 'exhausted'
})()
const promise = task.finally(() => { pages.delete(binding) })
pages.set(binding, promise)
@@ -5,7 +5,7 @@ import type {
} from '@deepseek-ai/dsh-client-ui-conversation/client'
import type {} from '@deepseek-ai/dsh-compaction/types'
import type {} from '@deepseek-ai/dsh-tools'
import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types'
import type {} from '@deepseek-ai/dsh-user-approval/types'
import type { SessionEventMap } from '@deepseek-ai/dsh-session/types'
import type { PerfEventMetrics } from './perf-contract.ts'
@@ -13,9 +13,15 @@ import type { PerfEventMetrics } from './perf-contract.ts'
/** Event shape a Conversation Definition receives. */
type MatchEvent = Parameters<ConversationNodeDefinition['match']>[0]
/** Payload of an event whose type is known to the caller. */
function dataOf<T>(event: MatchEvent): T {
return (event as MatchEvent & { data: T }).data
/**
* Payload of an event whose type is known to the caller.
* @param event - event seen by the Definition match.
* @param type - event discriminant already narrowed by the caller.
* @returns the typed event payload.
*/
function dataOf<K extends keyof SessionEventMap>(event: MatchEvent, type: K): SessionEventMap[K] {
void type
return (event as MatchEvent & { type: K; data: SessionEventMap[K] }).data
}
/**
@@ -113,7 +119,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
}
const eventType: string = event.type
if (eventType === 'request/context') {
const data = dataOf<SessionEventMap['request/context']>(event)
const data = dataOf(event, 'request/context')
return {
...base,
kind: 'request-context',
@@ -124,7 +130,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
}
}
if (eventType === 'llm/retry') {
const data = dataOf<LlmRetryEventData>(event)
const data = dataOf(event, 'llm/retry')
return {
...base,
kind: 'model-retry',
@@ -138,7 +144,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
}
}
if (eventType === 'assistant/attempt') {
const data = dataOf<{ turn: number; step: number }>(event)
const data = dataOf(event, 'assistant/attempt')
return {
...base,
kind: 'model-attempt',
@@ -148,20 +154,18 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
}
}
if (eventType === 'user/message') {
const data = dataOf<{
source: { kind: string }
content: readonly { type: string; text?: string }[]
}>(event)
if (data.source.kind !== 'runtime-context' && data.source.kind !== 'time-context') return null
const data = dataOf(event, 'user/message')
const kind: string = data.source.kind
if (kind !== 'runtime-context' && kind !== 'time-context') return null
return {
...base,
kind: 'context-refresh',
status: data.source.kind,
status: kind,
chars: contentChars(data.content),
}
}
if (eventType === 'approval/asked') {
const data = dataOf<{ toolName: string; reason?: string }>(event)
const data = dataOf(event, 'approval/asked')
return {
...base,
kind: 'approval',
@@ -171,7 +175,7 @@ export function eventMetrics(event: MatchEvent, location: ConversationLocation |
}
}
if (eventType === 'approval/decided') {
const data = dataOf<{ outcome: string }>(event)
const data = dataOf(event, 'approval/decided')
return { ...base, kind: 'approval', status: data.outcome }
}
return null
@@ -450,14 +450,12 @@ describe('browser plugin registration', () => {
const injectEntry = bench.entries()[0]!.inject as ((sessionId: SessionId) => PerfViewInjected) | undefined
const injected = injectEntry!(SID)
bench.setHasMore(true)
const gate = Promise.withResolvers<undefined>()
bench.loadOlder.mockImplementationOnce(() => gate.promise)
bench.loadOlder.mockImplementationOnce(() => undefined)
const first = injected.loadOlder()
const second = injected.loadOlder()
expect(second).toBe(first)
await Promise.resolve()
expect(bench.loadOlder).toHaveBeenCalledTimes(1)
gate.resolve(undefined)
expect(await first).toBe('retry')
expect(bench.loadOlder).toHaveBeenCalledTimes(1)