@nextclaw/ui 0.5.26 → 0.5.28

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 (32) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/{ChannelsList-D0Wk08Ki.js → ChannelsList-DgkIu7_t.js} +1 -1
  3. package/dist/assets/ChatPage-CjzR-76f.js +32 -0
  4. package/dist/assets/{CronConfig-D-3Y8kWb.js → CronConfig-DREWLzKD.js} +1 -1
  5. package/dist/assets/{DocBrowser-BSPKhqrK.js → DocBrowser-fXdmzwRi.js} +1 -1
  6. package/dist/assets/{MarketplacePage-Dkm2FTtN.js → MarketplacePage-zKC-b_O_.js} +1 -1
  7. package/dist/assets/{ModelConfig-2cpAmvGq.js → ModelConfig-BdogzFBq.js} +1 -1
  8. package/dist/assets/{ProvidersList-Dot21pAy.js → ProvidersList-BLScOe9j.js} +1 -1
  9. package/dist/assets/{RuntimeConfig-BNw_Ms_Y.js → RuntimeConfig-CGsoLtsV.js} +1 -1
  10. package/dist/assets/{SecretsConfig-z8M3PDJP.js → SecretsConfig-B8ZDpRgB.js} +1 -1
  11. package/dist/assets/{SessionsConfig-XVHZ-FG5.js → SessionsConfig-CcpfGazP.js} +1 -1
  12. package/dist/assets/{action-link-CpPJJN-z.js → action-link-RjYHzlQk.js} +1 -1
  13. package/dist/assets/{card-DsZ2Am92.js → card-Bt8T3JA3.js} +1 -1
  14. package/dist/assets/chat-message-D0s61C4e.js +5 -0
  15. package/dist/assets/{dialog-BysNu5hM.js → dialog-BNi5ymWD.js} +1 -1
  16. package/dist/assets/{index-Bny21Br0.js → index-CtNWUrVR.js} +2 -2
  17. package/dist/assets/{label-q6RASlER.js → label-BDCnjFl3.js} +1 -1
  18. package/dist/assets/{page-layout-WiVrFc8t.js → page-layout-DWVnm0X8.js} +1 -1
  19. package/dist/assets/{switch-DM_YYUgB.js → switch-CrmdAOBw.js} +1 -1
  20. package/dist/assets/{tabs-custom-mlgm-IGH.js → tabs-custom-WJxQwDQy.js} +1 -1
  21. package/dist/assets/useConfig-COoN7EVf.js +6 -0
  22. package/dist/assets/{useConfirmDialog-DamaA60g.js → useConfirmDialog-C5hEiEeb.js} +1 -1
  23. package/dist/index.html +1 -1
  24. package/package.json +1 -1
  25. package/src/api/config.ts +14 -1
  26. package/src/api/types.ts +14 -0
  27. package/src/components/chat/ChatPage.tsx +66 -36
  28. package/src/components/chat/ChatThread.tsx +58 -32
  29. package/src/lib/chat-message.ts +169 -153
  30. package/dist/assets/ChatPage-Deg2lBH4.js +0 -32
  31. package/dist/assets/chat-message-Jxa8JFA_.js +0 -9
  32. package/dist/assets/useConfig-BOn-kp8G.js +0 -6
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import type { SessionEntryView, SessionMessageView } from '@/api/types';
2
+ import type { SessionEntryView, SessionEventView } from '@/api/types';
3
3
  import { sendChatTurnStream } from '@/api/config';
4
4
  import { useConfig, useDeleteSession, useSessionHistory, useSessions } from '@/hooks/useConfig';
5
5
  import { useConfirmDialog } from '@/hooks/useConfirmDialog';
@@ -9,6 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
9
9
  import { PageHeader, PageLayout } from '@/components/layout/page-layout';
10
10
  import { ChatThread } from '@/components/chat/ChatThread';
11
11
  import { cn } from '@/lib/utils';
