agent-memory-brain 0.2.0__py3-none-any.whl
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.
- agent_memory_brain-0.2.0.dist-info/METADATA +98 -0
- agent_memory_brain-0.2.0.dist-info/RECORD +54 -0
- agent_memory_brain-0.2.0.dist-info/WHEEL +4 -0
- agent_memory_brain-0.2.0.dist-info/entry_points.txt +3 -0
- agent_memory_brain-0.2.0.dist-info/licenses/LICENSE +67 -0
- devmem/__init__.py +3 -0
- devmem/ado.py +433 -0
- devmem/agent_colors.py +74 -0
- devmem/cli.py +500 -0
- devmem/db.py +1069 -0
- devmem/dispatch.py +478 -0
- devmem/embed.py +111 -0
- devmem/filelock.py +53 -0
- devmem/git_sync.py +261 -0
- devmem/guard.py +214 -0
- devmem/hook_log.py +49 -0
- devmem/ingest/__init__.py +1 -0
- devmem/ingest/claude.py +87 -0
- devmem/ingest/codex.py +175 -0
- devmem/ingest/runner.py +318 -0
- devmem/ingest/subagents.py +250 -0
- devmem/maintenance.py +801 -0
- devmem/mcp_server.py +542 -0
- devmem/messages.py +684 -0
- devmem/panel/__init__.py +1 -0
- devmem/panel/app.py +971 -0
- devmem/panel/state.py +358 -0
- devmem/panel/static/base.css +27 -0
- devmem/panel/static/brain.css +473 -0
- devmem/panel/static/brain.js +1118 -0
- devmem/panel/static/brain3d.js +600 -0
- devmem/panel/static/chat.js +18 -0
- devmem/panel/static/common.js +208 -0
- devmem/panel/static/site.css +508 -0
- devmem/panel/static/tokens.css +19 -0
- devmem/panel/templates/ado.html +62 -0
- devmem/panel/templates/agents.html +35 -0
- devmem/panel/templates/audit.html +19 -0
- devmem/panel/templates/base.html +42 -0
- devmem/panel/templates/brain.html +101 -0
- devmem/panel/templates/maintenance.html +32 -0
- devmem/panel/templates/memory.html +71 -0
- devmem/panel/templates/message_thread.html +49 -0
- devmem/panel/templates/messages.html +8 -0
- devmem/panel/templates/overview.html +109 -0
- devmem/panel/templates/project.html +48 -0
- devmem/panel/templates/projects.html +21 -0
- devmem/panel/templates/session.html +49 -0
- devmem/panel/templates/sessions.html +67 -0
- devmem/panel/templates/subagent.html +27 -0
- devmem/procs.py +10 -0
- devmem/projects.py +17 -0
- devmem/secrets.py +9 -0
- devmem/skills.py +105 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
// devmem brain wall: SSE client, panels and the canvas "brain" network.
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var U = window.devmem;
|
|
6
|
+
var REDUCED = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
7
|
+
var AGENT_COLORS = { claude: "#d6c1a4", "claude-subagent": "#998a75", codex: "#6db0f4" };
|
|
8
|
+
var KIND_COLORS = { note: "#88a8c9", decision: "#aa9dc5", preference: "#c7959c", snippet: "#90b697", todo: "#ccb484" };
|
|
9
|
+
var LINK_COLORS = { blocks: "#f47c70", depends_on: "#ccb484", relates_to: "#9c9890", overlaps: "#66635d", fixes: "#90b697", supersedes: "#4a463f" };
|
|
10
|
+
var KIND_ORDER = ["decision", "preference", "note", "snippet", "todo"];
|
|
11
|
+
var STALE_S = 6 * 3600;
|
|
12
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
13
|
+
|
|
14
|
+
function agentColor(agent) {
|
|
15
|
+
if (AGENT_COLORS[agent]) return AGENT_COLORS[agent];
|
|
16
|
+
agent = String(agent || "");
|
|
17
|
+
if (agent.indexOf("codex") >= 0) return AGENT_COLORS.codex;
|
|
18
|
+
if (agent.indexOf("claude") >= 0) return AGENT_COLORS.claude;
|
|
19
|
+
return "#9c9890";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function agentShort(agent) {
|
|
23
|
+
return agent === "claude-subagent" ? "subagent" : String(agent || "agent");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function esc(s) {
|
|
27
|
+
return String(s === null || s === undefined ? "" : s).replace(/[&<>"']/g, function (c) {
|
|
28
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ------------------------------------------------------------------ wall mode + clock
|
|
33
|
+
|
|
34
|
+
// The body state class is "wall-mode": plain "wall" collided with the .wall grid container.
|
|
35
|
+
var hintTimer = null;
|
|
36
|
+
|
|
37
|
+
function showWallHint() {
|
|
38
|
+
var hint = $("wall-hint");
|
|
39
|
+
hint.hidden = false;
|
|
40
|
+
hint.classList.add("show");
|
|
41
|
+
clearTimeout(hintTimer);
|
|
42
|
+
hintTimer = setTimeout(function () { hint.classList.remove("show"); }, 3000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isWall() { return document.body.classList.contains("wall-mode"); }
|
|
46
|
+
|
|
47
|
+
function setWall(on) {
|
|
48
|
+
document.body.classList.toggle("wall-mode", on);
|
|
49
|
+
try { localStorage.setItem("devmem.wall", on ? "1" : "0"); } catch (e) { /* storage unavailable */ }
|
|
50
|
+
if (on) showWallHint();
|
|
51
|
+
setTimeout(function () { if (window.devmemBrain) window.devmemBrain.resize(); }, 50);
|
|
52
|
+
}
|
|
53
|
+
(function initWall() {
|
|
54
|
+
var on = false;
|
|
55
|
+
try { on = localStorage.getItem("devmem.wall") === "1"; } catch (e) { /* ignore */ }
|
|
56
|
+
if (on) { document.body.classList.add("wall-mode"); showWallHint(); }
|
|
57
|
+
$("wall-toggle").addEventListener("click", function () { setWall(true); });
|
|
58
|
+
$("wall-exit").addEventListener("click", function () { setWall(false); });
|
|
59
|
+
document.addEventListener("keydown", function (e) {
|
|
60
|
+
if (e.ctrlKey || e.metaKey || e.altKey || /input|textarea|select/i.test(e.target.tagName)) return;
|
|
61
|
+
if (e.key === "w" || e.key === "W") setWall(!isWall());
|
|
62
|
+
else if (e.key === "Escape" && isWall()) setWall(false);
|
|
63
|
+
});
|
|
64
|
+
})();
|
|
65
|
+
|
|
66
|
+
// ------------------------------------------------------------------ gauges
|
|
67
|
+
|
|
68
|
+
var EXPECTED_LIMITS = [
|
|
69
|
+
{ agent: "claude", label: "five_hour", window_minutes: 300 },
|
|
70
|
+
{ agent: "claude", label: "seven_day", window_minutes: 10080 },
|
|
71
|
+
{ agent: "codex", label: "primary", window_minutes: 300 },
|
|
72
|
+
{ agent: "codex", label: "secondary", window_minutes: 10080 },
|
|
73
|
+
];
|
|
74
|
+
var gaugeEls = {};
|
|
75
|
+
var ARC_R = 80, ARC_C = 2 * Math.PI * ARC_R, ARC_LEN = ARC_C * 0.75;
|
|
76
|
+
|
|
77
|
+
function windowName(l) {
|
|
78
|
+
var named = { five_hour: "5h", seven_day: "weekly" };
|
|
79
|
+
if (named[l.label]) return named[l.label];
|
|
80
|
+
if (/^seven_day_/.test(l.label)) return "weekly " + l.label.replace(/^seven_day_/, "").replace(/_/g, " ");
|
|
81
|
+
var m = l.window_minutes;
|
|
82
|
+
var base = m === 300 ? "5h" : m === 10080 ? "weekly" : m ? (m % 1440 === 0 ? m / 1440 + "d" : Math.round(m / 60) + "h") : l.label;
|
|
83
|
+
var prefix = l.label.indexOf(":") > 0 ? l.label.split(":")[0] + " " : "";
|
|
84
|
+
return prefix + base;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function gaugeColor(left) {
|
|
88
|
+
if (left === null || left === undefined) return "#66635d";
|
|
89
|
+
if (left <= 10) return "#f47c70";
|
|
90
|
+
if (left <= 25) return "#f97c3d";
|
|
91
|
+
if (left <= 50) return "#ebb25f";
|
|
92
|
+
return "#cecac3";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function makeGauge(key) {
|
|
96
|
+
var el = document.createElement("div");
|
|
97
|
+
el.className = "gauge";
|
|
98
|
+
el.innerHTML =
|
|
99
|
+
'<svg viewBox="0 0 200 178" role="img">' +
|
|
100
|
+
'<circle class="track" cx="100" cy="100" r="' + ARC_R + '" stroke-dasharray="' + ARC_LEN + " " + ARC_C + '" transform="rotate(135 100 100)"/>' +
|
|
101
|
+
'<circle class="val" cx="100" cy="100" r="' + ARC_R + '" stroke-dasharray="0 ' + ARC_C + '" transform="rotate(135 100 100)"/>' +
|
|
102
|
+
'<text class="pct" x="100" y="108" text-anchor="middle"><tspan class="n">—</tspan><tspan class="pct-unit" dx="2">%</tspan></text>' +
|
|
103
|
+
'<text class="sub" x="100" y="134" text-anchor="middle">left</text>' +
|
|
104
|
+
"</svg>" +
|
|
105
|
+
'<div class="g-label"></div><div class="g-reset"></div><div class="g-asof"></div>';
|
|
106
|
+
$("gauges").appendChild(el);
|
|
107
|
+
gaugeEls[key] = el;
|
|
108
|
+
return el;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var limitsData = [];
|
|
112
|
+
|
|
113
|
+
function renderLimits(limits) {
|
|
114
|
+
var byKey = {};
|
|
115
|
+
limits.forEach(function (l) { byKey[l.agent + ":" + l.label] = l; });
|
|
116
|
+
var list = EXPECTED_LIMITS.map(function (e) { return byKey[e.agent + ":" + e.label] || Object.assign({ missing: true }, e); });
|
|
117
|
+
limits.forEach(function (l) {
|
|
118
|
+
if (!EXPECTED_LIMITS.some(function (e) { return e.agent === l.agent && e.label === l.label; })) list.push(l);
|
|
119
|
+
});
|
|
120
|
+
limitsData = list;
|
|
121
|
+
list.forEach(function (l) {
|
|
122
|
+
var key = l.agent + ":" + l.label;
|
|
123
|
+
var el = gaugeEls[key] || makeGauge(key);
|
|
124
|
+
var left = l.missing ? null : l.left_percent;
|
|
125
|
+
var stale = !l.missing && (l.stale || (l.age_s !== null && l.age_s > STALE_S));
|
|
126
|
+
el.classList.toggle("nodata", !!l.missing);
|
|
127
|
+
el.classList.toggle("stale", !!stale);
|
|
128
|
+
var color = l.missing || stale ? "#66635d" : gaugeColor(left);
|
|
129
|
+
el.style.setProperty("--g", color);
|
|
130
|
+
el.querySelector(".val").setAttribute("stroke-dasharray", (left === null ? 0 : ARC_LEN * Math.max(0, Math.min(100, left)) / 100) + " " + ARC_C);
|
|
131
|
+
el.querySelector(".n").textContent = left === null ? "—" : Math.round(left);
|
|
132
|
+
el.querySelector(".sub").textContent = l.missing ? "no data" : stale ? "stale" : "left";
|
|
133
|
+
el.querySelector(".g-label").textContent = (l.agent === "codex" ? "Codex" : l.agent === "claude" ? "Claude" : l.agent) + " · " + windowName(l);
|
|
134
|
+
el.dataset.resets = l.resets_at || "";
|
|
135
|
+
el.dataset.asof = l.as_of || "";
|
|
136
|
+
el.dataset.missing = l.missing ? "1" : "";
|
|
137
|
+
});
|
|
138
|
+
tickGauges();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function tickGauges() {
|
|
142
|
+
Object.keys(gaugeEls).forEach(function (key) {
|
|
143
|
+
var el = gaugeEls[key];
|
|
144
|
+
var reset = el.querySelector(".g-reset"), asof = el.querySelector(".g-asof");
|
|
145
|
+
if (el.dataset.missing) { reset.textContent = "no data yet"; asof.textContent = ""; return; }
|
|
146
|
+
var r = U.parseTs(el.dataset.resets);
|
|
147
|
+
if (r) {
|
|
148
|
+
var s = (r.getTime() - Date.now()) / 1000;
|
|
149
|
+
reset.innerHTML = s > 0 ? "resets in " + U.fmtDuration(s) : '<span class="flag">window reset · awaiting update</span>';
|
|
150
|
+
} else reset.textContent = "reset time unknown";
|
|
151
|
+
asof.textContent = el.dataset.asof ? "as of " + U.relTime(el.dataset.asof) : "";
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ------------------------------------------------------------------ memory kinds
|
|
156
|
+
|
|
157
|
+
function renderCounts(counts) {
|
|
158
|
+
var by = counts.memories_by_kind || {};
|
|
159
|
+
$("mem-total").textContent = counts.memories_total || 0;
|
|
160
|
+
var max = Math.max(1, Math.max.apply(null, KIND_ORDER.map(function (k) { return by[k] || 0; })));
|
|
161
|
+
$("kinds").innerHTML = KIND_ORDER.map(function (k) {
|
|
162
|
+
return '<div class="kind-row" style="--c:' + KIND_COLORS[k] + '"><span class="k">' + k + '</span>' +
|
|
163
|
+
'<span class="bar"><i style="width:' + ((by[k] || 0) / max * 100).toFixed(1) + '%"></i></span><b>' + (by[k] || 0) + "</b></div>";
|
|
164
|
+
}).join("");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ------------------------------------------------------------------ reminders
|
|
168
|
+
|
|
169
|
+
var remindersKey = null, reminderRows = [], finishedReminders = {};
|
|
170
|
+
|
|
171
|
+
function renderReminders(items) {
|
|
172
|
+
var visible = (items || []).filter(function (item) { return !finishedReminders[item.id]; });
|
|
173
|
+
var key = JSON.stringify(visible);
|
|
174
|
+
if (key === remindersKey) return;
|
|
175
|
+
remindersKey = key;
|
|
176
|
+
reminderRows = visible;
|
|
177
|
+
var list = $("reminders"), count = $("reminders-count");
|
|
178
|
+
count.textContent = visible.length || "";
|
|
179
|
+
list.replaceChildren();
|
|
180
|
+
if (!visible.length) {
|
|
181
|
+
var empty = document.createElement("li");
|
|
182
|
+
empty.className = "empty";
|
|
183
|
+
empty.textContent = "Nothing to come back to.";
|
|
184
|
+
list.appendChild(empty);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
visible.forEach(function (item) {
|
|
188
|
+
var row = document.createElement("li"), title = document.createElement("span"), button = document.createElement("button"), error = document.createElement("span");
|
|
189
|
+
title.className = "title";
|
|
190
|
+
title.textContent = item.title;
|
|
191
|
+
button.type = "button";
|
|
192
|
+
button.textContent = "Done";
|
|
193
|
+
button.setAttribute("aria-label", "Mark reminder done: " + item.title);
|
|
194
|
+
error.className = "error";
|
|
195
|
+
button.addEventListener("click", function () {
|
|
196
|
+
button.disabled = true;
|
|
197
|
+
error.textContent = "";
|
|
198
|
+
fetch("/memory/" + encodeURIComponent(item.id) + "/reminder-done", { method: "POST" }).then(function (response) {
|
|
199
|
+
if (!response.ok) throw new Error("reminder update failed");
|
|
200
|
+
finishedReminders[item.id] = true;
|
|
201
|
+
renderReminders(reminderRows.filter(function (reminder) { return reminder.id !== item.id; }));
|
|
202
|
+
}).catch(function () {
|
|
203
|
+
button.disabled = false;
|
|
204
|
+
error.textContent = "Could not mark done.";
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
row.append(title, button, error);
|
|
208
|
+
list.appendChild(row);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ------------------------------------------------------------------ Azure DevOps card (the server only returns cached data here)
|
|
213
|
+
|
|
214
|
+
function renderAdoCard(snapshot) {
|
|
215
|
+
var card = $("ado-card");
|
|
216
|
+
if (!snapshot || !snapshot.configured) { card.hidden = true; return; }
|
|
217
|
+
card.hidden = false;
|
|
218
|
+
var heading = $("ado-heading");
|
|
219
|
+
if (snapshot.stale) {
|
|
220
|
+
heading.innerHTML = '<a href="/ado">ADO</a> <span class="badge ado-card-stale">stale</span>';
|
|
221
|
+
card.title = (snapshot.error || "ADO data is stale") + "; last refresh: " + (snapshot.refreshed_at || "unknown");
|
|
222
|
+
} else {
|
|
223
|
+
heading.innerHTML = '<a href="/ado">ADO</a>';
|
|
224
|
+
card.title = "";
|
|
225
|
+
}
|
|
226
|
+
var count = function (name) { return (snapshot[name] || []).length; };
|
|
227
|
+
$("ado-counts").innerHTML =
|
|
228
|
+
'<span><b>' + count("to_review") + '</b> to review</span>' +
|
|
229
|
+
'<span><b>' + count("my_approved_not_completed") + '</b> approved</span>' +
|
|
230
|
+
'<span><b>' + count("my_changes_requested") + '</b> changes</span>' +
|
|
231
|
+
'<span><b>' + count("my_waiting_on_others") + '</b> waiting</span>' +
|
|
232
|
+
'<span><b>' + count("open_tasks") + '</b> tasks</span>';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function refreshAdoCard() {
|
|
236
|
+
fetch("/api/ado").then(function (response) {
|
|
237
|
+
return response.ok ? response.json() : null;
|
|
238
|
+
}).then(renderAdoCard).catch(function (e) { console.warn("devmem: /api/ado failed", e); });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ------------------------------------------------------------------ agents
|
|
242
|
+
|
|
243
|
+
function subagentRows(list) {
|
|
244
|
+
return '<ul class="subagents">' + list.map(function (x) {
|
|
245
|
+
var last = x.last_tool ? '<span class="tool">' + esc(x.last_tool) + "</span> " + esc(x.last_arg || "") : esc(x.description || "");
|
|
246
|
+
return '<li class="sub ' + esc(x.status) + '" title="' + esc(x.status) + '"><span class="dot"></span>' +
|
|
247
|
+
'<a class="s-type" href="/messages/agent/' + encodeURIComponent(x.agent_id) + '">' + esc(x.agent_type || "subagent") + "</a>" +
|
|
248
|
+
'<span class="s-last">' + last + "</span></li>";
|
|
249
|
+
}).join("") + "</ul>";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderAgents(a) {
|
|
253
|
+
var sessions = a.sessions || [], clients = a.connected || [], subs = a.subagents || {}, shownSubs = {};
|
|
254
|
+
var rows = sessions.slice(0, 8).map(function (s) {
|
|
255
|
+
// claude and claude-subagent rows share a session id: its subagents are listed once, under the first row.
|
|
256
|
+
var children = s.agent.indexOf("claude") === 0 && subs[s.session_id] && !shownSubs[s.session_id] ? subs[s.session_id] : null;
|
|
257
|
+
if (children) shownSubs[s.session_id] = 1;
|
|
258
|
+
var connected = clients.some(function (c) { return c.family === (s.agent.indexOf("codex") >= 0 ? "codex" : "claude") && c.project === s.project; });
|
|
259
|
+
var last = s.last_tool ? '<span class="tool">' + esc(s.last_tool) + "</span> " + esc(s.last_summary || "") :
|
|
260
|
+
(s.tokens_10m ? U.fmtTokens(s.tokens_10m) + " tokens in 10 min" : "");
|
|
261
|
+
return '<li class="agent' + (s.age_s !== null && s.age_s < 120 ? " active" : "") + '" style="--c:' + agentColor(s.agent) + '">' +
|
|
262
|
+
'<span class="pip"></span><span class="a-main"><b>' + esc(agentShort(s.agent)) + "</b> · " + esc(s.project_name) +
|
|
263
|
+
(connected ? ' <span class="hint">· mcp</span>' : "") + '</span><span class="a-age" data-ts="' + esc(s.last_ts) + '">' + U.relTime(s.last_ts) + "</span>" +
|
|
264
|
+
'<span class="a-last">' + last + "</span>" + (children ? subagentRows(children) : "") + "</li>";
|
|
265
|
+
});
|
|
266
|
+
clients.forEach(function (c) {
|
|
267
|
+
var shown = sessions.some(function (s) { return (s.agent.indexOf("codex") >= 0 ? "codex" : "claude") === c.family && s.project === c.project; });
|
|
268
|
+
if (shown) return;
|
|
269
|
+
rows.push('<li class="agent" style="--c:' + agentColor(c.family) + '"><span class="pip"></span>' +
|
|
270
|
+
'<span class="a-main"><b>' + esc(c.client_name || c.family) + "</b> · " + esc(c.project_name) + '</span><span class="a-age">connected</span>' +
|
|
271
|
+
'<span class="a-last">' + (c.last_tool ? 'memory: <span class="tool">' + esc(c.last_tool) + "</span>" : "idle") + "</span></li>");
|
|
272
|
+
});
|
|
273
|
+
$("agents").innerHTML = rows.length ? rows.join("") : '<li class="empty">No agent activity in the last 10 minutes.</li>';
|
|
274
|
+
$("agents-hint").textContent = clients.length + " connected · " + sessions.length + " active";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ------------------------------------------------------------------ feed
|
|
278
|
+
|
|
279
|
+
var feedSeen = null;
|
|
280
|
+
|
|
281
|
+
var feedKey = null;
|
|
282
|
+
|
|
283
|
+
function renderFeed(items) {
|
|
284
|
+
var key = items.map(function (e) { return e.id; }).join(",");
|
|
285
|
+
if (key === feedKey) return;
|
|
286
|
+
feedKey = key;
|
|
287
|
+
var first = feedSeen === null;
|
|
288
|
+
var seen = feedSeen || {};
|
|
289
|
+
var next = {};
|
|
290
|
+
var ul = $("feed");
|
|
291
|
+
ul.innerHTML = items.map(function (e) {
|
|
292
|
+
next[e.id] = 1;
|
|
293
|
+
var fresh = !first && !seen[e.id] && !REDUCED;
|
|
294
|
+
return '<li class="' + esc(e.kind) + (fresh ? " enter" : "") + '" style="--c:' + agentColor(e.agent) + '">' +
|
|
295
|
+
'<span class="f-badge">' + esc(agentShort(e.agent)) + "</span>" +
|
|
296
|
+
'<span class="f-main"><span class="f-proj">' + esc(e.project_name) + '</span><span class="f-tool">' + esc(e.name) + "</span></span>" +
|
|
297
|
+
'<span class="f-time" data-ts="' + esc(e.ts) + '">' + U.relTime(e.ts) + "</span>" +
|
|
298
|
+
(e.summary ? '<span class="f-sum">' + esc(e.summary) + "</span>" : "") + "</li>";
|
|
299
|
+
}).join("") || '<li class="empty">No tool activity yet. It appears here as agents work.</li>';
|
|
300
|
+
feedSeen = next;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ------------------------------------------------------------------ messages
|
|
304
|
+
|
|
305
|
+
var msgSeen = null;
|
|
306
|
+
|
|
307
|
+
function renderMessages(m) {
|
|
308
|
+
var items = m.items || [], first = msgSeen === null, seen = msgSeen || {}, next = {};
|
|
309
|
+
// Compact chat bubbles, newest first; each opens its chat on /messages.
|
|
310
|
+
$("msgs").innerHTML = items.map(function (x) {
|
|
311
|
+
next[x.id] = 1;
|
|
312
|
+
var fresh = !first && !seen[x.id] && !REDUCED;
|
|
313
|
+
var color = x.mine ? "var(--color-user)" : x.who === "Claude" ? "var(--color-claude)" : "oklch(78% 0.07 " + (x.hue || 0) + ")";
|
|
314
|
+
var href = x.chat ? "/messages?chat=" + encodeURIComponent(x.chat) : "/messages/thread/" + x.thread_id;
|
|
315
|
+
return '<li class="mb' + (x.mine ? " mine" : "") + (x.unread ? " unread" : "") + (fresh ? " enter" : "") + '" style="--c:' + color + '">' +
|
|
316
|
+
'<a href="' + href + '"><span class="m-line">' + esc((x.icon || "✉") + " " + (x.headline || x.text || x.line)) + "</span>" +
|
|
317
|
+
'<span class="m-time" data-ts="' + esc(x.created_at) + '">' + U.relTime(x.created_at) + "</span></a></li>";
|
|
318
|
+
}).join("") || '<li class="empty">No messages yet. Agents post with message_post; subagent tasks and reports show up here.</li>';
|
|
319
|
+
msgSeen = next;
|
|
320
|
+
$("msg-unread").textContent = m.unread ? m.unread + " unread" : "";
|
|
321
|
+
var badge = $("nav-msg-badge");
|
|
322
|
+
if (badge) { badge.textContent = m.badge || 0; badge.hidden = !m.badge; }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function tickRelTimes() {
|
|
326
|
+
document.querySelectorAll("[data-ts]").forEach(function (el) { el.textContent = U.relTime(el.dataset.ts); });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ------------------------------------------------------------------ brain renderer
|
|
330
|
+
|
|
331
|
+
var Brain2D = (function () {
|
|
332
|
+
var canvas = $("brain"), wrap = $("brain-wrap"), tip = $("brain-tip");
|
|
333
|
+
var ctx = canvas.getContext("2d");
|
|
334
|
+
var W = 0, H = 0, DPR = 1, G = null; // size and geometry
|
|
335
|
+
var outline = null; // pre-rendered silhouette
|
|
336
|
+
var nodes = [], nodeById = {}, memLinks = [], agentLinks = [], projectLinks = [];
|
|
337
|
+
var sim = null, hasD3 = !!(window.d3 && window.d3.forceSimulation);
|
|
338
|
+
var impulses = [], flashes = {}, pendingBloom = {};
|
|
339
|
+
var sprites = {};
|
|
340
|
+
var running = false, lastFrame = 0, firstGraph = true, dirty = true, hover = null, active = true;
|
|
341
|
+
var start = performance.now();
|
|
342
|
+
var FONT = getComputedStyle(document.body).fontFamily;
|
|
343
|
+
|
|
344
|
+
function sprite(color) {
|
|
345
|
+
if (sprites[color]) return sprites[color];
|
|
346
|
+
var s = document.createElement("canvas"), n = 64;
|
|
347
|
+
s.width = s.height = n;
|
|
348
|
+
var c = s.getContext("2d"), g = c.createRadialGradient(n / 2, n / 2, 0, n / 2, n / 2, n / 2);
|
|
349
|
+
g.addColorStop(0, color);
|
|
350
|
+
g.addColorStop(0.25, color + "aa");
|
|
351
|
+
g.addColorStop(1, color + "00");
|
|
352
|
+
c.fillStyle = g;
|
|
353
|
+
c.fillRect(0, 0, n, n);
|
|
354
|
+
sprites[color] = s;
|
|
355
|
+
return s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function geometry() {
|
|
359
|
+
var ry = Math.min(H * 0.37, W / 4.4), rx = ry * 0.93;
|
|
360
|
+
var cx = W / 2, cy = H * 0.5;
|
|
361
|
+
return { cx: cx, cy: cy, rx: rx, ry: ry, s: ry / 320,
|
|
362
|
+
hemis: [{ x: cx - rx * 0.95, y: cy }, { x: cx + rx * 0.95, y: cy }] };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function wobble(theta, seed) {
|
|
366
|
+
return 1 + 0.022 * Math.sin(7 * theta + seed) + 0.014 * Math.sin(13 * theta + seed * 2.3) + 0.008 * Math.sin(23 * theta + seed);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function renderOutline() {
|
|
370
|
+
outline = document.createElement("canvas");
|
|
371
|
+
outline.width = Math.max(1, Math.round(W * DPR));
|
|
372
|
+
outline.height = Math.max(1, Math.round(H * DPR));
|
|
373
|
+
var c = outline.getContext("2d");
|
|
374
|
+
c.scale(DPR, DPR);
|
|
375
|
+
var paths = G.hemis.map(function (h, i) {
|
|
376
|
+
var p = new Path2D();
|
|
377
|
+
for (var k = 0; k <= 160; k++) {
|
|
378
|
+
var t = (k / 160) * Math.PI * 2, w = wobble(t, i * 1.7 + 0.4);
|
|
379
|
+
// flatten the inner side so the two hemispheres meet along a fissure
|
|
380
|
+
var inner = (i === 0 ? Math.cos(t) > 0 : Math.cos(t) < 0) ? 0.97 : 1;
|
|
381
|
+
var x = h.x + Math.cos(t) * G.rx * w * inner, y = h.y + Math.sin(t) * G.ry * w;
|
|
382
|
+
if (k === 0) p.moveTo(x, y); else p.lineTo(x, y);
|
|
383
|
+
}
|
|
384
|
+
p.closePath();
|
|
385
|
+
return p;
|
|
386
|
+
});
|
|
387
|
+
// inner fill
|
|
388
|
+
paths.forEach(function (p, i) {
|
|
389
|
+
var h = G.hemis[i];
|
|
390
|
+
var g = c.createRadialGradient(h.x, h.y - G.ry * 0.2, G.ry * 0.1, h.x, h.y, G.ry * 1.1);
|
|
391
|
+
g.addColorStop(0, "rgba(239,235,228,0.025)");
|
|
392
|
+
g.addColorStop(1, "rgba(239,235,228,0.005)");
|
|
393
|
+
c.fillStyle = g;
|
|
394
|
+
c.fill(p);
|
|
395
|
+
});
|
|
396
|
+
// soft glow outline (shadowBlur only here, rendered once per resize)
|
|
397
|
+
c.lineWidth = 1.6;
|
|
398
|
+
c.strokeStyle = "rgba(239,235,228,0.22)";
|
|
399
|
+
c.shadowColor = "rgba(0,0,0,0)";
|
|
400
|
+
c.shadowBlur = 22 * G.s + 6;
|
|
401
|
+
paths.forEach(function (p) { c.stroke(p); });
|
|
402
|
+
c.shadowBlur = 0;
|
|
403
|
+
c.strokeStyle = "rgba(239,235,228,0.10)";
|
|
404
|
+
c.lineWidth = 1;
|
|
405
|
+
paths.forEach(function (p) { c.stroke(p); });
|
|
406
|
+
// gyri: faint curved strokes inside each hemisphere
|
|
407
|
+
var rnd = mulberry(7);
|
|
408
|
+
c.strokeStyle = "rgba(239,235,228,0.04)";
|
|
409
|
+
c.lineWidth = 1.2;
|
|
410
|
+
G.hemis.forEach(function (h) {
|
|
411
|
+
for (var k = 0; k < 16; k++) {
|
|
412
|
+
var a = rnd() * Math.PI * 2, d = 0.25 + rnd() * 0.6;
|
|
413
|
+
var x = h.x + Math.cos(a) * G.rx * d, y = h.y + Math.sin(a) * G.ry * d;
|
|
414
|
+
var len = (0.18 + rnd() * 0.25) * G.rx, dir = a + Math.PI / 2 + (rnd() - 0.5);
|
|
415
|
+
c.beginPath();
|
|
416
|
+
c.moveTo(x - Math.cos(dir) * len / 2, y - Math.sin(dir) * len / 2);
|
|
417
|
+
c.quadraticCurveTo(x + (rnd() - 0.5) * len * 0.6, y + (rnd() - 0.5) * len * 0.6, x + Math.cos(dir) * len / 2, y + Math.sin(dir) * len / 2);
|
|
418
|
+
c.stroke();
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
// longitudinal fissure
|
|
422
|
+
c.strokeStyle = "rgba(239,235,228,0.07)";
|
|
423
|
+
c.beginPath();
|
|
424
|
+
c.moveTo(G.cx, G.cy - G.ry * 0.92);
|
|
425
|
+
c.bezierCurveTo(G.cx - 6, G.cy - G.ry * 0.3, G.cx + 6, G.cy + G.ry * 0.3, G.cx, G.cy + G.ry * 0.92);
|
|
426
|
+
c.stroke();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function mulberry(a) {
|
|
430
|
+
return function () {
|
|
431
|
+
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
|
432
|
+
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
433
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
434
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function hash(str) {
|
|
439
|
+
var h = 2166136261;
|
|
440
|
+
for (var i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
441
|
+
return (h >>> 0) / 4294967296;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---- anchors: where each cluster lives inside the silhouette
|
|
445
|
+
function assignAnchors() {
|
|
446
|
+
var projects = nodes.filter(function (n) { return n.type === "project"; });
|
|
447
|
+
var real = projects.filter(function (p) { return p.id !== "other"; });
|
|
448
|
+
var halves = [[], []];
|
|
449
|
+
real.forEach(function (p, i) { halves[i % 2].push(p); });
|
|
450
|
+
halves.forEach(function (list, side) {
|
|
451
|
+
var h = G.hemis[side], n = list.length;
|
|
452
|
+
list.forEach(function (p, i) {
|
|
453
|
+
// spread over the outer side of each hemisphere; the inner (fissure) side holds global memories
|
|
454
|
+
var frac = n === 1 ? 0.5 : i / (n - 1);
|
|
455
|
+
var a = side === 0 ? Math.PI * (0.42 + 1.16 * frac) : Math.PI * (-0.58 + 1.16 * frac);
|
|
456
|
+
var d = n === 1 ? 0.3 : n === 2 ? 0.45 : (i % 2 ? 0.34 : 0.62);
|
|
457
|
+
p.ax = h.x + Math.cos(a) * G.rx * d;
|
|
458
|
+
p.ay = h.y + Math.sin(a) * G.ry * d;
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
projects.forEach(function (p) {
|
|
462
|
+
if (p.id === "other") { p.ax = G.cx; p.ay = G.cy + G.ry * 0.62; }
|
|
463
|
+
});
|
|
464
|
+
var hub = nodeById.__brain__;
|
|
465
|
+
if (hub) { hub.ax = G.cx; hub.ay = G.cy; hub.fx = G.cx; hub.fy = G.cy; }
|
|
466
|
+
nodes.forEach(function (n) {
|
|
467
|
+
if (n.type === "memory") {
|
|
468
|
+
var parent = nodeById[n.cluster === "global" ? "__brain__" : "p:" + n.cluster];
|
|
469
|
+
n.ax = parent ? parent.ax : G.cx;
|
|
470
|
+
n.ay = parent ? parent.ay : G.cy;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
// agents on the rim: spread along an ellipse just outside the brain
|
|
474
|
+
var agents = nodes.filter(function (n) { return n.type === "agent"; });
|
|
475
|
+
var ex = G.rx * 1.95 + 30 * G.s, ey = G.ry * 1.17;
|
|
476
|
+
var top = agents.filter(function (a, i) { return i % 2 === 0; }), bottom = agents.filter(function (a, i) { return i % 2 === 1; });
|
|
477
|
+
[[top, -Math.PI / 2], [bottom, Math.PI / 2]].forEach(function (group) {
|
|
478
|
+
var list = group[0], centre = group[1], step = Math.min(0.55, 2.2 / Math.max(list.length, 1));
|
|
479
|
+
list.forEach(function (a, k) {
|
|
480
|
+
var ang = centre + (k - (list.length - 1) / 2) * step;
|
|
481
|
+
var x = G.cx + Math.cos(ang) * ex, y = G.cy + Math.sin(ang) * ey;
|
|
482
|
+
var p = a.project && nodeById["p:" + a.project];
|
|
483
|
+
if (p && list.length === 1) x += (p.ax - G.cx) * 0.35; // lean toward its lobe
|
|
484
|
+
a.fx = a.x = Math.max(40, Math.min(W - 40, x));
|
|
485
|
+
a.fy = a.y = Math.max(28, Math.min(H - 28, y));
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function contain(alpha) {
|
|
491
|
+
var k = 0.9;
|
|
492
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
493
|
+
var n = nodes[i];
|
|
494
|
+
if (n.type === "agent" || n.fx !== undefined && n.fx !== null) continue;
|
|
495
|
+
var best = 1e9, hb = null;
|
|
496
|
+
for (var j = 0; j < 2; j++) {
|
|
497
|
+
var h = G.hemis[j], dx = (n.x - h.x) / G.rx, dy = (n.y - h.y) / G.ry, d = Math.sqrt(dx * dx + dy * dy);
|
|
498
|
+
if (d < best) { best = d; hb = h; }
|
|
499
|
+
}
|
|
500
|
+
if (best > k) {
|
|
501
|
+
n.x = hb.x + (n.x - hb.x) * k / best;
|
|
502
|
+
n.y = hb.y + (n.y - hb.y) * k / best;
|
|
503
|
+
n.vx *= 0.5; n.vy *= 0.5;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function phyllotaxis(n, i, cx, cy, spacing) {
|
|
509
|
+
var r = spacing * Math.sqrt(i + 0.5), a = i * 2.39996;
|
|
510
|
+
return [cx + r * Math.cos(a), cy + r * Math.sin(a)];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function initialPositions() {
|
|
514
|
+
var counters = {};
|
|
515
|
+
nodes.forEach(function (n) {
|
|
516
|
+
if (n.type === "memory" && n.x === undefined) {
|
|
517
|
+
var c = counters[n.cluster] = (counters[n.cluster] || 0) + 1;
|
|
518
|
+
var p = phyllotaxis(n, c, n.ax, n.ay, 7 * G.s + 2);
|
|
519
|
+
n.x = p[0]; n.y = p[1];
|
|
520
|
+
} else if (n.type === "project" && n.x === undefined) {
|
|
521
|
+
n.x = n.ax; n.y = n.ay;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function buildSim(alpha) {
|
|
527
|
+
initialPositions(); // before sim.nodes(): d3 would otherwise seed new nodes near (0, 0)
|
|
528
|
+
if (!hasD3) { contain(); dirty = true; return; }
|
|
529
|
+
var d3 = window.d3;
|
|
530
|
+
if (!sim) {
|
|
531
|
+
sim = d3.forceSimulation().stop().alphaDecay(0.035).velocityDecay(0.38)
|
|
532
|
+
.force("charge", d3.forceManyBody().strength(function (n) { return n.type === "project" ? -70 * G.s : -5 * G.s; }).distanceMax(90))
|
|
533
|
+
.force("collide", d3.forceCollide().radius(function (n) { return n.r + 1.6; }).iterations(1))
|
|
534
|
+
.force("x", d3.forceX(function (n) { return n.ax; }).strength(function (n) { return n.type === "project" ? 0.35 : 0.06; }))
|
|
535
|
+
.force("y", d3.forceY(function (n) { return n.ay; }).strength(function (n) { return n.type === "project" ? 0.35 : 0.06; }))
|
|
536
|
+
.force("contain", contain);
|
|
537
|
+
}
|
|
538
|
+
sim.nodes(nodes.filter(function (n) { return n.type !== "agent"; }));
|
|
539
|
+
sim.force("link", d3.forceLink(memLinks).id(function (n) { return n.id; })
|
|
540
|
+
.distance(function (l) { return l.kind === "ml" ? 30 * G.s + 8 : (l.target.cluster === "global" ? 14 : 18) * G.s + 6; })
|
|
541
|
+
.strength(function (l) { return l.kind === "ml" ? 0.025 : 0.12; }));
|
|
542
|
+
sim.force("charge").strength(function (n) { return n.type === "project" ? -70 * G.s : -5 * G.s; });
|
|
543
|
+
sim.force("x").x(function (n) { return n.ax; });
|
|
544
|
+
sim.force("y").y(function (n) { return n.ay; });
|
|
545
|
+
sim.alpha(Math.max(sim.alpha(), alpha));
|
|
546
|
+
dirty = true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
var pendingGraph = null;
|
|
550
|
+
|
|
551
|
+
function setGraph(g) {
|
|
552
|
+
if (!G) { pendingGraph = g; return; }
|
|
553
|
+
var old = nodeById;
|
|
554
|
+
var next = [], byId = {};
|
|
555
|
+
var now = performance.now();
|
|
556
|
+
function keep(id, props) {
|
|
557
|
+
var n = old[id] || { id: id, phase: hash(id) * Math.PI * 2, born: firstGraph ? now + Math.random() * 1400 : now };
|
|
558
|
+
Object.assign(n, props);
|
|
559
|
+
next.push(n); byId[id] = n;
|
|
560
|
+
return n;
|
|
561
|
+
}
|
|
562
|
+
keep("__brain__", { type: "hub", r: 16 * G.s + 5, name: "Brain", color: "#efebe4" });
|
|
563
|
+
(g.projects || []).forEach(function (p) {
|
|
564
|
+
keep("p:" + p.id, { type: "project", project: p.id, name: p.name, path: p.path, memories: p.memories, activity: p.activity,
|
|
565
|
+
r: Math.min(16, 6 + Math.sqrt(p.memories || 0) * 1.3) * G.s + 3 });
|
|
566
|
+
});
|
|
567
|
+
(g.memories || []).forEach(function (m) {
|
|
568
|
+
var isNew = !old["m:" + m.id] && !firstGraph;
|
|
569
|
+
var n = keep("m:" + m.id, { type: "memory", mid: m.id, title: m.title, kind: m.kind, cluster: m.cluster, project: m.project,
|
|
570
|
+
r: 2.6 * G.s + 1, color: KIND_COLORS[m.kind] || "#9c9890" });
|
|
571
|
+
if (isNew || pendingBloom[m.id]) { n.bloom = now; delete pendingBloom[m.id]; }
|
|
572
|
+
});
|
|
573
|
+
(g.agents || []).forEach(function (a) {
|
|
574
|
+
keep("a:" + a.id, { type: "agent", agent: a.agent, session: a.session_id, project: a.project, name: a.project_name,
|
|
575
|
+
active: a.active, connected: a.connected, r: 6 * G.s + 2.5, color: agentColor(a.agent) });
|
|
576
|
+
});
|
|
577
|
+
nodes = next; nodeById = byId;
|
|
578
|
+
memLinks = []; agentLinks = []; projectLinks = [];
|
|
579
|
+
nodes.forEach(function (n) {
|
|
580
|
+
if (n.type === "memory") {
|
|
581
|
+
var parent = n.cluster === "global" ? "__brain__" : "p:" + n.cluster;
|
|
582
|
+
if (byId[parent]) memLinks.push({ source: parent, target: n.id, kind: "pm" });
|
|
583
|
+
} else if (n.type === "agent") {
|
|
584
|
+
agentLinks.push({ source: n, target: byId.__brain__ });
|
|
585
|
+
} else if (n.type === "project") {
|
|
586
|
+
projectLinks.push({ source: byId.__brain__, target: n });
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
(g.links || []).forEach(function (l) {
|
|
590
|
+
if (byId["m:" + l.from_id] && byId["m:" + l.to_id]) memLinks.push({ source: "m:" + l.from_id, target: "m:" + l.to_id, kind: "ml", type: l.type });
|
|
591
|
+
});
|
|
592
|
+
assignAnchors();
|
|
593
|
+
buildSim(firstGraph ? 1 : 0.25);
|
|
594
|
+
firstGraph = false;
|
|
595
|
+
kick();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ---- impulses
|
|
599
|
+
function curvePoints(a, b) {
|
|
600
|
+
var mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
|
|
601
|
+
var cx = mx + (G.cx - mx) * 0.25, cy = my + (G.cy - my) * 0.25;
|
|
602
|
+
var pts = [];
|
|
603
|
+
for (var i = 0; i <= 12; i++) {
|
|
604
|
+
var t = i / 12, u = 1 - t;
|
|
605
|
+
pts.push([u * u * a.x + 2 * u * t * cx + t * t * b.x, u * u * a.y + 2 * u * t * cy + t * t * b.y]);
|
|
606
|
+
}
|
|
607
|
+
return pts;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
var lastImpulse = {};
|
|
611
|
+
|
|
612
|
+
function findAgent(p) {
|
|
613
|
+
var exact = nodeById["a:" + p.agent + ":" + p.session_id];
|
|
614
|
+
if (exact) return exact;
|
|
615
|
+
var fam = String(p.agent || "").indexOf("codex") >= 0 ? "codex" : "claude";
|
|
616
|
+
var best = null;
|
|
617
|
+
nodes.forEach(function (n) {
|
|
618
|
+
if (n.type !== "agent") return;
|
|
619
|
+
var nf = n.agent.indexOf("codex") >= 0 ? "codex" : "claude";
|
|
620
|
+
if (nf === fam && (!best || n.project === p.project)) best = n;
|
|
621
|
+
});
|
|
622
|
+
return best;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function pulse(p) {
|
|
626
|
+
if (!G) return;
|
|
627
|
+
var hub = nodeById.__brain__, project = p.project ? nodeById["p:" + p.project] : null;
|
|
628
|
+
var agent = findAgent(p), now = performance.now();
|
|
629
|
+
if (p.type === "ado") {
|
|
630
|
+
if (hub) flashes[hub.id] = now;
|
|
631
|
+
dirty = true;
|
|
632
|
+
kick();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (p.type === "memory") {
|
|
636
|
+
if (agent) agent.lit = now;
|
|
637
|
+
if (!REDUCED && agent && hub) impulses.push({ path: curvePoints(agent, hub), start: now, dur: 900, color: agent.color, small: false, project: null, targets: [] });
|
|
638
|
+
setTimeout(function () {
|
|
639
|
+
if (hub) flashes[hub.id] = performance.now();
|
|
640
|
+
pendingBloom[p.id] = 1;
|
|
641
|
+
var existing = nodeById["m:" + p.id];
|
|
642
|
+
if (existing) existing.bloom = performance.now();
|
|
643
|
+
if (project) flashes[project.id] = performance.now();
|
|
644
|
+
kick();
|
|
645
|
+
}, agent && hub && !REDUCED ? 900 : 0);
|
|
646
|
+
kick();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
var key = (agent ? agent.id : "x") + p.type;
|
|
650
|
+
if (lastImpulse[key] && now - lastImpulse[key] < 350) return;
|
|
651
|
+
lastImpulse[key] = now;
|
|
652
|
+
if (agent) agent.lit = now;
|
|
653
|
+
if (!project) { kick(); return; }
|
|
654
|
+
if (REDUCED) { if (hub) flashes[hub.id] = now; flashes[project.id] = now; dirty = true; return; }
|
|
655
|
+
var color = agent ? agent.color : agentColor(p.agent);
|
|
656
|
+
// continue into one or two neurons of the lobe
|
|
657
|
+
var mems = nodes.filter(function (n) { return n.type === "memory" && n.cluster === p.project; });
|
|
658
|
+
var targets = [];
|
|
659
|
+
for (var i = 0; i < Math.min(2, mems.length); i++) targets.push(mems[Math.floor(Math.random() * mems.length)]);
|
|
660
|
+
var dur = p.type === "usage" ? 900 : 1100, firstEnd = now;
|
|
661
|
+
if (agent && hub) {
|
|
662
|
+
impulses.push({ path: curvePoints(agent, hub), start: now, dur: dur, color: color, small: p.type === "usage", project: hub, targets: [] });
|
|
663
|
+
firstEnd += dur;
|
|
664
|
+
}
|
|
665
|
+
if (hub) {
|
|
666
|
+
impulses.push({ path: curvePoints(hub, project), start: firstEnd, dur: dur, color: color, small: p.type === "usage", project: project, targets: [] });
|
|
667
|
+
firstEnd += dur;
|
|
668
|
+
}
|
|
669
|
+
targets.forEach(function (target) {
|
|
670
|
+
impulses.push({ path: curvePoints(project, target), start: firstEnd, dur: dur, color: color, small: p.type === "usage", project: project, targets: [target] });
|
|
671
|
+
});
|
|
672
|
+
if (impulses.length > 60) impulses.splice(0, impulses.length - 60);
|
|
673
|
+
kick();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function pointAlong(path, t) {
|
|
677
|
+
var f = t * (path.length - 1), i = Math.min(path.length - 2, Math.floor(f)), r = f - i;
|
|
678
|
+
return [path[i][0] + (path[i + 1][0] - path[i][0]) * r, path[i][1] + (path[i + 1][1] - path[i][1]) * r];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ---- frame
|
|
682
|
+
function draw(now) {
|
|
683
|
+
var t = (now - start) / 1000;
|
|
684
|
+
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
685
|
+
ctx.clearRect(0, 0, W, H);
|
|
686
|
+
if (outline) ctx.drawImage(outline, 0, 0, W, H);
|
|
687
|
+
|
|
688
|
+
// synapses: project -> memories
|
|
689
|
+
ctx.lineWidth = 0.7;
|
|
690
|
+
ctx.strokeStyle = "rgba(239,235,228,0.06)";
|
|
691
|
+
ctx.beginPath();
|
|
692
|
+
for (var i = 0; i < memLinks.length; i++) {
|
|
693
|
+
var l = memLinks[i], s = typeof l.source === "object" ? l.source : nodeById[l.source], d = typeof l.target === "object" ? l.target : nodeById[l.target];
|
|
694
|
+
if (!s || !d || s.x === undefined) continue;
|
|
695
|
+
ctx.moveTo(s.x, s.y); ctx.lineTo(d.x, d.y);
|
|
696
|
+
}
|
|
697
|
+
ctx.stroke();
|
|
698
|
+
|
|
699
|
+
memLinks.forEach(function (l) {
|
|
700
|
+
if (l.kind !== "ml") return;
|
|
701
|
+
var s = typeof l.source === "object" ? l.source : nodeById[l.source], d = typeof l.target === "object" ? l.target : nodeById[l.target];
|
|
702
|
+
if (!s || !d || s.x === undefined || d.x === undefined) return;
|
|
703
|
+
var color = LINK_COLORS[l.type] || "#9c9890";
|
|
704
|
+
ctx.strokeStyle = color + "66"; ctx.lineWidth = 0.65;
|
|
705
|
+
ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(d.x, d.y); ctx.stroke();
|
|
706
|
+
if (!REDUCED && /^(blocks|depends_on|supersedes)$/.test(l.type)) {
|
|
707
|
+
var q = (t * 0.18 + hash(l.source + ">" + l.target + l.type)) % 1;
|
|
708
|
+
ctx.fillStyle = color; ctx.beginPath(); ctx.arc(s.x + (d.x - s.x) * q, s.y + (d.y - s.y) * q, 1.2, 0, Math.PI * 2); ctx.fill();
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// synapses: agent -> brain, then brain -> project
|
|
713
|
+
agentLinks.forEach(function (l) {
|
|
714
|
+
var pts = curvePoints(l.source, l.target);
|
|
715
|
+
ctx.strokeStyle = l.source.color + (l.source.active ? "55" : "26");
|
|
716
|
+
ctx.lineWidth = l.source.active ? 1.3 : 0.9;
|
|
717
|
+
ctx.beginPath();
|
|
718
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
719
|
+
for (var k = 1; k < pts.length; k++) ctx.lineTo(pts[k][0], pts[k][1]);
|
|
720
|
+
ctx.stroke();
|
|
721
|
+
});
|
|
722
|
+
projectLinks.forEach(function (l) {
|
|
723
|
+
var pts = curvePoints(l.source, l.target);
|
|
724
|
+
ctx.strokeStyle = "rgba(239,235,228,0.18)";
|
|
725
|
+
ctx.lineWidth = 1;
|
|
726
|
+
ctx.beginPath();
|
|
727
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
728
|
+
for (var k = 1; k < pts.length; k++) ctx.lineTo(pts[k][0], pts[k][1]);
|
|
729
|
+
ctx.stroke();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// memories (neurons)
|
|
733
|
+
var anyAnim = false;
|
|
734
|
+
for (var m = 0; m < nodes.length; m++) {
|
|
735
|
+
var n = nodes[m];
|
|
736
|
+
if (n.type !== "memory" || n.x === undefined) continue;
|
|
737
|
+
var age = now - n.born;
|
|
738
|
+
if (age < 0) { anyAnim = true; continue; }
|
|
739
|
+
var appear = Math.min(1, age / 600);
|
|
740
|
+
if (appear < 1) anyAnim = true;
|
|
741
|
+
var tw = REDUCED ? 0.8 : 0.62 + 0.3 * Math.sin(t * (0.6 + (n.phase % 1) * 0.9) + n.phase);
|
|
742
|
+
var lit = n.lit && now - n.lit < 900 ? 1 - (now - n.lit) / 900 : 0;
|
|
743
|
+
if (lit) anyAnim = true;
|
|
744
|
+
var r = n.r * appear * (1 + lit * 0.8);
|
|
745
|
+
ctx.globalAlpha = Math.min(1, (tw + lit) * appear);
|
|
746
|
+
var sz = r * 5;
|
|
747
|
+
ctx.drawImage(sprite(n.color), n.x - sz / 2, n.y - sz / 2, sz, sz);
|
|
748
|
+
ctx.fillStyle = n.color;
|
|
749
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, Math.max(0.6, r * 0.55), 0, Math.PI * 2); ctx.fill();
|
|
750
|
+
if (n.bloom) {
|
|
751
|
+
var b = (now - n.bloom) / 1800;
|
|
752
|
+
if (b < 1) {
|
|
753
|
+
anyAnim = true;
|
|
754
|
+
ctx.globalAlpha = (1 - b) * 0.9;
|
|
755
|
+
ctx.strokeStyle = n.color;
|
|
756
|
+
ctx.lineWidth = 2;
|
|
757
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, n.r + b * 34 * G.s + 4, 0, Math.PI * 2); ctx.stroke();
|
|
758
|
+
var bs = (1 - b) * 40 * G.s + 8;
|
|
759
|
+
ctx.drawImage(sprite(n.color), n.x - bs / 2, n.y - bs / 2, bs, bs);
|
|
760
|
+
} else n.bloom = 0;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
ctx.globalAlpha = 1;
|
|
764
|
+
|
|
765
|
+
// Brain hub
|
|
766
|
+
var hub = nodeById.__brain__;
|
|
767
|
+
if (hub && hub.x !== undefined) {
|
|
768
|
+
var hf = flashes[hub.id] ? (now - flashes[hub.id]) / 1400 : 1;
|
|
769
|
+
if (hf < 1) anyAnim = true; else delete flashes[hub.id];
|
|
770
|
+
var hl = hf < 1 ? 1 - hf : 0;
|
|
771
|
+
ctx.globalAlpha = 0.9 + hl * 0.1;
|
|
772
|
+
var hs = hub.r * (6.8 + hl * 4);
|
|
773
|
+
ctx.drawImage(sprite("#3a3631"), hub.x - hs / 2, hub.y - hs / 2, hs, hs);
|
|
774
|
+
ctx.globalAlpha = 1;
|
|
775
|
+
ctx.fillStyle = "#efebe4";
|
|
776
|
+
ctx.beginPath(); ctx.arc(hub.x, hub.y, hub.r * 0.48, 0, Math.PI * 2); ctx.fill();
|
|
777
|
+
ctx.font = "500 " + Math.max(11, fs - 1) + "px " + FONT;
|
|
778
|
+
ctx.textAlign = "center";
|
|
779
|
+
var hubLabel = "Brain", hubW = ctx.measureText(hubLabel).width;
|
|
780
|
+
ctx.fillStyle = "rgba(3,2,1,0.78)";
|
|
781
|
+
ctx.fillRect(hub.x - hubW / 2 - 5, hub.y + hub.r + 5, hubW + 10, fs + 4);
|
|
782
|
+
ctx.fillStyle = "#efebe4";
|
|
783
|
+
ctx.fillText(hubLabel, hub.x, hub.y + hub.r + 7);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// impulses
|
|
787
|
+
for (var q = impulses.length - 1; q >= 0; q--) {
|
|
788
|
+
var im = impulses[q], p = (now - im.start) / im.dur;
|
|
789
|
+
if (p < 0) { anyAnim = true; continue; }
|
|
790
|
+
if (p >= 1) {
|
|
791
|
+
if (im.project) flashes[im.project.id] = now;
|
|
792
|
+
im.targets.forEach(function (tn) { tn.lit = now; });
|
|
793
|
+
impulses.splice(q, 1);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
anyAnim = true;
|
|
797
|
+
var e = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
|
798
|
+
var sp = sprite(im.color), size = (im.small ? 16 : 26) * G.s + 8;
|
|
799
|
+
for (var tr = 0; tr < 5; tr++) {
|
|
800
|
+
var pp = pointAlong(im.path, Math.max(0, e - tr * 0.035));
|
|
801
|
+
ctx.globalAlpha = (1 - tr / 5) * 0.9;
|
|
802
|
+
var ts = size * (1 - tr * 0.13);
|
|
803
|
+
ctx.drawImage(sp, pp[0] - ts / 2, pp[1] - ts / 2, ts, ts);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
ctx.globalAlpha = 1;
|
|
807
|
+
|
|
808
|
+
// projects (lobes)
|
|
809
|
+
var fs = Math.max(11, Math.min(17, 15 * G.s + 4));
|
|
810
|
+
ctx.textAlign = "center";
|
|
811
|
+
ctx.textBaseline = "top";
|
|
812
|
+
ctx.font = "500 " + fs + "px " + FONT;
|
|
813
|
+
nodes.forEach(function (n) {
|
|
814
|
+
if (n.type !== "project" || n.x === undefined) return;
|
|
815
|
+
var f = flashes[n.id] ? (now - flashes[n.id]) / 1400 : 1;
|
|
816
|
+
if (f < 1) anyAnim = true; else delete flashes[n.id];
|
|
817
|
+
var fl = f < 1 ? 1 - f : 0;
|
|
818
|
+
var breathe = REDUCED ? 0 : 0.08 * Math.sin(t * 0.8 + n.phase);
|
|
819
|
+
var gs = n.r * (4.2 + breathe + fl * 4);
|
|
820
|
+
ctx.globalAlpha = 0.55 + fl * 0.45;
|
|
821
|
+
ctx.drawImage(sprite(n.id === "p:other" ? "#24211c" : "#3a3631"), n.x - gs / 2, n.y - gs / 2, gs, gs);
|
|
822
|
+
if (fl) {
|
|
823
|
+
ctx.globalAlpha = fl;
|
|
824
|
+
var fs2 = gs * 1.3;
|
|
825
|
+
ctx.drawImage(sprite("#5c3a22"), n.x - fs2 / 2, n.y - fs2 / 2, fs2, fs2);
|
|
826
|
+
ctx.strokeStyle = "rgba(249,124,61," + fl + ")";
|
|
827
|
+
ctx.lineWidth = 1.5;
|
|
828
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, n.r + (1 - fl) * 40 * G.s, 0, Math.PI * 2); ctx.stroke();
|
|
829
|
+
}
|
|
830
|
+
ctx.globalAlpha = 1;
|
|
831
|
+
ctx.fillStyle = "#cecac3";
|
|
832
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, n.r * 0.5, 0, Math.PI * 2); ctx.fill();
|
|
833
|
+
ctx.fillStyle = "rgba(3,2,1,0.78)";
|
|
834
|
+
var label = n.name.length > 22 ? n.name.slice(0, 21) + "…" : n.name;
|
|
835
|
+
var tw2 = ctx.measureText(label).width;
|
|
836
|
+
ctx.fillRect(n.x - tw2 / 2 - 5, n.y + n.r + 3, tw2 + 10, fs + 4);
|
|
837
|
+
ctx.fillStyle = fl ? "#f97c3d" : "rgba(206,202,195,0.92)";
|
|
838
|
+
ctx.fillText(label, n.x, n.y + n.r + 5);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// agents (rim)
|
|
842
|
+
ctx.font = "500 " + Math.max(10, fs - 2) + "px " + FONT;
|
|
843
|
+
nodes.forEach(function (n) {
|
|
844
|
+
if (n.type !== "agent") return;
|
|
845
|
+
var lit = n.lit && now - n.lit < 1200 ? 1 - (now - n.lit) / 1200 : 0;
|
|
846
|
+
if (lit) anyAnim = true;
|
|
847
|
+
var pul = n.active && !REDUCED ? 0.5 + 0.5 * Math.sin(t * 3 + n.phase) : 0.3;
|
|
848
|
+
var as = n.r * (5 + pul * 1.5 + lit * 4);
|
|
849
|
+
ctx.globalAlpha = n.active || n.connected ? 0.9 : 0.45;
|
|
850
|
+
ctx.drawImage(sprite(n.color), n.x - as / 2, n.y - as / 2, as, as);
|
|
851
|
+
ctx.globalAlpha = 1;
|
|
852
|
+
ctx.fillStyle = n.active || lit ? "#efebe4" : n.color;
|
|
853
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, n.r * 0.6, 0, Math.PI * 2); ctx.fill();
|
|
854
|
+
if (n.connected) {
|
|
855
|
+
ctx.strokeStyle = n.color + "99"; ctx.lineWidth = 1;
|
|
856
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 3, 0, Math.PI * 2); ctx.stroke();
|
|
857
|
+
}
|
|
858
|
+
var above = n.y < G.cy;
|
|
859
|
+
ctx.textBaseline = above ? "bottom" : "top";
|
|
860
|
+
ctx.fillStyle = n.color;
|
|
861
|
+
ctx.fillText(agentShort(n.agent) + " · " + (n.name || ""), n.x, above ? n.y - n.r - 6 : n.y + n.r + 6);
|
|
862
|
+
});
|
|
863
|
+
ctx.textBaseline = "top";
|
|
864
|
+
|
|
865
|
+
if (hover) drawHover(hover);
|
|
866
|
+
return anyAnim;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function drawHover(n) {
|
|
870
|
+
ctx.strokeStyle = "rgba(239,235,228,0.8)";
|
|
871
|
+
ctx.lineWidth = 1.2;
|
|
872
|
+
ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 4, 0, Math.PI * 2); ctx.stroke();
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function loop(now) {
|
|
876
|
+
if (!running || !active) { running = false; return; }
|
|
877
|
+
requestAnimationFrame(loop);
|
|
878
|
+
var simActive = sim && sim.alpha() > sim.alphaMin();
|
|
879
|
+
var busy = simActive || impulses.length > 0;
|
|
880
|
+
var minGap = busy ? 33 : 50; // ~30 fps while animating, ~20 fps idle twinkle
|
|
881
|
+
if (now - lastFrame < minGap) return;
|
|
882
|
+
lastFrame = now;
|
|
883
|
+
if (simActive) sim.tick();
|
|
884
|
+
var anim = draw(now);
|
|
885
|
+
if (REDUCED && !simActive && !anim && !impulses.length) { running = false; } // static until something changes
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function kick() {
|
|
889
|
+
dirty = true;
|
|
890
|
+
if (document.hidden || !active) return;
|
|
891
|
+
if (!running) { running = true; requestAnimationFrame(loop); }
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function resize() {
|
|
895
|
+
var r = wrap.getBoundingClientRect();
|
|
896
|
+
if (r.width < 10 || r.height < 10) return;
|
|
897
|
+
W = r.width; H = r.height;
|
|
898
|
+
DPR = Math.min(window.devicePixelRatio || 1, 1.5);
|
|
899
|
+
canvas.width = Math.round(W * DPR); canvas.height = Math.round(H * DPR);
|
|
900
|
+
var oldG = G;
|
|
901
|
+
G = geometry();
|
|
902
|
+
renderOutline();
|
|
903
|
+
if (oldG && nodes.length) {
|
|
904
|
+
var sx = G.s / oldG.s;
|
|
905
|
+
nodes.forEach(function (n) {
|
|
906
|
+
if (n.x !== undefined) { n.x = G.cx + (n.x - oldG.cx) * sx; n.y = G.cy + (n.y - oldG.cy) * sx; }
|
|
907
|
+
if (n.type === "memory") n.r = 2.6 * G.s + 1;
|
|
908
|
+
if (n.type === "agent") n.r = 6 * G.s + 2.5;
|
|
909
|
+
});
|
|
910
|
+
assignAnchors();
|
|
911
|
+
buildSim(0.15);
|
|
912
|
+
}
|
|
913
|
+
if (pendingGraph) { var pg = pendingGraph; pendingGraph = null; setGraph(pg); }
|
|
914
|
+
kick();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// hover + click
|
|
918
|
+
canvas.addEventListener("mousemove", function (e) {
|
|
919
|
+
var r = canvas.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top;
|
|
920
|
+
var best = null, bd = 14 * 14;
|
|
921
|
+
nodes.forEach(function (n) {
|
|
922
|
+
if (n.x === undefined || n.type === "hub") return;
|
|
923
|
+
var d = (n.x - x) * (n.x - x) + (n.y - y) * (n.y - y);
|
|
924
|
+
if (d < Math.max(bd, n.r * n.r)) { bd = d; best = n; }
|
|
925
|
+
});
|
|
926
|
+
hover = best;
|
|
927
|
+
if (!best) { tip.hidden = true; canvas.style.cursor = ""; kick(); return; }
|
|
928
|
+
canvas.style.cursor = best.type === "project" && best.path ? "pointer" : "";
|
|
929
|
+
var html = best.type === "memory" ? '<b style="color:' + best.color + '">' + esc(best.kind) + "</b> " + esc(best.title) +
|
|
930
|
+
'<div class="muted">' + esc(best.project || "global") + "</div>"
|
|
931
|
+
: best.type === "project" ? "<b>" + esc(best.name) + '</b><div class="muted">' + (best.memories || 0) + " memories · " + (best.activity || 0) + " events 7d</div>"
|
|
932
|
+
: '<b style="color:' + best.color + '">' + esc(best.agent) + "</b> · " + esc(best.name || "") +
|
|
933
|
+
'<div class="muted">' + (best.active ? "active" : "idle") + (best.connected ? " · mcp connected" : "") + "</div>";
|
|
934
|
+
tip.innerHTML = html;
|
|
935
|
+
tip.hidden = false;
|
|
936
|
+
tip.style.left = Math.min(W - 330, x + 14) + "px";
|
|
937
|
+
tip.style.top = Math.min(H - 60, y + 12) + "px";
|
|
938
|
+
kick();
|
|
939
|
+
});
|
|
940
|
+
canvas.addEventListener("mouseleave", function () { hover = null; tip.hidden = true; kick(); });
|
|
941
|
+
canvas.addEventListener("click", function () {
|
|
942
|
+
if (hover && hover.type === "project" && hover.path) location.href = "/projects/detail?path=" + encodeURIComponent(hover.path);
|
|
943
|
+
if (hover && hover.type === "memory") location.href = "/memory?q=" + encodeURIComponent(hover.title || "");
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
document.addEventListener("visibilitychange", function () {
|
|
947
|
+
if (document.hidden) running = false; else kick();
|
|
948
|
+
});
|
|
949
|
+
if (window.ResizeObserver) new ResizeObserver(function () { resize(); }).observe(wrap);
|
|
950
|
+
else window.addEventListener("resize", resize);
|
|
951
|
+
|
|
952
|
+
function setActive(on) {
|
|
953
|
+
active = on;
|
|
954
|
+
canvas.hidden = !on;
|
|
955
|
+
if (!on) { tip.hidden = true; running = false; } else { resize(); kick(); }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
resize();
|
|
959
|
+
return { setGraph: setGraph, pulse: pulse, resize: resize, setActive: setActive };
|
|
960
|
+
})();
|
|
961
|
+
|
|
962
|
+
// ------------------------------------------------------------------ 2D / 3D switch
|
|
963
|
+
|
|
964
|
+
// Glow is owned by brain3d.js; show the saved state even before (or without) the 3D view starting.
|
|
965
|
+
(function () {
|
|
966
|
+
var glow = $("glow-toggle"), on = false;
|
|
967
|
+
if (!glow) return;
|
|
968
|
+
try { on = localStorage.getItem("devmem.bloom") === "1"; } catch (e) { /* default off */ }
|
|
969
|
+
glow.classList.toggle("on", on);
|
|
970
|
+
glow.setAttribute("aria-pressed", on ? "true" : "false");
|
|
971
|
+
glow.textContent = on ? "● Glow on" : "○ Glow off";
|
|
972
|
+
})();
|
|
973
|
+
|
|
974
|
+
var Brain = (function () {
|
|
975
|
+
var mode = "3d", impl3d = null, lastGraph = null, reason = "";
|
|
976
|
+
var btn = $("dim-toggle"), box3d = $("brain3d");
|
|
977
|
+
try { mode = localStorage.getItem("devmem.brainMode") === "2d" ? "2d" : "3d"; } catch (e) { /* default 3D */ }
|
|
978
|
+
|
|
979
|
+
function ensure3d() {
|
|
980
|
+
if (impl3d) return true;
|
|
981
|
+
if (!window.DevmemBrain3D || !window.ForceGraph3D) { reason = "3D library did not load"; return false; }
|
|
982
|
+
if (!window.DevmemBrain3D.available()) { reason = "WebGL unavailable"; return false; }
|
|
983
|
+
try {
|
|
984
|
+
box3d.hidden = false;
|
|
985
|
+
impl3d = window.DevmemBrain3D.create(box3d, { agentColor: agentColor, agentShort: agentShort, esc: esc, reduced: REDUCED });
|
|
986
|
+
if (lastGraph) impl3d.setGraph(lastGraph);
|
|
987
|
+
return true;
|
|
988
|
+
} catch (err) {
|
|
989
|
+
console.error("devmem 3D failed, using 2D", err);
|
|
990
|
+
reason = "3D failed to start";
|
|
991
|
+
box3d.hidden = true;
|
|
992
|
+
impl3d = null;
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function apply(wanted, remember) {
|
|
998
|
+
var use3d = wanted === "3d" && ensure3d();
|
|
999
|
+
mode = use3d ? "3d" : "2d";
|
|
1000
|
+
box3d.hidden = !use3d;
|
|
1001
|
+
$("brain-wrap").classList.toggle("is3d", use3d);
|
|
1002
|
+
if (impl3d) impl3d.setActive(use3d);
|
|
1003
|
+
if (use3d && lastGraph) impl3d.setGraph(lastGraph); // merge by id: positions are kept
|
|
1004
|
+
Brain2D.setActive(!use3d);
|
|
1005
|
+
if (!use3d && lastGraph) Brain2D.setGraph(lastGraph);
|
|
1006
|
+
btn.innerHTML = '<span class="seg' + (use3d ? "" : " active") + '">2D</span><span class="seg' + (use3d ? " active" : "") + '">3D</span>';
|
|
1007
|
+
document.body.classList.toggle("view-2d", !use3d);
|
|
1008
|
+
btn.title = use3d ? "Showing the 3D web; click for the flat 2D brain" :
|
|
1009
|
+
(wanted === "3d" && reason ? "3D unavailable (" + reason + "); showing 2D" : "Showing the 2D brain; click for the 3D web");
|
|
1010
|
+
btn.classList.toggle("unavailable", wanted === "3d" && !use3d);
|
|
1011
|
+
if (remember) { try { localStorage.setItem("devmem.brainMode", wanted); } catch (e) { /* ignore */ } }
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
btn.addEventListener("click", function () { apply(mode === "3d" ? "2d" : "3d", true); });
|
|
1015
|
+
apply(mode, false);
|
|
1016
|
+
|
|
1017
|
+
return {
|
|
1018
|
+
setGraph: function (g) {
|
|
1019
|
+
lastGraph = g;
|
|
1020
|
+
if (mode === "3d" && impl3d) impl3d.setGraph(g); else Brain2D.setGraph(g);
|
|
1021
|
+
},
|
|
1022
|
+
pulse: function (p) { if (mode === "3d" && impl3d) impl3d.pulse(p); else Brain2D.pulse(p); },
|
|
1023
|
+
resize: function () { if (mode === "3d" && impl3d) impl3d.resize(); else Brain2D.resize(); },
|
|
1024
|
+
mode: function () { return mode; },
|
|
1025
|
+
};
|
|
1026
|
+
})();
|
|
1027
|
+
window.devmemBrain = Brain;
|
|
1028
|
+
|
|
1029
|
+
// ------------------------------------------------------------------ state + stream
|
|
1030
|
+
|
|
1031
|
+
var lastState = null;
|
|
1032
|
+
|
|
1033
|
+
function applyState(s) {
|
|
1034
|
+
lastState = Object.assign({}, lastState || {}, s);
|
|
1035
|
+
if (s.graph) Brain.setGraph(s.graph);
|
|
1036
|
+
renderLimits(s.limits || []);
|
|
1037
|
+
renderAgents(s.agents || {});
|
|
1038
|
+
renderFeed(s.activity || []);
|
|
1039
|
+
renderCounts(s.counts || {});
|
|
1040
|
+
if (Object.prototype.hasOwnProperty.call(s, "reminders")) {
|
|
1041
|
+
var serverReminders = {};
|
|
1042
|
+
(s.reminders || []).forEach(function (item) { serverReminders[item.id] = true; });
|
|
1043
|
+
Object.keys(finishedReminders).forEach(function (id) {
|
|
1044
|
+
if (!serverReminders[id]) delete finishedReminders[id];
|
|
1045
|
+
});
|
|
1046
|
+
renderReminders(s.reminders);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
var es = null, lastMessage = 0, backoff = 1000;
|
|
1051
|
+
|
|
1052
|
+
function setStatus(state, text) {
|
|
1053
|
+
var el = $("status");
|
|
1054
|
+
el.dataset.state = state;
|
|
1055
|
+
el.querySelector(".txt").textContent = text;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function disconnect() { if (es) { es.onerror = null; es.close(); es = null; } }
|
|
1059
|
+
function connect() {
|
|
1060
|
+
disconnect();
|
|
1061
|
+
if (document.hidden) return;
|
|
1062
|
+
setStatus("connecting", "connecting");
|
|
1063
|
+
es = new EventSource("/api/stream");
|
|
1064
|
+
es.addEventListener("state", function (e) {
|
|
1065
|
+
lastMessage = Date.now(); backoff = 1000;
|
|
1066
|
+
setStatus("live", "live");
|
|
1067
|
+
try { applyState(JSON.parse(e.data)); } catch (err) { console.error(err); }
|
|
1068
|
+
});
|
|
1069
|
+
es.addEventListener("messages", function (e) {
|
|
1070
|
+
lastMessage = Date.now();
|
|
1071
|
+
try { renderMessages(JSON.parse(e.data)); } catch (err) { console.error(err); }
|
|
1072
|
+
});
|
|
1073
|
+
es.addEventListener("pulse", function (e) {
|
|
1074
|
+
lastMessage = Date.now();
|
|
1075
|
+
try {
|
|
1076
|
+
var pulses = JSON.parse(e.data);
|
|
1077
|
+
pulses.forEach(Brain.pulse);
|
|
1078
|
+
if (pulses.some(function (pulse) { return pulse.type === "ado"; })) refreshAdoCard();
|
|
1079
|
+
} catch (err) { console.error(err); }
|
|
1080
|
+
});
|
|
1081
|
+
es.onerror = function () {
|
|
1082
|
+
setStatus("offline", "offline · reconnecting");
|
|
1083
|
+
if (es.readyState === EventSource.CLOSED) {
|
|
1084
|
+
setTimeout(connect, backoff);
|
|
1085
|
+
backoff = Math.min(backoff * 2, 15000);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
refreshAdoCard();
|
|
1091
|
+
setInterval(refreshAdoCard, 30000);
|
|
1092
|
+
|
|
1093
|
+
// Watchdog: a half-open connection (laptop sleep, server restart) sends nothing; reconnect it.
|
|
1094
|
+
setInterval(function () {
|
|
1095
|
+
if (!es) return; // hidden: no stream on purpose
|
|
1096
|
+
if (lastMessage && Date.now() - lastMessage > 12000) {
|
|
1097
|
+
setStatus("offline", "offline · reconnecting");
|
|
1098
|
+
lastMessage = Date.now();
|
|
1099
|
+
connect();
|
|
1100
|
+
}
|
|
1101
|
+
}, 3000);
|
|
1102
|
+
|
|
1103
|
+
// fix(2026-10-28): browsers allow 6 connections per site. A stream left open by a background tab or by a page kept for Back used
|
|
1104
|
+
// one up each, until new pages hung loading forever. Only the visible page keeps its stream.
|
|
1105
|
+
window.addEventListener("pagehide", disconnect);
|
|
1106
|
+
window.addEventListener("pageshow", function (e) { if (e.persisted) connect(); });
|
|
1107
|
+
document.addEventListener("visibilitychange", function () { if (document.hidden) disconnect(); else if (!es) { lastMessage = Date.now(); connect(); } });
|
|
1108
|
+
|
|
1109
|
+
setInterval(function () {
|
|
1110
|
+
var d = new Date();
|
|
1111
|
+
$("clock").textContent = d.toLocaleDateString(undefined, { weekday: "short", day: "numeric", month: "short" }) + " " +
|
|
1112
|
+
d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
1113
|
+
tickGauges();
|
|
1114
|
+
tickRelTimes();
|
|
1115
|
+
}, 1000);
|
|
1116
|
+
|
|
1117
|
+
connect();
|
|
1118
|
+
})();
|