@nextclaw/ui 0.5.21 → 0.5.23

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 (43) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/ChannelsList-AKnD2r1L.js +1 -0
  3. package/dist/assets/ChatPage-DidO_pAN.js +32 -0
  4. package/dist/assets/{CronConfig-9dYfTRJl.js → CronConfig-C1pm-oKA.js} +1 -1
  5. package/dist/assets/{DocBrowser-BIV0vpA0.js → DocBrowser-BY90Lf6L.js} +1 -1
  6. package/dist/assets/{MarketplacePage-2Zi0JSVi.js → MarketplacePage-BRtmhP3G.js} +1 -1
  7. package/dist/assets/{ModelConfig-h21P5rV0.js → ModelConfig-Dga1Ko7_.js} +1 -1
  8. package/dist/assets/{ProvidersList-DEaK1a3y.js → ProvidersList-DvCoBTrT.js} +1 -1
  9. package/dist/assets/{RuntimeConfig-DXMzf-gF.js → RuntimeConfig-2aqBJ6Xn.js} +1 -1
  10. package/dist/assets/SecretsConfig-wlnh__z0.js +3 -0
  11. package/dist/assets/{SessionsConfig-SdXvn_9E.js → SessionsConfig-CN2WymbH.js} +2 -2
  12. package/dist/assets/{action-link-C9xMkxl2.js → action-link-DLZDwUfD.js} +1 -1
  13. package/dist/assets/{card-Cnqfntk5.js → card-D3dD-I5t.js} +1 -1
  14. package/dist/assets/chat-message-DZV2Z5oc.js +5 -0
  15. package/dist/assets/{dialog-DJs630RE.js → dialog-DZ0VC-RD.js} +1 -1
  16. package/dist/assets/index-BsDasSXm.css +1 -0
  17. package/dist/assets/index-D_vv0E-O.js +2 -0
  18. package/dist/assets/{label-CXGuE6Oa.js → label-CJIvvG6o.js} +1 -1
  19. package/dist/assets/{page-layout-BVZlyPFt.js → page-layout-CLgr0qym.js} +1 -1
  20. package/dist/assets/{switch-BLF45eI3.js → switch-C-0Q8OH2.js} +1 -1
  21. package/dist/assets/{tabs-custom-DQ0GpEV5.js → tabs-custom-D4Gs3BGM.js} +1 -1
  22. package/dist/assets/useConfig-R5uGhZtD.js +1 -0
  23. package/dist/assets/{useConfirmDialog-CK7KAyDf.js → useConfirmDialog-AMeSTA83.js} +1 -1
  24. package/dist/assets/{vendor-RXIbhDBC.js → vendor-H2M3a_4Z.js} +1 -1
  25. package/dist/index.html +3 -3
  26. package/package.json +1 -1
  27. package/src/App.tsx +2 -0
  28. package/src/api/config.ts +16 -0
  29. package/src/api/types.ts +52 -0
  30. package/src/components/chat/ChatPage.tsx +67 -9
  31. package/src/components/chat/ChatThread.tsx +12 -3
  32. package/src/components/config/ChannelForm.tsx +9 -0
  33. package/src/components/config/SecretsConfig.tsx +469 -0
  34. package/src/components/layout/Sidebar.tsx +6 -1
  35. package/src/hooks/useConfig.ts +17 -0
  36. package/src/lib/chat-message.ts +80 -2
  37. package/src/lib/i18n.ts +42 -0
  38. package/dist/assets/ChannelsList-TFFw4Cem.js +0 -1
  39. package/dist/assets/ChatPage-BUm3UPap.js +0 -32
  40. package/dist/assets/chat-message-B7oqvJ2d.js +0 -3
  41. package/dist/assets/index-CrUDzcei.js +0 -2
  42. package/dist/assets/index-Zy7fAOe1.css +0 -1
  43. package/dist/assets/useConfig-vFQvF4kn.js +0 -1
package/src/api/types.ts CHANGED
@@ -175,6 +175,57 @@ export type RuntimeConfigUpdate = {
175
175
  session?: SessionConfigView;
176
176
  };
177
177
 
