@nextclaw/ui 0.6.1 → 0.6.3

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-CkCpHSto.js → ChannelsList-Bga6n85j.js} +1 -1
  3. package/dist/assets/ChatPage-B-Yk3kkv.js +32 -0
  4. package/dist/assets/{DocBrowser-B5Aqiz6W.js → DocBrowser-dv57PRp5.js} +1 -1
  5. package/dist/assets/{MarketplacePage-BIi0bBdW.js → MarketplacePage-j6p73Hjo.js} +1 -1
  6. package/dist/assets/{ModelConfig-BTFiEAxQ.js → ModelConfig-BiKSDp5h.js} +1 -1
  7. package/dist/assets/{ProvidersList-cdk1d-G_.js → ProvidersList-B7ZfRUkD.js} +1 -1
  8. package/dist/assets/{RuntimeConfig-CFqFsXmR.js → RuntimeConfig-Bpt9UNb6.js} +1 -1
  9. package/dist/assets/{SecretsConfig-CIKasCek.js → SecretsConfig-Ds00G-_O.js} +2 -2
  10. package/dist/assets/{SessionsConfig-mnCLFtbo.js → SessionsConfig-Mjet4opU.js} +1 -1
  11. package/dist/assets/{card-C1BUfR85.js → card-C7JJ5BGA.js} +1 -1
  12. package/dist/assets/index-BiJ2xs5X.css +1 -0
  13. package/dist/assets/{index-Dxas8MJ9.js → index-Cb9xiqC5.js} +2 -2
  14. package/dist/assets/{label-CwWfYbuj.js → label-DHJKdaUl.js} +1 -1
  15. package/dist/assets/{logos-DDyjHSEU.js → logos-fPO_amyL.js} +1 -1
  16. package/dist/assets/{page-layout-DKTRKcHL.js → page-layout-CF0JQsWW.js} +1 -1
  17. package/dist/assets/{switch-Bi3yeYiC.js → switch-C1hgy-fE.js} +1 -1
  18. package/dist/assets/{tabs-custom-HZFNZrc0.js → tabs-custom-OyoLf5ZM.js} +1 -1
  19. package/dist/assets/useConfig-D_G46zbo.js +6 -0
  20. package/dist/assets/{useConfirmDialog-DwD21HlD.js → useConfirmDialog-_0u6i3cI.js} +1 -1
  21. package/dist/index.html +2 -2
  22. package/package.json +1 -1
  23. package/src/api/config.ts +77 -12
  24. package/src/api/types.ts +24 -0
  25. package/src/components/chat/ChatConversationPanel.tsx +5 -20
  26. package/src/components/chat/ChatPage.tsx +49 -8
  27. package/src/components/chat/useChatStreamController.ts +192 -115
  28. package/src/hooks/useConfig.ts +29 -0
  29. package/src/hooks/useWebSocket.ts +21 -0
  30. package/dist/assets/ChatPage-DM4XNsrW.js +0 -32
  31. package/dist/assets/index-P4YzN9iS.css +0 -1
  32. package/dist/assets/useConfig-CgzVQTZl.js +0 -6
package/src/api/config.ts CHANGED
@@ -24,6 +24,9 @@ import type {
24
24
  ChatCapabilitiesView,
25
25
  ChatTurnStopRequest,
26
26
  ChatTurnStopResult,
27
+ ChatRunListView,
28
+ ChatRunState,
29
+ ChatRunView,
27
30
  CronListView,
28
31
  CronEnableRequest,
29
32
  CronRunRequest,
@@ -270,6 +273,39 @@ export async function stopChatTurn(data: ChatTurnStopRequest): Promise<ChatTurnS
270
273
  return response.data;
271
274
  }
272
275
 
