agentgui 1.0.238 → 1.0.240
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 +732 -3
- package/static/index.html +69 -1
- package/static/js/client.js +90 -14
- package/static/js/features.js +14 -0
- package/static/js/streaming-renderer.js +16 -0
- package/static/js/voice.js +13 -1
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();
|
|
@@ -1773,10 +2500,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1773
2500
|
const remoteUrl = result.trim();
|
|
1774
2501
|
const statusResult = execSync('git status --porcelain', { encoding: 'utf-8', cwd: STARTUP_CWD });
|
|
1775
2502
|
const hasChanges = statusResult.trim().length > 0;
|
|
2503
|
+
const unpushedResult = execSync('git rev-list --count --not --remotes 2>/dev/null', { encoding: 'utf-8', cwd: STARTUP_CWD });
|
|
2504
|
+
const hasUnpushed = parseInt(unpushedResult.trim() || '0', 10) > 0;
|
|
1776
2505
|
const ownsRemote = !remoteUrl.includes('github.com/') || remoteUrl.includes(process.env.GITHUB_USER || '');
|
|
1777
|
-
sendJSON(req, res, 200, { ownsRemote, hasChanges, remoteUrl });
|
|
2506
|
+
sendJSON(req, res, 200, { ownsRemote, hasChanges, hasUnpushed, remoteUrl });
|
|
1778
2507
|
} catch {
|
|
1779
|
-
sendJSON(req, res, 200, { ownsRemote: false, hasChanges: false, remoteUrl: '' });
|
|
2508
|
+
sendJSON(req, res, 200, { ownsRemote: false, hasChanges: false, hasUnpushed: false, remoteUrl: '' });
|
|
1780
2509
|
}
|
|
1781
2510
|
return;
|
|
1782
2511
|
}
|
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) {
|
|
@@ -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
|
|
|
@@ -788,28 +843,21 @@ class AgentGUIClient {
|
|
|
788
843
|
this.enableControls();
|
|
789
844
|
this.emit('streaming:complete', data);
|
|
790
845
|
|
|
791
|
-
|
|
792
|
-
this._promptPushIfWeOwnRemote();
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
_isACPAgent(agentId) {
|
|
797
|
-
const acpAgents = ['opencode', 'gemini', 'goose', 'openhands', 'augment', 'cline', 'kimi', 'qwen', 'codex', 'mistral', 'kiro', 'fast-agent'];
|
|
798
|
-
return acpAgents.includes(agentId);
|
|
846
|
+
this._promptPushIfWeOwnRemote();
|
|
799
847
|
}
|
|
800
848
|
|
|
801
849
|
async _promptPushIfWeOwnRemote() {
|
|
802
850
|
try {
|
|
803
851
|
const result = await fetch(window.__BASE_URL + '/api/git/check-remote-ownership');
|
|
804
|
-
const { ownsRemote, hasChanges, remoteUrl } = await result.json();
|
|
805
|
-
if (ownsRemote && hasChanges) {
|
|
806
|
-
const
|
|
807
|
-
if (
|
|
808
|
-
|
|
852
|
+
const { ownsRemote, hasChanges, hasUnpushed, remoteUrl } = await result.json();
|
|
853
|
+
if (ownsRemote && (hasChanges || hasUnpushed)) {
|
|
854
|
+
const conv = this.state.currentConversation;
|
|
855
|
+
if (conv) {
|
|
856
|
+
this.streamToConversation(conv.id, 'Push the changes to the remote repository.', conv.agentId);
|
|
809
857
|
}
|
|
810
858
|
}
|
|
811
859
|
} catch (e) {
|
|
812
|
-
console.warn('
|
|
860
|
+
console.warn('Auto-push check failed:', e);
|
|
813
861
|
}
|
|
814
862
|
}
|
|
815
863
|
|
|
@@ -1788,6 +1836,23 @@ class AgentGUIClient {
|
|
|
1788
1836
|
});
|
|
1789
1837
|
}
|
|
1790
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
|
+
|
|
1791
1856
|
async loadModelsForAgent(agentId) {
|
|
1792
1857
|
if (!agentId || !this.ui.modelSelector) return;
|
|
1793
1858
|
const cached = this._modelCache.get(agentId);
|
|
@@ -1938,6 +2003,7 @@ class AgentGUIClient {
|
|
|
1938
2003
|
this._modelDownloadInProgress = false;
|
|
1939
2004
|
console.log('[Models] Download complete');
|
|
1940
2005
|
this._updateConnectionIndicator(this.wsManager?.latency?.quality || 'unknown');
|
|
2006
|
+
this._updateVoiceTabState();
|
|
1941
2007
|
return;
|
|
1942
2008
|
}
|
|
1943
2009
|
|
|
@@ -1947,6 +2013,16 @@ class AgentGUIClient {
|
|
|
1947
2013
|
}
|
|
1948
2014
|
}
|
|
1949
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
|
+
|
|
1950
2026
|
_toggleConnectionTooltip() {
|
|
1951
2027
|
let tooltip = document.getElementById('connection-tooltip');
|
|
1952
2028
|
if (tooltip) { tooltip.remove(); return; }
|
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);
|
package/static/js/voice.js
CHANGED
|
@@ -296,7 +296,19 @@
|
|
|
296
296
|
if (!ttsEnabled) return;
|
|
297
297
|
var clean = text.replace(/<[^>]*>/g, '').trim();
|
|
298
298
|
if (!clean) return;
|
|
299
|
-
|
|
299
|
+
var parts = [];
|
|
300
|
+
if (typeof agentGUIClient !== 'undefined' && agentGUIClient && typeof agentGUIClient.parseMarkdownCodeBlocks === 'function') {
|
|
301
|
+
parts = agentGUIClient.parseMarkdownCodeBlocks(clean);
|
|
302
|
+
} else {
|
|
303
|
+
parts = [{ type: 'text', content: clean }];
|
|
304
|
+
}
|
|
305
|
+
parts.forEach(function(part) {
|
|
306
|
+
if (part.type === 'code') return;
|
|
307
|
+
var segment = part.content.trim();
|
|
308
|
+
if (segment) {
|
|
309
|
+
speechQueue.push(segment);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
300
312
|
processQueue();
|
|
301
313
|
}
|
|
302
314
|
|