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. | 1 | import { Info } from "lucide-react"; |
| 2 | import { Link, data } from "react-router"; | |
| 3 | ||
| 4 | import type { Route } from "./+types/org-chart"; | |
| 5 | import { AgentPersonFace, PersonFace, TeamKindMark } from "../../components/people"; | |
| 6 | import { Hint } from "../../components/ui/hint"; | |
| 7 | import { page } from "../../lib/meta"; | |
| 8 | import { type OrgNode, agentPath, orgChart, peopleAgent, personName, personPath } from "../../lib/people"; | |
| 9 | import { teamPath } from "../../lib/teams"; | |
| 10 | import { identity, workspaceAgents } from "../../lib/services.server"; | |
| 11 | import { getViewer, roleIn, unwrap } from "../../lib/session.server"; | |
| 12 | ||
| 13 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 14 | return page(args, { title: `Org chart · ${params.owner} · g1t` }); | |
| 15 | } | |
| 16 | ||
| 17 | export 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 | ||
| 27 | export 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. */ | |
| 86 | function 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 | } |