AIencoder commited on
Commit
ab2aa4f
·
verified ·
1 Parent(s): 8e142f7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +705 -423
app.py CHANGED
@@ -14,654 +14,936 @@ import struct
14
  import termios
15
  import threading
16
  import re
 
 
 
17
  import tempfile
 
18
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, GenerationConfig
19
- from typing import List
20
  from functools import lru_cache
21
 
22
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
23
 
24
- # --- Configuration ---
25
  MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
26
  DEVICE = "cpu"
27
  DTYPE = torch.float32
28
  DISPLAY_NUM = ":99"
29
  SCREEN_W, SCREEN_H = 800, 600
 
30
 
31
- # --- Virtual Display ---
 
 
32
  class VirtualDisplay:
33
  def __init__(self):
34
  self.xvfb_proc = None
35
  self.display = DISPLAY_NUM
 
36
  self._start_xvfb()
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def _start_xvfb(self):
39
  try:
40
- subprocess.run(["pkill", "-f", f"Xvfb {self.display}"],
41
- capture_output=True, timeout=5)
42
  time.sleep(0.2)
43
  self.xvfb_proc = subprocess.Popen(
44
- ["Xvfb", self.display, "-screen", "0",
45
- f"{SCREEN_W}x{SCREEN_H}x24", "-ac", "-nolisten", "tcp"],
46
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
47
  time.sleep(0.5)
48
- if self.xvfb_proc.poll() is not None:
49
- print("[Xvfb] Failed, installing...")
50
- subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
51
- subprocess.run(["apt-get", "install", "-y", "-qq", "xvfb", "x11-utils",
52
- "imagemagick"], capture_output=True, timeout=120)
53
- self.xvfb_proc = subprocess.Popen(
54
- ["Xvfb", self.display, "-screen", "0",
55
- f"{SCREEN_W}x{SCREEN_H}x24", "-ac", "-nolisten", "tcp"],
56
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
57
- time.sleep(0.5)
58
  if self.xvfb_proc.poll() is None:
59
  print(f"[Xvfb] Running on {self.display}")
60
  os.environ["DISPLAY"] = self.display
61
  else:
62
- print("[Xvfb] Could not start")
63
- self.xvfb_proc = None
64
- except FileNotFoundError:
65
- print("[Xvfb] Not found, installing...")
66
- try:
67
- subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
68
- subprocess.run(["apt-get", "install", "-y", "-qq", "xvfb", "x11-utils",
69
- "imagemagick"], capture_output=True, timeout=120)
70
- self._start_xvfb()
71
- except Exception as e:
72
- print(f"[Xvfb] Install failed: {e}")
73
  self.xvfb_proc = None
74
  except Exception as e:
75
- print(f"[Xvfb] Error: {e}")
76
- self.xvfb_proc = None
77
-
78
- def capture_screenshot(self) -> str | None:
79
- if not self.is_running:
80
- return None
81
- try:
82
- tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
83
- tmp.close()
84
- result = subprocess.run(
85
- ["import", "-window", "root", "-display", self.display, tmp.name],
86
- capture_output=True, timeout=5)
87
- if result.returncode == 0 and os.path.getsize(tmp.name) > 100:
88
- return tmp.name
89
- os.unlink(tmp.name)
90
- except Exception:
91
- pass
 
 
92
  return None
93
 
94
  @property
95
- def is_running(self) -> bool:
96
- return self.xvfb_proc is not None and self.xvfb_proc.poll() is None
97
-
98
  def cleanup(self):
99
  if self.xvfb_proc:
100
  self.xvfb_proc.terminate()
101
- try:
102
- self.xvfb_proc.wait(timeout=3)
103
- except subprocess.TimeoutExpired:
104
- self.xvfb_proc.kill()
105
 
106
  vdisplay = VirtualDisplay()
107
 
108
- # --- File System ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  class FileSystem:
110
  def __init__(self):
111
- self.files = {
112
- "main.py": "# Start coding here\nprint('Hello Axon Pro (Python Edition)')",
113
- "utils.py": "# Utility functions\ndef add(a, b):\n return a + b",
114
  "notes.txt": "Project requirements:\n- Build a cool AI app\n- Use Gradio"
115
  }
116
  self.current_file = "main.py"
117
-
118
- def save_file(self, content: str) -> None:
 
 
 
 
 
 
 
 
 
 
119
  if self.current_file:
120
  self.files[self.current_file] = content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
- def get_current_file_content(self) -> str:
123
- return self.files.get(self.current_file, "")
124
-
125
- def set_current_file(self, filename: str) -> None:
126
- if filename in self.files:
127
- self.current_file = filename
128
-
129
- def create_file(self, filename: str, content: str = "") -> None:
130
- if filename not in self.files:
131
- self.files[filename] = content
132
- self.current_file = filename
133
 
134
- def get_all_files(self) -> List[str]:
135
- return list(self.files.keys())
 
 
 
 
 
136
 
137
- fs = FileSystem()
 
 
 
 
 
138
 
139
- # --- PTY Terminal ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  class PTYTerminal:
141
  STRIP_ANSI = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[.*?[@-~]|\r')
142
-
143
  def __init__(self):
144
- self.master_fd = None
145
- self.pid = None
146
- self.lock = threading.Lock()
147
- self.log_lines: List[str] = []
148
- self.max_lines = 800
149
  self._spawn_shell()
150
- self._append("AXON TERMINAL v3.0")
151
  self._append("══════════════════════════════════════")
152
- self._append("Full PTY shell everything works:")
153
- self._append(" pip/npm/apt-get/git/curl/wget/make")
154
- self._append(" python/node — GUI apps render to Display")
155
  self._append("══════════════════════════════════════")
156
 
157
  def _spawn_shell(self):
158
  try:
159
- pid, fd = pty.openpty()
160
- self.pid = os.fork()
161
  if self.pid == 0:
162
- os.close(pid)
163
- os.setsid()
164
- fcntl.ioctl(fd, termios.TIOCSCTTY, 0)
165
- os.dup2(fd, 0); os.dup2(fd, 1); os.dup2(fd, 2)
166
- if fd > 2: os.close(fd)
167
  env = os.environ.copy()
168
- env.update({"TERM": "dumb", "PS1": "$ ",
169
- "DEBIAN_FRONTEND": "noninteractive", "DISPLAY": DISPLAY_NUM})
170
- os.execvpe("/bin/bash", ["/bin/bash", "--norc", "--noprofile", "-i"], env)
171
  else:
172
- os.close(fd)
173
- self.master_fd = pid
174
- flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
175
- fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
176
- fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ,
177
- struct.pack("HHHH", 40, 120, 0, 0))
178
- time.sleep(0.3)
179
- self._read(0.2)
180
- except Exception as e:
181
- self._append(f"[Shell Error] {e}")
182
- self.master_fd = None
183
-
184
- def _read(self, timeout=0.1) -> str:
185
- if self.master_fd is None: return ""
186
- out = []
187
- deadline = time.time() + timeout
188
  while True:
189
- rem = deadline - time.time()
190
- if rem <= 0: break
191
  try:
192
- r, _, _ = select.select([self.master_fd], [], [], min(rem, 0.05))
193
- if r:
194
- c = os.read(self.master_fd, 4096)
195
- if c:
196
- out.append(c.decode("utf-8", errors="replace"))
197
- deadline = time.time() + 0.15
198
  else: break
199
  elif out: break
200
  except OSError: break
201
  return "".join(out)
202
 
203
- def _clean(self, text):
204
- c = self.STRIP_ANSI.sub("", text)
205
- return "".join(ch for ch in c if ch in "\n\t" or ord(ch) >= 32)
 
 
 
206
 
207
- def _append(self, text):
208
- self.log_lines.append(text)
209
- while len(self.log_lines) > self.max_lines:
210
- self.log_lines.pop(0)
211
-
212
- def get_log(self) -> str:
213
- return "\n".join(self.log_lines)
214
-
215
- def run_command(self, cmd: str) -> str:
216
- cmd = cmd.strip()
217
  if not cmd: return self.get_log()
 
 
 
 
 
218
  with self.lock:
219
- if cmd.lower() == "clear":
220
- self.log_lines = []
221
- return ""
222
- if self.master_fd is None:
223
- self._append(f"$ {cmd}")
224
- self._append("[Error] No shell. Restart Space.")
225
- return self.get_log()
226
- self._read(0.05)
227
- self._append(f"$ {cmd}")
228
- try:
229
- os.write(self.master_fd, (cmd + "\n").encode())
230
- except OSError as e:
231
- self._append(f"[Write Error] {e}")
232
  return self.get_log()
233
-
234
- parts = cmd.split()
235
- base = parts[0].lower() if parts else ""
236
- long = ["pip","pip3","npm","npx","apt-get","apt","git",
237
- "wget","curl","make","cmake","cargo","yarn","conda"]
238
- wait = 180 if base in long else (60 if base in ("python","python3","node") else 15)
239
-
240
- chunks = []
241
- start = time.time()
242
- idle = 0
243
- while time.time() - start < wait:
244
- c = self._read(0.3)
245
- if c:
246
- idle = 0; chunks.append(c)
247
- if "".join(chunks).rstrip().endswith("$"): break
248
  else:
249
- idle += 1
250
- if base not in long and idle >= 3: break
251
- if base in long and idle >= 10: break
252
-
253
- raw = self._clean("".join(chunks))
254
- lines = raw.split("\n")
255
- filtered = []
256
- skip = True
257
  for l in lines:
258
- s = l.strip()
259
- if skip and s == cmd.strip():
260
- skip = False; continue
261
- if s in ("$", "$ "): continue
262
  filtered.append(l)
263
- result = "\n".join(filtered).strip()
264
  if result: self._append(result)
265
  return self.get_log()
266
 
267
- def run_editor_code(self, code: str) -> str:
268
- with self.lock:
269
- self._append("$ python [editor]")
270
- tmp = "/tmp/_axon_run.py"
271
- try:
272
- with open(tmp, "w") as f: f.write(code)
273
- except Exception as e:
274
- with self.lock: self._append(f"[Error] {e}")
275
- return self.get_log()
276
- try:
277
- env = os.environ.copy()
278
- env["DISPLAY"] = DISPLAY_NUM
279
- r = subprocess.run([sys.executable, tmp], capture_output=True,
280
- text=True, timeout=30, env=env)
281
- with self.lock:
282
- if r.stdout.strip(): self._append(r.stdout.rstrip())
283
- if r.stderr.strip(): self._append(r.stderr.rstrip())
284
- if not r.stdout.strip() and not r.stderr.strip():
285
- self._append("(No output)")
286
- except subprocess.TimeoutExpired:
287
- with self.lock: self._append("[Timed out 30s]")
288
- except Exception as e:
289
- with self.lock: self._append(f"[Error] {e}")
290
- return self.get_log()
291
 
292
  def cleanup(self):
293
- if self.pid and self.pid > 0:
294
  try: os.kill(self.pid, signal.SIGTERM)
295
- except ProcessLookupError: pass
296
- if self.master_fd is not None:
297
  try: os.close(self.master_fd)
298
- except OSError: pass
299
 
300
  terminal = PTYTerminal()
301
 
302
- # --- Model ---
 
 
303
  @lru_cache(maxsize=1)
304
  def load_model():
305
- print("Loading TinyLlama...")
306
- t0 = time.time()
307
  try:
308
- tok = AutoTokenizer.from_pretrained(MODEL_NAME)
309
- mdl = AutoModelForCausalLM.from_pretrained(
310
- MODEL_NAME, dtype=DTYPE, device_map=DEVICE, low_cpu_mem_usage=True)
311
- gc = GenerationConfig(max_new_tokens=256, temperature=0.2, top_p=0.95, do_sample=True)
312
- pipe = pipeline("text-generation", model=mdl, tokenizer=tok, generation_config=gc)
313
- print(f"Loaded in {time.time()-t0:.1f}s")
314
- return pipe
315
- except Exception as e:
316
- print(f"Model error: {e}")
317
- return None
318
-
319
- def _llm(system, user, max_tok=512):
320
- m = load_model()
321
  if not m: return "Error: model not loaded"
322
- prompt = f"<|system|>{system}</s><|user|>\n{user}\n</s><|assistant|>"
323
- try:
324
- return m(prompt, max_new_tokens=max_tok)[0]["generated_text"].strip()
325
- except Exception as e:
326
- return str(e)
327
-
328
- def generate_completion(code): return _llm("Complete this Python code. No explanations.", code, 256)
329
- def explain_code(code): return _llm("Explain this Python code concisely.", code)
330
- def refactor_code(code): return _llm("Refactor for PEP 8 and best practices.", code)
331
- def generate_code(p): return _llm(f"Write Python code for: {p}", "")
332
-
333
- def create_diff_view(orig, mod):
334
- import difflib
335
- return "\n".join(difflib.unified_diff(orig.splitlines(), mod.splitlines(), lineterm=""))
336
-
337
- # ═══════════════════════════════════════════════════════════
338
- # THEME — only valid params
339
- # ═══════════════════════════════════════════════════════════
340
-
341
  theme = gr.themes.Default(
342
- font=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"],
343
- primary_hue="cyan",
344
- neutral_hue="gray",
345
  ).set(
346
- body_background_fill="#080808",
347
- body_text_color="#e0e0e0",
348
- button_primary_background_fill="#0ff",
349
- button_primary_background_fill_hover="#0dd",
350
  button_primary_text_color="#000",
351
- button_secondary_background_fill="#1a1a1a",
352
- button_secondary_background_fill_hover="#2a2a2a",
353
- button_secondary_text_color="#0ff",
354
- button_secondary_border_color="#0ff4",
355
- block_background_fill="#0d0d0d",
356
- block_border_color="#1a1a1a",
357
  block_label_text_color="#0f8",
358
- input_background_fill="#0a0a0a",
359
- input_border_color="#1a3a2a",
360
- input_placeholder_color="#0f83",
361
  )
362
 
363
- # ═══════════════════════════════════════════════════════════
364
- # CSS
365
- # ═══════════════════════════════════════════════════════════
366
-
367
  css = """
368
  * { border-radius: 0 !important; }
369
  .gradio-container { max-width: 100% !important; }
370
-
371
- /* ─── Header ─── */
372
  .axon-header {
373
  background: linear-gradient(90deg, #080808 0%, #0a1a1a 50%, #080808 100%);
374
- border-bottom: 1px solid #0ff3;
375
- padding: 12px 20px !important;
376
- margin-bottom: 8px;
377
  }
378
  .axon-header h1 {
379
  font-family: 'IBM Plex Mono', monospace !important;
380
  background: linear-gradient(90deg, #0ff, #0f8);
381
- -webkit-background-clip: text;
382
- -webkit-text-fill-color: transparent;
383
- font-size: 1.4em !important;
384
- letter-spacing: 3px;
385
- margin: 0 !important;
386
  }
387
  .axon-header p { color: #0f84 !important; font-size: 0.75em; letter-spacing: 1px; }
388
-
389
- /* ─── Terminal ─── */
390
  .terminal-box textarea {
391
- background: #050505 !important;
392
- color: #00ff41 !important;
393
  font-family: 'IBM Plex Mono', monospace !important;
394
- font-size: 12px !important;
395
- line-height: 1.5 !important;
396
- border: 1px solid #0f31 !important;
397
- text-shadow: 0 0 4px #0f32;
398
  }
399
  .terminal-input input {
400
- background: #050505 !important;
401
- color: #00ff41 !important;
402
  font-family: 'IBM Plex Mono', monospace !important;
403
- border: 1px solid #0f32 !important;
404
- font-size: 12px !important;
405
- caret-color: #0f8 !important;
406
  }
407
  .terminal-input input::placeholder { color: #0f83 !important; }
408
  .terminal-input input:focus { border-color: #0ff !important; box-shadow: 0 0 8px #0ff2 !important; }
409
-
410
- /* ─── Editor ─── */
411
  .code-editor textarea, .code-editor .cm-editor {
412
- font-family: 'IBM Plex Mono', monospace !important;
413
- font-size: 13px !important;
414
  }
415
-
416
- /* ─── File explorer ─── */
417
- .file-explorer { border-right: 1px solid #1a2a2a !important; }
418
-
419
- /* ─── Display ─── */
420
- .display-panel { border: 1px solid #1a2a2a !important; background: #050505 !important; }
421
- .display-panel img { image-rendering: pixelated; }
422
-
423
- /* ─── Toolbar buttons ─── */
424
  .toolbar-btn {
425
- font-size: 0.8em !important;
426
- padding: 6px 12px !important;
427
- letter-spacing: 0.5px;
428
- text-transform: uppercase;
429
- font-weight: 600 !important;
430
- border: 1px solid transparent !important;
431
- transition: all 0.2s ease !important;
432
  }
433
  .toolbar-btn:hover { border-color: #0ff !important; box-shadow: 0 0 12px #0ff2 !important; }
434
-
435
- /* ─── Accordion ─── */
436
  .gr-accordion { border: 1px solid #1a2a2a !important; background: #0a0a0a !important; }
437
  .gr-accordion > .label-wrap {
438
- background: #0d0d0d !important;
439
- border-bottom: 1px solid #1a2a2a !important;
440
- transition: background 0.2s ease;
441
  }
442
  .gr-accordion > .label-wrap:hover { background: #121a1a !important; }
443
  .gr-accordion > .label-wrap span {
444
- color: #0ff !important;
445
- font-family: 'IBM Plex Mono', monospace !important;
446
- letter-spacing: 1px;
447
- font-size: 0.85em;
448
  }
449
-
450
- /* ─── Tabs ─── */
451
  .tabs > .tab-nav > button {
452
  font-family: 'IBM Plex Mono', monospace !important;
453
  letter-spacing: 1px; font-size: 0.8em; text-transform: uppercase;
454
  color: #888 !important; border-bottom: 2px solid transparent !important;
455
- transition: all 0.2s ease;
456
  }
457
  .tabs > .tab-nav > button.selected {
458
- color: #0ff !important; border-bottom-color: #0ff !important;
459
- text-shadow: 0 0 8px #0ff4;
460
  }
461
-
462
- /* ─── Status ─── */
463
  .status-bar {
464
  background: #0a0a0a !important; border-top: 1px solid #1a2a2a;
465
  padding: 4px 12px !important; font-size: 11px !important; color: #0f84 !important;
466
  }
467
  .status-bar strong { color: #0ff !important; }
468
-
469
- /* ─── Scrollbar ─── */
470
  ::-webkit-scrollbar { width: 6px; height: 6px; }
471
  ::-webkit-scrollbar-track { background: #0a0a0a; }
472
  ::-webkit-scrollbar-thumb { background: #0f83; }
473
- ::-webkit-scrollbar-thumb:hover { background: #0ff; }
474
-
475
- /* ─── Generic input text color (covers what input_text_color would do) ─── */
476
  input, textarea, select { color: #0f8 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  """
478
 
479
- # ═══════════════════════════════════════════════════════════
480
- # UI LAYOUT
481
- # ═══════════════════════════════════════════════════════════
482
-
483
  with gr.Blocks(title="Axon Pro — Python AI IDE") as demo:
484
 
485
  with gr.Row(elem_classes="axon-header"):
486
- gr.Markdown("# ⬡ AXON PRO\n\nPYTHON AI IDE — v3.0")
487
 
488
  with gr.Row(equal_height=False):
489
 
490
- # ── LEFT: FILE EXPLORER ──
491
- with gr.Column(scale=1, min_width=180, elem_classes="file-explorer"):
 
 
492
  with gr.Accordion("📁 EXPLORER", open=True):
493
- file_list = gr.Dropdown(
494
- choices=fs.get_all_files(), value=fs.current_file,
495
- label="", interactive=True, container=False)
496
  new_file_name = gr.Textbox(placeholder="filename.py", label="", container=False)
497
  with gr.Row():
498
- new_file_btn = gr.Button("+ NEW", variant="secondary", size="sm",
499
- elem_classes="toolbar-btn")
500
- save_btn = gr.Button("SAVE", variant="secondary", size="sm",
501
- elem_classes="toolbar-btn")
502
-
503
- # ── CENTER: EDITOR + TERMINAL ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  with gr.Column(scale=4):
505
- editor = gr.Code(
506
- value=fs.get_current_file_content(),
507
- label=f" {fs.current_file}",
508
- language="python", lines=22, interactive=True,
509
- elem_classes="code-editor")
510
 
511
  with gr.Row():
512
  run_btn = gr.Button("▶ RUN", variant="primary", elem_classes="toolbar-btn")
 
 
513
  complete_btn = gr.Button("✦ COMPLETE", variant="secondary", elem_classes="toolbar-btn")
514
  explain_btn = gr.Button("◈ EXPLAIN", variant="secondary", elem_classes="toolbar-btn")
515
  refactor_btn = gr.Button("⟲ REFACTOR", variant="secondary", elem_classes="toolbar-btn")
516
 
517
  with gr.Tabs() as bottom_tabs:
 
518
  with gr.Tab("⌘ TERMINAL", id="terminal-tab"):
519
- terminal_output = gr.Textbox(
520
- value=terminal.get_log(), lines=10, max_lines=25,
521
- interactive=False, elem_classes="terminal-box",
522
- label="", show_label=False)
523
  with gr.Row():
524
  terminal_input = gr.Textbox(
525
- placeholder="$ pip install pygame | npm i | git clone | ls ...",
526
- scale=9, container=False, elem_classes="terminal-input")
527
  terminal_run_btn = gr.Button("⏎", variant="primary", scale=1, min_width=45)
528
- clear_btn = gr.Button("CLEAR", variant="secondary", size="sm",
529
- elem_classes="toolbar-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
 
531
  with gr.Tab("💬 AI CHAT", id="chat-tab"):
532
  chat_history = gr.Chatbot(label="", height=250)
533
  with gr.Row():
534
- chat_input = gr.Textbox(placeholder="Describe the code you want...",
535
  scale=8, container=False)
536
- send_btn = gr.Button("GEN", variant="primary", scale=1,
537
- elem_classes="toolbar-btn")
538
 
539
  with gr.Tab("⇄ DIFF", id="diff-tab"):
540
- diff_view = gr.Code(label="AI Changes", language="python",
541
- interactive=False, lines=15)
542
  with gr.Row():
543
- apply_btn = gr.Button("✓ APPLY", variant="primary",
544
- elem_classes="toolbar-btn")
545
- discard_btn = gr.Button("✗ DISCARD", variant="secondary",
546
- elem_classes="toolbar-btn")
547
 
548
- # ── RIGHT: DISPLAY ──
549
  with gr.Column(scale=2, min_width=250):
550
  with gr.Accordion("🖥 DISPLAY OUTPUT", open=True):
551
- gr.Markdown(
552
- "<small style='color:#0f84'>GUI apps (pygame, tkinter, etc.) "
553
- "render here via virtual display</small>")
554
- display_image = gr.Image(
555
- label="", type="filepath", interactive=False,
556
- elem_classes="display-panel", height=400)
557
  with gr.Row():
558
- capture_btn = gr.Button("📸 CAPTURE", variant="secondary", size="sm",
559
- elem_classes="toolbar-btn")
560
- auto_refresh = gr.Checkbox(label="Auto-refresh", value=False)
561
- display_status = gr.Markdown(
562
- f"<small style='color:#0f84'>Xvfb: "
563
- f"{'● running' if vdisplay.is_running else '○ stopped'} "
564
- f"| {SCREEN_W}x{SCREEN_H}</small>")
565
 
566
  status_bar = gr.Markdown(
567
- f"**AXON PRO v3.0** │ Python {sys.version.split()[0]} │ CPU │ "
568
- f"TinyLlama-1.1B │ PTY Shell Xvfb {'ON' if vdisplay.is_running else 'OFF'}",
569
  elem_classes="status-bar")
570
 
571
- # ── State ──
572
  diff_original_state = gr.State("")
573
  diff_modified_state = gr.State("")
 
 
 
 
 
574
 
575
- # ── Handlers ──
576
  def update_file(content):
577
  fs.save_file(content)
 
578
 
579
  def load_file(filename):
580
  fs.set_current_file(filename)
581
- return fs.get_current_file_content(), gr.update(label=f" {filename}")
 
582
 
583
  def create_file_fn(name):
584
- if name and "." in name:
585
  fs.create_file(name)
 
586
  return (gr.update(choices=fs.get_all_files(), value=name),
587
- fs.get_current_file_content(), gr.update(label=f" {name}"))
588
- return gr.update(choices=fs.get_all_files()), fs.get_current_file_content(), gr.update()
 
 
 
 
 
 
 
 
 
589
 
590
  def terminal_cmd(cmd):
591
  return terminal.run_command(cmd), ""
592
 
593
- def clear_term():
594
- terminal.log_lines = []
595
- return ""
596
-
597
- def run_editor(content):
598
- return terminal.run_editor_code(content)
599
 
600
- def complete_fn(content):
601
- return content + "\n" + generate_completion(content)
602
 
603
- def explain_fn(content):
604
- exp = explain_code(content)
605
- fs.save_file(content)
606
- mod = f'"""\nEXPLANATION:\n{exp}\n"""\n\n{content}'
607
- return create_diff_view(content, mod), content, mod
608
 
609
- def refactor_fn(content):
610
- ref = refactor_code(content)
611
  fs.save_file(content)
612
- return create_diff_view(content, ref), content, ref
613
-
614
- def generate_fn(prompt_text, history):
615
- gen = generate_code(prompt_text)
616
- diff = create_diff_view("", gen)
617
- new_h = history + [
618
- {"role": "user", "content": prompt_text},
619
- {"role": "assistant", "content": "Code generated check DIFF tab"},
620
- ]
621
- return diff, "", gen, new_h, ""
622
-
623
- def capture_display():
624
- return vdisplay.capture_screenshot()
625
-
626
- # ── Wiring ──
627
- editor.change(update_file, editor, None)
628
- file_list.change(load_file, file_list, [editor, editor])
629
- new_file_btn.click(create_file_fn, new_file_name, [file_list, editor, editor])
630
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  terminal_input.submit(terminal_cmd, terminal_input, [terminal_output, terminal_input])
632
  terminal_run_btn.click(terminal_cmd, terminal_input, [terminal_output, terminal_input])
633
  clear_btn.click(clear_term, None, terminal_output)
 
 
634
 
 
635
  run_btn.click(run_editor, editor, terminal_output)
636
- complete_btn.click(complete_fn, editor, editor)
 
637
 
638
- explain_btn.click(explain_fn, editor,
639
- [diff_view, diff_original_state, diff_modified_state]
 
640
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
641
-
642
- refactor_btn.click(refactor_fn, editor,
643
- [diff_view, diff_original_state, diff_modified_state]
644
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
645
-
646
  chat_input.submit(generate_fn, [chat_input, chat_history],
647
  [diff_view, diff_original_state, diff_modified_state, chat_history, chat_input]
648
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
649
-
650
  send_btn.click(generate_fn, [chat_input, chat_history],
651
  [diff_view, diff_original_state, diff_modified_state, chat_history, chat_input]
652
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
653
 
654
- apply_btn.click(lambda mod: mod, diff_modified_state, editor
 
655
  ).then(lambda: gr.Tabs(selected="terminal-tab"), None, bottom_tabs)
656
-
657
- discard_btn.click(lambda: gr.update(), None, editor
658
  ).then(lambda: gr.Tabs(selected="terminal-tab"), None, bottom_tabs)
659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  capture_btn.click(capture_display, None, display_image)
 
 
661
 
662
  if __name__ == "__main__":
663
  import atexit
664
  atexit.register(terminal.cleanup)
 
665
  atexit.register(vdisplay.cleanup)
666
  demo.launch(server_name="0.0.0.0", server_port=7860,
667
  theme=theme, css=css, ssr_mode=False)
 
14
  import termios
15
  import threading
16
  import re
17
+ import ast
18
+ import json
19
+ import types
20
  import tempfile
21
+ from pathlib import Path
22
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, GenerationConfig
23
+ from typing import List, Dict, Optional
24
  from functools import lru_cache
25
 
26
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
27
 
 
28
  MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
29
  DEVICE = "cpu"
30
  DTYPE = torch.float32
31
  DISPLAY_NUM = ":99"
32
  SCREEN_W, SCREEN_H = 800, 600
33
+ SNIPPETS_FILE = "/tmp/axon_snippets.json"
34
 
35
+ # ═══════════════════════════════════════
36
+ # Virtual Display
37
+ # ═══════════════════════════════════════
38
  class VirtualDisplay:
39
  def __init__(self):
40
  self.xvfb_proc = None
41
  self.display = DISPLAY_NUM
42
+ self._ensure_deps()
43
  self._start_xvfb()
44
 
45
+ def _ensure_deps(self):
46
+ needed = []
47
+ for cmd in ["Xvfb", "xdpyinfo"]:
48
+ if subprocess.run(["which", cmd], capture_output=True).returncode != 0:
49
+ needed.extend(["xvfb", "x11-utils"]); break
50
+ for cmd in ["scrot", "import"]:
51
+ if subprocess.run(["which", cmd], capture_output=True).returncode == 0: break
52
+ else:
53
+ needed.extend(["scrot", "imagemagick"])
54
+ if needed:
55
+ print(f"[Xvfb] Installing: {set(needed)}")
56
+ subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
57
+ subprocess.run(["apt-get", "install", "-y", "-qq"] + list(set(needed)),
58
+ capture_output=True, timeout=120)
59
+
60
  def _start_xvfb(self):
61
  try:
62
+ subprocess.run(["pkill", "-f", f"Xvfb {self.display}"], capture_output=True, timeout=5)
 
63
  time.sleep(0.2)
64
  self.xvfb_proc = subprocess.Popen(
65
+ ["Xvfb", self.display, "-screen", "0", f"{SCREEN_W}x{SCREEN_H}x24",
66
+ "-ac", "-nolisten", "tcp"],
67
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
68
  time.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
69
  if self.xvfb_proc.poll() is None:
70
  print(f"[Xvfb] Running on {self.display}")
71
  os.environ["DISPLAY"] = self.display
72
  else:
 
 
 
 
 
 
 
 
 
 
 
73
  self.xvfb_proc = None
74
  except Exception as e:
75
+ print(f"[Xvfb] {e}"); self.xvfb_proc = None
76
+
77
+ def capture(self):
78
+ if not self.is_running: return None
79
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False); tmp.close()
80
+ for cmd in [
81
+ ["scrot", "--display", self.display, tmp.name],
82
+ f"xwd -root -display {self.display} | convert xwd:- png:{tmp.name}",
83
+ ["import", "-window", "root", "-display", self.display, tmp.name],
84
+ ]:
85
+ try:
86
+ kw = {"shell": True} if isinstance(cmd, str) else {}
87
+ r = subprocess.run(cmd, capture_output=True, timeout=5,
88
+ env={**os.environ, "DISPLAY": self.display}, **kw)
89
+ if r.returncode == 0 and os.path.exists(tmp.name) and os.path.getsize(tmp.name) > 200:
90
+ return tmp.name
91
+ except: pass
92
+ try: os.unlink(tmp.name)
93
+ except: pass
94
  return None
95
 
96
  @property
97
+ def is_running(self): return self.xvfb_proc is not None and self.xvfb_proc.poll() is None
 
 
98
  def cleanup(self):
99
  if self.xvfb_proc:
100
  self.xvfb_proc.terminate()
101
+ try: self.xvfb_proc.wait(timeout=3)
102
+ except: self.xvfb_proc.kill()
 
 
103
 
104
  vdisplay = VirtualDisplay()
105
 
106
+ # ═══════════════════════════════════════
107
+ # GUI Process Manager
108
+ # ═══════════════════════════════════════
109
+ class GUIProcessManager:
110
+ def __init__(self):
111
+ self.process = None
112
+ def launch(self, code):
113
+ self.stop()
114
+ tmp = "/tmp/_axon_gui_run.py"
115
+ with open(tmp, "w") as f: f.write(code)
116
+ env = os.environ.copy()
117
+ env.update({"DISPLAY": DISPLAY_NUM, "SDL_VIDEODRIVER": "x11"})
118
+ try:
119
+ self.process = subprocess.Popen([sys.executable, tmp], stdout=subprocess.PIPE,
120
+ stderr=subprocess.PIPE, env=env, preexec_fn=os.setsid)
121
+ time.sleep(1.0)
122
+ if self.process.poll() is not None:
123
+ out, err = self.process.communicate(timeout=2)
124
+ msg = (out.decode(errors="replace") + "\n" + err.decode(errors="replace")).strip()
125
+ return f"[Exited immediately]\n{msg}" if msg else "[Exited]"
126
+ return f"[GUI launched — PID {self.process.pid}] CAPTURE to see output."
127
+ except Exception as e: return f"[Error] {e}"
128
+ def stop(self):
129
+ if self.process and self.process.poll() is None:
130
+ pid = self.process.pid
131
+ try: os.killpg(os.getpgid(pid), signal.SIGTERM); self.process.wait(timeout=3)
132
+ except:
133
+ try: os.killpg(os.getpgid(pid), signal.SIGKILL)
134
+ except: pass
135
+ self.process = None; return f"[Stopped PID {pid}]"
136
+ self.process = None; return "[No process]"
137
+ @property
138
+ def is_running(self): return self.process is not None and self.process.poll() is None
139
+ def get_status(self): return f"● PID {self.process.pid}" if self.is_running else "○ idle"
140
+
141
+ gui_mgr = GUIProcessManager()
142
+
143
+ # ═══════════════════════════════════════
144
+ # File System (with cross-file imports)
145
+ # ═══════════════════════════════════════
146
  class FileSystem:
147
  def __init__(self):
148
+ self.files: Dict[str, str] = {
149
+ "main.py": '# Start coding here\nfrom utils import add\n\nresult = add(3, 7)\nprint(f"3 + 7 = {result}")',
150
+ "utils.py": "# Utility functions\ndef add(a, b):\n return a + b\n\ndef multiply(a, b):\n return a * b",
151
  "notes.txt": "Project requirements:\n- Build a cool AI app\n- Use Gradio"
152
  }
153
  self.current_file = "main.py"
154
+ self._sync_dir = "/tmp/axon_workspace"
155
+ self._sync_to_disk()
156
+
157
+ def _sync_to_disk(self):
158
+ """Write all virtual files to disk so cross-file imports work."""
159
+ os.makedirs(self._sync_dir, exist_ok=True)
160
+ for name, content in self.files.items():
161
+ path = os.path.join(self._sync_dir, name)
162
+ with open(path, "w") as f:
163
+ f.write(content)
164
+
165
+ def save_file(self, content):
166
  if self.current_file:
167
  self.files[self.current_file] = content
168
+ # Sync to disk for imports
169
+ path = os.path.join(self._sync_dir, self.current_file)
170
+ os.makedirs(os.path.dirname(path) if "/" in self.current_file else self._sync_dir, exist_ok=True)
171
+ with open(path, "w") as f:
172
+ f.write(content)
173
+
174
+ def get_current_file_content(self): return self.files.get(self.current_file, "")
175
+ def set_current_file(self, f):
176
+ if f in self.files: self.current_file = f
177
+ def create_file(self, f, c=""):
178
+ if f not in self.files:
179
+ self.files[f] = c; self.current_file = f
180
+ self._sync_to_disk()
181
+ def delete_file(self, f):
182
+ if f in self.files and len(self.files) > 1:
183
+ del self.files[f]
184
+ path = os.path.join(self._sync_dir, f)
185
+ if os.path.exists(path): os.unlink(path)
186
+ self.current_file = list(self.files.keys())[0]
187
+ return True
188
+ return False
189
+ def rename_file(self, old, new):
190
+ if old in self.files and new not in self.files:
191
+ self.files[new] = self.files.pop(old)
192
+ old_path = os.path.join(self._sync_dir, old)
193
+ if os.path.exists(old_path): os.unlink(old_path)
194
+ self._sync_to_disk()
195
+ if self.current_file == old: self.current_file = new
196
+ return True
197
+ return False
198
+ def get_all_files(self): return list(self.files.keys())
199
 
200
+ fs = FileSystem()
 
 
 
 
 
 
 
 
 
 
201
 
202
+ # ═══════════════════════════════════════
203
+ # Snippet Library
204
+ # ═══════════════════════════════════════
205
+ class SnippetLibrary:
206
+ def __init__(self):
207
+ self.snippets: Dict[str, dict] = {}
208
+ self._load()
209
 
210
+ def _load(self):
211
+ try:
212
+ if os.path.exists(SNIPPETS_FILE):
213
+ with open(SNIPPETS_FILE) as f:
214
+ self.snippets = json.load(f)
215
+ except: self.snippets = {}
216
 
217
+ def _save(self):
218
+ try:
219
+ with open(SNIPPETS_FILE, "w") as f:
220
+ json.dump(self.snippets, f, indent=2)
221
+ except: pass
222
+
223
+ def add(self, name: str, code: str, tags: str = "") -> str:
224
+ if not name.strip(): return "Error: name required"
225
+ self.snippets[name.strip()] = {
226
+ "code": code,
227
+ "tags": [t.strip() for t in tags.split(",") if t.strip()],
228
+ "created": time.strftime("%Y-%m-%d %H:%M"),
229
+ }
230
+ self._save()
231
+ return f"Saved snippet: {name.strip()}"
232
+
233
+ def get(self, name: str) -> str:
234
+ s = self.snippets.get(name)
235
+ return s["code"] if s else ""
236
+
237
+ def delete(self, name: str) -> str:
238
+ if name in self.snippets:
239
+ del self.snippets[name]
240
+ self._save()
241
+ return f"Deleted: {name}"
242
+ return "Not found"
243
+
244
+ def search(self, query: str) -> List[str]:
245
+ q = query.lower()
246
+ results = []
247
+ for name, data in self.snippets.items():
248
+ if q in name.lower() or any(q in t.lower() for t in data.get("tags", [])):
249
+ results.append(name)
250
+ return results if query else list(self.snippets.keys())
251
+
252
+ def list_all(self) -> str:
253
+ if not self.snippets: return "No snippets saved yet."
254
+ lines = []
255
+ for name, data in self.snippets.items():
256
+ tags = ", ".join(data.get("tags", []))
257
+ lines.append(f" {name} [{tags}] ({data.get('created', '?')})")
258
+ return "\n".join(lines)
259
+
260
+ def get_names(self) -> List[str]:
261
+ return list(self.snippets.keys())
262
+
263
+ snippets = SnippetLibrary()
264
+
265
+ # ═══════════════════════════════════════
266
+ # Code Structure Analyzer
267
+ # ═══════════════════════════════════════
268
+ class CodeAnalyzer:
269
+ """Parses Python code and extracts structure (functions, classes, imports)."""
270
+
271
+ @staticmethod
272
+ def analyze(code: str) -> str:
273
+ if not code.strip(): return "Empty file"
274
+ try:
275
+ tree = ast.parse(code)
276
+ except SyntaxError as e:
277
+ return f"⚠ Syntax error: line {e.lineno}: {e.msg}"
278
+
279
+ lines = []
280
+ imports = []
281
+ functions = []
282
+ classes = []
283
+
284
+ for node in ast.walk(tree):
285
+ if isinstance(node, ast.Import):
286
+ for alias in node.names:
287
+ imports.append(alias.name)
288
+ elif isinstance(node, ast.ImportFrom):
289
+ imports.append(f"from {node.module} import {', '.join(a.name for a in node.names)}")
290
+ elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
291
+ # Check if it's a method (inside a class)
292
+ args = ", ".join(a.arg for a in node.args.args)
293
+ prefix = "async " if isinstance(node, ast.AsyncFunctionDef) else ""
294
+ decorators = [f"@{ast.dump(d)}" for d in node.decorator_list] if hasattr(node, 'decorator_list') else []
295
+ ret = ""
296
+ if node.returns:
297
+ try: ret = f" → {ast.unparse(node.returns)}"
298
+ except: ret = " → ?"
299
+ functions.append((node.lineno, f"{prefix}def {node.name}({args}){ret}"))
300
+ elif isinstance(node, ast.ClassDef):
301
+ bases = ", ".join(ast.unparse(b) for b in node.bases) if node.bases else ""
302
+ bases_str = f"({bases})" if bases else ""
303
+ methods = []
304
+ for item in node.body:
305
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
306
+ methods.append(item.name)
307
+ classes.append((node.lineno, node.name + bases_str, methods))
308
+
309
+ # Count stats
310
+ total_lines = len(code.splitlines())
311
+ non_empty = len([l for l in code.splitlines() if l.strip()])
312
+
313
+ # Build output
314
+ lines.append(f"📊 {total_lines} lines ({non_empty} non-empty)")
315
+ lines.append("")
316
+
317
+ if imports:
318
+ lines.append(f"📦 IMPORTS ({len(imports)})")
319
+ for i in imports:
320
+ lines.append(f" {i}")
321
+ lines.append("")
322
+
323
+ if classes:
324
+ lines.append(f"🏗 CLASSES ({len(classes)})")
325
+ for lineno, name, methods in classes:
326
+ lines.append(f" L{lineno}: class {name}")
327
+ for m in methods:
328
+ lines.append(f" .{m}()")
329
+ lines.append("")
330
+
331
+ if functions:
332
+ # Filter out class methods (they're shown under classes)
333
+ class_methods = set()
334
+ for _, _, methods in classes:
335
+ class_methods.update(methods)
336
+ standalone = [(ln, f) for ln, f in functions if not any(
337
+ f.split("(")[0].replace("def ", "").replace("async ", "").strip() in class_methods
338
+ for _ in [None]
339
+ )]
340
+ # Actually just show all functions for simplicity
341
+ lines.append(f"⚡ FUNCTIONS ({len(functions)})")
342
+ for lineno, sig in functions:
343
+ lines.append(f" L{lineno}: {sig}")
344
+
345
+ if not imports and not functions and not classes:
346
+ lines.append("(No functions, classes, or imports found)")
347
+
348
+ return "\n".join(lines)
349
+
350
+ analyzer = CodeAnalyzer()
351
+
352
+ # ═══════════════════════════════════════
353
+ # Find & Replace
354
+ # ═══════════════════════════════════════
355
+ class FindReplace:
356
+ @staticmethod
357
+ def find_in_file(code: str, query: str, case_sensitive: bool = False) -> str:
358
+ if not query: return ""
359
+ lines = code.splitlines()
360
+ results = []
361
+ for i, line in enumerate(lines, 1):
362
+ check_line = line if case_sensitive else line.lower()
363
+ check_query = query if case_sensitive else query.lower()
364
+ if check_query in check_line:
365
+ results.append(f" L{i}: {line.strip()}")
366
+ if results:
367
+ return f"Found {len(results)} match(es):\n" + "\n".join(results)
368
+ return "No matches found."
369
+
370
+ @staticmethod
371
+ def find_in_all_files(files: Dict[str, str], query: str) -> str:
372
+ if not query: return ""
373
+ results = []
374
+ for fname, content in files.items():
375
+ lines = content.splitlines()
376
+ for i, line in enumerate(lines, 1):
377
+ if query.lower() in line.lower():
378
+ results.append(f" {fname}:L{i}: {line.strip()}")
379
+ if results:
380
+ return f"Found {len(results)} match(es) across all files:\n" + "\n".join(results)
381
+ return "No matches found."
382
+
383
+ @staticmethod
384
+ def replace_in_file(code: str, find: str, replace: str, case_sensitive: bool = False) -> tuple:
385
+ if not find: return code, "Nothing to find."
386
+ if case_sensitive:
387
+ count = code.count(find)
388
+ new_code = code.replace(find, replace)
389
+ else:
390
+ pattern = re.compile(re.escape(find), re.IGNORECASE)
391
+ matches = pattern.findall(code)
392
+ count = len(matches)
393
+ new_code = pattern.sub(replace, code)
394
+ return new_code, f"Replaced {count} occurrence(s)."
395
+
396
+ finder = FindReplace()
397
+
398
+ # ═══════════════════════════════════════
399
+ # PTY Terminal (with history)
400
+ # ═══════════════════════════════════════
401
  class PTYTerminal:
402
  STRIP_ANSI = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[.*?[@-~]|\r')
 
403
  def __init__(self):
404
+ self.master_fd = None; self.pid = None
405
+ self.lock = threading.Lock(); self.log_lines = []; self.max_lines = 800
406
+ self.cmd_history: List[str] = []
407
+ self.max_history = 100
 
408
  self._spawn_shell()
409
+ self._append("AXON TERMINAL v4.0")
410
  self._append("══════════════════════════════════════")
411
+ self._append("Full PTY shell · Cross-file imports")
412
+ self._append(" pip/npm/apt-get/git/curl/make")
413
+ self._append(" GUI apps Display · History → ↑↓")
414
  self._append("══════════════════════════════════════")
415
 
416
  def _spawn_shell(self):
417
  try:
418
+ pid, fd = pty.openpty(); self.pid = os.fork()
 
419
  if self.pid == 0:
420
+ os.close(pid); os.setsid(); fcntl.ioctl(fd, termios.TIOCSCTTY, 0)
421
+ os.dup2(fd,0); os.dup2(fd,1); os.dup2(fd,2)
422
+ if fd>2: os.close(fd)
 
 
423
  env = os.environ.copy()
424
+ env.update({"TERM":"dumb","PS1":"$ ","DEBIAN_FRONTEND":"noninteractive","DISPLAY":DISPLAY_NUM})
425
+ os.execvpe("/bin/bash",["/bin/bash","--norc","--noprofile","-i"],env)
 
426
  else:
427
+ os.close(fd); self.master_fd = pid
428
+ fl = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
429
+ fcntl.fcntl(self.master_fd, fcntl.F_SETFL, fl|os.O_NONBLOCK)
430
+ fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, struct.pack("HHHH",40,120,0,0))
431
+ time.sleep(0.3); self._read(0.2)
432
+ except Exception as e: self._append(f"[Shell Error] {e}"); self.master_fd=None
433
+
434
+ def _read(self, timeout=0.1):
435
+ if not self.master_fd: return ""
436
+ out=[]; dl=time.time()+timeout
 
 
 
 
 
 
437
  while True:
438
+ r=dl-time.time()
439
+ if r<=0: break
440
  try:
441
+ rd,_,_=select.select([self.master_fd],[],[],min(r,0.05))
442
+ if rd:
443
+ c=os.read(self.master_fd,4096)
444
+ if c: out.append(c.decode("utf-8",errors="replace")); dl=time.time()+0.15
 
 
445
  else: break
446
  elif out: break
447
  except OSError: break
448
  return "".join(out)
449
 
450
+ def _clean(self, t):
451
+ c=self.STRIP_ANSI.sub("",t); return "".join(ch for ch in c if ch in "\n\t" or ord(ch)>=32)
452
+ def _append(self, t):
453
+ self.log_lines.append(t)
454
+ while len(self.log_lines)>self.max_lines: self.log_lines.pop(0)
455
+ def get_log(self): return "\n".join(self.log_lines)
456
 
457
+ def run_command(self, cmd):
458
+ cmd=cmd.strip()
 
 
 
 
 
 
 
 
459
  if not cmd: return self.get_log()
460
+ # Save to history
461
+ if not self.cmd_history or self.cmd_history[-1] != cmd:
462
+ self.cmd_history.append(cmd)
463
+ if len(self.cmd_history) > self.max_history:
464
+ self.cmd_history.pop(0)
465
  with self.lock:
466
+ if cmd.lower()=="clear": self.log_lines=[]; return ""
467
+ if cmd.lower()=="history":
468
+ self._append("Command history:")
469
+ for i, c in enumerate(self.cmd_history[-20:], 1):
470
+ self._append(f" {i:3d} {c}")
 
 
 
 
 
 
 
 
471
  return self.get_log()
472
+ if not self.master_fd:
473
+ self._append(f"$ {cmd}"); self._append("[Error] No shell."); return self.get_log()
474
+ self._read(0.05); self._append(f"$ {cmd}")
475
+ try: os.write(self.master_fd,(cmd+"\n").encode())
476
+ except OSError as e: self._append(f"[Write Error] {e}"); return self.get_log()
477
+ parts=cmd.split(); base=parts[0].lower() if parts else ""
478
+ lng=["pip","pip3","npm","npx","apt-get","apt","git","wget","curl","make","cmake","cargo","yarn","conda"]
479
+ wait=180 if base in lng else(60 if base in("python","python3","node")else 15)
480
+ chunks=[]; start=time.time(); idle=0
481
+ while time.time()-start<wait:
482
+ c=self._read(0.3)
483
+ if c: idle=0; chunks.append(c)
 
 
 
484
  else:
485
+ idle+=1
486
+ if base not in lng and idle>=3: break
487
+ if base in lng and idle>=10: break
488
+ if chunks and "".join(chunks).rstrip().endswith("$"): break
489
+ raw=self._clean("".join(chunks)); lines=raw.split("\n"); filtered=[]; skip=True
 
 
 
490
  for l in lines:
491
+ s=l.strip()
492
+ if skip and s==cmd.strip(): skip=False; continue
493
+ if s in("$","$ "): continue
 
494
  filtered.append(l)
495
+ result="\n".join(filtered).strip()
496
  if result: self._append(result)
497
  return self.get_log()
498
 
499
+ def get_history_choices(self):
500
+ return list(reversed(self.cmd_history[-30:]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
  def cleanup(self):
503
+ if self.pid and self.pid>0:
504
  try: os.kill(self.pid, signal.SIGTERM)
505
+ except: pass
506
+ if self.master_fd:
507
  try: os.close(self.master_fd)
508
+ except: pass
509
 
510
  terminal = PTYTerminal()
511
 
512
+ # ═══════════════════════════════════════
513
+ # Model
514
+ # ═══════════════════════════════════════
515
  @lru_cache(maxsize=1)
516
  def load_model():
517
+ print("Loading TinyLlama..."); t0=time.time()
 
518
  try:
519
+ tok=AutoTokenizer.from_pretrained(MODEL_NAME)
520
+ mdl=AutoModelForCausalLM.from_pretrained(MODEL_NAME,dtype=DTYPE,device_map=DEVICE,low_cpu_mem_usage=True)
521
+ gc=GenerationConfig(max_new_tokens=256,temperature=0.2,top_p=0.95,do_sample=True)
522
+ pipe=pipeline("text-generation",model=mdl,tokenizer=tok,generation_config=gc)
523
+ print(f"Loaded in {time.time()-t0:.1f}s"); return pipe
524
+ except Exception as e: print(f"Model error: {e}"); return None
525
+
526
+ def _llm(s,u,mt=512):
527
+ m=load_model()
 
 
 
 
528
  if not m: return "Error: model not loaded"
529
+ try: return m(f"<|system|>{s}</s><|user|>\n{u}\n</s><|assistant|>",max_new_tokens=mt)[0]["generated_text"].strip()
530
+ except Exception as e: return str(e)
531
+
532
+ def generate_completion(c): return _llm("Complete this Python code. No explanations.",c,256)
533
+ def explain_code(c): return _llm("Explain this Python code concisely.",c)
534
+ def refactor_code(c): return _llm("Refactor for PEP 8 and best practices.",c)
535
+ def generate_code(p): return _llm(f"Write Python code for: {p}","")
536
+ def create_diff_view(o,m):
537
+ import difflib; return "\n".join(difflib.unified_diff(o.splitlines(),m.splitlines(),lineterm=""))
538
+
539
+ GUI_HINTS=["pygame","tkinter","turtle","pyglet","arcade","kivy","wx","Gtk","QApplication",
540
+ "display.set_mode","mainloop()"]
541
+ def is_gui_code(c): return any(h in c for h in GUI_HINTS)
542
+
543
+ # ═══════════════════════════════════════
544
+ # Theme + CSS
545
+ # ═══════════════════════════════════════
 
 
546
  theme = gr.themes.Default(
547
+ font=[gr.themes.GoogleFont("IBM Plex Mono"),"monospace"],
548
+ primary_hue="cyan", neutral_hue="gray",
 
549
  ).set(
550
+ body_background_fill="#080808", body_text_color="#e0e0e0",
551
+ button_primary_background_fill="#0ff", button_primary_background_fill_hover="#0dd",
 
 
552
  button_primary_text_color="#000",
553
+ button_secondary_background_fill="#1a1a1a", button_secondary_background_fill_hover="#2a2a2a",
554
+ button_secondary_text_color="#0ff", button_secondary_border_color="#0ff4",
555
+ block_background_fill="#0d0d0d", block_border_color="#1a1a1a",
 
 
 
556
  block_label_text_color="#0f8",
557
+ input_background_fill="#0a0a0a", input_border_color="#1a3a2a", input_placeholder_color="#0f83",
 
 
558
  )
559
 
 
 
 
 
560
  css = """
561
  * { border-radius: 0 !important; }
562
  .gradio-container { max-width: 100% !important; }
 
 
563
  .axon-header {
564
  background: linear-gradient(90deg, #080808 0%, #0a1a1a 50%, #080808 100%);
565
+ border-bottom: 1px solid #0ff3; padding: 12px 20px !important; margin-bottom: 8px;
 
 
566
  }
567
  .axon-header h1 {
568
  font-family: 'IBM Plex Mono', monospace !important;
569
  background: linear-gradient(90deg, #0ff, #0f8);
570
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
571
+ font-size: 1.4em !important; letter-spacing: 3px; margin: 0 !important;
 
 
 
572
  }
573
  .axon-header p { color: #0f84 !important; font-size: 0.75em; letter-spacing: 1px; }
 
 
574
  .terminal-box textarea {
575
+ background: #050505 !important; color: #00ff41 !important;
 
576
  font-family: 'IBM Plex Mono', monospace !important;
577
+ font-size: 12px !important; line-height: 1.5 !important;
578
+ border: 1px solid #0f31 !important; text-shadow: 0 0 4px #0f32;
 
 
579
  }
580
  .terminal-input input {
581
+ background: #050505 !important; color: #00ff41 !important;
 
582
  font-family: 'IBM Plex Mono', monospace !important;
583
+ border: 1px solid #0f32 !important; font-size: 12px !important;
 
 
584
  }
585
  .terminal-input input::placeholder { color: #0f83 !important; }
586
  .terminal-input input:focus { border-color: #0ff !important; box-shadow: 0 0 8px #0ff2 !important; }
 
 
587
  .code-editor textarea, .code-editor .cm-editor {
588
+ font-family: 'IBM Plex Mono', monospace !important; font-size: 13px !important;
 
589
  }
 
 
 
 
 
 
 
 
 
590
  .toolbar-btn {
591
+ font-size: 0.8em !important; padding: 6px 12px !important; letter-spacing: 0.5px;
592
+ text-transform: uppercase; font-weight: 600 !important;
593
+ border: 1px solid transparent !important; transition: all 0.2s ease !important;
 
 
 
 
594
  }
595
  .toolbar-btn:hover { border-color: #0ff !important; box-shadow: 0 0 12px #0ff2 !important; }
 
 
596
  .gr-accordion { border: 1px solid #1a2a2a !important; background: #0a0a0a !important; }
597
  .gr-accordion > .label-wrap {
598
+ background: #0d0d0d !important; border-bottom: 1px solid #1a2a2a !important;
 
 
599
  }
600
  .gr-accordion > .label-wrap:hover { background: #121a1a !important; }
601
  .gr-accordion > .label-wrap span {
602
+ color: #0ff !important; font-family: 'IBM Plex Mono', monospace !important;
603
+ letter-spacing: 1px; font-size: 0.85em;
 
 
604
  }
 
 
605
  .tabs > .tab-nav > button {
606
  font-family: 'IBM Plex Mono', monospace !important;
607
  letter-spacing: 1px; font-size: 0.8em; text-transform: uppercase;
608
  color: #888 !important; border-bottom: 2px solid transparent !important;
 
609
  }
610
  .tabs > .tab-nav > button.selected {
611
+ color: #0ff !important; border-bottom-color: #0ff !important; text-shadow: 0 0 8px #0ff4;
 
612
  }
 
 
613
  .status-bar {
614
  background: #0a0a0a !important; border-top: 1px solid #1a2a2a;
615
  padding: 4px 12px !important; font-size: 11px !important; color: #0f84 !important;
616
  }
617
  .status-bar strong { color: #0ff !important; }
 
 
618
  ::-webkit-scrollbar { width: 6px; height: 6px; }
619
  ::-webkit-scrollbar-track { background: #0a0a0a; }
620
  ::-webkit-scrollbar-thumb { background: #0f83; }
 
 
 
621
  input, textarea, select { color: #0f8 !important; }
622
+ .stop-btn { background: #a00 !important; color: #fff !important; border: none !important; }
623
+ .stop-btn:hover { background: #c00 !important; }
624
+ /* Structure map */
625
+ .structure-box textarea {
626
+ background: #050505 !important; color: #0ff !important;
627
+ font-family: 'IBM Plex Mono', monospace !important; font-size: 11px !important;
628
+ }
629
+ /* Snippet code */
630
+ .snippet-code textarea {
631
+ background: #050505 !important; color: #c8e6c9 !important;
632
+ font-family: 'IBM Plex Mono', monospace !important; font-size: 12px !important;
633
+ }
634
+ /* Find results */
635
+ .find-results textarea {
636
+ background: #050505 !important; color: #ffab40 !important;
637
+ font-family: 'IBM Plex Mono', monospace !important; font-size: 11px !important;
638
+ }
639
+ /* History dropdown */
640
+ .history-dd { font-size: 12px !important; }
641
  """
642
 
643
+ # ═══════════════════════════════════════
644
+ # UI
645
+ # ═══════════════════════════════════════
 
646
  with gr.Blocks(title="Axon Pro — Python AI IDE") as demo:
647
 
648
  with gr.Row(elem_classes="axon-header"):
649
+ gr.Markdown("# ⬡ AXON PRO\n\nPYTHON AI IDE — v4.0")
650
 
651
  with gr.Row(equal_height=False):
652
 
653
+ # ══ LEFT SIDEBAR ══
654
+ with gr.Column(scale=1, min_width=200):
655
+
656
+ # File Explorer
657
  with gr.Accordion("📁 EXPLORER", open=True):
658
+ file_list = gr.Dropdown(choices=fs.get_all_files(), value=fs.current_file,
659
+ label="", interactive=True, container=False)
 
660
  new_file_name = gr.Textbox(placeholder="filename.py", label="", container=False)
661
  with gr.Row():
662
+ new_file_btn = gr.Button("+ NEW", variant="secondary", size="sm", elem_classes="toolbar-btn")
663
+ save_btn = gr.Button("SAVE", variant="secondary", size="sm", elem_classes="toolbar-btn")
664
+ del_file_btn = gr.Button("✗ DEL", variant="secondary", size="sm", elem_classes="toolbar-btn stop-btn")
665
+
666
+ # Code Structure
667
+ with gr.Accordion("🗺 STRUCTURE", open=True):
668
+ structure_view = gr.Textbox(
669
+ value=analyzer.analyze(fs.get_current_file_content()),
670
+ lines=10, max_lines=20, interactive=False,
671
+ elem_classes="structure-box", label="", show_label=False)
672
+
673
+ # Snippets
674
+ with gr.Accordion("📋 SNIPPETS", open=False):
675
+ snippet_list = gr.Dropdown(choices=snippets.get_names(), label="Saved Snippets",
676
+ interactive=True, container=False, elem_classes="history-dd")
677
+ snippet_search = gr.Textbox(placeholder="Search snippets...", label="", container=False)
678
+ with gr.Row():
679
+ snippet_insert_btn = gr.Button("INSERT", variant="primary", size="sm", elem_classes="toolbar-btn")
680
+ snippet_del_btn = gr.Button("DEL", variant="secondary", size="sm", elem_classes="toolbar-btn stop-btn")
681
+ with gr.Accordion("Save New Snippet", open=False):
682
+ snippet_name = gr.Textbox(placeholder="snippet name", label="", container=False)
683
+ snippet_tags = gr.Textbox(placeholder="tags: api, util, loop", label="", container=False)
684
+ snippet_save_btn = gr.Button("SAVE SNIPPET", variant="primary", size="sm", elem_classes="toolbar-btn")
685
+ snippet_status = gr.Markdown("")
686
+
687
+ # ══ CENTER ══
688
  with gr.Column(scale=4):
689
+ editor = gr.Code(value=fs.get_current_file_content(), label=f" {fs.current_file}",
690
+ language="python", lines=22, interactive=True, elem_classes="code-editor")
 
 
 
691
 
692
  with gr.Row():
693
  run_btn = gr.Button("▶ RUN", variant="primary", elem_classes="toolbar-btn")
694
+ run_gui_btn = gr.Button("🖥 GUI", variant="primary", elem_classes="toolbar-btn")
695
+ stop_gui_btn = gr.Button("■ STOP", variant="secondary", size="sm", elem_classes="toolbar-btn stop-btn")
696
  complete_btn = gr.Button("✦ COMPLETE", variant="secondary", elem_classes="toolbar-btn")
697
  explain_btn = gr.Button("◈ EXPLAIN", variant="secondary", elem_classes="toolbar-btn")
698
  refactor_btn = gr.Button("⟲ REFACTOR", variant="secondary", elem_classes="toolbar-btn")
699
 
700
  with gr.Tabs() as bottom_tabs:
701
+
702
  with gr.Tab("⌘ TERMINAL", id="terminal-tab"):
703
+ terminal_output = gr.Textbox(value=terminal.get_log(), lines=10, max_lines=25,
704
+ interactive=False, elem_classes="terminal-box", label="", show_label=False)
 
 
705
  with gr.Row():
706
  terminal_input = gr.Textbox(
707
+ placeholder="$ pip install pygame | npm i | git clone ...",
708
+ scale=8, container=False, elem_classes="terminal-input")
709
  terminal_run_btn = gr.Button("⏎", variant="primary", scale=1, min_width=45)
710
+ with gr.Row():
711
+ clear_btn = gr.Button("CLEAR", variant="secondary", size="sm", elem_classes="toolbar-btn")
712
+ history_dd = gr.Dropdown(choices=[], label="", container=False,
713
+ interactive=True, scale=3, elem_classes="history-dd")
714
+ history_refresh = gr.Button("↻ HISTORY", variant="secondary", size="sm",
715
+ elem_classes="toolbar-btn")
716
+
717
+ with gr.Tab("🔍 FIND", id="find-tab"):
718
+ with gr.Row():
719
+ find_query = gr.Textbox(placeholder="Search for...", label="Find", scale=4)
720
+ replace_query = gr.Textbox(placeholder="Replace with...", label="Replace", scale=4)
721
+ find_case = gr.Checkbox(label="Case sensitive", value=False)
722
+ with gr.Row():
723
+ find_btn = gr.Button("FIND", variant="primary", size="sm", elem_classes="toolbar-btn")
724
+ find_all_btn = gr.Button("FIND ALL FILES", variant="secondary", size="sm", elem_classes="toolbar-btn")
725
+ replace_btn = gr.Button("REPLACE ALL", variant="secondary", size="sm", elem_classes="toolbar-btn")
726
+ find_results = gr.Textbox(value="", lines=6, interactive=False,
727
+ elem_classes="find-results", label="", show_label=False)
728
 
729
  with gr.Tab("💬 AI CHAT", id="chat-tab"):
730
  chat_history = gr.Chatbot(label="", height=250)
731
  with gr.Row():
732
+ chat_input = gr.Textbox(placeholder="Describe code you want...",
733
  scale=8, container=False)
734
+ send_btn = gr.Button("GEN", variant="primary", scale=1, elem_classes="toolbar-btn")
 
735
 
736
  with gr.Tab("⇄ DIFF", id="diff-tab"):
737
+ diff_view = gr.Code(label="AI Changes", language="python", interactive=False, lines=15)
 
738
  with gr.Row():
739
+ apply_btn = gr.Button("✓ APPLY", variant="primary", elem_classes="toolbar-btn")
740
+ discard_btn = gr.Button("✗ DISCARD", variant="secondary", elem_classes="toolbar-btn")
 
 
741
 
742
+ # ══ RIGHT: DISPLAY ══
743
  with gr.Column(scale=2, min_width=250):
744
  with gr.Accordion("🖥 DISPLAY OUTPUT", open=True):
745
+ display_image = gr.Image(label="", type="filepath", interactive=False, height=420)
 
 
 
 
 
746
  with gr.Row():
747
+ capture_btn = gr.Button("📸 CAPTURE", variant="secondary", size="sm", elem_classes="toolbar-btn")
748
+ auto_capture = gr.Checkbox(label="Auto (2s)", value=False)
749
+ gui_status = gr.Markdown(
750
+ f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} "
751
+ f"| {SCREEN_W}x{SCREEN_H} | {gui_mgr.get_status()}</small>")
 
 
752
 
753
  status_bar = gr.Markdown(
754
+ f"**AXON PRO v4.0** │ Python {sys.version.split()[0]} │ CPU │ "
755
+ f"TinyLlama-1.1B │ PTY + Xvfb Snippets + Structure + Find",
756
  elem_classes="status-bar")
757
 
758
+ # State
759
  diff_original_state = gr.State("")
760
  diff_modified_state = gr.State("")
761
+ auto_timer = gr.Timer(2, active=False)
762
+
763
+ # ═══════════════════════════════════════
764
+ # HANDLERS
765
+ # ═══════════════════════════════════════
766
 
 
767
  def update_file(content):
768
  fs.save_file(content)
769
+ return analyzer.analyze(content)
770
 
771
  def load_file(filename):
772
  fs.set_current_file(filename)
773
+ content = fs.get_current_file_content()
774
+ return content, gr.update(label=f" {filename}"), analyzer.analyze(content)
775
 
776
  def create_file_fn(name):
777
+ if name and ("." in name or name.endswith("")):
778
  fs.create_file(name)
779
+ content = fs.get_current_file_content()
780
  return (gr.update(choices=fs.get_all_files(), value=name),
781
+ content, gr.update(label=f" {name}"), analyzer.analyze(content))
782
+ return gr.update(choices=fs.get_all_files()), fs.get_current_file_content(), gr.update(), gr.update()
783
+
784
+ def delete_file_fn():
785
+ name = fs.current_file
786
+ if fs.delete_file(name):
787
+ content = fs.get_current_file_content()
788
+ return (gr.update(choices=fs.get_all_files(), value=fs.current_file),
789
+ content, gr.update(label=f" {fs.current_file}"),
790
+ analyzer.analyze(content))
791
+ return gr.update(), fs.get_current_file_content(), gr.update(), gr.update()
792
 
793
  def terminal_cmd(cmd):
794
  return terminal.run_command(cmd), ""
795
 
796
+ def clear_term(): terminal.log_lines=[]; return ""
 
 
 
 
 
797
 
798
+ def refresh_history():
799
+ return gr.update(choices=terminal.get_history_choices())
800
 
801
+ def select_history(cmd):
802
+ if cmd: return cmd
803
+ return gr.update()
 
 
804
 
805
+ # Run with cross-file import support
806
+ def run_editor(content):
807
  fs.save_file(content)
808
+ if is_gui_code(content): return run_gui(content)[0]
809
+ terminal._append("$ python [editor]")
810
+ tmp = "/tmp/_axon_run.py"
811
+ with open(tmp, "w") as f: f.write(content)
812
+ try:
813
+ env = os.environ.copy()
814
+ env["DISPLAY"] = DISPLAY_NUM
815
+ # Add workspace to PYTHONPATH so cross-file imports work
816
+ env["PYTHONPATH"] = fs._sync_dir + ":" + env.get("PYTHONPATH", "")
817
+ r = subprocess.run([sys.executable, tmp], capture_output=True, text=True,
818
+ timeout=30, env=env, cwd=fs._sync_dir)
819
+ if r.stdout.strip(): terminal._append(r.stdout.rstrip())
820
+ if r.stderr.strip(): terminal._append(r.stderr.rstrip())
821
+ if not r.stdout.strip() and not r.stderr.strip(): terminal._append("(No output)")
822
+ except subprocess.TimeoutExpired: terminal._append("[Timed out 30s]")
823
+ except Exception as e: terminal._append(f"[Error] {e}")
824
+ return terminal.get_log()
825
+
826
+ def run_gui(content):
827
+ fs.save_file(content); msg=gui_mgr.launch(content); terminal._append(msg)
828
+ time.sleep(1.5); ss=vdisplay.capture()
829
+ st=f"<small>Xvfb: ● ON | {SCREEN_W}x{SCREEN_H} | {gui_mgr.get_status()}</small>"
830
+ return terminal.get_log(), ss, st
831
+
832
+ def stop_gui():
833
+ msg=gui_mgr.stop(); terminal._append(msg)
834
+ st=f"<small>Xvfb: {'● ON' if vdisplay.is_running else '○ OFF'} | {SCREEN_W}x{SCREEN_H} | {gui_mgr.get_status()}</small>"
835
+ return terminal.get_log(), st
836
+
837
+ def capture_display(): return vdisplay.capture()
838
+ def auto_capture_tick():
839
+ if gui_mgr.is_running: return vdisplay.capture()
840
+ return gr.update()
841
+ def toggle_auto(c): return gr.Timer(2, active=c)
842
+
843
+ def complete_fn(c):
844
+ result = c+"\n"+generate_completion(c)
845
+ return result, analyzer.analyze(result)
846
+ def explain_fn(c):
847
+ e=explain_code(c); fs.save_file(c); m=f'"""\nEXPLANATION:\n{e}\n"""\n\n{c}'
848
+ return create_diff_view(c,m),c,m
849
+ def refactor_fn(c):
850
+ r=refactor_code(c); fs.save_file(c); return create_diff_view(c,r),c,r
851
+ def generate_fn(p,h):
852
+ g=generate_code(p); d=create_diff_view("",g)
853
+ nh=h+[{"role":"user","content":p},{"role":"assistant","content":"Code generated → DIFF tab"}]
854
+ return d,"",g,nh,""
855
+
856
+ # Snippets
857
+ def save_snippet(name, tags, editor_content):
858
+ msg = snippets.add(name, editor_content, tags)
859
+ return msg, gr.update(choices=snippets.get_names())
860
+
861
+ def insert_snippet(name):
862
+ code = snippets.get(name)
863
+ return code if code else gr.update()
864
+
865
+ def delete_snippet(name):
866
+ msg = snippets.delete(name)
867
+ return msg, gr.update(choices=snippets.get_names())
868
+
869
+ def search_snippets(query):
870
+ results = snippets.search(query)
871
+ return gr.update(choices=results)
872
+
873
+ # Find & Replace
874
+ def do_find(code, query, case):
875
+ return finder.find_in_file(code, query, case)
876
+
877
+ def do_find_all(query):
878
+ return finder.find_in_all_files(fs.files, query)
879
+
880
+ def do_replace(code, find_q, replace_q, case):
881
+ new_code, msg = finder.replace_in_file(code, find_q, replace_q, case)
882
+ fs.save_file(new_code)
883
+ return new_code, msg, analyzer.analyze(new_code)
884
+
885
+ # ═══════════════════════════════════════
886
+ # WIRING
887
+ # ═══════════════════════════════════════
888
+
889
+ # File operations
890
+ editor.change(update_file, editor, structure_view)
891
+ file_list.change(load_file, file_list, [editor, editor, structure_view])
892
+ new_file_btn.click(create_file_fn, new_file_name, [file_list, editor, editor, structure_view])
893
+ del_file_btn.click(delete_file_fn, None, [file_list, editor, editor, structure_view])
894
+
895
+ # Terminal
896
  terminal_input.submit(terminal_cmd, terminal_input, [terminal_output, terminal_input])
897
  terminal_run_btn.click(terminal_cmd, terminal_input, [terminal_output, terminal_input])
898
  clear_btn.click(clear_term, None, terminal_output)
899
+ history_refresh.click(refresh_history, None, history_dd)
900
+ history_dd.change(select_history, history_dd, terminal_input)
901
 
902
+ # Run
903
  run_btn.click(run_editor, editor, terminal_output)
904
+ run_gui_btn.click(run_gui, editor, [terminal_output, display_image, gui_status])
905
+ stop_gui_btn.click(stop_gui, None, [terminal_output, gui_status])
906
 
907
+ # AI
908
+ complete_btn.click(complete_fn, editor, [editor, structure_view])
909
+ explain_btn.click(explain_fn, editor, [diff_view, diff_original_state, diff_modified_state]
910
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
911
+ refactor_btn.click(refactor_fn, editor, [diff_view, diff_original_state, diff_modified_state]
 
 
912
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
 
913
  chat_input.submit(generate_fn, [chat_input, chat_history],
914
  [diff_view, diff_original_state, diff_modified_state, chat_history, chat_input]
915
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
 
916
  send_btn.click(generate_fn, [chat_input, chat_history],
917
  [diff_view, diff_original_state, diff_modified_state, chat_history, chat_input]
918
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
919
 
920
+ # Diff
921
+ apply_btn.click(lambda mod: (mod, analyzer.analyze(mod)), diff_modified_state, [editor, structure_view]
922
  ).then(lambda: gr.Tabs(selected="terminal-tab"), None, bottom_tabs)
923
+ discard_btn.click(lambda: (gr.update(), gr.update()), None, [editor, structure_view]
 
924
  ).then(lambda: gr.Tabs(selected="terminal-tab"), None, bottom_tabs)
925
 
926
+ # Snippets
927
+ snippet_save_btn.click(save_snippet, [snippet_name, snippet_tags, editor], [snippet_status, snippet_list])
928
+ snippet_insert_btn.click(insert_snippet, snippet_list, editor)
929
+ snippet_del_btn.click(delete_snippet, snippet_list, [snippet_status, snippet_list])
930
+ snippet_search.change(search_snippets, snippet_search, snippet_list)
931
+
932
+ # Find & Replace
933
+ find_btn.click(do_find, [editor, find_query, find_case], find_results)
934
+ find_all_btn.click(do_find_all, find_query, find_results)
935
+ replace_btn.click(do_replace, [editor, find_query, replace_query, find_case],
936
+ [editor, find_results, structure_view])
937
+
938
+ # Display
939
  capture_btn.click(capture_display, None, display_image)
940
+ auto_capture.change(toggle_auto, auto_capture, auto_timer)
941
+ auto_timer.tick(auto_capture_tick, None, display_image)
942
 
943
  if __name__ == "__main__":
944
  import atexit
945
  atexit.register(terminal.cleanup)
946
+ atexit.register(gui_mgr.stop)
947
  atexit.register(vdisplay.cleanup)
948
  demo.launch(server_name="0.0.0.0", server_port=7860,
949
  theme=theme, css=css, ssr_mode=False)