diff --git a/changelog.d/added-10632.md b/changelog.d/added-10632.md new file mode 100644 index 0000000000..16042d339d --- /dev/null +++ b/changelog.d/added-10632.md @@ -0,0 +1 @@ +- dashboard: Agents sidebar link + up-next agent tiles in top navbar (#10632) diff --git a/src/pkg/dashboard/agent_collapsed_tiles_test.go b/src/pkg/dashboard/agent_collapsed_tiles_test.go index 85c1be73a2..dd7586091a 100644 --- a/src/pkg/dashboard/agent_collapsed_tiles_test.go +++ b/src/pkg/dashboard/agent_collapsed_tiles_test.go @@ -40,6 +40,26 @@ func TestAgentsCollapsedSummaryRendersPerAgentTiles(t *testing.T) { } } +func TestAgentsSidebarLinkAndNavbarUpNextContracts(t *testing.T) { + html := indexHTML(t) + for _, want := range []string{ + `data-section="agents-section" data-action="ocNavigate" data-arg0="agents-section">🤖Agents`, + `id="agent-navbar-upnext"`, + `class="agent-navbar-upnext"`, + `function renderAgentNavbarUpNext(agents, nowMs)`, + `sortAgentsForCollapsedTiles(agents, now).filter(a => agentNavbarHasSchedule(a, now)).slice(0, 3)`, + `function openAgentsUpNextPanel()`, + `data-action="openAgentsUpNextPanel"`, + `renderAgentNavbarUpNext(agents, Date.now())`, + `refreshAgentNavbarUpNextCountdowns(now);`, + `@media (max-width: 1180px) { .agent-navbar-upnext { display: none !important; } }`, + } { + if !strings.Contains(html, want) { + t.Fatalf("agents sidebar/navbar up-next contract missing %q", want) + } + } +} + func TestAgentsCollapsedTileSortOrder(t *testing.T) { node, err := exec.LookPath("node") if err != nil { diff --git a/src/pkg/dashboard/static/index.html b/src/pkg/dashboard/static/index.html index 5bb8055b2a..c9b3f7bd2b 100644 --- a/src/pkg/dashboard/static/index.html +++ b/src/pkg/dashboard/static/index.html @@ -3311,7 +3311,22 @@ .agent-collapsed-tile.stopped .agent-tile-dot, .agent-collapsed-tile.needs-login .agent-tile-dot, .repo-collapsed-tile.error .agent-tile-dot { background: var(--red); box-shadow: 0 0 0 1px color-mix(in srgb, var(--red) 40%, transparent); } .repo-collapsed-tile.blocked .agent-tile-dot { background: var(--yellow); box-shadow: 0 0 0 1px color-mix(in srgb, var(--yellow) 40%, transparent); } .agent-collapsed-tile.disabled .agent-tile-dot, .repo-collapsed-tile.paused .agent-tile-dot, .repo-collapsed-tile.idle .agent-tile-dot { background: var(--muted); box-shadow: none; } + .agent-navbar-upnext { display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; max-width: min(480px, 34vw); overflow: hidden; } + .agent-navbar-upnext[hidden] { display: none; } + .agent-navbar-tile { flex: 0 1 150px; min-width: 0; height: 32px; border: 1px solid var(--line-subtle); border-radius: var(--r-pill); background: color-mix(in srgb, var(--surface-2) 64%, transparent); color: var(--text); cursor: pointer; display: inline-grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: 1fr 1fr; align-items: center; column-gap: var(--sp-2); box-sizing: border-box; font: inherit; font-size: var(--fs-2xs); line-height: 1.05; text-align: left; } + .agent-navbar-tile:hover, .agent-navbar-tile:focus-visible { border-color: var(--border); background: var(--surface-2); outline: none; } + .agent-navbar-tile.up-next { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent); } + .agent-navbar-tile.active { border-color: color-mix(in srgb, var(--green) 70%, var(--line-subtle)); } + .agent-navbar-tile.stopped, .agent-navbar-tile.needs-login { border-color: color-mix(in srgb, var(--red) 45%, var(--line-subtle)); } + .agent-navbar-tile .agent-tile-dot { grid-row: 1 / 3; margin-left: var(--sp-2); } + .agent-navbar-tile .agent-tile-name { grid-column: 2; } + .agent-navbar-tile .agent-tile-next { grid-column: 2 / 4; color: var(--muted); } + .agent-navbar-tile .agent-up-next { grid-column: 3; } + .agent-navbar-tile.active .agent-tile-dot { animation: green-pulse 2.4s ease-in-out infinite; } + .agent-navbar-tile.stopped .agent-tile-dot, .agent-navbar-tile.needs-login .agent-tile-dot { background: var(--red); box-shadow: 0 0 0 1px color-mix(in srgb, var(--red) 40%, transparent); } + @media (max-width: 1180px) { .agent-navbar-upnext { display: none !important; } } @media (prefers-reduced-motion: reduce) { .agent-collapsed-tile.active .agent-tile-dot { animation: none; } } + @media (prefers-reduced-motion: reduce) { .agent-navbar-tile.active .agent-tile-dot { animation: none; } } .mini-summary-strip .sparkline { display: inline-flex; margin: 0; } .mini-summary-strip .sparkline-line, .mini-spark .sparkline-line { stroke-dasharray: 160; stroke-dashoffset: 160; animation: mini-spark-draw 650ms ease-out forwards; } .mini-gauge-track { stroke: var(--line-subtle); } @@ -4219,6 +4234,7 @@

🐝 Getting Started🪙Tokens 💵Cost 🔎Review Queue + 🤖Agents 📋Audit Log 🔍Diagnostics @@ -4291,6 +4307,7 @@

🐝 Getting Started +
@@ -5729,8 +5746,9 @@

What sh } function refreshAgentsCollapsedCountdowns(nowMs) { const strip = document.querySelector('.agents-collapsed-tiles'); - if (!strip) return; const now = Number.isFinite(nowMs) ? nowMs : Date.now(); + refreshAgentNavbarUpNextCountdowns(now); + if (!strip) return; const tiles = Array.from(strip.querySelectorAll('.agent-collapsed-tile')); tiles.forEach(tile => { const rawNext = String(tile.getAttribute('data-agent-next-kick-ms') || '').trim(); @@ -5761,6 +5779,50 @@

What sh if (window._agentsCollapsedCountdownInterval) return; window._agentsCollapsedCountdownInterval = setInterval(refreshAgentsCollapsedCountdowns, AGENTS_COLLAPSED_COUNTDOWN_TICK_MS); } + function agentNavbarHasSchedule(a, nowMs) { + return !!a && !agentIsDisabled(a) && (a.busy === 'working' || Number.isFinite(agentCollapsedNextKickMs(a, nowMs)) || a.continuous === true); + } + function agentNavbarTileHtml(a, nowMs, isUpNext) { + const name = (a && (a.displayName || a.name)) || 'agent'; + const disabled = agentIsDisabled(a); + const isDown = a && a.state !== 'running'; + const active = !!a && !disabled && a.busy === 'working'; + const auth = agentCollapsedAuthInfo(a || {}, isDown); + const nextMs = agentCollapsedNextKickMs(a, nowMs); + const countdown = agentCollapsedNextLabel(a, nowMs); + const classes = ['agent-navbar-tile', active ? 'active' : '', isDown && !disabled ? 'stopped' : '', auth.needsLogin ? 'needs-login' : '', isUpNext ? 'up-next' : ''].filter(Boolean).join(' '); + const title = `${name} +next kick: ${agentCollapsedTimestampDetail(nextMs, a && (a.nextKickAt || a.nextKick))}`; + const upNext = isUpNext ? 'up next' : ''; + return ``; + } + function renderAgentNavbarUpNext(agents, nowMs) { + const wrap = document.getElementById('agent-navbar-upnext'); + if (!wrap) return; + const now = Number.isFinite(nowMs) ? nowMs : Date.now(); + const list = sortAgentsForCollapsedTiles(agents, now).filter(a => agentNavbarHasSchedule(a, now)).slice(0, 3); + wrap.hidden = list.length === 0; + wrap.innerHTML = list.length ? list.map((a, i) => agentNavbarTileHtml(a, now, i === 0)).join('') : ''; + if (list.length) ensureAgentsCollapsedCountdownTicker(); + } + function refreshAgentNavbarUpNextCountdowns(nowMs) { + const wrap = document.getElementById('agent-navbar-upnext'); + if (!wrap || wrap.hidden) return; + const now = Number.isFinite(nowMs) ? nowMs : Date.now(); + Array.from(wrap.querySelectorAll('.agent-navbar-tile')).forEach(tile => { + const rawNext = String(tile.getAttribute('data-agent-next-kick-ms') || '').trim(); + const nextMs = rawNext ? Number(rawNext) : NaN; + const countdown = tile.getAttribute('data-agent-active') === '1' ? 'now' : formatKickCountdown(Number.isFinite(nextMs) ? nextMs - now : null); + const el = tile.querySelector('.agent-tile-countdown'); + if (el && el.textContent !== countdown) el.textContent = countdown; + }); + } + function openAgentsUpNextPanel() { + navPeekRestore('agents-section', { instant: true }); + const expanded = navPeekExpand('agents-section'); + if (!expanded && isSectionCollapsed('agents-section')) toggleSection('agents-section', { collapsed: false, persist: false, navPeek: true }); + ocAfterNavLayoutSettles(['agents-section'], function() { ocScrollSectionIntoView('agents-section'); }); + } function agentsCollapsedTilesHtml(agents, nowMs) { const list = sortAgentsForCollapsedTiles(agents, nowMs); if (!list.length) return ''; @@ -9798,6 +9860,7 @@

What sh })() : (Array.isArray(agents) ? agents : []); refreshSectionCardShell('agents-section'); setSectionSummary('agents-section', agentsSectionSummary(agents)); + renderAgentNavbarUpNext(agents, Date.now()); const el = document.getElementById('agents'); // Preserve custom prompt input values across re-renders const savedInputs = {}; @@ -9807,6 +9870,7 @@

What sh }); const { sorted: sortedAgents } = _sortAgentsBySidebar(agents); if (sortedAgents.length === 0) { + renderAgentNavbarUpNext([], Date.now()); setSectionSummary('agents-section', agentsSectionSummary([])); setIfChanged(el, emptyStateCard('🤖', 'Add your first agent', 'Agents are the workers that scan repos, review changes, keep CI healthy, and write docs. Start from a quick template or build one from scratch. ' + diff --git a/src/pkg/dashboard/webstatic/style_ratchet_test.go b/src/pkg/dashboard/webstatic/style_ratchet_test.go index 04237ebc69..2c5b7697d5 100644 --- a/src/pkg/dashboard/webstatic/style_ratchet_test.go +++ b/src/pkg/dashboard/webstatic/style_ratchet_test.go @@ -28,7 +28,7 @@ var styleRatchetBaselines = map[string]styleRatchetCounts{ "operator static/index.html": { inlineStyles: 1588, rawColors: 143, - rawFontSizes: 666, + rawFontSizes: 666, rawPadding: 229, rawBorderRadii: 176}, "contributor landing": {