diff --git a/frontend/__tests__/components/browser.test.tsx b/frontend/__tests__/components/browser.test.tsx
index 304b49c3fc..dffd0b9eb2 100644
--- a/frontend/__tests__/components/browser.test.tsx
+++ b/frontend/__tests__/components/browser.test.tsx
@@ -1,6 +1,8 @@
import { describe, it, expect, afterEach, vi } from "vitest";
+import { screen, render } from "@testing-library/react";
+import React from "react";
-// Mock useParams before importing components
+// Mock modules before importing the component
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
return {
@@ -9,7 +11,11 @@ vi.mock("react-router", async () => {
};
});
-// Mock i18next
+vi.mock("#/context/conversation-context", () => ({
+ useConversation: () => ({ conversationId: "test-conversation-id" }),
+ ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
+}));
+
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
@@ -23,38 +29,56 @@ vi.mock("react-i18next", async () => {
};
});
-import { screen } from "@testing-library/react";
-import { renderWithProviders } from "../../test-utils";
+// Mock redux
+const mockDispatch = vi.fn();
+let mockBrowserState = {
+ url: "https://example.com",
+ screenshotSrc: "",
+};
+
+vi.mock("react-redux", async () => {
+ const actual = await vi.importActual("react-redux");
+ return {
+ ...actual,
+ useDispatch: () => mockDispatch,
+ useSelector: () => mockBrowserState,
+ };
+});
+
+// Import the component after all mocks are set up
import { BrowserPanel } from "#/components/features/browser/browser";
describe("Browser", () => {
afterEach(() => {
vi.clearAllMocks();
+ // Reset the mock state
+ mockBrowserState = {
+ url: "https://example.com",
+ screenshotSrc: "",
+ };
});
+
it("renders a message if no screenshotSrc is provided", () => {
- renderWithProviders(, {
- preloadedState: {
- browser: {
- url: "https://example.com",
- screenshotSrc: "",
- },
- },
- });
+ // Set the mock state for this test
+ mockBrowserState = {
+ url: "https://example.com",
+ screenshotSrc: "",
+ };
+
+ render();
// i18n empty message key
expect(screen.getByText("BROWSER$NO_PAGE_LOADED")).toBeInTheDocument();
});
it("renders the url and a screenshot", () => {
- renderWithProviders(, {
- preloadedState: {
- browser: {
- url: "https://example.com",
- screenshotSrc:
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
- },
- },
- });
+ // Set the mock state for this test
+ mockBrowserState = {
+ url: "https://example.com",
+ screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
+ };
+
+ render();
expect(screen.getByText("https://example.com")).toBeInTheDocument();
expect(screen.getByAltText("BROWSER$SCREENSHOT_ALT")).toBeInTheDocument();
diff --git a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
index 67e373f40f..ce8ad1c8a7 100644
--- a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
+++ b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
@@ -34,10 +34,6 @@ describe("ConversationPanel", () => {
}
});
- const { endSessionMock } = vi.hoisted(() => ({
- endSessionMock: vi.fn(),
- }));
-
beforeAll(() => {
vi.mock("react-router", async (importOriginal) => ({
...(await importOriginal()),
@@ -46,11 +42,6 @@ describe("ConversationPanel", () => {
useLocation: vi.fn(() => ({ pathname: "/conversation" })),
useParams: vi.fn(() => ({ conversationId: "2" })),
}));
-
- vi.mock("#/hooks/use-end-session", async (importOriginal) => ({
- ...(await importOriginal()),
- useEndSession: vi.fn(() => endSessionMock),
- }));
});
const mockConversations = [
@@ -145,47 +136,6 @@ describe("ConversationPanel", () => {
expect(cards).toHaveLength(3);
});
- it("should call endSession after deleting a conversation that is the current session", async () => {
- const user = userEvent.setup();
- const mockData = [...mockConversations];
- const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
- getUserConversationsSpy.mockImplementation(async () => mockData);
-
- const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
- deleteUserConversationSpy.mockImplementation(async (id: string) => {
- const index = mockData.findIndex(conv => conv.conversation_id === id);
- if (index !== -1) {
- mockData.splice(index, 1);
- }
- // Wait for React Query to update its cache
- await new Promise(resolve => setTimeout(resolve, 0));
- });
-
- renderConversationPanel();
-
- let cards = await screen.findAllByTestId("conversation-card");
- const ellipsisButton = within(cards[1]).getByTestId("ellipsis-button");
- await user.click(ellipsisButton);
- const deleteButton = screen.getByTestId("delete-button");
-
- // Click the second delete button
- await user.click(deleteButton);
-
- // Confirm the deletion
- const confirmButton = screen.getByRole("button", { name: /confirm/i });
- await user.click(confirmButton);
-
- expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
-
- // Wait for the cards to update with a longer timeout
- await waitFor(() => {
- const updatedCards = screen.getAllByTestId("conversation-card");
- expect(updatedCards).toHaveLength(2);
- }, { timeout: 2000 });
-
- expect(endSessionMock).toHaveBeenCalledOnce();
- });
-
it("should delete a conversation", async () => {
const user = userEvent.setup();
const mockData = [
diff --git a/frontend/__tests__/routes/_oh.app.test.tsx b/frontend/__tests__/routes/_oh.app.test.tsx
index 489cb37210..1b35e0430f 100644
--- a/frontend/__tests__/routes/_oh.app.test.tsx
+++ b/frontend/__tests__/routes/_oh.app.test.tsx
@@ -13,15 +13,7 @@ describe("App", () => {
{ Component: App, path: "/conversation/:conversationId" },
]);
- const { endSessionMock } = vi.hoisted(() => ({
- endSessionMock: vi.fn(),
- }));
-
beforeAll(() => {
- vi.mock("#/hooks/use-end-session", () => ({
- useEndSession: vi.fn(() => endSessionMock),
- }));
-
vi.mock("#/hooks/use-terminal", () => ({
useTerminal: vi.fn(),
}));
@@ -35,44 +27,4 @@ describe("App", () => {
renderWithProviders();
await screen.findByTestId("app-route");
});
-
- it("should call endSession if the user does not have permission to view conversation", async () => {
- const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
-
- getConversationSpy.mockResolvedValue(null);
- renderWithProviders();
-
- await waitFor(() => {
- expect(endSessionMock).toHaveBeenCalledOnce();
- expect(errorToastSpy).toHaveBeenCalledOnce();
- });
- });
-
- it("should not call endSession if the user has permission", async () => {
- const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
-
- getConversationSpy.mockResolvedValue({
- conversation_id: "9999",
- last_updated_at: "",
- created_at: "",
- title: "",
- selected_repository: "",
- status: "STOPPED",
- });
- const { rerender } = renderWithProviders(
- ,
- );
-
- await waitFor(() => {
- expect(endSessionMock).not.toHaveBeenCalled();
- expect(errorToastSpy).not.toHaveBeenCalled();
- });
-
- rerender();
-
- await waitFor(() => {
- expect(endSessionMock).not.toHaveBeenCalled();
- expect(errorToastSpy).not.toHaveBeenCalled();
- });
- });
});
diff --git a/frontend/src/components/features/browser/browser.tsx b/frontend/src/components/features/browser/browser.tsx
index 00bf8e1965..444c699037 100644
--- a/frontend/src/components/features/browser/browser.tsx
+++ b/frontend/src/components/features/browser/browser.tsx
@@ -1,12 +1,26 @@
-import { useSelector } from "react-redux";
+import { useEffect } from "react";
+import { useSelector, useDispatch } from "react-redux";
import { RootState } from "#/store";
import { BrowserSnapshot } from "./browser-snapshot";
import { EmptyBrowserMessage } from "./empty-browser-message";
+import { useConversation } from "#/context/conversation-context";
+import {
+ initialState as browserInitialState,
+ setUrl,
+ setScreenshotSrc,
+} from "#/state/browser-slice";
export function BrowserPanel() {
const { url, screenshotSrc } = useSelector(
(state: RootState) => state.browser,
);
+ const { conversationId } = useConversation();
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(setUrl(browserInitialState.url));
+ dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
+ }, [conversationId]);
const imgSrc =
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
diff --git a/frontend/src/components/features/conversation-panel/conversation-panel.tsx b/frontend/src/components/features/conversation-panel/conversation-panel.tsx
index f35ed756ad..2558438dc3 100644
--- a/frontend/src/components/features/conversation-panel/conversation-panel.tsx
+++ b/frontend/src/components/features/conversation-panel/conversation-panel.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { NavLink, useParams } from "react-router";
+import { NavLink, useParams, useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { ConversationCard } from "./conversation-card";
@@ -8,7 +8,6 @@ import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation"
import { ConfirmDeleteModal } from "./confirm-delete-modal";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation";
-import { useEndSession } from "#/hooks/use-end-session";
import { ExitConversationModal } from "./exit-conversation-modal";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
@@ -18,9 +17,9 @@ interface ConversationPanelProps {
export function ConversationPanel({ onClose }: ConversationPanelProps) {
const { t } = useTranslation();
- const { conversationId: cid } = useParams();
- const endSession = useEndSession();
+ const { conversationId: currentConversationId } = useParams();
const ref = useClickOutsideElement(onClose);
+ const navigate = useNavigate();
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
React.useState(false);
@@ -48,8 +47,8 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
{ conversationId: selectedConversationId },
{
onSuccess: () => {
- if (cid === selectedConversationId) {
- endSession();
+ if (selectedConversationId === currentConversationId) {
+ navigate("/");
}
},
},
@@ -129,7 +128,6 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
{confirmExitConversationModalVisible && (
{
- endSession();
onClose();
}}
onClose={() => setConfirmExitConversationModalVisible(false)}
diff --git a/frontend/src/components/features/sidebar/sidebar.tsx b/frontend/src/components/features/sidebar/sidebar.tsx
index 093044791b..59220eceda 100644
--- a/frontend/src/components/features/sidebar/sidebar.tsx
+++ b/frontend/src/components/features/sidebar/sidebar.tsx
@@ -1,34 +1,23 @@
import React from "react";
-import { FaListUl } from "react-icons/fa";
-import { useDispatch } from "react-redux";
import posthog from "posthog-js";
-import { NavLink, useLocation } from "react-router";
-import { useTranslation } from "react-i18next";
+import { useLocation } from "react-router";
import { useGitUser } from "#/hooks/query/use-git-user";
import { UserActions } from "./user-actions";
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
import { DocsButton } from "#/components/shared/buttons/docs-button";
-import { ExitProjectButton } from "#/components/shared/buttons/exit-project-button";
+import { NewProjectButton } from "#/components/shared/buttons/new-project-button";
import { SettingsButton } from "#/components/shared/buttons/settings-button";
+import { ConversationPanelButton } from "#/components/shared/buttons/conversation-panel-button";
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
import { useSettings } from "#/hooks/query/use-settings";
import { ConversationPanel } from "../conversation-panel/conversation-panel";
-import { useEndSession } from "#/hooks/use-end-session";
-import { setCurrentAgentState } from "#/state/agent-slice";
-import { AgentState } from "#/types/agent-state";
-import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
import { useLogout } from "#/hooks/mutation/use-logout";
import { useConfig } from "#/hooks/query/use-config";
-import { cn } from "#/utils/utils";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
-import { I18nKey } from "#/i18n/declaration";
export function Sidebar() {
- const { t } = useTranslation();
const location = useLocation();
- const dispatch = useDispatch();
- const endSession = useEndSession();
const user = useGitUser();
const { data: config } = useConfig();
const {
@@ -73,11 +62,6 @@ export function Sidebar() {
location.pathname,
]);
- const handleEndSession = () => {
- dispatch(setCurrentAgentState(AgentState.LOADING));
- endSession();
- };
-
const handleLogout = async () => {
await logout();
posthog.reset();
@@ -89,34 +73,18 @@ export function Sidebar() {