@nextclaw/ui 0.5.22 → 0.5.24

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 +12 -0
  2. package/dist/assets/{ChannelsList-dmUfRDVE.js → ChannelsList-CAuBEcOr.js} +1 -1
  3. package/dist/assets/ChatPage-Bxs3X5OC.js +32 -0
  4. package/dist/assets/{CronConfig-B15BA--M.js → CronConfig-jOEPCnpf.js} +1 -1
  5. package/dist/assets/{DocBrowser-CBwMA2wK.js → DocBrowser-wov_cBSN.js} +1 -1
  6. package/dist/assets/{MarketplacePage-BZRz4tCA.js → MarketplacePage-Cob6DGoO.js} +1 -1
  7. package/dist/assets/{ModelConfig-CByjZNvf.js → ModelConfig-C_Y3UDYr.js} +1 -1
  8. package/dist/assets/{ProvidersList-BbsQEnoZ.js → ProvidersList-6A6N2eDT.js} +1 -1
  9. package/dist/assets/{RuntimeConfig-CrQsFzP7.js → RuntimeConfig-B3k_dMdJ.js} +1 -1
  10. package/dist/assets/{SecretsConfig-DPZqWmry.js → SecretsConfig-BeCRCCEW.js} +2 -2
  11. package/dist/assets/{SessionsConfig-Bk7RCsWw.js → SessionsConfig-M32Qm7cL.js} +1 -1
  12. package/dist/assets/{action-link-CKSHFT5k.js → action-link-DsAjsb68.js} +1 -1
  13. package/dist/assets/{card-6hv7Kf0F.js → card-uTj7-9XS.js} +1 -1
  14. package/dist/assets/chat-message-DZV2Z5oc.js +5 -0
  15. package/dist/assets/{dialog-Bmy_bApp.js → dialog-FRtXcCmk.js} +1 -1
  16. package/dist/assets/{index-y6creQ7S.js → index-Dw8Ss2WH.js} +2 -2
  17. package/dist/assets/{label-Btp6gGxV.js → label-DAhEgM6-.js} +1 -1
  18. package/dist/assets/{page-layout-DWVKbt_g.js → page-layout-DMpNQawS.js} +1 -1
  19. package/dist/assets/{switch-AeXayTRS.js → switch-D8lSFzq4.js} +1 -1
  20. package/dist/assets/{tabs-custom-DEZwNpXo.js → tabs-custom-CsFrOXUS.js} +1 -1
  21. package/dist/assets/useConfig-Cjlx5C-1.js +6 -0
  22. package/dist/assets/{useConfirmDialog-BY0hni-H.js → useConfirmDialog-D3eI0Hfj.js} +1 -1
  23. package/dist/index.html +1 -1
  24. package/package.json +1 -1
  25. package/src/api/config.ts +159 -2
  26. package/src/api/types.ts +16 -0
  27. package/src/components/chat/ChatPage.tsx +74 -18
  28. package/src/components/chat/ChatThread.tsx +12 -3
  29. package/src/lib/chat-message.ts +80 -2
  30. package/dist/assets/ChatPage-CmRkhiCX.js +0 -32
  31. package/dist/assets/chat-message-B7oqvJ2d.js +0 -3
  32. package/dist/assets/useConfig-BiQH98MD.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { SessionEntryView, SessionMessageView } from '@/api/types';
3
- import { useConfig, useDeleteSession, useSendChatTurn, useSessionHistory, useSessions } from '@/hooks/useConfig';
3
+ import { sendChatTurnStream } from '@/api/config';
4
+ import { useConfig, useDeleteSession, useSessionHistory, useSessions } from '@/hooks/useConfig';
4
5
  import { useConfirmDialog } from '@/hooks/useConfirmDialog';
5
6
  import { Button } from '@/components/ui/button';
6
7
  import { Input } from '@/components/ui/input';
