From f9581d251681fa3f9e664083b166106e521e10ba Mon Sep 17 00:00:00 2001
From: Felix <24791380+vcfgv@users.noreply.github.com>
Date: Wed, 11 Feb 2026 13:55:34 +0800
Subject: [PATCH] fix(chat): improve handling of streaming messages and add
pending state management (#46)
---
src/pages/Chat/index.tsx | 14 +++++----
src/stores/chat.ts | 62 ++++++++++++++++++++++++++++++++++++----
2 files changed, 65 insertions(+), 11 deletions(-)
diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx
index 4837b934e..e74aa8f4d 100644
--- a/src/pages/Chat/index.tsx
+++ b/src/pages/Chat/index.tsx
@@ -72,7 +72,11 @@ export function Chat() {
}
// Extract streaming text for display
- const streamText = streamingMessage ? extractText(streamingMessage) : '';
+ const streamMsg = streamingMessage && typeof streamingMessage === 'object'
+ ? streamingMessage as unknown as { role?: string; content?: unknown; timestamp?: number }
+ : null;
+ const streamText = streamMsg ? extractText(streamMsg) : (typeof streamingMessage === 'string' ? streamingMessage : '');
+ const hasStreamText = streamText.trim().length > 0;
return (
@@ -101,12 +105,12 @@ export function Chat() {
))}
{/* Streaming message */}
- {sending && streamText && (
+ {sending && hasStreamText && (
)}
>
diff --git a/src/stores/chat.ts b/src/stores/chat.ts
index 9c6baad81..648762f5d 100644
--- a/src/stores/chat.ts
+++ b/src/stores/chat.ts
@@ -48,6 +48,8 @@ interface ChatState {
activeRunId: string | null;
streamingText: string;
streamingMessage: unknown | null;
+ pendingFinal: boolean;
+ lastUserMessageAt: number | null;
// Sessions
sessions: ChatSession[];
@@ -98,6 +100,25 @@ function isToolOnlyMessage(message: RawMessage | undefined): boolean {
return hasTool && !hasText && !hasNonToolContent;
}
+function hasNonToolAssistantContent(message: RawMessage | undefined): boolean {
+ if (!message) return false;
+ if (typeof message.content === 'string' && message.content.trim()) return true;
+
+ const content = message.content;
+ if (Array.isArray(content)) {
+ for (const block of content as ContentBlock[]) {
+ if (block.type === 'text' && block.text && block.text.trim()) return true;
+ if (block.type === 'thinking' && block.thinking && block.thinking.trim()) return true;
+ if (block.type === 'image') return true;
+ }
+ }
+
+ const msg = message as unknown as Record;
+ if (typeof msg.text === 'string' && msg.text.trim()) return true;
+
+ return false;
+}
+
// ── Store ────────────────────────────────────────────────────────
export const useChatStore = create((set, get) => ({
@@ -109,6 +130,8 @@ export const useChatStore = create((set, get) => ({
activeRunId: null,
streamingText: '',
streamingMessage: null,
+ pendingFinal: false,
+ lastUserMessageAt: null,
sessions: [],
currentSessionKey: 'main',
@@ -182,6 +205,8 @@ export const useChatStore = create((set, get) => ({
streamingMessage: null,
activeRunId: null,
error: null,
+ pendingFinal: false,
+ lastUserMessageAt: null,
});
// Load history for new session
get().loadHistory();
@@ -199,6 +224,8 @@ export const useChatStore = create((set, get) => ({
streamingMessage: null,
activeRunId: null,
error: null,
+ pendingFinal: false,
+ lastUserMessageAt: null,
});
// Reload sessions list to include the new one after first message
get().loadSessions();
@@ -222,6 +249,18 @@ export const useChatStore = create((set, get) => ({
const rawMessages = Array.isArray(data.messages) ? data.messages as RawMessage[] : [];
const thinkingLevel = data.thinkingLevel ? String(data.thinkingLevel) : null;
set({ messages: rawMessages, thinkingLevel, loading: false });
+ const { pendingFinal, lastUserMessageAt } = get();
+ if (pendingFinal) {
+ const recentAssistant = [...rawMessages].reverse().find((msg) => {
+ if (msg.role !== 'assistant') return false;
+ if (!hasNonToolAssistantContent(msg)) return false;
+ if (lastUserMessageAt && msg.timestamp && msg.timestamp < lastUserMessageAt) return false;
+ return true;
+ });
+ if (recentAssistant) {
+ set({ sending: false, activeRunId: null, pendingFinal: false });
+ }
+ }
} else {
set({ messages: [], loading: false });
}
@@ -252,6 +291,8 @@ export const useChatStore = create((set, get) => ({
error: null,
streamingText: '',
streamingMessage: null,
+ pendingFinal: false,
+ lastUserMessageAt: userMsg.timestamp ?? null,
}));
try {
@@ -295,7 +336,7 @@ export const useChatStore = create((set, get) => ({
abortRun: async () => {
const { currentSessionKey } = get();
- set({ sending: false, streamingText: '', streamingMessage: null });
+ set({ sending: false, streamingText: '', streamingMessage: null, pendingFinal: false, lastUserMessageAt: null });
try {
await window.electron.ipcRenderer.invoke(
@@ -331,6 +372,7 @@ export const useChatStore = create((set, get) => ({
const finalMsg = event.message as RawMessage | undefined;
if (finalMsg) {
const toolOnly = isToolOnlyMessage(finalMsg);
+ const hasOutput = hasNonToolAssistantContent(finalMsg);
const msgId = finalMsg.id || (toolOnly ? `run-${runId}-tool-${Date.now()}` : `run-${runId}`);
set((s) => {
// Check if message already exists (prevent duplicates)
@@ -340,11 +382,13 @@ export const useChatStore = create((set, get) => ({
return toolOnly ? {
streamingText: '',
streamingMessage: null,
+ pendingFinal: true,
} : {
streamingText: '',
streamingMessage: null,
- sending: false,
- activeRunId: null,
+ sending: hasOutput ? false : s.sending,
+ activeRunId: hasOutput ? null : s.activeRunId,
+ pendingFinal: hasOutput ? false : true,
};
}
return toolOnly ? {
@@ -355,6 +399,7 @@ export const useChatStore = create((set, get) => ({
}],
streamingText: '',
streamingMessage: null,
+ pendingFinal: true,
} : {
messages: [...s.messages, {
...finalMsg,
@@ -363,13 +408,14 @@ export const useChatStore = create((set, get) => ({
}],
streamingText: '',
streamingMessage: null,
- sending: false,
- activeRunId: null,
+ sending: hasOutput ? false : s.sending,
+ activeRunId: hasOutput ? null : s.activeRunId,
+ pendingFinal: hasOutput ? false : true,
};
});
} else {
// No message in final event - reload history to get complete data
- set({ streamingText: '', streamingMessage: null, sending: false, activeRunId: null });
+ set({ streamingText: '', streamingMessage: null, pendingFinal: true });
get().loadHistory();
}
break;
@@ -382,6 +428,8 @@ export const useChatStore = create((set, get) => ({
activeRunId: null,
streamingText: '',
streamingMessage: null,
+ pendingFinal: false,
+ lastUserMessageAt: null,
});
break;
}
@@ -391,6 +439,8 @@ export const useChatStore = create((set, get) => ({
activeRunId: null,
streamingText: '',
streamingMessage: null,
+ pendingFinal: false,
+ lastUserMessageAt: null,
});
break;
}