From aa274364bb51ffe49c2a324df59aaae56d7ab947 Mon Sep 17 00:00:00 2001 From: nousbot-eng Date: Mon, 20 Jul 2026 12:46:35 -0400 Subject: [PATCH] fmt(js): `npm run fix` on merge (#68135) Co-authored-by: github-actions[bot] --- .../hooks/use-message-stream/gateway-event.ts | 1 + .../session/hooks/use-message-stream/index.ts | 29 ++++++++++--------- .../interim-sealing.test.tsx | 13 +++++++-- apps/desktop/src/lib/chat-messages.test.ts | 14 ++++----- apps/desktop/src/lib/chat-messages.ts | 5 +--- ui-tui/src/app/createGatewayEventHandler.ts | 2 ++ ui-tui/src/app/turnController.ts | 8 ++++- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts b/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts index 1533181af11c..09ba6bdb52ad 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts @@ -409,6 +409,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) { if (sessionId) { flushQueuedDeltas(sessionId) const text = coerceGatewayText(payload?.text) + if (text) { finalizeInterimAssistantMessage(sessionId, text) } diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/index.ts b/apps/desktop/src/app/session/hooks/use-message-stream/index.ts index 7331ca946267..11a7878af069 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/index.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/index.ts @@ -379,6 +379,7 @@ export function useMessageStream({ } const authoritativeText = renderMediaTags(text).trim() + if (!authoritativeText) { return state } @@ -386,29 +387,30 @@ export function useMessageStream({ const streamId = state.streamId const replaceTextPart = (parts: ChatMessagePart[]) => { - const visibleText = stripGeneratedImageEchoes( - authoritativeText, generatedImageEchoSources(parts) - ).trim() + const visibleText = stripGeneratedImageEchoes(authoritativeText, generatedImageEchoSources(parts)).trim() + return mergeFinalAssistantText(parts, visibleText) } let nextMessages = state.messages + if (streamId && nextMessages.some(m => m.id === streamId)) { // Finalize the existing streaming bubble in place nextMessages = nextMessages.map(m => - m.id === streamId - ? { ...m, parts: replaceTextPart(m.parts), pending: false } - : m + m.id === streamId ? { ...m, parts: replaceTextPart(m.parts), pending: false } : m ) } else { // No streaming bubble — create a standalone interim message - nextMessages = [...nextMessages, { - id: `assistant-interim-${Date.now()}`, - role: 'assistant' as const, - parts: [assistantTextPart(authoritativeText)], - pending: false, - branchGroupId: state.pendingBranchGroup ?? undefined - }] + nextMessages = [ + ...nextMessages, + { + id: `assistant-interim-${Date.now()}`, + role: 'assistant' as const, + parts: [assistantTextPart(authoritativeText)], + pending: false, + branchGroupId: state.pendingBranchGroup ?? undefined + } + ] } return { @@ -451,6 +453,7 @@ export function useMessageStream({ const replaceTextPart = (parts: ChatMessagePart[]) => { const visibleFinalText = stripGeneratedImageEchoes(finalText, generatedImageEchoSources(parts)).trim() + return mergeFinalAssistantText(parts, visibleFinalText) } diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/interim-sealing.test.tsx b/apps/desktop/src/app/session/hooks/use-message-stream/interim-sealing.test.tsx index a31d20a9b4fe..2ee5bb720418 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/interim-sealing.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-message-stream/interim-sealing.test.tsx @@ -4,9 +4,9 @@ import { useEffect, useRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ClientSessionState } from '@/app/types' -import { createClientSessionState } from '@/lib/chat-runtime' import { chatMessageText } from '@/lib/chat-messages' -import { $todosBySession, clearSessionTodos, setSessionTodos } from '@/store/todos' +import { createClientSessionState } from '@/lib/chat-runtime' +import { clearSessionTodos } from '@/store/todos' import type { RpcEvent } from '@/types/hermes' import { useMessageStream } from './index' @@ -55,10 +55,13 @@ async function mountStream() { const start = () => act(() => handleEvent!({ payload: {}, session_id: SID, type: 'message.start' })) const delta = (text: string) => act(() => handleEvent!({ payload: { text }, session_id: SID, type: 'message.delta' })) + const interim = (text: string) => act(() => handleEvent!({ payload: { text, already_streamed: true }, session_id: SID, type: 'message.interim' })) + const complete = (text: string) => act(() => handleEvent!({ payload: { text }, session_id: SID, type: 'message.complete' })) + const completePreviewed = (text: string) => act(() => handleEvent!({ payload: { text, response_previewed: true }, session_id: SID, type: 'message.complete' })) @@ -69,11 +72,13 @@ function getState(): ClientSessionState { function assistantText(): string { const state = getState() const last = [...state.messages].reverse().find(m => m.role === 'assistant' && !m.hidden) + return last ? chatMessageText(last) : '' } function assistantMessages(): string[] { const state = getState() + return state.messages .filter(m => m.role === 'assistant' && !m.hidden) .map(m => chatMessageText(m)) @@ -222,7 +227,9 @@ describe('useMessageStream interim text sealing', () => { // Empty text await act(() => handleEvent!({ payload: { text: '' }, session_id: SID, type: 'message.interim' } as RpcEvent)) // Undefined text - await act(() => handleEvent!({ payload: { text: undefined }, session_id: SID, type: 'message.interim' } as RpcEvent)) + await act(() => + handleEvent!({ payload: { text: undefined }, session_id: SID, type: 'message.interim' } as RpcEvent) + ) // Turn continues without finalizing or throwing expect(getState().busy).toBe(true) diff --git a/apps/desktop/src/lib/chat-messages.test.ts b/apps/desktop/src/lib/chat-messages.test.ts index 64dc414642be..e63fae09cb89 100644 --- a/apps/desktop/src/lib/chat-messages.test.ts +++ b/apps/desktop/src/lib/chat-messages.test.ts @@ -804,10 +804,7 @@ describe('mergeFinalAssistantText', () => { }) it('drops reasoning that the final text fully covers (reasoning ⊆ final)', () => { - const parts = [ - reasoningPart('Let me check the files.'), - { type: 'text' as const, text: 'streamed' } - ] + const parts = [reasoningPart('Let me check the files.'), { type: 'text' as const, text: 'streamed' }] const result = mergeFinalAssistantText(parts, 'Let me check the files. Everything looks good.') @@ -819,7 +816,9 @@ describe('mergeFinalAssistantText', () => { // #61447: a short final ("Done.") must NOT swallow a longer reasoning block // that merely starts with it. const parts = [ - reasoningPart('Done. The root cause was a bare catch block swallowing Stripe errors. The fix adds proper error logging.'), + reasoningPart( + 'Done. The root cause was a bare catch block swallowing Stripe errors. The fix adds proper error logging.' + ), { type: 'text' as const, text: 'streamed' } ] @@ -842,10 +841,7 @@ describe('mergeFinalAssistantText', () => { }) it('handles empty final text', () => { - const parts = [ - { type: 'text' as const, text: 'streamed' }, - reasoningPart('some reasoning') - ] + const parts = [{ type: 'text' as const, text: 'streamed' }, reasoningPart('some reasoning')] const result = mergeFinalAssistantText(parts, '') diff --git a/apps/desktop/src/lib/chat-messages.ts b/apps/desktop/src/lib/chat-messages.ts index 221f6a16a064..4bc50d32569a 100644 --- a/apps/desktop/src/lib/chat-messages.ts +++ b/apps/desktop/src/lib/chat-messages.ts @@ -152,10 +152,7 @@ const normalizeWs = (value: string) => value.replace(/\s+/g, ' ').trim() * - Keeps all other part types (tool-call, image, etc.). * - Appends the final text as a new text part. */ -export function mergeFinalAssistantText( - parts: ChatMessagePart[], - finalText: string -): ChatMessagePart[] { +export function mergeFinalAssistantText(parts: ChatMessagePart[], finalText: string): ChatMessagePart[] { const dedupeReference = normalizeWs(finalText) const kept = parts.filter(part => { diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index d708cc8ccfbb..f1c87665f286 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -948,12 +948,14 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: return case 'message.interim': { const text = ev.payload?.text + if (typeof text === 'string' && text.trim()) { turnController.recordInterimMessage(text) } return } + case 'message.complete': { const { finalMessages, finalText, wasInterrupted } = turnController.recordMessageComplete(ev.payload ?? {}) diff --git a/ui-tui/src/app/turnController.ts b/ui-tui/src/app/turnController.ts index 63acc70276b9..a8afea727d99 100644 --- a/ui-tui/src/app/turnController.ts +++ b/ui-tui/src/app/turnController.ts @@ -555,7 +555,12 @@ class TurnController { this.flushPendingNotice() } - recordMessageComplete(payload: { rendered?: string; reasoning?: string; response_previewed?: boolean; text?: string }) { + recordMessageComplete(payload: { + rendered?: string + reasoning?: string + response_previewed?: boolean + text?: string + }) { this.closeReasoningSegment() // Ink renders markdown via ; the gateway's Rich-rendered ANSI @@ -687,6 +692,7 @@ class TurnController { } const authoritativeText = text.trimStart() + if (!authoritativeText) { return }