@nextclaw/ui 0.6.10 → 0.6.12

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 (90) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/assets/ChannelsList-DBDjwf-X.js +1 -0
  4. package/dist/assets/ChatPage-C18sGGk1.js +36 -0
  5. package/dist/assets/DocBrowser-ZOplDEMS.js +1 -0
  6. package/dist/assets/LogoBadge-2LMzEMwe.js +1 -0
  7. package/dist/assets/MarketplacePage-D4JHYcB5.js +49 -0
  8. package/dist/assets/ModelConfig-DZVvdLFq.js +1 -0
  9. package/dist/assets/ProvidersList-Dum31480.js +1 -0
  10. package/dist/assets/{RuntimeConfig-BO6s-ls-.js → RuntimeConfig-4sb3mpkd.js} +1 -1
  11. package/dist/assets/SearchConfig-B4u_MxRG.js +1 -0
  12. package/dist/assets/{SecretsConfig-mayFdxpM.js → SecretsConfig-BQXblZvb.js} +2 -2
  13. package/dist/assets/SessionsConfig-Jk29xjQU.js +2 -0
  14. package/dist/assets/{card-BP5YnL-G.js → card-BekAnCgX.js} +1 -1
  15. package/dist/assets/config-layout-BHnOoweL.js +1 -0
  16. package/dist/assets/index-BXwjfCEO.css +1 -0
  17. package/dist/assets/index-Dl6t70wA.js +8 -0
  18. package/dist/assets/{input-B1D2QX0O.js → input-MMn_Na9q.js} +1 -1
  19. package/dist/assets/{label-DW0j-fXA.js → label-Dg2ydpN0.js} +1 -1
  20. package/dist/assets/{page-layout-Ch-H9gD-.js → page-layout-7K0rcz0I.js} +1 -1
  21. package/dist/assets/session-run-status-CAdjSqeb.js +3 -0
  22. package/dist/assets/{switch-_cZHlGKB.js → switch-DnDMlDVu.js} +1 -1
  23. package/dist/assets/{tabs-custom-ARxqYYjG.js → tabs-custom-khLM8lWj.js} +1 -1
  24. package/dist/assets/{useConfirmDialog-BaU7nIat.js → useConfirmDialog-BYA1XnVU.js} +2 -2
  25. package/dist/assets/{vendor-C--HHaLf.js → vendor-d7E8OgNx.js} +84 -84
  26. package/dist/index.html +3 -3
  27. package/package.json +4 -2
  28. package/src/App.tsx +3 -2
  29. package/src/api/config.ts +212 -200
  30. package/src/api/types.ts +93 -24
  31. package/src/components/chat/ChatConversationPanel.tsx +102 -121
  32. package/src/components/chat/ChatPage.tsx +165 -437
  33. package/src/components/chat/ChatSidebar.tsx +30 -36
  34. package/src/components/chat/ChatThread.tsx +73 -131
  35. package/src/components/chat/chat-input/ChatInputBarView.tsx +82 -0
  36. package/src/components/chat/chat-input/ChatInputBottomToolbar.tsx +71 -0
  37. package/src/components/chat/chat-input/components/ChatInputModelStateHint.tsx +39 -0
  38. package/src/components/chat/chat-input/components/ChatInputSelectedSkillsSection.tsx +31 -0
  39. package/src/components/chat/chat-input/components/ChatInputSlashPanelSection.tsx +112 -0
  40. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputAttachButton.tsx +24 -0
  41. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputModelSelector.tsx +58 -0
  42. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSendControls.tsx +56 -0
  43. package/src/components/chat/chat-input/components/bottom-toolbar/ChatInputSessionTypeSelector.tsx +40 -0
  44. package/src/components/chat/chat-input/useChatInputBarController.ts +313 -0
  45. package/src/components/chat/chat-input.types.ts +15 -0
  46. package/src/components/chat/chat-page-data.ts +121 -0
  47. package/src/components/chat/chat-page-runtime.ts +221 -0
  48. package/src/components/chat/chat-session-route.ts +59 -0
  49. package/src/components/chat/chat-stream/nextbot-parsers.ts +52 -0
  50. package/src/components/chat/chat-stream/nextbot-runtime-agent.ts +413 -0
  51. package/src/components/chat/chat-stream/stream-event-adapter.ts +98 -0
  52. package/src/components/chat/chat-stream/transport.ts +159 -0
  53. package/src/components/chat/chat-stream/types.ts +76 -0
  54. package/src/components/chat/managers/chat-input.manager.ts +142 -0
  55. package/src/components/chat/managers/chat-run-status.manager.ts +32 -0
  56. package/src/components/chat/managers/chat-session-list.manager.ts +77 -0
  57. package/src/components/chat/managers/chat-stream-actions.manager.ts +34 -0
  58. package/src/components/chat/managers/chat-thread.manager.ts +86 -0
  59. package/src/components/chat/managers/chat-ui.manager.ts +65 -0
  60. package/src/components/chat/presenter/chat-presenter-context.tsx +25 -0
  61. package/src/components/chat/presenter/chat.presenter.ts +32 -0
  62. package/src/components/chat/stores/chat-input.store.ts +62 -0
  63. package/src/components/chat/stores/chat-run-status.store.ts +30 -0
  64. package/src/components/chat/stores/chat-session-list.store.ts +34 -0
  65. package/src/components/chat/stores/chat-thread.store.ts +52 -0
  66. package/src/components/chat/useChatRuntimeController.ts +134 -0
  67. package/src/components/chat/useChatSessionTypeState.ts +148 -0
  68. package/src/components/common/MaskedInput.tsx +1 -1
  69. package/src/components/config/SearchConfig.tsx +297 -0
  70. package/src/components/layout/Sidebar.tsx +6 -1
  71. package/src/hooks/useConfig.ts +48 -1
  72. package/src/hooks/useObservable.ts +20 -0
  73. package/src/lib/chat-message.ts +2 -202
  74. package/src/lib/chat-runtime-utils.ts +250 -0
  75. package/src/lib/i18n.ts +31 -0
  76. package/tsconfig.json +2 -1
  77. package/vite.config.ts +2 -1
  78. package/dist/assets/ChannelsList-TyMb5Mgz.js +0 -1
  79. package/dist/assets/ChatPage-CQerYqvy.js +0 -34
  80. package/dist/assets/DocBrowser-CNtrA0ps.js +0 -1
  81. package/dist/assets/LogoBadge-BLqiOM5D.js +0 -1
  82. package/dist/assets/MarketplacePage-CotZxxNe.js +0 -49
  83. package/dist/assets/ModelConfig-CCsQ8KFq.js +0 -1
  84. package/dist/assets/ProvidersList-BYYX5K_g.js +0 -1
  85. package/dist/assets/SessionsConfig-DAIczdBj.js +0 -2
  86. package/dist/assets/index-BUiahmWm.css +0 -1
  87. package/dist/assets/index-D6_5HaDl.js +0 -7
  88. package/dist/assets/session-run-status-BUYsQeWs.js +0 -5
  89. package/src/components/chat/ChatInputBar.tsx +0 -590
  90. package/src/components/chat/useChatStreamController.ts +0 -591
