diff --git a/src/backend/debug_logger.py b/src/backend/debug_logger.py
index d45e9a2..00be55c 100644
--- a/src/backend/debug_logger.py
+++ b/src/backend/debug_logger.py
@@ -1,12 +1,72 @@
+import ast
+import traceback
+from datetime import datetime
+
+
class DebugLogger:
- """
- Platzhalter für das Logging [1]
- """
def __init__(self):
self.logs = []
- def log(self, message):
- self.logs.append(message)
+ # ---------------- INTERNAL ----------------
+ def _entry(self, level: str, message: str) -> dict:
+ entry = {
+ "level": level,
+ "message": message,
+ "time": datetime.now().strftime("%H:%M:%S"),
+ }
+ self.logs.append(entry)
+ return entry
- def get_logs(self):
- return self.logs
\ No newline at end of file
+ # ---------------- PUBLIC API ----------------
+ def log(self, message: str):
+ self._entry("INFO", message)
+
+ def log_stdout(self, output: str):
+ if output.strip():
+ self._entry("OUTPUT", output.rstrip())
+
+ def log_stderr(self, error: str):
+ if error.strip():
+ self._entry("ERROR", error.rstrip())
+
+ def log_syntax_error(self, filepath: str, error: SyntaxError):
+ msg = f"SyntaxError in {filepath} (line {error.lineno}): {error.msg}"
+ self._entry("SYNTAX", msg)
+
+ def log_result(self, returncode: int):
+ level = "SUCCESS" if returncode == 0 else "FAIL"
+ msg = f"Process exited with code {returncode}"
+ self._entry(level, msg)
+
+ def log_exception(self, exc: Exception):
+ msg = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
+ self._entry("EXCEPTION", msg.rstrip())
+
+ # ---------------- ACCESSORS ----------------
+ def get_logs(self) -> list:
+ return self.logs
+
+ def get_logs_by_level(self, level: str) -> list:
+ return [e for e in self.logs if e["level"] == level.upper()]
+
+ def has_errors(self) -> bool:
+ return any(e["level"] in ("ERROR", "SYNTAX", "EXCEPTION", "FAIL") for e in self.logs)
+
+ def clear(self):
+ self.logs = []
+
+ def format(self) -> str:
+ lines = []
+ icons = {
+ "INFO": "ℹ",
+ "OUTPUT": "▶",
+ "ERROR": "✖",
+ "SYNTAX": "⚠",
+ "EXCEPTION": "💥",
+ "SUCCESS": "✔",
+ "FAIL": "✖",
+ }
+ for e in self.logs:
+ icon = icons.get(e["level"], "•")
+ lines.append(f"[{e['time']}] {icon} {e['message']}")
+ return "\n".join(lines)
\ No newline at end of file
diff --git a/src/backend/execution_engine.py b/src/backend/execution_engine.py
index 9ab6723..2bc891c 100644
--- a/src/backend/execution_engine.py
+++ b/src/backend/execution_engine.py
@@ -1,25 +1,62 @@
+import ast
import subprocess
import sys
from pathlib import Path
+from backend.debug_logger import DebugLogger
+
class ExecutionEngine:
+ def __init__(self):
+ self.logger = DebugLogger()
+
+ # ---------------- SYNTAX CHECK ----------------
+ def check_syntax(self, filepath: str) -> bool:
+ """Parse the file with ast. Returns True if valid, False if not."""
+ path = Path(filepath)
+ if path.suffix.lower() != ".py":
+ return True # skip non-Python files
+
+ try:
+ source = path.read_text(encoding="utf-8")
+ ast.parse(source, filename=str(path))
+ self.logger.log(f"Syntax OK: {path.name}")
+ return True
+ except SyntaxError as e:
+ self.logger.log_syntax_error(str(path), e)
+ return False
+ except Exception as e:
+ self.logger.log_exception(e)
+ return False
+
+ # ---------------- RUN ----------------
def run_file(self, filepath: str) -> dict:
+ self.logger.clear()
path = Path(filepath)
+ # --- guards ---
if not path.exists():
- return {"stdout": "", "stderr": f"File not found: {filepath}", "returncode": -1}
+ self.logger.log_stderr(f"File not found: {filepath}")
+ return self._result("", f"File not found: {filepath}", -1)
ext = path.suffix.lower()
if ext == ".py":
+ # syntax check before running
+ if not self.check_syntax(filepath):
+ err = self.logger.format()
+ return self._result("", err, -1)
cmd = [sys.executable, str(path)]
elif ext == ".js":
cmd = ["node", str(path)]
elif ext == ".sh":
cmd = ["bash", str(path)]
else:
- return {"stdout": "", "stderr": f"Unsupported file type: {ext}", "returncode": -1}
+ msg = f"Unsupported file type: {ext}"
+ self.logger.log_stderr(msg)
+ return self._result("", msg, -1)
+ # --- execute ---
+ self.logger.log(f"Running: {path.name}")
try:
result = subprocess.run(
cmd,
@@ -28,14 +65,35 @@ class ExecutionEngine:
timeout=30,
cwd=str(path.parent),
)
- return {
- "stdout": result.stdout,
- "stderr": result.stderr,
- "returncode": result.returncode,
- }
+ self.logger.log_stdout(result.stdout)
+ self.logger.log_stderr(result.stderr)
+ self.logger.log_result(result.returncode)
+
+ return self._result(result.stdout, result.stderr, result.returncode)
+
except subprocess.TimeoutExpired:
- return {"stdout": "", "stderr": "Timed out after 30 seconds.", "returncode": -1}
+ msg = "Timed out after 30 seconds."
+ self.logger.log_stderr(msg)
+ return self._result("", msg, -1)
except FileNotFoundError as e:
- return {"stdout": "", "stderr": f"Runtime not found: {e}", "returncode": -1}
+ msg = f"Runtime not found: {e}"
+ self.logger.log_stderr(msg)
+ return self._result("", msg, -1)
except Exception as e:
- return {"stdout": "", "stderr": f"Error: {e}", "returncode": -1}
\ No newline at end of file
+ self.logger.log_exception(e)
+ return self._result("", str(e), -1)
+
+ # ---------------- HELPERS ----------------
+ def _result(self, stdout: str, stderr: str, returncode: int) -> dict:
+ return {
+ "stdout": stdout,
+ "stderr": stderr,
+ "returncode": returncode,
+ "success": returncode == 0,
+ "logs": self.logger.get_logs(),
+ "log_text": self.logger.format(),
+ "has_errors": self.logger.has_errors(),
+ }
+
+ def get_logger(self) -> DebugLogger:
+ return self.logger
\ No newline at end of file
diff --git a/src/main.py b/src/main.py
index bcdfb63..65eb1bf 100644
--- a/src/main.py
+++ b/src/main.py
@@ -19,7 +19,7 @@ from backend.execution_engine import ExecutionEngine
st.set_page_config(
page_title="AI Code Editor",
page_icon="💻",
- layout="wide",
+ layout="wide"
)
# ---------------- FOLDER PICKER ----------------
@@ -38,7 +38,6 @@ def render_sidebar():
root_folder = st.session_state.get("root_folder")
with st.sidebar:
- # --- Button on top ---
if st.button("📂 Choose Folder", key="pick_folder"):
picked = pick_folder()
if picked:
@@ -50,7 +49,6 @@ def render_sidebar():
st.markdown("---")
- # --- Tree below ---
if root_folder:
try:
root_path = Path(root_folder)
@@ -73,6 +71,58 @@ def render_sidebar():
return None
+# ---------------- OUTPUT PANEL ----------------
+def render_output(result: dict):
+ """Render structured execution output from ExecutionEngine."""
+ success = result["returncode"] == 0
+ status_icon = "✅" if success else "❌"
+ status_text = f"Exited {result['returncode']}"
+
+ st.markdown(f"**{status_icon} {status_text}**")
+
+ # stdout
+ if result["stdout"].strip():
+ st.caption("stdout")
+ st.code(result["stdout"].rstrip(), language="text")
+
+ # stderr
+ if result["stderr"].strip():
+ st.caption("stderr")
+ st.code(result["stderr"].rstrip(), language="text")
+
+ # debug log entries (collapsed by default)
+ if result.get("logs"):
+ with st.expander("🪲 Debug log", expanded=False):
+ level_colors = {
+ "INFO": "#6c7086",
+ "OUTPUT": "#a6e3a1",
+ "ERROR": "#f38ba8",
+ "SYNTAX": "#fab387",
+ "EXCEPTION": "#f38ba8",
+ "SUCCESS": "#a6e3a1",
+ "FAIL": "#f38ba8",
+ }
+ icons = {
+ "INFO": "ℹ", "OUTPUT": "▶", "ERROR": "✖",
+ "SYNTAX": "⚠", "EXCEPTION": "💥", "SUCCESS": "✔", "FAIL": "✖",
+ }
+ lines = []
+ for e in result["logs"]:
+ color = level_colors.get(e["level"], "#cdd6f4")
+ icon = icons.get(e["level"], "•")
+ lines.append(
+ f'[{e["time"]}] '
+ f'{icon} {e["level"]} '
+ f'{e["message"]}'
+ )
+ st.markdown(
+ "
".join(lines),
+ unsafe_allow_html=True
+ )
+
+ if not result["stdout"].strip() and not result["stderr"].strip():
+ st.caption("No output.")
+
# ---------------- MAIN ----------------
def main():
file_manager = FileManager()
@@ -103,7 +153,7 @@ def main():
# ---------------- FILE SWITCH ----------------
if current_file != st.session_state.active_file:
st.session_state.active_file = current_file
- st.session_state.run_result = None # clear output on file switch
+ st.session_state.run_result = None
if current_file:
content = file_manager.read_file(current_file)
st.session_state.editor_content = content
@@ -119,7 +169,6 @@ def main():
col1, col2 = st.columns([3, 2])
with col1:
- # --- File name + Run button on same row ---
title_col, btn_col = st.columns([5, 1])
with title_col:
st.subheader(f"✏️ {Path(st.session_state.active_file).name}")
@@ -138,7 +187,6 @@ def main():
file_manager.save_file(st.session_state.active_file, new_value)
st.success("Saved ✔")
- # --- Run execution ---
if run_clicked:
with st.spinner("Running…"):
st.session_state.run_result = execution_engine.run_file(
@@ -149,23 +197,10 @@ def main():
chat = ChatInterface()
chat.display_chat(chat_manager)
- # --- Output panel ---
- result = st.session_state.run_result
- if result is not None:
+ if st.session_state.run_result is not None:
st.markdown("---")
- success = result["returncode"] == 0
-
- status = "✅ Exited 0" if success else f"❌ Exited {result['returncode']}"
- st.caption(f"Output — {status}")
-
- if result["stdout"]:
- st.code(result["stdout"], language="text")
-
- if result["stderr"]:
- st.code(result["stderr"], language="text")
-
- if not result["stdout"] and not result["stderr"]:
- st.caption("No output.")
+ st.caption("Output")
+ render_output(st.session_state.run_result)
else:
st.info("Select a file from the explorer")