@agents24/chat-react 0.3.2 → 0.4.0

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 (58) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +21 -66
  3. package/compatibility.json +54 -0
  4. package/dist/active-content.d.ts +10 -0
  5. package/dist/{chunk-EWZO4QJI.js → chunk-CHAU7FNW.js} +3 -3
  6. package/dist/chunk-CHAU7FNW.js.map +1 -0
  7. package/dist/{chunk-FFO6KEB4.js → chunk-LI67M74F.js} +98 -12
  8. package/dist/chunk-LI67M74F.js.map +1 -0
  9. package/dist/chunk-NLEOWTZG.js +1327 -0
  10. package/dist/chunk-NLEOWTZG.js.map +1 -0
  11. package/dist/index.cjs +66 -1852
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +0 -6
  14. package/dist/index.js +6 -1841
  15. package/dist/index.js.map +1 -1
  16. package/dist/latest-thread-scroller.d.ts +7 -7
  17. package/dist/mcp-oauth.d.ts +3 -1
  18. package/dist/renderers.d.ts +3 -3
  19. package/dist/styles.css +407 -0
  20. package/dist/templates/agent-chat-app.d.ts +18 -0
  21. package/dist/templates/agent-chat-layout.d.ts +69 -0
  22. package/dist/templates/agent-chat-shell.d.ts +1 -1
  23. package/dist/templates/index.cjs +1904 -18
  24. package/dist/templates/index.cjs.map +1 -1
  25. package/dist/templates/index.d.ts +2 -0
  26. package/dist/templates/index.js +529 -11
  27. package/dist/templates/index.js.map +1 -1
  28. package/dist/types.d.ts +12 -380
  29. package/dist/ui/agent-chat-actions.d.ts +1 -2
  30. package/dist/ui/agent-chat-composer.d.ts +2 -1
  31. package/dist/ui/agent-chat-message.d.ts +3 -3
  32. package/dist/ui/agent-response-timeline.d.ts +2 -2
  33. package/dist/ui/attachment.d.ts +9 -9
  34. package/dist/ui/bubble.d.ts +4 -4
  35. package/dist/ui/index.cjs +137 -43
  36. package/dist/ui/index.cjs.map +1 -1
  37. package/dist/ui/index.js +62 -1220
  38. package/dist/ui/index.js.map +1 -1
  39. package/dist/ui/marker.d.ts +3 -3
  40. package/dist/ui/mcp-connect-required-card.d.ts +1 -1
  41. package/dist/ui/message-response.d.ts +1 -1
  42. package/dist/ui/message.d.ts +6 -6
  43. package/package.json +27 -8
  44. package/dist/chunk-EWZO4QJI.js.map +0 -1
  45. package/dist/chunk-FFO6KEB4.js.map +0 -1
  46. package/dist/chunk-UU7OXHL3.js +0 -11
  47. package/dist/chunk-UU7OXHL3.js.map +0 -1
  48. package/dist/context-window.d.ts +0 -38
  49. package/dist/controller-actions.d.ts +0 -45
  50. package/dist/controller-helpers.d.ts +0 -27
  51. package/dist/controller-hitl.d.ts +0 -22
  52. package/dist/controller-options.d.ts +0 -15
  53. package/dist/controller-thread-events.d.ts +0 -7
  54. package/dist/controller.d.ts +0 -5
  55. package/dist/message-lifecycle.d.ts +0 -15
  56. package/dist/model.d.ts +0 -30
  57. package/dist/sse.d.ts +0 -5
  58. package/dist/transport.d.ts +0 -43