276
+ // GET /api/chat/runs
277
+ export async function fetchChatRuns(params?: {
278
+ sessionKey?: string;
279
+ states?: ChatRunState[];
280
+ limit?: number;
281
+ }): Promise<ChatRunListView> {
282
+ const query = new URLSearchParams();
283
+ if (params?.sessionKey?.trim()) {
284
+ query.set('sessionKey', params.sessionKey.trim());
285
+ }
286
+ if (Array.isArray(params?.states) && params.states.length > 0) {
287
+ query.set('states', params.states.join(','));
288
+ }
289
+ if (typeof params?.limit === 'number' && Number.isFinite(params.limit)) {
290
+ query.set('limit', String(Math.max(0, Math.trunc(params.limit))));
291
+ }
292
+ const suffix = query.toString();
293
+ const response = await api.get<ChatRunListView>(suffix ? `/api/chat/runs?${suffix}` : '/api/chat/runs');
294
+ if (!response.ok) {
295
+ throw new Error(response.error.message);
296
+ }
297
+ return response.data;
298
+ }
299
+
300
+ // GET /api/chat/runs/:runId
301
+ export async function fetchChatRun(runId: string): Promise<ChatRunView> {
302
+ const response = await api.get<ChatRunView>(`/api/chat/runs/${encodeURIComponent(runId)}`);
303
+ if (!response.ok) {
304
+ throw new Error(response.error.message);
305
+ }
306
+ return response.data;
307
+ }
308
+
273
309
  type ChatTurnStreamOptions = {
274
310
  signal?: AbortSignal;
275
311
  onReady?: (event: ChatTurnStreamReadyEvent) => void;
@@ -304,20 +340,10 @@ function parseSseFrame(frame: string): SseParsedEvent | null {
304
340
  };
305
341
  }
306
342
 
