agentgui 1.0.744 → 1.0.746

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
@@ -595,6 +595,21 @@ try {
595
595
  console.error('[Migration] Backfill error:', err.message);
596
596
  }
597
597
 
598
+ try {
599
+ const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'").get();
600
+ if (!hasFts) {
601
+ db.exec("CREATE VIRTUAL TABLE messages_fts USING fts5(content, conversationId UNINDEXED, role UNINDEXED, content_rowid='rowid')");
602
+ const msgs = db.prepare("SELECT rowid, content, conversationId, role FROM messages").all();
603
+ if (msgs.length > 0) {
604
+ const ins = db.prepare("INSERT INTO messages_fts(rowid, content, conversationId, role) VALUES (?, ?, ?, ?)");
605
+ const tx = db.transaction(() => { for (const m of msgs) ins.run(m.rowid, m.content, m.conversationId, m.role); });
606
+ tx();
607
+ console.log(`[Migration] FTS5 index created with ${msgs.length} messages`);
608
+ }
609
+ }
610
+ } catch (err) {
611
+ console.error('[Migration] FTS5 error:', err.message);
612
+ }
598
613
 
599
614
  const stmtCache = new Map();
600
615
  function prep(sql) {
@@ -774,6 +789,8 @@ export const queries = {
774
789
  );
775
790
  stmt.run(id, conversationId, role, storedContent, now);
776
791
 
792
+ try { prep('INSERT INTO messages_fts(rowid, content, conversationId, role) VALUES ((SELECT rowid FROM messages WHERE id = ?), ?, ?, ?)').run(id, storedContent, conversationId, role); } catch (_) {}
793
+
777
794
  const updateConvStmt = prep('UPDATE conversations SET updated_at = ? WHERE id = ?');
778
795
  updateConvStmt.run(now, conversationId);
779
796
 
@@ -1924,6 +1941,21 @@ export const queries = {
1924
1941
  return stmt.run(toolId, toolId, keepCount).changes;
1925
1942
  },
1926
1943
 
1944
+ searchMessages(query, limit = 50) {
1945
+ try {
1946
+ const stmt = prep(`
1947
+ SELECT m.id, m.conversationId, m.role, m.content, m.created_at,
1948
+ c.title as conversationTitle, c.agentType
1949
+ FROM messages_fts fts
1950
+ JOIN messages m ON m.rowid = fts.rowid
1951
+ JOIN conversations c ON c.id = m.conversationId
1952
+ WHERE messages_fts MATCH ?
1953
+ ORDER BY m.created_at DESC LIMIT ?
1954
+ `);
1955
+ return stmt.all(query, limit);
1956
+ } catch (_) { return []; }
1957
+ },
1958
+
1927
1959
  // ============ ACP-COMPATIBLE QUERIES ============
1928
1960
  ...createACPQueries(db, prep)
1929
1961
  };
@@ -135,6 +135,53 @@ export function register(router, deps) {
135
135
  return { markdown: md, title: conv.title };
136
136
  });
137
137
 
