consensus-cli 0.1.0
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/CHANGELOG.md +12 -0
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +134 -0
- package/dist/activity.js +14 -0
- package/dist/cli.js +61 -0
- package/dist/codexLogs.js +332 -0
- package/dist/redact.js +20 -0
- package/dist/scan.js +259 -0
- package/dist/server.js +61 -0
- package/dist/tail.js +27 -0
- package/dist/types.js +1 -0
- package/package.json +57 -0
- package/public/app.js +636 -0
- package/public/index.html +56 -0
- package/public/iso.js +68 -0
- package/public/style.css +364 -0
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "consensus-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/integrate-your-mind/consensus-cli.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/integrate-your-mind/consensus-cli/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/integrate-your-mind/consensus-cli#readme",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/server.js",
|
|
16
|
+
"bin": {
|
|
17
|
+
"consensus": "dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"public",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"NOTICE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"dev": "tsx watch src/server.ts",
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"start": "node dist/server.js",
|
|
31
|
+
"scan": "node dist/scan.js",
|
|
32
|
+
"tail": "node dist/tail.js",
|
|
33
|
+
"test": "npm run test:unit && npm run test:integration",
|
|
34
|
+
"test:unit": "node --test --import tsx tests/unit/**/*.test.ts",
|
|
35
|
+
"test:integration": "node --test --import tsx tests/integration/**/*.test.ts",
|
|
36
|
+
"test:watch": "node --test --watch --import tsx tests/unit/**/*.test.ts tests/integration/**/*.test.ts",
|
|
37
|
+
"test:ui": "playwright test",
|
|
38
|
+
"prepublishOnly": "npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"express": "^4.19.2",
|
|
42
|
+
"pidusage": "^3.0.2",
|
|
43
|
+
"ps-list": "^8.1.1",
|
|
44
|
+
"ws": "^8.17.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@playwright/test": "^1.58.0",
|
|
48
|
+
"@types/express": "^4.17.21",
|
|
49
|
+
"@types/node": "^20.14.2",
|
|
50
|
+
"@types/ws": "^8.5.10",
|
|
51
|
+
"tsx": "^4.15.7",
|
|
52
|
+
"typescript": "^5.5.4"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { isoToScreen, drawDiamond, drawBuilding, pointInDiamond } from "./iso.js";
|
|
2
|
+
|
|
3
|
+
const canvas = document.getElementById("scene");
|
|
4
|
+
const ctx = canvas.getContext("2d");
|
|
5
|
+
const tooltip = document.getElementById("tooltip");
|
|
6
|
+
const panel = document.getElementById("panel");
|
|
7
|
+
const panelContent = document.getElementById("panel-content");
|
|
8
|
+
const panelClose = document.getElementById("panel-close");
|
|
9
|
+
const statusEl = document.getElementById("status");
|
|
10
|
+
const countEl = document.getElementById("count");
|
|
11
|
+
const activeList = document.getElementById("active-list");
|
|
12
|
+
const searchInput = document.getElementById("search");
|
|
13
|
+
const laneTitle = document.querySelector(".lane-title");
|
|
14
|
+
|
|
15
|
+
const tileW = 96;
|
|
16
|
+
const tileH = 48;
|
|
17
|
+
const gridScale = 2;
|
|
18
|
+
|
|
19
|
+
const query = new URLSearchParams(window.location.search);
|
|
20
|
+
const mockMode = query.get("mock") === "1";
|
|
21
|
+
|
|
22
|
+
const statePalette = {
|
|
23
|
+
active: { top: "#3d8f7f", left: "#2d6d61", right: "#275b52", stroke: "#54cdb1" },
|
|
24
|
+
idle: { top: "#384a57", left: "#2b3943", right: "#25323b", stroke: "#4f6b7a" },
|
|
25
|
+
error: { top: "#82443c", left: "#6d3530", right: "#5a2c28", stroke: "#d1584b" },
|
|
26
|
+
};
|
|
27
|
+
const stateOpacity = {
|
|
28
|
+
active: 1,
|
|
29
|
+
idle: 0.35,
|
|
30
|
+
error: 0.9,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const view = {
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
scale: 1,
|
|
37
|
+
dragging: false,
|
|
38
|
+
lastX: 0,
|
|
39
|
+
lastY: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let deviceScale = 1;
|
|
43
|
+
let agents = [];
|
|
44
|
+
let hovered = null;
|
|
45
|
+
let selected = null;
|
|
46
|
+
let searchQuery = "";
|
|
47
|
+
let searchMatches = new Set();
|
|
48
|
+
|
|
49
|
+
const layout = new Map();
|
|
50
|
+
const occupied = new Map();
|
|
51
|
+
|
|
52
|
+
function resize() {
|
|
53
|
+
deviceScale = window.devicePixelRatio || 1;
|
|
54
|
+
canvas.width = window.innerWidth * deviceScale;
|
|
55
|
+
canvas.height = window.innerHeight * deviceScale;
|
|
56
|
+
canvas.style.width = `${window.innerWidth}px`;
|
|
57
|
+
canvas.style.height = `${window.innerHeight}px`;
|
|
58
|
+
ctx.setTransform(deviceScale, 0, 0, deviceScale, 0, 0);
|
|
59
|
+
view.x = window.innerWidth / 2;
|
|
60
|
+
view.y = window.innerHeight / 2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
window.addEventListener("resize", resize);
|
|
64
|
+
resize();
|
|
65
|
+
|
|
66
|
+
function hashString(input) {
|
|
67
|
+
let hash = 0;
|
|
68
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
69
|
+
hash = (hash << 5) - hash + input.charCodeAt(i);
|
|
70
|
+
hash |= 0;
|
|
71
|
+
}
|
|
72
|
+
return Math.abs(hash);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function keyForAgent(agent) {
|
|
76
|
+
return agent.repo || agent.cwd || agent.cmd || agent.id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function assignCoordinate(key) {
|
|
80
|
+
const hash = hashString(key);
|
|
81
|
+
const baseX = (hash % 16) - 8;
|
|
82
|
+
const baseY = ((hash >> 4) % 16) - 8;
|
|
83
|
+
const maxRadius = 20;
|
|
84
|
+
|
|
85
|
+
for (let radius = 0; radius <= maxRadius; radius += 1) {
|
|
86
|
+
for (let dx = -radius; dx <= radius; dx += 1) {
|
|
87
|
+
for (let dy = -radius; dy <= radius; dy += 1) {
|
|
88
|
+
if (Math.abs(dx) !== radius && Math.abs(dy) !== radius) continue;
|
|
89
|
+
const x = baseX + dx;
|
|
90
|
+
const y = baseY + dy;
|
|
91
|
+
const keyStr = `${x},${y}`;
|
|
92
|
+
if (!occupied.has(keyStr)) {
|
|
93
|
+
occupied.set(keyStr, key);
|
|
94
|
+
layout.set(key, { x: x * gridScale, y: y * gridScale });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
layout.set(key, { x: baseX * gridScale, y: baseY * gridScale });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateLayout(newAgents) {
|
|
105
|
+
const activeKeys = new Set();
|
|
106
|
+
for (const agent of newAgents) {
|
|
107
|
+
const key = keyForAgent(agent);
|
|
108
|
+
activeKeys.add(key);
|
|
109
|
+
if (!layout.has(key)) {
|
|
110
|
+
assignCoordinate(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const [key, coord] of layout.entries()) {
|
|
115
|
+
if (!activeKeys.has(key)) {
|
|
116
|
+
layout.delete(key);
|
|
117
|
+
occupied.delete(`${coord.x},${coord.y}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setStatus(text) {
|
|
123
|
+
statusEl.textContent = text;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setCount(count) {
|
|
127
|
+
countEl.textContent = `${count} agent${count === 1 ? "" : "s"}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatBytes(bytes) {
|
|
131
|
+
if (!bytes) return "0 MB";
|
|
132
|
+
const mb = bytes / (1024 * 1024);
|
|
133
|
+
return `${mb.toFixed(1)} MB`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatPercent(value) {
|
|
137
|
+
return `${value.toFixed(1)}%`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function truncate(text, max) {
|
|
141
|
+
if (!text) return "";
|
|
142
|
+
if (text.length <= max) return text;
|
|
143
|
+
return `${text.slice(0, max - 1)}…`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function escapeHtml(value) {
|
|
147
|
+
if (value === null || value === undefined) return "";
|
|
148
|
+
return String(value)
|
|
149
|
+
.replace(/&/g, "&")
|
|
150
|
+
.replace(/</g, "<")
|
|
151
|
+
.replace(/>/g, ">")
|
|
152
|
+
.replace(/\"/g, """)
|
|
153
|
+
.replace(/'/g, "'");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function labelFor(agent) {
|
|
157
|
+
if (agent.title) return agent.title;
|
|
158
|
+
if (agent.repo) return agent.repo;
|
|
159
|
+
return `codex#${agent.pid}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function matchesQuery(agent, query) {
|
|
163
|
+
const haystack = [
|
|
164
|
+
agent.pid,
|
|
165
|
+
agent.title,
|
|
166
|
+
agent.summary?.current,
|
|
167
|
+
agent.summary?.lastCommand,
|
|
168
|
+
agent.summary?.lastEdit,
|
|
169
|
+
agent.summary?.lastMessage,
|
|
170
|
+
agent.summary?.lastTool,
|
|
171
|
+
agent.summary?.lastPrompt,
|
|
172
|
+
agent.lastEventAt,
|
|
173
|
+
agent.cmd,
|
|
174
|
+
agent.cwd,
|
|
175
|
+
agent.sessionPath,
|
|
176
|
+
agent.model,
|
|
177
|
+
agent.repo,
|
|
178
|
+
agent.kind,
|
|
179
|
+
]
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.map((value) => String(value).toLowerCase())
|
|
182
|
+
.join(" ");
|
|
183
|
+
return haystack.includes(query);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function roundRect(ctx, x, y, width, height, radius) {
|
|
187
|
+
const r = Math.min(radius, width / 2, height / 2);
|
|
188
|
+
ctx.beginPath();
|
|
189
|
+
ctx.moveTo(x + r, y);
|
|
190
|
+
ctx.lineTo(x + width - r, y);
|
|
191
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
192
|
+
ctx.lineTo(x + width, y + height - r);
|
|
193
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
194
|
+
ctx.lineTo(x + r, y + height);
|
|
195
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
196
|
+
ctx.lineTo(x, y + r);
|
|
197
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
198
|
+
ctx.closePath();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function drawTag(ctx, x, y, text, accent) {
|
|
202
|
+
if (!text) return;
|
|
203
|
+
ctx.save();
|
|
204
|
+
ctx.font = "11px IBM Plex Mono";
|
|
205
|
+
ctx.textAlign = "center";
|
|
206
|
+
const paddingX = 8;
|
|
207
|
+
const paddingY = 4;
|
|
208
|
+
const width = ctx.measureText(text).width + paddingX * 2;
|
|
209
|
+
const height = 18;
|
|
210
|
+
const left = x - width / 2;
|
|
211
|
+
const top = y - height;
|
|
212
|
+
ctx.fillStyle = "rgba(10, 14, 18, 0.85)";
|
|
213
|
+
roundRect(ctx, left, top, width, height, 6);
|
|
214
|
+
ctx.fill();
|
|
215
|
+
ctx.strokeStyle = accent;
|
|
216
|
+
ctx.lineWidth = 1;
|
|
217
|
+
ctx.stroke();
|
|
218
|
+
ctx.fillStyle = "#e4e6eb";
|
|
219
|
+
ctx.fillText(text, x, top + height - paddingY - 2);
|
|
220
|
+
ctx.restore();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderActiveList(items) {
|
|
224
|
+
if (!activeList) return;
|
|
225
|
+
if (!items.length) {
|
|
226
|
+
activeList.innerHTML = "<div class=\"lane-meta\">No active agents.</div>";
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const sorted = [...items].sort((a, b) => {
|
|
230
|
+
const rank = { error: 0, active: 1, idle: 2 };
|
|
231
|
+
const rankA = rank[a.state] ?? 3;
|
|
232
|
+
const rankB = rank[b.state] ?? 3;
|
|
233
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
234
|
+
return b.cpu - a.cpu;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
activeList.innerHTML = sorted
|
|
238
|
+
.map((agent) => {
|
|
239
|
+
const doingRaw = agent.summary?.current || agent.doing || agent.cmdShort || "";
|
|
240
|
+
const doing = escapeHtml(truncate(doingRaw, 80));
|
|
241
|
+
const selectedClass = selected && selected.id === agent.id ? "is-selected" : "";
|
|
242
|
+
const label = escapeHtml(labelFor(agent));
|
|
243
|
+
return `
|
|
244
|
+
<button class="lane-item ${selectedClass}" type="button" data-id="${agent.id}">
|
|
245
|
+
<div class="lane-pill ${agent.state}"></div>
|
|
246
|
+
<div class="lane-copy">
|
|
247
|
+
<div class="lane-label">${label}</div>
|
|
248
|
+
<div class="lane-meta">${doing}</div>
|
|
249
|
+
</div>
|
|
250
|
+
</button>
|
|
251
|
+
`;
|
|
252
|
+
})
|
|
253
|
+
.join("");
|
|
254
|
+
|
|
255
|
+
Array.from(activeList.querySelectorAll(".lane-item")).forEach((item) => {
|
|
256
|
+
item.addEventListener("click", () => {
|
|
257
|
+
const id = item.getAttribute("data-id");
|
|
258
|
+
selected = sorted.find((agent) => agent.id === id) || null;
|
|
259
|
+
renderPanel(selected);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderPanel(agent) {
|
|
265
|
+
if (!agent) {
|
|
266
|
+
panel.classList.remove("open");
|
|
267
|
+
panelContent.innerHTML = "";
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
panel.classList.add("open");
|
|
271
|
+
const events = agent.events || [];
|
|
272
|
+
const orderedEvents = [...events].reverse();
|
|
273
|
+
const summary = agent.summary || {};
|
|
274
|
+
const summaryRows = [
|
|
275
|
+
["current", summary.current || agent.doing],
|
|
276
|
+
["last command", summary.lastCommand],
|
|
277
|
+
["last edit", summary.lastEdit],
|
|
278
|
+
["last tool", summary.lastTool],
|
|
279
|
+
["last message", summary.lastMessage],
|
|
280
|
+
["last prompt", summary.lastPrompt],
|
|
281
|
+
].filter((entry) => entry[1]);
|
|
282
|
+
const lastEventAt = agent.lastEventAt
|
|
283
|
+
? new Date(agent.lastEventAt).toLocaleTimeString()
|
|
284
|
+
: null;
|
|
285
|
+
const showMetadata = searchQuery.trim().length > 0;
|
|
286
|
+
panelContent.innerHTML = `
|
|
287
|
+
<div class="panel-section">
|
|
288
|
+
<h4>Identity</h4>
|
|
289
|
+
<div class="panel-list">
|
|
290
|
+
<div><span class="panel-key">name</span>${escapeHtml(labelFor(agent))}</div>
|
|
291
|
+
<div><span class="panel-key">pid</span>${escapeHtml(agent.pid)}</div>
|
|
292
|
+
<div><span class="panel-key">kind</span>${escapeHtml(agent.kind)}</div>
|
|
293
|
+
<div><span class="panel-key">state</span>${escapeHtml(agent.state)}</div>
|
|
294
|
+
${agent.startedAt ? `<div><span class="panel-key">started</span>${escapeHtml(new Date(agent.startedAt * 1000).toLocaleString())}</div>` : ""}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="panel-section">
|
|
298
|
+
<h4>Work</h4>
|
|
299
|
+
<div class="panel-list">
|
|
300
|
+
${
|
|
301
|
+
summaryRows.length
|
|
302
|
+
? summaryRows
|
|
303
|
+
.map(
|
|
304
|
+
([label, value]) =>
|
|
305
|
+
`<div><span class="panel-key">${escapeHtml(label)}</span>${escapeHtml(value)}</div>`
|
|
306
|
+
)
|
|
307
|
+
.join("")
|
|
308
|
+
: "<div>-</div>"
|
|
309
|
+
}
|
|
310
|
+
${lastEventAt ? `<div><span class="panel-key">last event</span>${escapeHtml(lastEventAt)}</div>` : ""}
|
|
311
|
+
<div><span class="panel-key">cpu</span>${escapeHtml(formatPercent(agent.cpu))}</div>
|
|
312
|
+
<div><span class="panel-key">mem</span>${escapeHtml(formatBytes(agent.mem))}</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
${
|
|
316
|
+
showMetadata
|
|
317
|
+
? `
|
|
318
|
+
<div class="panel-section">
|
|
319
|
+
<h4>Metadata</h4>
|
|
320
|
+
<div class="panel-list">
|
|
321
|
+
<div><span class="panel-key">repo</span>${escapeHtml(agent.repo || "-")}</div>
|
|
322
|
+
<div><span class="panel-key">cwd</span>${escapeHtml(agent.cwd || "-")}</div>
|
|
323
|
+
<div><span class="panel-key">session</span>${escapeHtml(agent.sessionPath || "-")}</div>
|
|
324
|
+
<div><span class="panel-key">cmd</span>${escapeHtml(agent.cmd || "-")}</div>
|
|
325
|
+
<div><span class="panel-key">model</span>${escapeHtml(agent.model || "-")}</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
`
|
|
329
|
+
: `
|
|
330
|
+
<div class="panel-section">
|
|
331
|
+
<h4>Metadata</h4>
|
|
332
|
+
<div class="panel-list">
|
|
333
|
+
<div>Search to reveal metadata.</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
`
|
|
337
|
+
}
|
|
338
|
+
<div class="panel-section">
|
|
339
|
+
<h4>Recent Events</h4>
|
|
340
|
+
<div class="panel-list">
|
|
341
|
+
${
|
|
342
|
+
orderedEvents.length
|
|
343
|
+
? orderedEvents
|
|
344
|
+
.map((ev) => {
|
|
345
|
+
const time = new Date(ev.ts).toLocaleTimeString();
|
|
346
|
+
return `<div>[${escapeHtml(time)}] ${escapeHtml(truncate(ev.summary, 120))}</div>`;
|
|
347
|
+
})
|
|
348
|
+
.join("")
|
|
349
|
+
: "<div>-</div>"
|
|
350
|
+
}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
panelClose.addEventListener("click", () => {
|
|
357
|
+
selected = null;
|
|
358
|
+
renderPanel(null);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
function draw() {
|
|
362
|
+
ctx.setTransform(deviceScale, 0, 0, deviceScale, 0, 0);
|
|
363
|
+
ctx.clearRect(0, 0, canvas.width / deviceScale, canvas.height / deviceScale);
|
|
364
|
+
ctx.save();
|
|
365
|
+
ctx.translate(view.x, view.y);
|
|
366
|
+
ctx.scale(view.scale, view.scale);
|
|
367
|
+
|
|
368
|
+
if (!agents.length) {
|
|
369
|
+
ctx.fillStyle = "rgba(228, 230, 235, 0.6)";
|
|
370
|
+
ctx.font = "16px Space Grotesk";
|
|
371
|
+
ctx.textAlign = "center";
|
|
372
|
+
ctx.fillText("No codex processes found", 0, 0);
|
|
373
|
+
ctx.restore();
|
|
374
|
+
requestAnimationFrame(draw);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
updateLayout(agents);
|
|
379
|
+
|
|
380
|
+
const drawList = agents
|
|
381
|
+
.map((agent) => {
|
|
382
|
+
const key = keyForAgent(agent);
|
|
383
|
+
const coord = layout.get(key) || { x: 0, y: 0 };
|
|
384
|
+
const screen = isoToScreen(coord.x, coord.y, tileW, tileH);
|
|
385
|
+
return { agent, key, coord, screen };
|
|
386
|
+
})
|
|
387
|
+
.sort((a, b) => a.coord.x + a.coord.y - (b.coord.x + b.coord.y));
|
|
388
|
+
|
|
389
|
+
const activeAgents = drawList
|
|
390
|
+
.filter((item) => item.agent.state !== "idle")
|
|
391
|
+
.sort((a, b) => b.agent.cpu - a.agent.cpu);
|
|
392
|
+
const topActiveIds = new Set(activeAgents.slice(0, 4).map((item) => item.agent.id));
|
|
393
|
+
|
|
394
|
+
const time = Date.now();
|
|
395
|
+
const hitList = [];
|
|
396
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
397
|
+
|
|
398
|
+
for (const item of drawList) {
|
|
399
|
+
const palette = statePalette[item.agent.state] || statePalette.idle;
|
|
400
|
+
const memMB = item.agent.mem / (1024 * 1024);
|
|
401
|
+
const heightBase = Math.min(120, Math.max(18, memMB * 0.4));
|
|
402
|
+
const pulse =
|
|
403
|
+
item.agent.state === "active" && !reducedMotion
|
|
404
|
+
? 4 + Math.sin(time / 200) * 3
|
|
405
|
+
: 0;
|
|
406
|
+
const idleScale = item.agent.state === "idle" ? 0.6 : 1;
|
|
407
|
+
const height = heightBase * idleScale + pulse;
|
|
408
|
+
|
|
409
|
+
const x = item.screen.x;
|
|
410
|
+
const y = item.screen.y;
|
|
411
|
+
|
|
412
|
+
ctx.globalAlpha = stateOpacity[item.agent.state] ?? 1;
|
|
413
|
+
drawBuilding(ctx, x, y, tileW, tileH, height, palette);
|
|
414
|
+
drawDiamond(ctx, x, y, tileW, tileH, "rgba(16, 22, 28, 0.8)", "#3e4e59");
|
|
415
|
+
ctx.globalAlpha = 1;
|
|
416
|
+
|
|
417
|
+
const roofSize = tileW * 0.28;
|
|
418
|
+
drawDiamond(
|
|
419
|
+
ctx,
|
|
420
|
+
x,
|
|
421
|
+
y - height - tileH * 0.15,
|
|
422
|
+
roofSize,
|
|
423
|
+
roofSize * 0.5,
|
|
424
|
+
palette.stroke,
|
|
425
|
+
null
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (selected && selected.id === item.agent.id) {
|
|
429
|
+
drawDiamond(ctx, x, y, tileW + 10, tileH + 6, "rgba(0,0,0,0)", "#57f2c6");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
ctx.fillStyle = "rgba(10, 12, 15, 0.6)";
|
|
433
|
+
ctx.beginPath();
|
|
434
|
+
ctx.ellipse(x, y + tileH * 0.7, tileW * 0.4, tileH * 0.2, 0, 0, Math.PI * 2);
|
|
435
|
+
ctx.fill();
|
|
436
|
+
|
|
437
|
+
const isHovered = hovered && hovered.id === item.agent.id;
|
|
438
|
+
const isSelected = selected && selected.id === item.agent.id;
|
|
439
|
+
const showActiveTag = topActiveIds.has(item.agent.id);
|
|
440
|
+
if (isHovered || isSelected) {
|
|
441
|
+
const label = truncate(labelFor(item.agent), 20);
|
|
442
|
+
drawTag(ctx, x, y - height - tileH * 0.6, label, "rgba(87, 242, 198, 0.6)");
|
|
443
|
+
const doing = truncate(item.agent.summary?.current || item.agent.doing || "", 36);
|
|
444
|
+
drawTag(ctx, x, y - height - tileH * 0.9, doing, "rgba(87, 242, 198, 0.35)");
|
|
445
|
+
} else if (showActiveTag) {
|
|
446
|
+
const doing = truncate(
|
|
447
|
+
item.agent.summary?.current || item.agent.doing || labelFor(item.agent),
|
|
448
|
+
32
|
|
449
|
+
);
|
|
450
|
+
drawTag(ctx, x, y - height - tileH * 0.7, doing, "rgba(87, 242, 198, 0.35)");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
hitList.push({
|
|
454
|
+
x,
|
|
455
|
+
y,
|
|
456
|
+
agent: item.agent,
|
|
457
|
+
key: item.key,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
ctx.restore();
|
|
462
|
+
|
|
463
|
+
canvas._hitList = hitList;
|
|
464
|
+
requestAnimationFrame(draw);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function screenToWorld(x, y) {
|
|
468
|
+
return {
|
|
469
|
+
x: (x - view.x) / view.scale,
|
|
470
|
+
y: (y - view.y) / view.scale,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
canvas.addEventListener("mousemove", (event) => {
|
|
475
|
+
const rect = canvas.getBoundingClientRect();
|
|
476
|
+
const pos = screenToWorld(event.clientX - rect.left, event.clientY - rect.top);
|
|
477
|
+
const hitList = canvas._hitList || [];
|
|
478
|
+
let found = null;
|
|
479
|
+
for (let i = hitList.length - 1; i >= 0; i -= 1) {
|
|
480
|
+
const item = hitList[i];
|
|
481
|
+
if (pointInDiamond(pos.x, pos.y, item.x, item.y, tileW, tileH)) {
|
|
482
|
+
found = item.agent;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
hovered = found;
|
|
487
|
+
if (hovered) {
|
|
488
|
+
tooltip.classList.remove("hidden");
|
|
489
|
+
tooltip.style.left = `${event.clientX}px`;
|
|
490
|
+
tooltip.style.top = `${event.clientY}px`;
|
|
491
|
+
const doing = truncate(hovered.summary?.current || hovered.doing || hovered.cmdShort || "", 120);
|
|
492
|
+
tooltip.textContent = `${labelFor(hovered)} | ${doing}`;
|
|
493
|
+
} else {
|
|
494
|
+
tooltip.classList.add("hidden");
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
canvas.addEventListener("mouseleave", () => {
|
|
499
|
+
hovered = null;
|
|
500
|
+
tooltip.classList.add("hidden");
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
canvas.addEventListener("click", () => {
|
|
504
|
+
if (hovered) {
|
|
505
|
+
selected = hovered;
|
|
506
|
+
renderPanel(selected);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
canvas.addEventListener("mousedown", (event) => {
|
|
511
|
+
view.dragging = true;
|
|
512
|
+
view.lastX = event.clientX;
|
|
513
|
+
view.lastY = event.clientY;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
window.addEventListener("mouseup", () => {
|
|
517
|
+
view.dragging = false;
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
window.addEventListener("mousemove", (event) => {
|
|
521
|
+
if (!view.dragging) return;
|
|
522
|
+
const dx = event.clientX - view.lastX;
|
|
523
|
+
const dy = event.clientY - view.lastY;
|
|
524
|
+
view.x += dx;
|
|
525
|
+
view.y += dy;
|
|
526
|
+
view.lastX = event.clientX;
|
|
527
|
+
view.lastY = event.clientY;
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (searchInput) {
|
|
531
|
+
searchInput.addEventListener("input", (event) => {
|
|
532
|
+
const target = event.target;
|
|
533
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
534
|
+
searchQuery = target.value || "";
|
|
535
|
+
applySnapshot({ agents, ts: Date.now() });
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
canvas.addEventListener(
|
|
540
|
+
"wheel",
|
|
541
|
+
(event) => {
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
const delta = Math.sign(event.deltaY) * -0.1;
|
|
544
|
+
view.scale = Math.min(2.5, Math.max(0.4, view.scale + delta));
|
|
545
|
+
},
|
|
546
|
+
{ passive: false }
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
canvas.addEventListener("keydown", (event) => {
|
|
550
|
+
const panStep = 24;
|
|
551
|
+
switch (event.key) {
|
|
552
|
+
case "ArrowUp":
|
|
553
|
+
view.y += panStep;
|
|
554
|
+
event.preventDefault();
|
|
555
|
+
break;
|
|
556
|
+
case "ArrowDown":
|
|
557
|
+
view.y -= panStep;
|
|
558
|
+
event.preventDefault();
|
|
559
|
+
break;
|
|
560
|
+
case "ArrowLeft":
|
|
561
|
+
view.x += panStep;
|
|
562
|
+
event.preventDefault();
|
|
563
|
+
break;
|
|
564
|
+
case "ArrowRight":
|
|
565
|
+
view.x -= panStep;
|
|
566
|
+
event.preventDefault();
|
|
567
|
+
break;
|
|
568
|
+
case "+":
|
|
569
|
+
case "=":
|
|
570
|
+
view.scale = Math.min(2.5, view.scale + 0.1);
|
|
571
|
+
event.preventDefault();
|
|
572
|
+
break;
|
|
573
|
+
case "-":
|
|
574
|
+
view.scale = Math.max(0.4, view.scale - 0.1);
|
|
575
|
+
event.preventDefault();
|
|
576
|
+
break;
|
|
577
|
+
default:
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
function connect() {
|
|
583
|
+
const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
584
|
+
const ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
|
|
585
|
+
|
|
586
|
+
ws.addEventListener("open", () => {
|
|
587
|
+
setStatus("live");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
ws.addEventListener("message", (event) => {
|
|
591
|
+
const payload = JSON.parse(event.data);
|
|
592
|
+
applySnapshot(payload);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
ws.addEventListener("close", () => {
|
|
596
|
+
setStatus("disconnected");
|
|
597
|
+
setTimeout(connect, 1000);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function applySnapshot(payload) {
|
|
602
|
+
agents = payload.agents || [];
|
|
603
|
+
setCount(agents.length);
|
|
604
|
+
const query = searchQuery.trim().toLowerCase();
|
|
605
|
+
searchMatches = new Set(
|
|
606
|
+
query ? agents.filter((agent) => matchesQuery(agent, query)).map((agent) => agent.id) : []
|
|
607
|
+
);
|
|
608
|
+
const visibleAgents = query
|
|
609
|
+
? agents.filter((agent) => searchMatches.has(agent.id))
|
|
610
|
+
: agents;
|
|
611
|
+
const listAgents = query
|
|
612
|
+
? visibleAgents
|
|
613
|
+
: visibleAgents.filter((agent) => agent.state !== "idle");
|
|
614
|
+
if (laneTitle) {
|
|
615
|
+
laneTitle.textContent = query ? "search results" : "active agents";
|
|
616
|
+
}
|
|
617
|
+
renderActiveList(listAgents);
|
|
618
|
+
if (selected) {
|
|
619
|
+
selected = agents.find((agent) => agent.id === selected.id) || selected;
|
|
620
|
+
renderPanel(selected);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (mockMode) {
|
|
625
|
+
setStatus("mock");
|
|
626
|
+
window.__consensusMock = {
|
|
627
|
+
setSnapshot: (snapshot) => applySnapshot(snapshot || {}),
|
|
628
|
+
setAgents: (nextAgents) =>
|
|
629
|
+
applySnapshot({ agents: nextAgents || [], ts: Date.now() }),
|
|
630
|
+
getAgents: () => agents,
|
|
631
|
+
};
|
|
632
|
+
} else {
|
|
633
|
+
connect();
|
|
634
|
+
}
|
|
635
|
+
renderPanel(null);
|
|
636
|
+
requestAnimationFrame(draw);
|