178
+ export type SecretSourceView = "env" | "file" | "exec";
179
+
180
+ export type SecretRefView = {
181
+ source: SecretSourceView;
182
+ provider?: string;
183
+ id: string;
184
+ };
185
+
186
+ export type SecretProviderEnvView = {
187
+ source: "env";
188
+ prefix?: string;
189
+ };
190
+
191
+ export type SecretProviderFileView = {
192
+ source: "file";
193
+ path: string;
194
+ format?: "json";
195
+ };
196
+
197
+ export type SecretProviderExecView = {
198
+ source: "exec";
199
+ command: string;
200
+ args?: string[];
201
+ cwd?: string;
202
+ timeoutMs?: number;
203
+ };
204
+
205
+ export type SecretProviderView = SecretProviderEnvView | SecretProviderFileView | SecretProviderExecView;
206
+
207
+ export type SecretsView = {
208
+ enabled: boolean;
209
+ defaults: {
210
+ env?: string;
211
+ file?: string;
212
+ exec?: string;
213
+ };
214
+ providers: Record<string, SecretProviderView>;
215
+ refs: Record<string, SecretRefView>;
216
+ };
217
+
218
+ export type SecretsConfigUpdate = {
219
+ enabled?: boolean;
220
+ defaults?: {
221
+ env?: string | null;
222
+ file?: string | null;
223
+ exec?: string | null;
224
+ };
225
+ providers?: Record<string, SecretProviderView> | null;
226
+ refs?: Record<string, SecretRefView> | null;
227
+ };
228
+
178
229
  export type ChannelConfigUpdate = Record<string, unknown>;
179
230
 
