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,1965 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
8
+ <meta name="apple-mobile-web-app-title" content="LuckyD Code">
9
+ <meta name="mobile-web-app-capable" content="yes">
10
+ <meta name="theme-color" content="#0d1117">
11
+ <link rel="manifest" href="/manifest.json">
12
+ <link rel="apple-touch-icon" href="/icon-192.png">
13
+ <link rel="icon" href="/icon-192.png" type="image/png">
14
+ <title>LuckyD Code</title>
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
16
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js"></script>
18
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
19
+ <style>
20
+ /* === Auto-generated by build_html.py v202604281724 === */
21
+ /* === Auto-generated by build_html.py v202604281723 === */
22
+ /* === Auto-generated by build_html.py v202604281723 === */
23
+ :root {
24
+ --bg: #1a1a2e;
25
+ --surface: #16213e;
26
+ --surface2: #1f2b47;
27
+ --border: #2a3a5e;
28
+ --text: #e0e0e0;
29
+ --text-dim: #8892b0;
30
+ --accent: #4fc3f7;
31
+ --accent2: #81c784;
32
+ --accent3: #ffb74d;
33
+ --danger: #ef5350;
34
+ --msg-user: #1e3a5f;
35
+ --msg-assistant: #1a1a2e;
36
+ --sidebar-w: 280px;
37
+ --header-h: 52px;
38
+ --radius: 8px;
39
+ }
40
+
41
+ /* Light theme overrides */
42
+ .light {
43
+ --bg: #f5f5f5;
44
+ --surface: #ffffff;
45
+ --surface2: #e8e8e8;
46
+ --border: #d0d0d0;
47
+ --text: #1a1a2e;
48
+ --text-dim: #666;
49
+ --accent: #1976d2;
50
+ --accent2: #388e3c;
51
+ --accent3: #f57c00;
52
+ --danger: #d32f2f;
53
+ --msg-user: #e3f2fd;
54
+ --msg-assistant: #ffffff;
55
+ }
56
+
57
+ * { margin: 0; padding: 0; box-sizing: border-box; }
58
+
59
+ body {
60
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
61
+ background: var(--bg);
62
+ color: var(--text);
63
+ height: 100vh;
64
+ overflow: hidden;
65
+ }
66
+
67
+ /* Layout */
68
+ .app { display: flex; height: 100vh; }
69
+
70
+ /* Sidebar */
71
+ .sidebar {
72
+ width: var(--sidebar-w);
73
+ background: var(--surface);
74
+ border-right: 1px solid var(--border);
75
+ display: flex;
76
+ flex-direction: column;
77
+ flex-shrink: 0;
78
+ transition: transform .3s;
79
+ }
80
+ .sidebar-header {
81
+ padding: 14px 16px;
82
+ border-bottom: 1px solid var(--border);
83
+ font-weight: 600;
84
+ font-size: 14px;
85
+ color: var(--text-dim);
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: center;
89
+ }
90
+ .sidebar-header button {
91
+ background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 14px;
92
+ }
93
+ .sidebar-content {
94
+ flex: 1;
95
+ overflow-y: auto;
96
+ padding: 8px;
97
+ }
98
+ .file-item {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 8px;
102
+ padding: 6px 10px;
103
+ border-radius: 4px;
104
+ cursor: pointer;
105
+ font-size: 13px;
106
+ color: var(--text-dim);
107
+ transition: background .15s;
108
+ }
109
+ .file-item:hover { background: var(--surface2); color: var(--text); }
110
+ .file-item .icon { font-size: 14px; }
111
+ .file-item.dir { color: var(--accent); }
112
+ .file-path { padding: 6px 10px; font-size: 11px; color: var(--text-dim); border-top: 1px solid var(--border); word-break: break-all; }
113
+
114
+ .sidebar-toggle {
115
+ display: none;
116
+ background: none;
117
+ border: none;
118
+ color: var(--text);
119
+ font-size: 20px;
120
+ cursor: pointer;
121
+ padding: 8px;
122
+ }
123
+
124
+ /* Main area */
125
+ .main {
126
+ flex: 1;
127
+ display: flex;
128
+ flex-direction: column;
129
+ min-width: 0;
130
+ }
131
+
132
+ /* Header */
133
+ .header {
134
+ height: var(--header-h);
135
+ display: flex;
136
+ align-items: center;
137
+ padding: 0 16px;
138
+ border-bottom: 1px solid var(--border);
139
+ gap: 12px;
140
+ flex-shrink: 0;
141
+ }
142
+ .header h1 {
143
+ font-size: 16px;
144
+ font-weight: 600;
145
+ }
146
+ .header .model-badge {
147
+ font-size: 11px;
148
+ color: var(--accent);
149
+ background: rgba(79,195,247,.15);
150
+ padding: 2px 8px;
151
+ border-radius: 10px;
152
+ }
153
+ .header-actions {
154
+ margin-left: auto;
155
+ display: flex;
156
+ gap: 8px;
157
+ }
158
+ .header-actions button {
159
+ background: none; border: none; color: var(--text-dim); cursor: pointer;
160
+ padding: 4px 8px; border-radius: 4px; font-size: 13px;
161
+ }
162
+ .header-actions button:hover { background: var(--surface2); color: var(--text); }
163
+
164
+ /* Messages */
165
+ .messages {
166
+ flex: 1;
167
+ overflow-y: auto;
168
+ padding: 16px;
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 12px;
172
+ }
173
+ .message {
174
+ max-width: 85%;
175
+ padding: 12px 16px;
176
+ border-radius: var(--radius);
177
+ line-height: 1.5;
178
+ font-size: 14px;
179
+ animation: fadeIn .2s;
180
+ }
181
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
182
+
183
+ .message.user {
184
+ align-self: flex-end;
185
+ background: var(--msg-user);
186
+ border-bottom-right-radius: 2px;
187
+ }
188
+ .message.assistant {
189
+ align-self: flex-start;
190
+ background: var(--msg-assistant);
191
+ border: 1px solid var(--border);
192
+ border-bottom-left-radius: 2px;
193
+ }
194
+ .message.tool-call {
195
+ align-self: flex-start;
196
+ background: rgba(255,183,77,.08);
197
+ border: 1px solid rgba(255,183,77,.2);
198
+ font-size: 13px;
199
+ color: var(--accent3);
200
+ width: 100%;
201
+ max-width: 100%;
202
+ }
203
+ .message.tool-call .tool-name { font-weight: 600; }
204
+ .message.tool-call .tool-result { color: var(--text-dim); margin-top: 4px; font-family: monospace; font-size: 12px; }
205
+ .message.error {
206
+ align-self: center;
207
+ background: rgba(239,83,80,.1);
208
+ border: 1px solid rgba(239,83,80,.3);
209
+ color: var(--danger);
210
+ font-size: 13px;
211
+ }
212
+
213
+ /* Message content markdown */
214
+ .message p { margin-bottom: 8px; }
215
+ .message p:last-child { margin-bottom: 0; }
216
+ .message ul, .message ol { margin: 4px 0 8px 20px; }
217
+ .message li { margin-bottom: 2px; }
218
+ .message code {
219
+ background: rgba(255,255,255,.08);
220
+ padding: 2px 6px;
221
+ border-radius: 3px;
222
+ font-size: 13px;
223
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
224
+ }
225
+ .message pre {
226
+ margin: 8px 0;
227
+ border-radius: 6px;
228
+ overflow-x: auto;
229
+ }
230
+ .message pre code {
231
+ background: none;
232
+ padding: 0;
233
+ font-size: 13px;
234
+ }
235
+ .message h1, .message h2, .message h3, .message h4 {
236
+ margin: 12px 0 6px;
237
+ color: var(--accent);
238
+ }
239
+ .message h1 { font-size: 18px; }
240
+ .message h2 { font-size: 16px; }
241
+ .message h3 { font-size: 15px; }
242
+ .message blockquote {
243
+ border-left: 3px solid var(--accent);
244
+ padding-left: 12px;
245
+ color: var(--text-dim);
246
+ margin: 8px 0;
247
+ }
248
+ .message table {
249
+ border-collapse: collapse;
250
+ margin: 8px 0;
251
+ font-size: 13px;
252
+ }
253
+ .message th, .message td {
254
+ border: 1px solid var(--border);
255
+ padding: 6px 10px;
256
+ text-align: left;
257
+ }
258
+ .message th { background: var(--surface2); }
259
+
260
+ /* Typing indicator */
261
+ .typing {
262
+ align-self: flex-start;
263
+ display: flex;
264
+ gap: 4px;
265
+ padding: 12px 16px;
266
+ background: var(--msg-assistant);
267
+ border: 1px solid var(--border);
268
+ border-radius: var(--radius);
269
+ }
270
+ .typing span {
271
+ width: 8px; height: 8px; border-radius: 50%;
272
+ background: var(--text-dim);
273
+ animation: typingDot 1.4s infinite;
274
+ }
275
+ .typing span:nth-child(2) { animation-delay: .2s; }
276
+ .typing span:nth-child(3) { animation-delay: .4s; }
277
+ @keyframes typingDot { 0%,60%,100% { opacity: .3; transform: scale(1); } 30% { opacity: 1; transform: scale(1.2); } }
278
+
279
+ /* Input area */
280
+ .input-area {
281
+ padding: 12px 16px;
282
+ border-top: 1px solid var(--border);
283
+ background: var(--surface);
284
+ flex-shrink: 0;
285
+ }
286
+ .input-row {
287
+ display: flex;
288
+ gap: 8px;
289
+ align-items: flex-end;
290
+ }
291
+ .input-row textarea {
292
+ flex: 1;
293
+ background: var(--surface2);
294
+ border: 1px solid var(--border);
295
+ border-radius: var(--radius);
296
+ color: var(--text);
297
+ padding: 10px 14px;
298
+ font-size: 14px;
299
+ font-family: inherit;
300
+ resize: none;
301
+ outline: none;
302
+ min-height: 42px;
303
+ max-height: 150px;
304
+ line-height: 1.4;
305
+ }
306
+ .input-row textarea:focus { border-color: var(--accent); }
307
+ .input-row textarea::placeholder { color: var(--text-dim); }
308
+
309
+ .input-row button {
310
+ background: var(--accent);
311
+ border: none;
312
+ color: #0d1117;
313
+ width: 42px;
314
+ height: 42px;
315
+ border-radius: 50%;
316
+ cursor: pointer;
317
+ font-size: 18px;
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ transition: background .15s, opacity .15s;
322
+ flex-shrink: 0;
323
+ }
324
+ .input-row button:hover { background: #39b0e8; }
325
+ .input-row button:disabled { opacity: .4; cursor: not-allowed; }
326
+ .input-row button.voice { background: var(--surface2); color: var(--text); }
327
+ .input-row button.voice:hover { background: var(--border); }
328
+ .input-row button.voice.listening { background: var(--danger); color: white; animation: pulse 1s infinite; }
329
+ @keyframes pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(239,83,80,.4); } 50% { box-shadow: 0 0 0 8px rgba(239,83,80,0); } }
330
+
331
+ .input-hint {
332
+ font-size: 11px;
333
+ color: var(--text-dim);
334
+ margin-top: 6px;
335
+ text-align: center;
336
+ }
337
+ .input-hint kbd {
338
+ background: var(--surface2);
339
+ padding: 1px 5px;
340
+ border-radius: 3px;
341
+ font-size: 10px;
342
+ font-family: inherit;
343
+ border: 1px solid var(--border);
344
+ }
345
+
346
+ /* Scrollbar */
347
+ ::-webkit-scrollbar { width: 6px; }
348
+ ::-webkit-scrollbar-track { background: transparent; }
349
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
350
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
351
+
352
+ /* Mobile responsive */
353
+ @media (max-width: 768px) {
354
+ .sidebar {
355
+ position: fixed;
356
+ left: 0;
357
+ top: 0;
358
+ bottom: 0;
359
+ z-index: 100;
360
+ transform: translateX(-100%);
361
+ }
362
+ .sidebar.open { transform: translateX(0); }
363
+ .sidebar-toggle { display: block; }
364
+ .message { max-width: 95%; }
365
+ .overlay {
366
+ display: none;
367
+ position: fixed;
368
+ inset: 0;
369
+ background: rgba(0,0,0,.5);
370
+ z-index: 99;
371
+ }
372
+ .overlay.show { display: block; }
373
+ }
374
+
375
+ /* Copy button for code blocks */
376
+ .copy-btn {
377
+ position: absolute;
378
+ top: 6px;
379
+ right: 6px;
380
+ background: rgba(255,255,255,.1);
381
+ border: 1px solid var(--border);
382
+ color: var(--text-dim);
383
+ padding: 4px 8px;
384
+ border-radius: 4px;
385
+ cursor: pointer;
386
+ font-size: 12px;
387
+ opacity: 0;
388
+ transition: opacity .2s;
389
+ }
390
+ pre:hover .copy-btn { opacity: 1; }
391
+ .copy-btn:hover { background: rgba(255,255,255,.2); color: var(--text); }
392
+
393
+ /* File editor */
394
+ .editor-overlay {
395
+ display: none;
396
+ position: fixed;
397
+ inset: 0;
398
+ background: rgba(0,0,0,.6);
399
+ z-index: 200;
400
+ align-items: center;
401
+ justify-content: center;
402
+ padding: 20px;
403
+ }
404
+ .editor-overlay.show { display: flex; }
405
+ .editor-panel {
406
+ background: var(--surface);
407
+ border: 1px solid var(--border);
408
+ border-radius: var(--radius);
409
+ width: 100%;
410
+ max-width: 800px;
411
+ max-height: 85vh;
412
+ display: flex;
413
+ flex-direction: column;
414
+ box-shadow: 0 8px 32px rgba(0,0,0,.4);
415
+ }
416
+ .editor-header {
417
+ display: flex;
418
+ align-items: center;
419
+ justify-content: space-between;
420
+ padding: 12px 16px;
421
+ border-bottom: 1px solid var(--border);
422
+ }
423
+ .editor-header h3 { font-size: 14px; font-weight: 600; word-break: break-all; }
424
+ .editor-actions { display: flex; gap: 6px; }
425
+ .editor-actions button {
426
+ background: var(--surface2);
427
+ border: 1px solid var(--border);
428
+ color: var(--text);
429
+ padding: 6px 14px;
430
+ border-radius: 4px;
431
+ cursor: pointer;
432
+ font-size: 13px;
433
+ transition: background .15s;
434
+ }
435
+ .editor-actions button:hover { background: var(--border); }
436
+ .editor-actions button.save {
437
+ background: var(--accent);
438
+ color: #0d1117;
439
+ border-color: var(--accent);
440
+ font-weight: 600;
441
+ }
442
+ .editor-actions button.save:hover { opacity: .85; }
443
+ .editor-actions button.save:disabled { opacity: .4; cursor: not-allowed; }
444
+ .editor-body {
445
+ flex: 1;
446
+ overflow-y: auto;
447
+ padding: 0;
448
+ }
449
+ .editor-body textarea {
450
+ width: 100%;
451
+ min-height: 300px;
452
+ max-height: 65vh;
453
+ border: none;
454
+ background: var(--bg);
455
+ color: var(--text);
456
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
457
+ font-size: 13px;
458
+ line-height: 1.5;
459
+ padding: 16px;
460
+ resize: vertical;
461
+ outline: none;
462
+ tab-size: 2;
463
+ }
464
+ .editor-body .readonly-view {
465
+ padding: 16px;
466
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
467
+ font-size: 13px;
468
+ line-height: 1.5;
469
+ white-space: pre-wrap;
470
+ overflow-x: auto;
471
+ color: var(--text);
472
+ }
473
+ .editor-footer {
474
+ padding: 10px 16px;
475
+ border-top: 1px solid var(--border);
476
+ font-size: 12px;
477
+ color: var(--text-dim);
478
+ display: flex;
479
+ justify-content: space-between;
480
+ }
481
+ .editor-footer .modified { color: var(--accent3); }
482
+
483
+ /* Connection status */
484
+ .status-dot {
485
+ width: 8px;
486
+ height: 8px;
487
+ border-radius: 50%;
488
+ display: inline-block;
489
+ margin-right: 6px;
490
+ }
491
+ .status-dot.connected { background: var(--accent2); }
492
+ .status-dot.disconnected { background: var(--danger); }
493
+ .status-dot.connecting { background: var(--accent3); }
494
+ </style>
495
+ </head>
496
+ <body>
497
+ <div class="app">
498
+ <!-- Sidebar backdrop for mobile -->
499
+ <div class="overlay" id="sidebarOverlay"></div>
500
+
501
+ <!-- Sidebar -->
502
+ <aside class="sidebar" id="sidebar">
503
+ <div class="sidebar-header">
504
+ Files
505
+ <button onclick="loadFiles()" title="Refresh">&circlearrowleft;</button>
506
+ </div>
507
+ <div class="sidebar-content" id="fileList">
508
+ <div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading files...</div>
509
+ </div>
510
+ <div class="file-path" id="filePath">.</div>
511
+ </aside>
512
+
513
+ <!-- Main -->
514
+ <div class="main">
515
+ <!-- Header -->
516
+ <div class="header">
517
+ <button class="sidebar-toggle" id="sidebarToggle" onclick="toggleSidebar()">&#9776;</button>
518
+ <h1>LuckyD Code</h1>
519
+ <span class="model-badge" id="modelBadge">luckyd-chat</span>
520
+ <div class="header-actions">
521
+ <span class="status-dot disconnected" id="statusDot"></span>
522
+ <button id="speakToggle" onclick="toggleSpeak()" title="Voice output"><i class="fas fa-volume-up"></i></button>
523
+ <button id="autoLoopToggle" onclick="toggleAutoLoop()" title="Auto-listen"><i class="fas fa-sync-alt"></i></button>
524
+ <button id="themeToggle" onclick="toggleTheme()" title="Toggle theme"><i class="fas fa-moon"></i></button>
525
+ <button onclick="clearChat()" title="Clear conversation"><i class="fas fa-trash-alt"></i></button>
526
+ </div>
527
+ </div>
528
+
529
+ <!-- Messages -->
530
+ <div class="messages" id="messages"></div>
531
+
532
+ <!-- Input -->
533
+ <div class="input-area">
534
+ <div class="input-row">
535
+ <button class="voice" id="voiceBtn" onclick="toggleVoice()" title="Voice input">&#127897;</button>
536
+ <textarea id="input" rows="1" placeholder="Type a message..." maxlength="10000"></textarea>
537
+ <button id="sendBtn" onclick="sendMessage()" title="Send">&#10148;</button>
538
+ </div>
539
+ <div class="input-hint">
540
+ <kbd>Enter</kbd> to send &bull; <kbd>Shift+Enter</kbd> for newline
541
+ </div>
542
+ </div>
543
+ </div>
544
+ </div>
545
+
546
+ <!-- File editor overlay -->
547
+ <div class="editor-overlay" id="editorOverlay">
548
+ <div class="editor-panel">
549
+ <div class="editor-header">
550
+ <h3 id="editorTitle">File</h3>
551
+ <div class="editor-actions">
552
+ <button onclick="toggleEditorMode()" id="editorModeBtn"><i class="fas fa-edit"></i> Edit</button>
553
+ <button class="save" onclick="saveEditorFile()" id="editorSaveBtn" disabled><i class="fas fa-check"></i> Save</button>
554
+ <button onclick="closeEditor()"><i class="fas fa-times"></i> Close</button>
555
+ </div>
556
+ </div>
557
+ <div class="editor-body">
558
+ <div class="readonly-view" id="editorReadView"></div>
559
+ <textarea id="editorTextarea" style="display:none;"></textarea>
560
+ </div>
561
+ <div class="editor-footer">
562
+ <span id="editorFileInfo"></span>
563
+ <span id="editorModified" class="modified" style="display:none;">Modified</span>
564
+ </div>
565
+ </div>
566
+ </div>
567
+
568
+ <script>
569
+ // Configure marked
570
+ marked.setOptions({
571
+ highlight: function(code, lang) {
572
+ if (lang && hljs.getLanguage(lang)) {
573
+ try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
574
+ }
575
+ try { return hljs.highlightAuto(code).value; } catch(e) {}
576
+ return code;
577
+ },
578
+ breaks: true,
579
+ gfm: true,
580
+ });
581
+
582
+ // State
583
+ let ws = null;
584
+ let isProcessing = false;
585
+ let currentAssistantMsg = null;
586
+ let currentAssistantDiv = null;
587
+ let currentToolMsg = null;
588
+ let reconnectTimer = null;
589
+ let reconnectAttempts = 0;
590
+ let lastSendTime = 0;
591
+ let messageQueue = [];
592
+ let renderTimeout = null;
593
+
594
+ // DOM refs
595
+ const messagesEl = document.getElementById('messages');
596
+ const inputEl = document.getElementById('input');
597
+ const sendBtn = document.getElementById('sendBtn');
598
+ const voiceBtn = document.getElementById('voiceBtn');
599
+ const statusDot = document.getElementById('statusDot');
600
+ const fileList = document.getElementById('fileList');
601
+ const filePath = document.getElementById('filePath');
602
+ const sidebar = document.getElementById('sidebar');
603
+ const overlay = document.getElementById('sidebarOverlay');
604
+
605
+ // Auto-resize textarea
606
+ inputEl.addEventListener('input', function() {
607
+ this.style.height = 'auto';
608
+ this.style.height = Math.min(this.scrollHeight, 150) + 'px';
609
+ });
610
+
611
+ // Enter to send, Shift+Enter for newline, Ctrl+Enter also sends
612
+ inputEl.addEventListener('keydown', function(e) {
613
+ if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && e.ctrlKey)) {
614
+ e.preventDefault();
615
+ sendMessage();
616
+ }
617
+ });
618
+
619
+ // WebSocket connection with exponential backoff
620
+ function connect() {
621
+ if (ws && ws.readyState === WebSocket.OPEN) return;
622
+
623
+ statusDot.className = 'status-dot connecting';
624
+
625
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
626
+ const url = `${protocol}//${location.host}/ws`;
627
+
628
+ ws = new WebSocket(url);
629
+
630
+ ws.onopen = function() {
631
+ statusDot.className = 'status-dot connected';
632
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
633
+ reconnectAttempts = 0;
634
+ removeSystemMessage('disconnected');
635
+ addSystemMessage('Connected');
636
+
637
+ // Flush queued messages
638
+ while (messageQueue.length > 0) {
639
+ var q = messageQueue.shift();
640
+ ws.send(q);
641
+ }
642
+ // Re-enable input if stuck
643
+ if (isProcessing) {
644
+ isProcessing = false;
645
+ setInputEnabled(true);
646
+ removeTyping();
647
+ }
648
+ };
649
+
650
+ ws.onclose = function() {
651
+ statusDot.className = 'status-dot disconnected';
652
+ if (!reconnectTimer) {
653
+ reconnectAttempts++;
654
+ var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
655
+ addSystemMessage('Reconnecting in ' + Math.round(delay / 1000) + 's...');
656
+ reconnectTimer = setTimeout(connect, delay);
657
+ }
658
+ };
659
+
660
+ ws.onerror = function() {
661
+ statusDot.className = 'status-dot disconnected';
662
+ };
663
+
664
+ ws.onmessage = function(event) {
665
+ try {
666
+ var msg = JSON.parse(event.data);
667
+ handleMessage(msg);
668
+ } catch (e) {
669
+ console.error('Parse error:', e);
670
+ }
671
+ };
672
+ }
673
+
674
+ // Message handler
675
+ function handleMessage(msg) {
676
+ switch (msg.type) {
677
+ case 'text':
678
+ if (!currentAssistantMsg) {
679
+ currentAssistantDiv = createMessageDiv('assistant');
680
+ currentAssistantMsg = {div: currentAssistantDiv, text: ''};
681
+ removeTyping();
682
+ }
683
+ currentAssistantMsg.text += msg.content;
684
+ currentAssistantMsg.div.innerHTML = marked.parse(currentAssistantMsg.text);
685
+ // Debounced syntax highlighting
686
+ if (renderTimeout) clearTimeout(renderTimeout);
687
+ renderTimeout = setTimeout(function() {
688
+ currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
689
+ hljs.highlightElement(b);
690
+ });
691
+ }, 200);
692
+ break;
693
+
694
+ case 'tool':
695
+ // Tool call start
696
+ removeTyping();
697
+ if (currentAssistantMsg) {
698
+ currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
699
+ hljs.highlightElement(b);
700
+ });
701
+ }
702
+ currentToolMsg = createMessageDiv('tool-call');
703
+ currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span> Running...`;
704
+ break;
705
+
706
+ case 'tool_result':
707
+ if (currentToolMsg) {
708
+ currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span><div class="tool-result">${escapeHtml(msg.content)}</div>`;
709
+ }
710
+ break;
711
+
712
+ case 'error':
713
+ removeTyping();
714
+ createMessageDiv('error').textContent = msg.content;
715
+ break;
716
+
717
+ case 'done':
718
+ isProcessing = false;
719
+ // Speak the response and auto-loop
720
+ const spokenText = currentAssistantMsg ? currentAssistantMsg.text : '';
721
+ currentAssistantMsg = null;
722
+ currentAssistantDiv = null;
723
+ currentToolMsg = null;
724
+ setInputEnabled(true);
725
+ if (renderTimeout) clearTimeout(renderTimeout);
726
+ renderTimeout = null;
727
+ if (spokenText) {
728
+ speakText(spokenText).then(() => startAutoLoop());
729
+ } else {
730
+ startAutoLoop();
731
+ }
732
+ break;
733
+
734
+ case 'cleared':
735
+ messagesEl.innerHTML = '';
736
+ break;
737
+ }
738
+
739
+ scrollToBottom();
740
+ }
741
+
742
+ function createMessageDiv(role) {
743
+ const div = document.createElement('div');
744
+ div.className = `message ${role}`;
745
+ messagesEl.appendChild(div);
746
+ return div;
747
+ }
748
+
749
+ function renderAndScroll(el) {
750
+ const text = el.textContent || el.innerText;
751
+ el.innerHTML = marked.parse(text);
752
+ el.querySelectorAll('pre code').forEach(function(b) {
753
+ hljs.highlightElement(b);
754
+ // Add copy button
755
+ const pre = b.parentElement;
756
+ if (pre && !pre.querySelector('.copy-btn')) {
757
+ const btn = document.createElement('button');
758
+ btn.className = 'copy-btn';
759
+ btn.innerHTML = '<i class="fas fa-copy"></i>';
760
+ btn.onclick = function() {
761
+ navigator.clipboard.writeText(b.textContent).then(function() {
762
+ btn.innerHTML = '<i class="fas fa-check"></i>';
763
+ setTimeout(function() { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
764
+ });
765
+ };
766
+ pre.style.position = 'relative';
767
+ pre.appendChild(btn);
768
+ }
769
+ });
770
+ }
771
+
772
+ // Escape HTML for tool results
773
+ function escapeHtml(text) {
774
+ const d = document.createElement('div');
775
+ d.textContent = text;
776
+ return d.innerHTML;
777
+ }
778
+
779
+ function removeTyping() {
780
+ const typing = document.querySelector('.typing');
781
+ if (typing) typing.remove();
782
+ }
783
+
784
+ function scrollToBottom() {
785
+ messagesEl.scrollTop = messagesEl.scrollHeight;
786
+ }
787
+
788
+ function addSystemMessage(text) {
789
+ const div = document.createElement('div');
790
+ div.style.cssText = 'text-align:center;font-size:12px;color:var(--text-dim);padding:4px;';
791
+ div.textContent = text;
792
+ messagesEl.appendChild(div);
793
+ scrollToBottom();
794
+ }
795
+
796
+ // Send message
797
+ function removeSystemMessage(id) {
798
+ var el = document.querySelector('.system-msg-' + id);
799
+ if (el) el.remove();
800
+ }
801
+
802
+ function sendMessage() {
803
+ var text = inputEl.value.trim();
804
+ if (!text || isProcessing) return;
805
+
806
+ // Rate limiting: max 1 message per 500ms
807
+ var now = Date.now();
808
+ if (now - lastSendTime < 500) {
809
+ addSystemMessage('Please wait...');
810
+ return;
811
+ }
812
+
813
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
814
+ addSystemMessage('Not connected. Message queued.', 'disconnected');
815
+ messageQueue.push(JSON.stringify({ type: 'message', content: text }));
816
+ connect();
817
+ return;
818
+ }
819
+
820
+ // Input length limit
821
+ if (text.length > 10000) {
822
+ addSystemMessage('Message too long (max 10000 characters)');
823
+ return;
824
+ }
825
+
826
+ // Add user message
827
+ var userDiv = createMessageDiv('user');
828
+ userDiv.textContent = text;
829
+ scrollToBottom();
830
+
831
+ inputEl.value = '';
832
+ inputEl.style.height = 'auto';
833
+
834
+ lastSendTime = now;
835
+ isProcessing = true;
836
+ setInputEnabled(false);
837
+
838
+ // Add typing indicator
839
+ var typing = document.createElement('div');
840
+ typing.className = 'typing';
841
+ typing.innerHTML = '<span></span><span></span><span></span>';
842
+ messagesEl.appendChild(typing);
843
+ scrollToBottom();
844
+
845
+ // Reset accumulators
846
+ currentAssistantMsg = null;
847
+ currentToolMsg = null;
848
+
849
+ ws.send(JSON.stringify({ type: 'message', content: text }));
850
+ }
851
+
852
+ function setInputEnabled(enabled) {
853
+ inputEl.disabled = !enabled;
854
+ sendBtn.disabled = !enabled;
855
+ if (enabled) inputEl.focus();
856
+ }
857
+
858
+ // Clear chat
859
+ function clearChat() {
860
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
861
+ ws.send(JSON.stringify({ type: 'clear' }));
862
+ }
863
+
864
+ // Voice input
865
+ let recognition = null;
866
+ let isListening = false;
867
+
868
+ function toggleVoice() {
869
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
870
+ addSystemMessage('Voice input not supported in this browser');
871
+ return;
872
+ }
873
+
874
+ if (isListening) {
875
+ recognition.stop();
876
+ return;
877
+ }
878
+
879
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
880
+ recognition = new SpeechRecognition();
881
+ recognition.lang = 'en-US';
882
+ recognition.interimResults = true;
883
+ recognition.continuous = true;
884
+
885
+ recognition.onstart = function() {
886
+ isListening = true;
887
+ voiceBtn.classList.add('listening');
888
+ voiceBtn.title = 'Listening...';
889
+ };
890
+
891
+ recognition.onresult = function(e) {
892
+ let transcript = '';
893
+ for (let i = e.resultIndex; i < e.results.length; i++) {
894
+ transcript += e.results[i][0].transcript;
895
+ }
896
+ inputEl.value = transcript;
897
+ inputEl.style.height = 'auto';
898
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
899
+ };
900
+
901
+ recognition.onend = function() {
902
+ isListening = false;
903
+ voiceBtn.classList.remove('listening');
904
+ voiceBtn.title = 'Voice input';
905
+ // Auto-send if we got something
906
+ if (inputEl.value.trim()) {
907
+ sendMessage();
908
+ }
909
+ };
910
+
911
+ recognition.onerror = function(e) {
912
+ isListening = false;
913
+ voiceBtn.classList.remove('listening');
914
+ voiceBtn.title = 'Voice input';
915
+ if (e.error !== 'no-speech') {
916
+ addSystemMessage('Voice error: ' + e.error);
917
+ }
918
+ };
919
+
920
+ recognition.start();
921
+ }
922
+
923
+ // Voice output (TTS)
924
+ let speakEnabled = true;
925
+ let autoLoopEnabled = false;
926
+
927
+ function toggleSpeak() {
928
+ speakEnabled = !speakEnabled;
929
+ const btn = document.getElementById('speakToggle');
930
+ if (speakEnabled) {
931
+ btn.innerHTML = '<i class="fas fa-volume-up"></i>';
932
+ btn.style.color = '';
933
+ } else {
934
+ btn.innerHTML = '<i class="fas fa-volume-mute"></i>';
935
+ btn.style.color = 'var(--danger)';
936
+ window.speechSynthesis.cancel();
937
+ }
938
+ }
939
+
940
+ function toggleAutoLoop() {
941
+ autoLoopEnabled = !autoLoopEnabled;
942
+ const btn = document.getElementById('autoLoopToggle');
943
+ if (autoLoopEnabled) {
944
+ btn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>';
945
+ btn.style.color = 'var(--accent)';
946
+ speakEnabled = true;
947
+ document.getElementById('speakToggle').innerHTML = '<i class="fas fa-volume-up"></i>';
948
+ document.getElementById('speakToggle').style.color = '';
949
+ } else {
950
+ btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
951
+ btn.style.color = '';
952
+ }
953
+ }
954
+
955
+ function speakText(text) {
956
+ if (!speakEnabled || !window.speechSynthesis) return Promise.resolve();
957
+ // Clean text for speech: remove markdown, code blocks, etc.
958
+ let clean = text
959
+ .replace(/```[\s\S]*?```/g, ' code block omitted ')
960
+ .replace(/`([^`]+)`/g, ' $1 ')
961
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
962
+ .replace(/\*([^*]+)\*/g, '$1')
963
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
964
+ .replace(/#{1,6}\s*/g, '')
965
+ .replace(/[-*]\s/g, '')
966
+ .replace(/\n{2}/g, '. ')
967
+ .replace(/\n/g, ' ')
968
+ .trim();
969
+
970
+ if (!clean) return Promise.resolve();
971
+
972
+ return new Promise((resolve) => {
973
+ const utterance = new SpeechSynthesisUtterance(clean);
974
+ utterance.lang = 'en-US';
975
+ utterance.rate = 1.1;
976
+ utterance.pitch = 1.0;
977
+ utterance.volume = 1.0;
978
+ utterance.onend = resolve;
979
+ utterance.onerror = resolve;
980
+ window.speechSynthesis.speak(utterance);
981
+ });
982
+ }
983
+
984
+ function startAutoLoop() {
985
+ if (!autoLoopEnabled) return;
986
+ // Small delay then re-activate mic
987
+ setTimeout(() => {
988
+ if (autoLoopEnabled && !isListening && document.visibilityState === 'visible') {
989
+ toggleVoice();
990
+ }
991
+ }, 800);
992
+ }
993
+
994
+ // Sidebar toggle
995
+ function toggleSidebar() {
996
+ sidebar.classList.toggle('open');
997
+ overlay.classList.toggle('show');
998
+ }
999
+
1000
+ overlay.addEventListener('click', function() {
1001
+ sidebar.classList.remove('open');
1002
+ overlay.classList.remove('show');
1003
+ });
1004
+
1005
+ // File browser
1006
+ async function loadFiles(dir) {
1007
+ if (!dir) dir = '.';
1008
+ try {
1009
+ fileList.innerHTML = '<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading...</div>';
1010
+ const resp = await fetch(`/api/files?dir=${encodeURIComponent(dir)}`);
1011
+ const data = await resp.json();
1012
+
1013
+ if (data.error) {
1014
+ fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Error: ' + data.error + '</div>';
1015
+ return;
1016
+ }
1017
+
1018
+ filePath.textContent = data.path;
1019
+ fileList.innerHTML = '';
1020
+
1021
+ // Parent dir link
1022
+ if (dir !== '.') {
1023
+ const parent = document.createElement('div');
1024
+ parent.className = 'file-item dir';
1025
+ parent.innerHTML = '<span class="icon">&#128193;</span> ..';
1026
+ parent.onclick = function() {
1027
+ const p = dir.split('/').slice(0, -1).join('/') || '.';
1028
+ loadFiles(p);
1029
+ };
1030
+ fileList.appendChild(parent);
1031
+ }
1032
+
1033
+ data.files.forEach(function(f) {
1034
+ const item = document.createElement('div');
1035
+ item.className = 'file-item' + (f.is_dir ? ' dir' : '');
1036
+ const iconClass = getFileIcon(f.name, f.is_dir);
1037
+ item.innerHTML = '<span class="icon"><i class="' + iconClass + '"></i></span> ' + f.name;
1038
+ if (!f.is_dir) {
1039
+ const size = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
1040
+ item.title = size;
1041
+ }
1042
+ item.onclick = function() {
1043
+ if (f.is_dir) {
1044
+ loadFiles(dir + '/' + f.name);
1045
+ } else {
1046
+ openFile(dir + '/' + f.name);
1047
+ }
1048
+ };
1049
+ fileList.appendChild(item);
1050
+ });
1051
+ } catch (e) {
1052
+ fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Failed to load files</div>';
1053
+ }
1054
+ }
1055
+
1056
+ // File editor state
1057
+ let currentEditorFile = '';
1058
+ let editorOriginalContent = '';
1059
+ let isEditing = false;
1060
+
1061
+ async function openFile(path) {
1062
+ try {
1063
+ const resp = await fetch(`/api/read-file?path=${encodeURIComponent(path)}`);
1064
+ const data = await resp.json();
1065
+ if (data.content !== undefined) {
1066
+ sidebar.classList.remove('open');
1067
+ overlay.classList.remove('show');
1068
+ openEditor(path, data.content);
1069
+ } else if (data.error) {
1070
+ addSystemMessage('Error: ' + data.error);
1071
+ }
1072
+ } catch(e) {
1073
+ addSystemMessage('Error reading file');
1074
+ }
1075
+ }
1076
+
1077
+ function openEditor(path, content) {
1078
+ currentEditorFile = path;
1079
+ editorOriginalContent = content;
1080
+ isEditing = false;
1081
+
1082
+ document.getElementById('editorTitle').textContent = path;
1083
+ document.getElementById('editorFileInfo').textContent = (content.length) + ' chars';
1084
+ document.getElementById('editorModified').style.display = 'none';
1085
+
1086
+ const readView = document.getElementById('editorReadView');
1087
+ readView.textContent = content;
1088
+ readView.style.display = 'block';
1089
+
1090
+ const textarea = document.getElementById('editorTextarea');
1091
+ textarea.value = content;
1092
+ textarea.style.display = 'none';
1093
+ textarea.oninput = function() {
1094
+ document.getElementById('editorModified').style.display = 'inline';
1095
+ document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
1096
+ };
1097
+
1098
+ document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
1099
+ document.getElementById('editorSaveBtn').disabled = true;
1100
+
1101
+ document.getElementById('editorOverlay').classList.add('show');
1102
+ }
1103
+
1104
+ function closeEditor() {
1105
+ document.getElementById('editorOverlay').classList.remove('show');
1106
+ currentEditorFile = '';
1107
+ editorOriginalContent = '';
1108
+ }
1109
+
1110
+ function toggleEditorMode() {
1111
+ const readView = document.getElementById('editorReadView');
1112
+ const textarea = document.getElementById('editorTextarea');
1113
+ const modeBtn = document.getElementById('editorModeBtn');
1114
+
1115
+ if (!isEditing) {
1116
+ // Switch to edit mode
1117
+ textarea.value = readView.textContent;
1118
+ textarea.style.display = 'block';
1119
+ readView.style.display = 'none';
1120
+ textarea.focus();
1121
+ isEditing = true;
1122
+ modeBtn.innerHTML = '<i class="fas fa-eye"></i> View';
1123
+ document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
1124
+ } else {
1125
+ // Switch back to view mode (discard unsaved changes)
1126
+ readView.textContent = editorOriginalContent;
1127
+ readView.style.display = 'block';
1128
+ textarea.style.display = 'none';
1129
+ isEditing = false;
1130
+ modeBtn.innerHTML = '<i class="fas fa-edit"></i> Edit';
1131
+ document.getElementById('editorSaveBtn').disabled = true;
1132
+ document.getElementById('editorModified').style.display = 'none';
1133
+ }
1134
+ }
1135
+
1136
+ async function saveEditorFile() {
1137
+ const textarea = document.getElementById('editorTextarea');
1138
+ const content = textarea.value;
1139
+ const path = currentEditorFile;
1140
+
1141
+ if (!path || content === editorOriginalContent) return;
1142
+
1143
+ const saveBtn = document.getElementById('editorSaveBtn');
1144
+ saveBtn.disabled = true;
1145
+ saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
1146
+
1147
+ try {
1148
+ const resp = await fetch('/api/write-file', {
1149
+ method: 'POST',
1150
+ headers: { 'Content-Type': 'application/json' },
1151
+ body: JSON.stringify({ path: path, content: content }),
1152
+ });
1153
+ const data = await resp.json();
1154
+ if (data.status === 'written') {
1155
+ editorOriginalContent = content;
1156
+ isEditing = false;
1157
+ document.getElementById('editorReadView').textContent = content;
1158
+ document.getElementById('editorReadView').style.display = 'block';
1159
+ textarea.style.display = 'none';
1160
+ document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
1161
+ document.getElementById('editorModified').style.display = 'none';
1162
+ document.getElementById('editorFileInfo').textContent = content.length + ' chars (saved)';
1163
+ addSystemMessage('File saved: ' + path);
1164
+ } else {
1165
+ addSystemMessage('Save failed: ' + (data.error || 'unknown error'));
1166
+ }
1167
+ } catch(e) {
1168
+ addSystemMessage('Save error: ' + e.message);
1169
+ } finally {
1170
+ saveBtn.innerHTML = '<i class="fas fa-check"></i> Save';
1171
+ saveBtn.disabled = true;
1172
+ }
1173
+ }
1174
+
1175
+ // Keyboard shortcut: Ctrl+S to save in editor
1176
+ document.addEventListener('keydown', function(e) {
1177
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
1178
+ const overlay = document.getElementById('editorOverlay');
1179
+ if (overlay.classList.contains('show') && isEditing) {
1180
+ e.preventDefault();
1181
+ saveEditorFile();
1182
+ }
1183
+ }
1184
+ // Escape to close editor
1185
+ if (e.key === 'Escape') {
1186
+ const overlay = document.getElementById('editorOverlay');
1187
+ if (overlay.classList.contains('show')) {
1188
+ closeEditor();
1189
+ }
1190
+ }
1191
+ });
1192
+
1193
+ // Load tools + model info
1194
+
1195
+ // Load tools + model info
1196
+ async function loadMeta() {
1197
+ try {
1198
+ const resp = await fetch('/api/tools');
1199
+ const data = await resp.json();
1200
+ if (data.tools) {
1201
+ // Could show tool count somewhere
1202
+ }
1203
+ } catch(e) {}
1204
+ }
1205
+
1206
+ // Theme toggle
1207
+ function toggleTheme() {
1208
+ const isLight = document.body.classList.toggle('light');
1209
+ const icon = document.querySelector('#themeToggle i');
1210
+ icon.className = isLight ? 'fas fa-sun' : 'fas fa-moon';
1211
+ localStorage.setItem('theme', isLight ? 'light' : 'dark');
1212
+ }
1213
+
1214
+ // Load saved theme
1215
+ const savedTheme = localStorage.getItem('theme');
1216
+ if (savedTheme === 'light') {
1217
+ document.body.classList.add('light');
1218
+ document.querySelector('#themeToggle i').className = 'fas fa-sun';
1219
+ }
1220
+
1221
+ // Improve file item icons
1222
+ function getFileIcon(name, isDir) {
1223
+ if (isDir) return 'fa-solid fa-folder';
1224
+ const ext = name.split('.').pop().toLowerCase();
1225
+ const iconMap = {
1226
+ 'js': 'fa-brands fa-js',
1227
+ 'ts': 'fa-brands fa-js',
1228
+ 'jsx': 'fa-brands fa-react',
1229
+ 'tsx': 'fa-brands fa-react',
1230
+ 'py': 'fa-brands fa-python',
1231
+ 'html': 'fa-brands fa-html5',
1232
+ 'css': 'fa-brands fa-css3-alt',
1233
+ 'json': 'fa-solid fa-code',
1234
+ 'md': 'fa-solid fa-file-lines',
1235
+ 'txt': 'fa-solid fa-file-lines',
1236
+ 'yaml': 'fa-solid fa-file',
1237
+ 'yml': 'fa-solid fa-file',
1238
+ 'toml': 'fa-solid fa-file',
1239
+ 'env': 'fa-solid fa-gear',
1240
+ 'gitignore': 'fa-solid fa-eye-slash',
1241
+ 'jpg': 'fa-solid fa-image',
1242
+ 'png': 'fa-solid fa-image',
1243
+ 'svg': 'fa-solid fa-image',
1244
+ 'ico': 'fa-solid fa-image',
1245
+ 'woff2': 'fa-solid fa-font',
1246
+ 'woff': 'fa-solid fa-font',
1247
+ 'ttf': 'fa-solid fa-font',
1248
+ };
1249
+ return iconMap[ext] || 'fa-solid fa-file';
1250
+ }
1251
+
1252
+ // Connect on page load
1253
+ document.addEventListener('DOMContentLoaded', function() {
1254
+ // Register service worker for PWA
1255
+ if ('serviceWorker' in navigator) {
1256
+ navigator.serviceWorker.register('/sw.js').catch(function() {});
1257
+ }
1258
+ connect();
1259
+ loadFiles();
1260
+ loadMeta();
1261
+ inputEl.focus();
1262
+ });
1263
+ </script>
1264
+ <script>
1265
+ // === end ===
1266
+ console.log('LuckyD Code Web UI loaded');
1267
+ </script>
1268
+ </body>
1269
+ </html>
1270
+ <!-- duplicate block removed by fix --><!--
1271
+ highlight: function(code, lang) {
1272
+ if (lang && hljs.getLanguage(lang)) {
1273
+ try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
1274
+ }
1275
+ try { return hljs.highlightAuto(code).value; } catch(e) {}
1276
+ return code;
1277
+ },
1278
+ breaks: true,
1279
+ gfm: true,
1280
+ });
1281
+
1282
+ // State
1283
+ let ws = null;
1284
+ let isProcessing = false;
1285
+ let currentAssistantMsg = null;
1286
+ let currentAssistantDiv = null;
1287
+ let currentToolMsg = null;
1288
+ let reconnectTimer = null;
1289
+ let reconnectAttempts = 0;
1290
+ let lastSendTime = 0;
1291
+ let messageQueue = [];
1292
+ let renderTimeout = null;
1293
+
1294
+ // DOM refs
1295
+ const messagesEl = document.getElementById('messages');
1296
+ const inputEl = document.getElementById('input');
1297
+ const sendBtn = document.getElementById('sendBtn');
1298
+ const voiceBtn = document.getElementById('voiceBtn');
1299
+ const statusDot = document.getElementById('statusDot');
1300
+ const fileList = document.getElementById('fileList');
1301
+ const filePath = document.getElementById('filePath');
1302
+ const sidebar = document.getElementById('sidebar');
1303
+ const overlay = document.getElementById('sidebarOverlay');
1304
+
1305
+ // Auto-resize textarea
1306
+ inputEl.addEventListener('input', function() {
1307
+ this.style.height = 'auto';
1308
+ this.style.height = Math.min(this.scrollHeight, 150) + 'px';
1309
+ });
1310
+
1311
+ // Enter to send, Shift+Enter for newline, Ctrl+Enter also sends
1312
+ inputEl.addEventListener('keydown', function(e) {
1313
+ if ((e.key === 'Enter' && !e.shiftKey) || (e.key === 'Enter' && e.ctrlKey)) {
1314
+ e.preventDefault();
1315
+ sendMessage();
1316
+ }
1317
+ });
1318
+
1319
+ // WebSocket connection with exponential backoff
1320
+ function connect() {
1321
+ if (ws && ws.readyState === WebSocket.OPEN) return;
1322
+
1323
+ statusDot.className = 'status-dot connecting';
1324
+
1325
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1326
+ const url = `${protocol}//${location.host}/ws`;
1327
+
1328
+ ws = new WebSocket(url);
1329
+
1330
+ ws.onopen = function() {
1331
+ statusDot.className = 'status-dot connected';
1332
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
1333
+ reconnectAttempts = 0;
1334
+ removeSystemMessage('disconnected');
1335
+ addSystemMessage('Connected');
1336
+
1337
+ // Flush queued messages
1338
+ while (messageQueue.length > 0) {
1339
+ var q = messageQueue.shift();
1340
+ ws.send(q);
1341
+ }
1342
+ // Re-enable input if stuck
1343
+ if (isProcessing) {
1344
+ isProcessing = false;
1345
+ setInputEnabled(true);
1346
+ removeTyping();
1347
+ }
1348
+ };
1349
+
1350
+ ws.onclose = function() {
1351
+ statusDot.className = 'status-dot disconnected';
1352
+ if (!reconnectTimer) {
1353
+ reconnectAttempts++;
1354
+ var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
1355
+ addSystemMessage('Reconnecting in ' + Math.round(delay / 1000) + 's...');
1356
+ reconnectTimer = setTimeout(connect, delay);
1357
+ }
1358
+ };
1359
+
1360
+ ws.onerror = function() {
1361
+ statusDot.className = 'status-dot disconnected';
1362
+ };
1363
+
1364
+ ws.onmessage = function(event) {
1365
+ try {
1366
+ var msg = JSON.parse(event.data);
1367
+ handleMessage(msg);
1368
+ } catch (e) {
1369
+ console.error('Parse error:', e);
1370
+ }
1371
+ };
1372
+ }
1373
+
1374
+ // Message handler
1375
+ function handleMessage(msg) {
1376
+ switch (msg.type) {
1377
+ case 'text':
1378
+ if (!currentAssistantMsg) {
1379
+ currentAssistantDiv = createMessageDiv('assistant');
1380
+ currentAssistantMsg = {div: currentAssistantDiv, text: ''};
1381
+ removeTyping();
1382
+ }
1383
+ currentAssistantMsg.text += msg.content;
1384
+ currentAssistantMsg.div.innerHTML = marked.parse(currentAssistantMsg.text);
1385
+ // Debounced syntax highlighting
1386
+ if (renderTimeout) clearTimeout(renderTimeout);
1387
+ renderTimeout = setTimeout(function() {
1388
+ currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
1389
+ hljs.highlightElement(b);
1390
+ });
1391
+ }, 200);
1392
+ break;
1393
+
1394
+ case 'tool':
1395
+ // Tool call start
1396
+ removeTyping();
1397
+ if (currentAssistantMsg) {
1398
+ currentAssistantMsg.div.querySelectorAll('pre code').forEach(function(b) {
1399
+ hljs.highlightElement(b);
1400
+ });
1401
+ }
1402
+ currentToolMsg = createMessageDiv('tool-call');
1403
+ currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span> Running...`;
1404
+ break;
1405
+
1406
+ case 'tool_result':
1407
+ if (currentToolMsg) {
1408
+ currentToolMsg.innerHTML = `<span class="tool-name">[Tool: ${msg.name}]</span><div class="tool-result">${escapeHtml(msg.content)}</div>`;
1409
+ }
1410
+ break;
1411
+
1412
+ case 'error':
1413
+ removeTyping();
1414
+ createMessageDiv('error').textContent = msg.content;
1415
+ break;
1416
+
1417
+ case 'done':
1418
+ isProcessing = false;
1419
+ // Speak the response and auto-loop
1420
+ const spokenText = currentAssistantMsg ? currentAssistantMsg.text : '';
1421
+ currentAssistantMsg = null;
1422
+ currentAssistantDiv = null;
1423
+ currentToolMsg = null;
1424
+ setInputEnabled(true);
1425
+ if (renderTimeout) clearTimeout(renderTimeout);
1426
+ renderTimeout = null;
1427
+ if (spokenText) {
1428
+ speakText(spokenText).then(() => startAutoLoop());
1429
+ } else {
1430
+ startAutoLoop();
1431
+ }
1432
+ break;
1433
+
1434
+ case 'cleared':
1435
+ messagesEl.innerHTML = '';
1436
+ break;
1437
+ }
1438
+
1439
+ scrollToBottom();
1440
+ }
1441
+
1442
+ function createMessageDiv(role) {
1443
+ const div = document.createElement('div');
1444
+ div.className = `message ${role}`;
1445
+ messagesEl.appendChild(div);
1446
+ return div;
1447
+ }
1448
+
1449
+ function renderAndScroll(el) {
1450
+ const text = el.textContent || el.innerText;
1451
+ el.innerHTML = marked.parse(text);
1452
+ el.querySelectorAll('pre code').forEach(function(b) {
1453
+ hljs.highlightElement(b);
1454
+ // Add copy button
1455
+ const pre = b.parentElement;
1456
+ if (pre && !pre.querySelector('.copy-btn')) {
1457
+ const btn = document.createElement('button');
1458
+ btn.className = 'copy-btn';
1459
+ btn.innerHTML = '<i class="fas fa-copy"></i>';
1460
+ btn.onclick = function() {
1461
+ navigator.clipboard.writeText(b.textContent).then(function() {
1462
+ btn.innerHTML = '<i class="fas fa-check"></i>';
1463
+ setTimeout(function() { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
1464
+ });
1465
+ };
1466
+ pre.style.position = 'relative';
1467
+ pre.appendChild(btn);
1468
+ }
1469
+ });
1470
+ }
1471
+
1472
+ // Escape HTML for tool results
1473
+ function escapeHtml(text) {
1474
+ const d = document.createElement('div');
1475
+ d.textContent = text;
1476
+ return d.innerHTML;
1477
+ }
1478
+
1479
+ function removeTyping() {
1480
+ const typing = document.querySelector('.typing');
1481
+ if (typing) typing.remove();
1482
+ }
1483
+
1484
+ function scrollToBottom() {
1485
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1486
+ }
1487
+
1488
+ function addSystemMessage(text) {
1489
+ const div = document.createElement('div');
1490
+ div.style.cssText = 'text-align:center;font-size:12px;color:var(--text-dim);padding:4px;';
1491
+ div.textContent = text;
1492
+ messagesEl.appendChild(div);
1493
+ scrollToBottom();
1494
+ }
1495
+
1496
+ // Send message
1497
+ function removeSystemMessage(id) {
1498
+ var el = document.querySelector('.system-msg-' + id);
1499
+ if (el) el.remove();
1500
+ }
1501
+
1502
+ function sendMessage() {
1503
+ var text = inputEl.value.trim();
1504
+ if (!text || isProcessing) return;
1505
+
1506
+ // Rate limiting: max 1 message per 500ms
1507
+ var now = Date.now();
1508
+ if (now - lastSendTime < 500) {
1509
+ addSystemMessage('Please wait...');
1510
+ return;
1511
+ }
1512
+
1513
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
1514
+ addSystemMessage('Not connected. Message queued.', 'disconnected');
1515
+ messageQueue.push(JSON.stringify({ type: 'message', content: text }));
1516
+ connect();
1517
+ return;
1518
+ }
1519
+
1520
+ // Input length limit
1521
+ if (text.length > 10000) {
1522
+ addSystemMessage('Message too long (max 10000 characters)');
1523
+ return;
1524
+ }
1525
+
1526
+ // Add user message
1527
+ var userDiv = createMessageDiv('user');
1528
+ userDiv.textContent = text;
1529
+ scrollToBottom();
1530
+
1531
+ inputEl.value = '';
1532
+ inputEl.style.height = 'auto';
1533
+
1534
+ lastSendTime = now;
1535
+ isProcessing = true;
1536
+ setInputEnabled(false);
1537
+
1538
+ // Add typing indicator
1539
+ var typing = document.createElement('div');
1540
+ typing.className = 'typing';
1541
+ typing.innerHTML = '<span></span><span></span><span></span>';
1542
+ messagesEl.appendChild(typing);
1543
+ scrollToBottom();
1544
+
1545
+ // Reset accumulators
1546
+ currentAssistantMsg = null;
1547
+ currentToolMsg = null;
1548
+
1549
+ ws.send(JSON.stringify({ type: 'message', content: text }));
1550
+ }
1551
+
1552
+ function setInputEnabled(enabled) {
1553
+ inputEl.disabled = !enabled;
1554
+ sendBtn.disabled = !enabled;
1555
+ if (enabled) inputEl.focus();
1556
+ }
1557
+
1558
+ // Clear chat
1559
+ function clearChat() {
1560
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
1561
+ ws.send(JSON.stringify({ type: 'clear' }));
1562
+ }
1563
+
1564
+ // Voice input
1565
+ let recognition = null;
1566
+ let isListening = false;
1567
+
1568
+ function toggleVoice() {
1569
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
1570
+ addSystemMessage('Voice input not supported in this browser');
1571
+ return;
1572
+ }
1573
+
1574
+ if (isListening) {
1575
+ recognition.stop();
1576
+ return;
1577
+ }
1578
+
1579
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1580
+ recognition = new SpeechRecognition();
1581
+ recognition.lang = 'en-US';
1582
+ recognition.interimResults = true;
1583
+ recognition.continuous = true;
1584
+
1585
+ recognition.onstart = function() {
1586
+ isListening = true;
1587
+ voiceBtn.classList.add('listening');
1588
+ voiceBtn.title = 'Listening...';
1589
+ };
1590
+
1591
+ recognition.onresult = function(e) {
1592
+ let transcript = '';
1593
+ for (let i = e.resultIndex; i < e.results.length; i++) {
1594
+ transcript += e.results[i][0].transcript;
1595
+ }
1596
+ inputEl.value = transcript;
1597
+ inputEl.style.height = 'auto';
1598
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 150) + 'px';
1599
+ };
1600
+
1601
+ recognition.onend = function() {
1602
+ isListening = false;
1603
+ voiceBtn.classList.remove('listening');
1604
+ voiceBtn.title = 'Voice input';
1605
+ // Auto-send if we got something
1606
+ if (inputEl.value.trim()) {
1607
+ sendMessage();
1608
+ }
1609
+ };
1610
+
1611
+ recognition.onerror = function(e) {
1612
+ isListening = false;
1613
+ voiceBtn.classList.remove('listening');
1614
+ voiceBtn.title = 'Voice input';
1615
+ if (e.error !== 'no-speech') {
1616
+ addSystemMessage('Voice error: ' + e.error);
1617
+ }
1618
+ };
1619
+
1620
+ recognition.start();
1621
+ }
1622
+
1623
+ // Voice output (TTS)
1624
+ let speakEnabled = true;
1625
+ let autoLoopEnabled = false;
1626
+
1627
+ function toggleSpeak() {
1628
+ speakEnabled = !speakEnabled;
1629
+ const btn = document.getElementById('speakToggle');
1630
+ if (speakEnabled) {
1631
+ btn.innerHTML = '<i class="fas fa-volume-up"></i>';
1632
+ btn.style.color = '';
1633
+ } else {
1634
+ btn.innerHTML = '<i class="fas fa-volume-mute"></i>';
1635
+ btn.style.color = 'var(--danger)';
1636
+ window.speechSynthesis.cancel();
1637
+ }
1638
+ }
1639
+
1640
+ function toggleAutoLoop() {
1641
+ autoLoopEnabled = !autoLoopEnabled;
1642
+ const btn = document.getElementById('autoLoopToggle');
1643
+ if (autoLoopEnabled) {
1644
+ btn.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>';
1645
+ btn.style.color = 'var(--accent)';
1646
+ speakEnabled = true;
1647
+ document.getElementById('speakToggle').innerHTML = '<i class="fas fa-volume-up"></i>';
1648
+ document.getElementById('speakToggle').style.color = '';
1649
+ } else {
1650
+ btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
1651
+ btn.style.color = '';
1652
+ }
1653
+ }
1654
+
1655
+ function speakText(text) {
1656
+ if (!speakEnabled || !window.speechSynthesis) return Promise.resolve();
1657
+ // Clean text for speech: remove markdown, code blocks, etc.
1658
+ let clean = text
1659
+ .replace(/```[\s\S]*?```/g, ' code block omitted ')
1660
+ .replace(/`([^`]+)`/g, ' $1 ')
1661
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
1662
+ .replace(/\*([^*]+)\*/g, '$1')
1663
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
1664
+ .replace(/#{1,6}\s*/g, '')
1665
+ .replace(/[-*]\s/g, '')
1666
+ .replace(/\n{2}/g, '. ')
1667
+ .replace(/\n/g, ' ')
1668
+ .trim();
1669
+
1670
+ if (!clean) return Promise.resolve();
1671
+
1672
+ return new Promise((resolve) => {
1673
+ const utterance = new SpeechSynthesisUtterance(clean);
1674
+ utterance.lang = 'en-US';
1675
+ utterance.rate = 1.1;
1676
+ utterance.pitch = 1.0;
1677
+ utterance.volume = 1.0;
1678
+ utterance.onend = resolve;
1679
+ utterance.onerror = resolve;
1680
+ window.speechSynthesis.speak(utterance);
1681
+ });
1682
+ }
1683
+
1684
+ function startAutoLoop() {
1685
+ if (!autoLoopEnabled) return;
1686
+ // Small delay then re-activate mic
1687
+ setTimeout(() => {
1688
+ if (autoLoopEnabled && !isListening && document.visibilityState === 'visible') {
1689
+ toggleVoice();
1690
+ }
1691
+ }, 800);
1692
+ }
1693
+
1694
+ // Sidebar toggle
1695
+ function toggleSidebar() {
1696
+ sidebar.classList.toggle('open');
1697
+ overlay.classList.toggle('show');
1698
+ }
1699
+
1700
+ overlay.addEventListener('click', function() {
1701
+ sidebar.classList.remove('open');
1702
+ overlay.classList.remove('show');
1703
+ });
1704
+
1705
+ // File browser
1706
+ async function loadFiles(dir) {
1707
+ if (!dir) dir = '.';
1708
+ try {
1709
+ fileList.innerHTML = '<div style="color:var(--text-dim);padding:16px;text-align:center;font-size:13px;">Loading...</div>';
1710
+ const resp = await fetch(`/api/files?dir=${encodeURIComponent(dir)}`);
1711
+ const data = await resp.json();
1712
+
1713
+ if (data.error) {
1714
+ fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Error: ' + data.error + '</div>';
1715
+ return;
1716
+ }
1717
+
1718
+ filePath.textContent = data.path;
1719
+ fileList.innerHTML = '';
1720
+
1721
+ // Parent dir link
1722
+ if (dir !== '.') {
1723
+ const parent = document.createElement('div');
1724
+ parent.className = 'file-item dir';
1725
+ parent.innerHTML = '<span class="icon">&#128193;</span> ..';
1726
+ parent.onclick = function() {
1727
+ const p = dir.split('/').slice(0, -1).join('/') || '.';
1728
+ loadFiles(p);
1729
+ };
1730
+ fileList.appendChild(parent);
1731
+ }
1732
+
1733
+ data.files.forEach(function(f) {
1734
+ const item = document.createElement('div');
1735
+ item.className = 'file-item' + (f.is_dir ? ' dir' : '');
1736
+ const iconClass = getFileIcon(f.name, f.is_dir);
1737
+ item.innerHTML = '<span class="icon"><i class="' + iconClass + '"></i></span> ' + f.name;
1738
+ if (!f.is_dir) {
1739
+ const size = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
1740
+ item.title = size;
1741
+ }
1742
+ item.onclick = function() {
1743
+ if (f.is_dir) {
1744
+ loadFiles(dir + '/' + f.name);
1745
+ } else {
1746
+ openFile(dir + '/' + f.name);
1747
+ }
1748
+ };
1749
+ fileList.appendChild(item);
1750
+ });
1751
+ } catch (e) {
1752
+ fileList.innerHTML = '<div style="color:var(--danger);padding:16px;">Failed to load files</div>';
1753
+ }
1754
+ }
1755
+
1756
+ // File editor state
1757
+ let currentEditorFile = '';
1758
+ let editorOriginalContent = '';
1759
+ let isEditing = false;
1760
+
1761
+ async function openFile(path) {
1762
+ try {
1763
+ const resp = await fetch(`/api/read-file?path=${encodeURIComponent(path)}`);
1764
+ const data = await resp.json();
1765
+ if (data.content !== undefined) {
1766
+ sidebar.classList.remove('open');
1767
+ overlay.classList.remove('show');
1768
+ openEditor(path, data.content);
1769
+ } else if (data.error) {
1770
+ addSystemMessage('Error: ' + data.error);
1771
+ }
1772
+ } catch(e) {
1773
+ addSystemMessage('Error reading file');
1774
+ }
1775
+ }
1776
+
1777
+ function openEditor(path, content) {
1778
+ currentEditorFile = path;
1779
+ editorOriginalContent = content;
1780
+ isEditing = false;
1781
+
1782
+ document.getElementById('editorTitle').textContent = path;
1783
+ document.getElementById('editorFileInfo').textContent = (content.length) + ' chars';
1784
+ document.getElementById('editorModified').style.display = 'none';
1785
+
1786
+ const readView = document.getElementById('editorReadView');
1787
+ readView.textContent = content;
1788
+ readView.style.display = 'block';
1789
+
1790
+ const textarea = document.getElementById('editorTextarea');
1791
+ textarea.value = content;
1792
+ textarea.style.display = 'none';
1793
+ textarea.oninput = function() {
1794
+ document.getElementById('editorModified').style.display = 'inline';
1795
+ document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
1796
+ };
1797
+
1798
+ document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
1799
+ document.getElementById('editorSaveBtn').disabled = true;
1800
+
1801
+ document.getElementById('editorOverlay').classList.add('show');
1802
+ }
1803
+
1804
+ function closeEditor() {
1805
+ document.getElementById('editorOverlay').classList.remove('show');
1806
+ currentEditorFile = '';
1807
+ editorOriginalContent = '';
1808
+ }
1809
+
1810
+ function toggleEditorMode() {
1811
+ const readView = document.getElementById('editorReadView');
1812
+ const textarea = document.getElementById('editorTextarea');
1813
+ const modeBtn = document.getElementById('editorModeBtn');
1814
+
1815
+ if (!isEditing) {
1816
+ // Switch to edit mode
1817
+ textarea.value = readView.textContent;
1818
+ textarea.style.display = 'block';
1819
+ readView.style.display = 'none';
1820
+ textarea.focus();
1821
+ isEditing = true;
1822
+ modeBtn.innerHTML = '<i class="fas fa-eye"></i> View';
1823
+ document.getElementById('editorSaveBtn').disabled = (textarea.value === editorOriginalContent);
1824
+ } else {
1825
+ // Switch back to view mode (discard unsaved changes)
1826
+ readView.textContent = editorOriginalContent;
1827
+ readView.style.display = 'block';
1828
+ textarea.style.display = 'none';
1829
+ isEditing = false;
1830
+ modeBtn.innerHTML = '<i class="fas fa-edit"></i> Edit';
1831
+ document.getElementById('editorSaveBtn').disabled = true;
1832
+ document.getElementById('editorModified').style.display = 'none';
1833
+ }
1834
+ }
1835
+
1836
+ async function saveEditorFile() {
1837
+ const textarea = document.getElementById('editorTextarea');
1838
+ const content = textarea.value;
1839
+ const path = currentEditorFile;
1840
+
1841
+ if (!path || content === editorOriginalContent) return;
1842
+
1843
+ const saveBtn = document.getElementById('editorSaveBtn');
1844
+ saveBtn.disabled = true;
1845
+ saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Saving...';
1846
+
1847
+ try {
1848
+ const resp = await fetch('/api/write-file', {
1849
+ method: 'POST',
1850
+ headers: { 'Content-Type': 'application/json' },
1851
+ body: JSON.stringify({ path: path, content: content }),
1852
+ });
1853
+ const data = await resp.json();
1854
+ if (data.status === 'written') {
1855
+ editorOriginalContent = content;
1856
+ isEditing = false;
1857
+ document.getElementById('editorReadView').textContent = content;
1858
+ document.getElementById('editorReadView').style.display = 'block';
1859
+ textarea.style.display = 'none';
1860
+ document.getElementById('editorModeBtn').innerHTML = '<i class="fas fa-edit"></i> Edit';
1861
+ document.getElementById('editorModified').style.display = 'none';
1862
+ document.getElementById('editorFileInfo').textContent = content.length + ' chars (saved)';
1863
+ addSystemMessage('File saved: ' + path);
1864
+ } else {
1865
+ addSystemMessage('Save failed: ' + (data.error || 'unknown error'));
1866
+ }
1867
+ } catch(e) {
1868
+ addSystemMessage('Save error: ' + e.message);
1869
+ } finally {
1870
+ saveBtn.innerHTML = '<i class="fas fa-check"></i> Save';
1871
+ saveBtn.disabled = true;
1872
+ }
1873
+ }
1874
+
1875
+ // Keyboard shortcut: Ctrl+S to save in editor
1876
+ document.addEventListener('keydown', function(e) {
1877
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
1878
+ const overlay = document.getElementById('editorOverlay');
1879
+ if (overlay.classList.contains('show') && isEditing) {
1880
+ e.preventDefault();
1881
+ saveEditorFile();
1882
+ }
1883
+ }
1884
+ // Escape to close editor
1885
+ if (e.key === 'Escape') {
1886
+ const overlay = document.getElementById('editorOverlay');
1887
+ if (overlay.classList.contains('show')) {
1888
+ closeEditor();
1889
+ }
1890
+ }
1891
+ });
1892
+
1893
+ // Load tools + model info
1894
+
1895
+ // Load tools + model info
1896
+ async function loadMeta() {
1897
+ try {
1898
+ const resp = await fetch('/api/tools');
1899
+ const data = await resp.json();
1900
+ if (data.tools) {
1901
+ // Could show tool count somewhere
1902
+ }
1903
+ } catch(e) {}
1904
+ }
1905
+
1906
+ // Theme toggle
1907
+ function toggleTheme() {
1908
+ const isLight = document.body.classList.toggle('light');
1909
+ const icon = document.querySelector('#themeToggle i');
1910
+ icon.className = isLight ? 'fas fa-sun' : 'fas fa-moon';
1911
+ localStorage.setItem('theme', isLight ? 'light' : 'dark');
1912
+ }
1913
+
1914
+ // Load saved theme
1915
+ const savedTheme = localStorage.getItem('theme');
1916
+ if (savedTheme === 'light') {
1917
+ document.body.classList.add('light');
1918
+ document.querySelector('#themeToggle i').className = 'fas fa-sun';
1919
+ }
1920
+
1921
+ // Improve file item icons
1922
+ function getFileIcon(name, isDir) {
1923
+ if (isDir) return 'fa-solid fa-folder';
1924
+ const ext = name.split('.').pop().toLowerCase();
1925
+ const iconMap = {
1926
+ 'js': 'fa-brands fa-js',
1927
+ 'ts': 'fa-brands fa-js',
1928
+ 'jsx': 'fa-brands fa-react',
1929
+ 'tsx': 'fa-brands fa-react',
1930
+ 'py': 'fa-brands fa-python',
1931
+ 'html': 'fa-brands fa-html5',
1932
+ 'css': 'fa-brands fa-css3-alt',
1933
+ 'json': 'fa-solid fa-code',
1934
+ 'md': 'fa-solid fa-file-lines',
1935
+ 'txt': 'fa-solid fa-file-lines',
1936
+ 'yaml': 'fa-solid fa-file',
1937
+ 'yml': 'fa-solid fa-file',
1938
+ 'toml': 'fa-solid fa-file',
1939
+ 'env': 'fa-solid fa-gear',
1940
+ 'gitignore': 'fa-solid fa-eye-slash',
1941
+ 'jpg': 'fa-solid fa-image',
1942
+ 'png': 'fa-solid fa-image',
1943
+ 'svg': 'fa-solid fa-image',
1944
+ 'ico': 'fa-solid fa-image',
1945
+ 'woff2': 'fa-solid fa-font',
1946
+ 'woff': 'fa-solid fa-font',
1947
+ 'ttf': 'fa-solid fa-font',
1948
+ };
1949
+ return iconMap[ext] || 'fa-solid fa-file';
1950
+ }
1951
+
1952
+ // Connect on page load
1953
+ document.addEventListener('DOMContentLoaded', function() {
1954
+ // Register service worker for PWA
1955
+ if ('serviceWorker' in navigator) {
1956
+ navigator.serviceWorker.register('/sw.js').catch(function() {});
1957
+ }
1958
+ connect();
1959
+ loadFiles();
1960
+ loadMeta();
1961
+ inputEl.focus();
1962
+ });
1963
+ </script>
1964
+ </body>
1965
+ </html>