agentgui 1.0.768 → 1.0.770
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/CLAUDE.md +2 -0
- package/lib/routes-conversations.js +87 -0
- package/package.json +1 -1
- package/server.js +4 -88
- package/static/app.js +76 -0
- package/static/index.html +2 -0
package/CLAUDE.md
CHANGED
|
@@ -50,6 +50,8 @@ lib/routes-speech.js Speech/TTS HTTP route handlers (stt, tts, voices, speech-
|
|
|
50
50
|
lib/routes-oauth.js OAuth HTTP route handlers (gemini-oauth/*, codex-oauth/*)
|
|
51
51
|
lib/routes-tools.js Tool management HTTP route handlers (list, install, update, history, refresh)
|
|
52
52
|
lib/routes-util.js Utility HTTP route handlers (clone, folders, git, home, version, import)
|
|
53
|
+
lib/routes-conversations.js Conversation CRUD HTTP route handlers (list, create, get, update, delete, archive, restore)
|
|
54
|
+
lib/routes-debug.js Debug/backup/restore/ws-stats HTTP route handlers
|
|
53
55
|
lib/routes-threads.js Thread CRUD HTTP route handlers (ACP v0.2.3 thread API)
|
|
54
56
|
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
55
57
|
lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
function expandTilde(p) { return p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p; }
|
|
5
|
+
|
|
6
|
+
export function register(deps) {
|
|
7
|
+
const { sendJSON, parseBody, queries, activeExecutions, broadcastSync } = deps;
|
|
8
|
+
const routes = {};
|
|
9
|
+
|
|
10
|
+
routes['GET /api/conversations'] = async (req, res) => {
|
|
11
|
+
const conversations = queries.getConversationsList();
|
|
12
|
+
const activeSessionConvIds = new Set(queries.getActiveSessionConversationIds());
|
|
13
|
+
for (const conv of conversations) {
|
|
14
|
+
if (conv.isStreaming && !activeExecutions.has(conv.id) && !activeSessionConvIds.has(conv.id)) conv.isStreaming = 0;
|
|
15
|
+
}
|
|
16
|
+
sendJSON(req, res, 200, { conversations });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
routes['POST /api/conversations'] = async (req, res) => {
|
|
20
|
+
const body = await parseBody(req);
|
|
21
|
+
const normalizedWorkingDir = body.workingDirectory ? path.resolve(expandTilde(body.workingDirectory)) : null;
|
|
22
|
+
const conversation = queries.createConversation(body.agentId, body.title, normalizedWorkingDir, body.model || null);
|
|
23
|
+
queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
|
|
24
|
+
broadcastSync({ type: 'conversation_created', conversation });
|
|
25
|
+
sendJSON(req, res, 201, { conversation });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
routes['GET /api/conversations/archived'] = async (req, res) => {
|
|
29
|
+
sendJSON(req, res, 200, { conversations: queries.getArchivedConversations() });
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
routes['_match'] = (method, pathOnly) => {
|
|
33
|
+
const key = `${method} ${pathOnly}`;
|
|
34
|
+
if (routes[key]) return routes[key];
|
|
35
|
+
let m;
|
|
36
|
+
if ((m = pathOnly.match(/^\/api\/conversations\/([^/]+)$/))) {
|
|
37
|
+
if (method === 'GET') return (req, res) => handleGetConv(req, res, m[1]);
|
|
38
|
+
if (method === 'POST' || method === 'PUT') return (req, res) => handleUpdateConv(req, res, m[1]);
|
|
39
|
+
if (method === 'DELETE') return (req, res) => handleDeleteConv(req, res, m[1]);
|
|
40
|
+
}
|
|
41
|
+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/conversations\/([^/]+)\/archive$/)))
|
|
42
|
+
return (req, res) => handleArchive(req, res, m[1]);
|
|
43
|
+
if (method === 'POST' && (m = pathOnly.match(/^\/api\/conversations\/([^/]+)\/restore$/)))
|
|
44
|
+
return (req, res) => handleRestore(req, res, m[1]);
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function handleGetConv(req, res, id) {
|
|
49
|
+
const conv = queries.getConversation(id);
|
|
50
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
51
|
+
const latestSession = queries.getLatestSession(id);
|
|
52
|
+
sendJSON(req, res, 200, { conversation: conv, isActivelyStreaming: activeExecutions.has(id), latestSession });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function handleUpdateConv(req, res, id) {
|
|
56
|
+
const body = await parseBody(req);
|
|
57
|
+
if (body.workingDirectory) body.workingDirectory = path.resolve(expandTilde(body.workingDirectory));
|
|
58
|
+
const conv = queries.updateConversation(id, body);
|
|
59
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
60
|
+
queries.createEvent('conversation.updated', body, id);
|
|
61
|
+
broadcastSync({ type: 'conversation_updated', conversation: conv });
|
|
62
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function handleDeleteConv(req, res, id) {
|
|
66
|
+
const deleted = queries.deleteConversation(id);
|
|
67
|
+
if (!deleted) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
68
|
+
broadcastSync({ type: 'conversation_deleted', conversationId: id });
|
|
69
|
+
sendJSON(req, res, 200, { deleted: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function handleArchive(req, res, id) {
|
|
73
|
+
const conv = queries.archiveConversation(id);
|
|
74
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
75
|
+
broadcastSync({ type: 'conversation_deleted', conversationId: id });
|
|
76
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function handleRestore(req, res, id) {
|
|
80
|
+
const conv = queries.restoreConversation(id);
|
|
81
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
82
|
+
broadcastSync({ type: 'conversation_created', conversation: conv });
|
|
83
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return routes;
|
|
87
|
+
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { register as registerUtilRoutes } from './lib/routes-util.js';
|
|
|
24
24
|
import { register as registerToolRoutes } from './lib/routes-tools.js';
|
|
25
25
|
import { register as registerThreadRoutes } from './lib/routes-threads.js';
|
|
26
26
|
import { register as registerDebugRoutes } from './lib/routes-debug.js';
|
|
27
|
+
import { register as registerConvRoutes } from './lib/routes-conversations.js';
|
|
27
28
|
import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
28
29
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
29
30
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
@@ -546,94 +547,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
546
547
|
return;
|
|
547
548
|
}
|
|
548
549
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
// Filter out stale streaming state using a single bulk query instead of N+1 per-conversation queries
|
|
552
|
-
const activeSessionConvIds = new Set(queries.getActiveSessionConversationIds());
|
|
553
|
-
for (const conv of conversations) {
|
|
554
|
-
if (conv.isStreaming && !activeExecutions.has(conv.id) && !activeSessionConvIds.has(conv.id)) {
|
|
555
|
-
conv.isStreaming = 0;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
sendJSON(req, res, 200, { conversations });
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (pathOnly === '/api/conversations' && req.method === 'POST') {
|
|
563
|
-
const body = await parseBody(req);
|
|
564
|
-
// Normalize working directory to avoid Windows path issues; expand ~ to home
|
|
565
|
-
const expandTilde = p => p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
|
|
566
|
-
const normalizedWorkingDir = body.workingDirectory ? path.resolve(expandTilde(body.workingDirectory)) : null;
|
|
567
|
-
const conversation = queries.createConversation(body.agentId, body.title, normalizedWorkingDir, body.model || null);
|
|
568
|
-
queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
|
|
569
|
-
broadcastSync({ type: 'conversation_created', conversation });
|
|
570
|
-
sendJSON(req, res, 201, { conversation });
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const convMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)$/);
|
|
575
|
-
if (convMatch) {
|
|
576
|
-
if (req.method === 'GET') {
|
|
577
|
-
const conv = queries.getConversation(convMatch[1]);
|
|
578
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
579
|
-
|
|
580
|
-
const latestSession = queries.getLatestSession(convMatch[1]);
|
|
581
|
-
const isActivelyStreaming = activeExecutions.has(convMatch[1]);
|
|
582
|
-
|
|
583
|
-
sendJSON(req, res, 200, {
|
|
584
|
-
conversation: conv,
|
|
585
|
-
isActivelyStreaming,
|
|
586
|
-
latestSession
|
|
587
|
-
});
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (req.method === 'POST' || req.method === 'PUT') {
|
|
592
|
-
const body = await parseBody(req);
|
|
593
|
-
// Normalize working directory if present to avoid Windows path issues; expand ~ to home
|
|
594
|
-
if (body.workingDirectory) {
|
|
595
|
-
const expandTilde = p => p && p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
|
|
596
|
-
body.workingDirectory = path.resolve(expandTilde(body.workingDirectory));
|
|
597
|
-
}
|
|
598
|
-
const conv = queries.updateConversation(convMatch[1], body);
|
|
599
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
600
|
-
queries.createEvent('conversation.updated', body, convMatch[1]);
|
|
601
|
-
broadcastSync({ type: 'conversation_updated', conversation: conv });
|
|
602
|
-
sendJSON(req, res, 200, { conversation: conv });
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (req.method === 'DELETE') {
|
|
607
|
-
const deleted = queries.deleteConversation(convMatch[1]);
|
|
608
|
-
if (!deleted) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
609
|
-
broadcastSync({ type: 'conversation_deleted', conversationId: convMatch[1] });
|
|
610
|
-
sendJSON(req, res, 200, { deleted: true });
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (pathOnly === '/api/conversations/archived' && req.method === 'GET') {
|
|
616
|
-
sendJSON(req, res, 200, { conversations: queries.getArchivedConversations() });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const archiveMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/archive$/);
|
|
621
|
-
if (archiveMatch && req.method === 'POST') {
|
|
622
|
-
const conv = queries.archiveConversation(archiveMatch[1]);
|
|
623
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
624
|
-
broadcastSync({ type: 'conversation_deleted', conversationId: archiveMatch[1] });
|
|
625
|
-
sendJSON(req, res, 200, { conversation: conv });
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const restoreMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/restore$/);
|
|
630
|
-
if (restoreMatch && req.method === 'POST') {
|
|
631
|
-
const conv = queries.restoreConversation(restoreMatch[1]);
|
|
632
|
-
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
633
|
-
broadcastSync({ type: 'conversation_created', conversation: conv });
|
|
634
|
-
sendJSON(req, res, 200, { conversation: conv });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
550
|
+
const convHandler = _convRoutes._match(req.method, pathOnly);
|
|
551
|
+
if (convHandler) { await convHandler(req, res); return; }
|
|
637
552
|
|
|
638
553
|
const messagesMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
639
554
|
if (messagesMatch) {
|
|
@@ -2850,6 +2765,7 @@ const _utilRoutes = registerUtilRoutes({ sendJSON, parseBody, queries, STARTUP_C
|
|
|
2850
2765
|
const _toolRoutes = registerToolRoutes({ sendJSON, parseBody, queries, broadcastSync, logError, toolManager });
|
|
2851
2766
|
const _threadRoutes = registerThreadRoutes({ sendJSON, parseBody, queries });
|
|
2852
2767
|
const _debugRoutes = registerDebugRoutes({ sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath });
|
|
2768
|
+
const _convRoutes = registerConvRoutes({ sendJSON, parseBody, queries, activeExecutions, broadcastSync });
|
|
2853
2769
|
|
|
2854
2770
|
registerConvHandlers(wsRouter, {
|
|
2855
2771
|
queries, activeExecutions, rateLimitState,
|
package/static/app.js
CHANGED
|
@@ -827,4 +827,80 @@ document.addEventListener('keydown', (e) => {
|
|
|
827
827
|
window.showErrorToast = showErrorToast;
|
|
828
828
|
})();
|
|
829
829
|
|
|
830
|
+
(function initImportButton() {
|
|
831
|
+
const btn = document.getElementById('importConversationBtn');
|
|
832
|
+
if (!btn) return;
|
|
833
|
+
btn.addEventListener('click', () => {
|
|
834
|
+
const input = document.createElement('input');
|
|
835
|
+
input.type = 'file';
|
|
836
|
+
input.accept = '.json';
|
|
837
|
+
input.addEventListener('change', async () => {
|
|
838
|
+
if (!input.files[0]) return;
|
|
839
|
+
try {
|
|
840
|
+
const text = await input.files[0].text();
|
|
841
|
+
const data = JSON.parse(text);
|
|
842
|
+
if (!data.conversation || !data.messages) {
|
|
843
|
+
window.showErrorToast('Import Failed', 'Invalid format. Export a conversation as JSON first.');
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
await window.wsClient.rpc('conv.import', data);
|
|
847
|
+
} catch (e) {
|
|
848
|
+
window.showErrorToast('Import Failed', e.message);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
input.click();
|
|
852
|
+
});
|
|
853
|
+
})();
|
|
854
|
+
|
|
855
|
+
(function initArchivedView() {
|
|
856
|
+
const btn = document.getElementById('viewArchivedBtn');
|
|
857
|
+
if (!btn) return;
|
|
858
|
+
let showing = false;
|
|
859
|
+
btn.addEventListener('click', async () => {
|
|
860
|
+
const list = document.querySelector('[data-conversation-list]');
|
|
861
|
+
if (!list) return;
|
|
862
|
+
if (showing) {
|
|
863
|
+
showing = false;
|
|
864
|
+
btn.textContent = 'Archived';
|
|
865
|
+
if (window.conversationManager) window.conversationManager.render();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
const base = window.__BASE_URL || '';
|
|
870
|
+
const resp = await fetch(`${base}/api/conversations/archived`);
|
|
871
|
+
const { conversations } = await resp.json();
|
|
872
|
+
if (!conversations || conversations.length === 0) {
|
|
873
|
+
window.UIDialog?.alert('No archived conversations', 'Archived');
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
showing = true;
|
|
877
|
+
btn.textContent = 'Back';
|
|
878
|
+
list.innerHTML = conversations.map(c => `
|
|
879
|
+
<li class="conversation-item" style="cursor:default;">
|
|
880
|
+
<div class="conversation-item-content">
|
|
881
|
+
<div class="conversation-item-title">${window._escHtml(c.title || 'Untitled')}</div>
|
|
882
|
+
<div class="conversation-item-meta">${new Date(c.created_at).toLocaleDateString()}</div>
|
|
883
|
+
</div>
|
|
884
|
+
<button class="conversation-item-archive" title="Restore" data-restore-conv="${c.id}" style="opacity:1;">
|
|
885
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
886
|
+
<polyline points="1 4 1 10 7 10"></polyline>
|
|
887
|
+
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
|
|
888
|
+
</svg>
|
|
889
|
+
</button>
|
|
890
|
+
</li>
|
|
891
|
+
`).join('');
|
|
892
|
+
list.addEventListener('click', async (e) => {
|
|
893
|
+
const restoreBtn = e.target.closest('[data-restore-conv]');
|
|
894
|
+
if (!restoreBtn) return;
|
|
895
|
+
const id = restoreBtn.dataset.restoreConv;
|
|
896
|
+
await fetch(`${base}/api/conversations/${id}/restore`, { method: 'POST' });
|
|
897
|
+
restoreBtn.closest('li').remove();
|
|
898
|
+
if (!list.querySelector('li')) { showing = false; btn.textContent = 'Archived'; if (window.conversationManager) window.conversationManager.render(); }
|
|
899
|
+
}, { once: false });
|
|
900
|
+
} catch (e) {
|
|
901
|
+
window.showErrorToast('Load Failed', e.message);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
})();
|
|
905
|
+
|
|
830
906
|
window.addEventListener('load', initializeApp);
|
package/static/index.html
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
<div class="sidebar-header">
|
|
35
35
|
<h2>History</h2>
|
|
36
36
|
<div class="sidebar-header-actions">
|
|
37
|
+
<button id="importConversationBtn" class="sidebar-clone-btn" title="Import conversation from JSON">Import</button>
|
|
38
|
+
<button id="viewArchivedBtn" class="sidebar-clone-btn" title="View archived conversations">Archived</button>
|
|
37
39
|
<button id="deleteAllConversationsBtn" class="sidebar-clone-btn" data-delete-all-conversations title="Delete all conversations and Claude Code artifacts">Clear All</button>
|
|
38
40
|
<button id="cloneRepoBtn" class="sidebar-clone-btn" data-clone-repo title="Clone a GitHub repo">Clone</button>
|
|
39
41
|
<button id="newConversationBtn" class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
|