@nextclaw/ui 0.6.9 → 0.6.11

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 (83) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/assets/{ChannelsList-DACqpUYZ.js → ChannelsList-C49JQ-Zt.js} +1 -1
  4. package/dist/assets/ChatPage-DIx05c6s.js +36 -0
  5. package/dist/assets/{DocBrowser-D7mjKkGe.js → DocBrowser-CpOosDEI.js} +1 -1
  6. package/dist/assets/{LogoBadge-BlDT-g9R.js → LogoBadge-CL_8ZPXU.js} +1 -1
  7. package/dist/assets/MarketplacePage-BOzko5s9.js +49 -0
  8. package/dist/assets/{ModelConfig-DwRU5qrw.js → ModelConfig-BZ4ZfaQB.js} +1 -1
  9. package/dist/assets/ProvidersList-fPpJ5gl6.js +1 -0
  10. package/dist/assets/{RuntimeConfig-C7BRLGSC.js → RuntimeConfig-Dt9pLB9P.js} +1 -1
  11. package/dist/assets/{SecretsConfig-D5xZh7VF.js → SecretsConfig-C1PU0Yy8.js} +2 -2
  12. package/dist/assets/{SessionsConfig-ovpj_otA.js → SessionsConfig-EskBOofQ.js} +2 -2
  13. package/dist/assets/{card-Bf4CtrW8.js → card-C7Gtw2Vs.js} +1 -1
  14. package/dist/assets/index-Cn6_2To7.js +8 -0
  15. package/dist/assets/index-nEYGCJTC.css +1 -0
  16. package/dist/assets/{input-CaKJyoWZ.js → input-oBvxsnV9.js} +1 -1
  17. package/dist/assets/{label-BaXSWTKI.js → label-C7F8lMpQ.js} +1 -1
  18. package/dist/assets/{page-layout-DA6PFRtQ.js → page-layout-DO8BlScF.js} +1 -1
  19. package/dist/assets/session-run-status-Kg0FwAPn.js +3 -0
  20. package/dist/assets/{switch-Cvd5wZs-.js → switch-C6a5GyZB.js} +1 -1
  21. package/dist/assets/{tabs-custom-0PybLkXs.js → tabs-custom-BatFap5k.js} +1 -1
  22. package/dist/assets/{useConfirmDialog-DdtpSju1.js → useConfirmDialog-zJzVKMdu.js} +2 -2
  23. package/dist/assets/{vendor-C--HHaLf.js → vendor-TlME1INH.js} +84 -84
  24. package/dist/index.html +3 -3
  25. package/package.json +4 -2
  26. package/src/App.tsx +1 -2
  27. package/src/api/config.ts +205 -202
  28. package/src/api/types.ts +54 -24
  29. package/src/components/chat/ChatConversationPanel.tsx +102 -121
  30. package/src/components/chat/ChatPage.tsx +165 -437
  31. package/src/components/chat/ChatSidebar.tsx +30 -36
  32. package/src/components/chat/ChatThread.tsx +73 -131
  33. package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
  34. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
  35. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
  36. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
  37. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
  38. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
  39. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
  40. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
  41. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
  42. package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
  43. package/src/components/chat/chat-input.types.ts +15 -0
  44. package/src/components/chat/chat-page-data.ts +121 -0
  45. package/src/components/chat/chat-page-runtime.ts +221 -0
  46. package/src/components/chat/chat-session-route.ts +59 -0
  47. package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
  48. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
  49. package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
  50. package/src/components/chat/chat-stream/transport.ts +159 -0
  51. package/src/components/chat/chat-stream/types.ts +76 -0
  52. package/src/components/chat/managers/chat-input.manager.ts +142 -0
  53. package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
  54. package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
  55. package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
  56. package/src/components/chat/managers/chat-thread.manager.ts +86 -0
  57. package/src/components/chat/managers/chat-ui.manager.ts +65 -0
  58. package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
  59. package/src/components/chat/presenter/chat.presenter.ts +32 -0
  60. package/src/components/chat/stores/chat-input.store.ts +62 -0
  61. package/src/components/chat/stores/chat-run-status.store.ts +30 -0
  62. package/src/components/chat/stores/chat-session-list.store.ts +34 -0
  63. package/src/components/chat/stores/chat-thread.store.ts +52 -0
  64. package/src/components/chat/useChatRuntimeController.ts +134 -0
  65. package/src/components/chat/useChatSessionTypeState.ts +148 -0
  66. package/src/components/common/MaskedInput.tsx +1 -1
  67. package/src/components/config/ProviderForm.tsx +221 -14
  68. package/src/hooks/useConfig.ts +33 -2
  69. package/src/hooks/useObservable.ts +20 -0
  70. package/src/hooks/useWebSocket.ts +23 -1
  71. package/src/lib/chat-message.ts +2 -202
  72. package/src/lib/chat-runtime-utils.ts +250 -0
  73. package/src/lib/i18n.ts +11 -0
  74. package/tsconfig.json +2 -1
  75. package/vite.config.ts +2 -1
  76. package/dist/assets/ChatPage-iji0RkTR.js +0 -34
  77. package/dist/assets/MarketplacePage-CZq3jVgg.js +0 -49
  78. package/dist/assets/ProvidersList-DFxN3pjx.js +0 -1
  79. package/dist/assets/index-C_DhisNo.css +0 -1
  80. package/dist/assets/index-dKTqKCJo.js +0 -7
  81. package/dist/assets/session-run-status-CllIZxNf.js +0 -5
  82. package/src/components/chat/ChatInputBar.tsx +0 -590
  83. package/src/components/chat/useChatStreamController.ts +0 -591
