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🐛0
+
@@ -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": {