Repository Public
1'use client';2import Link from 'next/link';3import { ArrowRight, Plus } from 'lucide-react';4import { useMissions, useModels, useSpend, useSystem, useWaits } from '@/lib/queries';5import { useUi } from '@/stores/ui';6import { useNow } from '@/hooks/use-viewport';7import { isActive } from '@/lib/status';8import { fmtTime, fmtUsd } from '@/lib/format';9import { Button } from '@/components/ui/button';10import { EmptyState, ErrorState, Skeleton } from '@/components/ui/misc';11import { IllusAllClear, IllusMission } from '@/components/ui/illustrations';12import { WaitCard } from '@/components/waits/wait-card';13import { MissionCard, MissionRow } from '@/components/missions/mission-card';14import { BudgetBar } from '@/components/missions/bits';15import { HealthDot, HealthStrip, healthItems } from '@/components/system/health';1617function SectionTitle({ children, count, aside, tone }: { children: React.ReactNode; count?: number; aside?: React.ReactNode; tone?: 'info' }) {18 return (19 <div className="mb-3 flex items-center gap-2.5">20 <h2 className="text-[13px] font-bold uppercase tracking-[0.07em] text-text-2">{children}</h2>21 {count != null && count > 0 && <span className={tone === 'info' ? 'tnum flex h-5 min-w-5 items-center justify-center rounded-full bg-info-ink px-1.5 text-[11px] font-bold text-info-fg' : 'tnum flex h-5 min-w-5 items-center justify-center rounded-full bg-surface-2 px-1.5 text-[11px] font-semibold text-text-2'}>{count}</span>}22 <span className="flex-1" />23 {aside}24 </div>25 );26}2728export default function HomePage() {29 const waits = useWaits();30 const missions = useMissions();31 const models = useModels().data ?? [];32 const spend = useSpend();33 const system = useSystem();34 const now = useNow(15_000);35 const openNew = useUi((s) => s.openNewMission);36 const active = (missions.data ?? []).filter((m) => isActive(m.status));37 const recent = (missions.data ?? []).filter((m) => !isActive(m.status)).slice(0, 4);38 const pending = waits.data ?? [];39 const s = spend.data;40 const left = s ? s.dailyBudgetUsd - s.todayUsd : null;41 const health = healthItems(system.data, now);42 const problems = health.filter((h) => h.ok === false);43 const unknown = health.filter((h) => h.ok == null).length;4445 return (46 <div className="h-full overflow-y-auto" data-testid="home">47 <div className="mx-auto grid w-full max-w-6xl gap-x-8 gap-y-7 px-4 pb-32 pt-2 md:px-8 md:pb-12 md:pt-7 lg:grid-cols-[minmax(0,1fr)_320px]">48 <div className="flex min-w-0 flex-col gap-8">49 <section aria-label="Needs you" data-testid="needs-you">50 <SectionTitle count={pending.length} tone="info" aside={pending.length > 0 ? <Link href="/approvals/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">Open inbox <ArrowRight className="size-3" aria-hidden /></Link> : null}>51 Needs you52 </SectionTitle>53 {waits.isError ? (54 <div className="card"><ErrorState error={waits.error} onRetry={() => waits.refetch()} /></div>55 ) : waits.isPending ? (56 <Skeleton className="h-36 rounded-md" />57 ) : pending.length === 0 ? (58 <div className="card flex items-center gap-4 px-5 py-4" data-testid="needs-you-empty">59 <div className="shrink-0 scale-[.8]"><IllusAllClear /></div>60 <div className="min-w-0">61 <p className="text-[15px] font-semibold tracking-tight">Nothing needs you</p>62 <p className="mt-0.5 text-[13px] leading-relaxed text-text-2">Approvals and questions from running agents appear here the moment they are raised.</p>63 </div>64 </div>65 ) : (66 <div className="flex flex-col gap-3">67 {pending.map((w) => <WaitCard key={w.id} wait={w} compact />)}68 </div>69 )}70 </section>7172 {/* phones and tablets: spend and health as two compact tiles right after what needs the owner */}73 <div className="-my-2 grid grid-cols-2 gap-3 lg:hidden" data-testid="home-strip">74 <Link href="/spend/" className="card pressable px-3.5 py-2.5">75 <p className="text-[11px] font-semibold uppercase tracking-wide text-muted">Today</p>76 <p className="tnum mt-0.5 truncate text-[17px] font-bold tracking-tight">{fmtUsd(s?.todayUsd)} <span className="text-xs font-normal text-muted">/ {fmtUsd(s?.dailyBudgetUsd, { compact: true })}</span></p>77 <BudgetBar spent={s?.todayUsd} budget={s?.dailyBudgetUsd} className="mt-1.5" label="Daily cap used" />78 </Link>79 <Link href="/system/" className="card pressable px-3.5 py-2.5">80 <p className="text-[11px] font-semibold uppercase tracking-wide text-muted">Health</p>81 <p className="mt-0.5 truncate text-[15px] font-semibold tracking-tight">{problems.length > 0 ? `${problems.length} problem${problems.length === 1 ? '' : 's'}` : unknown === health.length ? 'Unknown' : unknown > 0 ? 'Partly known' : 'All systems OK'}</p>82 <p className="mt-1.5 flex items-center gap-1.5" aria-hidden>83 {health.map((h) => <HealthDot key={h.key} ok={h.ok} />)}84 <span className="truncate text-[11px] text-muted">{problems[0]?.label ?? 'worker · db · keys'}</span>85 </p>86 </Link>87 </div>88 <section aria-label="Running missions" data-testid="running">89 <SectionTitle count={active.length} aside={<Button size="sm" variant="brand" className="h-8 rounded-full px-3.5 font-semibold md:hidden" onClick={() => openNew()} data-testid="new-mission-page"><Plus className="size-3.5" strokeWidth={2.6} /> New mission</Button>}>90 Active missions91 </SectionTitle>92 {missions.isError ? (93 <div className="card"><ErrorState error={missions.error} onRetry={() => missions.refetch()} /></div>94 ) : missions.isPending ? (95 <div className="grid gap-3 md:grid-cols-2"><Skeleton className="h-36 rounded-md" /><Skeleton className="h-36 rounded-md" /></div>96 ) : active.length === 0 ? (97 <div className="card">98 <EmptyState99 illustration={<IllusMission />}100 title="No mission is running"101 description="Give an agent an objective, a budget and a project. It works in a sandbox and comes back to you when it needs a decision."102 action={<Button variant="brand" className="h-9 rounded-full px-5 font-semibold pointer-coarse:h-12" onClick={() => openNew()} data-testid="empty-new-mission"><Plus className="size-4" strokeWidth={2.6} /> New mission</Button>}103 className="py-9"104 />105 </div>106 ) : (107 <div className="grid gap-3 md:grid-cols-2">108 {active.map((m) => <MissionCard key={m.id} mission={m} models={models} />)}109 </div>110 )}111 </section>112113 {recent.length > 0 && (114 <section aria-label="Recently finished">115 <SectionTitle aside={<Link href="/missions/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">All missions <ArrowRight className="size-3" aria-hidden /></Link>}>Recently finished</SectionTitle>116 <div className="card divide-y divide-line overflow-hidden">117 {recent.map((m) => <MissionRow key={m.id} mission={m} models={models} />)}118 </div>119 </section>120 )}121 </div>122123 <aside className="hidden min-w-0 flex-col gap-7 lg:flex">124 <section aria-label="Spend today" data-testid="spend-today">125 <SectionTitle aside={<Link href="/spend/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">Details <ArrowRight className="size-3" aria-hidden /></Link>}>Today</SectionTitle>126 <div className="card p-4">127 {spend.isError ? (128 <ErrorState error={spend.error} onRetry={() => spend.refetch()} className="py-4" />129 ) : (130 <>131 <p className="tnum flex items-baseline gap-2">132 <span className="text-[30px] font-bold leading-none tracking-tight" data-testid="today-usd">{fmtUsd(s?.todayUsd)}</span>133 <span className="text-[13px] text-muted">of {fmtUsd(s?.dailyBudgetUsd, { compact: true })} daily cap</span>134 </p>135 <BudgetBar spent={s?.todayUsd} budget={s?.dailyBudgetUsd} className="mt-3 h-2" label="Daily cap used" />136 <p className="tnum mt-2 text-xs text-muted">137 {left == null ? '—' : left > 0 ? `${fmtUsd(left)} left before new model calls stop for the day` : 'Cap reached — model calls are stopped until tomorrow'}138 </p>139 </>140 )}141 </div>142 </section>143144 <section aria-label="System health" data-testid="health">145 <SectionTitle aside={<Link href="/system/" className="inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline">System <ArrowRight className="size-3" aria-hidden /></Link>}>Health</SectionTitle>146 <div className="card p-3">147 {system.isError ? <ErrorState error={system.error} onRetry={() => system.refetch()} className="py-4" /> : <HealthStrip items={health} />}148 <p className="tnum mt-2 px-1 text-[11px] text-muted">{system.data?.collectedAt ? `as of ${fmtTime(system.data.collectedAt)}` : system.isPending ? 'checking…' : 'collection time unknown'}</p>149 </div>150 </section>151 </aside>152 </div>153 </div>154 );155}156