daedalus-cli 3.72.0 → 3.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,15 +25,711 @@ if (clearBtn) {
25
25
  });
26
26
  }
27
27
 
28
+ const chatMessages = document.getElementById('chat-messages');
29
+ const chatForm = document.getElementById('chat-form');
30
+ const chatInput = document.getElementById('chat-input');
31
+ const chatStatusBadge = document.getElementById('chat-status-badge');
32
+
33
+ function highlightSyntax(code) {
34
+ const escaped = code
35
+ .replace(/&/g, '&')
36
+ .replace(/</g, '&lt;')
37
+ .replace(/>/g, '&gt;');
38
+
39
+ return escaped
40
+ .replace(/\b(const|let|var|function|return|if|else|for|while|import|export|from|async|await|class|type|interface|def|self|pub|fn|struct|match|case|switch|try|catch|throw|finally|yield)\b/g, '<span class="hl-keyword">$1</span>')
41
+ .replace(/\b(true|false|null|undefined|None|True|False|nil)\b/g, '<span class="hl-boolean">$1</span>')
42
+ .replace(/\b(\d+(\.\d+)?)\b/g, '<span class="hl-number">$1</span>')
43
+ .replace(/(["'`])(.*?)\1/g, '<span class="hl-string">$1$2$1</span>')
44
+ .replace(/(\/\/.*|\/\*[\s\S]*?\*\/|#.*)/g, '<span class="hl-comment">$1</span>');
45
+ }
46
+
47
+ function renderMarkdown(text) {
48
+ let html = '';
49
+ if (typeof window.marked !== 'undefined' && typeof window.marked.parse === 'function') {
50
+ try {
51
+ html = window.marked.parse(text, { breaks: true, gfm: true });
52
+ } catch {
53
+ html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
54
+ }
55
+ } else {
56
+ html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
57
+ }
58
+
59
+ const tempDiv = document.createElement('div');
60
+ tempDiv.innerHTML = html;
61
+
62
+ tempDiv.querySelectorAll('pre').forEach(pre => {
63
+ const code = pre.querySelector('code');
64
+ const rawCode = code ? (code.textContent || '') : (pre.textContent || '');
65
+ const langMatch = code?.className?.match(/language-([a-zA-Z0-9_-]+)/);
66
+ const lang = langMatch ? langMatch[1] : 'CODE';
67
+
68
+ const wrapper = document.createElement('div');
69
+ wrapper.className = 'code-block-wrapper';
70
+
71
+ const header = document.createElement('div');
72
+ header.className = 'code-block-header';
73
+
74
+ const langSpan = document.createElement('span');
75
+ langSpan.className = 'code-lang';
76
+ langSpan.textContent = lang;
77
+
78
+ const copyBtn = document.createElement('button');
79
+ copyBtn.type = 'button';
80
+ copyBtn.className = 'code-copy-btn';
81
+ copyBtn.textContent = 'COPY CODE';
82
+ copyBtn.addEventListener('click', (e) => {
83
+ e.stopPropagation();
84
+ navigator.clipboard.writeText(rawCode).then(() => {
85
+ copyBtn.textContent = 'COPIED!';
86
+ copyBtn.classList.add('copied');
87
+ setTimeout(() => {
88
+ copyBtn.textContent = 'COPY CODE';
89
+ copyBtn.classList.remove('copied');
90
+ }, 1800);
91
+ });
92
+ });
93
+
94
+ header.appendChild(langSpan);
95
+ header.appendChild(copyBtn);
96
+
97
+ if (code) {
98
+ code.innerHTML = highlightSyntax(rawCode);
99
+ }
100
+
101
+ pre.parentNode.insertBefore(wrapper, pre);
102
+ wrapper.appendChild(header);
103
+ wrapper.appendChild(pre);
104
+ });
105
+
106
+ return tempDiv.innerHTML;
107
+ }
108
+
109
+ function addChatMessage(role, text, roleBadge = null, imageBase64 = null, timestamp = null) {
110
+ if (!chatMessages) return;
111
+ const msgEl = document.createElement('div');
112
+ msgEl.className = `chat-msg ${role}`;
113
+
114
+ const header = document.createElement('div');
115
+ header.className = 'msg-header';
116
+
117
+ const sender = document.createElement('span');
118
+ sender.className = 'sender';
119
+ sender.textContent = role === 'user' ? 'YOU' : 'DAEDALUS';
120
+ header.appendChild(sender);
121
+
122
+ if (roleBadge) {
123
+ const badge = document.createElement('span');
124
+ badge.className = 'badge-role';
125
+ badge.textContent = roleBadge;
126
+ header.appendChild(badge);
127
+ }
128
+
129
+ const timeStr = new Date(timestamp || Date.now()).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
130
+ const timeSpan = document.createElement('span');
131
+ timeSpan.className = 'msg-timestamp';
132
+ timeSpan.textContent = timeStr;
133
+ header.appendChild(timeSpan);
134
+
135
+ if (role === 'assistant') {
136
+ const copyBtn = document.createElement('button');
137
+ copyBtn.className = 'copy-btn';
138
+ copyBtn.textContent = 'COPY';
139
+ copyBtn.addEventListener('click', () => {
140
+ const raw = body.dataset.raw || body.textContent || '';
141
+ navigator.clipboard.writeText(raw).then(() => {
142
+ copyBtn.textContent = 'COPIED!';
143
+ copyBtn.classList.add('copied');
144
+ setTimeout(() => {
145
+ copyBtn.textContent = 'COPY';
146
+ copyBtn.classList.remove('copied');
147
+ }, 1800);
148
+ });
149
+ });
150
+ header.appendChild(copyBtn);
151
+ }
152
+
153
+ const body = document.createElement('div');
154
+ body.className = 'msg-body';
155
+ body.dataset.raw = text;
156
+
157
+ if (imageBase64) {
158
+ const img = document.createElement('img');
159
+ img.className = 'chat-msg-img';
160
+ img.src = imageBase64.startsWith('data:') ? imageBase64 : `data:image/png;base64,${imageBase64}`;
161
+ img.alt = 'Uploaded image';
162
+ body.appendChild(img);
163
+ }
164
+
165
+ if (role === 'assistant') {
166
+ const textContainer = document.createElement('div');
167
+ textContainer.innerHTML = renderMarkdown(text);
168
+ body.appendChild(textContainer);
169
+ } else {
170
+ const textSpan = document.createElement('div');
171
+ textSpan.textContent = text;
172
+ body.appendChild(textSpan);
173
+ }
174
+
175
+ msgEl.appendChild(header);
176
+ msgEl.appendChild(body);
177
+ chatMessages.appendChild(msgEl);
178
+ chatMessages.scrollTop = chatMessages.scrollHeight;
179
+ return body;
180
+ }
181
+
182
+ let activeAssistantBody = null;
183
+ let thinkingEl = null;
184
+ let currentRunningToolEl = null;
185
+
186
+ function renderToolStart(toolName) {
187
+ if (!chatMessages) return null;
188
+ const accordion = document.createElement('div');
189
+ accordion.className = 'tool-accordion running';
190
+
191
+ const header = document.createElement('div');
192
+ header.className = 'tool-accordion-header';
193
+
194
+ const left = document.createElement('div');
195
+ left.className = 'tool-accordion-left';
196
+
197
+ const icon = document.createElement('span');
198
+ icon.className = 'tool-accordion-icon';
199
+ icon.textContent = '⚡';
200
+
201
+ const name = document.createElement('span');
202
+ name.className = 'tool-accordion-name';
203
+ name.textContent = toolName;
204
+
205
+ left.appendChild(icon);
206
+ left.appendChild(name);
207
+
208
+ const right = document.createElement('div');
209
+ right.style.display = 'flex';
210
+ right.style.alignItems = 'center';
211
+
212
+ const status = document.createElement('span');
213
+ status.className = 'tool-accordion-status running';
214
+ status.textContent = 'RUNNING';
215
+
216
+ const chevron = document.createElement('span');
217
+ chevron.className = 'tool-accordion-chevron';
218
+ chevron.textContent = '▶';
219
+
220
+ right.appendChild(status);
221
+ right.appendChild(chevron);
222
+
223
+ header.appendChild(left);
224
+ header.appendChild(right);
225
+
226
+ const body = document.createElement('div');
227
+ body.className = 'tool-accordion-body hidden';
228
+ body.textContent = `Executing tool: ${toolName}...`;
229
+
230
+ header.addEventListener('click', () => {
231
+ accordion.classList.toggle('open');
232
+ body.classList.toggle('hidden');
233
+ });
234
+
235
+ accordion.appendChild(header);
236
+ accordion.appendChild(body);
237
+ chatMessages.appendChild(accordion);
238
+ chatMessages.scrollTop = chatMessages.scrollHeight;
239
+ currentRunningToolEl = accordion;
240
+ return accordion;
241
+ }
242
+
243
+ function renderToolResult(toolName, resultSummary) {
244
+ let accordion = currentRunningToolEl;
245
+ if (!accordion) {
246
+ accordion = renderToolStart(toolName);
247
+ }
248
+ if (accordion) {
249
+ accordion.classList.remove('running');
250
+ accordion.classList.add('completed');
251
+
252
+ const status = accordion.querySelector('.tool-accordion-status');
253
+ if (status) {
254
+ status.className = 'tool-accordion-status completed';
255
+ status.textContent = 'COMPLETED';
256
+ }
257
+
258
+ const body = accordion.querySelector('.tool-accordion-body');
259
+ if (body) {
260
+ body.textContent = resultSummary || `Tool ${toolName} execution finished.`;
261
+ }
262
+ }
263
+ currentRunningToolEl = null;
264
+ }
265
+
266
+ function showThinkingSpinner() {
267
+ removeThinkingSpinner();
268
+ thinkingEl = document.createElement('div');
269
+ thinkingEl.className = 'chat-msg thinking';
270
+ thinkingEl.innerHTML = `
271
+ <div class="thinking-dots"><span></span><span></span><span></span></div>
272
+ <span class="thinking-text">Daedalus is consulting the labyrinth...</span>
273
+ `;
274
+ chatMessages.appendChild(thinkingEl);
275
+ chatMessages.scrollTop = chatMessages.scrollHeight;
276
+ }
277
+
278
+ function removeThinkingSpinner() {
279
+ if (thinkingEl && thinkingEl.parentNode) {
280
+ thinkingEl.parentNode.removeChild(thinkingEl);
281
+ }
282
+ thinkingEl = null;
283
+ }
284
+
285
+ function updateThinkingSpinner(text) {
286
+ if (thinkingEl && chatMessages) {
287
+ const label = thinkingEl.querySelector('.thinking-text');
288
+ if (label) label.textContent = text;
289
+ chatMessages.appendChild(thinkingEl);
290
+ chatMessages.scrollTop = chatMessages.scrollHeight;
291
+ }
292
+ }
293
+
294
+ const fileInput = document.getElementById('file-input');
295
+ const attachBtn = document.getElementById('attach-btn');
296
+ const attachmentPreviewEl = document.getElementById('attachment-preview');
297
+ const chatPanel = document.querySelector('.chat-panel');
298
+
299
+ let currentAttachment = null;
300
+
301
+ function setAttachment(attachment) {
302
+ currentAttachment = attachment;
303
+ if (!attachmentPreviewEl) return;
304
+ attachmentPreviewEl.innerHTML = '';
305
+ if (!attachment) {
306
+ attachmentPreviewEl.classList.add('hidden');
307
+ return;
308
+ }
309
+ attachmentPreviewEl.classList.remove('hidden');
310
+
311
+ const chip = document.createElement('div');
312
+ chip.className = 'attachment-chip';
313
+
314
+ if (attachment.isImage && attachment.dataUrl) {
315
+ const thumb = document.createElement('img');
316
+ thumb.className = 'attachment-thumb';
317
+ thumb.src = attachment.dataUrl;
318
+ chip.appendChild(thumb);
319
+ }
320
+
321
+ const name = document.createElement('span');
322
+ name.className = 'attachment-name';
323
+ name.textContent = attachment.name;
324
+ chip.appendChild(name);
325
+
326
+ const removeBtn = document.createElement('button');
327
+ removeBtn.className = 'attachment-remove';
328
+ removeBtn.textContent = '×';
329
+ removeBtn.title = 'Remove attachment';
330
+ removeBtn.addEventListener('click', () => setAttachment(null));
331
+ chip.appendChild(removeBtn);
332
+
333
+ attachmentPreviewEl.appendChild(chip);
334
+ }
335
+
336
+ function processFile(file) {
337
+ if (!file) return;
338
+ const isImage = file.type.startsWith('image/');
339
+ const reader = new FileReader();
340
+ if (isImage) {
341
+ reader.onload = (e) => {
342
+ const dataUrl = e.target.result;
343
+ const base64 = typeof dataUrl === 'string' ? dataUrl.split(',')[1] : '';
344
+ setAttachment({
345
+ name: file.name || 'image.png',
346
+ isImage: true,
347
+ dataUrl: typeof dataUrl === 'string' ? dataUrl : '',
348
+ base64,
349
+ });
350
+ };
351
+ reader.readAsDataURL(file);
352
+ } else {
353
+ reader.onload = (e) => {
354
+ const textContent = typeof e.target.result === 'string' ? e.target.result : '';
355
+ setAttachment({
356
+ name: file.name,
357
+ isImage: false,
358
+ textContent,
359
+ });
360
+ };
361
+ reader.readAsText(file);
362
+ }
363
+ }
364
+
365
+ if (attachBtn && fileInput) {
366
+ attachBtn.addEventListener('click', () => fileInput.click());
367
+ fileInput.addEventListener('change', (e) => {
368
+ if (e.target.files?.[0]) {
369
+ processFile(e.target.files[0]);
370
+ fileInput.value = '';
371
+ }
372
+ });
373
+ }
374
+
375
+ document.addEventListener('paste', (e) => {
376
+ if (!e.clipboardData?.items) return;
377
+ for (const item of e.clipboardData.items) {
378
+ if (item.type.startsWith('image/')) {
379
+ const file = item.getAsFile();
380
+ if (file) {
381
+ processFile(file);
382
+ break;
383
+ }
384
+ }
385
+ }
386
+ });
387
+
388
+ if (chatPanel) {
389
+ chatPanel.addEventListener('dragover', (e) => {
390
+ e.preventDefault();
391
+ chatPanel.classList.add('drag-over');
392
+ });
393
+ chatPanel.addEventListener('dragleave', (e) => {
394
+ if (!chatPanel.contains(e.relatedTarget)) {
395
+ chatPanel.classList.remove('drag-over');
396
+ }
397
+ });
398
+ chatPanel.addEventListener('drop', (e) => {
399
+ e.preventDefault();
400
+ chatPanel.classList.remove('drag-over');
401
+ if (e.dataTransfer?.files?.[0]) {
402
+ processFile(e.dataTransfer.files[0]);
403
+ }
404
+ });
405
+ }
406
+
407
+ const sentHistory = [];
408
+ let sentIndex = -1;
409
+
410
+ function autoResizeInput() {
411
+ if (!chatInput) return;
412
+ chatInput.style.height = 'auto';
413
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 180) + 'px';
414
+ }
415
+
416
+ if (chatForm && chatInput) {
417
+ chatInput.addEventListener('input', autoResizeInput);
418
+
419
+ chatInput.addEventListener('keydown', (e) => {
420
+ if (e.key === 'Enter' && !e.shiftKey) {
421
+ e.preventDefault();
422
+ chatForm.dispatchEvent(new Event('submit', { cancelable: true }));
423
+ return;
424
+ }
425
+
426
+ if (e.key === 'Enter' && e.shiftKey) {
427
+ requestAnimationFrame(autoResizeInput);
428
+ return;
429
+ }
430
+
431
+ if (e.key === 'Escape') {
432
+ chatInput.value = '';
433
+ chatInput.style.height = 'auto';
434
+ sentIndex = -1;
435
+ return;
436
+ }
437
+
438
+ if (e.key === 'ArrowUp') {
439
+ if (chatInput.selectionStart === 0 && chatInput.selectionEnd === 0 && sentHistory.length > 0) {
440
+ if (sentIndex === -1) {
441
+ sentIndex = sentHistory.length - 1;
442
+ } else if (sentIndex > 0) {
443
+ sentIndex--;
444
+ }
445
+ chatInput.value = sentHistory[sentIndex] || '';
446
+ autoResizeInput();
447
+ e.preventDefault();
448
+ }
449
+ } else if (e.key === 'ArrowDown') {
450
+ if (sentIndex !== -1) {
451
+ if (sentIndex < sentHistory.length - 1) {
452
+ sentIndex++;
453
+ chatInput.value = sentHistory[sentIndex] || '';
454
+ } else {
455
+ sentIndex = -1;
456
+ chatInput.value = '';
457
+ }
458
+ autoResizeInput();
459
+ e.preventDefault();
460
+ }
461
+ }
462
+ });
463
+
464
+ chatForm.addEventListener('submit', async (e) => {
465
+ e.preventDefault();
466
+ const text = chatInput.value.trim();
467
+ if (!text && !currentAttachment) return;
468
+
469
+ let effectiveText = text;
470
+ if (currentAttachment && !currentAttachment.isImage && currentAttachment.textContent) {
471
+ effectiveText = effectiveText
472
+ ? `${effectiveText}\n\n[Attached file: ${currentAttachment.name}]\n\`\`\`\n${currentAttachment.textContent}\n\`\`\``
473
+ : `[Attached file: ${currentAttachment.name}]\n\`\`\`\n${currentAttachment.textContent}\n\`\`\``;
474
+ }
475
+
476
+ const imageBase64 = currentAttachment?.isImage ? currentAttachment.base64 : undefined;
477
+ const attachmentSnap = currentAttachment;
478
+ setAttachment(null);
479
+
480
+ sentHistory.push(text || (attachmentSnap ? `[Attached ${attachmentSnap.name}]` : ''));
481
+ sentIndex = -1;
482
+ chatInput.value = '';
483
+ chatInput.style.height = 'auto';
484
+ if (chatStatusBadge) chatStatusBadge.textContent = 'THINKING...';
485
+
486
+ addChatMessage('user', effectiveText || 'Attached image', null, imageBase64);
487
+ activeAssistantBody = null;
488
+ showThinkingSpinner();
489
+
490
+ try {
491
+ const res = await fetch('/api/chat', {
492
+ method: 'POST',
493
+ headers: { 'Content-Type': 'application/json' },
494
+ body: JSON.stringify({ message: effectiveText, imageBase64 })
495
+ });
496
+
497
+ if (!res.ok) {
498
+ removeThinkingSpinner();
499
+ const errData = await res.json().catch(() => ({}));
500
+ addChatMessage('error', `Error: ${errData.error || res.statusText}`);
501
+ if (chatStatusBadge) chatStatusBadge.textContent = 'ERROR';
502
+ }
503
+ } catch (err) {
504
+ removeThinkingSpinner();
505
+ addChatMessage('error', `Network Error: ${err.message}`);
506
+ if (chatStatusBadge) chatStatusBadge.textContent = 'ERROR';
507
+ }
508
+ });
509
+ }
510
+
511
+ // Sidebar Tab Switching
512
+ document.querySelectorAll('.sidebar-tab').forEach(tab => {
513
+ tab.addEventListener('click', () => {
514
+ document.querySelectorAll('.sidebar-tab').forEach(t => t.classList.remove('active'));
515
+ document.querySelectorAll('.sidebar-tab-content').forEach(c => c.classList.add('hidden'));
516
+ tab.classList.add('active');
517
+ const tabName = tab.getAttribute('data-tab');
518
+ const target = document.getElementById('tab-' + tabName);
519
+ if (target) target.classList.remove('hidden');
520
+ if (tabName === 'files') loadFileTree();
521
+ if (tabName === 'context') loadContextFiles();
522
+ if (tabName === 'sessions') loadSessions();
523
+ });
524
+ });
525
+
526
+ // Cheat Sheet Click-to-Insert Handler
527
+ document.querySelectorAll('.cmd-chip').forEach(chip => {
528
+ chip.addEventListener('click', () => {
529
+ const cmd = chip.getAttribute('data-cmd');
530
+ if (!cmd || !chatInput) return;
531
+ chatInput.value = cmd;
532
+ chatInput.focus();
533
+ });
534
+ });
535
+
536
+ // File Tree
537
+ const fileTreeEl = document.getElementById('file-tree');
538
+ const fileTreeCwdEl = document.getElementById('file-tree-cwd');
539
+ const fileTreeRefreshBtn = document.getElementById('file-tree-refresh');
540
+ let fileTreeLoaded = false;
541
+
542
+ function fileIcon(node) {
543
+ if (node.type === 'dir') return '▶';
544
+ const ext = node.name.split('.').pop() || '';
545
+ const icons = { ts: '𝑇', js: '𝑱', json: '{}', md: '≡', css: '⌗', html: '◇', txt: '·', yml: '⚙', yaml: '⚙', sh: '$', py: '𝑃', go: 'G', rs: '⚙' };
546
+ return icons[ext] || '·';
547
+ }
548
+
549
+ function renderTreeNodes(nodes, depth) {
550
+ const ul = document.createElement('ul');
551
+ ul.className = 'tree-list';
552
+ for (const node of nodes) {
553
+ const li = document.createElement('li');
554
+ li.className = 'tree-node';
555
+ li.style.paddingLeft = (depth * 12) + 'px';
556
+
557
+ if (node.type === 'dir') {
558
+ const toggle = document.createElement('span');
559
+ toggle.className = 'tree-icon tree-dir-icon';
560
+ toggle.textContent = fileIcon(node);
561
+
562
+ const label = document.createElement('span');
563
+ label.className = 'tree-label tree-dir';
564
+ label.textContent = node.name;
565
+
566
+ let childrenEl = null;
567
+ let expanded = false;
568
+
569
+ const expand = () => {
570
+ expanded = !expanded;
571
+ toggle.classList.toggle('expanded', expanded);
572
+ if (expanded && !childrenEl && node.children?.length) {
573
+ childrenEl = renderTreeNodes(node.children, 0);
574
+ li.appendChild(childrenEl);
575
+ }
576
+ if (childrenEl) childrenEl.classList.toggle('hidden', !expanded);
577
+ };
578
+
579
+ toggle.addEventListener('click', expand);
580
+ label.addEventListener('click', expand);
581
+ li.appendChild(toggle);
582
+ li.appendChild(label);
583
+ } else {
584
+ const icon = document.createElement('span');
585
+ icon.className = 'tree-icon tree-file-icon';
586
+ icon.textContent = fileIcon(node);
587
+
588
+ const label = document.createElement('span');
589
+ label.className = 'tree-label tree-file';
590
+ label.textContent = node.name;
591
+ label.title = node.path;
592
+
593
+ label.addEventListener('click', () => {
594
+ if (!chatInput) return;
595
+ const current = chatInput.value;
596
+ chatInput.value = current ? current + ' ' + node.path : node.path;
597
+ chatInput.focus();
598
+ });
599
+
600
+ li.appendChild(icon);
601
+ li.appendChild(label);
602
+ }
603
+
604
+ ul.appendChild(li);
605
+ }
606
+ return ul;
607
+ }
608
+
609
+ async function loadFileTree() {
610
+ if (!fileTreeEl) return;
611
+ if (fileTreeLoaded) return;
612
+ fileTreeEl.innerHTML = '<div class="file-tree-loading">Loading...</div>';
613
+ try {
614
+ const res = await fetch('/api/files');
615
+ if (!res.ok) throw new Error('Failed to load');
616
+ const data = await res.json();
617
+ if (fileTreeCwdEl) fileTreeCwdEl.textContent = data.cwd || '—';
618
+ fileTreeEl.innerHTML = '';
619
+ if (!data.tree?.length) {
620
+ fileTreeEl.innerHTML = '<div class="file-tree-loading">No files found.</div>';
621
+ return;
622
+ }
623
+ fileTreeEl.appendChild(renderTreeNodes(data.tree, 0));
624
+ fileTreeLoaded = true;
625
+ } catch {
626
+ fileTreeEl.innerHTML = '<div class="file-tree-loading">Failed to load tree.</div>';
627
+ }
628
+ }
629
+
630
+ if (fileTreeRefreshBtn) {
631
+ fileTreeRefreshBtn.addEventListener('click', () => {
632
+ fileTreeLoaded = false;
633
+ loadFileTree();
634
+ });
635
+ }
636
+
637
+ // Active Context Files
638
+ const contextFilesListEl = document.getElementById('context-files-list');
639
+ const contextRefreshBtn = document.getElementById('context-refresh-btn');
640
+
641
+ async function loadContextFiles() {
642
+ if (!contextFilesListEl) return;
643
+ contextFilesListEl.innerHTML = '<div class="file-tree-loading">Loading context...</div>';
644
+ try {
645
+ const res = await fetch('/api/context');
646
+ if (!res.ok) throw new Error('Failed to load context');
647
+ const data = await res.json();
648
+ contextFilesListEl.innerHTML = '';
649
+ if (!data.files || data.files.length === 0) {
650
+ contextFilesListEl.innerHTML = '<div class="file-tree-loading">No active files in context.</div>';
651
+ return;
652
+ }
653
+ data.files.forEach(file => {
654
+ const chip = document.createElement('div');
655
+ chip.className = 'context-file-chip';
656
+
657
+ const name = document.createElement('span');
658
+ name.className = 'context-file-name';
659
+ name.textContent = file;
660
+ name.title = `Click to insert ${file}`;
661
+ name.addEventListener('click', () => {
662
+ if (!chatInput) return;
663
+ const current = chatInput.value;
664
+ chatInput.value = current ? current + ' ' + file : file;
665
+ chatInput.focus();
666
+ });
667
+
668
+ const removeBtn = document.createElement('button');
669
+ removeBtn.className = 'context-file-remove';
670
+ removeBtn.textContent = '×';
671
+ removeBtn.title = `Remove ${file} from context`;
672
+ removeBtn.addEventListener('click', async (e) => {
673
+ e.stopPropagation();
674
+ try {
675
+ await fetch('/api/context', {
676
+ method: 'DELETE',
677
+ headers: { 'Content-Type': 'application/json' },
678
+ body: JSON.stringify({ file })
679
+ });
680
+ loadContextFiles();
681
+ } catch {}
682
+ });
683
+
684
+ chip.appendChild(name);
685
+ chip.appendChild(removeBtn);
686
+ contextFilesListEl.appendChild(chip);
687
+ });
688
+ } catch {
689
+ contextFilesListEl.innerHTML = '<div class="file-tree-loading">Failed to load context files.</div>';
690
+ }
691
+ }
692
+
693
+ if (contextRefreshBtn) {
694
+ contextRefreshBtn.addEventListener('click', () => {
695
+ loadContextFiles();
696
+ });
697
+ }
698
+
699
+ // Chat History Loading
700
+ let historyLoaded = false;
701
+ async function loadChatHistory() {
702
+ if (historyLoaded) return;
703
+ try {
704
+ const res = await fetch('/api/history');
705
+ if (!res.ok) return;
706
+ const data = await res.json();
707
+ if (data.history && Array.isArray(data.history)) {
708
+ const validHistory = data.history.filter(item => item.role === 'user' || item.role === 'assistant');
709
+ if (validHistory.length > 0) {
710
+ chatMessages.innerHTML = '';
711
+ validHistory.forEach(item => {
712
+ addChatMessage(item.role === 'assistant' ? 'assistant' : 'user', item.text, null, null, item.timestamp);
713
+ });
714
+ }
715
+ }
716
+ historyLoaded = true;
717
+ } catch {}
718
+ }
719
+
720
+ let reconnectAttempts = 0;
721
+
28
722
  function connectSSE() {
29
723
  const eventSource = new EventSource('/telemetry');
30
724
 
31
725
  eventSource.onopen = () => {
726
+ reconnectAttempts = 0;
32
727
  if (statusEl) {
33
728
  statusEl.className = 'status connected';
34
729
  statusEl.textContent = '● CONNECTED';
35
730
  }
36
731
  addLog('Connected to Daedalus telemetry stream.');
732
+ loadChatHistory();
37
733
  };
38
734
 
39
735
  eventSource.onmessage = (event) => {
@@ -47,16 +743,67 @@ function connectSSE() {
47
743
  return;
48
744
  }
49
745
 
746
+ if (data.type === 'chat_token') {
747
+ if (data.role === 'user') {
748
+ // already added locally or by another client
749
+ } else {
750
+ removeThinkingSpinner();
751
+ if (!activeAssistantBody) {
752
+ activeAssistantBody = addChatMessage('assistant', data.text || '', 'STREAM', null, data.timestamp);
753
+ } else if (data.text) {
754
+ const raw = (activeAssistantBody.dataset.raw || '') + data.text;
755
+ activeAssistantBody.dataset.raw = raw;
756
+ activeAssistantBody.innerHTML = renderMarkdown(raw);
757
+ chatMessages.scrollTop = chatMessages.scrollHeight;
758
+ }
759
+ if (chatStatusBadge) chatStatusBadge.textContent = 'STREAMING...';
760
+ }
761
+ return;
762
+ }
763
+
764
+ if (data.type === 'chat_tool_start') {
765
+ addLog(`⚡ Agent tool started: <strong>${data.tool}</strong>`);
766
+ if (chatStatusBadge) chatStatusBadge.textContent = `TOOL: ${data.tool}`;
767
+ updateThinkingSpinner(`Running: ${data.tool}...`);
768
+ renderToolStart(data.tool);
769
+ return;
770
+ }
771
+
772
+ if (data.type === 'chat_tool_result') {
773
+ addLog(`✔ Agent tool finished: <strong>${data.tool}</strong>`);
774
+ updateThinkingSpinner('Daedalus is processing results...');
775
+ renderToolResult(data.tool, data.content || data.result);
776
+ return;
777
+ }
778
+
779
+ if (data.type === 'chat_done') {
780
+ removeThinkingSpinner();
781
+ if (activeAssistantBody && activeAssistantBody.dataset.raw) {
782
+ activeAssistantBody.innerHTML = renderMarkdown(activeAssistantBody.dataset.raw);
783
+ }
784
+ activeAssistantBody = null;
785
+ if (chatStatusBadge) chatStatusBadge.textContent = 'READY';
786
+ addLog('Chat completion finished.');
787
+ return;
788
+ }
789
+
790
+ if (data.type === 'chat_error') {
791
+ removeThinkingSpinner();
792
+ addChatMessage('error', `Execution error: ${data.content}`, null, null, data.timestamp);
793
+ if (chatStatusBadge) chatStatusBadge.textContent = 'ERROR';
794
+ return;
795
+ }
796
+
50
797
  if (data.metric && metrics[data.metric]) {
51
- metrics[data.metric].value.textContent = data.value;
798
+ metrics[data.metric].value.textContent = data.value + '%';
52
799
  metrics[data.metric].status.textContent = data.value > 80 ? 'HIGH' : (data.value > 40 ? 'BUSY' : 'NORMAL');
53
800
  addLog(`Received metric update: <strong>${data.metric.toUpperCase()}</strong> = ${data.value}%`);
54
801
  } else if (data.cpu !== undefined || data.memory !== undefined) {
55
802
  if (data.cpu !== undefined && metrics.cpu) {
56
- metrics.cpu.value.textContent = data.cpu;
803
+ metrics.cpu.value.textContent = data.cpu + '%';
57
804
  }
58
805
  if (data.memory !== undefined && metrics.memory) {
59
- metrics.memory.value.textContent = data.memory;
806
+ metrics.memory.value.textContent = data.memory + '%';
60
807
  }
61
808
  addLog(`Telemetry update: CPU=${data.cpu || '--'}%, MEM=${data.memory || '--'}%`);
62
809
  }
@@ -66,13 +813,266 @@ function connectSSE() {
66
813
  };
67
814
 
68
815
  eventSource.onerror = () => {
816
+ reconnectAttempts++;
817
+ const delay = Math.min(16000, Math.pow(2, reconnectAttempts - 1) * 1000);
69
818
  if (statusEl) {
70
- statusEl.className = 'status disconnected';
71
- statusEl.textContent = '● DISCONNECTED';
819
+ statusEl.className = 'status reconnecting';
820
+ statusEl.textContent = `● RECONNECTING (${Math.round(delay / 1000)}s)...`;
72
821
  }
73
822
  eventSource.close();
74
- setTimeout(connectSSE, 3000);
823
+ setTimeout(connectSSE, delay);
75
824
  };
76
825
  }
77
826
 
827
+ // Chronicles (Saved Sessions) Management
828
+ const sessionsListEl = document.getElementById('sessions-list');
829
+ const sessionNewBtn = document.getElementById('session-new-btn');
830
+ const sessionRefreshBtn = document.getElementById('session-refresh-btn');
831
+
832
+ async function loadSessions() {
833
+ if (!sessionsListEl) return;
834
+ sessionsListEl.innerHTML = '<div class="file-tree-loading">Accessing session archives...</div>';
835
+ try {
836
+ const res = await fetch('/api/sessions');
837
+ if (!res.ok) throw new Error('Failed to load sessions');
838
+ const data = await res.json();
839
+ sessionsListEl.innerHTML = '';
840
+ if (!data.sessions || data.sessions.length === 0) {
841
+ sessionsListEl.innerHTML = '<div class="file-tree-loading">No saved sessions found.</div>';
842
+ return;
843
+ }
844
+
845
+ data.sessions.forEach(sess => {
846
+ const card = document.createElement('div');
847
+ card.className = 'session-card';
848
+
849
+ const header = document.createElement('div');
850
+ header.className = 'session-card-header';
851
+
852
+ const title = document.createElement('span');
853
+ title.className = 'session-card-title';
854
+ title.textContent = sess.title || sess.id.slice(0, 16);
855
+ title.title = sess.title || sess.id;
856
+
857
+ const date = document.createElement('span');
858
+ date.className = 'session-card-date';
859
+ date.textContent = new Date(sess.updated_at || sess.created_at).toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
860
+
861
+ header.appendChild(title);
862
+ header.appendChild(date);
863
+
864
+ const meta = document.createElement('div');
865
+ meta.className = 'session-card-meta';
866
+
867
+ const turns = document.createElement('span');
868
+ turns.className = 'session-card-date';
869
+ turns.textContent = `${sess.turns_count || 0} turns`;
870
+
871
+ const actions = document.createElement('div');
872
+ actions.className = 'session-actions';
873
+
874
+ const resumeBtn = document.createElement('button');
875
+ resumeBtn.className = 'session-btn';
876
+ resumeBtn.textContent = 'RESUME';
877
+ resumeBtn.title = 'Resume this session';
878
+ resumeBtn.addEventListener('click', async (e) => {
879
+ e.stopPropagation();
880
+ resumeBtn.textContent = '...';
881
+ try {
882
+ const r = await fetch('/api/sessions/resume', {
883
+ method: 'POST',
884
+ headers: { 'Content-Type': 'application/json' },
885
+ body: JSON.stringify({ sessionId: sess.id }),
886
+ });
887
+ if (r.ok) {
888
+ historyLoaded = false;
889
+ await loadChatHistory();
890
+ addLog(`Resumed session: <strong>${sess.title || sess.id}</strong>`);
891
+ loadSessions();
892
+ }
893
+ } catch {
894
+ resumeBtn.textContent = 'RESUME';
895
+ }
896
+ });
897
+
898
+ const delBtn = document.createElement('button');
899
+ delBtn.className = 'session-btn delete';
900
+ delBtn.textContent = 'DEL';
901
+ delBtn.title = 'Delete session';
902
+ delBtn.addEventListener('click', async (e) => {
903
+ e.stopPropagation();
904
+ if (!confirm(`Delete session "${sess.title || sess.id}"?`)) return;
905
+ try {
906
+ await fetch('/api/sessions', {
907
+ method: 'DELETE',
908
+ headers: { 'Content-Type': 'application/json' },
909
+ body: JSON.stringify({ sessionId: sess.id }),
910
+ });
911
+ loadSessions();
912
+ } catch {}
913
+ });
914
+
915
+ actions.appendChild(resumeBtn);
916
+ actions.appendChild(delBtn);
917
+
918
+ meta.appendChild(turns);
919
+ meta.appendChild(actions);
920
+
921
+ card.appendChild(header);
922
+ card.appendChild(meta);
923
+ sessionsListEl.appendChild(card);
924
+ });
925
+ } catch {
926
+ sessionsListEl.innerHTML = '<div class="file-tree-loading">Failed to load session archives.</div>';
927
+ }
928
+ }
929
+
930
+ if (sessionNewBtn) {
931
+ sessionNewBtn.addEventListener('click', async () => {
932
+ try {
933
+ const res = await fetch('/api/sessions/new', { method: 'POST' });
934
+ if (res.ok) {
935
+ historyLoaded = false;
936
+ chatMessages.innerHTML = '';
937
+ addChatMessage('assistant', 'New session initialized. Sanctum is ready for instructions.', 'ARCHITECT');
938
+ loadSessions();
939
+ loadContextFiles();
940
+ }
941
+ } catch {}
942
+ });
943
+ }
944
+
945
+ if (sessionRefreshBtn) {
946
+ sessionRefreshBtn.addEventListener('click', () => loadSessions());
947
+ }
948
+
949
+ // New Chat / New Rite Button Handler
950
+ const newChatBtn = document.getElementById('new-chat-btn');
951
+ if (newChatBtn) {
952
+ newChatBtn.addEventListener('click', async () => {
953
+ chatMessages.innerHTML = '';
954
+ activeAssistantBody = null;
955
+ addChatMessage('assistant', 'Sanctum console refreshed. Ready for consultation.', 'ARCHITECT');
956
+ if (chatStatusBadge) chatStatusBadge.textContent = 'SANCTUM READY';
957
+ try {
958
+ await fetch('/api/sessions/new', { method: 'POST' });
959
+ loadSessions();
960
+ loadContextFiles();
961
+ } catch {}
962
+ });
963
+ }
964
+
965
+ // Model Switcher Modal & Active Model Badge
966
+ const activeModelBadge = document.getElementById('active-model-badge');
967
+ const modelLabel = document.getElementById('model-label');
968
+ const modelModal = document.getElementById('model-modal');
969
+ const modelModalClose = document.getElementById('model-modal-close');
970
+ const modelOptionsList = document.getElementById('model-options-list');
971
+
972
+ const DEFAULT_MODELS = [
973
+ { id: 'auto', name: 'Auto / Smart Router', desc: 'Dynamically routes based on task complexity and health' },
974
+ { id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', desc: 'Anthropic — Superior reasoning & code generation' },
975
+ { id: 'gpt-4o', name: 'GPT-4o', desc: 'OpenAI — High-speed multimodal intelligence' },
976
+ { id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', desc: 'Google — Vast context window & deep analysis' },
977
+ { id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', desc: 'Google — Ultra-low latency responses' },
978
+ { id: 'deepseek-chat', name: 'DeepSeek V3', desc: 'DeepSeek — High-efficiency coding & logic' },
979
+ { id: 'ollama/llama3.1', name: 'Llama 3.1 / Local', desc: 'Local-first offline execution via Ollama' },
980
+ ];
981
+
982
+ async function loadModels() {
983
+ try {
984
+ const res = await fetch('/api/models');
985
+ if (!res.ok) return;
986
+ const data = await res.json();
987
+ const currentModel = data.activeModel || 'auto';
988
+ if (modelLabel) {
989
+ modelLabel.textContent = `MODEL: ${currentModel.toUpperCase()}`;
990
+ }
991
+
992
+ if (modelOptionsList) {
993
+ const models = (data.availableModels && data.availableModels.length > 0)
994
+ ? data.availableModels
995
+ : DEFAULT_MODELS;
996
+
997
+ modelOptionsList.innerHTML = '';
998
+ models.forEach(m => {
999
+ const card = document.createElement('div');
1000
+ card.className = `model-option-card ${m.id === currentModel ? 'active' : ''}`;
1001
+
1002
+ const header = document.createElement('div');
1003
+ header.className = 'model-option-header';
1004
+
1005
+ const name = document.createElement('span');
1006
+ name.className = 'model-option-name';
1007
+ name.textContent = m.name || m.id;
1008
+
1009
+ const badge = document.createElement('span');
1010
+ badge.className = 'model-option-badge';
1011
+ badge.textContent = m.provider ? m.provider.toUpperCase() : (m.id === currentModel ? 'ACTIVE' : 'SELECT');
1012
+
1013
+ header.appendChild(name);
1014
+ header.appendChild(badge);
1015
+
1016
+ const desc = document.createElement('div');
1017
+ desc.className = 'model-option-desc';
1018
+ desc.textContent = m.desc || `Switch active engine to ${m.id}`;
1019
+
1020
+ card.appendChild(header);
1021
+ card.appendChild(desc);
1022
+
1023
+ card.addEventListener('click', async () => {
1024
+ try {
1025
+ await fetch('/api/models/switch', {
1026
+ method: 'POST',
1027
+ headers: { 'Content-Type': 'application/json' },
1028
+ body: JSON.stringify({ model: m.id })
1029
+ });
1030
+ if (modelLabel) modelLabel.textContent = `MODEL: ${m.id.toUpperCase()}`;
1031
+ addLog(`Switched active model to <strong>${m.name || m.id}</strong>`);
1032
+ closeModelModal();
1033
+ loadModels();
1034
+ } catch {}
1035
+ });
1036
+
1037
+ modelOptionsList.appendChild(card);
1038
+ });
1039
+ }
1040
+ } catch {}
1041
+ }
1042
+
1043
+ function openModelModal() {
1044
+ if (modelModal) {
1045
+ modelModal.classList.remove('hidden');
1046
+ loadModels();
1047
+ }
1048
+ }
1049
+
1050
+ function closeModelModal() {
1051
+ if (modelModal) {
1052
+ modelModal.classList.add('hidden');
1053
+ }
1054
+ }
1055
+
1056
+ if (activeModelBadge) {
1057
+ activeModelBadge.addEventListener('click', openModelModal);
1058
+ }
1059
+
1060
+ if (modelModalClose) {
1061
+ modelModalClose.addEventListener('click', closeModelModal);
1062
+ }
1063
+
1064
+ if (modelModal) {
1065
+ modelModal.addEventListener('click', (e) => {
1066
+ if (e.target === modelModal) closeModelModal();
1067
+ });
1068
+ }
1069
+
78
1070
  connectSSE();
1071
+ loadModels();
1072
+
1073
+ setInterval(() => {
1074
+ if (lastUpdateEl) {
1075
+ lastUpdateEl.textContent = new Date().toLocaleTimeString();
1076
+ }
1077
+ }, 1000);
1078
+