agentgui 1.0.742 → 1.0.744
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/database.js +8 -5
- package/lib/ws-handlers-conv.js +18 -0
- package/package.json +1 -1
- package/server.js +59 -0
- package/static/js/client.js +39 -0
- package/static/js/streaming-renderer.js +11 -0
- package/static/theme.js +7 -4
package/database.js
CHANGED
|
@@ -421,7 +421,8 @@ try {
|
|
|
421
421
|
claudeSessionId: 'TEXT',
|
|
422
422
|
isStreaming: 'INTEGER DEFAULT 0',
|
|
423
423
|
model: 'TEXT',
|
|
424
|
-
subAgent: 'TEXT'
|
|
424
|
+
subAgent: 'TEXT',
|
|
425
|
+
pinned: 'INTEGER DEFAULT 0'
|
|
425
426
|
};
|
|
426
427
|
|
|
427
428
|
let addedColumns = false;
|
|
@@ -644,13 +645,13 @@ export const queries = {
|
|
|
644
645
|
|
|
645
646
|
getConversationsList() {
|
|
646
647
|
const stmt = prep(
|
|
647
|
-
'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent FROM conversations WHERE status != ? ORDER BY updated_at DESC'
|
|
648
|
+
'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent, pinned FROM conversations WHERE status != ? ORDER BY pinned DESC, updated_at DESC'
|
|
648
649
|
);
|
|
649
650
|
return stmt.all('deleted');
|
|
650
651
|
},
|
|
651
652
|
|
|
652
653
|
getConversations() {
|
|
653
|
-
const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
|
|
654
|
+
const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY pinned DESC, updated_at DESC');
|
|
654
655
|
return stmt.all('deleted');
|
|
655
656
|
},
|
|
656
657
|
|
|
@@ -665,11 +666,12 @@ export const queries = {
|
|
|
665
666
|
const agentType = data.agentType !== undefined ? data.agentType : conv.agentType;
|
|
666
667
|
const model = data.model !== undefined ? data.model : conv.model;
|
|
667
668
|
const subAgent = data.subAgent !== undefined ? data.subAgent : conv.subAgent;
|
|
669
|
+
const pinned = data.pinned !== undefined ? (data.pinned ? 1 : 0) : (conv.pinned || 0);
|
|
668
670
|
|
|
669
671
|
const stmt = prep(
|
|
670
|
-
`UPDATE conversations SET title = ?, status = ?, agentId = ?, agentType = ?, model = ?, subAgent = ?, updated_at = ? WHERE id = ?`
|
|
672
|
+
`UPDATE conversations SET title = ?, status = ?, agentId = ?, agentType = ?, model = ?, subAgent = ?, pinned = ?, updated_at = ? WHERE id = ?`
|
|
671
673
|
);
|
|
672
|
-
stmt.run(title, status, agentId, agentType, model, subAgent, now, id);
|
|
674
|
+
stmt.run(title, status, agentId, agentType, model, subAgent, pinned, now, id);
|
|
673
675
|
|
|
674
676
|
return {
|
|
675
677
|
...conv,
|
|
@@ -679,6 +681,7 @@ export const queries = {
|
|
|
679
681
|
agentType,
|
|
680
682
|
model,
|
|
681
683
|
subAgent,
|
|
684
|
+
pinned,
|
|
682
685
|
updated_at: now
|
|
683
686
|
};
|
|
684
687
|
},
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -117,6 +117,24 @@ export function register(router, deps) {
|
|
|
117
117
|
return { ok: true, chunks: result.chunks, total: result.total, hasMore: result.hasMore, limit: result.limit };
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
router.handle('conv.export', (p) => {
|
|
121
|
+
const conv = queries.getConversation(p.id);
|
|
122
|
+
if (!conv) notFound();
|
|
123
|
+
const msgs = queries.getConversationMessages(p.id);
|
|
124
|
+
const format = p.format || 'markdown';
|
|
125
|
+
if (format === 'json') return { conversation: conv, messages: msgs };
|
|
126
|
+
let md = `# ${conv.title || 'Conversation'}\n\n`;
|
|
127
|
+
md += `Agent: ${conv.agentType || 'unknown'} | Created: ${new Date(conv.created_at).toISOString()}\n\n---\n\n`;
|
|
128
|
+
for (const m of msgs) {
|
|
129
|
+
const role = m.role === 'user' ? 'User' : 'Assistant';
|
|
130
|
+
md += `## ${role}\n\n`;
|
|
131
|
+
let content = m.content;
|
|
132
|
+
try { const parsed = JSON.parse(content); if (Array.isArray(parsed)) { content = parsed.map(b => b.text || b.content || JSON.stringify(b)).join('\n'); } } catch {}
|
|
133
|
+
md += content + '\n\n---\n\n';
|
|
134
|
+
}
|
|
135
|
+
return { markdown: md, title: conv.title };
|
|
136
|
+
});
|
|
137
|
+
|
|
120
138
|
router.handle('conv.cancel', (p) => {
|
|
121
139
|
if (!execMachine.isActive(p.id)) notFound('No active execution to cancel');
|
|
122
140
|
const ctx = execMachine.getContext(p.id);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -2107,6 +2107,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
2107
2107
|
return;
|
|
2108
2108
|
}
|
|
2109
2109
|
|
|
2110
|
+
if (pathOnly === '/api/backup' && req.method === 'GET') {
|
|
2111
|
+
const dbPath = path.join(os.homedir(), '.gmgui', 'data.db');
|
|
2112
|
+
if (!fs.existsSync(dbPath)) { sendJSON(req, res, 404, { error: 'Database not found' }); return; }
|
|
2113
|
+
const stat = fs.statSync(dbPath);
|
|
2114
|
+
res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment; filename="agentgui-backup.db"', 'Content-Length': stat.size });
|
|
2115
|
+
fs.createReadStream(dbPath).pipe(res);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2110
2119
|
if (pathOnly === '/api/debug/machines' && req.method === 'GET' && process.env.DEBUG) {
|
|
2111
2120
|
const toolSnap = {};
|
|
2112
2121
|
for (const [id, actor] of toolInstallMachine.getMachineActors()) {
|
|
@@ -3684,6 +3693,41 @@ const _assetCache = new LRUCache({ max: 200 });
|
|
|
3684
3693
|
let _htmlCache = null;
|
|
3685
3694
|
let _htmlCacheEtag = null;
|
|
3686
3695
|
|
|
3696
|
+
function warmAssetCache() {
|
|
3697
|
+
const dirs = ['js', 'css', 'lib', 'vendor'];
|
|
3698
|
+
let count = 0;
|
|
3699
|
+
for (const dir of dirs) {
|
|
3700
|
+
const full = path.join(staticDir, dir);
|
|
3701
|
+
if (!fs.existsSync(full)) continue;
|
|
3702
|
+
for (const file of fs.readdirSync(full)) {
|
|
3703
|
+
const filePath = path.join(full, file);
|
|
3704
|
+
try {
|
|
3705
|
+
const stats = fs.statSync(filePath);
|
|
3706
|
+
if (!stats.isFile()) continue;
|
|
3707
|
+
const etag = generateETag(stats);
|
|
3708
|
+
if (_assetCache.has(etag)) continue;
|
|
3709
|
+
const raw = fs.readFileSync(filePath);
|
|
3710
|
+
const entry = raw.length < 860 ? { raw, gz: null } : { raw, gz: zlib.gzipSync(raw, { level: 6 }) };
|
|
3711
|
+
_assetCache.set(etag, entry);
|
|
3712
|
+
count++;
|
|
3713
|
+
} catch (_) {}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
for (const file of ['app.js', 'theme.js']) {
|
|
3717
|
+
const filePath = path.join(staticDir, file);
|
|
3718
|
+
try {
|
|
3719
|
+
const stats = fs.statSync(filePath);
|
|
3720
|
+
const etag = generateETag(stats);
|
|
3721
|
+
if (!_assetCache.has(etag)) {
|
|
3722
|
+
const raw = fs.readFileSync(filePath);
|
|
3723
|
+
_assetCache.set(etag, raw.length < 860 ? { raw, gz: null } : { raw, gz: zlib.gzipSync(raw, { level: 6 }) });
|
|
3724
|
+
count++;
|
|
3725
|
+
}
|
|
3726
|
+
} catch (_) {}
|
|
3727
|
+
}
|
|
3728
|
+
if (count > 0) console.log(`[CACHE] Pre-warmed ${count} static assets`);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3687
3731
|
function serveFile(filePath, res, req) {
|
|
3688
3732
|
const ext = path.extname(filePath).toLowerCase();
|
|
3689
3733
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
@@ -5109,6 +5153,7 @@ function onServerReady() {
|
|
|
5109
5153
|
}
|
|
5110
5154
|
|
|
5111
5155
|
recoverStaleSessions();
|
|
5156
|
+
warmAssetCache();
|
|
5112
5157
|
|
|
5113
5158
|
try {
|
|
5114
5159
|
jsonlWatcher = new JsonlWatcher({ broadcastSync, queries, ownedSessionIds });
|
|
@@ -5120,6 +5165,20 @@ function onServerReady() {
|
|
|
5120
5165
|
|
|
5121
5166
|
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
5122
5167
|
|
|
5168
|
+
setInterval(() => {
|
|
5169
|
+
try {
|
|
5170
|
+
const streaming = queries.getStreamingConversations();
|
|
5171
|
+
let cleared = 0;
|
|
5172
|
+
for (const c of streaming) {
|
|
5173
|
+
if (!activeExecutions.has(c.id)) {
|
|
5174
|
+
queries.setIsStreaming(c.id, false);
|
|
5175
|
+
cleared++;
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
if (cleared > 0) debugLog(`[HEALTH] Cleared ${cleared} stale streaming flag(s)`);
|
|
5179
|
+
} catch (e) { debugLog(`[HEALTH] Error: ${e.message}`); }
|
|
5180
|
+
}, 5 * 60 * 1000);
|
|
5181
|
+
|
|
5123
5182
|
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|
|
5124
5183
|
|
|
5125
5184
|
startACPTools().then(() => {
|
package/static/js/client.js
CHANGED
|
@@ -591,6 +591,27 @@ class AgentGUIClient {
|
|
|
591
591
|
});
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
+
document.addEventListener('keydown', (e) => {
|
|
595
|
+
if (e.key === 'n' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
|
596
|
+
e.preventDefault();
|
|
597
|
+
const newBtn = document.querySelector('[data-new-conversation], #newConversationBtn, .new-conversation-btn');
|
|
598
|
+
if (newBtn) newBtn.click();
|
|
599
|
+
}
|
|
600
|
+
if (e.key === 'b' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
const toggleBtn = document.querySelector('[data-sidebar-toggle]');
|
|
603
|
+
if (toggleBtn) toggleBtn.click();
|
|
604
|
+
}
|
|
605
|
+
if (e.key === 'Escape') {
|
|
606
|
+
const activeEl = document.activeElement;
|
|
607
|
+
if (activeEl && activeEl.tagName === 'TEXTAREA') { activeEl.blur(); return; }
|
|
608
|
+
if (this.state.isStreaming) {
|
|
609
|
+
const cancelBtn = document.querySelector('#cancelBtn, [data-cancel-btn]');
|
|
610
|
+
if (cancelBtn && cancelBtn.offsetParent !== null) cancelBtn.click();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
594
615
|
// Setup theme toggle
|
|
595
616
|
const themeToggle = document.querySelector('[data-theme-toggle]');
|
|
596
617
|
if (themeToggle) {
|
|
@@ -893,6 +914,16 @@ class AgentGUIClient {
|
|
|
893
914
|
this.scrollToBottom(true);
|
|
894
915
|
}
|
|
895
916
|
|
|
917
|
+
this._streamStartedAt = Date.now();
|
|
918
|
+
if (this._elapsedTimer) clearInterval(this._elapsedTimer);
|
|
919
|
+
this._elapsedTimer = setInterval(() => {
|
|
920
|
+
const label = streamingDiv?.querySelector('.streaming-indicator-label');
|
|
921
|
+
if (label) {
|
|
922
|
+
const sec = ((Date.now() - this._streamStartedAt) / 1000) | 0;
|
|
923
|
+
label.textContent = sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
|
|
924
|
+
}
|
|
925
|
+
}, 1000);
|
|
926
|
+
|
|
896
927
|
// Reset rendered block seq tracker for this session
|
|
897
928
|
this._renderedSeqs[data.sessionId] = new Set();
|
|
898
929
|
|
|
@@ -917,6 +948,9 @@ class AgentGUIClient {
|
|
|
917
948
|
}
|
|
918
949
|
|
|
919
950
|
handleStreamingProgress(data) {
|
|
951
|
+
try { return this._handleStreamingProgressInner(data); } catch (e) { console.error('[render-error] streaming progress:', e); }
|
|
952
|
+
}
|
|
953
|
+
_handleStreamingProgressInner(data) {
|
|
920
954
|
if (!data.block || !data.sessionId) return;
|
|
921
955
|
|
|
922
956
|
// Deduplicate by seq number to guarantee exactly-once rendering
|
|
@@ -1092,6 +1126,7 @@ class AgentGUIClient {
|
|
|
1092
1126
|
console.error('Streaming error:', data);
|
|
1093
1127
|
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
1094
1128
|
this._clearThinkingCountdown();
|
|
1129
|
+
if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
|
|
1095
1130
|
|
|
1096
1131
|
// Hide stop and inject buttons on error
|
|
1097
1132
|
if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
|
|
@@ -1170,6 +1205,7 @@ class AgentGUIClient {
|
|
|
1170
1205
|
this._dbg('Streaming completed:', data);
|
|
1171
1206
|
if (window.promptMachineAPI) window.promptMachineAPI.send({ type: 'READY' });
|
|
1172
1207
|
this._clearThinkingCountdown();
|
|
1208
|
+
if (this._elapsedTimer) { clearInterval(this._elapsedTimer); this._elapsedTimer = null; }
|
|
1173
1209
|
|
|
1174
1210
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
1175
1211
|
if (conversationId) this.invalidateCache(conversationId);
|
|
@@ -2186,6 +2222,9 @@ class AgentGUIClient {
|
|
|
2186
2222
|
}
|
|
2187
2223
|
|
|
2188
2224
|
renderChunk(chunk) {
|
|
2225
|
+
try { return this._renderChunkInner(chunk); } catch (e) { console.error('[render-error] chunk:', e); }
|
|
2226
|
+
}
|
|
2227
|
+
_renderChunkInner(chunk) {
|
|
2189
2228
|
if (!chunk || !chunk.block) return;
|
|
2190
2229
|
const seq = chunk.sequence;
|
|
2191
2230
|
if (seq !== undefined) {
|
|
@@ -781,6 +781,7 @@ class StreamingRenderer {
|
|
|
781
781
|
const details = document.createElement('details');
|
|
782
782
|
details.className = 'block-tool-use folded-tool';
|
|
783
783
|
if (block.id) details.dataset.toolUseId = block.id;
|
|
784
|
+
details.dataset.startedAt = Date.now();
|
|
784
785
|
details.classList.add(this._getBlockTypeClass('tool_use'));
|
|
785
786
|
details.classList.add(this._getToolColorClass(toolName));
|
|
786
787
|
const normalizedForOpen = toolName.replace(/^mcp__.*?__/, '');
|
|
@@ -2228,6 +2229,16 @@ class StreamingRenderer {
|
|
|
2228
2229
|
statusSpan.className = 'folded-tool-status';
|
|
2229
2230
|
statusSpan.innerHTML = statusSvg;
|
|
2230
2231
|
summary.appendChild(statusSpan);
|
|
2232
|
+
const startedAt = parseInt(toolUseEl.dataset.startedAt);
|
|
2233
|
+
if (startedAt > 0) {
|
|
2234
|
+
const ms = Date.now() - startedAt;
|
|
2235
|
+
const label = ms < 1000 ? ms + 'ms' : (ms / 1000).toFixed(1) + 's';
|
|
2236
|
+
const dur = document.createElement('span');
|
|
2237
|
+
dur.className = 'folded-tool-duration';
|
|
2238
|
+
dur.style.cssText = 'margin-left:0.375rem;font-size:0.6rem;opacity:0.45;font-weight:400';
|
|
2239
|
+
dur.textContent = label;
|
|
2240
|
+
summary.appendChild(dur);
|
|
2241
|
+
}
|
|
2231
2242
|
}
|
|
2232
2243
|
|
|
2233
2244
|
const renderedContent = StreamingRenderer.renderSmartContentHTML(contentStr, this.escapeHtml.bind(this), true);
|
package/static/theme.js
CHANGED
|
@@ -53,15 +53,18 @@ class ThemeManager {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
toggleTheme() {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
this.setTheme(
|
|
56
|
+
const saved = localStorage.getItem(this.THEME_KEY);
|
|
57
|
+
const current = document.documentElement.getAttribute('data-theme') || 'light';
|
|
58
|
+
if (saved === 'dark') { this.setTheme('light'); }
|
|
59
|
+
else if (saved === 'light') { localStorage.removeItem(this.THEME_KEY); this.setTheme(this.SYSTEM_DARK_MODE.matches ? 'dark' : 'light'); this.updateThemeIcon('auto'); return; }
|
|
60
|
+
else { this.setTheme('dark'); }
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
updateThemeIcon(theme) {
|
|
62
64
|
const icon = document.querySelector('.theme-icon');
|
|
63
65
|
if (icon) {
|
|
64
|
-
icon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
|
66
|
+
icon.textContent = theme === 'auto' ? '⚙️' : theme === 'dark' ? '☀️' : '🌙';
|
|
67
|
+
icon.title = theme === 'auto' ? 'Theme: Auto (follows OS)' : theme === 'dark' ? 'Theme: Dark' : 'Theme: Light';
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|