AIencoder commited on
Commit
49b6de2
·
verified ·
1 Parent(s): fa0ec0e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +406 -326
app.py CHANGED
@@ -5,12 +5,19 @@ import torch
5
  import time
6
  import sys
7
  import subprocess
8
- import shlex
 
 
 
 
 
 
 
 
9
  from io import StringIO
10
  import contextlib
11
- from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
12
  from typing import List, Tuple
13
- import os
14
  from functools import lru_cache
15
 
16
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
@@ -18,9 +25,9 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
18
  # --- Configuration ---
19
  MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
20
  DEVICE = "cpu"
21
- DTYPE = torch.float32
22
 
23
- # --- File System (Python Version) ---
24
  class FileSystem:
25
  def __init__(self):
26
  self.files = {
@@ -29,259 +36,273 @@ class FileSystem:
29
  "notes.txt": "Project requirements:\n- Build a cool AI app\n- Use Gradio"
30
  }
31
  self.current_file = "main.py"
32
-
33
  def save_file(self, content: str) -> None:
34
  if self.current_file:
35
  self.files[self.current_file] = content
36
-
37
  def get_current_file_content(self) -> str:
38
  return self.files.get(self.current_file, "")
39
-
40
  def set_current_file(self, filename: str) -> None:
41
  if filename in self.files:
42
  self.current_file = filename
43
-
44
  def create_file(self, filename: str, content: str = "") -> None:
45
  if filename not in self.files:
46
  self.files[filename] = content
47
  self.current_file = filename
48
-
49
  def get_all_files(self) -> List[str]:
50
  return list(self.files.keys())
51
 
52
  fs = FileSystem()
53
 
54
- # --- Terminal Engine ---
55
- class Terminal:
56
- """A real interactive terminal that handles pip, shell commands, and python execution."""
57
-
58
- WELCOME = (
59
- "Axon Pro Terminal v1.1\n"
60
- "─────────────────────────────────────\n"
61
- " pip install <pkg> Install packages\n"
62
- " pip uninstall <pkg> Remove packages\n"
63
- " pip list Show installed\n"
64
- " python <file.py> Run a script\n"
65
- " Any shell command ls, cat, echo, etc.\n"
66
- "─────────────────────────────────────\n"
67
- )
68
-
69
- BLOCKED_COMMANDS = [
70
- "rm -rf /", "rm -rf /*", "mkfs", "dd if=", ":(){", "fork",
71
- "shutdown", "reboot", "halt", "poweroff", "init 0", "init 6",
72
- ]
73
-
74
  def __init__(self):
75
- self.history: List[str] = [self.WELCOME]
76
- self.env = {} # Persistent Python globals across exec calls
77
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  def get_log(self) -> str:
79
- return "\n".join(self.history)
80
-
81
- def _append(self, text: str):
82
- self.history.append(text)
83
- # Keep buffer from growing forever (~500 lines max)
84
- total_lines = sum(s.count("\n") + 1 for s in self.history)
85
- while total_lines > 500 and len(self.history) > 1:
86
- removed = self.history.pop(0)
87
- total_lines -= removed.count("\n") + 1
88
-
89
- def _is_blocked(self, cmd: str) -> bool:
90
- cmd_lower = cmd.lower().strip()
91
- return any(b in cmd_lower for b in self.BLOCKED_COMMANDS)
92
-
93
  def run_command(self, cmd: str) -> str:
94
- """Process a terminal command and return the full terminal log."""
95
  cmd = cmd.strip()
96
  if not cmd:
97
  return self.get_log()
98
-
99
- self._append(f"$ {cmd}")
100
-
101
- if self._is_blocked(cmd):
102
- self._append("⛔ Command blocked for safety.")
103
- return self.get_log()
104
-
105
- parts = cmd.split()
106
- base = parts[0].lower()
107
-
108
- if base == "clear":
109
- self.history = []
110
- return ""
111
- elif base == "help":
112
- self._append(self.WELCOME)
113
- elif base in ("pip", "pip3"):
114
- self._handle_pip(parts)
115
- elif base in ("python", "python3"):
116
- self._handle_python(cmd, parts)
117
- else:
118
- self._handle_shell(cmd)
119
-
120
- return self.get_log()
121
-
122
- def _handle_pip(self, parts: List[str]):
123
- """Handle pip install/uninstall/list/show/freeze commands."""
124
- if len(parts) < 2:
125
- self._append("Usage: pip install <pkg> | pip uninstall <pkg> | pip list | pip show <pkg> | pip freeze")
126
- return
127
-
128
- subcmd = parts[1].lower()
129
-
130
- if subcmd == "install" and len(parts) >= 3:
131
- pkgs = parts[2:]
132
- self._append(f"Installing: {' '.join(pkgs)}...")
133
- try:
134
- result = subprocess.run(
135
- [sys.executable, "-m", "pip", "install"] + pkgs,
136
- capture_output=True, text=True, timeout=180
137
- )
138
- stdout = result.stdout.strip()
139
- stderr = result.stderr.strip()
140
-
141
- if result.returncode == 0:
142
- for line in stdout.split("\n"):
143
- low = line.lower()
144
- if any(kw in low for kw in ["successfully", "already satisfied", "requirement"]):
145
- self._append(line)
146
- if not any("successfully" in l.lower() or "already satisfied" in l.lower() for l in stdout.split("\n")):
147
- self._append(stdout.split("\n")[-1] if stdout else "Done.")
148
- self._append("✅ pip install completed")
149
- else:
150
- err_lines = (stderr or stdout).split("\n")[-6:]
151
- for line in err_lines:
152
- self._append(line)
153
- self._append("❌ pip install failed")
154
-
155
- except subprocess.TimeoutExpired:
156
- self._append("❌ Timed out (>180s). Try a smaller package or check connection.")
157
- except Exception as e:
158
- self._append(f"❌ Error: {e}")
159
-
160
- elif subcmd == "uninstall" and len(parts) >= 3:
161
- pkg = parts[2]
162
- protected = ["gradio", "torch", "transformers", "pip", "setuptools", "wheel", "numpy"]
163
- if pkg.lower() in protected:
164
- self._append(f"⛔ '{pkg}' is protected and cannot be uninstalled.")
165
- return
166
- self._append(f"Uninstalling {pkg}...")
167
- try:
168
- result = subprocess.run(
169
- [sys.executable, "-m", "pip", "uninstall", "-y", pkg],
170
- capture_output=True, text=True, timeout=60
171
- )
172
- if result.returncode == 0:
173
- self._append(f"✅ Uninstalled {pkg}")
174
- else:
175
- self._append(f"❌ {result.stderr.strip()}")
176
- except Exception as e:
177
- self._append(f"❌ Error: {e}")
178
-
179
- elif subcmd == "list":
180
- try:
181
- result = subprocess.run(
182
- [sys.executable, "-m", "pip", "list", "--format=columns"],
183
- capture_output=True, text=True, timeout=30
184
- )
185
- self._append(result.stdout.strip() if result.stdout else "No packages found.")
186
- except Exception as e:
187
- self._append(f"Error: {e}")
188
-
189
- elif subcmd == "show" and len(parts) >= 3:
190
  try:
