agentgui 1.0.239 → 1.0.241
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 +27 -0
- package/package.json +1 -1
- package/server.js +728 -1
- package/static/index.html +69 -1
- package/static/js/client.js +88 -5
- package/static/js/event-consolidator.js +5 -2
- package/static/js/features.js +14 -0
- package/static/js/streaming-renderer.js +16 -0
package/database.js
CHANGED
|
@@ -308,6 +308,11 @@ export const queries = {
|
|
|
308
308
|
return stmt.all('deleted');
|
|
309
309
|
},
|
|
310
310
|
|
|
311
|
+
getConversations() {
|
|
312
|
+
const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
|
|
313
|
+
return stmt.all('deleted');
|
|
314
|
+
},
|
|
315
|
+
|
|
311
316
|
updateConversation(id, data) {
|
|
312
317
|
const conv = this.getConversation(id);
|
|
313
318
|
if (!conv) return null;
|
|
@@ -568,6 +573,28 @@ export const queries = {
|
|
|
568
573
|
return stmt.all();
|
|
569
574
|
},
|
|
570
575
|
|
|
576
|
+
getSessionsByConversation(conversationId, limit = 10, offset = 0) {
|
|
577
|
+
const stmt = prep(
|
|
578
|
+
'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
|
|
579
|
+
);
|
|
580
|
+
return stmt.all(conversationId, limit, offset);
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
getAllSessions(limit = 100) {
|
|
584
|
+
const stmt = prep(
|
|
585
|
+
'SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?'
|
|
586
|
+
);
|
|
587
|
+
return stmt.all(limit);
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
deleteSession(id) {
|
|
591
|
+
const stmt = prep('DELETE FROM sessions WHERE id = ?');
|
|
592
|
+
const result = stmt.run(id);
|
|
593
|
+
prep('DELETE FROM chunks WHERE sessionId = ?').run(id);
|
|
594
|
+
prep('DELETE FROM events WHERE sessionId = ?').run(id);
|
|
595
|
+
return result.changes || 0;
|
|
596
|
+
},
|
|
597
|
+
|
|
571
598
|
createEvent(type, data, conversationId = null, sessionId = null) {
|
|
572
599
|
const id = generateId('evt');
|
|
573
600
|
const now = Date.now();
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1217,7 +1217,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1217
1217
|
startTime: Date.now(),
|
|
1218
1218
|
duration: 0,
|
|
1219
1219
|
eventCount: 0
|
|
1220
|
-
|
|
1220
|
+
}
|
|
1221
1221
|
};
|
|
1222
1222
|
|
|
1223
1223
|
if (filterType) {
|
|
@@ -1231,6 +1231,163 @@ const server = http.createServer(async (req, res) => {
|
|
|
1231
1231
|
return;
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
|
+
const runsMatch = pathOnly.match(/^\/api\/runs$/);
|
|
1235
|
+
if (runsMatch && req.method === 'POST') {
|
|
1236
|
+
let body = '';
|
|
1237
|
+
for await (const chunk of req) { body += chunk; }
|
|
1238
|
+
let parsed = {};
|
|
1239
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1240
|
+
|
|
1241
|
+
const { input, agentId, webhook } = parsed;
|
|
1242
|
+
if (!input) {
|
|
1243
|
+
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const resolvedAgentId = agentId || 'claude-code';
|
|
1248
|
+
const resolvedModel = parsed.model || null;
|
|
1249
|
+
const cwd = parsed.workingDirectory || STARTUP_CWD;
|
|
1250
|
+
|
|
1251
|
+
const thread = queries.createConversation(resolvedAgentId, 'Stateless Run', cwd);
|
|
1252
|
+
const session = queries.createSession(thread.id, resolvedAgentId, 'pending');
|
|
1253
|
+
const message = queries.createMessage(thread.id, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
1254
|
+
|
|
1255
|
+
processMessageWithStreaming(thread.id, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
1256
|
+
|
|
1257
|
+
sendJSON(req, res, 200, {
|
|
1258
|
+
id: session.id,
|
|
1259
|
+
status: 'pending',
|
|
1260
|
+
started_at: session.started_at,
|
|
1261
|
+
agentId: resolvedAgentId
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const runsSearchMatch = pathOnly.match(/^\/api\/runs\/search$/);
|
|
1267
|
+
if (runsSearchMatch && req.method === 'POST') {
|
|
1268
|
+
const sessions = queries.getAllSessions();
|
|
1269
|
+
const runs = sessions.slice(0, 50).map(s => ({
|
|
1270
|
+
id: s.id,
|
|
1271
|
+
status: s.status,
|
|
1272
|
+
started_at: s.started_at,
|
|
1273
|
+
completed_at: s.completed_at,
|
|
1274
|
+
agentId: s.agentId,
|
|
1275
|
+
input: null,
|
|
1276
|
+
output: null
|
|
1277
|
+
})).reverse();
|
|
1278
|
+
sendJSON(req, res, 200, runs);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const runByIdMatch = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1283
|
+
if (runByIdMatch) {
|
|
1284
|
+
const runId = runByIdMatch[1];
|
|
1285
|
+
const session = queries.getSession(runId);
|
|
1286
|
+
|
|
1287
|
+
if (!session) {
|
|
1288
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (req.method === 'GET') {
|
|
1293
|
+
sendJSON(req, res, 200, {
|
|
1294
|
+
id: session.id,
|
|
1295
|
+
status: session.status,
|
|
1296
|
+
started_at: session.started_at,
|
|
1297
|
+
completed_at: session.completed_at,
|
|
1298
|
+
agentId: session.agentId,
|
|
1299
|
+
input: null,
|
|
1300
|
+
output: null
|
|
1301
|
+
});
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (req.method === 'DELETE') {
|
|
1306
|
+
queries.deleteSession(runId);
|
|
1307
|
+
sendJSON(req, res, 204, {});
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (req.method === 'POST') {
|
|
1312
|
+
if (session.status !== 'interrupted') {
|
|
1313
|
+
sendJSON(req, res, 409, { error: 'Can only resume interrupted runs' });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
let body = '';
|
|
1318
|
+
for await (const chunk of req) { body += chunk; }
|
|
1319
|
+
let parsed = {};
|
|
1320
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1321
|
+
|
|
1322
|
+
const { input } = parsed;
|
|
1323
|
+
if (!input) {
|
|
1324
|
+
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const conv = queries.getConversation(session.conversationId);
|
|
1329
|
+
const resolvedAgentId = session.agentId || conv?.agentId || 'claude-code';
|
|
1330
|
+
const resolvedModel = conv?.model || null;
|
|
1331
|
+
const cwd = conv?.workingDirectory || STARTUP_CWD;
|
|
1332
|
+
|
|
1333
|
+
queries.updateSession(runId, { status: 'pending' });
|
|
1334
|
+
|
|
1335
|
+
const message = queries.createMessage(session.conversationId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
1336
|
+
|
|
1337
|
+
processMessageWithStreaming(session.conversationId, message.id, runId, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
1338
|
+
|
|
1339
|
+
sendJSON(req, res, 200, {
|
|
1340
|
+
id: session.id,
|
|
1341
|
+
status: 'pending',
|
|
1342
|
+
started_at: session.started_at,
|
|
1343
|
+
agentId: resolvedAgentId
|
|
1344
|
+
});
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const runCancelMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
1350
|
+
if (runCancelMatch && req.method === 'POST') {
|
|
1351
|
+
const runId = runCancelMatch[1];
|
|
1352
|
+
const session = queries.getSession(runId);
|
|
1353
|
+
|
|
1354
|
+
if (!session) {
|
|
1355
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const conversationId = session.conversationId;
|
|
1360
|
+
const entry = activeExecutions.get(conversationId);
|
|
1361
|
+
|
|
1362
|
+
if (entry && entry.sessionId === runId) {
|
|
1363
|
+
const { pid } = entry;
|
|
1364
|
+
if (pid) {
|
|
1365
|
+
try {
|
|
1366
|
+
process.kill(-pid, 'SIGKILL');
|
|
1367
|
+
} catch {
|
|
1368
|
+
try {
|
|
1369
|
+
process.kill(pid, 'SIGKILL');
|
|
1370
|
+
} catch (e) {}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
queries.updateSession(runId, { status: 'interrupted', completed_at: Date.now() });
|
|
1376
|
+
queries.setIsStreaming(conversationId, false);
|
|
1377
|
+
activeExecutions.delete(conversationId);
|
|
1378
|
+
|
|
1379
|
+
broadcastSync({
|
|
1380
|
+
type: 'streaming_complete',
|
|
1381
|
+
sessionId: runId,
|
|
1382
|
+
conversationId,
|
|
1383
|
+
interrupted: true,
|
|
1384
|
+
timestamp: Date.now()
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
sendJSON(req, res, 204, {});
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1234
1391
|
const scriptsMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/scripts$/);
|
|
1235
1392
|
if (scriptsMatch && req.method === 'GET') {
|
|
1236
1393
|
const conv = queries.getConversation(scriptsMatch[1]);
|
|
@@ -1307,11 +1464,227 @@ const server = http.createServer(async (req, res) => {
|
|
|
1307
1464
|
return;
|
|
1308
1465
|
}
|
|
1309
1466
|
|
|
1467
|
+
const cancelRunMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/cancel$/);
|
|
1468
|
+
if (cancelRunMatch && req.method === 'POST') {
|
|
1469
|
+
const conversationId = cancelRunMatch[1];
|
|
1470
|
+
const entry = activeExecutions.get(conversationId);
|
|
1471
|
+
|
|
1472
|
+
if (!entry) {
|
|
1473
|
+
sendJSON(req, res, 404, { error: 'No active execution to cancel' });
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const { pid, sessionId } = entry;
|
|
1478
|
+
|
|
1479
|
+
if (pid) {
|
|
1480
|
+
try {
|
|
1481
|
+
process.kill(-pid, 'SIGKILL');
|
|
1482
|
+
} catch {
|
|
1483
|
+
try {
|
|
1484
|
+
process.kill(pid, 'SIGKILL');
|
|
1485
|
+
} catch (e) {}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (sessionId) {
|
|
1490
|
+
queries.updateSession(sessionId, {
|
|
1491
|
+
status: 'interrupted',
|
|
1492
|
+
completed_at: Date.now()
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
queries.setIsStreaming(conversationId, false);
|
|
1497
|
+
activeExecutions.delete(conversationId);
|
|
1498
|
+
|
|
1499
|
+
broadcastSync({
|
|
1500
|
+
type: 'streaming_complete',
|
|
1501
|
+
sessionId,
|
|
1502
|
+
conversationId,
|
|
1503
|
+
interrupted: true,
|
|
1504
|
+
timestamp: Date.now()
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
sendJSON(req, res, 200, { ok: true, cancelled: true, conversationId, sessionId });
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const resumeRunMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/resume$/);
|
|
1512
|
+
if (resumeRunMatch && req.method === 'POST') {
|
|
1513
|
+
const conversationId = resumeRunMatch[1];
|
|
1514
|
+
const conv = queries.getConversation(conversationId);
|
|
1515
|
+
|
|
1516
|
+
if (!conv) {
|
|
1517
|
+
sendJSON(req, res, 404, { error: 'Conversation not found' });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const activeEntry = activeExecutions.get(conversationId);
|
|
1522
|
+
if (activeEntry) {
|
|
1523
|
+
sendJSON(req, res, 409, { error: 'Conversation already has an active execution' });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
let body = '';
|
|
1528
|
+
for await (const chunk of req) { body += chunk; }
|
|
1529
|
+
let parsed = {};
|
|
1530
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1531
|
+
|
|
1532
|
+
const { content, agentId } = parsed;
|
|
1533
|
+
if (!content) {
|
|
1534
|
+
sendJSON(req, res, 400, { error: 'Missing content in request body' });
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const resolvedAgentId = agentId || conv.agentId || 'claude-code';
|
|
1539
|
+
const resolvedModel = parsed.model || conv.model || null;
|
|
1540
|
+
const cwd = conv.workingDirectory || STARTUP_CWD;
|
|
1541
|
+
|
|
1542
|
+
const session = queries.createSession(conversationId, resolvedAgentId, 'pending');
|
|
1543
|
+
|
|
1544
|
+
const message = queries.createMessage(conversationId, 'user', content);
|
|
1545
|
+
|
|
1546
|
+
processMessageWithStreaming(conversationId, message.id, session.id, content, resolvedAgentId, resolvedModel);
|
|
1547
|
+
|
|
1548
|
+
sendJSON(req, res, 200, {
|
|
1549
|
+
ok: true,
|
|
1550
|
+
conversationId,
|
|
1551
|
+
sessionId: session.id,
|
|
1552
|
+
messageId: message.id,
|
|
1553
|
+
resumed: true
|
|
1554
|
+
});
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
const injectMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/inject$/);
|
|
1559
|
+
if (injectMatch && req.method === 'POST') {
|
|
1560
|
+
const conversationId = injectMatch[1];
|
|
1561
|
+
const conv = queries.getConversation(conversationId);
|
|
1562
|
+
|
|
1563
|
+
if (!conv) {
|
|
1564
|
+
sendJSON(req, res, 404, { error: 'Conversation not found' });
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
let body = '';
|
|
1569
|
+
for await (const chunk of req) { body += chunk; }
|
|
1570
|
+
let parsed = {};
|
|
1571
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1572
|
+
|
|
1573
|
+
const { content, eager } = parsed;
|
|
1574
|
+
if (!content) {
|
|
1575
|
+
sendJSON(req, res, 400, { error: 'Missing content in request body' });
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
const entry = activeExecutions.get(conversationId);
|
|
1580
|
+
|
|
1581
|
+
if (entry && eager) {
|
|
1582
|
+
sendJSON(req, res, 409, { error: 'Cannot eagerly inject while execution is running - message queued' });
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const message = queries.createMessage(conversationId, 'user', '[INJECTED] ' + content);
|
|
1587
|
+
|
|
1588
|
+
if (!entry) {
|
|
1589
|
+
const resolvedAgentId = conv.agentId || 'claude-code';
|
|
1590
|
+
const resolvedModel = conv.model || null;
|
|
1591
|
+
const cwd = conv.workingDirectory || STARTUP_CWD;
|
|
1592
|
+
const session = queries.createSession(conversationId, resolvedAgentId, 'pending');
|
|
1593
|
+
processMessageWithStreaming(conversationId, message.id, session.id, message.content, resolvedAgentId, resolvedModel);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
sendJSON(req, res, 200, { ok: true, injected: true, conversationId, messageId: message.id });
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1310
1600
|
if (pathOnly === '/api/agents' && req.method === 'GET') {
|
|
1311
1601
|
sendJSON(req, res, 200, { agents: discoveredAgents });
|
|
1312
1602
|
return;
|
|
1313
1603
|
}
|
|
1314
1604
|
|
|
1605
|
+
const agentsSearchMatch = pathOnly.match(/^\/api\/agents\/search$/);
|
|
1606
|
+
if (agentsSearchMatch && req.method === 'POST') {
|
|
1607
|
+
let body = '';
|
|
1608
|
+
for await (const chunk of req) { body += chunk; }
|
|
1609
|
+
let parsed = {};
|
|
1610
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1611
|
+
|
|
1612
|
+
const { query } = parsed;
|
|
1613
|
+
let results = discoveredAgents;
|
|
1614
|
+
|
|
1615
|
+
if (query) {
|
|
1616
|
+
const q = query.toLowerCase();
|
|
1617
|
+
results = discoveredAgents.filter(a =>
|
|
1618
|
+
a.name.toLowerCase().includes(q) ||
|
|
1619
|
+
a.id.toLowerCase().includes(q) ||
|
|
1620
|
+
(a.description && a.description.toLowerCase().includes(q))
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const agents = results.map(a => ({
|
|
1625
|
+
id: a.id,
|
|
1626
|
+
name: a.name,
|
|
1627
|
+
description: a.description || '',
|
|
1628
|
+
icon: a.icon || null,
|
|
1629
|
+
status: 'available'
|
|
1630
|
+
}));
|
|
1631
|
+
|
|
1632
|
+
sendJSON(req, res, 200, agents);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const agentByIdMatch = pathOnly.match(/^\/api\/agents\/([^/]+)$/);
|
|
1637
|
+
if (agentByIdMatch && req.method === 'GET') {
|
|
1638
|
+
const agentId = agentByIdMatch[1];
|
|
1639
|
+
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
1640
|
+
|
|
1641
|
+
if (!agent) {
|
|
1642
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
sendJSON(req, res, 200, {
|
|
1647
|
+
id: agent.id,
|
|
1648
|
+
name: agent.name,
|
|
1649
|
+
description: agent.description || '',
|
|
1650
|
+
icon: agent.icon || null,
|
|
1651
|
+
status: 'available'
|
|
1652
|
+
});
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const agentDescriptorMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/descriptor$/);
|
|
1657
|
+
if (agentDescriptorMatch && req.method === 'GET') {
|
|
1658
|
+
const agentId = agentDescriptorMatch[1];
|
|
1659
|
+
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
1660
|
+
|
|
1661
|
+
if (!agent) {
|
|
1662
|
+
sendJSON(req, res, 404, { error: 'Agent not found' });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
sendJSON(req, res, 200, {
|
|
1667
|
+
agentId: agent.id,
|
|
1668
|
+
agentName: agent.name,
|
|
1669
|
+
protocol: agent.protocol || 'direct',
|
|
1670
|
+
capabilities: {
|
|
1671
|
+
streaming: true,
|
|
1672
|
+
cancel: true,
|
|
1673
|
+
resume: agent.protocol === 'direct',
|
|
1674
|
+
stateful: true
|
|
1675
|
+
},
|
|
1676
|
+
inputSchema: {
|
|
1677
|
+
type: 'object',
|
|
1678
|
+
properties: {
|
|
1679
|
+
content: { type: 'string', description: 'The prompt to send to the agent' }
|
|
1680
|
+
},
|
|
1681
|
+
required: ['content']
|
|
1682
|
+
},
|
|
1683
|
+
stateFormat: 'opaque'
|
|
1684
|
+
});
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1315
1688
|
const modelsMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/models$/);
|
|
1316
1689
|
if (modelsMatch && req.method === 'GET') {
|
|
1317
1690
|
const agentId = modelsMatch[1];
|
|
@@ -1584,6 +1957,336 @@ const server = http.createServer(async (req, res) => {
|
|
|
1584
1957
|
return;
|
|
1585
1958
|
}
|
|
1586
1959
|
|
|
1960
|
+
const threadsMatch = pathOnly.match(/^\/api\/threads$/);
|
|
1961
|
+
if (threadsMatch && req.method === 'POST') {
|
|
1962
|
+
let body = '';
|
|
1963
|
+
for await (const chunk of req) { body += chunk; }
|
|
1964
|
+
let parsed = {};
|
|
1965
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1966
|
+
|
|
1967
|
+
const thread = queries.createConversation(parsed.agentId || 'claude-code', parsed.title || 'New Thread', parsed.workingDirectory || STARTUP_CWD);
|
|
1968
|
+
sendJSON(req, res, 200, {
|
|
1969
|
+
id: thread.id,
|
|
1970
|
+
agentId: thread.agentId,
|
|
1971
|
+
title: thread.title,
|
|
1972
|
+
created_at: thread.created_at,
|
|
1973
|
+
status: thread.status,
|
|
1974
|
+
state: null
|
|
1975
|
+
});
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const threadsSearchMatch = pathOnly.match(/^\/api\/threads\/search$/);
|
|
1980
|
+
if (threadsSearchMatch && req.method === 'POST') {
|
|
1981
|
+
const conversations = queries.getConversations();
|
|
1982
|
+
const threads = conversations.map(c => ({
|
|
1983
|
+
id: c.id,
|
|
1984
|
+
agentId: c.agentId,
|
|
1985
|
+
title: c.title,
|
|
1986
|
+
created_at: c.created_at,
|
|
1987
|
+
updated_at: c.updated_at,
|
|
1988
|
+
status: c.status,
|
|
1989
|
+
state: null
|
|
1990
|
+
}));
|
|
1991
|
+
sendJSON(req, res, 200, threads);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const threadByIdMatch = pathOnly.match(/^\/api\/threads\/([^/]+)$/);
|
|
1996
|
+
if (threadByIdMatch) {
|
|
1997
|
+
const threadId = threadByIdMatch[1];
|
|
1998
|
+
const conv = queries.getConversation(threadId);
|
|
1999
|
+
|
|
2000
|
+
if (!conv) {
|
|
2001
|
+
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
if (req.method === 'GET') {
|
|
2006
|
+
sendJSON(req, res, 200, {
|
|
2007
|
+
id: conv.id,
|
|
2008
|
+
agentId: conv.agentId,
|
|
2009
|
+
title: conv.title,
|
|
2010
|
+
created_at: conv.created_at,
|
|
2011
|
+
updated_at: conv.updated_at,
|
|
2012
|
+
status: conv.status,
|
|
2013
|
+
state: null
|
|
2014
|
+
});
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
if (req.method === 'DELETE') {
|
|
2019
|
+
const activeEntry = activeExecutions.get(threadId);
|
|
2020
|
+
if (activeEntry) {
|
|
2021
|
+
sendJSON(req, res, 409, { error: 'Thread has an active run, cannot delete' });
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
queries.deleteConversation(threadId);
|
|
2025
|
+
sendJSON(req, res, 204, {});
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
if (req.method === 'PATCH') {
|
|
2030
|
+
let body = '';
|
|
2031
|
+
for await (const chunk of req) { body += chunk; }
|
|
2032
|
+
let parsed = {};
|
|
2033
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2034
|
+
|
|
2035
|
+
const updates = {};
|
|
2036
|
+
if (parsed.title !== undefined) updates.title = parsed.title;
|
|
2037
|
+
if (parsed.state !== undefined) updates.state = parsed.state;
|
|
2038
|
+
|
|
2039
|
+
if (Object.keys(updates).length > 0) {
|
|
2040
|
+
queries.updateConversation(threadId, updates);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const updated = queries.getConversation(threadId);
|
|
2044
|
+
sendJSON(req, res, 200, {
|
|
2045
|
+
id: updated.id,
|
|
2046
|
+
agentId: updated.agentId,
|
|
2047
|
+
title: updated.title,
|
|
2048
|
+
created_at: updated.created_at,
|
|
2049
|
+
updated_at: updated.updated_at,
|
|
2050
|
+
status: updated.status,
|
|
2051
|
+
state: updated.state
|
|
2052
|
+
});
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
const threadHistoryMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/history$/);
|
|
2058
|
+
if (threadHistoryMatch && req.method === 'GET') {
|
|
2059
|
+
const threadId = threadHistoryMatch[1];
|
|
2060
|
+
const conv = queries.getConversation(threadId);
|
|
2061
|
+
|
|
2062
|
+
if (!conv) {
|
|
2063
|
+
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '10', 10);
|
|
2068
|
+
const sessions = queries.getSessionsByConversation(threadId, limit);
|
|
2069
|
+
|
|
2070
|
+
const history = sessions.map(s => ({
|
|
2071
|
+
checkpoint: s.id,
|
|
2072
|
+
state: null,
|
|
2073
|
+
created_at: s.started_at,
|
|
2074
|
+
runId: s.id,
|
|
2075
|
+
status: s.status
|
|
2076
|
+
})).reverse();
|
|
2077
|
+
|
|
2078
|
+
sendJSON(req, res, 200, history);
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const threadCopyMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/copy$/);
|
|
2083
|
+
if (threadCopyMatch && req.method === 'POST') {
|
|
2084
|
+
const threadId = threadCopyMatch[1];
|
|
2085
|
+
const original = queries.getConversation(threadId);
|
|
2086
|
+
|
|
2087
|
+
if (!original) {
|
|
2088
|
+
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const newThread = queries.createConversation(original.agentId, original.title + ' (copy)', original.workingDirectory);
|
|
2093
|
+
|
|
2094
|
+
const messages = queries.getMessages(threadId, 1000, 0);
|
|
2095
|
+
for (const msg of messages) {
|
|
2096
|
+
queries.createMessage(newThread.id, msg.role, msg.content);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
sendJSON(req, res, 200, {
|
|
2100
|
+
id: newThread.id,
|
|
2101
|
+
agentId: newThread.agentId,
|
|
2102
|
+
title: newThread.title,
|
|
2103
|
+
created_at: newThread.created_at,
|
|
2104
|
+
status: newThread.status,
|
|
2105
|
+
state: null
|
|
2106
|
+
});
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
const threadRunsMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs$/);
|
|
2111
|
+
if (threadRunsMatch) {
|
|
2112
|
+
const threadId = threadRunsMatch[1];
|
|
2113
|
+
const conv = queries.getConversation(threadId);
|
|
2114
|
+
|
|
2115
|
+
if (!conv) {
|
|
2116
|
+
sendJSON(req, res, 404, { error: 'Thread not found' });
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
if (req.method === 'GET') {
|
|
2121
|
+
const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '10', 10);
|
|
2122
|
+
const offset = parseInt(new URL(req.url, 'http://localhost').searchParams.get('offset') || '0', 10);
|
|
2123
|
+
const sessions = queries.getSessionsByConversation(threadId, limit, offset);
|
|
2124
|
+
|
|
2125
|
+
const runs = sessions.map(s => ({
|
|
2126
|
+
id: s.id,
|
|
2127
|
+
threadId: s.conversationId,
|
|
2128
|
+
status: s.status,
|
|
2129
|
+
started_at: s.started_at,
|
|
2130
|
+
completed_at: s.completed_at,
|
|
2131
|
+
agentId: s.agentId,
|
|
2132
|
+
input: null,
|
|
2133
|
+
output: null
|
|
2134
|
+
}));
|
|
2135
|
+
|
|
2136
|
+
sendJSON(req, res, 200, runs);
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (req.method === 'POST') {
|
|
2141
|
+
const activeEntry = activeExecutions.get(threadId);
|
|
2142
|
+
if (activeEntry) {
|
|
2143
|
+
sendJSON(req, res, 409, { error: 'Thread already has an active run' });
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
let body = '';
|
|
2148
|
+
for await (const chunk of req) { body += chunk; }
|
|
2149
|
+
let parsed = {};
|
|
2150
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2151
|
+
|
|
2152
|
+
const { input, agentId, webhook } = parsed;
|
|
2153
|
+
if (!input) {
|
|
2154
|
+
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const resolvedAgentId = agentId || conv.agentId || 'claude-code';
|
|
2159
|
+
const resolvedModel = parsed.model || conv.model || null;
|
|
2160
|
+
const cwd = conv.workingDirectory || STARTUP_CWD;
|
|
2161
|
+
|
|
2162
|
+
const session = queries.createSession(threadId, resolvedAgentId, 'pending');
|
|
2163
|
+
const message = queries.createMessage(threadId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
2164
|
+
|
|
2165
|
+
processMessageWithStreaming(threadId, message.id, session.id, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
2166
|
+
|
|
2167
|
+
sendJSON(req, res, 200, {
|
|
2168
|
+
id: session.id,
|
|
2169
|
+
threadId: threadId,
|
|
2170
|
+
status: 'pending',
|
|
2171
|
+
started_at: session.started_at,
|
|
2172
|
+
agentId: resolvedAgentId
|
|
2173
|
+
});
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
const threadRunByIdMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)$/);
|
|
2179
|
+
if (threadRunByIdMatch) {
|
|
2180
|
+
const threadId = threadRunByIdMatch[1];
|
|
2181
|
+
const runId = threadRunByIdMatch[2];
|
|
2182
|
+
const session = queries.getSession(runId);
|
|
2183
|
+
|
|
2184
|
+
if (!session || session.conversationId !== threadId) {
|
|
2185
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if (req.method === 'GET') {
|
|
2190
|
+
sendJSON(req, res, 200, {
|
|
2191
|
+
id: session.id,
|
|
2192
|
+
threadId: session.conversationId,
|
|
2193
|
+
status: session.status,
|
|
2194
|
+
started_at: session.started_at,
|
|
2195
|
+
completed_at: session.completed_at,
|
|
2196
|
+
agentId: session.agentId,
|
|
2197
|
+
input: null,
|
|
2198
|
+
output: null
|
|
2199
|
+
});
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
if (req.method === 'DELETE') {
|
|
2204
|
+
queries.deleteSession(runId);
|
|
2205
|
+
sendJSON(req, res, 204, {});
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
if (req.method === 'POST') {
|
|
2210
|
+
if (session.status !== 'interrupted') {
|
|
2211
|
+
sendJSON(req, res, 409, { error: 'Can only resume interrupted runs' });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
let body = '';
|
|
2216
|
+
for await (const chunk of req) { body += chunk; }
|
|
2217
|
+
let parsed = {};
|
|
2218
|
+
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
2219
|
+
|
|
2220
|
+
const { input } = parsed;
|
|
2221
|
+
if (!input) {
|
|
2222
|
+
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const conv = queries.getConversation(threadId);
|
|
2227
|
+
const resolvedAgentId = session.agentId || conv.agentId || 'claude-code';
|
|
2228
|
+
const resolvedModel = conv?.model || null;
|
|
2229
|
+
const cwd = conv?.workingDirectory || STARTUP_CWD;
|
|
2230
|
+
|
|
2231
|
+
queries.updateSession(runId, { status: 'pending' });
|
|
2232
|
+
|
|
2233
|
+
const message = queries.createMessage(threadId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
2234
|
+
|
|
2235
|
+
processMessageWithStreaming(threadId, message.id, runId, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
2236
|
+
|
|
2237
|
+
sendJSON(req, res, 200, {
|
|
2238
|
+
id: session.id,
|
|
2239
|
+
threadId: threadId,
|
|
2240
|
+
status: 'pending',
|
|
2241
|
+
started_at: session.started_at,
|
|
2242
|
+
agentId: resolvedAgentId
|
|
2243
|
+
});
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const threadRunCancelMatch = pathOnly.match(/^\/api\/threads\/([^/]+)\/runs\/([^/]+)\/cancel$/);
|
|
2249
|
+
if (threadRunCancelMatch && req.method === 'POST') {
|
|
2250
|
+
const threadId = threadRunCancelMatch[1];
|
|
2251
|
+
const runId = threadRunCancelMatch[2];
|
|
2252
|
+
const session = queries.getSession(runId);
|
|
2253
|
+
|
|
2254
|
+
if (!session || session.conversationId !== threadId) {
|
|
2255
|
+
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
const entry = activeExecutions.get(threadId);
|
|
2260
|
+
|
|
2261
|
+
if (entry && entry.sessionId === runId) {
|
|
2262
|
+
const { pid } = entry;
|
|
2263
|
+
if (pid) {
|
|
2264
|
+
try {
|
|
2265
|
+
process.kill(-pid, 'SIGKILL');
|
|
2266
|
+
} catch {
|
|
2267
|
+
try {
|
|
2268
|
+
process.kill(pid, 'SIGKILL');
|
|
2269
|
+
} catch (e) {}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
queries.updateSession(runId, { status: 'interrupted', completed_at: Date.now() });
|
|
2275
|
+
queries.setIsStreaming(threadId, false);
|
|
2276
|
+
activeExecutions.delete(threadId);
|
|
2277
|
+
|
|
2278
|
+
broadcastSync({
|
|
2279
|
+
type: 'streaming_complete',
|
|
2280
|
+
sessionId: runId,
|
|
2281
|
+
conversationId: threadId,
|
|
2282
|
+
interrupted: true,
|
|
2283
|
+
timestamp: Date.now()
|
|
2284
|
+
});
|
|
2285
|
+
|
|
2286
|
+
sendJSON(req, res, 204, {});
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
1587
2290
|
if (pathOnly === '/api/stt' && req.method === 'POST') {
|
|
1588
2291
|
try {
|
|
1589
2292
|
const chunks = [];
|
|
@@ -1716,6 +2419,30 @@ const server = http.createServer(async (req, res) => {
|
|
|
1716
2419
|
return;
|
|
1717
2420
|
}
|
|
1718
2421
|
|
|
2422
|
+
if (pathOnly === '/api/speech-status' && req.method === 'POST') {
|
|
2423
|
+
const body = await parseBody(req);
|
|
2424
|
+
if (body.forceDownload) {
|
|
2425
|
+
modelDownloadState.complete = false;
|
|
2426
|
+
modelDownloadState.downloading = false;
|
|
2427
|
+
modelDownloadState.error = null;
|
|
2428
|
+
ensureModelsDownloaded().then(ok => {
|
|
2429
|
+
broadcastSync({
|
|
2430
|
+
type: 'model_download_progress',
|
|
2431
|
+
progress: { done: true, complete: ok, error: ok ? null : 'Download failed' }
|
|
2432
|
+
});
|
|
2433
|
+
}).catch(err => {
|
|
2434
|
+
broadcastSync({
|
|
2435
|
+
type: 'model_download_progress',
|
|
2436
|
+
progress: { done: true, error: err.message }
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2439
|
+
sendJSON(req, res, 200, { ok: true, message: 'Starting model download' });
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
sendJSON(req, res, 400, { error: 'Unknown request' });
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
1719
2446
|
if (pathOnly === '/api/clone' && req.method === 'POST') {
|
|
1720
2447
|
const body = await parseBody(req);
|
|
1721
2448
|
const repo = (body.repo || '').trim();
|
package/static/index.html
CHANGED
|
@@ -38,6 +38,16 @@
|
|
|
38
38
|
--sidebar-width: 300px;
|
|
39
39
|
--header-height: 52px;
|
|
40
40
|
--msg-max-width: 100%;
|
|
41
|
+
--block-color-0: #8b5cf6;
|
|
42
|
+
--block-color-1: #ec4899;
|
|
43
|
+
--block-color-2: #14b8a6;
|
|
44
|
+
--block-color-3: #f97316;
|
|
45
|
+
--block-color-4: #06b6d4;
|
|
46
|
+
--block-color-5: #84cc16;
|
|
47
|
+
--block-color-6: #a855f7;
|
|
48
|
+
--block-color-7: #f43f5e;
|
|
49
|
+
--block-color-8: #22c55e;
|
|
50
|
+
--block-color-9: #eab308;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
html.dark {
|
|
@@ -46,6 +56,16 @@
|
|
|
46
56
|
--color-text-primary: #f9fafb;
|
|
47
57
|
--color-text-secondary: #d1d5db;
|
|
48
58
|
--color-border: #374151;
|
|
59
|
+
--block-color-0: #a78bfa;
|
|
60
|
+
--block-color-1: #f472b6;
|
|
61
|
+
--block-color-2: #2dd4bf;
|
|
62
|
+
--block-color-3: #fb923c;
|
|
63
|
+
--block-color-4: #22d3ee;
|
|
64
|
+
--block-color-5: #a3e635;
|
|
65
|
+
--block-color-6: #c084fc;
|
|
66
|
+
--block-color-7: #fb7185;
|
|
67
|
+
--block-color-8: #4ade80;
|
|
68
|
+
--block-color-9: #facc15;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
html, body {
|
|
@@ -878,6 +898,45 @@
|
|
|
878
898
|
|
|
879
899
|
.send-btn svg { width: 18px; height: 18px; }
|
|
880
900
|
|
|
901
|
+
.stop-btn {
|
|
902
|
+
display: none;
|
|
903
|
+
align-items: center;
|
|
904
|
+
justify-content: center;
|
|
905
|
+
width: 40px;
|
|
906
|
+
height: 40px;
|
|
907
|
+
background-color: #dc2626;
|
|
908
|
+
color: white;
|
|
909
|
+
border: none;
|
|
910
|
+
border-radius: 0.5rem;
|
|
911
|
+
cursor: pointer;
|
|
912
|
+
flex-shrink: 0;
|
|
913
|
+
transition: background-color 0.15s;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.stop-btn:hover:not(:disabled) { background-color: #b91c1c; }
|
|
917
|
+
.stop-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
918
|
+
.stop-btn.visible { display: flex; }
|
|
919
|
+
|
|
920
|
+
.inject-btn {
|
|
921
|
+
display: none;
|
|
922
|
+
align-items: center;
|
|
923
|
+
justify-content: center;
|
|
924
|
+
width: 40px;
|
|
925
|
+
height: 40px;
|
|
926
|
+
background-color: #f59e0b;
|
|
927
|
+
color: white;
|
|
928
|
+
border: none;
|
|
929
|
+
border-radius: 0.5rem;
|
|
930
|
+
cursor: pointer;
|
|
931
|
+
flex-shrink: 0;
|
|
932
|
+
transition: background-color 0.15s;
|
|
933
|
+
margin-right: 0.25rem;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.inject-btn:hover:not(:disabled) { background-color: #d97706; }
|
|
937
|
+
.inject-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
938
|
+
.inject-btn.visible { display: flex; }
|
|
939
|
+
|
|
881
940
|
/* ===== OVERLAY for mobile sidebar ===== */
|
|
882
941
|
.sidebar-overlay {
|
|
883
942
|
display: none;
|
|
@@ -1275,7 +1334,6 @@
|
|
|
1275
1334
|
.streaming-blocks { contain: content; }
|
|
1276
1335
|
.sidebar-list { contain: strict; content-visibility: auto; }
|
|
1277
1336
|
.message { contain: layout style; content-visibility: auto; contain-intrinsic-size: auto 80px; }
|
|
1278
|
-
#output-scroll { will-change: transform; }
|
|
1279
1337
|
|
|
1280
1338
|
.voice-block .voice-result-stats {
|
|
1281
1339
|
font-size: 0.8rem;
|
|
@@ -2489,6 +2547,16 @@
|
|
|
2489
2547
|
aria-label="Message input"
|
|
2490
2548
|
rows="1"
|
|
2491
2549
|
></textarea>
|
|
2550
|
+
<button class="inject-btn" id="injectBtn" title="Inject instructions into running agent" aria-label="Inject instructions">
|
|
2551
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
|
2552
|
+
<path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
|
|
2553
|
+
</svg>
|
|
2554
|
+
</button>
|
|
2555
|
+
<button class="stop-btn" id="stopBtn" title="Stop running agent (emergency)" aria-label="Stop agent">
|
|
2556
|
+
<svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
|
|
2557
|
+
<path d="M6 6h12v12H6z"/>
|
|
2558
|
+
</svg>
|
|
2559
|
+
</button>
|
|
2492
2560
|
<button class="send-btn" data-send-button title="Send message (Ctrl+Enter)" aria-label="Send message">
|
|
2493
2561
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
2494
2562
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
package/static/js/client.js
CHANGED
|
@@ -110,6 +110,9 @@ class AgentGUIClient {
|
|
|
110
110
|
await this.loadAgents();
|
|
111
111
|
await this.loadConversations();
|
|
112
112
|
|
|
113
|
+
// Check speech model status
|
|
114
|
+
await this.checkSpeechStatus();
|
|
115
|
+
|
|
113
116
|
// Enable controls for initial interaction
|
|
114
117
|
this.enableControls();
|
|
115
118
|
|
|
@@ -344,6 +347,43 @@ class AgentGUIClient {
|
|
|
344
347
|
this.ui.sendButton.addEventListener('click', () => this.startExecution());
|
|
345
348
|
}
|
|
346
349
|
|
|
350
|
+
this.ui.stopButton = document.getElementById('stopBtn');
|
|
351
|
+
this.ui.injectButton = document.getElementById('injectBtn');
|
|
352
|
+
|
|
353
|
+
if (this.ui.stopButton) {
|
|
354
|
+
this.ui.stopButton.addEventListener('click', async () => {
|
|
355
|
+
if (!this.state.currentConversation) return;
|
|
356
|
+
try {
|
|
357
|
+
const resp = await fetch(`${window.__BASE_URL}/api/conversations/${this.state.currentConversation.id}/cancel`, {
|
|
358
|
+
method: 'POST'
|
|
359
|
+
});
|
|
360
|
+
const data = await resp.json();
|
|
361
|
+
console.log('Stop response:', data);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.error('Failed to stop:', err);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (this.ui.injectButton) {
|
|
369
|
+
this.ui.injectButton.addEventListener('click', async () => {
|
|
370
|
+
if (!this.state.currentConversation) return;
|
|
371
|
+
const instructions = prompt('Enter instructions to inject into the running agent:');
|
|
372
|
+
if (!instructions) return;
|
|
373
|
+
try {
|
|
374
|
+
const resp = await fetch(`${window.__BASE_URL}/api/conversations/${this.state.currentConversation.id}/inject`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: { 'Content-Type': 'application/json' },
|
|
377
|
+
body: JSON.stringify({ content: instructions })
|
|
378
|
+
});
|
|
379
|
+
const data = await resp.json();
|
|
380
|
+
console.log('Inject response:', data);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
console.error('Failed to inject:', err);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
347
387
|
if (this.ui.messageInput) {
|
|
348
388
|
this.ui.messageInput.addEventListener('keydown', (e) => {
|
|
349
389
|
if (e.key === 'Enter' && e.ctrlKey) {
|
|
@@ -468,6 +508,11 @@ class AgentGUIClient {
|
|
|
468
508
|
this._serverProcessingEstimate = 0.7 * this._serverProcessingEstimate + 0.3 * serverTime;
|
|
469
509
|
}
|
|
470
510
|
|
|
511
|
+
// Show stop and inject buttons when streaming starts
|
|
512
|
+
if (this.ui.stopButton) this.ui.stopButton.classList.add('visible');
|
|
513
|
+
if (this.ui.injectButton) this.ui.injectButton.classList.add('visible');
|
|
514
|
+
if (this.ui.sendButton) this.ui.sendButton.style.display = 'none';
|
|
515
|
+
|
|
471
516
|
// If this streaming event is for a different conversation than what we are viewing,
|
|
472
517
|
// just track the state but do not modify the DOM or start polling
|
|
473
518
|
if (this.state.currentConversation?.id !== data.conversationId) {
|
|
@@ -587,7 +632,7 @@ class AgentGUIClient {
|
|
|
587
632
|
</div>
|
|
588
633
|
`;
|
|
589
634
|
messagesEl.appendChild(streamingDiv);
|
|
590
|
-
this.scrollToBottom();
|
|
635
|
+
this.scrollToBottom(true);
|
|
591
636
|
}
|
|
592
637
|
|
|
593
638
|
// Start polling for chunks from database
|
|
@@ -642,12 +687,12 @@ class AgentGUIClient {
|
|
|
642
687
|
return `<div style="padding:0.5rem;background:var(--color-bg-secondary);border-radius:0.375rem;border:1px solid var(--color-border)"><div style="font-size:0.7rem;font-weight:600;text-transform:uppercase;margin-bottom:0.25rem">${this.escapeHtml(block.type)}</div>${fieldsHtml}</div>`;
|
|
643
688
|
}
|
|
644
689
|
|
|
645
|
-
scrollToBottom() {
|
|
690
|
+
scrollToBottom(force = false) {
|
|
646
691
|
const scrollContainer = document.getElementById('output-scroll');
|
|
647
692
|
if (!scrollContainer) return;
|
|
648
693
|
const distFromBottom = scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight;
|
|
649
694
|
|
|
650
|
-
if (distFromBottom > 150) {
|
|
695
|
+
if (!force && distFromBottom > 150) {
|
|
651
696
|
this._unseenCount = (this._unseenCount || 0) + 1;
|
|
652
697
|
this._showNewContentPill();
|
|
653
698
|
return;
|
|
@@ -656,7 +701,7 @@ class AgentGUIClient {
|
|
|
656
701
|
const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
657
702
|
const isStreaming = this.state.streamingConversations.size > 0;
|
|
658
703
|
|
|
659
|
-
if (!isStreaming || !this._scrollKalman || Math.abs(maxScroll - scrollContainer.scrollTop) > 2000) {
|
|
704
|
+
if (!isStreaming || !this._scrollKalman || Math.abs(maxScroll - scrollContainer.scrollTop) > 2000 || force) {
|
|
660
705
|
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
661
706
|
this._removeNewContentPill();
|
|
662
707
|
this._scrollAnimating = false;
|
|
@@ -720,6 +765,11 @@ class AgentGUIClient {
|
|
|
720
765
|
console.error('Streaming error:', data);
|
|
721
766
|
this._clearThinkingCountdown();
|
|
722
767
|
|
|
768
|
+
// Hide stop and inject buttons on error
|
|
769
|
+
if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
|
|
770
|
+
if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
|
|
771
|
+
if (this.ui.sendButton) this.ui.sendButton.style.display = '';
|
|
772
|
+
|
|
723
773
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
724
774
|
|
|
725
775
|
// If this event is for a conversation we are NOT currently viewing, just track state
|
|
@@ -752,6 +802,11 @@ class AgentGUIClient {
|
|
|
752
802
|
console.log('Streaming completed:', data);
|
|
753
803
|
this._clearThinkingCountdown();
|
|
754
804
|
|
|
805
|
+
// Hide stop and inject buttons when streaming completes
|
|
806
|
+
if (this.ui.stopButton) this.ui.stopButton.classList.remove('visible');
|
|
807
|
+
if (this.ui.injectButton) this.ui.injectButton.classList.remove('visible');
|
|
808
|
+
if (this.ui.sendButton) this.ui.sendButton.style.display = '';
|
|
809
|
+
|
|
755
810
|
const conversationId = data.conversationId || this.state.currentSession?.conversationId;
|
|
756
811
|
if (conversationId) this.invalidateCache(conversationId);
|
|
757
812
|
|
|
@@ -1220,7 +1275,7 @@ class AgentGUIClient {
|
|
|
1220
1275
|
div.id = pendingId;
|
|
1221
1276
|
div.innerHTML = `<div class="message-role">User</div><div class="message-text">${this.escapeHtml(content)}</div><div class="message-timestamp" style="opacity:0.5">Sending...</div>`;
|
|
1222
1277
|
messagesEl.appendChild(div);
|
|
1223
|
-
this.scrollToBottom();
|
|
1278
|
+
this.scrollToBottom(true);
|
|
1224
1279
|
}
|
|
1225
1280
|
|
|
1226
1281
|
_confirmOptimisticMessage(pendingId) {
|
|
@@ -1781,6 +1836,23 @@ class AgentGUIClient {
|
|
|
1781
1836
|
});
|
|
1782
1837
|
}
|
|
1783
1838
|
|
|
1839
|
+
async checkSpeechStatus() {
|
|
1840
|
+
try {
|
|
1841
|
+
const response = await fetch(window.__BASE_URL + '/api/speech-status');
|
|
1842
|
+
const status = await response.json();
|
|
1843
|
+
if (status.modelsComplete) {
|
|
1844
|
+
this._modelDownloadProgress = { done: true, complete: true };
|
|
1845
|
+
this._modelDownloadInProgress = false;
|
|
1846
|
+
} else if (status.modelsDownloading) {
|
|
1847
|
+
this._modelDownloadProgress = status.modelsProgress || { downloading: true };
|
|
1848
|
+
this._modelDownloadInProgress = true;
|
|
1849
|
+
}
|
|
1850
|
+
this._updateVoiceTabState();
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
console.error('Failed to check speech status:', error);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1784
1856
|
async loadModelsForAgent(agentId) {
|
|
1785
1857
|
if (!agentId || !this.ui.modelSelector) return;
|
|
1786
1858
|
const cached = this._modelCache.get(agentId);
|
|
@@ -1931,6 +2003,7 @@ class AgentGUIClient {
|
|
|
1931
2003
|
this._modelDownloadInProgress = false;
|
|
1932
2004
|
console.log('[Models] Download complete');
|
|
1933
2005
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2006
|
+
this._updateVoiceTabState();
|
|
1934
2007
|
return;
|
|
1935
2008
|
}
|
|
1936
2009
|
|
|
@@ -1940,6 +2013,16 @@ class AgentGUIClient {
|
|
|
1940
2013
|
}
|
|
1941
2014
|
}
|
|
1942
2015
|
|
|
2016
|
+
_updateVoiceTabState() {
|
|
2017
|
+
var voiceBtn = document.querySelector('[data-view="voice"]');
|
|
2018
|
+
if (voiceBtn) {
|
|
2019
|
+
var isReady = this._modelDownloadProgress?.done === true ||
|
|
2020
|
+
this._modelDownloadProgress?.complete === true;
|
|
2021
|
+
voiceBtn.disabled = !isReady;
|
|
2022
|
+
voiceBtn.title = isReady ? 'Voice' : 'Downloading voice models...';
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1943
2026
|
_toggleConnectionTooltip() {
|
|
1944
2027
|
let tooltip = document.getElementById('connection-tooltip');
|
|
1945
2028
|
if (tooltip) { tooltip.remove(); return; }
|
|
@@ -42,11 +42,14 @@ class EventConsolidator {
|
|
|
42
42
|
for (const c of chunks) {
|
|
43
43
|
if (c.block?.type === 'text') {
|
|
44
44
|
if (pending) {
|
|
45
|
-
const
|
|
45
|
+
const pendingText = pending.block.text || '';
|
|
46
|
+
const newText = c.block.text || '';
|
|
47
|
+
const combined = pendingText + newText;
|
|
46
48
|
if (combined.length <= MAX_MERGE) {
|
|
49
|
+
const needsSpace = pendingText.length > 0 && !pendingText.endsWith(' ') && !pendingText.endsWith('\n') && newText.length > 0 && !newText.startsWith(' ') && !newText.startsWith('\n');
|
|
47
50
|
pending = {
|
|
48
51
|
...pending,
|
|
49
|
-
block: { ...pending.block, text: combined },
|
|
52
|
+
block: { ...pending.block, text: needsSpace ? pendingText + ' ' + newText : combined },
|
|
50
53
|
created_at: c.created_at,
|
|
51
54
|
_mergedSequences: [...(pending._mergedSequences || [pending.sequence]), c.sequence]
|
|
52
55
|
};
|
package/static/js/features.js
CHANGED
|
@@ -188,11 +188,25 @@
|
|
|
188
188
|
buttons.forEach(function(btn) {
|
|
189
189
|
btn.addEventListener('click', function() {
|
|
190
190
|
var view = btn.dataset.view;
|
|
191
|
+
if (view === 'voice' && !isVoiceReady()) {
|
|
192
|
+
showToast('Downloading voice models... please wait', 'info');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
191
195
|
switchView(view);
|
|
192
196
|
});
|
|
193
197
|
});
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
function isVoiceReady() {
|
|
201
|
+
if (window.agentGUIClient && window.agentGUIClient._modelDownloadInProgress === false) {
|
|
202
|
+
return window.agentGUIClient._modelDownloadProgress?.done === true ||
|
|
203
|
+
window.agentGUIClient._modelDownloadProgress?.complete === true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
window.__checkVoiceReady = isVoiceReady;
|
|
209
|
+
|
|
196
210
|
function switchView(view) {
|
|
197
211
|
currentView = view;
|
|
198
212
|
var bar = document.getElementById('viewToggleBar');
|
|
@@ -399,9 +399,25 @@ class StreamingRenderer {
|
|
|
399
399
|
div.className = 'block-text';
|
|
400
400
|
if (isHtml) div.classList.add('html-content');
|
|
401
401
|
div.innerHTML = html;
|
|
402
|
+
const colorIndex = this._getUniqueColorIndex(text);
|
|
403
|
+
div.style.borderLeft = `3px solid var(--block-color-${colorIndex})`;
|
|
402
404
|
return div;
|
|
403
405
|
}
|
|
404
406
|
|
|
407
|
+
_getUniqueColorIndex(text) {
|
|
408
|
+
if (!this._colorIndexMap) {
|
|
409
|
+
this._colorIndexMap = new Map();
|
|
410
|
+
this._colorCounter = 0;
|
|
411
|
+
}
|
|
412
|
+
let index = this._colorIndexMap.get(text);
|
|
413
|
+
if (index === undefined) {
|
|
414
|
+
index = this._colorCounter % 10;
|
|
415
|
+
this._colorIndexMap.set(text, index);
|
|
416
|
+
this._colorCounter++;
|
|
417
|
+
}
|
|
418
|
+
return index;
|
|
419
|
+
}
|
|
420
|
+
|
|
405
421
|
containsHtmlTags(text) {
|
|
406
422
|
const htmlPattern = /<(?:div|table|section|article|ul|ol|dl|nav|header|footer|main|aside|figure|details|summary|h[1-6]|p|blockquote|pre|code|span|strong|em|a|img|br|hr|li|td|tr|th|thead|tbody|tfoot)\b[^>]*>/i;
|
|
407
423
|
return htmlPattern.test(text);
|