AIencoder commited on
Commit
d36b3a0
·
verified ·
1 Parent(s): 15b3e55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +174 -306
app.py CHANGED
@@ -14,10 +14,7 @@ import struct
14
  import termios
15
  import threading
16
  import re
17
- import base64
18
  import tempfile
19
- from io import StringIO
20
- from pathlib import Path
21
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, GenerationConfig
22
  from typing import List
23
  from functools import lru_cache
@@ -31,78 +28,62 @@ DTYPE = torch.float32
31
  DISPLAY_NUM = ":99"
32
  SCREEN_W, SCREEN_H = 800, 600
33
 
34
- # --- Virtual Display Setup ---
35
  class VirtualDisplay:
36
- """Manages Xvfb virtual framebuffer for capturing GUI app output."""
37
-
38
  def __init__(self):
39
  self.xvfb_proc = None
40
  self.display = DISPLAY_NUM
41
- self.width = SCREEN_W
42
- self.height = SCREEN_H
43
  self._start_xvfb()
44
 
45
  def _start_xvfb(self):
46
- """Start Xvfb virtual framebuffer."""
47
  try:
48
- # Kill any existing Xvfb on this display
49
  subprocess.run(["pkill", "-f", f"Xvfb {self.display}"],
50
  capture_output=True, timeout=5)
51
  time.sleep(0.2)
52
-
53
  self.xvfb_proc = subprocess.Popen(
54
  ["Xvfb", self.display, "-screen", "0",
55
- f"{self.width}x{self.height}x24", "-ac", "-nolisten", "tcp"],
56
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
57
- )
58
  time.sleep(0.5)
59
-
60
- # Verify it started
61
  if self.xvfb_proc.poll() is not None:
62
- print("[VirtualDisplay] Xvfb failed to start, trying to install...")
63
  subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
64
  subprocess.run(["apt-get", "install", "-y", "-qq", "xvfb", "x11-utils",
65
  "imagemagick"], capture_output=True, timeout=120)
66
  self.xvfb_proc = subprocess.Popen(
67
  ["Xvfb", self.display, "-screen", "0",
68
- f"{self.width}x{self.height}x24", "-ac", "-nolisten", "tcp"],
69
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
70
- )
71
  time.sleep(0.5)
72
-
73
  if self.xvfb_proc.poll() is None:
74
- print(f"[VirtualDisplay] Xvfb running on {self.display}")
75
  os.environ["DISPLAY"] = self.display
76
  else:
77
- print("[VirtualDisplay] Xvfb could not start")
78
  self.xvfb_proc = None
79
-
80
  except FileNotFoundError:
81
- print("[VirtualDisplay] Xvfb not found, installing...")
82
  try:
83
  subprocess.run(["apt-get", "update", "-qq"], capture_output=True, timeout=60)
84
  subprocess.run(["apt-get", "install", "-y", "-qq", "xvfb", "x11-utils",
85
  "imagemagick"], capture_output=True, timeout=120)
86
  self._start_xvfb()
87
  except Exception as e:
88
- print(f"[VirtualDisplay] Install failed: {e}")
89
  self.xvfb_proc = None
90
  except Exception as e:
91
- print(f"[VirtualDisplay] Error: {e}")
92
  self.xvfb_proc = None
93
 
94
  def capture_screenshot(self) -> str | None:
95
- """Capture current display as base64 PNG. Returns None on failure."""
96
- if self.xvfb_proc is None or self.xvfb_proc.poll() is not None:
97
  return None
98
  try:
99
  tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
100
  tmp.close()
101
- # Try xdotool + import (ImageMagick)
102
  result = subprocess.run(
103
  ["import", "-window", "root", "-display", self.display, tmp.name],
104
- capture_output=True, timeout=5
105
- )
106
  if result.returncode == 0 and os.path.getsize(tmp.name) > 100:
107
  return tmp.name
108
  os.unlink(tmp.name)
@@ -166,12 +147,12 @@ class PTYTerminal:
166
  self.log_lines: List[str] = []
