agentgui 1.0.8 → 1.0.10
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/acp-launcher.js +150 -205
- package/color-table.html +41 -0
- package/database.js +145 -96
- package/package.json +1 -1
- package/server.js +1 -1
- package/static/app.js +114 -53
- package/static/index.html +37 -36
- package/static/styles.css +27 -39
- package/test-block.html +181 -0
- package/static/rippleui.css +0 -208
package/database.js
CHANGED
|
@@ -99,56 +99,54 @@ function migrateFromJson() {
|
|
|
99
99
|
const content = fs.readFileSync(oldJsonPath, 'utf-8');
|
|
100
100
|
const data = JSON.parse(content);
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const migrationStmt = db.transaction(() => {
|
|
103
|
+
if (data.conversations) {
|
|
104
|
+
for (const id in data.conversations) {
|
|
105
|
+
const conv = data.conversations[id];
|
|
106
|
+
db.prepare(
|
|
107
|
+
`INSERT OR REPLACE INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
|
|
108
|
+
).run(conv.id, conv.agentId, conv.title || null, conv.created_at, conv.updated_at, conv.status || 'active');
|
|
109
|
+
}
|
|
109
110
|
}
|
|
110
|
-
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
if (data.messages) {
|
|
113
|
+
for (const id in data.messages) {
|
|
114
|
+
const msg = data.messages[id];
|
|
115
|
+
db.prepare(
|
|
116
|
+
`INSERT OR REPLACE INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
|
|
117
|
+
).run(msg.id, msg.conversationId, msg.role, msg.content, msg.created_at);
|
|
118
|
+
}
|
|
119
119
|
}
|
|
120
|
-
}
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
if (data.sessions) {
|
|
122
|
+
for (const id in data.sessions) {
|
|
123
|
+
const sess = data.sessions[id];
|
|
124
|
+
db.prepare(
|
|
125
|
+
`INSERT OR REPLACE INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
126
|
+
).run(sess.id, sess.conversationId, sess.status, sess.started_at, sess.completed_at || null, sess.response || null, sess.error || null);
|
|
127
|
+
}
|
|
129
128
|
}
|
|
130
|
-
}
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
if (data.events) {
|
|
131
|
+
for (const id in data.events) {
|
|
132
|
+
const evt = data.events[id];
|
|
133
|
+
db.prepare(
|
|
134
|
+
`INSERT OR REPLACE INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
|
|
135
|
+
).run(evt.id, evt.type, evt.conversationId || null, evt.sessionId || null, JSON.stringify(evt.data), evt.created_at);
|
|
136
|
+
}
|
|
139
137
|
}
|
|
140
|
-
}
|
|
141
138
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
if (data.idempotencyKeys) {
|
|
140
|
+
for (const key in data.idempotencyKeys) {
|
|
141
|
+
const entry = data.idempotencyKeys[key];
|
|
142
|
+
db.prepare(
|
|
143
|
+
`INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)`
|
|
144
|
+
).run(key, JSON.stringify(entry.value), entry.created_at, entry.ttl);
|
|
145
|
+
}
|
|
149
146
|
}
|
|
150
|
-
}
|
|
147
|
+
});
|
|
151
148
|
|
|
149
|
+
migrationStmt();
|
|
152
150
|
fs.renameSync(oldJsonPath, `${oldJsonPath}.migrated`);
|
|
153
151
|
console.log('Migrated data from JSON to SQLite');
|
|
154
152
|
} catch (e) {
|
|
@@ -188,8 +186,8 @@ export const queries = {
|
|
|
188
186
|
},
|
|
189
187
|
|
|
190
188
|
getAllConversations() {
|
|
191
|
-
const stmt = db.prepare('SELECT * FROM conversations ORDER BY updated_at DESC');
|
|
192
|
-
return stmt.all();
|
|
189
|
+
const stmt = db.prepare('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
|
|
190
|
+
return stmt.all('deleted');
|
|
193
191
|
},
|
|
194
192
|
|
|
195
193
|
updateConversation(id, data) {
|
|
@@ -221,11 +219,12 @@ export const queries = {
|
|
|
221
219
|
|
|
222
220
|
const id = generateId('msg');
|
|
223
221
|
const now = Date.now();
|
|
222
|
+
const storedContent = typeof content === 'string' ? content : JSON.stringify(content);
|
|
224
223
|
|
|
225
224
|
const stmt = db.prepare(
|
|
226
225
|
`INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
|
|
227
226
|
);
|
|
228
|
-
stmt.run(id, conversationId, role,
|
|
227
|
+
stmt.run(id, conversationId, role, storedContent, now);
|
|
229
228
|
|
|
230
229
|
const updateConvStmt = db.prepare('UPDATE conversations SET updated_at = ? WHERE id = ?');
|
|
231
230
|
updateConvStmt.run(now, conversationId);
|
|
@@ -294,7 +293,8 @@ export const queries = {
|
|
|
294
293
|
if (!session) return null;
|
|
295
294
|
|
|
296
295
|
const status = data.status !== undefined ? data.status : session.status;
|
|
297
|
-
const
|
|
296
|
+
const rawResponse = data.response !== undefined ? data.response : session.response;
|
|
297
|
+
const response = rawResponse && typeof rawResponse === 'object' ? JSON.stringify(rawResponse) : rawResponse;
|
|
298
298
|
const error = data.error !== undefined ? data.error : session.error;
|
|
299
299
|
const completed_at = data.completed_at !== undefined ? data.completed_at : session.completed_at;
|
|
300
300
|
|
|
@@ -387,22 +387,28 @@ export const queries = {
|
|
|
387
387
|
const conv = this.getConversation(id);
|
|
388
388
|
if (!conv) return false;
|
|
389
389
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
390
|
+
const deleteStmt = db.transaction(() => {
|
|
391
|
+
db.prepare('DELETE FROM events WHERE conversationId = ?').run(id);
|
|
392
|
+
db.prepare('DELETE FROM sessions WHERE conversationId = ?').run(id);
|
|
393
|
+
db.prepare('DELETE FROM messages WHERE conversationId = ?').run(id);
|
|
394
|
+
db.prepare('UPDATE conversations SET status = ? WHERE id = ?').run('deleted', id);
|
|
395
|
+
});
|
|
394
396
|
|
|
397
|
+
deleteStmt();
|
|
395
398
|
return true;
|
|
396
399
|
},
|
|
397
400
|
|
|
398
401
|
cleanup() {
|
|
399
402
|
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
|
|
403
|
+
const now = Date.now();
|
|
400
404
|
|
|
401
|
-
|
|
402
|
-
|
|
405
|
+
const cleanupStmt = db.transaction(() => {
|
|
406
|
+
db.prepare('DELETE FROM events WHERE created_at < ?').run(thirtyDaysAgo);
|
|
407
|
+
db.prepare('DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?').run(thirtyDaysAgo);
|
|
408
|
+
db.prepare('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
|
|
409
|
+
});
|
|
403
410
|
|
|
404
|
-
|
|
405
|
-
db.prepare('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
|
|
411
|
+
cleanupStmt();
|
|
406
412
|
},
|
|
407
413
|
|
|
408
414
|
setIdempotencyKey(key, value) {
|
|
@@ -435,36 +441,37 @@ export const queries = {
|
|
|
435
441
|
},
|
|
436
442
|
|
|
437
443
|
discoverClaudeCodeConversations() {
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (!fs.existsSync(conversationsDir)) {
|
|
442
|
-
return [];
|
|
443
|
-
}
|
|
444
|
+
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
445
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
444
446
|
|
|
445
447
|
const discovered = [];
|
|
446
448
|
try {
|
|
447
|
-
const
|
|
448
|
-
for (const
|
|
449
|
-
if (!
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (!fs.existsSync(metadataPath) || !fs.existsSync(messagesPath)) continue;
|
|
449
|
+
const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
450
|
+
for (const dir of dirs) {
|
|
451
|
+
if (!dir.isDirectory()) continue;
|
|
452
|
+
const dirPath = path.join(projectsDir, dir.name);
|
|
453
|
+
const indexPath = path.join(dirPath, 'sessions-index.json');
|
|
454
|
+
if (!fs.existsSync(indexPath)) continue;
|
|
455
455
|
|
|
456
456
|
try {
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
457
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
458
|
+
const projectPath = index.originalPath || dir.name.replace(/^-/, '/').replace(/-/g, '/');
|
|
459
|
+
for (const entry of (index.entries || [])) {
|
|
460
|
+
if (!entry.sessionId || entry.messageCount === 0) continue;
|
|
461
|
+
discovered.push({
|
|
462
|
+
id: entry.sessionId,
|
|
463
|
+
jsonlPath: entry.fullPath || path.join(dirPath, `${entry.sessionId}.jsonl`),
|
|
464
|
+
title: entry.summary || entry.firstPrompt || 'Claude Code Session',
|
|
465
|
+
projectPath,
|
|
466
|
+
created: entry.created ? new Date(entry.created).getTime() : entry.fileMtime,
|
|
467
|
+
modified: entry.modified ? new Date(entry.modified).getTime() : entry.fileMtime,
|
|
468
|
+
messageCount: entry.messageCount,
|
|
469
|
+
gitBranch: entry.gitBranch,
|
|
470
|
+
source: 'claude-code'
|
|
471
|
+
});
|
|
472
|
+
}
|
|
466
473
|
} catch (e) {
|
|
467
|
-
console.error(`Error reading
|
|
474
|
+
console.error(`Error reading index ${indexPath}:`, e.message);
|
|
468
475
|
}
|
|
469
476
|
}
|
|
470
477
|
} catch (e) {
|
|
@@ -474,39 +481,81 @@ export const queries = {
|
|
|
474
481
|
return discovered;
|
|
475
482
|
},
|
|
476
483
|
|
|
484
|
+
parseJsonlMessages(jsonlPath) {
|
|
485
|
+
if (!fs.existsSync(jsonlPath)) return [];
|
|
486
|
+
const messages = [];
|
|
487
|
+
try {
|
|
488
|
+
const lines = fs.readFileSync(jsonlPath, 'utf-8').split('\n');
|
|
489
|
+
for (const line of lines) {
|
|
490
|
+
if (!line.trim()) continue;
|
|
491
|
+
try {
|
|
492
|
+
const obj = JSON.parse(line);
|
|
493
|
+
if (obj.type === 'user' && obj.message?.content) {
|
|
494
|
+
const content = typeof obj.message.content === 'string'
|
|
495
|
+
? obj.message.content
|
|
496
|
+
: Array.isArray(obj.message.content)
|
|
497
|
+
? obj.message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
498
|
+
: JSON.stringify(obj.message.content);
|
|
499
|
+
if (content && !content.startsWith('[{"tool_use_id"')) {
|
|
500
|
+
messages.push({ id: obj.uuid || generateId('msg'), role: 'user', content, created_at: new Date(obj.timestamp).getTime() });
|
|
501
|
+
}
|
|
502
|
+
} else if (obj.type === 'assistant' && obj.message?.content) {
|
|
503
|
+
let text = '';
|
|
504
|
+
const content = obj.message.content;
|
|
505
|
+
if (Array.isArray(content)) {
|
|
506
|
+
for (const c of content) {
|
|
507
|
+
if (c.type === 'text' && c.text) text += c.text;
|
|
508
|
+
}
|
|
509
|
+
} else if (typeof content === 'string') {
|
|
510
|
+
text = content;
|
|
511
|
+
}
|
|
512
|
+
if (text) {
|
|
513
|
+
messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch (_) {}
|
|
517
|
+
}
|
|
518
|
+
} catch (e) {
|
|
519
|
+
console.error(`Error parsing JSONL ${jsonlPath}:`, e.message);
|
|
520
|
+
}
|
|
521
|
+
return messages;
|
|
522
|
+
},
|
|
523
|
+
|
|
477
524
|
importClaudeCodeConversations() {
|
|
478
525
|
const discovered = this.discoverClaudeCodeConversations();
|
|
479
526
|
const imported = [];
|
|
480
527
|
|
|
481
528
|
for (const conv of discovered) {
|
|
482
529
|
try {
|
|
483
|
-
const existingConv = db.prepare('SELECT id FROM conversations WHERE id = ?').get(conv.id);
|
|
530
|
+
const existingConv = db.prepare('SELECT id, status FROM conversations WHERE id = ?').get(conv.id);
|
|
484
531
|
if (existingConv) {
|
|
485
|
-
imported.push({ id: conv.id, status: 'skipped', reason: '
|
|
532
|
+
imported.push({ id: conv.id, status: 'skipped', reason: existingConv.status === 'deleted' ? 'deleted' : 'exists' });
|
|
486
533
|
continue;
|
|
487
534
|
}
|
|
488
535
|
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
536
|
+
const projectName = conv.projectPath ? path.basename(conv.projectPath) : '';
|
|
537
|
+
const title = conv.title || 'Claude Code Session';
|
|
538
|
+
const displayTitle = projectName ? `[${projectName}] ${title}` : title;
|
|
539
|
+
|
|
540
|
+
const messages = this.parseJsonlMessages(conv.jsonlPath);
|
|
541
|
+
|
|
542
|
+
const importStmt = db.transaction(() => {
|
|
543
|
+
db.prepare(
|
|
544
|
+
`INSERT INTO conversations (id, agentId, title, created_at, updated_at, status) VALUES (?, ?, ?, ?, ?, ?)`
|
|
545
|
+
).run(conv.id, 'claude-code', displayTitle, conv.created, conv.modified, 'active');
|
|
546
|
+
|
|
547
|
+
for (const msg of messages) {
|
|
548
|
+
try {
|
|
549
|
+
db.prepare(
|
|
550
|
+
`INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
|
|
551
|
+
).run(msg.id, conv.id, msg.role, msg.content, msg.created_at);
|
|
552
|
+
} catch (_) {}
|
|
504
553
|
}
|
|
505
|
-
}
|
|
554
|
+
});
|
|
506
555
|
|
|
507
|
-
|
|
556
|
+
importStmt();
|
|
557
|
+
imported.push({ id: conv.id, status: 'imported', title: displayTitle, messages: messages.length });
|
|
508
558
|
} catch (e) {
|
|
509
|
-
console.error(`Error importing Claude Code conversation ${conv.id}:`, e.message);
|
|
510
559
|
imported.push({ id: conv.id, status: 'error', error: e.message });
|
|
511
560
|
}
|
|
512
561
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -330,7 +330,7 @@ async function processMessage(conversationId, messageId, sessionId, content, age
|
|
|
330
330
|
const result = await conn.sendPrompt(content);
|
|
331
331
|
conn.onUpdate = null;
|
|
332
332
|
|
|
333
|
-
const responseText = fullText || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
|
|
333
|
+
const responseText = fullText || result?.result || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
|
|
334
334
|
const messageContent = blocks.length > 0 ? { text: responseText, blocks } : responseText;
|
|
335
335
|
|
|
336
336
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
package/static/app.js
CHANGED
|
@@ -454,6 +454,25 @@ class GMGUIApp {
|
|
|
454
454
|
this.renderAgentCards();
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
groupConsecutiveMessages(messages) {
|
|
458
|
+
if (!messages.length) return [];
|
|
459
|
+
const grouped = [];
|
|
460
|
+
let current = { ...messages[0], content: typeof messages[0].content === 'string' ? messages[0].content : messages[0].content };
|
|
461
|
+
for (let i = 1; i < messages.length; i++) {
|
|
462
|
+
const msg = messages[i];
|
|
463
|
+
if (msg.role === current.role && msg.role === 'assistant') {
|
|
464
|
+
const curText = typeof current.content === 'string' ? current.content : (current.content?.text || '');
|
|
465
|
+
const msgText = typeof msg.content === 'string' ? msg.content : (msg.content?.text || '');
|
|
466
|
+
current = { ...current, content: curText + '\n\n' + msgText };
|
|
467
|
+
} else {
|
|
468
|
+
grouped.push(current);
|
|
469
|
+
current = { ...msg };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
grouped.push(current);
|
|
473
|
+
return grouped;
|
|
474
|
+
}
|
|
475
|
+
|
|
457
476
|
async displayConversation(id) {
|
|
458
477
|
this.currentConversation = id;
|
|
459
478
|
const conv = this.conversations.get(id);
|
|
@@ -479,7 +498,8 @@ class GMGUIApp {
|
|
|
479
498
|
`;
|
|
480
499
|
this.renderAgentCards();
|
|
481
500
|
} else {
|
|
482
|
-
|
|
501
|
+
const grouped = this.groupConsecutiveMessages(messages);
|
|
502
|
+
grouped.forEach(msg => this.addMessageToDisplay(msg));
|
|
483
503
|
|
|
484
504
|
if (this.settings.autoScroll) {
|
|
485
505
|
div.scrollTop = div.scrollHeight;
|
|
@@ -490,43 +510,75 @@ class GMGUIApp {
|
|
|
490
510
|
}
|
|
491
511
|
|
|
492
512
|
|
|
513
|
+
sanitizeHtml(raw) {
|
|
514
|
+
const tmp = document.createElement('div');
|
|
515
|
+
tmp.innerHTML = raw;
|
|
516
|
+
tmp.querySelectorAll('script,iframe,object,embed,form,meta,link').forEach(el => el.remove());
|
|
517
|
+
tmp.querySelectorAll('*').forEach(el => {
|
|
518
|
+
for (const attr of Array.from(el.attributes)) {
|
|
519
|
+
if (attr.name.startsWith('on')) el.removeAttribute(attr.name);
|
|
520
|
+
if (attr.name === 'href' && attr.value.trim().toLowerCase().startsWith('javascript:')) el.removeAttribute(attr.name);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
return tmp.innerHTML;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
looksLikeHtml(text) {
|
|
527
|
+
const trimmed = text.trim();
|
|
528
|
+
if (/^<[a-z][\s\S]*>/i.test(trimmed)) return true;
|
|
529
|
+
if (/<\/(div|span|p|table|ul|ol|h[1-6]|section|article|header|footer|nav|main|aside|details|summary|figure|figcaption|blockquote|pre|code|a|strong|em|img|br|hr)>/i.test(trimmed)) return true;
|
|
530
|
+
const tagCount = (trimmed.match(/<[a-z][^>]*>/gi) || []).length;
|
|
531
|
+
if (tagCount >= 3) return true;
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
493
535
|
parseAndRenderContent(content) {
|
|
494
536
|
const elements = [];
|
|
495
|
-
if (typeof content
|
|
496
|
-
const htmlCodeBlockRegex = /```html\n([\s\S]*?)\n```/g;
|
|
497
|
-
let lastIndex = 0;
|
|
498
|
-
let match;
|
|
499
|
-
|
|
500
|
-
while ((match = htmlCodeBlockRegex.exec(content)) !== null) {
|
|
501
|
-
if (match.index > lastIndex) {
|
|
502
|
-
const textBefore = content.substring(lastIndex, match.index);
|
|
503
|
-
if (textBefore.trim()) {
|
|
504
|
-
const bubble = document.createElement('div');
|
|
505
|
-
bubble.className = 'message-bubble';
|
|
506
|
-
bubble.textContent = textBefore;
|
|
507
|
-
elements.push(bubble);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
537
|
+
if (typeof content !== 'string') return null;
|
|
510
538
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
lastIndex = htmlCodeBlockRegex.lastIndex;
|
|
515
|
-
}
|
|
539
|
+
const htmlCodeBlockRegex = /```html\n([\s\S]*?)\n```/g;
|
|
540
|
+
let lastIndex = 0;
|
|
541
|
+
let match;
|
|
516
542
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
bubble.textContent = textAfter;
|
|
523
|
-
elements.push(bubble);
|
|
543
|
+
while ((match = htmlCodeBlockRegex.exec(content)) !== null) {
|
|
544
|
+
if (match.index > lastIndex) {
|
|
545
|
+
const textBefore = content.substring(lastIndex, match.index);
|
|
546
|
+
if (textBefore.trim()) {
|
|
547
|
+
elements.push(this.renderTextOrHtml(textBefore));
|
|
524
548
|
}
|
|
525
549
|
}
|
|
550
|
+
elements.push(this.createSandboxedHtml(match[1]));
|
|
551
|
+
lastIndex = htmlCodeBlockRegex.lastIndex;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (lastIndex < content.length) {
|
|
555
|
+
const remaining = content.substring(lastIndex);
|
|
556
|
+
if (remaining.trim()) {
|
|
557
|
+
elements.push(this.renderTextOrHtml(remaining));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
526
560
|
|
|
527
|
-
|
|
561
|
+
return elements.length > 0 ? elements : null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
renderTextOrHtml(text) {
|
|
565
|
+
if (this.looksLikeHtml(text)) {
|
|
566
|
+
return this.createSandboxedHtml(text);
|
|
528
567
|
}
|
|
529
|
-
|
|
568
|
+
const bubble = document.createElement('div');
|
|
569
|
+
bubble.className = 'message-bubble';
|
|
570
|
+
bubble.textContent = text;
|
|
571
|
+
return bubble;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
createSandboxedHtml(rawHtml) {
|
|
575
|
+
const wrap = document.createElement('div');
|
|
576
|
+
wrap.className = 'html-block rendered-html';
|
|
577
|
+
const content = document.createElement('div');
|
|
578
|
+
content.className = 'html-content';
|
|
579
|
+
content.innerHTML = this.sanitizeHtml(rawHtml);
|
|
580
|
+
wrap.appendChild(content);
|
|
581
|
+
return wrap;
|
|
530
582
|
}
|
|
531
583
|
|
|
532
584
|
addMessageToDisplay(msg) {
|
|
@@ -624,7 +676,8 @@ class GMGUIApp {
|
|
|
624
676
|
const conv = this.conversations.get(this.currentConversation);
|
|
625
677
|
|
|
626
678
|
const idempotencyKey = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
627
|
-
|
|
679
|
+
const tempId = `pending-${idempotencyKey}`;
|
|
680
|
+
this.addMessageToDisplay({ role: 'user', content: message, id: tempId });
|
|
628
681
|
input.value = '';
|
|
629
682
|
this.updateSendButtonState();
|
|
630
683
|
|
|
@@ -646,6 +699,8 @@ class GMGUIApp {
|
|
|
646
699
|
return;
|
|
647
700
|
}
|
|
648
701
|
const data = await res.json();
|
|
702
|
+
const optimisticEl = document.querySelector(`[data-message-id="${tempId}"]`);
|
|
703
|
+
if (optimisticEl) optimisticEl.dataset.messageId = data.message.id;
|
|
649
704
|
this.idempotencyKeys.set(idempotencyKey, data.session.id);
|
|
650
705
|
this.startPollingMessages(this.currentConversation);
|
|
651
706
|
} catch (e) {
|
|
@@ -665,8 +720,12 @@ class GMGUIApp {
|
|
|
665
720
|
if (this.pollingInterval) clearInterval(this.pollingInterval);
|
|
666
721
|
|
|
667
722
|
let pollCount = 0;
|
|
668
|
-
const maxNoResponsePolls = 60;
|
|
669
|
-
let
|
|
723
|
+
const maxNoResponsePolls = 60;
|
|
724
|
+
let lastKnownIds = new Set(
|
|
725
|
+
Array.from(document.querySelectorAll('#chatMessages [data-message-id]'))
|
|
726
|
+
.map(el => el.dataset.messageId)
|
|
727
|
+
.filter(id => id && !id.startsWith('pending-'))
|
|
728
|
+
);
|
|
670
729
|
|
|
671
730
|
this.pollingInterval = setInterval(async () => {
|
|
672
731
|
try {
|
|
@@ -674,17 +733,19 @@ class GMGUIApp {
|
|
|
674
733
|
const data = await res.json();
|
|
675
734
|
const messages = data.messages || [];
|
|
676
735
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
newMessages.forEach(msg => {
|
|
736
|
+
let added = false;
|
|
737
|
+
messages.forEach(msg => {
|
|
738
|
+
if (msg.id && !lastKnownIds.has(msg.id)) {
|
|
681
739
|
const existingEl = document.querySelector(`[data-message-id="${msg.id}"]`);
|
|
682
740
|
if (!existingEl) {
|
|
683
741
|
this.addMessageToDisplay(msg);
|
|
742
|
+
added = true;
|
|
684
743
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
744
|
+
lastKnownIds.add(msg.id);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
if (added) {
|
|
748
|
+
pollCount = 0;
|
|
688
749
|
|
|
689
750
|
if (this.settings.autoScroll) {
|
|
690
751
|
const div = document.getElementById('chatMessages');
|
|
@@ -775,7 +836,7 @@ class GMGUIApp {
|
|
|
775
836
|
|
|
776
837
|
createHtmlBlock(event) {
|
|
777
838
|
const wrap = document.createElement('div');
|
|
778
|
-
wrap.className = 'html-block';
|
|
839
|
+
wrap.className = 'html-block rendered-html';
|
|
779
840
|
if (event.id) wrap.id = `html-${event.id}`;
|
|
780
841
|
if (event.title) {
|
|
781
842
|
const header = document.createElement('div');
|
|
@@ -785,7 +846,7 @@ class GMGUIApp {
|
|
|
785
846
|
}
|
|
786
847
|
const content = document.createElement('div');
|
|
787
848
|
content.className = 'html-content';
|
|
788
|
-
content.innerHTML = event.html;
|
|
849
|
+
content.innerHTML = this.sanitizeHtml(event.html);
|
|
789
850
|
wrap.appendChild(content);
|
|
790
851
|
return wrap;
|
|
791
852
|
}
|
|
@@ -812,22 +873,22 @@ class GMGUIApp {
|
|
|
812
873
|
|
|
813
874
|
updateSendButtonState() {
|
|
814
875
|
const input = document.getElementById('messageInput');
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
876
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
877
|
+
if (sendBtn) sendBtn.disabled = !input || !input.value.trim();
|
|
817
878
|
}
|
|
818
879
|
|
|
819
880
|
openFolderBrowser() {
|
|
820
|
-
const
|
|
821
|
-
if (!
|
|
881
|
+
const dlgModal = document.getElementById('folderBrowserModal');
|
|
882
|
+
if (!dlgModal) return;
|
|
822
883
|
const pathInput = document.getElementById('folderPath');
|
|
823
884
|
pathInput.value = '~/';
|
|
824
885
|
this.loadFolderContents(this.expandHome('~/'));
|
|
825
|
-
|
|
886
|
+
dlgModal.classList.add('active');
|
|
826
887
|
}
|
|
827
888
|
|
|
828
889
|
closeFolderBrowser() {
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
890
|
+
const dlgModal = document.getElementById('folderBrowserModal');
|
|
891
|
+
if (dlgModal) dlgModal.classList.remove('active');
|
|
831
892
|
}
|
|
832
893
|
|
|
833
894
|
async loadFolderContents(folderPath) {
|
|
@@ -895,13 +956,13 @@ function escapeHtml(text) {
|
|
|
895
956
|
}
|
|
896
957
|
|
|
897
958
|
function showNewChatModal() {
|
|
898
|
-
const
|
|
899
|
-
if (
|
|
959
|
+
const dlgModal = document.getElementById('newChatModal');
|
|
960
|
+
if (dlgModal) dlgModal.classList.add('active');
|
|
900
961
|
}
|
|
901
962
|
|
|
902
963
|
function closeNewChatModal() {
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
964
|
+
const dlgModal = document.getElementById('newChatModal');
|
|
965
|
+
if (dlgModal) dlgModal.classList.remove('active');
|
|
905
966
|
}
|
|
906
967
|
|
|
907
968
|
function createChatInWorkspace() {
|