""" AutoDev - Web UI Serves a live dashboard showing LLM activity, file tree, and file contents. Uses SSE (Server-Sent Events) for real-time updates. No external dependencies. """ import http.server import json import os import queue import threading import urllib.parse from . import config # Global event queue โ logger pushes events, SSE endpoint streams them _event_queue: queue.Queue = queue.Queue(maxsize=5000) _workdir: str = "" def push_event(event_type: str, data: dict): """Push an event to all connected web clients.""" try: _event_queue.put_nowait({"type": event_type, "data": data}) except queue.Full: pass class WebHandler(http.server.BaseHTTPRequestHandler): def log_message(self, format, *args): pass # Suppress default logging def do_GET(self): try: parsed = urllib.parse.urlparse(self.path) path = parsed.path params = urllib.parse.parse_qs(parsed.query) if path == "/": self._serve_html() elif path == "/events": self._serve_sse() elif path == "/api/files": self._serve_file_tree() elif path == "/api/file": filepath = params.get("path", [""])[0] self._serve_file_content(filepath) else: self.send_error(404) except (BrokenPipeError, ConnectionResetError, OSError): pass def _serve_html(self): self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(HTML_PAGE.encode("utf-8")) def _serve_sse(self): self.send_response(200) self.send_header("Content-Type", "text/event-stream") self.send_header("Cache-Control", "no-cache") self.send_header("Connection", "keep-alive") self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() try: while True: try: event = _event_queue.get(timeout=1) line = f"data: {json.dumps(event)}\n\n" self.wfile.write(line.encode("utf-8")) self.wfile.flush() except queue.Empty: # Send keepalive self.wfile.write(b": keepalive\n\n") self.wfile.flush() except (BrokenPipeError, ConnectionResetError, OSError): pass def _serve_file_tree(self): files = [] for root, dirs, filenames in os.walk(_workdir): # Skip hidden dirs and autodev backups dirs[:] = [d for d in dirs if not d.startswith(".") and d != "__pycache__"] for fname in sorted(filenames): if fname.startswith("."): continue full = os.path.join(root, fname) rel = os.path.relpath(full, _workdir) try: size = os.path.getsize(full) except OSError: size = 0 files.append({"path": rel, "size": size}) self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps(files).encode("utf-8")) def _serve_file_content(self, filepath: str): if not filepath: self.send_error(400, "Missing path parameter") return full = os.path.realpath(os.path.join(_workdir, filepath)) if not full.startswith(os.path.realpath(_workdir)): self.send_error(403, "Path outside workspace") return try: with open(full, "r") as f: content = f.read() self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"path": filepath, "content": content}).encode("utf-8")) except (IOError, UnicodeDecodeError): self.send_error(404, "File not found or not readable") def start_web_server(port: int, workdir: str): """Start the web UI server in a background thread.""" global _workdir _workdir = workdir server = http.server.HTTPServer(("0.0.0.0", port), WebHandler) server.daemon_threads = True thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() return server HTML_PAGE = """