package/dist/index.js CHANGED
@@ -6,1619 +6,15 @@ import {
6
6
  LatestThreadScrollerOutline,
7
7
  LatestThreadScrollerProvider,
8
8
  LatestThreadScrollerRoot,
9
- LatestThreadScrollerViewport
10
- } from "./chunk-FFO6KEB4.js";
9
+ LatestThreadScrollerViewport,
10
+ validateMcpCallbackResult,
11
+ waitForMcpOauthComplete
12
+ } from "./chunk-LI67M74F.js";
11
13
  import {
12
14
  getActiveStreamingTextPartId,
13
15
  isActiveStreamingTextPart,
14
16
  useStreamingText
15
- } from "./chunk-EWZO4QJI.js";
16
-
17
- // src/controller.ts
18
- import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
19
-
20
- // src/controller-actions.ts
21
- import { useCallback } from "react";
22
- function useControllerMessageActions(input) {
23
- const handleCopy = useCallback((content, messageId) => {
24
- navigator.clipboard?.writeText(content);
25
- input.setCopiedMessageId(messageId);
26
- setTimeout(() => input.setCopiedMessageId(null), 200);
27
- }, [input]);
28
- const handleLike = useCallback(async (msg) => {
29
- const nextLiked = !input.liked[msg.id];
30
- input.setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
31
- if (nextLiked) input.setDisliked((prev) => ({ ...prev, [msg.id]: false }));
32
- }, [input]);
33
- const handleDislike = useCallback(async (msg) => {
34
- const nextDisliked = !input.disliked[msg.id];
35
- input.setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
36
- if (nextDisliked) input.setLiked((prev) => ({ ...prev, [msg.id]: false }));
37
- }, [input]);
38
- const handleRetry = useCallback(async (msg) => {
39
- const index = input.messagesRef.current.findIndex((message) => message.id === msg.id);
40
- if (index <= 0) return;
41
- const userMessage = input.messagesRef.current[index - 1];
42
- if (userMessage.role !== "user") return;
43
- const trimmed = input.messagesRef.current.slice(0, index);
44
- input.setMessages(trimmed);
45
- input.messagesRef.current = trimmed;
46
- if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, trimmed);
47
- await input.handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
48
- }, [input]);
49
- const upsertLiveVoiceMessage = useCallback((payload) => {
50
- const content = payload.content?.trim() ?? "";
51
- if (!content && !payload.citations?.length && !payload.reasoningSteps?.length) return;
52
- input.setMessages((prev) => {
53
- const currentId = input.liveVoiceIdsRef.current[payload.role];
54
- const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
55
- const nextMessage = {
56
- id: currentId || input.createId(),
57
- role: payload.role,
58
- content,
59
- createdAt: /* @__PURE__ */ new Date(),
60
- isFinal: Boolean(payload.isFinal),
61
- isVoice: payload.role === "user",
62
- parts: content ? [{ id: input.createId(), type: "text", kind: "text", text: content }] : [],
63
- citations: payload.citations,
64
- reasoningSteps: payload.reasoningSteps
65
- };
66
- const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
67
- input.liveVoiceIdsRef.current[payload.role] = payload.isFinal ? void 0 : nextMessage.id;
68
- input.messagesRef.current = next;
69
- if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, next);
70
- return next;
71
- });
72
- }, [input]);
73
- const startNewThread = useCallback(() => {
74
- input.detachActiveStream();
75
- input.requestSeqRef.current += 1;
76
- input.activeThreadIdRef.current = null;
77
- input.loadedThreadIdRef.current = null;
78
- input.nextBeforeTurnIndexRef.current = null;
79
- input.hasOlderTurnsRef.current = false;
80
- input.storage.setActiveThreadId?.(null);
81
- input.onActiveThreadIdChange?.(null);
82
- input.setMessages([]);
83
- input.messagesRef.current = [];
84
- input.setHasOlderTurns(false);
85
- input.setIsLoadingOlder(false);
86
- input.setContextStatus(null);
87
- input.setLoadingHistory(false);
88
- }, [input]);
89
- return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
90
- }
91
-
92
- // src/controller-hitl.ts
93
- import { useCallback as useCallback2, useMemo, useState } from "react";
94
- function useControllerHitl({
95
- messages,
96
- transport,
97
- activeRunId,
98
- activeRunIdRef,
99
- activeThreadIdRef,
100
- runStream
101
- }) {
102
- const [isResolvingHitl, setIsResolvingHitl] = useState(false);
103
- const pendingHitl = useMemo(() => {
104
- for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
105
- const parts = messages[messageIndex]?.parts || [];
106
- for (let partIndex = parts.length - 1; partIndex >= 0; partIndex -= 1) {
107
- const part = parts[partIndex];
108
- if (part?.kind === "hitl" && part.status === "pending") return part;
109
- }
110
- }
111
- return null;
112
- }, [messages]);
113
- const resumeHitl = useCallback2(async (input) => {
114
- if (!transport.resumeHitl) throw new Error("HITL resume is not configured.");
115
- const runId = input.runId || activeRunIdRef.current || activeRunId;
116
- if (!runId) throw new Error("No paused run is available to resume.");
117
- setIsResolvingHitl(true);
118
- try {
119
- const result = await transport.resumeHitl({ ...input, runId });
120
- const threadId = result.thread_id || activeThreadIdRef.current;
121
- if (threadId) await runStream({ mode: "attach", runId: result.run_id || runId, threadId });
122
- return result;
123
- } finally {
124
- setIsResolvingHitl(false);
125
- }
126
- }, [activeRunId, activeRunIdRef, activeThreadIdRef, runStream, transport]);
127
- return { pendingHitl, isResolvingHitl, resumeHitl };
128
- }
129
-
130
- // src/controller-thread-events.ts
131
- import { useEffect, useRef } from "react";
132
-
133
- // src/context-window.ts
134
- var SOURCE_PRIORITY = {
135
- unknown: 0,
136
- heuristic_estimate: 1,
137
- tokenizer_estimate: 1,
138
- text_estimate: 1,
139
- estimated: 1,
140
- multimodal_estimate: 2,
141
- hf_tokenizer: 3,
142
- runtime_tokenizer: 3,
143
- provider_count_api: 4,
144
- provider_usage: 5,
145
- exact: 5
146
- };
147
- var STAGE_PRIORITY = {
148
- preflight: 0,
149
- sent_prompt: 1,
150
- final_usage: 2
151
- };
152
- function numberOrNull(value) {
153
- return typeof value === "number" && Number.isFinite(value) ? value : null;
154
- }
155
- function stagePriority(window2) {
156
- return STAGE_PRIORITY[window2?.stage || "sent_prompt"] ?? STAGE_PRIORITY.sent_prompt;
157
- }
158
- function hasRenderableContextWindow(window2) {
159
- const maxTokens = window2?.max_tokens;
160
- return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0;
161
- }
162
- function windowWeight(window2) {
163
- if (!window2) return [0, 0];
164
- return [stagePriority(window2), SOURCE_PRIORITY[window2.source] || 0];
165
- }
166
- function normalizeContextCompression(value) {
167
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
168
- const payload = value;
169
- return {
170
- active: Boolean(payload.active),
171
- reason: typeof payload.reason === "string" ? payload.reason : null,
172
- input_tokens: numberOrNull(payload.input_tokens),
173
- max_tokens: numberOrNull(payload.max_tokens),
174
- usage_ratio: numberOrNull(payload.usage_ratio),
175
- full_frame_count: numberOrNull(payload.full_frame_count),
176
- compact_frame_count: numberOrNull(payload.compact_frame_count),
177
- dropped_frame_count: numberOrNull(payload.dropped_frame_count),
178
- artifact_ref_count: numberOrNull(payload.artifact_ref_count),
179
- compression_trigger_budget: numberOrNull(payload.compression_trigger_budget),
180
- compression_target_budget: numberOrNull(payload.compression_target_budget),
181
- compression_target_ratio: numberOrNull(payload.compression_target_ratio),
182
- threshold_used: numberOrNull(payload.threshold_used)
183
- };
184
- }
185
- function normalizeContextWindow(value) {
186
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
187
- const payload = value;
188
- const rawSource = String(payload.source || "").trim();
189
- const source = rawSource === "exact" || rawSource === "estimated" || rawSource === "unknown" || rawSource === "provider_count_api" || rawSource === "provider_usage" || rawSource === "runtime_tokenizer" || rawSource === "hf_tokenizer" || rawSource === "multimodal_estimate" || rawSource === "text_estimate" || rawSource === "tokenizer_estimate" || rawSource === "heuristic_estimate" ? rawSource : rawSource ? "estimated" : "unknown";
190
- const rawStage = String(payload.stage || "").trim();
191
- const stage = rawStage === "preflight" || rawStage === "sent_prompt" || rawStage === "final_usage" ? rawStage : "sent_prompt";
192
- const rawConfidence = String(payload.confidence || "").trim();
193
- const confidence = rawConfidence === "exact" || rawConfidence === "high" || rawConfidence === "medium" || rawConfidence === "low" || rawConfidence === "unknown" ? rawConfidence : null;
194
- return {
195
- source,
196
- run_id: typeof payload.run_id === "string" ? payload.run_id : null,
197
- stage,
198
- confidence,
199
- counter: typeof payload.counter === "string" ? payload.counter : null,
200
- model_id: typeof payload.model_id === "string" ? payload.model_id : null,
201
- max_tokens: numberOrNull(payload.max_tokens),
202
- max_tokens_source: typeof payload.max_tokens_source === "string" ? payload.max_tokens_source : null,
203
- input_tokens: numberOrNull(payload.input_tokens),
204
- remaining_tokens: numberOrNull(payload.remaining_tokens),
205
- usage_ratio: numberOrNull(payload.usage_ratio),
206
- assembly: payload.assembly && typeof payload.assembly === "object" && !Array.isArray(payload.assembly) ? payload.assembly : null,
207
- context_compression: normalizeContextCompression(payload.context_compression)
208
- };
209
- }
210
- function mergeContextWindow(current, incoming) {
211
- if (!incoming) return current ?? null;
212
- if (!current) return incoming;
213
- if (hasRenderableContextWindow(current) && !hasRenderableContextWindow(incoming)) return current;
214
- const currentRunId = current.run_id || null;
215
- const incomingRunId = incoming.run_id || null;
216
- if (incomingRunId && currentRunId && incomingRunId !== currentRunId) {
217
- return stagePriority(incoming) >= STAGE_PRIORITY.sent_prompt ? incoming : current;
218
- }
219
- const currentWeight = windowWeight(current);
220
- const incomingWeight = windowWeight(incoming);
221
- return incomingWeight[0] > currentWeight[0] || incomingWeight[0] === currentWeight[0] && incomingWeight[1] >= currentWeight[1] ? incoming : current;
222
- }
223
- function mergeContextWindowUpdate(current, incoming) {
224
- return mergeContextWindow(current, normalizeContextWindow(incoming));
225
- }
226
- function compressionFromContextWindow(contextWindow) {
227
- if (!contextWindow) return null;
228
- if (contextWindow.context_compression) return contextWindow.context_compression;
229
- const assembly = contextWindow.assembly;
230
- if (!assembly || typeof assembly !== "object" || Array.isArray(assembly)) return null;
231
- const artifactRefs = Array.isArray(assembly.artifact_refs) ? assembly.artifact_refs : [];
232
- const compactFrameCount = numberOrNull(assembly.compact_frame_count) ?? 0;
233
- const droppedFrameCount = numberOrNull(assembly.dropped_frame_count) ?? 0;
234
- const reason = typeof assembly.compaction_reason === "string" ? assembly.compaction_reason : "none";
235
- return {
236
- active: reason !== "none" || compactFrameCount > 0 || droppedFrameCount > 0,
237
- reason,
238
- input_tokens: contextWindow.input_tokens ?? null,
239
- max_tokens: contextWindow.max_tokens ?? null,
240
- usage_ratio: contextWindow.usage_ratio ?? null,
241
- full_frame_count: numberOrNull(assembly.full_frame_count),
242
- compact_frame_count: compactFrameCount,
243
- dropped_frame_count: droppedFrameCount,
244
- artifact_ref_count: artifactRefs.length,
245
- compression_trigger_budget: numberOrNull(assembly.compression_trigger_budget),
246
- compression_target_budget: numberOrNull(assembly.compression_target_budget),
247
- compression_target_ratio: numberOrNull(assembly.compression_target_ratio),
248
- threshold_used: numberOrNull(assembly.threshold_used)
249
- };
250
- }
251
-
252
- // src/model.ts
253
- var DEFAULT_THREAD_PAGE_SIZE = 5;
254
- var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling", "paused"])).has(String(status || "").toLowerCase());
255
- var isAutoAttachThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
256
- var createChatId = () => {
257
- if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
258
- return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
259
- };
260
- var titleFromMessage = (text, files = []) => {
261
- const base = text.trim() || files[0]?.filename || "New chat";
262
- return base.replace(/\s+/g, " ").slice(0, 48);
263
- };
264
- var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
265
- var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
266
- var assistantTextFromParts = (parts) => (parts || []).filter((part) => part.kind === "text").map((part) => part.text).join("\n\n").trim();
267
- var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
268
- if (!hasAttachments) return text;
269
- const marker = "Attached text file (";
270
- const trimmed = text.trim();
271
- if (trimmed.startsWith(marker)) return "";
272
- const markerIndex = text.indexOf(`
273
-
274
- ${marker}`);
275
- if (markerIndex === -1) return text;
276
- return text.slice(0, markerIndex).trim();
277
- };
278
- var attachmentsFromTurn = (turn) => (turn.attachments || []).map((attachment, index) => {
279
- const record = attachment || {};
280
- return {
281
- ...record,
282
- type: "file",
283
- url: String(record.url || ""),
284
- filename: String(record.filename || record.name || `attachment-${index + 1}`),
285
- mediaType: String(record.mime_type || record.mediaType || record.type || "application/octet-stream")
286
- };
287
- });
288
- var tokenUsageFromTurn = (turn) => {
289
- const usage = turn.run_usage;
290
- if (!usage || typeof usage !== "object") return void 0;
291
- return {
292
- inputTokens: usage.input_tokens ?? null,
293
- outputTokens: usage.output_tokens ?? null,
294
- totalTokens: usage.total_tokens ?? null,
295
- usageSource: usage.source ?? null
296
- };
297
- };
298
- var latestEventPayloadValue = (events, key) => {
299
- for (let index = (events || []).length - 1; index >= 0; index -= 1) {
300
- const payload = events?.[index]?.payload;
301
- if (payload && payload[key] !== void 0 && payload[key] !== null) return payload[key];
302
- }
303
- return void 0;
304
- };
305
- var responseBlocksFromTurn = (turn) => {
306
- if (Array.isArray(turn.response_blocks) && turn.response_blocks.length > 0) return turn.response_blocks;
307
- const fromEvents = latestEventPayloadValue(turn.run_events, "response_blocks");
308
- return Array.isArray(fromEvents) ? fromEvents : [];
309
- };
310
- var assistantTextFromEvents = (events) => {
311
- const assistantText = latestEventPayloadValue(events, "assistant_output_text");
312
- if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
313
- return "";
314
- };
315
- var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
316
- var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
317
- var HITL_ACTIONS_BY_KIND = {
318
- tool_review: ["approve", "reject"],
319
- mcp_auth: ["connect", "skip"],
320
- user_approval: ["approve", "reject"],
321
- app_data_permission: ["approve", "reject"]
322
- };
323
- var HITL_STATUSES = ["pending", "resolved", "expired", "cancelled", "invalidated"];
324
- function exactHitlActions(kind, value) {
325
- if (!Array.isArray(value)) throw new Error("Invalid V2 HITL response block.");
326
- const actions = value.map((item) => String(item));
327
- const expected = HITL_ACTIONS_BY_KIND[kind];
328
- if (actions.length !== expected.length || actions.some((action, index) => action !== expected[index])) {
329
- throw new Error("Invalid V2 HITL response block.");
330
- }
331
- return actions;
332
- }
333
- var toolStateFromStatus = (status) => {
334
- const normalized = String(status || "").trim().toLowerCase();
335
- if (["failed", "error"].includes(normalized)) return "output-error";
336
- if (["cancelled", "canceled"].includes(normalized)) return "cancelled";
337
- if (["complete", "completed", "done", "success"].includes(normalized)) return "output-available";
338
- if (["running", "active", "pending", "streaming"].includes(normalized)) return "input-available";
339
- return "input-available";
340
- };
341
- var toolNameFromRecord = (tool) => String(tool.toolName || tool.tool_name || tool.name || tool.slug || tool.action || "tool").trim() || "tool";
342
- var toolCallIdFromRecords = (block, tool) => optionalString(tool.toolCallId) || optionalString(tool.tool_call_id) || optionalString(block.toolCallId) || optionalString(block.tool_call_id) || optionalString(block.id) || null;
343
- var presentationFromTool = (tool) => {
344
- const presentation = asRecord(tool.presentation);
345
- const outputView = asRecord(tool.outputView) || asRecord(tool.output_view);
346
- const next = presentation ? { ...presentation } : {};
347
- [
348
- "title",
349
- "activity",
350
- "detail",
351
- "category",
352
- "groupKey",
353
- "groupLabel",
354
- "showOutput",
355
- "outputView"
356
- ].forEach((key) => {
357
- if (tool[key] !== void 0) {
358
- next[key] = tool[key];
359
- }
360
- });
361
- if (tool.group_key !== void 0) next.group_key = tool.group_key;
362
- if (tool.group_label !== void 0) next.group_label = tool.group_label;
363
- if (tool.show_output !== void 0) next.show_output = tool.show_output;
364
- if (outputView && next.outputView === void 0) next.outputView = outputView;
365
- return Object.keys(next).length > 0 ? next : null;
366
- };
367
- var createToolPart = (block, index) => {
368
- const tool = asRecord(block.tool) || {};
369
- const toolName = toolNameFromRecord(tool);
370
- const errorText = optionalString(tool.errorText) || optionalString(tool.error) || optionalString(block.error) || null;
371
- return {
372
- id: String(block.id || `${block.kind || "tool"}-${index}`),
373
- type: `tool-${toolName}`,
374
- kind: "tool",
375
- toolName,
376
- toolCallId: toolCallIdFromRecords(block, tool),
377
- state: toolStateFromStatus(block.status),
378
- input: tool.input ?? block.input,
379
- output: tool.output ?? block.output,
380
- errorText,
381
- presentation: presentationFromTool(tool),
382
- raw: block
383
- };
384
- };
385
- var createDataPart = (block, index, name = String(block.kind || "data")) => ({
386
- id: String(block.id || `${name}-${index}`),
387
- type: "data",
388
- kind: "data",
389
- name,
390
- data: block,
391
- raw: block
392
- });
393
- var partsFromResponseBlocks = (blocks, fallbackText) => {
394
- const parts = [];
395
- (blocks || []).forEach((block, index) => {
396
- const id = String(block.id || `${block.kind || "block"}-${index}`);
397
- if (block.kind === "assistant_text" && typeof block.text === "string") {
398
- parts.push({ id, type: "text", kind: "text", text: String(block.text), raw: block });
399
- return;
400
- }
401
- if (block.kind === "tool_call") {
402
- parts.push(createToolPart(block, index));
403
- return;
404
- }
405
- if (block.kind === "ui_blocks") {
406
- parts.push({
407
- id,
408
- type: "ui-blocks",
409
- kind: "ui-blocks",
410
- state: toolStateFromStatus(block.status),
411
- toolCallId: optionalString(block.toolCallId) || optionalString(block.tool_call_id) || null,
412
- contractVersion: optionalString(block.contractVersion) || optionalString(block.contract_version) || null,
413
- bundle: asRecord(block.bundle),
414
- errorText: optionalString(block.error) || null,
415
- raw: block
416
- });
417
- return;
418
- }
419
- if (block.kind === "reasoning_note") {
420
- parts.push({
421
- id,
422
- type: "reasoning",
423
- kind: "reasoning",
424
- label: optionalString(block.label) || null,
425
- text: optionalString(block.text) || optionalString(block.summary) || null,
426
- status: optionalString(block.status) || null,
427
- raw: block
428
- });
429
- return;
430
- }
431
- if (block.kind === "hitl_request") {
432
- const hitl = asRecord(block.hitl) || block;
433
- if (hitl.schema_version !== "agents24.hitl.interrupt.v2") {
434
- throw new Error("Invalid V2 HITL response block.");
435
- }
436
- const interruptId = optionalString(block.interruptId) || optionalString(hitl.interrupt_id);
437
- const hitlKind = optionalString(block.hitlKind) || optionalString(hitl.kind);
438
- const status = optionalString(block.status) || optionalString(hitl.status) || "pending";
439
- if (!interruptId || !["tool_review", "mcp_auth", "user_approval", "app_data_permission"].includes(String(hitlKind)) || !HITL_STATUSES.includes(status)) {
440
- throw new Error("Invalid V2 HITL response block.");
441
- }
442
- const kind = hitlKind;
443
- const allowedActions = exactHitlActions(kind, hitl.allowed_actions);
444
- const resolution = asRecord(block.resolution);
445
- const resolver = asRecord(resolution?.resolver);
446
- parts.push({
447
- id,
448
- type: "hitl",
449
- kind: "hitl",
450
- interruptId,
451
- hitlKind: kind,
452
- message: optionalString(hitl.message) || optionalString(block.text) || "Input is required to continue.",
453
- allowedActions,
454
- status,
455
- presentation: asRecord(hitl.presentation) || {},
456
- resolution: resolution ? {
457
- action: ["approve", "reject", "connect", "skip"].includes(String(resolution.action)) ? String(resolution.action) : null,
458
- outcome: optionalString(resolution.outcome),
459
- reason: optionalString(resolution.reason),
460
- resolvedAt: optionalString(resolution.resolved_at),
461
- resolver: resolver ? {
462
- principalType: optionalString(resolver.principal_type) || optionalString(resolver.principalType) || null,
463
- principalId: optionalString(resolver.principal_id) || optionalString(resolver.principalId) || null
464
- } : null
465
- } : null,
466
- raw: block
467
- });
468
- return;
469
- }
470
- if (block.kind === "error") {
471
- parts.push({
472
- id,
473
- type: "error",
474
- kind: "error",
475
- errorText: optionalString(block.error) || optionalString(block.message) || "Error",
476
- raw: block
477
- });
478
- return;
479
- }
480
- parts.push(createDataPart(block, index));
481
- });
482
- if (!parts.some((part) => part.kind === "text") && fallbackText?.trim()) {
483
- parts.push({ id: "assistant-fallback-text", type: "text", kind: "text", text: fallbackText });
484
- }
485
- return parts;
486
- };
487
- var mergeReasoningSteps = (steps, options) => {
488
- if (!steps?.length) return [];
489
- const merged = [];
490
- const indexMap = /* @__PURE__ */ new Map();
491
- steps.forEach((step) => {
492
- const key = String(step.label);
493
- const status = options?.finalize ? "complete" : step.status || "complete";
494
- if (indexMap.has(key)) {
495
- const index = indexMap.get(key);
496
- merged[index] = { ...merged[index], ...step, status };
497
- } else {
498
- indexMap.set(key, merged.push({ ...step, status }) - 1);
499
- }
500
- });
501
- return merged;
502
- };
503
- var reasoningStepsFromParts = (parts) => (parts || []).map((block) => {
504
- if (block.kind === "tool") {
505
- const label = optionalString(block.presentation?.activity) || optionalString(block.presentation?.title) || block.toolName;
506
- return {
507
- label,
508
- status: block.state === "input-available" || block.state === "input-streaming" ? "active" : "complete",
509
- description: optionalString(block.presentation?.detail) || ""
510
- };
511
- }
512
- if (block.kind === "reasoning") {
513
- return {
514
- label: block.label || "Reasoning",
515
- status: block.status === "running" ? "active" : "complete",
516
- description: block.text || ""
517
- };
518
- }
519
- return null;
520
- }).filter(Boolean);
521
- var turnToMessages = (turn, activeRunId) => {
522
- const messages = [];
523
- const createdAt = turn.created_at ? new Date(turn.created_at) : /* @__PURE__ */ new Date();
524
- const baseId = turn.id || turn.run_id || createChatId();
525
- const responseBlocks = responseBlocksFromTurn(turn);
526
- const isRunning = isRunningThreadStatus(turn.status) || Boolean(turn.run_id && activeRunId === turn.run_id);
527
- const attachments = attachmentsFromTurn(turn);
528
- const userContent = displayTextWithoutInlineAttachments(turn.user_input_text || "", attachments.length > 0);
529
- if (userContent || attachments.length > 0) {
530
- messages.push({
531
- id: `${baseId}-user`,
532
- role: "user",
533
- content: userContent,
534
- createdAt,
535
- parts: userContent ? [{ id: `${baseId}-user-text`, type: "text", kind: "text", text: userContent }] : [],
536
- attachments
537
- });
538
- }
539
- const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
540
- if (assistantText || responseBlocks.length > 0 || isRunning) {
541
- const parts = partsFromResponseBlocks(responseBlocks, assistantText);
542
- messages.push({
543
- id: `${baseId}-assistant`,
544
- role: "assistant",
545
- runId: turn.run_id || null,
546
- content: assistantText || assistantTextFromParts(parts),
547
- createdAt: turn.completed_at ? new Date(turn.completed_at) : createdAt,
548
- parts,
549
- reasoningSteps: mergeReasoningSteps(reasoningStepsFromParts(parts), { finalize: !isRunning }),
550
- isFinal: !isRunning,
551
- tokenUsage: tokenUsageFromTurn(turn)
552
- });
553
- }
554
- return messages;
555
- };
556
- var threadDetailToMessages = (thread) => {
557
- const activeRun = activeRunIdFromThreadDetail(thread);
558
- return (thread.turns || []).flatMap((turn) => turnToMessages(turn, activeRun));
559
- };
560
- var threadPaging = (thread) => {
561
- const nextBeforeTurnIndex = thread.paging?.next_before_turn_index ?? null;
562
- return {
563
- hasOlderTurns: Boolean(thread.paging?.has_more || nextBeforeTurnIndex !== null),
564
- nextBeforeTurnIndex
565
- };
566
- };
567
- var latestContextWindowFromThread = (thread) => {
568
- const threadContextWindow = normalizeContextWindow(thread.context_window);
569
- if (threadContextWindow) return threadContextWindow;
570
- for (let index = (thread.turns || []).length - 1; index >= 0; index -= 1) {
571
- const contextWindow = normalizeContextWindow(thread.turns?.[index]?.context_window);
572
- if (contextWindow) return contextWindow;
573
- }
574
- return null;
575
- };
576
- var activeRunIdFromThread = (thread) => {
577
- const hasCamelActiveRun = Boolean(
578
- thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
579
- );
580
- const hasCamelLastRunStatus = Boolean(
581
- thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
582
- );
583
- const hasCamelLastRunId = Boolean(
584
- thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
585
- );
586
- const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
587
- const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
588
- const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
589
- if (activeRunId && isRunningThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
590
- const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
591
- return lastRunId && isRunningThreadStatus(lastRunStatus) ? String(lastRunId) : null;
592
- };
593
- var autoAttachRunIdFromThread = (thread) => {
594
- const hasCamelActiveRun = Boolean(
595
- thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
596
- );
597
- const hasCamelLastRunStatus = Boolean(
598
- thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
599
- );
600
- const hasCamelLastRunId = Boolean(
601
- thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
602
- );
603
- const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
604
- const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
605
- const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
606
- if (activeRunId && isAutoAttachThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
607
- const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
608
- return lastRunId && isAutoAttachThreadStatus(lastRunStatus) ? String(lastRunId) : null;
609
- };
610
- var hasUnfinishedAssistantMessage = (messages) => Boolean(messages?.some((message) => message.role === "assistant" && message.isFinal === false));
611
- var hasStaleUnfinishedAssistantCache = (thread) => hasUnfinishedAssistantMessage(thread?.messages) && !activeRunIdFromThread(thread);
612
- var activeRunIdFromThreadDetail = (thread) => {
613
- const summaryRunId = activeRunIdFromThread(thread);
614
- if (summaryRunId) return summaryRunId;
615
- const runningTurn = [...thread?.turns || []].reverse().find((turn) => turn.run_id && isRunningThreadStatus(turn.status));
616
- return runningTurn?.run_id ? String(runningTurn.run_id) : null;
617
- };
618
-
619
- // src/controller-helpers.ts
620
- var threadSummaryToStored = (thread) => ({
621
- ...thread,
622
- id: String(thread.id),
623
- title: String(thread.title || "New chat"),
624
- updated_at: String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString()),
625
- messages: [],
626
- isHydrated: false
627
- });
628
- var isThreadNotFoundError = (error) => {
629
- if (!error || typeof error !== "object" || !("status" in error)) return false;
630
- return Number(error.status) === 404;
631
- };
632
- var isAbortError = (error) => {
633
- if (!error || typeof error !== "object") return false;
634
- const maybe = error;
635
- return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
636
- };
637
- var abortDetachedStream = (controller) => {
638
- if (!controller || controller.signal.aborted) return;
639
- try {
640
- const reason = typeof DOMException !== "undefined" ? new DOMException("Chat stream detached.", "AbortError") : new Error("Chat stream detached.");
641
- controller.abort(reason);
642
- } catch {
643
- }
644
- };
645
- var hasRunState = (thread) => Boolean(
646
- thread.active_run || thread.activeRun || thread.last_run_id || thread.lastRunId || thread.last_run_status || thread.lastRunStatus || thread.isRunning
647
- );
648
- var mergeStoredThreadsForRefresh = (currentThreads, serverThreads) => {
649
- const currentById = new Map(currentThreads.map((thread) => [thread.id, thread]));
650
- const serverIds = new Set(serverThreads.map((thread) => thread.id));
651
- const mergedServerThreads = serverThreads.map((serverThread) => {
652
- const current = currentById.get(serverThread.id);
653
- if (!current) return serverThread;
654
- const incomingHasRunState = hasRunState(serverThread);
655
- return {
656
- ...current,
657
- ...serverThread,
658
- messages: current.messages || [],
659
- isHydrated: current.isHydrated,
660
- hasOlderTurns: current.hasOlderTurns,
661
- nextBeforeTurnIndex: current.nextBeforeTurnIndex,
662
- active_run: incomingHasRunState ? serverThread.active_run : current.active_run,
663
- activeRun: incomingHasRunState ? serverThread.activeRun : current.activeRun,
664
- last_run_id: incomingHasRunState ? serverThread.last_run_id : current.last_run_id,
665
- lastRunId: incomingHasRunState ? serverThread.lastRunId : current.lastRunId,
666
- last_run_status: incomingHasRunState ? serverThread.last_run_status : current.last_run_status,
667
- lastRunStatus: incomingHasRunState ? serverThread.lastRunStatus : current.lastRunStatus,
668
- lastEventSeq: incomingHasRunState ? serverThread.lastEventSeq : current.lastEventSeq,
669
- isRunning: incomingHasRunState ? serverThread.isRunning : current.isRunning
670
- };
671
- });
672
- const localOnlyThreads = currentThreads.filter((thread) => {
673
- if (serverIds.has(thread.id)) return false;
674
- return Boolean(thread.isHydrated || thread.messages?.length || thread.isRunning || thread.activeRun || thread.active_run);
675
- });
676
- return [...mergedServerThreads, ...localOnlyThreads];
677
- };
678
- var stableStringify = (value) => JSON.stringify(value ?? null);
679
- var sameStoredThread = (left, right) => {
680
- if (left === right) return true;
681
- if (!left || !right) return false;
682
- return stableStringify(left) === stableStringify(right);
683
- };
684
- var applyThreadSummaryEvent = (currentThreads, event) => {
685
- if (event.event === "snapshot_required") return currentThreads;
686
- if (event.event === "thread.deleted") {
687
- const next2 = currentThreads.filter((thread) => thread.id !== event.thread_id);
688
- return next2.length === currentThreads.length ? currentThreads : next2;
689
- }
690
- const incoming = threadSummaryToStored(event.thread);
691
- const index = currentThreads.findIndex((thread) => thread.id === incoming.id);
692
- const sortByActivity = (threads) => [...threads].sort((a, b) => {
693
- const left = Date.parse(String(a.updated_at || a.last_activity_at || a.created_at || ""));
694
- const right = Date.parse(String(b.updated_at || b.last_activity_at || b.created_at || ""));
695
- return (Number.isFinite(right) ? right : 0) - (Number.isFinite(left) ? left : 0);
696
- });
697
- if (index === -1) return sortByActivity([...currentThreads, incoming]);
698
- const [merged] = mergeStoredThreadsForRefresh([currentThreads[index]], [incoming]);
699
- if (sameStoredThread(currentThreads[index], merged)) return currentThreads;
700
- const next = [...currentThreads];
701
- next[index] = merged;
702
- return sortByActivity(next);
703
- };
704
-
705
- // src/controller-thread-events.ts
706
- function useControllerThreadEvents({
707
- transport,
708
- storage,
709
- refresh,
710
- setThreads
711
- }) {
712
- const cursorRef = useRef(null);
713
- useEffect(() => {
714
- if (!transport.subscribeThreadEvents) return;
715
- let cancelled = false;
716
- let retryTimeout = null;
717
- let controller = null;
718
- const connect = () => {
719
- if (cancelled) return;
720
- controller = new AbortController();
721
- transport.subscribeThreadEvents?.(
722
- { cursor: cursorRef.current, signal: controller.signal },
723
- async (event) => {
724
- if (typeof event.cursor === "number") cursorRef.current = event.cursor;
725
- if (event.event === "snapshot_required") {
726
- await refresh().catch(() => void 0);
727
- return;
728
- }
729
- storage.setThreads(applyThreadSummaryEvent(storage.listThreads(), event));
730
- setThreads(storage.listThreads());
731
- }
732
- ).catch((error) => {
733
- if (cancelled || isAbortError(error)) return;
734
- retryTimeout = setTimeout(connect, 1500);
735
- });
736
- };
737
- connect();
738
- return () => {
739
- cancelled = true;
740
- if (retryTimeout) clearTimeout(retryTimeout);
741
- controller?.abort();
742
- };
743
- }, [refresh, setThreads, storage, transport]);
744
- }
745
-
746
- // src/message-lifecycle.ts
747
- function findStableAssistantMessageIndex(messages, input) {
748
- return messages.findIndex(
749
- (message) => message.role === "assistant" && (input.messageId && message.id === input.messageId || Boolean(input.runId && message.runId === input.runId))
750
- );
751
- }
752
- function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
753
- const next = [...messages];
754
- if (!next.some((message) => message.id === userMessage.id)) {
755
- next.push(userMessage);
756
- }
757
- if (!next.some(
758
- (message) => message.id === assistantMessage.id || Boolean(
759
- assistantMessage.runId && message.role === "assistant" && message.runId === assistantMessage.runId
760
- )
761
- )) {
762
- next.push(assistantMessage);
763
- }
764
- return next;
765
- }
766
- function upsertStableAssistantMessage(messages, input) {
767
- const next = [...messages];
768
- for (const baseMessage of input.baseMessages || []) {
769
- const matchesTarget = baseMessage.role === "assistant" && (input.messageId && baseMessage.id === input.messageId || Boolean(input.runId && baseMessage.runId === input.runId));
770
- if (!matchesTarget && !next.some((message) => message.id === baseMessage.id)) {
771
- next.push(baseMessage);
772
- }
773
- }
774
- const index = findStableAssistantMessageIndex(next, input);
775
- if (index === -1) {
776
- next.push(input.create());
777
- return next;
778
- }
779
- next[index] = input.update(next[index]);
780
- return next;
781
- }
782
-
783
- // src/controller.ts
784
- function useAgents24ChatController(options) {
785
- const {
786
- transport,
787
- storage,
788
- activeThreadId: controlledActiveThreadId,
789
- pageSize = DEFAULT_THREAD_PAGE_SIZE,
790
- storageKey,
791
- createId = createChatId,
792
- onActiveThreadIdChange,
793
- onSourceClick,
794
- onStreamErrorMessage,
795
- onRuntimeEvent,
796
- onThreadDetailLoaded
797
- } = options;
798
- const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
799
- const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
800
- const [messages, setMessages] = useState2(() => initialCached?.messages || []);
801
- const [isLoading, setIsLoading] = useState2(false);
802
- const [isLoadingHistory, setIsLoadingHistory] = useState2(() => Boolean(activeThreadId) && !initialCached?.messages?.length);
803
- const [isLoadingOlder, setIsLoadingOlder] = useState2(false);
804
- const [hasOlderTurns, setHasOlderTurns] = useState2(Boolean(initialCached?.hasOlderTurns));
805
- const [streamingContent, setStreamingContent] = useState2("");
806
- const [streamingMessageId, setStreamingMessageId] = useState2(null);
807
- const [contextStatus, setContextStatus] = useState2(null);
808
- const [currentReasoning, setCurrentReasoning] = useState2([]);
809
- const [liked, setLiked] = useState2({});
810
- const [disliked, setDisliked] = useState2({});
811
- const [copiedMessageId, setCopiedMessageId] = useState2(null);
812
- const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState2(null);
813
- const [threads, setThreads] = useState2(() => storage.listThreads());
814
- const [isRefreshingThreads, setIsRefreshingThreads] = useState2(false);
815
- const [isSelectingThread, setIsSelectingThread] = useState2(false);
816
- const [activeRunId, setActiveRunId] = useState2(null);
817
- const textareaRef = useRef2(null);
818
- const activeThreadIdRef = useRef2(activeThreadId);
819
- const messagesRef = useRef2(messages);
820
- const nextBeforeTurnIndexRef = useRef2(initialCached?.nextBeforeTurnIndex ?? null);
821
- const hasOlderTurnsRef = useRef2(Boolean(initialCached?.hasOlderTurns));
822
- const isLoadingOlderRef = useRef2(false);
823
- const loadedThreadIdRef = useRef2(initialCached?.messages?.length ? activeThreadId : null);
824
- const requestSeqRef = useRef2(0);
825
- const isLoadingHistoryRef = useRef2(isLoadingHistory);
826
- const activeRunIdRef = useRef2(null);
827
- const reattachedRunIdRef = useRef2(null);
828
- const abortControllerRef = useRef2(null);
829
- const [lifecycleAbortController] = useState2(() => new AbortController());
830
- const streamingContentRef = useRef2("");
831
- const streamingMessageIdRef = useRef2(null);
832
- const reasoningRef = useRef2([]);
833
- const liveVoiceIdsRef = useRef2({});
834
- const refreshSeqRef = useRef2(0);
835
- const setActiveRunIdValue = useCallback3((runId) => {
836
- activeRunIdRef.current = runId;
837
- setActiveRunId(runId);
838
- }, []);
839
- const syncThreadsFromStorage = useCallback3(() => {
840
- setThreads(storage.listThreads());
841
- }, [storage]);
842
- const upsertStoredThread = useCallback3(
843
- (thread) => {
844
- storage.upsertThread(thread);
845
- syncThreadsFromStorage();
846
- },
847
- [storage, syncThreadsFromStorage]
848
- );
849
- useEffect2(() => {
850
- messagesRef.current = messages;
851
- }, [messages]);
852
- const persistThread = useCallback3(
853
- (threadId, nextMessages, paging, options2) => {
854
- const existing = storage.getThread(threadId);
855
- const firstUser = nextMessages.find((message) => message.role === "user");
856
- upsertStoredThread({
857
- ...existing || {},
858
- id: threadId,
859
- title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
860
- updated_at: options2?.updatedAt || (options2?.touch ? (/* @__PURE__ */ new Date()).toISOString() : existing?.updated_at) || (/* @__PURE__ */ new Date()).toISOString(),
861
- messages: nextMessages,
862
- isHydrated: true,
863
- hasOlderTurns: paging?.hasOlderTurns ?? hasOlderTurnsRef.current,
864
- nextBeforeTurnIndex: paging?.nextBeforeTurnIndex ?? nextBeforeTurnIndexRef.current
865
- });
866
- },
867
- [storage, upsertStoredThread]
868
- );
869
- const markThreadRunStatus = useCallback3(
870
- (threadId, runId, status, lastEventSeq) => {
871
- const existing = storage.getThread(threadId);
872
- if (!existing || !runId) return;
873
- upsertStoredThread({
874
- ...existing,
875
- last_run_id: runId,
876
- last_run_status: status,
877
- active_run: {
878
- run_id: runId,
879
- status,
880
- created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
881
- },
882
- lastRunId: runId,
883
- lastRunStatus: status,
884
- activeRun: {
885
- run_id: runId,
886
- status,
887
- created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
888
- },
889
- lastEventSeq: typeof lastEventSeq === "number" ? lastEventSeq : existing.lastEventSeq ?? null,
890
- isRunning: activeRunIdFromThread({
891
- lastRunId: runId,
892
- lastRunStatus: status,
893
- activeRun: { run_id: runId, status }
894
- }) !== null
895
- });
896
- },
897
- [storage, upsertStoredThread]
898
- );
899
- const refresh = useCallback3(async () => {
900
- const seq = ++refreshSeqRef.current;
901
- setIsRefreshingThreads(true);
902
- try {
903
- const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
904
- if (lifecycleAbortController.signal.aborted) return;
905
- if (seq !== refreshSeqRef.current) return;
906
- const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
907
- storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
908
- setThreads(storage.listThreads());
909
- } finally {
910
- if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
911
- }
912
- }, [lifecycleAbortController, storage, transport]);
913
- const applyThreadId = useCallback3(
914
- (threadId, baseMessages) => {
915
- if (!threadId || activeThreadIdRef.current === threadId) return;
916
- activeThreadIdRef.current = threadId;
917
- persistThread(threadId, messagesRef.current.length > 0 ? messagesRef.current : baseMessages, void 0, { touch: true });
918
- storage.setActiveThreadId?.(threadId);
919
- onActiveThreadIdChange?.(threadId);
920
- },
921
- [onActiveThreadIdChange, persistThread, storage]
922
- );
923
- const setStreamingText = (value) => {
924
- streamingContentRef.current = value;
925
- setStreamingContent(value);
926
- };
927
- const setLoadingHistory = useCallback3((value) => {
928
- isLoadingHistoryRef.current = value;
929
- setIsLoadingHistory(value);
930
- }, []);
931
- const setReasoningSteps = useCallback3((value) => {
932
- reasoningRef.current = value || [];
933
- setCurrentReasoning(value || []);
934
- }, []);
935
- const detachActiveStream = useCallback3(() => {
936
- const controller = abortControllerRef.current;
937
- abortControllerRef.current = null;
938
- abortDetachedStream(controller);
939
- setActiveRunIdValue(null);
940
- reattachedRunIdRef.current = null;
941
- streamingMessageIdRef.current = null;
942
- streamingContentRef.current = "";
943
- reasoningRef.current = [];
944
- setStreamingMessageId(null);
945
- setIsLoading(false);
946
- setStreamingContent("");
947
- setCurrentReasoning([]);
948
- }, [setActiveRunIdValue]);
949
- const clearMissingThread = useCallback3(
950
- (threadId) => {
951
- storage.deleteThread?.(threadId);
952
- syncThreadsFromStorage();
953
- if (activeThreadIdRef.current !== threadId) return;
954
- requestSeqRef.current += 1;
955
- detachActiveStream();
956
- activeThreadIdRef.current = null;
957
- loadedThreadIdRef.current = null;
958
- nextBeforeTurnIndexRef.current = null;
959
- hasOlderTurnsRef.current = false;
960
- storage.setActiveThreadId?.(null);
961
- onActiveThreadIdChange?.(null);
962
- setMessages([]);
963
- messagesRef.current = [];
964
- setHasOlderTurns(false);
965
- setIsLoadingOlder(false);
966
- setContextStatus(null);
967
- setLoadingHistory(false);
968
- },
969
- [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
970
- );
971
- const setLiveAssistantMessage = useCallback3(
972
- (input) => {
973
- setMessages((prev) => {
974
- const next = upsertStableAssistantMessage(prev, {
975
- baseMessages: input.baseMessages,
976
- messageId: input.messageId,
977
- runId: input.runId,
978
- create: () => ({
979
- id: input.messageId,
980
- role: "assistant",
981
- runId: input.runId ?? null,
982
- content: input.content,
983
- createdAt: /* @__PURE__ */ new Date(),
984
- reasoningSteps: input.reasoning,
985
- isFinal: false,
986
- parts: input.parts || []
987
- }),
988
- update: (message) => ({
989
- ...message,
990
- runId: input.runId ?? message.runId ?? null,
991
- content: input.content,
992
- reasoningSteps: input.reasoning,
993
- isFinal: false,
994
- parts: input.parts ?? message.parts
995
- })
996
- });
997
- messagesRef.current = next;
998
- return next;
999
- });
1000
- },
1001
- []
1002
- );
1003
- const finalizeAssistantMessage = useCallback3(
1004
- (input) => {
1005
- const content = input.error || input.assistantText.trim();
1006
- if (!content) return input.baseMessages;
1007
- const existingIndex = input.baseMessages.findIndex(
1008
- (message) => input.messageId && message.id === input.messageId || input.runId && message.role === "assistant" && message.runId === input.runId
1009
- );
1010
- const existing = existingIndex >= 0 ? input.baseMessages[existingIndex] : void 0;
1011
- const parts = input.parts || existing?.parts || partsFromResponseBlocks(void 0, content);
1012
- const assistant = {
1013
- id: existing?.id || input.messageId || createId(),
1014
- role: "assistant",
1015
- runId: input.runId ?? existing?.runId ?? null,
1016
- content,
1017
- createdAt: /* @__PURE__ */ new Date(),
1018
- isFinal: true,
1019
- parts,
1020
- reasoningSteps: mergeReasoningSteps(input.reasoning, { finalize: true }),
1021
- thinkingDurationMs: input.thinkingDurationMs
1022
- };
1023
- const completed = upsertStableAssistantMessage(input.baseMessages, {
1024
- messageId: input.messageId,
1025
- runId: input.runId,
1026
- create: () => assistant,
1027
- update: (message) => ({
1028
- ...message,
1029
- ...assistant,
1030
- id: message.id,
1031
- createdAt: message.createdAt
1032
- })
1033
- });
1034
- setMessages(completed);
1035
- messagesRef.current = completed;
1036
- if (input.threadId) {
1037
- persistThread(input.threadId, completed);
1038
- const existingThread = storage.getThread(input.threadId);
1039
- if (existingThread && input.runId) {
1040
- upsertStoredThread({
1041
- ...existingThread,
1042
- messages: completed,
1043
- last_run_id: input.runId,
1044
- last_run_status: input.error ? "failed" : "completed",
1045
- active_run: null,
1046
- lastRunId: input.runId,
1047
- lastRunStatus: input.error ? "failed" : "completed",
1048
- activeRun: null,
1049
- isRunning: false
1050
- });
1051
- }
1052
- }
1053
- return completed;
1054
- },
1055
- [createId, persistThread, storage, upsertStoredThread]
1056
- );
1057
- const loadThread = useCallback3(
1058
- async (threadId) => {
1059
- const seq = ++requestSeqRef.current;
1060
- setLoadingHistory(true);
1061
- setIsLoadingOlder(false);
1062
- isLoadingOlderRef.current = false;
1063
- try {
1064
- const detail = await transport.getThread({
1065
- threadId,
1066
- limit: pageSize,
1067
- includeRunEvents: false,
1068
- signal: lifecycleAbortController.signal
1069
- });
1070
- if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
1071
- void onThreadDetailLoaded?.(detail);
1072
- const nextMessages = threadDetailToMessages(detail);
1073
- const paging = threadPaging(detail);
1074
- setContextStatus(latestContextWindowFromThread(detail));
1075
- setMessages(nextMessages);
1076
- messagesRef.current = nextMessages;
1077
- setHasOlderTurns(paging.hasOlderTurns);
1078
- hasOlderTurnsRef.current = paging.hasOlderTurns;
1079
- nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
1080
- loadedThreadIdRef.current = threadId;
1081
- upsertStoredThread({
1082
- ...threadSummaryToStored(detail),
1083
- messages: nextMessages,
1084
- isHydrated: true,
1085
- hasOlderTurns: paging.hasOlderTurns,
1086
- nextBeforeTurnIndex: paging.nextBeforeTurnIndex,
1087
- updated_at: threadActivityDate(detail)
1088
- });
1089
- } catch (error) {
1090
- if (isAbortError(error)) return;
1091
- if (isThreadNotFoundError(error)) {
1092
- clearMissingThread(threadId);
1093
- return;
1094
- }
1095
- throw error;
1096
- } finally {
1097
- if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
1098
- }
1099
- },
1100
- [clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
1101
- );
1102
- const loadOlderTurns = useCallback3(async () => {
1103
- const threadId = activeThreadIdRef.current;
1104
- const beforeTurnIndex = nextBeforeTurnIndexRef.current;
1105
- if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
1106
- isLoadingOlderRef.current = true;
1107
- setIsLoadingOlder(true);
1108
- try {
1109
- const detail = await transport.getThread({
1110
- threadId,
1111
- limit: pageSize,
1112
- beforeTurnIndex,
1113
- includeRunEvents: false,
1114
- signal: lifecycleAbortController.signal
1115
- });
1116
- if (activeThreadIdRef.current !== threadId) return;
1117
- const older = threadDetailToMessages(detail);
1118
- const paging = threadPaging(detail);
1119
- const next = [...older, ...messagesRef.current];
1120
- setMessages(next);
1121
- messagesRef.current = next;
1122
- setHasOlderTurns(paging.hasOlderTurns);
1123
- hasOlderTurnsRef.current = paging.hasOlderTurns;
1124
- nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
1125
- persistThread(threadId, next, paging);
1126
- } catch (error) {
1127
- if (isAbortError(error)) return;
1128
- if (isThreadNotFoundError(error)) {
1129
- clearMissingThread(threadId);
1130
- return;
1131
- }
1132
- throw error;
1133
- } finally {
1134
- if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
1135
- isLoadingOlderRef.current = false;
1136
- }
1137
- }, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
1138
- const handleStreamEvent = useCallback3(
1139
- (input) => {
1140
- const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
1141
- const payload = event.payload || {};
1142
- const responseBlocks = Array.isArray(payload.response_blocks) ? payload.response_blocks : null;
1143
- if (event.run_id) setActiveRunIdValue(event.run_id);
1144
- void onRuntimeEvent?.(event, {
1145
- mode,
1146
- assistantMessageId,
1147
- threadId: streamThreadIdRef.current,
1148
- runId: event.run_id || activeRunIdRef.current,
1149
- startedAt
1150
- });
1151
- setContextStatus((current) => mergeContextWindowUpdate(current, payload.context_window));
1152
- if (responseBlocks) {
1153
- const blockText = String(payload.assistant_output_text || "") || assistantTextFromResponseBlocks(responseBlocks) || streamingContentRef.current;
1154
- if (blockText) setStreamingText(blockText);
1155
- const parts = partsFromResponseBlocks(responseBlocks, blockText);
1156
- const reasoning = mergeReasoningSteps([...reasoningRef.current, ...reasoningStepsFromParts(parts)]);
1157
- setReasoningSteps(reasoning);
1158
- setLiveAssistantMessage({
1159
- messageId: assistantMessageId,
1160
- runId: event.run_id || activeRunIdRef.current,
1161
- content: blockText,
1162
- reasoning,
1163
- parts,
1164
- baseMessages
1165
- });
1166
- }
1167
- if (event.event === "run.accepted" || event.event === "run.snapshot") {
1168
- const nextThreadId = String(payload.thread_id || streamThreadIdRef.current || "");
1169
- if (nextThreadId) {
1170
- streamThreadIdRef.current = nextThreadId;
1171
- applyThreadId(nextThreadId, baseMessages);
1172
- markThreadRunStatus(nextThreadId, event.run_id || activeRunIdRef.current || "", String(payload.status || "running"), typeof payload.last_event_seq === "number" ? payload.last_event_seq : null);
1173
- }
1174
- return;
1175
- }
1176
- const isTerminal = event.event === "run.completed" || event.event === "run.cancelled" || event.event === "reshet.stream.result";
1177
- const isFailed = event.event === "run.failed" || event.event.endsWith(".stream.error");
1178
- if (!isTerminal && !isFailed) return;
1179
- const terminalThreadId = String(payload.thread_id || "");
1180
- if (terminalThreadId && !streamThreadIdRef.current) {
1181
- streamThreadIdRef.current = terminalThreadId;
1182
- applyThreadId(terminalThreadId, baseMessages);
1183
- }
1184
- const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
1185
- finalizeAssistantMessage({
1186
- threadId: streamThreadIdRef.current || activeThreadIdRef.current,
1187
- baseMessages: messagesRef.current,
1188
- assistantText: finalText,
1189
- reasoning: reasoningRef.current,
1190
- thinkingDurationMs: Date.now() - startedAt,
1191
- messageId: assistantMessageId,
1192
- runId: event.run_id || activeRunIdRef.current,
1193
- parts: responseBlocks ? partsFromResponseBlocks(responseBlocks, finalText) : void 0,
1194
- error: isFailed ? String(payload.message || payload.error || event.diagnostics?.[0]?.message || onStreamErrorMessage?.(event) || "The chat run failed.") : void 0
1195
- });
1196
- },
1197
- [applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
1198
- );
1199
- const runStream = useCallback3(
1200
- async (input) => {
1201
- const startedAt = Date.now();
1202
- const controller = new AbortController();
1203
- requestSeqRef.current += 1;
1204
- abortControllerRef.current = controller;
1205
- setIsLoading(true);
1206
- setStreamingText("");
1207
- setReasoningSteps([{ label: input.mode === "attach" ? "Reconnecting" : "Connecting", status: "active" }]);
1208
- const streamThreadIdRef = { current: input.mode === "attach" ? input.threadId : activeThreadIdRef.current };
1209
- const attachAssistantMessage = input.mode === "attach" ? [...messagesRef.current].reverse().find(
1210
- (message) => message.role === "assistant" && (message.runId === input.runId || message.isFinal === false)
1211
- ) : void 0;
1212
- let assistantMessageId = input.mode === "attach" ? attachAssistantMessage?.id || `${input.runId}-assistant` : createId();
1213
- let baseMessages = messagesRef.current;
1214
- if (input.mode === "submit") {
1215
- const userMessage = {
1216
- id: createId(),
1217
- role: "user",
1218
- content: input.message.text,
1219
- createdAt: /* @__PURE__ */ new Date(),
1220
- parts: input.message.text ? [{ id: createId(), type: "text", kind: "text", text: input.message.text }] : [],
1221
- attachments: input.message.files
1222
- };
1223
- baseMessages = [...messagesRef.current, userMessage];
1224
- }
1225
- if (!messagesRef.current.some((message) => message.id === assistantMessageId)) {
1226
- const assistantMessage = {
1227
- id: assistantMessageId,
1228
- role: "assistant",
1229
- content: "",
1230
- createdAt: /* @__PURE__ */ new Date(),
1231
- isFinal: false,
1232
- parts: [],
1233
- reasoningSteps: []
1234
- };
1235
- const liveMessages = [...baseMessages, assistantMessage];
1236
- setMessages(liveMessages);
1237
- messagesRef.current = liveMessages;
1238
- } else if (input.mode === "attach" && attachAssistantMessage) {
1239
- const nextMessages = messagesRef.current.map(
1240
- (message) => message.id === attachAssistantMessage.id ? { ...message, runId: message.runId || input.runId, isFinal: false } : message
1241
- );
1242
- setMessages(nextMessages);
1243
- messagesRef.current = nextMessages;
1244
- baseMessages = nextMessages;
1245
- if (streamThreadIdRef.current) persistThread(streamThreadIdRef.current, nextMessages);
1246
- }
1247
- streamingMessageIdRef.current = assistantMessageId;
1248
- setStreamingMessageId(assistantMessageId);
1249
- if (streamThreadIdRef.current) {
1250
- persistThread(
1251
- streamThreadIdRef.current,
1252
- messagesRef.current.length > 0 ? messagesRef.current : baseMessages,
1253
- void 0,
1254
- { touch: input.mode === "submit" }
1255
- );
1256
- }
1257
- try {
1258
- let streamResult = null;
1259
- if (input.mode === "attach") {
1260
- setActiveRunIdValue(input.runId);
1261
- reattachedRunIdRef.current = input.runId;
1262
- streamResult = await transport.attachRun(
1263
- { runId: input.runId, signal: controller.signal },
1264
- (event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
1265
- );
1266
- } else {
1267
- streamResult = await transport.streamMessage(
1268
- { ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
1269
- (event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
1270
- );
1271
- }
1272
- if (streamResult?.runId) setActiveRunIdValue(streamResult.runId);
1273
- if (streamResult?.threadId) {
1274
- streamThreadIdRef.current = streamResult.threadId;
1275
- applyThreadId(streamResult.threadId, baseMessages);
1276
- if (streamResult.runId) {
1277
- markThreadRunStatus(streamResult.threadId, streamResult.runId, "running");
1278
- }
1279
- }
1280
- if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
1281
- finalizeAssistantMessage({
1282
- threadId: streamThreadIdRef.current,
1283
- baseMessages: messagesRef.current,
1284
- assistantText: streamingContentRef.current,
1285
- reasoning: reasoningRef.current,
1286
- thinkingDurationMs: Date.now() - startedAt,
1287
- messageId: assistantMessageId,
1288
- runId: activeRunIdRef.current
1289
- });
1290
- }
1291
- await refresh().catch(() => void 0);
1292
- } catch (error) {
1293
- if (!isAbortError(error)) {
1294
- finalizeAssistantMessage({
1295
- threadId: streamThreadIdRef.current,
1296
- baseMessages: messagesRef.current,
1297
- assistantText: streamingContentRef.current,
1298
- reasoning: reasoningRef.current,
1299
- thinkingDurationMs: Date.now() - startedAt,
1300
- messageId: assistantMessageId,
1301
- runId: activeRunIdRef.current,
1302
- error: onStreamErrorMessage?.(error) || error.message || "Failed to reach the chat agent."
1303
- });
1304
- }
1305
- } finally {
1306
- const isCurrentStream = abortControllerRef.current === controller || streamingMessageIdRef.current === assistantMessageId;
1307
- if (abortControllerRef.current === controller) abortControllerRef.current = null;
1308
- if (isCurrentStream) {
1309
- setActiveRunIdValue(null);
1310
- streamingMessageIdRef.current = null;
1311
- reattachedRunIdRef.current = null;
1312
- setStreamingMessageId(null);
1313
- setIsLoading(false);
1314
- setStreamingText("");
1315
- setReasoningSteps([]);
1316
- setLastThinkingDurationMs(Date.now() - startedAt);
1317
- }
1318
- }
1319
- },
1320
- [createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
1321
- );
1322
- const handleSubmit = useCallback3(
1323
- async (message) => {
1324
- if (!message.text.trim() && !(message.files || []).length) return;
1325
- await runStream({ mode: "submit", message });
1326
- },
1327
- [runStream]
1328
- );
1329
- const attachRun = useCallback3(async (runId, threadId) => {
1330
- const resolvedThreadId = threadId ?? activeThreadIdRef.current;
1331
- if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
1332
- }, [runStream]);
1333
- const { pendingHitl, isResolvingHitl, resumeHitl } = useControllerHitl({
1334
- messages,
1335
- transport,
1336
- activeRunId,
1337
- activeRunIdRef,
1338
- activeThreadIdRef,
1339
- runStream
1340
- });
1341
- const handleStop = useCallback3(() => {
1342
- const runId = activeRunIdRef.current;
1343
- const partial = streamingContentRef.current;
1344
- const liveMessageId = streamingMessageIdRef.current;
1345
- abortDetachedStream(abortControllerRef.current);
1346
- abortControllerRef.current = null;
1347
- setActiveRunIdValue(null);
1348
- streamingMessageIdRef.current = null;
1349
- setStreamingMessageId(null);
1350
- setIsLoading(false);
1351
- setStreamingText("");
1352
- setReasoningSteps([]);
1353
- if (runId) transport.cancelRun({ runId, assistantOutputText: partial }).catch(() => void 0);
1354
- if (partial.trim()) {
1355
- finalizeAssistantMessage({
1356
- threadId: activeThreadIdRef.current,
1357
- baseMessages: messagesRef.current,
1358
- assistantText: partial,
1359
- reasoning: reasoningRef.current,
1360
- thinkingDurationMs: lastThinkingDurationMs || 0,
1361
- messageId: liveMessageId
1362
- });
1363
- }
1364
- }, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
1365
- useEffect2(() => {
1366
- refresh().catch((error) => {
1367
- if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
1368
- setThreads(storage.listThreads());
1369
- }
1370
- });
1371
- }, [lifecycleAbortController, refresh, storage]);
1372
- useEffect2(() => {
1373
- return () => {
1374
- lifecycleAbortController.abort();
1375
- abortDetachedStream(abortControllerRef.current);
1376
- abortControllerRef.current = null;
1377
- };
1378
- }, [lifecycleAbortController]);
1379
- useControllerThreadEvents({ transport, storage, refresh, setThreads });
1380
- useEffect2(() => {
1381
- syncThreadsFromStorage();
1382
- }, [storageKey, syncThreadsFromStorage]);
1383
- useEffect2(() => {
1384
- const previous = activeThreadIdRef.current;
1385
- activeThreadIdRef.current = activeThreadId;
1386
- if (activeThreadId && previous === activeThreadId && (activeRunIdRef.current || streamingMessageIdRef.current)) {
1387
- return;
1388
- }
1389
- if (previous && previous !== activeThreadId && abortControllerRef.current) {
1390
- detachActiveStream();
1391
- }
1392
- if (!activeThreadId) {
1393
- if (previous === null && loadedThreadIdRef.current === null && messagesRef.current.length === 0 && !hasOlderTurnsRef.current && !isLoadingHistoryRef.current && !isLoadingOlderRef.current && nextBeforeTurnIndexRef.current === null) {
1394
- return;
1395
- }
1396
- requestSeqRef.current += 1;
1397
- loadedThreadIdRef.current = null;
1398
- setMessages([]);
1399
- messagesRef.current = [];
1400
- setHasOlderTurns(false);
1401
- setIsLoadingOlder(false);
1402
- setContextStatus(null);
1403
- nextBeforeTurnIndexRef.current = null;
1404
- setLoadingHistory(false);
1405
- return;
1406
- }
1407
- const cached = storage.getThread(activeThreadId);
1408
- const cachedMessages = cached?.isHydrated && cached.messages?.length ? cached.messages : null;
1409
- const hasCachedMessages = Boolean(cachedMessages);
1410
- const shouldRefreshStaleCache = hasCachedMessages && hasStaleUnfinishedAssistantCache(cached);
1411
- if (cached && cachedMessages) {
1412
- setMessages(cachedMessages);
1413
- messagesRef.current = cachedMessages;
1414
- setHasOlderTurns(Boolean(cached.hasOlderTurns));
1415
- hasOlderTurnsRef.current = Boolean(cached.hasOlderTurns);
1416
- nextBeforeTurnIndexRef.current = cached.nextBeforeTurnIndex ?? null;
1417
- loadedThreadIdRef.current = activeThreadId;
1418
- setLoadingHistory(false);
1419
- if (!shouldRefreshStaleCache) return;
1420
- }
1421
- if (!hasCachedMessages) {
1422
- setMessages([]);
1423
- messagesRef.current = [];
1424
- }
1425
- void loadThread(activeThreadId).catch(() => setLoadingHistory(false));
1426
- }, [activeThreadId, detachActiveStream, loadThread, setLoadingHistory, storage]);
1427
- useEffect2(() => {
1428
- const threadId = activeThreadId;
1429
- if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
1430
- const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
1431
- if (!runId || reattachedRunIdRef.current === runId) return;
1432
- void runStream({ mode: "attach", threadId, runId });
1433
- }, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
1434
- useEffect2(() => {
1435
- const threadId = activeThreadId;
1436
- if (!threadId || isLoadingHistoryRef.current || activeRunIdRef.current || streamingMessageIdRef.current) {
1437
- return;
1438
- }
1439
- const cached = storage.getThread(threadId);
1440
- if (!hasStaleUnfinishedAssistantCache(cached)) return;
1441
- void loadThread(threadId).catch(() => setLoadingHistory(false));
1442
- }, [activeThreadId, loadThread, setLoadingHistory, storage, storageKey]);
1443
- const {
1444
- handleCopy,
1445
- handleDislike,
1446
- handleLike,
1447
- handleRetry,
1448
- startNewThread,
1449
- upsertLiveVoiceMessage
1450
- } = useControllerMessageActions({
1451
- activeThreadIdRef,
1452
- createId,
1453
- detachActiveStream,
1454
- disliked,
1455
- handleSubmit,
1456
- hasOlderTurnsRef,
1457
- liked,
1458
- liveVoiceIdsRef,
1459
- loadedThreadIdRef,
1460
- messagesRef,
1461
- nextBeforeTurnIndexRef,
1462
- onActiveThreadIdChange,
1463
- persistThread,
1464
- requestSeqRef,
1465
- setContextStatus,
1466
- setCopiedMessageId,
1467
- setDisliked,
1468
- setHasOlderTurns,
1469
- setIsLoadingOlder,
1470
- setLiked,
1471
- setLoadingHistory,
1472
- setMessages,
1473
- storage
1474
- });
1475
- const loadThreadById = useCallback3(
1476
- async (threadId) => {
1477
- if (!threadId) return;
1478
- setIsSelectingThread(true);
1479
- if (activeThreadIdRef.current !== threadId) {
1480
- if (abortControllerRef.current) detachActiveStream();
1481
- activeThreadIdRef.current = threadId;
1482
- storage.setActiveThreadId?.(threadId);
1483
- onActiveThreadIdChange?.(threadId);
1484
- }
1485
- try {
1486
- await loadThread(threadId);
1487
- } finally {
1488
- if (activeThreadIdRef.current === threadId) setIsSelectingThread(false);
1489
- }
1490
- },
1491
- [detachActiveStream, loadThread, onActiveThreadIdChange, storage]
1492
- );
1493
- const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
1494
- return useMemo2(() => ({
1495
- threads,
1496
- activeThreadId,
1497
- activeThread,
1498
- messages,
1499
- streamingContent,
1500
- streamingMessageId,
1501
- contextStatus,
1502
- currentReasoning,
1503
- isLoading,
1504
- isLoadingHistory,
1505
- isLoadingOlder,
1506
- isRefreshingThreads,
1507
- isSelectingThread,
1508
- hasOlderTurns,
1509
- liked,
1510
- disliked,
1511
- copiedMessageId,
1512
- lastThinkingDurationMs,
1513
- activeRunId,
1514
- pendingHitl,
1515
- isResolvingHitl,
1516
- handleSubmit,
1517
- attachRun,
1518
- resumeHitl,
1519
- handleStop,
1520
- handleCopy,
1521
- handleLike,
1522
- handleDislike,
1523
- handleRetry,
1524
- handleSourceClick: (citations) => onSourceClick?.(citations),
1525
- upsertLiveVoiceMessage,
1526
- startNewThread,
1527
- loadThreadById,
1528
- loadOlderTurns,
1529
- refresh,
1530
- textareaRef
1531
- }), [
1532
- copiedMessageId,
1533
- activeRunId,
1534
- activeThread,
1535
- activeThreadId,
1536
- attachRun,
1537
- contextStatus,
1538
- currentReasoning,
1539
- disliked,
1540
- handleCopy,
1541
- handleDislike,
1542
- handleLike,
1543
- handleRetry,
1544
- handleStop,
1545
- handleSubmit,
1546
- hasOlderTurns,
1547
- isLoading,
1548
- isLoadingHistory,
1549
- isLoadingOlder,
1550
- isRefreshingThreads,
1551
- isSelectingThread,
1552
- lastThinkingDurationMs,
1553
- liked,
1554
- loadThreadById,
1555
- loadOlderTurns,
1556
- messages,
1557
- isResolvingHitl,
1558
- onSourceClick,
1559
- pendingHitl,
1560
- refresh,
1561
- resumeHitl,
1562
- streamingContent,
1563
- streamingMessageId,
1564
- startNewThread,
1565
- threads,
1566
- upsertLiveVoiceMessage
1567
- ]);
1568
- }
1569
-
1570
- // src/mcp-oauth.ts
1571
- var CONNECTION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1572
- function normalizedOrigin(value) {
1573
- try {
1574
- return new URL(value).origin;
1575
- } catch {
1576
- throw new Error("MCP authorization returned an invalid callback origin.");
1577
- }
1578
- }
1579
- function waitForMcpOauthComplete({
1580
- popup,
1581
- callbackOrigin,
1582
- serverId,
1583
- timeoutMs = 12e4
1584
- }) {
1585
- if (!popup) return Promise.reject(new Error("Could not open the MCP authorization popup."));
1586
- const expectedOrigin = normalizedOrigin(callbackOrigin);
1587
- const expectedServerId = String(serverId || "").trim();
1588
- if (!expectedServerId) return Promise.reject(new Error("MCP authorization is missing its server identity."));
1589
- return new Promise((resolve, reject) => {
1590
- let settled = false;
1591
- const finish = (result) => {
1592
- if (settled) return;
1593
- settled = true;
1594
- window.clearTimeout(timeout);
1595
- window.clearInterval(closeTimer);
1596
- window.removeEventListener("message", onMessage);
1597
- if (result instanceof Error) reject(result);
1598
- else resolve(result);
1599
- };
1600
- const onMessage = (event) => {
1601
- if (event.source !== popup || event.origin !== expectedOrigin) return;
1602
- const data = event.data;
1603
- if (data?.type !== "mcp-oauth-complete" || data.server_id !== expectedServerId) return;
1604
- if (!data.success) {
1605
- finish(new Error("MCP authorization failed."));
1606
- return;
1607
- }
1608
- const connectionId = String(data.connection_id || "").trim();
1609
- if (!CONNECTION_ID_PATTERN.test(connectionId)) return;
1610
- finish({ connectionId });
1611
- };
1612
- const timeout = window.setTimeout(
1613
- () => finish(new Error("MCP authorization timed out.")),
1614
- Math.max(1, timeoutMs)
1615
- );
1616
- const closeTimer = window.setInterval(() => {
1617
- if (popup.closed) finish(new Error("MCP authorization window was closed before connecting."));
1618
- }, 500);
1619
- window.addEventListener("message", onMessage);
1620
- });
1621
- }
17
+ } from "./chunk-CHAU7FNW.js";
1622
18
 
1623
19
  // src/renderers.tsx
1624
20
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1670,204 +66,6 @@ var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ jsx(
1670
66
  part.id
1671
67
  );
1672
68
 
1673
- // src/sse.ts
1674
- var isAbortError2 = (error) => {
1675
- if (!error || typeof error !== "object") return false;
1676
- const maybe = error;
1677
- return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
1678
- };
1679
- var parseSseBlock = (block) => {
1680
- const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.replace(/^data:\s?/, "")).join("\n").trim();
1681
- if (!data) return null;
1682
- return JSON.parse(data);
1683
- };
1684
- var consumeSseResponse = async (response, onEvent, options = {}) => {
1685
- if (!response.ok) {
1686
- let message = response.statusText || "Failed to open chat stream.";
1687
- try {
1688
- const errorData = await response.json();
1689
- message = String(errorData.detail || errorData.message || message);
1690
- } catch {
1691
- }
1692
- throw new Error(message);
1693
- }
1694
- const reader = response.body?.getReader();
1695
- if (!reader) throw new Error("The chat stream did not return a readable body.");
1696
- const decoder = new TextDecoder();
1697
- let buffer = "";
1698
- let threadId = response.headers.get("X-Thread-ID") || null;
1699
- let runId = response.headers.get("X-Run-ID") || null;
1700
- let closed = false;
1701
- const closeReader = () => {
1702
- closed = true;
1703
- queueMicrotask(() => {
1704
- void reader.cancel().catch(() => {
1705
- });
1706
- });
1707
- };
1708
- if (options.signal?.aborted) {
1709
- closeReader();
1710
- return { threadId, runId };
1711
- }
1712
- options.signal?.addEventListener("abort", closeReader, { once: true });
1713
- try {
1714
- while (!closed) {
1715
- const { value, done } = await reader.read();
1716
- if (done) break;
1717
- buffer += decoder.decode(value, { stream: true });
1718
- let boundary = buffer.indexOf("\n\n");
1719
- while (boundary !== -1) {
1720
- const block = buffer.slice(0, boundary);
1721
- buffer = buffer.slice(boundary + 2);
1722
- boundary = buffer.indexOf("\n\n");
1723
- const event = parseSseBlock(block);
1724
- if (!event) continue;
1725
- if (event.run_id) runId = event.run_id;
1726
- const payloadThreadId = event.payload?.thread_id;
1727
- if (payloadThreadId) threadId = String(payloadThreadId);
1728
- await onEvent(event);
1729
- }
1730
- }
1731
- } catch (error) {
1732
- if (!isAbortError2(error)) throw error;
1733
- } finally {
1734
- options.signal?.removeEventListener("abort", closeReader);
1735
- }
1736
- return { threadId, runId };
1737
- };
1738
-
1739
- // src/transport.ts
1740
- var jsonHeaders = (headers) => ({
1741
- ...headers || {},
1742
- Accept: "application/json",
1743
- "Content-Type": "application/json"
1744
- });
1745
- var streamHeaders = (headers) => ({
1746
- ...headers || {},
1747
- Accept: "text/event-stream",
1748
- "Content-Type": "application/json"
1749
- });
1750
- var ChatTransportError = class extends Error {
1751
- constructor(message, status) {
1752
- super(message);
1753
- this.name = "ChatTransportError";
1754
- this.status = status;
1755
- }
1756
- };
1757
- var transportError = (response, fallback) => new ChatTransportError(response.statusText || fallback, response.status);
1758
- var createFetchChatTransport = ({
1759
- routes,
1760
- fetchImpl = fetch,
1761
- headers,
1762
- streamBody
1763
- }) => {
1764
- const loadHeaders = async () => headers?.() ?? {};
1765
- return {
1766
- async listThreads(input) {
1767
- const response = await fetchImpl(routes.listThreads(), {
1768
- signal: input?.signal,
1769
- headers: jsonHeaders(await loadHeaders())
1770
- });
1771
- if (!response.ok) throw transportError(response, "Failed to list chat threads.");
1772
- return response.json();
1773
- },
1774
- async getThread(input) {
1775
- const response = await fetchImpl(routes.getThread(input), {
1776
- signal: input.signal,
1777
- headers: jsonHeaders(await loadHeaders())
1778
- });
1779
- if (!response.ok) throw transportError(response, "Failed to load chat thread.");
1780
- return response.json();
1781
- },
1782
- async streamMessage(input, onEvent) {
1783
- const response = await fetchImpl(routes.streamMessage(), {
1784
- method: "POST",
1785
- signal: input.signal,
1786
- headers: streamHeaders(await loadHeaders()),
1787
- body: JSON.stringify(
1788
- streamBody?.(input) ?? {
1789
- input: input.text,
1790
- thread_id: input.threadId || void 0,
1791
- files: input.files || []
1792
- }
1793
- )
1794
- });
1795
- return consumeSseResponse(response, onEvent, { signal: input.signal });
1796
- },
1797
- async attachRun(input, onEvent) {
1798
- const response = await fetchImpl(routes.attachRun(input), {
1799
- method: "POST",
1800
- signal: input.signal,
1801
- headers: streamHeaders(await loadHeaders()),
1802
- body: JSON.stringify({})
1803
- });
1804
- return consumeSseResponse(response, onEvent, { signal: input.signal });
1805
- },
1806
- async cancelRun(input) {
1807
- const response = await fetchImpl(routes.cancelRun(input), {
1808
- method: "POST",
1809
- headers: jsonHeaders(await loadHeaders()),
1810
- body: JSON.stringify({ assistant_output_text: input.assistantOutputText || void 0 })
1811
- });
1812
- if (!response.ok) throw transportError(response, "Failed to cancel chat run.");
1813
- return response.json();
1814
- },
1815
- async deleteThread(input) {
1816
- if (!routes.deleteThread) return { deleted: false };
1817
- const response = await fetchImpl(routes.deleteThread(input), {
1818
- method: "DELETE",
1819
- headers: jsonHeaders(await loadHeaders())
1820
- });
1821
- if (!response.ok) throw transportError(response, "Failed to delete chat thread.");
1822
- return response.json();
1823
- },
1824
- async subscribeThreadEvents(input, onEvent) {
1825
- if (!routes.threadEvents) return;
1826
- const response = await fetchImpl(routes.threadEvents({ cursor: input.cursor }), {
1827
- method: "GET",
1828
- signal: input.signal,
1829
- headers: streamHeaders(await loadHeaders())
1830
- });
1831
- await consumeSseResponse(
1832
- response,
1833
- (event) => onEvent(event),
1834
- { signal: input.signal }
1835
- );
1836
- },
1837
- async resumeHitl(input) {
1838
- if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
1839
- const response = await fetchImpl(routes.resumeRun({ runId: input.runId }), {
1840
- method: "POST",
1841
- headers: jsonHeaders(await loadHeaders()),
1842
- body: JSON.stringify({
1843
- schema_version: "agents24.hitl.resume.v2",
1844
- interrupt_id: input.interruptId,
1845
- action: input.action,
1846
- comment: input.comment,
1847
- client: input.client
1848
- })
1849
- });
1850
- if (!response.ok) throw transportError(response, "Failed to resume chat run.");
1851
- return response.json();
1852
- },
1853
- async startMcpAuth(input) {
1854
- if (!routes.startMcpAuth) throw new ChatTransportError("MCP auth is not configured.", 501);
1855
- const response = await fetchImpl(routes.startMcpAuth({ runId: input.runId, serverId: input.serverId }), {
1856
- method: "POST",
1857
- headers: jsonHeaders(await loadHeaders()),
1858
- body: JSON.stringify({
1859
- interrupt_id: input.interruptId || void 0,
1860
- principal_type: input.principalType || void 0,
1861
- principal_id: input.principalId || void 0,
1862
- client: input.client
1863
- })
1864
- });
1865
- if (!response.ok) throw transportError(response, "Failed to start MCP authorization.");
1866
- return response.json();
1867
- }
1868
- };
1869
- };
1870
-
1871
69
  // src/viewport.ts
1872
70
  import {
1873
71
  MessageScroller,
@@ -1876,8 +74,6 @@ import {
1876
74
  useMessageScrollerVisibility
1877
75
  } from "@shadcn/react/message-scroller";
1878
76
  export {
1879
- ChatTransportError,
1880
- DEFAULT_THREAD_PAGE_SIZE,
1881
77
  DefaultChatPart,
1882
78
  DefaultToolPart,
1883
79
  LatestThreadScroller,
@@ -1889,45 +85,14 @@ export {
1889
85
  LatestThreadScrollerRoot,
1890
86
  LatestThreadScrollerViewport,
1891
87
  MessageScroller,
1892
- activeRunIdFromThread,
1893
- activeRunIdFromThreadDetail,
1894
- appendStableStreamingTurn,
1895
- assistantTextFromParts,
1896
- assistantTextFromResponseBlocks,
1897
- autoAttachRunIdFromThread,
1898
- compressionFromContextWindow,
1899
- consumeSseResponse,
1900
- createChatId,
1901
- createFetchChatTransport,
1902
- findStableAssistantMessageIndex,
1903
88
  getActiveStreamingTextPartId,
1904
- hasStaleUnfinishedAssistantCache,
1905
- hasUnfinishedAssistantMessage,
1906
89
  isActiveStreamingTextPart,
1907
- isAutoAttachThreadStatus,
1908
- isRunningThreadStatus,
1909
- latestContextWindowFromThread,
1910
- mergeContextWindow,
1911
- mergeContextWindowUpdate,
1912
- mergeReasoningSteps,
1913
- mergeStoredThreadsForRefresh,
1914
- normalizeContextCompression,
1915
- normalizeContextWindow,
1916
- parseSseBlock,
1917
- partsFromResponseBlocks,
1918
- reasoningStepsFromParts,
1919
90
  renderChatPart,
1920
- threadActivityDate,
1921
- threadDetailToMessages,
1922
- threadPaging,
1923
- titleFromMessage,
1924
- toolStateFromStatus,
1925
- upsertStableAssistantMessage,
1926
- useAgents24ChatController,
1927
91
  useMessageScroller,
1928
92
  useMessageScrollerScrollable,
1929
93
  useMessageScrollerVisibility,
1930
94
  useStreamingText,
95
+ validateMcpCallbackResult,
1931
96
  waitForMcpOauthComplete
1932
97
  };
1933
98
  //# sourceMappingURL=index.js.map