From 8adbb76bd76d1792211dc20f6fc51fe414009fd9 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Wed, 24 Sep 2025 22:52:48 +0700 Subject: [PATCH] refactor(frontend): migration of browser-slice.ts to zustand (#11081) --- .../__tests__/components/browser.test.tsx | 32 +++++++++------ .../components/features/browser/browser.tsx | 18 ++------- frontend/src/services/observations.ts | 39 ++++++++++++------- frontend/src/state/browser-slice.ts | 25 ------------ frontend/src/store.ts | 2 - frontend/src/stores/browser-store.ts | 26 +++++++++++++ 6 files changed, 76 insertions(+), 66 deletions(-) delete mode 100644 frontend/src/state/browser-slice.ts create mode 100644 frontend/src/stores/browser-store.ts diff --git a/frontend/__tests__/components/browser.test.tsx b/frontend/__tests__/components/browser.test.tsx index 1f768535da..df6aeec640 100644 --- a/frontend/__tests__/components/browser.test.tsx +++ b/frontend/__tests__/components/browser.test.tsx @@ -13,7 +13,8 @@ vi.mock("react-router", async () => { vi.mock("#/context/conversation-context", () => ({ useConversation: () => ({ conversationId: "test-conversation-id" }), - ConversationProvider: ({ children }: { children: React.ReactNode }) => children, + ConversationProvider: ({ children }: { children: React.ReactNode }) => + children, })); vi.mock("react-i18next", async () => { @@ -29,21 +30,18 @@ vi.mock("react-i18next", async () => { }; }); -// Mock redux -const mockDispatch = vi.fn(); +// Mock Zustand browser store let mockBrowserState = { url: "https://example.com", screenshotSrc: "", + setUrl: vi.fn(), + setScreenshotSrc: vi.fn(), + reset: vi.fn(), }; -vi.mock("react-redux", async () => { - const actual = await vi.importActual("react-redux"); - return { - ...actual, - useDispatch: () => mockDispatch, - useSelector: () => mockBrowserState, - }; -}); +vi.mock("#/stores/browser-store", () => ({ + useBrowserStore: () => mockBrowserState, +})); // Import the component after all mocks are set up import { BrowserPanel } from "#/components/features/browser/browser"; @@ -55,6 +53,9 @@ describe("Browser", () => { mockBrowserState = { url: "https://example.com", screenshotSrc: "", + setUrl: vi.fn(), + setScreenshotSrc: vi.fn(), + reset: vi.fn(), }; }); @@ -63,6 +64,9 @@ describe("Browser", () => { mockBrowserState = { url: "https://example.com", screenshotSrc: "", + setUrl: vi.fn(), + setScreenshotSrc: vi.fn(), + reset: vi.fn(), }; render(); @@ -75,7 +79,11 @@ describe("Browser", () => { // Set the mock state for this test mockBrowserState = { url: "https://example.com", - screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==", + screenshotSrc: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==", + setUrl: vi.fn(), + setScreenshotSrc: vi.fn(), + reset: vi.fn(), }; render(); diff --git a/frontend/src/components/features/browser/browser.tsx b/frontend/src/components/features/browser/browser.tsx index 44e06869ec..8c3842edd4 100644 --- a/frontend/src/components/features/browser/browser.tsx +++ b/frontend/src/components/features/browser/browser.tsx @@ -1,26 +1,16 @@ 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 { useConversationId } from "#/hooks/use-conversation-id"; -import { - initialState as browserInitialState, - setUrl, - setScreenshotSrc, -} from "#/state/browser-slice"; +import { useBrowserStore } from "#/stores/browser-store"; export function BrowserPanel() { - const { url, screenshotSrc } = useSelector( - (state: RootState) => state.browser, - ); + const { url, screenshotSrc, reset } = useBrowserStore(); const { conversationId } = useConversationId(); - const dispatch = useDispatch(); useEffect(() => { - dispatch(setUrl(browserInitialState.url)); - dispatch(setScreenshotSrc(browserInitialState.screenshotSrc)); - }, [conversationId]); + reset(); + }, [conversationId, reset]); const imgSrc = screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,") diff --git a/frontend/src/services/observations.ts b/frontend/src/services/observations.ts index 3bf8c016c3..030e8c6b71 100644 --- a/frontend/src/services/observations.ts +++ b/frontend/src/services/observations.ts @@ -1,10 +1,10 @@ import { setCurrentAgentState } from "#/state/agent-slice"; -import { setUrl, setScreenshotSrc } from "#/state/browser-slice"; import store from "#/store"; import { ObservationMessage } from "#/types/message"; import { useCommandStore } from "#/state/command-store"; import { appendJupyterOutput } from "#/state/jupyter-slice"; import ObservationType from "#/types/observation-type"; +import { useBrowserStore } from "#/stores/browser-store"; export function handleObservationMessage(message: ObservationMessage) { switch (message.observation) { @@ -34,11 +34,14 @@ export function handleObservationMessage(message: ObservationMessage) { break; case ObservationType.BROWSE: case ObservationType.BROWSE_INTERACTIVE: - if (message.extras?.screenshot) { - store.dispatch(setScreenshotSrc(message.extras?.screenshot)); + if ( + message.extras?.screenshot && + typeof message.extras.screenshot === "string" + ) { + useBrowserStore.getState().setScreenshotSrc(message.extras.screenshot); } - if (message.extras?.url) { - store.dispatch(setUrl(message.extras.url)); + if (message.extras?.url && typeof message.extras.url === "string") { + useBrowserStore.getState().setUrl(message.extras.url); } break; case ObservationType.AGENT_STATE_CHANGED: @@ -63,19 +66,29 @@ export function handleObservationMessage(message: ObservationMessage) { switch (observation) { case "browse": - if (message.extras?.screenshot) { - store.dispatch(setScreenshotSrc(message.extras.screenshot)); + if ( + message.extras?.screenshot && + typeof message.extras.screenshot === "string" + ) { + useBrowserStore + .getState() + .setScreenshotSrc(message.extras.screenshot); } - if (message.extras?.url) { - store.dispatch(setUrl(message.extras.url)); + if (message.extras?.url && typeof message.extras.url === "string") { + useBrowserStore.getState().setUrl(message.extras.url); } break; case "browse_interactive": - if (message.extras?.screenshot) { - store.dispatch(setScreenshotSrc(message.extras.screenshot)); + if ( + message.extras?.screenshot && + typeof message.extras.screenshot === "string" + ) { + useBrowserStore + .getState() + .setScreenshotSrc(message.extras.screenshot); } - if (message.extras?.url) { - store.dispatch(setUrl(message.extras.url)); + if (message.extras?.url && typeof message.extras.url === "string") { + useBrowserStore.getState().setUrl(message.extras.url); } break; default: diff --git a/frontend/src/state/browser-slice.ts b/frontend/src/state/browser-slice.ts deleted file mode 100644 index fc05f0c508..0000000000 --- a/frontend/src/state/browser-slice.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; - -export const initialState = { - // URL of browser window (placeholder for now, will be replaced with the actual URL later) - url: "https://github.com/All-Hands-AI/OpenHands", - // Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later) - screenshotSrc: "", -}; - -export const browserSlice = createSlice({ - name: "browser", - initialState, - reducers: { - setUrl: (state, action) => { - state.url = action.payload; - }, - setScreenshotSrc: (state, action) => { - state.screenshotSrc = action.payload; - }, - }, -}); - -export const { setUrl, setScreenshotSrc } = browserSlice.actions; - -export default browserSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 8c79ff2e0c..37723a5e8f 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -1,12 +1,10 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; import agentReducer from "./state/agent-slice"; -import browserReducer from "./state/browser-slice"; import { jupyterReducer } from "./state/jupyter-slice"; import microagentManagementReducer from "./state/microagent-management-slice"; import eventMessageReducer from "./state/event-message-slice"; export const rootReducer = combineReducers({ - browser: browserReducer, agent: agentReducer, jupyter: jupyterReducer, microagentManagement: microagentManagementReducer, diff --git a/frontend/src/stores/browser-store.ts b/frontend/src/stores/browser-store.ts new file mode 100644 index 0000000000..cb28f3aa67 --- /dev/null +++ b/frontend/src/stores/browser-store.ts @@ -0,0 +1,26 @@ +import { create } from "zustand"; + +interface BrowserState { + // URL of browser window (placeholder for now, will be replaced with the actual URL later) + url: string; + // Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later) + screenshotSrc: string; +} + +interface BrowserStore extends BrowserState { + setUrl: (url: string) => void; + setScreenshotSrc: (screenshotSrc: string) => void; + reset: () => void; +} + +const initialState: BrowserState = { + url: "https://github.com/All-Hands-AI/OpenHands", + screenshotSrc: "", +}; + +export const useBrowserStore = create((set) => ({ + ...initialState, + setUrl: (url: string) => set({ url }), + setScreenshotSrc: (screenshotSrc: string) => set({ screenshotSrc }), + reset: () => set(initialState), +}));