luckyd-code 1.2.2__py3-none-any.whl

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 (127) hide show
  1. luckyd_code/__init__.py +54 -0
  2. luckyd_code/__main__.py +5 -0
  3. luckyd_code/_agent_loop.py +551 -0
  4. luckyd_code/_data_dir.py +73 -0
  5. luckyd_code/agent.py +38 -0
  6. luckyd_code/analytics/__init__.py +18 -0
  7. luckyd_code/analytics/reporter.py +195 -0
  8. luckyd_code/analytics/scanner.py +443 -0
  9. luckyd_code/analytics/smells.py +316 -0
  10. luckyd_code/analytics/trends.py +303 -0
  11. luckyd_code/api.py +473 -0
  12. luckyd_code/audit_daemon.py +845 -0
  13. luckyd_code/autonomous_fixer.py +473 -0
  14. luckyd_code/background.py +159 -0
  15. luckyd_code/backup.py +237 -0
  16. luckyd_code/brain/__init__.py +84 -0
  17. luckyd_code/brain/assembler.py +100 -0
  18. luckyd_code/brain/chunker.py +345 -0
  19. luckyd_code/brain/constants.py +73 -0
  20. luckyd_code/brain/embedder.py +163 -0
  21. luckyd_code/brain/graph.py +311 -0
  22. luckyd_code/brain/indexer.py +316 -0
  23. luckyd_code/brain/parser.py +140 -0
  24. luckyd_code/brain/retriever.py +234 -0
  25. luckyd_code/cli.py +894 -0
  26. luckyd_code/cli_commands/__init__.py +1 -0
  27. luckyd_code/cli_commands/audit.py +120 -0
  28. luckyd_code/cli_commands/background.py +83 -0
  29. luckyd_code/cli_commands/brain.py +87 -0
  30. luckyd_code/cli_commands/config.py +75 -0
  31. luckyd_code/cli_commands/dispatcher.py +695 -0
  32. luckyd_code/cli_commands/sessions.py +41 -0
  33. luckyd_code/cli_entry.py +147 -0
  34. luckyd_code/cli_utils.py +112 -0
  35. luckyd_code/config.py +205 -0
  36. luckyd_code/context.py +214 -0
  37. luckyd_code/cost_tracker.py +209 -0
  38. luckyd_code/error_reporter.py +508 -0
  39. luckyd_code/exceptions.py +39 -0
  40. luckyd_code/export.py +126 -0
  41. luckyd_code/feedback_analyzer.py +290 -0
  42. luckyd_code/file_watcher.py +258 -0
  43. luckyd_code/git/__init__.py +11 -0
  44. luckyd_code/git/auto_commit.py +157 -0
  45. luckyd_code/git/tools.py +85 -0
  46. luckyd_code/hooks.py +236 -0
  47. luckyd_code/indexer.py +280 -0
  48. luckyd_code/init.py +39 -0
  49. luckyd_code/keybindings.py +77 -0
  50. luckyd_code/log.py +55 -0
  51. luckyd_code/mcp/__init__.py +6 -0
  52. luckyd_code/mcp/client.py +184 -0
  53. luckyd_code/memory/__init__.py +19 -0
  54. luckyd_code/memory/manager.py +339 -0
  55. luckyd_code/metrics/__init__.py +5 -0
  56. luckyd_code/model_registry.py +131 -0
  57. luckyd_code/orchestrator.py +204 -0
  58. luckyd_code/permissions/__init__.py +1 -0
  59. luckyd_code/permissions/manager.py +103 -0
  60. luckyd_code/planner.py +361 -0
  61. luckyd_code/plugins.py +91 -0
  62. luckyd_code/py.typed +0 -0
  63. luckyd_code/retry.py +57 -0
  64. luckyd_code/router.py +417 -0
  65. luckyd_code/sandbox.py +156 -0
  66. luckyd_code/self_critique.py +2 -0
  67. luckyd_code/self_improve.py +274 -0
  68. luckyd_code/sessions.py +114 -0
  69. luckyd_code/settings.py +72 -0
  70. luckyd_code/skills/__init__.py +8 -0
  71. luckyd_code/skills/review.py +22 -0
  72. luckyd_code/skills/security.py +17 -0
  73. luckyd_code/tasks/__init__.py +1 -0
  74. luckyd_code/tasks/manager.py +102 -0
  75. luckyd_code/templates/icon-192.png +0 -0
  76. luckyd_code/templates/icon-512.png +0 -0
  77. luckyd_code/templates/index.html +1965 -0
  78. luckyd_code/templates/manifest.json +14 -0
  79. luckyd_code/templates/src/app.js +694 -0
  80. luckyd_code/templates/src/body.html +767 -0
  81. luckyd_code/templates/src/cdn.txt +2 -0
  82. luckyd_code/templates/src/style.css +474 -0
  83. luckyd_code/templates/sw.js +31 -0
  84. luckyd_code/templates/test.html +6 -0
  85. luckyd_code/themes.py +48 -0
  86. luckyd_code/tools/__init__.py +97 -0
  87. luckyd_code/tools/agent_tools.py +65 -0
  88. luckyd_code/tools/bash.py +360 -0
  89. luckyd_code/tools/brain_tools.py +137 -0
  90. luckyd_code/tools/browser.py +369 -0
  91. luckyd_code/tools/datetime_tool.py +34 -0
  92. luckyd_code/tools/dockerfile_gen.py +212 -0
  93. luckyd_code/tools/file_ops.py +381 -0
  94. luckyd_code/tools/game_gen.py +360 -0
  95. luckyd_code/tools/git_tools.py +130 -0
  96. luckyd_code/tools/git_worktree.py +63 -0
  97. luckyd_code/tools/path_validate.py +64 -0
  98. luckyd_code/tools/project_gen.py +187 -0
  99. luckyd_code/tools/readme_gen.py +227 -0
  100. luckyd_code/tools/registry.py +157 -0
  101. luckyd_code/tools/shell_detect.py +109 -0
  102. luckyd_code/tools/web.py +89 -0
  103. luckyd_code/tools/youtube.py +187 -0
  104. luckyd_code/tools_bridge.py +144 -0
  105. luckyd_code/undo.py +126 -0
  106. luckyd_code/update.py +60 -0
  107. luckyd_code/verify.py +360 -0
  108. luckyd_code/web_app.py +176 -0
  109. luckyd_code/web_routes/__init__.py +23 -0
  110. luckyd_code/web_routes/background.py +73 -0
  111. luckyd_code/web_routes/brain.py +109 -0
  112. luckyd_code/web_routes/cost.py +12 -0
  113. luckyd_code/web_routes/files.py +133 -0
  114. luckyd_code/web_routes/memories.py +94 -0
  115. luckyd_code/web_routes/misc.py +67 -0
  116. luckyd_code/web_routes/project.py +48 -0
  117. luckyd_code/web_routes/review.py +20 -0
  118. luckyd_code/web_routes/sessions.py +44 -0
  119. luckyd_code/web_routes/settings.py +43 -0
  120. luckyd_code/web_routes/static.py +70 -0
  121. luckyd_code/web_routes/update.py +19 -0
  122. luckyd_code/web_routes/ws.py +237 -0
  123. luckyd_code-1.2.2.dist-info/METADATA +297 -0
  124. luckyd_code-1.2.2.dist-info/RECORD +127 -0
  125. luckyd_code-1.2.2.dist-info/WHEEL +4 -0
  126. luckyd_code-1.2.2.dist-info/entry_points.txt +3 -0
  127. luckyd_code-1.2.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "LuckyD Code",