@@ -1,591 +0,0 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
3
- import type { ChatRunView, SessionEventView } from '@/api/types';
4
- import { sendChatTurnStream, stopChatTurn, streamChatRun } from '@/api/config';
5
-
6
- type PendingChatMessage = {
7
- id: number;
8
- message: string;
9
- sessionKey: string;
10
- agentId: string;
11
- model?: string;
12
- requestedSkills?: string[];
13
- stopSupported?: boolean;
14
- stopReason?: string;
15
- };
16
-
17
- type ActiveRunState = {
18
- localRunId: number;
19
- sessionKey: string;
20
- agentId?: string;
21
- requestAbortController: AbortController;
22
- backendRunId?: string;
23
- backendStopSupported: boolean;
24
- backendStopReason?: string;
25
- };
26
-
27
- type SendMessageParams = {
28
- message: string;
29
- sessionKey: string;
30
- agentId: string;
31
- model?: string;
32
- requestedSkills?: string[];
33
- stopSupported?: boolean;
34
- stopReason?: string;
35
- restoreDraftOnError?: boolean;
36
- };
37
-
38
- type UseChatStreamControllerParams = {
39
- nextOptimisticUserSeq: number;
40
- selectedSessionKeyRef: MutableRefObject<string | null>;
41
- setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
42
- setDraft: Dispatch<SetStateAction<string>>;
43
- refetchSessions: () => Promise<unknown>;
44
- refetchHistory: () => Promise<unknown>;
45
- };
46
-
47
- function formatSendError(error: unknown): string {
48
- if (error instanceof Error) {
49
- const message = error.message.trim();
50
- if (message) {
51
- return message;
52
- }
53
- }
54
- const raw = String(error ?? '').trim();
55
- return raw || 'Failed to send message';
56
- }
57
-
58
- type StreamSetters = {
59
- setOptimisticUserEvent: Dispatch<SetStateAction<SessionEventView | null>>;
60
- setStreamingSessionEvents: Dispatch<SetStateAction<SessionEventView[]>>;
61
- setStreamingAssistantText: Dispatch<SetStateAction<string>>;
62
- setStreamingAssistantTimestamp: Dispatch<SetStateAction<string | null>>;
63
- setIsSending: Dispatch<SetStateAction<boolean>>;
64
- setIsAwaitingAssistantOutput: Dispatch<SetStateAction<boolean>>;
65
- setCanStopCurrentRun: Dispatch<SetStateAction<boolean>>;
66
- setStopDisabledReason: Dispatch<SetStateAction<string | null>>;
67
- setLastSendError: Dispatch<SetStateAction<string | null>>;
68
- };
69
-
70
- function clearStreamingState(setters: StreamSetters) {
71
- setters.setIsSending(false);
72
- setters.setOptimisticUserEvent(null);
73
- setters.setStreamingSessionEvents([]);
74
- setters.setStreamingAssistantText('');
75
- setters.setStreamingAssistantTimestamp(null);
76
- setters.setIsAwaitingAssistantOutput(false);
77
- setters.setCanStopCurrentRun(false);
78
- setters.setStopDisabledReason(null);
79
- setters.setLastSendError(null);
80
- }
81
-
82
- function normalizeRequestedSkills(value: string[] | undefined): string[] {
83
- if (!Array.isArray(value)) {
84
- return [];
85
- }
86
- const deduped = new Set<string>();
87
- for (const item of value) {
88
- const trimmed = item.trim();
89
- if (trimmed) {
90
- deduped.add(trimmed);
91
- }
92
- }
93
- return [...deduped];
94
- }
95
-
96
- function isAbortLikeError(error: unknown): boolean {
97
- if (error instanceof DOMException && error.name === 'AbortError') {
98
- return true;
99
- }
100
- if (error instanceof Error) {
101
- if (error.name === 'AbortError') {
102
- return true;
103
- }
104
- const lower = error.message.toLowerCase();
105
- if (lower.includes('aborted') || lower.includes('abort')) {
106
- return true;
107
- }
108
- }
109
- return false;
110
- }
111
-
112
- function buildLocalAssistantEvent(content: string, eventType = 'message.assistant.local'): SessionEventView {
113
- const timestamp = new Date().toISOString();
114
- return {
115
- seq: Date.now(),
116
- type: eventType,
117
- timestamp,
118
- message: {
119
- role: 'assistant',
120
- content,
121
- timestamp
122
- }
123
- };
124
- }
125
-
126
- async function refetchIfSessionVisible(params: {
127
- selectedSessionKeyRef: MutableRefObject<string | null>;
128
- currentSessionKey: string;
129
- resultSessionKey?: string;
130
- refetchSessions: () => Promise<unknown>;
131
- refetchHistory: () => Promise<unknown>;
132
- }): Promise<void> {
133
- await params.refetchSessions();
134
- const activeSessionKey = params.selectedSessionKeyRef.current;
135
- if (
136
- !activeSessionKey ||
137
- activeSessionKey === params.currentSessionKey ||
138
- (params.resultSessionKey && activeSessionKey === params.resultSessionKey)
139
- ) {
140
- await params.refetchHistory();
141
- }
142
- }
143
-
144
- function upsertStreamingEvent(
145
- setStreamingSessionEvents: Dispatch<SetStateAction<SessionEventView[]>>,
146
- event: SessionEventView
147
- ) {
148
- setStreamingSessionEvents((prev) => {
149
- const next = [...prev];
150
- const hit = next.findIndex((streamEvent) => streamEvent.seq === event.seq);
151
- if (hit >= 0) {
152
- next[hit] = event;
153
- } else {
154
- next.push(event);
155
- }
156
- return next;
157
- });
158
- }
159
-
160
- type ExecuteStreamRunParams = {
161
- runId: number;
162
- runIdRef: MutableRefObject<number>;
163
- activeRunRef: MutableRefObject<ActiveRunState | null>;
164
- selectedSessionKeyRef: MutableRefObject<string | null>;
165
- setSelectedSessionKey: Dispatch<SetStateAction<string | null>>;
166
- setDraft: Dispatch<SetStateAction<string>>;
167
- refetchSessions: () => Promise<unknown>;
168
- refetchHistory: () => Promise<unknown>;
169
- restoreDraftOnError?: boolean;
170
- sourceSessionKey: string;
171
- sourceAgentId?: string;
172
- sourceMessage?: string;
173
- sourceStopSupported?: boolean;
174
- sourceStopReason?: string;
175
- optimisticUserEvent: SessionEventView | null;
176
- openStream: (params: {
177
- signal: AbortSignal;
178
- onReady: (event: { runId?: string; stopSupported?: boolean; stopReason?: string; sessionKey: string }) => void;
179
- onDelta: (event: { delta: string }) => void;
180
- onSessionEvent: (event: { data: SessionEventView }) => void;
181
- }) => Promise<{ sessionKey: string; reply: string }>;
182
- setters: StreamSetters;
183
- };
184
-
185
- async function executeStreamRun(params: ExecuteStreamRunParams): Promise<void> {
186
- const {
187
- runId,
188
- runIdRef,
189
- activeRunRef,
190
- selectedSessionKeyRef,
191
- setSelectedSessionKey,
192
- setDraft,
193
- refetchSessions,
194
- refetchHistory,
195
- restoreDraftOnError,
196
- sourceSessionKey,
197
- sourceAgentId,
198
- sourceMessage,
199
- sourceStopSupported,
200
- sourceStopReason,
201
- optimisticUserEvent,
202
- openStream,
203
- setters
204
- } = params;
205
-
206
- const requestAbortController = new AbortController();
207
- activeRunRef.current = {
208
- localRunId: runId,
209
- sessionKey: sourceSessionKey,
210
- ...(sourceAgentId ? { agentId: sourceAgentId } : {}),
211
- requestAbortController,
212
- backendStopSupported: Boolean(sourceStopSupported),
213
- ...(sourceStopReason ? { backendStopReason: sourceStopReason } : {})
214
- };
215
-
216
- setters.setStreamingSessionEvents([]);
217
- setters.setStreamingAssistantText('');
218
- setters.setStreamingAssistantTimestamp(null);
219
- setters.setOptimisticUserEvent(optimisticUserEvent);
220
- setters.setIsSending(true);
221
- setters.setIsAwaitingAssistantOutput(true);
222
- setters.setCanStopCurrentRun(false);
223
- setters.setStopDisabledReason(sourceStopSupported ? '__preparing__' : sourceStopReason ?? null);
224
- setters.setLastSendError(null);
225
-
226
- let streamText = '';
227
- try {
228
- let hasAssistantSessionEvent = false;
229
- let hasUserSessionEvent = false;
230
- const streamTimestamp = new Date().toISOString();
231
- setters.setStreamingAssistantTimestamp(streamTimestamp);
232
-
233
- const result = await openStream({
234
- signal: requestAbortController.signal,
235
- onReady: (event) => {
236
- if (runId !== runIdRef.current) {
237
- return;
238
- }
239
- const activeRun = activeRunRef.current;
240
- if (activeRun && activeRun.localRunId === runId) {
241
- activeRun.backendRunId = event.runId?.trim() || undefined;
242
- if (typeof event.stopSupported === 'boolean') {
243
- activeRun.backendStopSupported = event.stopSupported;
244
- }
245
- if (typeof event.stopReason === 'string' && event.stopReason.trim().length > 0) {
246
- activeRun.backendStopReason = event.stopReason.trim();
247
- }
248
- const canStopNow = Boolean(activeRun.backendStopSupported && activeRun.backendRunId);
249
- setters.setCanStopCurrentRun(canStopNow);
250
- setters.setStopDisabledReason(
251
- canStopNow
252
- ? null
253
- : activeRun.backendStopReason ?? (activeRun.backendStopSupported ? '__preparing__' : null)
254
- );
255
- }
256
- if (event.sessionKey) {
257
- setSelectedSessionKey((prev) => (prev === event.sessionKey ? prev : event.sessionKey));
258
- }
259
- },
260
- onDelta: (event) => {
261
- if (runId !== runIdRef.current) {
262
- return;
263
- }
264
- streamText += event.delta;
265
- setters.setStreamingAssistantText(streamText);
266
- setters.setIsAwaitingAssistantOutput(false);
267
- },
268
- onSessionEvent: (event) => {
269
- if (runId !== runIdRef.current) {
270
- return;
271
- }
272
- if (event.data.message?.role === 'user') {
273
- hasUserSessionEvent = true;
274
- setters.setOptimisticUserEvent(null);
275
- }
276
- upsertStreamingEvent(setters.setStreamingSessionEvents, event.data);
277
- if (event.data.message?.role === 'assistant') {
278
- hasAssistantSessionEvent = true;
279
- streamText = '';
280
- setters.setStreamingAssistantText('');
281
- setters.setIsAwaitingAssistantOutput(false);
282
- }
283
- }
284
- });
285
- if (runId !== runIdRef.current) {
286
- return;
287
- }
288
- setters.setOptimisticUserEvent(null);
289
- if (result.sessionKey !== sourceSessionKey) {
290
- setSelectedSessionKey(result.sessionKey);
291
- }
292
-
293
- const finalReply = typeof result.reply === 'string' ? result.reply.trim() : '';
294
- const localAssistantText = !hasAssistantSessionEvent ? (streamText.trim() || finalReply) : '';
295
- const isSlashCommandMessage = typeof sourceMessage === 'string' && sourceMessage.trim().startsWith('/');
296
- const shouldKeepLocalUserCommand =
297
- !hasUserSessionEvent &&
298
- optimisticUserEvent?.message?.role === 'user' &&
299
- isSlashCommandMessage;
300
- await refetchIfSessionVisible({
301
- selectedSessionKeyRef,
302
- currentSessionKey: sourceSessionKey,
303
- resultSessionKey: result.sessionKey,
304
- refetchSessions,
305
- refetchHistory
306
- });
307
-
308
- const localEvents: SessionEventView[] = [];
309
- if (shouldKeepLocalUserCommand && optimisticUserEvent) {
310
- localEvents.push(optimisticUserEvent);
311
- }
312
- if (localAssistantText) {
313
- localEvents.push(buildLocalAssistantEvent(localAssistantText));
314
- }
315
- setters.setStreamingSessionEvents(localEvents);
316
-
317
- setters.setStreamingAssistantText('');
318
- setters.setStreamingAssistantTimestamp(null);
319
- setters.setIsAwaitingAssistantOutput(false);
320
- setters.setIsSending(false);
321
- setters.setCanStopCurrentRun(false);
322
- setters.setStopDisabledReason(null);
323
- setters.setLastSendError(null);
324
- activeRunRef.current = null;
325
- } catch (error) {
326
- if (runId !== runIdRef.current) {
327
- return;
328
- }
329
- const wasAborted = requestAbortController.signal.aborted || isAbortLikeError(error);
330
- runIdRef.current += 1;
331
- if (wasAborted) {
332
- clearStreamingState(setters);
333
- activeRunRef.current = null;
334
- await refetchIfSessionVisible({
335
- selectedSessionKeyRef,
336
- currentSessionKey: sourceSessionKey,
337
- refetchSessions,
338
- refetchHistory
339
- });
340
- return;
341
- }
342
-
343
- clearStreamingState(setters);
344
- const sendError = formatSendError(error);
345
- setters.setLastSendError(sendError);
346
- setters.setStreamingSessionEvents([buildLocalAssistantEvent(sendError, 'message.assistant.error.local')]);
347
- activeRunRef.current = null;
348
- if (restoreDraftOnError) {
349
- setDraft((prev) => (prev.trim().length === 0 && sourceMessage ? sourceMessage : prev));
350
- }
351
- }
352
- }
353
-
354
- export function useChatStreamController(params: UseChatStreamControllerParams) {
355
- const [optimisticUserEvent, setOptimisticUserEvent] = useState<SessionEventView | null>(null);
356
- const [streamingSessionEvents, setStreamingSessionEvents] = useState<SessionEventView[]>([]);
357
- const [streamingAssistantText, setStreamingAssistantText] = useState('');
358
- const [streamingAssistantTimestamp, setStreamingAssistantTimestamp] = useState<string | null>(null);
359
- const [isSending, setIsSending] = useState(false);
360
- const [isAwaitingAssistantOutput, setIsAwaitingAssistantOutput] = useState(false);
361
- const [queuedMessages, setQueuedMessages] = useState<PendingChatMessage[]>([]);
362
- const [canStopCurrentRun, setCanStopCurrentRun] = useState(false);
363
- const [stopDisabledReason, setStopDisabledReason] = useState<string | null>(null);
364
- const [lastSendError, setLastSendError] = useState<string | null>(null);
365
-
366
- const streamRunIdRef = useRef(0);
367
- const queueIdRef = useRef(0);
368
- const activeRunRef = useRef<ActiveRunState | null>(null);
369
-
370
- const resetStreamState = useCallback(() => {
371
- streamRunIdRef.current += 1;
372
- setQueuedMessages([]);
373
- activeRunRef.current?.requestAbortController.abort();
374
- activeRunRef.current = null;
375
- clearStreamingState({
376
- setOptimisticUserEvent,
377
- setStreamingSessionEvents,
378
- setStreamingAssistantText,
379
- setStreamingAssistantTimestamp,
380
- setIsSending,
381
- setIsAwaitingAssistantOutput,
382
- setCanStopCurrentRun,
383
- setStopDisabledReason,
384
- setLastSendError
385
- });
386
- }, []);
387
-
388
- useEffect(() => {
389
- return () => {
390
- streamRunIdRef.current += 1;
391
- activeRunRef.current?.requestAbortController.abort();
392
- activeRunRef.current = null;
393
- };
394
- }, []);
395
-
396
- const runSend = useCallback(
397
- async (item: PendingChatMessage, options?: { restoreDraftOnError?: boolean }) => {
398
- setLastSendError(null);
399
- streamRunIdRef.current += 1;
400
- const requestedSkills = normalizeRequestedSkills(item.requestedSkills);
401
- await executeStreamRun({
402
- runId: streamRunIdRef.current,
403
- runIdRef: streamRunIdRef,
404
- activeRunRef,
405
- selectedSessionKeyRef: params.selectedSessionKeyRef,
406
- setSelectedSessionKey: params.setSelectedSessionKey,
407
- setDraft: params.setDraft,
408
- refetchSessions: params.refetchSessions,
409
- refetchHistory: params.refetchHistory,
410
- restoreDraftOnError: options?.restoreDraftOnError,
411
- sourceSessionKey: item.sessionKey,
412
- sourceAgentId: item.agentId,
413
- sourceMessage: item.message,
414
- sourceStopSupported: item.stopSupported,
415
- sourceStopReason: item.stopReason,
416
- optimisticUserEvent: {
417
- seq: params.nextOptimisticUserSeq,
418
- type: 'message.user.optimistic',
419
- timestamp: new Date().toISOString(),
420
- message: {
421
- role: 'user',
422
- content: item.message,
423
- timestamp: new Date().toISOString()
424
- }
425
- },
426
- openStream: ({ signal, onReady, onDelta, onSessionEvent }) =>
427
- sendChatTurnStream(
428
- {
429
- message: item.message,
430
- sessionKey: item.sessionKey,
431
- agentId: item.agentId,
432
- ...(item.model ? { model: item.model } : {}),
433
- ...(requestedSkills.length > 0
434
- ? {
435
- metadata: {
436
- requested_skills: requestedSkills
437
- }
438
- }
439
- : {}),
440
- channel: 'ui',
441
- chatId: 'web-ui'
442
- },
443
- { signal, onReady, onDelta, onSessionEvent }
444
- ),
445
- setters: {
446
- setOptimisticUserEvent,
447
- setStreamingSessionEvents,
448
- setStreamingAssistantText,
449
- setStreamingAssistantTimestamp,
450
- setIsSending,
451
- setIsAwaitingAssistantOutput,
452
- setCanStopCurrentRun,
453
- setStopDisabledReason,
454
- setLastSendError
455
- }
456
- });
457
- },
458
- [params]
459
- );
460
-
461
- const resumeRun = useCallback(
462
- async (run: ChatRunView) => {
463
- const runId = run.runId?.trim();
464
- const sessionKey = run.sessionKey?.trim();
465
- if (!runId || !sessionKey) {
466
- return;
467
- }
468
- const active = activeRunRef.current;
469
- if (active?.backendRunId === runId) {
470
- return;
471
- }
472
- if (isSending && active) {
473
- return;
474
- }
475
-
476
- setLastSendError(null);
477
- streamRunIdRef.current += 1;
478
- await executeStreamRun({
479
- runId: streamRunIdRef.current,
480
- runIdRef: streamRunIdRef,
481
- activeRunRef,
482
- selectedSessionKeyRef: params.selectedSessionKeyRef,
483
- setSelectedSessionKey: params.setSelectedSessionKey,
484
- setDraft: params.setDraft,
485
- refetchSessions: params.refetchSessions,
486
- refetchHistory: params.refetchHistory,
487
- sourceSessionKey: sessionKey,
488
- sourceAgentId: run.agentId,
489
- sourceStopSupported: run.stopSupported,
490
- sourceStopReason: run.stopReason,
491
- optimisticUserEvent: null,
492
- openStream: ({ signal, onReady, onDelta, onSessionEvent }) =>
493
- streamChatRun(
494
- {
495
- runId
496
- },
497
- { signal, onReady, onDelta, onSessionEvent }
498
- ),
499
- setters: {
500
- setOptimisticUserEvent,
501
- setStreamingSessionEvents,
502
- setStreamingAssistantText,
503
- setStreamingAssistantTimestamp,
504
- setIsSending,
505
- setIsAwaitingAssistantOutput,
506
- setCanStopCurrentRun,
507
- setStopDisabledReason,
508
- setLastSendError
509
- }
510
- });
511
- },
512
- [isSending, params]
513
- );
514
-
515
- useEffect(() => {
516
- if (isSending || queuedMessages.length === 0) {
517
- return;
518
- }
519
- const [next, ...rest] = queuedMessages;
520
- setQueuedMessages(rest);
521
- void runSend(next, { restoreDraftOnError: true });
522
- }, [isSending, queuedMessages, runSend]);
523
-
524
- const sendMessage = useCallback(
525
- async (payload: SendMessageParams) => {
526
- setLastSendError(null);
527
- queueIdRef.current += 1;
528
- const item: PendingChatMessage = {
529
- id: queueIdRef.current,
530
- message: payload.message,
531
- sessionKey: payload.sessionKey,
532
- agentId: payload.agentId,
533
- ...(payload.model ? { model: payload.model } : {}),
534
- ...(payload.requestedSkills && payload.requestedSkills.length > 0
535
- ? { requestedSkills: payload.requestedSkills }
536
- : {}),
537
- ...(typeof payload.stopSupported === 'boolean' ? { stopSupported: payload.stopSupported } : {}),
538
- ...(payload.stopReason ? { stopReason: payload.stopReason } : {})
539
- };
540
- if (isSending) {
541
- setQueuedMessages((prev) => [...prev, item]);
542
- return;
543
- }
544
- await runSend(item, { restoreDraftOnError: payload.restoreDraftOnError });
545
- },
546
- [isSending, runSend]
547
- );
548
-
549
- const stopCurrentRun = useCallback(async () => {
550
- const activeRun = activeRunRef.current;
551
- if (!activeRun) {
552
- return;
553
- }
554
- if (!activeRun.backendStopSupported) {
555
- return;
556
- }
557
-
558
- setCanStopCurrentRun(false);
559
- setQueuedMessages([]);
560
- if (activeRun.backendRunId) {
561
- try {
562
- await stopChatTurn({
563
- runId: activeRun.backendRunId,
564
- sessionKey: activeRun.sessionKey,
565
- ...(activeRun.agentId ? { agentId: activeRun.agentId } : {})
566
- });
567
- } catch {
568
- // Keep local abort as fallback even if stop API fails.
569
- }
570
- }
571
- activeRun.requestAbortController.abort();
572
- }, []);
573
-
574
- return {
575
- optimisticUserEvent,
576
- streamingSessionEvents,
577
- streamingAssistantText,
578
- streamingAssistantTimestamp,
579
- isSending,
580
- isAwaitingAssistantOutput,
581
- queuedCount: queuedMessages.length,
582
- canStopCurrentRun,
583
- stopDisabledReason,
584
- lastSendError,
585
- activeBackendRunId: activeRunRef.current?.backendRunId ?? null,
586
- sendMessage,
587
- resumeRun,
588
- stopCurrentRun,
589
- resetStreamState
590
- };
591
- }