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.
- package/chat.py +40 -20
- package/desktop.py +339 -90
- package/package.json +1 -1
- 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
|
-
#
|
|
7221
|
-
|
|
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
|
-
|
|
7227
|
-
|
|
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
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
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
|
-
"**
|
|
114
|
-
"`/new`
|
|
115
|
-
"
|
|
116
|
-
"`/models`
|
|
117
|
-
"
|
|
118
|
-
"`/
|
|
119
|
-
"`/
|
|
120
|
-
"`/
|
|
121
|
-
"`/
|
|
122
|
-
"`/
|
|
123
|
-
"
|
|
124
|
-
"`/
|
|
125
|
-
"
|
|
126
|
-
"`/
|
|
127
|
-
"
|
|
128
|
-
"`/
|
|
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"
|
|
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"
|
|
765
|
+
<select id="modelSelect"></select>
|
|
522
766
|
</div>
|
|
523
767
|
<div class="select-row">
|
|
524
|
-
<select id="personaSelect"
|
|
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
|
|
545
|
-
<button
|
|
546
|
-
<button
|
|
547
|
-
<button
|
|
548
|
-
<button
|
|
549
|
-
<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"
|
|
559
|
-
<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 · Local AI · 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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
list.innerHTML = chats.map(c
|
|
647
|
-
'<div class="chat-item"
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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':'')+'"
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1047
|
+
var r = JSON.parse(await pywebview.api.send_message(text));
|
|
805
1048
|
document.getElementById('think').className = 'thinking';
|
|
806
|
-
|
|
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 · ' + 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
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 #
|
|
719
|
+
return None # Unknown command
|
|
666
720
|
|
|
667
721
|
return True
|
|
668
722
|
|