diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx
index 8c9f9d528c..fe2f4f3625 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/CopilotShell.tsx
@@ -3,10 +3,7 @@
import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import type { ReactNode } from "react";
-import { useCallback, useEffect } from "react";
-import { useCopilotStore } from "../../copilot-page-store";
import { DesktopSidebar } from "./components/DesktopSidebar/DesktopSidebar";
-import { LoadingState } from "./components/LoadingState/LoadingState";
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
import { MobileHeader } from "./components/MobileHeader/MobileHeader";
import { useCopilotShell } from "./useCopilotShell";
@@ -24,85 +21,16 @@ export function CopilotShell({ children }: Props) {
hasActiveSession,
sessions,
currentSessionId,
- handleSelectSession,
- performSelectSession,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
- handleNewChat,
- performNewChat,
+ handleNewChatClick,
+ handleSessionClick,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
- isReadyToShowContent,
} = useCopilotShell();
- const setNewChatHandler = useCopilotStore((s) => s.setNewChatHandler);
- const setNewChatWithInterruptHandler = useCopilotStore(
- (s) => s.setNewChatWithInterruptHandler,
- );
- const setSelectSessionHandler = useCopilotStore(
- (s) => s.setSelectSessionHandler,
- );
- const setSelectSessionWithInterruptHandler = useCopilotStore(
- (s) => s.setSelectSessionWithInterruptHandler,
- );
- const requestNewChat = useCopilotStore((s) => s.requestNewChat);
- const requestSelectSession = useCopilotStore((s) => s.requestSelectSession);
-
- const stableHandleNewChat = useCallback(handleNewChat, [handleNewChat]);
- const stablePerformNewChat = useCallback(performNewChat, [performNewChat]);
-
- useEffect(
- function registerNewChatHandlers() {
- setNewChatHandler(stableHandleNewChat);
- setNewChatWithInterruptHandler(stablePerformNewChat);
- return function cleanup() {
- setNewChatHandler(null);
- setNewChatWithInterruptHandler(null);
- };
- },
- [
- stableHandleNewChat,
- stablePerformNewChat,
- setNewChatHandler,
- setNewChatWithInterruptHandler,
- ],
- );
-
- const stableHandleSelectSession = useCallback(handleSelectSession, [
- handleSelectSession,
- ]);
-
- const stablePerformSelectSession = useCallback(performSelectSession, [
- performSelectSession,
- ]);
-
- useEffect(
- function registerSelectSessionHandlers() {
- setSelectSessionHandler(stableHandleSelectSession);
- setSelectSessionWithInterruptHandler(stablePerformSelectSession);
- return function cleanup() {
- setSelectSessionHandler(null);
- setSelectSessionWithInterruptHandler(null);
- };
- },
- [
- stableHandleSelectSession,
- stablePerformSelectSession,
- setSelectSessionHandler,
- setSelectSessionWithInterruptHandler,
- ],
- );
-
- function handleNewChatClick() {
- requestNewChat();
- }
-
- function handleSessionClick(sessionId: string) {
- requestSelectSession(sessionId);
- }
-
if (!isLoggedIn) {
return (
@@ -132,9 +60,7 @@ export function CopilotShell({ children }: Props) {
{isMobile &&
}
-
- {isReadyToShowContent ? children : }
-
+
{children}
{isMobile && (
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/LoadingState/LoadingState.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/LoadingState/LoadingState.tsx
deleted file mode 100644
index 21b1663916..0000000000
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/LoadingState/LoadingState.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
-
-export function LoadingState() {
- return (
-
-
-
-
- Loading your chats...
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts
index c9504e49a9..2ef63a4422 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/MobileDrawer/useMobileDrawer.ts
@@ -3,17 +3,17 @@ import { useState } from "react";
export function useMobileDrawer() {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
- function handleOpenDrawer() {
+ const handleOpenDrawer = () => {
setIsDrawerOpen(true);
- }
+ };
- function handleCloseDrawer() {
+ const handleCloseDrawer = () => {
setIsDrawerOpen(false);
- }
+ };
- function handleDrawerOpenChange(open: boolean) {
+ const handleDrawerOpenChange = (open: boolean) => {
setIsDrawerOpen(open);
- }
+ };
return {
isDrawerOpen,
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts
index 1f241f992a..11ddd937af 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts
@@ -1,11 +1,6 @@
-import {
- getGetV2ListSessionsQueryKey,
- useGetV2ListSessions,
-} from "@/app/api/__generated__/endpoints/chat/chat";
+import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse";
import { okData } from "@/app/api/helpers";
-import { useChatStore } from "@/components/contextual/Chat/chat-store";
-import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
const PAGE_SIZE = 50;
@@ -16,12 +11,12 @@ export interface UseSessionsPaginationArgs {
export function useSessionsPagination({ enabled }: UseSessionsPaginationArgs) {
const [offset, setOffset] = useState(0);
+
const [accumulatedSessions, setAccumulatedSessions] = useState<
SessionSummaryResponse[]
>([]);
+
const [totalCount, setTotalCount] = useState
(null);
- const queryClient = useQueryClient();
- const onStreamComplete = useChatStore((state) => state.onStreamComplete);
const { data, isLoading, isFetching, isError } = useGetV2ListSessions(
{ limit: PAGE_SIZE, offset },
@@ -32,38 +27,23 @@ export function useSessionsPagination({ enabled }: UseSessionsPaginationArgs) {
},
);
- useEffect(function refreshOnStreamComplete() {
- const unsubscribe = onStreamComplete(function handleStreamComplete() {
- setOffset(0);
+ useEffect(() => {
+ const responseData = okData(data);
+ if (responseData) {
+ const newSessions = responseData.sessions;
+ const total = responseData.total;
+ setTotalCount(total);
+
+ if (offset === 0) {
+ setAccumulatedSessions(newSessions);
+ } else {
+ setAccumulatedSessions((prev) => [...prev, ...newSessions]);
+ }
+ } else if (!enabled) {
setAccumulatedSessions([]);
setTotalCount(null);
- queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
- });
- return unsubscribe;
- }, []);
-
- useEffect(
- function updateSessionsFromResponse() {
- const responseData = okData(data);
- if (responseData) {
- const newSessions = responseData.sessions;
- const total = responseData.total;
- setTotalCount(total);
-
- if (offset === 0) {
- setAccumulatedSessions(newSessions);
- } else {
- setAccumulatedSessions((prev) => [...prev, ...newSessions]);
- }
- } else if (!enabled) {
- setAccumulatedSessions([]);
- setTotalCount(null);
- }
- },
- [data, offset, enabled],
- );
+ }
+ }, [data, offset, enabled]);
const hasNextPage =
totalCount !== null && accumulatedSessions.length < totalCount;
@@ -86,17 +66,17 @@ export function useSessionsPagination({ enabled }: UseSessionsPaginationArgs) {
}
}, [hasNextPage, isFetching, isLoading, isError, totalCount]);
- function fetchNextPage() {
+ const fetchNextPage = () => {
if (hasNextPage && !isFetching) {
setOffset((prev) => prev + PAGE_SIZE);
}
- }
+ };
- function reset() {
+ const reset = () => {
setOffset(0);
setAccumulatedSessions([]);
setTotalCount(null);
- }
+ };
return {
sessions: accumulatedSessions,
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts
index 3e932848a0..ef0d414edf 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/helpers.ts
@@ -104,76 +104,3 @@ export function mergeCurrentSessionIntoList(
export function getCurrentSessionId(searchParams: URLSearchParams) {
return searchParams.get("sessionId");
}
-
-export function shouldAutoSelectSession(
- areAllSessionsLoaded: boolean,
- hasAutoSelectedSession: boolean,
- paramSessionId: string | null,
- visibleSessions: SessionSummaryResponse[],
- accumulatedSessions: SessionSummaryResponse[],
- isLoading: boolean,
- totalCount: number | null,
-) {
- if (!areAllSessionsLoaded || hasAutoSelectedSession) {
- return {
- shouldSelect: false,
- sessionIdToSelect: null,
- shouldCreate: false,
- };
- }
-
- if (paramSessionId) {
- return {
- shouldSelect: false,
- sessionIdToSelect: null,
- shouldCreate: false,
- };
- }
-
- if (visibleSessions.length > 0) {
- return {
- shouldSelect: true,
- sessionIdToSelect: visibleSessions[0].id,
- shouldCreate: false,
- };
- }
-
- if (accumulatedSessions.length === 0 && !isLoading && totalCount === 0) {
- return { shouldSelect: false, sessionIdToSelect: null, shouldCreate: true };
- }
-
- if (totalCount === 0) {
- return {
- shouldSelect: false,
- sessionIdToSelect: null,
- shouldCreate: false,
- };
- }
-
- return { shouldSelect: false, sessionIdToSelect: null, shouldCreate: false };
-}
-
-export function checkReadyToShowContent(
- areAllSessionsLoaded: boolean,
- paramSessionId: string | null,
- accumulatedSessions: SessionSummaryResponse[],
- isCurrentSessionLoading: boolean,
- currentSessionData: SessionDetailResponse | null | undefined,
- hasAutoSelectedSession: boolean,
-) {
- if (!areAllSessionsLoaded) return false;
-
- if (paramSessionId) {
- const sessionFound = accumulatedSessions.some(
- (s) => s.id === paramSessionId,
- );
- return (
- sessionFound ||
- (!isCurrentSessionLoading &&
- currentSessionData !== undefined &&
- currentSessionData !== null)
- );
- }
-
- return hasAutoSelectedSession;
-}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts
index 3154df2975..8429b75b2b 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useCopilotShell.ts
@@ -5,25 +5,18 @@ import {
getGetV2ListSessionsQueryKey,
useGetV2GetSession,
} from "@/app/api/__generated__/endpoints/chat/chat";
-import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse";
import { okData } from "@/app/api/helpers";
import { useChatStore } from "@/components/contextual/Chat/chat-store";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useQueryClient } from "@tanstack/react-query";
-import { parseAsString, useQueryState } from "nuqs";
import { usePathname, useSearchParams } from "next/navigation";
-import { useEffect, useRef, useState } from "react";
+import { useRef } from "react";
import { useCopilotStore } from "../../copilot-page-store";
+import { useCopilotSessionId } from "../../useCopilotSessionId";
import { useMobileDrawer } from "./components/MobileDrawer/useMobileDrawer";
-import { useSessionsPagination } from "./components/SessionsList/useSessionsPagination";
-import {
- checkReadyToShowContent,
- convertSessionDetailToSummary,
- filterVisibleSessions,
- getCurrentSessionId,
- mergeCurrentSessionIntoList,
-} from "./helpers";
+import { getCurrentSessionId } from "./helpers";
+import { useShellSessionList } from "./useShellSessionList";
export function useCopilotShell() {
const pathname = usePathname();
@@ -34,7 +27,7 @@ export function useCopilotShell() {
const isMobile =
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
- const [, setUrlSessionId] = useQueryState("sessionId", parseAsString);
+ const { urlSessionId, setUrlSessionId } = useCopilotSessionId();
const isOnHomepage = pathname === "/copilot";
const paramSessionId = searchParams.get("sessionId");
@@ -48,235 +41,113 @@ export function useCopilotShell() {
const paginationEnabled = !isMobile || isDrawerOpen || !!paramSessionId;
- const {
- sessions: accumulatedSessions,
- isLoading: isSessionsLoading,
- isFetching: isSessionsFetching,
- hasNextPage,
- areAllSessionsLoaded,
- fetchNextPage,
- reset: resetPagination,
- } = useSessionsPagination({
- enabled: paginationEnabled,
- });
-
const currentSessionId = getCurrentSessionId(searchParams);
- const { data: currentSessionData, isLoading: isCurrentSessionLoading } =
- useGetV2GetSession(currentSessionId || "", {
+ const { data: currentSessionData } = useGetV2GetSession(
+ currentSessionId || "",
+ {
query: {
enabled: !!currentSessionId,
select: okData,
},
- });
-
- const [hasAutoSelectedSession, setHasAutoSelectedSession] = useState(false);
- const hasAutoSelectedRef = useRef(false);
- const recentlyCreatedSessionsRef = useRef<
- Map
- >(new Map());
-
- const [optimisticSessionId, setOptimisticSessionId] = useState(
- null,
- );
-
- useEffect(
- function clearOptimisticWhenUrlMatches() {
- if (optimisticSessionId && currentSessionId === optimisticSessionId) {
- setOptimisticSessionId(null);
- }
},
- [currentSessionId, optimisticSessionId],
);
- // Mark as auto-selected when sessionId is in URL
- useEffect(() => {
- if (paramSessionId && !hasAutoSelectedRef.current) {
- hasAutoSelectedRef.current = true;
- setHasAutoSelectedSession(true);
- }
- }, [paramSessionId]);
-
- // On homepage without sessionId, mark as ready immediately
- useEffect(() => {
- if (isOnHomepage && !paramSessionId && !hasAutoSelectedRef.current) {
- hasAutoSelectedRef.current = true;
- setHasAutoSelectedSession(true);
- }
- }, [isOnHomepage, paramSessionId]);
-
- // Invalidate sessions list when navigating to homepage (to show newly created sessions)
- useEffect(() => {
- if (isOnHomepage && !paramSessionId) {
- queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
- }
- }, [isOnHomepage, paramSessionId, queryClient]);
-
- // Track newly created sessions to ensure they stay visible even when switching away
- useEffect(() => {
- if (currentSessionId && currentSessionData) {
- const isNewSession =
- currentSessionData.updated_at === currentSessionData.created_at;
- const isNotInAccumulated = !accumulatedSessions.some(
- (s) => s.id === currentSessionId,
- );
- if (isNewSession || isNotInAccumulated) {
- const summary = convertSessionDetailToSummary(currentSessionData);
- recentlyCreatedSessionsRef.current.set(currentSessionId, summary);
- }
- }
- }, [currentSessionId, currentSessionData, accumulatedSessions]);
-
- // Clean up recently created sessions that are now in the accumulated list
- useEffect(() => {
- for (const sessionId of recentlyCreatedSessionsRef.current.keys()) {
- if (accumulatedSessions.some((s) => s.id === sessionId)) {
- recentlyCreatedSessionsRef.current.delete(sessionId);
- }
- }
- }, [accumulatedSessions]);
-
- // Reset pagination when query becomes disabled
- const prevPaginationEnabledRef = useRef(paginationEnabled);
- useEffect(() => {
- if (prevPaginationEnabledRef.current && !paginationEnabled) {
- resetPagination();
- resetAutoSelect();
- }
- prevPaginationEnabledRef.current = paginationEnabled;
- }, [paginationEnabled, resetPagination]);
-
- const sessions = mergeCurrentSessionIntoList(
- accumulatedSessions,
+ const {
+ sessions,
+ isLoading,
+ isSessionsFetching,
+ hasNextPage,
+ fetchNextPage,
+ resetPagination,
+ recentlyCreatedSessionsRef,
+ } = useShellSessionList({
+ paginationEnabled,
currentSessionId,
currentSessionData,
- recentlyCreatedSessionsRef.current,
- );
-
- const visibleSessions = filterVisibleSessions(sessions);
-
- const sidebarSelectedSessionId =
- isOnHomepage && !paramSessionId && !optimisticSessionId
- ? null
- : optimisticSessionId || currentSessionId;
-
- const isReadyToShowContent = isOnHomepage
- ? true
- : checkReadyToShowContent(
- areAllSessionsLoaded,
- paramSessionId,
- accumulatedSessions,
- isCurrentSessionLoading,
- currentSessionData,
- hasAutoSelectedSession,
- );
+ isOnHomepage,
+ paramSessionId,
+ });
const stopStream = useChatStore((s) => s.stopStream);
const onStreamComplete = useChatStore((s) => s.onStreamComplete);
+ const isStreaming = useCopilotStore((s) => s.isStreaming);
const setIsSwitchingSession = useCopilotStore((s) => s.setIsSwitchingSession);
+ const openInterruptModal = useCopilotStore((s) => s.openInterruptModal);
- async function performSelectSession(sessionId: string) {
- if (sessionId === currentSessionId) return;
+ const pendingActionRef = useRef<(() => void) | null>(null);
- const sourceSessionId = currentSessionId;
+ async function stopCurrentStream() {
+ if (!currentSessionId) return;
- if (sourceSessionId) {
- setIsSwitchingSession(true);
-
- await new Promise(function waitForStreamComplete(resolve) {
- const unsubscribe = onStreamComplete(
- function handleComplete(completedId) {
- if (completedId === sourceSessionId) {
- clearTimeout(timeout);
- unsubscribe();
- resolve();
- }
- },
- );
- const timeout = setTimeout(function handleTimeout() {
+ setIsSwitchingSession(true);
+ await new Promise((resolve) => {
+ const unsubscribe = onStreamComplete((completedId) => {
+ if (completedId === currentSessionId) {
+ clearTimeout(timeout);
unsubscribe();
resolve();
- }, 3000);
- stopStream(sourceSessionId);
+ }
});
+ const timeout = setTimeout(() => {
+ unsubscribe();
+ resolve();
+ }, 3000);
+ stopStream(currentSessionId);
+ });
- queryClient.invalidateQueries({
- queryKey: getGetV2GetSessionQueryKey(sourceSessionId),
- });
- }
-
- setOptimisticSessionId(sessionId);
- setUrlSessionId(sessionId, { shallow: false });
+ queryClient.invalidateQueries({
+ queryKey: getGetV2GetSessionQueryKey(currentSessionId),
+ });
setIsSwitchingSession(false);
- if (isMobile) handleCloseDrawer();
}
- function handleSelectSession(sessionId: string) {
+ function selectSession(sessionId: string) {
if (sessionId === currentSessionId) return;
- setOptimisticSessionId(sessionId);
+ if (recentlyCreatedSessionsRef.current.has(sessionId)) {
+ queryClient.invalidateQueries({
+ queryKey: getGetV2GetSessionQueryKey(sessionId),
+ });
+ }
setUrlSessionId(sessionId, { shallow: false });
if (isMobile) handleCloseDrawer();
}
- async function performNewChat() {
- const sourceSessionId = currentSessionId;
+ function startNewChat() {
+ resetPagination();
+ queryClient.invalidateQueries({
+ queryKey: getGetV2ListSessionsQueryKey(),
+ });
+ setUrlSessionId(null, { shallow: false });
+ if (isMobile) handleCloseDrawer();
+ }
- if (sourceSessionId) {
- setIsSwitchingSession(true);
+ function handleSessionClick(sessionId: string) {
+ if (sessionId === currentSessionId) return;
- await new Promise(function waitForStreamComplete(resolve) {
- const unsubscribe = onStreamComplete(
- function handleComplete(completedId) {
- if (completedId === sourceSessionId) {
- clearTimeout(timeout);
- unsubscribe();
- resolve();
- }
- },
- );
- const timeout = setTimeout(function handleTimeout() {
- unsubscribe();
- resolve();
- }, 3000);
- stopStream(sourceSessionId);
- });
-
- queryClient.invalidateQueries({
- queryKey: getGetV2GetSessionQueryKey(sourceSessionId),
- });
- setIsSwitchingSession(false);
+ if (isStreaming) {
+ pendingActionRef.current = async () => {
+ await stopCurrentStream();
+ selectSession(sessionId);
+ };
+ openInterruptModal(pendingActionRef.current);
+ } else {
+ selectSession(sessionId);
}
-
- resetAutoSelect();
- resetPagination();
- queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
- setUrlSessionId(null, { shallow: false });
- setOptimisticSessionId(null);
- if (isMobile) handleCloseDrawer();
}
- function handleNewChat() {
- resetAutoSelect();
- resetPagination();
- queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
- setUrlSessionId(null, { shallow: false });
- setOptimisticSessionId(null);
- if (isMobile) handleCloseDrawer();
+ function handleNewChatClick() {
+ if (isStreaming) {
+ pendingActionRef.current = async () => {
+ await stopCurrentStream();
+ startNewChat();
+ };
+ openInterruptModal(pendingActionRef.current);
+ } else {
+ startNewChat();
+ }
}
- function resetAutoSelect() {
- hasAutoSelectedRef.current = false;
- setHasAutoSelectedSession(false);
- }
-
- const isLoading = isSessionsLoading && accumulatedSessions.length === 0;
-
return {
isMobile,
isDrawerOpen,
@@ -284,18 +155,15 @@ export function useCopilotShell() {
hasActiveSession:
Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)),
isLoading,
- sessions: visibleSessions,
- currentSessionId: sidebarSelectedSessionId,
- handleSelectSession,
- performSelectSession,
+ sessions,
+ currentSessionId: urlSessionId,
handleOpenDrawer,
handleCloseDrawer,
handleDrawerOpenChange,
- handleNewChat,
- performNewChat,
+ handleNewChatClick,
+ handleSessionClick,
hasNextPage,
isFetchingNextPage: isSessionsFetching,
fetchNextPage,
- isReadyToShowContent,
};
}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useShellSessionList.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useShellSessionList.ts
new file mode 100644
index 0000000000..30e2b6aba1
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/useShellSessionList.ts
@@ -0,0 +1,115 @@
+import { getGetV2ListSessionsQueryKey } from "@/app/api/__generated__/endpoints/chat/chat";
+import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
+import type { SessionSummaryResponse } from "@/app/api/__generated__/models/sessionSummaryResponse";
+import { useChatStore } from "@/components/contextual/Chat/chat-store";
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect, useMemo, useRef } from "react";
+import { useSessionsPagination } from "./components/SessionsList/useSessionsPagination";
+import {
+ convertSessionDetailToSummary,
+ filterVisibleSessions,
+ mergeCurrentSessionIntoList,
+} from "./helpers";
+
+interface UseShellSessionListArgs {
+ paginationEnabled: boolean;
+ currentSessionId: string | null;
+ currentSessionData: SessionDetailResponse | null | undefined;
+ isOnHomepage: boolean;
+ paramSessionId: string | null;
+}
+
+export function useShellSessionList({
+ paginationEnabled,
+ currentSessionId,
+ currentSessionData,
+ isOnHomepage,
+ paramSessionId,
+}: UseShellSessionListArgs) {
+ const queryClient = useQueryClient();
+ const onStreamComplete = useChatStore((s) => s.onStreamComplete);
+
+ const {
+ sessions: accumulatedSessions,
+ isLoading: isSessionsLoading,
+ isFetching: isSessionsFetching,
+ hasNextPage,
+ fetchNextPage,
+ reset: resetPagination,
+ } = useSessionsPagination({
+ enabled: paginationEnabled,
+ });
+
+ const recentlyCreatedSessionsRef = useRef<
+ Map
+ >(new Map());
+
+ useEffect(() => {
+ if (isOnHomepage && !paramSessionId) {
+ queryClient.invalidateQueries({
+ queryKey: getGetV2ListSessionsQueryKey(),
+ });
+ }
+ }, [isOnHomepage, paramSessionId, queryClient]);
+
+ useEffect(() => {
+ if (currentSessionId && currentSessionData) {
+ const isNewSession =
+ currentSessionData.updated_at === currentSessionData.created_at;
+ const isNotInAccumulated = !accumulatedSessions.some(
+ (s) => s.id === currentSessionId,
+ );
+ if (isNewSession || isNotInAccumulated) {
+ const summary = convertSessionDetailToSummary(currentSessionData);
+ recentlyCreatedSessionsRef.current.set(currentSessionId, summary);
+ }
+ }
+ }, [currentSessionId, currentSessionData, accumulatedSessions]);
+
+ useEffect(() => {
+ for (const sessionId of recentlyCreatedSessionsRef.current.keys()) {
+ if (accumulatedSessions.some((s) => s.id === sessionId)) {
+ recentlyCreatedSessionsRef.current.delete(sessionId);
+ }
+ }
+ }, [accumulatedSessions]);
+
+ useEffect(() => {
+ const unsubscribe = onStreamComplete((completedSessionId) => {
+ if (recentlyCreatedSessionsRef.current.has(completedSessionId)) {
+ queryClient.invalidateQueries({
+ queryKey: getGetV2ListSessionsQueryKey(),
+ });
+ }
+ });
+ return unsubscribe;
+ }, [onStreamComplete, queryClient]);
+
+ const sessions = useMemo(
+ () =>
+ mergeCurrentSessionIntoList(
+ accumulatedSessions,
+ currentSessionId,
+ currentSessionData,
+ recentlyCreatedSessionsRef.current,
+ ),
+ [accumulatedSessions, currentSessionId, currentSessionData],
+ );
+
+ const visibleSessions = useMemo(
+ () => filterVisibleSessions(sessions),
+ [sessions],
+ );
+
+ const isLoading = isSessionsLoading && accumulatedSessions.length === 0;
+
+ return {
+ sessions: visibleSessions,
+ isLoading,
+ isSessionsFetching,
+ hasNextPage,
+ fetchNextPage,
+ resetPagination,
+ recentlyCreatedSessionsRef,
+ };
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/copilot-page-store.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/copilot-page-store.ts
index 486d31865b..201c8824c1 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/copilot-page-store.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/copilot-page-store.ts
@@ -7,25 +7,12 @@ interface CopilotStoreState {
isSwitchingSession: boolean;
isInterruptModalOpen: boolean;
pendingAction: (() => void) | null;
- newChatHandler: (() => void) | null;
- newChatWithInterruptHandler: (() => void) | null;
- selectSessionHandler: ((sessionId: string) => void) | null;
- selectSessionWithInterruptHandler: ((sessionId: string) => void) | null;
}
interface CopilotStoreActions {
setIsStreaming: (isStreaming: boolean) => void;
setIsSwitchingSession: (isSwitchingSession: boolean) => void;
- setNewChatHandler: (handler: (() => void) | null) => void;
- setNewChatWithInterruptHandler: (handler: (() => void) | null) => void;
- setSelectSessionHandler: (
- handler: ((sessionId: string) => void) | null,
- ) => void;
- setSelectSessionWithInterruptHandler: (
- handler: ((sessionId: string) => void) | null,
- ) => void;
- requestNewChat: () => void;
- requestSelectSession: (sessionId: string) => void;
+ openInterruptModal: (onConfirm: () => void) => void;
confirmInterrupt: () => void;
cancelInterrupt: () => void;
}
@@ -37,10 +24,6 @@ export const useCopilotStore = create((set, get) => ({
isSwitchingSession: false,
isInterruptModalOpen: false,
pendingAction: null,
- newChatHandler: null,
- newChatWithInterruptHandler: null,
- selectSessionHandler: null,
- selectSessionWithInterruptHandler: null,
setIsStreaming(isStreaming) {
set({ isStreaming });
@@ -50,51 +33,8 @@ export const useCopilotStore = create((set, get) => ({
set({ isSwitchingSession });
},
- setNewChatHandler(handler) {
- set({ newChatHandler: handler });
- },
-
- setNewChatWithInterruptHandler(handler) {
- set({ newChatWithInterruptHandler: handler });
- },
-
- setSelectSessionHandler(handler) {
- set({ selectSessionHandler: handler });
- },
-
- setSelectSessionWithInterruptHandler(handler) {
- set({ selectSessionWithInterruptHandler: handler });
- },
-
- requestNewChat() {
- const { isStreaming, newChatHandler, newChatWithInterruptHandler } = get();
- if (isStreaming) {
- if (!newChatWithInterruptHandler) return;
- set({
- isInterruptModalOpen: true,
- pendingAction: newChatWithInterruptHandler,
- });
- } else if (newChatHandler) {
- newChatHandler();
- }
- },
-
- requestSelectSession(sessionId) {
- const {
- isStreaming,
- selectSessionHandler,
- selectSessionWithInterruptHandler,
- } = get();
- if (isStreaming) {
- if (!selectSessionWithInterruptHandler) return;
- set({
- isInterruptModalOpen: true,
- pendingAction: () => selectSessionWithInterruptHandler(sessionId),
- });
- } else {
- if (!selectSessionHandler) return;
- selectSessionHandler(sessionId);
- }
+ openInterruptModal(onConfirm) {
+ set({ isInterruptModalOpen: true, pendingAction: onConfirm });
},
confirmInterrupt() {
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts
index a5818f0a9f..692a5741f4 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/helpers.ts
@@ -1,28 +1,5 @@
import type { User } from "@supabase/supabase-js";
-export type PageState =
- | { type: "welcome" }
- | { type: "newChat" }
- | { type: "creating"; prompt: string }
- | { type: "chat"; sessionId: string; initialPrompt?: string };
-
-export function getInitialPromptFromState(
- pageState: PageState,
- storedInitialPrompt: string | undefined,
-) {
- if (storedInitialPrompt) return storedInitialPrompt;
- if (pageState.type === "creating") return pageState.prompt;
- if (pageState.type === "chat") return pageState.initialPrompt;
-}
-
-export function shouldResetToWelcome(pageState: PageState) {
- return (
- pageState.type !== "newChat" &&
- pageState.type !== "creating" &&
- pageState.type !== "welcome"
- );
-}
-
export function getGreetingName(user?: User | null): string {
if (!user) return "there";
const metadata = user.user_metadata as Record | undefined;
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx
index 008b06fcda..dfc531557f 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx
@@ -1,7 +1,6 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
-
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
import { Text } from "@/components/atoms/Text/Text";
import { Chat } from "@/components/contextual/Chat/Chat";
@@ -16,7 +15,15 @@ export default function CopilotPage() {
const isInterruptModalOpen = useCopilotStore((s) => s.isInterruptModalOpen);
const confirmInterrupt = useCopilotStore((s) => s.confirmInterrupt);
const cancelInterrupt = useCopilotStore((s) => s.cancelInterrupt);
- const { greetingName, quickActions, isLoading, pageState, isReady } = state;
+ const {
+ greetingName,
+ quickActions,
+ isLoading,
+ isCreating,
+ hasSession,
+ initialPrompt,
+ isReady,
+ } = state;
const {
handleQuickAction,
startChatWithPrompt,
@@ -26,14 +33,12 @@ export default function CopilotPage() {
if (!isReady) return null;
- if (pageState.type === "chat") {
+ if (hasSession) {
return (
@@ -77,13 +82,13 @@ export default function CopilotPage() {
);
}
- if (pageState.type === "newChat" || pageState.type === "creating") {
+ if (isCreating) {
return (
- Loading your chats...
+ Creating your chat...
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
index 8cf4599a12..35401d4415 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
@@ -14,60 +14,10 @@ import * as Sentry from "@sentry/nextjs";
import { useQueryClient } from "@tanstack/react-query";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation";
-import { useEffect, useReducer } from "react";
+import { useEffect, useRef, useState } from "react";
import { useCopilotStore } from "./copilot-page-store";
-import { getGreetingName, getQuickActions, type PageState } from "./helpers";
-import { useCopilotURLState } from "./useCopilotURLState";
-
-type CopilotState = {
- pageState: PageState;
- initialPrompts: Record
;
- previousSessionId: string | null;
-};
-
-type CopilotAction =
- | { type: "setPageState"; pageState: PageState }
- | { type: "setInitialPrompt"; sessionId: string; prompt: string }
- | { type: "setPreviousSessionId"; sessionId: string | null };
-
-function isSamePageState(next: PageState, current: PageState) {
- if (next.type !== current.type) return false;
- if (next.type === "creating" && current.type === "creating") {
- return next.prompt === current.prompt;
- }
- if (next.type === "chat" && current.type === "chat") {
- return (
- next.sessionId === current.sessionId &&
- next.initialPrompt === current.initialPrompt
- );
- }
- return true;
-}
-
-function copilotReducer(
- state: CopilotState,
- action: CopilotAction,
-): CopilotState {
- if (action.type === "setPageState") {
- if (isSamePageState(action.pageState, state.pageState)) return state;
- return { ...state, pageState: action.pageState };
- }
- if (action.type === "setInitialPrompt") {
- if (state.initialPrompts[action.sessionId] === action.prompt) return state;
- return {
- ...state,
- initialPrompts: {
- ...state.initialPrompts,
- [action.sessionId]: action.prompt,
- },
- };
- }
- if (action.type === "setPreviousSessionId") {
- if (state.previousSessionId === action.sessionId) return state;
- return { ...state, previousSessionId: action.sessionId };
- }
- return state;
-}
+import { getGreetingName, getQuickActions } from "./helpers";
+import { useCopilotSessionId } from "./useCopilotSessionId";
export function useCopilotPage() {
const router = useRouter();
@@ -75,6 +25,7 @@ export function useCopilotPage() {
const { user, isLoggedIn, isUserLoading } = useSupabase();
const { toast } = useToast();
+ const { urlSessionId, setUrlSessionId } = useCopilotSessionId();
const setIsStreaming = useCopilotStore((s) => s.setIsStreaming);
const isChatEnabled = useGetFlag(Flag.CHAT);
@@ -86,72 +37,30 @@ export function useCopilotPage() {
const isFlagReady =
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
- const [state, dispatch] = useReducer(copilotReducer, {
- pageState: { type: "welcome" },
- initialPrompts: {},
- previousSessionId: null,
- });
+ const [isCreating, setIsCreating] = useState(false);
+ const initialPromptsRef = useRef>({});
const greetingName = getGreetingName(user);
const quickActions = getQuickActions();
- function setPageState(pageState: PageState) {
- dispatch({ type: "setPageState", pageState });
- }
+ const hasSession = Boolean(urlSessionId);
+ const initialPrompt = urlSessionId
+ ? initialPromptsRef.current[urlSessionId]
+ : undefined;
- function setInitialPrompt(sessionId: string, prompt: string) {
- dispatch({ type: "setInitialPrompt", sessionId, prompt });
- }
-
- function setPreviousSessionId(sessionId: string | null) {
- dispatch({ type: "setPreviousSessionId", sessionId });
- }
-
- const { setUrlSessionId } = useCopilotURLState({
- pageState: state.pageState,
- initialPrompts: state.initialPrompts,
- previousSessionId: state.previousSessionId,
- setPageState,
- setInitialPrompt,
- setPreviousSessionId,
- });
-
- useEffect(
- function transitionNewChatToWelcome() {
- if (state.pageState.type === "newChat") {
- function setWelcomeState() {
- dispatch({ type: "setPageState", pageState: { type: "welcome" } });
- }
-
- const timer = setTimeout(setWelcomeState, 300);
-
- return function cleanup() {
- clearTimeout(timer);
- };
- }
- },
- [state.pageState.type],
- );
-
- useEffect(
- function ensureAccess() {
- if (!isFlagReady) return;
- if (isChatEnabled === false) {
- router.replace(homepageRoute);
- }
- },
- [homepageRoute, isChatEnabled, isFlagReady, router],
- );
+ useEffect(() => {
+ if (!isFlagReady) return;
+ if (isChatEnabled === false) {
+ router.replace(homepageRoute);
+ }
+ }, [homepageRoute, isChatEnabled, isFlagReady, router]);
async function startChatWithPrompt(prompt: string) {
if (!prompt?.trim()) return;
- if (state.pageState.type === "creating") return;
+ if (isCreating) return;
const trimmedPrompt = prompt.trim();
- dispatch({
- type: "setPageState",
- pageState: { type: "creating", prompt: trimmedPrompt },
- });
+ setIsCreating(true);
try {
const sessionResponse = await postV2CreateSession({
@@ -163,27 +72,19 @@ export function useCopilotPage() {
}
const sessionId = sessionResponse.data.id;
-
- dispatch({
- type: "setInitialPrompt",
- sessionId,
- prompt: trimmedPrompt,
- });
+ initialPromptsRef.current[sessionId] = trimmedPrompt;
await queryClient.invalidateQueries({
queryKey: getGetV2ListSessionsQueryKey(),
});
await setUrlSessionId(sessionId, { shallow: false });
- dispatch({
- type: "setPageState",
- pageState: { type: "chat", sessionId, initialPrompt: trimmedPrompt },
- });
} catch (error) {
console.error("[CopilotPage] Failed to start chat:", error);
toast({ title: "Failed to start chat", variant: "destructive" });
Sentry.captureException(error);
- dispatch({ type: "setPageState", pageState: { type: "welcome" } });
+ } finally {
+ setIsCreating(false);
}
}
@@ -204,7 +105,9 @@ export function useCopilotPage() {
greetingName,
quickActions,
isLoading: isUserLoading,
- pageState: state.pageState,
+ isCreating,
+ hasSession,
+ initialPrompt,
isReady: isFlagReady && isChatEnabled !== false && isLoggedIn,
},
handlers: {
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotSessionId.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotSessionId.ts
new file mode 100644
index 0000000000..87f9b7d3ae
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotSessionId.ts
@@ -0,0 +1,10 @@
+import { parseAsString, useQueryState } from "nuqs";
+
+export function useCopilotSessionId() {
+ const [urlSessionId, setUrlSessionId] = useQueryState(
+ "sessionId",
+ parseAsString,
+ );
+
+ return { urlSessionId, setUrlSessionId };
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts
deleted file mode 100644
index 5e37e29a15..0000000000
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { parseAsString, useQueryState } from "nuqs";
-import { useLayoutEffect } from "react";
-import {
- getInitialPromptFromState,
- type PageState,
- shouldResetToWelcome,
-} from "./helpers";
-
-interface UseCopilotUrlStateArgs {
- pageState: PageState;
- initialPrompts: Record;
- previousSessionId: string | null;
- setPageState: (pageState: PageState) => void;
- setInitialPrompt: (sessionId: string, prompt: string) => void;
- setPreviousSessionId: (sessionId: string | null) => void;
-}
-
-export function useCopilotURLState({
- pageState,
- initialPrompts,
- previousSessionId,
- setPageState,
- setInitialPrompt,
- setPreviousSessionId,
-}: UseCopilotUrlStateArgs) {
- const [urlSessionId, setUrlSessionId] = useQueryState(
- "sessionId",
- parseAsString,
- );
-
- function syncSessionFromUrl() {
- if (urlSessionId) {
- if (pageState.type === "chat" && pageState.sessionId === urlSessionId) {
- setPreviousSessionId(urlSessionId);
- return;
- }
-
- const storedInitialPrompt = initialPrompts[urlSessionId];
- const currentInitialPrompt = getInitialPromptFromState(
- pageState,
- storedInitialPrompt,
- );
-
- if (currentInitialPrompt) {
- setInitialPrompt(urlSessionId, currentInitialPrompt);
- }
-
- setPageState({
- type: "chat",
- sessionId: urlSessionId,
- initialPrompt: currentInitialPrompt,
- });
- setPreviousSessionId(urlSessionId);
- return;
- }
-
- const wasInChat = previousSessionId !== null && pageState.type === "chat";
- setPreviousSessionId(null);
- if (wasInChat) {
- setPageState({ type: "newChat" });
- return;
- }
-
- if (shouldResetToWelcome(pageState)) {
- setPageState({ type: "welcome" });
- }
- }
-
- useLayoutEffect(syncSessionFromUrl, [
- urlSessionId,
- pageState.type,
- previousSessionId,
- initialPrompts,
- ]);
-
- return {
- urlSessionId,
- setUrlSessionId,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
index a7a5f61674..d16fd60b98 100644
--- a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
+++ b/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useCopilotSessionId } from "@/app/(platform)/copilot/useCopilotSessionId";
import { useCopilotStore } from "@/app/(platform)/copilot/copilot-page-store";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text";
@@ -11,7 +12,6 @@ import { useChat } from "./useChat";
export interface ChatProps {
className?: string;
- urlSessionId?: string | null;
initialPrompt?: string;
onSessionNotFound?: () => void;
onStreamingChange?: (isStreaming: boolean) => void;
@@ -19,11 +19,11 @@ export interface ChatProps {
export function Chat({
className,
- urlSessionId,
initialPrompt,
onSessionNotFound,
onStreamingChange,
}: ChatProps) {
+ const { urlSessionId } = useCopilotSessionId();
const hasHandledNotFoundRef = useRef(false);
const isSwitchingSession = useCopilotStore((s) => s.isSwitchingSession);
const {
@@ -37,17 +37,20 @@ export function Chat({
showLoader,
} = useChat({ urlSessionId });
- useEffect(
- function handleMissingSession() {
- if (!onSessionNotFound) return;
- if (!urlSessionId) return;
- if (!isSessionNotFound || isLoading || isCreating) return;
- if (hasHandledNotFoundRef.current) return;
- hasHandledNotFoundRef.current = true;
- onSessionNotFound();
- },
- [onSessionNotFound, urlSessionId, isSessionNotFound, isLoading, isCreating],
- );
+ useEffect(() => {
+ if (!onSessionNotFound) return;
+ if (!urlSessionId) return;
+ if (!isSessionNotFound || isLoading || isCreating) return;
+ if (hasHandledNotFoundRef.current) return;
+ hasHandledNotFoundRef.current = true;
+ onSessionNotFound();
+ }, [
+ onSessionNotFound,
+ urlSessionId,
+ isSessionNotFound,
+ isLoading,
+ isCreating,
+ ]);
const shouldShowLoader =
(showLoader && (isLoading || isCreating)) || isSwitchingSession;