- {isOpen && node.children && (
+ {isOpen && children && (
- {node.children.map((child, index) => (
-
+ {children.map((child, index) => (
+
))}
)}
diff --git a/frontend/src/components/file-explorer/utils.test.ts b/frontend/src/components/file-explorer/utils.test.ts
deleted file mode 100644
index fd51731dad67..000000000000
--- a/frontend/src/components/file-explorer/utils.test.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { removeEmptyNodes } from "./utils";
-
-test("removeEmptyNodes removes empty arrays", () => {
- const root = {
- name: "a",
- children: [
- {
- name: "b",
- children: [],
- },
- {
- name: "c",
- children: [
- {
- name: "d",
- children: [],
- },
- ],
- },
- ],
- };
-
- expect(removeEmptyNodes(root)).toEqual({
- name: "a",
- children: [
- {
- name: "b",
- },
- {
- name: "c",
- children: [
- {
- name: "d",
- },
- ],
- },
- ],
- });
-});
diff --git a/frontend/src/components/file-explorer/utils.ts b/frontend/src/components/file-explorer/utils.ts
deleted file mode 100644
index 08060ff3f758..000000000000
--- a/frontend/src/components/file-explorer/utils.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { WorkspaceFile } from "#/services/fileService";
-
-export const removeEmptyNodes = (root: WorkspaceFile): WorkspaceFile => {
- if (root.children) {
- const children = root.children
- .map(removeEmptyNodes)
- .filter((node) => node !== undefined);
- return {
- ...root,
- children: children.length ? children : undefined,
- };
- }
- return root;
-};
diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts
index 4603eaa3ae1a..83c4fd78eb66 100644
--- a/frontend/src/services/actions.ts
+++ b/frontend/src/services/actions.ts
@@ -1,6 +1,6 @@
import { setScreenshotSrc, setUrl } from "#/state/browserSlice";
-import { addAssistantMessage } from "#/state/chatSlice";
-import { setCode, updatePath } from "#/state/codeSlice";
+import { addAssistantMessage, addUserMessage } from "#/state/chatSlice";
+import { setCode, setActiveFilepath } from "#/state/codeSlice";
import { appendInput } from "#/state/commandSlice";
import { appendJupyterInput } from "#/state/jupyterSlice";
import { setRootTask } from "#/state/taskSlice";
@@ -24,11 +24,15 @@ const messageActions = {
},
[ActionType.WRITE]: (message: ActionMessage) => {
const { path, content } = message.args;
- store.dispatch(updatePath(path));
+ store.dispatch(setActiveFilepath(path));
store.dispatch(setCode(content));
},
[ActionType.MESSAGE]: (message: ActionMessage) => {
- store.dispatch(addAssistantMessage(message.args.content));
+ if (message.source === "user") {
+ store.dispatch(addUserMessage(message.args.content));
+ } else {
+ store.dispatch(addAssistantMessage(message.args.content));
+ }
},
[ActionType.FINISH]: (message: ActionMessage) => {
store.dispatch(addAssistantMessage(message.message));
diff --git a/frontend/src/services/fileService.ts b/frontend/src/services/fileService.ts
index 17fe10ec7e5f..21e8f7f054d2 100644
--- a/frontend/src/services/fileService.ts
+++ b/frontend/src/services/fileService.ts
@@ -1,8 +1,3 @@
-export type WorkspaceFile = {
- name: string;
- children?: WorkspaceFile[];
-};
-
export async function selectFile(file: string): Promise
{
const res = await fetch(`/api/select-file?file=${file}`);
const data = await res.json();
@@ -30,8 +25,8 @@ export async function uploadFiles(files: FileList) {
}
}
-export async function getWorkspace(): Promise {
- const res = await fetch("/api/refresh-files");
+export async function listFiles(path: string = "/"): Promise {
+ const res = await fetch(`/api/list-files?path=${path}`);
const data = await res.json();
- return data as WorkspaceFile;
+ return data as string[];
}
diff --git a/frontend/src/state/codeSlice.ts b/frontend/src/state/codeSlice.ts
index 9e8af5e2030d..3f5dd6571a5a 100644
--- a/frontend/src/state/codeSlice.ts
+++ b/frontend/src/state/codeSlice.ts
@@ -1,12 +1,9 @@
import { createSlice } from "@reduxjs/toolkit";
-import { INode, flattenTree } from "react-accessible-treeview";
-import { IFlatMetadata } from "react-accessible-treeview/dist/TreeView/utils";
-import { WorkspaceFile } from "#/services/fileService";
export const initialState = {
code: "",
- selectedIds: [] as number[],
- workspaceFolder: { name: "" } as WorkspaceFile,
+ path: "",
+ refreshID: 0,
};
export const codeSlice = createSlice({
@@ -16,56 +13,15 @@ export const codeSlice = createSlice({
setCode: (state, action) => {
state.code = action.payload;
},
- updatePath: (state, action) => {
- const path = action.payload;
- const pathParts = path.split("/");
- let current = state.workspaceFolder;
-
- for (let i = 0; i < pathParts.length - 1; i += 1) {
- const folderName = pathParts[i];
- let folder = current.children?.find((file) => file.name === folderName);
-
- if (!folder) {
- folder = { name: folderName, children: [] };
- current.children?.push(folder);
- }
-
- current = folder;
- }
-
- const fileName = pathParts[pathParts.length - 1];
- if (!current.children?.find((file) => file.name === fileName)) {
- current.children?.push({ name: fileName });
- }
-
- const data = flattenTree(state.workspaceFolder);
- const checkPath: (
- file: INode,
- pathIndex: number,
- ) => boolean = (file, pathIndex) => {
- if (pathIndex < 0) {
- if (file.parent === null) return true;
- return false;
- }
- if (pathIndex >= 0 && file.name !== pathParts[pathIndex]) {
- return false;
- }
- return checkPath(
- data.find((f) => f.id === file.parent)!,
- pathIndex - 1,
- );
- };
- const selected = data
- .filter((file) => checkPath(file, pathParts.length - 1))
- .map((file) => file.id) as number[];
- state.selectedIds = selected;
+ setActiveFilepath: (state, action) => {
+ state.path = action.payload;
},
- updateWorkspace: (state, action) => {
- state.workspaceFolder = action.payload;
+ setRefreshID: (state, action) => {
+ state.refreshID = action.payload;
},
},
});
-export const { setCode, updatePath, updateWorkspace } = codeSlice.actions;
+export const { setCode, setActiveFilepath, setRefreshID } = codeSlice.actions;
export default codeSlice.reducer;
diff --git a/frontend/src/types/Message.tsx b/frontend/src/types/Message.tsx
index 1ff375e9050a..3d3c5e25cde7 100644
--- a/frontend/src/types/Message.tsx
+++ b/frontend/src/types/Message.tsx
@@ -1,4 +1,7 @@
export interface ActionMessage {
+ // Either 'agent' or 'user'
+ source: string;
+
// The action to be taken
action: string;
diff --git a/opendevin/runtime/docker/ssh_box.py b/opendevin/runtime/docker/ssh_box.py
index 9d7e0cc3dc5d..dd50c38ad322 100644
--- a/opendevin/runtime/docker/ssh_box.py
+++ b/opendevin/runtime/docker/ssh_box.py
@@ -253,7 +253,13 @@ def __init__(
raise e
time.sleep(5)
self.setup_user()
- self.start_ssh_session()
+
+ try:
+ self.start_ssh_session()
+ except pxssh.ExceptionPxssh as e:
+ self.close()
+ raise e
+
# make sure /tmp always exists
self.execute('mkdir -p /tmp')
# set git config
diff --git a/opendevin/server/listen.py b/opendevin/server/listen.py
index 71911282fded..cf21cbfa2deb 100644
--- a/opendevin/server/listen.py
+++ b/opendevin/server/listen.py
@@ -1,3 +1,4 @@
+import os
import shutil
import uuid
import warnings
@@ -6,7 +7,7 @@
with warnings.catch_warnings():
warnings.simplefilter('ignore')
import litellm
-from fastapi import Depends, FastAPI, Response, UploadFile, WebSocket, status
+from fastapi import Depends, FastAPI, Request, Response, UploadFile, WebSocket, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -17,7 +18,6 @@
from opendevin.core.config import config
from opendevin.core.logger import opendevin_logger as logger
from opendevin.llm import bedrock
-from opendevin.runtime import files
from opendevin.server.agent import agent_manager
from opendevin.server.auth import get_sid_from_token, sign_token
from opendevin.server.session import message_stack, session_manager
@@ -225,18 +225,33 @@ async def del_messages(
return {'ok': True}
-@app.get('/api/refresh-files')
-def refresh_files():
+@app.get('/api/list-files')
+def list_files(request: Request, path: str = '/'):
"""
- Refresh files.
+ List files.
- To refresh files:
+ To list files:
```sh
- curl http://localhost:3000/api/refresh-files
+ curl http://localhost:3000/api/list-files
```
"""
- structure = files.get_folder_structure(Path(str(config.workspace_base)))
- return structure.to_dict()
+ if path.startswith('/'):
+ path = path[1:]
+ abs_path = os.path.join(config.workspace_base, path)
+ try:
+ files = os.listdir(abs_path)
+ except Exception as e:
+ logger.error(f'Error listing files: {e}', exc_info=False)
+ return JSONResponse(
+ status_code=status.HTTP_404_NOT_FOUND,
+ content={'error': 'Path not found'},
+ )
+ files = [os.path.join(path, f) for f in files]
+ files = [
+ f + '/' if os.path.isdir(os.path.join(config.workspace_base, f)) else f
+ for f in files
+ ]
+ return files
@app.get('/api/select-file')
diff --git a/opendevin/server/mock/listen.py b/opendevin/server/mock/listen.py
index eb3752b0a444..5a4d36a13d83 100644
--- a/opendevin/server/mock/listen.py
+++ b/opendevin/server/mock/listen.py
@@ -62,14 +62,9 @@ async def get_message_total():
return {'msg_total': 0}
-@app.get('/api/refresh-files')
+@app.get('/api/list-files')
def refresh_files():
- return {
- 'name': 'workspace',
- 'children': [
- {'name': 'hello_world.py', 'children': []},
- ],
- }
+ return ['hello_world.py']
if __name__ == '__main__':
diff --git a/opendevin/server/session/msg_stack.py b/opendevin/server/session/msg_stack.py
index 0c8a64d0e806..6e0862af4426 100644
--- a/opendevin/server/session/msg_stack.py
+++ b/opendevin/server/session/msg_stack.py
@@ -99,7 +99,7 @@ async def _del_messages(self, del_sid: str):
new_data = {}
for sid, msgs in data.items():
if sid != del_sid:
- new_data[sid] = [Message.from_dict(msg) for msg in msgs]
+ new_data[sid] = msgs
# Move the file pointer to the beginning of the file to overwrite the original contents
file.seek(0)
# clean previous content