@@ -0,0 +1,413 @@
1
+ import { EventType, type AgentEvent, type IAgent, type RunAgentInput } from '@nextclaw/agent-chat';
2
+ import { Observable, type Subscribable } from 'rxjs';
3
+ import {
4
+ buildDeltaEvents,
5
+ buildDeltaMessageId,
6
+ buildSessionEventEvents,
7
+ isAssistantSessionEvent,
8
+ shouldCloseDeltaOnSessionEvent
9
+ } from './stream-event-adapter';
10
+ import { openResumeRunStream, openSendTurnStream, requestStopRun } from './transport';
11
+ import type {
12
+ ActiveRunState,
13
+ NextbotAgentRunMetadata,
14
+ StreamDeltaEvent,
15
+ StreamReadyEvent,
16
+ StreamSessionEvent
17
+ } from './types';
18
+ import { isAbortLikeError } from '@/lib/chat-runtime-utils';
19
+
20
+ type SendRunMetadata = Extract<NextbotAgentRunMetadata, { mode: 'send' }>;
21
+ type ResumeRunMetadata = Extract<NextbotAgentRunMetadata, { mode: 'resume' }>;
22
+ type StreamResult = { sessionKey: string; reply: string };
23
+ type EmitEvent = (event: AgentEvent) => void;
24
+ type StreamRuntimeState = {
25
+ deltaMessageId: string;
26
+ deltaStarted: boolean;
27
+ deltaClosed: boolean;
28
+ hasAssistantSessionEvent: boolean;
29
+ hasAssistantOutput: boolean;
30
+ };
31
+ type RunObservableParams = {
32
+ metadata: NextbotAgentRunMetadata;
33
+ clientRunId: string;
34
+ abortController: AbortController;
35
+ runState: ActiveRunState;
36
+ };
37
+
38
+ function createBackendRunId(): string {
39
+ const now = Date.now().toString(36);
40
+ const rand = Math.random().toString(36).slice(2, 10);
41
+ return `run-${now}-${rand}`;
42
+ }
43
+
44
+ export type NextbotRunMetadataPayload =
45
+ | {
46
+ driver: 'nextbot-stream';
47
+ kind: 'ready';
48
+ sessionKey?: string;
49
+ backendRunId?: string;
50
+ stopSupported?: boolean;
51
+ stopReason?: string;
52
+ requestedAt?: string;
53
+ }
54
+ | {
55
+ driver: 'nextbot-stream';
56
+ kind: 'final';
57
+ sessionKey: string;
58
+ reply: string;
59
+ hasAssistantSessionEvent: boolean;
60
+ };
61
+
62
+ function readRunMetadata(input: RunAgentInput): NextbotAgentRunMetadata | null {
63
+ const raw = input.metadata;
64
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
65
+ return null;
66
+ }
67
+ const record = raw as Record<string, unknown>;
68
+ if (record.driver !== 'nextbot-stream') {
69
+ return null;
70
+ }
71
+ const mode = record.mode;
72
+ if (mode === 'send') {
73
+ const payload = record.payload;
74
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
75
+ return null;
76
+ }
77
+ const requestedSkills = Array.isArray(record.requestedSkills)
78
+ ? record.requestedSkills.filter((item): item is string => typeof item === 'string')
79
+ : [];
80
+ return {
81
+ driver: 'nextbot-stream',
82
+ mode: 'send',
83
+ payload: payload as SendRunMetadata['payload'],
84
+ requestedSkills
85
+ };
86
+ }
87
+ if (mode === 'resume') {
88
+ const runId = typeof record.runId === 'string' ? record.runId.trim() : '';
89
+ if (!runId) {
90
+ return null;
91
+ }
92
+ const fromEventIndex =
93
+ typeof record.fromEventIndex === 'number' && Number.isFinite(record.fromEventIndex)
94
+ ? Math.max(0, Math.trunc(record.fromEventIndex))
95
+ : undefined;
96
+ const sessionKey = typeof record.sessionKey === 'string' ? record.sessionKey.trim() : '';
97
+ const agentId = typeof record.agentId === 'string' ? record.agentId.trim() : '';
98
+ const stopSupported = typeof record.stopSupported === 'boolean' ? record.stopSupported : undefined;
99
+ const stopReason = typeof record.stopReason === 'string' ? record.stopReason.trim() : '';
100
+ return {
101
+ driver: 'nextbot-stream',
102
+ mode: 'resume',
103
+ runId,
104
+ ...(typeof fromEventIndex === 'number' ? { fromEventIndex } : {}),
105
+ ...(sessionKey ? { sessionKey } : {}),
106
+ ...(agentId ? { agentId } : {}),
107
+ ...(typeof stopSupported === 'boolean' ? { stopSupported } : {}),
108
+ ...(stopReason ? { stopReason } : {})
109
+ };
110
+ }
111
+ return null;
112
+ }
113
+
114
+ function buildInitialRunState(metadata: NextbotAgentRunMetadata): ActiveRunState {
115
+ if (metadata.mode === 'send') {
116
+ const backendRunId = metadata.payload.runId?.trim() || '';
117
+ return {
118
+ localRunId: 0,
119
+ sessionKey: metadata.payload.sessionKey,
120
+ ...(metadata.payload.agentId ? { agentId: metadata.payload.agentId } : {}),
121
+ ...(backendRunId ? { backendRunId } : {}),
122
+ backendStopSupported: Boolean(metadata.payload.stopSupported),
123
+ ...(metadata.payload.stopReason ? { backendStopReason: metadata.payload.stopReason } : {})
124
+ };
125
+ }
126
+ return {
127
+ localRunId: 0,
128
+ sessionKey: metadata.sessionKey ?? '',
129
+ ...(metadata.agentId ? { agentId: metadata.agentId } : {}),
130
+ backendRunId: metadata.runId,
131
+ backendStopSupported: Boolean(metadata.stopSupported),
132
+ ...(metadata.stopReason ? { backendStopReason: metadata.stopReason } : {})
133
+ };
134
+ }
135
+
136
+ function withEnsuredSendRunId(metadata: NextbotAgentRunMetadata): NextbotAgentRunMetadata {
137
+ if (metadata.mode !== 'send') {
138
+ return metadata;
139
+ }
140
+ const runId = metadata.payload.runId?.trim();
141
+ if (runId) {
142
+ return metadata;
143
+ }
144
+ return {
145
+ ...metadata,
146
+ payload: {
147
+ ...metadata.payload,
148
+ runId: createBackendRunId()
149
+ }
150
+ };
151
+ }
152
+
153
+ function updateRunStateFromReady(runState: ActiveRunState, event: {
154
+ sessionKey?: string;
155
+ runId?: string;
156
+ stopSupported?: boolean;
157
+ stopReason?: string;
158
+ }) {
159
+ runState.backendRunId = event.runId?.trim() || runState.backendRunId;
160
+ runState.backendStopSupported =
161
+ typeof event.stopSupported === 'boolean' ? event.stopSupported : runState.backendStopSupported;
162
+ if (event.stopReason?.trim()) {
163
+ runState.backendStopReason = event.stopReason.trim();
164
+ }
165
+ if (event.sessionKey?.trim()) {
166
+ runState.sessionKey = event.sessionKey.trim();
167
+ }
168
+ }
169
+
170
+ function createStreamRuntimeState(): StreamRuntimeState {
171
+ return {
172
+ deltaMessageId: buildDeltaMessageId(),
173
+ deltaStarted: false,
174
+ deltaClosed: false,
175
+ hasAssistantSessionEvent: false,
176
+ hasAssistantOutput: false
177
+ };
178
+ }
179
+
180
+ function buildReadyMetadata(event: StreamReadyEvent): NextbotRunMetadataPayload {
181
+ return {
182
+ driver: 'nextbot-stream',
183
+ kind: 'ready',
184
+ ...(event.sessionKey?.trim() ? { sessionKey: event.sessionKey.trim() } : {}),
185
+ ...(event.runId?.trim() ? { backendRunId: event.runId.trim() } : {}),
186
+ ...(typeof event.stopSupported === 'boolean' ? { stopSupported: event.stopSupported } : {}),
187
+ ...(event.stopReason?.trim() ? { stopReason: event.stopReason.trim() } : {}),
188
+ ...(event.requestedAt ? { requestedAt: event.requestedAt } : {})
189
+ };
190
+ }
191
+
192
+ function buildFinalMetadata(result: StreamResult, hasAssistantSessionEvent: boolean): NextbotRunMetadataPayload {
193
+ return {
194
+ driver: 'nextbot-stream',
195
+ kind: 'final',
196
+ sessionKey: result.sessionKey,
197
+ reply: result.reply,
198
+ hasAssistantSessionEvent
199
+ };
200
+ }
201
+
202
+ function emitRunMetadata(emit: EmitEvent, clientRunId: string, metadata: NextbotRunMetadataPayload) {
203
+ emit({
204
+ type: EventType.RUN_METADATA,
205
+ runId: clientRunId,
206
+ metadata
207
+ });
208
+ }
209
+
210
+ function closeDelta(runtime: StreamRuntimeState, emit: EmitEvent) {
211
+ if (!runtime.deltaStarted || runtime.deltaClosed) {
212
+ return;
213
+ }
214
+ emit({ type: EventType.TEXT_END, messageId: runtime.deltaMessageId });
215
+ runtime.deltaClosed = true;
216
+ }
217
+
218
+ function handleDelta(runtime: StreamRuntimeState, emit: EmitEvent, event: StreamDeltaEvent) {
219
+ const events = buildDeltaEvents({
220
+ messageId: runtime.deltaMessageId,
221
+ delta: event.delta,
222
+ started: runtime.deltaStarted
223
+ });
224
+ if (events.length > 0) {
225
+ runtime.deltaStarted = true;
226
+ runtime.deltaClosed = false;
227
+ runtime.hasAssistantOutput = true;
228
+ }
229
+ for (const streamEvent of events) {
230
+ emit(streamEvent);
231
+ }
232
+ }
233
+
234
+ function handleSession(runtime: StreamRuntimeState, emit: EmitEvent, event: StreamSessionEvent) {
235
+ if (shouldCloseDeltaOnSessionEvent(event.data)) {
236
+ closeDelta(runtime, emit);
237
+ runtime.deltaMessageId = buildDeltaMessageId();
238
+ runtime.deltaStarted = false;
239
+ runtime.deltaClosed = false;
240
+ }
241
+ if (isAssistantSessionEvent(event.data)) {
242
+ runtime.hasAssistantSessionEvent = true;
243
+ }
244
+ for (const streamEvent of buildSessionEventEvents({
245
+ event: event.data,
246
+ messageId: runtime.deltaMessageId
247
+ })) {
248
+ emit(streamEvent);
249
+ }
250
+ }
251
+
252
+ function emitFallbackReplyIfNeeded(runtime: StreamRuntimeState, emit: EmitEvent, reply: string) {
253
+ const fallbackText = reply.trim();
254
+ if (runtime.hasAssistantOutput || !fallbackText) {
255
+ return;
256
+ }
257
+ const fallbackMessageId = buildDeltaMessageId();
258
+ emit({ type: EventType.TEXT_START, messageId: fallbackMessageId });
259
+ emit({ type: EventType.TEXT_DELTA, messageId: fallbackMessageId, delta: fallbackText });
260
+ emit({ type: EventType.TEXT_END, messageId: fallbackMessageId });
261
+ }
262
+
263
+ export class NextbotRuntimeAgent implements IAgent {
264
+ private activeAbortController: AbortController | null = null;
265
+ private activeRunState: ActiveRunState | null = null;
266
+
267
+ private buildMissingMetadataObservable = (clientRunId: string): Observable<AgentEvent> =>
268
+ new Observable<AgentEvent>((subscriber) => {
269
+ subscriber.next({
270
+ type: EventType.RUN_ERROR,
271
+ runId: clientRunId,
272
+ error: 'nextbot runtime metadata is required'
273
+ });
274
+ subscriber.complete();
275
+ });
276
+
277
+ private openRunStream = (params: {
278
+ metadata: NextbotAgentRunMetadata;
279
+ signal: AbortSignal;
280
+ onReady: (event: StreamReadyEvent) => void;
281
+ onDelta: (event: StreamDeltaEvent) => void;
282
+ onSessionEvent: (event: StreamSessionEvent) => void;
283
+ }): Promise<StreamResult> => {
284
+ const { metadata, signal, onReady, onDelta, onSessionEvent } = params;
285
+ if (metadata.mode === 'send') {
286
+ return openSendTurnStream({
287
+ item: metadata.payload,
288
+ requestedSkills: metadata.requestedSkills,
289
+ signal,
290
+ onReady,
291
+ onDelta,
292
+ onSessionEvent
293
+ });
294
+ }
295
+ return openResumeRunStream({
296
+ runId: (metadata as ResumeRunMetadata).runId,
297
+ fromEventIndex: (metadata as ResumeRunMetadata).fromEventIndex,
298
+ signal,
299
+ onReady,
300
+ onDelta,
301
+ onSessionEvent
302
+ });
303
+ };
304
+
305
+ private finalizeRunState = (abortController: AbortController, runState: ActiveRunState) => {
306
+ if (this.activeAbortController === abortController) {
307
+ this.activeAbortController = null;
308
+ }
309
+ if (this.activeRunState === runState) {
310
+ this.activeRunState = null;
311
+ }
312
+ };
313
+
314
+ private createRunObservable = ({
315
+ metadata,
316
+ clientRunId,
317
+ abortController,
318
+ runState
319
+ }: RunObservableParams): Observable<AgentEvent> =>
320
+ new Observable<AgentEvent>((subscriber) => {
321
+ const runtime = createStreamRuntimeState();
322
+ let disposed = false;
323
+ const emit: EmitEvent = (event) => {
324
+ if (!disposed) {
325
+ subscriber.next(event);
326
+ }
327
+ };
328
+
329
+ emit({ type: EventType.RUN_STARTED, runId: clientRunId });
330
+
331
+ const streamTask = this.openRunStream({
332
+ metadata,
333
+ signal: abortController.signal,
334
+ onReady: (event) => {
335
+ if (this.activeRunState) {
336
+ updateRunStateFromReady(this.activeRunState, event);
337
+ }
338
+ emitRunMetadata(emit, clientRunId, buildReadyMetadata(event));
339
+ },
340
+ onDelta: (event) => {
341
+ handleDelta(runtime, emit, event);
342
+ },
343
+ onSessionEvent: (event) => {
344
+ handleSession(runtime, emit, event);
345
+ }
346
+ });
347
+
348
+ void streamTask
349
+ .then((result) => {
350
+ closeDelta(runtime, emit);
351
+ emitFallbackReplyIfNeeded(runtime, emit, result.reply);
352
+ emitRunMetadata(emit, clientRunId, buildFinalMetadata(result, runtime.hasAssistantSessionEvent));
353
+ emit({
354
+ type: EventType.RUN_FINISHED,
355
+ runId: clientRunId
356
+ });
357
+ subscriber.complete();
358
+ })
359
+ .catch((error) => {
360
+ closeDelta(runtime, emit);
361
+ if (!isAbortLikeError(error)) {
362
+ emit({
363
+ type: EventType.RUN_ERROR,
364
+ runId: clientRunId,
365
+ error: error instanceof Error ? error.message : String(error)
366
+ });
367
+ }
368
+ subscriber.complete();
369
+ })
370
+ .finally(() => {
371
+ this.finalizeRunState(abortController, runState);
372
+ });
373
+
374
+ return () => {
375
+ disposed = true;
376
+ abortController.abort();
377
+ if (this.activeAbortController === abortController) {
378
+ this.activeAbortController = null;
379
+ }
380
+ };
381
+ });
382
+
383
+ abortRun = () => {
384
+ const activeRunState = this.activeRunState;
385
+ if (activeRunState?.backendStopSupported) {
386
+ void requestStopRun(activeRunState);
387
+ }
388
+ this.activeAbortController?.abort();
389
+ this.activeAbortController = null;
390
+ this.activeRunState = null;
391
+ };
392
+
393
+ run = (input: RunAgentInput): Subscribable<AgentEvent> => {
394
+ const metadata = readRunMetadata(input);
395
+ const clientRunId = typeof input.runId === 'string' && input.runId.trim() ? input.runId : `run-${Date.now()}`;
396
+ if (!metadata) {
397
+ return this.buildMissingMetadataObservable(clientRunId);
398
+ }
399
+
400
+ const normalizedMetadata = withEnsuredSendRunId(metadata);
401
+ this.abortRun();
402
+ const abortController = new AbortController();
403
+ this.activeAbortController = abortController;
404
+ const runState = buildInitialRunState(normalizedMetadata);
405
+ this.activeRunState = runState;
406
+ return this.createRunObservable({
407
+ metadata: normalizedMetadata,
408
+ clientRunId,
409
+ abortController,
410
+ runState
411
+ });
412
+ };
413
+ }
@@ -0,0 +1,98 @@
1
+ import { EventType, type AgentEvent } from '@nextclaw/agent-chat';
2
+ import type { SessionEventView } from '@/api/types';
3
+
4
+ export function buildDeltaEvents(params: { messageId: string; delta: string; started: boolean }): AgentEvent[] {
5
+ if (!params.delta) {
6
+ return [];
7
+ }
8
+ if (params.started) {
9
+ return [{ type: EventType.TEXT_DELTA, messageId: params.messageId, delta: params.delta }];
10
+ }
11
+ return [
12
+ { type: EventType.TEXT_START, messageId: params.messageId },
13
+ { type: EventType.TEXT_DELTA, messageId: params.messageId, delta: params.delta }
14
+ ];
15
+ }
16
+
17
+ export function shouldCloseDeltaOnSessionEvent(event: SessionEventView): boolean {
18
+ void event;
19
+ return false;
20
+ }
21
+
22
+ export function isAssistantSessionEvent(event: SessionEventView): boolean {
23
+ const role = event.message?.role?.toLowerCase().trim();
24
+ return role === 'assistant';
25
+ }
26
+
27
+ export function buildSessionEventEvents(params: { event: SessionEventView; messageId: string }): AgentEvent[] {
28
+ const { event, messageId } = params;
29
+ const message = event.message;
30
+ if (!message) {
31
+ return [];
32
+ }
33
+ const role = message.role?.toLowerCase().trim();
34
+ if (!role) {
35
+ return [];
36
+ }
37
+
38
+ const events: AgentEvent[] = [];
39
+
40
+ if (typeof message.reasoning_content === 'string' && message.reasoning_content.trim()) {
41
+ events.push({ type: EventType.REASONING_START, messageId });
42
+ events.push({
43
+ type: EventType.REASONING_DELTA,
44
+ messageId,
45
+ delta: message.reasoning_content.trim()
46
+ });
47
+ events.push({ type: EventType.REASONING_END, messageId });
48
+ }
49
+
50
+ if (Array.isArray(message.tool_calls)) {
51
+ for (let index = 0; index < message.tool_calls.length; index += 1) {
52
+ const call = message.tool_calls[index];
53
+ if (!call || typeof call !== 'object') {
54
+ continue;
55
+ }
56
+ const callRecord = call as Record<string, unknown>;
57
+ const fnValue = callRecord.function;
58
+ const fn = typeof fnValue === 'object' && fnValue ? (fnValue as { name?: unknown; arguments?: unknown }) : null;
59
+ const seqPrefix = typeof event.seq === 'number' ? String(event.seq) : 'unknown';
60
+ const toolCallId = typeof callRecord.id === 'string' ? callRecord.id : `tool-${seqPrefix}-${index}`;
61
+ const toolName = typeof fn?.name === 'string' ? fn.name : typeof callRecord.name === 'string' ? callRecord.name : 'tool';
62
+ const rawArgs = fn?.arguments ?? callRecord.arguments ?? '';
63
+ const args = typeof rawArgs === 'string' ? rawArgs : JSON.stringify(rawArgs ?? {});
64
+ events.push({
65
+ type: EventType.TOOL_CALL_START,
66
+ messageId,
67
+ toolCallId,
68
+ toolName
69
+ });
70
+ events.push({
71
+ type: EventType.TOOL_CALL_ARGS,
72
+ toolCallId,
73
+ args
74
+ });
75
+ events.push({
76
+ type: EventType.TOOL_CALL_END,
77
+ toolCallId
78
+ });
79
+ }
80
+ }
81
+
82
+ if (role === 'tool' || role === 'tool_result' || role === 'toolresult' || role === 'function') {
83
+ const toolCallId = typeof message.tool_call_id === 'string' ? message.tool_call_id.trim() : '';
84
+ if (toolCallId) {
85
+ events.push({
86
+ type: EventType.TOOL_CALL_RESULT,
87
+ toolCallId,
88
+ content: message.content
89
+ });
90
+ }
91
+ }
92
+
93
+ return events;
94
+ }
95
+
96
+ export function buildDeltaMessageId(): string {
97
+ return `stream-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
98
+ }
@@ -0,0 +1,159 @@
1
+ import { fetchChatRuns, sendChatTurnStream, stopChatTurn, streamChatRun } from '@/api/config';
2
+ import type { ActiveRunState, SendMessageParams, StreamDeltaEvent, StreamReadyEvent, StreamSessionEvent } from './types';
3
+
4
+ function buildSendTurnPayload(item: SendMessageParams, requestedSkills: string[]) {
5
+ const metadata: Record<string, unknown> = {};
6
+ if (item.sessionType) {
7
+ metadata.session_type = item.sessionType;
8
+ }
9
+ if (requestedSkills.length > 0) {
10
+ metadata.requested_skills = requestedSkills;
11
+ }
12
+ return {
13
+ message: item.message,
14
+ ...(item.runId ? { runId: item.runId } : {}),
15
+ sessionKey: item.sessionKey,
16
+ agentId: item.agentId,
17
+ ...(item.model ? { model: item.model } : {}),
18
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
19
+ channel: 'ui',
20
+ chatId: 'web-ui'
21
+ };
22
+ }
23
+
24
+ export async function openSendTurnStream(params: {
25
+ item: SendMessageParams;
26
+ requestedSkills: string[];
27
+ signal: AbortSignal;
28
+ onReady: (event: StreamReadyEvent) => void;
29
+ onDelta: (event: StreamDeltaEvent) => void;
30
+ onSessionEvent: (event: StreamSessionEvent) => void;
31
+ }) {
32
+ return sendChatTurnStream(buildSendTurnPayload(params.item, params.requestedSkills), {
33
+ signal: params.signal,
34
+ onReady: params.onReady,
35
+ onDelta: params.onDelta,
36
+ onSessionEvent: params.onSessionEvent
37
+ });
38
+ }
39
+
40
+ export async function openResumeRunStream(params: {
41
+ runId: string;
42
+ fromEventIndex?: number;
43
+ signal: AbortSignal;
44
+ onReady: (event: StreamReadyEvent) => void;
45
+ onDelta: (event: StreamDeltaEvent) => void;
46
+ onSessionEvent: (event: StreamSessionEvent) => void;
47
+ }) {
48
+ return streamChatRun(
49
+ {
50
+ runId: params.runId,
51
+ ...(typeof params.fromEventIndex === 'number' ? { fromEventIndex: params.fromEventIndex } : {})
52
+ },
53
+ {
54
+ signal: params.signal,
55
+ onReady: params.onReady,
56
+ onDelta: params.onDelta,
57
+ onSessionEvent: params.onSessionEvent
58
+ }
59
+ );
60
+ }
61
+
62
+ export async function requestStopRun(activeRun: ActiveRunState): Promise<void> {
63
+ if (!activeRun.backendStopSupported) {
64
+ return;
65
+ }
66
+
67
+ try {
68
+ const attemptedRunIds = new Set<string>();
69
+ const knownRunId = activeRun.backendRunId?.trim();
70
+ if (knownRunId) {
71
+ attemptedRunIds.add(knownRunId);
72
+ const stopped = await stopRunById(knownRunId);
73
+ if (stopped) {
74
+ return;
75
+ }
76
+ }
77
+
78
+ const candidateRunIds = await resolveStopCandidateRunIds(activeRun);
79
+ for (const runId of candidateRunIds) {
80
+ if (attemptedRunIds.has(runId)) {
81
+ continue;
82
+ }
83
+ attemptedRunIds.add(runId);
84
+ const stopped = await stopRunById(runId);
85
+ if (stopped) {
86
+ return;
87
+ }
88
+ }
89
+ if (knownRunId) {
90
+ const stopped = await stopRunById(knownRunId);
91
+ if (stopped) {
92
+ return;
93
+ }
94
+ }
95
+ } catch {
96
+ // Keep local abort as fallback even if stop API fails.
97
+ }
98
+ }
99
+
100
+ async function stopRunById(runId: string): Promise<boolean> {
101
+ const normalizedRunId = runId.trim();
102
+ if (!normalizedRunId) {
103
+ return false;
104
+ }
105
+ try {
106
+ const result = await stopChatTurn({
107
+ runId: normalizedRunId
108
+ });
109
+ return result.stopped === true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ async function resolveStopCandidateRunIds(activeRun: ActiveRunState): Promise<string[]> {
116
+ const sessionKey = activeRun.sessionKey?.trim();
117
+ if (!sessionKey) {
118
+ return [];
119
+ }
120
+ const attempts = 8;
121
+ const delayMs = 120;
122
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
123
+ const runIds = await listActiveRunIdsBySession(activeRun, sessionKey);
124
+ if (runIds.length > 0) {
125
+ return runIds;
126
+ }
127
+ if (attempt < attempts - 1) {
128
+ await sleep(delayMs);
129
+ }
130
+ }
131
+ return [];
132
+ }
133
+
134
+ async function listActiveRunIdsBySession(activeRun: ActiveRunState, sessionKey: string): Promise<string[]> {
135
+ try {
136
+ const response = await fetchChatRuns({
137
+ sessionKey,
138
+ states: ['queued', 'running'],
139
+ limit: 50
140
+ });
141
+ const primary = response.runs
142
+ .filter((run) => run.runId?.trim() && run.sessionKey === sessionKey && run.agentId === activeRun.agentId)
143
+ .map((run) => run.runId.trim());
144
+ if (primary.length > 0) {
145
+ return primary;
146
+ }
147
+ return response.runs
148
+ .filter((run) => run.runId?.trim() && run.sessionKey === sessionKey)
149
+ .map((run) => run.runId.trim());
150
+ } catch {
151
+ return [];
152
+ }
153
+ }
154
+
155
+ function sleep(ms: number): Promise<void> {
156
+ return new Promise((resolve) => {
157
+ window.setTimeout(resolve, ms);
158
+ });
159
+ }