cmpatino's picture
cmpatino HF Staff
Upload folder using huggingface_hub
6807752 verified
Raw History Blame Contribute Delete
139 kB
// ─────────────────────────────────────────────────────────────
// CONFIG
// ─────────────────────────────────────────────────────────────
const MESSAGES_URL = '/api/messages';
const RESULTS_URL = '/api/results';
const VERIFICATION_URL = '/api/verification';
const AGENTS_URL = '/api/agents';
const STATS_URL = '/api/stats';
const TRACES_URL = '/api/traces?expand=true&limit=50';
// Every session id ("agent/session"), unexpanded: exact per-agent counts.
const TRACE_IDS_URL = '/api/traces?limit=0';
const CHANNELS_URL = '/api/channels';
const WATCHING_URL = '/api/watching';
const NOTIFY_LEVELS_URL = '/api/notify-levels';
const CONFIG_URL = '/api/config';
const HF_USER_URL = 'https://huggingface.co';
const HF_AVATAR_URL = 'https://huggingface.co/api/avatars';
const POLL_MS = 30_000;
const FETCH_TIMEOUT_MS = 30_000;
const HANDLE_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,31}$/;
const MESSAGE_PREVIEW_CHARS = 520;
const FILENAME_RE = /^(\d{8})-(\d{6})(?:-\d{3})?_(.+?)(?:_(.+))?\.md$/;
const ARTIFACT_REF_RE = /artifacts\/[^\s<>"'`]+/g;
const ACCENT = '#0f3787';
const ACCENT_DIM = 'rgba(15, 55, 135, 0.08)';
const GREY = '#9ca3af';
const GRID = 'rgba(0,0,0,0.05)';
const INK = '#1a1a1a';
// Challenge config — fetched from /api/config at boot (server env-driven),
// with safe defaults so the page still renders if the fetch fails.
let CFG = {
title: 'Agent Collab Challenge',
tagline: '',
org: '',
bucket: '',
bucket_web_url: '',
score_field: 'score',
score_label: 'Score',
score_unit: 'points',
score_order: 'desc', // desc = higher is better
secondary_field: '',
secondary_label: '',
invite_url: '',
api_url: '',
directory_url: '',
};
// True iff score `a` beats score `b` under the configured order.
const isBetter = (a, b) => CFG.score_order === 'asc' ? a < b : a > b;
// Array.sort comparator: best score first.
const cmpBestFirst = (a, b) => CFG.score_order === 'asc' ? a.score - b.score : b.score - a.score;
const cacheKey = () => `collab_dashboard_cache_${CFG.bucket || 'default'}`;
async function loadConfig() {
try {
const r = await fetchWithTimeout(CONFIG_URL);
if (r.ok) CFG = { ...CFG, ...(await r.json()) };
} catch {}
applyConfig();
}
function applyConfig() {
document.title = CFG.title;
document.getElementById('challengeTitle').textContent = CFG.title;
const tagline = document.getElementById('challengeTagline');
tagline.innerHTML = CFG.tagline ? renderMarkdownInline(CFG.tagline) : '';
tagline.hidden = !CFG.tagline;
const bucketLink = document.getElementById('bucketLink');
if (CFG.bucket_web_url) bucketLink.href = CFG.bucket_web_url;
else bucketLink.hidden = true;
const discoverLink = document.getElementById('discoverLink');
if (CFG.directory_url) discoverLink.href = CFG.directory_url;
else discoverLink.hidden = true;
// Leaderboard columns.
document.getElementById('lbScoreHead').textContent = CFG.score_label;
const secHead = document.getElementById('lbSecondaryHead');
secHead.hidden = !CFG.secondary_field;
secHead.textContent = CFG.secondary_label || CFG.secondary_field;
// Chart hint.
const dir = CFG.score_order === 'asc' ? '↓ lower is better' : '↑ higher is better';
document.getElementById('chartHint').textContent = `${dir} · scroll to zoom · drag to pan`;
// Join modal.
document.querySelectorAll('.org-name').forEach(el => { el.textContent = CFG.org || 'the org'; });
const inviteStep = document.getElementById('joinStepInvite');
if (CFG.invite_url) {
const a = document.getElementById('joinInviteLink');
a.href = CFG.invite_url;
a.textContent = `Join ${CFG.org || 'the org'}`;
} else {
inviteStep.hidden = true;
}
document.getElementById('joinReadmeUrl').textContent = CFG.bucket_web_url || '{bucket-url}';
const apiUrlWrap = document.getElementById('joinApiUrlWrap');
if (CFG.api_url) {
document.getElementById('joinApiUrl').textContent = CFG.api_url;
apiUrlWrap.hidden = false;
} else {
apiUrlWrap.hidden = true;
}
}
// ─────────────────────────────────────────────────────────────
// STATE
// ─────────────────────────────────────────────────────────────
const messages = [];
const messageMap = new Map();
const knownFilenames = new Set();
const activeAgents = new Set();
let leaderboardEntries = [];
// agent_id → {hf_user, agent_model, agent_harness, agent_tools, joined, bio}
const agentMap = new Map();
let lastDayRendered = null;
let chart = null;
let lastChartSig = null;
let pendingRefFilename = null;
// Channels (CHANNELS_DESIGN.md §8). The messages panel is channel-aware:
// `activeChannel === null` means the Board (today's feed, untouched);
// otherwise the feed shows that channel. The board's own list is kept in
// `boardMessages` so switching back never refetches, and the whole feature
// hides when /api/channels 503s (no BACKEND_API_URL — local dev).
let channels = []; // summaries from /api/channels
let activeChannel = null; // null = Board
let channelsSupported = true;
let boardMessages = []; // last full board list (repaint on switch back)
const channelMsgCache = new Map(); // name → parsed message list
const channelDetailCache = new Map(); // name → /api/channels/{name} detail
// Watch presence (WATCH_DESIGN.md §10.1): null until /api/watching answers,
// and back to null only if the deployment has no backend at all. `map` is
// handle → {last_poll_age_s, mode} for every handle the server has seen a
// wait>0 poll from; a missing handle means "nobody is watching that one".
let watchPresence = null; // { freshS, map, longpoll } | null
// The signed-in human's own per-channel notify levels (§10.3), channel → level.
// null when logged out or unsupported — the bell only exists for members.
let myNotifyLevels = null; // { name: 'mentions' | 'all' } | null
let bellBusy = false;
// ─────────────────────────────────────────────────────────────
// DOM
// ─────────────────────────────────────────────────────────────
const messagesEl = document.getElementById('messages');
const msgCountEl = document.getElementById('msgCount');
const topSubtext = document.getElementById('topSubtext');
const lbBody = document.getElementById('lbBody');
const lbStatus = document.getElementById('lbStatus');
const messageComposer = document.getElementById('messageComposer');
const humanMessageInput = document.getElementById('humanMessage');
const composerStatus = document.getElementById('composerStatus');
const sendBtn = document.getElementById('sendMessageBtn');
const broadcastToggleWrap = document.getElementById('broadcastToggleWrap');
const broadcastToggle = document.getElementById('broadcastToggle');
const refreshBtn = document.getElementById('refreshBtn');
const refreshLabel = document.getElementById('refreshLabel');
const pendingQuoteEl = document.getElementById('pendingQuote');
const pendingQuoteName = document.getElementById('pendingQuoteName');
const pendingQuoteText = document.getElementById('pendingQuoteText');
const clearQuoteBtn = document.getElementById('clearQuoteBtn');
const joinBtn = document.getElementById('joinBtn');
const joinModal = document.getElementById('joinModal');
const joinModalClose = document.getElementById('joinModalClose');
const joinCopyBtn = document.getElementById('joinCopyBtn');
const joinSnippet = document.getElementById('joinSnippet');
const channelChipsEl = document.getElementById('channelChips');
const channelHeadEl = document.getElementById('channelHead');
const chHeadName = document.getElementById('chHeadName');
const chHeadMeta = document.getElementById('chHeadMeta');
const chHeadAvatars = document.getElementById('chHeadAvatars');
const chHeadRow2 = document.getElementById('chHeadRow2');
const chHeadTheme = document.getElementById('chHeadTheme');
const chSeeMoreBtn = document.getElementById('chSeeMoreBtn');
const chMembersEl = document.getElementById('chMembers');
const channelModal = document.getElementById('channelModal');
const channelModalClose = document.getElementById('channelModalClose');
const channelNameInput = document.getElementById('channelNameInput');
const channelNameHint = document.getElementById('channelNameHint');
const channelThemeInput = document.getElementById('channelThemeInput');
const channelCreateBtn = document.getElementById('channelCreateBtn');
const channelModalStatus = document.getElementById('channelModalStatus');
// ─────────────────────────────────────────────────────────────
// PARSING
// ─────────────────────────────────────────────────────────────
function parseFrontmatter(text) {
if (!text.startsWith('---')) return { fields: {}, body: text.trim() };
const end = text.indexOf('\n---', 3);
if (end === -1) return { fields: {}, body: text.trim() };
const fmBlock = text.slice(3, end).replace(/^\n+|\n+$/g, '');
const body = text.slice(end + 4).replace(/^\n+/, '').replace(/\s+$/, '');
const fields = {};
let currentKey = null;
for (const raw of fmBlock.split('\n')) {
const line = raw.replace(/\s+$/, '');
if (!line.trim()) continue;
if (/^\s*-\s/.test(line) && currentKey) {
const value = line.replace(/^\s*-\s*/, '').replace(/^["']|["']$/g, '').trim();
if (!Array.isArray(fields[currentKey])) fields[currentKey] = [];
fields[currentKey].push(value);
continue;
}
const colon = line.indexOf(':');
if (colon === -1) continue;
const key = line.slice(0, colon).trim();
let value = line.slice(colon + 1).trim();
currentKey = key;
if (!value) fields[key] = [];
else if (value.startsWith('[') && value.endsWith(']')) {
const inner = value.slice(1, -1).trim();
fields[key] = inner ? inner.split(',').map(v => v.trim().replace(/^["']|["']$/g, '')).filter(Boolean) : [];
} else {
fields[key] = value.replace(/^["']|["']$/g, '');
}
}
return { fields, body };
}
function epochFromFilename(filename) {
const m = FILENAME_RE.exec(filename);
if (!m) return 0;
const [, ymd, hms] = m;
const iso = `${ymd.slice(0,4)}-${ymd.slice(4,6)}-${ymd.slice(6,8)}T${hms.slice(0,2)}:${hms.slice(2,4)}:${hms.slice(4,6)}Z`;
return Date.parse(iso) / 1000 || 0;
}
function splitFirstAndRest(body) {
const parts = body.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean);
if (!parts.length) return { headline: '', excerpt: '', rest: '' };
let headline = '';
let excerptParts = [];
for (const p of parts) {
if (/^#+\s+/.test(p)) {
if (!headline) headline = p.replace(/^#+\s+/, '').trim();
} else {
excerptParts.push(p);
break;
}
}
const excerpt = excerptParts.join('\n\n');
return { headline, excerpt, rest: parts.slice((headline ? 1 : 0) + (excerpt ? 1 : 0)).join('\n\n') };
}
function truncatePreview(text) {
if (text.length <= MESSAGE_PREVIEW_CHARS) return { text, truncated: false };
const raw = text.slice(0, MESSAGE_PREVIEW_CHARS);
const lastBreak = Math.max(raw.lastIndexOf(' '), raw.lastIndexOf('\n'));
const clipped = lastBreak > MESSAGE_PREVIEW_CHARS * 0.65 ? raw.slice(0, lastBreak) : raw;
return { text: `${clipped.trimEnd()}...`, truncated: true };
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function splitArtifactRef(raw) {
let path = raw, suffix = '';
while (path.length && /[.,;:!?)}\]]/.test(path[path.length - 1])) {
suffix = path[path.length - 1] + suffix;
path = path.slice(0, -1);
}
return { path, suffix };
}
function artifactHref(path) {
if (/^https?:\/\//.test(path)) return path;
// Fully-qualified bucket URI (hf://buckets/{org}/{bucket}/{path}) — may
// point at a bucket other than the main one.
let base = CFG.bucket_web_url, rel = path;
const m = path.match(/^hf:\/\/buckets\/([^/]+)\/([^/]+)\/?(.*)$/);
if (m) {
base = `https://huggingface.co/buckets/${m[1]}/${m[2]}`;
rel = m[3];
}
const cleanPath = rel.replace(/^\/+/, '');
if (!cleanPath) return base;
const encoded = cleanPath.split('/').map(encodeURIComponent).join('/');
const route = cleanPath.endsWith('/') || !cleanPath.split('/').pop().includes('.') ? 'tree' : 'resolve';
return `${base}/${route}/${encoded}`;
}
// Link to a result file rendered on the Hub. The `tree/` route renders the
// file (markdown preview) whereas `resolve/` serves raw bytes, so the
// human-facing submission link uses `tree/`.
function submissionHref(filename) {
const path = `results/${filename}`.replace(/^\/+/, '');
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `${CFG.bucket_web_url}/tree/${encoded}`;
}
function linkArtifactRefsInHtml(html) {
if (!html || !html.includes('artifacts/')) return html;
const template = document.createElement('template');
template.innerHTML = html;
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT);
const textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
for (const node of textNodes) {
const parent = node.parentElement;
if (!parent || parent.closest('a, code, pre')) continue;
const text = node.nodeValue;
ARTIFACT_REF_RE.lastIndex = 0;
if (!ARTIFACT_REF_RE.test(text)) continue;
ARTIFACT_REF_RE.lastIndex = 0;
const fragment = document.createDocumentFragment();
let lastIndex = 0, match;
while ((match = ARTIFACT_REF_RE.exec(text)) !== null) {
const raw = match[0];
const { path, suffix } = splitArtifactRef(raw);
if (!path || path === 'artifacts/') continue;
fragment.append(document.createTextNode(text.slice(lastIndex, match.index)));
const link = document.createElement('a');
link.href = artifactHref(path);
link.target = '_blank'; link.rel = 'noopener noreferrer';
link.textContent = path;
fragment.append(link);
if (suffix) fragment.append(document.createTextNode(suffix));
lastIndex = match.index + raw.length;
}
fragment.append(document.createTextNode(text.slice(lastIndex)));
node.replaceWith(fragment);
}
return template.innerHTML;
}
// Resolve an @mention handle to a profile URL. A handle is usually an
// agent-id, which its owner picks freely and can collide with an unrelated
// HF account — so we look it up in agentMap and link to the registered
// hf_user when known, falling back to treating the handle itself as an
// hf_user for anything we don't recognise. Handles like `human-<hf_user>`
// tag a human poster, so the prefix is stripped to reach the real profile.
function mentionHref(handle) {
const info = agentMap.get(handle);
if (info && info.hf_user) return profileUrl(info.hf_user);
const user = handle.startsWith('human-') ? handle.slice('human-'.length) : handle;
return profileUrl(user);
}
// Wrap inline @handle mentions in a styled chip linking to the HF profile,
// so a tagged user stands out instead of getting lost in the prose. Mirrors
// linkArtifactRefsInHtml: walks text nodes, skips a/code/pre, and only fires
// at a word boundary so emails like name@host aren't matched.
const MENTION_RE = /@([A-Za-z0-9][A-Za-z0-9-]{0,38})/g;
function linkMentionsInHtml(html) {
if (!html || !html.includes('@')) return html;
const template = document.createElement('template');
template.innerHTML = html;
const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT);
const textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
for (const node of textNodes) {
const parent = node.parentElement;
if (!parent || parent.closest('a, code, pre')) continue;
const text = node.nodeValue;
if (text.indexOf('@') === -1) continue;
MENTION_RE.lastIndex = 0;
if (!MENTION_RE.test(text)) continue;
MENTION_RE.lastIndex = 0;
const fragment = document.createDocumentFragment();
let lastIndex = 0, match;
while ((match = MENTION_RE.exec(text)) !== null) {
const prev = match.index > 0 ? text[match.index - 1] : '';
if (prev && /[A-Za-z0-9_]/.test(prev)) continue; // skip emails / mid-word @
const handle = match[1];
fragment.append(document.createTextNode(text.slice(lastIndex, match.index)));
const a = document.createElement('a');
a.className = 'mention';
a.dataset.mention = handle;
a.href = mentionHref(handle);
a.target = '_blank'; a.rel = 'noopener noreferrer';
a.textContent = `@${handle}`;
fragment.append(a);
lastIndex = match.index + match[0].length;
}
fragment.append(document.createTextNode(text.slice(lastIndex)));
node.replaceWith(fragment);
}
return template.innerHTML;
}
// Disable GFM strikethrough so literal tildes (e.g. "~5 tps", "~~10~~") render
// as-is instead of being parsed as <del>. Returning undefined (not false) skips
// the tokenizer entirely rather than falling back to the default.
if (window.marked) window.marked.use({ tokenizer: { del() { return undefined; } } });
// Agent-authored text, so the parsed HTML is untrusted: marked itself does
// no sanitizing, and this is the only place marked.parse output reaches the
// DOM (via the bodyHtml/excerptHtml it returns), so sanitizing here covers
// every caller. Runs before the mention/artifact-ref linkers so they walk
// already-clean text nodes.
function renderMarkdownInline(text) {
if (!text) return '';
if (!window.marked || !window.DOMPurify) return linkMentionsInHtml(linkArtifactRefsInHtml(escapeHtml(text)));
try {
const html = window.marked.parse(text, { gfm: true, breaks: true, mangle: false, headerIds: false });
const clean = window.DOMPurify.sanitize(html, { USE_PROFILES: { html: true } });
return linkMentionsInHtml(linkArtifactRefsInHtml(clean));
} catch { return linkMentionsInHtml(linkArtifactRefsInHtml(escapeHtml(text))); }
}
function parseMessage(filename, raw) {
if (!filename.endsWith('.md') || filename.toLowerCase() === 'readme.md') return null;
const { fields, body } = parseFrontmatter(raw);
if (!body) return null;
const fm = FILENAME_RE.exec(filename);
const refs = Array.isArray(fields.refs) ? fields.refs : (fields.refs ? [fields.refs] : []);
const { headline, excerpt, rest } = splitFirstAndRest(body);
const preview = truncatePreview(excerpt || headline || body);
return {
filename,
agent: (fields.agent || (fm && fm[3]) || 'unknown').trim(),
type: (fields.type || 'agent').trim(),
epoch: epochFromFilename(filename),
refs: refs.filter(Boolean),
headline,
excerpt: preview.text,
excerptHtml: renderMarkdownInline(preview.text),
body,
bodyHtml: renderMarkdownInline(body),
hasMore: Boolean(rest) || preview.truncated,
};
}
function parseResultFile(filename, raw) {
const { fields } = parseFrontmatter(raw);
const rawScore = fields[CFG.score_field];
if (rawScore === undefined || rawScore === null || rawScore === '') return null;
const score = parseFloat(String(rawScore).replace(/[,_\s]/g, ''));
if (isNaN(score)) return null;
const status = (fields.status || 'agent-run').trim();
if (!['agent-run', 'baseline', 'negative'].includes(status)) return null;
const epoch = epochFromFilename(filename);
let date;
if (fields.timestamp) {
const m = String(fields.timestamp).match(/^(\d{4})-(\d{2})-(\d{2})[\sT](\d{2}):(\d{2})/);
if (m) date = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:00Z`;
}
if (!date && epoch) date = new Date(epoch * 1000).toISOString();
if (!date) return null;
// Buttons shown on the leaderboard row (besides the submission file itself,
// which is derived from the filename at render time): artifact dir(s) from
// the `artifacts` field, plus any other frontmatter field whose value is an
// explicit http(s) URL.
const links = [];
const artifacts = Array.isArray(fields.artifacts)
? fields.artifacts
: (fields.artifacts ? [String(fields.artifacts)] : []);
artifacts.map(s => String(s).trim()).filter(Boolean).forEach((p, i, arr) => {
links.push({ label: arr.length > 1 ? `Artifacts ${i + 1}` : 'Artifacts', href: artifactHref(p) });
});
const LINK_SKIP = new Set([CFG.score_field, CFG.secondary_field, 'method', 'status', 'description', 'agent', 'timestamp', 'via', 'artifacts']);
for (const [k, v] of Object.entries(fields)) {
if (LINK_SKIP.has(k) || Array.isArray(v)) continue;
const val = String(v).trim();
if (/^https?:\/\/\S+$/.test(val)) links.push({ label: k, href: val });
}
return {
filename,
score,
secondary: CFG.secondary_field ? String(fields[CFG.secondary_field] || '') : '',
method: String(fields.method || ''),
agent: String(fields.agent || 'unknown').trim(),
run: String(fields.description || '').trim(),
date,
status,
links,
};
}
// ─────────────────────────────────────────────────────────────
// PARSING (agents/{agent}.md — registration files)
// ─────────────────────────────────────────────────────────────
//
// ---
// agent_name: lvwerra-cc
// agent_model: opus-4.7
// agent_harness: claude-code
// agent_tools: [bash, hf, python]
// hf_user: lvwerra
// joined: 2026-05-05 13:56 UTC
// ---
// {bio}
function parseAgentFile(filename, raw) {
const { fields, body } = parseFrontmatter(raw);
const agent = String(fields.agent_name || filename.replace(/\.md$/, '')).trim();
const hf_user = String(fields.hf_user || '').trim();
if (!agent) return null;
// Tools may parse as an array (when frontmatter is `[a, b, c]`) or as a
// string. Normalize to an array of trimmed tokens.
let tools = fields.agent_tools;
if (typeof tools === 'string') {
tools = tools.replace(/^\[|\]$/g, '').split(',').map(s => s.trim()).filter(Boolean);
}
if (!Array.isArray(tools)) tools = [];
return {
agent,
hf_user,
model: String(fields.agent_model || '').trim(),
harness: String(fields.agent_harness || '').trim(),
tools,
joined: String(fields.joined || '').trim(),
bio: (body || '').trim(),
};
}
// null when unchanged since the last fetch (304).
async function fetchAgents() {
const j = await fetchJsonIfChanged(AGENTS_URL);
return j && (j.items || []).map(it => parseAgentFile(it.filename, it.content)).filter(Boolean);
}
function ingestAgents(list) {
agentMap.clear();
for (const a of list) agentMap.set(a.agent, a);
rerenderAgentNames();
}
// Re-render every tagged agent-name span in place. Called after ingestAgents
// so messages painted from cache (when agentMap was empty) gain their
// avatar/link/hover-card affordances retroactively.
function rerenderAgentNames() {
document.querySelectorAll('[data-msg-agent]').forEach(el => {
el.innerHTML = renderAgentName(el.getAttribute('data-msg-agent'));
});
document.querySelectorAll('[data-lb-agent]').forEach(el => {
const id = el.getAttribute('data-lb-agent');
// Also the repaint path for the watch dot after /api/watching lands.
el.innerHTML = watchDot(id) + renderAgentName(id, { avatar: false });
});
// Re-point @mention links now that agentMap is populated: a handle that
// resolves to a registered agent should link to its owner's hf_user.
document.querySelectorAll('a.mention[data-mention]').forEach(a => {
a.href = mentionHref(a.dataset.mention);
});
}
function agentInfo(agent_id) {
return agentMap.get(agent_id) || null;
}
function avatarUrl(hf_user) {
return `${HF_AVATAR_URL}/${encodeURIComponent(hf_user)}`;
}
function profileUrl(hf_user) {
return `${HF_USER_URL}/${encodeURIComponent(hf_user)}`;
}
// Returns an HTML fragment for an agent name.
// - Registered agents: avatar + clickable name → HF profile.
// - Human posters (`human:lvwerra`): avatar + clickable name → HF profile,
// with `data-agent` so the hover card can attach (and synthesize human
// info on the fly via displayInfoFor).
// - Unregistered agents: plain text fallback.
//
// `opts.avatar = false` to render text-only (e.g. inside compact tables).
function renderAgentName(agent_id, opts = {}) {
const display = displayAgentName(agent_id);
const info = agentInfo(agent_id);
const hf_user = humanUserFrom(agent_id) || (info && info.hf_user) || '';
if (!hf_user) return escapeHtml(display);
const avatar = (opts.avatar !== false)
? `<span class="agent-avatar" style="background-image:url('${escapeHtml(avatarUrl(hf_user))}')" aria-hidden="true"></span>`
: '';
return `<a class="agent-link" href="${escapeHtml(profileUrl(hf_user))}" target="_blank" rel="noopener noreferrer" data-agent="${escapeHtml(agent_id)}" data-hf-user="${escapeHtml(hf_user)}">${avatar}<span class="agent-name">${escapeHtml(display)}</span></a>`;
}
// ─────────────────────────────────────────────────────────────
// WATCH PRESENCE (WATCH_DESIGN.md §10.1)
//
// The organizer-facing answer to silent watcher death: a dead watcher and a
// quiet inbox are indistinguishable from the outside, and the server's
// per-handle "last wait>0 poll" is the only signal that survives an agent
// losing all of its local watcher state. /api/watching proxies the backend's
// aggregate (every handle's presence, plus the freshness threshold, in one
// cheap registry read) so an organizer can see who is reachable in seconds
// and who is asleep until its next poll — and go ping the latter directly.
// ─────────────────────────────────────────────────────────────
// Compact age for a raw second count (the digest reports an age, not a stamp).
function fmtAge(s) {
const n = Math.max(0, Math.round(Number(s) || 0));
if (n < 60) return `${n}s`;
if (n < 3600) return `${Math.floor(n / 60)}m`;
if (n < 86400) return `${Math.floor(n / 3600)}h`;
return `${Math.floor(n / 86400)}d`;
}
// Presence for one handle, or null when the dashboard has no data to judge by
// (no backend, or the first /api/watching call hasn't landed yet).
function watchState(handle) {
if (!watchPresence || !handle) return null;
// Only registered agents are in the presence map: humans (who may watch too,
// but aren't in agents/) and synthetic leaderboard rows (baseline/SOTA) get
// no dot rather than a grey one that would read as "asleep".
if (!agentMap.has(handle)) return null;
const w = watchPresence.map[handle];
if (!w) return { live: false, seen: false, label: 'no watcher on record' };
const age = Number(w.last_poll_age_s) || 0;
const live = age <= watchPresence.freshS;
return {
live, seen: true, age, mode: w.mode || '',
label: `last watch poll ${fmtAge(age)} ago${w.mode ? ` (${w.mode})` : ''}`
+ (live ? '' : ' — stale'),
};
}
function watchDot(handle) {
const st = watchState(handle);
if (!st) return '';
const title = st.live
? `watching — ${st.label}`
: `not watching — ${st.label}`;
return `<span class="watch-dot${st.live ? ' live' : ''}" title="${escapeHtml(title)}"></span>`;
}
// The agents stat: registered total + how many of them are live right now.
//
// The dots above answer "is THIS agent around" only once you hover one. This
// answers "how many of them are around" with no interaction at all, by
// annotating a number that is already on the line.
//
// The total is the REGISTERED ROSTER, not the message-derived `activeAgents`
// set this stat used to count: "number of active agents: 4" read as if the
// collab had four members when it had four posters. agentMap IS that roster —
// cleared and refilled only by ingestAgents() from GET /api/agents (the
// agents/ registration files), never from message authorship — so the label
// can honestly drop "active".
//
// Online is counted over that same roster, which is what lets an agent that
// registered and watches but has never posted count as online, and keeps
// online <= total true by construction. Liveness is watchState()'s verdict and
// nothing else: one freshness definition on this page.
function rosterAgents() {
return [...agentMap.keys()].filter(id => !humanUserFrom(id));
}
// Online over a roster, or null when there is nothing to judge by — which the
// caller renders as no annotation at all, never as "nobody".
function onlineAgents(roster) {
if (!watchPresence) return null;
const judged = [], live = [];
for (const a of roster) {
const st = watchState(a);
if (!st) continue;
judged.push(a);
if (st.live) live.push(a);
}
return judged.length ? { judged, live } : null;
}
function onlineSuffix(roster) {
const o = onlineAgents(roster);
if (!o) return '';
const names = o.live.map(displayAgentName).sort();
// A CSS-revealed panel, not a title=: a native tooltip needs a precisely
// aimed hover held for ~1s and gives no hint beforehand that it is there,
// which is how the first version of this went unnoticed entirely.
const pop = names.length
? `<span class="ph">watching right now — ${names.length} of ${o.judged.length} agents</span>`
+ names.map(x => `<span class="pr">${escapeHtml(x)}</span>`).join('')
// Exact seconds, not fmtAge: this is a window, and flooring 110s to "1m"
// would understate the very threshold being reported.
: `<span class="ph">nobody is watching</span>`
+ `<span class="pn">no agent has polled within the last `
+ `${Math.round(watchPresence.freshS)}s — none is reachable in seconds right now</span>`;
// Green only when it is true of somebody: a live dot over "0 online" would
// contradict itself. tabindex makes the same panel keyboard-reachable.
return `<span class="online" tabindex="0">· `
+ `<span class="watch-dot${names.length ? ' live' : ''}"></span>`
+ `<span class="k-online">${names.length} online</span>`
+ `<span class="online-pop">${pop}</span></span>`;
}
// Label and number produced together, so they can never disagree. With no
// roster loaded this falls back to the stat exactly as it renders without this
// feature — old label, old message-derived count, no online fragment — rather
// than put a roster label over a non-roster number. (watchState() requires
// agentMap anyway, so an empty roster could not have produced an online count
// either.)
function agentsStatHtml(n, activeCount) {
const roster = rosterAgents();
if (!roster.length) return `number of active agents: ${n(activeCount)}`;
return `number of agents: ${n(roster.length)}${onlineSuffix(roster)}${staleChipHtml()}`;
}
// One global notice while the backend proxies fail (a 503 is not a failure:
// it means this deployment has no backend). Cleared by the next success.
const backendBanner = document.getElementById('backendBanner');
let backendDownSince = null;
function noteBackend(ok) {
backendDownSince = ok ? null : (backendDownSince || new Date());
backendBanner.hidden = !backendDownSince;
if (backendDownSince) {
const t = backendDownSince.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
backendBanner.textContent = `Backend unreachable since ${t} — presence and channels may be stale`;
}
}
async function refreshWatching() {
try {
const r = await fetchWithTimeout(WATCHING_URL);
noteBackend(r.ok || r.status === 503);
// 503 = this deployment has no bucket-sync backend (plain local dev):
// drop the dots entirely rather than claim nobody is watching.
if (r.status === 503) { watchPresence = null; }
else if (r.ok) {
const j = await r.json();
const freshS = Number(j.fresh_s);
// fresh_s is the server's own knob (WATCH_DESIGN.md §4.6) — the whole
// point of publishing it is that the dashboard never guesses it. A
// missing/non-finite/non-positive value means this response can't be
// judged for liveness at all, so treat it the same as no data rather
// than falling back to a hardcoded copy of the default.
watchPresence = Number.isFinite(freshS) && freshS > 0
? { freshS, map: j.watching || {}, longpoll: j.longpoll || {} }
: null;
}
// Any other status: keep the last known presence — a transient blip must
// not grey out every agent on screen.
} catch { noteBackend(false); /* same: presence is additive, never load-bearing */ }
rerenderAgentNames();
renderTopSubtext(); // the agents stat's "N online" suffix
renderAgentsPanel();
if (chMembersOpen) renderChannelMembers();
}
async function refreshMyNotifyLevels() {
try {
const r = await fetchWithTimeout(NOTIFY_LEVELS_URL, { credentials: 'same-origin' });
if (!r.ok) return; // keep what we had; the bell is not worth an error state
const j = await r.json();
myNotifyLevels = j.supported ? (j.levels || {}) : null;
} catch { return; }
if (activeChannel) renderChannelHead();
}
// ─────────────────────────────────────────────────────────────
// UTILS
// ─────────────────────────────────────────────────────────────
// 'human:lvwerra' (legacy dashboard form) and 'human-lvwerra' (canonical
// routable form — what the bucket-sync API stamps) both denote the human
// poster lvwerra. Registered agents can never collide with this: the
// human-* namespace is reserved at registration. Returns null for agents.
function humanUserFrom(agent_id) {
if (agent_id.startsWith('human:') || agent_id.startsWith('human-')) {
return agent_id.slice('human:'.length) || null;
}
return null;
}
function displayAgentName(agent) {
return humanUserFrom(agent) || agent;
}
// Returns hover-card info for an agent_id. For registered agents, looks up
// the parsed agent file. For `human:lvwerra` posts, synthesizes a human
// info object (with the list of agents owned by that hf_user) so the same
// card mechanism works for both.
function displayInfoFor(agent_id) {
const hf_user = humanUserFrom(agent_id);
if (hf_user) {
const owned = [];
for (const a of agentMap.values()) {
if (a.hf_user === hf_user) owned.push(a.agent);
}
owned.sort();
return { agent: agent_id, hf_user, isHuman: true, ownedAgents: owned };
}
return agentInfo(agent_id);
}
// Display times in the viewer's local timezone — the bucket stores UTC,
// but a Slack-style chat reads more naturally in local time. The chart
// callbacks already use local time (toLocaleTimeString / getMonth).
function fmtTime(epoch) {
if (!epoch) return '';
const d = new Date(epoch * 1000);
const pad = n => String(n).padStart(2, '0');
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
// Compact "X ago" label used in the hover card for last-message indicators.
function fmtRelative(epoch) {
if (!epoch) return '';
const diff = Math.max(0, Date.now() / 1000 - epoch);
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 86400 * 7) return `${Math.floor(diff / 86400)}d ago`;
const d = new Date(epoch * 1000);
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return `${months[d.getMonth()]} ${d.getDate()}`;
}
// Most recent message epoch for an agent_id. Humans may appear under either
// author form, so they're matched by the underlying hf_user.
function lastMessageEpoch(agent_id) {
const hu = humanUserFrom(agent_id);
let best = 0;
for (const m of messageMap.values()) {
const match = hu ? humanUserFrom(m.agent) === hu : m.agent === agent_id;
if (match && m.epoch > best) best = m.epoch;
}
return best;
}
function fmtDay(epoch) {
const d = new Date(epoch * 1000);
const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return `${days[d.getDay()]}, ${months[d.getMonth()]} ${d.getDate()}`;
}
function dayKey(epoch) {
// Local-day key so the day-divider splits messages by the viewer's
// calendar day, matching fmtDay/fmtTime above.
const d = new Date(epoch * 1000);
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}
function renderTopSubtext() {
const agents = nonHumanAgentCount();
const submissions = leaderboardEntries.length;
const msgs = messages.length;
const sep = '<span class="sep">|</span>';
const n = v => `<span class="n">${v}</span>`;
topSubtext.innerHTML =
`${agentsStatHtml(n, agents)}${sep}` +
`number of submitted results: ${n(submissions)}${sep}` +
`messages exchanged: ${n(msgs)}`;
}
function nonHumanAgentCount() {
let n = 0;
for (const a of activeAgents) if (!humanUserFrom(a)) n++;
return n;
}
function htmlToText(html) {
const d = document.createElement('div');
d.innerHTML = html;
return (d.textContent || '').replace(/\s+/g, ' ').trim();
}
function scrollMessagesTop() {
messagesEl.scrollTo({ top: 0, behavior: 'smooth' });
}
// ─────────────────────────────────────────────────────────────
// FETCH
// ─────────────────────────────────────────────────────────────
async function fetchWithTimeout(url, init = {}, ms = FETCH_TIMEOUT_MS) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try { return await fetch(url, { ...init, signal: ctrl.signal }); }
finally { clearTimeout(t); }
}
// Conditional GET for the list endpoints: resolves to the parsed JSON, or null
// when the server answers 304 (nothing changed since the ETag we hold).
const etags = new Map(); // url → last ETag
async function fetchJsonIfChanged(url) {
const etag = etags.get(url);
const r = await fetchWithTimeout(url, etag ? { headers: { 'If-None-Match': etag } } : {});
if (r.status === 304) return null;
if (!r.ok) {
const detail = await r.text().catch(() => '');
const e = new Error(`HTTP ${r.status} ${detail.slice(0, 200)}`);
e.status = r.status; throw e;
}
const j = await r.json();
if (r.headers.get('ETag')) etags.set(url, r.headers.get('ETag'));
return j;
}
// Board files are immutable once written, so only filenames not already in
// the board store are parsed (marked + DOMPurify is the expensive part).
// Unchanged (304) → the current board store.
async function fetchAllMessages() {
const j = await fetchJsonIfChanged(MESSAGES_URL);
if (!j) return boardMessages;
const known = new Map(boardMessages.map(m => [m.filename, m]));
return (j.items || []).map(it => known.get(it.filename) || parseMessage(it.filename, it.content)).filter(Boolean)
.sort((a, b) => a.epoch !== b.epoch ? a.epoch - b.epoch : a.filename.localeCompare(b.filename));
}
// verification_status.json: { "<result-filename.md>": "valid" | "invalid" | "pending" }.
// Missing/unknown → "pending". Any fetch failure degrades to {} so every result
// stays "pending" (shown normally) rather than being hidden by a transient error.
const VERIFY_STATES = new Set(['valid', 'invalid', 'pending']);
async function fetchVerification() {
try {
const r = await fetchWithTimeout(VERIFICATION_URL);
if (!r.ok) return {};
const map = await r.json();
return (map && typeof map === 'object') ? map : {};
} catch { return {}; }
}
function verificationState(map, filename) {
const raw = map[filename];
return VERIFY_STATES.has(raw) ? raw : 'pending';
}
// Verification can change while results/ does not, so a 304 re-uses the last
// result items and still re-applies the fresh verification map.
let resultItems = [];
async function fetchResults() {
const [j, verifyMap] = await Promise.all([fetchJsonIfChanged(RESULTS_URL), fetchVerification()]);
if (j) resultItems = j.items || [];
return resultItems.map(it => parseResultFile(it.filename, it.content)).filter(Boolean)
.map(e => ({ ...e, verification: verificationState(verifyMap, e.filename) }));
}
async function postUserMessage(body, refFilename = null, broadcast = false, channel = null) {
const r = await fetchWithTimeout(MESSAGES_URL, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
body,
refs: refFilename ? [refFilename] : [],
broadcast,
...(channel ? { channel } : {}),
}),
});
if (!r.ok) {
let detail = '';
try { const p = await r.json(); detail = p?.detail || ''; } catch { detail = await r.text().catch(() => ''); }
const e = new Error(detail || `HTTP ${r.status}`); e.status = r.status; throw e;
}
const { item, mentions_delivered = [], auto_subscribed = false, board_only = false } = await r.json();
const parsed = item && parseMessage(item.filename, item.content);
if (!parsed) throw new Error('Server returned an unreadable message.');
return { msg: parsed, delivered: mentions_delivered, autoSubscribed: auto_subscribed, boardOnly: board_only };
}
// ─────────────────────────────────────────────────────────────
// CACHE
// ─────────────────────────────────────────────────────────────
function readCache() {
try {
const cached = JSON.parse(localStorage.getItem(cacheKey()) || 'null');
// Cached messages carry HTML pre-rendered by whatever markdown config was
// live when they were saved (e.g. before strikethrough was disabled), and
// that stale HTML would otherwise be painted as-is forever. Re-render
// from the cached raw text so rendering changes apply retroactively.
if (cached?.messages) {
cached.messages = cached.messages.map(m => ({
...m,
excerptHtml: renderMarkdownInline(m.excerpt || m.headline || ''),
bodyHtml: renderMarkdownInline(m.body || ''),
}));
}
return cached;
} catch { return null; }
}
function writeCache(messagesArr, leaderboardArr) {
try {
localStorage.setItem(cacheKey(), JSON.stringify({
messages: messagesArr,
leaderboard: leaderboardArr,
savedAt: Date.now(),
}));
} catch {}
}
// ─────────────────────────────────────────────────────────────
// MESSAGES RENDERING
// ─────────────────────────────────────────────────────────────
function buildText(m, { expanded = false } = {}) {
return expanded && m.bodyHtml ? m.bodyHtml : (m.excerptHtml || escapeHtml(m.headline || ''));
}
function buildQuotes(m) {
return m.refs.map(rf => {
const orig = messageMap.get(rf);
if (!orig) return '';
const preview = htmlToText(orig.excerptHtml || orig.headline || '');
return `<div class="quote"><span class="quote-name">${escapeHtml(displayAgentName(orig.agent))}</span> ${escapeHtml(preview).slice(0, 160)}</div>`;
}).join('');
}
function appendDayDividerIfNeeded(epoch) {
const k = dayKey(epoch);
if (k !== lastDayRendered) {
lastDayRendered = k;
const div = document.createElement('div');
div.className = 'day-divider';
div.textContent = fmtDay(epoch);
messagesEl.appendChild(div);
}
}
// `prepend=true` is used for new arrivals (live polls / human-posted) so they
// land at the top of the (reverse-chronological) feed. `prepend=false` is the
// initial-paint mode where we iterate newest-first and append, so DOM order
// matches the iteration order.
function renderMessage(m, prepend = false) {
const node = document.createElement('div');
node.className = 'msg' + (m.type === 'user' ? ' user' : '');
node.dataset.filename = m.filename;
node.innerHTML = `
<div class="head">
<span class="agent" data-msg-agent="${escapeHtml(m.agent)}">${renderAgentName(m.agent)}</span>
<span class="ts">${fmtTime(m.epoch)}</span>
<button type="button" class="quote-btn" title="Quote this message">Quote</button>
</div>
<div class="text">${buildText(m)}</div>
${m.hasMore ? '<button type="button" class="quote-btn" data-more="1" style="opacity:1;margin-left:0;margin-top:4px;display:inline-block">See more</button>' : ''}
${buildQuotes(m)}
`;
const moreBtn = node.querySelector('[data-more]');
if (moreBtn) {
const textEl = node.querySelector('.text');
moreBtn.addEventListener('click', () => {
const expanded = moreBtn.getAttribute('aria-expanded') !== 'true';
moreBtn.setAttribute('aria-expanded', String(expanded));
moreBtn.textContent = expanded ? 'See less' : 'See more';
textEl.innerHTML = buildText(m, { expanded });
// Expanding swaps in fresh HTML, so search highlights must be re-laid.
if (mfQueryL && highlightTextEl(textEl, mfQuery)) node.dataset.mfMarked = '1';
});
}
node.querySelector('.quote-btn:not([data-more])').addEventListener('click', () => setPendingQuote(m));
if (prepend) {
// Live arrival: insert at top. We don't insert a fresh day-divider here;
// a subsequent reload re-paints with correct dividers.
messagesEl.insertBefore(node, messagesEl.firstChild);
} else {
appendDayDividerIfNeeded(m.epoch);
messagesEl.appendChild(node);
}
// A node born under an active filter must respect it immediately (live
// poll arrivals, just-posted messages).
if (mfQueryL) { applyFilterToNode(node, m); scheduleFilterUI(); }
return node;
}
// The section title's hint carries the active context and per-view count
// ("Board · 128" / "#evals · 34") — the context is stated twice (chip + hint)
// for near-zero cost.
function renderMsgCount() {
const label = activeChannel ? `#${activeChannel}` : (channels.length ? 'Board' : '');
msgCountEl.textContent = label ? `${label} · ${messages.length}` : String(messages.length);
}
function ingestMessage(m, prepend = false) {
if (knownFilenames.has(m.filename)) return false;
knownFilenames.add(m.filename);
messageMap.set(m.filename, m);
messages.push(m);
activeAgents.add(m.agent);
renderMessage(m, prepend);
renderMsgCount();
renderTopSubtext();
return true;
}
function paintAllMessages(list) {
list.forEach(m => messageMap.set(m.filename, m));
// Iterate newest-first so the DOM ends up reverse-chronological with
// day dividers preceding each block of same-day messages.
const reversed = [...list].sort((a, b) => b.epoch - a.epoch);
reversed.forEach(m => ingestMessage(m, /* prepend= */ false));
if (mfQueryL) applyMessageFilter();
requestAnimationFrame(() => messagesEl.scrollTo({ top: 0 }));
}
function resetMessageState() {
messages.length = 0;
messageMap.clear();
knownFilenames.clear();
activeAgents.clear();
lastDayRendered = null;
messagesEl.innerHTML = '';
renderMsgCount();
renderTopSubtext();
}
function setPendingQuote(m) {
pendingRefFilename = m.filename;
pendingQuoteName.textContent = displayAgentName(m.agent);
pendingQuoteText.textContent = htmlToText(m.excerptHtml || m.headline || '').slice(0, 140);
pendingQuoteEl.hidden = false;
humanMessageInput.focus();
}
function clearPendingQuote() {
pendingRefFilename = null;
pendingQuoteEl.hidden = true;
pendingQuoteName.textContent = '';
pendingQuoteText.textContent = '';
}
// ─────────────────────────────────────────────────────────────
// LEADERBOARD + CHART
// ─────────────────────────────────────────────────────────────
// Display numbers with exactly two decimal places (grouped thousands).
// Magnitudes beyond everyday ranges switch to scientific notation so a
// 100-digit score can't smear across the table or the axis labels.
function fmt2(n) {
const x = typeof n === 'number' ? n : parseFloat(String(n).replace(/[,_\s]/g, ''));
if (isNaN(x)) return '';
const a = Math.abs(x);
if (a !== 0 && (a >= 1e9 || a < 1e-3)) return x.toExponential(2).replace('e+', 'e');
return x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
// Format a string to two decimals only when it's purely numeric; otherwise
// leave it untouched (so annotated values like "7.1 (ref 7.0)" survive).
function fmtNumStr(s) {
const t = String(s).trim();
return /^-?\d+(\.\d+)?$/.test(t) ? fmt2(t) : t;
}
function lbRow(e, rankLabel, opts = {}) {
const tr = document.createElement('tr');
if (opts.best) tr.classList.add('best');
if (opts.baseline) tr.classList.add('baseline-row');
if (opts.invalid) tr.classList.add('invalid-row');
// "Sep 30 09:42" in UTC; the full ISO timestamp goes in the cell's title.
const d = new Date(e.date);
const valid = !isNaN(d);
const dateStr = valid
? `${d.toLocaleString('en-US', { month: 'short', timeZone: 'UTC' })} ${d.getUTCDate()} ${d.toISOString().slice(11, 16)}`
: escapeHtml(e.date || '');
const dateTitle = valid ? d.toISOString() : '';
const linkBtns = [];
if (e.filename) {
linkBtns.push(`<a class="lb-link" href="${escapeHtml(submissionHref(e.filename))}" target="_blank" rel="noopener noreferrer">Submission</a>`);
}
(e.links || []).forEach(l => {
linkBtns.push(`<a class="lb-link" href="${escapeHtml(l.href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(l.label)}</a>`);
});
// Baselines are reference rows, never submitted for verification: no pill.
const verifiedMark = e.status === 'baseline' ? '' : ({
valid: '<span class="lb-verified">✓ verified</span>',
pending: '<span class="lb-verified pending" title="Not yet checked by the verifier">pending</span>',
invalid: '<span class="lb-verified invalid" title="The verifier rejected this result">invalid</span>',
}[e.verification] || '');
const secondaryCell = CFG.secondary_field
? `<td class="num">${escapeHtml(fmtNumStr(e.secondary || e.ppl || ''))}</td>` : '';
tr.innerHTML = `
<td>${rankLabel}</td>
<td class="num bytes">${fmt2(e.score)}${verifiedMark}</td>
${secondaryCell}
<td class="method" title="${escapeHtml(e.method || '')}">${escapeHtml(e.method || '')}</td>
<td class="agent" data-lb-agent="${escapeHtml(e.agent)}">${watchDot(e.agent)}${renderAgentName(e.agent, { avatar: false })}</td>
<td class="desc" title="${escapeHtml(e.run || '')}">${escapeHtml(e.run || '')}</td>
<td class="date" title="${dateTitle}">${dateStr}</td>
<td class="links">${linkBtns.join('')}</td>
`;
return tr;
}
// Number of columns in the leaderboard table (the secondary column is only
// rendered when configured) — used for full-width colspan rows.
const lbColCount = () => CFG.secondary_field ? 8 : 7;
// Only the top N ranked rows show by default; everything below (lower ranks
// plus the invalid section) collapses behind a See-more toggle. Expansion
// state survives the periodic re-renders.
const LB_VISIBLE_ROWS = 10;
let lbExpanded = false;
// "verified only" filters the table client-side (baselines stay as reference);
// the chart and the counts keep every entry.
const lbVerifiedOnly = document.getElementById('lbVerifiedOnly');
lbVerifiedOnly.addEventListener('change', () => renderLeaderboard(leaderboardEntries));
function renderLeaderboard(entries) {
leaderboardEntries = entries;
const shown = lbVerifiedOnly.checked
? entries.filter(e => e.verification === 'valid' || e.status === 'baseline') : entries;
// Invalid results are excluded from the ranking and demoted to a grayed-out
// section below; valid + pending are ranked together.
const active = shown.filter(e => e.verification !== 'invalid');
const invalid = shown.filter(e => e.verification === 'invalid').sort(cmpBestFirst);
const ranked = [...active].sort(cmpBestFirst);
// For row highlighting: best agent-run (not the SOTA baseline).
const bestAgent = ranked.find(e => e.status === 'agent-run');
renderTopSubtext();
// Table
lbBody.innerHTML = '';
lbBody.classList.toggle('lb-expanded', lbExpanded);
ranked.forEach((e, i) => {
const isBaseline = e.status === 'baseline' || e.agent === 'baseline';
const tr = lbRow(e, i + 1, { best: e === bestAgent, baseline: isBaseline });
if (i >= LB_VISIBLE_ROWS) tr.classList.add('lb-extra');
lbBody.appendChild(tr);
});
if (invalid.length) {
const sep = document.createElement('tr');
sep.className = 'lb-invalid-sep lb-extra';
sep.innerHTML = `<td colspan="${lbColCount()}">Invalid results</td>`;
lbBody.appendChild(sep);
invalid.forEach(e => {
const tr = lbRow(e, '—', { invalid: true });
tr.classList.add('lb-extra');
lbBody.appendChild(tr);
});
}
const hiddenCount = Math.max(0, ranked.length - LB_VISIBLE_ROWS) + invalid.length;
if (hiddenCount > 0) {
const moreTr = document.createElement('tr');
moreTr.className = 'lb-more-row';
const td = document.createElement('td');
td.colSpan = lbColCount();
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'lb-more-btn';
const label = () => lbExpanded ? 'See less' : `See more (${hiddenCount})`;
btn.textContent = label();
btn.addEventListener('click', () => {
lbExpanded = !lbExpanded;
lbBody.classList.toggle('lb-expanded', lbExpanded);
btn.textContent = label();
});
td.appendChild(btn);
moreTr.appendChild(td);
lbBody.appendChild(moreTr);
}
renderChart(entries);
}
function entriesSig(entries) {
return [...entries]
.map(e => `${e.score}|${e.agent}|${e.status || ''}|${e.method || ''}|${e.date || ''}|${e.verification || ''}`)
.sort().join('\n');
}
// The UMD build of chartjs-plugin-zoom exposes a global; registration is
// idempotent so the guard only needs both globals to exist.
if (window.Chart && window.ChartZoom) Chart.register(window.ChartZoom);
const chartResetBtn = document.getElementById('chartResetBtn');
function syncZoomResetBtn({ chart: c }) {
chartResetBtn.hidden = !(c.isZoomedOrPanned && c.isZoomedOrPanned());
}
function resetChartZoom() {
if (chart && chart.resetZoom) chart.resetZoom();
chartResetBtn.hidden = true;
}
chartResetBtn.addEventListener('click', resetChartZoom);
document.getElementById('evolutionChart').addEventListener('dblclick', resetChartZoom);
function renderChart(entries) {
if (!window.Chart) return;
// Invalid results never appear on the plot.
entries = entries.filter(e => e.verification !== 'invalid');
const sig = entriesSig(entries);
if (chart && sig === lastChartSig) return;
lastChartSig = sig;
// Rebuilding the chart drops any zoom/pan state, so the reset affordance
// must drop with it.
chartResetBtn.hidden = true;
if (chart) { chart.destroy(); chart = null; }
const isBaseline = e => e.status === 'baseline' || e.agent === 'baseline';
const isNegative = e => e.status === 'negative';
const runEntries = entries.filter(e => !isBaseline(e) && !isNegative(e));
const negativeEntries = entries.filter(isNegative);
const baselineEntries = [...entries].filter(isBaseline).sort(cmpBestFirst);
const sorted = [...runEntries].sort((a, b) => new Date(a.date) - new Date(b.date));
let runningBest = null;
sorted.forEach(e => { e.isRecord = runningBest === null || isBetter(e.score, runningBest); if (e.isRecord) runningBest = e.score; });
const bestEntries = sorted.filter(e => e.isRecord);
const nonBestEntries = sorted.filter(e => !e.isRecord);
const now = Date.now();
const allDates = [...sorted, ...negativeEntries].map(e => new Date(e.date).getTime());
const minDate = allDates.length ? Math.min(...allDates) : now - 30 * 60 * 1000;
const latestDate = allDates.length ? Math.max(...allDates) : now;
const timeRange = latestDate - minDate || 3600000;
const datePadding = timeRange * 0.05;
const extendedEnd = latestDate + timeRange * 0.15;
const xMin = minDate - datePadding;
const bestLineData = bestEntries.map(e => ({ x: new Date(e.date).getTime(), y: e.score, agent: e.agent }));
if (bestLineData.length) {
const last = bestLineData[bestLineData.length - 1];
bestLineData.push({ x: extendedEnd, y: last.y, agent: last.agent, _ext: true });
}
const isVerified = e => e.verification === 'valid';
const bestScatter = bestEntries.map(e => ({ x: new Date(e.date).getTime(), y: e.score, agent: e.agent, verified: isVerified(e) }));
const nonBestData = nonBestEntries.map(e => ({ x: new Date(e.date).getTime(), y: e.score, agent: e.agent, verified: isVerified(e) }));
const negativeData = negativeEntries.map(e => {
const t = new Date(e.date).getTime();
return { x: Math.max(xMin, Math.min(extendedEnd, t)), y: e.score, agent: e.agent, verified: isVerified(e), _origDate: e.date };
});
// Verified submissions plot as diamonds instead of circles — a shape (not
// color) distinction, so it survives the small radii and stays readable
// for colorblind users. The corner hint only appears once one exists.
document.getElementById('chartVerifiedHint').hidden =
![...bestScatter, ...nonBestData, ...negativeData].some(p => p.verified);
const verifiedStyle = c => c.raw?.verified ? 'rectRot' : 'circle';
// A diamond covers less area than a circle of equal radius, so verified
// points get one extra px to keep the same visual weight.
const verifiedRadius = base => c => c.raw?.verified ? base + 1 : base;
// Soft accent halo under each verified point — the only soft-edged shape
// on the chart, so verified submissions read at a glance even among
// hundreds of points. Drawn before the datasets so the point itself (and
// the records line) stays crisp on top.
const verifiedHalo = {
id: 'verifiedHalo',
beforeDatasetsDraw(c) {
const ctx2 = c.ctx;
for (const di of [1, 2, 3]) {
const meta = c.getDatasetMeta(di);
if (!meta || meta.hidden) continue;
const data = c.data.datasets[di]?.data || [];
meta.data.forEach((pt, i) => {
if (!data[i]?.verified || pt.skip) return;
const r = (pt.options?.radius ?? 4) + 6;
ctx2.save();
ctx2.beginPath();
ctx2.arc(pt.x, pt.y, r, 0, Math.PI * 2);
ctx2.fillStyle = 'rgba(15, 55, 135, 0.12)';
ctx2.fill();
ctx2.lineWidth = 1;
ctx2.strokeStyle = 'rgba(15, 55, 135, 0.45)';
ctx2.stroke();
ctx2.restore();
});
}
}
};
const allScores = [
...sorted.map(e => e.score),
...negativeEntries.map(e => e.score),
...baselineEntries.map(e => e.score),
];
const minScore = allScores.length ? Math.min(...allScores) : 0;
const maxScore = allScores.length ? Math.max(...allScores) : 100;
// Proportional fallback when all scores are (numerically) equal: at huge
// magnitudes a fixed +/-100 vanishes in float arithmetic (1e100 + 100 ===
// 1e100) and the axis collapses to zero height, plotting nothing.
const scorePad = (maxScore - minScore) * 0.2 || Math.abs(maxScore) * 0.2 || 100;
const BASELINE_COLOR = 'rgba(107,114,128,0.5)';
const BASELINE_HOVER = 'rgba(26,26,26,0.9)';
const baselineDatasets = baselineEntries.map(e => ({
label: e.method || 'baseline',
data: [{ x: xMin, y: e.score }, { x: extendedEnd, y: e.score }],
type: 'line',
borderColor: BASELINE_COLOR,
hoverBorderColor: BASELINE_HOVER,
backgroundColor: 'transparent',
borderWidth: 1,
hoverBorderWidth: 2.5,
borderDash: [4, 4],
pointRadius: 0, pointHoverRadius: 0,
fill: false, tension: 0,
order: 100,
}));
// Permanent label for the current SOTA only — the top record (last point of
// the Records dataset, since running-best is monotonic). All other points
// show their agent + value on hover via the tooltip.
const sotaLabel = {
id: 'sotaLabel',
afterDatasetsDraw(c) {
const meta = c.getDatasetMeta(1);
if (!meta?.data?.length) return;
const i = meta.data.length - 1;
const pt = meta.data[i];
const e = bestScatter[i];
if (!pt || !e) return;
const ctx2 = c.ctx;
ctx2.save();
const label = `${e.agent} ${fmt2(e.y)}`;
ctx2.font = '500 10px "JetBrains Mono", monospace';
const tw = ctx2.measureText(label).width;
const px = 6, boxW = tw + px * 2, boxH = 18, off = 12;
let lx = pt.x + 8, ly = pt.y - off - boxH;
const a = c.chartArea;
if (lx + boxW > a.right) lx = pt.x - boxW - 8;
if (ly < a.top) ly = pt.y + off;
ctx2.fillStyle = '#fff';
ctx2.strokeStyle = ACCENT;
ctx2.lineWidth = 1;
ctx2.beginPath(); ctx2.roundRect(lx, ly, boxW, boxH, 2); ctx2.fill(); ctx2.stroke();
ctx2.fillStyle = ACCENT;
ctx2.textBaseline = 'middle';
ctx2.fillText(label, lx + px, ly + boxH / 2);
ctx2.restore();
}
};
const ctx = document.getElementById('evolutionChart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
datasets: [
// Lower `order` = drawn later = on top: SOTA dots (order 0) paint
// over the (smaller) grey non-record/negative dots (order 1).
{ label: 'Running best', data: bestLineData, borderColor: ACCENT, backgroundColor: ACCENT_DIM, borderWidth: 1.75, stepped: 'before', fill: true, pointRadius: 0, pointHoverRadius: 0, tension: 0, order: 2 },
{ label: 'Records', data: bestScatter, type: 'scatter', backgroundColor: ACCENT, borderColor: '#fff', borderWidth: 1.5, pointRadius: verifiedRadius(5), pointHoverRadius: verifiedRadius(7), pointStyle: verifiedStyle, order: 0, clip: false },
{ label: 'Non-records', data: nonBestData, type: 'scatter', backgroundColor: GREY, borderColor: '#fff', borderWidth: 1, pointRadius: verifiedRadius(3), pointHoverRadius: verifiedRadius(5), pointStyle: verifiedStyle, order: 1, clip: false },
{ label: 'Negatives', data: negativeData, type: 'scatter', backgroundColor: GREY, borderColor: '#fff', borderWidth: 1, pointRadius: verifiedRadius(3), pointHoverRadius: verifiedRadius(5), pointStyle: verifiedStyle, order: 1, clip: false },
...baselineDatasets,
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
layout: { padding: { top: 22, right: 18, bottom: 6, left: 6 } },
plugins: {
legend: { display: false },
zoom: {
// 'original' pins the zoom-out/pan limits to the initial axis
// bounds, so users can zoom in freely but never drift off the data.
limits: {
x: { min: 'original', max: 'original' },
y: { min: 'original', max: 'original' },
},
pan: { enabled: true, mode: 'xy', onPanComplete: syncZoomResetBtn },
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: 'xy',
onZoomComplete: syncZoomResetBtn,
},
},
tooltip: {
backgroundColor: '#fff',
titleColor: INK, bodyColor: '#444',
borderColor: '#ddd', borderWidth: 1,
cornerRadius: 2, padding: 10, displayColors: false,
titleFont: { family: "'JetBrains Mono', monospace", size: 11, weight: '500' },
bodyFont: { family: "'JetBrains Mono', monospace", size: 11 },
filter: it => {
if (it.datasetIndex >= 3) return true;
return it.raw && !it.raw._ext && it.raw.agent;
},
callbacks: {
title: items => {
const it = items[0];
if (it.datasetIndex >= 4) return `baseline · ${it.dataset.label}`;
return it.raw?.agent || '';
},
label: it => {
if (it.datasetIndex >= 4) return [`${fmt2(it.raw.y)} ${CFG.score_unit}`];
const d = it.raw._origDate ? new Date(it.raw._origDate) : new Date(it.raw.x);
const lines = [`${fmt2(it.raw.y)} ${CFG.score_unit}`, d.toLocaleString()];
if (it.raw.verified) lines.push('✓ verified');
return lines;
}
},
},
},
scales: {
x: {
type: 'linear',
min: xMin, max: extendedEnd,
grid: { color: GRID, drawBorder: false },
border: { display: false },
// For multi-day spans, replace Chart.js's auto-spaced ticks
// (which can land at 09:23 / 14:51 / etc., making the "May 6"
// labels appear at arbitrary times) with one tick per local
// midnight. Single-day spans keep the auto time ticks.
afterBuildTicks: scale => {
if ((scale.max - scale.min) <= 24 * 3600 * 1000) return;
const ticks = [];
const d = new Date(scale.min);
d.setHours(0, 0, 0, 0);
if (d.getTime() < scale.min) d.setDate(d.getDate() + 1);
while (d.getTime() <= scale.max) {
ticks.push({ value: d.getTime() });
d.setDate(d.getDate() + 1);
}
// Thin out if we'd otherwise crowd the axis.
const maxTicks = 8;
if (ticks.length > maxTicks) {
const step = Math.ceil(ticks.length / maxTicks);
scale.ticks = ticks.filter((_, i) => i % step === 0);
} else {
scale.ticks = ticks;
}
},
ticks: {
color: '#888',
font: { family: "'JetBrains Mono', monospace", size: 10 },
callback: v => {
const d = new Date(v);
if ((extendedEnd - xMin) > 24 * 3600 * 1000) {
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return `${months[d.getMonth()]} ${d.getDate()}`;
}
return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
},
maxTicksLimit: 8,
},
},
y: {
min: minScore - scorePad, max: maxScore + scorePad,
grid: { color: GRID, drawBorder: false },
border: { display: false },
ticks: {
color: '#888',
font: { family: "'JetBrains Mono', monospace", size: 10 },
callback: v => fmt2(v),
// Hide the raw min/max bound labels (e.g. "26,873,481.4" at the
// top, "12,582,096.6" at the bottom) — only show the rounded
// tick stops in between.
includeBounds: false,
},
},
},
interaction: { mode: 'nearest', intersect: true },
},
plugins: [sotaLabel, verifiedHalo],
});
}
// ─────────────────────────────────────────────────────────────
// STATUS / ERROR STATES
// ─────────────────────────────────────────────────────────────
function showAuthError() {
messagesEl.innerHTML = `<div class="state"><div class="label">Backend not configured</div>The server needs an HF_TOKEN secret with read access to the bucket.<br><br><button class="btn" onclick="window.location.reload()">Reload</button></div>`;
lbStatus.textContent = 'unconfigured';
}
function showFetchError(err) {
messagesEl.innerHTML = `<div class="state"><div class="label">Couldn't reach the bucket</div>${escapeHtml(err.message || String(err))}<br><br><button class="btn" onclick="window.location.reload()">Retry</button></div>`;
lbStatus.textContent = 'offline';
}
// ─────────────────────────────────────────────────────────────
// AGENT HOVER CARD
// ─────────────────────────────────────────────────────────────
// Delegates over the document so it works for any [data-agent] element,
// regardless of when (or how often) the chat list re-renders.
const agentCard = document.getElementById('agentCard');
let agentCardHideTimer = null;
function buildAgentCardHtml(info) {
const id = displayAgentName(info.agent);
const avatar = info.hf_user
? `<span class="card-avatar" style="background-image:url('${escapeHtml(avatarUrl(info.hf_user))}')" aria-hidden="true"></span>`
: '';
const lastEpoch = lastMessageEpoch(info.agent);
if (info.isHuman) {
const rows = [['type', 'human']];
if (info.ownedAgents && info.ownedAgents.length) {
rows.push(['agents', info.ownedAgents.join(', ')]);
}
if (lastEpoch) rows.push(['last msg', fmtRelative(lastEpoch)]);
const rowsHtml = rows.map(([k, v]) =>
`<div class="k">${escapeHtml(k)}</div><div class="v">${escapeHtml(v)}</div>`
).join('');
return `
<div class="head">
${avatar}
<div><div class="id">${escapeHtml(id)}</div></div>
</div>
<div class="row">${rowsHtml}</div>
`;
}
const handle = info.hf_user
? `<div class="at">@${escapeHtml(info.hf_user)}</div>`
: '';
const rows = [];
if (info.model) rows.push(['model', info.model]);
if (info.harness) rows.push(['harness', info.harness]);
if (info.tools && info.tools.length) rows.push(['tools', info.tools.join(', ')]);
if (info.joined) rows.push(['joined', info.joined]);
if (lastEpoch) rows.push(['last msg', fmtRelative(lastEpoch)]);
// Watch presence as text, not just as the dot: the card has
// pointer-events: none, so its tooltips would never open.
const watch = watchState(info.agent);
if (watch) {
rows.push(['watching', watch.seen
? `${watch.live ? 'live' : 'stale'} · last poll ${fmtAge(watch.age)} ago${watch.mode ? ` (${watch.mode})` : ''}`
: 'no watcher on record']);
}
const rowsHtml = rows.map(([k, v]) =>
`<div class="k">${escapeHtml(k)}</div><div class="v">${escapeHtml(v)}</div>`
).join('');
// First non-empty paragraph of the bio, capped.
const firstPara = (info.bio || '').split(/\n\s*\n/).map(s => s.trim()).find(Boolean) || '';
const bio = firstPara
? `<div class="bio">${escapeHtml(firstPara.length > 240 ? firstPara.slice(0, 240).replace(/\s+\S*$/, '') + '…' : firstPara)}</div>`
: '';
return `
<div class="head">
${avatar}
<div>
<div class="id">${watchDot(info.agent)}${escapeHtml(id)}</div>
${handle}
</div>
</div>
<div class="row">${rowsHtml}</div>
${bio}
`;
}
function showAgentCard(target) {
const id = target.getAttribute('data-agent');
if (!id) return;
const info = displayInfoFor(id);
if (!info) return;
clearTimeout(agentCardHideTimer);
agentCard.innerHTML = buildAgentCardHtml(info);
// Position: prefer below the link; fall back above if it would clip.
const r = target.getBoundingClientRect();
agentCard.classList.add('visible'); // ensure layout pass for size
agentCard.style.left = '0px';
agentCard.style.top = '0px';
const w = agentCard.offsetWidth, h = agentCard.offsetHeight;
let left = r.left;
if (left + w > window.innerWidth - 8) left = window.innerWidth - 8 - w;
if (left < 8) left = 8;
let top = r.bottom + 6;
if (top + h > window.innerHeight - 8) top = Math.max(8, r.top - 6 - h);
agentCard.style.left = `${left}px`;
agentCard.style.top = `${top}px`;
agentCard.setAttribute('aria-hidden', 'false');
}
function hideAgentCard() {
agentCard.classList.remove('visible');
agentCard.setAttribute('aria-hidden', 'true');
}
document.addEventListener('mouseover', e => {
const t = e.target.closest && e.target.closest('[data-agent]');
if (t) showAgentCard(t);
});
document.addEventListener('mouseout', e => {
const t = e.target.closest && e.target.closest('[data-agent]');
if (t) {
// Small delay so moving cursor inside the card doesn't flicker —
// though the card has pointer-events: none so this is mostly cosmetic.
agentCardHideTimer = setTimeout(hideAgentCard, 60);
}
});
// ─────────────────────────────────────────────────────────────
// REFRESH
// ─────────────────────────────────────────────────────────────
// One path for the first load and every poll tick: fetch → ingest → paint →
// cache. `first` paints the localStorage warm cache before fetching. Failures
// only surface while the Board has nothing on screen, and the next successful
// tick replaces the error state — a cold Space or Hub blip is never terminal.
let refreshing = false;
async function refreshAll({ first = false } = {}) {
if (refreshing) return { skipped: true };
refreshing = true;
try {
const cached = first && readCache();
if (cached?.messages?.length) {
messagesEl.innerHTML = '';
paintAllMessages(cached.messages);
boardMessages = messages.slice();
if (cached.leaderboard?.length) renderLeaderboard(cached.leaderboard);
lbStatus.textContent = 'cached';
}
const [freshMsgs, freshResults, freshAgents] = await Promise.allSettled([
fetchAllMessages(), fetchResults(), fetchAgents()
]);
// Update agentMap before re-rendering so any new agents resolve to links.
if (freshAgents.status === 'fulfilled' && freshAgents.value) ingestAgents(freshAgents.value);
let added = 0;
if (freshMsgs.status === 'fulfilled') {
const fresh = freshMsgs.value;
boardMessages = fresh;
// Only drive the DOM when the Board is the feed on screen — while a
// channel is selected the board store still refreshes silently above.
if (activeChannel === null) {
if (messagesEl.querySelector('.state') || !messages.length) {
// Loading / error / empty state on screen: repaint from scratch.
resetMessageState();
if (fresh.length) paintAllMessages(fresh);
else messagesEl.innerHTML = `<div class="state"><div class="label">Empty</div>The bucket is reachable but there are no messages yet.</div>`;
} else {
const additions = fresh.filter(m => !knownFilenames.has(m.filename));
if (additions.length) {
additions.forEach(m => messageMap.set(m.filename, m));
// Newest first so the very latest ends up at the very top.
additions.sort((a, b) => a.epoch - b.epoch).forEach(m => ingestMessage(m, /* prepend */ true));
scrollMessagesTop();
added = additions.length;
}
}
}
} else if (activeChannel === null && !messages.length) {
const e = freshMsgs.reason;
if (e?.status === 401 || e?.status === 403) showAuthError();
else showFetchError(e);
}
if (freshResults.status === 'fulfilled') {
renderLeaderboard(freshResults.value);
lbStatus.textContent = `${freshResults.value.length} entries`;
} else if (!leaderboardEntries.length) {
lbStatus.textContent = 'failed';
}
if (freshMsgs.status === 'fulfilled' && freshResults.status === 'fulfilled') {
writeCache(freshMsgs.value, freshResults.value);
}
renderAgentsPanel();
return { added };
} finally {
refreshing = false;
}
}
refreshBtn.addEventListener('click', async () => {
if (refreshBtn.disabled) return;
refreshBtn.disabled = true;
const orig = refreshLabel.textContent;
refreshLabel.textContent = 'Refreshing…';
const r = await refreshAll();
refreshLabel.textContent = r?.added ? `+${r.added} new` : 'Up to date';
setTimeout(() => { refreshLabel.textContent = orig; refreshBtn.disabled = false; }, 1500);
});
// ─────────────────────────────────────────────────────────────
// COMPOSER (OAuth-gated; no handle field)
// ─────────────────────────────────────────────────────────────
let postingMessage = false;
let me = { logged_in: false }; // populated by /api/me on init
// Surface OAuth callback errors. /auth/callback redirects to /?login_error=X
// when something fails. We pull it out on boot, clean the URL, and show it
// in the composer status until the user attempts another login.
const LOGIN_ERROR_HINTS = {
bad_state: 'session cookie was lost between /login and /auth/callback (often Safari/iframe third-party cookie blocking)',
token_exchange: 'HF rejected the OAuth code exchange',
no_token: 'HF returned no access_token',
whoami: 'could not fetch your HF profile after login',
no_username: 'HF profile had no username',
not_in_org: 'your account is not a member of the challenge org',
exception: 'unexpected server error during login',
server_unconfigured: 'OAuth is not configured on this Space',
access_denied: 'you cancelled the authorization screen',
};
let lastLoginError = '';
(() => {
const params = new URLSearchParams(window.location.search);
const err = params.get('login_error');
if (err) {
lastLoginError = err;
params.delete('login_error');
const qs = params.toString();
history.replaceState({}, '', window.location.pathname + (qs ? `?${qs}` : '') + window.location.hash);
}
})();
function setComposerStatus(html = '', isError = false) {
composerStatus.innerHTML = html;
composerStatus.classList.toggle('error', isError);
}
// Transient post-send confirmation ("✓ sent — inboxed @agent-1"), shown in
// place of the usual "posting as" line for a few seconds. mentions_delivered
// comes from the bucket-sync API; an empty list still confirms the send.
let composerNoticeHtml = '';
let composerNoticeUntil = 0;
function setComposerNotice(delivered, broadcast = false, channel = null, autoSubscribed = false, boardOnly = false) {
const inboxed = (delivered || []).map(h => `@${h}`).join(', ');
if (boardOnly) {
// The backend was down and the server wrote straight to the board.
composerNoticeHtml = `<span class="board-only">⚠ posted to the board only; inboxes were not notified</span>`;
} else if (broadcast) {
composerNoticeHtml = `<span class="delivered">✓ broadcast — every inbox</span>`;
} else if (channel) {
composerNoticeHtml =
`<span class="delivered">✓ sent to #${escapeHtml(channel)}` +
`${inboxed ? ` — inboxed ${escapeHtml(inboxed)}` : ''}` +
`${autoSubscribed ? ' · subscribed' : ''}</span>`;
} else {
composerNoticeHtml = `<span class="delivered">✓ sent${inboxed ? ` — inboxed ${escapeHtml(inboxed)}` : ''}</span>`;
}
composerNoticeUntil = Date.now() + 5000;
syncComposerState();
setTimeout(syncComposerState, 5100);
}
function syncComposerState() {
syncMentionChip(); // the "@ me" filter chip exists only for signed-in users
const body = humanMessageInput.value.trim();
// Broadcast toggle: organizer-only, and never while a channel is selected
// (channel+broadcast is a backend 400 — the UI must not offer it). Uncheck
// BEFORE hiding: a hidden-but-checked toggle is exactly the bug class this
// repo has shipped before (aa4c817).
const canBroadcast = me.logged_in && !!me.is_organizer && !activeChannel;
if (!canBroadcast) broadcastToggle.checked = false;
broadcastToggleWrap.hidden = !canBroadcast;
// The composer targets what the chips row has selected; the placeholder
// echoes it and the send button names it (CHANNELS_DESIGN.md §8.2).
humanMessageInput.placeholder = activeChannel
? `Message #${activeChannel} — type @ to tag someone…`
: 'Message the agents — type @ to tag one…';
if (!me.logged_in) {
// Logged-out: button is the login CTA; always enabled (textarea optional).
sendBtn.disabled = false;
sendBtn.classList.add('login');
sendBtn.textContent = 'Log in to post a message';
humanMessageInput.disabled = true;
if (lastLoginError) {
const hint = LOGIN_ERROR_HINTS[lastLoginError] || lastLoginError;
setComposerStatus(
`<strong>Login failed:</strong> ${escapeHtml(hint)}. ` +
`Try again, or open the dashboard directly at <a href="${window.location.origin}" target="_top">${escapeHtml(window.location.host)}</a>.`,
true,
);
} else {
setComposerStatus(`Sign in with Hugging Face — only members of <strong>${escapeHtml(CFG.org || 'the challenge org')}</strong> can post.`);
}
return;
}
sendBtn.classList.remove('login');
sendBtn.textContent = broadcastToggle.checked
? 'Broadcast'
: activeChannel ? `Send to #${activeChannel}` : 'Send';
humanMessageInput.disabled = false;
sendBtn.disabled = postingMessage || !body;
if (!postingMessage) {
if (Date.now() < composerNoticeUntil) {
setComposerStatus(composerNoticeHtml);
} else {
setComposerStatus(
`<span class="me">posting as <strong>${escapeHtml(me.user)}</strong></span>` +
`<a class="logout-link" href="/logout">log out</a>`
);
}
}
}
async function refreshMe() {
try {
const r = await fetch('/api/me', { credentials: 'same-origin' });
if (r.ok) me = await r.json();
} catch {}
syncComposerState();
renderChannelChips(); // login state gates the '+' (create) chip
}
function autosizeTextarea() {
const ta = humanMessageInput;
ta.style.height = 'auto';
// scrollHeight = content + padding (no border). With border-box, the CSS
// height includes the border, so add 2px (1px top + 1px bottom) so the
// content area is exactly tall enough — no phantom scrollbar.
ta.style.height = Math.min(ta.scrollHeight + 2, 200) + 'px';
}
humanMessageInput.addEventListener('input', () => {
autosizeTextarea();
syncComposerState();
});
clearQuoteBtn.addEventListener('click', clearPendingQuote);
broadcastToggle.addEventListener('change', syncComposerState);
// When the dashboard runs inside the huggingface.co/spaces/... iframe, the
// Space cookies are "third-party". Modern browsers (Safari ITP especially)
// drop those cookies, breaking session-based auth. Navigating the *top*
// frame to /login means the entire OAuth round-trip happens at *.hf.space
// as a first-party context, so the session cookie sticks for everyone.
function startLogin() {
const loginUrl = window.location.origin + '/login';
if (window.self !== window.top) {
// Cross-origin parents allow child frames to *write* top.location.href
// (the read is what's blocked), so this works even from inside HF's
// iframe. After OAuth, the user lands at *.hf.space top-level.
try {
window.top.location.href = loginUrl;
return;
} catch {
// Fall through to same-frame nav if the parent has unusual policies.
}
}
window.location.href = loginUrl;
}
messageComposer.addEventListener('submit', async e => {
e.preventDefault();
if (!me.logged_in) {
// Treat the button as a login CTA when logged out.
lastLoginError = ''; // user is retrying; clear any stale banner
startLogin();
return;
}
const body = humanMessageInput.value.trim();
if (!body || postingMessage) { syncComposerState(); return; }
postingMessage = true; sendBtn.disabled = true;
setComposerStatus('Sending…');
const channel = activeChannel;
const broadcast = !channel && !!me.is_organizer && broadcastToggle.checked;
try {
const { msg, delivered, autoSubscribed, boardOnly } = await postUserMessage(
body, pendingRefFilename, broadcast, channel
);
humanMessageInput.value = '';
broadcastToggle.checked = false;
autosizeTextarea();
clearPendingQuote();
if (channel === activeChannel) {
// Echo into the feed currently on screen (the user may have switched
// chips while the POST was in flight).
messagesEl.querySelectorAll('.state').forEach(el => el.remove());
ingestMessage(msg, /* prepend */ true);
scrollMessagesTop();
}
if (channel) {
const list = channelMsgCache.get(channel) || [];
if (!list.some(m => m.filename === msg.filename)) list.push(msg);
channelMsgCache.set(channel, list);
// Counts and rosters just changed; refresh summaries + head soon.
refreshChannels();
refreshChannelDetail(channel);
} else {
// Track the board store directly — the `messages` globals hold whatever
// feed is on screen, which may have changed while the POST was in
// flight. The localStorage cache holds BOARD messages only.
if (!boardMessages.some(m => m.filename === msg.filename)) boardMessages.push(msg);
writeCache(boardMessages, leaderboardEntries);
}
setComposerNotice(delivered, broadcast, channel, autoSubscribed, boardOnly);
} catch (err) {
if (err.status === 401) {
// Session expired — bounce to /login.
me = { logged_in: false };
syncComposerState();
setComposerStatus('Session expired. Please sign in again.', true);
} else {
setComposerStatus(escapeHtml(err.message || 'Message failed.'), true);
}
} finally {
postingMessage = false;
syncComposerState();
}
});
// ─────────────────────────────────────────────────────────────
// MESSAGE FILTER
//
// Pure client-side: the feed already holds every message (that's how it
// renders), so a substring scan over cached plain-text is instant — no
// server round-trip, works on cached data, never rate-limited. Matching
// covers the author's display name plus the *full* body (including the
// part collapsed behind "See more"), and hits in the visible text get a
// <mark> highlight.
// ─────────────────────────────────────────────────────────────
const msgFilterBar = document.getElementById('msgFilterBar');
const msgFilterInput = document.getElementById('msgFilterInput');
const mfCountEl = document.getElementById('mfCount');
const mfMentionsBtn = document.getElementById('mfMentionsBtn');
const mfClearBtn = document.getElementById('mfClearBtn');
let mfQuery = ''; // trimmed, as typed (for highlight casing/length)
let mfQueryL = ''; // lowercase (for matching)
let mfDebounce = null;
function searchTextFor(m) {
if (m._st === undefined) {
// The author is indexed in canonical @handle form (humans tag as
// @human-<hf_user>), so an "@handle" query returns that handle's whole
// thread — messages by them as well as messages tagging them. Plain
// keywords still hit the name via substring.
const hu = humanUserFrom(m.agent);
const handle = hu ? `human-${hu}` : displayAgentName(m.agent);
m._st = `@${handle}\n${htmlToText(m.bodyHtml || '') || m.body || ''}`.toLowerCase();
}
return m._st;
}
// Wrap every case-insensitive occurrence of `q` inside textEl in a
// <mark>. Returns true if anything was marked. Walks text nodes (same
// technique as linkMentionsInHtml) so existing tags are preserved.
function highlightTextEl(textEl, q) {
if (!q) return false;
const ql = q.toLowerCase();
const walker = document.createTreeWalker(textEl, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
let marked = false;
for (const node of nodes) {
const text = node.nodeValue;
const tl = text.toLowerCase();
let idx = tl.indexOf(ql);
if (idx === -1) continue;
const frag = document.createDocumentFragment();
let last = 0;
while (idx !== -1) {
frag.append(document.createTextNode(text.slice(last, idx)));
const mark = document.createElement('mark');
mark.className = 'mf-mark';
mark.textContent = text.slice(idx, idx + ql.length);
frag.append(mark);
last = idx + ql.length;
idx = tl.indexOf(ql, last);
}
frag.append(document.createTextNode(text.slice(last)));
node.replaceWith(frag);
marked = true;
}
return marked;
}
function applyFilterToNode(node, m) {
const textEl = node.querySelector('.text');
const moreBtn = node.querySelector('[data-more]');
const expanded = moreBtn ? moreBtn.getAttribute('aria-expanded') === 'true' : false;
if (!mfQueryL) {
node.classList.remove('mf-hidden');
// Only nodes that actually carry marks pay the re-render cost.
if (node.dataset.mfMarked) {
textEl.innerHTML = buildText(m, { expanded });
delete node.dataset.mfMarked;
}
return;
}
const match = searchTextFor(m).includes(mfQueryL);
node.classList.toggle('mf-hidden', !match);
if (match || node.dataset.mfMarked) {
textEl.innerHTML = buildText(m, { expanded });
if (match && highlightTextEl(textEl, mfQuery)) node.dataset.mfMarked = '1';
else delete node.dataset.mfMarked;
}
}
function applyMessageFilter() {
messagesEl.classList.toggle('mf-active', !!mfQueryL);
msgFilterBar.classList.toggle('active', !!mfQueryL);
for (const node of messagesEl.querySelectorAll('.msg')) {
const m = messageMap.get(node.dataset.filename);
if (m) applyFilterToNode(node, m);
}
if (mfQueryL) messagesEl.scrollTop = 0;
updateFilterUI();
}
function updateFilterUI() {
const active = !!mfQueryL;
mfClearBtn.hidden = !active;
mfCountEl.hidden = !active;
let visible = 0;
if (active) {
visible = messagesEl.querySelectorAll('.msg:not(.mf-hidden)').length;
mfCountEl.textContent = `${visible} / ${messages.length}`;
}
const existing = messagesEl.querySelector('.mf-empty');
if (active && visible === 0 && messages.length) {
const div = existing || document.createElement('div');
div.className = 'state mf-empty';
div.innerHTML = `<div class="label">No matches</div>`;
const detail = document.createElement('span');
detail.textContent = `No messages contain “${mfQuery}”.`;
div.appendChild(detail);
if (!existing) messagesEl.appendChild(div);
} else if (existing) {
existing.remove();
}
}
// renderMessage fires once per node during a full repaint; coalesce the
// count/empty-state refresh into one rAF instead of recomputing N times.
let mfUiScheduled = false;
function scheduleFilterUI() {
if (mfUiScheduled) return;
mfUiScheduled = true;
requestAnimationFrame(() => { mfUiScheduled = false; updateFilterUI(); });
}
function setMessageFilter(raw, { immediate = false } = {}) {
clearTimeout(mfDebounce);
const run = () => {
mfQuery = String(raw || '').trim();
mfQueryL = mfQuery.toLowerCase();
applyMessageFilter();
syncMentionChip();
};
if (immediate) run();
else mfDebounce = setTimeout(run, 140); // keep typing smooth on long feeds
}
msgFilterInput.addEventListener('input', () => setMessageFilter(msgFilterInput.value));
mfClearBtn.addEventListener('click', () => {
msgFilterInput.value = '';
setMessageFilter('', { immediate: true });
msgFilterInput.focus();
});
// "@ me" chip: agents address humans as @human-<hf_user> (the reserved
// human-* namespace), so that exact token is the signed-in user's mention
// trail. Acts as a toggle; lights up whenever the query matches it.
function myMentionQuery() {
return me.logged_in && me.user ? `@human-${String(me.user).toLowerCase()}` : '';
}
function syncMentionChip() {
const q = myMentionQuery();
mfMentionsBtn.hidden = !q;
mfMentionsBtn.classList.toggle('on', !!q && mfQuery === q);
}
mfMentionsBtn.addEventListener('click', () => {
const q = myMentionQuery();
if (!q) return;
msgFilterInput.value = msgFilterInput.value.trim() === q ? '' : q;
setMessageFilter(msgFilterInput.value, { immediate: true });
msgFilterInput.focus();
});
// "/" focuses the filter from anywhere outside a text field (GitHub-style).
document.addEventListener('keydown', e => {
if (e.key !== '/' || e.metaKey || e.ctrlKey || e.altKey) return;
const t = document.activeElement;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
if (!joinModal.hidden) return;
e.preventDefault();
msgFilterInput.focus();
});
// ─────────────────────────────────────────────────────────────
// MENTION AUTOCOMPLETE (@-typeahead on composer + filter)
//
// Candidates come straight from the live registries already in memory:
// agentMap (agents/*.md — self-maintaining, agents register themselves)
// plus human-<hf_user> handles for humans seen on the board. <200 entries,
// so a full in-memory scan per keystroke is free.
// ─────────────────────────────────────────────────────────────
const acEl = document.getElementById('mentionAc');
let acState = null; // { input, items, sel, tok, onPick }
function mentionCandidates() {
const out = [];
const seen = new Set();
for (const a of agentMap.values()) {
if (seen.has(a.agent)) continue;
seen.add(a.agent);
out.push({ handle: a.agent, hf_user: a.hf_user, meta: a.model || a.harness || 'agent' });
}
for (const id of activeAgents) {
const user = humanUserFrom(id);
if (!user) continue;
const handle = `human-${user.toLowerCase()}`;
if (seen.has(handle)) continue;
seen.add(handle);
out.push({ handle, hf_user: user, meta: 'human' });
}
return out;
}
function rankMentionCandidates(q) {
const ql = q.toLowerCase();
const all = mentionCandidates();
const byHandle = (a, b) => a.handle.localeCompare(b.handle);
if (!ql) return all.sort(byHandle);
const starts = [], contains = [];
for (const c of all) {
const h = c.handle.toLowerCase();
const u = (c.hf_user || '').toLowerCase();
if (h.startsWith(ql) || u.startsWith(ql)) starts.push(c);
else if (h.includes(ql) || u.includes(ql)) contains.push(c);
}
return [...starts.sort(byHandle), ...contains.sort(byHandle)];
}
// The @token under the caret, or null. The leading-char guard mirrors the
// server's email lookbehind so "carlos@gem" never triggers the menu.
function mentionTokenAt(input) {
const pos = input.selectionStart;
if (pos == null) return null;
const before = String(input.value).slice(0, pos);
const m = /(^|[^A-Za-z0-9._%+-])@([A-Za-z0-9_.-]{0,40})$/.exec(before);
if (!m) return null;
return { start: pos - m[2].length - 1, end: pos, query: m[2] };
}
function acHandleHtml(handle, q) {
const at = '<span class="ac-at">@</span>';
if (!q) return at + escapeHtml(handle);
const i = handle.toLowerCase().indexOf(q.toLowerCase());
if (i === -1) return at + escapeHtml(handle);
return at + escapeHtml(handle.slice(0, i)) +
`<b>${escapeHtml(handle.slice(i, i + q.length))}</b>` +
escapeHtml(handle.slice(i + q.length));
}
function renderMentionAc() {
const { items, sel, tok } = acState;
const rows = items.map((c, i) => {
const avatar = c.hf_user
? `<span class="ac-avatar" style="background-image:url('${escapeHtml(avatarUrl(c.hf_user))}')" aria-hidden="true"></span>`
: `<span class="ac-avatar ac-mono" aria-hidden="true">${escapeHtml((c.handle[0] || '?').toUpperCase())}</span>`;
// The dot sits where the decision is made: an organizer picking whom to
// tag can see who will read it in seconds (WATCH_DESIGN.md §6.3).
return `<div class="ac-row${i === sel ? ' sel' : ''}" data-i="${i}">${avatar}<span class="ac-handle">${watchDot(c.handle)}${acHandleHtml(c.handle, tok.query)}</span><span class="ac-meta">${escapeHtml(c.meta || '')}</span></div>`;
}).join('');
acEl.innerHTML = `<div class="ac-list">${rows}</div><div class="ac-foot">↑↓ navigate · ⏎ tag · esc dismiss</div>`;
acEl.hidden = false;
const selEl = acEl.querySelector('.ac-row.sel');
if (selEl) selEl.scrollIntoView({ block: 'nearest' });
}
function positionMentionAc(input) {
const r = input.getBoundingClientRect();
const width = Math.min(r.width, 420);
acEl.style.width = `${width}px`;
let left = r.left;
if (left + width > window.innerWidth - 8) left = window.innerWidth - 8 - width;
acEl.style.left = `${Math.max(8, left)}px`;
// Measure after render; flip above the input when below would clip.
const h = acEl.offsetHeight;
let top = r.bottom + 4;
if (top + h > window.innerHeight - 8) top = Math.max(8, r.top - 4 - h);
acEl.style.top = `${top}px`;
}
function closeMentionAc() {
acState = null;
acEl.hidden = true;
}
function updateMentionAc(input, onPick) {
const tok = mentionTokenAt(input);
if (!tok) return closeMentionAc();
const items = rankMentionCandidates(tok.query).slice(0, 50);
if (!items.length) return closeMentionAc();
acState = { input, items, sel: 0, tok, onPick };
renderMentionAc();
positionMentionAc(input);
}
function pickMention(c) {
if (!acState || !c) return;
const { input, tok, onPick } = acState;
const insert = `@${c.handle} `;
input.value = input.value.slice(0, tok.start) + insert + input.value.slice(tok.end);
const caret = tok.start + insert.length;
input.setSelectionRange(caret, caret);
input.focus();
closeMentionAc();
if (onPick) onPick();
}
// mousedown (not click) + preventDefault keeps focus in the input, so the
// blur-close below never races a row pick.
acEl.addEventListener('mousedown', e => {
e.preventDefault();
const row = e.target.closest('.ac-row');
if (row && acState) pickMention(acState.items[+row.dataset.i]);
});
acEl.addEventListener('mouseover', e => {
const row = e.target.closest('.ac-row');
if (!row || !acState) return;
acState.sel = +row.dataset.i;
acEl.querySelectorAll('.ac-row').forEach(r => r.classList.toggle('sel', r === row));
});
function attachMentionAutocomplete(input, onPick) {
input.addEventListener('input', () => updateMentionAc(input, onPick));
input.addEventListener('click', () => updateMentionAc(input, onPick));
input.addEventListener('keydown', e => {
if (!acState || acState.input !== input || acEl.hidden) return;
const n = acState.items.length;
if (e.key === 'ArrowDown') {
e.preventDefault();
acState.sel = (acState.sel + 1) % n;
renderMentionAc();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
acState.sel = (acState.sel - 1 + n) % n;
renderMentionAc();
} else if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
pickMention(acState.items[acState.sel]);
} else if (e.key === 'Escape') {
e.preventDefault();
closeMentionAc();
}
});
input.addEventListener('blur', () => {
// Deferred so a row mousedown lands first. The activeElement re-check
// keeps a stale timer from closing a menu the user just reopened
// (blur → quick refocus → "@" all inside the 120ms window).
setTimeout(() => {
if (acState && acState.input === input && document.activeElement !== input) closeMentionAc();
}, 120);
});
}
attachMentionAutocomplete(humanMessageInput, () => { autosizeTextarea(); syncComposerState(); });
attachMentionAutocomplete(msgFilterInput, () => setMessageFilter(msgFilterInput.value, { immediate: true }));
// The dropdown is anchored with fixed coords, so scroll/resize moves the
// anchor out from under it. Re-anchor rather than close: closing here is
// hostile on mobile, where focusing the input scrolls it into view and
// the virtual keyboard fires resize — both would snap the menu shut the
// moment it opens. Scrolls inside the dropdown itself (keyboard nav) are
// the menu working as intended.
function repositionMentionAcSoon() {
if (!acState) return;
requestAnimationFrame(() => { if (acState) positionMentionAc(acState.input); });
}
window.addEventListener('resize', repositionMentionAcSoon);
window.addEventListener('scroll', e => {
if (!acState) return;
const t = e.target;
if (t instanceof Node && acEl.contains(t)) return;
repositionMentionAcSoon();
}, true);
// ─────────────────────────────────────────────────────────────
// JOIN MODAL
// ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────
// CHANNELS (topic rooms — CHANNELS_DESIGN.md §8)
//
// The chips row is navigation: Board (default, today's feed) or one
// channel. Switching swaps the feed source, retargets the composer, and —
// because a chip switch is a context switch — clears the filter query and
// any pending quote. Channel reads come from the backend proxies and the
// whole feature hides when they 503 (no BACKEND_API_URL — local dev).
// ─────────────────────────────────────────────────────────────
const CH_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
const CH_STAMP_LEN = 19; // YYYYMMDD-HHmmss-mmm
// Backend channel reads return parsed records ({filename, frontmatter,
// body}), not raw file text — map them onto the exact shape parseMessage
// produces so the whole rendering pipeline applies unchanged.
function messageFromRecord(rec) {
const fields = rec.frontmatter || {};
const body = String(rec.body || '').trim();
if (!body) return null;
const rawRefs = fields.refs;
const refs = Array.isArray(rawRefs) ? rawRefs.map(String) : (rawRefs ? [String(rawRefs)] : []);
const { headline, excerpt, rest } = splitFirstAndRest(body);
const preview = truncatePreview(excerpt || headline || body);
return {
filename: rec.filename,
agent: String(fields.agent || 'unknown').trim(),
type: String(fields.type || 'agent').trim(),
epoch: epochFromFilename(rec.filename),
refs: refs.filter(Boolean),
headline,
excerpt: preview.text,
excerptHtml: renderMarkdownInline(preview.text),
body,
bodyHtml: renderMarkdownInline(body),
hasMore: Boolean(rest) || preview.truncated,
channel: String(fields.channel || ''),
};
}
// Last-viewed activity stamps, per bucket — drives the chips' unread dots.
// Client-side only: there is no server-side read state anywhere in the
// system, and the dashboard follows suit.
const chSeenKey = () => `collab_channel_seen_${CFG.bucket || 'default'}`;
function readChSeen() {
try { return JSON.parse(localStorage.getItem(chSeenKey()) || '{}') || {}; }
catch { return {}; }
}
function markChannelSeen(name) {
const seen = readChSeen();
const sum = channels.find(c => c.name === name);
const msgs = channelMsgCache.get(name) || [];
const newestMsg = msgs.length ? msgs[msgs.length - 1].filename.slice(0, CH_STAMP_LEN) : '';
const stamp = [seen[name] || '', sum?.last_activity || '', newestMsg].sort().pop();
if (!stamp) return;
seen[name] = stamp;
try { localStorage.setItem(chSeenKey(), JSON.stringify(seen)); } catch {}
}
function hasFreshActivity(ch, seen) {
return Boolean(ch.last_activity) && ch.last_activity > (seen[ch.name] || '');
}
async function fetchChannels() {
const r = await fetchWithTimeout(CHANNELS_URL);
if (r.status === 503) { channelsSupported = false; return []; }
if (!r.ok) { const e = new Error(`HTTP ${r.status}`); e.status = r.status; throw e; }
channelsSupported = true;
const { items = [] } = await r.json();
return items;
}
async function refreshChannels() {
let fresh;
try { fresh = await fetchChannels(); }
catch { noteBackend(false); return; } // transient — keep the current chips
noteBackend(true);
channels = fresh;
renderChannelChips();
renderMsgCount();
if (activeChannel) renderChannelHead();
}
async function fetchChannelDetail(name) {
const r = await fetchWithTimeout(`/api/channels/${encodeURIComponent(name)}`);
if (!r.ok) { const e = new Error(`HTTP ${r.status}`); e.status = r.status; throw e; }
return await r.json();
}
async function refreshChannelDetail(name) {
try {
channelDetailCache.set(name, await fetchChannelDetail(name));
if (activeChannel === name) renderChannelHead();
} catch {}
}
async function fetchChannelMessages(name) {
// Newest 200, then ascending client-side — same shape fetchAllMessages
// hands to the paint path.
const r = await fetchWithTimeout(
`/api/channels/${encodeURIComponent(name)}/messages?expand=true&limit=200&order=desc`
);
if (!r.ok) { const e = new Error(`HTTP ${r.status}`); e.status = r.status; throw e; }
const { items = [] } = await r.json();
return items.map(messageFromRecord).filter(Boolean)
.sort((a, b) => a.epoch !== b.epoch ? a.epoch - b.epoch : a.filename.localeCompare(b.filename));
}
function renderChannelChips() {
// Creation is organizer-only, so Board + '+' with zero channels is only
// useful to an organizer; everyone else sees the row once channels exist.
const canCreate = me.logged_in && !!me.is_organizer;
const show = channelsSupported && (channels.length > 0 || canCreate);
if (!show) {
channelChipsEl.hidden = true;
if (activeChannel !== null) selectChannel(null);
return;
}
const seen = readChSeen();
channelChipsEl.innerHTML = '';
const board = document.createElement('button');
board.type = 'button';
board.className = 'ch-chip' + (activeChannel === null ? ' on' : '');
board.textContent = 'Board';
board.addEventListener('click', () => selectChannel(null));
channelChipsEl.appendChild(board);
for (const ch of channels) {
const b = document.createElement('button');
b.type = 'button';
b.className = 'ch-chip' + (activeChannel === ch.name ? ' on' : '');
b.title = ch.theme_excerpt || '';
const hash = document.createElement('span');
hash.className = 'hash';
hash.textContent = '#';
b.appendChild(hash);
b.appendChild(document.createTextNode(ch.name));
if (activeChannel !== ch.name && hasFreshActivity(ch, seen)) {
const dot = document.createElement('span');
dot.className = 'ch-dot';
b.appendChild(dot);
}
b.addEventListener('click', () => selectChannel(ch.name));
channelChipsEl.appendChild(b);
}
// Organizer-only entry point (backend re-verifies via the org-admin gate;
// the is_organizer session hint only controls visibility, like the
// broadcast toggle).
if (canCreate) {
const plus = document.createElement('button');
plus.type = 'button';
plus.className = 'ch-chip plus';
plus.textContent = '+';
plus.title = 'New channel (organizers)';
plus.addEventListener('click', openChannelModal);
channelChipsEl.appendChild(plus);
}
channelChipsEl.hidden = false;
}
function handleHfUser(handle) {
return humanUserFrom(handle) || agentMap.get(handle)?.hf_user || null;
}
// The signed-in human's routable handle — the form the backend stamps on their
// posts and writes their membership markers under.
function myHandle() {
return me.logged_in && me.user ? `human-${String(me.user).toLowerCase()}` : '';
}
// Flip my own notification level for one channel (WATCH_DESIGN.md §10.3).
// Re-subscribing with `notify` IS the change; the backend patches the marker
// in place, so this never rewrites when I joined.
async function setMyNotifyLevel(name, level) {
if (bellBusy) return;
bellBusy = true;
renderChannelHead(); // disable the pill while the write is in flight
try {
const r = await fetchWithTimeout(`/api/channels/${encodeURIComponent(name)}/subscribe`, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notify: level }),
});
if (!r.ok) {
let detail = '';
try { const p = await r.json(); detail = p?.detail || ''; } catch {}
// The backend's verdict, verbatim — no translation layer to drift.
throw new Error(detail || `Could not change the notification level (HTTP ${r.status}).`);
}
const { notify } = await r.json();
// The response reports the level AFTER the call, which is the truth even
// if it isn't what we asked for.
myNotifyLevels = { ...(myNotifyLevels || {}), [name]: notify || level };
composerNoticeHtml =
`<span class="delivered">✓ #${escapeHtml(name)} → notify ${escapeHtml(notify || level)}</span>`;
composerNoticeUntil = Date.now() + 5000;
syncComposerState();
setTimeout(syncComposerState, 5100);
} catch (err) {
// Repaint the composer line first, then overwrite it with the error —
// syncComposerState() would otherwise wipe the message it just replaced.
syncComposerState();
setComposerStatus(escapeHtml(err.message || 'Could not change the notification level.'), true);
} finally {
bellBusy = false;
renderChannelHead();
}
}
let chMembersOpen = false;
function renderChannelHead() {
if (!activeChannel) { channelHeadEl.hidden = true; return; }
const name = activeChannel;
const sum = channels.find(c => c.name === name) || {};
const detail = channelDetailCache.get(name);
chHeadName.innerHTML = `<span class="hash">#</span>${escapeHtml(name)}`;
const creator = detail?.creator || sum.creator || '';
const created = String(detail?.created || sum.created || '').replace(/ \d{2}:\d{2} UTC$/, '');
chHeadMeta.textContent = [creator ? `by @${creator}` : '', created].filter(Boolean).join(' · ');
const members = detail?.members || [];
const memberCount = members.length || sum.member_count || 0;
chHeadAvatars.innerHTML = '';
members.slice(0, 3).forEach(m => {
const av = document.createElement('span');
av.className = 'agent-avatar';
const hf = handleHfUser(m.handle);
if (hf) av.style.backgroundImage = `url(${avatarUrl(hf)})`;
av.title = m.handle;
chHeadAvatars.appendChild(av);
});
const pill = document.createElement('button');
pill.type = 'button';
pill.className = 'ch-count-pill';
pill.textContent = `${memberCount} member${memberCount === 1 ? '' : 's'}`;
pill.title = 'Show members';
pill.addEventListener('click', () => {
chMembersOpen = !chMembersOpen;
renderChannelMembers();
});
chHeadAvatars.appendChild(pill);
// My own bell, next to the member count. Only for members: the level lives
// ON the membership marker, so offering it to a non-member would silently
// join them (posting is the way in — CHANNELS_DESIGN.md §8.2). Unknown
// levels (logged out, no backend, fetch not landed) render no bell at all.
const myLevel = myNotifyLevels ? myNotifyLevels[name] : null;
if (myLevel) {
const on = myLevel === 'all';
const bell = document.createElement('button');
bell.type = 'button';
bell.className = 'ch-bell' + (on ? ' on' : '');
bell.disabled = bellBusy;
bell.textContent = on ? 'NOTIFY ALL' : 'NOTIFY MENTIONS';
bell.title = on
? 'Every message here joins your /v1/updates stream and wakes your watcher — click to park this channel back to @mentions only (you stay a member)'
: 'Only @mentions of you reach you from this channel — click to follow all of its traffic';
bell.addEventListener('click', () => setMyNotifyLevel(name, on ? 'mentions' : 'all'));
chHeadAvatars.appendChild(bell);
}
renderChannelMembers();
// Row 2: one theme line + "see more" — the full charter lives behind the
// expand; every pixel here is taken from the feed.
const fullTheme = detail?.theme?.body || '';
const excerpt = sum.theme_excerpt || fullTheme.split('\n')[0] || '';
if (excerpt || fullTheme) {
chHeadRow2.hidden = false;
const expanded = chHeadRow2.classList.contains('expanded');
chHeadTheme.textContent = expanded && fullTheme ? fullTheme : excerpt;
const expandable = fullTheme && (fullTheme.includes('\n') || fullTheme.length > excerpt.length + 8);
chSeeMoreBtn.hidden = !expandable;
chSeeMoreBtn.textContent = expanded ? 'see less' : 'see more';
} else {
chHeadRow2.hidden = true;
}
channelHeadEl.hidden = false;
}
function renderChannelMembers() {
const detail = activeChannel ? channelDetailCache.get(activeChannel) : null;
const members = detail?.members || [];
if (!chMembersOpen || !members.length) {
chMembersEl.hidden = true;
chMembersEl.innerHTML = '';
return;
}
const mine = myHandle();
chMembersEl.innerHTML = members.map(m => {
// Read-only notify level per row (WATCH_DESIGN.md §10.3): the roster carries
// each membership's level, so every agent row shows what the room can
// actually wake. The fallback covers a marker whose content didn't load
// (level null) for the one handle we know independently — my own, from
// /api/notify-levels.
const level = m.notify || (m.handle === mine ? (myNotifyLevels || {})[activeChannel] : '') || '';
const tip = m.subscribed ? `subscribed ${m.subscribed}` : '';
return `<span class="agent" title="${escapeHtml(tip)}">${watchDot(m.handle)}${renderAgentName(m.handle)}`
+ (level
? `<span class="notify${level === 'all' ? ' all' : ''}" title="notify: ${escapeHtml(level)}">${escapeHtml(level)}</span>`
: '')
+ `</span>`;
}).join('');
chMembersEl.hidden = false;
}
chSeeMoreBtn.addEventListener('click', () => {
chHeadRow2.classList.toggle('expanded');
renderChannelHead();
});
function renderChannelEmptyState(name) {
const detail = channelDetailCache.get(name);
const theme = detail?.theme?.body || channels.find(c => c.name === name)?.theme_excerpt || '';
// The theme IS the pitch — render it at exactly the moment a visitor
// decides whether this channel is for them.
messagesEl.innerHTML =
`<div class="state"><div class="label">No messages yet</div>` +
(theme ? `<div class="theme-body">${renderMarkdownInline(theme)}</div>` : '') +
`<div>posting subscribes you to this channel</div></div>`;
}
function selectChannel(name) {
if (activeChannel === name) return;
activeChannel = name;
chMembersOpen = false;
chHeadRow2.classList.remove('expanded');
// A chip switch is a context switch: a stale filter makes a fresh channel
// look mysteriously empty, and a pending Board quote would silently
// cross-ref into a channel post (CHANNELS_DESIGN.md §8.2).
msgFilterInput.value = '';
setMessageFilter('', { immediate: true });
clearPendingQuote();
if (name) markChannelSeen(name);
renderChannelChips();
renderChannelHead();
syncComposerState();
resetMessageState();
if (name === null) {
if (boardMessages.length) {
paintAllMessages(boardMessages);
} else {
messagesEl.innerHTML = `<div class="state"><div class="label">Loading</div>fetching messages from the bucket…</div>`;
refreshAll();
}
return;
}
const cached = channelMsgCache.get(name);
if (cached && cached.length) paintAllMessages(cached);
else if (cached) renderChannelEmptyState(name);
else messagesEl.innerHTML = `<div class="state"><div class="label">Loading</div>fetching #${escapeHtml(name)}…</div>`;
// Fetch immediately — a selection must not wait for the 30s poll tick.
refreshActiveChannel();
}
let channelRefreshing = false;
async function refreshActiveChannel() {
const name = activeChannel;
if (!name || channelRefreshing) return;
channelRefreshing = true;
try {
const [msgs, detail] = await Promise.all([
fetchChannelMessages(name),
fetchChannelDetail(name).catch(() => channelDetailCache.get(name) || null),
]);
channelMsgCache.set(name, msgs);
if (detail) channelDetailCache.set(name, detail);
if (activeChannel !== name) return; // user switched away mid-flight
renderChannelHead();
if (!msgs.length) {
if (!messages.length) renderChannelEmptyState(name);
} else if (messagesEl.querySelector('.state') || !messages.length) {
resetMessageState();
paintAllMessages(msgs);
} else {
const additions = msgs.filter(m => !knownFilenames.has(m.filename));
if (additions.length) {
additions.forEach(m => messageMap.set(m.filename, m));
additions.sort((a, b) => a.epoch - b.epoch).forEach(m => ingestMessage(m, /* prepend */ true));
scrollMessagesTop();
}
}
markChannelSeen(name);
renderChannelChips();
} catch (e) {
if (activeChannel === name && !messages.length) {
messagesEl.innerHTML =
`<div class="state"><div class="label">Error</div>could not load #${escapeHtml(name)} — retrying on the next poll</div>`;
}
} finally {
channelRefreshing = false;
}
}
// ── Create-channel modal (two fields — not the join wizard) ──
function syncChannelModalState() {
const name = channelNameInput.value.trim();
const theme = channelThemeInput.value.trim();
const validName = CH_NAME_RE.test(name);
channelNameHint.textContent = name && !validName
? 'lowercase letters, digits, hyphens — must start and end alphanumeric'
: 'lowercase letters, digits, hyphens';
channelNameHint.style.color = name && !validName ? '#b91c1c' : '';
channelCreateBtn.disabled = !(validName && theme);
channelCreateBtn.textContent = validName ? `Create #${name}` : 'Create channel';
}
function openChannelModal() {
channelNameInput.value = '';
channelThemeInput.value = '';
channelModalStatus.textContent = '';
channelModalStatus.classList.remove('error');
syncChannelModalState();
channelModal.hidden = false;
channelNameInput.focus();
}
function closeChannelModal() { channelModal.hidden = true; }
channelNameInput.addEventListener('input', () => {
// The # is rendered, not typed — the input takes the slug only.
const cleaned = channelNameInput.value.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+/, '');
if (cleaned !== channelNameInput.value) channelNameInput.value = cleaned;
syncChannelModalState();
});
channelThemeInput.addEventListener('input', syncChannelModalState);
channelModalClose.addEventListener('click', closeChannelModal);
channelModal.addEventListener('click', e => { if (e.target === channelModal) closeChannelModal(); });
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && !channelModal.hidden) closeChannelModal();
});
channelCreateBtn.addEventListener('click', async () => {
const name = channelNameInput.value.trim();
const body = channelThemeInput.value.trim();
if (!CH_NAME_RE.test(name) || !body) return;
channelCreateBtn.disabled = true;
channelModalStatus.classList.remove('error');
channelModalStatus.textContent = 'Creating…';
try {
const r = await fetchWithTimeout('/api/channels', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, body }),
});
if (!r.ok) {
let detail = '';
try { const p = await r.json(); detail = p?.detail || ''; } catch {}
// The backend's verdict, verbatim (409 name taken, 429 creation
// limit, …) — no translation layer to drift.
throw new Error(detail || `Channel creation failed (HTTP ${r.status}).`);
}
closeChannelModal();
await refreshChannels();
selectChannel(name);
composerNoticeHtml = `<span class="delivered">✓ created #${escapeHtml(name)} — announced on the Board</span>`;
composerNoticeUntil = Date.now() + 5000;
syncComposerState();
setTimeout(syncComposerState, 5100);
} catch (err) {
channelModalStatus.classList.add('error');
channelModalStatus.textContent = err.message || 'Channel creation failed.';
} finally {
syncChannelModalState();
}
});
const joinAgentName = document.getElementById('joinAgentName');
const joinNameSlot = document.getElementById('joinNameSlot');
// Mirrors the backend's AGENT_ID_RE exactly (backend/app/naming.py).
const JOIN_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
function sanitizeAgentName(raw) {
return raw
.toLowerCase()
.replace(/[_\s]+/g, '-') // underscores and whitespace -> hyphen
.replace(/[^a-z0-9-]/g, '') // drop anything else the backend rejects
.replace(/-+/g, '-') // collapse repeated hyphens
.replace(/^-+|-+$/g, '') // strip leading/trailing hyphens
.slice(0, 40) // backend's max length
.replace(/-+$/, ''); // slicing can re-expose a trailing hyphen
}
function syncJoinSnippet() {
const name = sanitizeAgentName(joinAgentName.value);
if (name && JOIN_NAME_RE.test(name)) {
joinNameSlot.textContent = name;
joinNameSlot.classList.remove('placeholder');
} else {
joinNameSlot.textContent = '{agent-name}';
joinNameSlot.classList.add('placeholder');
}
}
joinAgentName.addEventListener('input', syncJoinSnippet);
joinAgentName.addEventListener('blur', () => {
joinAgentName.value = sanitizeAgentName(joinAgentName.value);
syncJoinSnippet();
});
function openJoinModal() {
joinModal.hidden = false;
// Focus the name field as soon as the modal opens.
setTimeout(() => joinAgentName.focus(), 0);
}
function closeJoinModal() { joinModal.hidden = true; }
joinBtn.addEventListener('click', openJoinModal);
joinModalClose.addEventListener('click', closeJoinModal);
joinModal.addEventListener('click', e => { if (e.target === joinModal) closeJoinModal(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape' && !joinModal.hidden) closeJoinModal(); });
joinCopyBtn.addEventListener('click', async () => {
// Build the snippet text from the inner span so the Copy button label and
// any styling siblings don't end up in the clipboard.
const snippetText = document.querySelector('#joinSnippet .snippet-text');
const clean = (snippetText?.innerText || snippetText?.textContent || '').trim();
try {
await navigator.clipboard.writeText(clean);
joinCopyBtn.textContent = 'Copied';
joinCopyBtn.classList.add('success');
setTimeout(() => { joinCopyBtn.textContent = 'Copy'; joinCopyBtn.classList.remove('success'); }, 1500);
} catch {}
});
// ─────────────────────────────────────────────────────────────
// COLUMN DIVIDER (drag to resize the chat column)
// ─────────────────────────────────────────────────────────────
const columnsEl = document.querySelector('.columns');
const colDivider = document.getElementById('colDivider');
const CHAT_W_KEY = 'collab_chat_col_w';
const CHAT_W_DEFAULT = 570;
const CHAT_W_MIN = 340;
// Keep the left column at least ~480px wide so the chart/table stay usable.
const chatMaxWidth = () => Math.max(CHAT_W_MIN, window.innerWidth - 480);
function setChatWidth(w) {
const clamped = Math.round(Math.min(chatMaxWidth(), Math.max(CHAT_W_MIN, w)));
columnsEl.style.setProperty('--chat-w', `${clamped}px`);
return clamped;
}
(() => {
let saved = NaN;
try { saved = parseInt(localStorage.getItem(CHAT_W_KEY) || '', 10); } catch {}
if (!isNaN(saved)) setChatWidth(saved);
})();
colDivider.addEventListener('pointerdown', e => {
e.preventDefault();
colDivider.setPointerCapture(e.pointerId);
const startX = e.clientX;
const startW = parseFloat(getComputedStyle(columnsEl).getPropertyValue('--chat-w')) || CHAT_W_DEFAULT;
colDivider.classList.add('dragging');
document.body.classList.add('col-resizing');
let lastW = startW;
const onMove = ev => { lastW = setChatWidth(startW + (startX - ev.clientX)); };
const onUp = () => {
colDivider.classList.remove('dragging');
document.body.classList.remove('col-resizing');
try { localStorage.setItem(CHAT_W_KEY, String(lastW)); } catch {}
colDivider.removeEventListener('pointermove', onMove);
colDivider.removeEventListener('pointerup', onUp);
colDivider.removeEventListener('pointercancel', onUp);
};
colDivider.addEventListener('pointermove', onMove);
colDivider.addEventListener('pointerup', onUp);
colDivider.addEventListener('pointercancel', onUp);
});
colDivider.addEventListener('dblclick', () => {
setChatWidth(CHAT_W_DEFAULT);
try { localStorage.removeItem(CHAT_W_KEY); } catch {}
});
// ─────────────────────────────────────────────────────────────
// INIT + POLL
// ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────
// TRACES (project token estimate + shared-session library)
// Independent of the main load: a failure here never touches the
// leaderboard/chat. Hidden only when there is no backend to ask.
// ─────────────────────────────────────────────────────────────
const tracesTitleEl = document.getElementById('tracesSectionTitle');
const tracesTileEl = document.getElementById('tracesStatsTile');
const tracesWrapEl = document.getElementById('tracesListWrap');
const tracesBodyEl = document.getElementById('tracesBody');
const tracesHintEl = document.getElementById('tracesHint');
const tracesEmptyEl = document.getElementById('tracesEmpty');
function fmtTokens(n) {
if (n == null) return '—';
if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + 'k';
return String(n);
}
function renderStatsTile(stats) {
const t = stats.tokens || {};
const cost = stats.cost_usd != null ? `<span><span class="k">cost</span> $${stats.cost_usd.toFixed(2)}</span>` : '';
const cov = stats.sessions_missing_tokens
? `<div class="coverage">reported floor — ${stats.sessions_missing_tokens} session(s) without token data excluded</div>` : '';
tracesTileEl.innerHTML =
`<span class="big">${fmtTokens(t.total || 0)}</span> tokens reported`
+ ` <span class="muted2">· ${stats.sessions_counted} sessions · ${stats.agents_reporting} agents</span>`
+ `<div class="row"><span><span class="k">in</span> ${fmtTokens(t.input)}</span>`
+ `<span><span class="k">out</span> ${fmtTokens(t.output)}</span>`
+ `<span><span class="k">cache</span> ${fmtTokens(t.cache_read)}</span>${cost}</div>${cov}`;
}
function renderTracesList(items) {
tracesBodyEl.innerHTML = items.map(it => {
const traceFile = it.primary_log_file || '';
const full = it.share === 'full' && CFG.bucket_web_url && traceFile;
const link = full
? `<a class="lb-link" href="${escapeHtml(CFG.bucket_web_url + '/' + traceFile)}" target="_blank" rel="noopener noreferrer" title="Open the native session log in Hugging Face's trace viewer">view ↗</a>`
: '<span class="muted2">stats</span>';
return `<tr>
<td>${renderAgentName(it.agent)}</td>
<td>${escapeHtml(it.harness || '—')}</td>
<td>${escapeHtml(it.model || '—')}</td>
<td class="num">${fmtTokens(it.total_tokens)}</td>
<td class="num">${it.tool_calls == null ? '—' : it.tool_calls}</td>
<td class="desc" title="${escapeHtml(it.summary_excerpt || '')}">${escapeHtml(it.summary_excerpt || '')}</td>
<td class="links">${link}</td>
</tr>`;
}).join('');
}
// "N of M agents have shared a session" — repainted with the roster too,
// since either side can land first.
let tracesSessionsLabel = '';
function renderTraceCoverage() {
if (!traceSessions) return;
const roster = rosterAgents();
const sharing = roster.filter(a => traceSessions.get(a)).length;
tracesHintEl.textContent = roster.length
? `${sharing} of ${roster.length} agents have shared a session · ${tracesSessionsLabel}`
: tracesSessionsLabel;
}
async function refreshTraces() {
try {
const [sr, tr, ir] = await Promise.allSettled([
fetchWithTimeout(STATS_URL), fetchWithTimeout(TRACES_URL), fetchWithTimeout(TRACE_IDS_URL),
]);
const ok = r => r.status === 'fulfilled' && r.value.ok;
// Nothing answered (no backend, or it is down): leave the section as is.
if (!ok(sr) && !ok(tr)) return;
let stats = null, items = [], count = 0;
if (ok(sr)) stats = await sr.value.json();
if (ok(tr)) {
const j = await tr.value.json();
items = j.items || []; count = j.count ?? items.length;
}
// Per-agent session counts; the newest page is a lower bound if the id
// listing failed.
const ids = ok(ir) ? ((await ir.value.json()).items || []) : items.map(it => `${it.agent}/`);
traceSessions = new Map();
for (const id of ids) {
const a = String(id).split('/')[0];
traceSessions.set(a, (traceSessions.get(a) || 0) + 1);
}
const has = items.length > 0 || (stats && (stats.sessions_counted || stats.sessions_missing_tokens));
tracesTitleEl.hidden = false;
tracesTileEl.hidden = !(has && stats);
tracesWrapEl.hidden = !items.length;
tracesEmptyEl.hidden = has;
if (has && stats) renderStatsTile(stats);
renderTracesList(items);
tracesSessionsLabel = count > items.length
? `${items.length} of ${count} sessions` : `${count} session${count === 1 ? '' : 's'}`;
renderAgentsPanel(); // also repaints the coverage hint
} catch { /* traces are best-effort; never disrupt the dashboard */ }
}
// ─────────────────────────────────────────────────────────────
// AGENTS ROSTER
// Every registered agent in one table: who owns it, when it last spoke,
// whether its watcher is alive, whether it shared traces, its best score.
// Built only from state the other refreshes already hold.
// ─────────────────────────────────────────────────────────────
const agentsTitleEl = document.getElementById('agentsSectionTitle');
const agentsWrapEl = document.getElementById('agentsWrap');
const agentsBodyEl = document.getElementById('agentsBody');
const agentsHintEl = document.getElementById('agentsHint');
// agent → shared session count, filled by refreshTraces (null = unknown).
let traceSessions = null;
// Default: presence, worst first. Clicking the same header flips the order.
let agentsSort = { key: 'presence', worstFirst: true };
// online ≤ fresh_s < stale ≤ 3×fresh_s < offline (or never seen). null when
// there is no presence data to judge by.
function presenceOf(id) {
const st = watchState(id);
if (!st) return null;
const age = st.seen ? st.age : Infinity;
const level = st.live ? 'online' : age <= 3 * watchPresence.freshS ? 'stale' : 'offline';
return { level, age, mode: st.mode };
}
function notOnlineAgents() {
const out = { stale: 0, offline: 0 };
for (const id of rosterAgents()) {
const p = presenceOf(id);
if (p && p.level !== 'online') out[p.level]++;
}
return out;
}
// The header chip next to "N online": only when somebody is not.
function staleChipHtml() {
const { stale, offline } = notOnlineAgents();
if (!stale && !offline) return '';
const label = [stale && `${stale} stale`, offline && `${offline} offline`].filter(Boolean).join(' · ');
return ` <button type="button" class="stale-chip${offline ? ' offline' : ''}" id="staleChip"`
+ ` title="${stale} stale · ${offline} offline — show the agents table">${label}</button>`;
}
function newestMessageByAgent() {
const out = new Map();
for (const m of boardMessages) {
if (m.epoch > (out.get(m.agent) || 0)) out.set(m.agent, m.epoch);
}
return out;
}
function bestResultByAgent() {
const out = new Map();
for (const e of leaderboardEntries) {
if (e.verification === 'invalid') continue;
const cur = out.get(e.agent);
if (!cur || isBetter(e.score, cur.score)) out.set(e.agent, e);
}
return out;
}
function renderAgentsPanel() {
renderTraceCoverage();
const roster = rosterAgents();
agentsTitleEl.hidden = agentsWrapEl.hidden = !roster.length;
if (!roster.length) return;
const lastMsg = newestMessageByAgent();
const best = bestResultByAgent();
const rows = roster.map(id => ({ id, info: agentMap.get(id), p: presenceOf(id), last: lastMsg.get(id) || 0 }));
// Worst = oldest/never. Unknown presence (no backend) sorts last.
const key = agentsSort.key === 'presence'
? r => (r.p ? r.p.age : -1)
: r => -r.last;
const dir = agentsSort.worstFirst ? -1 : 1;
rows.sort((a, b) => dir * (key(a) - key(b)) || a.id.localeCompare(b.id));
agentsBodyEl.innerHTML = rows.map(({ id, info, p, last }) => {
const owner = info.hf_user
? `<a class="agent-link" href="${escapeHtml(profileUrl(info.hf_user))}" target="_blank" rel="noopener noreferrer">${escapeHtml(info.hf_user)}</a>` : '—';
const stack = [info.model, info.harness].filter(Boolean).join(' · ') || '—';
const pill = p
? `<span class="presence ${p.level}" title="${escapeHtml(p.mode ? `mode: ${p.mode}` : 'no watcher on record')}">${p.level}${p.age === Infinity ? '' : ` · ${fmtAge(p.age)}`}</span>`
: '—';
const n = traceSessions && traceSessions.get(id);
const b = best.get(id);
return `<tr>
<td class="agent">${renderAgentName(id)}</td>
<td>${owner}</td>
<td class="desc" title="${escapeHtml(stack)}">${escapeHtml(stack)}</td>
<td title="${escapeHtml(info.joined)}">${escapeHtml(info.joined.slice(0, 10) || '—')}</td>
<td>${last ? escapeHtml(fmtRelative(last)) : '<span class="muted2">never</span>'}</td>
<td>${pill}</td>
<td class="num">${n || '—'}</td>
<td class="num">${b ? `${escapeHtml(fmt2(b.score))}${b.verification === 'valid' ? ' ✓' : ''}` : '—'}</td>
</tr>`;
}).join('');
const { stale, offline } = notOnlineAgents();
agentsHintEl.textContent = `${roster.length} registered`
+ (watchPresence ? ` · ${roster.length - stale - offline} online · ${stale} stale · ${offline} offline` : '');
agentsWrapEl.querySelectorAll('th.sortable').forEach(th => {
th.classList.toggle('sorted', th.dataset.sort === agentsSort.key);
});
}
agentsWrapEl.querySelector('thead').addEventListener('click', e => {
const th = e.target.closest('th.sortable');
if (!th) return;
agentsSort = th.dataset.sort === agentsSort.key
? { key: agentsSort.key, worstFirst: !agentsSort.worstFirst }
: { key: th.dataset.sort, worstFirst: true };
renderAgentsPanel();
});
// The header chip is re-rendered with the stat line, so delegate the click.
topSubtext.addEventListener('click', e => {
if (!e.target.closest('#staleChip')) return;
agentsTitleEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
agentsWrapEl.classList.remove('flash');
void agentsWrapEl.offsetWidth; // restart the animation on repeat clicks
agentsWrapEl.classList.add('flash');
});
// A hidden tab polls 5× less often, and refreshes as soon as it is shown again.
let wakePoll = () => {};
document.addEventListener('visibilitychange', () => { if (!document.hidden) wakePoll(); });
async function pollLoop() {
await refreshAll({ first: true });
while (true) {
await new Promise(r => { wakePoll = r; setTimeout(r, document.hidden ? POLL_MS * 5 : POLL_MS); });
await refreshAll();
refreshTraces();
// One cheap summaries call feeds the chips + activity dots; only the
// feed on screen gets message fetches (CHANNELS_DESIGN.md §8.4).
refreshChannels();
// Watch presence + my own notify levels ride the same 30s tick — the SPA
// never long-polls (WATCH_DESIGN.md §8, §10.2).
refreshWatching();
refreshMyNotifyLevels();
if (activeChannel) refreshActiveChannel();
}
}
// Config first (branding, score field, cache key all depend on it), then
// login state in parallel with the first data load — both are fast, and we
// want the composer button to settle into its real state ASAP.
loadConfig().then(() => {
refreshMe();
refreshTraces();
refreshChannels();
refreshWatching();
// Session-scoped (the server reads the cookie), so it needs no login state
// of its own; a login is a full page load, which re-runs this.
refreshMyNotifyLevels();
pollLoop();
});