Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them. | 1 | /** |
| 2 | * Today's parts (routes/workspace/home.tsx): the day's agent work, Needs | |
| 3 | * attention, Spent today and Start here. Each takes what lib/today.ts | |
| 4 | * worked out and only draws it. | |
| 5 | */ | |
| 6 | import { | |
| 7 | AlertTriangle, | |
| 8 | ArrowRight, | |
| 9 | AtSign, | |
| 10 | Bell, | |
| 11 | Bot, | |
| 12 | CircleDollarSign, | |
| 13 | GitMerge, | |
| 14 | GitPullRequest, | |
| 15 | Hourglass, | |
| 16 | Mail, | |
| 17 | Rocket, | |
| 18 | Server, | |
| 19 | Sparkles, | |
| 20 | User, | |
| 21 | } from "lucide-react"; | |
| 22 | import type { ReactNode } from "react"; | |
| 23 | import { Link } from "react-router"; | |
| 24 | ||
| 25 | import { | |
| 26 | type Attention, | |
| 27 | type AttentionKind, | |
| 28 | type AttentionRow, | |
| 29 | type DayWork, | |
| 30 | type Spend, | |
| 31 | type Stake, | |
| 32 | type TaskOutcome, | |
| 33 | OUTCOME_LABEL, | |
| 34 | SOURCE_LABEL, | |
| 35 | dollars, | |
| 36 | trendLabel, | |
| 37 | waited, | |
| 38 | } from "../lib/today"; | |
| 39 | import { Skeleton } from "./ui/skeleton"; | |
| 40 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 41 | ||
| 42 | export function Card({ title, link, children, className = "" }: { title: ReactNode; link?: { label: string; to: string } | null; children: ReactNode; className?: string }) { | |
| 43 | return ( | |
| 44 | <section className={`rounded-2xl border border-line bg-surface ${className}`}> | |
| 45 | <header className="flex items-center gap-2 px-5 pt-4 pb-3"> | |
| 46 | <h2 className="text-sm font-semibold text-fg">{title}</h2> | |
| 47 | {link && ( | |
| 48 | <Link to={link.to} className="ml-auto inline-flex items-center gap-1 text-xs text-muted hover:text-fg"> | |
| 49 | {link.label} | |
| 50 | <ArrowRight size={12} /> | |
| 51 | </Link> | |
| 52 | )} | |
| 53 | </header> | |
| 54 | {children} | |
| 55 | </section> | |
| 56 | ); | |
| 57 | } | |
| 58 | ||
| 59 | export function Quiet({ children }: { children: ReactNode }) { | |
| 60 | return <p className="px-5 pb-5 text-sm text-muted">{children}</p>; | |
| 61 | } | |
| 62 | ||
| 63 | // --- The day's work ----------------------------------------------------------- | |
| 64 | ||
| 65 | /** Each outcome's mark: status colours, always beside a label in the legend. */ | |
| 66 | const MARK: Record<TaskOutcome, string> = { | |
| 67 | accepted: "bg-success", | |
| 68 | fixed: "bg-warn", | |
| 69 | finished: "bg-info", | |
| 70 | open: "bg-accent/45", | |
| 71 | dropped: "bg-danger", | |
| 72 | }; | |
| 73 | ||
| 74 | const percent = (value: number) => `${Math.round(value * 100)}%`; | |
| 75 | ||
| 76 | function Stat({ label, value, mark }: { label: string; value: number; mark?: string }) { | |
| 77 | return ( | |
| 78 | <div className="min-w-0"> | |
| 79 | <div className="flex items-center gap-1.5 text-xs text-muted"> | |
| 80 | {mark && <span className={`size-2 shrink-0 rounded-[2px] ${mark}`} aria-hidden />} | |
| 81 | <span className="truncate">{label}</span> | |
| 82 | </div> | |
| 83 | <div className="mt-1 text-xl font-semibold text-fg tabular-nums">{value.toLocaleString("en-US")}</div> | |
| 84 | </div> | |
| 85 | ); | |
| 86 | } | |
| 87 | ||
| 88 | export function DayCard({ work }: { work: DayWork | null }) { | |
| 89 | if (!work) { | |
| 90 | return ( | |
| 91 | <section className="rounded-2xl border border-line bg-surface p-5"> | |
| 92 | <p className="text-sm text-muted">Today's agent work couldn't be read right now. It shows here once Agents answers.</p> | |
| 93 | </section> | |
| 94 | ); | |
| 95 | } | |
| 96 | const { counts, rate, lastWeek, tasks } = work; | |
| 97 | const trend = trendLabel(rate, lastWeek); | |
| 98 | const up = rate && lastWeek ? Math.round(rate.value * 100) - Math.round(lastWeek.value * 100) : 0; | |
| 99 | // Without Code, nothing is reviewed: no accepted or fixed to count. | |
| 100 | const reviewed = work.code === "read"; | |
| 101 | const shown = (Object.keys(MARK) as TaskOutcome[]).filter((o) => counts[o] > 0 || (reviewed && (o === "accepted" || o === "fixed")) || o === "open"); | |
| 102 | return ( | |
| 103 | <section className="grid overflow-hidden rounded-2xl border border-line bg-surface md:grid-cols-[minmax(0,15rem)_minmax(0,1fr)]"> | |
| 104 | <div className="border-b border-line p-5 md:border-r md:border-b-0"> | |
| 105 | {work.code === "no_access" ? ( | |
| 106 | <> | |
| 107 | <h2 className="text-sm font-semibold text-fg">Finished today</h2> | |
| 108 | <div className="mt-2 text-5xl font-semibold tracking-tight text-fg tabular-nums">{(counts.accepted + counts.fixed + counts.finished).toLocaleString("en-US")}</div> | |
| 109 | <p className="mt-2 text-xs leading-relaxed text-muted">How much was accepted the first time is measured on pull requests, which are part of Code.</p> | |
| 110 | </> | |
| 111 | ) : ( | |
| 112 | <> | |
| 113 | <h2 className="text-sm font-semibold text-fg">Accepted first time</h2> | |
| 114 | {rate ? ( | |
| 115 | <> | |
| 116 | <div className="mt-2 text-5xl font-semibold tracking-tight text-fg tabular-nums">{percent(rate.value)}</div> | |
| 117 | <p className="mt-2 text-xs text-muted"> | |
| 118 | of {rate.of.toLocaleString("en-US")} agent {rate.of === 1 ? "pull request" : "pull requests"} merged or closed today | |
| 119 | </p> | |
| 120 | <p className={`mt-1.5 text-xs font-medium ${up > 0 ? "text-success" : up < 0 ? "text-warn" : "text-muted"}`}> | |
| 121 | {trend ?? (work.code === "read" ? "Nothing settled in the last 7 days to compare." : "")} | |
| 122 | </p> | |
| 123 | </> | |
| 124 | ) : ( | |
| 125 | <> | |
| 126 | <div className="mt-2 text-5xl font-semibold tracking-tight text-faint">—</div> | |
| 127 | <p className="mt-2 text-xs leading-relaxed text-muted"> | |
| 128 | {work.code === "unavailable" ? "Code didn't answer, so pull requests aren't counted." : "No agent pull request has merged or closed today yet."} | |
| 129 | {lastWeek ? ` The last 7 days: ${percent(lastWeek.value)} of ${lastWeek.of}.` : ""} | |
| 130 | </p> | |
| 131 | </> | |
| 132 | )} | |
| 133 | </> | |
| 134 | )} | |
| 135 | </div> | |
| 136 | <div className="min-w-0 p-5"> | |
| 137 | <div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> | |
| 138 | <Stat label="Tasks today" value={tasks.length} /> | |
| 139 | {reviewed ? ( | |
| 140 | <> | |
| 141 | <Stat label="Accepted first time" value={counts.accepted} mark={MARK.accepted} /> | |
| 142 | <Stat label="Fixed after review" value={counts.fixed} mark={MARK.fixed} /> | |
| 143 | </> | |
| 144 | ) : ( | |
| 145 | <> | |
| 146 | <Stat label="Sessions finished" value={counts.finished} mark={MARK.finished} /> | |
| 147 | <Stat label="Didn't finish" value={counts.dropped} mark={MARK.dropped} /> | |
| 148 | </> | |
| 149 | )} | |
| 150 | <Stat label="Still open" value={counts.open} mark={MARK.open} /> | |
| 151 | </div> | |
| 152 | {tasks.length === 0 ? ( | |
| 153 | <p className="mt-5 text-sm text-muted">No agent work today yet.</p> | |
| 154 | ) : ( | |
| 155 | <div className="mt-5"> | |
| 156 | <ul className="flex flex-wrap gap-[3px]" aria-label="Today's tasks"> | |
| 157 | {tasks.map((task) => ( | |
| 158 | <li key={task.key}> | |
| 159 | <Tooltip> | |
| 160 | <TooltipTrigger asChild> | |
| 161 | {task.to ? ( | |
| 162 | <Link to={task.to} aria-label={`${task.title ?? "A session"}: ${OUTCOME_LABEL[task.outcome]}`} className={`block h-6 w-2.5 rounded-[3px] ${MARK[task.outcome]} transition-opacity hover:opacity-70`} /> | |
| 163 | ) : ( | |
| 164 | <span className={`block h-6 w-2.5 rounded-[3px] ${MARK[task.outcome]}`} /> | |
| 165 | )} | |
| 166 | </TooltipTrigger> | |
| 167 | <TooltipContent> | |
| 168 | <span className="block font-medium">{task.title ?? "A session in a conversation you're not in"}</span> | |
| 169 | <span className="text-muted"> | |
| 170 | {OUTCOME_LABEL[task.outcome]} · {SOURCE_LABEL[task.source]} | |
| 171 | </span> | |
| 172 | </TooltipContent> | |
| 173 | </Tooltip> | |
| 174 | </li> | |
| 175 | ))} | |
| 176 | </ul> | |
| 177 | <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted"> | |
| 178 | <span className="text-faint">Every mark is one of today's tasks</span> | |
| 179 | {shown.map((outcome) => ( | |
| 180 | <span key={outcome} className="inline-flex items-center gap-1.5"> | |
| 181 | <span className={`size-2 rounded-[2px] ${MARK[outcome]}`} aria-hidden /> | |
| 182 | {OUTCOME_LABEL[outcome]} <span className="text-fg-soft tabular-nums">{counts[outcome]}</span> | |
| 183 | </span> | |
| 184 | ))} | |
| 185 | </div> | |
| 186 | </div> | |
| 187 | )} | |
| 188 | {(work.partial || work.sessions === "unavailable" || work.code === "unavailable") && ( | |
| 189 | <p className="mt-3 text-xs text-faint"> | |
| 190 | {work.sessions === "unavailable" | |
| 191 | ? "Agent sessions couldn't be read, so only pull requests are counted. " | |
| 192 | : work.code === "unavailable" | |
| 193 | ? "Code didn't answer, so only agent sessions are counted. " | |
| 194 | : ""} | |
| 195 | {work.partial ? "A list was cut short, so some of today's tasks may be missing." : ""} | |
| 196 | </p> | |
| 197 | )} | |
| 198 | </div> | |
| 199 | </section> | |
| 200 | ); | |
| 201 | } | |
| 202 | ||
| 203 | export function DaySkeleton() { | |
| 204 | return ( | |
| 205 | <section className="grid rounded-2xl border border-line bg-surface md:grid-cols-[15rem_1fr]" aria-busy="true"> | |
| 206 | <div className="space-y-3 border-b border-line p-5 md:border-r md:border-b-0"> | |
| 207 | <Skeleton className="h-3.5 w-32" /> | |
| 208 | <Skeleton className="h-12 w-24" /> | |
| 209 | <Skeleton className="h-3 w-40" /> | |
| 210 | </div> | |
| 211 | <div className="space-y-5 p-5"> | |
| 212 | <div className="grid grid-cols-2 gap-4 sm:grid-cols-4"> | |
| 213 | {[0, 1, 2, 3].map((i) => ( | |
| 214 | <div key={i} className="space-y-2"> | |
| 215 | <Skeleton className="h-3 w-20" /> | |
| 216 | <Skeleton className="h-5 w-8" /> | |
| 217 | </div> | |
| 218 | ))} | |
| 219 | </div> | |
| 220 | <Skeleton className="h-6 w-3/4" /> | |
| 221 | </div> | |
| 222 | </section> | |
| 223 | ); | |
| 224 | } | |
| 225 | ||
| 226 | // --- Needs attention ----------------------------------------------------------- | |
| 227 | ||
| 228 | const ICON: Record<AttentionKind, ReactNode> = { | |
| 229 | limit: <CircleDollarSign size={16} className="text-danger" />, | |
| 230 | deploy: <Rocket size={16} className="text-danger" />, | |
| 231 | session_cap: <CircleDollarSign size={16} className="text-warn" />, | |
| 232 | agent_budget: <CircleDollarSign size={16} className="text-danger" />, | |
| 233 | pull_blocked: <GitPullRequest size={16} className="text-warn" />, | |
| 234 | ready: <GitMerge size={16} className="text-accent" />, | |
| 235 | review: <GitPullRequest size={16} className="text-info" />, | |
| 236 | invitation: <Mail size={16} className="text-info" />, | |
| 237 | agent_waiting: <Sparkles size={16} className="text-warn" />, | |
| 238 | stuck: <Hourglass size={16} className="text-warn" />, | |
| 239 | runner: <Server size={16} className="text-warn" />, | |
| 240 | notification: <Bell size={16} className="text-warn" />, | |
| 241 | chat: <AtSign size={16} className="text-accent" />, | |
| 242 | }; | |
| 243 | ||
| 244 | const STAKE_TONE: Record<NonNullable<Stake["tone"]> | "none", string> = { | |
| 245 | danger: "text-danger", | |
| 246 | warn: "text-warn", | |
| 247 | accent: "text-accent", | |
| 248 | none: "text-fg-soft", | |
| 249 | }; | |
| 250 | ||
| 251 | function StakeText({ stake }: { stake: Stake }) { | |
| 252 | return <span className={`text-xs font-medium tabular-nums ${STAKE_TONE[stake.tone ?? "none"]}`}>{stake.text}</span>; | |
| 253 | } | |
| 254 | ||
| 255 | /** Most rows Needs attention shows. */ | |
| 256 | const ROWS = 6; | |
| 257 | ||
| 258 | function AttentionItem({ row }: { row: AttentionRow }) { | |
| 259 | return ( | |
| 260 | <li className="flex items-start gap-3 border-t border-line px-5 py-3 first:border-t-0"> | |
| 261 | <span className="mt-0.5 shrink-0">{ICON[row.kind]}</span> | |
| 262 | <div className="min-w-0 grow"> | |
| 263 | <Link to={row.action.to} className="block truncate text-sm font-semibold text-fg hover:underline"> | |
| 264 | {row.title} | |
| 265 | </Link> | |
| 266 | <p className="truncate text-xs text-muted">{row.detail}</p> | |
| 267 | {row.stake && ( | |
| 268 | <div className="mt-1 sm:hidden"> | |
| 269 | <StakeText stake={row.stake} /> | |
| 270 | </div> | |
| 271 | )} | |
| 272 | </div> | |
| 273 | <div className="hidden max-w-48 shrink-0 pt-0.5 text-right whitespace-nowrap sm:block">{row.stake && <StakeText stake={row.stake} />}</div> | |
| 274 | <Link | |
| 275 | to={row.action.to} | |
| 276 | className="inline-flex h-7 shrink-0 items-center rounded-md border border-line-strong px-2.5 text-xs font-medium text-fg transition-colors hover:border-accent/50 hover:bg-raised" | |
| 277 | > | |
| 278 | {row.action.label} | |
| 279 | </Link> | |
| 280 | </li> | |
| 281 | ); | |
| 282 | } | |
| 283 | ||
| 284 | export function AttentionList({ attention }: { attention: Attention }) { | |
| 285 | const { rows, missing } = attention; | |
| 286 | return ( | |
| 287 | <> | |
| 288 | {rows.length === 0 ? ( | |
| 289 | <Quiet>{missing.length > 0 ? "Nothing is waiting on you in what could be read." : "Nothing is waiting on you."}</Quiet> | |
| 290 | ) : ( | |
| 291 | <ul> | |
| 292 | {rows.slice(0, ROWS).map((row) => ( | |
| 293 | <AttentionItem key={row.key} row={row} /> | |
| 294 | ))} | |
| 295 | </ul> | |
| 296 | )} | |
| 297 | {rows.length > ROWS && ( | |
| 298 | <p className="border-t border-line px-5 py-2.5 text-xs text-muted"> | |
| 299 | {(rows.length - ROWS).toLocaleString("en-US")} more in{" "} | |
| 300 | <Link to="/notifications" className="text-fg-soft underline-offset-2 hover:underline"> | |
| 301 | Notifications | |
| 302 | </Link> | |
| 303 | , Chat and Code. | |
| 304 | </p> | |
| 305 | )} | |
| 306 | {missing.length > 0 && ( | |
| 307 | <p className="flex items-center gap-1.5 border-t border-line px-5 py-2.5 text-xs text-faint"> | |
| 308 | <AlertTriangle size={12} className="shrink-0" /> | |
| 309 | {listOf(missing)} didn't answer, so what waits there isn't listed. | |
| 310 | </p> | |
| 311 | )} | |
| 312 | </> | |
| 313 | ); | |
| 314 | } | |
| 315 | ||
| 316 | const listOf = (names: string[]) => (names.length <= 1 ? (names[0] ?? "") : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`); | |
| 317 | ||
| 318 | export function Sources({ work }: { work: DayWork | null }) { | |
| 319 | return ( | |
| 320 | <div className="border-t border-line px-5 py-4"> | |
| 321 | <h3 className="text-xs font-semibold text-fg-soft">Where today's work came from</h3> | |
| 322 | {!work ? ( | |
| 323 | <p className="mt-2 text-xs text-muted">Couldn't be read right now.</p> | |
| 324 | ) : work.sources.length === 0 ? ( | |
| 325 | <p className="mt-2 text-xs text-muted">No agent work today yet.</p> | |
| 326 | ) : ( | |
| 327 | <ul className="mt-2.5 flex flex-wrap gap-2"> | |
| 328 | {work.sources.map(({ source, count }) => ( | |
| 329 | <li key={source} className="inline-flex items-center gap-2 rounded-lg border border-line bg-raised/40 px-2.5 py-1.5 text-xs"> | |
| 330 | <span className="text-muted">{SOURCE_LABEL[source]}</span> | |
| 331 | <span className="font-semibold text-fg tabular-nums">{count.toLocaleString("en-US")}</span> | |
| 332 | </li> | |
| 333 | ))} | |
| 334 | </ul> | |
| 335 | )} | |
| 336 | </div> | |
| 337 | ); | |
| 338 | } | |
| 339 | ||
| 340 | export function RowsSkeleton({ rows = 3 }: { rows?: number }) { | |
| 341 | return ( | |
| 342 | <div className="space-y-4 px-5 pb-5" aria-busy="true"> | |
| 343 | {Array.from({ length: rows }, (_, i) => ( | |
| 344 | <div key={i} className="flex items-center gap-3"> | |
| 345 | <Skeleton className="size-4 rounded" /> | |
| 346 | <div className="grow space-y-1.5"> | |
| 347 | <Skeleton className="h-3.5" style={{ width: `${60 - i * 8}%` }} /> | |
| 348 | <Skeleton className="h-3" style={{ width: `${80 - i * 10}%` }} /> | |
| 349 | </div> | |
| 350 | <Skeleton className="h-7 w-16" /> | |
| 351 | </div> | |
| 352 | ))} | |
| 353 | </div> | |
| 354 | ); | |
| 355 | } | |
| 356 | ||
| 357 | // --- Spent today -------------------------------------------------------------- | |
| 358 | ||
| 359 | /** | |
| 360 | * Each kind of charge keeps its colour, whatever its rank: models one, | |
| 361 | * sandboxes and runners another, hosting a third, storage and the rest grey. | |
| 362 | */ | |
| 363 | function kindColour(kind: string): string { | |
| 364 | if (/agent|model|gateway/i.test(kind)) return "bg-accent"; | |
| 365 | if (/sandbox|runner/i.test(kind)) return "bg-info"; | |
| 366 | if (/deploy|domain/i.test(kind)) return "bg-fg-soft"; | |
| 367 | if (/storage|git|cache|package/i.test(kind)) return "bg-muted"; | |
| 368 | return "bg-faint"; | |
| 369 | } | |
| 370 | ||
| 371 | export function SpendCard({ spend, slug }: { spend: Spend | null; slug: string }) { | |
| 372 | return ( | |
| 373 | <Card title="Spent today" link={{ label: "Billing", to: `/${slug}/-/billing` }}> | |
| 374 | {!spend ? ( | |
| 375 | <Quiet>Billing didn't answer, so today's spend can't be shown right now.</Quiet> | |
| 376 | ) : ( | |
| 377 | <div className="px-5 pb-5"> | |
| 378 | <div className="text-3xl font-semibold tracking-tight text-fg tabular-nums">{dollars(spend.totalMicros)}</div> | |
| 379 | <p className="mt-1 text-xs text-muted">{dollars(spend.monthMicros)} this month so far</p> | |
| 380 | {spend.lines.length === 0 ? ( | |
| 381 | <p className="mt-4 text-sm text-muted">Nothing on today's statement yet.</p> | |
| 382 | ) : ( | |
| 383 | <> | |
| 384 | <div className="mt-4 flex h-2.5 gap-[2px] overflow-hidden rounded-full" role="img" aria-label="Today's spend by kind of charge"> | |
| 385 | {spend.lines.map((line) => ( | |
| 386 | <span key={line.kind} className={`h-full min-w-[3px] ${kindColour(line.kind)}`} style={{ flexGrow: line.micros }} /> | |
| 387 | ))} | |
| 388 | </div> | |
| 389 | <ul className="mt-3 space-y-2"> | |
| 390 | {spend.lines.map((line) => ( | |
| 391 | <li key={line.kind} className="flex items-center gap-2 text-sm"> | |
| 392 | <span className={`size-2 shrink-0 rounded-[2px] ${kindColour(line.kind)}`} aria-hidden /> | |
| 393 | <span className="min-w-0 grow truncate text-fg-soft">{line.kind}</span> | |
| 394 | <span className="text-fg tabular-nums">{dollars(line.micros)}</span> | |
| 395 | </li> | |
| 396 | ))} | |
| 397 | </ul> | |
| 398 | </> | |
| 399 | )} | |
| 400 | <p className="mt-4 text-[0.6875rem] leading-relaxed text-faint">At price, as your statement shows it. Statement days run midnight to midnight UTC.</p> | |
| 401 | </div> | |
| 402 | )} | |
| 403 | </Card> | |
| 404 | ); | |
| 405 | } | |
| 406 | ||
| 407 | export function SpendSkeleton() { | |
| 408 | return ( | |
| 409 | <div className="space-y-3 px-5 pb-5" aria-busy="true"> | |
| 410 | <Skeleton className="h-8 w-28" /> | |
| 411 | <Skeleton className="h-2.5 w-full rounded-full" /> | |
| 412 | <Skeleton className="h-3 w-3/4" /> | |
| 413 | <Skeleton className="h-3 w-2/3" /> | |
| 414 | </div> | |
| 415 | ); | |
| 416 | } | |
| 417 | ||
| 418 | // --- Start here --------------------------------------------------------------- | |
| 419 | ||
| 420 | export function StartHere({ attention, now }: { attention: Attention; now: number }) { | |
| 421 | const start = attention.start; | |
| 422 | return ( | |
| 423 | <Card | |
| 424 | title={ | |
| 425 | <> | |
| 426 | Start here <span className="font-normal text-faint">· picked by g1t</span> | |
| 427 | </> | |
| 428 | } | |
| 429 | > | |
| 430 | {!start ? ( | |
| 431 | <Quiet>{attention.missing.length > 0 ? "Nothing to pick from what could be read." : "Nothing is waiting on you, so there's nothing to pick."}</Quiet> | |
| 432 | ) : ( | |
| 433 | <div className="px-5 pb-5"> | |
| 434 | <div className="flex items-start gap-3"> | |
| 435 | <span className="mt-0.5 shrink-0">{ICON[start.row.kind]}</span> | |
| 436 | <div className="min-w-0"> | |
| 437 | <p className="text-[0.9375rem] leading-snug font-semibold text-fg">{start.row.title}</p> | |
| 438 | <p className="mt-1 line-clamp-2 text-xs text-muted">{start.row.detail}</p> | |
| 439 | </div> | |
| 440 | </div> | |
| 441 | <dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs"> | |
| 442 | {start.row.owner && ( | |
| 443 | <> | |
| 444 | <dt className="text-muted">Owner</dt> | |
| 445 | <dd className="flex min-w-0 items-center gap-1.5 text-fg-soft"> | |
| 446 | {start.row.owner.agent ? <Bot size={12} className="shrink-0 text-accent" /> : <User size={12} className="shrink-0 text-muted" />} | |
| 447 | <span className="truncate">{start.row.owner.agent ? `@${start.row.owner.name}` : start.row.owner.name}</span> | |
| 448 | </dd> | |
| 449 | </> | |
| 450 | )} | |
| 451 | <dt className="text-muted">Next</dt> | |
| 452 | <dd className="text-fg-soft">{start.row.action.label}</dd> | |
| 453 | {start.row.at > 0 && ( | |
| 454 | <> | |
| 455 | <dt className="text-muted">Waiting</dt> | |
| 456 | <dd className="text-fg-soft" suppressHydrationWarning> | |
| 457 | {waited(start.row.at, now)} | |
| 458 | </dd> | |
| 459 | </> | |
| 460 | )} | |
| 461 | {start.row.stake && ( | |
| 462 | <> | |
| 463 | <dt className="text-muted">At stake</dt> | |
| 464 | <dd> | |
| 465 | <StakeText stake={start.row.stake} /> | |
| 466 | </dd> | |
| 467 | </> | |
| 468 | )} | |
| 469 | </dl> | |
| 470 | <p className="mt-4 rounded-lg bg-raised/60 px-3 py-2.5 text-xs leading-relaxed text-fg-soft"> | |
| 471 | <span className="font-medium text-fg">Why this one: </span> | |
| 472 | {start.why} | |
| 473 | </p> | |
| 474 | <Link | |
| 475 | to={start.row.action.to} | |
| 476 | className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent-hover" | |
| 477 | > | |
| 478 | {start.row.action.label} | |
| 479 | <ArrowRight size={14} /> | |
| 480 | </Link> | |
| 481 | </div> | |
| 482 | )} | |
| 483 | </Card> | |
| 484 | ); | |
| 485 | } |