fix(chat): improve handling of streaming messages and add pending state management (#46)
This commit is contained in:
@@ -72,7 +72,11 @@ export function Chat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract streaming text for display
|
// 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 (
|
return (
|
||||||
<div className="flex flex-col -m-6" style={{ height: 'calc(100vh - 2.5rem)' }}>
|
<div className="flex flex-col -m-6" style={{ height: 'calc(100vh - 2.5rem)' }}>
|
||||||
@@ -101,12 +105,12 @@ export function Chat() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Streaming message */}
|
{/* Streaming message */}
|
||||||
{sending && streamText && (
|
{sending && hasStreamText && (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
message={{
|
message={{
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: streamingMessage as unknown as string,
|
content: streamMsg?.content ?? streamText,
|
||||||
timestamp: streamingTimestamp,
|
timestamp: streamMsg?.timestamp ?? streamingTimestamp,
|
||||||
}}
|
}}
|
||||||
showThinking={showThinking}
|
showThinking={showThinking}
|
||||||
isStreaming
|
isStreaming
|
||||||
@@ -114,7 +118,7 @@ export function Chat() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Typing indicator when sending but no stream yet */}
|
{/* Typing indicator when sending but no stream yet */}
|
||||||
{sending && !streamText && (
|
{sending && !hasStreamText && (
|
||||||
<TypingIndicator />
|
<TypingIndicator />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ interface ChatState {
|
|||||||
activeRunId: string | null;
|
activeRunId: string | null;
|
||||||
streamingText: string;
|
streamingText: string;
|
||||||
streamingMessage: unknown | null;
|
streamingMessage: unknown | null;
|
||||||
|
pendingFinal: boolean;
|
||||||
|
lastUserMessageAt: number | null;
|
||||||
|
|
||||||
// Sessions
|
// Sessions
|
||||||
sessions: ChatSession[];
|
sessions: ChatSession[];
|
||||||
@@ -98,6 +100,25 @@ function isToolOnlyMessage(message: RawMessage | undefined): boolean {
|
|||||||
return hasTool && !hasText && !hasNonToolContent;
|
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<string, unknown>;
|
||||||
|
if (typeof msg.text === 'string' && msg.text.trim()) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Store ────────────────────────────────────────────────────────
|
// ── Store ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const useChatStore = create<ChatState>((set, get) => ({
|
export const useChatStore = create<ChatState>((set, get) => ({
|
||||||
@@ -109,6 +130,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
activeRunId: null,
|
activeRunId: null,
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
|
pendingFinal: false,
|
||||||
|
lastUserMessageAt: null,
|
||||||
|
|
||||||
sessions: [],
|
sessions: [],
|
||||||
currentSessionKey: 'main',
|
currentSessionKey: 'main',
|
||||||
@@ -182,6 +205,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
activeRunId: null,
|
activeRunId: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
pendingFinal: false,
|
||||||
|
lastUserMessageAt: null,
|
||||||
});
|
});
|
||||||
// Load history for new session
|
// Load history for new session
|
||||||
get().loadHistory();
|
get().loadHistory();
|
||||||
@@ -199,6 +224,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
activeRunId: null,
|
activeRunId: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
pendingFinal: false,
|
||||||
|
lastUserMessageAt: null,
|
||||||
});
|
});
|
||||||
// Reload sessions list to include the new one after first message
|
// Reload sessions list to include the new one after first message
|
||||||
get().loadSessions();
|
get().loadSessions();
|
||||||
@@ -222,6 +249,18 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
const rawMessages = Array.isArray(data.messages) ? data.messages as RawMessage[] : [];
|
const rawMessages = Array.isArray(data.messages) ? data.messages as RawMessage[] : [];
|
||||||
const thinkingLevel = data.thinkingLevel ? String(data.thinkingLevel) : null;
|
const thinkingLevel = data.thinkingLevel ? String(data.thinkingLevel) : null;
|
||||||
set({ messages: rawMessages, thinkingLevel, loading: false });
|
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 {
|
} else {
|
||||||
set({ messages: [], loading: false });
|
set({ messages: [], loading: false });
|
||||||
}
|
}
|
||||||
@@ -252,6 +291,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
error: null,
|
error: null,
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
|
pendingFinal: false,
|
||||||
|
lastUserMessageAt: userMsg.timestamp ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -295,7 +336,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
|
|
||||||
abortRun: async () => {
|
abortRun: async () => {
|
||||||
const { currentSessionKey } = get();
|
const { currentSessionKey } = get();
|
||||||
set({ sending: false, streamingText: '', streamingMessage: null });
|
set({ sending: false, streamingText: '', streamingMessage: null, pendingFinal: false, lastUserMessageAt: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electron.ipcRenderer.invoke(
|
await window.electron.ipcRenderer.invoke(
|
||||||
@@ -331,6 +372,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
const finalMsg = event.message as RawMessage | undefined;
|
const finalMsg = event.message as RawMessage | undefined;
|
||||||
if (finalMsg) {
|
if (finalMsg) {
|
||||||
const toolOnly = isToolOnlyMessage(finalMsg);
|
const toolOnly = isToolOnlyMessage(finalMsg);
|
||||||
|
const hasOutput = hasNonToolAssistantContent(finalMsg);
|
||||||
const msgId = finalMsg.id || (toolOnly ? `run-${runId}-tool-${Date.now()}` : `run-${runId}`);
|
const msgId = finalMsg.id || (toolOnly ? `run-${runId}-tool-${Date.now()}` : `run-${runId}`);
|
||||||
set((s) => {
|
set((s) => {
|
||||||
// Check if message already exists (prevent duplicates)
|
// Check if message already exists (prevent duplicates)
|
||||||
@@ -340,11 +382,13 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
return toolOnly ? {
|
return toolOnly ? {
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
|
pendingFinal: true,
|
||||||
} : {
|
} : {
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
sending: false,
|
sending: hasOutput ? false : s.sending,
|
||||||
activeRunId: null,
|
activeRunId: hasOutput ? null : s.activeRunId,
|
||||||
|
pendingFinal: hasOutput ? false : true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return toolOnly ? {
|
return toolOnly ? {
|
||||||
@@ -355,6 +399,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
}],
|
}],
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
|
pendingFinal: true,
|
||||||
} : {
|
} : {
|
||||||
messages: [...s.messages, {
|
messages: [...s.messages, {
|
||||||
...finalMsg,
|
...finalMsg,
|
||||||
@@ -363,13 +408,14 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
}],
|
}],
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
sending: false,
|
sending: hasOutput ? false : s.sending,
|
||||||
activeRunId: null,
|
activeRunId: hasOutput ? null : s.activeRunId,
|
||||||
|
pendingFinal: hasOutput ? false : true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No message in final event - reload history to get complete data
|
// 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();
|
get().loadHistory();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -382,6 +428,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
activeRunId: null,
|
activeRunId: null,
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
|
pendingFinal: false,
|
||||||
|
lastUserMessageAt: null,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -391,6 +439,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
activeRunId: null,
|
activeRunId: null,
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
streamingMessage: null,
|
streamingMessage: null,
|
||||||
|
pendingFinal: false,
|
||||||
|
lastUserMessageAt: null,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user