167
  self.max_lines = 800
168
  self._spawn_shell()
169
- self._append_line("AXON TERMINAL v3.0")
170
- self._append_line("══════════════════════════════════════")
171
- self._append_line("Full PTY shell — everything works:")
172
- self._append_line(" pip/npm/apt-get/git/curl/wget/make")
173
- self._append_line(" python/node — GUI apps render to Display")
174
- self._append_line("══════════════════════════════════════")
175
 
176
  def _spawn_shell(self):
177
  try:
@@ -180,63 +161,50 @@ class PTYTerminal:
180
  if self.pid == 0:
181
  os.close(pid)
182
  os.setsid()
183
- slave_fd = fd
184
- fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
185
- os.dup2(slave_fd, 0)
186
- os.dup2(slave_fd, 1)
187
- os.dup2(slave_fd, 2)
188
- if slave_fd > 2:
189
- os.close(slave_fd)
190
  env = os.environ.copy()
191
- env["TERM"] = "dumb"
192
- env["PS1"] = "$ "
193
- env["DEBIAN_FRONTEND"] = "noninteractive"
194
- env["DISPLAY"] = DISPLAY_NUM
195
  os.execvpe("/bin/bash", ["/bin/bash", "--norc", "--noprofile", "-i"], env)
196
  else:
197
  os.close(fd)
198
  self.master_fd = pid
199
  flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
200
  fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
201
- winsize = struct.pack("HHHH", 40, 120, 0, 0)
202
- fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize)
203
  time.sleep(0.3)
204
- self._read_output()
205
  except Exception as e:
206
- self._append_line(f"[Shell Error] {e}")
207
  self.master_fd = None
208
 
209
- def _read_output(self, timeout: float = 0.1) -> str:
210
- if self.master_fd is None:
211
- return ""
212
- output = []
213
  deadline = time.time() + timeout
214
  while True:
215
- remaining = deadline - time.time()
216
- if remaining <= 0:
217
- break
218
  try:
219
- ready, _, _ = select.select([self.master_fd], [], [], min(remaining, 0.05))
220
- if ready:
221
- chunk = os.read(self.master_fd, 4096)
222
- if chunk:
223
- output.append(chunk.decode("utf-8", errors="replace"))
224
  deadline = time.time() + 0.15
225
- else:
226
- break
227
- else:
228
- if output:
229
- break
230
- except OSError:
231
- break
232
- return "".join(output)
233
-
234
- def _clean_output(self, text: str) -> str:
235
- cleaned = self.STRIP_ANSI.sub("", text)
236
- cleaned = "".join(ch for ch in cleaned if ch == "\n" or ch == "\t" or (ord(ch) >= 32))
237
- return cleaned
238
-
239
- def _append_line(self, text: str):
240
  self.log_lines.append(text)
241
  while len(self.log_lines) > self.max_lines:
242
  self.log_lines.pop(0)
@@ -246,110 +214,92 @@ class PTYTerminal:
246
 
247
  def run_command(self, cmd: str) -> str:
248
  cmd = cmd.strip()
249
- if not cmd:
250
- return self.get_log()
251
  with self.lock:
252
  if cmd.lower() == "clear":
253
  self.log_lines = []
254
  return ""
255
  if self.master_fd is None:
256
- self._append_line(f"$ {cmd}")
257
- self._append_line("[Error] No shell. Restart Space.")
258
  return self.get_log()
259
- self._read_output(0.05)
260
- self._append_line(f"$ {cmd}")
261
  try:
262
  os.write(self.master_fd, (cmd + "\n").encode())
263
  except OSError as e:
264
- self._append_line(f"[Write Error] {e}")
265
  return self.get_log()
 
266
  parts = cmd.split()
267
  base = parts[0].lower() if parts else ""
268
- long_cmds = ["pip", "pip3", "npm", "npx", "apt-get", "apt", "git",
269
- "wget", "curl", "make", "cmake", "cargo", "yarn", "conda"]
270
- wait = 180 if base in long_cmds else (60 if base in ("python", "python3", "node") else 15)
271
- all_output = []
 
