codegpt-ai 2.29.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/chat.py +40 -20
  2. package/desktop.py +339 -90
  3. package/package.json +1 -1
  4. package/tui.py +55 -1
package/chat.py CHANGED
@@ -7217,40 +7217,46 @@ def main():
7217
7217
  subprocess.run([sys.executable, tui_py])
7218
7218
  print_header(model)
7219
7219
  continue
7220
- # Find desktop.py check multiple locations
7221
- desktop_paths = [
7220
+ # Always ensure desktop.py is at ~/.codegpt/desktop.py (reliable location)
7221
+ desktop_py = os.path.join(str(Path.home()), ".codegpt", "desktop.py")
7222
+
7223
+ # Check source locations first, copy to ~/.codegpt if found
7224
+ source_paths = [
7222
7225
  os.path.join(str(Path(__file__).parent), "desktop.py"),
7223
7226
  os.path.join(str(Path.home()), "codegpt", "desktop.py"),
7224
7227
  os.path.join(str(Path.home()), "OneDrive", "Desktop", "Coding", "claude-chat", "desktop.py"),
7225
7228
  ]
7226
- desktop_py = None
7227
- for dp in desktop_paths:
7228
- if os.path.isfile(dp):
7229
- desktop_py = dp
7230
- break
7231
-
7232
- if not desktop_py:
7233
- # Download it
7234
- print_sys("Downloading desktop app...")
7235
- desktop_py = os.path.join(str(Path.home()), ".codegpt", "desktop.py")
7236
- try:
7237
- r = requests.get("https://raw.githubusercontent.com/CCguvycu/codegpt/master/desktop.py", timeout=15)
7229
+ for sp in source_paths:
7230
+ if os.path.isfile(sp):
7238
7231
  Path(desktop_py).parent.mkdir(parents=True, exist_ok=True)
7239
- Path(desktop_py).write_text(r.text, encoding="utf-8")
7240
- print_success("Downloaded.")
7241
- except Exception as e:
7242
- print_err(f"Cannot download: {e}")
7243
- continue
7232
+ import shutil as _sh
7233
+ _sh.copy2(sp, desktop_py)
7234
+ break
7235
+ else:
7236
+ # Download from GitHub
7237
+ if not os.path.isfile(desktop_py):
7238
+ print_sys("Downloading desktop app...")
7239
+ try:
7240
+ r = requests.get("https://raw.githubusercontent.com/CCguvycu/codegpt/master/desktop.py", timeout=15)
7241
+ Path(desktop_py).parent.mkdir(parents=True, exist_ok=True)
7242
+ Path(desktop_py).write_text(r.text, encoding="utf-8")
7243
+ except Exception as e:
7244
+ print_err(f"Cannot download: {e}")
7245
+ continue
7244
7246
 
7245
7247
  # Install pywebview if needed
7246
7248
  try:
7247
7249
  import webview # noqa
7248
7250
  except (ImportError, Exception):
7249
7251
  print_sys("Installing pywebview...")
7250
- subprocess.run(
7252
+ result = subprocess.run(
7251
7253
  [sys.executable, "-m", "pip", "install", "pywebview"],
7252
7254
  capture_output=True, text=True, timeout=120,
7253
7255
  )
7256
+ if result.returncode != 0:
7257
+ print_err("Cannot install pywebview.")
7258
+ print_sys("Try: pip install pywebview")
7259
+ continue
7254
7260
 
7255
7261
  print_sys("Launching desktop app...")
7256
7262
  subprocess.Popen(
@@ -7261,6 +7267,20 @@ def main():
7261
7267
  continue
7262
7268
 
7263
7269
  elif cmd == "/tui":
7270
+ # TUI is for Termux/mobile only — PC uses /desktop or full CLI
7271
+ if not os.path.exists("/data/data/com.termux"):
7272
+ console.print()
7273
+ console.print(Text.from_markup(" [yellow]⚠ /tui is for Termux (Android) only[/]"))
7274
+ console.print()
7275
+ console.print(Text.from_markup(" [dim]Why:[/] TUI mode is a lightweight version for mobile terminals."))
7276
+ console.print(Text.from_markup(" [dim]On PC you already have the full CLI with 123 commands.[/]"))
7277
+ console.print()
7278
+ console.print(Text.from_markup(" [bold]Use instead:[/]"))
7279
+ console.print(Text.from_markup(" [bright_blue]/desktop[/] — open the desktop GUI app"))
7280
+ console.print(Text.from_markup(" [bright_blue]/help[/] — see all 123 commands"))
7281
+ console.print()
7282
+ continue
7283
+
7264
7284
  tui_paths = [
7265
7285
  os.path.join(str(Path(__file__).parent), "tui.py"),
7266
7286
  os.path.join(str(Path.home()), "codegpt", "tui.py"),
package/desktop.py CHANGED
@@ -110,24 +110,22 @@ class Api:
110
110
 
111
111
  if cmd == "/help":
112
112
  return (
113
- "**Commands:**\n"
114
- "`/new` New conversation\n"
115
- "`/model <name>` — Switch model\n"
116
- "`/models` List models\n"
117
- "`/persona <name>` — Switch persona (default, hacker, teacher, roast, architect, minimal)\n"
118
- "`/clear`Clear chat\n"
119
- "`/think`Toggle deep thinking\n"
120
- "`/temp <0-2>` — Set temperature\n"
121
- "`/system <prompt>` — Set system prompt\n"
122
- "`/tokens`Show token count\n"
123
- "`/history` — Show message count\n"
124
- "`/server` Show server info\n"
125
- "`/connect <ip>` — Connect to remote Ollama\n"
126
- "`/export` Export chat as text\n"
127
- "`/weather <city>` — Get weather\n"
128
- "`/open <url>` Open URL in browser\n"
129
- "`/agent <name> <task>` — Run an AI agent\n"
130
- "`/browse <url>` — Fetch and summarize a URL\n"
113
+ "**Chat**\n"
114
+ "`/new` `/save` `/clear` `/export` `/history` `/compact` `/search`\n\n"
115
+ "**Model**\n"
116
+ "`/model` `/models` `/persona` `/think` `/temp` `/system` `/tokens`\n\n"
117
+ "**AI**\n"
118
+ "`/agent <name> <task>` coder, debugger, reviewer, architect, pentester, explainer, optimizer, researcher\n"
119
+ "`/all <question>` All 8 agents answer in parallel\n"
120
+ "`/vote <question>` — Agents vote with consensus\n"
121
+ "`/swarm <task>` — 4-agent pipeline (architect→coder→reviewer→optimizer)\n"
122
+ "`/p <template> <text>` Prompt templates (debug, review, explain...)\n\n"
123
+ "**Memory**\n"
124
+ "`/mem save <text>` `/mem list` `/mem clear` `/rate good|bad` `/regen`\n\n"
125
+ "**Connect**\n"
126
+ "`/connect <ip>` `/server` `/weather` `/browse <url>` `/open <url>`\n\n"
127
+ "**Info**\n"
128
+ "`/usage` `/profile` `/prompts` `/shortcuts` `/sysinfo`\n"
131
129
  )
132
130
 
133
131
  elif cmd == "/new":
@@ -307,6 +305,252 @@ class Api:
307
305
  return f"Unknown agent. Available: {', '.join(agents.keys())}"
308
306
  return "Usage: /agent coder build a flask API"
309
307
 
308
+ elif cmd == "/all":
309
+ if not args:
310
+ return "Usage: /all what database should I use?"
311
+ agents = {
312
+ "coder": "You are an expert programmer. Write clean code.",
313
+ "debugger": "You are a debugging expert. Find and fix bugs.",
314
+ "reviewer": "You are a code reviewer. Check bugs, security, performance.",
315
+ "architect": "You are a system architect. Design with diagrams.",
316
+ "pentester": "You are an ethical pentester. Find vulnerabilities.",
317
+ "explainer": "You are a teacher. Explain simply with analogies.",
318
+ "optimizer": "You are a performance engineer. Optimize code.",
319
+ "researcher": "You are a research analyst. Deep-dive into topics.",
320
+ }
321
+ import threading
322
+ results = {}
323
+ def _q(n, s):
324
+ try:
325
+ r = requests.post(OLLAMA_URL, json={"model": self.model, "messages": [
326
+ {"role": "system", "content": s}, {"role": "user", "content": args}
327
+ ], "stream": False}, timeout=90)
328
+ results[n] = r.json().get("message", {}).get("content", "")
329
+ except: results[n] = "(error)"
330
+ threads = [threading.Thread(target=_q, args=(n, s), daemon=True) for n, s in agents.items()]
331
+ for t in threads: t.start()
332
+ for t in threads: t.join(timeout=90)
333
+ output = "**All 8 agents responded:**\n\n"
334
+ for n, resp in results.items():
335
+ output += f"**{n}:** {resp[:150]}...\n\n"
336
+ return output
337
+
338
+ elif cmd == "/vote":
339
+ if not args:
340
+ return "Usage: /vote Flask or FastAPI?"
341
+ agents = {"coder": "Expert programmer", "architect": "System architect",
342
+ "reviewer": "Code reviewer", "pentester": "Security expert"}
343
+ results = {}
344
+ for n, desc in agents.items():
345
+ try:
346
+ r = requests.post(OLLAMA_URL, json={"model": self.model, "messages": [
347
+ {"role": "system", "content": f"You are a {desc}. Answer in 1-2 sentences with confidence: HIGH/MEDIUM/LOW."},
348
+ {"role": "user", "content": args}
349
+ ], "stream": False}, timeout=60)
350
+ results[n] = r.json().get("message", {}).get("content", "")
351
+ except: results[n] = "(error)"
352
+ output = "**Agent Votes:**\n\n"
353
+ for n, resp in results.items():
354
+ output += f"**{n}:** {resp}\n\n"
355
+ return output
356
+
357
+ elif cmd == "/swarm":
358
+ if not args:
359
+ return "Usage: /swarm build a REST API with auth"
360
+ pipeline = [
361
+ ("architect", "Design the approach.", "You are a system architect."),
362
+ ("coder", "Implement the solution.", "You are an expert programmer."),
363
+ ("reviewer", "Review for bugs.", "You are a code reviewer."),
364
+ ("optimizer", "Optimize performance.", "You are a performance engineer."),
365
+ ]
366
+ accumulated = f"Task: {args}"
367
+ output = "**Swarm Pipeline:**\n\n"
368
+ for name, instruction, sys_p in pipeline:
369
+ try:
370
+ r = requests.post(OLLAMA_URL, json={"model": self.model, "messages": [
371
+ {"role": "system", "content": sys_p},
372
+ {"role": "user", "content": f"{instruction}\n\nContext:\n{accumulated}"}
373
+ ], "stream": False}, timeout=90)
374
+ resp = r.json().get("message", {}).get("content", "")
375
+ output += f"**Step: {name}**\n{resp}\n\n"
376
+ accumulated += f"\n\n{name}: {resp}"
377
+ except: output += f"**{name}:** (error)\n\n"
378
+ return output
379
+
380
+ elif cmd == "/save":
381
+ if not self.messages:
382
+ return "Nothing to save."
383
+ self._auto_save()
384
+ return f"Saved ({len(self.messages)} messages)."
385
+
386
+ elif cmd == "/copy":
387
+ ai_msgs = [m for m in self.messages if m["role"] == "assistant"]
388
+ if ai_msgs:
389
+ return "**Last response copied to clipboard** (use Ctrl+C in the response)"
390
+ return "No response to copy."
391
+
392
+ elif cmd == "/regen":
393
+ if self.messages and self.messages[-1]["role"] == "assistant":
394
+ self.messages.pop()
395
+ if self.messages and self.messages[-1]["role"] == "user":
396
+ last_q = self.messages.pop()["content"]
397
+ return self.send_message(last_q)
398
+ return "Nothing to regenerate."
399
+
400
+ elif cmd == "/compact":
401
+ if len(self.messages) < 4:
402
+ return "Not enough messages to compact."
403
+ try:
404
+ r = requests.post(OLLAMA_URL, json={"model": self.model, "messages": [
405
+ {"role": "user", "content": "Summarize this conversation in 3-5 bullet points:\n" +
406
+ "\n".join(f"{m['role']}: {m['content'][:200]}" for m in self.messages)}
407
+ ], "stream": False}, timeout=60)
408
+ summary = r.json().get("message", {}).get("content", "")
409
+ keep = self.messages[-4:]
410
+ self.messages = [{"role": "assistant", "content": f"[Summary]\n{summary}"}] + keep
411
+ return f"Compacted: {len(self.messages)} messages remaining."
412
+ except: return "Compact failed."
413
+
414
+ elif cmd == "/rate":
415
+ rating = args.lower() if args else ""
416
+ if rating in ("good", "bad", "+", "-"):
417
+ ratings_file = Path.home() / ".codegpt" / "ratings.json"
418
+ ratings = []
419
+ if ratings_file.exists():
420
+ try: ratings = json.loads(ratings_file.read_text())
421
+ except: pass
422
+ ai_msgs = [m for m in self.messages if m["role"] == "assistant"]
423
+ if ai_msgs:
424
+ ratings.append({"rating": "good" if rating in ("good", "+") else "bad",
425
+ "response": ai_msgs[-1]["content"][:200],
426
+ "timestamp": datetime.now().isoformat()})
427
+ ratings_file.parent.mkdir(parents=True, exist_ok=True)
428
+ ratings_file.write_text(json.dumps(ratings))
429
+ return f"Rated: **{rating}** ({len(ratings)} total)"
430
+ return "Usage: /rate good or /rate bad"
431
+
432
+ elif cmd == "/usage":
433
+ profile = {}
434
+ if profile_file.exists():
435
+ try: profile = json.loads(profile_file.read_text())
436
+ except: pass
437
+ return (
438
+ f"**Session:** {self.total_tokens} tokens, {len(self.messages)} messages\n"
439
+ f"**Model:** {self.model}\n"
440
+ f"**Persona:** {self.persona}\n\n"
441
+ f"**Lifetime:** {profile.get('total_messages', 0)} messages, "
442
+ f"{profile.get('total_sessions', 0)} sessions"
443
+ )
444
+
445
+ elif cmd == "/profile":
446
+ profile = {}
447
+ if profile_file.exists():
448
+ try: profile = json.loads(profile_file.read_text())
449
+ except: pass
450
+ return (
451
+ f"**{profile.get('name', 'User')}**\n"
452
+ f"Bio: {profile.get('bio', 'not set')}\n"
453
+ f"Model: {profile.get('model', MODEL)}\n"
454
+ f"Persona: {profile.get('persona', 'default')}\n"
455
+ f"Sessions: {profile.get('total_sessions', 0)}"
456
+ )
457
+
458
+ elif cmd == "/search":
459
+ if not args:
460
+ return "Usage: /search keyword"
461
+ matches = [m for m in self.messages if args.lower() in m["content"].lower()]
462
+ if matches:
463
+ output = f"**{len(matches)} matches for '{args}':**\n\n"
464
+ for m in matches[:5]:
465
+ role = "You" if m["role"] == "user" else "AI"
466
+ pos = m["content"].lower().find(args.lower())
467
+ snippet = m["content"][max(0,pos-30):pos+len(args)+30]
468
+ output += f"**{role}:** ...{snippet}...\n"
469
+ return output
470
+ return f"No matches for '{args}'."
471
+
472
+ elif cmd == "/mem":
473
+ parts = args.split(maxsplit=1)
474
+ sub = parts[0] if parts else ""
475
+ mem_file = Path.home() / ".codegpt" / "memory" / "memories.json"
476
+
477
+ if sub == "save" and len(parts) >= 2:
478
+ mems = []
479
+ if mem_file.exists():
480
+ try: mems = json.loads(mem_file.read_text())
481
+ except: pass
482
+ mems.append({"content": parts[1], "timestamp": datetime.now().isoformat()})
483
+ mem_file.parent.mkdir(parents=True, exist_ok=True)
484
+ mem_file.write_text(json.dumps(mems))
485
+ return f"Remembered ({len(mems)} memories)."
486
+ elif sub == "list" or sub == "recall":
487
+ if mem_file.exists():
488
+ mems = json.loads(mem_file.read_text())
489
+ if mems:
490
+ output = f"**{len(mems)} memories:**\n\n"
491
+ for m in mems[-10:]:
492
+ output += f"- {m['content']}\n"
493
+ return output
494
+ return "No memories saved."
495
+ elif sub == "clear":
496
+ if mem_file.exists(): mem_file.write_text("[]")
497
+ return "All memories cleared."
498
+ return "Usage: /mem save <text>, /mem list, /mem clear"
499
+
500
+ elif cmd == "/prompts":
501
+ templates = {
502
+ "explain": "Explain this concept clearly: ",
503
+ "debug": "Debug this code: ",
504
+ "review": "Review this code: ",
505
+ "refactor": "Refactor this code: ",
506
+ "test": "Write tests for: ",
507
+ "optimize": "Optimize this code: ",
508
+ "security": "Check for vulnerabilities: ",
509
+ }
510
+ output = "**Prompt Templates:**\n\n"
511
+ for name, prefix in templates.items():
512
+ output += f"`/p {name} <text>` — {prefix}\n"
513
+ return output
514
+
515
+ elif cmd == "/p":
516
+ parts = args.split(maxsplit=1)
517
+ if len(parts) >= 2:
518
+ templates = {
519
+ "explain": "Explain this concept clearly with examples: ",
520
+ "debug": "Debug this code. Find bugs and fix them: ",
521
+ "review": "Review this code for quality, security, performance: ",
522
+ "refactor": "Refactor this code to be cleaner: ",
523
+ "test": "Write unit tests for this code: ",
524
+ "optimize": "Optimize this code for performance: ",
525
+ "security": "Analyze for security vulnerabilities: ",
526
+ }
527
+ if parts[0] in templates:
528
+ full = templates[parts[0]] + parts[1]
529
+ return json.loads(self.send_message(full)).get("content", "")
530
+ return "Usage: /p debug my_function()"
531
+
532
+ elif cmd == "/sysinfo":
533
+ import platform
534
+ return (
535
+ f"**System:**\n"
536
+ f"OS: {platform.system()} {platform.release()}\n"
537
+ f"Machine: {platform.machine()}\n"
538
+ f"Python: {platform.python_version()}\n"
539
+ f"Host: {platform.node()}"
540
+ )
541
+
542
+ elif cmd == "/shortcuts":
543
+ return (
544
+ "**Keyboard Shortcuts:**\n"
545
+ "`Enter` — Send message\n"
546
+ "`Shift+Enter` — New line\n"
547
+ "`Ctrl+N` — New chat\n"
548
+ "`/` — Show command menu\n"
549
+ "`Arrow Up/Down` — Navigate command menu\n"
550
+ "`Tab` — Select command\n"
551
+ "`Escape` — Close command menu"
552
+ )
553
+
310
554
  return None # Not a command — send as regular message
311
555
 
312
556
  def new_chat(self):
@@ -513,15 +757,15 @@ pre:hover .copy-btn { opacity: 1; }
513
757
  <div class="sidebar">
514
758
  <div class="sidebar-header">
515
759
  <h2>CHATS</h2>
516
- <button class="new-btn" onclick="newChat()">+ New Chat</button>
760
+ <button class="new-btn" id="newChatBtn">+ New Chat</button>
517
761
  </div>
518
762
  <div class="chat-list" id="chatList"></div>
519
763
  <div class="sidebar-bottom">
520
764
  <div class="select-row">
521
- <select id="modelSelect" onchange="setModel(this.value)"></select>
765
+ <select id="modelSelect"></select>
522
766
  </div>
523
767
  <div class="select-row">
524
- <select id="personaSelect" onchange="setPersona(this.value)"></select>
768
+ <select id="personaSelect"></select>
525
769
  </div>
526
770
  <div class="sidebar-info" id="sidebarInfo">CodeGPT v2.0</div>
527
771
  </div>
@@ -540,13 +784,13 @@ pre:hover .copy-btn { opacity: 1; }
540
784
  <div class="welcome" id="welcome">
541
785
  <h2 id="greeting"></h2>
542
786
  <p>How can I help you today?</p>
543
- <div class="chips">
544
- <button onclick="go('Explain how REST APIs work')">Explain REST APIs</button>
545
- <button onclick="go('Write a Python function to find primes')">Prime numbers</button>
546
- <button onclick="go('What are the OWASP top 10?')">OWASP Top 10</button>
547
- <button onclick="go('Design a login system with JWT')">JWT Auth</button>
548
- <button onclick="go('Compare Flask vs FastAPI')">Flask vs FastAPI</button>
549
- <button onclick="go('Write a bash script to backup files')">Backup script</button>
787
+ <div class="chips" id="chips">
788
+ <button data-q="Explain how REST APIs work">Explain REST APIs</button>
789
+ <button data-q="Write a Python function to find primes">Prime numbers</button>
790
+ <button data-q="What are the OWASP top 10?">OWASP Top 10</button>
791
+ <button data-q="Design a login system with JWT">JWT Auth</button>
792
+ <button data-q="Compare Flask vs FastAPI">Flask vs FastAPI</button>
793
+ <button data-q="Write a bash script to backup files">Backup script</button>
550
794
  </div>
551
795
  </div>
552
796
  <div class="thinking" id="think"><div class="spinner"></div> Thinking...</div>
@@ -555,8 +799,8 @@ pre:hover .copy-btn { opacity: 1; }
555
799
  <div class="input-area" style="position:relative">
556
800
  <div class="cmd-menu" id="cmdMenu"></div>
557
801
  <div class="input-wrap">
558
- <textarea id="inp" placeholder="Message CodeGPT... (type / for commands)" rows="1" onkeydown="key(event)" oninput="onInput(event)" autofocus></textarea>
559
- <button onclick="send()" id="btn">Send</button>
802
+ <textarea id="inp" placeholder="Message CodeGPT... (type / for commands)" rows="1" autofocus></textarea>
803
+ <button id="btn">Send</button>
560
804
  </div>
561
805
  </div>
562
806
  <div class="footer">
@@ -565,54 +809,13 @@ pre:hover .copy-btn { opacity: 1; }
565
809
  </div>
566
810
  </div>
567
811
 
568
- <div id="welcomeModal" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:200;display:flex;align-items:center;justify-content:center">
569
- <div style="background:#161b22;border:1px solid #30363d;border-radius:16px;padding:32px 40px;max-width:480px;width:90%;text-align:center">
570
- <h2 style="font-size:22px;margin-bottom:4px"><span style="color:#f85149">Code</span><span style="color:#58a6ff">GPT</span> Desktop</h2>
571
- <p style="color:#7d8590;font-size:13px;margin-bottom:20px">v2.0 &middot; Local AI &middot; Powered by Ollama</p>
572
- <div style="text-align:left;margin:16px 0">
573
- <p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">123</span> commands <span style="color:#7d8590">type / to see all</span></p>
574
- <p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">8</span> AI agents <span style="color:#7d8590">coder, reviewer, architect</span></p>
575
- <p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">26</span> tools <span style="color:#7d8590">Claude, Codex, Gemini</span></p>
576
- <p style="padding:6px 0;font-size:14px"><span style="color:#58a6ff;font-weight:700;margin-right:10px">6</span> personas <span style="color:#7d8590">hacker, teacher, minimal</span></p>
577
- </div>
578
- <p style="color:#7d8590;font-size:12px;margin-top:16px">Click anywhere or wait to start</p>
579
- </div>
580
- </div>
581
-
582
812
  <script>
583
813
  let busy = false;
584
814
 
585
- // Welcome — auto-dismiss after 3 seconds, or click/key
586
- var _welcomed = false;
587
- function _dismissWelcome() {
588
- if (_welcomed) return;
589
- _welcomed = true;
590
- var m = document.getElementById('welcomeModal');
591
- if (m) {
592
- m.style.display = 'none';
593
- m.style.visibility = 'hidden';
594
- m.style.pointerEvents = 'none';
595
- m.style.opacity = '0';
596
- try { m.parentNode.removeChild(m); } catch(e) {}
597
- }
598
- // Clean up listeners
599
- document.removeEventListener('mousedown', _dismissWelcome);
600
- document.removeEventListener('keydown', _dismissWelcome);
601
- if (_dismissTimer) clearTimeout(_dismissTimer);
602
- // Focus input
603
- setTimeout(function() {
604
- var inp = document.getElementById('inp');
605
- if (inp) inp.focus();
606
- }, 100);
607
- }
608
- var _dismissTimer = setTimeout(_dismissWelcome, 3000);
609
- document.addEventListener('mousedown', _dismissWelcome);
610
- document.addEventListener('keydown', _dismissWelcome);
611
-
612
815
  async function init() {
613
- const name = await pywebview.api.get_username();
614
- const h = new Date().getHours();
615
- const g = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
816
+ var name = await pywebview.api.get_username();
817
+ var h = new Date().getHours();
818
+ var g = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
616
819
  document.getElementById('greeting').textContent = g + ', ' + name;
617
820
 
618
821
  await loadModels();
@@ -620,6 +823,22 @@ async function init() {
620
823
  await loadChats();
621
824
  checkStatus();
622
825
  setInterval(checkStatus, 30000);
826
+
827
+ // Bind all events (no onclick attributes — pywebview blocks them)
828
+ document.getElementById('btn').addEventListener('click', function() { send(); });
829
+ document.getElementById('newChatBtn').addEventListener('click', function() { newChat(); });
830
+ document.getElementById('inp').addEventListener('keydown', function(e) { key(e); });
831
+ document.getElementById('inp').addEventListener('input', function(e) { onInput(e); });
832
+ document.getElementById('modelSelect').addEventListener('change', function() { setModel(this.value); });
833
+ document.getElementById('personaSelect').addEventListener('change', function() { setPersona(this.value); });
834
+
835
+ // Suggestion chips
836
+ document.querySelectorAll('#chips button').forEach(function(btn) {
837
+ btn.addEventListener('click', function() { go(this.getAttribute('data-q')); });
838
+ });
839
+
840
+ // Focus input
841
+ document.getElementById('inp').focus();
623
842
  }
624
843
 
625
844
  async function checkStatus() {
@@ -641,14 +860,25 @@ async function loadPersonas() {
641
860
  }
642
861
 
643
862
  async function loadChats() {
644
- const chats = JSON.parse(await pywebview.api.get_chat_history());
645
- const list = document.getElementById('chatList');
646
- list.innerHTML = chats.map(c =>
647
- '<div class="chat-item" onclick="loadChat(\''+c.id+'\')">' +
648
- '<span>'+c.title+'</span>' +
649
- '<span class="del" onclick="event.stopPropagation();deleteChat(\''+c.id+'\')">x</span>' +
650
- '</div>'
651
- ).join('') || '<div style="padding:20px;text-align:center;color:var(--dim);font-size:12px">No chats yet</div>';
863
+ var chats = JSON.parse(await pywebview.api.get_chat_history());
864
+ var list = document.getElementById('chatList');
865
+ list.innerHTML = chats.map(function(c) {
866
+ return '<div class="chat-item" data-id="'+c.id+'">' +
867
+ '<span>'+c.title+'</span>' +
868
+ '<span class="del" data-del="'+c.id+'">x</span>' +
869
+ '</div>';
870
+ }).join('') || '<div style="padding:20px;text-align:center;color:var(--dim);font-size:12px">No chats yet</div>';
871
+
872
+ // Bind click events via delegation
873
+ list.querySelectorAll('.chat-item').forEach(function(item) {
874
+ item.addEventListener('click', function(e) {
875
+ if (e.target.classList.contains('del')) {
876
+ deleteChat(e.target.getAttribute('data-del'));
877
+ } else {
878
+ loadChat(item.getAttribute('data-id'));
879
+ }
880
+ });
881
+ });
652
882
  }
653
883
 
654
884
  async function loadChat(id) {
@@ -739,10 +969,14 @@ function onInput(e) {
739
969
  const matches = typed === '/' ? CMDS : CMDS.filter(c => c.name.startsWith(typed));
740
970
  if (matches.length > 0) {
741
971
  cmdIdx = 0;
742
- menu.innerHTML = matches.map((c, i) =>
743
- '<div class="cmd-item'+(i===0?' sel':'')+'" onclick="pickCmd(\\''+c.name+'\\')"><span class="name">'+c.name+'</span><span class="desc">'+c.desc+'</span></div>'
744
- ).join('');
972
+ menu.innerHTML = matches.map(function(c, i) {
973
+ return '<div class="cmd-item'+(i===0?' sel':'')+'" data-cmd="'+c.name+'"><span class="name">'+c.name+'</span><span class="desc">'+c.desc+'</span></div>';
974
+ }).join('');
745
975
  menu.className = 'cmd-menu show';
976
+ // Bind clicks
977
+ menu.querySelectorAll('.cmd-item').forEach(function(item) {
978
+ item.addEventListener('click', function() { pickCmd(item.getAttribute('data-cmd')); });
979
+ });
746
980
  } else {
747
981
  menu.className = 'cmd-menu';
748
982
  }
@@ -792,18 +1026,33 @@ async function setModel(m) { await pywebview.api.set_model(m); checkStatus(); }
792
1026
  async function setPersona(p) { await pywebview.api.set_persona(p); }
793
1027
 
794
1028
  async function send() {
795
- const inp = document.getElementById('inp');
796
- const text = inp.value.trim();
1029
+ var inp = document.getElementById('inp');
1030
+ var text = inp.value.trim();
797
1031
  if (!text || busy) return;
798
1032
  inp.value = ''; inp.style.height = 'auto';
1033
+
1034
+ // Hide command menu
1035
+ document.getElementById('cmdMenu').className = 'cmd-menu';
1036
+
799
1037
  busy = true;
800
1038
  document.getElementById('btn').disabled = true;
801
- addMsg('user', text);
1039
+
1040
+ // Show user message (unless slash command)
1041
+ if (!text.startsWith('/')) {
1042
+ addMsg('user', text);
1043
+ }
1044
+
802
1045
  document.getElementById('think').className = 'thinking on';
803
1046
 
804
- const r = JSON.parse(await pywebview.api.send_message(text));
1047
+ var r = JSON.parse(await pywebview.api.send_message(text));
805
1048
  document.getElementById('think').className = 'thinking';
806
- addMsg('ai', r.content, r.tokens + ' tokens &middot; ' + r.elapsed + 's');
1049
+
1050
+ if (r.is_system) {
1051
+ // Slash command result — show as system message
1052
+ addMsg('ai', r.content, 'command');
1053
+ } else {
1054
+ addMsg('ai', r.content, r.tokens + ' tokens &middot; ' + r.elapsed + 's');
1055
+ }
807
1056
  document.getElementById('tc').textContent = r.total_tokens.toLocaleString() + ' tokens';
808
1057
 
809
1058
  busy = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegpt-ai",
3
- "version": "2.29.0",
3
+ "version": "2.35.0",
4
4
  "description": "Local AI Assistant Hub — 123 commands, 26 tools, 8 agents, multi-AI, security. No cloud needed.",
5
5
  "author": "ArukuX",
6
6
  "license": "MIT",
package/tui.py CHANGED
@@ -661,8 +661,62 @@ def handle_command(text):
661
661
  console.print(Text.from_markup(f" [bright_blue]{c:<14}[/] [dim]{desc}[/]"))
662
662
  console.print()
663
663
 
664
+ # Command exists in autocomplete but not handled in TUI
665
+ elif cmd[1:] and any(cmd == c for c in TUI_COMMANDS):
666
+ cli_only = {
667
+ "/vote": "Runs all agents to vote — needs full agent system",
668
+ "/swarm": "6-agent pipeline — needs full agent system",
669
+ "/team": "Group chat with 2 AIs — needs team chat engine",
670
+ "/room": "Chat room with 3+ AIs — needs room engine",
671
+ "/spectate": "Watch AIs debate — needs spectate engine",
672
+ "/dm": "Direct message agent — needs message bus",
673
+ "/race": "Race all models — needs multi-model runner",
674
+ "/compare": "Compare 2 models — needs compare engine",
675
+ "/chain": "Chain prompts — needs chain engine",
676
+ "/lab": "AI Lab experiments — needs lab module",
677
+ "/train": "AI Training Lab — needs training system",
678
+ "/mem": "AI memory — needs memory system",
679
+ "/skill": "Create custom command — needs skill system",
680
+ "/skills": "List custom skills — needs skill system",
681
+ "/auto": "AI creates a skill — needs skill + AI system",
682
+ "/cron": "Schedule tasks — needs cron system",
683
+ "/tools": "AI tool integrations — needs tool launcher",
684
+ "/github": "GitHub tools — needs gh CLI integration",
685
+ "/spotify": "Spotify controls — needs media key system",
686
+ "/volume": "System volume — needs OS integration",
687
+ "/sysinfo": "System info — needs OS queries",
688
+ "/security": "Security dashboard — needs security module",
689
+ "/permissions": "Permissions — needs permission system",
690
+ "/audit": "Audit log — needs security module",
691
+ "/pin-set": "Set PIN — needs PIN system",
692
+ "/lock": "Lock session — needs PIN system",
693
+ "/qr": "QR code — needs qrcode library",
694
+ "/broadcast": "Message all tools — needs message bus",
695
+ "/inbox": "Check messages — needs message bus",
696
+ "/feed": "Message feed — needs message bus",
697
+ "/monitor": "Live dashboard — needs monitor system",
698
+ "/hub": "Command center — needs hub system",
699
+ "/shortcuts": "Keyboard shortcuts — needs shortcut system",
700
+ "/prompts": "Prompt templates — needs template library",
701
+ "/desktop": "Desktop app — needs GUI (pywebview)",
702
+ "/tui": "Already in TUI mode",
703
+ "/search": "Search conversation — needs search engine",
704
+ "/diff": "Compare responses — needs diff engine",
705
+ "/pin": "Pin message — needs pin system",
706
+ "/pins": "Show pins — needs pin system",
707
+ "/fork": "Fork conversation — needs fork system",
708
+ "/compact": "Compact conversation — needs compact system",
709
+ "/setname": "Set name — needs profile system",
710
+ "/setbio": "Set bio — needs profile system",
711
+ }
712
+ reason = cli_only.get(cmd, "This command needs the full CLI")
713
+ console.print()
714
+ console.print(Text.from_markup(f" [yellow]⚠ {cmd}[/] — [dim]TUI mode only[/]"))
715
+ console.print(Text.from_markup(f" [dim]{reason}[/]"))
716
+ console.print(Text.from_markup(f" [bright_blue]Run the full CLI:[/] [dim]python chat.py[/]"))
717
+ console.print()
664
718
  else:
665
- return None # Not a command
719
+ return None # Unknown command
666
720
 
667
721
  return True
668
722