diff --git a/dashboard/public/app.js b/dashboard/public/app.js index 64705472..75da0070 100644 --- a/dashboard/public/app.js +++ b/dashboard/public/app.js @@ -137,6 +137,13 @@ const $historySelect = $("history-select"); const $historyPanel = $("history-panel"); const $historyBody = $("history-body"); +// ─── Repo Filter State ────────────────────────────────────────────────────── + +const $repoFilter = $("repo-filter"); +let selectedRepo = ""; // "" means "All repos" +let knownRepos = []; // sorted list of known repo IDs +let repoFilterVisible = false; + // ─── History State ────────────────────────────────────────────────────────── let historyList = []; // compact batch summaries @@ -147,6 +154,97 @@ let viewingHistoryId = null; // batchId if viewing history, null if live let viewerMode = null; // "conversation" | "status-md" | null let viewerTarget = null; // session name (conversation) or taskId (status-md) +// ─── Repo Helpers ─────────────────────────────────────────────────────────── + +/** + * Build a sorted, deduplicated list of repo IDs from the batch payload. + * Returns empty array when mode !== "workspace" or when fewer than 2 repos. + */ +function buildRepoSet(batch) { + if (!batch || batch.mode !== "workspace") return []; + + const repos = new Set(); + for (const lane of (batch.lanes || [])) { + if (lane.repoId) repos.add(lane.repoId); + } + for (const task of (batch.tasks || [])) { + const rid = task.resolvedRepoId || task.repoId; + if (rid) repos.add(rid); + } + for (const mr of (batch.mergeResults || [])) { + for (const rr of (mr.repoResults || [])) { + if (rr.repoId) repos.add(rr.repoId); + } + } + const sorted = Array.from(repos).sort(); + return sorted.length >= 2 ? sorted : []; +} + +/** + * Update the repo filter dropdown options and visibility. + * Resets selection to "All repos" if the previously selected repo disappeared. + */ +function updateRepoFilter(repos) { + knownRepos = repos; + const shouldShow = repos.length >= 2; + + if (shouldShow !== repoFilterVisible) { + $repoFilter.style.display = shouldShow ? "" : "none"; + repoFilterVisible = shouldShow; + } + + if (!shouldShow) { + selectedRepo = ""; + return; + } + + // If selected repo disappeared, reset to "All" + if (selectedRepo && !repos.includes(selectedRepo)) { + selectedRepo = ""; + } + + // Rebuild options only if repo set changed + const currentOpts = Array.from($repoFilter.options).slice(1).map(o => o.value); + const changed = currentOpts.length !== repos.length || currentOpts.some((v, i) => v !== repos[i]); + if (changed) { + // Preserve selection + const prev = selectedRepo; + $repoFilter.innerHTML = ''; + for (const r of repos) { + const opt = document.createElement("option"); + opt.value = r; + opt.textContent = r; + $repoFilter.appendChild(opt); + } + $repoFilter.value = prev; + } +} + +/** Get the effective repo ID for a task (prefer resolvedRepoId, fallback repoId). */ +function taskRepoId(task) { + return task.resolvedRepoId || task.repoId || undefined; +} + +/** Render a repo badge span. Returns "" if repoId is falsy or repos not active. */ +function repoBadgeHtml(repoId, extraClass) { + if (!repoId || knownRepos.length < 2) return ""; + return `${escapeHtml(repoId)}`; +} + +// Repo filter change handler +$repoFilter.addEventListener("change", (e) => { + selectedRepo = e.target.value; + // Re-render with current data + if (currentData) { + const batch = currentData.batch; + const tmux = currentData.tmuxSessions || []; + if (batch) { + renderLanesTasks(batch, tmux); + renderMergeAgents(batch, tmux); + } + } +}); + // ─── Render: Header ───────────────────────────────────────────────────────── function renderHeader(batch) { @@ -288,9 +386,18 @@ function renderLanesTasks(batch, tmuxSessions) { const tasks = batch.tasks || []; const tmuxSet = new Set(tmuxSessions || []); const laneStates = currentData?.laneStates || {}; + const showRepos = knownRepos.length >= 2; let html = ""; for (const lane of batch.lanes) { + // Repo filtering: if a repo is selected, skip lanes that don't match + if (selectedRepo && showRepos) { + const laneTasks = (lane.taskIds || []).map(tid => tasks.find(t => t.taskId === tid)).filter(Boolean); + const laneMatchesRepo = (lane.repoId === selectedRepo) || + laneTasks.some(t => (taskRepoId(t) || lane.repoId) === selectedRepo); + if (!laneMatchesRepo) continue; + } + const alive = tmuxSet.has(lane.tmuxSessionName); const tmuxCmd = `tmux attach -t ${lane.tmuxSessionName}`; @@ -301,6 +408,9 @@ function renderLanesTasks(batch, tmuxSessions) { html += `