Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor session management #1810

Merged
merged 89 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
8a13c22
refactor session mgmt
rbren May 15, 2024
2cf184c
defer file handling to runtime
rbren May 15, 2024
4758599
add todo
rbren May 15, 2024
e854fe5
Merge branch 'main' into rb/api-sessions
rbren May 15, 2024
b8a7714
refactor sessions a bit more
rbren May 15, 2024
68efd09
remove messages logic from FE
rbren May 15, 2024
833e94b
fix up socket handshake
rbren May 15, 2024
ebe9fe4
refactor frontend auth a bit
rbren May 15, 2024
8160402
first pass at redoing file explorer
rbren May 15, 2024
34640d2
implement directory suffix
rbren May 15, 2024
1956844
fix up file tree
rbren May 15, 2024
dae8568
close agent on websocket close
rbren May 15, 2024
e973237
remove session saving
rbren May 15, 2024
c130a06
move file refresh
rbren May 15, 2024
500dff9
remove getWorkspace
rbren May 15, 2024
9eb29ec
plumb path/code differently
rbren May 15, 2024
9b80827
fix build issues
rbren May 15, 2024
87c4288
fix the tests
rbren May 15, 2024
ca285f1
Merge branch 'main' into rb/api-sessions
rbren May 16, 2024
19db96f
fix npm build
rbren May 16, 2024
b19d4bb
add session rehydration
rbren May 16, 2024
863d5d0
fix event serialization
rbren May 16, 2024
4206db8
logspam
rbren May 16, 2024
a3732d9
fix user message rehydration
rbren May 16, 2024
6d6c645
add get_event fn
rbren May 16, 2024
d3d7efd
agent state restoration
rbren May 16, 2024
fe4e41f
change history tracking for codeact
rbren May 16, 2024
20aeffe
fix responsiveness of init
rbren May 16, 2024
2579245
Merge branch 'main' into rb/api-sessions
rbren May 16, 2024
76c07a4
fix lint
rbren May 16, 2024
1415928
lint
rbren May 16, 2024
bfcef57
delint
rbren May 16, 2024
d4b569d
fix prop
rbren May 16, 2024
e74beb2
update tests
rbren May 16, 2024
706b9a7
Merge branch 'main' into rb/api-sessions
rbren May 17, 2024
f71d7b4
Merge branch 'main' into rb/api-sessions
rbren May 18, 2024
386fe72
Merge branch 'main' into rb/api-sessions
rbren May 18, 2024
df9c2b4
logspam
rbren May 18, 2024
2e8b2e9
lint
rbren May 18, 2024
ba8f384
fix test
rbren May 18, 2024
088be2e
Merge branch 'main' into rb/api-sessions
rbren May 20, 2024
2409028
revert codeact
rbren May 20, 2024
0ae31c2
change fileService to use API
rbren May 20, 2024
e8d54a1
fix up session loading
rbren May 20, 2024
41e2b96
delint
rbren May 20, 2024
38f32d6
delint
rbren May 20, 2024
dbb3551
fix integration tests
rbren May 20, 2024
b721bca
Merge branch 'main' into rb/api-sessions
rbren May 20, 2024
d7c0999
revert test
rbren May 20, 2024
c3fd1a8
fix up access to options endpoints
rbren May 20, 2024
57be4d4
fix initial files load
rbren May 20, 2024
15d1a3a
delint
rbren May 20, 2024
52e995b
fix file initialization
rbren May 20, 2024
35d8004
fix mock server
rbren May 20, 2024
4edfe0f
fixl int
rbren May 20, 2024
4eb486d
fix auth for html
rbren May 20, 2024
37af906
Merge branch 'main' into rb/api-sessions
xingyaoww May 21, 2024
d971f91
Update frontend/src/i18n/translation.json
rbren May 21, 2024
badbfa2
Merge branch 'main' into rb/api-sessions
rbren May 21, 2024
4df4239
refactor sessions and sockets
rbren May 21, 2024
27aaa06
avoid reinitializing the same session
rbren May 21, 2024
0499085
fix reconnect issue
rbren May 21, 2024
e7c03f0
change up intro message
rbren May 21, 2024
faeda8c
more guards on reinit
rbren May 21, 2024
3a7ad8c
rename agent_session
rbren May 21, 2024
a639e83
delint
rbren May 21, 2024
e67fb29
Merge branch 'main' into rb/api-sessions
rbren May 21, 2024
97c4d16
fix a bunch of tests
rbren May 21, 2024
8caacd5
delint
rbren May 21, 2024
b478668
fix last test
rbren May 21, 2024
f560023
remove code editor context
rbren May 21, 2024
245b419
fix build
rbren May 21, 2024
033aa8b
fix any
rbren May 21, 2024
1412763
fix dot notation
rbren May 21, 2024
6792acb
Merge branch 'main' into rb/api-sessions
neubig May 21, 2024
591dd16
Merge branch 'main' into rb/api-sessions
rbren May 22, 2024
1030bd8
Update frontend/src/services/api.ts
rbren May 22, 2024
ad97f41
Merge branch 'rb/api-sessions' of ssh://github.com/opendevin/opendevi…
rbren May 22, 2024
0c4dff7
fix up error handling
rbren May 22, 2024
a2af497
Update opendevin/server/session/agent.py
rbren May 22, 2024
105340e
Update opendevin/server/session/agent.py
rbren May 22, 2024
5d8abb3
Update frontend/src/services/session.ts
rbren May 22, 2024
10ccb6a
fix build errs
rbren May 22, 2024
c908a67
Merge branch 'rb/api-sessions' of ssh://github.com/opendevin/opendevi…
rbren May 22, 2024
b1b6e8f
fix else
rbren May 22, 2024
1358f40
add closed state
rbren May 22, 2024
5637aab
delint
rbren May 22, 2024
faf4ac4
Update opendevin/server/session/session.py
rbren May 22, 2024
4715765
Merge branch 'main' into rb/api-sessions
rbren May 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 7 additions & 24 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useDisclosure } from "@nextui-org/react";
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { Toaster } from "react-hot-toast";
import CogTooth from "#/assets/cog-tooth";
import ChatInterface from "#/components/chat/ChatInterface";
Expand All @@ -8,15 +8,13 @@ import { Container, Orientation } from "#/components/Resizable";
import Workspace from "#/components/Workspace";
import LoadPreviousSessionModal from "#/components/modals/load-previous-session/LoadPreviousSessionModal";
import SettingsModal from "#/components/modals/settings/SettingsModal";
import { fetchMsgTotal } from "#/services/session";
import Socket from "#/services/socket";
import { ResFetchMsgTotal } from "#/types/ResponseType";
import "./App.css";
import AgentControlBar from "./components/AgentControlBar";
import AgentStatusBar from "./components/AgentStatusBar";
import Terminal from "./components/terminal/Terminal";
import { initializeAgent } from "./services/agent";
import { settingsAreUpToDate } from "./services/settings";
import Session from "#/services/session";
import { getToken } from "#/services/auth";
import { settingsAreUpToDate } from "#/services/settings";

