@nextclaw/ui 0.5.45 → 0.5.47

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.
Files changed (37) hide show
  1. package/.eslintrc.cjs +9 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/assets/{ChannelsList-CGlQJOKR.js → ChannelsList-B6N0kXyK.js} +1 -1
  4. package/dist/assets/ChatPage-DsDFvVQX.js +32 -0
  5. package/dist/assets/CronConfig-Cbz6V8MU.js +1 -0
  6. package/dist/assets/DocBrowser-hQzP4Iai.js +1 -0
  7. package/dist/assets/{MarketplacePage-_bRwL8Je.js → MarketplacePage-DMoWoU1y.js} +1 -1
  8. package/dist/assets/{ModelConfig-DHLdWlPT.js → ModelConfig-BXjF-qbA.js} +1 -1
  9. package/dist/assets/ProvidersList-D3hfY5U7.js +1 -0
  10. package/dist/assets/RuntimeConfig-DJ7qIejp.js +1 -0
  11. package/dist/assets/{SecretsConfig-CBpnJpdi.js → SecretsConfig-BFDeNvwV.js} +2 -2
  12. package/dist/assets/{SessionsConfig-FmbzF3JO.js → SessionsConfig-CJF7lPkX.js} +2 -2
  13. package/dist/assets/{card-D79QtyfR.js → card-BREZdIEb.js} +1 -1
  14. package/dist/assets/chat-message-pw9oafI4.js +5 -0
  15. package/dist/assets/{index-nfl5TEOq.js → index-uTbQ-MAY.js} +2 -2
  16. package/dist/assets/{label-C0lAPrBs.js → label-CzMB2yjV.js} +1 -1
  17. package/dist/assets/{logos-DdLfIYd-.js → logos-vVtRUuoo.js} +1 -1
  18. package/dist/assets/{page-layout-BuP_1ihv.js → page-layout-B07kdurB.js} +1 -1
  19. package/dist/assets/{switch-Dmt2u3GV.js → switch-Cr6cemeT.js} +1 -1
  20. package/dist/assets/{tabs-custom-s1WUaOad.js → tabs-custom-BzcvgsvR.js} +1 -1
  21. package/dist/assets/{useConfig-BGr-ekoe.js → useConfig-B4Y6cGwc.js} +1 -1
  22. package/dist/assets/{useConfirmDialog-D_YoV8_w.js → useConfirmDialog-Dc5WHCUf.js} +1 -1
  23. package/dist/assets/{vendor-DfLizrKM.js → vendor-Dh04PGww.js} +1 -1
  24. package/dist/index.html +2 -2
  25. package/package.json +1 -1
  26. package/src/components/chat/ChatConversationPanel.tsx +148 -0
  27. package/src/components/chat/ChatPage.tsx +84 -352
  28. package/src/components/chat/ChatSessionsSidebar.tsx +100 -0
  29. package/src/components/chat/ChatThread.tsx +23 -16
  30. package/src/components/chat/useChatStreamController.ts +268 -0
  31. package/src/lib/chat-message.ts +89 -65
  32. package/dist/assets/ChatPage-DNQw6cPm.js +0 -32
  33. package/dist/assets/CronConfig-CZE4jEWp.js +0 -1
  34. package/dist/assets/DocBrowser-BCqnlevu.js +0 -1
  35. package/dist/assets/ProvidersList-BS-3jQfk.js +0 -1
  36. package/dist/assets/RuntimeConfig-C0kVY3Y0.js +0 -1
  37. package/dist/assets/chat-message-D0s61C4e.js +0 -5
@@ -7,7 +7,7 @@ import {
7
7
  extractToolCards,
8
8
  normalizeChatRole,
9
9
  type ChatRole,
10
- type ChatTimelineAssistantFlowItem,
10
+ type ChatTimelineAssistantTurnItem,
11
11
  type ToolCard
12
12
  } from '@/lib/chat-message';
13
13
  import { formatDateTime, t } from '@/lib/i18n';
@@ -188,20 +188,27 @@ function MessageCard({ message }: { message: SessionMessageView }) {
188
188
  );
189
189
  }
190
190
 
