mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +08:00
fix: merge streamed tail text into the previous assistant bubble
Keep late-arriving streamed punctuation attached to the active assistant reply so desktop chat no longer renders the final character as its own bubble. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5a86ab097f
commit
144182db63
@ -385,6 +385,95 @@ describe('chatStore history mapping', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('merges trailing streamed punctuation into the previous assistant bubble on completion', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[TEST_SESSION_ID]: {
|
||||
messages: [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
type: 'assistant_text',
|
||||
content: '你好',
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
chatState: 'streaming',
|
||||
connectionState: 'connected',
|
||||
streamingText: '。',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
agentTaskNotifications: {},
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'message_complete',
|
||||
usage: { input_tokens: 1, output_tokens: 1 },
|
||||
})
|
||||
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
||||
{
|
||||
type: 'assistant_text',
|
||||
content: '你好。',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('merges streamed tail text when status changes out of streaming', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[TEST_SESSION_ID]: {
|
||||
messages: [
|
||||
{
|
||||
id: 'assistant-1',
|
||||
type: 'assistant_text',
|
||||
content: '分析完成',
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
chatState: 'streaming',
|
||||
connectionState: 'connected',
|
||||
streamingText: '。',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
agentTaskNotifications: {},
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'status',
|
||||
state: 'idle',
|
||||
tokens: 3,
|
||||
})
|
||||
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
||||
{
|
||||
type: 'assistant_text',
|
||||
content: '分析完成。',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('flushes the previous assistant draft before starting a new user turn', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
|
||||
@ -108,6 +108,29 @@ const pendingTaskToolUseIds = new Set<string>()
|
||||
let msgCounter = 0
|
||||
const nextId = () => `msg-${++msgCounter}-${Date.now()}`
|
||||
|
||||
function appendAssistantTextMessage(messages: UIMessage[], content: string): UIMessage[] {
|
||||
if (!content) return messages
|
||||
const last = messages[messages.length - 1]
|
||||
if (last?.type === 'assistant_text') {
|
||||
return [
|
||||
...messages.slice(0, -1),
|
||||
{
|
||||
...last,
|
||||
content: last.content + content,
|
||||
},
|
||||
]
|
||||
}
|
||||
return [
|
||||
...messages,
|
||||
{
|
||||
id: nextId(),
|
||||
type: 'assistant_text',
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Streaming throttle for content_delta
|
||||
let pendingDelta = ''
|
||||
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@ -218,15 +241,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
const pendingAssistantText = `${session.streamingText}${bufferedDelta}`.trim()
|
||||
|
||||
const newMessages = pendingAssistantText
|
||||
? [
|
||||
...session.messages,
|
||||
{
|
||||
id: nextId(),
|
||||
type: 'assistant_text' as const,
|
||||
content: pendingAssistantText,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
]
|
||||
? appendAssistantTextMessage(session.messages, pendingAssistantText)
|
||||
: [...session.messages]
|
||||
if (!isMemberSession && allTasksDone) {
|
||||
newMessages.push({
|
||||
@ -392,6 +407,16 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
break
|
||||
|
||||
case 'status':
|
||||
// Flush any pending delta before processing status change
|
||||
if (flushTimer && msg.state !== 'streaming') {
|
||||
clearTimeout(flushTimer)
|
||||
flushTimer = null
|
||||
}
|
||||
if (pendingDelta && msg.state !== 'streaming') {
|
||||
const deltaText = pendingDelta
|
||||
pendingDelta = ''
|
||||
set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + deltaText })) }))
|
||||
}
|
||||
update((session) => {
|
||||
const pendingText = session.streamingText.trim()
|
||||
const shouldFlush = pendingText && session.chatState === 'streaming' && msg.state !== 'streaming'
|
||||
@ -401,7 +426,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
...(msg.tokens ? { tokenUsage: { ...session.tokenUsage, output_tokens: msg.tokens } } : {}),
|
||||
...(msg.state === 'idle' ? { activeThinkingId: null, statusVerb: '' } : {}),
|
||||
...(shouldFlush ? {
|
||||
messages: [...session.messages, { id: nextId(), type: 'assistant_text' as const, content: pendingText, timestamp: Date.now() }],
|
||||
messages: appendAssistantTextMessage(session.messages, pendingText),
|
||||
streamingText: '',
|
||||
} : {}),
|
||||
}
|
||||
@ -423,7 +448,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
const pendingText = session.streamingText.trim()
|
||||
if (pendingText) {
|
||||
update((s) => ({
|
||||
messages: [...s.messages, { id: nextId(), type: 'assistant_text' as const, content: pendingText, timestamp: Date.now() }],
|
||||
messages: appendAssistantTextMessage(s.messages, pendingText),
|
||||
streamingText: '',
|
||||
}))
|
||||
}
|
||||
@ -460,7 +485,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
update((s) => {
|
||||
const pendingText = s.streamingText.trim()
|
||||
const base = pendingText
|
||||
? [...s.messages, { id: nextId(), type: 'assistant_text' as const, content: pendingText, timestamp: Date.now() }]
|
||||
? appendAssistantTextMessage(s.messages, pendingText)
|
||||
: s.messages
|
||||
const last = base[base.length - 1]
|
||||
if (last && last.type === 'thinking') {
|
||||
@ -553,13 +578,25 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
break
|
||||
|
||||
case 'message_complete': {
|
||||
// Flush any pending delta before processing complete
|
||||
if (flushTimer) {
|
||||
clearTimeout(flushTimer)
|
||||
flushTimer = null
|
||||
}
|
||||
if (pendingDelta) {
|
||||
const deltaText = pendingDelta
|
||||
pendingDelta = ''
|
||||
set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + deltaText })) }))
|
||||
}
|
||||
const session = get().sessions[sessionId]
|
||||
if (!session) break
|
||||
const text = session.streamingText
|
||||
const text = session.streamingText.trim()
|
||||
if (text) {
|
||||
update((s) => ({
|
||||
messages: [...s.messages, { id: nextId(), type: 'assistant_text', content: text, timestamp: Date.now() }],
|
||||
streamingText: '',
|
||||
set((s) => ({
|
||||
sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({
|
||||
messages: appendAssistantTextMessage(sess.messages, text),
|
||||
streamingText: '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
if (session.elapsedTimer) clearInterval(session.elapsedTimer)
|
||||
@ -579,7 +616,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
const pendingText = s.streamingText.trim()
|
||||
const newMessages = [...s.messages]
|
||||
if (pendingText) {
|
||||
newMessages.push({ id: nextId(), type: 'assistant_text' as const, content: pendingText, timestamp: Date.now() })
|
||||
newMessages.splice(0, newMessages.length, ...appendAssistantTextMessage(newMessages, pendingText))
|
||||
}
|
||||
newMessages.push({ id: nextId(), type: 'error', message: msg.message, code: msg.code, timestamp: Date.now() })
|
||||
return {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user