Skip to content

Commit dce1c64

Browse files
sonpiazclaude
andcommitted
feat: URL-based tab navigation + agent status dots on Hall of Fame
- Tabs now update URL query param (?view=ceo, ?view=kanban, ?view=comms) - Right-click copy link + browser back/forward works - Add online/idle/offline status dots to /agents podium + leaderboard - Wrap Home in Suspense for useSearchParams compatibility Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent c238e91 commit dce1c64

File tree

3 files changed

+54
-30
lines changed

3 files changed

+54
-30
lines changed

app/agents/page.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ const PODIUM_STYLES: Record<number, { border: string; bg: string; label: string;
2626
3: { border: "border-amber-700/40", bg: "bg-amber-700/10", label: "3rd", size: "text-2xl" },
2727
};
2828

29+
const STATUS_DOT: Record<string, string> = {
30+
online: "bg-green-500",
31+
busy: "bg-yellow-500",
32+
idle: "bg-zinc-500",
33+
offline: "bg-zinc-700",
34+
};
35+
2936
const BADGE_COLORS: Record<string, string> = {
3037
"Top Performer": "bg-yellow-500/20 text-yellow-400",
3138
"Most Productive": "bg-emerald-500/20 text-emerald-400",
@@ -83,8 +90,11 @@ export default function HallOfFamePage() {
8390
<div className="text-[10px] font-bold uppercase tracking-wider text-zinc-500 mb-2">
8491
{style.label}
8592
</div>
86-
<div className={cn("font-bold uppercase mb-1", style.size, AGENT_COLORS[agent.name.toLowerCase()] ?? "text-zinc-300")}>
87-
{agent.name}
93+
<div className="flex items-center justify-center gap-2 mb-1">
94+
<span className={cn("h-2 w-2 rounded-full shrink-0", STATUS_DOT[agent.status?.toLowerCase()] ?? STATUS_DOT.offline)} />
95+
<span className={cn("font-bold uppercase", style.size, AGENT_COLORS[agent.name.toLowerCase()] ?? "text-zinc-300")}>
96+
{agent.name}
97+
</span>
8898
</div>
8999
<div className="text-[10px] text-zinc-500 uppercase mb-3">{agent.role}</div>
90100
<div className="text-lg font-bold tabular-nums text-zinc-200">
@@ -153,9 +163,12 @@ export default function HallOfFamePage() {
153163
<td className="px-3 py-2.5">
154164
<Link
155165
href={`/agents/${agent.name.toLowerCase()}`}
156-
className={cn("font-bold uppercase hover:underline", AGENT_COLORS[agent.name.toLowerCase()] ?? "text-zinc-300")}
166+
className="flex items-center gap-2 hover:underline"
157167
>
158-
{agent.name}
168+
<span className={cn("h-1.5 w-1.5 rounded-full shrink-0", STATUS_DOT[agent.status?.toLowerCase()] ?? STATUS_DOT.offline)} />
169+
<span className={cn("font-bold uppercase", AGENT_COLORS[agent.name.toLowerCase()] ?? "text-zinc-300")}>
170+
{agent.name}
171+
</span>
159172
</Link>
160173
</td>
161174
<td className="text-right px-3 py-2.5 tabular-nums text-zinc-400">{agent.tasksCompleted}</td>

app/page.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useState, useMemo } from "react";
3+
import { useState, useMemo, useCallback, Suspense } from "react";
4+
import { useSearchParams, useRouter } from "next/navigation";
45
import { useQuery } from "convex/react";
56
import { api } from "@/convex/_generated/api";
67
import { Id } from "@/convex/_generated/dataModel";
@@ -26,6 +27,14 @@ import { sortAgents, AGENT_ORDER } from "@/lib/constants";
2627

2728
/** AGT-181: 2-panel layout — [Sidebar 220px] | [Kanban flex-1]. Agent Profile → Modal, Activity → Drawer */
2829
export default function Home() {
30+
return (
31+
<Suspense>
32+
<HomeContent />
33+
</Suspense>
34+
);
35+
}
36+
37+
function HomeContent() {
2938
const [date, setDate] = useState(new Date());
3039
const [dateMode, setDateMode] = useState<DateFilterMode>("day");
3140
const [settingsOpen, setSettingsOpen] = useState(false);
@@ -36,7 +45,13 @@ export default function Home() {
3645
const [dispatchQueueOpen, setDispatchQueueOpen] = useState(false);
3746
const [agentSettingsId, setAgentSettingsId] = useState<Id<"agents"> | null>(null);
3847
const [shortcutsHelpOpen, setShortcutsHelpOpen] = useState(false);
39-
const [activeViewTab, setActiveViewTab] = useState<MainViewTab>("kanban");
48+
const searchParams = useSearchParams();
49+
const router = useRouter();
50+
const viewParam = searchParams.get("view") as MainViewTab | null;
51+
const activeViewTab: MainViewTab = viewParam && ["ceo", "kanban", "comms"].includes(viewParam) ? viewParam : "kanban";
52+
const setActiveViewTab = useCallback((tab: MainViewTab) => {
53+
router.replace(`/?view=${tab}`, { scroll: false });
54+
}, [router]);
4055

4156
const agents = useQuery(api.agents.list);
4257

components/evox/ViewTabs.tsx

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,16 @@ interface ViewTabsProps {
1111
className?: string;
1212
}
1313

14-
const tabs: { id: MainViewTab; label: string }[] = [
15-
{ id: "ceo", label: "Overview" },
16-
{ id: "kanban", label: "Kanban" },
17-
{ id: "comms", label: "Comms" },
18-
];
19-
20-
const externalLinks: { label: string; href: string }[] = [
21-
{ label: "Team", href: "/agents" },
14+
const tabs: { id: MainViewTab; label: string; href: string }[] = [
15+
{ id: "ceo", label: "Overview", href: "/?view=ceo" },
16+
{ id: "kanban", label: "Kanban", href: "/?view=kanban" },
17+
{ id: "comms", label: "Comms", href: "/?view=comms" },
2218
];
2319

2420
/**
25-
* AGT-206: View Tabs — Vercel-style navigation
26-
* Clean text tabs with active underline indicator.
27-
* "Team" links to /agents (Hall of Fame + Career Profiles).
21+
* AGT-206: View Tabs — Vercel-style navigation with URL links.
22+
* Each tab updates the URL query param (?view=...).
23+
* "Team" navigates to /agents.
2824
*/
2925
export function ViewTabs({ activeTab, onTabChange, className }: ViewTabsProps) {
3026
return (
@@ -37,12 +33,15 @@ export function ViewTabs({ activeTab, onTabChange, className }: ViewTabsProps) {
3733
aria-label="Dashboard views"
3834
>
3935
{tabs.map((tab) => (
40-
<button
36+
<Link
4137
key={tab.id}
42-
type="button"
38+
href={tab.href}
4339
role="tab"
4440
aria-selected={activeTab === tab.id}
45-
onClick={() => onTabChange(tab.id)}
41+
onClick={(e) => {
42+
e.preventDefault();
43+
onTabChange(tab.id);
44+
}}
4645
className={cn(
4746
"relative px-3 py-2.5 text-sm transition-colors whitespace-nowrap shrink-0",
4847
activeTab === tab.id
@@ -54,18 +53,15 @@ export function ViewTabs({ activeTab, onTabChange, className }: ViewTabsProps) {
5453
{activeTab === tab.id && (
5554
<span className="absolute bottom-0 left-0 right-0 h-px bg-white" />
5655
)}
57-
</button>
58-
))}
59-
60-
{externalLinks.map((link) => (
61-
<Link
62-
key={link.href}
63-
href={link.href}
64-
className="relative px-3 py-2.5 text-sm text-zinc-500 hover:text-zinc-300 transition-colors whitespace-nowrap shrink-0"
65-
>
66-
{link.label}
6756
</Link>
6857
))}
58+
59+
<Link
60+
href="/agents"
61+
className="relative px-3 py-2.5 text-sm text-zinc-500 hover:text-zinc-300 transition-colors whitespace-nowrap shrink-0"
62+
>
63+
Team
64+
</Link>
6965
</div>
7066
);
7167
}

0 commit comments

Comments
 (0)