Skip to content
210 linesCodeBlameRaw
1import { ArrowUpRight, Bot, ChevronRight, KeyRound, MessageSquare, Sparkles, UsersRound } from "lucide-react";
2import type { ReactNode } from "react";
3import { Link, data } from "react-router";
4
5import type { Route } from "./+types/agent";
6import { AgentLink, AgentPersonFace, AgentTag, Coming, PersonFace, PersonLink, TeamKindMark, ToldText } from "../../../components/people";
7import { statusLabel } from "../../../components/chat/marks";
8import { ButtonLink } from "../../../components/ui";
9import { Badge } from "../../../components/ui/badge";
10import { page } from "../../../lib/meta";
11import { agentsOn, leads, peopleAgent, teamsOfAgent } from "../../../lib/people";
12import { teamPath } from "../../../lib/teams";
13import { money } from "../../../lib/usage";
14import { identity, workspaceAgents } from "../../../lib/services.server";
15import { getViewer, roleIn, unwrap } from "../../../lib/session.server";
16
17export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `${loaderData?.agent.display_name ?? params.handle} · People · ${params.owner} · g1t` });
19}
20
21export async function loader({ params, context }: Route.LoaderArgs) {
22 const viewer = getViewer(context);
23 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
24 const [found, directory, listed, told] = await Promise.all([
25 workspaceAgents.get(params.owner, params.handle, viewer!),
26 identity.peopleDirectory(viewer, params.owner).then(unwrap),
27 workspaceAgents.list(params.owner, viewer!).catch(() => null),
28 workspaceAgents.teamContext(params.owner, params.handle, viewer!).catch(() => null),
29 ]);
30 if (!found.ok) throw data(null, { status: 404 });
31 const agent = found.value;
32 const agents = listed?.ok ? listed.value.map(peopleAgent) : [peopleAgent(agent)];
33 const teams = teamsOfAgent(directory.teams, agent);
34 // Everyone on its teams, each once, leads first.
35 const names = new Set(teams.flatMap((team) => team.people.map((p) => p.username)));
36 const leadNames = new Set(teams.flatMap((team) => (team.lead?.kind === "user" ? [team.lead.username] : [])));
37 const teammates = directory.people
38 .filter((person) => names.has(person.username))
39 .sort((a, b) => Number(leadNames.has(b.username)) - Number(leadNames.has(a.username)))
40 .map(({ user_id, username, display_username, name, avatar, title, owns }) => ({ user_id, username, display_username, name, avatar, title, owns, lead: leadNames.has(username) }));
41 return {
42 slug: params.owner.toLowerCase(),
43 agent: { ...peopleAgent(agent), monthly_micros: agent.budget.monthly_micros },
44 teams,
45 teammates,
46 agents,
47 told: told?.ok ? told.value.text : null,
48 };
49}
50
51export default function AgentProfile({ loaderData }: Route.ComponentProps) {
52 const { slug, agent, teams, teammates, agents, told } = loaderData;
53 const otherAgents = teams
54 .flatMap((team) => agentsOn(team, agents))
55 .filter((other, index, all) => other.id !== agent.id && all.findIndex((a) => a.id === other.id) === index);
56
57 return (
58 <div className="space-y-8">
59 <nav aria-label="Breadcrumb" className="flex items-center gap-1 text-sm text-muted">
60 <Link to={`/${slug}/-/people?kind=agents`} className="hover:text-fg">
61 People
62 </Link>
63 <ChevronRight size={13} className="text-faint" />
64 <span className="truncate text-fg">{agent.display_name}</span>
65 </nav>
66
67 <header className="flex flex-col gap-5 border-b border-line pb-6 sm:flex-row sm:items-start">
68 <div className="self-start">
69 <AgentPersonFace agent={agent} size={72} />
70 </div>
71 <div className="min-w-0 grow space-y-2">
72 <div className="flex flex-wrap items-center gap-2">
73 <h1 className="text-2xl font-semibold tracking-tight">{agent.display_name}</h1>
74 <AgentTag />
75 </div>
76 <p className="text-sm text-muted">{[agent.title || agent.role, `@${agent.handle}`].join(" · ")}</p>
77 <p className="text-sm text-muted">{statusLabel(agent.status)}</p>
78 </div>
79 <div className="flex shrink-0 flex-wrap gap-2">
80 <ButtonLink to={`/${slug}/-/chat?agent=${agent.handle}`}>
81 <MessageSquare size={15} />
82 Message
83 </ButtonLink>
84 <ButtonLink to={`/${slug}/-/agents/${agent.handle}`} variant="quiet">
85 Sessions and settings
86 <ArrowUpRight size={14} />
87 </ButtonLink>
88 </div>
89 </header>
90
91 {agent.responsibilities.length > 0 && (
92 <section className="flex flex-wrap items-center gap-1.5">
93 <span className="mr-1 text-sm text-muted">Responsible for</span>
94 {agent.responsibilities.map((duty) => (
95 <Badge key={duty} tone="info">
96 {duty}
97 </Badge>
98 ))}
99 </section>
100 )}
101
102 <div className="grid gap-4 md:grid-cols-2">
103 <Card icon={<UsersRound size={14} />} title="Teams">
104 {teams.length ? (
105 <ul className="space-y-2">
106 {teams.map((team) => (
107 <li key={team.slug} className="flex flex-wrap items-center gap-2">
108 <TeamKindMark people={team.people.length} agents={agentsOn(team, agents).length} />
109 <Link to={teamPath(slug, team.slug)} className="font-medium hover:text-accent">
110 {team.name}
111 </Link>
112 {leads(team.lead, { id: agent.id }) && <Badge tone="accent">Lead</Badge>}
113 {agent.team === team.slug && <Badge>Home team</Badge>}
114 </li>
115 ))}
116 </ul>
117 ) : (
118 <Quiet>Not on a team yet. Add it to one from the team's page.</Quiet>
119 )}
120 </Card>
121
122 <Card icon={<Sparkles size={14} />} title="Who it works with">
123 {teammates.length ? (
124 <ul className="space-y-2">
125 {teammates.map((person) => (
126 <li key={person.username} className="flex min-w-0 items-center gap-2.5 text-sm">
127 <PersonFace person={person} size={24} ring="var(--color-surface)" />
128 <span className="min-w-0 grow truncate">
129 <PersonLink workspace={slug} person={person} className="font-medium" />
130 {person.title && <span className="text-muted"> · {person.title}</span>}
131 </span>
132 {person.lead && <Badge tone="accent">Lead</Badge>}
133 </li>
134 ))}
135 </ul>
136 ) : (
137 <Quiet>{teams.length ? "Its teams are agents only." : "No people yet."}</Quiet>
138 )}
139 </Card>
140
141 <Card icon={<Bot size={14} />} title="Agents on its teams">
142 {otherAgents.length ? (
143 <ul className="space-y-2">
144 {otherAgents.map((other) => (
145 <li key={other.id} className="flex min-w-0 items-center gap-2.5 text-sm">
146 <AgentPersonFace agent={other} size={24} ring="var(--color-surface)" />
147 <span className="min-w-0 truncate">
148 <AgentLink workspace={slug} agent={other} className="font-medium" />
149 <span className="text-muted"> · {other.title || other.role}</span>
150 </span>
151 </li>
152 ))}
153 </ul>
154 ) : (
155 <Quiet>No other agents on its teams.</Quiet>
156 )}
157 </Card>
158
159 <Card icon={<KeyRound size={14} />} title="Access">
160 <dl className="space-y-3 text-sm">
161 <div>
162 <dt className="text-xs text-faint">Code</dt>
163 <dd className="mt-0.5">What the person who asks it can read, and only what everyone in the conversation can see</dd>
164 </div>
165 <div>
166 <dt className="flex items-center gap-2 text-xs text-faint">
167 Storage <Coming />
168 </dt>
169 <dd className="mt-0.5 text-muted">Which of the workspace's files it may read and write comes with Storage.</dd>
170 </div>
171 <div>
172 <dt className="text-xs text-faint">Spent this month</dt>
173 <dd className="mt-0.5 tabular-nums">
174 {money(agent.spent_month_micros)}
175 {agent.monthly_micros != null && <span className="text-muted"> of {money(agent.monthly_micros)}</span>}
176 </dd>
177 </div>
178 </dl>
179 </Card>
180 </div>
181
182 <section className="rounded-xl border border-accent/40 bg-surface p-4">
183 <div className="mb-3 flex flex-wrap items-baseline justify-between gap-2">
184 <h2 className="flex items-center gap-2 text-sm font-medium">
185 <Sparkles size={14} className="text-accent" />
186 What {agent.display_name} knows about its teams
187 </h2>
188 <span className="text-xs text-faint">Told every turn, from the team pages and who's around now</span>
189 </div>
190 {told ? <ToldText text={told} /> : <Quiet>Nothing yet: it's on no visible team. Once it is, it's told who leads, who owns what and who to page.</Quiet>}
191 </section>
192 </div>
193 );
194}
195
196function Card({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) {
197 return (
198 <section className="rounded-xl border border-line bg-surface p-4">
199 <h2 className="mb-3 flex items-center gap-2 text-sm font-medium text-muted">
200 <span className="text-faint">{icon}</span>
201 {title}
202 </h2>
203 {children}
204 </section>
205 );
206}
207
208function Quiet({ children }: { children: ReactNode }) {
209 return <p className="text-sm text-faint">{children}</p>;
210}