refactor(frontend): migration of agent-slice.ts to zustand (#11102)

This commit is contained in:
Hiep Le 2025-09-25 12:44:21 +07:00 committed by GitHub
parent f8f74858da
commit e376c2bfd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 297 additions and 327 deletions

View File

@ -1,36 +1,16 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { renderWithQueryAndI18n } from "test-utils";
import { ServerStatus } from "#/components/features/controls/server-status";
import { ServerStatusContextMenu } from "#/components/features/controls/server-status-context-menu";
import { ConversationStatus } from "#/types/conversation-status";
import { AgentState } from "#/types/agent-state";
import { useAgentStore } from "#/stores/agent-store";
// Mock the conversation slice actions
vi.mock("#/state/conversation-slice", () => ({
setShouldStopConversation: vi.fn(),
setShouldStartConversation: vi.fn(),
default: {
name: "conversation",
initialState: {
isRightPanelShown: true,
shouldStopConversation: false,
shouldStartConversation: false,
},
reducers: {},
},
}));
// Mock react-redux
vi.mock("react-redux", () => ({
useSelector: vi.fn((selector) => {
// Mock the selector to return different agent states based on test needs
return {
curAgentState: AgentState.RUNNING,
};
}),
Provider: ({ children }: { children: React.ReactNode }) => children,
// Mock the agent store
vi.mock("#/stores/agent-store", () => ({
useAgentStore: vi.fn(),
}));
// Mock the custom hooks
@ -86,13 +66,25 @@ vi.mock("react-i18next", async () => {
});
describe("ServerStatus", () => {
// Helper function to mock agent store with specific state
const mockAgentStore = (agentState: AgentState) => {
vi.mocked(useAgentStore).mockReturnValue({
curAgentState: agentState,
setCurrentAgentState: vi.fn(),
reset: vi.fn(),
});
};
afterEach(() => {
vi.clearAllMocks();
});
it("should render server status with different conversation statuses", () => {
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
// Test RUNNING status
const { rerender } = renderWithProviders(
const { rerender } = renderWithQueryAndI18n(
<ServerStatus conversationStatus="RUNNING" />,
);
expect(screen.getByText("Running")).toBeInTheDocument();
@ -112,7 +104,11 @@ describe("ServerStatus", () => {
it("should show context menu when clicked with RUNNING status", async () => {
const user = userEvent.setup();
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithQueryAndI18n(<ServerStatus conversationStatus="RUNNING" />);
const statusContainer = screen.getByText("Running").closest("div");
expect(statusContainer).toBeInTheDocument();
@ -128,7 +124,11 @@ describe("ServerStatus", () => {
it("should show context menu when clicked with STOPPED status", async () => {
const user = userEvent.setup();
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
// Mock agent store to return STOPPED state
mockAgentStore(AgentState.STOPPED);
renderWithQueryAndI18n(<ServerStatus conversationStatus="STOPPED" />);
const statusContainer = screen.getByText("Server Stopped").closest("div");
expect(statusContainer).toBeInTheDocument();
@ -144,7 +144,11 @@ describe("ServerStatus", () => {
it("should not show context menu when clicked with other statuses", async () => {
const user = userEvent.setup();
renderWithProviders(<ServerStatus conversationStatus="STARTING" />);
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithQueryAndI18n(<ServerStatus conversationStatus="STARTING" />);
const statusContainer = screen.getByText("Running").closest("div");
expect(statusContainer).toBeInTheDocument();
@ -163,7 +167,10 @@ describe("ServerStatus", () => {
// Clear previous calls
mockStopConversationMutate.mockClear();
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithQueryAndI18n(<ServerStatus conversationStatus="RUNNING" />);
const statusContainer = screen.getByText("Running").closest("div");
await user.click(statusContainer!);
@ -182,7 +189,10 @@ describe("ServerStatus", () => {
// Clear previous calls
mockStartConversationMutate.mockClear();
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
// Mock agent store to return STOPPED state
mockAgentStore(AgentState.STOPPED);
renderWithQueryAndI18n(<ServerStatus conversationStatus="STOPPED" />);
const statusContainer = screen.getByText("Server Stopped").closest("div");
await user.click(statusContainer!);
@ -198,7 +208,11 @@ describe("ServerStatus", () => {
it("should close context menu after stop server action", async () => {
const user = userEvent.setup();
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithQueryAndI18n(<ServerStatus conversationStatus="RUNNING" />);
const statusContainer = screen.getByText("Running").closest("div");
await user.click(statusContainer!);
@ -214,7 +228,11 @@ describe("ServerStatus", () => {
it("should close context menu after start server action", async () => {
const user = userEvent.setup();
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
// Mock agent store to return STOPPED state
mockAgentStore(AgentState.STOPPED);
renderWithQueryAndI18n(<ServerStatus conversationStatus="STOPPED" />);
const statusContainer = screen.getByText("Server Stopped").closest("div");
await user.click(statusContainer!);
@ -229,7 +247,10 @@ describe("ServerStatus", () => {
});
it("should handle null conversation status", () => {
renderWithProviders(<ServerStatus conversationStatus={null} />);
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithQueryAndI18n(<ServerStatus conversationStatus={null} />);
const statusText = screen.getByText("Running");
expect(statusText).toBeInTheDocument();
@ -247,7 +268,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should render stop server button when status is RUNNING", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="RUNNING"
@ -260,7 +281,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should render start server button when status is STOPPED", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="STOPPED"
@ -273,7 +294,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should not render stop server button when onStopServer is not provided", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="RUNNING"
@ -284,7 +305,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should not render start server button when onStartServer is not provided", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="STOPPED"
@ -298,7 +319,7 @@ describe("ServerStatusContextMenu", () => {
const user = userEvent.setup();
const onStopServer = vi.fn();
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="RUNNING"
@ -316,7 +337,7 @@ describe("ServerStatusContextMenu", () => {
const user = userEvent.setup();
const onStartServer = vi.fn();
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="STOPPED"
@ -331,7 +352,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should render correct text content for stop server button", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="RUNNING"
@ -345,7 +366,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should render correct text content for start server button", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="STOPPED"
@ -361,7 +382,7 @@ describe("ServerStatusContextMenu", () => {
it("should call onClose when context menu is closed", () => {
const onClose = vi.fn();
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
onClose={onClose}
@ -376,7 +397,7 @@ describe("ServerStatusContextMenu", () => {
});
it("should not render any buttons for other conversation statuses", () => {
renderWithProviders(
renderWithQueryAndI18n(
<ServerStatusContextMenu
{...defaultProps}
conversationStatus="STARTING"

View File

@ -5,6 +5,18 @@ import { MemoryRouter } from "react-router";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
import { renderWithProviders } from "../../test-utils";
import { AgentState } from "#/types/agent-state";
import { useAgentStore } from "#/stores/agent-store";
import { useConversationStore } from "#/state/conversation-store";
// Mock the agent store
vi.mock("#/stores/agent-store", () => ({
useAgentStore: vi.fn(),
}));
// Mock the conversation store
vi.mock("#/state/conversation-store", () => ({
useConversationStore: vi.fn(),
}));
// Mock React Router hooks
vi.mock("react-router", async () => {
@ -47,6 +59,49 @@ describe("InteractiveChatBox", () => {
const onSubmitMock = vi.fn();
const onStopMock = vi.fn();
// Helper function to mock stores
const mockStores = (agentState: AgentState = AgentState.INIT) => {
vi.mocked(useAgentStore).mockReturnValue({
curAgentState: agentState,
setCurrentAgentState: vi.fn(),
reset: vi.fn(),
});
vi.mocked(useConversationStore).mockReturnValue({
images: [],
files: [],
addImages: vi.fn(),
addFiles: vi.fn(),
clearAllFiles: vi.fn(),
addFileLoading: vi.fn(),
removeFileLoading: vi.fn(),
addImageLoading: vi.fn(),
removeImageLoading: vi.fn(),
submittedMessage: null,
setShouldHideSuggestions: vi.fn(),
setSubmittedMessage: vi.fn(),
isRightPanelShown: true,
selectedTab: "editor" as const,
loadingFiles: [],
loadingImages: [],
messageToSend: null,
shouldShownAgentLoading: false,
shouldHideSuggestions: false,
hasRightPanelToggled: true,
setIsRightPanelShown: vi.fn(),
setSelectedTab: vi.fn(),
setShouldShownAgentLoading: vi.fn(),
removeImage: vi.fn(),
removeFile: vi.fn(),
clearImages: vi.fn(),
clearFiles: vi.fn(),
clearAllLoading: vi.fn(),
setMessageToSend: vi.fn(),
resetConversationState: vi.fn(),
setHasRightPanelToggled: vi.fn(),
});
};
// Helper function to render with Router context
const renderInteractiveChatBox = (props: any, options: any = {}) => {
return renderWithProviders(
@ -68,22 +123,12 @@ describe("InteractiveChatBox", () => {
});
it("should render", () => {
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: false,
hasSubstantiveAgentActions: false,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.INIT,
},
},
},
);
mockStores(AgentState.INIT);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
const chatBox = screen.getByTestId("interactive-chat-box");
expect(chatBox).toBeInTheDocument();
@ -91,33 +136,12 @@ describe("InteractiveChatBox", () => {
it("should set custom values", async () => {
const user = userEvent.setup();
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: true,
hasSubstantiveAgentActions: true,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.AWAITING_USER_INPUT,
},
conversation: {
isRightPanelShown: true,
shouldStopConversation: false,
shouldStartConversation: false,
images: [],
files: [],
loadingFiles: [],
loadingImages: [],
messageToSend: null,
shouldShownAgentLoading: false,
},
},
},
);
mockStores(AgentState.AWAITING_USER_INPUT);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
const textbox = screen.getByTestId("chat-input");
@ -129,22 +153,12 @@ describe("InteractiveChatBox", () => {
it("should display the image previews when images are uploaded", async () => {
const user = userEvent.setup();
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: false,
hasSubstantiveAgentActions: false,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.INIT,
},
},
},
);
mockStores(AgentState.INIT);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
// Create a larger file to ensure it passes validation
const fileContent = new Array(1024).fill("a").join(""); // 1KB file
@ -166,22 +180,12 @@ describe("InteractiveChatBox", () => {
it("should remove the image preview when the close button is clicked", async () => {
const user = userEvent.setup();
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: false,
hasSubstantiveAgentActions: false,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.INIT,
},
},
},
);
mockStores(AgentState.INIT);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
const fileContent = new Array(1024).fill("a").join(""); // 1KB file
const file = new File([fileContent], "chucknorris.png", {
@ -201,22 +205,12 @@ describe("InteractiveChatBox", () => {
it("should call onSubmit with the message and images", async () => {
const user = userEvent.setup();
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: false,
hasSubstantiveAgentActions: false,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.INIT,
},
},
},
);
mockStores(AgentState.INIT);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
const textarea = screen.getByTestId("chat-input");
@ -242,22 +236,12 @@ describe("InteractiveChatBox", () => {
it("should disable the submit button when agent is loading", async () => {
const user = userEvent.setup();
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: false,
hasSubstantiveAgentActions: false,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.LOADING,
},
},
},
);
mockStores(AgentState.LOADING);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
const button = screen.getByTestId("submit-button");
expect(button).toBeDisabled();
@ -268,23 +252,14 @@ describe("InteractiveChatBox", () => {
it("should display the stop button when agent is running and call onStop when clicked", async () => {
const user = userEvent.setup();
renderInteractiveChatBox(
{
onSubmit: onSubmitMock,
onStop: onStopMock,
isWaitingForUserInput: false,
hasSubstantiveAgentActions: true,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
},
},
},
);
mockStores(AgentState.RUNNING);
renderInteractiveChatBox({
onSubmit: onSubmitMock,
onStop: onStopMock,
});
// The stop button should be available when agent is running
const stopButton = screen.getByTestId("stop-button");
expect(stopButton).toBeInTheDocument();
@ -297,33 +272,12 @@ describe("InteractiveChatBox", () => {
const onSubmit = vi.fn();
const onStop = vi.fn();
const { rerender } = renderInteractiveChatBox(
{
onSubmit: onSubmit,
onStop: onStop,
isWaitingForUserInput: true,
hasSubstantiveAgentActions: true,
optimisticUserMessage: false,
},
{
preloadedState: {
agent: {
curAgentState: AgentState.AWAITING_USER_INPUT,
},
conversation: {
isRightPanelShown: true,
shouldStopConversation: false,
shouldStartConversation: false,
images: [],
files: [],
loadingFiles: [],
loadingImages: [],
messageToSend: null,
shouldShownAgentLoading: false,
},
},
},
);
mockStores(AgentState.AWAITING_USER_INPUT);
const { rerender } = renderInteractiveChatBox({
onSubmit: onSubmit,
onStop: onStop,
});
// Verify text input has the initial value
const textarea = screen.getByTestId("chat-input");

View File

@ -1,25 +1,23 @@
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { JupyterEditor } from "#/components/features/jupyter/jupyter";
import { useJupyterStore } from "#/state/jupyter-store";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { AgentState } from "#/types/agent-state";
import { useAgentStore } from "#/stores/agent-store";
import { useJupyterStore } from "#/state/jupyter-store";
// Mock the agent store
vi.mock("#/stores/agent-store", () => ({
useAgentStore: vi.fn(),
}));
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("JupyterEditor", () => {
const mockStore = configureStore({
reducer: {
fileState: () => ({}),
initalQuery: () => ({}),
browser: () => ({}),
chat: () => ({}),
code: () => ({}),
cmd: () => ({}),
agent: () => ({}),
securityAnalyzer: () => ({}),
status: () => ({}),
},
});
beforeEach(() => {
// Reset the Zustand store before each test
useJupyterStore.setState({
@ -32,12 +30,17 @@ describe("JupyterEditor", () => {
});
it("should have a scrollable container", () => {
// Mock agent store to return RUNNING state (not in RUNTIME_INACTIVE_STATES)
vi.mocked(useAgentStore).mockReturnValue({
curAgentState: AgentState.RUNNING,
setCurrentAgentState: vi.fn(),
reset: vi.fn(),
});
render(
<Provider store={mockStore}>
<div style={{ height: "100vh" }}>
<JupyterEditor maxWidth={800} />
</div>
</Provider>,
<div style={{ height: "100vh" }}>
<JupyterEditor maxWidth={800} />
</div>,
);
const container = screen.getByTestId("jupyter-container");

View File

@ -1,23 +1,21 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { renderWithQueryAndI18n } from "test-utils";
import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { AgentState } from "#/types/agent-state";
import { useAgentStore } from "#/stores/agent-store";
vi.mock("react-redux", async () => {
const actual = await vi.importActual("react-redux");
return {
...actual,
useDispatch: () => vi.fn(),
useSelector: () => ({
agent: {
curAgentState: AgentState.AWAITING_USER_INPUT,
},
}),
};
});
// Mock the agent store
vi.mock("#/stores/agent-store", () => ({
useAgentStore: vi.fn(),
}));
// Mock the conversation ID hook
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({ conversationId: "test-conversation-id" }),
}));
describe("MicroagentsModal - Refresh Button", () => {
const mockOnClose = vi.fn();
@ -47,10 +45,17 @@ describe("MicroagentsModal - Refresh Button", () => {
// Reset all mocks before each test
vi.clearAllMocks();
// Setup default mock for getUserConversations
// Setup default mock for getMicroagents
vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({
microagents: mockMicroagents,
});
// Mock the agent store to return a ready state
vi.mocked(useAgentStore).mockReturnValue({
curAgentState: AgentState.AWAITING_USER_INPUT,
setCurrentAgentState: vi.fn(),
reset: vi.fn(),
});
});
afterEach(() => {
@ -58,10 +63,11 @@ describe("MicroagentsModal - Refresh Button", () => {
});
describe("Refresh Button Rendering", () => {
it("should render the refresh button with correct text and test ID", () => {
renderWithProviders(<MicroagentsModal {...defaultProps} />);
it("should render the refresh button with correct text and test ID", async () => {
renderWithQueryAndI18n(<MicroagentsModal {...defaultProps} />);
const refreshButton = screen.getByTestId("refresh-microagents");
// Wait for the component to load and render the refresh button
const refreshButton = await screen.findByTestId("refresh-microagents");
expect(refreshButton).toBeInTheDocument();
expect(refreshButton).toHaveTextContent("BUTTON$REFRESH");
});
@ -71,11 +77,12 @@ describe("MicroagentsModal - Refresh Button", () => {
it("should call refetch when refresh button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<MicroagentsModal {...defaultProps} />);
renderWithQueryAndI18n(<MicroagentsModal {...defaultProps} />);
const refreshSpy = vi.spyOn(ConversationService, "getMicroagents");
const refreshButton = screen.getByTestId("refresh-microagents");
// Wait for the component to load and render the refresh button
const refreshButton = await screen.findByTestId("refresh-microagents");
await user.click(refreshButton);
expect(refreshSpy).toHaveBeenCalledTimes(1);

View File

@ -3,7 +3,8 @@ import { afterEach } from "node:test";
import { useTerminal } from "#/hooks/use-terminal";
import { Command, useCommandStore } from "#/state/command-store";
import { AgentState } from "#/types/agent-state";
import { renderWithProviders } from "../../test-utils";
import { renderWithQueryAndI18n } from "../../test-utils";
import { useAgentStore } from "#/stores/agent-store";
// Mock the WsClient context
vi.mock("#/context/ws-client-provider", () => ({
@ -22,6 +23,8 @@ interface TestTerminalComponentProps {
function TestTerminalComponent({ commands }: TestTerminalComponentProps) {
// Set commands in Zustand store
useCommandStore.setState({ commands });
// Set agent state in Zustand store
useAgentStore.setState({ curAgentState: AgentState.RUNNING });
const ref = useTerminal();
return <div ref={ref} />;
}
@ -57,11 +60,7 @@ describe("useTerminal", () => {
});
it("should render", () => {
renderWithProviders(<TestTerminalComponent commands={[]} />, {
preloadedState: {
agent: { curAgentState: AgentState.RUNNING },
},
});
renderWithQueryAndI18n(<TestTerminalComponent commands={[]} />);
});
it("should render the commands in the terminal", () => {
@ -70,11 +69,7 @@ describe("useTerminal", () => {
{ content: "hello", type: "output" },
];
renderWithProviders(<TestTerminalComponent commands={commands} />, {
preloadedState: {
agent: { curAgentState: AgentState.RUNNING },
},
});
renderWithQueryAndI18n(<TestTerminalComponent commands={commands} />);
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
@ -92,11 +87,7 @@ describe("useTerminal", () => {
{ content: secret, type: "output" },
];
renderWithProviders(<TestTerminalComponent commands={commands} />, {
preloadedState: {
agent: { curAgentState: AgentState.RUNNING },
},
});
renderWithQueryAndI18n(<TestTerminalComponent commands={commands} />);
// This test is no longer relevant as secrets filtering has been removed
});

View File

@ -1,4 +1,3 @@
import { useSelector } from "react-redux";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
@ -7,7 +6,6 @@ import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { TrajectoryActions } from "../trajectory/trajectory-actions";
import { createChatMessage } from "#/services/chat-service";
import { InteractiveChatBox } from "./interactive-chat-box";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { isOpenHandsAction } from "#/types/core/guards";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
@ -19,6 +17,7 @@ import { Messages } from "./messages";
import { ChatSuggestions } from "./chat-suggestions";
import { ScrollProvider } from "#/context/scroll-context";
import { useInitialQueryStore } from "#/stores/initial-query-store";
import { useAgentStore } from "#/stores/agent-store";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
@ -63,7 +62,7 @@ export function ChatInterface() {
} = useScrollToBottom(scrollRef);
const { data: config } = useConfig();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const [feedbackPolarity, setFeedbackPolarity] = React.useState<
"positive" | "negative"

View File

@ -1,4 +1,3 @@
import { useSelector } from "react-redux";
import { isFileImage } from "#/utils/is-file-image";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { validateFiles } from "#/utils/file-validation";
@ -7,8 +6,8 @@ import { AgentState } from "#/types/agent-state";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { GitControlBar } from "./git-control-bar";
import { useConversationStore } from "#/state/conversation-store";
import { useAgentStore } from "#/stores/agent-store";
import { processFiles, processImages } from "#/utils/file-processing";
import { RootState } from "#/store";
interface InteractiveChatBoxProps {
onSubmit: (message: string, images: File[], files: File[]) => void;
@ -30,9 +29,7 @@ export function InteractiveChatBox({
addImageLoading,
removeImageLoading,
} = useConversationStore();
const curAgentState = useSelector(
(state: RootState) => state.agent.curAgentState,
);
const { curAgentState } = useAgentStore();
const { data: conversation } = useActiveConversation();
// Helper function to validate and filter files

View File

@ -1,7 +1,5 @@
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { useEffect } from "react";
import { RootState } from "#/store";
import { useStatusStore } from "#/state/status-store";
import { useWsClient } from "#/context/ws-client-provider";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
@ -14,6 +12,7 @@ import { cn } from "#/utils/utils";
import { AgentLoading } from "./agent-loading";
import { useConversationStore } from "#/state/conversation-store";
import CircleErrorIcon from "#/icons/circle-error.svg?react";
import { useAgentStore } from "#/stores/agent-store";
export interface AgentStatusProps {
className?: string;
@ -30,7 +29,7 @@ export function AgentStatus({
}: AgentStatusProps) {
const { t } = useTranslation();
const { setShouldShownAgentLoading } = useConversationStore();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const { curStatusMessage } = useStatusStore();
const { webSocketStatus } = useWsClient();
const { data: conversation } = useActiveConversation();

View File

@ -1,16 +1,15 @@
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import DebugStackframeDot from "#/icons/debug-stackframe-dot.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { ConversationStatus } from "#/types/conversation-status";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { ServerStatusContextMenu } from "./server-status-context-menu";
import { useStartConversation } from "#/hooks/mutation/use-start-conversation";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useStopConversation } from "#/hooks/mutation/use-stop-conversation";
import { useAgentStore } from "#/stores/agent-store";
export interface ServerStatusProps {
className?: string;
@ -23,7 +22,7 @@ export function ServerStatus({
}: ServerStatusProps) {
const [showContextMenu, setShowContextMenu] = useState(false);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const { t } = useTranslation();
const { conversationId } = useConversationId();

View File

@ -1,17 +1,16 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { I18nKey } from "#/i18n/declaration";
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { Typography } from "#/ui/typography";
import { MicroagentsModalHeader } from "./microagents-modal-header";
import { MicroagentsLoadingState } from "./microagents-loading-state";
import { MicroagentsEmptyState } from "./microagents-empty-state";
import { MicroagentItem } from "./microagent-item";
import { useAgentStore } from "#/stores/agent-store";
interface MicroagentsModalProps {
onClose: () => void;
@ -19,7 +18,7 @@ interface MicroagentsModalProps {
export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
const { t } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const [expandedAgents, setExpandedAgents] = useState<Record<string, boolean>>(
{},
);

View File

@ -1,4 +1,3 @@
import { useSelector } from "react-redux";
import { FaExternalLinkAlt } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
@ -6,10 +5,10 @@ import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import { useConversationId } from "#/hooks/use-conversation-id";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { RootState } from "#/store";
import { useAgentStore } from "#/stores/agent-store";
export function VSCodeTooltipContent() {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const { t } = useTranslation();
const { conversationId } = useConversationId();

View File

@ -1,7 +1,5 @@
import React from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { RootState } from "#/store";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
import { JupyterCell } from "./jupyter-cell";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
@ -9,6 +7,7 @@ import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { I18nKey } from "#/i18n/declaration";
import JupyterLargeIcon from "#/icons/jupyter-large.svg?react";
import { WaitingForRuntimeMessage } from "../chat/waiting-for-runtime-message";
import { useAgentStore } from "#/stores/agent-store";
import { useJupyterStore } from "#/state/jupyter-store";
interface JupyterEditorProps {
@ -16,8 +15,9 @@ interface JupyterEditorProps {
}
export function JupyterEditor({ maxWidth }: JupyterEditorProps) {
const { curAgentState } = useAgentStore();
const cells = useJupyterStore((state) => state.cells);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const jupyterRef = React.useRef<HTMLDivElement>(null);

View File

@ -1,13 +1,12 @@
import { useSelector } from "react-redux";
import { RootState } from "#/store";
import { useTerminal } from "#/hooks/use-terminal";
import "@xterm/xterm/css/xterm.css";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { cn } from "#/utils/utils";
import { WaitingForRuntimeMessage } from "../chat/waiting-for-runtime-message";
import { useAgentStore } from "#/stores/agent-store";
function Terminal() {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);

View File

@ -1,13 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { useSelector } from "react-redux";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useConversationId } from "../use-conversation-id";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { useAgentStore } from "#/stores/agent-store";
export const useConversationMicroagents = () => {
const { conversationId } = useConversationId();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
return useQuery({
queryKey: ["conversation", conversationId, "microagents"],

View File

@ -1,10 +1,8 @@
import { useSelector } from "react-redux";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useAgentStore } from "#/stores/agent-store";
export const useHandleRuntimeActive = () => {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const runtimeActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);

View File

@ -1,7 +1,6 @@
import { useSelector } from "react-redux";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useActiveConversation } from "./query/use-active-conversation";
import { useAgentStore } from "#/stores/agent-store";
/**
* Hook to determine if the runtime is ready for operations
@ -10,7 +9,7 @@ import { useActiveConversation } from "./query/use-active-conversation";
*/
export const useRuntimeIsReady = (): boolean => {
const { data: conversation } = useActiveConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
return (
conversation?.status === "RUNNING" &&

View File

@ -1,13 +1,12 @@
import { FitAddon } from "@xterm/addon-fit";
import { Terminal } from "@xterm/xterm";
import React from "react";
import { useSelector } from "react-redux";
import { Command, useCommandStore } from "#/state/command-store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useWsClient } from "#/context/ws-client-provider";
import { getTerminalCommand } from "#/services/terminal-service";
import { parseTerminalOutput } from "#/utils/parse-terminal-output";
import { RootState } from "#/store";
import { useAgentStore } from "#/stores/agent-store";
/*
NOTE: Tests for this hook are indirectly covered by the tests for the XTermTerminal component.
@ -38,7 +37,7 @@ const persistentLastCommandIndex = { current: 0 };
export const useTerminal = () => {
const { send } = useWsClient();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const commands = useCommandStore((state) => state.commands);
const terminal = React.useRef<Terminal | null>(null);
const fitAddon = React.useRef<FitAddon | null>(null);

View File

@ -1,13 +1,12 @@
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import React from "react";
import { FileDiffViewer } from "#/components/features/diff-viewer/file-diff-viewer";
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
import { useGetGitChanges } from "#/hooks/query/use-get-git-changes";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { RandomTip } from "#/components/features/tips/random-tip";
import { useAgentStore } from "#/stores/agent-store";
// Error message patterns
const GIT_REPO_ERROR_PATTERN = /not a git repository/i;
@ -34,7 +33,7 @@ function GitChanges() {
null,
);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
const isNotGitRepoError =

View File

@ -1,6 +1,5 @@
import React from "react";
import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { useQueryClient } from "@tanstack/react-query";
import { useConversationId } from "#/hooks/use-conversation-id";
@ -8,7 +7,7 @@ import { useCommandStore } from "#/state/command-store";
import { useEffectOnce } from "#/hooks/use-effect-once";
import { useJupyterStore } from "#/state/jupyter-store";
import { useConversationStore } from "#/state/conversation-store";
import { setCurrentAgentState } from "#/state/agent-slice";
import { useAgentStore } from "#/stores/agent-store";
import { AgentState } from "#/types/agent-state";
import { useBatchFeedback } from "#/hooks/query/use-batch-feedback";
@ -39,9 +38,11 @@ function AppContent() {
const { data: isAuthed } = useIsAuthed();
const { providers } = useUserProviders();
const { resetConversationState } = useConversationStore();
const dispatch = useDispatch();
const navigate = useNavigate();
const clearTerminal = useCommandStore((state) => state.clearTerminal);
const setCurrentAgentState = useAgentStore(
(state) => state.setCurrentAgentState,
);
const clearJupyter = useJupyterStore((state) => state.clearJupyter);
const queryClient = useQueryClient();
@ -89,14 +90,19 @@ function AppContent() {
clearTerminal();
clearJupyter();
resetConversationState();
dispatch(setCurrentAgentState(AgentState.LOADING));
}, [conversationId, clearTerminal, resetConversationState]);
setCurrentAgentState(AgentState.LOADING);
}, [
conversationId,
clearTerminal,
setCurrentAgentState,
resetConversationState,
]);
useEffectOnce(() => {
clearTerminal();
clearJupyter();
resetConversationState();
dispatch(setCurrentAgentState(AgentState.LOADING));
setCurrentAgentState(AgentState.LOADING);
});
return (

View File

@ -1,17 +1,16 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
import { VSCODE_IN_NEW_TAB } from "#/utils/feature-flags";
import { WaitingForRuntimeMessage } from "#/components/features/chat/waiting-for-runtime-message";
import { useAgentStore } from "#/stores/agent-store";
function VSCodeTab() {
const { t } = useTranslation();
const { data, isLoading, error } = useVSCodeUrl();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curAgentState } = useAgentStore();
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [isCrossProtocol, setIsCrossProtocol] = useState(false);

View File

@ -1,10 +1,10 @@
import { setCurrentAgentState } from "#/state/agent-slice";
import store from "#/store";
import { ObservationMessage } from "#/types/message";
import { useJupyterStore } from "#/state/jupyter-store";
import { useCommandStore } from "#/state/command-store";
import ObservationType from "#/types/observation-type";
import { useBrowserStore } from "#/stores/browser-store";
import { useAgentStore } from "#/stores/agent-store";
import { AgentState } from "#/types/agent-state";
export function handleObservationMessage(message: ObservationMessage) {
switch (message.observation) {
@ -43,7 +43,11 @@ export function handleObservationMessage(message: ObservationMessage) {
}
break;
case ObservationType.AGENT_STATE_CHANGED:
store.dispatch(setCurrentAgentState(message.extras.agent_state));
if (typeof message.extras.agent_state === "string") {
useAgentStore
.getState()
.setCurrentAgentState(message.extras.agent_state as AgentState);
}
break;
case ObservationType.DELEGATE:
case ObservationType.READ:

View File

@ -1,18 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
import { AgentState } from "#/types/agent-state";
export const agentSlice = createSlice({
name: "agent",
initialState: {
curAgentState: AgentState.LOADING,
},
reducers: {
setCurrentAgentState: (state, action) => {
state.curAgentState = action.payload;
},
},
});
export const { setCurrentAgentState } = agentSlice.actions;
export default agentSlice.reducer;

View File

@ -1,9 +1,6 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import agentReducer from "./state/agent-slice";
export const rootReducer = combineReducers({
agent: agentReducer,
});
export const rootReducer = combineReducers({});
const store = configureStore({
reducer: rootReducer,

View File

@ -0,0 +1,21 @@
import { create } from "zustand";
import { AgentState } from "#/types/agent-state";
interface AgentStateData {
curAgentState: AgentState;
}
interface AgentStore extends AgentStateData {
setCurrentAgentState: (state: AgentState) => void;
reset: () => void;
}
const initialState: AgentStateData = {
curAgentState: AgentState.LOADING,
};
export const useAgentStore = create<AgentStore>((set) => ({
...initialState,
setCurrentAgentState: (state: AgentState) => set({ curAgentState: state }),
reset: () => set(initialState),
}));