diff --git a/frontend/.eslintrc b/frontend/.eslintrc index dcdefc7039..e259190dc5 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -25,6 +25,8 @@ "state" ] }], + // For https://stackoverflow.com/questions/55844608/stuck-with-eslint-error-i-e-separately-loops-should-be-avoided-in-favor-of-arra + "no-restricted-syntax": "off", "import/prefer-default-export": "off", "no-underscore-dangle": "off", "jsx-a11y/no-static-element-interactions": "off", diff --git a/frontend/src/components/SettingModal.tsx b/frontend/src/components/SettingModal.tsx index 2c226d6af2..fdac5880eb 100644 --- a/frontend/src/components/SettingModal.tsx +++ b/frontend/src/components/SettingModal.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; import { Modal, ModalContent, @@ -13,13 +14,18 @@ import { import { KeyboardEvent } from "@react-types/shared/src/events"; import { INITIAL_AGENTS, - changeAgent, - changeDirectory as sendChangeDirectorySocketMessage, - changeModel, fetchModels, fetchAgents, INITIAL_MODELS, + sendSettings, } from "../services/settingsService"; +import { + setModel, + setAgent, + setWorkspaceDirectory, +} from "../state/settingsSlice"; +import store, { RootState } from "../store"; +import socket from "../socket/socket"; interface Props { isOpen: boolean; @@ -34,18 +40,15 @@ const cachedAgents = JSON.parse( ); function SettingModal({ isOpen, onClose }: Props): JSX.Element { - const [workspaceDirectory, setWorkspaceDirectory] = useState( - localStorage.getItem("workspaceDirectory") || "./workspace", - ); - const [model, setModel] = useState( - localStorage.getItem("model") || "gpt-3.5-turbo-1106", + const model = useSelector((state: RootState) => state.settings.model); + const agent = useSelector((state: RootState) => state.settings.agent); + const workspaceDirectory = useSelector( + (state: RootState) => state.settings.workspaceDirectory, ); + const [supportedModels, setSupportedModels] = useState( cachedModels.length > 0 ? cachedModels : INITIAL_MODELS, ); - const [agent, setAgent] = useState( - localStorage.getItem("agent") || "MonologueAgent", - ); const [supportedAgents, setSupportedAgents] = useState( cachedAgents.length > 0 ? cachedAgents : INITIAL_AGENTS, ); @@ -62,9 +65,7 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element { }, []); const handleSaveCfg = () => { - sendChangeDirectorySocketMessage(workspaceDirectory); - changeModel(model); - changeAgent(agent); + sendSettings(socket, { model, agent, workspaceDirectory }); localStorage.setItem("model", model); localStorage.setItem("workspaceDirectory", workspaceDirectory); localStorage.setItem("agent", agent); @@ -87,7 +88,9 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element { label="OpenDevin Workspace Directory" defaultValue={workspaceDirectory} placeholder="Default: ./workspace" - onChange={(e) => setWorkspaceDirectory(e.target.value)} + onChange={(e) => + store.dispatch(setWorkspaceDirectory(e.target.value)) + } /> { - setModel(key as string); + store.dispatch(setModel(key as string)); }} onKeyDown={(e: KeyboardEvent) => e.continuePropagation()} defaultFilter={customFilter} @@ -122,7 +125,7 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element { defaultSelectedKey={agent} // className="max-w-xs" onSelectionChange={(key) => { - setAgent(key as string); + store.dispatch(setAgent(key as string)); }} onKeyDown={(e: KeyboardEvent) => e.continuePropagation()} defaultFilter={customFilter} diff --git a/frontend/src/services/settingsService.ts b/frontend/src/services/settingsService.ts index 45eed57398..3ff8d1be6d 100644 --- a/frontend/src/services/settingsService.ts +++ b/frontend/src/services/settingsService.ts @@ -1,4 +1,3 @@ -import socket from "../socket/socket"; import { appendAssistantMessage } from "../state/chatSlice"; import { setInitialized } from "../state/taskSlice"; import store from "../store"; @@ -27,22 +26,32 @@ export const INITIAL_AGENTS = ["MonologueAgent", "CodeActAgent"]; export type Agent = (typeof INITIAL_AGENTS)[number]; -function changeSetting(setting: string, value: string): void { - const event = { action: "initialize", args: { [setting]: value } }; +// Map Redux settings to socket event arguments +const SETTINGS_MAP = new Map([ + ["model", "model"], + ["agent", "agent_cls"], + ["workspaceDirectory", "directory"], +]); + +// Send settings to the server +export function sendSettings( + socket: WebSocket, + reduxSettings: { [id: string]: string }, + appendMessages: boolean = true, +): void { + const socketSettings = Object.fromEntries( + Object.entries(reduxSettings).map(([setting, value]) => [ + SETTINGS_MAP.get(setting) || setting, + value, + ]), + ); + const event = { action: "initialize", args: socketSettings }; const eventString = JSON.stringify(event); socket.send(eventString); store.dispatch(setInitialized(false)); - store.dispatch(appendAssistantMessage(`Changed ${setting} to "${value}"`)); -} - -export function changeModel(model: Model): void { - changeSetting("model", model); -} - -export function changeAgent(agent: Agent): void { - changeSetting("agent_cls", agent); -} - -export function changeDirectory(directory: string): void { - changeSetting("directory", directory); + if (appendMessages) { + for (const [setting, value] of Object.entries(reduxSettings)) { + store.dispatch(appendAssistantMessage(`Set ${setting} to "${value}"`)); + } + } } diff --git a/frontend/src/socket/socket.ts b/frontend/src/socket/socket.ts index 4ec70278ad..5284a4fc7f 100644 --- a/frontend/src/socket/socket.ts +++ b/frontend/src/socket/socket.ts @@ -3,6 +3,7 @@ import { ActionMessage, ObservationMessage } from "../types/Message"; import { appendError } from "../state/errorsSlice"; import { handleActionMessage } from "./actions"; import { handleObservationMessage } from "./observations"; +import { sendSettings } from "../services/settingsService"; type SocketMessage = ActionMessage | ObservationMessage; @@ -10,6 +11,10 @@ const WS_URL = `ws://${window.location.host}/ws`; const socket = new WebSocket(WS_URL); +socket.addEventListener("open", () => { + const { settings } = store.getState(); + sendSettings(socket, settings, false); +}); socket.addEventListener("message", (event) => { const socketMessage = JSON.parse(event.data) as SocketMessage; if ("action" in socketMessage) { diff --git a/frontend/src/state/settingsSlice.ts b/frontend/src/state/settingsSlice.ts new file mode 100644 index 0000000000..c24b92daf6 --- /dev/null +++ b/frontend/src/state/settingsSlice.ts @@ -0,0 +1,27 @@ +import { createSlice } from "@reduxjs/toolkit"; + +export const settingsSlice = createSlice({ + name: "settings", + initialState: { + model: localStorage.getItem("model") || "gpt-4-0125-preview", + agent: localStorage.getItem("agent") || "MonologueAgent", + workspaceDirectory: + localStorage.getItem("workspaceDirectory") || "./workspace", + }, + reducers: { + setModel: (state, action) => { + state.model = action.payload; + }, + setAgent: (state, action) => { + state.agent = action.payload; + }, + setWorkspaceDirectory: (state, action) => { + state.workspaceDirectory = action.payload; + }, + }, +}); + +export const { setModel, setAgent, setWorkspaceDirectory } = + settingsSlice.actions; + +export default settingsSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 03639d02c5..d92d02758f 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -4,6 +4,7 @@ import chatReducer from "./state/chatSlice"; import codeReducer from "./state/codeSlice"; import taskReducer from "./state/taskSlice"; import errorsReducer from "./state/errorsSlice"; +import settingsReducer from "./state/settingsSlice"; const store = configureStore({ reducer: { @@ -12,6 +13,7 @@ const store = configureStore({ code: codeReducer, task: taskReducer, errors: errorsReducer, + settings: settingsReducer, }, }); diff --git a/opendevin/server/session.py b/opendevin/server/session.py index f1f2e0088a..def7536b71 100644 --- a/opendevin/server/session.py +++ b/opendevin/server/session.py @@ -40,7 +40,6 @@ class Session: self.controller: Optional[AgentController] = None self.agent: Optional[Agent] = None self.agent_task = None - asyncio.create_task(self.create_controller(), name="create controller") # FIXME: starting the docker container synchronously causes a websocket error... async def send_error(self, message): """Sends an error message to the client.