Skip to content

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.sh
129 linesCodeBlameRaw

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.

People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.1import { Info } from "lucide-react";
2import { Link, data } from "react-router";
3
4import type { Route } from "./+types/org-chart";
5import { AgentPersonFace, PersonFace, TeamKindMark } from "../../components/people";
6import { Hint } from "../../components/ui/hint";
7import { page } from "../../lib/meta";
8import { type OrgNode, agentPath, orgChart, peopleAgent, personName, personPath } from "../../lib/people";
9import { teamPath } from "../../lib/teams";
10import { identity, workspaceAgents } from "../../lib/services.server";
11import { getViewer, roleIn, unwrap } from "../../lib/session.server";
12
13export function meta({ params, ...args }: Route.MetaArgs) {
14 return page(args, { title: `Org chart · ${params.owner} · g1t` });
15}
16
17export async function loader({ params, context }: Route.LoaderArgs) {
18 const viewer = getViewer(context);
19 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
20 const [directory, agents] = await Promise.all([
21 identity.peopleDirectory(viewer, params.owner).then(unwrap),
22 workspaceAgents.list(params.owner, viewer!).catch(() => null),
23 ]);
24 return { slug: params.owner.toLowerCase(), directory, agents: agents?.ok ? agents.value.map(peopleAgent) : [] };
25}
26
27export default function OrgChartPage({ loaderData }: Route.ComponentProps) {
28 const { slug, directory, agents } = loaderData;
29 const chart = orgChart(directory, agents);
30 const lines = directory.people.some((person) => person.manager);
31
32 return (
33 <div className="space-y-6">
34 {!lines && (
35 <p className="flex items-start gap-2 rounded-lg border border-line bg-surface px-3 py-2.5 text-sm text-muted">
36 <Info size={15} className="mt-0.5 shrink-0 text-faint" />
37 {directory.can_manage
38 ? "No one reports to anyone yet. Set who each person reports to under Edit profile on their profile."
39 : "No reporting lines yet. An owner sets who each person reports to on their profile."}
40 </p>
41 )}
42 <section aria-label="Reporting lines" className="overflow-x-auto rounded-xl border border-line bg-surface p-4 sm:p-5">
43 <ul className="space-y-1">
44 {chart.roots.map((root) => (
45 <Branch key={root.person.username} slug={slug} node={root} />
46 ))}
47 </ul>
48 <p className="mt-4 text-xs text-faint">Agents appear beside the person who leads their team.</p>
49 </section>
50
51 {chart.unled.length > 0 && (
52 <section aria-labelledby="unled" className="space-y-3">
53 <h2 id="unled" className="text-sm font-medium text-muted">
54 Teams no person leads
55 </h2>
56 <ul className="grid gap-3 sm:grid-cols-2">
57 {chart.unled.map(({ team, agents: teamAgents }) => (
58 <li key={team.slug} className="rounded-xl border border-line bg-surface p-4">
59 <div className="flex items-center gap-2">
60 <TeamKindMark people={team.people.length} agents={teamAgents.length} />
61 <Link to={teamPath(slug, team.slug)} className="font-medium hover:text-accent">
62 {team.name}
63 </Link>
64 {team.lead?.kind === "agent" && (
65 <span className="text-xs text-faint">led by {teamAgents.find((a) => a.id === (team.lead as { agent_id: string }).agent_id)?.display_name ?? "an agent"}</span>
66 )}
67 </div>
68 <div className="mt-3 flex flex-wrap gap-2">
69 {teamAgents.map((agent) => (
70 <Link key={agent.id} to={agentPath(slug, agent.handle)} className="inline-flex items-center gap-1.5 rounded-full border border-line py-0.5 pr-2.5 pl-0.5 text-xs hover:border-line-strong">
71 <AgentPersonFace agent={agent} size={20} ring="var(--color-surface)" />
72 {agent.display_name}
73 </Link>
74 ))}
75 </div>
76 </li>
77 ))}
78 </ul>
79 </section>
80 )}
81 </div>
82 );
83}
84
85/** One person, the agents of the teams they lead, and everyone who reports to them below. */
86function Branch({ slug, node }: { slug: string; node: OrgNode }) {
87 const { person } = node;
88 return (
89 <li>
90 <div className="flex flex-wrap items-center gap-2 py-1">
91 <Link
92 to={personPath(slug, person.username)}
93 prefetch="intent"
94 className="inline-flex min-w-0 items-center gap-2.5 rounded-lg border border-line bg-bg py-1.5 pr-3 pl-1.5 transition-colors hover:border-accent/60"
95 >
96 <PersonFace person={person} size={28} />
97 <span className="grid min-w-0 text-left">
98 <span className="truncate text-sm font-medium">{personName(person)}</span>
99 <span className="truncate text-xs text-muted">{person.title ?? `@${person.username}`}</span>
100 </span>
101 </Link>
102 {node.agents.length > 0 && (
103 <span className="inline-flex items-center gap-2">
104 <span className="flex">
105 {node.agents.slice(0, 6).map((agent, index) => (
106 <Hint key={agent.id} label={`${agent.display_name} · ${agent.title || agent.role}`}>
107 <Link to={agentPath(slug, agent.handle)} className={index ? "-ml-1.5" : undefined} aria-label={agent.display_name}>
108 <AgentPersonFace agent={agent} size={22} ring="var(--color-surface)" />
109 </Link>
110 </Hint>
111 ))}
112 </span>
113 <span className="text-xs text-faint">
114 {node.agents.length > 6 ? `+${node.agents.length - 6} · ` : ""}
115 {node.leads.map((team) => team.name).join(", ")}
116 </span>
117 </span>
118 )}
119 </div>
120 {node.reports.length > 0 && (
121 <ul className="ml-[17px] space-y-1 border-l border-dashed border-line-strong pl-5 sm:pl-7">
122 {node.reports.map((report) => (
123 <Branch key={report.person.username} slug={slug} node={report} />
124 ))}
125 </ul>
126 )}
127 </li>
128 );
129}