fix(web): 修复切换会话时消息乱序问题 (#150)

* feat(web): add built-in Nerd Font for terminal icon support

Add MesloLGLDZ Nerd Font as a built-in web font to ensure Powerlevel10k
and other Nerd Font-dependent prompts render correctly in the web terminal,
even on devices without local Nerd Fonts installed (e.g., mobile phones).

Changes:
- Add MesloLGLDZNerdFontMono-Regular.woff2 to web/public/fonts/
- Load font via FontFace API with BASE_URL support for subpath deployments
- Merge font loading and terminal creation in same useEffect to ensure
  font is ready before terminal renders

Fixes #121

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use CDN for Nerd Font instead of bundled file

Load MesloLGLDZ Nerd Font from jsDelivr CDN instead of bundling the 1.2MB
font file. This reduces the bundle size significantly while still ensuring
Nerd Font icons display correctly on all devices.

CDN URL: https://cdn.jsdelivr.net/gh/mshaugh/nerdfont-webfonts@v3.3.0/build/fonts/MesloLGLDZNerdFontMono-Regular.woff2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(web): prevent message ordering issues when switching sessions

When switching between sessions or loading paginated messages, old tool
blocks from agentState.requests were being mixed with current messages,
causing display order issues.

Root cause: reduceChatBlocks creates tool blocks for all permissions in
agentState, including those older than the current message page. These
old blocks (with earlier createdAt) would appear mixed with newer messages.

Fix:
1. In reducer.ts: Skip creating permission-only tool blocks if their
   createdAt is older than the oldest message in the current view
2. In SessionChat.tsx: Clear caches synchronously in useMemo when session
   changes, since useEffect runs after render

Closes #148

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: tfq <tfq@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
lost
2026-02-03 12:51:27 +08:00
committed by GitHub
co-authored by HAPI tfq Claude Opus 4.5
parent 2735421e3f
commit 2f6bbcd400
3 changed files with 25 additions and 0 deletions
+13
View File
@@ -48,11 +48,24 @@ export function reduceChatBlocks(
let hasReadyEvent = rootResult.hasReadyEvent
// Only create permission-only tool cards when there is no tool call/result in the transcript.
// Also skip if the permission is older than the oldest message in the current view,
// to avoid mixing old tool cards with newer messages when paginating.
const oldestMessageTime = normalized.length > 0
? Math.min(...normalized.map(m => m.createdAt))
: null
for (const [id, entry] of permissionsById) {
if (toolIdsInMessages.has(id)) continue
if (rootResult.toolBlocksById.has(id)) continue
const createdAt = entry.permission.createdAt ?? Date.now()
// Skip permissions that are older than the oldest message in the current view.
// These will be shown when the user loads older messages.
if (oldestMessageTime !== null && createdAt < oldestMessageTime) {
continue
}
const block = ensureToolBlock(rootResult.blocks, rootResult.toolBlocksById, id, {
createdAt,
localId: null,
+10
View File
@@ -141,12 +141,22 @@ export function SessionChat(props: {
voice.toggleMic()
}, [voice])
// Track session id to clear caches when it changes
const prevSessionIdRef = useRef<string | null>(null)
useEffect(() => {
normalizedCacheRef.current.clear()
blocksByIdRef.current.clear()
}, [props.session.id])
const normalizedMessages: NormalizedMessage[] = useMemo(() => {
// Clear caches immediately when session changes (before useEffect runs)
if (prevSessionIdRef.current !== null && prevSessionIdRef.current !== props.session.id) {
normalizedCacheRef.current.clear()
blocksByIdRef.current.clear()
}
prevSessionIdRef.current = props.session.id
const cache = normalizedCacheRef.current
const normalized: NormalizedMessage[] = []
const seen = new Set<string>()
+2
View File
@@ -23,9 +23,11 @@ function isOptimisticMessage(msg: DecryptedMessage): boolean {
function compareMessages(a: DecryptedMessage, b: DecryptedMessage): number {
const aSeq = typeof a.seq === 'number' ? a.seq : null
const bSeq = typeof b.seq === 'number' ? b.seq : null
if (aSeq !== null && bSeq !== null && aSeq !== bSeq) {
return aSeq - bSeq
}
if (a.createdAt !== b.createdAt) {
return a.createdAt - b.createdAt
}