@@ -68,15 +69,17 @@ export function ChatPage() {
68
69
  const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
69
70
  const [selectedAgentId, setSelectedAgentId] = useState('main');
70
71
  const [optimisticUserMessage, setOptimisticUserMessage] = useState<SessionMessageView | null>(null);
72
+ const [streamingAssistantMessage, setStreamingAssistantMessage] = useState<SessionMessageView | null>(null);
73
+ const [isSending, setIsSending] = useState(false);
71
74
 
72
75
  const { confirm, ConfirmDialog } = useConfirmDialog();
73
76
  const threadRef = useRef<HTMLDivElement | null>(null);
77
+ const streamRunIdRef = useRef(0);
74
78
 
75
79
  const configQuery = useConfig();
76
80
  const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
77
81
  const historyQuery = useSessionHistory(selectedSessionKey, 300);
78
82
  const deleteSession = useDeleteSession();
79
- const sendChatTurn = useSendChatTurn();
80
83
 
81
84
  const agentOptions = useMemo(() => {
82
85
  const list = configQuery.data?.agents.list ?? [];
@@ -96,12 +99,20 @@ export function ChatPage() {
96
99
  );
97
100
 
98
101
  const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
102
+ const isGenerating = isSending;
99
103
  const mergedMessages = useMemo(() => {
100
- if (!optimisticUserMessage) {
104
+ if (!optimisticUserMessage && !streamingAssistantMessage) {
101
105
  return historyMessages;
102
106
  }
103
- return [...historyMessages, optimisticUserMessage];
104
- }, [historyMessages, optimisticUserMessage]);
107
+ const next = [...historyMessages];
108
+ if (optimisticUserMessage) {
109
+ next.push(optimisticUserMessage);
110
+ }
111
+ if (streamingAssistantMessage) {
112
+ next.push(streamingAssistantMessage);
113
+ }
114
+ return next;
115
+ }, [historyMessages, optimisticUserMessage, streamingAssistantMessage]);
105
116
 
106
117
  useEffect(() => {
107
118
  if (!selectedSessionKey && sessions.length > 0) {
@@ -129,9 +140,18 @@ export function ChatPage() {
129
140
  return;
130
141
  }
131
142
  element.scrollTop = element.scrollHeight;
132
- }, [mergedMessages.length, sendChatTurn.isPending, selectedSessionKey]);
143
+ }, [mergedMessages, isSending, selectedSessionKey]);
144
+
145
+ useEffect(() => {
146
+ return () => {
147
+ streamRunIdRef.current += 1;
148
+ };
149
+ }, []);
133
150
 
134
151
  const createNewSession = () => {
152
+ streamRunIdRef.current += 1;
153
+ setIsSending(false);
154
+ setStreamingAssistantMessage(null);
135
155
  const next = buildNewSessionKey(selectedAgentId);
136
156
  setSelectedSessionKey(next);
137
157
  setOptimisticUserMessage(null);
@@ -153,6 +173,9 @@ export function ChatPage() {
153
173
  { key: selectedSessionKey },
154
174
  {
155
175
  onSuccess: async () => {
176
+ streamRunIdRef.current += 1;
177
+ setIsSending(false);
178
+ setStreamingAssistantMessage(null);
156
179
  setSelectedSessionKey(null);
157
180
  setOptimisticUserMessage(null);
158
181
  await sessionsQuery.refetch();
@@ -163,10 +186,12 @@ export function ChatPage() {
163
186
 
164
187
  const handleSend = async () => {
165
188
  const message = draft.trim();
166
- if (!message || sendChatTurn.isPending) {
189
+ if (!message || isGenerating) {
167
190
  return;
168
191
  }
169
192
 
193
+ streamRunIdRef.current += 1;
194
+ setStreamingAssistantMessage(null);
170
195
  const hadActiveSession = Boolean(selectedSessionKey);
171
196
  const sessionKey = selectedSessionKey ?? buildNewSessionKey(selectedAgentId);
172
197
  if (!selectedSessionKey) {
@@ -178,17 +203,43 @@ export function ChatPage() {
178
203
  content: message,
179
204
  timestamp: new Date().toISOString()
180
205
  });
206
+ setIsSending(true);
181
207
 
182
208
  try {
183
- const result = await sendChatTurn.mutateAsync({
184
- data: {
185
- message,
186
- sessionKey,
187
- agentId: selectedAgentId,
188
- channel: 'ui',
189
- chatId: 'web-ui'
209
+ const runId = streamRunIdRef.current;
210
+ let streamText = '';
211
+ const streamTimestamp = new Date().toISOString();
212
+
213
+ const result = await sendChatTurnStream({
214
+ message,
215
+ sessionKey,
216
+ agentId: selectedAgentId,
217
+ channel: 'ui',
218
+ chatId: 'web-ui'
219
+ }, {
220
+ onReady: (event) => {
221
+ if (runId !== streamRunIdRef.current) {
222
+ return;
223
+ }
224
+ if (event.sessionKey && event.sessionKey !== selectedSessionKey) {
225
+ setSelectedSessionKey(event.sessionKey);
226
+ }
227
+ },
228
+ onDelta: (event) => {
229
+ if (runId !== streamRunIdRef.current) {
230
+ return;
231
+ }
232
+ streamText += event.delta;
233
+ setStreamingAssistantMessage({
234
+ role: 'assistant',
235
+ content: streamText,
236
+ timestamp: streamTimestamp
237
+ });
190
238
  }
191
239
  });
240
+ if (runId !== streamRunIdRef.current) {
241
+ return;
242
+ }
192
243
  setOptimisticUserMessage(null);
193
244
  if (result.sessionKey !== sessionKey) {
194
245
  setSelectedSessionKey(result.sessionKey);
@@ -197,7 +248,12 @@ export function ChatPage() {
197
248
  if (hadActiveSession) {
198
249
  await historyQuery.refetch();
199
250
  }
251
+ setStreamingAssistantMessage(null);
252
+ setIsSending(false);
200
253
  } catch {
254
+ streamRunIdRef.current += 1;
255
+ setIsSending(false);
256
+ setStreamingAssistantMessage(null);
201
257
  setOptimisticUserMessage(null);
202
258
  setDraft(message);
203
259
  }
@@ -335,7 +391,7 @@ export function ChatPage() {
335
391
  {mergedMessages.length === 0 ? (
336
392
  <div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
337
393
  ) : (
338
- <ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending} />
394
+ <ChatThread messages={mergedMessages} isSending={isSending && !streamingAssistantMessage} />
339
395
  )}
340
396
  </>
341
397
  )}
@@ -354,7 +410,7 @@ export function ChatPage() {
354
410
  }}
355
411
  placeholder={t('chatInputPlaceholder')}
356
412
  className="w-full min-h-[68px] max-h-[220px] resize-y bg-transparent outline-none text-sm px-2 py-1.5 text-gray-800 placeholder:text-gray-400"
357
- disabled={sendChatTurn.isPending}
413
+ disabled={isGenerating}
358
414
  />
359
415
  <div className="flex items-center justify-between px-2 pb-1">
360
416
  <div className="text-[11px] text-gray-400">{t('chatInputHint')}</div>
@@ -362,10 +418,10 @@ export function ChatPage() {
362
418
  size="sm"
363
419
  className="rounded-lg"
364
420
  onClick={() => void handleSend()}
365
- disabled={sendChatTurn.isPending || draft.trim().length === 0}
421
+ disabled={isGenerating || draft.trim().length === 0}
366
422
  >
367
423
  <Send className="h-3.5 w-3.5 mr-1.5" />
368
- {sendChatTurn.isPending ? t('chatSending') : t('chatSend')}
424
+ {isGenerating ? t('chatSending') : t('chatSend')}
369
425
  </Button>
370
426
  </div>
371
427
  </div>
@@ -1,7 +1,14 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { SessionMessageView } from '@/api/types';
3
3
  import { cn } from '@/lib/utils';
4
- import { extractMessageText, extractToolCards, groupChatMessages, type ChatRole, type ToolCard } from '@/lib/chat-message';
4
+ import {
5
+ combineToolCallAndResults,
6
+ extractMessageText,
7
+ extractToolCards,
8
+ groupChatMessages,
9
+ type ChatRole,
10
+ type ToolCard
11
+ } from '@/lib/chat-message';
5
12
  import { formatDateTime, t } from '@/lib/i18n';
6
13
  import ReactMarkdown from 'react-markdown';
7
14
  import rehypeSanitize from 'rehype-sanitize';
@@ -101,6 +108,7 @@ function ToolCardView({ card }: { card: ToolCard }) {
101
108
  const output = card.text?.trim() ?? '';
102
109
  const showDetails = output.length > TOOL_OUTPUT_PREVIEW_MAX || output.includes('\n');
103
110
  const preview = showDetails ? `${output.slice(0, TOOL_OUTPUT_PREVIEW_MAX)}…` : output;
111
+ const showOutputSection = card.kind === 'result' || card.hasResult;
104
112
 
105
113
  return (
106
114
  <div className="rounded-xl border border-amber-200/80 bg-amber-50/60 px-3 py-2.5">
@@ -112,7 +120,7 @@ function ToolCardView({ card }: { card: ToolCard }) {
112
120
  {card.detail && (
113
121
  <div className="mt-1 text-[11px] text-amber-800/90 font-mono break-words">{card.detail}</div>
114
122
  )}
115
- {card.kind === 'result' && (
123
+ {showOutputSection && (
116
124
  <div className="mt-2">
117
125
  {!output ? (
118
126
  <div className="text-[11px] text-amber-700/80">{t('chatToolNoOutput')}</div>
@@ -175,7 +183,8 @@ function MessageCard({ message, role }: { message: SessionMessageView; role: Cha
175
183
  }
176
184
 
177
185
  export function ChatThread({ messages, isSending, className }: ChatThreadProps) {
178
- const groups = useMemo(() => groupChatMessages(messages), [messages]);
186
+ const preparedMessages = useMemo(() => combineToolCallAndResults(messages), [messages]);
187
+ const groups = useMemo(() => groupChatMessages(preparedMessages), [preparedMessages]);
179
188
 
180
189
  return (
181
190
  <div className={cn('space-y-5', className)}>
@@ -8,6 +8,7 @@ export type ToolCard = {
8
8
  detail?: string;
9
9
  text?: string;
10
10
  callId?: string;
11
+ hasResult?: boolean;
11
12
  };
12
13
 
13
14
  export type GroupedChatMessage = {
@@ -158,11 +159,15 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
158
159
  const fn = isRecord(call.function) ? call.function : null;
159
160
  const name = toToolName(fn?.name ?? call.name);
160
161
  const args = fn?.arguments ?? call.arguments;
162
+ const resultText = typeof call.result_text === 'string' ? call.result_text.trim() : '';
163
+ const hasResult = call.has_result === true || typeof call.result_text === 'string';
161
164
  cards.push({
162
165
  kind: 'call',
163
166
  name,
164
167
  detail: summarizeToolArgs(args),
165
- callId: typeof call.id === 'string' ? call.id : undefined
168
+ callId: typeof call.id === 'string' ? call.id : undefined,
169
+ text: resultText,
170
+ hasResult
166
171
  });
167
172
  }
168
173
 
@@ -173,13 +178,86 @@ export function extractToolCards(message: SessionMessageView): ToolCard[] {
173
178
  kind: 'result',
174
179
  name: toToolName(message.name ?? cards[0]?.name),
175
180
  text,
176
- callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined
181
+ callId: typeof message.tool_call_id === 'string' ? message.tool_call_id : undefined,
182
+ hasResult: true
177
183
  });
178
184
  }
179
185
 
180
186
  return cards;
181
187
  }
182
188
 
189
+ type ToolResultBucket = {
190
+ name?: string;
191
+ texts: string[];
192
+ };
193
+
194
+ function cloneMessageForMerge(message: SessionMessageView): SessionMessageView {
195
+ return {
196
+ ...message,
197
+ tool_calls: Array.isArray(message.tool_calls)
198
+ ? message.tool_calls.map((call) => (isRecord(call) ? { ...call } : call))
199
+ : message.tool_calls
200
+ };
201
+ }
202
+
203
+ export function combineToolCallAndResults(messages: SessionMessageView[]): SessionMessageView[] {
204
+ const cloned = messages.map(cloneMessageForMerge);
205
+ const resultByCallId = new Map<string, ToolResultBucket>();
206
+
207
+ for (const message of cloned) {
208
+ if (normalizeChatRole(message) !== 'tool') {
209
+ continue;
210
+ }
211
+ if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
212
+ continue;
213
+ }
214
+
215
+ const callId = message.tool_call_id.trim();
216
+ const text = extractMessageText(message.content).trim();
217
+ const existing = resultByCallId.get(callId) ?? { texts: [] };
218
+ if (typeof message.name === 'string' && message.name.trim()) {
219
+ existing.name = message.name.trim();
220
+ }
221
+ existing.texts.push(text);
222
+ resultByCallId.set(callId, existing);
223
+ }
224
+
225
+ const consumedCallIds = new Set<string>();
226
+
227
+ for (const message of cloned) {
228
+ if (normalizeChatRole(message) !== 'assistant' || !Array.isArray(message.tool_calls)) {
229
+ continue;
230
+ }
231
+
232
+ message.tool_calls = message.tool_calls.map((call) => {
233
+ if (!isRecord(call) || typeof call.id !== 'string') {
234
+ return call;
235
+ }
236
+ const result = resultByCallId.get(call.id);
237
+ if (!result) {
238
+ return call;
239
+ }
240
+ consumedCallIds.add(call.id);
241
+ return {
242
+ ...call,
243
+ result_text: result.texts.filter(Boolean).join('\n\n'),
244
+ has_result: true,
245
+ result_name: result.name
246
+ };
247
+ }) as Array<Record<string, unknown>>;
248
+ }
249
+
250
+ return cloned.filter((message) => {
251
+ if (normalizeChatRole(message) !== 'tool') {
252
+ return true;
253
+ }
254
+ if (typeof message.tool_call_id !== 'string' || !message.tool_call_id.trim()) {
255
+ return true;
256
+ }
257
+ return !consumedCallIds.has(message.tool_call_id.trim());
258
+ });
259
+ }
260
+
183
261
  export function groupChatMessages(messages: SessionMessageView[]): GroupedChatMessage[] {
184
262
  const groups: GroupedChatMessage[] = [];
185
263
  let lastTs = 0;