180
231
  export type ConfigView = {
@@ -207,6 +258,7 @@ export type ConfigView = {
207
258
  session?: SessionConfigView;
208
259
  tools?: Record<string, unknown>;
209
260
  gateway?: Record<string, unknown>;
261
+ secrets?: SecretsView;
210
262
  };
211
263
 
212
264
  export type ProviderSpecView = {
@@ -12,6 +12,21 @@ import { formatDateTime, t } from '@/lib/i18n';
12
12
  import { MessageSquareText, Plus, RefreshCw, Search, Send, Trash2 } from 'lucide-react';
13
13
 
14
14
  const CHAT_SESSION_STORAGE_KEY = 'nextclaw.ui.chat.activeSession';
15
+ const STREAM_FRAME_MS = 18;
16
+
17
+ function streamChunkSize(remaining: number): number {
18
+ if (remaining > 2400) return 120;
19
+ if (remaining > 1200) return 72;
20
+ if (remaining > 600) return 40;
21
+ if (remaining > 220) return 20;
22
+ return 8;
23
+ }
24
+
25
+ function delay(ms: number): Promise<void> {
26
+ return new Promise((resolve) => {
27
+ window.setTimeout(resolve, ms);
28
+ });
29
+ }
15
30
 
16
31
  function readStoredSessionKey(): string | null {
17
32
  if (typeof window === 'undefined') {
@@ -68,9 +83,11 @@ export function ChatPage() {
68
83
  const [selectedSessionKey, setSelectedSessionKey] = useState<string | null>(() => readStoredSessionKey());
69
84
  const [selectedAgentId, setSelectedAgentId] = useState('main');
70
85
  const [optimisticUserMessage, setOptimisticUserMessage] = useState<SessionMessageView | null>(null);
86
+ const [streamingAssistantMessage, setStreamingAssistantMessage] = useState<SessionMessageView | null>(null);
71
87
 
72
88
  const { confirm, ConfirmDialog } = useConfirmDialog();
73
89
  const threadRef = useRef<HTMLDivElement | null>(null);
90
+ const streamRunIdRef = useRef(0);
74
91
 
75
92
  const configQuery = useConfig();
76
93
  const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
@@ -96,12 +113,20 @@ export function ChatPage() {
96
113
  );
97
114
 
98
115
  const historyMessages = useMemo(() => historyQuery.data?.messages ?? [], [historyQuery.data?.messages]);
116
+ const isGenerating = sendChatTurn.isPending || Boolean(streamingAssistantMessage);
99
117
  const mergedMessages = useMemo(() => {
100
- if (!optimisticUserMessage) {
118
+ if (!optimisticUserMessage && !streamingAssistantMessage) {
101
119
  return historyMessages;
102
120
  }
103
- return [...historyMessages, optimisticUserMessage];
104
- }, [historyMessages, optimisticUserMessage]);
121
+ const next = [...historyMessages];
122
+ if (optimisticUserMessage) {
123
+ next.push(optimisticUserMessage);
124
+ }
125
+ if (streamingAssistantMessage) {
126
+ next.push(streamingAssistantMessage);
127
+ }
128
+ return next;
129
+ }, [historyMessages, optimisticUserMessage, streamingAssistantMessage]);
105
130
 
106
131
  useEffect(() => {
107
132
  if (!selectedSessionKey && sessions.length > 0) {
@@ -129,9 +154,17 @@ export function ChatPage() {
129
154
  return;
130
155
  }
131
156
  element.scrollTop = element.scrollHeight;
132
- }, [mergedMessages.length, sendChatTurn.isPending, selectedSessionKey]);
157
+ }, [mergedMessages, sendChatTurn.isPending, selectedSessionKey]);
158
+
159
+ useEffect(() => {
160
+ return () => {
161
+ streamRunIdRef.current += 1;
162
+ };
163
+ }, []);
133
164
 
134
165
  const createNewSession = () => {
166
+ streamRunIdRef.current += 1;
167
+ setStreamingAssistantMessage(null);
135
168
  const next = buildNewSessionKey(selectedAgentId);
136
169
  setSelectedSessionKey(next);
137
170
  setOptimisticUserMessage(null);
@@ -153,6 +186,8 @@ export function ChatPage() {
153
186
  { key: selectedSessionKey },
154
187
  {
155
188
  onSuccess: async () => {
189
+ streamRunIdRef.current += 1;
190
+ setStreamingAssistantMessage(null);
156
191
  setSelectedSessionKey(null);
157
192
  setOptimisticUserMessage(null);
158
193
  await sessionsQuery.refetch();
@@ -163,10 +198,12 @@ export function ChatPage() {
163
198
 
164
199
  const handleSend = async () => {
165
200
  const message = draft.trim();
166
- if (!message || sendChatTurn.isPending) {
201
+ if (!message || isGenerating) {
167
202
  return;
168
203
  }
169
204
 
205
+ streamRunIdRef.current += 1;
206
+ setStreamingAssistantMessage(null);
170
207
  const hadActiveSession = Boolean(selectedSessionKey);
171
208
  const sessionKey = selectedSessionKey ?? buildNewSessionKey(selectedAgentId);
172
209
  if (!selectedSessionKey) {
@@ -193,11 +230,32 @@ export function ChatPage() {
193
230
  if (result.sessionKey !== sessionKey) {
194
231
  setSelectedSessionKey(result.sessionKey);
195
232
  }
233
+ const replyText = typeof result.reply === 'string' ? result.reply : '';
234
+ let previewRunId: number | null = null;
235
+ if (replyText.trim()) {
236
+ previewRunId = ++streamRunIdRef.current;
237
+ const timestamp = new Date().toISOString();
238
+ let cursor = 0;
239
+ while (cursor < replyText.length && previewRunId === streamRunIdRef.current) {
240
+ cursor = Math.min(replyText.length, cursor + streamChunkSize(replyText.length - cursor));
241
+ setStreamingAssistantMessage({
242
+ role: 'assistant',
243
+ content: replyText.slice(0, cursor),
244
+ timestamp
245
+ });
246
+ await delay(STREAM_FRAME_MS);
247
+ }
248
+ }
196
249
  await sessionsQuery.refetch();
197
250
  if (hadActiveSession) {
198
251
  await historyQuery.refetch();
199
252
  }
253
+ if (previewRunId && previewRunId === streamRunIdRef.current) {
254
+ setStreamingAssistantMessage(null);
255
+ }
200
256
  } catch {
257
+ streamRunIdRef.current += 1;
258
+ setStreamingAssistantMessage(null);
201
259
  setOptimisticUserMessage(null);
202
260
  setDraft(message);
203
261
  }
@@ -335,7 +393,7 @@ export function ChatPage() {
335
393
  {mergedMessages.length === 0 ? (
336
394
  <div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
337
395
  ) : (
338
- <ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending} />
396
+ <ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending && !streamingAssistantMessage} />
339
397
  )}
340
398
  </>
341
399
  )}
@@ -354,7 +412,7 @@ export function ChatPage() {
354
412
  }}
355
413
  placeholder={t('chatInputPlaceholder')}
356
414
  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}
415
+ disabled={isGenerating}
358
416
  />
359
417
  <div className="flex items-center justify-between px-2 pb-1">
360
418
  <div className="text-[11px] text-gray-400">{t('chatInputHint')}</div>
@@ -362,10 +420,10 @@ export function ChatPage() {
362
420
  size="sm"
363
421
  className="rounded-lg"
364
422
  onClick={() => void handleSend()}
365
- disabled={sendChatTurn.isPending || draft.trim().length === 0}
423
+ disabled={isGenerating || draft.trim().length === 0}
366
424
  >
367
425
  <Send className="h-3.5 w-3.5 mr-1.5" />
368
- {sendChatTurn.isPending ? t('chatSending') : t('chatSend')}
426
+ {isGenerating ? t('chatSending') : t('chatSend')}
369
427
  </Button>
370
428
  </div>
371
429
  </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)}>
@@ -256,6 +256,15 @@ export function ChannelForm() {
256
256
  if (!channelName) return;
257
257
 
258
258
  const payload: Record<string, unknown> = { ...formData };
259
+ for (const field of fields) {
260
+ if (field.type !== 'password') {
261
+ continue;
262
+ }
263
+ const value = payload[field.name];
264
+ if (typeof value !== 'string' || value.length === 0) {
265
+ delete payload[field.name];
266
+ }
267
+ }
259
268
  for (const field of fields) {
260
269
  if (field.type !== 'json') {
261
270
  continue;