g1t/apps/web/app/components/activity.tsx
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.
| What happened across an outcome, as a feed beside its graph | 1 | import { |
| 2 | CircleCheck, | |
| 3 | CircleDot, | |
| 4 | CircleSlash, | |
| 5 | GitMerge, | |
| 6 | GitPullRequest, | |
| 7 | MessageSquare, | |
| 8 | Play, | |
| 9 | Terminal, | |
| 10 | } from "lucide-react"; | |
| 11 | import type { ReactNode } from "react"; | |
| 12 | import { Link } from "react-router"; | |
| 13 | ||
| 14 | import type { G1tEvent } from "@g1t/contracts"; | |
| 15 | ||
| 16 | import { Avatar, TimeAgo } from "./ui"; | |
| 17 | ||
| 18 | /** The accounts that are g1t's own agents and machinery. */ | |
| 19 | const AGENTS = new Set(["g1t-agent", "g1t"]); | |
| 20 | ||
| 21 | type Line = { icon: ReactNode; tone: string; actor: string | null; text: ReactNode; coordination?: boolean }; | |
| 22 | ||
| 23 | /** One event as a sentence, or null for those not worth a line. */ | |
| 24 | function line(event: G1tEvent, base: string): Line | null { | |
| 25 | const ref = (number: number) => ( | |
| 26 | <Link to={`${base}/issues/${number}`} prefetch="intent" className="font-medium text-fg hover:underline"> | |
| 27 | #{number} | |
| 28 | </Link> | |
| 29 | ); | |
| 30 | const actor = event.actor; | |
| 31 | switch (event.type) { | |
| 32 | case "issue.opened": | |
| 33 | return { | |
| 34 | icon: <CircleDot size={14} />, | |
| 35 | tone: actor && AGENTS.has(actor) ? "text-merged" : "text-muted", | |
| 36 | actor, | |
| 37 | text: actor && AGENTS.has(actor) ? <>filed {ref(event.data.number)} for something it found</> : <>opened {ref(event.data.number)}</>, | |
| 38 | coordination: actor != null && AGENTS.has(actor), | |
| 39 | }; | |
| 40 | case "pull.opened": | |
| 41 | return { | |
| 42 | icon: <Play size={14} />, | |
| 43 | tone: "text-merged", | |
| 44 | actor: event.data.agent, | |
| 45 | text: ( | |
| 46 | <> | |
| 47 | started on {event.data.issue != null ? ref(event.data.issue) : "a change"} in {ref(event.data.number)} | |
| 48 | </> | |
| 49 | ), | |
| 50 | }; | |
| 51 | case "pull.ready": | |
| 52 | return { icon: <GitPullRequest size={14} />, tone: "text-info", actor, text: <>marked {ref(event.data.number)} ready for review</> }; | |
| 53 | case "checks.completed": | |
| 54 | return { | |
| 55 | icon: <Terminal size={14} />, | |
| 56 | tone: event.data.status === "passed" ? "text-accent" : "text-danger", | |
| 57 | actor: null, | |
| 58 | text: ( | |
| 59 | <> | |
| 60 | checks {event.data.status} on {ref(event.data.number)} | |
| 61 | </> | |
| 62 | ), | |
| 63 | }; | |
| 64 | case "review.completed": | |
| 65 | return { | |
| 66 | icon: event.data.verdict === "approve" ? <CircleCheck size={14} /> : <CircleSlash size={14} />, | |
| 67 | tone: event.data.verdict === "approve" ? "text-accent" : "text-warn", | |
| 68 | actor: "g1t-agent", | |
| 69 | text: ( | |
| 70 | <> | |
| 71 | {event.data.verdict === "approve" ? "approved" : "asked for changes on"} {ref(event.data.number)} | |
| 72 | </> | |
| 73 | ), | |
| 74 | }; | |
| 75 | case "comment.created": | |
| 76 | if (event.data.verdict) { | |
| 77 | return { | |
| 78 | icon: event.data.verdict === "approve" ? <CircleCheck size={14} /> : <CircleSlash size={14} />, | |
| 79 | tone: event.data.verdict === "approve" ? "text-accent" : "text-warn", | |
| 80 | actor, | |
| 81 | text: ( | |
| 82 | <> | |
| 83 | {event.data.verdict === "approve" ? "approved" : "asked for changes on"} {ref(event.data.number)} | |
| 84 | </> | |
| 85 | ), | |
| 86 | }; | |
| 87 | } | |
| 88 | return { | |
| 89 | icon: <MessageSquare size={14} />, | |
| 90 | tone: actor && AGENTS.has(actor) ? "text-merged" : "text-muted", | |
| 91 | actor, | |
| 92 | text: <>commented on {ref(event.data.number)}</>, | |
| 93 | coordination: actor != null && AGENTS.has(actor), | |
| 94 | }; | |
| 95 | case "pull.merged": | |
| 96 | return { icon: <GitMerge size={14} />, tone: "text-accent", actor, text: <>landed {ref(event.data.number)} on main</> }; | |
| 97 | case "pull.merge_requested": | |
| 98 | return { icon: <GitMerge size={14} />, tone: "text-muted", actor, text: <>is bringing {ref(event.data.number)} up to date</> }; | |
| 99 | default: | |
| 100 | return null; | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * What happened across an outcome's issues and pull requests, newest | |
| 106 | * first. Agents talking to each other through the forge stand out. | |
| 107 | */ | |
| 108 | export function Activity({ events, base }: { events: G1tEvent[]; base: string }) { | |
| 109 | const lines = events.map((event) => ({ event, line: line(event, base) })).filter((item) => item.line != null); | |
| 110 | if (lines.length === 0) return null; | |
| 111 | return ( | |
| 112 | <ol className="space-y-0.5"> | |
| 113 | {lines.map(({ event, line }) => ( | |
| 114 | <li | |
| 115 | key={event.id} | |
| 116 | className={`flex items-center gap-2.5 rounded-lg px-2 py-1.5 text-sm ${ | |
| 117 | line!.coordination ? "bg-merged/[0.07] ring-1 ring-merged/25" : "" | |
| 118 | }`} | |
| 119 | > | |
| 120 | <span className={`shrink-0 ${line!.tone}`}>{line!.icon}</span> | |
| 121 | {line!.actor && ( | |
| 122 | <span className="flex shrink-0 items-center gap-1.5 font-mono text-xs text-fg-soft"> | |
| 123 | <Avatar name={line!.actor} size={14} /> | |
| 124 | {line!.actor} | |
| 125 | </span> | |
| 126 | )} | |
| 127 | <span className="min-w-0 grow truncate text-muted">{line!.text}</span> | |
| 128 | <span className="shrink-0 text-xs text-faint"> | |
| 129 | <TimeAgo at={event.time} /> | |
| 130 | </span> | |
| 131 | </li> | |
| 132 | ))} | |
| 133 | </ol> | |
| 134 | ); | |
| 135 | } |