3
+ "short_name": "LuckyD",
4
+ "description": "AI-powered coding assistant with voice",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#0d1117",
8
+ "theme_color": "#0d1117",
9
+ "orientation": "any",
10
+ "icons": [
11
+ {"src": "/icon-192.png", "sizes": "192x192", "type": "image/png"},
12
+ {"src": "/icon-512.png", "sizes": "512x512", "type": "image/png"}
13
+ ]
14
+ }
@@ -0,0 +1,694 @@
1
+ // Configure marked
2
+ marked.setOptions({
3
+ highlight: function(code, lang) {
4
+ if (lang && hljs.getLanguage(lang)) {
5
+ try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
6
+ }
7
+ try { return hljs.highlightAuto(code).value; } catch(e) {}
8
+ return code;
9
+ },
10
+ breaks: true,
11
+ gfm: true,
12
+ });
13
+
14
+ // State
15
+ let ws = null;
16
+ let isProcessing = false;
17
+ let currentAssistantMsg = null;
18
+ let currentAssistantDiv = null;
19
+ let currentToolMsg = null;
20
+ let reconnectTimer = null;
21
+ let reconnectAttempts = 0;
22
+ let lastSendTime = 0;
23
+ let messageQueue = [];
24
+ let renderTimeout = null;
25
+
26
+ // DOM refs
27
+ const messagesEl = document.getElementById('messages');
28
+ const inputEl = document.getElementById('input');
29
+ const sendBtn = document.getElementById('sendBtn');
30
+ const voiceBtn = document.getElementById('voiceBtn');
31
+ const statusDot = document.getElementById('statusDot');
32
+ const fileList = document.getElementById('fileList');
33
+ const filePath = document.getElementById('filePath');
34
+ const sidebar = document.getElementById('sidebar');
35
+ const overlay = document.getElementById('sidebarOverlay');
36
+
37
+ // Auto-resize textarea
38
+ inputEl.addEventListener('input', function() {
39
+ this.style.height = 'auto';
40
+ this.style.height = Math.min(this.scrollHeight, 150) + 'px';
41
+ });
42
+
43
+ // Enter to send, Shift+Enter for newline, Ctrl+Enter also sends
44
+ inputEl.addEventListener('keydown', function(e) {
45
+ if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && e.ctrlKey)) {
46
+ e.preventDefault();
47
+ sendMessage();
48
+ }
49
+ });
50
+
51
+ // WebSocket connection with exponential backoff
52
+ function connect() {
53
+ if (ws && ws.readyState === WebSocket.OPEN) return;
54
+
55
+ statusDot.className = 'status-dot connecting';
56
+
57
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
58
+ const url = `${protocol}//${location.host}/ws`;
59
+
60
+ ws = new WebSocket(url);
61
+
62
+ ws.onopen = function() {
63
+ statusDot.className = 'status-dot connected';
64
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
65
+ reconnectAttempts = 0;
66
+ removeSystemMessage('disconnected');
67
+ addSystemMessage('Connected');
68
+
69
+ // Flush queued messages
70
+ while (messageQueue.length > 0) {
71
+ var q = messageQueue.shift();
72
+ ws.send(q);
73
+ }
74
+ // Re-enable input if stuck
75
+ if (isProcessing) {
76
+ isProcessing = false;
77
+ setInputEnabled(true);
78
+ removeTyping();
79
+ }
80
+ };
81
+
82
+ ws.onclose = function() {
83
+ statusDot.className = 'status-dot disconnected';
84
+ if (!reconnectTimer) {
85
+ reconnectAttempts++;
86
+ var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
87
+ addSystemMessage('Reconnecting in ' + Math.round(delay / 1000) + 's...');
88
+ reconnectTimer = setTimeout(connect, delay);
89
+ }
90
+ };
91
+
92
+ ws.onerror = function() {
93
+ statusDot.className = 'status-dot disconnected';
94
+ };
95
+
96
+ ws.onmessage = function(event) {
97
+ try {
98
+ var msg = JSON.parse(event.data);
99
+ handleMessage(msg);
100
+ } catch (e) {
101
+ console.error('Parse error:', e);
102
+ }
103
+ };
104
+ }
105
+
106
+ // Message handler
107
+ function handleMessage(msg) {
108
+ switch (msg.type) {
109
+ case 'text':
110
+ if (!currentAssistantMsg) {
111
+ currentAssistantDiv = createMessageDiv('assistant');
112
+ currentAssistantMsg = {div: currentAssistantDiv, text: ''};
113
+ removeTyping();
114
+ }
115
+ currentAssistantMsg.text += msg.content;
116
+ currentAssistantMsg.div.innerHTML = marked.parse(currentAssistantMsg.text);
117
+ // Debounced syntax highlighting
118
+ if (renderTimeout) clearTimeout(renderTimeout);
119
+ renderTimeout = setTimeout(function() {
120
+ currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
121
+ hljs.highlightElement(b);
122
+ });
123
+ }, 200);
124
+ break;
125
+
126
+ case 'tool':
127
+ // Tool call start
128
+ removeTyping();
129
+ if (currentAssistantMsg) {
130
+ currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
131
+ hljs.highlightElement(b);
132
+ });
133
+ }
134
+ currentToolMsg = createMessageDiv('tool-call');
135
+ currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span> Running...`;
136
+ break;
137
+
138
+ case 'tool_result':
139
+ if (currentToolMsg) {
140
+ currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span><div class="tool-result">${escapeHtml(msg.content)}</div>`;
141
+ }
142
+ break;
143
+
144
+ case 'error':
145
+ removeTyping();
146
+ createMessageDiv('error').textContent = msg.content;
147
+ break;
148
+
149
+ case 'done':
150
+ isProcessing = false;
151
+ // Speak the response and auto-loop
152
+ const spokenText = currentAssistantMsg ? currentAssistantMsg.text : '';
153
+ currentAssistantMsg = null;
154
+ currentAssistantDiv = null;
155
+ currentToolMsg = null;
156
+ setInputEnabled(true);
157
+ if (renderTimeout) clearTimeout(renderTimeout);
158
+ renderTimeout = null;
159
+ if (spokenText) {
160
+ speakText(spokenText).then(() => startAutoLoop());
161
+ } else {
162
+ startAutoLoop();
163
+ }
164
+ break;
165
+
166
+ case 'cleared':
167
+ messagesEl.innerHTML = '';
168
+ break;
169
+ }
170
+
171
+ scrollToBottom();
172
+ }
173
+
174
+ function createMessageDiv(role) {
175
+ const div = document.createElement('div');
176
+ div.className = `message ${role}`;
177
+ messagesEl.appendChild(div);
178
+ return div;
179
+ }
180
+
181
+ function renderAndScroll(el) {
182
+ const text = el.textContent || el.innerText;
183
+ el.innerHTML = marked.parse(text);
184
+ el.querySelectorAll('pre code').forEach(function(b) {
185
+ hljs.highlightElement(b);
186
+ // Add copy button
187
+ const pre = b.parentElement;
188
+ if (pre && !pre.querySelector('.copy-btn')) {
189
+ const btn = document.createElement('button');
190
+ btn.className = 'copy-btn';
191
+ btn.innerHTML = '<i class="fas fa-copy"></i>';
192
+ btn.onclick = function() {
193
+ navigator.clipboard.writeText(b.textContent).then(function() {
194
+ btn.innerHTML = '<i class="fas fa-check"></i>';
195
+ setTimeout(function() { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
196
+ });
197
+ };
198
+ pre.style.position = 'relative';
199
+ pre.appendChild(btn);
200
+ }
201
+ });
202
+ }
203
+
204
+ // Escape HTML for tool results
205
+ function escapeHtml(text) {
206
+ const d = document.createElement('div');
207
+ d.textContent = text;
208
+ return d.innerHTML;
209
+ }
210
+
211
+ function removeTyping() {
212
+ const typing = document.querySelector('.typing');
213
+ if (typing) typing.remove();
214
+ }
215
+
216
+ function scrollToBottom() {
217
+ messagesEl.scrollTop = messagesEl.scrollHeight;
218
+ }
219
+
220
+ function addSystemMessage(text) {
221
+ const div = document.createElement('div');
222
+ div.style.cssText = 'text-align:center;font-size:12px;color:var(--text-dim);padding:4px;';
223
+ div.textContent = text;
224
+ messagesEl.appendChild(div);
225
+ scrollToBottom();
226
+ }
227
+
228
+ // Send message
229
+ function removeSystemMessage(id) {
230
+ var el = document.querySelector('.system-msg-' + id);
231
+ if (el) el.remove();
232
+ }
233
+
234
+ function sendMessage() {
235
+ var text = inputEl.value.trim();
236
+ if (!text || isProcessing) return;
237
+
238
+ // Rate limiting: max 1 message per 500ms
239
+ var now = Date.now();
240
+ if (now - lastSendTime < 500) {
241
+ addSystemMessage('Please wait...');
242
+ return;
243
+ }
244
+
245
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
246
+ addSystemMessage('Not connected. Message queued.', 'disconnected');
247
+ messageQueue.push(JSON.stringify({ type: 'message', content: text }));
248
+ connect();
249
+ return;
250
+ }
251
+
252
+ // Input length limit
253
+ if (text.length > 10000) {
254
+ addSystemMessage('Message too long (max 10000 characters)');
255
+ return;
256
+ }
257
+
258
+ // Add user message
259
+ var userDiv = createMessageDiv('user');
260
+ userDiv.textContent = text;
261
+ scrollToBottom();
262
+
263
+ inputEl.value = '';
264
+ inputEl.style.height = 'auto';
265
+
266
+ lastSendTime = now;
267
+ isProcessing = true;
268
+ setInputEnabled(false);
269
+
270
+ // Add typing indicator
271
+ var typing = document.createElement('div');
272
+ typing.className = 'typing';
273
+ typing.innerHTML = '<span></span><span></span><span></span>';
274
+ messagesEl.appendChild(typing);
275
+ scrollToBottom();
276
+
277
+ // Reset accumulators
278
+ currentAssistantMsg = null;
279
+ currentToolMsg = null;
280
+
281
+ ws.send(JSON.stringify({ type: 'message', content: text }));
282
+ }
283
+
284
+ function setInputEnabled(enabled) {
285
+ inputEl.disabled = !enabled;
286
+ sendBtn.disabled = !enabled;
287
+ if (enabled) inputEl.focus();
288
+ }
289
+
290
+ // Clear chat
291
+ function clearChat() {
292
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
293
+ ws.send(JSON.stringify({ type: 'clear' }));
294
+ }
295
+
296
+ // Voice input
297
+ let recognition = null;
298
+ let isListening = false;
299
+
300
+ function toggleVoice() {
301
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
302
+ addSystemMessage('Voice input not supported in this browser');
303
+ return;
304
+ }
305
+
306
+ if (isListening) {
307
+ recognition.stop();
308
+ return;
309
+ }
310
+
311
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
312
+ recognition = new SpeechRecognition();
313
+ recognition.lang = 'en-US';
314
+ recognition.interimResults = true;
315
+ recognition.continuous = true;
316
+
317
+ recognition.onstart = function() {
318
+ isListening = true;
319
+ voiceBtn.classList.add('listening');
320
+ voiceBtn.title = 'Listening...';
321
+ };
322
+
323
+ recognition.onresult = function(e) {
324
+ let transcript = '';
325
+ for (let i = e.resultIndex; i < e.results.length; i++) {
326
+ transcript += e.results[i][0].transcript;
327
+ }
328
+ inputEl.value = transcript;
329
+ inputEl.style.height = 'auto';
330
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
331
+ };
332
+
333
+ recognition.onend = function() {
334
+ isListening = false;
335
+ voiceBtn.classList.remove('listening');
336
+ voiceBtn.title = 'Voice input';
337
+ // Auto-send if we got something
338
+ if (inputEl.value.trim()) {
339
+ sendMessage();
340
+ }
341
+ };
342
+
343
+ recognition.onerror = function(e) {
344
+ isListening = false;
345
+ voiceBtn.classList.remove('listening');
346
+ voiceBtn.title = 'Voice input';
347
+ if (e.error !== 'no-speech') {
348
+ addSystemMessage('Voice error: ' + e.error);
349
+ }
350
+ };
351
+
352
+ recognition.start();
353
+ }
354
+
355
+ // Voice output (TTS)
356
+ let speakEnabled = true;
357
+ let autoLoopEnabled = false;
358
+
359
+ function toggleSpeak() {
360
+ speakEnabled = !speakEnabled;
361
+ const btn = document.getElementById('speakToggle');
362
+ if (speakEnabled) {
363
+ btn.innerHTML = '<i class="fas fa-volume-up"></i>';
364
+ btn.style.color = '';
365
+ } else {
366
+ btn.innerHTML = '<i class="fas fa-volume-mute"></i>';
367
+ btn.style.color = 'var(--danger)';
368
+ window.speechSynthesis.cancel();
369
+ }
370
+ }
371
+
372
+ function toggleAutoLoop() {
373
+ autoLoopEnabled = !autoLoopEnabled;
374
+ const btn = document.getElementById('autoLoopToggle');
375
+ if (autoLoopEnabled) {
376
+ btn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>';
377
+ btn.style.color = 'var(--accent)';
378
+ speakEnabled = true;
379
+ document.getElementById('speakToggle').innerHTML = '<i class="fas fa-volume-up"></i>';
380
+ document.getElementById('speakToggle').style.color = '';
381
+ } else {
382
+ btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
383
+ btn.style.color = '';
384
+ }
385
+ }
386
+
387
+ function speakText(text) {
388
+ if (!speakEnabled || !window.speechSynthesis) return Promise.resolve();
389
+ // Clean text for speech: remove markdown, code blocks, etc.
390
+ let clean = text
391
+ .replace(/```[\s\S]*?```/g, ' code block omitted ')
392
+ .replace(/`([^`]+)`/g, ' $1 ')
393
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
394
+ .replace(/\*([^*]+)\*/g, '$1')
395
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
396
+ .replace(/#{1,6}\s*/g, '')
397
+ .replace(/[-*]\s/g, '')
398
+ .replace(/\n{2}/g, '. ')
399
+ .replace(/\n/g, ' ')
400
+ .trim();
401
+
402
+ if (!clean) return Promise.resolve();
403
+
404
+ return new Promise((resolve) => {
405
+ const utterance = new SpeechSynthesisUtterance(clean);
406
+ utterance.lang = 'en-US';
407
+ utterance.rate = 1.1;
408
+ utterance.pitch = 1.0;
409
+ utterance.volume = 1.0;
410
+ utterance.onend = resolve;
411
+ utterance.onerror = resolve;
412
+ window.speechSynthesis.speak(utterance);
413
+ });
414
+ }
415
+
416
+ function startAutoLoop() {
417
+ if (!autoLoopEnabled) return;
418
+ // Small delay then re-activate mic
419
+ setTimeout(() => {
420
+ if (autoLoopEnabled && !isListening && document.visibilityState === 'visible') {
421
+ toggleVoice();
422
+ }
423
+ }, 800);
424
+ }
425
+
426
+ // Sidebar toggle
427
+ function toggleSidebar() {
428
+ sidebar.classList.toggle('open');
429
+ overlay.classList.toggle('show');
430
+ }
431
+
432
+ overlay.addEventListener('click', function() {
433
+ sidebar.classList.remove('open');
434
+ overlay.classList.remove('show');
435
+ });
436
+
437
+ // File browser
438
+ async function loadFiles(dir) {
439
+ if (!dir) dir = '.';
440
+ try {
441
+ fileList.innerHTML = '<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading...</div>';
442
+ const resp = await fetch(`/api/files?dir=${encodeURIComponent(dir)}`);
443
+ const data = await resp.json();
444
+
445
+ if (data.error) {
446
+ fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Error: ' + data.error + '</div>';
447
+ return;
448
+ }
449
+
450
+ filePath.textContent = data.path;
451
+ fileList.innerHTML = '';
452
+
453
+ // Parent dir link
454
+ if (dir !== '.') {
455
+ const parent = document.createElement('div');
456
+ parent.className = 'file-item dir';
457
+ parent.innerHTML = '<span class="icon">&#128193;</span> ..';
458
+ parent.onclick = function() {
459
+ const p = dir.split('/').slice(0, -1).join('/') || '.';
460
+ loadFiles(p);
461
+ };
462
+ fileList.appendChild(parent);
463
+ }
464
+
465
+ data.files.forEach(function(f) {
466
+ const item = document.createElement('div');
467
+ item.className = 'file-item' + (f.is_dir ? ' dir' : '');
468
+ const iconClass = getFileIcon(f.name, f.is_dir);
469
+ item.innerHTML = '<span class="icon"><i class="' + iconClass + '"></i></span> ' + f.name;
470
+ if (!f.is_dir) {
471
+ const size = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
472
+ item.title = size;
473
+ }
474
+ item.onclick = function() {
475
+ if (f.is_dir) {
476
+ loadFiles(dir + '/' + f.name);
477
+ } else {
478
+ openFile(dir + '/' + f.name);
479
+ }
480
+ };
481
+ fileList.appendChild(item);
482
+ });
483
+ } catch (e) {
484
+ fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Failed to load files</div>';
485
+ }
486
+ }
487
+
488
+ // File editor state
489
+ let currentEditorFile = '';
490
+ let editorOriginalContent = '';
491
+ let isEditing = false;
492
+
493
+ async function openFile(path) {
494
+ try {
495
+ const resp = await fetch(`/api/read-file?path=${encodeURIComponent(path)}`);
496
+ const data = await resp.json();
497
+ if (data.content !== undefined) {
498
+ sidebar.classList.remove('open');
499
+ overlay.classList.remove('show');
500
+ openEditor(path, data.content);
501
+ } else if (data.error) {
502
+ addSystemMessage('Error: ' + data.error);
503
+ }
504
+ } catch(e) {
505
+ addSystemMessage('Error reading file');
506
+ }
507
+ }
508
+
509
+ function openEditor(path, content) {
510
+ currentEditorFile = path;
511
+ editorOriginalContent = content;
512
+ isEditing = false;
513
+
514
+ document.getElementById('editorTitle').textContent = path;
515
+ document.getElementById('editorFileInfo').textContent = (content.length) + ' chars';
516
+ document.getElementById('editorModified').style.display = 'none';
517
+
518
+ const readView = document.getElementById('editorReadView');
519
+ readView.textContent = content;
520
+ readView.style.display = 'block';
521
+
522
+ const textarea = document.getElementById('editorTextarea');
523
+ textarea.value = content;
524
+ textarea.style.display = 'none';
525
+ textarea.oninput = function() {
526
+ document.getElementById('editorModified').style.display = 'inline';
527
+ document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
528
+ };
529
+
530
+ document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
531
+ document.getElementById('editorSaveBtn').disabled = true;
532
+
533
+ document.getElementById('editorOverlay').classList.add('show');
534
+ }
535
+
536
+ function closeEditor() {
537
+ document.getElementById('editorOverlay').classList.remove('show');
538
+ currentEditorFile = '';
539
+ editorOriginalContent = '';
540
+ }
541
+
542
+ function toggleEditorMode() {
543
+ const readView = document.getElementById('editorReadView');
544
+ const textarea = document.getElementById('editorTextarea');
545
+ const modeBtn = document.getElementById('editorModeBtn');
546
+
547
+ if (!isEditing) {
548
+ // Switch to edit mode
549
+ textarea.value = readView.textContent;
550
+ textarea.style.display = 'block';
551
+ readView.style.display = 'none';
552
+ textarea.focus();
553
+ isEditing = true;
554
+ modeBtn.innerHTML = '<i class="fas fa-eye"></i> View';
555
+ document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
556
+ } else {
557
+ // Switch back to view mode (discard unsaved changes)
558
+ readView.textContent = editorOriginalContent;
559
+ readView.style.display = 'block';
560
+ textarea.style.display = 'none';
561
+ isEditing = false;
562
+ modeBtn.innerHTML = '<i class="fas fa-edit"></i> Edit';
563
+ document.getElementById('editorSaveBtn').disabled = true;
564
+ document.getElementById('editorModified').style.display = 'none';
565
+ }
566
+ }
567
+
568
+ async function saveEditorFile() {
569
+ const textarea = document.getElementById('editorTextarea');
570
+ const content = textarea.value;
571
+ const path = currentEditorFile;
572
+
573
+ if (!path || content === editorOriginalContent) return;
574
+
575
+ const saveBtn = document.getElementById('editorSaveBtn');
576
+ saveBtn.disabled = true;
577
+ saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
578
+
579
+ try {
580
+ const resp = await fetch('/api/write-file', {
581
+ method: 'POST',
582
+ headers: { 'Content-Type': 'application/json' },
583
+ body: JSON.stringify({ path: path, content: content }),
584
+ });
585
+ const data = await resp.json();
586
+ if (data.status === 'written') {
587
+ editorOriginalContent = content;
588
+ isEditing = false;
589
+ document.getElementById('editorReadView').textContent = content;
590
+ document.getElementById('editorReadView').style.display = 'block';
591
+ textarea.style.display = 'none';
592
+ document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
593
+ document.getElementById('editorModified').style.display = 'none';
594
+ document.getElementById('editorFileInfo').textContent = content.length + ' chars (saved)';
595
+ addSystemMessage('File saved: ' + path);
596
+ } else {
597
+ addSystemMessage('Save failed: ' + (data.error || 'unknown error'));
598
+ }
599
+ } catch(e) {
600
+ addSystemMessage('Save error: ' + e.message);
601
+ } finally {
602
+ saveBtn.innerHTML = '<i class="fas fa-check"></i> Save';
603
+ saveBtn.disabled = true;
604
+ }
605
+ }
606
+
607
+ // Keyboard shortcut: Ctrl+S to save in editor
608
+ document.addEventListener('keydown', function(e) {
609
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
610
+ const overlay = document.getElementById('editorOverlay');
611
+ if (overlay.classList.contains('show') && isEditing) {
612
+ e.preventDefault();
613
+ saveEditorFile();
614
+ }
615
+ }
616
+ // Escape to close editor
617
+ if (e.key === 'Escape') {
618
+ const overlay = document.getElementById('editorOverlay');
619
+ if (overlay.classList.contains('show')) {
620
+ closeEditor();
621
+ }
622
+ }
623
+ });
624
+
625
+ // Load tools + model info
626
+
627
+ // Load tools + model info
628
+ async function loadMeta() {
629
+ try {
630
+ const resp = await fetch('/api/tools');
631
+ const data = await resp.json();
632
+ if (data.tools) {
633
+ // Could show tool count somewhere
634
+ }
635
+ } catch(e) {}
636
+ }
637
+
638
+ // Theme toggle
639
+ function toggleTheme() {
640
+ const isLight = document.body.classList.toggle('light');
641
+ const icon = document.querySelector('#themeToggle i');
642
+ icon.className = isLight ? 'fas fa-sun' : 'fas fa-moon';
643
+ localStorage.setItem('theme', isLight ? 'light' : 'dark');
644
+ }
645
+
646
+ // Load saved theme
647
+ const savedTheme = localStorage.getItem('theme');
648
+ if (savedTheme === 'light') {
649
+ document.body.classList.add('light');
650
+ document.querySelector('#themeToggle i').className = 'fas fa-sun';
651
+ }
652
+
653
+ // Improve file item icons
654
+ function getFileIcon(name, isDir) {
655
+ if (isDir) return 'fa-solid fa-folder';
656
+ const ext = name.split('.').pop().toLowerCase();
657
+ const iconMap = {
658
+ 'js': 'fa-brands fa-js',
659
+ 'ts': 'fa-brands fa-js',
660
+ 'jsx': 'fa-brands fa-react',
661
+ 'tsx': 'fa-brands fa-react',
662
+ 'py': 'fa-brands fa-python',
663
+ 'html': 'fa-brands fa-html5',
664
+ 'css': 'fa-brands fa-css3-alt',
665
+ 'json': 'fa-solid fa-code',
666
+ 'md': 'fa-solid fa-file-lines',
667
+ 'txt': 'fa-solid fa-file-lines',
668
+ 'yaml': 'fa-solid fa-file',
669
+ 'yml': 'fa-solid fa-file',
670
+ 'toml': 'fa-solid fa-file',
671
+ 'env': 'fa-solid fa-gear',
672
+ 'gitignore': 'fa-solid fa-eye-slash',
673
+ 'jpg': 'fa-solid fa-image',
674
+ 'png': 'fa-solid fa-image',
675
+ 'svg': 'fa-solid fa-image',
676
+ 'ico': 'fa-solid fa-image',
677
+ 'woff2': 'fa-solid fa-font',
678
+ 'woff': 'fa-solid fa-font',
679
+ 'ttf': 'fa-solid fa-font',
680
+ };
681
+ return iconMap[ext] || 'fa-solid fa-file';
682
+ }
683
+
684
+ // Connect on page load
685
+ document.addEventListener('DOMContentLoaded', function() {
686
+ // Register service worker for PWA
687
+ if ('serviceWorker' in navigator) {
688
+ navigator.serviceWorker.register('/sw.js').catch(function() {});
689
+ }
690
+ connect();
691
+ loadFiles();
692
+ loadMeta();
693
+ inputEl.focus();
694
+ });