191
- function AssistantFlowCard({ item }: { item: ChatTimelineAssistantFlowItem }) {
191
+ function AssistantTurnCard({ item }: { item: ChatTimelineAssistantTurnItem }) {
192
192
  return (
193
193
  <div className="rounded-2xl border px-4 py-3 shadow-sm bg-white text-gray-900 border-gray-200">
194
- {item.primaryText && <MarkdownBlock text={item.primaryText} role="assistant" />}
195
- {item.primaryReasoning && <ReasoningBlock reasoning={item.primaryReasoning} isUser={false} />}
196
- {item.toolCards.length > 0 && (
197
- <div className={cn('space-y-2', (item.primaryText || item.primaryReasoning) && 'mt-3')}>
198
- {item.toolCards.map((card, index) => (
199
- <ToolCardView key={`${card.kind}-${card.name}-${card.callId ?? index}`} card={card} />
200
- ))}
201
- </div>
202
- )}
203
- {item.followupReasoning && <ReasoningBlock reasoning={item.followupReasoning} isUser={false} />}
204
- {item.followupText && <div className="mt-3"><MarkdownBlock text={item.followupText} role="assistant" /></div>}
194
+ <div className="space-y-3">
195
+ {item.segments.map((segment, index) => {
196
+ if (segment.kind === 'assistant_message') {
197
+ const hasText = Boolean(segment.text);
198
+ const hasReasoning = Boolean(segment.reasoning);
199
+ if (!hasText && !hasReasoning) {
200
+ return null;
201
+ }
202
+ return (
203
+ <div key={`${segment.key}-${index}`}>
204
+ {hasText && <MarkdownBlock text={segment.text} role="assistant" />}
205
+ {hasReasoning && <ReasoningBlock reasoning={segment.reasoning} isUser={false} />}
206
+ </div>
207
+ );
208
+ }
209
+ return <ToolCardView key={`${segment.key}-${index}`} card={segment.card} />;
210
+ })}
211
+ </div>
205
212
  </div>
206
213
  );
207
214
  }
@@ -212,14 +219,14 @@ export function ChatThread({ events, isSending, className }: ChatThreadProps) {
212
219
  return (
213
220
  <div className={cn('space-y-5', className)}>
214
221
  {timeline.map((item) => {
215
- const role = item.kind === 'assistant_flow' ? 'assistant' : item.role;
222
+ const role = item.kind === 'assistant_turn' ? 'assistant' : item.role;
216
223
  const isUser = role === 'user';
217
224
  return (
218
225
  <div key={item.key} className={cn('flex gap-3', isUser ? 'justify-end' : 'justify-start')}>
219
226
  {!isUser && <RoleAvatar role={role} />}
220
227
  <div className={cn('max-w-[88%] min-w-[280px] space-y-2', isUser && 'flex flex-col items-end')}>
221
- {item.kind === 'assistant_flow' ? (
222
- <AssistantFlowCard item={item} />
228
+ {item.kind === 'assistant_turn' ? (
229
+ <AssistantTurnCard item={item} />
223
230
  ) : (
224
231
  <MessageCard message={item.message} />
225
232
  )}
@@ -0,0 +1,268 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
+ import type { SessionEventView } from '@/api/types';
4
+ import { sendChatTurnStream } from '@/api/config';
5
+
6
+ type PendingChatMessage = {
7
+ id: number;
8
+ message: string;
9
+ sessionKey: string;
10
+ agentId: string;
11
+ };
12
+
13
+ type SendMessageParams = {
14
+ message: string;
15
+ sessionKey: string;
16
+ agentId: string;
17
+ restoreDraftOnError?: boolean;
18
+ };
19
+
20
+ type UseChatStreamControllerParams = {
21
+ nextOptimisticUserSeq: number;
22
+ selectedSessionKeyRef: MutableRefObject<string | null>;
23
+ setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
24
+ setDraft: Dispatch<SetStateAction<string>>;
25
+ refetchSessions: () => Promise<unknown>;
26
+ refetchHistory: () => Promise<unknown>;
27
+ };
28
+
29
+ type StreamSetters = {
30
+ setOptimisticUserEvent: Dispatch<SetStateAction<SessionEventView | null>>;
31
+ setStreamingSessionEvents: Dispatch<SetStateAction<SessionEventView[]>>;
32
+ setStreamingAssistantText: Dispatch<SetStateAction<string>>;
33
+ setStreamingAssistantTimestamp: Dispatch<SetStateAction<string | null>>;
34
+ setIsSending: Dispatch<SetStateAction<boolean>>;
35
+ setIsAwaitingAssistantOutput: Dispatch<SetStateAction<boolean>>;
36
+ };
37
+
38
+ function clearStreamingState(setters: StreamSetters) {
39
+ setters.setIsSending(false);
40
+ setters.setOptimisticUserEvent(null);
41
+ setters.setStreamingSessionEvents([]);
42
+ setters.setStreamingAssistantText('');
43
+ setters.setStreamingAssistantTimestamp(null);
44
+ setters.setIsAwaitingAssistantOutput(false);
45
+ }
46
+
47
+ async function executeSendRun(params: {
48
+ item: PendingChatMessage;
49
+ runId: number;
50
+ runIdRef: MutableRefObject<number>;
51
+ nextOptimisticUserSeq: number;
52
+ selectedSessionKeyRef: MutableRefObject<string | null>;
53
+ setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
54
+ setDraft: Dispatch<SetStateAction<string>>;
55
+ refetchSessions: () => Promise<unknown>;
56
+ refetchHistory: () => Promise<unknown>;
57
+ restoreDraftOnError?: boolean;
58
+ setters: StreamSetters;
59
+ }): Promise<void> {
60
+ const {
61
+ item,
62
+ runId,
63
+ runIdRef,
64
+ nextOptimisticUserSeq,
65
+ selectedSessionKeyRef,
66
+ setSelectedSessionKey,
67
+ setDraft,
68
+ refetchSessions,
69
+ refetchHistory,
70
+ restoreDraftOnError,
71
+ setters
72
+ } = params;
73
+
74
+ setters.setStreamingSessionEvents([]);
75
+ setters.setStreamingAssistantText('');
76
+ setters.setStreamingAssistantTimestamp(null);
77
+ setters.setOptimisticUserEvent({
78
+ seq: nextOptimisticUserSeq,
79
+ type: 'message.user.optimistic',
80
+ timestamp: new Date().toISOString(),
81
+ message: {
82
+ role: 'user',
83
+ content: item.message,
84
+ timestamp: new Date().toISOString()
85
+ }
86
+ });
87
+ setters.setIsSending(true);
88
+ setters.setIsAwaitingAssistantOutput(true);
89
+
90
+ try {
91
+ let streamText = '';
92
+ const streamTimestamp = new Date().toISOString();
93
+ setters.setStreamingAssistantTimestamp(streamTimestamp);
94
+
95
+ const result = await sendChatTurnStream({
96
+ message: item.message,
97
+ sessionKey: item.sessionKey,
98
+ agentId: item.agentId,
99
+ channel: 'ui',
100
+ chatId: 'web-ui'
101
+ }, {
102
+ onReady: (event) => {
103
+ if (runId !== runIdRef.current) {
104
+ return;
105
+ }
106
+ if (event.sessionKey) {
107
+ setSelectedSessionKey((prev) => prev === event.sessionKey ? prev : event.sessionKey);
108
+ }
109
+ },
110
+ onDelta: (event) => {
111
+ if (runId !== runIdRef.current) {
112
+ return;
113
+ }
114
+ streamText += event.delta;
115
+ setters.setStreamingAssistantText(streamText);
116
+ setters.setIsAwaitingAssistantOutput(false);
117
+ },
118
+ onSessionEvent: (event) => {
119
+ if (runId !== runIdRef.current) {
120
+ return;
121
+ }
122
+ if (event.data.message?.role === 'user') {
123
+ setters.setOptimisticUserEvent(null);
124
+ }
125
+ setters.setStreamingSessionEvents((prev) => {
126
+ const next = [...prev];
127
+ const hit = next.findIndex((streamEvent) => streamEvent.seq === event.data.seq);
128
+ if (hit >= 0) {
129
+ next[hit] = event.data;
130
+ } else {
131
+ next.push(event.data);
132
+ }
133
+ return next;
134
+ });
135
+ if (event.data.message?.role === 'assistant') {
136
+ // Reset delta accumulator once assistant event lands in session timeline.
137
+ streamText = '';
138
+ setters.setStreamingAssistantText('');
139
+ setters.setIsAwaitingAssistantOutput(false);
140
+ }
141
+ }
142
+ });
143
+ if (runId !== runIdRef.current) {
144
+ return;
145
+ }
146
+ setters.setOptimisticUserEvent(null);
147
+ if (result.sessionKey !== item.sessionKey) {
148
+ setSelectedSessionKey(result.sessionKey);
149
+ }
150
+ await refetchSessions();
151
+ const activeSessionKey = selectedSessionKeyRef.current;
152
+ if (!activeSessionKey || activeSessionKey === item.sessionKey || activeSessionKey === result.sessionKey) {
153
+ await refetchHistory();
154
+ }
155
+ setters.setStreamingSessionEvents([]);
156
+ setters.setStreamingAssistantText('');
157
+ setters.setStreamingAssistantTimestamp(null);
158
+ setters.setIsAwaitingAssistantOutput(false);
159
+ setters.setIsSending(false);
160
+ } catch {
161
+ if (runId !== runIdRef.current) {
162
+ return;
163
+ }
164
+ runIdRef.current += 1;
165
+ clearStreamingState(setters);
166
+ if (restoreDraftOnError) {
167
+ setDraft((prev) => prev.trim().length === 0 ? item.message : prev);
168
+ }
169
+ }
170
+ }
171
+
172
+ export function useChatStreamController(params: UseChatStreamControllerParams) {
173
+ const [optimisticUserEvent, setOptimisticUserEvent] = useState<SessionEventView | null>(null);
174
+ const [streamingSessionEvents, setStreamingSessionEvents] = useState<SessionEventView[]>([]);
175
+ const [streamingAssistantText, setStreamingAssistantText] = useState('');
176
+ const [streamingAssistantTimestamp, setStreamingAssistantTimestamp] = useState<string | null>(null);
177
+ const [isSending, setIsSending] = useState(false);
178
+ const [isAwaitingAssistantOutput, setIsAwaitingAssistantOutput] = useState(false);
179
+ const [queuedMessages, setQueuedMessages] = useState<PendingChatMessage[]>([]);
180
+
181
+ const streamRunIdRef = useRef(0);
182
+ const queueIdRef = useRef(0);
183
+
184
+ const resetStreamState = useCallback(() => {
185
+ streamRunIdRef.current += 1;
186
+ setQueuedMessages([]);
187
+ clearStreamingState({
188
+ setOptimisticUserEvent,
189
+ setStreamingSessionEvents,
190
+ setStreamingAssistantText,
191
+ setStreamingAssistantTimestamp,
192
+ setIsSending,
193
+ setIsAwaitingAssistantOutput
194
+ });
195
+ }, []);
196
+
197
+ useEffect(() => {
198
+ return () => {
199
+ streamRunIdRef.current += 1;
200
+ };
201
+ }, []);
202
+
203
+ const runSend = useCallback(
204
+ async (item: PendingChatMessage, options?: { restoreDraftOnError?: boolean }) => {
205
+ streamRunIdRef.current += 1;
206
+ await executeSendRun({
207
+ item,
208
+ runId: streamRunIdRef.current,
209
+ runIdRef: streamRunIdRef,
210
+ nextOptimisticUserSeq: params.nextOptimisticUserSeq,
211
+ selectedSessionKeyRef: params.selectedSessionKeyRef,
212
+ setSelectedSessionKey: params.setSelectedSessionKey,
213
+ setDraft: params.setDraft,
214
+ refetchSessions: params.refetchSessions,
215
+ refetchHistory: params.refetchHistory,
216
+ restoreDraftOnError: options?.restoreDraftOnError,
217
+ setters: {
218
+ setOptimisticUserEvent,
219
+ setStreamingSessionEvents,
220
+ setStreamingAssistantText,
221
+ setStreamingAssistantTimestamp,
222
+ setIsSending,
223
+ setIsAwaitingAssistantOutput
224
+ }
225
+ });
226
+ },
227
+ [params]
228
+ );
229
+
230
+ useEffect(() => {
231
+ if (isSending || queuedMessages.length === 0) {
232
+ return;
233
+ }
234
+ const [next, ...rest] = queuedMessages;
235
+ setQueuedMessages(rest);
236
+ void runSend(next, { restoreDraftOnError: true });
237
+ }, [isSending, queuedMessages, runSend]);
238
+
239
+ const sendMessage = useCallback(
240
+ async (payload: SendMessageParams) => {
241
+ queueIdRef.current += 1;
242
+ const item: PendingChatMessage = {
243
+ id: queueIdRef.current,
244
+ message: payload.message,
245
+ sessionKey: payload.sessionKey,
246
+ agentId: payload.agentId
247
+ };
248
+ if (isSending) {
249
+ setQueuedMessages((prev) => [...prev, item]);
250
+ return;
251
+ }
252
+ await runSend(item, { restoreDraftOnError: payload.restoreDraftOnError });
253
+ },
254
+ [isSending, runSend]
255
+ );
256
+
257
+ return {
258
+ optimisticUserEvent,
259
+ streamingSessionEvents,
260
+ streamingAssistantText,
261
+ streamingAssistantTimestamp,
262
+ isSending,
263
+ isAwaitingAssistantOutput,
264
+ queuedCount: queuedMessages.length,
265
+ sendMessage,
266
+ resetStreamState
267
+ };
268
+ }
@@ -19,19 +19,28 @@ export type ChatTimelineMessageItem = {
19
19
  message: SessionMessageView;
20
20
  };
21
21
 
22
- export type ChatTimelineAssistantFlowItem = {
23
- kind: 'assistant_flow';
22
+ export type ChatTimelineAssistantTurnSegment =
23
+ | {
24
+ kind: 'assistant_message';
25
+ key: string;
26
+ text: string;
27
+ reasoning: string;
28
+ }
29
+ | {
30
+ kind: 'tool_card';
31
+ key: string;
32
+ card: ToolCard;
33
+ };
34
+
35
+ export type ChatTimelineAssistantTurnItem = {
36
+ kind: 'assistant_turn';
24
37
  key: string;
25
38
  role: 'assistant';
26
39
  timestamp: string;
27
- primaryText: string;
28
- primaryReasoning: string;
29
- followupText: string;
30
- followupReasoning: string;
31
- toolCards: ToolCard[];
40
+ segments: ChatTimelineAssistantTurnSegment[];
32
41
  };
33
42
 
34
- export type ChatTimelineItem = ChatTimelineMessageItem | ChatTimelineAssistantFlowItem;
43
+ export type ChatTimelineItem = ChatTimelineMessageItem | ChatTimelineAssistantTurnItem;
35
44
 
36
45
  const TOOL_DETAIL_FIELDS = ['cmd', 'command', 'query', 'q', 'path', 'url', 'to', 'channel', 'agentId', 'sessionKey'];
37
46
 
@@ -263,17 +272,54 @@ export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[
263
272
  });
264
273
 
265
274
  const timeline: ChatTimelineItem[] = [];
266
- let activeFlow:
275
+ let activeTurn:
267
276
  | {
268
- item: ChatTimelineAssistantFlowItem;
277
+ item: ChatTimelineAssistantTurnItem;
269
278
  cardByCallId: Map<string, ToolCard>;
270
- pendingCallIds: Set<string>;
271
- awaitingFollowup: boolean;
272
279
  }
273
280
  | null = null;
274
281
 
275
- const closeActiveFlow = () => {
276
- activeFlow = null;
282
+ const closeActiveTurn = () => {
283
+ activeTurn = null;
284
+ };
285
+
286
+ const ensureActiveTurn = (eventKey: string, timestamp: string) => {
287
+ if (activeTurn) {
288
+ activeTurn.item.timestamp = timestamp;
289
+ return activeTurn;
290
+ }
291
+ const item: ChatTimelineAssistantTurnItem = {
292
+ kind: 'assistant_turn',
293
+ key: `turn-${eventKey}`,
294
+ role: 'assistant',
295
+ timestamp,
296
+ segments: []
297
+ };
298
+ timeline.push(item);
299
+ activeTurn = {
300
+ item,
301
+ cardByCallId: new Map<string, ToolCard>()
302
+ };
303
+ return activeTurn;
304
+ };
305
+
306
+ const pushAssistantMessageSegment = (
307
+ target: { item: ChatTimelineAssistantTurnItem },
308
+ eventKey: string,
309
+ message: SessionMessageView
310
+ ) => {
311
+ const text = extractMessageText(message.content).trim();
312
+ const reasoning =
313
+ typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
314
+ if (!text && !reasoning) {
315
+ return;
316
+ }
317
+ target.item.segments.push({
318
+ kind: 'assistant_message',
319
+ key: `assistant-${eventKey}-${target.item.segments.length}`,
320
+ text,
321
+ reasoning
322
+ });
277
323
  };
278
324
 
279
325
  for (const event of normalized) {
@@ -287,80 +333,58 @@ export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[
287
333
  typeof message.timestamp === 'string' && message.timestamp
288
334
  ? message.timestamp
289
335
  : event.timestamp;
336
+ const eventKey = `${event._seq}-${event._idx}`;
337
+
338
+ if (role === 'assistant') {
339
+ const turn = ensureActiveTurn(eventKey, timestamp);
340
+ pushAssistantMessageSegment(turn, eventKey, message);
341
+ if (!hasToolCalls(message)) {
342
+ continue;
343
+ }
290
344
 
291
- if (role === 'assistant' && hasToolCalls(message)) {
292
- closeActiveFlow();
293
345
  const toolCards = buildToolCallCards(message);
294
- const item: ChatTimelineAssistantFlowItem = {
295
- kind: 'assistant_flow',
296
- key: `flow-${event._seq}-${event._idx}`,
297
- role: 'assistant',
298
- timestamp,
299
- primaryText: extractMessageText(message.content).trim(),
300
- primaryReasoning:
301
- typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '',
302
- followupText: '',
303
- followupReasoning: '',
304
- toolCards
305
- };
306
-
307
- const cardByCallId = new Map<string, ToolCard>();
308
- const pendingCallIds = new Set<string>();
309
346
  for (const card of toolCards) {
347
+ turn.item.segments.push({
348
+ kind: 'tool_card',
349
+ key: `tool-call-${eventKey}-${turn.item.segments.length}`,
350
+ card
351
+ });
310
352
  if (typeof card.callId === 'string' && card.callId.trim()) {
311
- cardByCallId.set(card.callId, card);
312
- pendingCallIds.add(card.callId);
353
+ turn.cardByCallId.set(card.callId, card);
313
354
  }
314
355
  }
315
-
316
- timeline.push(item);
317
- activeFlow = {
318
- item,
319
- cardByCallId,
320
- pendingCallIds,
321
- awaitingFollowup: pendingCallIds.size === 0
322
- };
323
356
  continue;
324
357
  }
325
358
 
326
359
  if (role === 'tool') {
360
+ const turn = ensureActiveTurn(eventKey, timestamp);
327
361
  const callId =
328
362
  typeof message.tool_call_id === 'string' && message.tool_call_id.trim()
329
363
  ? message.tool_call_id.trim()
330
364
  : undefined;
331
- if (activeFlow && callId && activeFlow.cardByCallId.has(callId)) {
332
- const card = activeFlow.cardByCallId.get(callId)!;
365
+ if (callId && turn.cardByCallId.has(callId)) {
366
+ const card = turn.cardByCallId.get(callId)!;
333
367
  const resultText = extractMessageText(message.content).trim();
334
368
  card.text = appendText(card.text ?? '', resultText);
335
369
  card.hasResult = true;
336
370
  if (typeof message.name === 'string' && message.name.trim()) {
337
371
  card.name = message.name.trim();
338
372
  }
339
- activeFlow.pendingCallIds.delete(callId);
340
- activeFlow.awaitingFollowup = activeFlow.pendingCallIds.size === 0;
341
- activeFlow.item.timestamp = timestamp;
373
+ turn.item.timestamp = timestamp;
342
374
  continue;
343
375
  }
344
376
 
345
- timeline.push({
346
- kind: 'message',
347
- key: `message-${event._seq}-${event._idx}`,
348
- role,
349
- timestamp,
350
- message
377
+ turn.item.segments.push({
378
+ kind: 'tool_card',
379
+ key: `tool-result-${eventKey}-${turn.item.segments.length}`,
380
+ card: {
381
+ kind: 'result',
382
+ name: toToolName(message.name),
383
+ text: extractMessageText(message.content).trim(),
384
+ callId,
385
+ hasResult: true
386
+ }
351
387
  });
352
- closeActiveFlow();
353
- continue;
354
- }
355
-
356
- if (role === 'assistant' && activeFlow && activeFlow.awaitingFollowup && !hasToolCalls(message)) {
357
- const text = extractMessageText(message.content).trim();
358
- const reasoning =
359
- typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
360
- activeFlow.item.followupText = appendText(activeFlow.item.followupText, text);
361
- activeFlow.item.followupReasoning = appendText(activeFlow.item.followupReasoning, reasoning);
362
- activeFlow.item.timestamp = timestamp;
363
- closeActiveFlow();
364
388
  continue;
365
389
  }
366
390
 
@@ -371,7 +395,7 @@ export function buildChatTimeline(events: SessionEventView[]): ChatTimelineItem[
371
395
  timestamp,
372
396
  message
373
397
  });
374
- closeActiveFlow();
398
+ closeActiveTurn();
375
399
  }
376
400
 
377
401
  return timeline;