191
- result = subprocess.run(
192
- [sys.executable, "-m", "pip", "show", parts[2]],
193
- capture_output=True, text=True, timeout=15
194
- )
195
- if result.returncode == 0:
196
- self._append(result.stdout.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  else:
198
- self._append(f"Package '{parts[2]}' not found.")
199
- except Exception as e:
200
- self._append(f"Error: {e}")
201
-
202
- elif subcmd == "freeze":
203
- try:
204
- result = subprocess.run(
205
- [sys.executable, "-m", "pip", "freeze"],
206
- capture_output=True, text=True, timeout=30
207
- )
208
- self._append(result.stdout.strip() if result.stdout else "No packages.")
209
- except Exception as e:
210
- self._append(f"Error: {e}")
211
-
212
- else:
213
- self._append(f"pip {subcmd}: unsupported. Use install, uninstall, list, show, freeze.")
214
-
215
- def _handle_python(self, cmd: str, parts: List[str]):
216
- """Handle: python file.py, python -c 'code', python -m module."""
217
- if len(parts) < 2:
218
- self._append(f"Python {sys.version.split()[0]}")
219
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- if parts[1] == "-c" and len(parts) >= 3:
222
- code = cmd.split("-c", 1)[1].strip().strip("'\"")
223
- self._exec_python(code)
224
- elif parts[1] == "-m" and len(parts) >= 3:
225
- try:
226
- result = subprocess.run(
227
- [sys.executable] + parts[1:],
228
- capture_output=True, text=True, timeout=30
229
- )
230
- if result.stdout:
231
- self._append(result.stdout.strip())
232
- if result.stderr:
233
- self._append(result.stderr.strip())
234
- except subprocess.TimeoutExpired:
235
- self._append("Process timed out (30s limit).")
236
- except Exception as e:
237
- self._append(f"Error: {e}")
238
- else:
239
- filename = parts[1]
240
- if filename in fs.files:
241
- self._exec_python(fs.files[filename])
242
- elif os.path.exists(filename):
243
- with open(filename, "r") as f:
244
- self._exec_python(f.read())
245
- else:
246
- self._append(f"File not found: {filename}")
247
-
248
- def _exec_python(self, code: str):
249
- """Execute python code with persistent globals and capture output."""
250
- buf = StringIO()
251
- start = time.time()
252
  try:
253
- with contextlib.redirect_stdout(buf):
254
- exec(code, self.env)
255
- elapsed = time.time() - start
256
- out = buf.getvalue()
257
- if out:
258
- self._append(out.rstrip())
259
- self._append(f"[Done in {elapsed:.4f}s]")
260
  except Exception as e:
261
- out = buf.getvalue()
262
- if out:
263
- self._append(out.rstrip())
264
- self._append(f"Error: {e}")
265
-
266
- def _handle_shell(self, cmd: str):
267
- """Handle generic shell commands."""
268
  try:
269
  result = subprocess.run(
270
- cmd, shell=True,
271
  capture_output=True, text=True, timeout=30
272
  )
273
- if result.stdout:
274
- self._append(result.stdout.rstrip())
275
- if result.stderr:
276
- self._append(result.stderr.rstrip())
277
- if result.returncode != 0 and not result.stdout and not result.stderr:
278
- self._append(f"Command exited with code {result.returncode}")
 
 
 
279
  except subprocess.TimeoutExpired:
280
- self._append("⏱ Command timed out (30s limit).")
 
281
  except Exception as e:
282
- self._append(f"Error: {e}")
 
283
 
284
- terminal_engine = Terminal()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
  # --- Model Loading ---
287
  @lru_cache(maxsize=1)
@@ -292,18 +313,21 @@ def load_model():
292
  tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
293
  model = AutoModelForCausalLM.from_pretrained(
294
  MODEL_NAME,
295
- torch_dtype=DTYPE,
296
  device_map=DEVICE,
297
  low_cpu_mem_usage=True
298
  )
 
 
 
 
 
 
299
  pipe = pipeline(
300
  "text-generation",
301
  model=model,
302
  tokenizer=tokenizer,
303
- max_new_tokens=256,
304
- temperature=0.2,
305
- top_p=0.95,
306
- do_sample=True
307
  )
308
  print(f"Model loaded in {time.time() - start_time:.2f}s")
309
  return pipe
@@ -313,61 +337,47 @@ def load_model():
313
 
314
  # --- AI Functions ---
315
 
316
- def run_editor_code(code: str) -> str:
317
- """Run code from the ▶ Run button and pipe output into the terminal."""
318
- buf = StringIO()
319
- start = time.time()
320
- try:
321
- with contextlib.redirect_stdout(buf):
322
- exec(code, {}, {})
323
- elapsed = time.time() - start
324
- result = buf.getvalue()
325
- if not result:
326
- result = "(No output)"
327
- terminal_engine._append(f"$ python [editor]\n{result.rstrip()}\n[Done in {elapsed:.4f}s]")
328
- return terminal_engine.get_log()
329
- except Exception as e:
330
- out = buf.getvalue()
331
- terminal_engine._append(f"$ python [editor]\n{out}Error: {e}")
332
- return terminal_engine.get_log()
333
-
334
  def generate_completion(code: str) -> str:
335
  model = load_model()
336
- if not model: return "# Error: Model failed to load."
 
337
  prompt = f"<|system|>You are a Python expert. Complete the code. No explanations.</s><|user|>\n{code}\n</s><|assistant|>"
338
  try:
339
  result = model(prompt, num_return_sequences=1, return_full_text=False)
340
- return result[0]['generated_text'].strip()
341
  except Exception as e:
342
  return f"# Error: {str(e)}"
343
 
344
  def explain_code(code: str) -> str:
345
  model = load_model()
346
- if not model: return "Error loading model."
 
347
  prompt = f"<|system|>Explain this Python code concisely.</s><|user|>\n{code}\n</s><|assistant|>"
348
  try:
349
  result = model(prompt, max_new_tokens=512)
350
- return result[0]['generated_text'].strip()
351
  except Exception as e:
352
  return str(e)
353
 
354
  def refactor_code(code: str) -> str:
355
  model = load_model()
356
- if not model: return "Error loading model."
 
357
  prompt = f"<|system|>Refactor this Python code for PEP 8 and best practices.</s><|user|>\n{code}\n</s><|assistant|>"
358
  try:
359
  result = model(prompt, max_new_tokens=512)
360
- return result[0]['generated_text'].strip()
361
  except Exception as e:
362
  return str(e)
363
 
364
  def generate_code(prompt_text: str) -> str:
365
  model = load_model()
366
- if not model: return "Error loading model."
 
367
  prompt = f"<|system|>Write Python code for: {prompt_text}</s><|assistant|>"
368
  try:
369
  result = model(prompt, max_new_tokens=512)
370
- return result[0]['generated_text'].strip()
371
  except Exception as e:
372
  return str(e)
373
 
@@ -379,7 +389,7 @@ def create_diff_view(original: str, modified: str) -> str:
379
  # --- Gradio UI ---
380
 
381
  theme = gr.themes.Default(
382
- font=[gr.themes.GoogleFont('JetBrains Mono'), 'monospace'],
383
  primary_hue="blue",
384
  neutral_hue="gray",
385
  ).set(
@@ -390,28 +400,33 @@ theme = gr.themes.Default(
390
  )
391
 
392
  css = """
393
- * { border-radius: 0 !important; }
394
- .editor-container { height: 50vh; }
395
- .terminal-box textarea {
396
- background-color: #0d0d0d !important;
397
- color: #00ff41 !important;
398
- font-family: 'JetBrains Mono', monospace !important;
399
  font-size: 13px !important;
 
400
  }
401
  .terminal-input input {
402
- background-color: #0d0d0d !important;
403
  color: #00ff41 !important;
404
  font-family: 'JetBrains Mono', monospace !important;
405
- border: 1px solid #333 !important;
 
 
 
 
406
  }
407
  .diff-view { height: 40vh; }
408
  .code-wrap { white-space: pre-wrap !important; }
 
409
  """
410
 
411
  with gr.Blocks(title="Axon Pro - Python IDE") as demo:
412
-
413
  gr.Markdown("# 🐍 Axon Pro — Python AI IDE")
414
-
415
  with gr.Tabs() as main_tabs:
416
  with gr.Tab("Editor", id="editor-tab"):
417
  with gr.Row(equal_height=True):
@@ -419,17 +434,19 @@ with gr.Blocks(title="Axon Pro - Python IDE") as demo:
419
  with gr.Column(scale=1, min_width=200):
420
  gr.Markdown("### 📁 Explorer")
421
  file_list = gr.Dropdown(
422
- choices=fs.get_all_files(),
423
  value=fs.current_file,
424
  label="Files",
425
  interactive=True,
426
- container=False
427
  )
428
  with gr.Row():
429
  new_file_btn = gr.Button("➕ New", variant="secondary")
430
  save_btn = gr.Button("💾 Save", variant="secondary")
431
- new_file_name = gr.Textbox(placeholder="script.py", label="New File", container=False)
432
-
 
 
433
  # Editor
434
  with gr.Column(scale=4):
435
  editor = gr.Code(
@@ -438,82 +455,109 @@ with gr.Blocks(title="Axon Pro - Python IDE") as demo:
438
  language="python",
439
  lines=20,
440
  interactive=True,
441
- elem_classes="code-wrap"
442
  )
443
-
444
  with gr.Row():
445
  run_btn = gr.Button("▶ Run", variant="primary")
446
  complete_btn = gr.Button("✨ Complete", variant="secondary")
447
  explain_btn = gr.Button("📝 Explain", variant="secondary")
448
  refactor_btn = gr.Button("🔧 Refactor", variant="secondary")
449
-
450
  with gr.Tabs():
451
  with gr.Tab("Terminal", id="terminal-tab"):
452
  terminal_output = gr.Textbox(
453
- value=terminal_engine.get_log(),
454
- lines=10,
 
455
  interactive=False,
456
  elem_classes="terminal-box",
457
  label="Terminal",
458
- show_label=False
459
  )
460
  with gr.Row():
461
  terminal_input = gr.Textbox(
462
- placeholder="$ pip install pygame, ls, python main.py ...",
463
  scale=8,
464
  container=False,
465
- elem_classes="terminal-input"
 
 
 
466
  )
467
- terminal_run_btn = gr.Button("⏎", variant="primary", scale=1, min_width=50)
468
- clear_btn = gr.Button("Clear", variant="secondary", size="sm")
469
-
 
470
  with gr.Tab("AI Chat", id="chat-tab"):
471
- chat_history = gr.Chatbot(label="Axon AI", height=300)
 
 
472
  with gr.Row():
473
- chat_input = gr.Textbox(placeholder="Ask to generate Python code...", scale=7, container=False)
474
- send_btn = gr.Button("Generate", variant="primary", scale=1)
475
-
 
 
 
 
 
 
476
  with gr.Tab("Diff View", id="diff-tab"):
477
- diff_view = gr.Code(label="AI Changes", language="python", elem_classes="diff-view code-wrap", interactive=False)
 
 
 
 
 
478
  with gr.Row():
479
  apply_btn = gr.Button("Apply Changes", variant="primary")
480
  discard_btn = gr.Button("Discard Changes", variant="secondary")
481
-
482
- status_bar = gr.Markdown("**AXON PRO v1.1** | Python 3.x | CPU Mode | TinyLlama-1.1B | Terminal + pip", elem_classes="status-bar")
483
-
 
 
 
484
  # State
485
  current_file_state = gr.State(fs.current_file)
486
  diff_original_state = gr.State("")
487
  diff_modified_state = gr.State("")
488
  diff_mode_state = gr.State(False)
489
-
490
- # --- Functions ---
491
  def update_file(content):
492
  fs.save_file(content)
493
  return fs.get_current_file_content()
494
-
495
  def load_file(filename):
496
  fs.set_current_file(filename)
497
  return fs.get_current_file_content(), filename
498
-
499
  def create_file(name):
500
  if name and "." in name:
501
  fs.create_file(name)
502
- return gr.update(choices=fs.get_all_files(), value=name), fs.get_current_file_content()
 
 
 
503
  return gr.update(choices=fs.get_all_files()), fs.get_current_file_content()
504
-
505
  def terminal_command(cmd):
506
- log = terminal_engine.run_command(cmd)
507
  return log, ""
508
-
509
- def clear_terminal():
510
- terminal_engine.history = []
511
  return ""
512
-
 
 
 
513
  def complete_wrapper(content):
514
  comp = generate_completion(content)
515
  return content + "\n" + comp
516
-
517
  def explain_wrapper(content):
518
  explanation = explain_code(content)
519
  fs.save_file(content)
@@ -521,61 +565,97 @@ with gr.Blocks(title="Axon Pro - Python IDE") as demo:
521
  diff_mod = f'"""\nEXPLANATION:\n{explanation}\n"""\n\n{content}'
522
  diff = create_diff_view(diff_orig, diff_mod)
523
  return diff, diff_orig, diff_mod, True
524
-
525
  def refactor_wrapper(content):
526
  refactored = refactor_code(content)
527
  fs.save_file(content)
528
  diff = create_diff_view(content, refactored)
529
  return diff, content, refactored, True
530
 
531
- def generate_wrapper(prompt, history):
532
- generated = generate_code(prompt)
533
  diff = create_diff_view("", generated)
534
- new_hist = history + [[prompt, "Generated code available in Diff View"]]
535
- return diff, "", generated, True, new_hist, ""
 
 
 
536
 
537
  # --- Wiring ---
538
  editor.change(update_file, editor, None)
539
  file_list.change(load_file, file_list, [editor, current_file_state])
540
  new_file_btn.click(create_file, new_file_name, [file_list, editor])
541
-
542
- # Terminal — type commands and hit enter or click ⏎
543
- terminal_input.submit(terminal_command, terminal_input, [terminal_output, terminal_input])
544
- terminal_run_btn.click(terminal_command, terminal_input, [terminal_output, terminal_input])
545
- clear_btn.click(clear_terminal, None, terminal_output)
546
-
547
- # Run button pipes editor code into terminal
548
- run_btn.click(run_editor_code, editor, terminal_output)
 
 
 
 
549
  complete_btn.click(complete_wrapper, editor, editor)
550
-
 
551
  explain_btn.click(
552
- explain_wrapper, editor,
553
- [diff_view, diff_original_state, diff_modified_state, diff_mode_state]
 
554
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
555
-
556
  refactor_btn.click(
557
- refactor_wrapper, editor,
558
- [diff_view, diff_original_state, diff_modified_state, diff_mode_state]
 
559
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
560
-
561
  chat_input.submit(
562
- generate_wrapper, [chat_input, chat_history],
563
- [diff_view, diff_original_state, diff_modified_state, diff_mode_state, chat_history, chat_input]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
565
-
 
566
  apply_btn.click(
567
  lambda mod: (mod, False), diff_modified_state, [editor, diff_mode_state]
568
  ).then(lambda: gr.Tabs(selected="editor-tab"), None, main_tabs)
569
-
570
  discard_btn.click(
571
  lambda: (gr.update(), False), None, [editor, diff_mode_state]
572
  ).then(lambda: gr.Tabs(selected="editor-tab"), None, main_tabs)
573
 
 
574
  if __name__ == "__main__":
 
 
 
575
  demo.launch(
576
- server_name="0.0.0.0",
577
  server_port=7860,
578
  theme=theme,
579
  css=css,
580
- ssr_mode=False
581
  )
 
5
  import time
6
  import sys
7
  import subprocess
8
+ import os
9
+ import pty
10
+ import select
11
+ import signal
12
+ import fcntl
13
+ import struct
14
+ import termios
15
+ import threading
16
+ import re
17
  from io import StringIO
18
  import contextlib
19
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline, GenerationConfig
20
  from typing import List, Tuple
 
21
  from functools import lru_cache
22
 
23
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
 
25
  # --- Configuration ---
26
  MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
27
  DEVICE = "cpu"
28
+ DTYPE = torch.float32
29
 
30
+ # --- File System ---
31
  class FileSystem:
32
  def __init__(self):
33
  self.files = {
 
36
  "notes.txt": "Project requirements:\n- Build a cool AI app\n- Use Gradio"
37
  }
38
  self.current_file = "main.py"
39
+
40
  def save_file(self, content: str) -> None:
41
  if self.current_file:
42
  self.files[self.current_file] = content
43
+
44
  def get_current_file_content(self) -> str:
45
  return self.files.get(self.current_file, "")
46
+
47
  def set_current_file(self, filename: str) -> None:
48
  if filename in self.files:
49
  self.current_file = filename
50
+
51
  def create_file(self, filename: str, content: str = "") -> None:
52
  if filename not in self.files:
53
  self.files[filename] = content
54
  self.current_file = filename
55
+
56
  def get_all_files(self) -> List[str]:
57
  return list(self.files.keys())
58
 
59
  fs = FileSystem()
60
 
61
+ # --- PTY Terminal Engine ---
62
+ class PTYTerminal:
63
+ """
64
+ Real PTY-based terminal. Spawns an actual bash shell so every command
65
+ that works in a normal Debian/Ubuntu terminal works here — pip, npm,
66
+ apt-get, git, curl, python, node, you name it.
67
+ """
68
+
69
+ STRIP_ANSI = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[.*?[@-~]|\r')
70
+
 
 
 
 
 
 
 
 
 
 
71
  def __init__(self):
72
+ self.master_fd = None
73
+ self.pid = None
74
+ self.lock = threading.Lock()
75
+ self.log_lines: List[str] = []
76
+ self.max_lines = 800
77
+ self._spawn_shell()
78
+ self._append_line("Axon Pro Terminal v2.0 — Full PTY Shell")
79
+ self._append_line("────────────────────────────────────────")
80
+ self._append_line("This is a real bash shell. Everything works:")
81
+ self._append_line(" pip install <pkg> npm install <pkg>")
82
+ self._append_line(" apt-get install <pkg> git clone <url>")
83
+ self._append_line(" python script.py node app.js")
84
+ self._append_line(" curl, wget, ls, cat, grep, etc.")
85
+ self._append_line("────────────────────────────────────────")
86
+
87
+ def _spawn_shell(self):
88
+ """Fork a real bash process attached to a PTY."""
89
+ try:
90
+ pid, fd = pty.openpty()
91
+ self.pid = os.fork()
92
+ if self.pid == 0:
93
+ # Child process — become the shell
94
+ os.close(pid)
95
+ os.setsid()
96
+ # Set slave as controlling terminal
97
+ slave_fd = fd
98
+ fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
99
+ os.dup2(slave_fd, 0)
100
+ os.dup2(slave_fd, 1)
101
+ os.dup2(slave_fd, 2)
102
+ if slave_fd > 2:
103
+ os.close(slave_fd)
104
+ env = os.environ.copy()
105
+ env["TERM"] = "dumb" # Prevent color codes / fancy prompts
106
+ env["PS1"] = "$ " # Simple prompt
107
+ env["DEBIAN_FRONTEND"] = "noninteractive"
108
+ os.execvpe("/bin/bash", ["/bin/bash", "--norc", "--noprofile", "-i"], env)
109
+ else:
110
+ # Parent process
111
+ os.close(fd)
112
+ self.master_fd = pid
113
+ # Set non-blocking reads
114
+ flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
115
+ fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
116
+ # Set terminal size (cols x rows)
117
+ winsize = struct.pack("HHHH", 40, 120, 0, 0)
118
+ fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize)
119
+ # Drain initial bash output
120
+ time.sleep(0.3)
121
+ self._read_output()
122
+ except Exception as e:
123
+ self._append_line(f"[Terminal Error] Failed to spawn shell: {e}")
124
+ self.master_fd = None
125
+
126
+ def _read_output(self, timeout: float = 0.1) -> str:
127
+ """Read all available output from the PTY."""
128
+ if self.master_fd is None:
129
+ return ""
130
+ output = []
131
+ deadline = time.time() + timeout
132
+ while True:
133
+ remaining = deadline - time.time()
134
+ if remaining <= 0:
135
+ break
136
+ try:
137
+ ready, _, _ = select.select([self.master_fd], [], [], min(remaining, 0.05))
138
+ if ready:
139
+ chunk = os.read(self.master_fd, 4096)
140
+ if chunk:
141
+ output.append(chunk.decode("utf-8", errors="replace"))
142
+ deadline = time.time() + 0.15 # Extend deadline while data flows
143
+ else:
144
+ break
145
+ else:
146
+ if output:
147
+ break
148
+ except OSError:
149
+ break
150
+ return "".join(output)
151
+
152
+ def _clean_output(self, text: str) -> str:
153
+ """Strip ANSI escape codes and control characters."""
154
+ cleaned = self.STRIP_ANSI.sub("", text)
155
+ # Remove null bytes and other control chars (keep newlines/tabs)
156
+ cleaned = "".join(ch for ch in cleaned if ch == "\n" or ch == "\t" or (ord(ch) >= 32))
157
+ return cleaned
158
+
159
+ def _append_line(self, text: str):
160
+ self.log_lines.append(text)
161
+ while len(self.log_lines) > self.max_lines:
162
+ self.log_lines.pop(0)
163
+
164
  def get_log(self) -> str:
165
+ return "\n".join(self.log_lines)
166
+
 
 
 
 
 
 
 
 
 
 
 
 
167
  def run_command(self, cmd: str) -> str:
168
+ """Send a command to the real bash shell and capture output."""
169
  cmd = cmd.strip()
170
  if not cmd:
171
  return self.get_log()
172
+
173
+ with self.lock:
174
+ if cmd.lower() == "clear":
175
+ self.log_lines = []
176
+ return ""
177
+
178
+ if self.master_fd is None:
179
+ self._append_line(f"$ {cmd}")
180
+ self._append_line("[Terminal Error] No shell process. Restart the Space.")
181
+ return self.get_log()
182
+
183
+ # Drain any leftover output
184
+ self._read_output(0.05)
185
+
186
+ self._append_line(f"$ {cmd}")
187
+
188
+ # Write the command to bash
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  try:
190
+ os.write(self.master_fd, (cmd + "\n").encode())
191
+ except OSError as e:
192
+ self._append_line(f"[Write Error] {e}")
193
+ return self.get_log()
194
+
195
+ # Determine how long to wait based on command type
196
+ parts = cmd.split()
197
+ base = parts[0].lower() if parts else ""
198
+ long_commands = ["pip", "pip3", "npm", "npx", "apt-get", "apt", "git", "wget",
199
+ "curl", "make", "cmake", "cargo", "yarn", "conda"]
200
+
201
+ if base in long_commands:
202
+ wait_time = 180 # 3 minutes for installs
203
+ elif base in ("python", "python3", "node"):
204
+ wait_time = 60
205
+ else:
206
+ wait_time = 15
207
+
208
+ # Read output with smart timeout — stop early when command finishes
209
+ all_output = []
210
+ start = time.time()
211
+ idle_count = 0
212
+
213
+ while time.time() - start < wait_time:
214
+ chunk = self._read_output(0.3)
215
+ if chunk:
216
+ idle_count = 0
217
+ all_output.append(chunk)
218
+ # Check if we see our prompt back (command finished)
219
+ combined = "".join(all_output)
220
+ if combined.rstrip().endswith("$"):
221
+ break
222
  else:
223
+ idle_count += 1
224
+ # For short commands, bail after a brief idle
225
+ if base not in long_commands and idle_count >= 3:
226
+ break
227
+ # For long commands, bail after longer idle
228
+ if base in long_commands and idle_count >= 10:
229
+ break
230
+
231
+ raw = "".join(all_output)
232
+ cleaned = self._clean_output(raw)
233
+
234
+ # Remove the echoed command from output (bash echoes what you type)
235
+ lines = cleaned.split("\n")
236
+ filtered = []
237
+ skip_echo = True
238
+ for line in lines:
239
+ stripped = line.strip()
240
+ if skip_echo and stripped == cmd.strip():
241
+ skip_echo = False
242
+ continue
243
+ # Skip bare prompt lines
244
+ if stripped == "$" or stripped == "$ ":
245
+ continue
246
+ filtered.append(line)
247
+
248
+ result = "\n".join(filtered).strip()
249
+ if result:
250
+ self._append_line(result)
251
+
252
+ return self.get_log()
253
+
254
+ def run_editor_code(self, code: str) -> str:
255
+ """Execute code from the editor by writing it to a temp file and running it."""
256
+ with self.lock:
257
+ self._append_line("$ python [editor]")
258
 
259
+ # Write code to a temp file and run it through the PTY
260
+ tmp_path = "/tmp/_axon_editor_run.py"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  try:
262
+ with open(tmp_path, "w") as f:
263
+ f.write(code)
 
 
 
 
 
264
  except Exception as e:
265
+ with self.lock:
266
+ self._append_line(f"[Error] Could not write temp file: {e}")
267
+ return self.get_log()
268
+
269
+ # Run through the shell (but bypass the echo/prompt logic — use subprocess for cleaner output)
 
 
270
  try:
271
  result = subprocess.run(
272
+ [sys.executable, tmp_path],
273
  capture_output=True, text=True, timeout=30
274
  )
275
+ if result.stdout.strip():
276
+ with self.lock:
277
+ self._append_line(result.stdout.rstrip())
278
+ if result.stderr.strip():
279
+ with self.lock:
280
+ self._append_line(result.stderr.rstrip())
281
+ if not result.stdout.strip() and not result.stderr.strip():
282
+ with self.lock:
283
+ self._append_line("(No output)")
284
  except subprocess.TimeoutExpired:
285
+ with self.lock:
286
+ self._append_line("[Timed out after 30s]")
287
  except Exception as e:
288
+ with self.lock:
289
+ self._append_line(f"[Error] {e}")
290
 
291
+ return self.get_log()
292
+
293
+ def cleanup(self):
294
+ if self.pid and self.pid > 0:
295
+ try:
296
+ os.kill(self.pid, signal.SIGTERM)
297
+ except ProcessLookupError:
298
+ pass
299
+ if self.master_fd is not None:
300
+ try:
301
+ os.close(self.master_fd)
302
+ except OSError:
303
+ pass
304
+
305
+ terminal = PTYTerminal()
306
 
307
  # --- Model Loading ---
308
  @lru_cache(maxsize=1)
 
313
  tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
314
  model = AutoModelForCausalLM.from_pretrained(
315
  MODEL_NAME,
316
+ dtype=DTYPE,
317
  device_map=DEVICE,
318
  low_cpu_mem_usage=True
319
  )
320
+ gen_config = GenerationConfig(
321
+ max_new_tokens=256,
322
+ temperature=0.2,
323
+ top_p=0.95,
324
+ do_sample=True,
325
+ )
326
  pipe = pipeline(
327
  "text-generation",
328
  model=model,
329
  tokenizer=tokenizer,
330
+ generation_config=gen_config,
 
 
 
331
  )
332
  print(f"Model loaded in {time.time() - start_time:.2f}s")
333
  return pipe
 
337
 
338
  # --- AI Functions ---
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  def generate_completion(code: str) -> str:
341
  model = load_model()
342
+ if not model:
343
+ return "# Error: Model failed to load."
344
  prompt = f"<|system|>You are a Python expert. Complete the code. No explanations.</s><|user|>\n{code}\n</s><|assistant|>"
345
  try:
346
  result = model(prompt, num_return_sequences=1, return_full_text=False)
347
+ return result[0]["generated_text"].strip()
348
  except Exception as e:
349
  return f"# Error: {str(e)}"
350
 
351
  def explain_code(code: str) -> str:
352
  model = load_model()
353
+ if not model:
354
+ return "Error loading model."
355
  prompt = f"<|system|>Explain this Python code concisely.</s><|user|>\n{code}\n</s><|assistant|>"
356
  try:
357
  result = model(prompt, max_new_tokens=512)
358
+ return result[0]["generated_text"].strip()
359
  except Exception as e:
360
  return str(e)
361
 
362
  def refactor_code(code: str) -> str:
363
  model = load_model()
364
+ if not model:
365
+ return "Error loading model."
366
  prompt = f"<|system|>Refactor this Python code for PEP 8 and best practices.</s><|user|>\n{code}\n</s><|assistant|>"
367
  try:
368
  result = model(prompt, max_new_tokens=512)
369
+ return result[0]["generated_text"].strip()
370
  except Exception as e:
371
  return str(e)
372
 
373
  def generate_code(prompt_text: str) -> str:
374
  model = load_model()
375
+ if not model:
376
+ return "Error loading model."
377
  prompt = f"<|system|>Write Python code for: {prompt_text}</s><|assistant|>"
378
  try:
379
  result = model(prompt, max_new_tokens=512)
380
+ return result[0]["generated_text"].strip()
381
  except Exception as e:
382
  return str(e)
383
 
 
389
  # --- Gradio UI ---
390
 
391
  theme = gr.themes.Default(
392
+ font=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
393
  primary_hue="blue",
394
  neutral_hue="gray",
395
  ).set(
 
400
  )
401
 
402
  css = """
403
+ * { border-radius: 0 !important; }
404
+ .terminal-box textarea {
405
+ background-color: #0a0a0a !important;
406
+ color: #00ff41 !important;
407
+ font-family: 'JetBrains Mono', monospace !important;
 
408
  font-size: 13px !important;
409
+ line-height: 1.4 !important;
410
  }
411
  .terminal-input input {
412
+ background-color: #0a0a0a !important;
413
  color: #00ff41 !important;
414
  font-family: 'JetBrains Mono', monospace !important;
415
+ border: 1px solid #1a3a1a !important;
416
+ font-size: 13px !important;
417
+ }
418
+ .terminal-input input::placeholder {
419
+ color: #2a6a2a !important;
420
  }
421
  .diff-view { height: 40vh; }
422
  .code-wrap { white-space: pre-wrap !important; }
423
+ .status-bar { font-size: 12px; opacity: 0.7; }
424
  """
425
 
426
  with gr.Blocks(title="Axon Pro - Python IDE") as demo:
427
+
428
  gr.Markdown("# 🐍 Axon Pro — Python AI IDE")
429
+
430
  with gr.Tabs() as main_tabs:
431
  with gr.Tab("Editor", id="editor-tab"):
432
  with gr.Row(equal_height=True):
 
434
  with gr.Column(scale=1, min_width=200):
435
  gr.Markdown("### 📁 Explorer")
436
  file_list = gr.Dropdown(
437
+ choices=fs.get_all_files(),
438
  value=fs.current_file,
439
  label="Files",
440
  interactive=True,
441
+ container=False,
442
  )
443
  with gr.Row():
444
  new_file_btn = gr.Button("➕ New", variant="secondary")
445
  save_btn = gr.Button("💾 Save", variant="secondary")
446
+ new_file_name = gr.Textbox(
447
+ placeholder="script.py", label="New File", container=False
448
+ )
449
+
450
  # Editor
451
  with gr.Column(scale=4):
452
  editor = gr.Code(
 
455
  language="python",
456
  lines=20,
457
  interactive=True,
458
+ elem_classes="code-wrap",
459
  )
460
+
461
  with gr.Row():
462
  run_btn = gr.Button("▶ Run", variant="primary")
463
  complete_btn = gr.Button("✨ Complete", variant="secondary")
464
  explain_btn = gr.Button("📝 Explain", variant="secondary")
465
  refactor_btn = gr.Button("🔧 Refactor", variant="secondary")
466
+
467
  with gr.Tabs():
468
  with gr.Tab("Terminal", id="terminal-tab"):
469
  terminal_output = gr.Textbox(
470
+ value=terminal.get_log(),
471
+ lines=12,
472
+ max_lines=30,
473
  interactive=False,
474
  elem_classes="terminal-box",
475
  label="Terminal",
476
+ show_label=False,
477
  )
478
  with gr.Row():
479
  terminal_input = gr.Textbox(
480
+ placeholder="$ type any command: pip install pygame, npm i, git clone, ls ...",
481
  scale=8,
482
  container=False,
483
+ elem_classes="terminal-input",
484
+ )
485
+ terminal_run_btn = gr.Button(
486
+ "⏎", variant="primary", scale=1, min_width=50
487
  )
488
+ clear_btn = gr.Button(
489
+ "Clear", variant="secondary", size="sm"
490
+ )
491
+
492
  with gr.Tab("AI Chat", id="chat-tab"):
493
+ chat_history = gr.Chatbot(
494
+ label="Axon AI", height=300, type="messages"
495
+ )
496
  with gr.Row():
497
+ chat_input = gr.Textbox(
498
+ placeholder="Ask to generate Python code...",
499
+ scale=7,
500
+ container=False,
501
+ )
502
+ send_btn = gr.Button(
503
+ "Generate", variant="primary", scale=1
504
+ )
505
+
506
  with gr.Tab("Diff View", id="diff-tab"):
507
+ diff_view = gr.Code(
508
+ label="AI Changes",
509
+ language="python",
510
+ elem_classes="diff-view code-wrap",
511
+ interactive=False,
512
+ )
513
  with gr.Row():
514
  apply_btn = gr.Button("Apply Changes", variant="primary")
515
  discard_btn = gr.Button("Discard Changes", variant="secondary")
516
+
517
+ status_bar = gr.Markdown(
518
+ "**AXON PRO v2.0** | Python 3.x | CPU Mode | TinyLlama-1.1B | Full PTY Terminal",
519
+ elem_classes="status-bar",
520
+ )
521
+
522
  # State
523
  current_file_state = gr.State(fs.current_file)
524
  diff_original_state = gr.State("")
525
  diff_modified_state = gr.State("")
526
  diff_mode_state = gr.State(False)
527
+
528
+ # --- Handler Functions ---
529
  def update_file(content):
530
  fs.save_file(content)
531
  return fs.get_current_file_content()
532
+
533
  def load_file(filename):
534
  fs.set_current_file(filename)
535
  return fs.get_current_file_content(), filename
536
+
537
  def create_file(name):
538
  if name and "." in name:
539
  fs.create_file(name)
540
+ return (
541
+ gr.update(choices=fs.get_all_files(), value=name),
542
+ fs.get_current_file_content(),
543
+ )
544
  return gr.update(choices=fs.get_all_files()), fs.get_current_file_content()
545
+
546
  def terminal_command(cmd):
547
+ log = terminal.run_command(cmd)
548
  return log, ""
549
+
550
+ def clear_terminal_fn():
551
+ terminal.log_lines = []
552
  return ""
553
+
554
+ def run_editor_wrapper(content):
555
+ return terminal.run_editor_code(content)
556
+
557
  def complete_wrapper(content):
558
  comp = generate_completion(content)
559
  return content + "\n" + comp
560
+
561
  def explain_wrapper(content):
562
  explanation = explain_code(content)
563
  fs.save_file(content)
 
565
  diff_mod = f'"""\nEXPLANATION:\n{explanation}\n"""\n\n{content}'
566
  diff = create_diff_view(diff_orig, diff_mod)
567
  return diff, diff_orig, diff_mod, True
568
+
569
  def refactor_wrapper(content):
570
  refactored = refactor_code(content)
571
  fs.save_file(content)
572
  diff = create_diff_view(content, refactored)
573
  return diff, content, refactored, True
574
 
575
+ def generate_wrapper(prompt_text, history):
576
+ generated = generate_code(prompt_text)
577
  diff = create_diff_view("", generated)
578
+ new_history = history + [
579
+ {"role": "user", "content": prompt_text},
580
+ {"role": "assistant", "content": "Generated code is available in the Diff View tab."},
581
+ ]
582
+ return diff, "", generated, True, new_history, ""
583
 
584
  # --- Wiring ---
585
  editor.change(update_file, editor, None)
586
  file_list.change(load_file, file_list, [editor, current_file_state])
587
  new_file_btn.click(create_file, new_file_name, [file_list, editor])
588
+
589
+ # Terminal
590
+ terminal_input.submit(
591
+ terminal_command, terminal_input, [terminal_output, terminal_input]
592
+ )
593
+ terminal_run_btn.click(
594
+ terminal_command, terminal_input, [terminal_output, terminal_input]
595
+ )
596
+ clear_btn.click(clear_terminal_fn, None, terminal_output)
597
+
598
+ # Run button
599
+ run_btn.click(run_editor_wrapper, editor, terminal_output)
600
  complete_btn.click(complete_wrapper, editor, editor)
601
+
602
+ # AI features → Diff tab
603
  explain_btn.click(
604
+ explain_wrapper,
605
+ editor,
606
+ [diff_view, diff_original_state, diff_modified_state, diff_mode_state],
607
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
608
+
609
  refactor_btn.click(
610
+ refactor_wrapper,
611
+ editor,
612
+ [diff_view, diff_original_state, diff_modified_state, diff_mode_state],
613
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
614
+
615
  chat_input.submit(
616
+ generate_wrapper,
617
+ [chat_input, chat_history],
618
+ [
619
+ diff_view,
620
+ diff_original_state,
621
+ diff_modified_state,
622
+ diff_mode_state,
623
+ chat_history,
624
+ chat_input,
625
+ ],
626
+ ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
627
+
628
+ send_btn.click(
629
+ generate_wrapper,
630
+ [chat_input, chat_history],
631
+ [
632
+ diff_view,
633
+ diff_original_state,
634
+ diff_modified_state,
635
+ diff_mode_state,
636
+ chat_history,
637
+ chat_input,
638
+ ],
639
  ).then(lambda: gr.Tabs(selected="diff-tab"), None, main_tabs)
640
+
641
+ # Diff actions
642
  apply_btn.click(
643
  lambda mod: (mod, False), diff_modified_state, [editor, diff_mode_state]
644
  ).then(lambda: gr.Tabs(selected="editor-tab"), None, main_tabs)
645
+
646
  discard_btn.click(
647
  lambda: (gr.update(), False), None, [editor, diff_mode_state]
648
  ).then(lambda: gr.Tabs(selected="editor-tab"), None, main_tabs)
649
 
650
+
651
  if __name__ == "__main__":
652
+ import atexit
653
+ atexit.register(terminal.cleanup)
654
+
655
  demo.launch(
656
+ server_name="0.0.0.0",
657
  server_port=7860,
658
  theme=theme,
659
  css=css,
660
+ ssr_mode=False,
661
  )