@echomem/mcp 1.4.6 → 1.4.7
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/assets/hud/session-viewer.html +1699 -0
- package/dist/city/10-problems-report.html +649 -0
- package/dist/city/echo-ai-city-only.html +39 -647
- package/dist/city/echo-ai-city-only.template.html +4 -4
- package/dist/forensics-10-problems.js +632 -0
- package/dist/hud/cli.js +0 -0
- package/dist/hud/electron-main.js +48 -4
- package/dist/hud/preload.cjs +9 -0
- package/dist/hud/server.js +93 -0
- package/dist/hud/web.js +82 -7
- package/dist/index.js +247 -14
- package/dist/setup.js +2 -2
- package/dist/v1-contract.js +134 -3
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ const COLLAPSED_HEIGHT = 112;
|
|
|
12
12
|
const EXPANDED_HEIGHT = 460;
|
|
13
13
|
let hudServer = null;
|
|
14
14
|
let mainWindow = null;
|
|
15
|
+
const dragState = new Map();
|
|
15
16
|
// Single-instance: a second `echomem-hud app` brings the existing HUD back instead of opening another.
|
|
16
17
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
|
17
18
|
if (!gotSingleInstanceLock)
|
|
@@ -31,6 +32,33 @@ app.whenReady().then(async () => {
|
|
|
31
32
|
win.setBounds({ ...bounds, height });
|
|
32
33
|
writeBounds(win);
|
|
33
34
|
});
|
|
35
|
+
ipcMain.on("hud:drag-start", (event, payload) => {
|
|
36
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
37
|
+
if (!win)
|
|
38
|
+
return;
|
|
39
|
+
const point = dragPoint(payload);
|
|
40
|
+
if (!point)
|
|
41
|
+
return;
|
|
42
|
+
dragState.set(event.sender.id, { ...point, bounds: win.getBounds() });
|
|
43
|
+
});
|
|
44
|
+
ipcMain.on("hud:drag-move", (event, payload) => {
|
|
45
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
46
|
+
const state = dragState.get(event.sender.id);
|
|
47
|
+
const point = dragPoint(payload);
|
|
48
|
+
if (!win || !state || !point)
|
|
49
|
+
return;
|
|
50
|
+
win.setBounds({
|
|
51
|
+
...state.bounds,
|
|
52
|
+
x: Math.round(state.bounds.x + point.startX - state.startX),
|
|
53
|
+
y: Math.round(state.bounds.y + point.startY - state.startY),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
ipcMain.on("hud:drag-end", (event) => {
|
|
57
|
+
dragState.delete(event.sender.id);
|
|
58
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
59
|
+
if (win)
|
|
60
|
+
writeBounds(win);
|
|
61
|
+
});
|
|
34
62
|
const preferredUrl = `http://127.0.0.1:${port}`;
|
|
35
63
|
const onReveal = (filePath) => {
|
|
36
64
|
try {
|
|
@@ -40,8 +68,16 @@ app.whenReady().then(async () => {
|
|
|
40
68
|
/* best-effort */
|
|
41
69
|
}
|
|
42
70
|
};
|
|
71
|
+
const onOpenExternal = (externalUrl) => {
|
|
72
|
+
try {
|
|
73
|
+
shell.openExternal(externalUrl);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* best-effort */
|
|
77
|
+
}
|
|
78
|
+
};
|
|
43
79
|
try {
|
|
44
|
-
hudServer = await createHudServer({ mode, port, onReveal });
|
|
80
|
+
hudServer = await createHudServer({ mode, port, onReveal, onOpenExternal });
|
|
45
81
|
createWindow(hudServer.url);
|
|
46
82
|
}
|
|
47
83
|
catch (error) {
|
|
@@ -49,7 +85,7 @@ app.whenReady().then(async () => {
|
|
|
49
85
|
createWindow(preferredUrl);
|
|
50
86
|
return;
|
|
51
87
|
}
|
|
52
|
-
hudServer = await createHudServer({ mode, port: 0, onReveal });
|
|
88
|
+
hudServer = await createHudServer({ mode, port: 0, onReveal, onOpenExternal });
|
|
53
89
|
createWindow(hudServer.url);
|
|
54
90
|
}
|
|
55
91
|
}).catch((error) => {
|
|
@@ -90,8 +126,11 @@ function createWindow(url) {
|
|
|
90
126
|
}
|
|
91
127
|
win.on("moved", () => writeBounds(win));
|
|
92
128
|
mainWindow = win;
|
|
93
|
-
win.on("closed", () => {
|
|
94
|
-
|
|
129
|
+
win.on("closed", () => {
|
|
130
|
+
dragState.delete(win.webContents.id);
|
|
131
|
+
if (mainWindow === win)
|
|
132
|
+
mainWindow = null;
|
|
133
|
+
});
|
|
95
134
|
win.webContents.on("context-menu", () => {
|
|
96
135
|
const menu = Menu.buildFromTemplate([
|
|
97
136
|
{ label: "Hide for now", click: () => { if (!win.isDestroyed())
|
|
@@ -207,6 +246,11 @@ function writeBounds(win) {
|
|
|
207
246
|
fs.mkdirSync(path.dirname(boundsPath()), { recursive: true });
|
|
208
247
|
fs.writeFileSync(boundsPath(), JSON.stringify(b, null, 2));
|
|
209
248
|
}
|
|
249
|
+
function dragPoint(payload) {
|
|
250
|
+
const startX = typeof payload.screenX === "number" && Number.isFinite(payload.screenX) ? payload.screenX : null;
|
|
251
|
+
const startY = typeof payload.screenY === "number" && Number.isFinite(payload.screenY) ? payload.screenY : null;
|
|
252
|
+
return startX === null || startY === null ? null : { startX, startY };
|
|
253
|
+
}
|
|
210
254
|
function parseFlags(argv) {
|
|
211
255
|
const parsed = {};
|
|
212
256
|
for (let i = 0; i < argv.length; i += 1) {
|
package/dist/hud/preload.cjs
CHANGED
|
@@ -7,4 +7,13 @@ electron_1.contextBridge.exposeInMainWorld("echomemHud", {
|
|
|
7
7
|
setOpen(open, height) {
|
|
8
8
|
electron_1.ipcRenderer.send("hud:set-open", { open, height });
|
|
9
9
|
},
|
|
10
|
+
startDrag(screenX, screenY) {
|
|
11
|
+
electron_1.ipcRenderer.send("hud:drag-start", { screenX, screenY });
|
|
12
|
+
},
|
|
13
|
+
moveDrag(screenX, screenY) {
|
|
14
|
+
electron_1.ipcRenderer.send("hud:drag-move", { screenX, screenY });
|
|
15
|
+
},
|
|
16
|
+
endDrag() {
|
|
17
|
+
electron_1.ipcRenderer.send("hud:drag-end");
|
|
18
|
+
},
|
|
10
19
|
});
|
package/dist/hud/server.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { HudMonitor } from "./monitor.js";
|
|
5
7
|
import { HUD_HTML } from "./web.js";
|
|
6
8
|
import { buildCapsuleText } from "./capsule.js";
|
|
9
|
+
import { homePath } from "./fs.js";
|
|
7
10
|
export async function createHudServer(opts = {}) {
|
|
8
11
|
const mode = opts.mode || "auto";
|
|
9
12
|
const port = opts.port ?? 17377;
|
|
@@ -47,6 +50,96 @@ export async function createHudServer(opts = {}) {
|
|
|
47
50
|
res.end(JSON.stringify({ ok: true, pinned: id && id !== "auto" ? id : null }));
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
53
|
+
if (url.pathname === "/viewer") {
|
|
54
|
+
const viewerPath = fileURLToPath(new URL("../../assets/hud/session-viewer.html", import.meta.url));
|
|
55
|
+
if (!fs.existsSync(viewerPath)) {
|
|
56
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
57
|
+
res.end("session viewer not found");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
61
|
+
fs.createReadStream(viewerPath).pipe(res);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (url.pathname === "/session-raw") {
|
|
65
|
+
// Serve a tracked session's raw log so the viewer can fetch it same-origin. Path-validated.
|
|
66
|
+
const target = url.searchParams.get("path") || "";
|
|
67
|
+
const known = new Set([
|
|
68
|
+
...(latest.recentSessions || []).map((s) => s.sourcePath),
|
|
69
|
+
...(latest.sessions || []).map((s) => s.sourcePath),
|
|
70
|
+
]);
|
|
71
|
+
if (!target || !known.has(target) || !fs.existsSync(target)) {
|
|
72
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
73
|
+
res.end("not a tracked session");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
77
|
+
fs.createReadStream(target).pipe(res);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (url.pathname === "/session-payload") {
|
|
81
|
+
// The REAL context window: Codex logs each API request (websocket request:) in logs_2.sqlite.
|
|
82
|
+
// Return one payload per line (NDJSON) so the same viewer can render them like records.
|
|
83
|
+
const target = url.searchParams.get("path") || "";
|
|
84
|
+
const known = new Set([
|
|
85
|
+
...(latest.recentSessions || []).map((s) => s.sourcePath),
|
|
86
|
+
...(latest.sessions || []).map((s) => s.sourcePath),
|
|
87
|
+
]);
|
|
88
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
89
|
+
if (!target || !known.has(target)) {
|
|
90
|
+
res.end(JSON.stringify({ note: "not a tracked session" }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Real request payloads are only logged by Codex (logs_2.sqlite). Claude sessions have none.
|
|
94
|
+
if (!path.basename(target).startsWith("rollout-")) {
|
|
95
|
+
res.end(JSON.stringify({ note: "This is a Claude session — the real request payload (context window) is only recorded for Codex. Switch to a Codex tab in the HUD, then open the viewer." }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const uuid = (path.basename(target).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) || [])[0] || "";
|
|
99
|
+
const db = homePath(".codex", "logs_2.sqlite");
|
|
100
|
+
if (!uuid || !fs.existsSync(db)) {
|
|
101
|
+
res.end(JSON.stringify({ note: "no Codex request-payload log found (logs_2.sqlite missing or no thread id)" }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const query = `SELECT feedback_log_body FROM logs WHERE thread_id='${uuid}' AND feedback_log_body LIKE '%websocket request: %' ORDER BY ts LIMIT 500;`;
|
|
106
|
+
const out = execFileSync("sqlite3", ["-json", db, query], { maxBuffer: 256 * 1024 * 1024 }).toString();
|
|
107
|
+
const rows = JSON.parse(out || "[]");
|
|
108
|
+
const lines = [];
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
const body = row.feedback_log_body || "";
|
|
111
|
+
const at = body.indexOf("websocket request: ");
|
|
112
|
+
if (at < 0)
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
lines.push(JSON.stringify(JSON.parse(body.slice(at + "websocket request: ".length))));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* skip unparseable payload */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
res.end(lines.length ? lines.join("\n") : JSON.stringify({ note: "no request payloads found for this thread" }));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
res.end(JSON.stringify({ note: "payload query failed", error: error instanceof Error ? error.message : String(error) }));
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (url.pathname === "/open-viewer") {
|
|
129
|
+
const target = url.searchParams.get("path") || "";
|
|
130
|
+
const known = new Set([
|
|
131
|
+
...(latest.recentSessions || []).map((s) => s.sourcePath),
|
|
132
|
+
...(latest.sessions || []).map((s) => s.sourcePath),
|
|
133
|
+
]);
|
|
134
|
+
const canOpen = Boolean(target) && known.has(target) && typeof opts.onOpenExternal === "function";
|
|
135
|
+
if (canOpen) {
|
|
136
|
+
const host = req.headers.host || `127.0.0.1:${port}`;
|
|
137
|
+
opts.onOpenExternal(`http://${host}/viewer?path=${encodeURIComponent(target)}`);
|
|
138
|
+
}
|
|
139
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
140
|
+
res.end(JSON.stringify({ ok: canOpen, reason: canOpen ? undefined : opts.onOpenExternal ? "unknown_path" : "no_desktop" }));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
50
143
|
if (url.pathname === "/reveal") {
|
|
51
144
|
const target = url.searchParams.get("path") || "";
|
|
52
145
|
// Only reveal paths we actually track — never open an arbitrary path from a query string.
|
package/dist/hud/web.js
CHANGED
|
@@ -37,12 +37,14 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
37
37
|
width: min(360px, calc(100vw - 16px));
|
|
38
38
|
background: transparent;
|
|
39
39
|
-webkit-font-smoothing: antialiased;
|
|
40
|
+
cursor: grab;
|
|
40
41
|
}
|
|
42
|
+
#hud.dragging { cursor: grabbing; }
|
|
41
43
|
.glance {
|
|
42
44
|
position: relative;
|
|
43
45
|
height: 112px;
|
|
44
46
|
user-select: none;
|
|
45
|
-
cursor:
|
|
47
|
+
cursor: grab;
|
|
46
48
|
}
|
|
47
49
|
.drag {
|
|
48
50
|
position: absolute;
|
|
@@ -51,8 +53,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
51
53
|
width: 20px;
|
|
52
54
|
height: 32px;
|
|
53
55
|
border-radius: 8px;
|
|
54
|
-
-webkit-app-region: drag;
|
|
55
56
|
opacity: 0.78;
|
|
57
|
+
pointer-events: none;
|
|
56
58
|
z-index: 5;
|
|
57
59
|
}
|
|
58
60
|
.drag::before {
|
|
@@ -75,7 +77,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
75
77
|
height: 70px;
|
|
76
78
|
overflow: hidden;
|
|
77
79
|
border-radius: 18px;
|
|
78
|
-
transition: filter 0.45s ease;
|
|
80
|
+
transition: filter 0.45s ease, transform 0.18s ease;
|
|
79
81
|
}
|
|
80
82
|
@keyframes echo-bob {
|
|
81
83
|
0%, 100% { transform: translateY(0); }
|
|
@@ -106,6 +108,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
106
108
|
border-radius: 34px;
|
|
107
109
|
background: #fdfaf2;
|
|
108
110
|
filter: var(--shadow);
|
|
111
|
+
transition: border-color 0.18s ease, background 0.18s ease, filter 0.18s ease;
|
|
109
112
|
}
|
|
110
113
|
.status-bubble::after {
|
|
111
114
|
content: "";
|
|
@@ -177,6 +180,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
177
180
|
background: var(--card);
|
|
178
181
|
color: var(--ink);
|
|
179
182
|
filter: var(--shadow);
|
|
183
|
+
cursor: grab;
|
|
180
184
|
}
|
|
181
185
|
#hud.open .details { display: block; }
|
|
182
186
|
.head {
|
|
@@ -387,6 +391,29 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
387
391
|
#hud.s-red .mascot-stage { filter: drop-shadow(0 3px 8px rgba(199, 55, 47, 0.48)); }
|
|
388
392
|
#hud.s-amber .mascot { animation-duration: 5.6s; }
|
|
389
393
|
#hud.s-red .mascot { animation-duration: 6.8s; }
|
|
394
|
+
#hud:hover .status-bubble {
|
|
395
|
+
border-color: rgba(26, 58, 143, 0.96);
|
|
396
|
+
background: #fff7e8;
|
|
397
|
+
filter: drop-shadow(0 3px 10px rgba(26, 58, 143, 0.18));
|
|
398
|
+
}
|
|
399
|
+
#hud:hover .drag::before {
|
|
400
|
+
background: rgba(26, 58, 143, 0.72);
|
|
401
|
+
box-shadow: 0 10px 0 rgba(26, 58, 143, 0.72), 10px 0 0 rgba(26, 58, 143, 0.72), 10px 10px 0 rgba(26, 58, 143, 0.72);
|
|
402
|
+
}
|
|
403
|
+
#hud:hover .mascot-stage { transform: translateY(-1px); }
|
|
404
|
+
#hud.dragging .glance,
|
|
405
|
+
#hud.dragging .details,
|
|
406
|
+
#hud.dragging button { cursor: grabbing; }
|
|
407
|
+
#hud.dragging .status-bubble {
|
|
408
|
+
border-color: rgba(199, 55, 47, 0.86);
|
|
409
|
+
background: #fff1df;
|
|
410
|
+
filter: drop-shadow(0 4px 12px rgba(199, 55, 47, 0.22));
|
|
411
|
+
}
|
|
412
|
+
#hud.dragging .drag::before {
|
|
413
|
+
background: rgba(199, 55, 47, 0.92);
|
|
414
|
+
box-shadow: 0 10px 0 rgba(199, 55, 47, 0.92), 10px 0 0 rgba(199, 55, 47, 0.92), 10px 10px 0 rgba(199, 55, 47, 0.92);
|
|
415
|
+
}
|
|
416
|
+
#hud.dragging .mascot-stage { transform: translateY(-2px) scale(1.02); }
|
|
390
417
|
</style>
|
|
391
418
|
</head>
|
|
392
419
|
<body>
|
|
@@ -429,7 +456,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
429
456
|
</div>
|
|
430
457
|
<div class="meta" id="meta"></div>
|
|
431
458
|
<div class="path" id="path"></div>
|
|
432
|
-
<button class="reveal" id="reveal" style="display:none">↗
|
|
459
|
+
<button class="reveal" id="reveal" style="display:none">↗ Open session in viewer</button>
|
|
433
460
|
<div class="missing" id="missing"></div>
|
|
434
461
|
</div>
|
|
435
462
|
</section>
|
|
@@ -468,11 +495,53 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
468
495
|
let activityEpoch = null;
|
|
469
496
|
let lastState = null;
|
|
470
497
|
let currentSrc = null;
|
|
498
|
+
let hudDrag = null;
|
|
499
|
+
let suppressNextToggle = false;
|
|
500
|
+
const dragBlockSelector = "button, input, textarea, select, a, pre, .tab, .switchbtn, .reveal, .renew-btn, .copy-btn";
|
|
501
|
+
|
|
502
|
+
hud.addEventListener("pointerdown", beginHudDrag);
|
|
503
|
+
window.addEventListener("pointermove", moveHudDrag);
|
|
504
|
+
window.addEventListener("pointerup", endHudDrag);
|
|
505
|
+
window.addEventListener("pointercancel", endHudDrag);
|
|
506
|
+
|
|
507
|
+
function beginHudDrag(event) {
|
|
508
|
+
if (event.button !== 0 || !(event.target instanceof HTMLElement)) return;
|
|
509
|
+
if (event.target.closest(dragBlockSelector)) return;
|
|
510
|
+
if (!window.echomemHud || typeof window.echomemHud.startDrag !== "function") return;
|
|
511
|
+
hudDrag = { pointerId: event.pointerId, startX: event.screenX, startY: event.screenY, moved: false };
|
|
512
|
+
try { hud.setPointerCapture(event.pointerId); } catch {}
|
|
513
|
+
window.echomemHud.startDrag(event.screenX, event.screenY);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function moveHudDrag(event) {
|
|
517
|
+
if (!hudDrag || event.pointerId !== hudDrag.pointerId) return;
|
|
518
|
+
if (!window.echomemHud || typeof window.echomemHud.moveDrag !== "function") return;
|
|
519
|
+
const dx = event.screenX - hudDrag.startX;
|
|
520
|
+
const dy = event.screenY - hudDrag.startY;
|
|
521
|
+
if (!hudDrag.moved && Math.hypot(dx, dy) < 3) return;
|
|
522
|
+
hudDrag.moved = true;
|
|
523
|
+
hud.classList.add("dragging");
|
|
524
|
+
window.echomemHud.moveDrag(event.screenX, event.screenY);
|
|
525
|
+
event.preventDefault();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function endHudDrag(event) {
|
|
529
|
+
if (!hudDrag || event.pointerId !== hudDrag.pointerId) return;
|
|
530
|
+
const moved = hudDrag.moved;
|
|
531
|
+
hudDrag = null;
|
|
532
|
+
hud.classList.remove("dragging");
|
|
533
|
+
try { hud.releasePointerCapture(event.pointerId); } catch {}
|
|
534
|
+
if (window.echomemHud && typeof window.echomemHud.endDrag === "function") window.echomemHud.endDrag();
|
|
535
|
+
if (moved) {
|
|
536
|
+
suppressNextToggle = true;
|
|
537
|
+
setTimeout(() => { suppressNextToggle = false; }, 180);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
471
540
|
|
|
472
|
-
//
|
|
541
|
+
// Open the active session in the session viewer (Electron opens the browser; no-op otherwise).
|
|
473
542
|
if (revealEl) revealEl.addEventListener("click", () => {
|
|
474
543
|
if (!currentSrc) return;
|
|
475
|
-
fetch("/
|
|
544
|
+
fetch("/open-viewer?path=" + encodeURIComponent(currentSrc)).catch(() => {});
|
|
476
545
|
});
|
|
477
546
|
|
|
478
547
|
// Circular top-right toggle: jump to the next agent's newest session (Claude ⇄ Codex).
|
|
@@ -507,7 +576,13 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
507
576
|
});
|
|
508
577
|
|
|
509
578
|
document.getElementById("toggle").addEventListener("click", (event) => {
|
|
510
|
-
if (
|
|
579
|
+
if (suppressNextToggle) {
|
|
580
|
+
suppressNextToggle = false;
|
|
581
|
+
event.preventDefault();
|
|
582
|
+
event.stopPropagation();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (event.target instanceof HTMLElement && event.target.closest(".switchbtn")) return;
|
|
511
586
|
hud.classList.toggle("open");
|
|
512
587
|
syncHeight();
|
|
513
588
|
});
|