307
- export async function sendChatTurnStream(
308
- data: ChatTurnRequest,
343
+ async function consumeChatTurnSseStream(
344
+ response: Response,
309
345
  options: ChatTurnStreamOptions = {}
310
346
  ): Promise<ChatTurnView> {
311
- const response = await fetch(`${API_BASE}/api/chat/turn/stream`, {
312
- method: 'POST',
313
- headers: {
314
- 'Content-Type': 'application/json',
315
- Accept: 'text/event-stream'
316
- },
317
- body: JSON.stringify(data),
318
- signal: options.signal
319
- });
320
-
321
347
  if (!response.ok) {
322
348
  let message = `chat stream failed (${response.status} ${response.statusText})`;
323
349
  try {
@@ -449,6 +475,45 @@ export async function sendChatTurnStream(
449
475
  return finalView;
450
476
  }
451
477
 
478
+ export async function sendChatTurnStream(
479
+ data: ChatTurnRequest,
480
+ options: ChatTurnStreamOptions = {}
481
+ ): Promise<ChatTurnView> {
482
+ const response = await fetch(`${API_BASE}/api/chat/turn/stream`, {
483
+ method: 'POST',
484
+ headers: {
485
+ 'Content-Type': 'application/json',
486
+ Accept: 'text/event-stream'
487
+ },
488
+ body: JSON.stringify(data),
489
+ signal: options.signal
490
+ });
491
+ return consumeChatTurnSseStream(response, options);
492
+ }
493
+
494
+ export async function streamChatRun(
495
+ params: {
496
+ runId: string;
497
+ fromEventIndex?: number;
498
+ },
499
+ options: ChatTurnStreamOptions = {}
500
+ ): Promise<ChatTurnView> {
501
+ const query = new URLSearchParams();
502
+ if (typeof params.fromEventIndex === 'number' && Number.isFinite(params.fromEventIndex)) {
503
+ query.set('fromEventIndex', String(Math.max(0, Math.trunc(params.fromEventIndex))));
504
+ }
505
+ const suffix = query.toString();
506
+ const url = `${API_BASE}/api/chat/runs/${encodeURIComponent(params.runId)}/stream${suffix ? `?${suffix}` : ''}`;
507
+ const response = await fetch(url, {
508
+ method: 'GET',
509
+ headers: {
510
+ Accept: 'text/event-stream'
511
+ },
512
+ signal: options.signal
513
+ });
514
+ return consumeChatTurnSseStream(response, options);
515
+ }
516
+
452
517
  // GET /api/cron
453
518
  export async function fetchCronJobs(params?: { all?: boolean }): Promise<CronListView> {
454
519
  const query = new URLSearchParams();
package/src/api/types.ts CHANGED
@@ -187,6 +187,29 @@ export type ChatTurnStopResult = {
187
187
  reason?: string;
188
188
  };
189
189
 
190
+ export type ChatRunState = 'queued' | 'running' | 'completed' | 'failed' | 'aborted';
191
+
192
+ export type ChatRunView = {
193
+ runId: string;
194
+ sessionKey: string;
195
+ agentId?: string;
196
+ model?: string;
197
+ state: ChatRunState;
198
+ requestedAt: string;
199
+ startedAt?: string;
200
+ completedAt?: string;
201
+ stopSupported: boolean;
202
+ stopReason?: string;
203
+ error?: string;
204
+ reply?: string;
205
+ eventCount: number;
206
+ };
207
+
208
+ export type ChatRunListView = {
209
+ runs: ChatRunView[];
210
+ total: number;
211
+ };
212
+
190
213
  export type ChatTurnStreamReadyEvent = {
191
214
  event: "ready";
192
215
  sessionKey: string;
@@ -469,6 +492,7 @@ export type ConfigActionExecuteResult = {
469
492
  // WebSocket events
470
493
  export type WsEvent =
471
494
  | { type: 'config.updated'; payload: { path: string } }
495
+ | { type: 'run.updated'; payload: { run: ChatRunView } }
472
496
  | { type: 'config.reload.started'; payload?: Record<string, unknown> }
473
497
  | { type: 'config.reload.finished'; payload?: Record<string, unknown> }
474
498
  | { type: 'error'; payload: { message: string; code?: string } }
@@ -8,6 +8,7 @@ import { t } from '@/lib/i18n';
8
8
  import { Trash2 } from 'lucide-react';
9
9
 
10
10
  type ChatConversationPanelProps = {
11
+ isProviderStateResolved: boolean;
11
12
  modelOptions: ChatModelOption[];
12
13
  selectedModel: string;
13
14
  onSelectedModelChange: (value: string) => void;
@@ -40,6 +41,7 @@ type ChatConversationPanelProps = {
40
41
  };
41
42
 
42
43
  export function ChatConversationPanel({
44
+ isProviderStateResolved,
43
45
  modelOptions,
44
46
  selectedModel,
45
47
  onSelectedModelChange,
@@ -72,18 +74,13 @@ export function ChatConversationPanel({
72
74
  }: ChatConversationPanelProps) {
73
75
  const showWelcome = !selectedSessionKey && mergedEvents.length === 0;
74
76
  const hasConfiguredModel = modelOptions.length > 0;
77
+ const shouldShowProviderHint = isProviderStateResolved && !hasConfiguredModel;
75
78
  const hideEmptyHint =
76
79
  isHistoryLoading &&
77
80
  mergedEvents.length === 0 &&
78
81
  !isSending &&
79
82
  !isAwaitingAssistantOutput &&
80
83
  !streamingAssistantText.trim();
81
- const shouldShowProviderSetup =
82
- !hasConfiguredModel &&
83
- !selectedSessionKey &&
84
- mergedEvents.length === 0 &&
85
- !hideEmptyHint &&
86
- !isSending;
87
84
 
88
85
  return (
89
86
  <section className="flex-1 min-h-0 flex flex-col overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
@@ -107,7 +104,7 @@ export function ChatConversationPanel({
107
104
  </div>
108
105
  )}
109
106
 
110
- {!hasConfiguredModel && !showWelcome && (
107
+ {shouldShowProviderHint && (
111
108
  <div className="px-5 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0">
112
109
  <span className="text-xs text-amber-800">{t('chatModelNoOptions')}</span>
113
110
  <button
@@ -123,19 +120,7 @@ export function ChatConversationPanel({
123
120
  {/* Message thread or welcome */}
124
121
  <div ref={threadRef} onScroll={onThreadScroll} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar">
125
122
  {showWelcome ? (
126
- shouldShowProviderSetup ? (
127
- <div className="h-full flex items-center justify-center p-8">
128
- <div className="w-full max-w-xl rounded-2xl border border-amber-200 bg-amber-50/70 p-6 text-center">
129
- <h2 className="text-lg font-semibold text-amber-900">{t('chatProviderSetupTitle')}</h2>
130
- <p className="mt-2 text-sm text-amber-800">{t('chatProviderSetupDescription')}</p>
131
- <Button className="mt-4" onClick={onGoToProviders}>
132
- {t('chatGoConfigureProvider')}
133
- </Button>
134
- </div>
135
- </div>
136
- ) : (
137
- <ChatWelcome onCreateSession={onCreateSession} />
138
- )
123
+ <ChatWelcome onCreateSession={onCreateSession} />
139
124
  ) : hideEmptyHint ? (
140
125
  <div className="h-full" />
141
126
  ) : mergedEvents.length === 0 ? (
@@ -7,7 +7,8 @@ import {
7
7
  useConfigMeta,
8
8
  useDeleteSession,
9
9
  useSessionHistory,
10
- useSessions
10
+ useSessions,
11
+ useChatRuns
11
12
  } from '@/hooks/useConfig';
12
13
  import { useMarketplaceInstalled } from '@/hooks/useMarketplace';
13
14
  import { useConfirmDialog } from '@/hooks/useConfirmDialog';
@@ -242,6 +243,9 @@ export function ChatPage({ view }: ChatPageProps) {
242
243
 
243
244
  const configQuery = useConfig();
244
245
  const configMetaQuery = useConfigMeta();
246
+ const isProviderStateResolved =
247
+ (configQuery.isFetched || configQuery.isSuccess) &&
248
+ (configMetaQuery.isFetched || configMetaQuery.isSuccess);
245
249
  const sessionsQuery = useSessions({ q: query.trim() || undefined, limit: 120, activeMinutes: 0 });
246
250
  const installedSkillsQuery = useMarketplaceInstalled('skill');
247
251
  const chatCapabilitiesQuery = useChatCapabilities({
@@ -334,6 +338,8 @@ export function ChatPage({ view }: ChatPageProps) {
334
338
  stopDisabledReason,
335
339
  lastSendError,
336
340
  sendMessage,
341
+ resumeRun,
342
+ activeBackendRunId,
337
343
  stopCurrentRun,
338
344
  resetStreamState
339
345
  } = useChatStreamController({
@@ -345,17 +351,51 @@ export function ChatPage({ view }: ChatPageProps) {
345
351
  refetchHistory: historyQuery.refetch
346
352
  });
347
353
 
354
+ const activeRunsQuery = useChatRuns(
355
+ selectedSessionKey
356
+ ? {
357
+ sessionKey: selectedSessionKey,
358
+ states: ['queued', 'running'],
359
+ limit: 5
360
+ }
361
+ : undefined
362
+ );
363
+ const activeRun = useMemo(() => {
364
+ const candidates = activeRunsQuery.data?.runs ?? [];
365
+ if (!selectedSessionKey) {
366
+ return null;
367
+ }
368
+ return candidates.find((entry) => entry.sessionKey === selectedSessionKey) ?? null;
369
+ }, [activeRunsQuery.data?.runs, selectedSessionKey]);
370
+
371
+ useEffect(() => {
372
+ if (view !== 'chat' || !selectedSessionKey || !activeRun) {
373
+ return;
374
+ }
375
+ if (activeBackendRunId === activeRun.runId) {
376
+ return;
377
+ }
378
+ void resumeRun(activeRun);
379
+ }, [activeBackendRunId, activeRun, resumeRun, selectedSessionKey, view]);
380
+
348
381
  const mergedEvents = useMemo(() => {
349
- const next = [...historyEvents];
382
+ const bySeq = new Map<number, SessionEventView>();
383
+ const append = (event: SessionEventView) => {
384
+ if (!Number.isFinite(event.seq)) {
385
+ return;
386
+ }
387
+ bySeq.set(event.seq, event);
388
+ };
389
+
390
+ historyEvents.forEach(append);
350
391
  if (optimisticUserEvent) {
351
- next.push(optimisticUserEvent);
392
+ append(optimisticUserEvent);
352
393
  }
353
- next.push(...streamingSessionEvents);
394
+ streamingSessionEvents.forEach(append);
395
+
396
+ const next = [...bySeq.values()].sort((left, right) => left.seq - right.seq);
354
397
  if (streamingAssistantText.trim()) {
355
- const maxSeq = next.reduce((max, event) => {
356
- const seq = Number.isFinite(event.seq) ? event.seq : 0;
357
- return seq > max ? seq : max;
358
- }, 0);
398
+ const maxSeq = next.reduce((max, event) => (event.seq > max ? event.seq : max), 0);
359
399
  next.push({
360
400
  seq: maxSeq + 1,
361
401
  type: 'stream.assistant_delta',
@@ -483,6 +523,7 @@ export function ChatPage({ view }: ChatPageProps) {
483
523
  };
484
524
 
485
525
  const conversationProps: ComponentProps<typeof ChatConversationPanel> = {
526
+ isProviderStateResolved,
486
527
  modelOptions,
487
528
  selectedModel,
488
529
  onSelectedModelChange: setSelectedModel,