vg-coder-cli 2.0.46 → 2.0.48
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.
- package/INTEGRATION.md +418 -0
- package/bin/vg-coder.js +7 -0
- package/bin/vg.js +10 -0
- package/dist/vg-coder-bundle.js +50 -44
- package/package.json +7 -2
- package/src/server/api-server.js +355 -2
- package/src/server/task-queue.js +705 -0
- package/src/server/task-store.js +112 -0
- package/src/server/task-webhook.js +48 -0
- package/src/server/views/css/agent-panel.css +101 -0
- package/src/server/views/js/features/agent-panel.js +230 -9
- package/src/server/views/js/features/git-view.js +1 -1
- package/src/server/views/js/features/task-worker.js +448 -0
- package/src/server/views/js/main.js +4 -0
- package/src/server/views/vg-coder/background.js +17860 -11946
- package/src/server/views/vg-coder/controller.js +42 -10
- package/src/server/views/vg-coder/manifest.json +2 -1
- package/src/server/views/vg-coder/sidepanel.js +13 -7
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
function newTaskId() {
|
|
6
|
+
const ts = Date.now();
|
|
7
|
+
const rand = crypto.randomBytes(3).toString('hex');
|
|
8
|
+
return `t_${ts}_${rand}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sanitizeTaskId(id) {
|
|
12
|
+
return String(id || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 80);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tasksRoot(workingDir) {
|
|
16
|
+
return path.join(workingDir, '.vg', 'tasks');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function taskDir(workingDir, taskId) {
|
|
20
|
+
return path.join(tasksRoot(workingDir), sanitizeTaskId(taskId));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function taskJsonPath(workingDir, taskId) {
|
|
24
|
+
return path.join(taskDir(workingDir, taskId), 'task.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resultMdPath(workingDir, taskId) {
|
|
28
|
+
return path.join(taskDir(workingDir, taskId), 'result.md');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function filesDir(workingDir, taskId) {
|
|
32
|
+
return path.join(taskDir(workingDir, taskId), 'files');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function saveTask(task) {
|
|
36
|
+
const file = taskJsonPath(task.workingDir, task.id);
|
|
37
|
+
await fs.ensureDir(path.dirname(file));
|
|
38
|
+
task.timing = task.timing || {};
|
|
39
|
+
await fs.writeJson(file, task, { spaces: 2 });
|
|
40
|
+
return task;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function loadTask(workingDir, taskId) {
|
|
44
|
+
const file = taskJsonPath(workingDir, taskId);
|
|
45
|
+
if (!await fs.pathExists(file)) return null;
|
|
46
|
+
return fs.readJson(file);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function writeResult(workingDir, taskId, markdown) {
|
|
50
|
+
const file = resultMdPath(workingDir, taskId);
|
|
51
|
+
await fs.ensureDir(path.dirname(file));
|
|
52
|
+
await fs.writeFile(file, markdown || '', 'utf8');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readResult(workingDir, taskId) {
|
|
56
|
+
const file = resultMdPath(workingDir, taskId);
|
|
57
|
+
if (!await fs.pathExists(file)) return '';
|
|
58
|
+
return fs.readFile(file, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function listTasks(workingDir, filter = {}) {
|
|
62
|
+
const root = tasksRoot(workingDir);
|
|
63
|
+
if (!await fs.pathExists(root)) return [];
|
|
64
|
+
const ids = (await fs.readdir(root)).filter(n => n.startsWith('t_'));
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const id of ids) {
|
|
67
|
+
try {
|
|
68
|
+
const t = await fs.readJson(taskJsonPath(workingDir, id));
|
|
69
|
+
if (filter.status && t.status !== filter.status) continue;
|
|
70
|
+
out.push({
|
|
71
|
+
id: t.id,
|
|
72
|
+
status: t.status,
|
|
73
|
+
prompt: (t.prompt || '').slice(0, 200),
|
|
74
|
+
createdAt: t.timing?.createdAt,
|
|
75
|
+
finishedAt: t.timing?.finishedAt,
|
|
76
|
+
durationMs: t.timing?.durationMs,
|
|
77
|
+
webhookUrl: t.webhookUrl || null
|
|
78
|
+
});
|
|
79
|
+
} catch (_) { /* skip */ }
|
|
80
|
+
}
|
|
81
|
+
out.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
82
|
+
if (filter.limit) return out.slice(0, filter.limit);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function rehydrate(workingDir) {
|
|
87
|
+
const root = tasksRoot(workingDir);
|
|
88
|
+
if (!await fs.pathExists(root)) return [];
|
|
89
|
+
const ids = (await fs.readdir(root)).filter(n => n.startsWith('t_'));
|
|
90
|
+
const tasks = [];
|
|
91
|
+
for (const id of ids) {
|
|
92
|
+
try { tasks.push(await fs.readJson(taskJsonPath(workingDir, id))); }
|
|
93
|
+
catch (_) { /* skip */ }
|
|
94
|
+
}
|
|
95
|
+
return tasks;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
newTaskId,
|
|
100
|
+
sanitizeTaskId,
|
|
101
|
+
tasksRoot,
|
|
102
|
+
taskDir,
|
|
103
|
+
taskJsonPath,
|
|
104
|
+
resultMdPath,
|
|
105
|
+
filesDir,
|
|
106
|
+
saveTask,
|
|
107
|
+
loadTask,
|
|
108
|
+
writeResult,
|
|
109
|
+
readResult,
|
|
110
|
+
listTasks,
|
|
111
|
+
rehydrate
|
|
112
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const store = require('./task-store');
|
|
3
|
+
|
|
4
|
+
const RETRY_DELAYS_MS = [1000, 4000, 16000];
|
|
5
|
+
|
|
6
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
7
|
+
|
|
8
|
+
async function deliver(task) {
|
|
9
|
+
if (!task?.webhookUrl) return;
|
|
10
|
+
|
|
11
|
+
const markdown = task.status === 'done' ? await store.readResult(task.workingDir, task.id) : '';
|
|
12
|
+
const body = {
|
|
13
|
+
taskId: task.id,
|
|
14
|
+
status: task.status,
|
|
15
|
+
result: { markdown, chatId: task.result?.chatId || null },
|
|
16
|
+
error: task.error || null,
|
|
17
|
+
durationMs: task.timing?.durationMs || null,
|
|
18
|
+
meta: task.meta || null
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
task.webhook = task.webhook || { attempts: [], deliveredAt: null };
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < RETRY_DELAYS_MS.length; i++) {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(task.webhookUrl, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify(body)
|
|
29
|
+
});
|
|
30
|
+
const ok = res.status >= 200 && res.status < 300;
|
|
31
|
+
task.webhook.attempts.push({ at: Date.now(), status: res.status, ok });
|
|
32
|
+
if (ok) {
|
|
33
|
+
task.webhook.deliveredAt = Date.now();
|
|
34
|
+
await store.saveTask(task);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
console.log(chalk.yellow(`[Webhook] ${task.id} attempt ${i + 1} → HTTP ${res.status}`));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
task.webhook.attempts.push({ at: Date.now(), error: err.message });
|
|
40
|
+
console.log(chalk.yellow(`[Webhook] ${task.id} attempt ${i + 1} → ${err.message}`));
|
|
41
|
+
}
|
|
42
|
+
if (i < RETRY_DELAYS_MS.length - 1) await sleep(RETRY_DELAYS_MS[i]);
|
|
43
|
+
}
|
|
44
|
+
await store.saveTask(task);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { deliver };
|
|
@@ -604,3 +604,104 @@
|
|
|
604
604
|
.agent-retry-btn:hover {
|
|
605
605
|
background: #b91c1c;
|
|
606
606
|
}
|
|
607
|
+
|
|
608
|
+
/* History Pane — mounted inside #tool-panel-agent */
|
|
609
|
+
#tool-panel-agent { position: relative; }
|
|
610
|
+
|
|
611
|
+
.agent-history-modal-inline {
|
|
612
|
+
position: absolute;
|
|
613
|
+
inset: 0;
|
|
614
|
+
z-index: 50;
|
|
615
|
+
background: #18181b;
|
|
616
|
+
color: #ededed;
|
|
617
|
+
display: flex;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
overflow: hidden;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.agent-history-pane {
|
|
623
|
+
flex: 1;
|
|
624
|
+
display: flex;
|
|
625
|
+
flex-direction: column;
|
|
626
|
+
min-height: 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.agent-history-header {
|
|
630
|
+
display: flex;
|
|
631
|
+
align-items: center;
|
|
632
|
+
justify-content: space-between;
|
|
633
|
+
padding: 12px 16px;
|
|
634
|
+
border-bottom: 1px solid #27272a;
|
|
635
|
+
font-weight: 600;
|
|
636
|
+
font-size: 14px;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.agent-history-close {
|
|
640
|
+
background: transparent;
|
|
641
|
+
border: none;
|
|
642
|
+
color: #a1a1aa;
|
|
643
|
+
font-size: 22px;
|
|
644
|
+
line-height: 1;
|
|
645
|
+
cursor: pointer;
|
|
646
|
+
padding: 0 4px;
|
|
647
|
+
}
|
|
648
|
+
.agent-history-close:hover { color: #ededed; }
|
|
649
|
+
|
|
650
|
+
.agent-history-list {
|
|
651
|
+
overflow-y: auto;
|
|
652
|
+
padding: 6px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.agent-history-empty {
|
|
656
|
+
padding: 24px;
|
|
657
|
+
text-align: center;
|
|
658
|
+
color: #71717a;
|
|
659
|
+
font-size: 13px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.agent-history-row {
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
gap: 8px;
|
|
666
|
+
padding: 10px 12px;
|
|
667
|
+
border-radius: 6px;
|
|
668
|
+
cursor: pointer;
|
|
669
|
+
transition: background 0.15s;
|
|
670
|
+
}
|
|
671
|
+
.agent-history-row:hover { background: #27272a; }
|
|
672
|
+
|
|
673
|
+
.agent-history-row-main { flex: 1; min-width: 0; }
|
|
674
|
+
|
|
675
|
+
.agent-history-title {
|
|
676
|
+
font-size: 13px;
|
|
677
|
+
font-weight: 500;
|
|
678
|
+
overflow: hidden;
|
|
679
|
+
text-overflow: ellipsis;
|
|
680
|
+
white-space: nowrap;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.agent-history-meta {
|
|
684
|
+
display: flex;
|
|
685
|
+
gap: 10px;
|
|
686
|
+
margin-top: 4px;
|
|
687
|
+
font-size: 11px;
|
|
688
|
+
color: #71717a;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.agent-history-delete {
|
|
692
|
+
background: transparent;
|
|
693
|
+
border: none;
|
|
694
|
+
color: #71717a;
|
|
695
|
+
font-size: 18px;
|
|
696
|
+
cursor: pointer;
|
|
697
|
+
padding: 2px 8px;
|
|
698
|
+
border-radius: 4px;
|
|
699
|
+
}
|
|
700
|
+
.agent-history-delete:hover { color: #ef4444; background: rgba(239, 68, 68, 0.1); }
|
|
701
|
+
|
|
702
|
+
[data-theme="light"] .agent-history-modal-inline {
|
|
703
|
+
background: #ffffff;
|
|
704
|
+
color: #18181b;
|
|
705
|
+
}
|
|
706
|
+
[data-theme="light"] .agent-history-header { border-bottom-color: #e5e5ea; }
|
|
707
|
+
[data-theme="light"] .agent-history-row:hover { background: #f2f2f7; }
|
|
@@ -4,6 +4,7 @@
|
|
|
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';
|
|
@@ -102,6 +103,19 @@ async function renderAgentPanel() {
|
|
|
102
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>
|
|
103
104
|
</svg>
|
|
104
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>
|
|
105
119
|
<button class="agent-btn" id="agent-clear-btn" title="Clear chat">
|
|
106
120
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
107
121
|
<polyline points="3 6 5 6 21 6"></polyline>
|
|
@@ -220,6 +234,18 @@ function attachEventListeners() {
|
|
|
220
234
|
clearBtn.addEventListener('click', handleClearChat);
|
|
221
235
|
}
|
|
222
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
|
+
|
|
223
249
|
// Drag & drop
|
|
224
250
|
const dropZone = getById('agent-input-wrapper');
|
|
225
251
|
if (dropZone) {
|
|
@@ -627,7 +653,7 @@ function addMessage(role, content, status = 'done') {
|
|
|
627
653
|
timestamp: new Date().toLocaleTimeString('vi-VN')
|
|
628
654
|
});
|
|
629
655
|
renderMessages();
|
|
630
|
-
|
|
656
|
+
scheduleAutoSave();
|
|
631
657
|
}
|
|
632
658
|
|
|
633
659
|
/**
|
|
@@ -637,30 +663,225 @@ function updateLastMessage(updates) {
|
|
|
637
663
|
if (messages.length === 0) return;
|
|
638
664
|
Object.assign(messages[messages.length - 1], updates);
|
|
639
665
|
renderMessages();
|
|
640
|
-
|
|
666
|
+
scheduleAutoSave();
|
|
641
667
|
}
|
|
642
668
|
|
|
643
669
|
/**
|
|
644
|
-
*
|
|
645
|
-
* No manual save needed
|
|
670
|
+
* Debounced auto-save to .vg/chats/<id>.json
|
|
646
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
|
+
}
|
|
647
715
|
|
|
648
716
|
/**
|
|
649
|
-
* Handle clear chat
|
|
717
|
+
* Handle clear chat — deletes server-side too
|
|
650
718
|
*/
|
|
651
|
-
function handleClearChat() {
|
|
719
|
+
async function handleClearChat() {
|
|
652
720
|
if (messages.length === 0) return;
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
+
}
|
|
656
730
|
}
|
|
657
731
|
|
|
732
|
+
currentChatId = null;
|
|
658
733
|
messages = [];
|
|
659
734
|
selectedFiles = [];
|
|
660
735
|
renderFileList();
|
|
661
736
|
renderMessages();
|
|
662
737
|
}
|
|
663
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
|
+
|
|
664
885
|
/**
|
|
665
886
|
* Handle add files
|
|
666
887
|
*/
|
|
@@ -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/
|
|
3
|
+
import { Diff2HtmlUI } from 'diff2html/lib-esm/ui/js/diff2html-ui';
|
|
4
4
|
|
|
5
5
|
let isGitMode = false;
|
|
6
6
|
|