12
+ import { buildFallbackEventsFromMessages } from '@/lib/chat-message';
12
13
  import { formatDateTime, t } from '@/lib/i18n';
13
14
  import { MessageSquareText, Plus, RefreshCw, Search, Send, Trash2 } from 'lucide-react';
14
15
 
@@ -93,8 +94,9 @@ export function ChatPage() {
93
94
  const [draft, setDraft] = useState('');
94
95
  const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
95
96
  const [selectedAgentId, setSelectedAgentId] = useState('main');
96
- const [optimisticUserMessage, setOptimisticUserMessage] = useState<SessionMessageView | null>(null);
97
- const [streamingAssistantMessage, setStreamingAssistantMessage] = useState<SessionMessageView | null>(null);
97
+ const [streamingSessionEvents, setStreamingSessionEvents] = useState<SessionEventView[]>([]);
98
+ const [streamingAssistantText, setStreamingAssistantText] = useState('');
99
+ const [streamingAssistantTimestamp, setStreamingAssistantTimestamp] = useState<string | null>(null);
98
100
  const [isSending, setIsSending] = useState(false);
99
101
  const [queuedMessages, setQueuedMessages] = useState<PendingChatMessage[]>([]);
100
102
 
@@ -143,20 +145,32 @@ export function ChatPage() {
143
145
  [selectedSessionKey, sessions]
144
146
  );
145
147
 
146
- const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
147
- const mergedMessages = useMemo(() => {
148
- if (!optimisticUserMessage && !streamingAssistantMessage) {
149
- return historyMessages;
150
- }
151
- const next = [...historyMessages];
152
- if (optimisticUserMessage) {
153
- next.push(optimisticUserMessage);
154
- }
155
- if (streamingAssistantMessage) {
156
- next.push(streamingAssistantMessage);
148
+ const historyData = historyQuery.data;
149
+ const historyMessages = historyData?.messages ?? [];
150
+ const historyEvents =
151
+ historyData?.events && historyData.events.length > 0
152
+ ? historyData.events
153
+ : buildFallbackEventsFromMessages(historyMessages);
154
+ const mergedEvents = useMemo(() => {
155
+ const next = [...historyEvents, ...streamingSessionEvents];
156
+ if (streamingAssistantText.trim()) {
157
+ const maxSeq = next.reduce((max, event) => {
158
+ const seq = Number.isFinite(event.seq) ? event.seq : 0;
159
+ return seq > max ? seq : max;
160
+ }, 0);
161
+ next.push({
162
+ seq: maxSeq + 1,
163
+ type: 'stream.assistant_delta',
164
+ timestamp: streamingAssistantTimestamp ?? new Date().toISOString(),
165
+ message: {
166
+ role: 'assistant',
167
+ content: streamingAssistantText,
168
+ timestamp: streamingAssistantTimestamp ?? new Date().toISOString()
169
+ }
170
+ });
157
171
  }
158
172
  return next;
159
- }, [historyMessages, optimisticUserMessage, streamingAssistantMessage]);
173
+ }, [historyEvents, streamingAssistantText, streamingAssistantTimestamp, streamingSessionEvents]);
160
174
 
161
175
  useEffect(() => {
162
176
  if (!selectedSessionKey && filteredSessions.length > 0) {
@@ -188,7 +202,7 @@ export function ChatPage() {
188
202
  return;
189
203
  }
190
204
  element.scrollTop = element.scrollHeight;
191
- }, [mergedMessages, isSending, selectedSessionKey]);
205
+ }, [mergedEvents, isSending, selectedSessionKey]);
192
206
 
193
207
  useEffect(() => {
194
208
  return () => {
@@ -200,10 +214,11 @@ export function ChatPage() {
200
214
  streamRunIdRef.current += 1;
201
215
  setIsSending(false);
202
216
  setQueuedMessages([]);
203
- setStreamingAssistantMessage(null);
217
+ setStreamingSessionEvents([]);
218
+ setStreamingAssistantText('');
219
+ setStreamingAssistantTimestamp(null);
204
220
  const next = buildNewSessionKey(selectedAgentId);
205
221
  setSelectedSessionKey(next);
206
- setOptimisticUserMessage(null);
207
222
  };
208
223
 
209
224
  const handleDeleteSession = async () => {
@@ -225,9 +240,10 @@ export function ChatPage() {
225
240
  streamRunIdRef.current += 1;
226
241
  setIsSending(false);
227
242
  setQueuedMessages([]);
228
- setStreamingAssistantMessage(null);
243
+ setStreamingSessionEvents([]);
244
+ setStreamingAssistantText('');
245
+ setStreamingAssistantTimestamp(null);
229
246
  setSelectedSessionKey(null);
230
- setOptimisticUserMessage(null);
231
247
  await sessionsQuery.refetch();
232
248
  }
233
249
  }
@@ -238,17 +254,15 @@ export function ChatPage() {
238
254
  streamRunIdRef.current += 1;
239
255
  const runId = streamRunIdRef.current;
240
256
 
241
- setStreamingAssistantMessage(null);
242
- setOptimisticUserMessage({
243
- role: 'user',
244
- content: item.message,
245
- timestamp: new Date().toISOString()
246
- });
257
+ setStreamingSessionEvents([]);
258
+ setStreamingAssistantText('');
259
+ setStreamingAssistantTimestamp(null);
247
260
  setIsSending(true);
248
261
 
249
262
  try {
250
263
  let streamText = '';
251
264
  const streamTimestamp = new Date().toISOString();
265
+ setStreamingAssistantTimestamp(streamTimestamp);
252
266
 
253
267
  const result = await sendChatTurnStream({
254
268
  message: item.message,
@@ -270,17 +284,30 @@ export function ChatPage() {
270
284
  return;
271
285
  }
272
286
  streamText += event.delta;
273
- setStreamingAssistantMessage({
274
- role: 'assistant',
275
- content: streamText,
276
- timestamp: streamTimestamp
287
+ setStreamingAssistantText(streamText);
288
+ },
289
+ onSessionEvent: (event) => {
290
+ if (runId !== streamRunIdRef.current) {
291
+ return;
292
+ }
293
+ setStreamingSessionEvents((prev) => {
294
+ const next = [...prev];
295
+ const hit = next.findIndex((item) => item.seq === event.data.seq);
296
+ if (hit >= 0) {
297
+ next[hit] = event.data;
298
+ } else {
299
+ next.push(event.data);
300
+ }
301
+ return next;
277
302
  });
303
+ if (event.data.message?.role === 'assistant') {
304
+ setStreamingAssistantText('');
305
+ }
278
306
  }
279
307
  });
280
308
  if (runId !== streamRunIdRef.current) {
281
309
  return;
282
310
  }
283
- setOptimisticUserMessage(null);
284
311
  if (result.sessionKey !== item.sessionKey) {
285
312
  setSelectedSessionKey(result.sessionKey);
286
313
  }
@@ -289,7 +316,9 @@ export function ChatPage() {
289
316
  if (!activeSessionKey || activeSessionKey === item.sessionKey || activeSessionKey === result.sessionKey) {
290
317
  await historyQuery.refetch();
291
318
  }
292
- setStreamingAssistantMessage(null);
319
+ setStreamingSessionEvents([]);
320
+ setStreamingAssistantText('');
321
+ setStreamingAssistantTimestamp(null);
293
322
  setIsSending(false);
294
323
  } catch {
295
324
  if (runId !== streamRunIdRef.current) {
@@ -297,8 +326,9 @@ export function ChatPage() {
297
326
  }
298
327
  streamRunIdRef.current += 1;
299
328
  setIsSending(false);
300
- setStreamingAssistantMessage(null);
301
- setOptimisticUserMessage(null);
329
+ setStreamingSessionEvents([]);
330
+ setStreamingAssistantText('');
331
+ setStreamingAssistantTimestamp(null);
302
332
  if (options?.restoreDraftOnError) {
303
333
  setDraft((prev) => prev.trim().length === 0 ? item.message : prev);
304
334
  }
@@ -486,10 +516,10 @@ export function ChatPage() {
486
516
  <div className="text-sm text-gray-500">{t('chatHistoryLoading')}</div>
487
517
  ) : (
488
518
  <>
489
- {mergedMessages.length === 0 ? (
519
+ {mergedEvents.length === 0 ? (
490
520
  <div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
491
521
  ) : (
492
- <ChatThread messages={mergedMessages} isSending={isSending && !streamingAssistantMessage} />
522
+ <ChatThread events={mergedEvents} isSending={isSending && !streamingAssistantText.trim()} />
493
523
  )}
494
524
  </>
495
525
  )}
@@ -1,12 +1,13 @@
1
1
  import { useMemo } from 'react';
2
- import type { SessionMessageView } from '@/api/types';
2
+ import type { SessionEventView, SessionMessageView } from '@/api/types';
3
3
  import { cn } from '@/lib/utils';
4
4
  import {
5
- combineToolCallAndResults,
5
+ buildChatTimeline,
6
6
  extractMessageText,
7
7
  extractToolCards,
8
- groupChatMessages,
8
+ normalizeChatRole,
9
9
  type ChatRole,
10
+ type ChatTimelineAssistantFlowItem,
10
11
  type ToolCard
11
12
  } from '@/lib/chat-message';
12
13
  import { formatDateTime, t } from '@/lib/i18n';
@@ -16,7 +17,7 @@ import remarkGfm from 'remark-gfm';
16
17
  import { Bot, Clock3, FileSearch, Globe, Search, SendHorizontal, Terminal, User, Wrench } from 'lucide-react';
17
18
 
18
19
  type ChatThreadProps = {
19
- messages: SessionMessageView[];
20
+ events: SessionEventView[];
20
21
  isSending: boolean;
21
22
  className?: string;
22
23
  };
@@ -142,12 +143,26 @@ function ToolCardView({ card }: { card: ToolCard }) {
142
143
  );
143
144
  }
144
145
 
145
- function MessageCard({ message, role }: { message: SessionMessageView; role: ChatRole }) {
146
- const text = extractMessageText(message.content).trim();
146
+ function ReasoningBlock({ reasoning, isUser }: { reasoning: string; isUser: boolean }) {
147
+ return (
148
+ <details className="mt-3">
149
+ <summary className={cn('cursor-pointer text-xs', isUser ? 'text-primary-100' : 'text-gray-500')}>
150
+ {t('chatReasoning')}
151
+ </summary>
152
+ <pre className={cn('mt-2 text-[11px] whitespace-pre-wrap break-words rounded-lg p-2', isUser ? 'bg-primary-700/60' : 'bg-gray-100')}>
153
+ {reasoning}
154
+ </pre>
155
+ </details>
156
+ );
157
+ }
158
+
159
+ function MessageCard({ message }: { message: SessionMessageView }) {
160
+ const role = normalizeChatRole(message);
161
+ const primaryText = extractMessageText(message.content).trim();
162
+ const primaryReasoning = typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
147
163
  const toolCards = extractToolCards(message);
148
- const reasoning = typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
149
- const shouldRenderText = Boolean(text) && !(role === 'tool' && toolCards.length > 0);
150
164
  const isUser = role === 'user';
165
+ const shouldRenderPrimaryText = Boolean(primaryText) && !(role === 'tool' && toolCards.length > 0);
151
166
 
152
167
  return (
153
168
  <div
@@ -160,19 +175,10 @@ function MessageCard({ message, role }: { message: SessionMessageView; role: Cha
160
175
  : 'bg-orange-50/70 text-gray-900 border-orange-200/80'
161
176
  )}
162
177
  >
163
- {shouldRenderText && <MarkdownBlock text={text} role={role} />}
164
- {reasoning && (
165
- <details className="mt-3">
166
- <summary className={cn('cursor-pointer text-xs', isUser ? 'text-primary-100' : 'text-gray-500')}>
167
- {t('chatReasoning')}
168
- </summary>
169
- <pre className={cn('mt-2 text-[11px] whitespace-pre-wrap break-words rounded-lg p-2', isUser ? 'bg-primary-700/60' : 'bg-gray-100')}>
170
- {reasoning}
171
- </pre>
172
- </details>
173
- )}
178
+ {shouldRenderPrimaryText && <MarkdownBlock text={primaryText} role={role} />}
179
+ {primaryReasoning && <ReasoningBlock reasoning={primaryReasoning} isUser={isUser} />}
174
180
  {toolCards.length > 0 && (
175
- <div className="mt-3 space-y-2">
181
+ <div className={cn('space-y-2', (shouldRenderPrimaryText || primaryReasoning) && 'mt-3')}>
176
182
  {toolCards.map((card, index) => (
177
183
  <ToolCardView key={`${card.kind}-${card.name}-${card.callId ?? index}`} card={card} />
178
184
  ))}
@@ -182,26 +188,46 @@ function MessageCard({ message, role }: { message: SessionMessageView; role: Cha
182
188
  );
183
189
  }
184
190
 
185
- export function ChatThread({ messages, isSending, className }: ChatThreadProps) {
186
- const preparedMessages = useMemo(() => combineToolCallAndResults(messages), [messages]);
187
- const groups = useMemo(() => groupChatMessages(preparedMessages), [preparedMessages]);
191
+ function AssistantFlowCard({ item }: { item: ChatTimelineAssistantFlowItem }) {
192
+ return (
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>}
205
+ </div>
206
+ );
207
+ }
208
+
209
+ export function ChatThread({ events, isSending, className }: ChatThreadProps) {
210
+ const timeline = useMemo(() => buildChatTimeline(events), [events]);
188
211
 
189
212
  return (
190
213
  <div className={cn('space-y-5', className)}>
191
- {groups.map((group) => {
192
- const isUser = group.role === 'user';
214
+ {timeline.map((item) => {
215
+ const role = item.kind === 'assistant_flow' ? 'assistant' : item.role;
216
+ const isUser = role === 'user';
193
217
  return (
194
- <div key={group.key} className={cn('flex gap-3', isUser ? 'justify-end' : 'justify-start')}>
195
- {!isUser && <RoleAvatar role={group.role} />}
218
+ <div key={item.key} className={cn('flex gap-3', isUser ? 'justify-end' : 'justify-start')}>
219
+ {!isUser && <RoleAvatar role={role} />}
196
220
  <div className={cn('max-w-[88%] min-w-[280px] space-y-2', isUser && 'flex flex-col items-end')}>
197
- {group.messages.map((message, index) => (
198
- <MessageCard key={`${group.key}-${index}`} message={message} role={group.role} />
199
- ))}
221
+ {item.kind === 'assistant_flow' ? (
222
+ <AssistantFlowCard item={item} />
223
+ ) : (
224
+ <MessageCard message={item.message} />
225
+ )}
200
226
  <div className={cn('text-[11px] px-1', isUser ? 'text-primary-300' : 'text-gray-400')}>
201
- {roleTitle(group.role)} · {formatDateTime(group.timestamp)}
227
+ {roleTitle(role)} · {formatDateTime(item.timestamp)}
202
228
  </div>
203
229
  </div>
204
- {isUser && <RoleAvatar role={group.role} />}
230
+ {isUser && <RoleAvatar role={role} />}
205
231
  </div>
206
232
  );
207
233
  })}