272
  start = time.time()
273
  idle = 0
274
  while time.time() - start < wait:
275
- chunk = self._read_output(0.3)
276
- if chunk:
277
- idle = 0
278
- all_output.append(chunk)
279
- if "".join(all_output).rstrip().endswith("$"):
280
- break
281
  else:
282
  idle += 1
283
- if base not in long_cmds and idle >= 3:
284
- break
285
- if base in long_cmds and idle >= 10:
286
- break
287
- raw = "".join(all_output)
288
- cleaned = self._clean_output(raw)
289
- lines = cleaned.split("\n")
290
  filtered = []
291
- skip_echo = True
292
- for line in lines:
293
- s = line.strip()
294
- if skip_echo and s == cmd.strip():
295
- skip_echo = False
296
- continue
297
- if s in ("$", "$ "):
298
- continue
299
- filtered.append(line)
300
  result = "\n".join(filtered).strip()
301
- if result:
302
- self._append_line(result)
303
  return self.get_log()
304
 
305
  def run_editor_code(self, code: str) -> str:
306
  with self.lock:
307
- self._append_line("$ python [editor]")
308
  tmp = "/tmp/_axon_run.py"
309
  try:
310
- with open(tmp, "w") as f:
311
- f.write(code)
312
  except Exception as e:
313
- with self.lock:
314
- self._append_line(f"[Error] {e}")
315
  return self.get_log()
316
  try:
317
  env = os.environ.copy()
318
  env["DISPLAY"] = DISPLAY_NUM
319
- result = subprocess.run(
320
- [sys.executable, tmp], capture_output=True, text=True,
321
- timeout=30, env=env
322
- )
323
  with self.lock:
324
- if result.stdout.strip():
325
- self._append_line(result.stdout.rstrip())
326
- if result.stderr.strip():
327
- self._append_line(result.stderr.rstrip())
328
- if not result.stdout.strip() and not result.stderr.strip():
329
- self._append_line("(No output)")
330
  except subprocess.TimeoutExpired:
331
- with self.lock:
332
- self._append_line("[Timed out 30s]")
333
  except Exception as e:
334
- with self.lock:
335
- self._append_line(f"[Error] {e}")
336
  return self.get_log()
337
 
338
  def cleanup(self):
339
  if self.pid and self.pid > 0:
340
- try:
341
- os.kill(self.pid, signal.SIGTERM)
342
- except ProcessLookupError:
343
- pass
344
  if self.master_fd is not None:
345
- try:
346
- os.close(self.master_fd)
347
- except OSError:
348
- pass
349
 
350
  terminal = PTYTerminal()
351
 
352
- # --- Model Loading ---
353
  @lru_cache(maxsize=1)
354
  def load_model():
355
  print("Loading TinyLlama...")
@@ -360,35 +310,32 @@ def load_model():
360
  MODEL_NAME, dtype=DTYPE, device_map=DEVICE, low_cpu_mem_usage=True)
361
  gc = GenerationConfig(max_new_tokens=256, temperature=0.2, top_p=0.95, do_sample=True)
362
  pipe = pipeline("text-generation", model=mdl, tokenizer=tok, generation_config=gc)
363
- print(f"Model loaded in {time.time()-t0:.1f}s")
364
  return pipe
365
  except Exception as e:
366
  print(f"Model error: {e}")
367
  return None
368
 
369
- # --- AI helpers ---
370
- def _llm(system: str, user: str, max_tok: int = 512) -> str:
371
  m = load_model()
372
- if not m:
373
- return "Error: model not loaded"
374
  prompt = f"<|system|>{system}</s><|user|>\n{user}\n</s><|assistant|>"
375
  try:
376
- r = m(prompt, max_new_tokens=max_tok)
377
- return r[0]["generated_text"].strip()
378
  except Exception as e:
379
  return str(e)
380
 
381
  def generate_completion(code): return _llm("Complete this Python code. No explanations.", code, 256)
