mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* fix(web): count unseen messages by rendered block, not raw message The "N new messages" pill counted raw DecryptedMessages while the timeline renders folded blocks, so the two never agreed. A subagent run is dozens of sidechain messages but a single Task card; a tool_use and its tool_result are two messages and one card; consecutive tools collapse into one group. The pill could read "47 new messages" when scrolling down revealed two new rows. collectNewUnseenIds never inspected isSidechain, and it could not: the reducer's grouping is stateful (it needs the Task tool_use before it can map parentToolUseId), so a per-message predicate in the store cannot reproduce it. Adding an isSidechain check there would also invert the error for orphan sidechain messages, which tracer.ts falls back to emitting at the top level. Instead, drop the store's unseen bookkeeping entirely and count what the renderer actually produced. Watermark the visible blocks when the user scrolls away from the tail, then count the blocks past the last one they had seen. The count is anchor-based rather than timestamp-based because the blocks array is not monotonic in createdAt: messages sort by invokedAt ?? createdAt, so a queued message carries an old createdAt while sitting at the end. Anchoring also makes prepended history free, since older blocks land before the anchor. Known limit, documented at the call site: once the history window fills up, mergeIntoWindow trims incoming messages off the tail, so the pill reports 0 instead of a count. Under-reporting is preferable here, and returning to the tail force-refetches the latest page anyway. * fix(web): keep unseen watermark stable across optimistic id replacement The watermark snapshotted only block.id, but that id is not stable for the user's own messages: mergeMessages replaces an optimistic row with a stored row that keeps localId under a new server id, and the user block renders with the message id. Scrolling into history while an own message was still optimistic meant its echo anchored one block earlier and bumped the pill by one, with no new rendered row. Track localId alongside id in the watermark and match on either. Reported by HAPI Bot on #1255. * fix(web): count joined assistant cards, not pre-join blocks visibleBlocks is still not one-to-one with rendered rows: assistant-ui joins a run of adjacent assistant-role blocks into a single card, so a response made of reasoning + text + a tool call was reported as three new messages instead of one, and appending another block to an in-flight response bumped the pill without adding a row. Walk the blocks after the anchor and only start a new row where the assistant run breaks. Role assignment is the part that would drift, so rather than restating it, visibleBlockRole moves from assistant-runtime.ts to toolGroups.ts (next to the VisibleChatBlock definition it describes) and both the runtime and the counter import the one copy. Reported by HAPI Bot on #1255.
35 lines
1.5 KiB
TypeScript
35 lines
1.5 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import type { VisibleChatBlock } from '@/chat/toolGroups'
|
|
import { countUnseenBlocks, createUnseenWatermark, type UnseenWatermark } from '@/chat/unseenBlocks'
|
|
|
|
/**
|
|
* Tracks how many rendered blocks appeared since the user scrolled away from
|
|
* the tail, for the "N new messages" pill.
|
|
*
|
|
* The watermark is captured during render rather than in an effect so the first
|
|
* frame after leaving the tail already reports 0 instead of briefly showing a
|
|
* stale count. This is React's "adjust state during render" pattern: the
|
|
* setState pair runs only on the frame where viewMode actually flips, and
|
|
* updating prevViewMode makes the condition false on the immediate re-render,
|
|
* so it converges instead of looping.
|
|
*
|
|
* viewMode is seeded from the caller's current value because the message window
|
|
* store keeps view mode per session in a module-level map — returning to a
|
|
* session that was left in history mode must not be mistaken for a fresh
|
|
* tail -> history transition and capture a watermark the user never saw.
|
|
*/
|
|
export function useUnseenBlockCount(
|
|
viewMode: 'tail' | 'history',
|
|
blocks: readonly VisibleChatBlock[]
|
|
): number {
|
|
const [prevViewMode, setPrevViewMode] = useState(viewMode)
|
|
const [watermark, setWatermark] = useState<UnseenWatermark | null>(null)
|
|
|
|
if (viewMode !== prevViewMode) {
|
|
setPrevViewMode(viewMode)
|
|
setWatermark(viewMode === 'history' ? createUnseenWatermark(blocks) : null)
|
|
}
|
|
|
|
return useMemo(() => countUnseenBlocks(blocks, watermark), [blocks, watermark])
|
|
}
|