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 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
  },
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.742",
3
+ "version": "1.0.744",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
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(() => {
@@ -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 currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
57
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
58
- this.setTheme(newTheme);
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