Add default-on toggle to hide empty collabs
Browse filesCollabs with no agents and no messages are hidden by default via a
checkbox above the directory table, with an "N empty hidden" note. The
filter is bypassed while stats are still loading so skeleton rows show.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- index.html +40 -1
index.html
CHANGED
|
@@ -40,6 +40,14 @@
|
|
| 40 |
.btn:hover { border-color: var(--muted-3); color: var(--ink); }
|
| 41 |
.btn:disabled { opacity: 0.6; cursor: wait; }
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
/* --- Table --- */
|
| 44 |
.tbl { border: 1px solid var(--border); background: #fff; margin-top: 20px; overflow-x: auto; }
|
| 45 |
.row, .thead {
|
|
@@ -184,6 +192,10 @@
|
|
| 184 |
</div>
|
| 185 |
|
| 186 |
<div class="tabpane" id="tab-directory">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
<div class="tbl" id="tbl">
|
| 188 |
<div class="thead">
|
| 189 |
<div class="h nosort"></div>
|
|
@@ -288,6 +300,13 @@ const rowsEl = document.getElementById("rows");
|
|
| 288 |
const stateEl = document.getElementById("state");
|
| 289 |
let ROWS = []; // [{space, st}]
|
| 290 |
let sort = { col: "last", dir: -1 }; // default: most recently active first
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
function rel(iso) {
|
| 293 |
if (!iso) return "·";
|
|
@@ -369,7 +388,19 @@ function renderRows() {
|
|
| 369 |
h.querySelector(".arr").textContent = active ? (sort.dir < 0 ? "▼" : "▲") : "";
|
| 370 |
});
|
| 371 |
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
const sp = r.space, st = r.st, cd = sp.cardData || {};
|
| 374 |
const title = cd.title || sp.id.split("/")[1];
|
| 375 |
const desc = cd.short_description || cd.description || "";
|
|
@@ -450,8 +481,14 @@ document.querySelectorAll(".thead .h[data-sort]").forEach(h => {
|
|
| 450 |
});
|
| 451 |
});
|
| 452 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
async function load() {
|
| 454 |
const btn = document.getElementById("refresh");
|
|
|
|
| 455 |
btn.disabled = true; btn.textContent = "↻ Loading…";
|
| 456 |
rowsEl.innerHTML = ""; stateEl.style.display = ""; stateEl.className = "state"; stateEl.textContent = "Discovering agent collabs…";
|
| 457 |
try {
|
|
@@ -469,6 +506,7 @@ async function load() {
|
|
| 469 |
if (r.st.agents) totA += r.st.agents;
|
| 470 |
if (r.st.messages) totM += r.st.messages;
|
| 471 |
}));
|
|
|
|
| 472 |
renderRows();
|
| 473 |
finishSummary(spaces.length, totA, totM);
|
| 474 |
if (new URLSearchParams(location.search).get("expand") === "1") {
|
|
@@ -477,6 +515,7 @@ async function load() {
|
|
| 477 |
} catch (e) {
|
| 478 |
stateEl.className = "state err"; stateEl.textContent = "Failed to load: " + e.message;
|
| 479 |
} finally {
|
|
|
|
| 480 |
btn.disabled = false; btn.textContent = "↻ Refresh";
|
| 481 |
}
|
| 482 |
}
|
|
|
|
| 40 |
.btn:hover { border-color: var(--muted-3); color: var(--ink); }
|
| 41 |
.btn:disabled { opacity: 0.6; cursor: wait; }
|
| 42 |
|
| 43 |
+
/* --- Filter bar --- */
|
| 44 |
+
.filterbar { display: flex; align-items: center; gap: 12px; margin-top: 20px; font-family: "JetBrains Mono", monospace; }
|
| 45 |
+
.toggle { display: inline-flex; align-items: center; gap: 7px; font-size: 10px; font-weight: 500; letter-spacing: 0.5px; text-transform: uppercase; color: var(--muted); cursor: pointer; user-select: none; }
|
| 46 |
+
.toggle:hover { color: var(--ink); }
|
| 47 |
+
.toggle input { width: 13px; height: 13px; cursor: pointer; accent-color: var(--accent); margin: 0; }
|
| 48 |
+
.filter-note { font-size: 10px; letter-spacing: 0.4px; color: var(--muted-4); font-variant-numeric: tabular-nums; }
|
| 49 |
+
.empty-note { padding: 30px 16px; text-align: center; font-family: "JetBrains Mono", monospace; font-size: 11px; color: var(--muted-3); line-height: 1.7; }
|
| 50 |
+
|
| 51 |
/* --- Table --- */
|
| 52 |
.tbl { border: 1px solid var(--border); background: #fff; margin-top: 20px; overflow-x: auto; }
|
| 53 |
.row, .thead {
|
|
|
|
| 192 |
</div>
|
| 193 |
|
| 194 |
<div class="tabpane" id="tab-directory">
|
| 195 |
+
<div class="filterbar">
|
| 196 |
+
<label class="toggle"><input type="checkbox" id="hideEmpty" checked><span>Hide empty collabs</span></label>
|
| 197 |
+
<span class="filter-note" id="hiddenNote"></span>
|
| 198 |
+
</div>
|
| 199 |
<div class="tbl" id="tbl">
|
| 200 |
<div class="thead">
|
| 201 |
<div class="h nosort"></div>
|
|
|
|
| 300 |
const stateEl = document.getElementById("state");
|
| 301 |
let ROWS = []; // [{space, st}]
|
| 302 |
let sort = { col: "last", dir: -1 }; // default: most recently active first
|
| 303 |
+
let hideEmpty = true; // default on: hide collabs with no agents and no messages
|
| 304 |
+
let loading = false; // while true, skip the empty filter so skeleton rows show
|
| 305 |
+
|
| 306 |
+
function rowIsEmpty(r) { // empty = no agents and no messages (null/0 both count as none)
|
| 307 |
+
const st = r.st || {};
|
| 308 |
+
return !(st.agents || st.messages);
|
| 309 |
+
}
|
| 310 |
|
| 311 |
function rel(iso) {
|
| 312 |
if (!iso) return "·";
|
|
|
|
| 388 |
h.querySelector(".arr").textContent = active ? (sort.dir < 0 ? "▼" : "▲") : "";
|
| 389 |
});
|
| 390 |
|
| 391 |
+
// hide empties (skipped during load so skeleton rows still appear while stats stream in)
|
| 392 |
+
const applyFilter = hideEmpty && !loading;
|
| 393 |
+
const filtered = applyFilter ? sorted.filter(r => !rowIsEmpty(r)) : sorted;
|
| 394 |
+
const hidden = ROWS.length - filtered.length;
|
| 395 |
+
const noteEl = document.getElementById("hiddenNote");
|
| 396 |
+
if (noteEl) noteEl.textContent = applyFilter && hidden > 0 ? `${hidden} empty hidden` : "";
|
| 397 |
+
|
| 398 |
+
if (applyFilter && filtered.length === 0 && ROWS.length > 0) {
|
| 399 |
+
rowsEl.innerHTML = `<div class="empty-note">All ${ROWS.length} discovered collabs are empty (no agents or messages).<br>Turn off “Hide empty collabs” to show them.</div>`;
|
| 400 |
+
return;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
rowsEl.innerHTML = filtered.map(r => {
|
| 404 |
const sp = r.space, st = r.st, cd = sp.cardData || {};
|
| 405 |
const title = cd.title || sp.id.split("/")[1];
|
| 406 |
const desc = cd.short_description || cd.description || "";
|
|
|
|
| 481 |
});
|
| 482 |
});
|
| 483 |
|
| 484 |
+
document.getElementById("hideEmpty").addEventListener("change", function () {
|
| 485 |
+
hideEmpty = this.checked;
|
| 486 |
+
renderRows();
|
| 487 |
+
});
|
| 488 |
+
|
| 489 |
async function load() {
|
| 490 |
const btn = document.getElementById("refresh");
|
| 491 |
+
loading = true;
|
| 492 |
btn.disabled = true; btn.textContent = "↻ Loading…";
|
| 493 |
rowsEl.innerHTML = ""; stateEl.style.display = ""; stateEl.className = "state"; stateEl.textContent = "Discovering agent collabs…";
|
| 494 |
try {
|
|
|
|
| 506 |
if (r.st.agents) totA += r.st.agents;
|
| 507 |
if (r.st.messages) totM += r.st.messages;
|
| 508 |
}));
|
| 509 |
+
loading = false;
|
| 510 |
renderRows();
|
| 511 |
finishSummary(spaces.length, totA, totM);
|
| 512 |
if (new URLSearchParams(location.search).get("expand") === "1") {
|
|
|
|
| 515 |
} catch (e) {
|
| 516 |
stateEl.className = "state err"; stateEl.textContent = "Failed to load: " + e.message;
|
| 517 |
} finally {
|
| 518 |
+
loading = false;
|
| 519 |
btn.disabled = false; btn.textContent = "↻ Refresh";
|
| 520 |
}
|
| 521 |
}
|