flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/web/app/components/lifecycle.tsx

135 lines4,293 bytesCodeBlame
1import { Check, Hand } from "lucide-react";
2
3import type { Lifecycle, Stage } from "@g1t/contracts";
4
5const STEPS = ["Change", "Checks", "Review", "Up to date", "Landing"];
6
7/** Which step a stage is at. A revision is the change being made again. */
8const STEP_OF: Record<Exclude<Stage, "needs_you">, number> = {
9 working: 0,
10 revising: 0,
11 checking: 1,
12 reviewing: 2,
13 catching_up: 3,
14 // Its change is made; it is answering another agent.
15 answering: 2,
16 ready: 4,
17 queued: 4,
18};
19
20/** A word or two for a stage, for lists. */
21export const STAGE_LABEL: Record<Stage, string> = {
22 working: "Making the change",
23 checking: "Running checks",
24 reviewing: "In review",
25 revising: "Revising",
26 catching_up: "Catching up",
27 answering: "Answering an agent",
28 queued: "In the merge queue",
29 ready: "Ready to merge",
30 needs_you: "Needs you",
31};
32
33/** Five dots showing how far along a pull request is. */
34export function StageDots({ stage }: { stage: Stage }) {
35 if (stage === "needs_you") return null;
36 const current = STEP_OF[stage];
37 return (
38 <span className="flex items-center gap-1" aria-hidden="true">
39 {STEPS.map((step, index) => (
40 <span
41 key={step}
42 className={`size-1.5 rounded-full ${
43 index < current || stage === "ready"
44 ? "bg-accent"
45 : index === current
46 ? "animate-pulse bg-accent"
47 : "bg-line-strong"
48 }`}
49 />
50 ))}
51 </span>
52 );
53}
54
55const TITLE: Record<Stage, string> = {
56 working: "g1t is seeing this through",
57 checking: "g1t is seeing this through",
58 reviewing: "g1t is seeing this through",
59 revising: "g1t is seeing this through",
60 catching_up: "g1t is seeing this through",
61 answering: "g1t is seeing this through",
62 queued: "In the merge queue",
63 ready: "Ready to merge",
64 needs_you: "Needs you",
65};
66
67/**
68 * Where a pull request made by a g1t agent stands between "assigned" and
69 * "ready to merge", and what is happening to it right now.
70 */
71export function LifecyclePanel({ lifecycle }: { lifecycle: Lifecycle }) {
72 const { stage, detail, revisions } = lifecycle;
73 if (stage === "needs_you") {
74 return (
75 <div className="mt-4 rounded-xl border border-warn/40 bg-warn/5 px-4 py-3 text-sm">
76 <p className="flex items-center gap-2.5 font-medium">
77 <Hand size={16} className="shrink-0 text-warn" />
78 {TITLE[stage]}
79 </p>
80 <p className="mt-1 text-muted">{detail}</p>
81 </div>
82 );
83 }
84 const current = STEP_OF[stage];
85 const ready = stage === "ready";
86 return (
87 <div
88 className={`mt-4 rounded-xl border px-4 py-3 text-sm ${
89 ready ? "border-accent/40 bg-accent/5" : "border-line bg-surface"
90 }`}
91 >
92 <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
93 <p className="font-medium">{TITLE[stage]}</p>
94 {revisions > 0 && (
95 <p className="text-xs text-faint">
96 Revised {revisions === 1 ? "once" : `${revisions} times`}
97 </p>
98 )}
99 </div>
100 <ol className="mt-3 flex flex-wrap items-center gap-x-2 gap-y-2 text-xs">
101 {STEPS.map((step, index) => {
102 const done = index < current || ready;
103 const active = index === current && !ready;
104 return (
105 <li key={step} className="flex items-center gap-2">
106 {index > 0 && (
107 <span
108 aria-hidden="true"
109 className={`h-px w-4 sm:w-6 ${done || active ? "bg-accent-dim" : "bg-line"}`}
110 />
111 )}
112 <span
113 className={`flex items-center gap-1.5 ${
114 done ? "text-fg" : active ? "font-medium text-accent" : "text-faint"
115 }`}
116 >
117 {done ? (
118 <Check size={13} className="text-accent" />
119 ) : (
120 <span
121 className={`size-1.5 rounded-full ${
122 active ? "animate-pulse bg-accent" : "bg-line-strong"
123 }`}
124 />
125 )}
126 {step}
127 </span>
128 </li>
129 );
130 })}
131 </ol>
132 <p className="mt-3 text-muted">{detail}</p>
133 </div>
134 );
135}