382
  def explain_code(code): return _llm("Explain this Python code concisely.", code)
383
  def refactor_code(code): return _llm("Refactor for PEP 8 and best practices.", code)
384
- def generate_code(prompt): return _llm(f"Write Python code for: {prompt}", "")
385
 
386
- def create_diff_view(original: str, modified: str) -> str:
387
  import difflib
388
- return "\n".join(difflib.unified_diff(original.splitlines(), modified.splitlines(), lineterm=""))
389
 
390
  # ═══════════════════════════════════════════════════════════
391
- # GRADIO UI Industrial Hacker Aesthetic
392
  # ═══════════════════════════════════════════════════════════
393
 
394
  theme = gr.themes.Default(
@@ -404,21 +351,24 @@ theme = gr.themes.Default(
404
  button_secondary_background_fill="#1a1a1a",
405
  button_secondary_background_fill_hover="#2a2a2a",
406
  button_secondary_text_color="#0ff",
407
- button_secondary_border_color="#0ff40",
408
  block_background_fill="#0d0d0d",
409
  block_border_color="#1a1a1a",
410
  block_label_text_color="#0f8",
411
  input_background_fill="#0a0a0a",
412
  input_border_color="#1a3a2a",
413
- input_text_color="#0f8",
414
  )
415
 
 
 
 
 
416
  css = """
417
- /* ═══ GLOBAL ═══ */
418
  * { border-radius: 0 !important; }
419
  .gradio-container { max-width: 100% !important; }
420
 
421
- /* ═══ HEADER ═══ */
422
  .axon-header {
423
  background: linear-gradient(90deg, #080808 0%, #0a1a1a 50%, #080808 100%);
424
  border-bottom: 1px solid #0ff3;
@@ -434,23 +384,9 @@ css = """
434
  letter-spacing: 3px;
435
  margin: 0 !important;
436
  }
437
- .axon-header p {
438
- color: #0f84 !important;
439
- font-size: 0.75em;
440
- letter-spacing: 1px;
441
- }
442
-
443
- /* ═══ PANELS ═══ */
444
- .panel-section {
445
- border: 1px solid #1a2a2a !important;
446
- background: #0a0a0a !important;
447
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
448
- }
449
- .panel-section:hover {
450
- border-color: #0ff3 !important;
451
- }
452
 
453
- /* ═══ TERMINAL ═══ */
454
  .terminal-box textarea {
455
  background: #050505 !important;
456
  color: #00ff41 !important;
@@ -471,29 +407,20 @@ css = """
471
  .terminal-input input::placeholder { color: #0f83 !important; }
472
  .terminal-input input:focus { border-color: #0ff !important; box-shadow: 0 0 8px #0ff2 !important; }
473
 
474
- /* ═══ CODE EDITOR ═══ */
475
  .code-editor textarea, .code-editor .cm-editor {
476
  font-family: 'IBM Plex Mono', monospace !important;
477
  font-size: 13px !important;
478
  }
479
 
480
- /* ═══ FILE EXPLORER ═══ */
481
- .file-explorer {
482
- border-right: 1px solid #1a2a2a !important;
483
- }
484
- .file-explorer .wrap { border-color: #0f32 !important; }
485
 
486
- /* ═══ DISPLAY PANEL ═══ */
487
- .display-panel {
488
- border: 1px solid #1a2a2a !important;
489
- background: #050505 !important;
490
- min-height: 200px;
491
- }
492
- .display-panel img {
493
- image-rendering: pixelated;
494
- }
495
 
496
- /* ═══ TOOLBAR ═══ */
497
  .toolbar-btn {
498
  font-size: 0.8em !important;
499
  padding: 6px 12px !important;
@@ -503,12 +430,9 @@ css = """
503
  border: 1px solid transparent !important;
504
  transition: all 0.2s ease !important;
505
  }
506
- .toolbar-btn:hover {
507
- border-color: #0ff !important;
508
- box-shadow: 0 0 12px #0ff2 !important;
509
- }
510
 
