fmt(js): npm run fix on merge (#68135)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
nousbot-eng 2026-07-20 12:46:35 -04:00 committed by GitHub
parent 470c7e2a60
commit aa274364bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 42 additions and 30 deletions

View file

@ -409,6 +409,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
if (sessionId) {
flushQueuedDeltas(sessionId)
const text = coerceGatewayText(payload?.text)
if (text) {
finalizeInterimAssistantMessage(sessionId, text)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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, '')

View file

@ -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 => {

View file

@ -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 ?? {})

View file

@ -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 <Md>; the gateway's Rich-rendered ANSI
@ -687,6 +692,7 @@ class TurnController {
}
const authoritativeText = text.trimStart()
if (!authoritativeText) {
return
}