Skip to content

Commit

Permalink
add console features
Browse files Browse the repository at this point in the history
  • Loading branch information
bigsk1 committed Jul 18, 2024
1 parent 2439767 commit 4a0a4c7
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 157 deletions.
149 changes: 86 additions & 63 deletions backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,55 +358,100 @@ async def chat(request: ChatRequest):
logger.error(f"Error in chat endpoint: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

def get_executable():
def get_shell():
return "cmd.exe" if platform.system() == "Windows" else "/bin/bash"

def execute_shell_command(command, cwd):
shell = get_shell()
if platform.system() == "Windows":
return shutil.which("cmd.exe")
result = subprocess.run([shell, "/c", command], capture_output=True, text=True, cwd=cwd)
else:
return shutil.which("bash")
result = subprocess.run([shell, "-c", command], capture_output=True, text=True, cwd=cwd)
return result.stdout + result.stderr

@api_router.post("/execute")
async def execute_command(request: CommandRequest):
try:
# Set the working directory to the projects folder
def ensure_in_projects_dir():
if not os.getcwd().startswith(PROJECTS_DIR):
os.chdir(PROJECTS_DIR)


@api_router.post("/console/execute")
async def console_execute_command(request: CommandRequest):
try:
ensure_in_projects_dir()
cwd = os.getcwd()
command_parts = request.command.split()
if command_parts[0] == 'python':
# If it's a Python command, use the Python interpreter
executable = shutil.which("python")
if not executable:
raise FileNotFoundError("Python executable not found")
result = subprocess.run(
[executable] + command_parts[1:],
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False
)
cmd = command_parts[0].lower()

if cmd == "cd":
return handle_cd(command_parts[1] if len(command_parts) > 1 else "")
elif cmd == "ls" or (cmd == "dir" and platform.system() == "Windows"):
return handle_ls(cwd)
elif cmd == "pwd":
return handle_pwd(cwd)
elif cmd == "echo":
return handle_echo(command_parts[1:], cwd)
elif cmd == "cat" or cmd == "type":
return handle_cat(command_parts[1] if len(command_parts) > 1 else "", cwd)
elif cmd == "mkdir":
return handle_mkdir(command_parts[1] if len(command_parts) > 1 else "", cwd)
elif cmd == "touch" or cmd == "echo.":
return handle_touch(command_parts[1] if len(command_parts) > 1 else "", cwd)
else:
# For other commands, use the system shell
executable = get_executable()
if not executable:
raise FileNotFoundError("Shell executable not found")
result = subprocess.run(
[executable, "/c" if os.name == "nt" else "-c", request.command],
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False
)

output = result.stdout + result.stderr
return {"result": output, "cwd": os.getcwd()}
except FileNotFoundError as e:
logger.error(f"FileNotFoundError in execute_command: {str(e)}", exc_info=True)
raise HTTPException(status_code=404, detail=str(e))
output = execute_shell_command(request.command, cwd)
return {"result": output, "cwd": os.path.relpath(cwd, PROJECTS_DIR)}
except Exception as e:
logger.error(f"Error in execute_command: {str(e)}", exc_info=True)
logger.error(f"Error in console_execute_command: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

def handle_cd(path):
try:
new_path = os.path.abspath(os.path.join(os.getcwd(), path))
if os.path.exists(new_path) and os.path.isdir(new_path) and new_path.startswith(PROJECTS_DIR):
os.chdir(new_path)
current_dir = os.path.relpath(os.getcwd(), PROJECTS_DIR)
return {"result": f"Changed directory to: {current_dir}", "cwd": current_dir}
else:
return {"result": f"Directory not found or access denied: {path}", "cwd": os.path.relpath(os.getcwd(), PROJECTS_DIR)}
except Exception as e:
return {"result": f"Error changing directory: {str(e)}", "cwd": os.path.relpath(os.getcwd(), PROJECTS_DIR)}

def handle_ls(cwd):
items = os.listdir(cwd)
return {"result": "\n".join(items), "cwd": os.path.relpath(cwd, PROJECTS_DIR)}

def handle_pwd(cwd):
return {"result": cwd, "cwd": os.path.relpath(cwd, PROJECTS_DIR)}

def handle_echo(args, cwd):
return {"result": " ".join(args), "cwd": os.path.relpath(cwd, PROJECTS_DIR)}

def handle_cat(filename, cwd):
try:
with open(os.path.join(cwd, filename), 'r') as file:
content = file.read()
return {"result": content, "cwd": os.path.relpath(cwd, PROJECTS_DIR)}
except Exception as e:
return {"result": f"Error reading file: {str(e)}", "cwd": os.path.relpath(cwd, PROJECTS_DIR)}

def handle_mkdir(dirname, cwd):
try:
os.mkdir(os.path.join(cwd, dirname))
return {"result": f"Directory created: {dirname}", "cwd": os.path.relpath(cwd, PROJECTS_DIR)}
except Exception as e:
return {"result": f"Error creating directory: {str(e)}", "cwd": os.path.relpath(cwd, PROJECTS_DIR)}

def handle_touch(filename, cwd):
try:
with open(os.path.join(cwd, filename), 'a'):
os.utime(os.path.join(cwd, filename), None)
return {"result": f"File touched: {filename}", "cwd": os.path.relpath(cwd, PROJECTS_DIR)}
except Exception as e:
return {"result": f"Error touching file: {str(e)}", "cwd": os.path.relpath(cwd, PROJECTS_DIR)}

@api_router.get("/console/cwd")
async def console_get_current_working_directory():
ensure_in_projects_dir()
return {"cwd": os.path.relpath(os.getcwd(), PROJECTS_DIR)}

@api_router.post("/run_python")
async def run_python(request: CommandRequest):
try:
Expand All @@ -431,28 +476,6 @@ async def run_python(request: CommandRequest):
logger.error(f"Error in run_python: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))

@api_router.get("/cwd")
async def get_current_working_directory():
return {"cwd": os.path.relpath(os.getcwd(), PROJECTS_DIR)}

@api_router.get("/ls")
async def list_directory(path: str = "."):
try:
full_path = os.path.join(PROJECTS_DIR, path)
contents = os.listdir(full_path)
return DirectoryContents(path=path, contents=contents)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@api_router.post("/cd")
async def change_directory(request: CommandRequest):
try:
new_dir = os.path.join(PROJECTS_DIR, request.command)
os.chdir(new_dir)
return {"cwd": os.path.relpath(os.getcwd(), PROJECTS_DIR)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@api_router.post("/pip_install")
async def pip_install(request: CommandRequest):
try:
Expand All @@ -462,7 +485,7 @@ async def pip_install(request: CommandRequest):
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
executable=get_executable()
executable=get_shell()
)
output = result.stdout.decode('utf-8') + result.stderr.decode('utf-8')
return {"result": output}
Expand Down
118 changes: 62 additions & 56 deletions frontend/src/components/Console.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,77 @@
import React, { useState, useRef } from 'react';
import axios, { AxiosError } from 'axios';
import {
Box, Input, VStack, Text, useColorMode, Button,
} from '@chakra-ui/react';
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { Box, Input, VStack, Text, useColorMode } from '@chakra-ui/react';

const API_URL = 'http://127.0.0.1:8000';

interface ErrorResponse {
detail: string;
interface TerminalLine {
content: string;
isCommand: boolean;
}

const Console: React.FC = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState<string[]>([]);
const [cwd, setCwd] = useState('');
const outputRef = useRef<HTMLDivElement>(null);
const [history, setHistory] = useState<TerminalLine[]>([]);
const [currentDirectory, setCurrentDirectory] = useState('');
const { colorMode } = useColorMode();
const terminalRef = useRef<HTMLDivElement>(null);

const runCommand = async (command: string) => {
useEffect(() => {
fetchCurrentDirectory();
}, []);

useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, [history]);

const fetchCurrentDirectory = async () => {
try {
const trimmedInput = command.trim();
setOutput(prev => [...prev, `$ ${trimmedInput}`]);

if (trimmedInput === 'clear') {
setOutput([]);
} else {
const response = await axios.post(`${API_URL}/api/execute`, { command: trimmedInput });
setOutput(prev => [...prev, response.data.result]);
if (response.data.cwd) {
setCwd(response.data.cwd);
}
}
const response = await axios.get(`${API_URL}/api/console/cwd`);
setCurrentDirectory(response.data.cwd);
} catch (error) {
console.error('Error executing command:', error);
let errorMessage = 'Error executing command. Please check the console for details.';
console.error('Error fetching current directory:', error);
addToHistory('Error fetching current directory', false);
}
};

if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<ErrorResponse>;
if (axiosError.response) {
errorMessage = `Error: ${axiosError.response.data.detail || 'Unknown error'}`;
} else if (axiosError.request) {
errorMessage = 'Error: No response received from server';
} else {
errorMessage = `Error: ${axiosError.message}`;
}
}
const addToHistory = (content: string, isCommand: boolean) => {
setHistory(prev => [...prev, { content, isCommand }]);
};

setOutput(prev => [...prev, errorMessage]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;

addToHistory(`${currentDirectory}$ ${input}`, true);

if (input.trim().toLowerCase() === 'clear') {
setHistory([]);
setInput('');
return;
}
setInput('');
};

const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
await runCommand(input);
try {
const response = await axios.post(`${API_URL}/api/console/execute`, { command: input });
if (response.data.result) {
addToHistory(response.data.result, false);
}
if (response.data.cwd) {
setCurrentDirectory(response.data.cwd);
}
} catch (error) {
console.error('Error executing command:', error);
addToHistory('Error executing command. Please check the console for details.', false);
}

setInput('');
};

return (
<VStack spacing={4} align="stretch" height="100%">
<Box
ref={outputRef}
ref={terminalRef}
bg={colorMode === 'dark' ? 'gray.800' : 'gray.100'}
color={colorMode === 'dark' ? 'white' : 'black'}
p={4}
Expand All @@ -71,26 +82,21 @@ const Console: React.FC = () => {
whiteSpace="pre-wrap"
textAlign="left"
>
{output.map((line, index) => (
<Text key={index}>{line}</Text>
{history.map((line, index) => (
<Text key={index} fontWeight={line.isCommand ? 'bold' : 'normal'}>
{line.content}
</Text>
))}
</Box>
<Box display="flex">
<Text mr={2} fontWeight="bold">{cwd}$</Text>
<form onSubmit={handleSubmit}>
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Enter command (type 'clear' to clear console)"
flex={1}
placeholder={`${currentDirectory}$ `}
bg={colorMode === 'dark' ? 'gray.700' : 'white'}
color={colorMode === 'dark' ? 'white' : 'black'}
/>
</Box>
<Box>
<Button onClick={() => runCommand('python --version')} mr={2}>Python Version</Button>
<Button onClick={() => runCommand('conda info --envs')} mr={2}>Conda Environments</Button>
<Button onClick={() => runCommand('pip list')} mr={2}>Installed Packages</Button>
<Button onClick={() => setOutput([])} colorScheme="red">Clear Console</Button>
</Box>
</form>
</VStack>
);
};
Expand Down
15 changes: 0 additions & 15 deletions projects/claude_plus_ascii.py

This file was deleted.

19 changes: 0 additions & 19 deletions projects/sample.py

This file was deleted.

8 changes: 4 additions & 4 deletions shared_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@
2. Provide the full path starting with 'projects/'
3. Include any initial content if specified
Example usage:
Example usages:
create_file({{"path": "/hello.txt", "content": "Hello, world!"}})
After using a tool, report the result to the user and ask if they want to take any further actions.
After using a tool and completing the task, report the result to the user and ask if they want to take any further actions.
Important guidelines:
1. Always use the appropriate tool for file operations and searches. Don't just describe actions, perform them.
2. All file operations are restricted to the 'projects' directory for security reasons. You cannot access or modify files outside this directory.
3. The system will ensure operations are within this directory.
2. All file operations are restricted to the 'projects' directory for security reasons. You cannot access or modify files above this directory.
3. The system will ensure operations are within this directory. Do not create a projects folder as you already start out in the projects folder. If asked to make an app create a new folder if needed and add files inside that folder.
4. After using a tool, report the result and ask if further actions are needed.
5. For uploaded files, analyze the contents immediately without using the read_file tool.
6. In auto mode, iterate through tasks autonomously, providing regular progress updates.
Expand Down

0 comments on commit 4a0a4c7

Please sign in to comment.