agentgui 1.0.532 → 1.0.534
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 +56 -0
- package/lib/pm2-manager.js +170 -0
- package/lib/tool-manager.js +4 -4
- package/lib/ws-handlers-conv.js +16 -0
- package/package.json +2 -1
- package/server.js +124 -24
- package/static/index.html +193 -13
- package/static/js/client.js +113 -0
- package/static/js/pm2-monitor.js +151 -0
package/database.js
CHANGED
|
@@ -1434,6 +1434,62 @@ export const queries = {
|
|
|
1434
1434
|
};
|
|
1435
1435
|
},
|
|
1436
1436
|
|
|
1437
|
+
getMessagesBefore(conversationId, beforeId, limit = 50) {
|
|
1438
|
+
const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
|
|
1439
|
+
const total = countStmt.get(conversationId).count;
|
|
1440
|
+
|
|
1441
|
+
const stmt = prep(`
|
|
1442
|
+
SELECT * FROM messages
|
|
1443
|
+
WHERE conversationId = ? AND id < (SELECT id FROM messages WHERE id = ?)
|
|
1444
|
+
ORDER BY created_at DESC LIMIT ?
|
|
1445
|
+
`);
|
|
1446
|
+
const messages = stmt.all(conversationId, beforeId, limit).reverse();
|
|
1447
|
+
|
|
1448
|
+
return {
|
|
1449
|
+
messages: messages.map(msg => {
|
|
1450
|
+
if (typeof msg.content === 'string') {
|
|
1451
|
+
try {
|
|
1452
|
+
msg.content = JSON.parse(msg.content);
|
|
1453
|
+
} catch (_) {}
|
|
1454
|
+
}
|
|
1455
|
+
return msg;
|
|
1456
|
+
}),
|
|
1457
|
+
total,
|
|
1458
|
+
limit,
|
|
1459
|
+
hasMore: total > (limit + 1)
|
|
1460
|
+
};
|
|
1461
|
+
},
|
|
1462
|
+
|
|
1463
|
+
getChunksBefore(conversationId, beforeTimestamp, limit = 500) {
|
|
1464
|
+
const countStmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
|
|
1465
|
+
const total = countStmt.get(conversationId).count;
|
|
1466
|
+
|
|
1467
|
+
const stmt = prep(`
|
|
1468
|
+
SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
|
1469
|
+
FROM chunks
|
|
1470
|
+
WHERE conversationId = ? AND created_at < ?
|
|
1471
|
+
ORDER BY created_at DESC LIMIT ?
|
|
1472
|
+
`);
|
|
1473
|
+
const rows = stmt.all(conversationId, beforeTimestamp, limit);
|
|
1474
|
+
rows.reverse();
|
|
1475
|
+
|
|
1476
|
+
return {
|
|
1477
|
+
chunks: rows.map(row => {
|
|
1478
|
+
try {
|
|
1479
|
+
return {
|
|
1480
|
+
...row,
|
|
1481
|
+
data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
|
|
1482
|
+
};
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
return row;
|
|
1485
|
+
}
|
|
1486
|
+
}),
|
|
1487
|
+
total,
|
|
1488
|
+
limit,
|
|
1489
|
+
hasMore: total > (limit + 1)
|
|
1490
|
+
};
|
|
1491
|
+
},
|
|
1492
|
+
|
|
1437
1493
|
getChunksSince(sessionId, timestamp) {
|
|
1438
1494
|
const stmt = prep(
|
|
1439
1495
|
`SELECT id, sessionId, conversationId, sequence, type, data, created_at
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import pm2 from 'pm2';
|
|
2
|
+
|
|
3
|
+
const ACTIVE_STATES = new Set(['online', 'launching', 'stopping', 'waiting restart']);
|
|
4
|
+
|
|
5
|
+
class PM2Manager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.connected = false;
|
|
8
|
+
this.monitoring = false;
|
|
9
|
+
this.monitorInterval = null;
|
|
10
|
+
this.broadcastFn = null;
|
|
11
|
+
this.logStreams = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async connect() {
|
|
15
|
+
if (this.connected) return true;
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
pm2.connect(false, (err) => {
|
|
18
|
+
if (err) { this.connected = false; reject(err); }
|
|
19
|
+
else { this.connected = true; resolve(true); }
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async ensureConnected() {
|
|
25
|
+
if (this.connected) return true;
|
|
26
|
+
await this.connect();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listProcesses() {
|
|
31
|
+
try {
|
|
32
|
+
await this.ensureConnected();
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
pm2.list((err, list) => {
|
|
35
|
+
if (err) { this.connected = false; resolve([]); return; }
|
|
36
|
+
if (!Array.isArray(list)) { resolve([]); return; }
|
|
37
|
+
resolve(list.map(proc => ({
|
|
38
|
+
name: proc.name,
|
|
39
|
+
pm_id: proc.pm_id,
|
|
40
|
+
status: proc.status,
|
|
41
|
+
pid: proc.pid,
|
|
42
|
+
cpu: proc.monit ? (proc.monit.cpu || 0) : 0,
|
|
43
|
+
memory: proc.monit ? (typeof proc.monit.memory === 'number' ? proc.monit.memory : 0) : 0,
|
|
44
|
+
uptime: proc.pm2_env ? proc.pm2_env.pm_uptime : null,
|
|
45
|
+
restarts: proc.pm2_env ? (proc.pm2_env.restart_time || 0) : 0,
|
|
46
|
+
watching: proc.pm2_env ? (proc.pm2_env.watch || false) : false,
|
|
47
|
+
isActive: ACTIVE_STATES.has(proc.status)
|
|
48
|
+
})));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
} catch (_) {
|
|
52
|
+
this.connected = false;
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async startMonitoring(broadcastFn) {
|
|
58
|
+
if (this.monitoring) return;
|
|
59
|
+
this.monitoring = true;
|
|
60
|
+
this.broadcastFn = broadcastFn;
|
|
61
|
+
const tick = async () => {
|
|
62
|
+
if (!this.monitoring) return;
|
|
63
|
+
try {
|
|
64
|
+
const processes = await this.listProcesses();
|
|
65
|
+
const hasActive = processes.some(p => p.isActive);
|
|
66
|
+
broadcastFn({ type: 'pm2_monit_update', processes, hasActive, available: true, timestamp: Date.now() });
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
};
|
|
69
|
+
this.monitorInterval = setInterval(tick, 2000);
|
|
70
|
+
await tick();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
stopMonitoring() {
|
|
74
|
+
this.monitoring = false;
|
|
75
|
+
if (this.monitorInterval) { clearInterval(this.monitorInterval); this.monitorInterval = null; }
|
|
76
|
+
this.broadcastFn = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async startProcess(name) {
|
|
80
|
+
try {
|
|
81
|
+
await this.ensureConnected();
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
pm2.start(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
|
|
84
|
+
});
|
|
85
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async stopProcess(name) {
|
|
89
|
+
try {
|
|
90
|
+
await this.ensureConnected();
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
pm2.stop(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
|
|
93
|
+
});
|
|
94
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async restartProcess(name) {
|
|
98
|
+
try {
|
|
99
|
+
await this.ensureConnected();
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
pm2.restart(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
|
|
102
|
+
});
|
|
103
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async deleteProcess(name) {
|
|
107
|
+
try {
|
|
108
|
+
await this.ensureConnected();
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
pm2.delete(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
|
|
111
|
+
});
|
|
112
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getLogs(name, options = {}) {
|
|
116
|
+
try {
|
|
117
|
+
await this.ensureConnected();
|
|
118
|
+
const existing = this.logStreams.get(name);
|
|
119
|
+
if (existing) { try { existing.destroy(); } catch (_) {} this.logStreams.delete(name); }
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const lines = [];
|
|
122
|
+
let settled = false;
|
|
123
|
+
const finish = (result) => { if (!settled) { settled = true; resolve(result); } };
|
|
124
|
+
const timer = setTimeout(() => finish({ success: true, logs: lines.join('\n') }), 4000);
|
|
125
|
+
try {
|
|
126
|
+
const stream = pm2.logs(name, { raw: true, lines: options.lines || 100, follow: false });
|
|
127
|
+
this.logStreams.set(name, stream);
|
|
128
|
+
stream.on('data', (c) => lines.push(c.toString()));
|
|
129
|
+
stream.on('end', () => { clearTimeout(timer); this.logStreams.delete(name); finish({ success: true, logs: lines.join('\n') }); });
|
|
130
|
+
stream.on('error', (e) => { clearTimeout(timer); this.logStreams.delete(name); finish({ success: false, error: e.message }); });
|
|
131
|
+
} catch (err) { clearTimeout(timer); finish({ success: false, error: err.message }); }
|
|
132
|
+
});
|
|
133
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async flushLogs(name) {
|
|
137
|
+
try {
|
|
138
|
+
await this.ensureConnected();
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
pm2.flush(name, (err) => resolve(err ? { success: false, error: err.message } : { success: true }));
|
|
141
|
+
});
|
|
142
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async ping() {
|
|
146
|
+
try {
|
|
147
|
+
await this.ensureConnected();
|
|
148
|
+
return { success: true, status: 'connected' };
|
|
149
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async heal() {
|
|
153
|
+
this.connected = false;
|
|
154
|
+
try {
|
|
155
|
+
await this.connect();
|
|
156
|
+
return { success: true };
|
|
157
|
+
} catch (err) { return { success: false, error: err.message }; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
disconnect() {
|
|
161
|
+
this.stopMonitoring();
|
|
162
|
+
for (const [, s] of this.logStreams) { try { s.destroy(); } catch (_) {} }
|
|
163
|
+
this.logStreams.clear();
|
|
164
|
+
this.connected = false;
|
|
165
|
+
try { pm2.disconnect(); } catch (_) {}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const pm2Manager = new PM2Manager();
|
|
170
|
+
export default pm2Manager;
|
package/lib/tool-manager.js
CHANGED
|
@@ -5,10 +5,10 @@ import path from 'path';
|
|
|
5
5
|
|
|
6
6
|
const isWindows = os.platform() === 'win32';
|
|
7
7
|
const TOOLS = [
|
|
8
|
-
{ id: 'gm-cc', name: 'gm-cc', pkg: '
|
|
9
|
-
{ id: 'gm-oc', name: 'gm-oc', pkg: '
|
|
10
|
-
{ id: 'gm-gc', name: 'gm-gc', pkg: '
|
|
11
|
-
{ id: 'gm-kilo', name: 'gm-kilo', pkg: '
|
|
8
|
+
{ id: 'gm-cc', name: 'gm-cc', pkg: 'gm-cc', pluginId: 'gm-cc' },
|
|
9
|
+
{ id: 'gm-oc', name: 'gm-oc', pkg: 'gm-oc', pluginId: 'gm-oc' },
|
|
10
|
+
{ id: 'gm-gc', name: 'gm-gc', pkg: 'gm-gc', pluginId: 'gm' },
|
|
11
|
+
{ id: 'gm-kilo', name: 'gm-kilo', pkg: 'gm-kilo', pluginId: 'gm-kilo' },
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
const statusCache = new Map();
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -73,6 +73,14 @@ export function register(router, deps) {
|
|
|
73
73
|
return { ok: true, chunks: since > 0 ? allChunks.filter(c => c.created_at > since) : allChunks };
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
router.handle('conv.chunks.earlier', (p) => {
|
|
77
|
+
if (!queries.getConversation(p.id)) notFound('Conversation not found');
|
|
78
|
+
const beforeTimestamp = parseInt(p.before || Date.now());
|
|
79
|
+
const limit = Math.min(p.limit || 500, 5000);
|
|
80
|
+
const result = queries.getChunksBefore(p.id, beforeTimestamp, limit);
|
|
81
|
+
return { ok: true, chunks: result.chunks, total: result.total, hasMore: result.hasMore, limit: result.limit };
|
|
82
|
+
});
|
|
83
|
+
|
|
76
84
|
router.handle('conv.cancel', (p) => {
|
|
77
85
|
const entry = activeExecutions.get(p.id);
|
|
78
86
|
if (!entry) notFound('No active execution to cancel');
|
|
@@ -104,6 +112,14 @@ export function register(router, deps) {
|
|
|
104
112
|
return queries.getPaginatedMessages(p.id, Math.min(p.limit || 50, 100), Math.max(p.offset || 0, 0));
|
|
105
113
|
});
|
|
106
114
|
|
|
115
|
+
router.handle('msg.ls.earlier', (p) => {
|
|
116
|
+
if (!p.id) fail(400, 'Missing conversation id');
|
|
117
|
+
if (!p.before) fail(400, 'Missing before messageId parameter');
|
|
118
|
+
const limit = Math.min(p.limit || 50, 100);
|
|
119
|
+
const result = queries.getMessagesBefore(p.id, p.before, limit);
|
|
120
|
+
return { ok: true, messages: result.messages, total: result.total, hasMore: result.hasMore, limit: result.limit };
|
|
121
|
+
});
|
|
122
|
+
|
|
107
123
|
function startExecution(convId, message, agentId, model, content, subAgent) {
|
|
108
124
|
const session = queries.createSession(convId);
|
|
109
125
|
queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, convId, session.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.534",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"node-pty": "^1.0.0",
|
|
36
36
|
"onnxruntime-node": "1.21.0",
|
|
37
37
|
"opencode-ai": "^1.2.15",
|
|
38
|
+
"pm2": "^5.4.3",
|
|
38
39
|
"puppeteer-core": "^24.37.5",
|
|
39
40
|
"webtalk": "^1.0.31",
|
|
40
41
|
"ws": "^8.14.2"
|
package/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
|
|
|
25
25
|
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, queryModels as queryACPModels, touch as touchACP } from './lib/acp-manager.js';
|
|
26
26
|
import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
|
|
27
27
|
import * as toolManager from './lib/tool-manager.js';
|
|
28
|
+
import { pm2Manager } from './lib/pm2-manager.js';
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -4045,6 +4046,7 @@ const hotReloadClients = [];
|
|
|
4045
4046
|
const syncClients = new Set();
|
|
4046
4047
|
const subscriptionIndex = new Map();
|
|
4047
4048
|
const sseStreamHandlers = new Map();
|
|
4049
|
+
const pm2Subscribers = new Set();
|
|
4048
4050
|
|
|
4049
4051
|
wss.on('connection', (ws, req) => {
|
|
4050
4052
|
// req.url in WebSocket is just the path (e.g., '/gm/sync'), not a full URL
|
|
@@ -4072,16 +4074,19 @@ wss.on('connection', (ws, req) => {
|
|
|
4072
4074
|
});
|
|
4073
4075
|
|
|
4074
4076
|
ws.on('pong', () => { ws.isAlive = true; });
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4077
|
+
ws.on('close', () => {
|
|
4078
|
+
if (ws.terminalProc) { try { ws.terminalProc.kill(); } catch(e) {} ws.terminalProc = null; }
|
|
4079
|
+
syncClients.delete(ws);
|
|
4080
|
+
wsOptimizer.removeClient(ws);
|
|
4081
|
+
for (const sub of ws.subscriptions) {
|
|
4082
|
+
const idx = subscriptionIndex.get(sub);
|
|
4083
|
+
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(sub); }
|
|
4084
|
+
}
|
|
4085
|
+
if (ws.pm2Subscribed) {
|
|
4086
|
+
pm2Subscribers.delete(ws);
|
|
4087
|
+
}
|
|
4088
|
+
console.log(`[WebSocket] Client ${ws.clientId} disconnected`);
|
|
4089
|
+
});
|
|
4085
4090
|
}
|
|
4086
4091
|
});
|
|
4087
4092
|
|
|
@@ -4094,7 +4099,11 @@ const BROADCAST_TYPES = new Set([
|
|
|
4094
4099
|
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error',
|
|
4095
4100
|
'tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed',
|
|
4096
4101
|
'tool_update_progress', 'tool_update_complete', 'tool_update_failed',
|
|
4097
|
-
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete'
|
|
4102
|
+
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete',
|
|
4103
|
+
'pm2_monit_update', 'pm2_monitoring_started', 'pm2_monitoring_stopped',
|
|
4104
|
+
'pm2_list_response', 'pm2_start_response', 'pm2_stop_response',
|
|
4105
|
+
'pm2_restart_response', 'pm2_delete_response', 'pm2_logs_response',
|
|
4106
|
+
'pm2_flush_logs_response', 'pm2_ping_response', 'pm2_unavailable'
|
|
4098
4107
|
]);
|
|
4099
4108
|
|
|
4100
4109
|
const wsOptimizer = new WSOptimizer();
|
|
@@ -4288,12 +4297,65 @@ wsRouter.onLegacy((data, ws) => {
|
|
|
4288
4297
|
}
|
|
4289
4298
|
} catch (e) {}
|
|
4290
4299
|
}
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4300
|
+
} else if (data.type === 'terminal_stop') {
|
|
4301
|
+
if (ws.terminalProc) {
|
|
4302
|
+
try { ws.terminalProc.kill(); } catch(e) {}
|
|
4303
|
+
ws.terminalProc = null;
|
|
4304
|
+
}
|
|
4305
|
+
} else if (data.type === 'pm2_list') {
|
|
4306
|
+
if (!pm2Manager.connected) {
|
|
4307
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
|
|
4308
|
+
} else {
|
|
4309
|
+
pm2Manager.listProcesses().then(processes => {
|
|
4310
|
+
if (ws.readyState === 1) {
|
|
4311
|
+
const hasActive = processes.some(p => ['online','launching','stopping','waiting restart'].includes(p.status));
|
|
4312
|
+
ws.send(JSON.stringify({ type: 'pm2_list_response', processes, hasActive }));
|
|
4313
|
+
}
|
|
4314
|
+
}).catch(() => {
|
|
4315
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pm2_unavailable', reason: 'list failed', timestamp: Date.now() }));
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
} else if (data.type === 'pm2_start_monitoring') {
|
|
4319
|
+
pm2Subscribers.add(ws);
|
|
4320
|
+
ws.pm2Subscribed = true;
|
|
4321
|
+
if (!pm2Manager.connected) {
|
|
4322
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'pm2_unavailable', reason: 'PM2 not connected', timestamp: Date.now() }));
|
|
4323
|
+
} else {
|
|
4324
|
+
ws.send(JSON.stringify({ type: 'pm2_monitoring_started' }));
|
|
4325
|
+
}
|
|
4326
|
+
} else if (data.type === 'pm2_stop_monitoring') {
|
|
4327
|
+
pm2Subscribers.delete(ws);
|
|
4328
|
+
ws.pm2Subscribed = false;
|
|
4329
|
+
ws.send(JSON.stringify({ type: 'pm2_monitoring_stopped' }));
|
|
4330
|
+
} else if (data.type === 'pm2_start') {
|
|
4331
|
+
pm2Manager.startProcess(data.name).then(result => {
|
|
4332
|
+
ws.send(JSON.stringify({ type: 'pm2_start_response', name: data.name, ...result }));
|
|
4333
|
+
});
|
|
4334
|
+
} else if (data.type === 'pm2_stop') {
|
|
4335
|
+
pm2Manager.stopProcess(data.name).then(result => {
|
|
4336
|
+
ws.send(JSON.stringify({ type: 'pm2_stop_response', name: data.name, ...result }));
|
|
4337
|
+
});
|
|
4338
|
+
} else if (data.type === 'pm2_restart') {
|
|
4339
|
+
pm2Manager.restartProcess(data.name).then(result => {
|
|
4340
|
+
ws.send(JSON.stringify({ type: 'pm2_restart_response', name: data.name, ...result }));
|
|
4341
|
+
});
|
|
4342
|
+
} else if (data.type === 'pm2_delete') {
|
|
4343
|
+
pm2Manager.deleteProcess(data.name).then(result => {
|
|
4344
|
+
ws.send(JSON.stringify({ type: 'pm2_delete_response', name: data.name, ...result }));
|
|
4345
|
+
});
|
|
4346
|
+
} else if (data.type === 'pm2_logs') {
|
|
4347
|
+
pm2Manager.getLogs(data.name, { lines: data.lines || 100 }).then(result => {
|
|
4348
|
+
ws.send(JSON.stringify({ type: 'pm2_logs_response', name: data.name, ...result }));
|
|
4349
|
+
});
|
|
4350
|
+
} else if (data.type === 'pm2_flush_logs') {
|
|
4351
|
+
pm2Manager.flushLogs(data.name).then(result => {
|
|
4352
|
+
ws.send(JSON.stringify({ type: 'pm2_flush_logs_response', name: data.name, ...result }));
|
|
4353
|
+
});
|
|
4354
|
+
} else if (data.type === 'pm2_ping') {
|
|
4355
|
+
pm2Manager.ping().then(result => {
|
|
4356
|
+
ws.send(JSON.stringify({ type: 'pm2_ping_response', ...result }));
|
|
4357
|
+
});
|
|
4295
4358
|
}
|
|
4296
|
-
}
|
|
4297
4359
|
|
|
4298
4360
|
} catch (err) { console.error('[WS-LEGACY] Handler error (contained):', err.message); }
|
|
4299
4361
|
});
|
|
@@ -4327,10 +4389,19 @@ if (watch) {
|
|
|
4327
4389
|
|
|
4328
4390
|
process.on('SIGTERM', () => {
|
|
4329
4391
|
console.log('[SIGNAL] SIGTERM received - graceful shutdown');
|
|
4330
|
-
|
|
4331
|
-
|
|
4392
|
+
try { pm2Manager.disconnect(); } catch (_) {}
|
|
4393
|
+
Promise.resolve().then(() => {
|
|
4394
|
+
stopACPTools().then(() => {
|
|
4395
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
4396
|
+
}).catch(() => {
|
|
4397
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
4398
|
+
});
|
|
4332
4399
|
}).catch(() => {
|
|
4333
|
-
|
|
4400
|
+
stopACPTools().then(() => {
|
|
4401
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
4402
|
+
}).catch(() => {
|
|
4403
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
4404
|
+
});
|
|
4334
4405
|
});
|
|
4335
4406
|
});
|
|
4336
4407
|
|
|
@@ -4602,15 +4673,44 @@ function onServerReady() {
|
|
|
4602
4673
|
}
|
|
4603
4674
|
});
|
|
4604
4675
|
|
|
4605
|
-
|
|
4676
|
+
getSpeech().then(s => s.preloadTTS()).catch(e => debugLog('[TTS] Preload failed: ' + e.message));
|
|
4606
4677
|
|
|
4607
|
-
|
|
4678
|
+
performAutoImport();
|
|
4608
4679
|
|
|
4609
|
-
|
|
4680
|
+
setInterval(performAutoImport, 30000);
|
|
4610
4681
|
|
|
4611
|
-
|
|
4682
|
+
setInterval(performAgentHealthCheck, 30000);
|
|
4612
4683
|
|
|
4613
|
-
|
|
4684
|
+
// Initialize PM2 monitoring - only when PM2 daemon is available
|
|
4685
|
+
const broadcastPM2 = (update) => {
|
|
4686
|
+
const msg = JSON.stringify(update);
|
|
4687
|
+
for (const client of pm2Subscribers) {
|
|
4688
|
+
if (client.readyState === 1) { try { client.send(msg); } catch (_) {} }
|
|
4689
|
+
}
|
|
4690
|
+
};
|
|
4691
|
+
|
|
4692
|
+
const startPM2Monitoring = async () => {
|
|
4693
|
+
try {
|
|
4694
|
+
await pm2Manager.connect();
|
|
4695
|
+
await pm2Manager.startMonitoring(broadcastPM2);
|
|
4696
|
+
console.log('[PM2] Monitoring started');
|
|
4697
|
+
} catch (err) {
|
|
4698
|
+
console.log('[PM2] Not available:', err.message);
|
|
4699
|
+
broadcastPM2({ type: 'pm2_unavailable', reason: err.message, timestamp: Date.now() });
|
|
4700
|
+
}
|
|
4701
|
+
};
|
|
4702
|
+
|
|
4703
|
+
setTimeout(startPM2Monitoring, 2000);
|
|
4704
|
+
|
|
4705
|
+
setInterval(async () => {
|
|
4706
|
+
if (!pm2Manager.connected && !pm2Manager.monitoring) {
|
|
4707
|
+
try {
|
|
4708
|
+
const healed = await pm2Manager.heal();
|
|
4709
|
+
if (healed.success) await pm2Manager.startMonitoring(broadcastPM2);
|
|
4710
|
+
} catch (_) {}
|
|
4711
|
+
}
|
|
4712
|
+
}, 30000);
|
|
4713
|
+
}
|
|
4614
4714
|
|
|
4615
4715
|
const importMtimeCache = new Map();
|
|
4616
4716
|
|
package/static/index.html
CHANGED
|
@@ -1202,6 +1202,175 @@
|
|
|
1202
1202
|
.sidebar-list { contain: strict; content-visibility: auto; }
|
|
1203
1203
|
.message { contain: layout style; content-visibility: auto; contain-intrinsic-size: auto 80px; }
|
|
1204
1204
|
|
|
1205
|
+
/* PM2 Monitor Panel */
|
|
1206
|
+
.pm2-monitor-panel {
|
|
1207
|
+
border-top: 1px solid var(--color-border);
|
|
1208
|
+
background: var(--color-bg-secondary);
|
|
1209
|
+
display: flex;
|
|
1210
|
+
flex-direction: column;
|
|
1211
|
+
max-height: 35vh;
|
|
1212
|
+
}
|
|
1213
|
+
.pm2-monitor-header {
|
|
1214
|
+
padding: 0.5rem 0.75rem;
|
|
1215
|
+
font-size: 0.75rem;
|
|
1216
|
+
font-weight: 600;
|
|
1217
|
+
text-transform: uppercase;
|
|
1218
|
+
letter-spacing: 0.05em;
|
|
1219
|
+
color: var(--color-text-secondary);
|
|
1220
|
+
display: flex;
|
|
1221
|
+
justify-content: space-between;
|
|
1222
|
+
align-items: center;
|
|
1223
|
+
background: var(--color-bg-primary);
|
|
1224
|
+
border-bottom: 1px solid var(--color-border);
|
|
1225
|
+
flex-shrink: 0;
|
|
1226
|
+
}
|
|
1227
|
+
.pm2-monitor-actions { display: flex; gap: 0.25rem; }
|
|
1228
|
+
.pm2-action-btn {
|
|
1229
|
+
background: none;
|
|
1230
|
+
border: none;
|
|
1231
|
+
color: var(--color-text-secondary);
|
|
1232
|
+
cursor: pointer;
|
|
1233
|
+
padding: 0.125rem 0.375rem;
|
|
1234
|
+
font-size: 0.875rem;
|
|
1235
|
+
line-height: 1;
|
|
1236
|
+
border-radius: 0.25rem;
|
|
1237
|
+
}
|
|
1238
|
+
.pm2-action-btn:hover { background: var(--color-bg-secondary); color: var(--color-text-primary); }
|
|
1239
|
+
.pm2-process-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 0.25rem 0; }
|
|
1240
|
+
.pm2-process-item {
|
|
1241
|
+
padding: 0.375rem 0.75rem;
|
|
1242
|
+
margin: 0.125rem 0.5rem;
|
|
1243
|
+
border-radius: 0.375rem;
|
|
1244
|
+
background: var(--color-bg-primary);
|
|
1245
|
+
border-left: 3px solid var(--color-border);
|
|
1246
|
+
font-size: 0.75rem;
|
|
1247
|
+
transition: background-color 0.15s;
|
|
1248
|
+
}
|
|
1249
|
+
.pm2-process-item:hover { background: var(--color-bg-secondary); }
|
|
1250
|
+
.pm2-process-item.online { border-left-color: var(--color-success); }
|
|
1251
|
+
.pm2-process-item.launching { border-left-color: var(--color-primary); }
|
|
1252
|
+
.pm2-process-item.stopping { border-left-color: var(--color-warning); }
|
|
1253
|
+
.pm2-process-item.stopped { border-left-color: var(--color-text-muted, #888); opacity: 0.7; }
|
|
1254
|
+
.pm2-process-item.errored { border-left-color: var(--color-error); }
|
|
1255
|
+
.pm2-process-item.waiting\ restart { border-left-color: var(--color-warning); }
|
|
1256
|
+
.pm2-status-dot {
|
|
1257
|
+
display: inline-block;
|
|
1258
|
+
width: 6px; height: 6px;
|
|
1259
|
+
border-radius: 50%;
|
|
1260
|
+
background: var(--color-text-secondary);
|
|
1261
|
+
vertical-align: middle;
|
|
1262
|
+
margin-left: 4px;
|
|
1263
|
+
}
|
|
1264
|
+
.pm2-status-online .pm2-status-dot,
|
|
1265
|
+
.pm2-process-item.online .pm2-status-dot { background: var(--color-success); }
|
|
1266
|
+
.pm2-status-launching .pm2-status-dot,
|
|
1267
|
+
.pm2-process-item.launching .pm2-status-dot { background: var(--color-primary); }
|
|
1268
|
+
.pm2-status-stopping .pm2-status-dot,
|
|
1269
|
+
.pm2-status-errored .pm2-status-dot,
|
|
1270
|
+
.pm2-process-item.errored .pm2-status-dot { background: var(--color-error); }
|
|
1271
|
+
.pm2-process-name {
|
|
1272
|
+
font-weight: 600;
|
|
1273
|
+
margin-bottom: 0.125rem;
|
|
1274
|
+
white-space: nowrap;
|
|
1275
|
+
overflow: hidden;
|
|
1276
|
+
text-overflow: ellipsis;
|
|
1277
|
+
}
|
|
1278
|
+
.pm2-process-meta {
|
|
1279
|
+
display: flex;
|
|
1280
|
+
gap: 0.5rem;
|
|
1281
|
+
font-size: 0.7rem;
|
|
1282
|
+
color: var(--color-text-secondary);
|
|
1283
|
+
flex-wrap: wrap;
|
|
1284
|
+
}
|
|
1285
|
+
.pm2-process-meta span {
|
|
1286
|
+
display: inline-flex;
|
|
1287
|
+
align-items: center;
|
|
1288
|
+
gap: 0.125rem;
|
|
1289
|
+
}
|
|
1290
|
+
.pm2-process-actions {
|
|
1291
|
+
margin-top: 0.25rem;
|
|
1292
|
+
display: flex;
|
|
1293
|
+
gap: 0.25rem;
|
|
1294
|
+
}
|
|
1295
|
+
.pm2-btn {
|
|
1296
|
+
padding: 0.125rem 0.375rem;
|
|
1297
|
+
font-size: 0.7rem;
|
|
1298
|
+
border-radius: 0.25rem;
|
|
1299
|
+
border: 1px solid var(--color-border);
|
|
1300
|
+
background: var(--color-bg-primary);
|
|
1301
|
+
color: var(--color-text-primary);
|
|
1302
|
+
cursor: pointer;
|
|
1303
|
+
transition: all 0.15s;
|
|
1304
|
+
}
|
|
1305
|
+
.pm2-btn:hover {
|
|
1306
|
+
background: var(--color-primary);
|
|
1307
|
+
color: white;
|
|
1308
|
+
border-color: var(--color-primary);
|
|
1309
|
+
}
|
|
1310
|
+
.pm2-btn.danger:hover {
|
|
1311
|
+
background: var(--color-error);
|
|
1312
|
+
border-color: var(--color-error);
|
|
1313
|
+
}
|
|
1314
|
+
.pm2-loading, .pm2-empty, .pm2-error {
|
|
1315
|
+
padding: 0.75rem;
|
|
1316
|
+
text-align: center;
|
|
1317
|
+
color: var(--color-text-secondary);
|
|
1318
|
+
font-size: 0.8rem;
|
|
1319
|
+
}
|
|
1320
|
+
@keyframes slideIn {
|
|
1321
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
1322
|
+
to { transform: translateX(0); opacity: 1; }
|
|
1323
|
+
}
|
|
1324
|
+
@keyframes fadeOut {
|
|
1325
|
+
from { opacity: 1; }
|
|
1326
|
+
to { opacity: 0; }
|
|
1327
|
+
}
|
|
1328
|
+
.pm2-logs-modal {
|
|
1329
|
+
position: fixed;
|
|
1330
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
1331
|
+
background: rgba(0,0,0,0.5);
|
|
1332
|
+
display: flex;
|
|
1333
|
+
align-items: center;
|
|
1334
|
+
justify-content: center;
|
|
1335
|
+
z-index: 1000;
|
|
1336
|
+
padding: 1rem;
|
|
1337
|
+
}
|
|
1338
|
+
.pm2-logs-content {
|
|
1339
|
+
background: var(--color-bg-primary);
|
|
1340
|
+
border-radius: 0.5rem;
|
|
1341
|
+
width: 100%;
|
|
1342
|
+
max-width: 800px;
|
|
1343
|
+
max-height: 80vh;
|
|
1344
|
+
display: flex;
|
|
1345
|
+
flex-direction: column;
|
|
1346
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
1347
|
+
}
|
|
1348
|
+
.pm2-logs-header {
|
|
1349
|
+
padding: 0.75rem 1rem;
|
|
1350
|
+
border-bottom: 1px solid var(--color-border);
|
|
1351
|
+
display: flex;
|
|
1352
|
+
justify-content: space-between;
|
|
1353
|
+
align-items: center;
|
|
1354
|
+
font-weight: 600;
|
|
1355
|
+
}
|
|
1356
|
+
.pm2-logs-body {
|
|
1357
|
+
flex: 1;
|
|
1358
|
+
overflow-y: auto;
|
|
1359
|
+
padding: 0.75rem 1rem;
|
|
1360
|
+
font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
|
|
1361
|
+
font-size: 0.75rem;
|
|
1362
|
+
white-space: pre-wrap;
|
|
1363
|
+
background: var(--color-bg-code);
|
|
1364
|
+
color: #e6edf3;
|
|
1365
|
+
}
|
|
1366
|
+
.pm2-logs-footer {
|
|
1367
|
+
padding: 0.5rem 1rem;
|
|
1368
|
+
border-top: 1px solid var(--color-border);
|
|
1369
|
+
display: flex;
|
|
1370
|
+
justify-content: flex-end;
|
|
1371
|
+
gap: 0.5rem;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1205
1374
|
/* ===== RESPONSIVE: TABLET ===== */
|
|
1206
1375
|
@media (min-width: 769px) and (max-width: 1024px) {
|
|
1207
1376
|
:root { --sidebar-width: 260px; }
|
|
@@ -2883,10 +3052,20 @@
|
|
|
2883
3052
|
<button class="clone-go-btn" id="cloneGoBtn" title="Clone">Go</button>
|
|
2884
3053
|
<button class="clone-cancel-btn" id="cloneCancelBtn" title="Cancel">×</button>
|
|
2885
3054
|
</div>
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3055
|
+
<ul class="sidebar-list" data-conversation-list>
|
|
3056
|
+
<li class="sidebar-empty" data-conversation-empty>No conversations yet</li>
|
|
3057
|
+
</ul>
|
|
3058
|
+
<!-- PM2 Monitor Panel: hidden until active processes detected -->
|
|
3059
|
+
<div class="pm2-monitor-panel" id="pm2MonitorPanel" style="display:none">
|
|
3060
|
+
<div class="pm2-monitor-header">
|
|
3061
|
+
<span>PM2 Processes</span>
|
|
3062
|
+
<div class="pm2-monitor-actions">
|
|
3063
|
+
<button class="pm2-action-btn" id="pm2RefreshBtn" title="Refresh">↻</button>
|
|
3064
|
+
</div>
|
|
3065
|
+
</div>
|
|
3066
|
+
<div class="pm2-process-list" id="pm2ProcessList"></div>
|
|
3067
|
+
</div>
|
|
3068
|
+
</aside>
|
|
2890
3069
|
|
|
2891
3070
|
<!-- ===== MAIN PANEL ===== -->
|
|
2892
3071
|
<main id="app" class="main-panel">
|
|
@@ -3075,15 +3254,16 @@
|
|
|
3075
3254
|
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
3076
3255
|
<script defer src="/gm/js/dialogs.js"></script>
|
|
3077
3256
|
<script defer src="/gm/js/ui-components.js"></script>
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3257
|
+
<script defer src="/gm/js/conversations.js"></script>
|
|
3258
|
+
<script defer src="/gm/js/terminal.js"></script>
|
|
3259
|
+
<script defer src="/gm/js/script-runner.js"></script>
|
|
3260
|
+
<script defer src="/gm/js/tools-manager.js"></script>
|
|
3261
|
+
<script defer src="/gm/js/stt-handler.js"></script>
|
|
3262
|
+
<script defer src="/gm/js/voice.js"></script>
|
|
3263
|
+
<script defer src="/gm/js/pm2-monitor.js"></script>
|
|
3264
|
+
<script defer src="/gm/js/client.js"></script>
|
|
3265
|
+
<script defer src="/gm/js/features.js"></script>
|
|
3266
|
+
<script defer src="/gm/js/agent-auth.js"></script>
|
|
3087
3267
|
|
|
3088
3268
|
<script>
|
|
3089
3269
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
package/static/js/client.js
CHANGED
|
@@ -2362,6 +2362,7 @@ class AgentGUIClient {
|
|
|
2362
2362
|
|
|
2363
2363
|
this.cacheCurrentConversation();
|
|
2364
2364
|
this.stopChunkPolling();
|
|
2365
|
+
this.removeScrollUpDetection();
|
|
2365
2366
|
if (this.renderer.resetScrollState) this.renderer.resetScrollState();
|
|
2366
2367
|
this._userScrolledUp = false;
|
|
2367
2368
|
this._removeNewContentPill();
|
|
@@ -2614,6 +2615,7 @@ class AgentGUIClient {
|
|
|
2614
2615
|
}
|
|
2615
2616
|
|
|
2616
2617
|
this.restoreScrollPosition(conversationId);
|
|
2618
|
+
this.setupScrollUpDetection(conversationId);
|
|
2617
2619
|
}
|
|
2618
2620
|
} catch (error) {
|
|
2619
2621
|
if (error.name === 'AbortError') return;
|
|
@@ -2622,6 +2624,117 @@ class AgentGUIClient {
|
|
|
2622
2624
|
}
|
|
2623
2625
|
}
|
|
2624
2626
|
|
|
2627
|
+
removeScrollUpDetection() {
|
|
2628
|
+
const scrollContainer = document.getElementById(this.config.scrollContainerId);
|
|
2629
|
+
if (scrollContainer && this._scrollUpHandler) {
|
|
2630
|
+
scrollContainer.removeEventListener('scroll', this._scrollUpHandler);
|
|
2631
|
+
this._scrollUpHandler = null;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
setupScrollUpDetection(conversationId) {
|
|
2636
|
+
const scrollContainer = document.getElementById(this.config.scrollContainerId);
|
|
2637
|
+
if (!scrollContainer) return;
|
|
2638
|
+
|
|
2639
|
+
if (!this._scrollDetectionState) this._scrollDetectionState = {};
|
|
2640
|
+
|
|
2641
|
+
const detectionState = {
|
|
2642
|
+
isLoading: false,
|
|
2643
|
+
oldestTimestamp: Date.now(),
|
|
2644
|
+
oldestMessageId: null,
|
|
2645
|
+
conversation: conversationId
|
|
2646
|
+
};
|
|
2647
|
+
|
|
2648
|
+
const handleScroll = async () => {
|
|
2649
|
+
const scrollTop = scrollContainer.scrollTop;
|
|
2650
|
+
const scrollHeight = scrollContainer.scrollHeight;
|
|
2651
|
+
const clientHeight = scrollContainer.clientHeight;
|
|
2652
|
+
const THRESHOLD = 300;
|
|
2653
|
+
|
|
2654
|
+
if (scrollTop < THRESHOLD && !detectionState.isLoading && scrollHeight > clientHeight) {
|
|
2655
|
+
detectionState.isLoading = true;
|
|
2656
|
+
|
|
2657
|
+
try {
|
|
2658
|
+
const messagesEl = document.querySelector('.conversation-messages');
|
|
2659
|
+
if (!messagesEl) {
|
|
2660
|
+
detectionState.isLoading = false;
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
const firstMessageEl = messagesEl.querySelector('.message[data-msg-id]');
|
|
2665
|
+
if (!firstMessageEl) {
|
|
2666
|
+
const firstChunkEl = messagesEl.querySelector('[data-chunk-created]');
|
|
2667
|
+
if (firstChunkEl) {
|
|
2668
|
+
detectionState.oldestTimestamp = parseInt(firstChunkEl.getAttribute('data-chunk-created')) || 0;
|
|
2669
|
+
}
|
|
2670
|
+
} else {
|
|
2671
|
+
detectionState.oldestMessageId = firstMessageEl.getAttribute('data-msg-id');
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
let result;
|
|
2675
|
+
if (detectionState.oldestMessageId) {
|
|
2676
|
+
result = await window.wsClient.rpc('msg.ls.earlier', {
|
|
2677
|
+
id: conversationId,
|
|
2678
|
+
before: detectionState.oldestMessageId,
|
|
2679
|
+
limit: 50
|
|
2680
|
+
});
|
|
2681
|
+
} else if (detectionState.oldestTimestamp > 0) {
|
|
2682
|
+
result = await window.wsClient.rpc('conv.chunks.earlier', {
|
|
2683
|
+
id: conversationId,
|
|
2684
|
+
before: detectionState.oldestTimestamp,
|
|
2685
|
+
limit: 500
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
if (result && ((result.messages && result.messages.length > 0) || (result.chunks && result.chunks.length > 0))) {
|
|
2690
|
+
const scrollHeightBefore = scrollContainer.scrollHeight;
|
|
2691
|
+
const scrollTopBefore = scrollContainer.scrollTop;
|
|
2692
|
+
const newContent = document.createDocumentFragment();
|
|
2693
|
+
|
|
2694
|
+
if (result.messages && result.messages.length > 0) {
|
|
2695
|
+
result.messages.forEach(msg => {
|
|
2696
|
+
const div = document.createElement('div');
|
|
2697
|
+
div.className = `message message-${msg.role}`;
|
|
2698
|
+
div.setAttribute('data-msg-id', msg.id);
|
|
2699
|
+
div.innerHTML = `<div class="message-role">${msg.role.charAt(0).toUpperCase() + msg.role.slice(1)}</div>${this.renderMessageContent(msg.content)}<div class="message-timestamp">${new Date(msg.created_at).toLocaleString()}</div>`;
|
|
2700
|
+
newContent.appendChild(div);
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
if (result.chunks && result.chunks.length > 0) {
|
|
2705
|
+
result.chunks.forEach(chunk => {
|
|
2706
|
+
const blockEl = this.renderer.renderBlock(chunk.data, chunk.type, false);
|
|
2707
|
+
if (blockEl) {
|
|
2708
|
+
const wrapper = document.createElement('div');
|
|
2709
|
+
wrapper.setAttribute('data-chunk-created', chunk.created_at);
|
|
2710
|
+
wrapper.appendChild(blockEl);
|
|
2711
|
+
newContent.appendChild(wrapper);
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
if (messagesEl.firstChild) {
|
|
2717
|
+
messagesEl.insertBefore(newContent, messagesEl.firstChild);
|
|
2718
|
+
} else {
|
|
2719
|
+
messagesEl.appendChild(newContent);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
const scrollHeightAfter = scrollContainer.scrollHeight;
|
|
2723
|
+
scrollContainer.scrollTop = scrollTopBefore + (scrollHeightAfter - scrollHeightBefore);
|
|
2724
|
+
}
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
console.error('Failed to load earlier messages:', error);
|
|
2727
|
+
} finally {
|
|
2728
|
+
detectionState.isLoading = false;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
|
|
2733
|
+
scrollContainer.removeEventListener('scroll', this._scrollUpHandler);
|
|
2734
|
+
this._scrollUpHandler = handleScroll;
|
|
2735
|
+
scrollContainer.addEventListener('scroll', this._scrollUpHandler, { passive: true });
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2625
2738
|
renderMessagesFragment(messages) {
|
|
2626
2739
|
const frag = document.createDocumentFragment();
|
|
2627
2740
|
if (messages.length === 0) {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
const ACTIVE_STATES = new Set(['online', 'launching', 'stopping', 'waiting restart']);
|
|
3
|
+
|
|
4
|
+
window.pm2Monitor = { processes: [], logsModalOpen: null, initialized: false };
|
|
5
|
+
|
|
6
|
+
const panel = document.getElementById('pm2MonitorPanel');
|
|
7
|
+
const list = document.getElementById('pm2ProcessList');
|
|
8
|
+
const refreshBtn = document.getElementById('pm2RefreshBtn');
|
|
9
|
+
|
|
10
|
+
function send(msg) {
|
|
11
|
+
if (window.wsManager && window.wsManager.isConnected) window.wsManager.sendMessage(msg);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function setPanelVisible(visible) {
|
|
15
|
+
if (!panel) return;
|
|
16
|
+
panel.style.display = visible ? 'flex' : 'none';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderProcessList(processes) {
|
|
20
|
+
if (!list) return;
|
|
21
|
+
const active = processes.filter(p => ACTIVE_STATES.has(p.status));
|
|
22
|
+
const inactive = processes.filter(p => !ACTIVE_STATES.has(p.status));
|
|
23
|
+
const ordered = [...active, ...inactive];
|
|
24
|
+
list.innerHTML = ordered.map(proc => {
|
|
25
|
+
const isActive = ACTIVE_STATES.has(proc.status);
|
|
26
|
+
const uptimeSec = proc.uptime ? Math.floor((Date.now() - proc.uptime) / 1000) : null;
|
|
27
|
+
return `<div class="pm2-process-item ${proc.status}" data-pm2-name="${escAttr(proc.name)}">
|
|
28
|
+
<div class="pm2-process-name">${escHtml(proc.name)} <span class="pm2-status-dot pm2-status-${proc.status}"></span></div>
|
|
29
|
+
<div class="pm2-process-meta">
|
|
30
|
+
<span>${proc.status}</span>
|
|
31
|
+
<span>${(proc.cpu || 0).toFixed(1)}% CPU</span>
|
|
32
|
+
<span>${fmtBytes(proc.memory)}</span>
|
|
33
|
+
${uptimeSec !== null ? `<span>${fmtUptime(uptimeSec)}</span>` : ''}
|
|
34
|
+
<span>#${proc.pid || '-'}</span>
|
|
35
|
+
${proc.restarts > 0 ? `<span>${proc.restarts}x restarts</span>` : ''}
|
|
36
|
+
</div>
|
|
37
|
+
<div class="pm2-process-actions">
|
|
38
|
+
${!isActive ? `<button class="pm2-btn" onclick="window.pm2Monitor.startProcess('${escAttr(proc.name)}')">Start</button>` : ''}
|
|
39
|
+
${proc.status === 'online' ? `<button class="pm2-btn danger" onclick="window.pm2Monitor.stopProcess('${escAttr(proc.name)}')">Stop</button>` : ''}
|
|
40
|
+
<button class="pm2-btn" onclick="window.pm2Monitor.restartProcess('${escAttr(proc.name)}')">Restart</button>
|
|
41
|
+
<button class="pm2-btn" onclick="window.pm2Monitor.showLogs('${escAttr(proc.name)}')">Logs</button>
|
|
42
|
+
<button class="pm2-btn danger" onclick="window.pm2Monitor.deleteProcess('${escAttr(proc.name)}')">Delete</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>`;
|
|
45
|
+
}).join('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleMessage(msg) {
|
|
49
|
+
if (msg.type === 'pm2_monit_update') {
|
|
50
|
+
const procs = msg.processes || [];
|
|
51
|
+
window.pm2Monitor.processes = procs;
|
|
52
|
+
const hasActive = msg.hasActive || procs.some(p => ACTIVE_STATES.has(p.status));
|
|
53
|
+
setPanelVisible(hasActive);
|
|
54
|
+
if (hasActive) renderProcessList(procs);
|
|
55
|
+
} else if (msg.type === 'pm2_list_response') {
|
|
56
|
+
const procs = msg.processes || [];
|
|
57
|
+
window.pm2Monitor.processes = procs;
|
|
58
|
+
const hasActive = procs.some(p => ACTIVE_STATES.has(p.status));
|
|
59
|
+
setPanelVisible(hasActive);
|
|
60
|
+
if (hasActive) renderProcessList(procs);
|
|
61
|
+
} else if (msg.type === 'pm2_unavailable') {
|
|
62
|
+
setPanelVisible(false);
|
|
63
|
+
} else if (msg.type === 'pm2_start_response' || msg.type === 'pm2_stop_response' ||
|
|
64
|
+
msg.type === 'pm2_restart_response' || msg.type === 'pm2_delete_response') {
|
|
65
|
+
const action = msg.type.replace('pm2_', '').replace('_response', '');
|
|
66
|
+
if (msg.success) {
|
|
67
|
+
toast(`PM2 ${action} ${msg.name} succeeded`, 'success');
|
|
68
|
+
setTimeout(() => send({ type: 'pm2_list' }), 500);
|
|
69
|
+
} else {
|
|
70
|
+
toast(`PM2 ${action} ${msg.name} failed: ${msg.error}`, 'error');
|
|
71
|
+
}
|
|
72
|
+
} else if (msg.type === 'pm2_logs_response') {
|
|
73
|
+
if (msg.success) showLogsModal(msg.name, msg.logs);
|
|
74
|
+
else toast(`Logs error: ${msg.error}`, 'error');
|
|
75
|
+
} else if (msg.type === 'pm2_flush_logs_response') {
|
|
76
|
+
toast(msg.success ? 'Logs flushed' : `Flush failed: ${msg.error}`, msg.success ? 'success' : 'error');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function init() {
|
|
81
|
+
if (window.pm2Monitor.initialized) return;
|
|
82
|
+
window.pm2Monitor.initialized = true;
|
|
83
|
+
setPanelVisible(false);
|
|
84
|
+
if (refreshBtn) refreshBtn.addEventListener('click', () => send({ type: 'pm2_list' }));
|
|
85
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && window.pm2Monitor.logsModalOpen) window.pm2Monitor.closeLogsModal(); });
|
|
86
|
+
if (window.wsManager) {
|
|
87
|
+
window.wsManager.on('message', handleMessage);
|
|
88
|
+
window.wsManager.on('connected', () => {
|
|
89
|
+
send({ type: 'pm2_start_monitoring' });
|
|
90
|
+
send({ type: 'pm2_list' });
|
|
91
|
+
});
|
|
92
|
+
if (window.wsManager.isConnected) {
|
|
93
|
+
send({ type: 'pm2_start_monitoring' });
|
|
94
|
+
send({ type: 'pm2_list' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function showLogsModal(name, logs) {
|
|
100
|
+
if (window.pm2Monitor.logsModalOpen) window.pm2Monitor.closeLogsModal();
|
|
101
|
+
const modal = document.createElement('div');
|
|
102
|
+
modal.className = 'pm2-logs-modal';
|
|
103
|
+
modal.innerHTML = `<div class="pm2-logs-content">
|
|
104
|
+
<div class="pm2-logs-header"><span>Logs: ${escHtml(name)}</span><button class="pm2-btn" onclick="window.pm2Monitor.closeLogsModal()">Close</button></div>
|
|
105
|
+
<div class="pm2-logs-body">${escHtml(logs || 'No logs available')}</div>
|
|
106
|
+
<div class="pm2-logs-footer">
|
|
107
|
+
<button class="pm2-btn" onclick="window.pm2Monitor.flushLogs('${escAttr(name)}')">Flush</button>
|
|
108
|
+
<button class="pm2-btn" onclick="window.pm2Monitor.closeLogsModal()">Close</button>
|
|
109
|
+
</div></div>`;
|
|
110
|
+
document.body.appendChild(modal);
|
|
111
|
+
window.pm2Monitor.logsModalOpen = name;
|
|
112
|
+
modal.addEventListener('click', (e) => { if (e.target === modal) window.pm2Monitor.closeLogsModal(); });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toast(msg, type) {
|
|
116
|
+
const t = document.createElement('div');
|
|
117
|
+
t.className = 'pm2-toast';
|
|
118
|
+
t.textContent = msg;
|
|
119
|
+
const bg = type === 'error' ? 'var(--color-error)' : type === 'success' ? 'var(--color-success)' : 'var(--color-primary)';
|
|
120
|
+
t.style.cssText = `position:fixed;top:1rem;right:1rem;padding:0.5rem 1rem;border-radius:0.375rem;font-size:0.8rem;color:white;z-index:10000;background:${bg}`;
|
|
121
|
+
document.body.appendChild(t);
|
|
122
|
+
setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(() => t.remove(), 300); }, 3000);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function escHtml(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
126
|
+
function escAttr(s) { return (s || '').replace(/"/g, '"').replace(/'/g, '''); }
|
|
127
|
+
function fmtBytes(b) {
|
|
128
|
+
if (!b || typeof b !== 'number') return '0 B';
|
|
129
|
+
const u = ['B','KB','MB','GB'], i = Math.min(Math.floor(Math.log(b) / Math.log(1024)), 3);
|
|
130
|
+
return (b / Math.pow(1024, i)).toFixed(1) + ' ' + u[i];
|
|
131
|
+
}
|
|
132
|
+
function fmtUptime(s) {
|
|
133
|
+
if (!s || s < 0) return '0s';
|
|
134
|
+
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
|
|
135
|
+
if (d > 0) return `${d}d ${h}h`;
|
|
136
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
137
|
+
if (m > 0) return `${m}m ${sec}s`;
|
|
138
|
+
return `${sec}s`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
window.pm2Monitor.closeLogsModal = () => { const m = document.querySelector('.pm2-logs-modal'); if (m) m.remove(); window.pm2Monitor.logsModalOpen = null; };
|
|
142
|
+
window.pm2Monitor.startProcess = (name) => send({ type: 'pm2_start', name });
|
|
143
|
+
window.pm2Monitor.stopProcess = (name) => send({ type: 'pm2_stop', name });
|
|
144
|
+
window.pm2Monitor.restartProcess = (name) => send({ type: 'pm2_restart', name });
|
|
145
|
+
window.pm2Monitor.deleteProcess = (name) => { if (confirm(`Delete PM2 process "${name}"?`)) send({ type: 'pm2_delete', name }); };
|
|
146
|
+
window.pm2Monitor.showLogs = (name) => send({ type: 'pm2_logs', name, lines: 200 });
|
|
147
|
+
window.pm2Monitor.flushLogs = (name) => send({ type: 'pm2_flush_logs', name });
|
|
148
|
+
|
|
149
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => setTimeout(init, 150));
|
|
150
|
+
else setTimeout(init, 150);
|
|
151
|
+
})();
|