interface Props {
setSettingOpen: (isOpen: boolean) => void;
Expand All @@ -43,8 +41,6 @@ function Controls({ setSettingOpen }: Props): JSX.Element {
let initOnce = false;

function App(): JSX.Element {
const [isWarned, setIsWarned] = useState(false);

const {
isOpen: settingsModalIsOpen,
onOpen: onSettingsModalOpen,
Expand All @@ -57,31 +53,18 @@ function App(): JSX.Element {
onOpenChange: onLoadPreviousSessionModalOpenChange,
} = useDisclosure();

const getMsgTotal = () => {
if (isWarned) return;
fetchMsgTotal()
.then((data: ResFetchMsgTotal) => {
if (data.msg_total > 0) {
onLoadPreviousSessionModalOpen();
setIsWarned(true);
}
})
.catch();
};

useEffect(() => {
if (initOnce) return;
initOnce = true;

if (!settingsAreUpToDate()) {
onSettingsModalOpen();
} else if (getToken()) {
onLoadPreviousSessionModalOpen();
} else {
initializeAgent();
Session.startNewSession();
}

Socket.registerCallback("open", [getMsgTotal]);

getMsgTotal();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down
9 changes: 0 additions & 9 deletions frontend/src/api/index.ts

This file was deleted.

3 changes: 0 additions & 3 deletions frontend/src/components/AgentControlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ArrowIcon from "#/assets/arrow";
import PauseIcon from "#/assets/pause";
import PlayIcon from "#/assets/play";
import { changeAgentState } from "#/services/agentStateService";
import { clearMsgs } from "#/services/session";
import store, { RootState } from "#/store";
import AgentState from "#/types/AgentState";
import { clearMessages } from "#/state/chatSlice";
Expand Down Expand Up @@ -73,7 +72,6 @@ function AgentControlBar() {
}

if (action === AgentState.STOPPED) {
clearMsgs().then().catch();
store.dispatch(clearMessages());
} else {
setIsLoading(true);
Expand All @@ -86,7 +84,6 @@ function AgentControlBar() {
useEffect(() => {
if (curAgentState === desiredState) {
if (curAgentState === AgentState.STOPPED) {
clearMsgs().then().catch();
store.dispatch(clearMessages());
}
setIsLoading(false);
Expand Down
104 changes: 41 additions & 63 deletions frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
import Editor, { Monaco } from "@monaco-editor/react";
import { Tab, Tabs } from "@nextui-org/react";
import type { editor } from "monaco-editor";
import React, { useMemo, useState } from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { VscCode } from "react-icons/vsc";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { I18nKey } from "#/i18n/declaration";
import { selectFile } from "#/services/fileService";
import { setCode } from "#/state/codeSlice";
import { RootState } from "#/store";
import FileExplorer from "./file-explorer/FileExplorer";
import { CodeEditorContext } from "./CodeEditorContext";

function CodeEditor(): JSX.Element {
const { t } = useTranslation();
const [selectedFileAbsolutePath, setSelectedFileAbsolutePath] = useState("");
const selectedFileName = useMemo(() => {
const paths = selectedFileAbsolutePath.split("/");
return paths[paths.length - 1];
}, [selectedFileAbsolutePath]);
const codeEditorContext = useMemo(
() => ({ selectedFileAbsolutePath }),
[selectedFileAbsolutePath],
);

const dispatch = useDispatch();
const code = useSelector((state: RootState) => state.code.code);
const activeFilepath = useSelector((state: RootState) => state.code.path);

const selectedFileName = useMemo(() => {
const paths = activeFilepath.split("/");
return paths[paths.length - 1];
}, [activeFilepath]);

const handleEditorDidMount = (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
Expand All @@ -46,57 +37,44 @@ function CodeEditor(): JSX.Element {
monaco.editor.setTheme("my-theme");
};

const updateCode = async () => {
const newCode = await selectFile(activeFilepath);
setSelectedFileAbsolutePath(activeFilepath);
dispatch(setCode(newCode));
};

React.useEffect(() => {
// FIXME: we can probably move this out of the component and into state/service
if (activeFilepath) updateCode();
}, [activeFilepath]);

return (
<div className="flex h-full w-full bg-neutral-900 transition-all duration-500 ease-in-out">
<CodeEditorContext.Provider value={codeEditorContext}>
<FileExplorer />
<div className="flex flex-col min-h-0 w-full">
<Tabs
disableCursorAnimation
classNames={{
base: "border-b border-divider border-neutral-600 mb-4",
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 border-divider",
cursor: "w-full bg-neutral-600 rounded-none",
tab: "max-w-fit px-4 h-[36px]",
tabContent: "group-data-[selected=true]:text-white",
}}
aria-label="Options"
>
<Tab
key={selectedFileName.toLocaleLowerCase()}
title={selectedFileName}
<FileExplorer />
<div className="flex flex-col min-h-0 w-full">
<Tabs
disableCursorAnimation
classNames={{
base: "border-b border-divider border-neutral-600 mb-4",
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 border-divider",
cursor: "w-full bg-neutral-600 rounded-none",
tab: "max-w-fit px-4 h-[36px]",
tabContent: "group-data-[selected=true]:text-white",
}}
aria-label="Options"
>
<Tab
key={selectedFileName.toLocaleLowerCase()}
title={selectedFileName}
/>
</Tabs>
<div className="flex grow items-center justify-center">
{selectedFileName === "" ? (
<div className="flex flex-col items-center text-neutral-400">
<VscCode size={100} />
{t(I18nKey.CODE_EDITOR$EMPTY_MESSAGE)}
</div>
) : (
<Editor
height="100%"
path={selectedFileName.toLocaleLowerCase()}
defaultValue=""
value={code}
onMount={handleEditorDidMount}
/>
</Tabs>
<div className="flex grow items-center justify-center">
{selectedFileName === "" ? (
<div className="flex flex-col items-center text-neutral-400">
<VscCode size={100} />
{t(I18nKey.CODE_EDITOR$EMPTY_MESSAGE)}
</div>
) : (
<Editor
height="100%"
path={selectedFileName.toLocaleLowerCase()}
defaultValue=""
value={code}
onMount={handleEditorDidMount}
/>
)}
</div>
)}
</div>
</CodeEditorContext.Provider>
</div>
</div>
);
}
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/components/CodeEditorContext.ts

This file was deleted.

17 changes: 9 additions & 8 deletions frontend/src/components/chat/ChatInterface.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { act } from "react-dom/test-utils";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import ChatInterface from "./ChatInterface";
import Socket from "#/services/socket";
import Session from "#/services/session";
import ActionType from "#/types/ActionType";
import { addAssistantMessage } from "#/state/chatSlice";
import AgentState from "#/types/AgentState";
Expand All @@ -15,16 +15,17 @@ vi.mock("#/hooks/useTyping", () => ({
useTyping: vi.fn((text: string) => text),
}));

const socketSpy = vi.spyOn(Socket, "send");
const sessionSpy = vi.spyOn(Session, "send");
vi.spyOn(Session, "isConnected").mockImplementation(() => true);

// This is for the scrollview ref in Chat.tsx
// TODO: Move this into test setup
HTMLElement.prototype.scrollTo = vi.fn(() => {});

describe("ChatInterface", () => {
it("should render the messages and input", () => {
it("should render empty message list and input", () => {
renderWithProviders(<ChatInterface />);
expect(screen.queryAllByTestId("message")).toHaveLength(1); // initial welcome message only
expect(screen.queryAllByTestId("message")).toHaveLength(0);
});

it("should render the new message the user has typed", async () => {
Expand Down Expand Up @@ -65,7 +66,7 @@ describe("ChatInterface", () => {
expect(screen.getByText("Hello to you!")).toBeInTheDocument();
});

it("should send the a start event to the Socket", () => {
it("should send the a start event to the Session", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
agent: {
Expand All @@ -83,10 +84,10 @@ describe("ChatInterface", () => {
action: ActionType.MESSAGE,
args: { content: "my message" },
};
expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event));
expect(sessionSpy).toHaveBeenCalledWith(JSON.stringify(event));
});

it("should send the a user message event to the Socket", () => {
it("should send the a user message event to the Session", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
agent: {
Expand All @@ -104,7 +105,7 @@ describe("ChatInterface", () => {
action: ActionType.MESSAGE,
args: { content: "my message" },
};
expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event));
expect(sessionSpy).toHaveBeenCalledWith(JSON.stringify(event));
});

it("should disable the user input if agent is not initialized", () => {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/chat/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { RootState } from "#/store";
import AgentState from "#/types/AgentState";
import { sendChatMessage } from "#/services/chatService";
import { addUserMessage } from "#/state/chatSlice";
import { addUserMessage, addAssistantMessage } from "#/state/chatSlice";
import { I18nKey } from "#/i18n/declaration";
import { useScrollToBottom } from "#/hooks/useScrollToBottom";

Expand Down Expand Up @@ -58,6 +58,12 @@
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
useScrollToBottom(scrollRef);

React.useEffect(() => {
if (curAgentState === AgentState.INIT && messages.length === 0) {
dispatch(addAssistantMessage(t(I18nKey.CHAT_INTERFACE$INITIAL_MESSAGE)));
}
}, [curAgentState]);

Check warning on line 65 in frontend/src/components/chat/ChatInterface.tsx

View workflow job for this annotation

GitHub Actions / Lint frontend

React Hook React.useEffect has missing dependencies: 'dispatch', 'messages.length', and 't'. Either include them or remove the dependency array

return (
<div className="flex flex-col h-full bg-neutral-800">
<div className="flex items-center gap-2 border-b border-neutral-600 text-sm px-4 py-2">
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/file-explorer/FileExplorer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { describe, it, expect, vi, Mock } from "vitest";
import FileExplorer from "./FileExplorer";
import { uploadFiles, listFiles } from "#/services/fileService";
import toast from "#/utils/toast";
import AgentState from "#/types/AgentState";

const toastSpy = vi.spyOn(toast, "stickyError");
const toastSpy = vi.spyOn(toast, "error");

vi.mock("../../services/fileService", async () => ({
listFiles: vi.fn(async (path: string = "/") => {
Expand Down Expand Up @@ -42,7 +43,13 @@ describe("FileExplorer", () => {
it.todo("should render an empty workspace");

it.only("should refetch the workspace when clicking the refresh button", async () => {
const { getByText } = renderWithProviders(<FileExplorer />);
const { getByText } = renderWithProviders(<FileExplorer />, {
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
},
},
});
await waitFor(() => {
expect(getByText("folder1")).toBeInTheDocument();
expect(getByText("file2.ts")).toBeInTheDocument();
Expand Down
Loading
Loading