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