From b89f2e51e45c00792a18384d067a30b2f1b8ceda Mon Sep 17 00:00:00 2001
From: Hiep Le <69354317+hieptl@users.noreply.github.com>
Date: Fri, 19 Sep 2025 23:52:21 +0700
Subject: [PATCH] refactor(frontend): migration of metrics-slice.ts to zustand
(#11018)
---
.../conversation-panel.test.tsx | 24 +-----
.../microagent-management.test.tsx | 85 -------------------
.../metrics-modal/metrics-modal.tsx | 5 +-
.../use-conversation-name-context-menu.ts | 5 +-
frontend/src/services/actions.ts | 4 +-
frontend/src/state/metrics-slice.ts | 35 --------
frontend/src/store.ts | 2 -
frontend/src/stores/metrics-store.ts | 27 ++++++
frontend/test-utils.tsx | 22 +++++
9 files changed, 58 insertions(+), 151 deletions(-)
delete mode 100644 frontend/src/state/metrics-slice.ts
create mode 100644 frontend/src/stores/metrics-store.ts
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 9ea5c0fefa..7df58fab97 100644
--- a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
+++ b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx
@@ -1,10 +1,9 @@
import { screen, waitFor, within } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
-import { QueryClientConfig } from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import React from "react";
-import { renderWithProviders } from "test-utils";
+import { renderWithQueryAndI18n } from "test-utils";
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { Conversation } from "#/api/open-hands.types";
@@ -18,16 +17,7 @@ describe("ConversationPanel", () => {
},
]);
- const renderConversationPanel = (config?: QueryClientConfig) =>
- renderWithProviders(, {
- preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
- },
- });
+ const renderConversationPanel = () => renderWithQueryAndI18n();
beforeAll(() => {
vi.mock("react-router", async (importOriginal) => ({
@@ -297,15 +287,7 @@ describe("ConversationPanel", () => {
},
]);
- renderWithProviders(, {
- preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
- },
- });
+ renderWithQueryAndI18n();
const toggleButton = screen.getByText("Toggle");
diff --git a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx
index 719b809f74..c0f6207b43 100644
--- a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx
+++ b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx
@@ -57,11 +57,6 @@ describe("MicroagentManagement", () => {
const renderMicroagentManagement = (config?: QueryClientConfig) =>
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
addMicroagentModalVisible: false,
updateMicroagentModalVisible: false,
@@ -1351,11 +1346,6 @@ describe("MicroagentManagement", () => {
// Render with modal already visible in Redux state
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: null,
addMicroagentModalVisible: true, // Start with modal visible
@@ -1646,11 +1636,6 @@ describe("MicroagentManagement", () => {
const renderMicroagentManagementMain = (selectedMicroagentItem: any) =>
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
addMicroagentModalVisible: false,
selectedRepository: {
@@ -1998,11 +1983,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible in Redux state
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2037,11 +2017,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible and selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2075,11 +2050,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible and selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2118,11 +2088,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible and selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2174,11 +2139,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2225,11 +2185,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2279,11 +2234,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible but no microagent data
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: null,
addMicroagentModalVisible: false,
@@ -2325,11 +2275,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible and microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2374,11 +2319,6 @@ describe("MicroagentManagement", () => {
// Render with update modal visible and microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForUpdate,
@@ -2561,11 +2501,6 @@ describe("MicroagentManagement", () => {
// Render with selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForLearn,
@@ -2601,11 +2536,6 @@ describe("MicroagentManagement", () => {
// Render with selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForLearn,
@@ -2658,11 +2588,6 @@ describe("MicroagentManagement", () => {
// Render with selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForLearn,
@@ -2718,11 +2643,6 @@ describe("MicroagentManagement", () => {
// Render with selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForLearn,
@@ -2776,11 +2696,6 @@ describe("MicroagentManagement", () => {
// Render with selected microagent
renderWithProviders(, {
preloadedState: {
- metrics: {
- cost: null,
- max_budget_per_task: null,
- usage: null,
- },
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForLearn,
diff --git a/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx b/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx
index e055bb5c6b..af6e314637 100644
--- a/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx
+++ b/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx
@@ -1,12 +1,11 @@
import { useTranslation } from "react-i18next";
-import { useSelector } from "react-redux";
import { BaseModal } from "../../../shared/modals/base-modal/base-modal";
-import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import { CostSection } from "./cost-section";
import { UsageSection } from "./usage-section";
import { ContextWindowSection } from "./context-window-section";
import { EmptyState } from "./empty-state";
+import useMetricsStore from "#/stores/metrics-store";
interface MetricsModalProps {
isOpen: boolean;
@@ -15,7 +14,7 @@ interface MetricsModalProps {
export function MetricsModal({ isOpen, onOpenChange }: MetricsModalProps) {
const { t } = useTranslation();
- const metrics = useSelector((state: RootState) => state.metrics);
+ const metrics = useMetricsStore();
return (
state.metrics);
+ const metrics = useMetricsStore();
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts
index 9cbfa34e3f..53fed792a7 100644
--- a/frontend/src/services/actions.ts
+++ b/frontend/src/services/actions.ts
@@ -1,7 +1,7 @@
import { trackError } from "#/utils/error-handler";
import { appendSecurityAnalyzerInput } from "#/state/security-analyzer-slice";
+import useMetricsStore from "#/stores/metrics-store";
import { useStatusStore } from "#/state/status-store";
-import { setMetrics } from "#/state/metrics-slice";
import store from "#/store";
import ActionType from "#/types/action-type";
import {
@@ -26,7 +26,7 @@ export function handleActionMessage(message: ActionMessage) {
max_budget_per_task: message.llm_metrics?.max_budget_per_task ?? null,
usage: message.llm_metrics?.accumulated_token_usage ?? null,
};
- store.dispatch(setMetrics(metrics));
+ useMetricsStore.getState().setMetrics(metrics);
}
if (message.action === ActionType.RUN) {
diff --git a/frontend/src/state/metrics-slice.ts b/frontend/src/state/metrics-slice.ts
deleted file mode 100644
index 77154340b1..0000000000
--- a/frontend/src/state/metrics-slice.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-
-interface MetricsState {
- cost: number | null;
- max_budget_per_task: number | null;
- usage: {
- prompt_tokens: number;
- completion_tokens: number;
- cache_read_tokens: number;
- cache_write_tokens: number;
- context_window: number;
- per_turn_token: number;
- } | null;
-}
-
-const initialState: MetricsState = {
- cost: null,
- max_budget_per_task: null,
- usage: null,
-};
-
-const metricsSlice = createSlice({
- name: "metrics",
- initialState,
- reducers: {
- setMetrics: (state, action: PayloadAction) => {
- state.cost = action.payload.cost;
- state.max_budget_per_task = action.payload.max_budget_per_task;
- state.usage = action.payload.usage;
- },
- },
-});
-
-export const { setMetrics } = metricsSlice.actions;
-export default metricsSlice.reducer;
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index 8c3b6813a3..8da8d3734d 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -4,7 +4,6 @@ import browserReducer from "./state/browser-slice";
import fileStateReducer from "./state/file-state-slice";
import { jupyterReducer } from "./state/jupyter-slice";
import securityAnalyzerReducer from "./state/security-analyzer-slice";
-import metricsReducer from "./state/metrics-slice";
import microagentManagementReducer from "./state/microagent-management-slice";
import conversationReducer from "./state/conversation-slice";
import eventMessageReducer from "./state/event-message-slice";
@@ -15,7 +14,6 @@ export const rootReducer = combineReducers({
agent: agentReducer,
jupyter: jupyterReducer,
securityAnalyzer: securityAnalyzerReducer,
- metrics: metricsReducer,
microagentManagement: microagentManagementReducer,
conversation: conversationReducer,
eventMessage: eventMessageReducer,
diff --git a/frontend/src/stores/metrics-store.ts b/frontend/src/stores/metrics-store.ts
new file mode 100644
index 0000000000..82060e28c4
--- /dev/null
+++ b/frontend/src/stores/metrics-store.ts
@@ -0,0 +1,27 @@
+import { create } from "zustand";
+
+interface MetricsState {
+ cost: number | null;
+ max_budget_per_task: number | null;
+ usage: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ cache_read_tokens: number;
+ cache_write_tokens: number;
+ context_window: number;
+ per_turn_token: number;
+ } | null;
+}
+
+interface MetricsStore extends MetricsState {
+ setMetrics: (metrics: MetricsState) => void;
+}
+
+const useMetricsStore = create((set) => ({
+ cost: null,
+ max_budget_per_task: null,
+ usage: null,
+ setMetrics: (metrics) => set(metrics),
+}));
+
+export default useMetricsStore;
diff --git a/frontend/test-utils.tsx b/frontend/test-utils.tsx
index e5ddd89fc1..c882a054d8 100644
--- a/frontend/test-utils.tsx
+++ b/frontend/test-utils.tsx
@@ -79,6 +79,28 @@ export function renderWithProviders(
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}
+// Export a render function for components that only need QueryClient and i18next providers
+// (without Redux store)
+export function renderWithQueryAndI18n(
+ ui: React.ReactElement,
+ renderOptions: Omit = {},
+) {
+ function Wrapper({ children }: PropsWithChildren) {
+ return (
+
+ {children}
+
+ );
+ }
+ return render(ui, { wrapper: Wrapper, ...renderOptions });
+}
+
export const createAxiosNotFoundErrorObject = () =>
new AxiosError(
"Request failed with status code 404",