vg-coder-cli 2.0.45 → 2.0.47

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.
@@ -4,9 +4,12 @@
4
4
  */
5
5
 
6
6
  import { getById } from '../utils.js';
7
+ import { API_BASE } from '../config.js';
7
8
  // Import markdown-it and mermaid from npm packages (bundled by webpack)
8
9
  import markdownit from 'markdown-it';
9
10
  import mermaid from 'mermaid';
11
+ // Import mermaid viewer for fullscreen functionality
12
+ import { createMermaidToolbar, openMermaidViewer, showToast as showMermaidToast } from './mermaid-viewer.js';
10
13
 
11
14
  // State
12
15
  let messages = [];
@@ -100,6 +103,19 @@ async function renderAgentPanel() {
100
103
  <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
101
104
  </svg>
102
105
  </button>
106
+ <button class="agent-btn" id="agent-history-btn" title="Chat history">
107
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
108
+ <circle cx="12" cy="12" r="10"></circle>
109
+ <polyline points="12 6 12 12 16 14"></polyline>
110
+ </svg>
111
+ </button>
112
+ <button class="agent-btn" id="agent-export-btn" title="Export current chat as .json">
113
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
114
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
115
+ <polyline points="7 10 12 15 17 10"></polyline>
116
+ <line x1="12" y1="15" x2="12" y2="3"></line>
117
+ </svg>
118
+ </button>
103
119
  <button class="agent-btn" id="agent-clear-btn" title="Clear chat">
104
120
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
105
121
  <polyline points="3 6 5 6 21 6"></polyline>
@@ -218,6 +234,18 @@ function attachEventListeners() {
218
234
  clearBtn.addEventListener('click', handleClearChat);
219
235
  }
220
236
 
237
+ // History button
238
+ const historyBtn = getById('agent-history-btn');
239
+ if (historyBtn) {
240
+ historyBtn.addEventListener('click', openHistoryModal);
241
+ }
242
+
243
+ // Export button
244
+ const exportBtn = getById('agent-export-btn');
245
+ if (exportBtn) {
246
+ exportBtn.addEventListener('click', exportCurrentChat);
247
+ }
248
+
221
249
  // Drag & drop
222
250
  const dropZone = getById('agent-input-wrapper');
223
251
  if (dropZone) {
@@ -473,17 +501,29 @@ async function processMermaidDiagrams(container) {
473
501
  const id = `agent-mermaid-${Date.now()}-${i}`;
474
502
  const { svg, bindFunctions } = await mermaid.render(id, code);
475
503
 
476
- // Create wrapper div for mermaid
504
+ // Create wrapper div for mermaid with toolbar
477
505
  const wrapper = document.createElement('div');
478
506
  wrapper.className = 'agent-mermaid';
479
- wrapper.innerHTML = svg;
507
+ wrapper.style.cssText = 'position: relative; margin: 16px 0; background: #161b22; border-radius: 6px; overflow: hidden;';
508
+
509
+ // Create toolbar using mermaid-viewer module
510
+ const toolbar = createMermaidToolbar(code, svg);
511
+
512
+ // Create diagram container
513
+ const diagramContainer = document.createElement('div');
514
+ diagramContainer.className = 'agent-mermaid-diagram';
515
+ diagramContainer.style.cssText = 'padding: 20px; display: flex; justify-content: center; align-items: center; overflow-x: auto;';
516
+ diagramContainer.innerHTML = svg;
517
+
518
+ wrapper.appendChild(toolbar);
519
+ wrapper.appendChild(diagramContainer);
480
520
 
481
521
  // Replace pre/code with wrapper
482
522
  preElement.replaceWith(wrapper);
483
523
 
484
524
  // Bind any interactive functions if present
485
525
  if (bindFunctions) {
486
- bindFunctions(wrapper);
526
+ bindFunctions(diagramContainer);
487
527
  }
488
528
 
489
529
  console.log(`[AgentPanel] Rendered mermaid diagram ${i + 1}`);
@@ -613,7 +653,7 @@ function addMessage(role, content, status = 'done') {
613
653
  timestamp: new Date().toLocaleTimeString('vi-VN')
614
654
  });
615
655
  renderMessages();
616
- // Adapter auto-saves, no manual save needed
656
+ scheduleAutoSave();
617
657
  }
618
658
 
619
659
  /**
@@ -623,30 +663,225 @@ function updateLastMessage(updates) {
623
663
  if (messages.length === 0) return;
624
664
  Object.assign(messages[messages.length - 1], updates);
625
665
  renderMessages();
626
- // Adapter handles storage
666
+ scheduleAutoSave();
627
667
  }
628
668
 
629
669
  /**
630
- * Auto-save is handled by adapter
631
- * No manual save needed
670
+ * Debounced auto-save to .vg/chats/<id>.json
632
671
  */
672
+ function scheduleAutoSave() {
673
+ if (autoSaveTimeout) clearTimeout(autoSaveTimeout);
674
+ autoSaveTimeout = setTimeout(saveCurrentChat, 800);
675
+ }
676
+
677
+ async function saveCurrentChat() {
678
+ if (messages.length === 0) return;
679
+
680
+ // Resolve chat ID: prefer URL-derived, fallback to local timestamp ID kept across saves
681
+ if (!currentChatId) {
682
+ currentChatId = window.AIChat?.getChatIdFromUrl?.() || `local-${Date.now()}`;
683
+ }
684
+
685
+ const source = inferChatSource();
686
+ const title = deriveChatTitle();
687
+
688
+ try {
689
+ const res = await fetch(`${API_BASE}/api/chats/${encodeURIComponent(currentChatId)}`, {
690
+ method: 'POST',
691
+ headers: { 'Content-Type': 'application/json' },
692
+ body: JSON.stringify({ source, title, messages })
693
+ });
694
+ if (!res.ok) console.warn('[AgentPanel] Save failed:', res.status);
695
+ } catch (err) {
696
+ console.warn('[AgentPanel] Save error:', err);
697
+ }
698
+ }
699
+
700
+ function inferChatSource() {
701
+ try {
702
+ const host = window.location?.hostname || '';
703
+ if (host.includes('aistudio.google.com')) return 'aistudio';
704
+ if (host.includes('chatgpt.com') || host.includes('chat.openai.com')) return 'chatgpt';
705
+ if (host.includes('claude.ai')) return 'claude';
706
+ return 'unknown';
707
+ } catch (_) { return 'unknown'; }
708
+ }
709
+
710
+ function deriveChatTitle() {
711
+ const firstUser = messages.find(m => m.role === 'user');
712
+ const raw = (firstUser?.content || '').replace(/\n+/g, ' ').trim();
713
+ return raw.length > 80 ? raw.slice(0, 80) + '…' : (raw || '(untitled)');
714
+ }
633
715
 
634
716
  /**
635
- * Handle clear chat
717
+ * Handle clear chat — deletes server-side too
636
718
  */
637
- function handleClearChat() {
719
+ async function handleClearChat() {
638
720
  if (messages.length === 0) return;
639
-
640
- if (confirm('Clear chat history?')) {
641
- console.log(`[AgentPanel] Deleted chat ${currentChatId}`);
721
+ if (!confirm('Clear chat history?')) return;
722
+
723
+ if (currentChatId) {
724
+ try {
725
+ await fetch(`${API_BASE}/api/chats/${encodeURIComponent(currentChatId)}`, { method: 'DELETE' });
726
+ console.log(`[AgentPanel] Deleted chat ${currentChatId}`);
727
+ } catch (err) {
728
+ console.warn('[AgentPanel] Delete error:', err);
729
+ }
642
730
  }
643
731
 
732
+ currentChatId = null;
644
733
  messages = [];
645
734
  selectedFiles = [];
646
735
  renderFileList();
647
736
  renderMessages();
648
737
  }
649
738
 
739
+ /**
740
+ * Export current chat to a downloadable .json file
741
+ */
742
+ function exportCurrentChat() {
743
+ if (messages.length === 0) {
744
+ alert('Chưa có tin nhắn để export.');
745
+ return;
746
+ }
747
+ const data = {
748
+ id: currentChatId || `local-${Date.now()}`,
749
+ source: inferChatSource(),
750
+ title: deriveChatTitle(),
751
+ exportedAt: Date.now(),
752
+ messages
753
+ };
754
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
755
+ const url = URL.createObjectURL(blob);
756
+ const safeId = String(data.id).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80);
757
+ const a = document.createElement('a');
758
+ a.href = url;
759
+ a.download = `chat-${safeId}.json`;
760
+ document.body.appendChild(a);
761
+ a.click();
762
+ document.body.removeChild(a);
763
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
764
+ }
765
+
766
+ /**
767
+ * History modal — list saved chats, click row to load
768
+ */
769
+ let _activeHistoryModal = null;
770
+ let _activeHistoryEscHandler = null;
771
+
772
+ async function openHistoryModal() {
773
+ closeHistoryModal();
774
+
775
+ // Mount inside the Agent tool panel so it sits within the panel layout
776
+ // and never gets covered by host-page UI or other shadow-root overlays.
777
+ const host = getById('tool-panel-agent') || getById('agent-panel-content')
778
+ || window.__VG_CODER_ROOT__ || document.body;
779
+
780
+ const modal = document.createElement('div');
781
+ modal.id = 'agent-history-modal';
782
+ modal.className = 'agent-history-modal agent-history-modal-inline';
783
+ modal.innerHTML = `
784
+ <div class="agent-history-pane">
785
+ <div class="agent-history-header">
786
+ <span>Chat History</span>
787
+ <button class="agent-history-close" title="Back to chat" type="button">←</button>
788
+ </div>
789
+ <div class="agent-history-list" id="agent-history-list">
790
+ <div class="agent-history-empty">⏳ Loading…</div>
791
+ </div>
792
+ </div>
793
+ `;
794
+ host.appendChild(modal);
795
+ _activeHistoryModal = modal;
796
+
797
+ const close = (e) => {
798
+ if (e) { e.preventDefault(); e.stopPropagation(); }
799
+ closeHistoryModal();
800
+ };
801
+
802
+ modal.querySelector('.agent-history-close').addEventListener('click', close);
803
+
804
+ _activeHistoryEscHandler = (e) => { if (e.key === 'Escape') close(e); };
805
+ document.addEventListener('keydown', _activeHistoryEscHandler);
806
+
807
+ try {
808
+ const res = await fetch(`${API_BASE}/api/chats`);
809
+ const data = await res.json();
810
+ const list = data.chats || [];
811
+ const listEl = modal.querySelector('#agent-history-list');
812
+
813
+ if (!list.length) {
814
+ listEl.innerHTML = `<div class="agent-history-empty">No saved chats yet.</div>`;
815
+ return;
816
+ }
817
+
818
+ listEl.innerHTML = list.map(c => `
819
+ <div class="agent-history-row" data-id="${escapeHtml(c.id)}">
820
+ <div class="agent-history-row-main">
821
+ <div class="agent-history-title">${escapeHtml(c.title)}</div>
822
+ <div class="agent-history-meta">
823
+ <span>${escapeHtml(c.source)}</span>
824
+ <span>${c.count} msgs</span>
825
+ <span>${new Date(c.updatedAt || 0).toLocaleString('vi-VN')}</span>
826
+ </div>
827
+ </div>
828
+ <button class="agent-history-delete" data-id="${escapeHtml(c.id)}" title="Delete" type="button">×</button>
829
+ </div>
830
+ `).join('');
831
+
832
+ listEl.querySelectorAll('.agent-history-row').forEach(row => {
833
+ row.addEventListener('click', async (e) => {
834
+ if (e.target.classList.contains('agent-history-delete')) return;
835
+ await loadChatById(row.dataset.id);
836
+ closeHistoryModal();
837
+ });
838
+ });
839
+ listEl.querySelectorAll('.agent-history-delete').forEach(btn => {
840
+ btn.addEventListener('click', async (e) => {
841
+ e.stopPropagation();
842
+ if (!confirm('Delete this chat?')) return;
843
+ await fetch(`${API_BASE}/api/chats/${encodeURIComponent(btn.dataset.id)}`, { method: 'DELETE' });
844
+ openHistoryModal();
845
+ });
846
+ });
847
+ } catch (err) {
848
+ console.error('[AgentPanel] Failed to load history list:', err);
849
+ const listEl = modal.querySelector('#agent-history-list');
850
+ if (listEl) {
851
+ listEl.innerHTML = `<div class="agent-history-empty">❌ ${escapeHtml(err.message)}</div>`;
852
+ }
853
+ }
854
+ }
855
+
856
+ function closeHistoryModal() {
857
+ if (_activeHistoryEscHandler) {
858
+ document.removeEventListener('keydown', _activeHistoryEscHandler);
859
+ _activeHistoryEscHandler = null;
860
+ }
861
+ if (_activeHistoryModal && _activeHistoryModal.parentNode) {
862
+ _activeHistoryModal.parentNode.removeChild(_activeHistoryModal);
863
+ }
864
+ _activeHistoryModal = null;
865
+ // Defensive cleanup in case ref was lost
866
+ const stale = (window.__VG_CODER_ROOT__ || document).querySelector('#agent-history-modal');
867
+ if (stale) stale.remove();
868
+ }
869
+
870
+ async function loadChatById(id) {
871
+ try {
872
+ const res = await fetch(`${API_BASE}/api/chats/${encodeURIComponent(id)}`);
873
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
874
+ const data = await res.json();
875
+ currentChatId = data.id;
876
+ messages = Array.isArray(data.messages) ? data.messages : [];
877
+ renderMessages();
878
+ console.log(`[AgentPanel] Loaded chat ${id} (${messages.length} msgs)`);
879
+ } catch (err) {
880
+ console.error('[AgentPanel] Load chat failed:', err);
881
+ alert(`Load chat failed: ${err.message}`);
882
+ }
883
+ }
884
+
650
885
  /**
651
886
  * Handle add files
652
887
  */
@@ -756,3 +991,4 @@ function escapeHtml(text) {
756
991
  div.textContent = text;
757
992
  return div.innerHTML;
758
993
  }
994
+
@@ -6,6 +6,8 @@ import hljs from 'highlight.js/lib/core';
6
6
  import markdownit from 'markdown-it';
7
7
  // Import mermaid for rendering diagrams
8
8
  import mermaid from 'mermaid';
9
+ // Import mermaid viewer for fullscreen functionality
10
+ import { createMermaidToolbar, openMermaidViewer, showToast as showMermaidToast } from './mermaid-viewer.js';
9
11
 
10
12
  // Import common languages to reduce bundle size
11
13
  import javascript from 'highlight.js/lib/languages/javascript';
@@ -80,17 +82,29 @@ async function renderMermaidDiagrams(container) {
80
82
  const id = `mermaid-diagram-${Date.now()}-${i}`;
81
83
  const { svg, bindFunctions } = await mermaid.render(id, code);
82
84
 
83
- // Create wrapper div for mermaid
85
+ // Create wrapper div for mermaid with toolbar
84
86
  const wrapper = document.createElement('div');
85
87
  wrapper.className = 'mermaid-diagram';
86
- wrapper.innerHTML = svg;
88
+ wrapper.style.cssText = 'position: relative; margin: 16px 0; background: #161b22; border-radius: 6px; overflow: hidden;';
89
+
90
+ // Create toolbar using mermaid-viewer module
91
+ const toolbar = createMermaidToolbar(code, svg);
92
+
93
+ // Create diagram container
94
+ const diagramContainer = document.createElement('div');
95
+ diagramContainer.className = 'mermaid-diagram-content';
96
+ diagramContainer.style.cssText = 'padding: 20px; display: flex; justify-content: center; align-items: center; overflow-x: auto;';
97
+ diagramContainer.innerHTML = svg;
98
+
99
+ wrapper.appendChild(toolbar);
100
+ wrapper.appendChild(diagramContainer);
87
101
 
88
102
  // Replace pre/code with wrapper
89
103
  pre.replaceWith(wrapper);
90
104
 
91
105
  // Bind any interactive functions if present
92
106
  if (bindFunctions) {
93
- bindFunctions(wrapper);
107
+ bindFunctions(diagramContainer);
94
108
  }
95
109
 
96
110
  console.log(`[Mermaid] Successfully rendered diagram ${i + 1}`);
@@ -184,3 +198,4 @@ function escapeHtml(text) {
184
198
  .replace(/"/g, "&quot;")
185
199
  .replace(/'/g, "&#039;");
186
200
  }
201
+
@@ -1,6 +1,6 @@
1
1
  import { getById, qsa, showToast } from '../utils.js';
2
2
  import { getGitDiff } from '../api.js';
3
- import { Diff2HtmlUI } from 'diff2html/lib-esm/src/ui/js/diff2html-ui';
3
+ import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui';
4
4
 
5
5
  let isGitMode = false;
6
6