export function renderHtml() { return ` Work Hub

Work Hub

Loading your command center…

Building a plan for your available time…
0 selected
Tracked repos are stored in ~/.copilot/extensions/work-hub/artifacts/config.json. Local checkouts under ~/Projects are auto-detected.

Tracked (0)

Discover your repos

Open this tab to load your repositories.
Data quality (0 warnings)
`; } function clientScript() { return ` const state = { model: null, tab: "focus", loading: false, discover: null, discoverLoading: false, filters: { text: "", repo: "", sort: "priority", chips: new Set(), repoText: "", repoSort: "attention", discoverText: "" }, cleanup: { text: "", repo: "", sort: "oldest", buckets: new Set(), selected: new Set() }, onboarding: { selected: new Set() } }; const $ = (id) => document.getElementById(id); const esc = (v) => String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" }[c])); const CHIP_DEFS = [ { id: "open-pr", label: "Open PRs", kind: "pr" }, { id: "open-issue", label: "Open issues", kind: "issue" }, { id: "needs-review", label: "Needs review" }, { id: "human", label: "Human authored" }, { id: "failing-checks", label: "Failing checks" }, { id: "assigned", label: "Assigned to me" }, { id: "stale-release", label: "Stale release" }, { id: "failing-deploy", label: "Failed deploy" }, { id: "active-session", label: "Active session" }, { id: "triage", label: "Session triage" }, ]; const CHIP_KINDS = Object.fromEntries(CHIP_DEFS.filter((c) => c.kind).map((c) => [c.id, c.kind])); async function api(path, options = {}) { const res = await fetch(path, { ...options, headers: { "Content-Type": "application/json", ...(options.headers || {}) } }); const json = await res.json(); if (!res.ok) throw new Error(json.error || "Request failed"); return json; } function setLoading(v) { state.loading = v; $("refresh").innerHTML = v ? ' Loading' : "↻ Refresh"; $("refresh").disabled = v; } async function load(force = false) { setLoading(true); try { state.model = force ? await api("/api/refresh", { method: "POST" }) : await api("/api/state"); render(); } catch (e) { $("subtitle").textContent = e.message; } finally { setLoading(false); } } async function saveFocus() { setLoading(true); try { state.model = await api("/api/focus", { method: "POST", body: JSON.stringify({ mood: $("mood").value, minutes: Number($("minutes").value), busyness: $("busyness").value, focusIntent: $("focusIntent").value }) }); render(); } catch (e) { $("subtitle").textContent = e.message; } finally { setLoading(false); } } function switchTab(tab) { state.tab = tab; document.querySelectorAll(".tab").forEach((el) => el.classList.toggle("active", el.dataset.tab === tab)); document.querySelectorAll(".view").forEach((el) => el.classList.toggle("active", el.dataset.view === tab)); if (tab === "manage" && !state.discover && !state.discoverLoading) loadDiscover(false); if (tab === "manage") renderManage(); } function render() { const m = state.model; if (!m) return; $("subtitle").innerHTML = "Updated " + new Date(m.generatedAt).toLocaleTimeString() + (m.currentLogin ? " · @" + esc(m.currentLogin) : "") + " · " + m.summary.repoCount + " repos"; $("mood").value = m.preferences.mood; $("minutes").value = String(m.preferences.minutes); $("busyness").value = m.preferences.busyness; $("focusIntent").value = m.preferences.focusIntent || "balanced"; $("workCount").textContent = m.focusItems.length; $("repoCount").textContent = m.repos.length; $("cleanupCount").textContent = (m.sessionInventory || []).length; $("errorCount").textContent = m.errors.length; renderRepoFilter(m.repos); renderOnboarding(); renderStats(m.summary); renderPlan(m); renderChips(); renderWork(); renderRepos(); renderCleanup(); renderErrors(m.errors); if (state.tab === "manage") renderManage(); } function discoverItems() { if (!state.discover) return []; const repos = (state.discover.repos || []).map((r) => ({ ...r, source: "github" })); const projects = (state.discover.projects || []).map((r) => ({ ...r, source: "local" })); const seen = new Set(); const items = []; for (const item of [...projects, ...repos]) { const key = (item.slug || "").toLowerCase(); if (!key || seen.has(key)) continue; seen.add(key); items.push(item); } return items; } function encodedRepo(item) { return JSON.stringify({ slug: item.slug, path: item.path || null }); } function renderOnboarding() { const panel = $("onboardingPanel"); const m = state.model; if (!m || m.onboarded) { panel.innerHTML = ""; return; } const items = discoverItems().slice(0, 12); const picks = items.length ? '
' + items.map((r, i) => { const id = "onboardPick" + i; const value = encodedRepo(r); return ''; }).join("") + '
' : '
Discover repositories and local projects to start picking what this hub should track.
'; panel.innerHTML = '
Set up your Work Hub
Pick GitHub repositories or local projects to track. Your choices stay local under ~/.copilot/extensions/work-hub/artifacts.
' + picks + '
'; $("onboardDiscover").addEventListener("click", () => loadDiscover(true)); $("onboardManage").addEventListener("click", () => switchTab("manage")); $("onboardTrack").addEventListener("click", trackSelectedOnboarding); panel.querySelectorAll("[data-onboard-pick]").forEach((el) => el.addEventListener("change", () => { if (el.checked) state.onboarding.selected.add(el.dataset.onboardPick); else state.onboarding.selected.delete(el.dataset.onboardPick); renderOnboarding(); })); } function renderStats(s) { const cards = [ { v: s.repoCount, l: "repos", tab: "repos" }, { v: s.needsHumanPrCount, l: "PRs need review", cls: s.needsHumanPrCount ? "alert" : "", chip: "needs-review" }, { v: s.humanIssueCount, l: "human issues", cls: s.humanIssueCount ? "warn" : "", chip: "human" }, { v: s.openPrCount, l: "open PRs", chip: "open-pr" }, { v: s.openIssueCount, l: "open issues", chip: "open-issue" }, { v: s.staleReleaseCount, l: "stale releases", cls: s.staleReleaseCount ? "warn" : "", chip: "stale-release" }, { v: s.failedDeployCount, l: "failed deploys", cls: s.failedDeployCount ? "alert" : "", chip: "failing-deploy" }, { v: s.activeSessionCount, l: "recent sessions", chip: "active-session" }, ]; $("stats").innerHTML = cards.map((c, i) => '
' + esc(c.v) + '' + esc(c.l) + '
').join(""); $("stats").querySelectorAll(".stat").forEach((el) => el.addEventListener("click", () => { const c = cards[Number(el.dataset.i)]; if (c.tab) { switchTab(c.tab); return; } if (c.chip) { state.filters.chips = new Set([c.chip]); renderChips(); renderWork(); switchTab("work"); } })); } function renderPlan(m) { const total = m.recommendations.reduce((a, r) => a + r.plannedMinutes, 0); const intentLabels = { balanced: "balanced priorities", prs: "PRs and reviews", "new-code": "new code", "issue-triage": "issue triage", maintenance: "maintenance" }; const intent = intentLabels[m.preferences.focusIntent || "balanced"] || "balanced priorities"; $("planSummary").innerHTML = m.recommendations.length ? "Given you're " + esc(m.preferences.mood) + " with " + esc(m.preferences.minutes) + " min (" + esc(m.preferences.busyness) + ") and want " + esc(intent) + ", here's what to do and how (~" + total + " min):" : "No focus items right now — you're all caught up. 🎉"; $("focusList").innerHTML = m.recommendations.length ? m.recommendations.map((it, i) => itemHtml(it, { rank: i + 1, planned: it.plannedMinutes })).join("") : '
Nothing needs your attention. Enjoy the calm.
'; wireRepoLinks($("focusList")); wireItemCards($("focusList")); } function wireRepoLinks(root) { root.querySelectorAll(".repo-link[data-detail]").forEach((el) => el.addEventListener("click", (e) => { e.stopPropagation(); openRepoDetail(el.dataset.detail); })); } function itemHtml(it, opts = {}) { const badges = (it.tags || []).map((t) => '' + esc(t.replace(/-/g, " ")) + '').join(""); const rank = opts.rank ? '#' + opts.rank + '' : ""; const right = opts.planned ? '' + esc(opts.planned) + ' min' : '' + esc(it.minutes) + ' min'; const link = it.url ? '' + esc(it.title) + '' : esc(it.title); const actionable = (it.kind === "pr" || it.kind === "issue") && it.number; const jumpable = it.kind === "session" && it.sessionId; const attrs = actionable ? ' class="card item actionable kind-' + esc(it.kind) + '" data-item-repo="' + esc(it.repo) + '" data-item-type="' + esc(it.kind) + '" data-item-number="' + esc(it.number) + '"' : ' class="card item kind-' + esc(it.kind) + '"'; const guidance = opts.rank && (it.what || it.how) ? '
' + (it.what ? '
What: ' + esc(it.what) + '
' : "") + (it.how ? '
How: ' + esc(it.how) + '
' : "") + '
' : ""; return '' + '
' + esc(it.kind) + '' + link + '
' + rank + right + '
' + '
· ' + esc(it.detail) + '
' + (badges ? '
' + badges + '
' : "") + (it.reasons && it.reasons.length ? '
Why now: ' + esc(it.reasons.join(", ")) + (opts.rank ? '' : ' · score ' + esc(it.score)) + '
' : "") + guidance + (actionable ? '
Click to view & act →
' : "") + (jumpable ? '
' : "") + ''; } function wireItemCards(root) { root.querySelectorAll(".item.actionable").forEach((el) => el.addEventListener("click", (e) => { if (e.target.closest("a, .repo-link")) return; openItemDetail(el.dataset.itemRepo, el.dataset.itemType, el.dataset.itemNumber); })); root.querySelectorAll(".jump-btn").forEach((el) => el.addEventListener("click", (e) => { e.stopPropagation(); jumpToSession(el); })); } async function jumpToSession(btn) { const original = btn.textContent; btn.disabled = true; btn.textContent = "Jumping…"; try { const res = await api("/api/session/jump", { method: "POST", body: JSON.stringify({ sessionId: btn.dataset.jump, repo: btn.dataset.jumpRepo, branch: btn.dataset.jumpBranch }) }); btn.textContent = "✓ " + (res.message || "Requested"); } catch (e) { btn.textContent = "⚠ " + e.message; btn.disabled = false; setTimeout(() => { btn.textContent = original; }, 3000); } } function renderRepoFilter(repos) { const cur = state.filters.repo; $("repoFilter").innerHTML = '' + repos.map((r) => '').join(""); $("repoFilter").value = cur; } function renderChips() { $("chips").innerHTML = CHIP_DEFS.map((c) => '').join("") + (state.filters.chips.size ? '' : ""); $("chips").querySelectorAll(".chip").forEach((el) => el.addEventListener("click", () => { const id = el.dataset.chip; if (id === "__clear") state.filters.chips.clear(); else if (state.filters.chips.has(id)) state.filters.chips.delete(id); else state.filters.chips.add(id); renderChips(); renderWork(); })); } function renderWork() { const m = state.model; if (!m) return; const f = state.filters; let items = m.focusItems.slice(); if (f.repo) items = items.filter((it) => it.repo === f.repo); if (f.text) { const q = f.text.toLowerCase(); items = items.filter((it) => (it.title + " " + it.repo + " " + it.detail).toLowerCase().includes(q)); } if (f.chips.size) { const kindChips = [...f.chips].filter((c) => CHIP_KINDS[c]).map((c) => CHIP_KINDS[c]); const tagChips = [...f.chips].filter((c) => !CHIP_KINDS[c]); if (kindChips.length) items = items.filter((it) => kindChips.includes(it.kind)); if (tagChips.length) items = items.filter((it) => tagChips.every((c) => (it.tags || []).includes(c))); } if (f.sort === "recent") items.sort((a, b) => (a.updatedAgeDays ?? 999) - (b.updatedAgeDays ?? 999)); else if (f.sort === "repo") items.sort((a, b) => a.repo.localeCompare(b.repo) || b.score - a.score); else items.sort((a, b) => b.score - a.score); $("workList").innerHTML = items.length ? items.map((it) => itemHtml(it)).join("") : '
No work matches these filters.
'; wireRepoLinks($("workList")); wireItemCards($("workList")); } const CLEANUP_BUCKETS = [ { id: "fresh", label: "Fresh (<2d)" }, { id: "aging", label: "Aging (2–7d)" }, { id: "stale", label: "Stale (7–30d)" }, { id: "ancient", label: "Ancient (30d+)" }, { id: "orphaned", label: "Archived (worktree gone)" }, ]; function cleanupInventory() { const m = state.model; if (!m) return []; return (m.sessionInventory || []).slice(); } function cleanupFiltered() { const c = state.cleanup; let items = cleanupInventory(); if (c.repo) items = items.filter((s) => s.repository === c.repo); if (c.text) { const q = c.text.toLowerCase(); items = items.filter((s) => (s.summary + " " + s.repository + " " + s.branch + " " + s.cwd).toLowerCase().includes(q)); } if (c.buckets.size) items = items.filter((s) => c.buckets.has(s.orphaned ? "orphaned" : s.bucket) || (c.buckets.has("orphaned") && s.orphaned)); if (c.sort === "newest") items.sort((a, b) => (a.ageDays ?? 1e9) - (b.ageDays ?? 1e9)); else if (c.sort === "repo") items.sort((a, b) => a.repository.localeCompare(b.repository) || (b.ageDays ?? 0) - (a.ageDays ?? 0)); else items.sort((a, b) => (b.ageDays ?? -1) - (a.ageDays ?? -1)); return items; } function renderCleanup() { const m = state.model; if (!m) return; const all = cleanupInventory(); // repo filter options const repos = [...new Set(all.map((s) => s.repository))].sort(); const sel = state.cleanup.repo; $("cleanupRepo").innerHTML = '' + repos.map((r) => '').join(""); // stats const s = m.summary; const orphaned = all.filter((x) => x.orphaned).length; const ancient = all.filter((x) => x.bucket === "ancient").length; const cards = [ { v: all.length, l: "total sessions" }, { v: s.staleSessionCount ?? 0, l: "stale or older", cls: (s.staleSessionCount ? "warn" : "") }, { v: ancient, l: "30d+ ancient", cls: ancient ? "warn" : "" }, { v: orphaned, l: "archived (worktree gone)", cls: orphaned ? "alert" : "" }, ]; $("cleanupStats").innerHTML = cards.map((c) => '
' + esc(c.v) + '' + esc(c.l) + '
').join(""); // bucket chips const cnt = (id) => all.filter((x) => id === "orphaned" ? x.orphaned : x.bucket === id).length; $("cleanupChips").innerHTML = CLEANUP_BUCKETS.map((b) => '').join("") + (state.cleanup.buckets.size ? '' : ""); $("cleanupChips").querySelectorAll(".chip").forEach((el) => el.addEventListener("click", () => { const id = el.dataset.cbucket; if (id === "__clear") state.cleanup.buckets.clear(); else if (state.cleanup.buckets.has(id)) state.cleanup.buckets.delete(id); else state.cleanup.buckets.add(id); renderCleanup(); })); // prune stale selections const validIds = new Set(all.map((x) => x.id)); for (const id of [...state.cleanup.selected]) if (!validIds.has(id)) state.cleanup.selected.delete(id); // list const items = cleanupFiltered(); $("cleanupList").innerHTML = items.length ? items.map(cleanupRowHtml).join("") : '
No sessions match these filters. 🎉
'; wireCleanupRows(); // select-all reflects shown selection const shownIds = items.map((x) => x.id); $("cleanupAll").checked = shownIds.length > 0 && shownIds.every((id) => state.cleanup.selected.has(id)); updateCleanupBar(); } function cleanupRowHtml(sn) { const checked = state.cleanup.selected.has(sn.id) ? " checked" : ""; const archived = sn.orphaned; const badge = archived ? '📦 archived' : '' + esc(sn.bucket) + ''; const where = archived ? "worktree removed" : (sn.isWorktree ? "worktree" : "main checkout"); const openBtn = archived ? '' : ''; const menu = '
' + '' + '
' + (archived ? '' : '') + '' + (sn.branch ? '' : '') + '
' + '
'; return '
' + '' + '
' + '
' + esc(sn.summary) + '
' + '
' + esc(sn.repository) + (sn.branch ? ' · ' + esc(sn.branch) + '' : "") + ' · ' + where + ' · last active ' + esc(sn.ageLabel) + ' ago
' + '
' + '
' + badge + openBtn + menu + '
' + '
'; } function closeCleanupMenus() { document.querySelectorAll(".crow-menu.open").forEach((el) => el.classList.remove("open")); } function wireCleanupRows() { $("cleanupList").querySelectorAll("[data-cselect]").forEach((el) => el.addEventListener("change", () => { const id = el.dataset.cselect; if (el.checked) state.cleanup.selected.add(id); else state.cleanup.selected.delete(id); el.closest(".cleanup-row").classList.toggle("selected", el.checked); updateCleanupBar(); const items = cleanupFiltered(); const shownIds = items.map((x) => x.id); $("cleanupAll").checked = shownIds.length > 0 && shownIds.every((x) => state.cleanup.selected.has(x)); })); const doJump = (el) => { el.dataset.jump = el.dataset.cjump || el.dataset.cjump2; jumpToSession(el); }; $("cleanupList").querySelectorAll("[data-cjump]").forEach((el) => el.addEventListener("click", () => doJump(el))); $("cleanupList").querySelectorAll("[data-cjump2]").forEach((el) => el.addEventListener("click", () => { closeCleanupMenus(); doJump(el); })); $("cleanupList").querySelectorAll("[data-cmenu]").forEach((el) => el.addEventListener("click", (e) => { e.stopPropagation(); const menu = $("cleanupList").querySelector('[data-menu-for="' + CSS.escape(el.dataset.cmenu) + '"]'); const wasOpen = menu.classList.contains("open"); closeCleanupMenus(); if (!wasOpen) menu.classList.add("open"); })); $("cleanupList").querySelectorAll("[data-cdel]").forEach((el) => el.addEventListener("click", () => { closeCleanupMenus(); deleteOneSession(el.dataset.cdel); })); $("cleanupList").querySelectorAll("[data-copybranch]").forEach((el) => el.addEventListener("click", () => { navigator.clipboard?.writeText(el.dataset.copybranch); el.textContent = "✓ Copied"; setTimeout(() => { el.textContent = "⧉ Copy branch name"; closeCleanupMenus(); }, 900); })); } async function requestSessionCleanup(list, btn, original) { btn.disabled = true; btn.innerHTML = ' Requesting…'; try { const payload = { sessions: list.map((s) => ({ id: s.id, repo: s.repository, branch: s.branch, summary: s.summary, ageLabel: s.ageLabel, archived: s.orphaned })) }; const res = await api("/api/session/cleanup", { method: "POST", body: JSON.stringify(payload) }); btn.innerHTML = "✓ " + (res.message || "Requested"); list.forEach((s) => state.cleanup.selected.delete(s.id)); setTimeout(() => { load(true); }, 500); } catch (e) { btn.disabled = false; btn.innerHTML = original; $("cleanupSelInfo").textContent = e.message; } } function deleteOneSession(id) { const sn = cleanupInventory().find((s) => s.id === id); if (!sn) return; if (!confirm('Clean up this session?\\n\\n' + sn.repository + (sn.branch ? ' · ' + sn.branch : '') + '\\n"' + sn.summary + '"' + (sn.orphaned ? '\\n\\n(Already archived — this removes the leftover record/worktree.)' : ''))) return; requestSessionCleanup([sn], $("cleanupDelete"), $("cleanupDelete").innerHTML); } function updateCleanupBar() { const n = state.cleanup.selected.size; $("cleanupSelInfo").textContent = n + " selected"; $("cleanupDelete").disabled = n === 0; $("cleanupDelete").innerHTML = n ? "🗑 Clean up " + n + " selected" : "🗑 Clean up selected"; } async function deleteSelectedSessions() { const all = cleanupInventory(); const chosen = all.filter((s) => state.cleanup.selected.has(s.id)); if (!chosen.length) return; requestSessionCleanup(chosen, $("cleanupDelete"), $("cleanupDelete").innerHTML); } function renderRepos() { const m = state.model; if (!m) return; const f = state.filters; let repos = m.repos.slice(); if (f.repoText) { const q = f.repoText.toLowerCase(); repos = repos.filter((r) => (r.slug + " " + (r.description || "") + " " + (r.language || "")).toLowerCase().includes(q)); } const toneRank = { danger: 0, attention: 1, active: 2, neutral: 3, good: 4, muted: 5 }; if (f.repoSort === "release") repos.sort((a, b) => (b.releaseAgeDays ?? 1e9) - (a.releaseAgeDays ?? 1e9)); else if (f.repoSort === "work") repos.sort((a, b) => (b.openPrCount + b.openIssueCount) - (a.openPrCount + a.openIssueCount)); else if (f.repoSort === "name") repos.sort((a, b) => a.slug.localeCompare(b.slug)); else repos.sort((a, b) => (toneRank[a.status.tone] ?? 9) - (toneRank[b.status.tone] ?? 9) || b.needsHumanPrCount - a.needsHumanPrCount); $("repoGrid").innerHTML = repos.map(repoHtml).join(""); $("repoGrid").querySelectorAll("[data-detail]").forEach((el) => el.addEventListener("click", (e) => { e.stopPropagation(); openRepoDetail(el.dataset.detail); })); $("repoGrid").querySelectorAll(".expand-btn:not([data-detail])").forEach((el) => el.addEventListener("click", (e) => { e.stopPropagation(); el.closest(".repo-card").classList.toggle("open"); })); $("repoGrid").querySelectorAll(".repo-card").forEach((el) => el.addEventListener("click", (e) => { if (e.target.closest("a,button")) return; openRepoDetail(el.dataset.repo); })); wireSubItems($("repoGrid")); } function wireSubItems(root) { root.querySelectorAll(".actionable-sub").forEach((el) => el.addEventListener("click", (e) => { if (e.target.closest("a")) return; e.stopPropagation(); openItemDetail(el.dataset.openRepo, el.dataset.openType, el.dataset.openNumber); })); } function repoHtml(r) { const sub = [ ...r.prs.map((p) => ({ t: "PR #" + p.number + " " + p.title, u: p.url, m: p.author, type: "pr", n: p.number })), ...r.issues.map((i) => ({ t: "Issue #" + i.number + " " + i.title, u: i.url, m: i.author, type: "issue", n: i.number })), ]; const subHtml = sub.length ? sub.slice(0, 12).map((s) => '
' + esc(s.t) + '' + esc(s.m) + '
').join("") : '
No open PRs or issues.
'; const local = r.localGit && r.localGit.available ? ((r.localGit.branchLine || "git").replace(/^## /, "") + " · " + (r.localGit.dirtyCount || 0) + " changed") : "no local checkout"; return '
' + '
' + esc(r.slug) + '' + (r.isPrivate ? ' private' : "") + '
' + esc(r.description || r.language || r.defaultBranch) + '
' + '' + esc(r.status.label) + '
' + '
' + metric(r.openPrCount, "PRs") + metric(r.openIssueCount, "Issues") + metric(r.needsHumanPrCount, "Review") + metric(r.releaseAgeLabel, "Release") + '
' + '
' + esc(r.status.detail) + '
' + (r.latestDeploy ? '
🚀 ' + esc(r.latestDeploy.environment) + ' ' + esc(r.latestDeploy.state) + ' ' + esc(ageLabelClient(r.latestDeploy.ageDays)) + ' ago' + (r.deployments.length > 1 ? ' +' + (r.deployments.length - 1) + ' env' : "") + '
' : '
🚀 no deployments
') + '
🖥 ' + esc(local) + '
' + '
' + '' + (sub.length ? '' : "") + '
' + '
' + subHtml + '
' + '
'; } function ageLabelClient(days) { if (days === null || days === undefined) return "unknown"; if (days === 0) return "today"; if (days === 1) return "1 day"; if (days < 30) return days + " days"; const months = Math.floor(days / 30); if (months < 18) return months + " mo"; return Math.floor(days / 365) + " yr"; } function metric(v, l) { return '
' + esc(v) + '' + esc(l) + '
'; } function renderErrors(errors) { $("errorList").innerHTML = errors.length ? errors.slice(0, 20).map((e) => '
' + esc(e.repo || e.source || "source") + ': ' + esc(e.message) + '
').join("") : '
All data sources returned successfully.
'; } /* ---- repo detail drawer ---- */ function openRepoDetail(slug) { const m = state.model; if (!m) return; const r = m.repos.find((x) => x.slug === slug); if (!r) return; const rel = r.latestRelease; const deployHtml = r.deployments && r.deployments.length ? r.deployments.map((d) => '
' + (d.url ? '' + esc(d.environment) + '' : '' + esc(d.environment) + '') + '
ref ' + esc(d.ref || "—") + '
' + esc(d.state) + '
' + esc(ageLabelClient(d.ageDays)) + ' ago
').join("") : '
No deployments found.
'; const prHtml = r.prs.length ? r.prs.map((p) => '
#' + p.number + ' ' + esc(p.title) + '
' + esc(p.author) + ' · ' + esc(p.reviewDecision) + (p.failingChecks ? ' · ⚠ checks failing' : '') + (p.isDraft ? ' · draft' : '') + ' · View & act →
').join("") : '
No open PRs.
'; const issueHtml = r.issues.length ? r.issues.map((i) => '
#' + i.number + ' ' + esc(i.title) + '
' + esc(i.author) + ' · ' + i.comments + ' comments' + (i.assignedToJames ? ' · assigned to you' : '') + ' · View & act →
').join("") : '
No open issues.
'; const g = r.localGit || {}; const localHtml = g.available ? '
Branch: ' + esc((g.branchLine || "").replace(/^## /, "") || "?") + '
' + metric(g.dirtyCount || 0, "changed") + metric(g.ahead || 0, "ahead") + metric(g.behind || 0, "behind") + metric(ageLabelClient(g.lastCommitAgeDays), "last commit") + '
' + esc(g.path || "") + '
' : '
No local checkout detected under ~/Projects.
'; const sessionHtml = r.activeSessions && r.activeSessions.length ? r.activeSessions.map((s) => '
' + esc(s.summary) + '
' + esc(s.branch || "?") + ' · updated ' + esc(ageLabelClient(s.ageDays)) + ' ago
').join("") : '
No recent sessions.
'; $("detailDrawer").innerHTML = '

' + esc(r.slug) + '

' + esc(r.description || r.language || r.defaultBranch) + '
' + '
' + '' + esc(r.status.label) + '' + 'Open on GitHub ↗' + (r.isPrivate ? 'private' : '') + (r.language ? '' + esc(r.language) + '' : '') + '★ ' + (r.stars || 0) + '' + '
' + '
' + esc(r.status.detail) + '
' + '

Overview

' + metric(r.openPrCount, "Open PRs") + metric(r.openIssueCount, "Open issues") + metric(r.needsHumanPrCount, "Need review") + metric(ageLabelClient(r.pushedAgeDays), "Last push") + '
' + '

Deployments

' + deployHtml + '
' + '

Latest release

' + (rel ? '
' + (rel.url ? '' + esc(rel.tagName || rel.name) + '' : '' + esc(rel.tagName || rel.name) + '') + '
' + esc(rel.kind) + ' · ' + esc(ageLabelClient(r.releaseAgeDays)) + ' ago
' : '
No release or tag found.
') + '
' + '

Open PRs (' + r.prs.length + ')

' + prHtml + '
' + '

Open issues (' + r.issues.length + ')

' + issueHtml + '
' + '

Local checkout

' + localHtml + '
' + '

Recent sessions

' + sessionHtml + '
'; $("detailOverlay").classList.add("open"); $("detailClose").addEventListener("click", closeRepoDetail); wireSubItems($("detailDrawer")); } function closeRepoDetail() { $("detailOverlay").classList.remove("open"); } /* ---- item detail + actions ---- */ let itemCtx = null; async function openItemDetail(repo, type, number) { itemCtx = { repo, type, number }; $("itemDrawer").innerHTML = '

' + esc(type.toUpperCase()) + ' #' + esc(number) + '

' + esc(repo) + '
'; $("itemOverlay").classList.add("open"); $("itemClose").addEventListener("click", closeItemDetail); try { const it = await api("/api/item?repo=" + encodeURIComponent(repo) + "&type=" + encodeURIComponent(type) + "&number=" + encodeURIComponent(number)); renderItemPane(it); } catch (e) { $("itemDrawer").innerHTML = '

Could not load

' + esc(e.message) + '
'; $("itemClose").addEventListener("click", closeItemDetail); } } function renderItemPane(it) { const isPr = it.type === "pr"; const stateLabel = it.state + (it.isDraft ? " · draft" : ""); const meta = []; meta.push('' + esc(stateLabel) + ''); if (isPr) { meta.push('' + esc(it.reviewDecision) + ''); if (it.checksTotal) meta.push('' + (it.checksFailing ? it.checksFailing + ' failing' : 'checks ok') + ''); meta.push('' + esc(it.mergeable) + ''); } it.labels.forEach((l) => meta.push('' + esc(l) + '')); const metrics = isPr ? '
' + metric('+' + (it.additions ?? 0), "additions") + metric('-' + (it.deletions ?? 0), "deletions") + metric(it.changedFiles ?? 0, "files") + metric(ageLabelClient(it.ageDays), "updated") + '
' : '
' + metric(it.comments.length, "comments") + metric(it.assignees.length, "assignees") + metric(ageLabelClient(it.ageDays), "updated") + '
'; const branchLine = isPr ? '
' + esc(it.headRefName) + ' → ' + esc(it.baseRefName) + '
' : ''; const assignLine = it.assignees.length ? '
Assignees: ' + esc(it.assignees.join(", ")) + '
' : ''; const bodyHtml = it.body.trim() ? '
' + esc(it.body) + '
' : '
No description.
'; const commentsHtml = it.comments.length ? it.comments.map((c) => '
' + esc(c.author || "unknown") + ' · ' + esc(ageLabelClient(c.ageDays)) + ' ago
' + esc(c.body) + '
').join("") : '
No comments yet.
'; const actions = []; actions.push(''); if (isPr) { if (it.isDraft) actions.push(''); actions.push(''); actions.push(''); actions.push(''); actions.push(''); } else { if (it.assignees.map((a) => a.toLowerCase()).includes((state.model.currentLogin || "").toLowerCase())) actions.push(''); else actions.push(''); } if (it.state && it.state.toUpperCase() === "OPEN") actions.push(''); else actions.push(''); $("itemDrawer").innerHTML = '

' + esc(it.title) + '

' + esc(it.repo) + ' · ' + esc(it.type.toUpperCase()) + ' #' + it.number + ' · by ' + esc(it.author || "unknown") + '
' + '
' + meta.join("") + '
' + branchLine + assignLine + '
' + metrics + '
' + '

Description

' + bodyHtml + '
' + '

Work on this

' + (isPr ? "" : '') + '
Plan proposes an approach; Implement starts coding' + (isPr ? "" : "; Refine spec sharpens requirements; Cloud assigns the issue to a remote coding session") + '. New session in the ' + esc(it.repo) + ' project.
' + '

Actions

' + actions.join("") + '
' + '' + '
' + '

Recent comments (' + it.comments.length + ')

' + commentsHtml + '
'; $("itemClose").addEventListener("click", closeItemDetail); $("itemDrawer").querySelectorAll("[data-act]").forEach((el) => el.addEventListener("click", () => runAct(el.dataset.act, it, el))); } async function runAct(act, it, btn) { const status = $("actStatus"); if (act === "open-gh") { window.open(it.url, "_blank", "noopener,noreferrer"); return; } if (act === "create-session") { const mode = btn && ["implement", "spec", "cloud"].includes(btn.dataset.mode) ? btn.dataset.mode : "plan"; const label = mode === "implement" ? "build" : mode === "spec" ? "spec-refinement" : mode === "cloud" ? "cloud" : "planning"; status.className = "act-status"; status.textContent = "Requesting a " + label + " session…"; $("itemDrawer").querySelectorAll("[data-act]").forEach((b) => b.disabled = true); try { const res = await api("/api/item/session", { method: "POST", body: JSON.stringify({ repo: it.repo, type: it.type, number: it.number, title: it.title, mode }) }); status.className = "act-status ok"; status.textContent = res.message || "Session requested — check your chat."; } catch (e) { status.className = "act-status err"; status.textContent = e.message; } finally { $("itemDrawer").querySelectorAll("[data-act]").forEach((b) => b.disabled = false); } return; } if ((act === "merge" || act === "close") && !confirm("Are you sure you want to " + act + " " + it.type.toUpperCase() + " #" + it.number + "?")) return; const body = act === "comment" ? ($("commentBox") ? $("commentBox").value.trim() : "") : ""; if (act === "comment" && !body) { status.className = "act-status err"; status.textContent = "Enter a comment first."; return; } status.className = "act-status"; status.textContent = "Working…"; $("itemDrawer").querySelectorAll("[data-act]").forEach((b) => b.disabled = true); try { const res = await api("/api/item/action", { method: "POST", body: JSON.stringify({ repo: it.repo, type: it.type, number: it.number, action: act, body }) }); status.className = "act-status ok"; status.textContent = (res.output || (act + " succeeded.")) + " · refreshing…"; const fresh = await api("/api/item?repo=" + encodeURIComponent(it.repo) + "&type=" + encodeURIComponent(it.type) + "&number=" + encodeURIComponent(it.number)); renderItemPane(fresh); await load(true); const s2 = $("actStatus"); if (s2) { s2.className = "act-status ok"; s2.textContent = res.output || (act + " succeeded."); } } catch (e) { status.className = "act-status err"; status.textContent = e.message; $("itemDrawer").querySelectorAll("[data-act]").forEach((b) => b.disabled = false); } } function closeItemDetail() { $("itemOverlay").classList.remove("open"); itemCtx = null; } /* ---- manage ---- */ async function loadDiscover(force) { state.discoverLoading = true; $("discoverGrid").innerHTML = '
'; try { state.discover = await api("/api/available-repos" + (force ? "?force=1" : "")); } catch (e) { $("discoverGrid").innerHTML = '
' + esc(e.message) + '
'; } finally { state.discoverLoading = false; renderManage(); renderOnboarding(); } } function renderManage() { const m = state.model; if (!m) return; const tracked = m.repos; $("trackedCount").textContent = tracked.length; $("trackedList").innerHTML = tracked.map((r) => '
' + esc(r.slug) + '' + (r.path ? ' local' : "") + '
' + esc(r.status ? r.status.label : "") + ' · ' + r.openPrCount + ' PRs · ' + r.openIssueCount + ' issues
').join("") || '
No repos tracked yet.
'; $("trackedList").querySelectorAll("[data-remove]").forEach((el) => el.addEventListener("click", () => removeRepo(el.dataset.remove))); if (!state.discover) { if (!state.discoverLoading) $("discoverGrid").innerHTML = '
Click “Reload my repos”.
'; return; } const trackedSet = new Set(tracked.map((r) => r.slug.toLowerCase())); const q = state.filters.discoverText.toLowerCase(); const repos = discoverItems().filter((r) => !q || (r.slug + " " + (r.description || "") + " " + (r.path || "")).toLowerCase().includes(q)); $("discoverGrid").innerHTML = repos.slice(0, 120).map((r) => { const isTracked = trackedSet.has(r.slug.toLowerCase()); const payload = esc(encodedRepo(r)); return '
' + esc(r.slug) + '' + (isTracked ? 'tracked' : '') + '
' + '
' + esc(r.description || "No description") + '
' + '
' + (r.source === "local" ? "local project · " + esc(r.path || "") : (r.isPrivate ? "private · " : "") + (r.isFork ? "fork · " : "") + "★ " + (r.stars || 0) + " · pushed " + esc(r.pushedAgeDays === null ? "?" : r.pushedAgeDays + "d ago")) + '
'; }).join("") || '
No repositories match.
'; $("discoverGrid").querySelectorAll("[data-add-repo]").forEach((el) => el.addEventListener("click", () => addRepo(JSON.parse(el.dataset.addRepo)))); } async function trackSelectedOnboarding() { const repos = [...state.onboarding.selected].map((value) => JSON.parse(value)); if (!repos.length) return; try { await api("/api/repos/set", { method: "POST", body: JSON.stringify({ repos, onboarded: true }) }); state.onboarding.selected.clear(); await load(true); } catch (e) { alert(e.message); } } async function addRepo(slug) { const repo = typeof slug === "string" ? { slug } : slug; try { await api("/api/repos/add", { method: "POST", body: JSON.stringify({ repos: [repo] }) }); await load(true); switchTab("manage"); } catch (e) { alert(e.message); } } async function removeRepo(slug) { try { await api("/api/repos/remove", { method: "POST", body: JSON.stringify({ slug }) }); await load(true); switchTab("manage"); } catch (e) { alert(e.message); } } /* ---- wiring ---- */ $("refresh").addEventListener("click", () => load(true)); ["mood", "minutes", "busyness", "focusIntent"].forEach((id) => $(id).addEventListener("change", saveFocus)); document.querySelectorAll(".tab").forEach((el) => el.addEventListener("click", () => switchTab(el.dataset.tab))); $("search").addEventListener("input", (e) => { state.filters.text = e.target.value; renderWork(); }); $("repoFilter").addEventListener("change", (e) => { state.filters.repo = e.target.value; renderWork(); }); $("sort").addEventListener("change", (e) => { state.filters.sort = e.target.value; renderWork(); }); $("repoSearch").addEventListener("input", (e) => { state.filters.repoText = e.target.value; renderRepos(); }); $("repoSort").addEventListener("change", (e) => { state.filters.repoSort = e.target.value; renderRepos(); }); $("cleanupSearch").addEventListener("input", (e) => { state.cleanup.text = e.target.value; renderCleanup(); }); $("cleanupRepo").addEventListener("change", (e) => { state.cleanup.repo = e.target.value; renderCleanup(); }); $("cleanupSort").addEventListener("change", (e) => { state.cleanup.sort = e.target.value; renderCleanup(); }); $("cleanupAll").addEventListener("change", (e) => { const items = cleanupFiltered(); if (e.target.checked) items.forEach((s) => state.cleanup.selected.add(s.id)); else items.forEach((s) => state.cleanup.selected.delete(s.id)); renderCleanup(); }); $("cleanupSelectStale").addEventListener("click", () => { cleanupInventory().forEach((s) => { if (s.orphaned || s.bucket === "stale" || s.bucket === "ancient") state.cleanup.selected.add(s.id); }); renderCleanup(); }); $("cleanupDelete").addEventListener("click", deleteSelectedSessions); $("discoverSearch").addEventListener("input", (e) => { state.filters.discoverText = e.target.value; renderManage(); }); $("reloadDiscover").addEventListener("click", () => loadDiscover(true)); $("addManual").addEventListener("click", () => { const v = $("manualRepo").value.trim(); if (v) { $("manualRepo").value = ""; addRepo(v); } }); $("manualRepo").addEventListener("keydown", (e) => { if (e.key === "Enter") $("addManual").click(); }); $("detailBg").addEventListener("click", closeRepoDetail); $("itemBg").addEventListener("click", closeItemDetail); document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeItemDetail(); closeRepoDetail(); closeCleanupMenus(); } }); document.addEventListener("click", (e) => { if (!e.target.closest(".crow-manage")) closeCleanupMenus(); }); const events = new EventSource("/events"); events.addEventListener("state", () => load(false)); load(false); `; }