@assistkick/create 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +3 -3
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +3 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +27 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +3 -3
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +81 -84
package/package.json
CHANGED
package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* text blocks, tool use cards, and tool result blocks.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { useState, useCallback } from 'react';
|
|
12
|
+
import { memo, useState, useCallback } from 'react';
|
|
13
13
|
import type { ChatMessage, TextBlock, ImageBlock, FileBlock } from '../hooks/use_chat_stream';
|
|
14
14
|
import { ChatMessageContent } from './ChatMessageContent';
|
|
15
15
|
import { HighlightedText } from './HighlightedText';
|
|
@@ -41,12 +41,12 @@ const getFileTypeLabel = (mimeType: string): string => {
|
|
|
41
41
|
return 'FILE';
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
export function ChatMessageBubble({ message, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageBubbleProps) {
|
|
44
|
+
export const ChatMessageBubble = memo(function ChatMessageBubble({ message, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageBubbleProps) {
|
|
45
45
|
if (message.role === 'user') {
|
|
46
46
|
return <UserBubble message={message} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />;
|
|
47
47
|
}
|
|
48
48
|
return <AssistantBubble message={message} searchQuery={searchQuery} activeMatchIndex={activeMatchIndex} matchIndexOffset={matchIndexOffset} />;
|
|
49
|
-
}
|
|
49
|
+
});
|
|
50
50
|
|
|
51
51
|
function UserBubble({ message, searchQuery, activeMatchIndex, matchIndexOffset }: { message: ChatMessage; searchQuery: string; activeMatchIndex: number; matchIndexOffset: number }) {
|
|
52
52
|
const textBlock = message.content.find((b) => b.type === 'text') as TextBlock | undefined;
|
package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - ToolResultBlock → collapsed ToolResultCard (shows tool output)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { memo } from 'react';
|
|
10
11
|
import ReactMarkdown from 'react-markdown';
|
|
11
12
|
import remarkGfm from 'remark-gfm';
|
|
12
13
|
import type { ContentBlock, ToolResultBlock, ToolUseBlock } from '../hooks/use_chat_stream';
|
|
@@ -67,7 +68,7 @@ const chatMarkdownClass = [
|
|
|
67
68
|
].join(' ');
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
export const ChatMessageContent = ({ content, isStreaming, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageContentProps) => {
|
|
71
|
+
export const ChatMessageContent = memo(({ content, isStreaming, searchQuery = '', activeMatchIndex = -1, matchIndexOffset = 0 }: ChatMessageContentProps) => {
|
|
71
72
|
const resultMap = buildResultMap(content);
|
|
72
73
|
|
|
73
74
|
// Pre-compute per-block match offsets so each text block knows its global offset
|
|
@@ -139,4 +140,4 @@ export const ChatMessageContent = ({ content, isStreaming, searchQuery = '', act
|
|
|
139
140
|
})}
|
|
140
141
|
</div>
|
|
141
142
|
);
|
|
142
|
-
};
|
|
143
|
+
});
|
|
@@ -75,6 +75,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
75
75
|
loadMessages,
|
|
76
76
|
attachSession,
|
|
77
77
|
messages,
|
|
78
|
+
streamingMessage,
|
|
78
79
|
streaming,
|
|
79
80
|
connected,
|
|
80
81
|
error,
|
|
@@ -136,12 +137,18 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
136
137
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
|
137
138
|
const sessionRestoredRef = useRef(false);
|
|
138
139
|
|
|
140
|
+
// Combined messages for search and sidebar (historical + streaming)
|
|
141
|
+
const allMessages = useMemo(
|
|
142
|
+
() => streamingMessage ? [...messages, streamingMessage] : messages,
|
|
143
|
+
[messages, streamingMessage],
|
|
144
|
+
);
|
|
145
|
+
|
|
139
146
|
// --- Search: compute total matches and per-message offsets ---
|
|
140
147
|
const { totalMatches, messageOffsets } = useMemo(() => {
|
|
141
148
|
if (!searchQuery) return { totalMatches: 0, messageOffsets: [] as number[] };
|
|
142
149
|
let total = 0;
|
|
143
150
|
const offsets: number[] = [];
|
|
144
|
-
for (const msg of
|
|
151
|
+
for (const msg of allMessages) {
|
|
145
152
|
offsets.push(total);
|
|
146
153
|
for (const block of msg.content) {
|
|
147
154
|
if (block.type === 'text') {
|
|
@@ -150,7 +157,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
159
|
return { totalMatches: total, messageOffsets: offsets };
|
|
153
|
-
}, [
|
|
160
|
+
}, [allMessages, searchQuery]);
|
|
154
161
|
|
|
155
162
|
// Reset active match when query changes
|
|
156
163
|
useEffect(() => {
|
|
@@ -320,7 +327,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
320
327
|
// Auto-scroll to bottom when new messages arrive
|
|
321
328
|
useEffect(() => {
|
|
322
329
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
323
|
-
}, [messages, queue]);
|
|
330
|
+
}, [messages, streamingMessage, queue]);
|
|
324
331
|
|
|
325
332
|
// Keep a ref to sessions so the onStreamEnd callback can look up session details
|
|
326
333
|
// without stale closure issues.
|
|
@@ -366,7 +373,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
366
373
|
// Save current session's input, messages, context usage, and system prompt
|
|
367
374
|
if (activeSessionId) {
|
|
368
375
|
inputCacheRef.current.set(activeSessionId, inputValue);
|
|
369
|
-
messagesCacheRef.current.set(activeSessionId, messages);
|
|
376
|
+
messagesCacheRef.current.set(activeSessionId, streamingMessage ? [...messages, streamingMessage] : messages);
|
|
370
377
|
if (contextUsage) contextUsageCacheRef.current.set(activeSessionId, contextUsage);
|
|
371
378
|
if (systemPrompt) systemPromptCacheRef.current.set(activeSessionId, systemPrompt);
|
|
372
379
|
}
|
|
@@ -424,7 +431,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
424
431
|
});
|
|
425
432
|
}
|
|
426
433
|
},
|
|
427
|
-
[activeSessionId, inputValue, messages, contextUsage, systemPrompt, clearMessages, restoreMessages, clearAttachments, loadMessages, attachSession, initContextUsage, initSystemPrompt, setQueueActiveSession],
|
|
434
|
+
[activeSessionId, inputValue, messages, streamingMessage, contextUsage, systemPrompt, clearMessages, restoreMessages, clearAttachments, loadMessages, attachSession, initContextUsage, initSystemPrompt, setQueueActiveSession],
|
|
428
435
|
);
|
|
429
436
|
|
|
430
437
|
// Handle selecting a newly created session
|
|
@@ -433,7 +440,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
433
440
|
// Save current session's state before switching
|
|
434
441
|
if (activeSessionId) {
|
|
435
442
|
inputCacheRef.current.set(activeSessionId, inputValue);
|
|
436
|
-
messagesCacheRef.current.set(activeSessionId, messages);
|
|
443
|
+
messagesCacheRef.current.set(activeSessionId, streamingMessage ? [...messages, streamingMessage] : messages);
|
|
437
444
|
if (contextUsage) contextUsageCacheRef.current.set(activeSessionId, contextUsage);
|
|
438
445
|
if (systemPrompt) systemPromptCacheRef.current.set(activeSessionId, systemPrompt);
|
|
439
446
|
}
|
|
@@ -445,7 +452,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
445
452
|
setIsNewSession(true);
|
|
446
453
|
setInputValue('');
|
|
447
454
|
},
|
|
448
|
-
[activeSessionId, inputValue, messages, contextUsage, systemPrompt, clearMessages, clearAttachments, initSystemPrompt, setQueueActiveSession],
|
|
455
|
+
[activeSessionId, inputValue, messages, streamingMessage, contextUsage, systemPrompt, clearMessages, clearAttachments, initSystemPrompt, setQueueActiveSession],
|
|
449
456
|
);
|
|
450
457
|
|
|
451
458
|
// File handling for attachments
|
|
@@ -691,6 +698,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
691
698
|
<SystemPromptAccordion prompt={displayedSystemPrompt} />
|
|
692
699
|
)}
|
|
693
700
|
|
|
701
|
+
{/* Historical messages — stable array, memoized components skip re-renders */}
|
|
694
702
|
{messages.map((msg, idx) => (
|
|
695
703
|
<ChatMessageBubble
|
|
696
704
|
key={msg.id}
|
|
@@ -701,6 +709,17 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
701
709
|
/>
|
|
702
710
|
))}
|
|
703
711
|
|
|
712
|
+
{/* Streaming message — isolated, only this re-renders per token */}
|
|
713
|
+
{streamingMessage && (
|
|
714
|
+
<ChatMessageBubble
|
|
715
|
+
key={streamingMessage.id}
|
|
716
|
+
message={streamingMessage}
|
|
717
|
+
searchQuery={searchQuery}
|
|
718
|
+
activeMatchIndex={activeMatchIndex}
|
|
719
|
+
matchIndexOffset={messageOffsets[messages.length] ?? 0}
|
|
720
|
+
/>
|
|
721
|
+
)}
|
|
722
|
+
|
|
704
723
|
{/* Queued messages */}
|
|
705
724
|
{queue.map((qm) => (
|
|
706
725
|
<QueuedMessageBubble
|
|
@@ -716,7 +735,7 @@ export function ChatView({ visible }: ChatViewProps) {
|
|
|
716
735
|
</div>
|
|
717
736
|
|
|
718
737
|
{/* Todo sidebar — shown when tasks exist */}
|
|
719
|
-
<ChatTodoSidebar messages={
|
|
738
|
+
<ChatTodoSidebar messages={allMessages} />
|
|
720
739
|
</div>
|
|
721
740
|
|
|
722
741
|
{/* Error banner */}
|
package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* content-secondary text, monospace font at 13px.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { useState } from 'react';
|
|
10
|
+
import { memo, useState } from 'react';
|
|
11
11
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
12
12
|
import { summarizeToolUse, toolIcon } from '../lib/tool_use_summary';
|
|
13
13
|
import { ToolDiffView } from './ToolDiffView';
|
|
@@ -64,7 +64,7 @@ const hasIntegratedView = (name: string): boolean =>
|
|
|
64
64
|
const hasGenericView = (name: string): boolean =>
|
|
65
65
|
!['Edit', 'Write', 'Read', 'Bash', 'Agent'].includes(name);
|
|
66
66
|
|
|
67
|
-
export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardProps) => {
|
|
67
|
+
export const ToolUseCard = memo(({ name, input, isStreaming, result }: ToolUseCardProps) => {
|
|
68
68
|
const [expanded, setExpanded] = useState(false);
|
|
69
69
|
const [viewTab, setViewTab] = useState<ViewTab>('humanized');
|
|
70
70
|
const summary = summarizeToolUse(name, input);
|
|
@@ -160,4 +160,4 @@ export const ToolUseCard = ({ name, input, isStreaming, result }: ToolUseCardPro
|
|
|
160
160
|
)}
|
|
161
161
|
</div>
|
|
162
162
|
);
|
|
163
|
-
};
|
|
163
|
+
});
|
|
@@ -214,6 +214,8 @@ export interface UseChatStreamReturn {
|
|
|
214
214
|
/** Attempt to re-attach to an in-flight stream after page reload. */
|
|
215
215
|
attachSession: (claudeSessionId: string) => void;
|
|
216
216
|
messages: ChatMessage[];
|
|
217
|
+
/** The currently streaming assistant message (separate from messages for render isolation). */
|
|
218
|
+
streamingMessage: ChatMessage | null;
|
|
217
219
|
streaming: boolean;
|
|
218
220
|
connected: boolean;
|
|
219
221
|
error: string | null;
|
|
@@ -355,6 +357,11 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
355
357
|
const [connected, setConnected] = useState(false);
|
|
356
358
|
const [streaming, setStreaming] = useState(false);
|
|
357
359
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
360
|
+
/** The currently streaming assistant message, isolated from historical messages
|
|
361
|
+
* so that per-token updates don't trigger re-renders of the entire message list. */
|
|
362
|
+
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
|
363
|
+
/** Ref mirror of streamingMessage for synchronous reads in event handlers. */
|
|
364
|
+
const streamingMessageRef = useRef<ChatMessage | null>(null);
|
|
358
365
|
const [error, setError] = useState<string | null>(null);
|
|
359
366
|
const [pendingPermission, setPendingPermission] = useState<PendingPermission | null>(null);
|
|
360
367
|
const [cancelledUserMessage, setCancelledUserMessage] = useState<string | null>(null);
|
|
@@ -394,17 +401,17 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
394
401
|
}
|
|
395
402
|
|
|
396
403
|
switch (msg.type) {
|
|
397
|
-
case 'stream_start':
|
|
404
|
+
case 'stream_start': {
|
|
398
405
|
// Only process if this event is for our active session
|
|
399
406
|
if (activeClaudeSessionRef.current && msg.claudeSessionId !== activeClaudeSessionRef.current) break;
|
|
400
407
|
setStreaming(true);
|
|
401
408
|
setError(null);
|
|
402
|
-
// Create an
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
]);
|
|
409
|
+
// Create an isolated streaming message (not in the messages array)
|
|
410
|
+
const startMsg: ChatMessage = {id: nextMessageId(), role: 'assistant', content: [], isStreaming: true};
|
|
411
|
+
streamingMessageRef.current = startMsg;
|
|
412
|
+
setStreamingMessage(startMsg);
|
|
407
413
|
break;
|
|
414
|
+
}
|
|
408
415
|
|
|
409
416
|
case 'stream_event':
|
|
410
417
|
// Only process events for our active session — prevents cross-session leaking
|
|
@@ -412,7 +419,7 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
412
419
|
handleStreamEvent(msg.event);
|
|
413
420
|
break;
|
|
414
421
|
|
|
415
|
-
case 'stream_end':
|
|
422
|
+
case 'stream_end': {
|
|
416
423
|
// Only process if this event is for our active session
|
|
417
424
|
if (activeClaudeSessionRef.current && msg.claudeSessionId !== activeClaudeSessionRef.current) break;
|
|
418
425
|
setStreaming(false);
|
|
@@ -421,40 +428,29 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
421
428
|
if (msg.error) {
|
|
422
429
|
setError(msg.error);
|
|
423
430
|
}
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
return updated;
|
|
434
|
-
});
|
|
431
|
+
// Merge the streaming message into historical messages
|
|
432
|
+
const endMsg = streamingMessageRef.current;
|
|
433
|
+
if (endMsg) {
|
|
434
|
+
setMessages(prev => [...prev, {...endMsg, isStreaming: false}]);
|
|
435
|
+
}
|
|
436
|
+
streamingMessageRef.current = null;
|
|
437
|
+
setStreamingMessage(null);
|
|
435
438
|
// Notify consumers (e.g. message queue) that streaming has ended
|
|
436
439
|
streamEndCallbackRef.current?.();
|
|
437
440
|
break;
|
|
441
|
+
}
|
|
438
442
|
|
|
439
443
|
case 'stream_cancelled':
|
|
440
444
|
// Only process if this event is for our active session
|
|
441
445
|
if (activeClaudeSessionRef.current && msg.claudeSessionId !== activeClaudeSessionRef.current) break;
|
|
442
446
|
setStreaming(false);
|
|
443
447
|
activeClaudeSessionRef.current = null;
|
|
444
|
-
// Discard the
|
|
445
|
-
|
|
448
|
+
// Discard the streaming assistant message
|
|
449
|
+
streamingMessageRef.current = null;
|
|
450
|
+
setStreamingMessage(null);
|
|
451
|
+
// Remove the last user message and prepopulate the input field
|
|
446
452
|
setMessages(prev => {
|
|
447
453
|
const updated = [...prev];
|
|
448
|
-
|
|
449
|
-
// Remove the last streaming assistant message (partial response)
|
|
450
|
-
for (let i = updated.length - 1; i >= 0; i--) {
|
|
451
|
-
if (updated[i].role === 'assistant' && updated[i].isStreaming) {
|
|
452
|
-
updated.splice(i, 1);
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Remove the last user message and capture its text
|
|
458
454
|
for (let i = updated.length - 1; i >= 0; i--) {
|
|
459
455
|
if (updated[i].role === 'user') {
|
|
460
456
|
const userContent = updated[i].content;
|
|
@@ -466,38 +462,46 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
466
462
|
break;
|
|
467
463
|
}
|
|
468
464
|
}
|
|
469
|
-
|
|
470
465
|
return updated;
|
|
471
466
|
});
|
|
472
467
|
break;
|
|
473
468
|
|
|
474
|
-
case 'stream_resumed':
|
|
469
|
+
case 'stream_resumed': {
|
|
475
470
|
// Re-attached to an in-flight stream after page reload or session switch.
|
|
476
|
-
// Update active session to the one we're re-attaching to.
|
|
477
471
|
activeClaudeSessionRef.current = msg.claudeSessionId;
|
|
478
|
-
// Mark the last assistant message as streaming so new events update it.
|
|
479
|
-
// If no assistant message exists (e.g. messages not yet persisted to DB),
|
|
480
|
-
// create an empty streaming assistant message for catch-up content.
|
|
481
472
|
setStreaming(true);
|
|
482
473
|
setError(null);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
case 'stream_error':
|
|
487
|
-
setStreaming(false);
|
|
488
|
-
setError(msg.error);
|
|
489
|
-
// Mark streaming message as done
|
|
474
|
+
// Extract last assistant message from historical messages into streamingMessage,
|
|
475
|
+
// or create a new empty one if none exists.
|
|
476
|
+
streamingMessageRef.current = null;
|
|
490
477
|
setMessages(prev => {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
break;
|
|
478
|
+
for (let i = prev.length - 1; i >= 0; i--) {
|
|
479
|
+
if (prev[i].role === 'assistant') {
|
|
480
|
+
streamingMessageRef.current = {...prev[i], isStreaming: true};
|
|
481
|
+
return [...prev.slice(0, i), ...prev.slice(i + 1)];
|
|
496
482
|
}
|
|
497
483
|
}
|
|
498
|
-
return
|
|
484
|
+
return prev;
|
|
499
485
|
});
|
|
486
|
+
if (!streamingMessageRef.current) {
|
|
487
|
+
streamingMessageRef.current = {id: nextMessageId(), role: 'assistant', content: [], isStreaming: true};
|
|
488
|
+
}
|
|
489
|
+
setStreamingMessage(streamingMessageRef.current);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
case 'stream_error': {
|
|
494
|
+
setStreaming(false);
|
|
495
|
+
setError(msg.error);
|
|
496
|
+
// Merge the streaming message into historical messages (marked as done)
|
|
497
|
+
const errMsg = streamingMessageRef.current;
|
|
498
|
+
if (errMsg) {
|
|
499
|
+
setMessages(prev => [...prev, {...errMsg, isStreaming: false}]);
|
|
500
|
+
}
|
|
501
|
+
streamingMessageRef.current = null;
|
|
502
|
+
setStreamingMessage(null);
|
|
500
503
|
break;
|
|
504
|
+
}
|
|
501
505
|
|
|
502
506
|
case 'permission_request':
|
|
503
507
|
setPendingPermission({
|
|
@@ -539,21 +543,17 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
539
543
|
const blocks = extractContentBlocks(event);
|
|
540
544
|
if (blocks.length === 0) return;
|
|
541
545
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
550
|
-
return updated;
|
|
551
|
-
});
|
|
546
|
+
// Update only the isolated streaming message — historical messages stay untouched
|
|
547
|
+
const current = streamingMessageRef.current;
|
|
548
|
+
if (current) {
|
|
549
|
+
const updated = {...current, content: mergeAssistantContent(current.content, blocks)};
|
|
550
|
+
streamingMessageRef.current = updated;
|
|
551
|
+
setStreamingMessage(updated);
|
|
552
|
+
}
|
|
552
553
|
return;
|
|
553
554
|
}
|
|
554
555
|
|
|
555
|
-
// Process tool result events — merge tool_result blocks into the
|
|
556
|
-
// streaming assistant message so ToolUseCard can show output.
|
|
556
|
+
// Process tool result events — merge tool_result blocks into the streaming message
|
|
557
557
|
if (event.type === 'tool_result' || event.type === 'human' || event.type === 'user') {
|
|
558
558
|
const blocks = extractContentBlocks(event);
|
|
559
559
|
const resultBlocks = blocks.filter(
|
|
@@ -561,30 +561,22 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
561
561
|
);
|
|
562
562
|
if (resultBlocks.length === 0) return;
|
|
563
563
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
updated[i] = {
|
|
579
|
-
...updated[i],
|
|
580
|
-
content: [...updated[i].content, ...newResults],
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
564
|
+
const current = streamingMessageRef.current;
|
|
565
|
+
if (current) {
|
|
566
|
+
const existingToolResultIds = new Set(
|
|
567
|
+
current.content
|
|
568
|
+
.filter((b): b is ToolResultBlock => b.type === 'tool_result')
|
|
569
|
+
.map(b => b.toolUseId),
|
|
570
|
+
);
|
|
571
|
+
const newResults = resultBlocks.filter(
|
|
572
|
+
b => !existingToolResultIds.has(b.toolUseId),
|
|
573
|
+
);
|
|
574
|
+
if (newResults.length > 0) {
|
|
575
|
+
const updated = {...current, content: [...current.content, ...newResults]};
|
|
576
|
+
streamingMessageRef.current = updated;
|
|
577
|
+
setStreamingMessage(updated);
|
|
585
578
|
}
|
|
586
|
-
|
|
587
|
-
});
|
|
579
|
+
}
|
|
588
580
|
}
|
|
589
581
|
|
|
590
582
|
// Extract contextWindow from result event's modelUsage
|
|
@@ -725,11 +717,15 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
725
717
|
|
|
726
718
|
const clearMessages = useCallback(() => {
|
|
727
719
|
setMessages([]);
|
|
720
|
+
streamingMessageRef.current = null;
|
|
721
|
+
setStreamingMessage(null);
|
|
728
722
|
setContextUsage(null);
|
|
729
723
|
}, []);
|
|
730
724
|
|
|
731
725
|
const restoreMessages = useCallback((msgs: ChatMessage[]) => {
|
|
732
726
|
setMessages(msgs);
|
|
727
|
+
streamingMessageRef.current = null;
|
|
728
|
+
setStreamingMessage(null);
|
|
733
729
|
}, []);
|
|
734
730
|
|
|
735
731
|
const initContextUsage = useCallback((usage: ContextUsage | null) => {
|
|
@@ -800,6 +796,7 @@ export const useChatStream = (): UseChatStreamReturn => {
|
|
|
800
796
|
loadMessages,
|
|
801
797
|
attachSession,
|
|
802
798
|
messages,
|
|
799
|
+
streamingMessage,
|
|
803
800
|
streaming,
|
|
804
801
|
connected,
|
|
805
802
|
error,
|