CoolFace
Apppublic

AIencoder/Axon-Pro-IDE

sourceHugging Facemitupdated 7mo agoView on Hugging Face
1likes
app.py1039 linesDownload Raw Back to root
1import warnings2warnings.filterwarnings("ignore")3import gradio as gr4import time5import sys6import subprocess7import os8import pty9import select10import signal11import fcntl12import struct13import termios14import threading15import re16import ast17import json18import tempfile19from pathlib import Path20from huggingface_hub import hf_hub_download21from typing import List, Dict22from functools import lru_cache23 24os.environ["TOKENIZERS_PARALLELISM"] = "false"25 26MODEL_REPO = "AIencoder/Qwen2.5CMR-Q4_K_M-GGUF"27MODEL_FILE = "qwen2.5cmr-q4_k_m.gguf"28DISPLAY_NUM = ":99"29SCREEN_W, SCREEN_H = 800, 60030SNIPPETS_FILE = "/tmp/axon_snippets.json"31 32# ═══════════════════════════════════════33# Virtual Display (Optimized for pygame/tkinter/turtle)34# ═══════════════════════════════════════35class VirtualDisplay:36    def __init__(self):37        self.xvfb_proc = None38        self.display = DISPLAY_NUM39        self._last_capture = None40        self._start_xvfb()41 42    def _start_xvfb(self):43        try:44            subprocess.run(["pkill", "-f", f"Xvfb {self.display}"],45                           capture_output=True, timeout=5)46            time.sleep(0.3)47            # Full-featured Xvfb: GLX for OpenGL, RENDER for anti-aliasing,48            # 24-bit color, no access control49            self.xvfb_proc = subprocess.Popen([50                "Xvfb", self.display,51                "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24",52                "-ac",                    # No access control53                "-nolisten", "tcp",       # Security54                "+extension", "GLX",      # OpenGL support (pygame)55                "+extension", "RENDER",   # Anti-aliased rendering56            ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)57            time.sleep(0.8)58            if self.xvfb_proc.poll() is None:59                os.environ["DISPLAY"] = self.display60                print(f"[Xvfb] Running on {self.display} ({SCREEN_W}x{SCREEN_H})")61            else:62                # Fallback: simpler Xvfb without extensions63                self.xvfb_proc = subprocess.Popen([64                    "Xvfb", self.display,65                    "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24", "-ac",66                ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)67                time.sleep(0.5)68                if self.xvfb_proc.poll() is None:69                    os.environ["DISPLAY"] = self.display70                    print(f"[Xvfb] Running (fallback mode)")71                else:72                    self.xvfb_proc = None73                    print("[Xvfb] Failed to start")74        except Exception as e:75            print(f"[Xvfb] {e}"); self.xvfb_proc = None76 77    def capture(self):78        """Capture the virtual display. Returns filepath or None."""79        if not self.is_running: return None80 81        # Clean up previous capture to avoid /tmp filling up82        if self._last_capture:83            try: os.unlink(self._last_capture)84            except: pass85 86        tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)87        tmp.close()88        cap_env = {**os.environ, "DISPLAY": self.display}89 90        # Method 1: xwd + convert (most reliable for Xvfb)91        try:92            xwd = subprocess.run(93                ["xwd", "-root", "-display", self.display, "-silent"],94                capture_output=True, timeout=3, env=cap_env)95            if xwd.returncode == 0 and xwd.stdout:96                conv = subprocess.run(97                    ["convert", "xwd:-", tmp.name],98                    input=xwd.stdout, capture_output=True, timeout=3)99                if conv.returncode == 0 and os.path.getsize(tmp.name) > 100:100                    self._last_capture = tmp.name101                    return tmp.name102        except: pass103 104        # Method 2: scrot105        try:106            r = subprocess.run(107                ["scrot", tmp.name],108                capture_output=True, timeout=3, env=cap_env)109            if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:110                self._last_capture = tmp.name111                return tmp.name112        except: pass113 114        # Method 3: import (ImageMagick)115        try:116            r = subprocess.run(117                f"import -window root -display {self.display} {tmp.name}",118                shell=True, capture_output=True, timeout=3, env=cap_env)119            if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 100:120                self._last_capture = tmp.name121                return tmp.name122        except: pass123 124        try: os.unlink(tmp.name)125        except: pass126        return None127 128    @property129    def is_running(self):130        return self.xvfb_proc is not None and self.xvfb_proc.poll() is None131 132    def cleanup(self):133        if self._last_capture:134            try: os.unlink(self._last_capture)135            except: pass136        if self.xvfb_proc:137            self.xvfb_proc.terminate()138            try: self.xvfb_proc.wait(timeout=3)139            except: self.xvfb_proc.kill()140 141vdisplay = VirtualDisplay()142 143# ═══════════════════════════════════════144# GUI Process Manager (pygame/tkinter/turtle)145# ═══════════════════════════════════════146GUI_LOG = "/tmp/_axon_gui.log"147 148class GUIProcessManager:149    def __init__(self):150        self.process = None151 152    def launch(self, code):153        self.stop()154        tmp = "/tmp/_axon_gui_run.py"155        with open(tmp, "w") as f: f.write(code)156 157        # Full environment for GUI frameworks158        env = os.environ.copy()159        env.update({160            "DISPLAY": DISPLAY_NUM,161            # SDL / pygame162            "SDL_VIDEODRIVER": "x11",163            "SDL_AUDIODRIVER": "dummy",        # No audio device in headless164            "PYGAME_HIDE_SUPPORT_PROMPT": "1",  # Suppress pygame welcome165            # Tkinter / general X11166            "XDG_RUNTIME_DIR": "/tmp",167            "MESA_GL_VERSION_OVERRIDE": "3.3",  # OpenGL compat168            "LIBGL_ALWAYS_SOFTWARE": "1",        # Software rendering (no GPU)169            # Python170            "PYTHONUNBUFFERED": "1",171            "PYTHONPATH": "/tmp/axon_workspace:" + env.get("PYTHONPATH", ""),172        })173 174        try:175            # Log file instead of PIPE — PIPE deadlocks pygame's output176            log_f = open(GUI_LOG, "w")177            self.process = subprocess.Popen(178                [sys.executable, "-u", tmp],  # -u = unbuffered179                stdout=log_f, stderr=log_f,180                env=env, preexec_fn=os.setsid, cwd="/tmp/axon_workspace")181 182            time.sleep(1.0)  # Give pygame time to create the window183 184            if self.process.poll() is not None:185                log_f.close()186                try:187                    with open(GUI_LOG) as f: msg = f.read().strip()188                except: msg = ""189                return f"[Exited immediately]\n{msg}" if msg else "[Exited — no output]"190 191            return f"[GUI launched — PID {self.process.pid}] Auto-refresh enabled."192        except Exception as e:193            return f"[Error] {e}"194 195    def get_log(self):196        try:197            with open(GUI_LOG) as f: return f.read()[-2000:]198        except: return ""199 200    def stop(self):201        if self.process and self.process.poll() is None:202            pid = self.process.pid203            try:204                os.killpg(os.getpgid(pid), signal.SIGTERM)205                self.process.wait(timeout=3)206            except:207                try: os.killpg(os.getpgid(pid), signal.SIGKILL)208                except: pass209            self.process = None210            return f"[Stopped PID {pid}]"211        self.process = None212        return "[No process]"213 214    @property215    def is_running(self): return self.process is not None and self.process.poll() is None216 217    def get_status(self): return f"● PID {self.process.pid}" if self.is_running else "○ idle"218 219gui_mgr = GUIProcessManager()220 221# ═══════════════════════════════════════222# File System223# ═══════════════════════════════════════224class FileSystem:225    def __init__(self):226        self.files: Dict[str, str] = {227            "main.py": '# Start coding here\nfrom utils import add\n\nresult = add(3, 7)\nprint(f"3 + 7 = {result}")',228            "utils.py": "def add(a, b):\n    return a + b\n\ndef multiply(a, b):\n    return a * b",229        }230        self.current_file = "main.py"231        self._sync_dir = "/tmp/axon_workspace"232        self._sync_to_disk()233 234    def _sync_to_disk(self):235        os.makedirs(self._sync_dir, exist_ok=True)236        for name, content in self.files.items():237            path = os.path.join(self._sync_dir, name)238            with open(path, "w") as f: f.write(content)239 240    def save_file(self, content):241        if self.current_file:242            self.files[self.current_file] = content243            path = os.path.join(self._sync_dir, self.current_file)244            os.makedirs(os.path.dirname(path) if "/" in self.current_file else self._sync_dir, exist_ok=True)245            with open(path, "w") as f: f.write(content)246 247    def get_current_file_content(self): return self.files.get(self.current_file, "")248    def set_current_file(self, f):249        if f in self.files: self.current_file = f250    def create_file(self, f, c=""):251        if f and f not in self.files:252            self.files[f] = c; self.current_file = f; self._sync_to_disk()253    def delete_file(self, f):254        if f in self.files and len(self.files) > 1:255            del self.files[f]256            path = os.path.join(self._sync_dir, f)257            if os.path.exists(path): os.unlink(path)258            self.current_file = list(self.files.keys())[0]259            return True260        return False261    def get_all_files(self): return list(self.files.keys())262 263fs = FileSystem()264 265# ═══════════════════════════════════════266# Snippet Library267# ═══════════════════════════════════════268class SnippetLibrary:269    def __init__(self):270        self.snippets = {}271        self._load()272    def _load(self):273        if os.path.exists(SNIPPETS_FILE):274            try:275                with open(SNIPPETS_FILE) as f: self.snippets = json.load(f)276            except: self.snippets = {}277    def _save(self):278        try:279            with open(SNIPPETS_FILE, "w") as f: json.dump(self.snippets, f)280        except: pass281    def add(self, name, code, tags=""):282        if not name.strip(): return "Error: name required"283        self.snippets[name.strip()] = {"code": code, "tags": tags, "created": time.strftime("%Y-%m-%d")}284        self._save(); return f"Saved: {name}"285    def get(self, name): return self.snippets.get(name, {}).get("code", "")286    def delete(self, name):287        if name in self.snippets: del self.snippets[name]; self._save(); return f"Deleted: {name}"288        return "Not found"289    def get_names(self): return list(self.snippets.keys())290 291snippets = SnippetLibrary()292 293# ═══════════════════════════════════════294# Code Analyzer & FindReplace295# ═══════════════════════════════════════296class CodeAnalyzer:297    @staticmethod298    def analyze(code: str) -> str:299        if not code or not code.strip(): return "Empty file"300        try: tree = ast.parse(code)301        except SyntaxError as e: return f"⚠ Syntax error line {e.lineno}: {e.msg}"302        lines = [f"📊 {len(code.splitlines())} lines"]303        imports = []304        for node in ast.walk(tree):305            if isinstance(node, ast.Import):306                for a in node.names: imports.append(a.name)307            elif isinstance(node, ast.ImportFrom):308                imports.append(f"from {node.module}")309            elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):310                args = ", ".join(a.arg for a in node.args.args)311                lines.append(f"  L{node.lineno} ⚡ def {node.name}({args})")312            elif isinstance(node, ast.ClassDef):313                lines.append(f"  L{node.lineno} 🏗 class {node.name}")314        if imports:315            lines.insert(1, f"📦 {', '.join(imports)}")316        return "\n".join(lines)317 318analyzer = CodeAnalyzer()319 320class FindReplace:321    @staticmethod322    def find_in_file(code, query, case):323        if not query: return ""324        res = []325        for i, line in enumerate(code.splitlines(), 1):326            match = (query in line) if case else (query.lower() in line.lower())327            if match:328                res.append(f"  L{i}: {line.strip()}")329        return f"Found {len(res)} match(es):\n" + "\n".join(res) if res else "No matches."330 331    @staticmethod332    def find_all_files(files, query):333        if not query: return ""334        res = []335        for fname, content in files.items():336            for i, line in enumerate(content.splitlines(), 1):337                if query.lower() in line.lower():338                    res.append(f"  {fname}:L{i}: {line.strip()}")339        return f"Found {len(res)} across all files:\n" + "\n".join(res) if res else "No matches."340 341    @staticmethod342    def replace_in_file(code, find, replace, case):343        if not find: return code, "Empty find."344        flags = 0 if case else re.IGNORECASE345        new_code, count = re.subn(re.escape(find), replace, code, flags=flags)346        return new_code, f"Replaced {count} occurrence(s)."347 348finder = FindReplace()349 350# ═══════════════════════════════════════351# PTY Terminal (fixed)352# ═══════════════════════════════════════353class PTYTerminal:354    STRIP_ANSI = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[.*?[@-~]|\r')355 356    def __init__(self):357        self.master_fd = None; self.pid = None358        self.lock = threading.Lock()359        self.log_lines: List[str] = []360        self.max_lines = 800361        self.cmd_history: List[str] = []362        self._spawn()363        self._append("AXON TERMINAL v4.0")364        self._append("══════════════════════════════════════")365        self._append("Full PTY shell · Cross-file imports")366        self._append("  pip/npm/apt-get/git/curl/make")367        self._append("  GUI apps → Display tab")368        self._append("══════════════════════════════════════")369 370    def _spawn(self):371        try:372            pid, fd = pty.openpty()373            self.pid = os.fork()374            if self.pid == 0:375                # Child — become the shell376                os.close(pid)377                os.setsid()378                fcntl.ioctl(fd, termios.TIOCSCTTY, 0)379                os.dup2(fd, 0); os.dup2(fd, 1); os.dup2(fd, 2)380                if fd > 2: os.close(fd)381                env = os.environ.copy()382                env.update({"TERM": "dumb", "PS1": "$ ",383                            "DEBIAN_FRONTEND": "noninteractive", "DISPLAY": DISPLAY_NUM})384                os.execvpe("/bin/bash", ["/bin/bash", "--norc", "--noprofile", "-i"], env)385            else:386                # Parent — hold the master fd387                os.close(fd)388                self.master_fd = pid389                # Set non-blocking properly390                flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)391                fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)392                # Set terminal size393                fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ,394                            struct.pack("HHHH", 40, 120, 0, 0))395                time.sleep(0.3)396                self._read_raw(0.2)  # Drain initial output397        except Exception as e:398            self._append(f"[Shell Error] {e}")399            self.master_fd = None400 401    def _read_raw(self, timeout=0.1) -> str:402        """Read raw bytes from PTY."""403        if not self.master_fd: return ""404        out = []; deadline = time.time() + timeout405        while True:406            rem = deadline - time.time()407            if rem <= 0: break408            try:409                r, _, _ = select.select([self.master_fd], [], [], min(rem, 0.05))410                if r:411                    c = os.read(self.master_fd, 4096)412                    if c:413                        out.append(c.decode("utf-8", errors="replace"))414                        deadline = time.time() + 0.15415                    else: break416                elif out: break417            except OSError: break418        return "".join(out)419 420    def _clean(self, text):421        """Strip ANSI escape codes."""422        c = self.STRIP_ANSI.sub("", text)423        return "".join(ch for ch in c if ch in "\n\t" or ord(ch) >= 32)424 425    def _append(self, text):426        self.log_lines.append(text)427        while len(self.log_lines) > self.max_lines:428            self.log_lines.pop(0)429 430    def get_log(self): return "\n".join(self.log_lines)431 432    def run_command(self, cmd):433        cmd = cmd.strip()434        if not cmd: return self.get_log()435 436        # Save history437        if not self.cmd_history or self.cmd_history[-1] != cmd:438            self.cmd_history.append(cmd)439            if len(self.cmd_history) > 100: self.cmd_history.pop(0)440 441        with self.lock:442            if cmd.lower() == "clear":443                self.log_lines = []; return ""444 445            if cmd.lower() == "history":446                self._append("Command history:")447                for i, c in enumerate(self.cmd_history[-20:], 1):448                    self._append(f"  {i:3d}  {c}")449                return self.get_log()450 451            if not self.master_fd:452                self._append(f"$ {cmd}")453                self._append("[Error] No shell.")454                return self.get_log()455 456            # Drain stale output457            self._read_raw(0.05)458            self._append(f"$ {cmd}")459 460            try:461                os.write(self.master_fd, (cmd + "\n").encode())462            except OSError as e:463                self._append(f"[Write Error] {e}")464                return self.get_log()465 466            # Determine timeout based on command type467            parts = cmd.split()468            base = parts[0].lower() if parts else ""469            long_cmds = ["pip","pip3","npm","npx","apt-get","apt","git",470                         "wget","curl","make","cmake","cargo","yarn","conda"]471            wait = 180 if base in long_cmds else (60 if base in ("python","python3","node") else 15)472 473            # Collect output474            chunks = []; start = time.time(); idle = 0475            while time.time() - start < wait:476                c = self._read_raw(0.3)477                if c:478                    idle = 0; chunks.append(c)479                    if "".join(chunks).rstrip().endswith("$"): break480                else:481                    idle += 1482                    if base not in long_cmds and idle >= 3: break483                    if base in long_cmds and idle >= 10: break484 485            # Clean and filter486            raw = self._clean("".join(chunks))487            lines = raw.split("\n")488            filtered = []; skip_echo = True489            for line in lines:490                s = line.strip()491                if skip_echo and s == cmd.strip():492                    skip_echo = False; continue493                if s in ("$", "$ "): continue494                filtered.append(line)495 496            result = "\n".join(filtered).strip()497            if result: self._append(result)498            return self.get_log()499 500    def get_history(self) -> List[str]:501        return list(reversed(self.cmd_history[-30:]))502 503    def cleanup(self):504        if self.pid and self.pid > 0:505            try: os.kill(self.pid, signal.SIGTERM)506            except: pass507        if self.master_fd:508            try: os.close(self.master_fd)509            except: pass510 511terminal = PTYTerminal()512 513# ═══════════════════════════════════════514# AI Model — Qwen2.5CMR Q4_K_M GGUF515# ═══════════════════════════════════════516_llm_instance = None517_llm_lock = threading.Lock()518 519def load_model():520    global _llm_instance521    if _llm_instance is not None:522        return _llm_instance523 524    with _llm_lock:525        if _llm_instance is not None:526            return _llm_instance527 528        print(f"Downloading {MODEL_REPO}/{MODEL_FILE}...")529        t0 = time.time()530        try:531            model_path = hf_hub_download(532                repo_id=MODEL_REPO,533                filename=MODEL_FILE,534            )535            print(f"Downloaded in {time.time()-t0:.1f}s, loading...")536 537            from llama_cpp import Llama538            _llm_instance = Llama(539                model_path=model_path,540                n_ctx=4096,541                n_threads=os.cpu_count() or 4,542                n_gpu_layers=0,  # CPU only543                verbose=False,544            )545            print(f"Model ready in {time.time()-t0:.1f}s total")546            return _llm_instance547        except Exception as e:548            print(f"Model error: {e}")549            return None550 551def ai_gen(system_prompt, code, max_tokens=300):552    llm = load_model()553    if not llm: return "Error: model failed to load"554 555    messages = [556        {"role": "system", "content": system_prompt},557        {"role": "user", "content": code},558    ]559 560    try:561        response = llm.create_chat_completion(562            messages=messages,563            max_tokens=max_tokens,564            temperature=0.3,565            top_p=0.9,566            repeat_penalty=1.1,567        )568        text = response["choices"][0]["message"]["content"].strip()569        return text if text else "(No output from model)"570    except Exception as e:571        return f"Error: {e}"572 573# ═══════════════════════════════════════574# GUI code detection575# ═══════════════════════════════════════576GUI_HINTS = [577    "pygame", "tkinter", "turtle", "pyglet", "arcade", "kivy",578    "display.set_mode", "mainloop()", "Tk()", "Canvas(",579    "pygame.init", "pygame.display", "screen.fill",580    "from turtle import", "import turtle",581]582def is_gui_code(c): return any(h in c for h in GUI_HINTS)583 584# ═══════════════════════════════════════585# Theme + CSS586# ═══════════════════════════════════════587theme = gr.themes.Default(588    primary_hue="cyan", neutral_hue="gray",589    font=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"]590).set(591    body_background_fill="#080808", body_text_color="#e0e0e0",592    button_primary_background_fill="#0ff", button_primary_background_fill_hover="#0dd",593    button_primary_text_color="#000",594    button_secondary_background_fill="#1a1a1a", button_secondary_background_fill_hover="#2a2a2a",595    button_secondary_text_color="#0ff", button_secondary_border_color="#0ff4",596    block_background_fill="#0d0d0d", block_border_color="#1a1a1a",597    block_label_text_color="#0f8",598    input_background_fill="#0a0a0a", input_border_color="#1a3a2a", input_placeholder_color="#0f83",599)600 601css = """602* { border-radius: 0 !important; }603.gradio-container { max-width: 100% !important; }604 605/* Header */606.axon-header {607    background: linear-gradient(90deg, #080808, #0a1a1a, #080808);608    border-bottom: 1px solid #0ff3; padding: 12px 20px !important; margin-bottom: 8px;609}610.axon-header h1 {611    font-family: 'IBM Plex Mono', monospace !important;612    background: linear-gradient(90deg, #0ff, #0f8);613    -webkit-background-clip: text; -webkit-text-fill-color: transparent;614    font-size: 1.4em !important; letter-spacing: 3px; margin: 0 !important;615}616.axon-header p { color: #0f84 !important; font-size: 0.75em; letter-spacing: 1px; }617 618/* Terminal */619.term-box textarea {620    background: #050505 !important; color: #00ff41 !important;621    font-family: 'IBM Plex Mono', monospace !important;622    font-size: 12px !important; line-height: 1.5 !important;623    border: 1px solid #0f31 !important; text-shadow: 0 0 4px #0f32;624}625.term-input input {626    background: #050505 !important; color: #00ff41 !important;627    font-family: 'IBM Plex Mono', monospace !important;628    border: 1px solid #0f32 !important; font-size: 12px !important;629}630.term-input input::placeholder { color: #0f83 !important; }631.term-input input:focus { border-color: #0ff !important; box-shadow: 0 0 8px #0ff2 !important; }632 633/* Editor */634.code-editor textarea, .code-editor .cm-editor {635    font-family: 'IBM Plex Mono', monospace !important; font-size: 13px !important;636}637 638/* Buttons */639.tb {640    font-size: 0.8em !important; padding: 6px 12px !important; letter-spacing: 0.5px;641    text-transform: uppercase; font-weight: 600 !important;642    border: 1px solid transparent !important; transition: all 0.2s ease !important;643}644.tb:hover { border-color: #0ff !important; box-shadow: 0 0 12px #0ff2 !important; }645 646/* Accordion */647.gr-accordion { border: 1px solid #1a2a2a !important; background: #0a0a0a !important; }648.gr-accordion > .label-wrap { background: #0d0d0d !important; border-bottom: 1px solid #1a2a2a !important; }649.gr-accordion > .label-wrap:hover { background: #121a1a !important; }650.gr-accordion > .label-wrap span {651    color: #0ff !important; font-family: 'IBM Plex Mono', monospace !important;652    letter-spacing: 1px; font-size: 0.85em;653}654 655/* Tabs */656.tabs > .tab-nav > button {657    font-family: 'IBM Plex Mono', monospace !important;658    letter-spacing: 1px; font-size: 0.8em; text-transform: uppercase;659    color: #888 !important; border-bottom: 2px solid transparent !important;660}661.tabs > .tab-nav > button.selected {662    color: #0ff !important; border-bottom-color: #0ff !important; text-shadow: 0 0 8px #0ff4;663}664 665/* Structure + find */666.struct-box textarea { background: #050505 !important; color: #0ff !important;667    font-family: 'IBM Plex Mono', monospace !important; font-size: 11px !important; }668.find-box textarea { background: #050505 !important; color: #ffab40 !important;669    font-family: 'IBM Plex Mono', monospace !important; font-size: 11px !important; }670 671/* Status bar */672.status-bar {673    background: #0a0a0a !important; border-top: 1px solid #1a2a2a;674    padding: 4px 12px !important; font-size: 11px !important; color: #0f84 !important;675}676.status-bar strong { color: #0ff !important; }677 678/* Misc */679::-webkit-scrollbar { width: 6px; height: 6px; }680::-webkit-scrollbar-track { background: #0a0a0a; }681::-webkit-scrollbar-thumb { background: #0f83; }682input, textarea, select { color: #0f8 !important; }683.stop-btn { background: #a00 !important; color: #fff !important; border: none !important; }684.stop-btn:hover { background: #c00 !important; }685"""686 687# ═══════════════════════════════════════688# UI689# ═══════════════════════════════════════690with gr.Blocks(title="Axon Pro") as demo:691 692    with gr.Row(elem_classes="axon-header"):693        gr.Markdown("# ⬡ AXON PRO\n\nPYTHON AI IDE — v4.0")694 695    with gr.Row(equal_height=False):696 697        # ══ LEFT SIDEBAR ══698        with gr.Column(scale=1, min_width=200):699            with gr.Accordion("📁 EXPLORER", open=True):700                file_list = gr.Dropdown(choices=fs.get_all_files(), value=fs.current_file,701                                         label="Files", interactive=True)702                new_file_txt = gr.Textbox(placeholder="new_file.py", show_label=False)703                with gr.Row():704                    new_btn = gr.Button("NEW", size="sm", elem_classes="tb")705                    save_btn = gr.Button("SAVE", size="sm", elem_classes="tb")706                    del_btn = gr.Button("DEL", size="sm", elem_classes="tb stop-btn")707 708            with gr.Accordion("🗺 STRUCTURE", open=True):709                structure_view = gr.Textbox(710                    value=analyzer.analyze(fs.get_current_file_content()),711                    label="", lines=10, interactive=False, elem_classes="struct-box",712                    show_label=False)713 714            with gr.Accordion("📋 SNIPPETS", open=False):715                snip_list = gr.Dropdown(choices=snippets.get_names(), label="Saved",716                                         interactive=True)717                snip_name = gr.Textbox(placeholder="Snippet name", show_label=False)718                snip_tags = gr.Textbox(placeholder="Tags: api, util, loop", show_label=False)719                with gr.Row():720                    snip_save = gr.Button("SAVE", size="sm", elem_classes="tb")721                    snip_insert = gr.Button("INSERT", size="sm", elem_classes="tb")722                    snip_del = gr.Button("DEL", size="sm", elem_classes="tb stop-btn")723                snip_status = gr.Markdown("")724 725        # ══ CENTER ══726        with gr.Column(scale=4):727            editor = gr.Code(value=fs.get_current_file_content(),728                             label=f"  {fs.current_file}", language="python",729                             lines=20, interactive=True, elem_classes="code-editor")730 731            with gr.Row():732                run_btn = gr.Button("▶ RUN", variant="primary", elem_classes="tb")733                stop_btn = gr.Button("■ STOP", size="sm", elem_classes="tb stop-btn")734                ai_complete = gr.Button("✦ COMPLETE", elem_classes="tb")735                ai_explain = gr.Button("◈ EXPLAIN", elem_classes="tb")736                ai_refactor = gr.Button("⟲ REFACTOR", elem_classes="tb")737 738            with gr.Tabs() as bottom_tabs:739 740                with gr.Tab("▶ OUTPUT", id="output-tab"):741                    run_output = gr.Textbox(value="Run your code to see output here.",742                                             lines=12, max_lines=25, interactive=False,743                                             elem_classes="term-box", label="", show_label=False)744 745                with gr.Tab("⌘ TERMINAL", id="term-tab"):746                    term_out = gr.Textbox(value=terminal.get_log(), lines=12, max_lines=25,747                                           interactive=False, elem_classes="term-box",748                                           label="", show_label=False)749                    with gr.Row():750                        term_in = gr.Textbox(placeholder="$ pip install, git clone, ls ...",751                                              show_label=False, elem_classes="term-input")752                    with gr.Row():753                        clear_btn = gr.Button("CLEAR", size="sm", elem_classes="tb")754                        hist_dd = gr.Dropdown(choices=[], label="", interactive=True,755                                               scale=3, container=False)756                        hist_btn = gr.Button("↻ HISTORY", size="sm", elem_classes="tb")757 758                with gr.Tab("🔍 FIND", id="find-tab"):759                    with gr.Row():760                        find_query = gr.Textbox(label="Find", scale=4)761                        replace_query = gr.Textbox(label="Replace", scale=4)762                        find_case = gr.Checkbox(label="Case", value=False)763                    with gr.Row():764                        find_btn = gr.Button("FIND", size="sm", elem_classes="tb")765                        find_all_btn = gr.Button("FIND ALL FILES", size="sm", elem_classes="tb")766                        replace_btn = gr.Button("REPLACE ALL", size="sm", elem_classes="tb")767                    find_results = gr.Textbox(value="", lines=6, interactive=False,768                                               elem_classes="find-box", label="", show_label=False)769 770                with gr.Tab("💬 AI CHAT", id="chat-tab"):771                    chat_history = gr.Chatbot(label="", height=250)772                    with gr.Row():773                        chat_input = gr.Textbox(placeholder="Describe code you want...",774                                                 scale=8, container=False)775                        send_btn = gr.Button("GEN", variant="primary", scale=1, elem_classes="tb")776 777                with gr.Tab("⇄ DIFF", id="diff-tab"):778                    diff_view = gr.Code(label="AI Changes", language="python",779                                         interactive=False, lines=15)780                    with gr.Row():781                        apply_btn = gr.Button("✓ APPLY", variant="primary", elem_classes="tb")782                        discard_btn = gr.Button("✗ DISCARD", elem_classes="tb")783 784        # ══ RIGHT: DISPLAY ══785        with gr.Column(scale=2, min_width=250):786            with gr.Accordion("🖥 DISPLAY OUTPUT", open=True):787                display_image = gr.Image(label="", type="filepath", interactive=False, height=420)788                with gr.Row():789                    capture_btn = gr.Button("📸 CAPTURE", size="sm", elem_classes="tb")790                    auto_capture = gr.Checkbox(label="Auto Refresh", value=False)791                    refresh_speed = gr.Dropdown(792                        choices=["0.5s", "1s", "2s", "5s"], value="1s",793                        label="", container=False, scale=1, interactive=True)794                gui_status = gr.Markdown(795                    f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} "796                    f"| {SCREEN_W}x{SCREEN_H} | SDL: x11 + dummy audio</small>")797 798    status_bar = gr.Markdown(799        f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "800        f"Qwen2.5CMR Q4_K_M │ PTY + Xvfb │ Snippets + Structure + Find",801        elem_classes="status-bar")802 803    # State804    diff_original = gr.State("")805    diff_modified = gr.State("")806    auto_timer = gr.Timer(1, active=False)807 808    # ═══════════════════════════════════════809    # HANDLERS810    # ═══════════════════════════════════════811 812    # --- File ops ---813    def on_editor_change(content):814        fs.save_file(content)815        return analyzer.analyze(content)816 817    def on_file_select(filename):818        fs.set_current_file(filename)819        c = fs.get_current_file_content()820        return c, gr.update(label=f"  {filename}"), analyzer.analyze(c)821 822    def on_new_file(name):823        if not name or not name.strip():824            return gr.update(), gr.update(), gr.update(), gr.update()825        name = name.strip()826        fs.create_file(name)827        c = fs.get_current_file_content()828        return (gr.update(choices=fs.get_all_files(), value=name),829                c, gr.update(label=f"  {name}"), analyzer.analyze(c))830 831    def on_save(code):832        fs.save_file(code)833        return (f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "834                f"Qwen2.5CMR Q4_K_M │ ✓ Saved {fs.current_file}")835 836    def on_delete():837        name = fs.current_file838        if fs.delete_file(name):839            c = fs.get_current_file_content()840            return (gr.update(choices=fs.get_all_files(), value=fs.current_file),841                    c, gr.update(label=f"  {fs.current_file}"), analyzer.analyze(c))842        return gr.update(), gr.update(), gr.update(), gr.update()843 844    # --- Run (smart: auto-detects GUI vs CLI) ---845    def on_run(code):846        fs.save_file(code)847 848        if is_gui_code(code):849            # GUI app — launch, take first screenshot, auto-enable refresh850            msg = gui_mgr.launch(code)851            time.sleep(0.5)  # Extra time for first frame852            ss = vdisplay.capture()853            status = f"<small>Xvfb: ● ON | {gui_mgr.get_status()}</small>"854            return msg, ss, status, gr.update(value=True)  # Enable auto-refresh855 856        # Normal script — run with subprocess, output goes to OUTPUT tab857        tmp = "/tmp/_axon_run.py"858        with open(tmp, "w") as f: f.write(code)859        try:860            env = os.environ.copy()861            env["DISPLAY"] = DISPLAY_NUM862            env["PYTHONPATH"] = fs._sync_dir + ":" + env.get("PYTHONPATH", "")863            r = subprocess.run([sys.executable, tmp], capture_output=True, text=True,864                               timeout=30, env=env, cwd=fs._sync_dir)865            output = ""866            if r.stdout.strip(): output += r.stdout.rstrip()867            if r.stderr.strip():868                if output: output += "\n"869                output += r.stderr.rstrip()870            if not output: output = "(No output)"871        except subprocess.TimeoutExpired:872            output = "[Timed out after 30s]"873        except Exception as e:874            output = f"[Error] {e}"875        return output, gr.update(), gr.update(), gr.update()876 877    def on_stop():878        msg = gui_mgr.stop()879        status = f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} | ○ idle</small>"880        return msg, status, gr.update(value=False)  # Disable auto-refresh881 882    # --- Terminal ---883    def on_term_cmd(cmd):884        return terminal.run_command(cmd), ""885 886    def on_clear():887        terminal.log_lines = []; return ""888 889    def on_refresh_hist():890        return gr.update(choices=terminal.get_history())891 892    def on_select_hist(cmd):893        return cmd if cmd else gr.update()894 895    # --- AI ---896    def on_complete(code):897        result = ai_gen("Complete this Python code. Only output the completion, no explanations.", code)898        new_code = code + "\n" + result899        return new_code, analyzer.analyze(new_code)900 901    def on_explain(code):902        explanation = ai_gen("Explain this Python code concisely.", code, 512)903        terminal._append(f"[AI Explanation]\n{explanation}")904        return terminal.get_log()905 906    def on_refactor(code):907        refactored = ai_gen("Refactor this Python code for PEP 8 and best practices. Output only code.", code, 512)908        fs.save_file(code)909        import difflib910        diff = "\n".join(difflib.unified_diff(code.splitlines(), refactored.splitlines(), lineterm=""))911        return diff, code, refactored912 913    def on_generate(prompt_text, history):914        generated = ai_gen(f"Write Python code for: {prompt_text}", "", 512)915        import difflib916        diff = "\n".join(difflib.unified_diff([], generated.splitlines(), lineterm=""))917        new_h = history + [918            {"role": "user", "content": prompt_text},919            {"role": "assistant", "content": "Code generated → check DIFF tab"},920        ]921        return diff, "", generated, new_h, ""922 923    def on_apply(modified):924        return modified, analyzer.analyze(modified)925 926    # --- Snippets ---927    def on_snip_save(name, tags, code):928        msg = snippets.add(name, code, tags)929        return msg, gr.update(choices=snippets.get_names())930 931    def on_snip_insert(name, code):932        snip_code = snippets.get(name)933        if snip_code:934            # Append snippet at cursor position (end of file)935            return code + "\n\n" + snip_code if code.strip() else snip_code936        return code937 938    def on_snip_del(name):939        msg = snippets.delete(name)940        return msg, gr.update(choices=snippets.get_names())941 942    # --- Find ---943    def on_find(code, query, case):944        return finder.find_in_file(code, query, case)945 946    def on_find_all(query):947        return finder.find_all_files(fs.files, query)948 949    def on_replace(code, fq, rq, case):950        new_code, msg = finder.replace_in_file(code, fq, rq, case)951        fs.save_file(new_code)952        return new_code, msg, analyzer.analyze(new_code)953 954    # --- Display ---955    def on_capture():956        return vdisplay.capture()957 958    def on_auto_tick():959        ss = vdisplay.capture()960        # Auto-disable if GUI process died961        if not gui_mgr.is_running:962            return ss if ss else gr.update()963        return ss if ss else gr.update()964 965    def on_auto_toggle(checked):966        return gr.Timer(1, active=checked)967 968    def on_speed_change(speed, auto_on):969        speed_map = {"0.5s": 0.5, "1s": 1, "2s": 2, "5s": 5}970        interval = speed_map.get(speed, 1)971        return gr.Timer(interval, active=auto_on)972 973    # ═══════════════════════════════════════974    # WIRING975    # ═══════════════════════════════════════976 977    # Files978    editor.change(on_editor_change, editor, structure_view)979    file_list.change(on_file_select, file_list, [editor, editor, structure_view])980    new_btn.click(on_new_file, new_file_txt, [file_list, editor, editor, structure_view])981    save_btn.click(on_save, editor, status_bar)982    del_btn.click(on_delete, None, [file_list, editor, editor, structure_view])983 984    # Run985    run_btn.click(on_run, editor, [run_output, display_image, gui_status, auto_capture]986    ).then(lambda: gr.Tabs(selected="output-tab"), None, bottom_tabs)987    stop_btn.click(on_stop, None, [run_output, gui_status, auto_capture])988 989    # Terminal990    term_in.submit(on_term_cmd, term_in, [term_out, term_in])991    clear_btn.click(on_clear, None, term_out)992    hist_btn.click(on_refresh_hist, None, hist_dd)993    hist_dd.change(on_select_hist, hist_dd, term_in)994 995    # AI996    ai_complete.click(on_complete, editor, [editor, structure_view])997    ai_explain.click(on_explain, editor, term_out)998    ai_refactor.click(on_refactor, editor, [diff_view, diff_original, diff_modified]999    ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)1000 1001    # Chat1002    chat_input.submit(on_generate, [chat_input, chat_history],1003        [diff_view, diff_original, diff_modified, chat_history, chat_input]1004    ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)1005    send_btn.click(on_generate, [chat_input, chat_history],1006        [diff_view, diff_original, diff_modified, chat_history, chat_input]1007    ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)1008 1009    # Diff1010    apply_btn.click(on_apply, diff_modified, [editor, structure_view]1011    ).then(lambda: gr.Tabs(selected="term-tab"), None, bottom_tabs)1012    discard_btn.click(lambda: (gr.update(), gr.update()), None, [editor, structure_view]1013    ).then(lambda: gr.Tabs(selected="term-tab"), None, bottom_tabs)1014 1015    # Snippets1016    snip_save.click(on_snip_save, [snip_name, snip_tags, editor], [snip_status, snip_list])1017    snip_insert.click(on_snip_insert, [snip_list, editor], editor)1018    snip_del.click(on_snip_del, snip_list, [snip_status, snip_list])1019 1020    # Find1021    find_btn.click(on_find, [editor, find_query, find_case], find_results)1022    find_all_btn.click(on_find_all, find_query, find_results)1023    replace_btn.click(on_replace, [editor, find_query, replace_query, find_case],1024                      [editor, find_results, structure_view])1025 1026    # Display1027    capture_btn.click(on_capture, None, display_image)1028    auto_capture.change(on_auto_toggle, auto_capture, auto_timer)1029    refresh_speed.change(on_speed_change, [refresh_speed, auto_capture], auto_timer)1030    auto_timer.tick(on_auto_tick, None, display_image)1031 1032 1033if __name__ == "__main__":1034    import atexit1035    atexit.register(terminal.cleanup)1036    atexit.register(gui_mgr.stop)1037    atexit.register(vdisplay.cleanup)1038    demo.queue().launch(server_name="0.0.0.0", server_port=7860,1039                        theme=theme, css=css, ssr_mode=False, allowed_paths=["/tmp"])