From 4f25c3009d29b57c9d8044536bbba0fb40f3c15e Mon Sep 17 00:00:00 2001 From: vhqtvn Date: Mon, 18 May 2026 22:17:18 +0700 Subject: [PATCH] fix(status): pick latest assistant message in single pass (#1297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(status): pick latest assistant message in single pass Sync reconciliation can splice messages out of array order, so the previous "filter + sort + last" pipeline could pick the wrong record as the latest assistant message and surface a stale status. Replace it with a single-pass scan that compares created-time (tiebreak by id) — same selection, but resilient to non-sorted message arrays. * refactor: use comparator + running max for latest assistant pick Address Greptile review: collapse the dual-exit-point single-pass max finder (separate continue + isNewer branches, repeated null checks) into a single isLater(a, b) comparator with one running-max comparison per iteration. Same selection rule, easier to verify. --------- Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com> --- packages/ui/src/hooks/useAssistantStatus.ts | 39 +++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/hooks/useAssistantStatus.ts b/packages/ui/src/hooks/useAssistantStatus.ts index a952fa02..cf3c217e 100644 --- a/packages/ui/src/hooks/useAssistantStatus.ts +++ b/packages/ui/src/hooks/useAssistantStatus.ts @@ -198,28 +198,29 @@ export function useAssistantStatus(): AssistantStatusSnapshot { return { activePartType: undefined, activeToolName: undefined, statusText: 'working', isGenericStatus: true }; } - const assistantMessages = sessionMessages - .filter( - (msg): msg is AssistantSessionMessageRecord => - isAssistantMessage(msg.info) && !isFullySyntheticMessage(msg.parts) - ); + // Pick the latest assistant message by created-time (tiebreak by id) in a + // single pass — sync reconciliation can splice messages out of array order. + const isLater = (a: AssistantSessionMessageRecord, b: AssistantSessionMessageRecord) => { + const aTime = typeof a.info.time?.created === 'number' ? a.info.time.created : null; + const bTime = typeof b.info.time?.created === 'number' ? b.info.time.created : null; + if (aTime !== null && bTime !== null && aTime !== bTime) return aTime > bTime; + return a.info.id.localeCompare(b.info.id) > 0; + }; - if (assistantMessages.length === 0) { - return { activePartType: undefined, activeToolName: undefined, statusText: 'working', isGenericStatus: true }; + let lastAssistant: AssistantSessionMessageRecord | null = null; + for (const candidate of sessionMessages) { + if (!isAssistantMessage(candidate.info) || isFullySyntheticMessage(candidate.parts)) { + continue; + } + const typed = candidate as AssistantSessionMessageRecord; + if (lastAssistant === null || isLater(typed, lastAssistant)) { + lastAssistant = typed; + } } - const sortedAssistantMessages = [...assistantMessages].sort((a, b) => { - const aCreated = typeof a.info.time?.created === 'number' ? a.info.time.created : null; - const bCreated = typeof b.info.time?.created === 'number' ? b.info.time.created : null; - - if (aCreated !== null && bCreated !== null && aCreated !== bCreated) { - return aCreated - bCreated; - } - - return a.info.id.localeCompare(b.info.id); - }); - - const lastAssistant = sortedAssistantMessages[sortedAssistantMessages.length - 1]; + if (!lastAssistant) { + return { activePartType: undefined, activeToolName: undefined, statusText: 'working', isGenericStatus: true }; + } let activePartType: 'text' | 'tool' | 'reasoning' | 'editing' | undefined = undefined; let activeToolName: string | undefined = undefined;