codegpt-ai 2.17.0 → 2.18.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/desktop.py +374 -0
- package/package.json +2 -1
package/desktop.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""CodeGPT Desktop — Claude Code + OpenClaw style GUI."""
|
|
2
|
+
import json
|
|
3
|
+
import threading
|
|
4
|
+
import requests
|
|
5
|
+
import webview
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Config
|
|
11
|
+
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434/api/chat")
|
|
12
|
+
MODEL = "llama3.2"
|
|
13
|
+
SYSTEM = "You are a helpful AI assistant. Be concise and technical."
|
|
14
|
+
|
|
15
|
+
# Load saved URL
|
|
16
|
+
saved_url = Path.home() / ".codegpt" / "ollama_url"
|
|
17
|
+
if saved_url.exists():
|
|
18
|
+
url = saved_url.read_text().strip()
|
|
19
|
+
if url:
|
|
20
|
+
OLLAMA_URL = url
|
|
21
|
+
if "/api/chat" not in OLLAMA_URL:
|
|
22
|
+
OLLAMA_URL = OLLAMA_URL.rstrip("/") + "/api/chat"
|
|
23
|
+
|
|
24
|
+
# Try connect
|
|
25
|
+
def try_connect(url):
|
|
26
|
+
try:
|
|
27
|
+
base = url.replace("/api/chat", "/api/tags")
|
|
28
|
+
r = requests.get(base, timeout=3)
|
|
29
|
+
return [m["name"] for m in r.json().get("models", [])]
|
|
30
|
+
except:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
# Auto-detect Ollama
|
|
34
|
+
if not try_connect(OLLAMA_URL):
|
|
35
|
+
for fallback in ["http://localhost:11434/api/chat", "http://127.0.0.1:11434/api/chat"]:
|
|
36
|
+
if try_connect(fallback):
|
|
37
|
+
OLLAMA_URL = fallback
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
# Load profile
|
|
41
|
+
profile_file = Path.home() / ".codegpt" / "profiles" / "cli_profile.json"
|
|
42
|
+
USERNAME = "User"
|
|
43
|
+
if profile_file.exists():
|
|
44
|
+
try:
|
|
45
|
+
p = json.loads(profile_file.read_text())
|
|
46
|
+
USERNAME = p.get("name", "User")
|
|
47
|
+
MODEL = p.get("model", MODEL)
|
|
48
|
+
except:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
OLLAMA_BASE = OLLAMA_URL.replace("/api/chat", "")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Api:
|
|
55
|
+
"""Python bridge — handles Ollama calls for the JS frontend."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.messages = []
|
|
59
|
+
self.total_tokens = 0
|
|
60
|
+
|
|
61
|
+
def check_status(self):
|
|
62
|
+
try:
|
|
63
|
+
r = requests.get(OLLAMA_BASE + "/api/tags", timeout=3)
|
|
64
|
+
models = [m["name"] for m in r.json().get("models", [])]
|
|
65
|
+
return json.dumps({"online": True, "models": models, "model": MODEL})
|
|
66
|
+
except:
|
|
67
|
+
return json.dumps({"online": False, "models": [], "model": MODEL})
|
|
68
|
+
|
|
69
|
+
def send_message(self, text):
|
|
70
|
+
self.messages.append({"role": "user", "content": text})
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
start = time.time()
|
|
74
|
+
resp = requests.post(
|
|
75
|
+
OLLAMA_URL,
|
|
76
|
+
json={
|
|
77
|
+
"model": MODEL,
|
|
78
|
+
"messages": [{"role": "system", "content": SYSTEM}] + self.messages,
|
|
79
|
+
"stream": False,
|
|
80
|
+
},
|
|
81
|
+
timeout=120,
|
|
82
|
+
)
|
|
83
|
+
data = resp.json()
|
|
84
|
+
content = data.get("message", {}).get("content", "No response.")
|
|
85
|
+
elapsed = round(time.time() - start, 1)
|
|
86
|
+
tokens = data.get("eval_count", 0)
|
|
87
|
+
self.total_tokens += tokens
|
|
88
|
+
self.messages.append({"role": "assistant", "content": content})
|
|
89
|
+
|
|
90
|
+
return json.dumps({
|
|
91
|
+
"content": content,
|
|
92
|
+
"tokens": tokens,
|
|
93
|
+
"elapsed": elapsed,
|
|
94
|
+
"total_tokens": self.total_tokens,
|
|
95
|
+
})
|
|
96
|
+
except Exception as e:
|
|
97
|
+
return json.dumps({
|
|
98
|
+
"content": f"Error: {str(e)}",
|
|
99
|
+
"tokens": 0,
|
|
100
|
+
"elapsed": 0,
|
|
101
|
+
"total_tokens": self.total_tokens,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
def new_chat(self):
|
|
105
|
+
self.messages = []
|
|
106
|
+
return "ok"
|
|
107
|
+
|
|
108
|
+
def get_username(self):
|
|
109
|
+
return USERNAME
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
HTML = """
|
|
113
|
+
<!DOCTYPE html>
|
|
114
|
+
<html>
|
|
115
|
+
<head>
|
|
116
|
+
<meta charset="utf-8">
|
|
117
|
+
<style>
|
|
118
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
119
|
+
|
|
120
|
+
:root {
|
|
121
|
+
--bg: #0d1117;
|
|
122
|
+
--surface: #161b22;
|
|
123
|
+
--border: #30363d;
|
|
124
|
+
--text: #e6edf3;
|
|
125
|
+
--dim: #7d8590;
|
|
126
|
+
--accent: #58a6ff;
|
|
127
|
+
--red: #f85149;
|
|
128
|
+
--green: #3fb950;
|
|
129
|
+
--user-bg: #1c2128;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
body {
|
|
133
|
+
background: var(--bg);
|
|
134
|
+
color: var(--text);
|
|
135
|
+
font-family: 'Segoe UI', -apple-system, sans-serif;
|
|
136
|
+
height: 100vh;
|
|
137
|
+
display: flex;
|
|
138
|
+
flex-direction: column;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.header {
|
|
142
|
+
background: var(--surface);
|
|
143
|
+
border-bottom: 1px solid var(--border);
|
|
144
|
+
padding: 12px 20px;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: space-between;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.header h1 { font-size: 16px; font-weight: 600; }
|
|
151
|
+
.header h1 .code { color: var(--red); }
|
|
152
|
+
.header h1 .gpt { color: var(--accent); }
|
|
153
|
+
.header-info { font-size: 12px; color: var(--dim); }
|
|
154
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
|
155
|
+
.dot.on { background: var(--green); }
|
|
156
|
+
.dot.off { background: var(--red); }
|
|
157
|
+
|
|
158
|
+
.messages {
|
|
159
|
+
flex: 1;
|
|
160
|
+
overflow-y: auto;
|
|
161
|
+
padding: 16px 20px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.msg { margin-bottom: 16px; animation: fadeIn 0.2s ease; }
|
|
165
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
|
|
166
|
+
|
|
167
|
+
.msg .role {
|
|
168
|
+
font-size: 12px; font-weight: 600; margin-bottom: 4px;
|
|
169
|
+
display: flex; align-items: center; gap: 6px;
|
|
170
|
+
}
|
|
171
|
+
.msg .icon {
|
|
172
|
+
width: 18px; height: 18px; border-radius: 4px;
|
|
173
|
+
display: inline-flex; align-items: center; justify-content: center; font-size: 10px;
|
|
174
|
+
}
|
|
175
|
+
.msg.user .role { color: var(--accent); }
|
|
176
|
+
.msg.user .icon { background: var(--accent); color: var(--bg); }
|
|
177
|
+
.msg.ai .role { color: var(--green); }
|
|
178
|
+
.msg.ai .icon { background: var(--green); color: var(--bg); }
|
|
179
|
+
|
|
180
|
+
.msg .body {
|
|
181
|
+
font-size: 14px; line-height: 1.6; padding: 10px 14px;
|
|
182
|
+
white-space: pre-wrap; word-wrap: break-word;
|
|
183
|
+
}
|
|
184
|
+
.msg.user .body { background: var(--user-bg); border: 1px solid var(--border); border-radius: 8px; }
|
|
185
|
+
.msg.ai .body { border-left: 2px solid var(--green); padding-left: 14px; }
|
|
186
|
+
|
|
187
|
+
.msg .body code { background: var(--surface); padding: 2px 6px; border-radius: 4px; font-family: 'Cascadia Code', monospace; font-size: 13px; }
|
|
188
|
+
.msg .body pre { background: var(--surface); padding: 12px; border-radius: 8px; margin: 8px 0; border: 1px solid var(--border); overflow-x: auto; }
|
|
189
|
+
.msg .body pre code { background: none; padding: 0; }
|
|
190
|
+
.msg .stats { font-size: 11px; color: var(--dim); margin-top: 4px; }
|
|
191
|
+
|
|
192
|
+
.thinking { display: none; color: var(--dim); font-size: 13px; margin-bottom: 16px; }
|
|
193
|
+
.thinking.on { display: block; }
|
|
194
|
+
.thinking .d span { animation: blink 1.4s infinite; }
|
|
195
|
+
.thinking .d span:nth-child(2) { animation-delay: 0.2s; }
|
|
196
|
+
.thinking .d span:nth-child(3) { animation-delay: 0.4s; }
|
|
197
|
+
@keyframes blink { 0%,100% { opacity: 0.2; } 50% { opacity: 1; } }
|
|
198
|
+
|
|
199
|
+
.input-area { background: var(--surface); border-top: 1px solid var(--border); padding: 12px 20px; }
|
|
200
|
+
.input-wrap { display: flex; gap: 8px; align-items: flex-end; }
|
|
201
|
+
.input-wrap textarea {
|
|
202
|
+
flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
203
|
+
color: var(--text); padding: 10px 14px; font-size: 14px; font-family: inherit;
|
|
204
|
+
resize: none; outline: none; min-height: 42px; max-height: 120px;
|
|
205
|
+
}
|
|
206
|
+
.input-wrap textarea:focus { border-color: var(--accent); }
|
|
207
|
+
.input-wrap button {
|
|
208
|
+
background: var(--accent); border: none; border-radius: 8px; color: white;
|
|
209
|
+
padding: 10px 16px; font-size: 14px; cursor: pointer; font-weight: 600;
|
|
210
|
+
}
|
|
211
|
+
.input-wrap button:hover { opacity: 0.9; }
|
|
212
|
+
.input-wrap button:disabled { opacity: 0.4; cursor: default; }
|
|
213
|
+
|
|
214
|
+
.footer {
|
|
215
|
+
background: var(--surface); border-top: 1px solid var(--border);
|
|
216
|
+
padding: 6px 20px; font-size: 11px; color: var(--dim);
|
|
217
|
+
display: flex; justify-content: space-between;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.welcome { text-align: center; padding: 60px 20px; color: var(--dim); }
|
|
221
|
+
.welcome h2 { font-size: 24px; margin-bottom: 8px; color: var(--text); }
|
|
222
|
+
.chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 24px; }
|
|
223
|
+
.chips button {
|
|
224
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
225
|
+
color: var(--text); padding: 8px 14px; font-size: 13px; cursor: pointer;
|
|
226
|
+
}
|
|
227
|
+
.chips button:hover { border-color: var(--accent); color: var(--accent); }
|
|
228
|
+
|
|
229
|
+
.new-btn {
|
|
230
|
+
background: transparent; border: 1px solid var(--border); border-radius: 6px;
|
|
231
|
+
color: var(--dim); padding: 4px 10px; font-size: 11px; cursor: pointer;
|
|
232
|
+
}
|
|
233
|
+
.new-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
234
|
+
</style>
|
|
235
|
+
</head>
|
|
236
|
+
<body>
|
|
237
|
+
|
|
238
|
+
<div class="header">
|
|
239
|
+
<h1><span class="code">Code</span><span class="gpt">GPT</span> <span style="color:#7d8590;font-weight:400;font-size:13px">Desktop</span></h1>
|
|
240
|
+
<div class="header-info">
|
|
241
|
+
<span class="dot" id="dot"></span><span id="st"></span>
|
|
242
|
+
· <span id="mn"></span>
|
|
243
|
+
· <button class="new-btn" onclick="newChat()">New Chat</button>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="messages" id="msgs">
|
|
248
|
+
<div class="welcome" id="welcome">
|
|
249
|
+
<h2 id="greeting"></h2>
|
|
250
|
+
<p>Type a message or pick a suggestion.</p>
|
|
251
|
+
<div class="chips">
|
|
252
|
+
<button onclick="go('Explain how REST APIs work')">REST APIs</button>
|
|
253
|
+
<button onclick="go('Write a Python function to find prime numbers')">Prime numbers</button>
|
|
254
|
+
<button onclick="go('What are the OWASP top 10?')">OWASP Top 10</button>
|
|
255
|
+
<button onclick="go('Design a login system with JWT')">JWT Auth</button>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="thinking" id="think"><span class="d">Thinking<span>.</span><span>.</span><span>.</span></span></div>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="input-area">
|
|
262
|
+
<div class="input-wrap">
|
|
263
|
+
<textarea id="inp" placeholder="Type a message..." rows="1" onkeydown="key(event)" autofocus></textarea>
|
|
264
|
+
<button onclick="send()" id="btn">Send</button>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div class="footer">
|
|
269
|
+
<span id="tc">0 tokens</span>
|
|
270
|
+
<span>CodeGPT v2.0 · Local AI · Ollama</span>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<script>
|
|
274
|
+
let busy = false;
|
|
275
|
+
|
|
276
|
+
async function init() {
|
|
277
|
+
const name = await pywebview.api.get_username();
|
|
278
|
+
const h = new Date().getHours();
|
|
279
|
+
const g = h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
|
|
280
|
+
document.getElementById('greeting').textContent = g + ', ' + name + '.';
|
|
281
|
+
checkStatus();
|
|
282
|
+
setInterval(checkStatus, 30000);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function checkStatus() {
|
|
286
|
+
const r = JSON.parse(await pywebview.api.check_status());
|
|
287
|
+
document.getElementById('dot').className = 'dot ' + (r.online ? 'on' : 'off');
|
|
288
|
+
document.getElementById('st').textContent = r.online ? 'connected' : 'offline';
|
|
289
|
+
document.getElementById('mn').textContent = r.model;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function esc(t) {
|
|
293
|
+
t = t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
294
|
+
t = t.replace(/```(\\w*)\\n([\\s\\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
295
|
+
t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
296
|
+
t = t.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
297
|
+
return t;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function addMsg(role, content, stats) {
|
|
301
|
+
const w = document.getElementById('welcome');
|
|
302
|
+
if (w) w.style.display = 'none';
|
|
303
|
+
const d = document.createElement('div');
|
|
304
|
+
d.className = 'msg ' + role;
|
|
305
|
+
const icon = role === 'user' ? 'U' : 'AI';
|
|
306
|
+
const label = role === 'user' ? 'You' : 'CodeGPT';
|
|
307
|
+
d.innerHTML = '<div class="role"><span class="icon">' + icon + '</span>' + label + '</div>'
|
|
308
|
+
+ '<div class="body">' + esc(content) + '</div>'
|
|
309
|
+
+ (stats ? '<div class="stats">' + stats + '</div>' : '');
|
|
310
|
+
const c = document.getElementById('msgs');
|
|
311
|
+
c.insertBefore(d, document.getElementById('think'));
|
|
312
|
+
c.scrollTop = c.scrollHeight;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function key(e) {
|
|
316
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
|
|
317
|
+
const el = e.target;
|
|
318
|
+
el.style.height = 'auto';
|
|
319
|
+
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function go(t) { document.getElementById('inp').value = t; send(); }
|
|
323
|
+
|
|
324
|
+
async function newChat() {
|
|
325
|
+
await pywebview.api.new_chat();
|
|
326
|
+
document.getElementById('msgs').innerHTML =
|
|
327
|
+
'<div class="welcome" id="welcome"><h2 id="greeting">New conversation</h2><p>Type a message.</p></div>'
|
|
328
|
+
+ '<div class="thinking" id="think"><span class="d">Thinking<span>.</span><span>.</span><span>.</span></span></div>';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function send() {
|
|
332
|
+
const inp = document.getElementById('inp');
|
|
333
|
+
const text = inp.value.trim();
|
|
334
|
+
if (!text || busy) return;
|
|
335
|
+
inp.value = ''; inp.style.height = 'auto';
|
|
336
|
+
busy = true;
|
|
337
|
+
document.getElementById('btn').disabled = true;
|
|
338
|
+
addMsg('user', text);
|
|
339
|
+
document.getElementById('think').className = 'thinking on';
|
|
340
|
+
|
|
341
|
+
const r = JSON.parse(await pywebview.api.send_message(text));
|
|
342
|
+
document.getElementById('think').className = 'thinking';
|
|
343
|
+
addMsg('ai', r.content, r.tokens + ' tokens · ' + r.elapsed + 's');
|
|
344
|
+
document.getElementById('tc').textContent = r.total_tokens.toLocaleString() + ' tokens';
|
|
345
|
+
|
|
346
|
+
busy = false;
|
|
347
|
+
document.getElementById('btn').disabled = false;
|
|
348
|
+
inp.focus();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
window.addEventListener('pywebviewready', init);
|
|
352
|
+
</script>
|
|
353
|
+
</body>
|
|
354
|
+
</html>
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def main():
|
|
359
|
+
api = Api()
|
|
360
|
+
window = webview.create_window(
|
|
361
|
+
"CodeGPT",
|
|
362
|
+
html=HTML,
|
|
363
|
+
js_api=api,
|
|
364
|
+
width=800,
|
|
365
|
+
height=650,
|
|
366
|
+
min_size=(400, 400),
|
|
367
|
+
background_color="#0d1117",
|
|
368
|
+
text_select=True,
|
|
369
|
+
)
|
|
370
|
+
webview.start(debug=False)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codegpt-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.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",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"bin/",
|
|
39
39
|
"chat.py",
|
|
40
|
+
"desktop.py",
|
|
40
41
|
"ai_cli/",
|
|
41
42
|
"CLAUDE.md",
|
|
42
43
|
"README.md"
|