@agents24/chat-react 0.1.5 → 0.1.7
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.
- package/README.md +4 -0
- package/dist/index.cjs +297 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -26
- package/dist/index.d.ts +90 -26
- package/dist/index.js +291 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,10 @@ The package owns thread hydration, older-page pagination, stream/reattach state,
|
|
|
20
20
|
- `partsFromResponseBlocks(blocks, fallbackText)`
|
|
21
21
|
- `renderChatPart(part, message, renderOptions)`
|
|
22
22
|
|
|
23
|
+
`useAgents24ChatController` exposes reactive `threads`, `activeThreadId`, `activeThread`, `activeRunId`, `isRefreshingThreads`, `loadThreadById(threadId)`, and `startNewThread()` in addition to message and stream state. It hydrates selected threads, detects active runs from `active_run`/`last_run_status`, attaches through the host transport, detaches local SSE streams on navigation, and refreshes history after terminal events.
|
|
24
|
+
|
|
25
|
+
Hosts can pass `onRuntimeEvent(event, context)` and `onThreadDetailLoaded(detail)` to layer product-specific runtime UI, trace panels, analytics, or metadata over the shared chat lifecycle without forking stream handling. That surface is enough for running-history badges and read-only live thread inspectors without a separate host-owned reattach state machine.
|
|
26
|
+
|
|
23
27
|
`ChatMessage.parts` is the only render model. Backend `response_blocks` are normalized into ordered parts such as `text`, `tool-get_meteo`, `ui-blocks`, `reasoning`, `approval`, `error`, and `data`. Tool UI is client-owned through `renderPart`, `toolRenderers`, and `fallbackToolRenderer`; the package does not infer English tool labels or group tool rows.
|
|
24
28
|
|
|
25
29
|
`useStreamingText` owns reusable visual pacing for active assistant text. It returns `displayedText`, `isAnimating`, `mode`, and `parseIncompleteMarkdown` so host renderers can pass those values into their markdown component without the package depending on any markdown/UI library. The package only chooses text timing, cache behavior, reduced-motion handling, and active text-part helpers.
|
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
activeRunIdFromThreadDetail: () => activeRunIdFromThreadDetail,
|
|
29
29
|
assistantTextFromParts: () => assistantTextFromParts,
|
|
30
30
|
assistantTextFromResponseBlocks: () => assistantTextFromResponseBlocks,
|
|
31
|
+
compressionFromContextWindow: () => compressionFromContextWindow,
|
|
31
32
|
consumeSseResponse: () => consumeSseResponse,
|
|
32
33
|
createChatId: () => createChatId,
|
|
33
34
|
createFetchChatTransport: () => createFetchChatTransport,
|
|
@@ -39,9 +40,14 @@ __export(index_exports, {
|
|
|
39
40
|
isAtTimelineLatestEdge: () => isAtTimelineLatestEdge,
|
|
40
41
|
isRunningThreadStatus: () => isRunningThreadStatus,
|
|
41
42
|
isScrollable: () => isScrollable,
|
|
43
|
+
isUserScrollIntentAwayFromLatest: () => isUserScrollIntentAwayFromLatest,
|
|
42
44
|
latestContextWindowFromThread: () => latestContextWindowFromThread,
|
|
45
|
+
mergeContextWindow: () => mergeContextWindow,
|
|
46
|
+
mergeContextWindowUpdate: () => mergeContextWindowUpdate,
|
|
43
47
|
mergeReasoningSteps: () => mergeReasoningSteps,
|
|
44
48
|
nextLatestFollowStateOnScroll: () => nextLatestFollowStateOnScroll,
|
|
49
|
+
normalizeContextCompression: () => normalizeContextCompression,
|
|
50
|
+
normalizeContextWindow: () => normalizeContextWindow,
|
|
45
51
|
parseSseBlock: () => parseSseBlock,
|
|
46
52
|
partsFromResponseBlocks: () => partsFromResponseBlocks,
|
|
47
53
|
reasoningStepsFromParts: () => reasoningStepsFromParts,
|
|
@@ -63,9 +69,128 @@ module.exports = __toCommonJS(index_exports);
|
|
|
63
69
|
// src/controller.ts
|
|
64
70
|
var import_react = require("react");
|
|
65
71
|
|
|
72
|
+
// src/context-window.ts
|
|
73
|
+
var SOURCE_PRIORITY = {
|
|
74
|
+
unknown: 0,
|
|
75
|
+
heuristic_estimate: 1,
|
|
76
|
+
tokenizer_estimate: 1,
|
|
77
|
+
text_estimate: 1,
|
|
78
|
+
estimated: 1,
|
|
79
|
+
multimodal_estimate: 2,
|
|
80
|
+
hf_tokenizer: 3,
|
|
81
|
+
runtime_tokenizer: 3,
|
|
82
|
+
provider_count_api: 4,
|
|
83
|
+
provider_usage: 5,
|
|
84
|
+
exact: 5
|
|
85
|
+
};
|
|
86
|
+
var STAGE_PRIORITY = {
|
|
87
|
+
preflight: 0,
|
|
88
|
+
sent_prompt: 1,
|
|
89
|
+
final_usage: 2
|
|
90
|
+
};
|
|
91
|
+
function numberOrNull(value) {
|
|
92
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
93
|
+
}
|
|
94
|
+
function stagePriority(window2) {
|
|
95
|
+
return STAGE_PRIORITY[window2?.stage || "sent_prompt"] ?? STAGE_PRIORITY.sent_prompt;
|
|
96
|
+
}
|
|
97
|
+
function hasRenderableContextWindow(window2) {
|
|
98
|
+
const maxTokens = window2?.max_tokens;
|
|
99
|
+
return typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0;
|
|
100
|
+
}
|
|
101
|
+
function windowWeight(window2) {
|
|
102
|
+
if (!window2) return [0, 0];
|
|
103
|
+
return [stagePriority(window2), SOURCE_PRIORITY[window2.source] || 0];
|
|
104
|
+
}
|
|
105
|
+
function normalizeContextCompression(value) {
|
|
106
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
107
|
+
const payload = value;
|
|
108
|
+
return {
|
|
109
|
+
active: Boolean(payload.active),
|
|
110
|
+
reason: typeof payload.reason === "string" ? payload.reason : null,
|
|
111
|
+
input_tokens: numberOrNull(payload.input_tokens),
|
|
112
|
+
max_tokens: numberOrNull(payload.max_tokens),
|
|
113
|
+
usage_ratio: numberOrNull(payload.usage_ratio),
|
|
114
|
+
full_frame_count: numberOrNull(payload.full_frame_count),
|
|
115
|
+
compact_frame_count: numberOrNull(payload.compact_frame_count),
|
|
116
|
+
dropped_frame_count: numberOrNull(payload.dropped_frame_count),
|
|
117
|
+
artifact_ref_count: numberOrNull(payload.artifact_ref_count),
|
|
118
|
+
compression_trigger_budget: numberOrNull(payload.compression_trigger_budget),
|
|
119
|
+
compression_target_budget: numberOrNull(payload.compression_target_budget),
|
|
120
|
+
compression_target_ratio: numberOrNull(payload.compression_target_ratio),
|
|
121
|
+
threshold_used: numberOrNull(payload.threshold_used)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function normalizeContextWindow(value) {
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
126
|
+
const payload = value;
|
|
127
|
+
const rawSource = String(payload.source || "").trim();
|
|
128
|
+
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";
|
|
129
|
+
const rawStage = String(payload.stage || "").trim();
|
|
130
|
+
const stage = rawStage === "preflight" || rawStage === "sent_prompt" || rawStage === "final_usage" ? rawStage : "sent_prompt";
|
|
131
|
+
const rawConfidence = String(payload.confidence || "").trim();
|
|
132
|
+
const confidence = rawConfidence === "exact" || rawConfidence === "high" || rawConfidence === "medium" || rawConfidence === "low" || rawConfidence === "unknown" ? rawConfidence : null;
|
|
133
|
+
return {
|
|
134
|
+
source,
|
|
135
|
+
run_id: typeof payload.run_id === "string" ? payload.run_id : null,
|
|
136
|
+
stage,
|
|
137
|
+
confidence,
|
|
138
|
+
counter: typeof payload.counter === "string" ? payload.counter : null,
|
|
139
|
+
model_id: typeof payload.model_id === "string" ? payload.model_id : null,
|
|
140
|
+
max_tokens: numberOrNull(payload.max_tokens),
|
|
141
|
+
max_tokens_source: typeof payload.max_tokens_source === "string" ? payload.max_tokens_source : null,
|
|
142
|
+
input_tokens: numberOrNull(payload.input_tokens),
|
|
143
|
+
remaining_tokens: numberOrNull(payload.remaining_tokens),
|
|
144
|
+
usage_ratio: numberOrNull(payload.usage_ratio),
|
|
145
|
+
assembly: payload.assembly && typeof payload.assembly === "object" && !Array.isArray(payload.assembly) ? payload.assembly : null,
|
|
146
|
+
context_compression: normalizeContextCompression(payload.context_compression)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function mergeContextWindow(current, incoming) {
|
|
150
|
+
if (!incoming) return current ?? null;
|
|
151
|
+
if (!current) return incoming;
|
|
152
|
+
if (hasRenderableContextWindow(current) && !hasRenderableContextWindow(incoming)) return current;
|
|
153
|
+
const currentRunId = current.run_id || null;
|
|
154
|
+
const incomingRunId = incoming.run_id || null;
|
|
155
|
+
if (incomingRunId && currentRunId && incomingRunId !== currentRunId) {
|
|
156
|
+
return stagePriority(incoming) >= STAGE_PRIORITY.sent_prompt ? incoming : current;
|
|
157
|
+
}
|
|
158
|
+
const currentWeight = windowWeight(current);
|
|
159
|
+
const incomingWeight = windowWeight(incoming);
|
|
160
|
+
return incomingWeight[0] > currentWeight[0] || incomingWeight[0] === currentWeight[0] && incomingWeight[1] >= currentWeight[1] ? incoming : current;
|
|
161
|
+
}
|
|
162
|
+
function mergeContextWindowUpdate(current, incoming) {
|
|
163
|
+
return mergeContextWindow(current, normalizeContextWindow(incoming));
|
|
164
|
+
}
|
|
165
|
+
function compressionFromContextWindow(contextWindow) {
|
|
166
|
+
if (!contextWindow) return null;
|
|
167
|
+
if (contextWindow.context_compression) return contextWindow.context_compression;
|
|
168
|
+
const assembly = contextWindow.assembly;
|
|
169
|
+
if (!assembly || typeof assembly !== "object" || Array.isArray(assembly)) return null;
|
|
170
|
+
const artifactRefs = Array.isArray(assembly.artifact_refs) ? assembly.artifact_refs : [];
|
|
171
|
+
const compactFrameCount = numberOrNull(assembly.compact_frame_count) ?? 0;
|
|
172
|
+
const droppedFrameCount = numberOrNull(assembly.dropped_frame_count) ?? 0;
|
|
173
|
+
const reason = typeof assembly.compaction_reason === "string" ? assembly.compaction_reason : "none";
|
|
174
|
+
return {
|
|
175
|
+
active: reason !== "none" || compactFrameCount > 0 || droppedFrameCount > 0,
|
|
176
|
+
reason,
|
|
177
|
+
input_tokens: contextWindow.input_tokens ?? null,
|
|
178
|
+
max_tokens: contextWindow.max_tokens ?? null,
|
|
179
|
+
usage_ratio: contextWindow.usage_ratio ?? null,
|
|
180
|
+
full_frame_count: numberOrNull(assembly.full_frame_count),
|
|
181
|
+
compact_frame_count: compactFrameCount,
|
|
182
|
+
dropped_frame_count: droppedFrameCount,
|
|
183
|
+
artifact_ref_count: artifactRefs.length,
|
|
184
|
+
compression_trigger_budget: numberOrNull(assembly.compression_trigger_budget),
|
|
185
|
+
compression_target_budget: numberOrNull(assembly.compression_target_budget),
|
|
186
|
+
compression_target_ratio: numberOrNull(assembly.compression_target_ratio),
|
|
187
|
+
threshold_used: numberOrNull(assembly.threshold_used)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
66
191
|
// src/model.ts
|
|
67
192
|
var DEFAULT_THREAD_PAGE_SIZE = 5;
|
|
68
|
-
var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running"])).has(String(status || "").toLowerCase());
|
|
193
|
+
var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
|
|
69
194
|
var createChatId = () => {
|
|
70
195
|
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
|
|
71
196
|
return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -104,6 +229,16 @@ var attachmentsFromTurn = (turn) => (turn.attachments || []).map((attachment, in
|
|
|
104
229
|
mediaType: String(record.mime_type || record.mediaType || record.type || "application/octet-stream")
|
|
105
230
|
};
|
|
106
231
|
});
|
|
232
|
+
var tokenUsageFromTurn = (turn) => {
|
|
233
|
+
const usage = turn.run_usage;
|
|
234
|
+
if (!usage || typeof usage !== "object") return void 0;
|
|
235
|
+
return {
|
|
236
|
+
inputTokens: usage.input_tokens ?? null,
|
|
237
|
+
outputTokens: usage.output_tokens ?? null,
|
|
238
|
+
totalTokens: usage.total_tokens ?? null,
|
|
239
|
+
usageSource: usage.source ?? null
|
|
240
|
+
};
|
|
241
|
+
};
|
|
107
242
|
var latestEventPayloadValue = (events, key) => {
|
|
108
243
|
for (let index = (events || []).length - 1; index >= 0; index -= 1) {
|
|
109
244
|
const payload = events?.[index]?.payload;
|
|
@@ -305,7 +440,8 @@ var turnToMessages = (turn, activeRunId) => {
|
|
|
305
440
|
createdAt: turn.completed_at ? new Date(turn.completed_at) : createdAt,
|
|
306
441
|
parts,
|
|
307
442
|
reasoningSteps: mergeReasoningSteps(reasoningStepsFromParts(parts), { finalize: !isRunning }),
|
|
308
|
-
isFinal: !isRunning
|
|
443
|
+
isFinal: !isRunning,
|
|
444
|
+
tokenUsage: tokenUsageFromTurn(turn)
|
|
309
445
|
});
|
|
310
446
|
}
|
|
311
447
|
return messages;
|
|
@@ -322,10 +458,11 @@ var threadPaging = (thread) => {
|
|
|
322
458
|
};
|
|
323
459
|
};
|
|
324
460
|
var latestContextWindowFromThread = (thread) => {
|
|
325
|
-
|
|
461
|
+
const threadContextWindow = normalizeContextWindow(thread.context_window);
|
|
462
|
+
if (threadContextWindow) return threadContextWindow;
|
|
326
463
|
for (let index = (thread.turns || []).length - 1; index >= 0; index -= 1) {
|
|
327
|
-
const
|
|
328
|
-
if (
|
|
464
|
+
const contextWindow = normalizeContextWindow(thread.turns?.[index]?.context_window);
|
|
465
|
+
if (contextWindow) return contextWindow;
|
|
329
466
|
}
|
|
330
467
|
return null;
|
|
331
468
|
};
|
|
@@ -356,10 +493,6 @@ var activeRunIdFromThreadDetail = (thread) => {
|
|
|
356
493
|
};
|
|
357
494
|
|
|
358
495
|
// src/controller.ts
|
|
359
|
-
var mergeContextWindow = (current, incoming) => {
|
|
360
|
-
if (!incoming || typeof incoming !== "object") return current;
|
|
361
|
-
return { ...current || {}, ...incoming };
|
|
362
|
-
};
|
|
363
496
|
var threadSummaryToStored = (thread) => ({
|
|
364
497
|
...thread,
|
|
365
498
|
id: String(thread.id),
|
|
@@ -377,7 +510,9 @@ function useAgents24ChatController({
|
|
|
377
510
|
createId = createChatId,
|
|
378
511
|
onActiveThreadIdChange,
|
|
379
512
|
onSourceClick,
|
|
380
|
-
onStreamErrorMessage
|
|
513
|
+
onStreamErrorMessage,
|
|
514
|
+
onRuntimeEvent,
|
|
515
|
+
onThreadDetailLoaded
|
|
381
516
|
}) {
|
|
382
517
|
const activeThreadId = controlledActiveThreadId ?? storage.getActiveThreadId?.() ?? null;
|
|
383
518
|
const initialCached = activeThreadId ? storage.getThread(activeThreadId) : void 0;
|
|
@@ -394,6 +529,9 @@ function useAgents24ChatController({
|
|
|
394
529
|
const [disliked, setDisliked] = (0, import_react.useState)({});
|
|
395
530
|
const [copiedMessageId, setCopiedMessageId] = (0, import_react.useState)(null);
|
|
396
531
|
const [lastThinkingDurationMs, setLastThinkingDurationMs] = (0, import_react.useState)(null);
|
|
532
|
+
const [threads, setThreads] = (0, import_react.useState)(() => storage.listThreads());
|
|
533
|
+
const [isRefreshingThreads, setIsRefreshingThreads] = (0, import_react.useState)(false);
|
|
534
|
+
const [activeRunId, setActiveRunId] = (0, import_react.useState)(null);
|
|
397
535
|
const textareaRef = (0, import_react.useRef)(null);
|
|
398
536
|
const activeThreadIdRef = (0, import_react.useRef)(activeThreadId);
|
|
399
537
|
const messagesRef = (0, import_react.useRef)(messages);
|
|
@@ -410,6 +548,20 @@ function useAgents24ChatController({
|
|
|
410
548
|
const streamingMessageIdRef = (0, import_react.useRef)(null);
|
|
411
549
|
const reasoningRef = (0, import_react.useRef)([]);
|
|
412
550
|
const liveVoiceIdsRef = (0, import_react.useRef)({});
|
|
551
|
+
const setActiveRunIdValue = (0, import_react.useCallback)((runId) => {
|
|
552
|
+
activeRunIdRef.current = runId;
|
|
553
|
+
setActiveRunId(runId);
|
|
554
|
+
}, []);
|
|
555
|
+
const syncThreadsFromStorage = (0, import_react.useCallback)(() => {
|
|
556
|
+
setThreads(storage.listThreads());
|
|
557
|
+
}, [storage]);
|
|
558
|
+
const upsertStoredThread = (0, import_react.useCallback)(
|
|
559
|
+
(thread) => {
|
|
560
|
+
storage.upsertThread(thread);
|
|
561
|
+
syncThreadsFromStorage();
|
|
562
|
+
},
|
|
563
|
+
[storage, syncThreadsFromStorage]
|
|
564
|
+
);
|
|
413
565
|
(0, import_react.useEffect)(() => {
|
|
414
566
|
messagesRef.current = messages;
|
|
415
567
|
}, [messages]);
|
|
@@ -417,7 +569,7 @@ function useAgents24ChatController({
|
|
|
417
569
|
(threadId, nextMessages, paging, options) => {
|
|
418
570
|
const existing = storage.getThread(threadId);
|
|
419
571
|
const firstUser = nextMessages.find((message) => message.role === "user");
|
|
420
|
-
|
|
572
|
+
upsertStoredThread({
|
|
421
573
|
...existing || {},
|
|
422
574
|
id: threadId,
|
|
423
575
|
title: existing?.title || (firstUser ? titleFromMessage(firstUser.content, firstUser.attachments || []) : "New chat"),
|
|
@@ -428,13 +580,13 @@ function useAgents24ChatController({
|
|
|
428
580
|
nextBeforeTurnIndex: paging?.nextBeforeTurnIndex ?? nextBeforeTurnIndexRef.current
|
|
429
581
|
});
|
|
430
582
|
},
|
|
431
|
-
[storage]
|
|
583
|
+
[storage, upsertStoredThread]
|
|
432
584
|
);
|
|
433
585
|
const markThreadRunStatus = (0, import_react.useCallback)(
|
|
434
586
|
(threadId, runId, status, lastEventSeq) => {
|
|
435
587
|
const existing = storage.getThread(threadId);
|
|
436
588
|
if (!existing || !runId) return;
|
|
437
|
-
|
|
589
|
+
upsertStoredThread({
|
|
438
590
|
...existing,
|
|
439
591
|
last_run_id: runId,
|
|
440
592
|
last_run_status: status,
|
|
@@ -451,14 +603,25 @@ function useAgents24ChatController({
|
|
|
451
603
|
created_at: existing.activeRun?.created_at ?? existing.active_run?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
452
604
|
},
|
|
453
605
|
lastEventSeq: typeof lastEventSeq === "number" ? lastEventSeq : existing.lastEventSeq ?? null,
|
|
454
|
-
isRunning:
|
|
606
|
+
isRunning: activeRunIdFromThread({
|
|
607
|
+
lastRunId: runId,
|
|
608
|
+
lastRunStatus: status,
|
|
609
|
+
activeRun: { run_id: runId, status }
|
|
610
|
+
}) !== null
|
|
455
611
|
});
|
|
456
612
|
},
|
|
457
|
-
[storage]
|
|
613
|
+
[storage, upsertStoredThread]
|
|
458
614
|
);
|
|
459
615
|
const refresh = (0, import_react.useCallback)(async () => {
|
|
460
|
-
|
|
461
|
-
|
|
616
|
+
setIsRefreshingThreads(true);
|
|
617
|
+
try {
|
|
618
|
+
const data = await transport.listThreads();
|
|
619
|
+
const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
|
|
620
|
+
storage.setThreads(nextThreads);
|
|
621
|
+
setThreads(storage.listThreads());
|
|
622
|
+
} finally {
|
|
623
|
+
setIsRefreshingThreads(false);
|
|
624
|
+
}
|
|
462
625
|
}, [storage, transport]);
|
|
463
626
|
const applyThreadId = (0, import_react.useCallback)(
|
|
464
627
|
(threadId, baseMessages) => {
|
|
@@ -486,7 +649,7 @@ function useAgents24ChatController({
|
|
|
486
649
|
const controller = abortControllerRef.current;
|
|
487
650
|
abortControllerRef.current = null;
|
|
488
651
|
controller?.abort();
|
|
489
|
-
|
|
652
|
+
setActiveRunIdValue(null);
|
|
490
653
|
reattachedRunIdRef.current = null;
|
|
491
654
|
streamingMessageIdRef.current = null;
|
|
492
655
|
streamingContentRef.current = "";
|
|
@@ -495,7 +658,7 @@ function useAgents24ChatController({
|
|
|
495
658
|
setIsLoading(false);
|
|
496
659
|
setStreamingContent("");
|
|
497
660
|
setCurrentReasoning([]);
|
|
498
|
-
}, []);
|
|
661
|
+
}, [setActiveRunIdValue]);
|
|
499
662
|
const setLiveAssistantMessage = (0, import_react.useCallback)(
|
|
500
663
|
(input) => {
|
|
501
664
|
setMessages((prev) => {
|
|
@@ -565,7 +728,7 @@ function useAgents24ChatController({
|
|
|
565
728
|
persistThread(input.threadId, completed);
|
|
566
729
|
const existingThread = storage.getThread(input.threadId);
|
|
567
730
|
if (existingThread && input.runId) {
|
|
568
|
-
|
|
731
|
+
upsertStoredThread({
|
|
569
732
|
...existingThread,
|
|
570
733
|
messages: completed,
|
|
571
734
|
last_run_id: input.runId,
|
|
@@ -580,7 +743,7 @@ function useAgents24ChatController({
|
|
|
580
743
|
}
|
|
581
744
|
return completed;
|
|
582
745
|
},
|
|
583
|
-
[createId, persistThread, storage]
|
|
746
|
+
[createId, persistThread, storage, upsertStoredThread]
|
|
584
747
|
);
|
|
585
748
|
const loadThread = (0, import_react.useCallback)(
|
|
586
749
|
async (threadId) => {
|
|
@@ -595,6 +758,7 @@ function useAgents24ChatController({
|
|
|
595
758
|
includeRunEvents: false
|
|
596
759
|
});
|
|
597
760
|
if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
|
|
761
|
+
void onThreadDetailLoaded?.(detail);
|
|
598
762
|
const nextMessages = threadDetailToMessages(detail);
|
|
599
763
|
const paging = threadPaging(detail);
|
|
600
764
|
setContextStatus(latestContextWindowFromThread(detail));
|
|
@@ -604,7 +768,7 @@ function useAgents24ChatController({
|
|
|
604
768
|
hasOlderTurnsRef.current = paging.hasOlderTurns;
|
|
605
769
|
nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
|
|
606
770
|
loadedThreadIdRef.current = threadId;
|
|
607
|
-
|
|
771
|
+
upsertStoredThread({
|
|
608
772
|
...threadSummaryToStored(detail),
|
|
609
773
|
messages: nextMessages,
|
|
610
774
|
isHydrated: true,
|
|
@@ -616,7 +780,7 @@ function useAgents24ChatController({
|
|
|
616
780
|
if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
|
|
617
781
|
}
|
|
618
782
|
},
|
|
619
|
-
[pageSize, setLoadingHistory,
|
|
783
|
+
[onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
|
|
620
784
|
);
|
|
621
785
|
const loadOlderTurns = (0, import_react.useCallback)(async () => {
|
|
622
786
|
const threadId = activeThreadIdRef.current;
|
|
@@ -648,11 +812,18 @@ function useAgents24ChatController({
|
|
|
648
812
|
}, [pageSize, persistThread, transport]);
|
|
649
813
|
const handleStreamEvent = (0, import_react.useCallback)(
|
|
650
814
|
(input) => {
|
|
651
|
-
const { event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
|
|
815
|
+
const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
|
|
652
816
|
const payload = event.payload || {};
|
|
653
817
|
const responseBlocks = Array.isArray(payload.response_blocks) ? payload.response_blocks : null;
|
|
654
|
-
if (event.run_id)
|
|
655
|
-
|
|
818
|
+
if (event.run_id) setActiveRunIdValue(event.run_id);
|
|
819
|
+
void onRuntimeEvent?.(event, {
|
|
820
|
+
mode,
|
|
821
|
+
assistantMessageId,
|
|
822
|
+
threadId: streamThreadIdRef.current,
|
|
823
|
+
runId: event.run_id || activeRunIdRef.current,
|
|
824
|
+
startedAt
|
|
825
|
+
});
|
|
826
|
+
setContextStatus((current) => mergeContextWindowUpdate(current, payload.context_window));
|
|
656
827
|
if (responseBlocks) {
|
|
657
828
|
const blockText = String(payload.assistant_output_text || "") || assistantTextFromResponseBlocks(responseBlocks) || streamingContentRef.current;
|
|
658
829
|
if (blockText) setStreamingText(blockText);
|
|
@@ -698,7 +869,7 @@ function useAgents24ChatController({
|
|
|
698
869
|
error: isFailed ? String(payload.message || payload.error || event.diagnostics?.[0]?.message || onStreamErrorMessage?.(event) || "The chat run failed.") : void 0
|
|
699
870
|
});
|
|
700
871
|
},
|
|
701
|
-
[applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onStreamErrorMessage, setLiveAssistantMessage, setReasoningSteps]
|
|
872
|
+
[applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
|
|
702
873
|
);
|
|
703
874
|
const runStream = (0, import_react.useCallback)(
|
|
704
875
|
async (input) => {
|
|
@@ -760,16 +931,16 @@ function useAgents24ChatController({
|
|
|
760
931
|
}
|
|
761
932
|
try {
|
|
762
933
|
if (input.mode === "attach") {
|
|
763
|
-
|
|
934
|
+
setActiveRunIdValue(input.runId);
|
|
764
935
|
reattachedRunIdRef.current = input.runId;
|
|
765
936
|
await transport.attachRun(
|
|
766
937
|
{ runId: input.runId, signal: controller.signal },
|
|
767
|
-
(event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
938
|
+
(event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
768
939
|
);
|
|
769
940
|
} else {
|
|
770
941
|
await transport.streamMessage(
|
|
771
|
-
{
|
|
772
|
-
(event) => handleStreamEvent({ event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
942
|
+
{ ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
|
|
943
|
+
(event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
773
944
|
);
|
|
774
945
|
}
|
|
775
946
|
if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
|
|
@@ -801,7 +972,7 @@ function useAgents24ChatController({
|
|
|
801
972
|
const isCurrentStream = abortControllerRef.current === controller || streamingMessageIdRef.current === assistantMessageId;
|
|
802
973
|
if (abortControllerRef.current === controller) abortControllerRef.current = null;
|
|
803
974
|
if (isCurrentStream) {
|
|
804
|
-
|
|
975
|
+
setActiveRunIdValue(null);
|
|
805
976
|
streamingMessageIdRef.current = null;
|
|
806
977
|
reattachedRunIdRef.current = null;
|
|
807
978
|
setStreamingMessageId(null);
|
|
@@ -812,7 +983,7 @@ function useAgents24ChatController({
|
|
|
812
983
|
}
|
|
813
984
|
}
|
|
814
985
|
},
|
|
815
|
-
[createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setReasoningSteps, transport]
|
|
986
|
+
[createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
|
|
816
987
|
);
|
|
817
988
|
const handleSubmit = (0, import_react.useCallback)(
|
|
818
989
|
async (message) => {
|
|
@@ -827,7 +998,7 @@ function useAgents24ChatController({
|
|
|
827
998
|
const liveMessageId = streamingMessageIdRef.current;
|
|
828
999
|
abortControllerRef.current?.abort();
|
|
829
1000
|
abortControllerRef.current = null;
|
|
830
|
-
|
|
1001
|
+
setActiveRunIdValue(null);
|
|
831
1002
|
streamingMessageIdRef.current = null;
|
|
832
1003
|
setStreamingMessageId(null);
|
|
833
1004
|
setIsLoading(false);
|
|
@@ -844,10 +1015,16 @@ function useAgents24ChatController({
|
|
|
844
1015
|
messageId: liveMessageId
|
|
845
1016
|
});
|
|
846
1017
|
}
|
|
847
|
-
}, [finalizeAssistantMessage, lastThinkingDurationMs, setReasoningSteps, transport]);
|
|
1018
|
+
}, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
|
|
848
1019
|
(0, import_react.useEffect)(() => {
|
|
849
|
-
refresh().catch(() =>
|
|
1020
|
+
refresh().catch(() => {
|
|
1021
|
+
storage.setThreads([]);
|
|
1022
|
+
setThreads([]);
|
|
1023
|
+
});
|
|
850
1024
|
}, [refresh, storage]);
|
|
1025
|
+
(0, import_react.useEffect)(() => {
|
|
1026
|
+
syncThreadsFromStorage();
|
|
1027
|
+
}, [storageKey, syncThreadsFromStorage]);
|
|
851
1028
|
(0, import_react.useEffect)(() => {
|
|
852
1029
|
const previous = activeThreadIdRef.current;
|
|
853
1030
|
activeThreadIdRef.current = activeThreadId;
|
|
@@ -955,7 +1132,40 @@ function useAgents24ChatController({
|
|
|
955
1132
|
return next;
|
|
956
1133
|
});
|
|
957
1134
|
}, [createId, persistThread]);
|
|
1135
|
+
const startNewThread = (0, import_react.useCallback)(() => {
|
|
1136
|
+
detachActiveStream();
|
|
1137
|
+
requestSeqRef.current += 1;
|
|
1138
|
+
activeThreadIdRef.current = null;
|
|
1139
|
+
loadedThreadIdRef.current = null;
|
|
1140
|
+
nextBeforeTurnIndexRef.current = null;
|
|
1141
|
+
hasOlderTurnsRef.current = false;
|
|
1142
|
+
storage.setActiveThreadId?.(null);
|
|
1143
|
+
onActiveThreadIdChange?.(null);
|
|
1144
|
+
setMessages([]);
|
|
1145
|
+
messagesRef.current = [];
|
|
1146
|
+
setHasOlderTurns(false);
|
|
1147
|
+
setIsLoadingOlder(false);
|
|
1148
|
+
setContextStatus(null);
|
|
1149
|
+
setLoadingHistory(false);
|
|
1150
|
+
}, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
|
|
1151
|
+
const loadThreadById = (0, import_react.useCallback)(
|
|
1152
|
+
async (threadId) => {
|
|
1153
|
+
if (!threadId) return;
|
|
1154
|
+
if (activeThreadIdRef.current !== threadId) {
|
|
1155
|
+
if (abortControllerRef.current) detachActiveStream();
|
|
1156
|
+
activeThreadIdRef.current = threadId;
|
|
1157
|
+
storage.setActiveThreadId?.(threadId);
|
|
1158
|
+
onActiveThreadIdChange?.(threadId);
|
|
1159
|
+
}
|
|
1160
|
+
await loadThread(threadId);
|
|
1161
|
+
},
|
|
1162
|
+
[detachActiveStream, loadThread, onActiveThreadIdChange, storage]
|
|
1163
|
+
);
|
|
1164
|
+
const activeThread = activeThreadId ? storage.getThread(activeThreadId) || threads.find((thread) => thread.id === activeThreadId) || null : null;
|
|
958
1165
|
return (0, import_react.useMemo)(() => ({
|
|
1166
|
+
threads,
|
|
1167
|
+
activeThreadId,
|
|
1168
|
+
activeThread,
|
|
959
1169
|
messages,
|
|
960
1170
|
streamingContent,
|
|
961
1171
|
streamingMessageId,
|
|
@@ -964,12 +1174,13 @@ function useAgents24ChatController({
|
|
|
964
1174
|
isLoading,
|
|
965
1175
|
isLoadingHistory,
|
|
966
1176
|
isLoadingOlder,
|
|
1177
|
+
isRefreshingThreads,
|
|
967
1178
|
hasOlderTurns,
|
|
968
1179
|
liked,
|
|
969
1180
|
disliked,
|
|
970
1181
|
copiedMessageId,
|
|
971
1182
|
lastThinkingDurationMs,
|
|
972
|
-
activeRunId
|
|
1183
|
+
activeRunId,
|
|
973
1184
|
handleSubmit,
|
|
974
1185
|
handleStop,
|
|
975
1186
|
handleCopy,
|
|
@@ -978,11 +1189,16 @@ function useAgents24ChatController({
|
|
|
978
1189
|
handleRetry,
|
|
979
1190
|
handleSourceClick: (citations) => onSourceClick?.(citations),
|
|
980
1191
|
upsertLiveVoiceMessage,
|
|
1192
|
+
startNewThread,
|
|
1193
|
+
loadThreadById,
|
|
981
1194
|
loadOlderTurns,
|
|
982
1195
|
refresh,
|
|
983
1196
|
textareaRef
|
|
984
1197
|
}), [
|
|
985
1198
|
copiedMessageId,
|
|
1199
|
+
activeRunId,
|
|
1200
|
+
activeThread,
|
|
1201
|
+
activeThreadId,
|
|
986
1202
|
contextStatus,
|
|
987
1203
|
currentReasoning,
|
|
988
1204
|
disliked,
|
|
@@ -996,14 +1212,18 @@ function useAgents24ChatController({
|
|
|
996
1212
|
isLoading,
|
|
997
1213
|
isLoadingHistory,
|
|
998
1214
|
isLoadingOlder,
|
|
1215
|
+
isRefreshingThreads,
|
|
999
1216
|
lastThinkingDurationMs,
|
|
1000
1217
|
liked,
|
|
1218
|
+
loadThreadById,
|
|
1001
1219
|
loadOlderTurns,
|
|
1002
1220
|
messages,
|
|
1003
1221
|
onSourceClick,
|
|
1004
1222
|
refresh,
|
|
1005
1223
|
streamingContent,
|
|
1006
1224
|
streamingMessageId,
|
|
1225
|
+
startNewThread,
|
|
1226
|
+
threads,
|
|
1007
1227
|
upsertLiveVoiceMessage
|
|
1008
1228
|
]);
|
|
1009
1229
|
}
|
|
@@ -1337,6 +1557,18 @@ var nextLatestFollowStateOnScroll = ({
|
|
|
1337
1557
|
if (isProgrammatic) return current;
|
|
1338
1558
|
return atLatest ? "following" : "detached";
|
|
1339
1559
|
};
|
|
1560
|
+
var isUserScrollIntentAwayFromLatest = (intent) => {
|
|
1561
|
+
if (!intent) return false;
|
|
1562
|
+
if ("deltaY" in intent && typeof intent.deltaY === "number") {
|
|
1563
|
+
return intent.deltaY < 0;
|
|
1564
|
+
}
|
|
1565
|
+
if ("key" in intent && typeof intent.key === "string") {
|
|
1566
|
+
if (intent.key === "ArrowUp" || intent.key === "PageUp" || intent.key === "Home") return true;
|
|
1567
|
+
if ((intent.key === " " || intent.key === "Spacebar") && intent.shiftKey) return true;
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
return "touches" in intent;
|
|
1571
|
+
};
|
|
1340
1572
|
function useLatestThreadViewport({
|
|
1341
1573
|
itemCount,
|
|
1342
1574
|
hasOlder,
|
|
@@ -1386,10 +1618,11 @@ function useLatestThreadViewport({
|
|
|
1386
1618
|
const reattachToLatest = (0, import_react3.useCallback)(() => {
|
|
1387
1619
|
scrollToLatest({ reattach: true });
|
|
1388
1620
|
}, [scrollToLatest]);
|
|
1389
|
-
const handleUserScrollIntent = (0, import_react3.useCallback)(() => {
|
|
1621
|
+
const handleUserScrollIntent = (0, import_react3.useCallback)((intent) => {
|
|
1390
1622
|
const element = scrollContainerRef.current;
|
|
1391
1623
|
if (!activeStreamKey || !element) return;
|
|
1392
|
-
|
|
1624
|
+
const atLatest = isAtTimelineLatestEdge(element, isTopOrigin);
|
|
1625
|
+
if (!atLatest || isUserScrollIntentAwayFromLatest(intent)) detachFromLatest();
|
|
1393
1626
|
}, [activeStreamKey, detachFromLatest, isTopOrigin]);
|
|
1394
1627
|
(0, import_react3.useEffect)(() => {
|
|
1395
1628
|
return () => {
|
|
@@ -1473,9 +1706,22 @@ function useLatestThreadViewport({
|
|
|
1473
1706
|
};
|
|
1474
1707
|
intrinsicResizePreserveRef.current.frame = requestAnimationFrame(preserveFrame);
|
|
1475
1708
|
}, [isTopOrigin, markProgrammaticScroll]);
|
|
1476
|
-
(0, import_react3.
|
|
1709
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
1477
1710
|
const key = activeStreamKey || null;
|
|
1478
|
-
|
|
1711
|
+
const previousKey = activeStreamKeyRef.current;
|
|
1712
|
+
if (!key) {
|
|
1713
|
+
activeStreamKeyRef.current = null;
|
|
1714
|
+
if (previousKey && shouldAutoFollow && followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1715
|
+
const frame2 = requestAnimationFrame(() => {
|
|
1716
|
+
if (followStateRef.current === "following" && !isLoadingOlder && !olderPageRequestInFlightRef.current) {
|
|
1717
|
+
scrollToLatest({ reattach: false });
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
return () => cancelAnimationFrame(frame2);
|
|
1721
|
+
}
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (previousKey === key) {
|
|
1479
1725
|
activeStreamKeyRef.current = key;
|
|
1480
1726
|
return;
|
|
1481
1727
|
}
|
|
@@ -1483,7 +1729,7 @@ function useLatestThreadViewport({
|
|
|
1483
1729
|
const frame = requestAnimationFrame(() => scrollToLatest());
|
|
1484
1730
|
activeStreamKeyRef.current = key;
|
|
1485
1731
|
return () => cancelAnimationFrame(frame);
|
|
1486
|
-
}, [activeStreamKey, scrollToLatest, setLatestFollowState]);
|
|
1732
|
+
}, [activeStreamKey, isLoadingOlder, scrollToLatest, setLatestFollowState, shouldAutoFollow]);
|
|
1487
1733
|
(0, import_react3.useLayoutEffect)(() => {
|
|
1488
1734
|
if (!activeStreamKey || !shouldAutoFollow || followStateRef.current !== "following") return;
|
|
1489
1735
|
if (isLoadingOlder || olderPageRequestInFlightRef.current) return;
|
|
@@ -1554,10 +1800,10 @@ function LatestThreadViewport({
|
|
|
1554
1800
|
{
|
|
1555
1801
|
ref: viewport.scrollContainerRef,
|
|
1556
1802
|
className,
|
|
1557
|
-
|
|
1803
|
+
onKeyDownCapture: viewport.handleUserScrollIntent,
|
|
1558
1804
|
onScroll: viewport.handleScroll,
|
|
1559
|
-
|
|
1560
|
-
|
|
1805
|
+
onTouchMoveCapture: viewport.handleUserScrollIntent,
|
|
1806
|
+
onWheelCapture: viewport.handleUserScrollIntent,
|
|
1561
1807
|
role: "log",
|
|
1562
1808
|
style: { overflowAnchor: "none" },
|
|
1563
1809
|
children: [
|
|
@@ -1578,6 +1824,7 @@ function LatestThreadViewport({
|
|
|
1578
1824
|
activeRunIdFromThreadDetail,
|
|
1579
1825
|
assistantTextFromParts,
|
|
1580
1826
|
assistantTextFromResponseBlocks,
|
|
1827
|
+
compressionFromContextWindow,
|
|
1581
1828
|
consumeSseResponse,
|
|
1582
1829
|
createChatId,
|
|
1583
1830
|
createFetchChatTransport,
|
|
@@ -1589,9 +1836,14 @@ function LatestThreadViewport({
|
|
|
1589
1836
|
isAtTimelineLatestEdge,
|
|
1590
1837
|
isRunningThreadStatus,
|
|
1591
1838
|
isScrollable,
|
|
1839
|
+
isUserScrollIntentAwayFromLatest,
|
|
1592
1840
|
latestContextWindowFromThread,
|
|
1841
|
+
mergeContextWindow,
|
|
1842
|
+
mergeContextWindowUpdate,
|
|
1593
1843
|
mergeReasoningSteps,
|
|
1594
1844
|
nextLatestFollowStateOnScroll,
|
|
1845
|
+
normalizeContextCompression,
|
|
1846
|
+
normalizeContextWindow,
|
|
1595
1847
|
parseSseBlock,
|
|
1596
1848
|
partsFromResponseBlocks,
|
|
1597
1849
|
reasoningStepsFromParts,
|