511
- /* ═══ COLLAPSIBLE SECTIONS (Accordion) ═══ */
512
  .gr-accordion { border: 1px solid #1a2a2a !important; background: #0a0a0a !important; }
513
  .gr-accordion > .label-wrap {
514
  background: #0d0d0d !important;
@@ -523,112 +447,92 @@ css = """
523
  font-size: 0.85em;
524
  }
525
 
526
- /* ═══ TABS ═══ */
527
  .tabs > .tab-nav > button {
528
  font-family: 'IBM Plex Mono', monospace !important;
529
- letter-spacing: 1px;
530
- font-size: 0.8em;
531
- text-transform: uppercase;
532
- color: #888 !important;
533
- border-bottom: 2px solid transparent !important;
534
  transition: all 0.2s ease;
535
  }
536
  .tabs > .tab-nav > button.selected {
537
- color: #0ff !important;
538
- border-bottom-color: #0ff !important;
539
  text-shadow: 0 0 8px #0ff4;
540
  }
541
 
542
- /* ═══ STATUS BAR ═══ */
543
  .status-bar {
544
- background: #0a0a0a !important;
545
- border-top: 1px solid #1a2a2a;
546
- padding: 4px 12px !important;
547
- font-size: 11px !important;
548
- color: #0f84 !important;
549
  }
550
  .status-bar strong { color: #0ff !important; }
551
 
552
- /* ═══ SCROLLBAR ═══ */
553
  ::-webkit-scrollbar { width: 6px; height: 6px; }
554
  ::-webkit-scrollbar-track { background: #0a0a0a; }
555
  ::-webkit-scrollbar-thumb { background: #0f83; }
556
  ::-webkit-scrollbar-thumb:hover { background: #0ff; }
557
 
558
- /* ═══ CHATBOT ═══ */
559
- .chatbot-container .message { border-radius: 0 !important; font-family: 'IBM Plex Mono', monospace !important; }
560
  """
561
 
 
 
562
  # ═══════════════════════════════════════════════════════════
563
 
564
  with gr.Blocks(title="Axon Pro — Python AI IDE") as demo:
565
 
566
- # ── HEADER ──
567
  with gr.Row(elem_classes="axon-header"):
568
  gr.Markdown("# ⬡ AXON PRO\n\nPYTHON AI IDE — v3.0")
569
 
570
- # ── MAIN LAYOUT ──
571
  with gr.Row(equal_height=False):
572
 
573
- # ══ LEFT: FILE EXPLORER (Collapsible) ══
574
  with gr.Column(scale=1, min_width=180, elem_classes="file-explorer"):
575
  with gr.Accordion("📁 EXPLORER", open=True):
576
  file_list = gr.Dropdown(
577
  choices=fs.get_all_files(), value=fs.current_file,
578
- label="", interactive=True, container=False,
579
- )
580
- new_file_name = gr.Textbox(
581
- placeholder="filename.py", label="", container=False
582
- )
583
  with gr.Row():
584
  new_file_btn = gr.Button("+ NEW", variant="secondary", size="sm",
585
  elem_classes="toolbar-btn")
586
  save_btn = gr.Button("SAVE", variant="secondary", size="sm",
587
  elem_classes="toolbar-btn")
588
 
589
- # ══ CENTER: EDITOR + TERMINAL ══
590
  with gr.Column(scale=4):
591
-
592
- # Code Editor
593
  editor = gr.Code(
594
  value=fs.get_current_file_content(),
595
  label=f" {fs.current_file}",
596
  language="python", lines=22, interactive=True,
597
- elem_classes="code-editor",
598
- )
599
 
600
- # Toolbar
601
  with gr.Row():
602
  run_btn = gr.Button("▶ RUN", variant="primary", elem_classes="toolbar-btn")
603
  complete_btn = gr.Button("✦ COMPLETE", variant="secondary", elem_classes="toolbar-btn")
604
  explain_btn = gr.Button("◈ EXPLAIN", variant="secondary", elem_classes="toolbar-btn")
605
  refactor_btn = gr.Button("⟲ REFACTOR", variant="secondary", elem_classes="toolbar-btn")
606
 
607
- # Bottom panels (Collapsible)
608
  with gr.Tabs() as bottom_tabs:
609
  with gr.Tab("⌘ TERMINAL", id="terminal-tab"):
610
  terminal_output = gr.Textbox(
611
  value=terminal.get_log(), lines=10, max_lines=25,
612
  interactive=False, elem_classes="terminal-box",
613
- label="", show_label=False,
614
- )
615
  with gr.Row():
616
  terminal_input = gr.Textbox(
617
  placeholder="$ pip install pygame | npm i | git clone | ls ...",
618
- scale=9, container=False, elem_classes="terminal-input",
619
- )
620
  terminal_run_btn = gr.Button("⏎", variant="primary", scale=1, min_width=45)
621
  clear_btn = gr.Button("CLEAR", variant="secondary", size="sm",
622
  elem_classes="toolbar-btn")
623
 
624
  with gr.Tab("💬 AI CHAT", id="chat-tab"):
625
- chat_history = gr.Chatbot(label="", height=250,
626
- elem_classes="chatbot-container")
627
  with gr.Row():
628
- chat_input = gr.Textbox(
629
- placeholder="Describe the code you want...",
630
- scale=8, container=False,
631
- )
632
  send_btn = gr.Button("GEN", variant="primary", scale=1,
633
  elem_classes="toolbar-btn")
634
 
@@ -641,44 +545,34 @@ with gr.Blocks(title="Axon Pro — Python AI IDE") as demo:
641
  discard_btn = gr.Button("✗ DISCARD", variant="secondary",
642
  elem_classes="toolbar-btn")
643
 
644
- # ══ RIGHT: DISPLAY PANEL (Collapsible) ══
645
  with gr.Column(scale=2, min_width=250):
646
  with gr.Accordion("🖥 DISPLAY OUTPUT", open=True):
647
  gr.Markdown(
648
  "<small style='color:#0f84'>GUI apps (pygame, tkinter, etc.) "
649
- "render here via virtual display</small>"
650
- )
651
  display_image = gr.Image(
652
  label="", type="filepath", interactive=False,
653
- elem_classes="display-panel", height=400,
654
- )
655
  with gr.Row():
656
  capture_btn = gr.Button("📸 CAPTURE", variant="secondary", size="sm",
657
  elem_classes="toolbar-btn")
658
  auto_refresh = gr.Checkbox(label="Auto-refresh", value=False)
659
-
660
  display_status = gr.Markdown(
661
  f"<small style='color:#0f84'>Xvfb: "
662
- f"{'● running' if vdisplay.is_running else '○ stopped'} "
663
- f"| {SCREEN_W}x{SCREEN_H}</small>"
664
- )
665
 
666
- # ── STATUS BAR ──
667
  status_bar = gr.Markdown(
668
  f"**AXON PRO v3.0** │ Python {sys.version.split()[0]} │ CPU │ "
669
- f"TinyLlama-1.1B │ PTY Shell │ "
670
- f"Xvfb {'ON' if vdisplay.is_running else 'OFF'}",
671
- elem_classes="status-bar",
672
- )
673
 
674
- # ── STATE ──
675
  diff_original_state = gr.State("")
676
  diff_modified_state = gr.State("")
677
 
678
- # ═══════════════════════���══════════════════
679
- # HANDLERS
680
- # ══════════════════════════════════════════
681
-
682
  def update_file(content):
683
  fs.save_file(content)
684
 
@@ -690,15 +584,11 @@ with gr.Blocks(title="Axon Pro — Python AI IDE") as demo:
690
  if name and "." in name:
691
  fs.create_file(name)
692
  return (gr.update(choices=fs.get_all_files(), value=name),
693
- fs.get_current_file_content(),
694
- gr.update(label=f" {name}"))
695
- return (gr.update(choices=fs.get_all_files()),
696
- fs.get_current_file_content(),
697
- gr.update())
698
 
699
  def terminal_cmd(cmd):
700
- log = terminal.run_command(cmd)
701
- return log, ""
702
 
703
  def clear_term():
704
  terminal.log_lines = []
@@ -711,89 +601,67 @@ with gr.Blocks(title="Axon Pro — Python AI IDE") as demo:
711
  return content + "\n" + generate_completion(content)
712
 
713
  def explain_fn(content):
714
- explanation = explain_code(content)
715
  fs.save_file(content)
716
- orig = content
717
- mod = f'"""\nEXPLANATION:\n{explanation}\n"""\n\n{content}'
718
- return create_diff_view(orig, mod), orig, mod
719
 
720
  def refactor_fn(content):
721
- refactored = refactor_code(content)
722
  fs.save_file(content)
723
- return create_diff_view(content, refactored), content, refactored
724
 
725
  def generate_fn(prompt_text, history):
726
- generated = generate_code(prompt_text)
727
- diff = create_diff_view("", generated)
728
- new_history = history + [
729
  {"role": "user", "content": prompt_text},
730
  {"role": "assistant", "content": "Code generated → check DIFF tab"},
731
  ]
732
- return diff, "", generated, new_history, ""
733
 
734
  def capture_display():
735
- path = vdisplay.capture_screenshot()
736
- if path:
737
- return path
738
- return None
739
-
740
- # ══════════════════════════════════════════
741
- # WIRING
742
- # ══════════════════════════════════════════
743
 
 
744
  editor.change(update_file, editor, None)
745
  file_list.change(load_file, file_list, [editor, editor])
746
  new_file_btn.click(create_file_fn, new_file_name, [file_list, editor, editor])
747
 
748
- # Terminal
749
  terminal_input.submit(terminal_cmd, terminal_input, [terminal_output, terminal_input])
750
  terminal_run_btn.click(terminal_cmd, terminal_input, [terminal_output, terminal_input])
751
  clear_btn.click(clear_term, None, terminal_output)
752
 
753
- # Editor actions
754
  run_btn.click(run_editor, editor, terminal_output)
755
  complete_btn.click(complete_fn, editor, editor)
756
 
757
- explain_btn.click(
758
- explain_fn, editor,
759
  [diff_view, diff_original_state, diff_modified_state]
760
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
761
 
762
- refactor_btn.click(
763
- refactor_fn, editor,
764
  [diff_view, diff_original_state, diff_modified_state]
765
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
766
 
767
- # Chat
768
- chat_input.submit(
769
- generate_fn, [chat_input, chat_history],
770
- [diff_view, diff_original_state, diff_modified_state, chat_history, chat_input],
771
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
772
 
773
- send_btn.click(
774
- generate_fn, [chat_input, chat_history],
775
- [diff_view, diff_original_state, diff_modified_state, chat_history, chat_input],
776
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, bottom_tabs)
777
 
778
- # Diff actions
779
- apply_btn.click(
780
- lambda mod: mod, diff_modified_state, editor
781
  ).then(lambda: gr.Tabs(selected="terminal-tab"), None, bottom_tabs)
782
 
783
- discard_btn.click(
784
- lambda: gr.update(), None, editor
785
  ).then(lambda: gr.Tabs(selected="terminal-tab"), None, bottom_tabs)
786
 
787
- # Display capture
788
  capture_btn.click(capture_display, None, display_image)
789
 
790
-
791
  if __name__ == "__main__":
792
  import atexit
793
  atexit.register(terminal.cleanup)
794
  atexit.register(vdisplay.cleanup)
795
-
796
- demo.launch(
797
- server_name="0.0.0.0", server_port=7860,
798
- theme=theme, css=css, ssr_mode=False,
799
- )
 
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
 
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)
 
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:
 
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)
 
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...")
 
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
+ # THEMEonly valid params
339
  # ═══════════════════════════════════════════════════════════
340
 
341
  theme = gr.themes.Default(
 
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;
 
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;
 
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;
 
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;
 
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
 
 
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
 
 
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 = []
 
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)