@copilotz/chat-adapter 0.9.22 → 0.9.24

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/dist/index.d.ts CHANGED
@@ -34,6 +34,7 @@ type RestThread = {
34
34
  createdAt?: string;
35
35
  updatedAt?: string;
36
36
  };
37
+ type ThreadActivityStatus = "idle" | "running" | "failed";
37
38
  type RestMessage = {
38
39
  id: string;
39
40
  threadId: string;
@@ -208,6 +209,15 @@ declare function useCopilotz({ userId, userName, userAvatar, assistantName, agen
208
209
  threads: ChatThread[];
209
210
  currentThreadId: string | null;
210
211
  isStreaming: boolean;
212
+ threadActivityStatus: ThreadActivityStatus;
213
+ isRecoveringStream: boolean;
214
+ activityNotice: {
215
+ tone: "info";
216
+ message: string;
217
+ } | {
218
+ tone: "error";
219
+ message: string;
220
+ } | null;
211
221
  specialState: SpecialChatState | null;
212
222
  clearSpecialState: () => void;
213
223
  userContextSeed: Partial<ChatUserContext>;
package/dist/index.js CHANGED
@@ -629,6 +629,9 @@ async function fetchThreadMessagesPage(threadId, options, getRequestHeaders) {
629
629
  if (options?.before) {
630
630
  params.set("before", options.before);
631
631
  }
632
+ if (options?.after) {
633
+ params.set("after", options.after);
634
+ }
632
635
  const res = await fetch(
633
636
  apiUrl(`/v1/threads/${threadId}/messages?${params.toString()}`),
634
637
  {
@@ -1636,6 +1639,61 @@ var createEmptyMessagePageInfo = () => ({
1636
1639
  oldestMessageId: null,
1637
1640
  newestMessageId: null
1638
1641
  });
1642
+ var stringifyForCompare = (value) => {
1643
+ if (value === void 0) return "";
1644
+ try {
1645
+ return JSON.stringify(value) ?? "";
1646
+ } catch {
1647
+ return String(value);
1648
+ }
1649
+ };
1650
+ var getMessageSignature = (message) => JSON.stringify({
1651
+ id: message.id,
1652
+ role: message.role,
1653
+ content: message.content ?? "",
1654
+ isStreaming: message.isStreaming === true,
1655
+ isComplete: message.isComplete === true,
1656
+ attachments: stringifyForCompare(message.attachments),
1657
+ activity: stringifyForCompare(message.activity),
1658
+ metadata: stringifyForCompare(message.metadata),
1659
+ sender: message.sender ? {
1660
+ id: message.sender.id,
1661
+ type: message.sender.type,
1662
+ name: message.sender.name,
1663
+ agentId: message.sender.agentId
1664
+ } : null
1665
+ });
1666
+ var reconcileThreadMessages = (currentMessages, freshMessages) => {
1667
+ if (freshMessages.length === 0) {
1668
+ return { messages: currentMessages, changed: false };
1669
+ }
1670
+ if (currentMessages.length === 0) {
1671
+ return { messages: freshMessages, changed: true };
1672
+ }
1673
+ const freshById = new Map(freshMessages.map((message) => [message.id, message]));
1674
+ const seen = /* @__PURE__ */ new Set();
1675
+ let changed = false;
1676
+ const nextMessages = currentMessages.map((message) => {
1677
+ const fresh = freshById.get(message.id);
1678
+ if (!fresh) return message;
1679
+ seen.add(message.id);
1680
+ if (getMessageSignature(message) === getMessageSignature(fresh)) {
1681
+ return message;
1682
+ }
1683
+ changed = true;
1684
+ return fresh;
1685
+ });
1686
+ const appended = freshMessages.filter((message) => !seen.has(message.id));
1687
+ if (appended.length > 0) {
1688
+ changed = true;
1689
+ nextMessages.push(...appended);
1690
+ nextMessages.sort((a, b) => {
1691
+ const timestampDelta = (a.timestamp ?? 0) - (b.timestamp ?? 0);
1692
+ return timestampDelta !== 0 ? timestampDelta : a.id.localeCompare(b.id);
1693
+ });
1694
+ }
1695
+ return changed ? { messages: nextMessages, changed } : { messages: currentMessages, changed: false };
1696
+ };
1639
1697
  var createPendingAssistantActivity = () => ({
1640
1698
  items: [
1641
1699
  {
@@ -1659,6 +1717,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1659
1717
  const [isLoadingOlderMessages, setIsLoadingOlderMessages] = useState2(false);
1660
1718
  const [messagePageInfo, setMessagePageInfo] = useState2(createEmptyMessagePageInfo);
1661
1719
  const [isStreaming, setIsStreaming] = useState2(false);
1720
+ const [threadActivityStatus, setThreadActivityStatus] = useState2("idle");
1721
+ const [isRecoveringStream, setIsRecoveringStream] = useState2(false);
1662
1722
  const [specialState, setSpecialState] = useState2(null);
1663
1723
  const [userContextSeed, setUserContextSeed] = useState2(initialContext || {});
1664
1724
  const preferredAgentRef = useRef2(preferredAgentName ?? null);
@@ -1893,6 +1953,34 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1893
1953
  },
1894
1954
  [getRequestHeaders, prepareThreadMessages]
1895
1955
  );
1956
+ const refreshThreadMessages = useCallback2(
1957
+ async (threadId) => {
1958
+ const requestId = messagesRequestRef.current;
1959
+ try {
1960
+ const page = await fetchThreadMessagesPage(threadId, { limit: THREAD_MESSAGES_PAGE_SIZE }, getRequestHeaders);
1961
+ const { viewMessages, toolResultUpdates } = await prepareThreadMessages(page.data);
1962
+ if (messagesRequestRef.current !== requestId) return;
1963
+ persistedToolUpdatesRef.current = [
1964
+ ...persistedToolUpdatesRef.current,
1965
+ ...toolResultUpdates.filter((update) => !persistedToolUpdatesRef.current.some((current) => matchesToolResultUpdate(current, update)))
1966
+ ];
1967
+ const hydratedMessages = mergePersistedToolResults(viewMessages, persistedToolUpdatesRef.current);
1968
+ setMessages((prev) => {
1969
+ const reconciled = reconcileThreadMessages(prev, hydratedMessages);
1970
+ return reconciled.changed ? reconciled.messages : prev;
1971
+ });
1972
+ setMessagePageInfo((prev) => ({
1973
+ hasMoreBefore: prev.hasMoreBefore || page.pageInfo.hasMoreBefore,
1974
+ oldestMessageId: prev.oldestMessageId ?? page.pageInfo.oldestMessageId,
1975
+ newestMessageId: page.pageInfo.newestMessageId ?? prev.newestMessageId
1976
+ }));
1977
+ } catch (error) {
1978
+ if (isAbortError(error)) return;
1979
+ console.error(`Error refreshing messages for thread ${threadId}`, error);
1980
+ }
1981
+ },
1982
+ [getRequestHeaders, prepareThreadMessages]
1983
+ );
1896
1984
  const loadOlderMessages = useCallback2(async () => {
1897
1985
  const threadId = currentThreadIdRef.current;
1898
1986
  const pageInfo = messagePageInfoRef.current;
@@ -1918,8 +2006,84 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1918
2006
  }
1919
2007
  }
1920
2008
  }, [getRequestHeaders, prepareThreadMessages]);
2009
+ const finalizeStreamingPlaceholders = useCallback2((nextActivityStatus = "idle") => {
2010
+ setThreadActivityStatus(nextActivityStatus);
2011
+ setIsRecoveringStream(false);
2012
+ setIsStreaming(false);
2013
+ setMessages((prev) => {
2014
+ const hasStreaming = prev.some((msg) => msg.isStreaming);
2015
+ if (!hasStreaming) return prev;
2016
+ return prev.map((msg) => msg.isStreaming ? closeAssistantMessage(msg) : msg);
2017
+ });
2018
+ }, []);
2019
+ const startThreadActivityRecovery = useCallback2(
2020
+ (threadId) => {
2021
+ const generation = ++recoveryPollGenerationRef.current;
2022
+ setThreadActivityStatus("running");
2023
+ setIsRecoveringStream(true);
2024
+ setIsStreaming(true);
2025
+ void (async () => {
2026
+ let delayMs = 2e3;
2027
+ let attempts = 0;
2028
+ await refreshThreadMessages(threadId);
2029
+ while (recoveryPollGenerationRef.current === generation) {
2030
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2031
+ if (recoveryPollGenerationRef.current !== generation) return;
2032
+ if (currentThreadIdRef.current !== threadId) return;
2033
+ try {
2034
+ const activity = await fetchThreadActivity(threadId, getRequestHeaders);
2035
+ setThreadActivityStatus(activity.status);
2036
+ if (activity.status === "running") {
2037
+ attempts += 1;
2038
+ if (attempts % 3 === 0) {
2039
+ await refreshThreadMessages(threadId);
2040
+ }
2041
+ if (attempts >= 15) {
2042
+ delayMs = 5e3;
2043
+ }
2044
+ continue;
2045
+ }
2046
+ await refreshThreadMessages(threadId);
2047
+ finalizeStreamingPlaceholders(activity.status);
2048
+ return;
2049
+ } catch (error) {
2050
+ if (isAbortError(error)) return;
2051
+ attempts += 1;
2052
+ if (attempts >= 15) {
2053
+ delayMs = 5e3;
2054
+ }
2055
+ }
2056
+ }
2057
+ })();
2058
+ },
2059
+ [finalizeStreamingPlaceholders, getRequestHeaders, refreshThreadMessages]
2060
+ );
2061
+ const refreshThreadActivity = useCallback2(
2062
+ async (threadId) => {
2063
+ try {
2064
+ const activity = await fetchThreadActivity(threadId, getRequestHeaders);
2065
+ setThreadActivityStatus(activity.status);
2066
+ if (activity.status === "running") {
2067
+ startThreadActivityRecovery(threadId);
2068
+ return;
2069
+ }
2070
+ setIsRecoveringStream(false);
2071
+ if (activity.status === "failed") {
2072
+ await refreshThreadMessages(threadId);
2073
+ }
2074
+ } catch (error) {
2075
+ if (isAbortError(error)) return;
2076
+ console.warn("Unable to load Copilotz thread activity", error);
2077
+ }
2078
+ },
2079
+ [getRequestHeaders, refreshThreadMessages, startThreadActivityRecovery]
2080
+ );
1921
2081
  const handleSelectThread = useCallback2(
1922
2082
  async (threadId) => {
2083
+ recoveryPollGenerationRef.current += 1;
2084
+ setThreadActivityStatus("idle");
2085
+ setIsRecoveringStream(false);
2086
+ setIsStreaming(false);
1923
2087
  setCurrentThreadId(threadId);
1924
2088
  setMessages([]);
1925
2089
  setMessagePageInfo(createEmptyMessagePageInfo());
@@ -1927,13 +2091,18 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1927
2091
  const extMap = threadExternalIdMapRef.current;
1928
2092
  setCurrentThreadExternalId(extMap[threadId] ?? null);
1929
2093
  await loadThreadMessages(threadId);
2094
+ await refreshThreadActivity(threadId);
1930
2095
  },
1931
- [loadThreadMessages]
2096
+ [loadThreadMessages, refreshThreadActivity]
1932
2097
  );
1933
2098
  const handleCreateThread = useCallback2((title) => {
1934
2099
  messagesRequestRef.current += 1;
1935
2100
  setIsMessagesLoading(false);
1936
2101
  setIsLoadingOlderMessages(false);
2102
+ recoveryPollGenerationRef.current += 1;
2103
+ setThreadActivityStatus("idle");
2104
+ setIsRecoveringStream(false);
2105
+ setIsStreaming(false);
1937
2106
  const id = generateId();
1938
2107
  const now = nowTs();
1939
2108
  const newThread = {
@@ -2090,6 +2259,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2090
2259
  recoveryPollGenerationRef.current += 1;
2091
2260
  abortControllerRef.current?.abort();
2092
2261
  abortControllerRef.current = null;
2262
+ setThreadActivityStatus("idle");
2263
+ setIsRecoveringStream(false);
2093
2264
  setIsStreaming(false);
2094
2265
  setMessages((prev) => {
2095
2266
  const hasStreaming = prev.some((msg) => msg.isStreaming);
@@ -2118,52 +2289,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2118
2289
  )
2119
2290
  );
2120
2291
  }, []);
2121
- const finalizeStreamingPlaceholders = useCallback2(() => {
2122
- setIsStreaming(false);
2123
- setMessages((prev) => {
2124
- const hasStreaming = prev.some((msg) => msg.isStreaming);
2125
- if (!hasStreaming) return prev;
2126
- return prev.map((msg) => msg.isStreaming ? closeAssistantMessage(msg) : msg);
2127
- });
2128
- }, []);
2129
- const startThreadActivityRecovery = useCallback2(
2130
- (threadId) => {
2131
- const generation = ++recoveryPollGenerationRef.current;
2132
- setIsStreaming(true);
2133
- void (async () => {
2134
- let delayMs = 2e3;
2135
- let attempts = 0;
2136
- while (recoveryPollGenerationRef.current === generation) {
2137
- await new Promise((resolve) => setTimeout(resolve, delayMs));
2138
- if (recoveryPollGenerationRef.current !== generation) return;
2139
- if (currentThreadIdRef.current !== threadId) return;
2140
- try {
2141
- const activity = await fetchThreadActivity(threadId, getRequestHeaders);
2142
- if (activity.status === "running") {
2143
- attempts += 1;
2144
- if (attempts % 3 === 0) {
2145
- await loadThreadMessages(threadId);
2146
- }
2147
- if (attempts >= 15) {
2148
- delayMs = 5e3;
2149
- }
2150
- continue;
2151
- }
2152
- await loadThreadMessages(threadId);
2153
- finalizeStreamingPlaceholders();
2154
- return;
2155
- } catch (error) {
2156
- if (isAbortError(error)) return;
2157
- attempts += 1;
2158
- if (attempts >= 15) {
2159
- delayMs = 5e3;
2160
- }
2161
- }
2162
- }
2163
- })();
2164
- },
2165
- [finalizeStreamingPlaceholders, getRequestHeaders, loadThreadMessages]
2166
- );
2167
2292
  const sendCopilotzMessage = useCallback2(
2168
2293
  async (params) => {
2169
2294
  let currentAssistantId = params.assistantMessageId ?? generateId();
@@ -2308,6 +2433,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2308
2433
  abortControllerRef.current = abortController;
2309
2434
  stopRequestedRef.current = false;
2310
2435
  recoveryPollGenerationRef.current += 1;
2436
+ setThreadActivityStatus("running");
2437
+ setIsRecoveringStream(false);
2311
2438
  setIsStreaming(true);
2312
2439
  liveToolUpdatesRef.current = [];
2313
2440
  let streamError = null;
@@ -2493,11 +2620,13 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2493
2620
  const activityThreadId = await resolveActivityThreadId();
2494
2621
  if (activityThreadId) {
2495
2622
  const activity = await fetchThreadActivity(activityThreadId, getRequestHeaders);
2623
+ setThreadActivityStatus(activity.status);
2496
2624
  if (activity.status === "running") {
2497
2625
  recoveryStarted = true;
2498
2626
  startThreadActivityRecovery(activityThreadId);
2499
2627
  } else if (streamError || activity.status === "failed") {
2500
- await loadThreadMessages(activityThreadId);
2628
+ setIsRecoveringStream(false);
2629
+ await refreshThreadMessages(activityThreadId);
2501
2630
  }
2502
2631
  }
2503
2632
  } catch (activityError) {
@@ -2516,7 +2645,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2516
2645
  }
2517
2646
  return currentAssistantId;
2518
2647
  },
2519
- [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, fetchAndSetThreadsState, finalizeStreamingPlaceholders, getRequestHeaders, loadThreadMessages, startThreadActivityRecovery]
2648
+ [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, fetchAndSetThreadsState, finalizeStreamingPlaceholders, getRequestHeaders, refreshThreadMessages, startThreadActivityRecovery]
2520
2649
  );
2521
2650
  const handleSendMessage = useCallback2(
2522
2651
  async (content, attachments = []) => {
@@ -2677,6 +2806,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2677
2806
  ]);
2678
2807
  setMessagePageInfo(createEmptyMessagePageInfo());
2679
2808
  persistedToolUpdatesRef.current = [];
2809
+ setThreadActivityStatus("running");
2810
+ setIsRecoveringStream(false);
2680
2811
  setSpecialState(null);
2681
2812
  try {
2682
2813
  await sendCopilotzMessage({
@@ -2730,6 +2861,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2730
2861
  setIsMessagesLoading(false);
2731
2862
  setIsLoadingOlderMessages(false);
2732
2863
  setIsStreaming(false);
2864
+ setThreadActivityStatus("idle");
2865
+ setIsRecoveringStream(false);
2733
2866
  setMessagePageInfo(createEmptyMessagePageInfo());
2734
2867
  persistedToolUpdatesRef.current = [];
2735
2868
  setSpecialState(null);
@@ -2746,6 +2879,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2746
2879
  const preferredThreadId = await fetchAndSetThreadsState(userId, urlPreferredThread);
2747
2880
  if (preferredThreadId) {
2748
2881
  await loadThreadMessages(preferredThreadId);
2882
+ await refreshThreadActivity(preferredThreadId);
2749
2883
  } else if (bootstrap) {
2750
2884
  await bootstrapConversation(userId);
2751
2885
  }
@@ -2755,7 +2889,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2755
2889
  initializationRef.current = { userId: null, started: false };
2756
2890
  reset();
2757
2891
  }
2758
- }, [userId, fetchAndSetThreadsState, loadThreadMessages, bootstrapConversation, reset, bootstrap, isUrlSyncEnabled]);
2892
+ }, [userId, fetchAndSetThreadsState, loadThreadMessages, refreshThreadActivity, bootstrapConversation, reset, bootstrap, isUrlSyncEnabled]);
2759
2893
  useEffect2(() => {
2760
2894
  if (!isUrlSyncEnabled) return;
2761
2895
  if (!initializationRef.current.started) return;
@@ -2772,6 +2906,13 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2772
2906
  }));
2773
2907
  }
2774
2908
  }, [currentThreadId, threadMetadataMap]);