138
+ router.handle('conv.sync', (p) => {
139
+ const conv = queries.getConversation(p.id);
140
+ if (!conv) notFound();
141
+ const machineSnap = execMachine.snapshot(p.id);
142
+ const executionState = machineSnap?.value || 'idle';
143
+ const latestSession = queries.getLatestSession(p.id);
144
+ const sinceSeq = parseInt(p.sinceSeq || '-1');
145
+ const since = parseInt(p.since || '0');
146
+ let missedChunks = [];
147
+ if (latestSession && sinceSeq >= 0) {
148
+ missedChunks = queries.getChunksSinceSeq(latestSession.id, sinceSeq);
149
+ } else if (latestSession && since > 0) {
150
+ missedChunks = queries.getConversationChunksSince(p.id, since);
151
+ }
152
+ return {
153
+ conversation: conv,
154
+ executionState,
155
+ isActivelyStreaming: execMachine.isActive(p.id),
156
+ latestSession,
157
+ missedChunks,
158
+ missedCount: missedChunks.length,
159
+ queueLength: machineSnap?.context?.queue?.length || 0
160
+ };
161
+ });
162
+
163
+ router.handle('conv.search', (p) => {
164
+ if (!p.query || typeof p.query !== 'string' || p.query.trim().length < 2) return { results: [] };
165
+ const limit = Math.min(parseInt(p.limit || '50'), 200);
166
+ return { results: queries.searchMessages(p.query.trim(), limit) };
167
+ });
168
+
169
+ router.handle('conv.prune', (p) => {
170
+ const conv = queries.getConversation(p.id);
171
+ if (!conv) notFound();
172
+ const keep = Math.max(parseInt(p.keep || '500'), 100);
173
+ const sessions = queries.getConversationSessions(p.id);
174
+ if (!sessions || sessions.length === 0) return { pruned: 0 };
175
+ const latestSessionId = sessions[0]?.id;
176
+ let pruned = 0;
177
+ for (const sess of sessions) {
178
+ if (sess.id === latestSessionId) continue;
179
+ const count = queries.getSessionChunks(sess.id)?.length || 0;
180
+ if (count > 0) { queries.deleteSessionChunks(sess.id); pruned += count; }
181
+ }
182
+ return { pruned, kept: latestSessionId, sessionsProcessed: sessions.length };
183
+ });
184
+
138
185
  router.handle('conv.cancel', (p) => {
139
186
  if (!execMachine.isActive(p.id)) notFound('No active execution to cancel');
140
187
  const ctx = execMachine.getContext(p.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.744",
3
+ "version": "1.0.746",
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
@@ -2116,6 +2116,28 @@ const server = http.createServer(async (req, res) => {
2116
2116
  return;
2117
2117
  }
2118
2118
 
2119
+ if (pathOnly === '/api/restore' && req.method === 'POST') {
2120
+ const dbPath = path.join(os.homedir(), '.gmgui', 'data.db');
2121
+ const backupPath = dbPath + '.bak-' + Date.now();
2122
+ const chunks = [];
2123
+ req.on('data', (chunk) => chunks.push(chunk));
2124
+ req.on('end', () => {
2125
+ try {
2126
+ const body = Buffer.concat(chunks);
2127
+ if (body.length < 100 || body.slice(0, 16).toString() !== 'SQLite format 3\0') {
2128
+ sendJSON(req, res, 400, { error: 'Invalid SQLite database file' });
2129
+ return;
2130
+ }
2131
+ fs.copyFileSync(dbPath, backupPath);
2132
+ fs.writeFileSync(dbPath, body);
2133
+ sendJSON(req, res, 200, { success: true, backupPath, size: body.length });
2134
+ } catch (e) {
2135
+ sendJSON(req, res, 500, { error: e.message });
2136
+ }
2137
+ });
2138
+ return;
2139
+ }
2140
+
2119
2141
  if (pathOnly === '/api/debug/machines' && req.method === 'GET' && process.env.DEBUG) {
2120
2142
  const toolSnap = {};
2121
2143
  for (const [id, actor] of toolInstallMachine.getMachineActors()) {
@@ -915,12 +915,14 @@ class AgentGUIClient {
915
915
  }
916
916
 
917
917
  this._streamStartedAt = Date.now();
918
+ this._sessionCost = 0;
918
919
  if (this._elapsedTimer) clearInterval(this._elapsedTimer);
919
920
  this._elapsedTimer = setInterval(() => {
920
921
  const label = streamingDiv?.querySelector('.streaming-indicator-label');
921
922
  if (label) {
922
923
  const sec = ((Date.now() - this._streamStartedAt) / 1000) | 0;
923
- label.textContent = sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
924
+ const time = sec < 60 ? sec + 's' : Math.floor(sec / 60) + 'm ' + (sec % 60) + 's';
925
+ label.textContent = this._sessionCost > 0 ? time + ' · $' + this._sessionCost.toFixed(4) : time;
924
926
  }
925
927
  }, 1000);
926
928
 
@@ -1040,6 +1042,10 @@ class AgentGUIClient {
1040
1042
  return;
1041
1043
  }
1042
1044
 
1045
+ if (block.type === 'result' && block.total_cost_usd) {
1046
+ this._sessionCost = (this._sessionCost || 0) + block.total_cost_usd;
1047
+ }
1048
+
1043
1049
  const el = this.renderer.renderBlock(block, data, blocksEl);
1044
1050
  if (el) {
1045
1051
  blocksEl.appendChild(el);