AIencoder/Axon-Pro-IDE
1
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"])