2909
+ const activityNotice = threadActivityStatus === "running" && isRecoveringStream ? {
2910
+ tone: "info",
2911
+ message: "The agent is still working. Loading updates..."
2912
+ } : threadActivityStatus === "failed" ? {
2913
+ tone: "error",
2914
+ message: "The last run failed. Refreshing the latest messages..."
2915
+ } : null;
2775
2916
  return {
2776
2917
  messages: messages.map(toPublicChatMessage),
2777
2918
  isMessagesLoading,
@@ -2780,6 +2921,9 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2780
2921
  threads,
2781
2922
  currentThreadId,
2782
2923
  isStreaming,
2924
+ threadActivityStatus,
2925
+ isRecoveringStream,
2926
+ activityNotice,
2783
2927
  specialState,
2784
2928
  clearSpecialState,
2785
2929
  userContextSeed,
@@ -2812,7 +2956,7 @@ var CopilotzChat = ({ userId, userName, userAvatar, userEmail, initialContext, b
2812
2956
  if (!targetAgentId) return null;
2813
2957
  return targetAgentId;
2814
2958
  }, [targetAgentId]);
2815
- const { messages, isMessagesLoading, isLoadingOlderMessages, messagePageInfo, threads, currentThreadId, isStreaming, specialState, clearSpecialState, userContextSeed, sendMessage, createThread, selectThread, renameThread, archiveThread, updateThreadTags, editMessage, deleteThread: deleteThread2, stopGeneration, loadOlderMessages } = useCopilotz({
2959
+ const { messages, isMessagesLoading, isLoadingOlderMessages, messagePageInfo, threads, currentThreadId, isStreaming, isRecoveringStream, activityNotice, specialState, clearSpecialState, userContextSeed, sendMessage, createThread, selectThread, renameThread, archiveThread, updateThreadTags, editMessage, deleteThread: deleteThread2, stopGeneration, loadOlderMessages } = useCopilotz({
2816
2960
  userId,
2817
2961
  userName,
2818
2962
  userAvatar,
@@ -2913,7 +3057,7 @@ var CopilotzChat = ({ userId, userName, userAvatar, userEmail, initialContext, b
2913
3057
  [userConfig?.branding?.title, userConfig?.branding?.avatar, userConfig?.branding?.subtitle]
2914
3058
  );
2915
3059
  const specialStateContent = specialState ? renderSpecialState?.(specialState, { clear: clearSpecialState }) : null;
2916
- return /* @__PURE__ */ jsx(ChatUserContextProvider, { initial: userContextSeed, children: specialStateContent ?? /* @__PURE__ */ jsx(ChatUI, { messages, isMessagesLoading, isLoadingOlderMessages, hasMoreMessagesBefore: messagePageInfo.hasMoreBefore, onLoadOlderMessages: loadOlderMessages, threads, currentThreadId, config: mergedConfig, callbacks: chatCallbacks, isGenerating: isStreaming, suggestions, agentOptions, selectedAgentId, onSelectAgent, participantIds, onParticipantsChange, targetAgentId, onTargetAgentChange, user: userProp, assistant: assistantProp, onAddMemory, onUpdateMemory, onDeleteMemory, userMenuSections, userMenuAdditionalItems, className }) });
3060
+ return /* @__PURE__ */ jsx(ChatUserContextProvider, { initial: userContextSeed, children: specialStateContent ?? /* @__PURE__ */ jsx(ChatUI, { messages, isMessagesLoading, isLoadingOlderMessages, hasMoreMessagesBefore: messagePageInfo.hasMoreBefore, activityNotice, isBackgroundRefreshingMessages: isRecoveringStream, onLoadOlderMessages: loadOlderMessages, threads, currentThreadId, config: mergedConfig, callbacks: chatCallbacks, isGenerating: isStreaming, suggestions, agentOptions, selectedAgentId, onSelectAgent, participantIds, onParticipantsChange, targetAgentId, onTargetAgentChange, user: userProp, assistant: assistantProp, onAddMemory, onUpdateMemory, onDeleteMemory, userMenuSections, userMenuAdditionalItems, className }) });
2917
3061
  };
2918
3062
  export {
2919
3063
  CopilotzChat,