@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.
- package/.eslintrc.cjs +9 -1
- package/CHANGELOG.md +12 -0
- package/dist/assets/{ChannelsList-CGlQJOKR.js → ChannelsList-B6N0kXyK.js} +1 -1
- package/dist/assets/ChatPage-DsDFvVQX.js +32 -0
- package/dist/assets/CronConfig-Cbz6V8MU.js +1 -0
- package/dist/assets/DocBrowser-hQzP4Iai.js +1 -0
- package/dist/assets/{MarketplacePage-_bRwL8Je.js → MarketplacePage-DMoWoU1y.js} +1 -1
- package/dist/assets/{ModelConfig-DHLdWlPT.js → ModelConfig-BXjF-qbA.js} +1 -1
- package/dist/assets/ProvidersList-D3hfY5U7.js +1 -0
- package/dist/assets/RuntimeConfig-DJ7qIejp.js +1 -0
- package/dist/assets/{SecretsConfig-CBpnJpdi.js → SecretsConfig-BFDeNvwV.js} +2 -2
- package/dist/assets/{SessionsConfig-FmbzF3JO.js → SessionsConfig-CJF7lPkX.js} +2 -2
- package/dist/assets/{card-D79QtyfR.js → card-BREZdIEb.js} +1 -1
- package/dist/assets/chat-message-pw9oafI4.js +5 -0
- package/dist/assets/{index-nfl5TEOq.js → index-uTbQ-MAY.js} +2 -2
- package/dist/assets/{label-C0lAPrBs.js → label-CzMB2yjV.js} +1 -1
- package/dist/assets/{logos-DdLfIYd-.js → logos-vVtRUuoo.js} +1 -1
- package/dist/assets/{page-layout-BuP_1ihv.js → page-layout-B07kdurB.js} +1 -1
- package/dist/assets/{switch-Dmt2u3GV.js → switch-Cr6cemeT.js} +1 -1
- package/dist/assets/{tabs-custom-s1WUaOad.js → tabs-custom-BzcvgsvR.js} +1 -1
- package/dist/assets/{useConfig-BGr-ekoe.js → useConfig-B4Y6cGwc.js} +1 -1
- package/dist/assets/{useConfirmDialog-D_YoV8_w.js → useConfirmDialog-Dc5WHCUf.js} +1 -1
- package/dist/assets/{vendor-DfLizrKM.js → vendor-Dh04PGww.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/components/chat/ChatConversationPanel.tsx +148 -0
- package/src/components/chat/ChatPage.tsx +84 -352
- package/src/components/chat/ChatSessionsSidebar.tsx +100 -0
- package/src/components/chat/ChatThread.tsx +23 -16
- package/src/components/chat/useChatStreamController.ts +268 -0
- package/src/lib/chat-message.ts +89 -65
- package/dist/assets/ChatPage-DNQw6cPm.js +0 -32
- package/dist/assets/CronConfig-CZE4jEWp.js +0 -1
- package/dist/assets/DocBrowser-BCqnlevu.js +0 -1
- package/dist/assets/ProvidersList-BS-3jQfk.js +0 -1
- package/dist/assets/RuntimeConfig-C0kVY3Y0.js +0 -1
- 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
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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 === '
|
|
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 === '
|
|
222
|
-
<
|
|
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
|
+
}
|
package/src/lib/chat-message.ts
CHANGED
|
@@ -19,19 +19,28 @@ export type ChatTimelineMessageItem = {
|
|
|
19
19
|
message: SessionMessageView;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export type
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
primaryReasoning: string;
|
|
29
|
-
followupText: string;
|
|
30
|
-
followupReasoning: string;
|
|
31
|
-
toolCards: ToolCard[];
|
|
40
|
+
segments: ChatTimelineAssistantTurnSegment[];
|
|
32
41
|
};
|
|
33
42
|
|
|
34
|
-
export type ChatTimelineItem = ChatTimelineMessageItem |
|
|
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
|
|
275
|
+
let activeTurn:
|
|
267
276
|
| {
|
|
268
|
-
item:
|
|
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
|
|
276
|
-
|
|
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 (
|
|
332
|
-
const card =
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
kind: '
|
|
347
|
-
key: `
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
398
|
+
closeActiveTurn();
|
|
375
399
|
}
|
|
376
400
|
|
|
377
401
|
return timeline;
|