diff --git a/site/src/pages/AgentsPage/AgentEmbedPage.tsx b/site/src/pages/AgentsPage/AgentEmbedPage.tsx index 407901e328..1271fbc1e7 100644 --- a/site/src/pages/AgentsPage/AgentEmbedPage.tsx +++ b/site/src/pages/AgentsPage/AgentEmbedPage.tsx @@ -92,10 +92,7 @@ const AgentEmbedPage: FC = () => { const embedSessionMutation = useMutation( bootstrapChatEmbedSession({ checks: permissionChecks }, queryClient), ); - const latestEmbedSessionMutationRef = useRef(embedSessionMutation); - useEffect(() => { - latestEmbedSessionMutationRef.current = embedSessionMutation; - }); + const inFlightBootstrapRef = useRef | null>(null); const [chatErrorReasons, setChatErrorReasons] = useState< @@ -258,7 +255,7 @@ const AgentEmbedPage: FC = () => { return; } - const bootstrapPromise = latestEmbedSessionMutationRef.current + const bootstrapPromise = embedSessionMutation .mutateAsync(token) .catch(() => undefined) .finally(() => { @@ -277,7 +274,7 @@ const AgentEmbedPage: FC = () => { return () => { window.removeEventListener("message", handleMessage); }; - }, [agentId, isAwaitingBootstrapMessage]); + }, [agentId, isAwaitingBootstrapMessage, embedSessionMutation]); const handleBootstrapRetry = () => { inFlightBootstrapRef.current = null; diff --git a/site/src/pages/AgentsPage/components/AgentDetail/ChatContext.ts b/site/src/pages/AgentsPage/components/AgentDetail/ChatContext.ts index f9bce2ed96..10af5a087b 100644 --- a/site/src/pages/AgentsPage/components/AgentDetail/ChatContext.ts +++ b/site/src/pages/AgentsPage/components/AgentDetail/ChatContext.ts @@ -4,6 +4,7 @@ import { watchChat } from "#/api/api"; import { chatMessagesKey, updateInfiniteChatsCache } from "#/api/queries/chats"; import type * as TypesGen from "#/api/typesGenerated"; import { asNumber, asString } from "#/components/ai-elements/runtimeTypeUtils"; +import { useEffectEvent } from "#/hooks/hookPolyfills"; import type { OneWayMessageEvent } from "#/utils/OneWayWebSocket"; import { createReconnectingWebSocket } from "#/utils/reconnectingWebSocket"; import { @@ -686,12 +687,8 @@ export const useChatStore = ( // can call them without including them in its dependency array. // This prevents the socket from tearing down when the parent // re-renders with new callback identities. - const setChatErrorReasonRef = useRef(setChatErrorReason); - const clearChatErrorReasonRef = useRef(clearChatErrorReason); - useEffect(() => { - setChatErrorReasonRef.current = setChatErrorReason; - clearChatErrorReasonRef.current = clearChatErrorReason; - }, [setChatErrorReason, clearChatErrorReason]); + const setChatErrorReasonStable = useEffectEvent(setChatErrorReason); + const clearChatErrorReasonStable = useEffectEvent(clearChatErrorReason); // True once the initial REST page has resolved for the current // chat. The WebSocket effect gates on this so that @@ -1009,7 +1006,7 @@ export const useChatStore = ( store.clearRetryState(); } if (nextStatus !== "error") { - clearChatErrorReasonRef.current(chatID); + clearChatErrorReasonStable(chatID); } updateSidebarChat((chat) => chat.status === nextStatus @@ -1026,7 +1023,7 @@ export const useChatStore = ( store.setChatStatus("error"); store.setStreamError(reason); store.clearRetryState(); - setChatErrorReasonRef.current(chatID, reason); + setChatErrorReasonStable(chatID, reason); updateSidebarChat((chat) => chat.status === "error" ? chat : { ...chat, status: "error" }, ); @@ -1099,7 +1096,14 @@ export const useChatStore = ( } activeChatIDRef.current = null; }; - }, [chatID, initialDataLoaded, queryClient, store]); + }, [ + chatID, + initialDataLoaded, + queryClient, + store, + setChatErrorReasonStable, + clearChatErrorReasonStable, + ]); return { store, clearStreamError: () => { diff --git a/site/src/pages/AgentsPage/components/AgentDetailView.tsx b/site/src/pages/AgentsPage/components/AgentDetailView.tsx index f2f899fd5d..3250416f45 100644 --- a/site/src/pages/AgentsPage/components/AgentDetailView.tsx +++ b/site/src/pages/AgentsPage/components/AgentDetailView.tsx @@ -610,7 +610,6 @@ const ScrollAnchoredContainer: FC<{ const observerRef = useRef(null); const isFetchingRef = useRef(isFetchingMoreMessages); const hasFetchedRef = useRef(false); - const onFetchRef = useRef(onFetchMoreMessages); const autoScrollRef = useRef(true); const contentRef = useRef(null); const pendingPrependRef = useRef<{ @@ -629,8 +628,7 @@ const ScrollAnchoredContainer: FC<{ if (isFetchingMoreMessages) { hasFetchedRef.current = true; } - onFetchRef.current = onFetchMoreMessages; - }, [isFetchingMoreMessages, onFetchMoreMessages]); + }, [isFetchingMoreMessages]); const [showScrollToBottom, setShowScrollToBottom] = useState(false); useEffect(() => { @@ -672,7 +670,7 @@ const ScrollAnchoredContainer: FC<{ contentWidth: contentRect.width, }; } - onFetchRef.current(); + onFetchMoreMessages(); } }, { @@ -700,7 +698,7 @@ const ScrollAnchoredContainer: FC<{ observer.disconnect(); observerRef.current = null; }; - }, [scrollContainerRef]); + }, [scrollContainerRef, onFetchMoreMessages]); // When a fetch completes, re-observe the sentinel to force // the IntersectionObserver to re-evaluate. The observer only diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 86a1b4612f..2559260911 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -84,6 +84,7 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; +import { useEffectEvent } from "#/hooks/hookPolyfills"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { UserDropdownContent } from "#/modules/dashboard/Navbar/UserDropdown/UserDropdownContent"; import { useDashboard } from "#/modules/dashboard/useDashboard"; @@ -1401,14 +1402,9 @@ const LoadMoreSentinel: FC<{ isFetchingNextPage?: boolean; }> = ({ onLoadMore, isFetchingNextPage }) => { const sentinelRef = useRef(null); - const onLoadMoreRef = useRef(onLoadMore); - - // Keep the callback ref in sync so the observer closure - // always calls the latest onLoadMore without needing to - // tear down and re-create the observer. - useEffect(() => { - onLoadMoreRef.current = onLoadMore; - }, [onLoadMore]); + const onLoadMoreStable = useEffectEvent(() => { + onLoadMore?.(); + }); useEffect(() => { // Don't observe while a fetch is in progress. When the @@ -1424,15 +1420,15 @@ const LoadMoreSentinel: FC<{ const observer = new IntersectionObserver( (entries) => { - if (entries[0]?.isIntersecting && onLoadMoreRef.current) { - onLoadMoreRef.current(); + if (entries[0]?.isIntersecting) { + onLoadMoreStable(); } }, { threshold: 0 }, ); observer.observe(el); return () => observer.disconnect(); - }, [isFetchingNextPage]); + }, [isFetchingNextPage, onLoadMoreStable]); return (
diff --git a/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts b/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts index e108c5075d..006ca4dec8 100644 --- a/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts +++ b/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts @@ -122,13 +122,6 @@ export function useDesktopConnection({ ); }, }); - // Stable ref so the effect can call the latest - // syncRemoteClipboardToLocal without listing it as a - // dependency (its identity may change across renders). - const syncClipboardRef = useRef(syncRemoteClipboardToLocal); - useEffect(() => { - syncClipboardRef.current = syncRemoteClipboardToLocal; - }, [syncRemoteClipboardToLocal]); const attach = (container: HTMLElement) => { const screen = offscreenContainerRef.current; @@ -256,7 +249,7 @@ export function useDesktopConnection({ return; } setRemoteClipboardText(text); - syncClipboardRef.current(text).catch((err) => { + syncRemoteClipboardToLocal(text).catch((err) => { console.error("Failed to sync remote clipboard to local:", err); }); }); @@ -500,7 +493,7 @@ export function useDesktopConnection({ setStatus("idle"); setHasConnected(false); }; - }, [chatId]); + }, [chatId, syncRemoteClipboardToLocal]); return { status,