Skip to content

Commit

People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so.

syntaqxcommitted Parentdd8c242Browse files
17 files+180−3990/17 viewed
+8−2
284284 | To | Where | Who |
285285 | --- | --- | --- |
286286 | Add it to teams as you make it | **Add to teams**, on the drafted card or the full form | Owners for any team; a team's maintainers for theirs |
287−| Add it to a team, or take it off | Its **Profile** tab or its People profile, under **Teams**: **Add to team** and **Remove** | Owners, and the team's maintainers |
287+| Add it to a team, or take it off | Its **Profile** tab, under **Teams**: **Add to team** and **Remove** | Owners, and the team's maintainers |
288288 | The same, from the team | The team's **Members** page, under **Agents** (see [agents on a team](/guides/teams/#agents-on-a-team)) | Owners, and the team's maintainers |
289289
290290 A personal agent is on no team; once an owner
291291 [promotes](#promote-a-personal-agent) it, add it to teams like anyone.
292292
293+An agent's page in Agents is its only profile. The workspace's
294+[People directory](/guides/people-and-teams/#the-directory) lists people
295+only; agents appear there on the teams they are on, under the team's
296+people and marked **Agent**, and in the org chart beside the person who
297+leads their team.
298+
293299 The Agents sidebar groups specialists by the teams they are on, each team
294300 by name: an agent on two teams shows under both, and agents on none show
295301 last, under **Not on a team**. Every turn, an agent is told who is on its
841847 | **Routines** | Work it does on a schedule or when something happens. See [routines](/guides/agent-routines/). |
842848 | **Spend** | Its [effort](#effort) and what each level has cost it, suggestions to spend less, and spend this month against its budget: by day, kind of work, model and who asked, and its costliest sessions. |
843849 | **Activity** | Its latest replies and sessions, with who asked, the model and the cost. |
844−| **Profile** | Its definition: identity, [face](#its-face), job, personality, models and effort, budget and what it may do alone, with every saved version. Owners edit a workspace agent here, and a member their own personal agent, in the form or [in words](#change-it-in-words). |
850+| **Profile** | The teams it is on, with what it is told about them every turn (see [teams](#teams)), and its definition: identity, [face](#its-face), job, personality, models and effort, budget and what it may do alone, with every saved version. Owners edit a workspace agent here, and a member their own personal agent, in the form or [in words](#change-it-in-words). |
845851
846852 The **Agents** page above them all shows the agent budget, sessions waiting
847853 on you and working now, every agent with its live sessions and its month's
+40−40
11 ---
22 title: People and teams
3−description: Find anyone in a workspace, person or agent, see what they own and who they report to, and know what agents are told about their teams.
3+description: Find anyone in a workspace, see what they own and who they report to, put agents on teams beside them, and know what agents are told about their teams.
44 ---
55
6−**People** is where a workspace's people and agents are listed together:
7−who they are, the teams they are on, what they own and who they report
8−to. Open it from **People** at the foot of the rail. Its sidebar has:
6+**People** is where a workspace's people are listed: who they are, the
7+teams they are on, what they own and who they report to. Agents are not
8+in it: each has its page under [Agents](/guides/agents/), and agents join
9+[teams](/guides/teams/) beside people. Open it from **People** at the foot
10+of the rail. Its sidebar has:
911
1012 | Page | Address | What it is |
1113 | --- | --- | --- |
12−| **Everyone** | `g1t.sh/<workspace>/-/people` | The [directory](#the-directory) of people and agents. |
14+| **Everyone** | `g1t.sh/<workspace>/-/people` | The [directory](#the-directory) of the workspace's people. |
1315 | **Teams** | `g1t.sh/<workspace>/-/teams` | The workspace's [teams](/guides/teams/). |
1416 | **Org chart** | `g1t.sh/<workspace>/-/org-chart` | Who reports to whom. See [the org chart](#the-org-chart). |
1517 | **Members and invites** | `g1t.sh/<workspace>/-/members` | Under **Membership**: roles, invitations and leaving. See [members and invites](#members-and-invites). |
1820
1921 ## The directory
2022
21−The directory, `g1t.sh/<workspace>/-/people`, lists people and agents in
22−one place, with one search box.
23+The directory, `g1t.sh/<workspace>/-/people`, lists the workspace's
24+people, with one search box and how many there are.
2325
2426 1. Open **People**, then **Everyone**.
25−2. Type in **Search by name, title, team or what they own**.
26−3. Choose **Everyone**, **People** or **Agents** to narrow the list. Each
27− shows how many it has.
27+2. Type in **Name, title, team or what they own**.
2828
29−The search matches:
30−
31−| | Matches on |
32−| --- | --- |
33−| **A person** | Name, username, title, teams, what they own, location and bio. |
34−| **An agent** | Name, handle, title, role, responsibilities and teams. |
35−
36−The address keeps what you chose, so you can share it:
37−`?q=billing` for the search, and `?kind=people` or `?kind=agents` for the
38−switch.
29+The search matches a person's name, username, title, teams, what they
30+own, location and bio. The address keeps the search, so you can share it:
31+`?q=billing`.
3932
4033 Each card shows:
4134
42−- the person's picture with a dot for whether they are around, or the
43− agent's picture with a dot for its status;
35+- the person's picture with a dot for whether they are around;
4436 - their name and title;
4537 - their teams;
4638 - their local time, from the time zone on their
4739 [profile](https://g1t.sh/settings/profile);
4840 - what they own.
4941
42+The directory is people only. The workspace's agents are not in it and a
43+search for one finds no one: they are listed under
44+[Agents](/guides/agents/), and a team's page lists the agents on it under
45+its people (see [agents on a team](/guides/teams/#agents-on-a-team)).
46+
5047 **Org chart** opens [the org chart](#the-org-chart). Owners also see
5148 **Invite people**, which opens [Members and invites](#members-and-invites).
5249
9188 When someone leaves the workspace, anyone who reported to them reports to
9289 no one, and any team they led has no lead.
9390
94−## An agent's profile
91+## Agents
9592
96−Each agent has a People profile too,
97−`g1t.sh/<workspace>/-/people/agents/<handle>`, with its title, handle and
98−status.
93+Agents are not in the directory and have no People profile. Each agent's
94+page is under [Agents](/guides/agents/#the-agents-page),
95+`g1t.sh/<workspace>/-/agents/<handle>`: its sessions, skills, abilities,
96+memory, routines, spend and activity, and its **Profile** tab, which has
97+its definition and, under **Teams**, the teams it is on and
98+[what it is told about them](#what-agents-are-told). Wherever People
99+names an agent, on a team's page, on a person's profile under **Agents
100+on their teams** or in the org chart, the name opens that page.
99101
100−| Card | What it shows |
101−| --- | --- |
102−| **Responsible for** | Its responsibilities, from its [profile](/guides/agents/#title-and-responsibilities). |
103−| **Teams** | The teams it is on, with **Lead** where it leads one. Owners and a team's maintainers choose **Remove** beside a team they manage, or a team and **Add to team**, as on the team's page. A personal agent is on no team. |
104−| **Who it works with** | The people on its teams, leads first. |
105−| **Agents on its teams** | The other agents on those teams. |
106−| **Access** | **Code**: what the person who asks it can read, and only what everyone in the conversation can see. **Storage**: coming. What it spent this month of its monthly budget. |
107−| **What** *agent* **knows about its teams** | The exact text it is told every turn. See [what agents are told](#what-agents-are-told). |
102+Agents still join teams like anyone: see [teams of any mix](#teams-of-any-mix)
103+below. The old address of an agent's People profile,
104+`g1t.sh/<workspace>/-/people/agents/<handle>`, sends you to its page in
105+Agents.
108106
109−**Message** opens your direct message with it. **Sessions and settings**
110−opens its page in [Agents](/guides/agents/).
111−
112107 ## Teams of any mix
113108
114109 A [team](/guides/teams/) can be people and agents together, people only,
119114
120115 | | Where |
121116 | --- | --- |
122−| Add or remove an agent | The team's **People and agents** tab, or **Teams** on the agent's profile. See [agents on a team](/guides/teams/#agents-on-a-team). |
117+| Add or remove an agent | The team's **People and agents** tab, or **Teams** on the agent's **Profile** tab in Agents. See [agents on a team](/guides/teams/#agents-on-a-team). |
123118 | Set the lead, channel and budget | The team's **Settings**. See [lead, channel and budget](/guides/teams/#lead-channel-and-budget). |
124119 | Storage for a team | Coming. |
125120
127122
128123 The org chart, `g1t.sh/<workspace>/-/org-chart`, draws the reporting
129124 lines as a tree: each person, with the people who report to them below.
130−Beside each person are the agents of the teams they lead.
125+Beside each person are the agents on the teams they lead, as members of
126+those teams: hover one for its name, title and that it is an agent, and
127+choose it to open its page in Agents. Agents have no place of their own
128+in the tree, since no one reports to an agent and an agent reports to no
129+one.
131130
132131 Under **Teams no person leads** are the teams led by an agent or by no
133132 one, each with its agents.
180179
181180 You can read what an agent is told:
182181
183−- on its People profile, under **What** *agent* **knows about its teams**;
182+- on its **Profile** tab in [Agents](/guides/agents/#the-agents-page),
183+ under **Teams**, by opening **What** *agent* **knows about its teams**;
184184 - on a team's **People and agents** tab, under **What the agents on**
185185 *team* **know about it**, for that team's part.
186186
+4−3
136136 3. To take an agent off, choose **Remove** beside it.
137137
138138 You can do the same from the agent's side: under **Teams** on its
139−**Profile** tab in [Agents](/guides/agents/#teams) or on its
140−[People profile](/guides/people-and-teams/#an-agents-profile), and under
141−**Add to teams** when you make it. A personal agent is on no team.
139+**Profile** tab in [Agents](/guides/agents/#teams), and under **Add to
140+teams** when you make it. A personal agent is on no team. Agents are not
141+in the [People directory](/guides/people-and-teams/#the-directory); a
142+team's page is where they are listed beside people.
142143
143144 An agent on a team works with the access of whoever asks it, not with the
144145 team's roles on repositories. Being on a team tells the agent about it
+3−3
487487 | **Projects** | `/-/projects` | Everyone | Every project you can see. See [the Projects page](#the-projects-page). |
488488 | [**Packages**](/guides/packages/) | `/-/packages` | Everyone | What the workspace publishes. A visitor opens it from **Packages** on the workspace's page. |
489489 | [**Teams**](/guides/teams/) | `/-/teams` | Members | Groups of members given roles on repositories together, mentioned as `@workspace/team` and asked to review together. Each team has its own page at `/-/teams/<team>`. |
490−| [**People**](/guides/people-and-teams/) | `/-/people` | Members | Everyone in the workspace, people and agents, with their titles, teams and what they own. Each has a profile at `/-/people/<username>`, or `/-/people/agents/<handle>` for an agent. |
491−| [**Org chart**](/guides/people-and-teams/#the-org-chart) | `/-/org-chart` | Members | Who reports to whom, with the agents of the teams each person leads. |
490+| [**People**](/guides/people-and-teams/) | `/-/people` | Members | The workspace's people, with their titles, teams and what they own. Each has a profile at `/-/people/<username>`. Agents are not in it; an agent's page is `/-/agents/<handle>`. |
491+| [**Org chart**](/guides/people-and-teams/#the-org-chart) | `/-/org-chart` | Members | Who reports to whom, with the agents on the teams each person leads beside them. |
492492 | **Members and invites** | `/-/members` | Members | Who belongs. Owners invite, change roles and remove people here. |
493493 | **Insights** | `/-/insights` | Members | Coming soon: how the whole workspace delivers. |
494494 | **Settings** | `/-/settings` | Owners | How the workspace is set up and connected (below). |
554554 | **Agents** | The workspace's agents and their sessions. See [agents](/guides/agents/). |
555555 | **Code** | Projects, pull requests and checks. Not shown to a member without [Code access](/guides/agent-access/). |
556556 | **Artifacts** | Documents, decks and pages. See [artifacts](/guides/artifacts/). |
557−| **People** | Everyone in the workspace, people and agents; its [teams](/guides/teams/); the org chart; and, under **Membership**, **Members and invites**. See [people and teams](/guides/people-and-teams/). |
557+| **People** | Everyone in the workspace (its people; agents are under Agents); its [teams](/guides/teams/), which agents can be on; the org chart; and, under **Membership**, **Members and invites**. See [people and teams](/guides/people-and-teams/). |
558558 | **Workspace** | Usage, billing, integrations, policies and settings. |
559559
560560 The app you are in sits on a filled square. Your account is the
+1−1
6464 | [Home](/guides/home/) | The front page: where you're needed now, what happened since you were last here, what is running, and where to start. | <Status is="preview" /> |
6565 | [Chat](/guides/chat/) | Channels, direct messages and threads, live. People and agents are members alike. No seats and no history limit. | <Status is="live" /> |
6666 | [Notifications](/guides/notifications/) | Everything waiting on you: mentions, reviews, approvals and alerts, with browser push. | <Status is="live" /> |
67−| [People and teams](/guides/people-and-teams/) | Everyone, people and agents, in one search, with profiles, reporting lines, an org chart and teams that include agents. Members, roles, nested teams and invites. | <Status is="live" /> |
67+| [People and teams](/guides/people-and-teams/) | Everyone in the workspace in one search, with profiles, reporting lines, an org chart and teams that include agents beside people. Members, roles, nested teams and invites. | <Status is="live" /> |
6868 | [Artifacts](/guides/artifacts/) | Documents written together, live, by people and agents. Private until you share them. | <Status is="live" /> |
6969 | More kinds of artifact | Slides, designs and dashboards that keep the query behind every number. | <Status is="coming" /> |
7070 | Scratchpads | Tools, reports and agents anyone can build on a copy of the data, used in production only after review. | <Status is="coming" /> |
+1−1
103103 );
104104 }
105105
106−/** An agent's name, linked to its People profile. */
106+/** An agent's name, linked to its page in Agents. */
107107 export function AgentLink({ workspace, agent, className }: { workspace: string; agent: Pick<PeopleAgent, "handle" | "display_name">; className?: string }) {
108108 return (
109109 <Link to={agentPath(workspace, agent.handle)} prefetch="intent" className={cn("hover:text-accent", className)}>
+3−2
13421342 }
13431343
13441344 /**
1345− * People's sidebar: everyone in the workspace, people and agents; its
1346− * teams; the org chart; and, to manage who belongs, members and invites.
1345+ * People's sidebar: everyone in the workspace (its people; agents are
1346+ * under Agents); its teams, which agents can be on; the org chart; and, to
1347+ * manage who belongs, members and invites.
13471348 */
13481349 function PeopleSidebar({ slug }: { slug: string }) {
13491350 const { pathname } = useLocation();
+19−7
66 import {
77 type PeopleAgent,
88 NO_TEAM,
9+ agentPath,
910 agentsOn,
1011 groupByTeam,
1112 teamsByAgent,
115116 assert.deepEqual(groupByTeam(agents.slice(3), {}).map((g) => g.label), [NO_TEAM]);
116117 });
117118
118−test("the directory finds people and agents in one search", () => {
119− const names = (query: string, kind: "everyone" | "people" | "agents" = "everyone") =>
120− directoryEntries(directory, agents, query, kind).map((e) => (e.kind === "person" ? e.person.username : `@${e.agent.handle}`));
121− assert.deepEqual(names("sales"), ["sofia", "jordan", "@david"]);
119+test("the directory lists people only, in one search; agents are never in it", () => {
120+ const names = (query: string) => directoryEntries(directory, query).map((e) => e.person.username);
121+ assert.deepEqual(names(""), ["chase", "sofia", "jordan", "kai"]);
122+ assert.deepEqual(names("sales"), ["sofia", "jordan"]);
122123 assert.deepEqual(names("halcyon"), ["jordan"]);
123− assert.deepEqual(names("errors"), ["@sentinel"]);
124− assert.deepEqual(names("", "agents"), ["@david", "@atlas", "@sentinel", "@pax"]);
125− assert.deepEqual(names("night"), ["@david", "@atlas", "@sentinel"]);
124+ assert.deepEqual(names("owner"), ["chase"]);
125+ // What only an agent would match (its handle, title or responsibilities) finds no one here.
126+ assert.deepEqual(names("david"), []);
127+ assert.deepEqual(names("errors"), []);
128+ // A team of agents only is no one's team in the directory.
129+ assert.deepEqual(names("night shift"), []);
126130 assert.equal(names("nobody here").length, 0);
131+ // Every entry is a person with their teams; nothing in the shape says "agent".
132+ const sofia = directoryEntries(directory, "sofia")[0]!;
133+ assert.deepEqual(Object.keys(sofia), ["person", "teams"]);
134+ assert.deepEqual(sofia.teams.map((t) => t.slug), ["sales"]);
135+});
136+
137+test("an agent's profile is its page in Agents, where People links to it", () => {
138+ assert.equal(agentPath("acme", "margo"), "/acme/-/agents/margo");
127139 });
128140
129141 test("the org chart follows reporting lines, with each team's agents beside its lead", () => {
+17−33
11 /**
2− * People on the site: the directory (people and agents in one search),
3− * profiles, teams of any mix, and the org chart. Types only from the
4− * contracts, so this file runs under `node --test`.
2+ * People on the site: the directory (people only; agents are on their
3+ * teams and under Agents), profiles, teams of any mix, and the org chart.
4+ * Types only from the contracts, so this file runs under `node --test`.
55 */
66 import type { DirectoryPerson, DirectoryTeam, PeopleDirectory, TeamLead, WorkspaceAgent } from "@g1t/contracts";
77
2222 return `/${workspace}/-/people/${username}`;
2323 }
2424
25−/** Where an agent's People profile is. */
25+/** Where an agent's profile is: its page in Agents. People has none; its old address there redirects. */
2626 export function agentPath(workspace: string, handle: string): string {
27− return `/${workspace}/-/people/agents/${handle}`;
27+ return `/${workspace}/-/agents/${handle}`;
2828 }
2929
3030 /** The agents on a team, in the order they were added, as found among `agents`. */
101101 return words.every((word) => text.includes(word));
102102 }
103103
104−export type DirectoryKind = "everyone" | "people" | "agents";
105−
106−export function directoryKind(value: string | null): DirectoryKind {
107− return value === "people" || value === "agents" ? value : "everyone";
108−}
109−
110−/** One card in the directory: a person or an agent. */
111−export type DirectoryEntry =
112− | { kind: "person"; person: DirectoryPerson; teams: DirectoryTeam[] }
113− | { kind: "agent"; agent: PeopleAgent; teams: DirectoryTeam[] };
104+/** One card in the directory: a person and their teams. */
105+export type DirectoryEntry = { person: DirectoryPerson; teams: DirectoryTeam[] };
114106
115107 /**
116− * Everyone matching `query`, people first by name, then agents (the
117− * orchestrator first, then by name). People match on their name, title,
118− * teams, what they own, where they are and their bio; agents on their
119− * name, handle, title, role, teams and responsibilities.
108+ * The people matching `query`, in the directory's order (by name). A
109+ * person matches on their name, username, title, teams, what they own,
110+ * where they are and their bio. The directory is people only: the
111+ * workspace's agents are listed under Agents and on the teams they are
112+ * on, never here, so a query that would only match an agent finds no one.
120113 */
121−export function directoryEntries(directory: PeopleDirectory, agents: readonly PeopleAgent[], query: string, kind: DirectoryKind): DirectoryEntry[] {
114+export function directoryEntries(directory: PeopleDirectory, query: string): DirectoryEntry[] {
122115 const out: DirectoryEntry[] = [];
123− if (kind !== "agents") {
124− for (const person of directory.people) {
125− const teams = teamsOfPerson(directory.teams, person.username);
126− const fields = [person.username, person.name, person.title, person.location, person.bio, ...person.owns, ...teams.map((team) => team.name), person.role === "owner" ? "owner" : null];
127− if (matches(query, fields)) out.push({ kind: "person", person, teams });
128− }
129− }
130− if (kind !== "people") {
131− for (const agent of agents) {
132− const teams = teamsOfAgent(directory.teams, agent);
133− const fields = [agent.handle, agent.display_name, agent.title, agent.role, ...agent.responsibilities, ...teams.map((team) => team.name), "agent"];
134− if (matches(query, fields)) out.push({ kind: "agent", agent, teams });
135− }
116+ for (const person of directory.people) {
117+ const teams = teamsOfPerson(directory.teams, person.username);
118+ const fields = [person.username, person.name, person.title, person.location, person.bio, ...person.owns, ...teams.map((team) => team.name), person.role === "owner" ? "owner" : null];
119+ if (matches(query, fields)) out.push({ person, teams });
136120 }
137121 return out;
138122 }
+1−0
102102 assert.equal(modeOf("/acme/-/people", "acme"), "people");
103103 assert.equal(modeOf("/acme/-/teams/web", "acme"), "people");
104104 assert.equal(modeOf("/acme/-/people/ana", "acme"), "people");
105+ // An agent's old People address (it redirects to Agents) is still read as People's until it does.
105106 assert.equal(modeOf("/acme/-/people/agents/margo", "acme"), "people");
106107 assert.equal(modeOf("/acme/-/org-chart", "acme"), "people");
107108 assert.equal(modeOf("/acme/-/members", "acme"), "people");
+2−1
9494 index("routes/workspace/overview.tsx"),
9595 // Its pages, each in the sidebar: Overview (above), Projects, People, Insights; Teams and Packages below.
9696 route("-/projects", "routes/workspace/projects.tsx"),
97− // People mode: the directory of people and agents, each one's profile, and the org chart.
97+ // People mode: the directory of people, each one's profile, and the org chart. Agents
98+ // are not in it; an agent's old People address redirects to its page in Agents.
9899 route("-/people", "routes/workspace/people.tsx"),
99100 route("-/people/agents/:handle", "routes/workspace/people/agent.tsx"),
100101 route("-/people/:username", "routes/workspace/people/person.tsx"),
+21−3
1−import { ChevronRight, History, UsersRound } from "lucide-react";
1+import { ChevronRight, History, Sparkles, UsersRound } from "lucide-react";
22 import { useState } from "react";
33 import { Form, data, redirect, useNavigation, useOutletContext } from "react-router";
44
1313 import { Alert } from "../../../components/ui/alert";
1414 import { Button } from "../../../components/ui/button";
1515 import { Card } from "../../../components/ui/card";
16+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../components/ui/collapsible";
17+import { ToldText } from "../../../components/people";
1618 import { isOrchestrator } from "../../../components/orchestrator";
1719 import { readAgentForm } from "../../../lib/agent-form";
1820 import { docs, workspaceAgents } from "../../../lib/services.server";
2123 import { AgentTeamsEditor } from "../../../components/teams";
2224 import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server";
2325
24−/** The teams it is on (and may join), and its saved versions. */
26+/** The teams it is on (and may join) and what it is told about them, and its saved versions. */
2527 export async function loader({ params, context, request }: Route.LoaderArgs) {
2628 const viewer = requireUser(context, request);
2729 const role = roleIn(viewer, params.owner);
28− const [teams, versions, spaces, effortCosts] = await Promise.all([
30+ const [teams, told, versions, spaces, effortCosts] = await Promise.all([
2931 workspaceAgents
3032 .get(params.owner.toLowerCase(), params.handle.toLowerCase(), viewer)
3133 .then((found) => (found.ok && found.value.scope !== "personal" ? agentTeamsFor(viewer, params.owner.toLowerCase(), found.value.id) : null))
3234 .catch(() => null),
35+ // What it is told about its teams every turn, word for word.
36+ readOrNull(workspaceAgents.teamContext(params.owner.toLowerCase(), params.handle.toLowerCase(), viewer)).then((context) => context?.text || null),
3337 readOrNull(workspaceAgents.versions(params.owner.toLowerCase(), params.handle.toLowerCase(), viewer)),
3438 readingSpaces(params.owner.toLowerCase(), viewer),
3539 // What each effort level has cost it, beside the control.
3741 ]);
3842 return {
3943 teams,
44+ told,
4045 slug: params.owner.toLowerCase(),
4146 versions,
4247 spaces,
140145 personal={personal}
141146 change={actionData && "teamChange" in actionData ? actionData.teamChange : null}
142147 />
148+ {loaderData.told && (
149+ <Collapsible className="mt-4 border-t border-line pt-3">
150+ <CollapsibleTrigger>
151+ <ChevronRight size={14} className="shrink-0 text-faint" />
152+ <Sparkles size={14} className="text-accent" />
153+ What {agent.display_name} knows about its teams
154+ </CollapsibleTrigger>
155+ <CollapsibleContent className="mt-3">
156+ <p className="mb-3 text-xs text-faint">Told every turn, from the team pages and who&apos;s around now.</p>
157+ <ToldText text={loaderData.told} />
158+ </CollapsibleContent>
159+ </Collapsible>
160+ )}
143161 </section>
144162 </Card>
145163 )}
+2−2
4949 <Branch key={root.person.username} slug={slug} node={root} />
5050 ))}
5151 </ul>
52− <p className="mt-4 text-xs text-faint">Agents appear beside the person who leads their team.</p>
52+ <p className="mt-4 text-xs text-faint">The agents on a team appear beside the person who leads it, as its members; each opens the agent's page in Agents.</p>
5353 </section>
5454 </Card>
5555
110110 <span className="inline-flex items-center gap-2">
111111 <span className="flex">
112112 {node.agents.slice(0, 6).map((agent, index) => (
113− <Hint key={agent.id} label={`${agent.display_name} · ${agent.title || agent.role}`}>
113+ <Hint key={agent.id} label={`${agent.display_name} · ${agent.title || agent.role} · agent`}>
114114 <Link to={agentPath(slug, agent.handle)} className={index ? "-ml-1.5" : undefined} aria-label={agent.display_name}>
115115 <AgentPersonFace agent={agent} size={22} ring="var(--color-surface)" />
116116 </Link>
+43−75
1−import { Network, Search, UserPlus } from "lucide-react";
1+import { Bot, Network, Search, UserPlus } from "lucide-react";
22 import { Link, data, useSearchParams, type ShouldRevalidateFunctionArgs } from "react-router";
33
44 import type { Route } from "./+types/people";
5−import { AgentPersonFace, AgentTag, LocalTime, PersonFace } from "../../components/people";
5+import { LocalTime, PersonFace } from "../../components/people";
66 import { ButtonLink, notACredential } from "../../components/ui";
77 import { Badge } from "../../components/ui/badge";
88 import { Button } from "../../components/ui/button";
99 import { Card } from "../../components/ui/card";
1010 import { page } from "../../lib/meta";
11−import { type DirectoryEntry, type DirectoryKind, agentPath, directoryEntries, directoryKind, peopleAgent, personName, personPath } from "../../lib/people";
12−import { identity, workspaceAgents } from "../../lib/services.server";
11+import { type DirectoryEntry, directoryEntries, personName, personPath } from "../../lib/people";
12+import { identity } from "../../lib/services.server";
1313 import { getViewer, roleIn, unwrap } from "../../lib/session.server";
1414
1515 export function meta({ params, ...args }: Route.MetaArgs) {
1616 return page(args, { title: `People · ${params.owner} · g1t` });
1717 }
1818
19+/**
20+ * The workspace's people. Its agents are not here: they are listed under
21+ * Agents, and a team's page lists the agents on it with its people.
22+ */
1923 export async function loader({ params, context }: Route.LoaderArgs) {
2024 const viewer = getViewer(context);
2125 // Who is in a workspace is its members' business.
2226 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
23− const [directory, agents] = await Promise.all([
24− identity.peopleDirectory(viewer, params.owner).then(unwrap),
25− workspaceAgents.list(params.owner, viewer!).catch(() => null),
26− ]);
27− return {
28− slug: params.owner.toLowerCase(),
29− directory,
30− agents: agents?.ok ? agents.value.map(peopleAgent) : [],
31− };
27+ const directory = await identity.peopleDirectory(viewer, params.owner).then(unwrap);
28+ return { slug: params.owner.toLowerCase(), directory };
3229 }
3330
3431 /** Searching changes only the address: everyone is already here. */
3734 return defaultShouldRevalidate;
3835 }
3936
40−const KINDS: { value: DirectoryKind; label: string }[] = [
41− { value: "everyone", label: "Everyone" },
42− { value: "people", label: "People" },
43− { value: "agents", label: "Agents" },
44−];
45−
4637 export default function PeopleDirectoryPage({ loaderData }: Route.ComponentProps) {
47− const { slug, directory, agents } = loaderData;
38+ const { slug, directory } = loaderData;
4839 const [params, setParams] = useSearchParams();
4940 const query = params.get("q") ?? "";
50− const kind = directoryKind(params.get("kind"));
51− const entries = directoryEntries(directory, agents, query, kind);
41+ const entries = directoryEntries(directory, query);
42+ const total = directory.people.length;
5243 const change = (key: string, value: string) =>
5344 setParams(
5445 (current) => {
5950 },
6051 { replace: true, preventScrollReset: true },
6152 );
62− const counts = { everyone: directory.people.length + agents.length, people: directory.people.length, agents: agents.length };
6353
6454 return (
6555 <div className="space-y-6">
6656 <div className="flex flex-col gap-3 md:flex-row md:items-center">
6757 <label className="relative block grow">
68− <span className="sr-only">Search people and agents</span>
58+ <span className="sr-only">Search people</span>
6959 <Search size={15} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
7060 <input
7161 type="search"
7767 />
7868 </label>
7969 <div className="flex flex-wrap items-center gap-2">
80− <Card role="group" aria-label="Show" tone="bg" radius="lg" className="inline-flex p-0.5">
81− {KINDS.map((option) => (
82− <button
83− key={option.value}
84− type="button"
85− aria-pressed={kind === option.value}
86− onClick={() => change("kind", option.value === "everyone" ? "" : option.value)}
87− className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
88− kind === option.value ? "bg-raised text-fg" : "text-muted hover:text-fg"
89− }`}
90− >
91− {option.label} <span className="text-faint tabular-nums">{counts[option.value]}</span>
92− </button>
93− ))}
94− </Card>
70+ <span role="status" className="mr-1 text-sm text-muted tabular-nums">
71+ {query ? `${entries.length} of ${total}` : total} {total === 1 && !query ? "person" : "people"}
72+ </span>
9573 <ButtonLink to={`/${slug}/-/org-chart`} variant="outline">
9674 <Network size={15} />
9775 Org chart
123101 ) : (
124102 <ul className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
125103 {entries.map((entry) => (
126− <li key={entry.kind === "person" ? `p:${entry.person.username}` : `a:${entry.agent.id}`}>
127− <EntryCard slug={slug} entry={entry} />
104+ <li key={entry.person.username}>
105+ <PersonCard slug={slug} entry={entry} />
128106 </li>
129107 ))}
130108 </ul>
131109 )}
110+
111+ <p className="flex items-start gap-2 text-xs text-faint">
112+ <Bot size={14} className="mt-px shrink-0" />
113+ <span>
114+ Agents aren&apos;t listed here. The workspace&apos;s agents are under{" "}
115+ <Link to={`/${slug}/-/agents`} className="text-muted hover:text-fg">
116+ Agents
117+ </Link>
118+ , and a <Link to={`/${slug}/-/teams`} className="text-muted hover:text-fg">team</Link> lists the agents on it with its people.
119+ </span>
120+ </p>
132121 </div>
133122 );
134123 }
135124
136−/** One person or agent: who they are, their title, their teams and, for a person, their local time. */
137−function EntryCard({ slug, entry }: { slug: string; entry: DirectoryEntry }) {
125+/** One person: who they are, their title, their teams and their local time. */
126+function PersonCard({ slug, entry }: { slug: string; entry: DirectoryEntry }) {
127+ const { person } = entry;
138128 const teams = entry.teams.map((team) => team.name);
139− if (entry.kind === "person") {
140− const { person } = entry;
141− return (
142− <Link
143− to={personPath(slug, person.username)}
144− prefetch="intent"
145− className="flex h-full items-start gap-3 rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong"
146− >
147− <PersonFace person={person} size={44} ring="var(--color-surface)" />
148− <span className="min-w-0 grow">
149− <span className="flex items-center gap-1.5">
150− <span className="truncate font-medium">{personName(person)}</span>
151− {person.role === "owner" && <Badge>Owner</Badge>}
152− </span>
153− <span className="block truncate text-sm text-muted">{person.title ?? `@${person.display_username ?? person.username}`}</span>
154− <span className="mt-1.5 block truncate text-xs text-faint">
155− {teams.length ? teams.join(", ") : "No team"}
156− <LocalTime zone={person.timezone} className="before:mx-1.5 before:content-['·']" />
157− </span>
158− {person.owns.length > 0 && <span className="mt-1 block truncate text-xs text-fg-soft">Owns {person.owns.join(", ")}</span>}
159− </span>
160− </Link>
161− );
162− }
163− const { agent } = entry;
164129 return (
165130 <Link
166− to={agentPath(slug, agent.handle)}
131+ to={personPath(slug, person.username)}
167132 prefetch="intent"
168133 className="flex h-full items-start gap-3 rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong"
169134 >
170− <AgentPersonFace agent={agent} size={44} ring="var(--color-surface)" />
135+ <PersonFace person={person} size={44} ring="var(--color-surface)" />
171136 <span className="min-w-0 grow">
172137 <span className="flex items-center gap-1.5">
173− <span className="truncate font-medium">{agent.display_name}</span>
174− <AgentTag />
138+ <span className="truncate font-medium">{personName(person)}</span>
139+ {person.role === "owner" && <Badge>Owner</Badge>}
140+ </span>
141+ <span className="block truncate text-sm text-muted">{person.title ?? `@${person.display_username ?? person.username}`}</span>
142+ <span className="mt-1.5 block truncate text-xs text-faint">
143+ {teams.length ? teams.join(", ") : "No team"}
144+ <LocalTime zone={person.timezone} className="before:mx-1.5 before:content-['·']" />
175145 </span>
176− <span className="block truncate text-sm text-muted">{agent.title || agent.role}</span>
177− <span className="mt-1.5 block truncate text-xs text-faint">{teams.length ? teams.join(", ") : "No team"}</span>
178− {agent.responsibilities.length > 0 && <span className="mt-1 block truncate text-xs text-fg-soft">{agent.responsibilities.slice(0, 2).join(" · ")}</span>}
146+ {person.owns.length > 0 && <span className="mt-1 block truncate text-xs text-fg-soft">Owns {person.owns.join(", ")}</span>}
179147 </span>
180148 </Link>
181149 );
+9−222
1−import { ArrowUpRight, Bot, ChevronRight, KeyRound, MessageSquare, Sparkles, UsersRound } from "lucide-react";
2−import type { ReactNode } from "react";
3−import { Link, data } from "react-router";
1+/**
2+ * An agent's old People address, `-/people/agents/:handle`. People lists
3+ * people only; an agent's profile is its page in Agents, so links to the
4+ * old address are sent there for good (lib/people.ts `agentPath`).
5+ */
6+import { redirect } from "react-router";
47
58 import type { Route } from "./+types/agent";
6−import { AgentLink, AgentPersonFace, AgentTag, Coming, PersonFace, PersonLink, TeamKindMark, ToldText } from "../../../components/people";
7−import { statusLabel } from "../../../components/chat/marks";
8−import { ButtonLink } from "../../../components/ui";
9−import { Badge } from "../../../components/ui/badge";
10−import { page } from "../../../lib/meta";
11−import { agentsOn, leads, peopleAgent, teamsOfAgent } from "../../../lib/people";
12−import { teamPath } from "../../../lib/teams";
13−import { money } from "../../../lib/money";
14−import { identity, workspaceAgents } from "../../../lib/services.server";
15−import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../../lib/session.server";
16−import { agentTeamsFor, changeAgentTeam } from "../../../lib/agent-teams.server";
17−import { AgentTeamsEditor } from "../../../components/teams";
18−
19−export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
20− return page(args, { title: `${loaderData?.agent.display_name ?? params.handle} · People · ${params.owner} · g1t` });
21−}
22−
23−export async function loader({ params, context }: Route.LoaderArgs) {
24− const viewer = getViewer(context);
25− if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
26− const [found, directory, listed, told] = await Promise.all([
27− workspaceAgents.get(params.owner, params.handle, viewer!),
28− identity.peopleDirectory(viewer, params.owner).then(unwrap),
29− workspaceAgents.list(params.owner, viewer!).catch(() => null),
30− workspaceAgents.teamContext(params.owner, params.handle, viewer!).catch(() => null),
31− ]);
32− if (!found.ok) throw data(null, { status: 404 });
33− const agent = found.value;
34− const agents = listed?.ok ? listed.value.map(peopleAgent) : [peopleAgent(agent)];
35− const teams = teamsOfAgent(directory.teams, agent);
36− // Its teams to change here, as a person's are changed on the team: Remove where the viewer manages one, and Add.
37− const editable = agent.scope === "personal" ? null : await agentTeamsFor(viewer!, params.owner.toLowerCase(), agent.id, directory);
38− // Everyone on its teams, each once, leads first.
39− const names = new Set(teams.flatMap((team) => team.people.map((p) => p.username)));
40− const leadNames = new Set(teams.flatMap((team) => (team.lead?.kind === "user" ? [team.lead.username] : [])));
41− const teammates = directory.people
42− .filter((person) => names.has(person.username))
43− .sort((a, b) => Number(leadNames.has(b.username)) - Number(leadNames.has(a.username)))
44− .map(({ user_id, username, display_username, name, avatar, title, owns }) => ({ user_id, username, display_username, name, avatar, title, owns, lead: leadNames.has(username) }));
45− return {
46− slug: params.owner.toLowerCase(),
47− agent: { ...peopleAgent(agent), monthly_micros: agent.budget.monthly_micros },
48− personal: agent.scope === "personal",
49− editable,
50− teams,
51− teammates,
52− agents,
53− told: told?.ok ? told.value.text : null,
54− };
55−}
56−
57−/** Adding the agent to a team, or taking it off one. */
58−export async function action({ request, params, context }: Route.ActionArgs) {
59− assertSameOrigin(request);
60− const viewer = requireUser(context, request);
61− if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
62− const changed = await changeAgentTeam(await request.formData(), viewer, params.owner.toLowerCase(), params.handle.toLowerCase());
63− return changed ?? { intent: "join-team" as const, team: "", error: "Unknown request." };
64−}
65−
66−export default function AgentProfile({ loaderData, actionData }: Route.ComponentProps) {
67− const { slug, agent, teams, teammates, agents, told, editable, personal } = loaderData;
68− const otherAgents = teams
69− .flatMap((team) => agentsOn(team, agents))
70− .filter((other, index, all) => other.id !== agent.id && all.findIndex((a) => a.id === other.id) === index);
71−
72− return (
73− <div className="space-y-8">
74− <nav aria-label="Breadcrumb" className="flex items-center gap-1 text-sm text-muted">
75− <Link to={`/${slug}/-/people?kind=agents`} className="hover:text-fg">
76− People
77− </Link>
78− <ChevronRight size={13} className="text-faint" />
79− <span className="truncate text-fg">{agent.display_name}</span>
80− </nav>
81−
82− <header className="flex flex-col gap-5 border-b border-line pb-6 sm:flex-row sm:items-start">
83− <div className="self-start">
84− <AgentPersonFace agent={agent} size={72} />
85− </div>
86− <div className="min-w-0 grow space-y-2">
87− <div className="flex flex-wrap items-center gap-2">
88− <h1 className="text-2xl font-semibold tracking-tight">{agent.display_name}</h1>
89− <AgentTag />
90− </div>
91− <p className="text-sm text-muted">{[agent.title || agent.role, `@${agent.handle}`].join(" · ")}</p>
92− <p className="text-sm text-muted">{statusLabel(agent.status)}</p>
93− </div>
94− <div className="flex shrink-0 flex-wrap gap-2">
95− <ButtonLink to={`/${slug}/-/chat?agent=${agent.handle}`}>
96− <MessageSquare size={15} />
97− Message
98− </ButtonLink>
99− <ButtonLink to={`/${slug}/-/agents/${agent.handle}`} variant="outline">
100− Sessions and settings
101− <ArrowUpRight size={14} />
102− </ButtonLink>
103− </div>
104− </header>
105−
106− {agent.responsibilities.length > 0 && (
107− <section className="flex flex-wrap items-center gap-1.5">
108− <span className="mr-1 text-sm text-muted">Responsible for</span>
109− {agent.responsibilities.map((duty) => (
110− <Badge key={duty} tone="info">
111− {duty}
112− </Badge>
113− ))}
114− </section>
115− )}
116−
117− <div className="grid gap-4 md:grid-cols-2">
118− <Card icon={<UsersRound size={14} />} title="Teams">
119− {editable || personal ? (
120− <AgentTeamsEditor slug={slug} name={agent.display_name} teams={editable} personal={personal} change={actionData} />
121− ) : teams.length ? (
122− <ul className="space-y-2">
123− {teams.map((team) => (
124− <li key={team.slug} className="flex flex-wrap items-center gap-2">
125− <TeamKindMark people={team.people.length} agents={agentsOn(team, agents).length} />
126− <Link to={teamPath(slug, team.slug)} className="font-medium hover:text-accent">
127− {team.name}
128− </Link>
129− {leads(team.lead, { id: agent.id }) && <Badge tone="accent">Lead</Badge>}
130− </li>
131− ))}
132− </ul>
133− ) : (
134− <Quiet>Not on a team yet.</Quiet>
135− )}
136− </Card>
137−
138− <Card icon={<Sparkles size={14} />} title="Who it works with">
139− {teammates.length ? (
140− <ul className="space-y-2">
141− {teammates.map((person) => (
142− <li key={person.username} className="flex min-w-0 items-center gap-2.5 text-sm">
143− <PersonFace person={person} size={24} ring="var(--color-surface)" />
144− <span className="min-w-0 grow truncate">
145− <PersonLink workspace={slug} person={person} className="font-medium" />
146− {person.title && <span className="text-muted"> · {person.title}</span>}
147− </span>
148− {person.lead && <Badge tone="accent">Lead</Badge>}
149− </li>
150− ))}
151− </ul>
152− ) : (
153− <Quiet>{teams.length ? "Its teams are agents only." : "No people yet."}</Quiet>
154− )}
155− </Card>
156−
157− <Card icon={<Bot size={14} />} title="Agents on its teams">
158− {otherAgents.length ? (
159− <ul className="space-y-2">
160− {otherAgents.map((other) => (
161− <li key={other.id} className="flex min-w-0 items-center gap-2.5 text-sm">
162− <AgentPersonFace agent={other} size={24} ring="var(--color-surface)" />
163− <span className="min-w-0 truncate">
164− <AgentLink workspace={slug} agent={other} className="font-medium" />
165− <span className="text-muted"> · {other.title || other.role}</span>
166− </span>
167− </li>
168− ))}
169− </ul>
170− ) : (
171− <Quiet>No other agents on its teams.</Quiet>
172− )}
173− </Card>
174−
175− <Card icon={<KeyRound size={14} />} title="Access">
176− <dl className="space-y-3 text-sm">
177− <div>
178− <dt className="text-xs text-faint">Code</dt>
179− <dd className="mt-0.5">What the person who asks it can read, and only what everyone in the conversation can see</dd>
180− </div>
181− <div>
182− <dt className="flex items-center gap-2 text-xs text-faint">
183− Storage <Coming />
184− </dt>
185− <dd className="mt-0.5 text-muted">Which of the workspace's files it may read and write comes with Storage.</dd>
186− </div>
187− <div>
188− <dt className="text-xs text-faint">Spent this month</dt>
189− <dd className="mt-0.5 tabular-nums">
190− {money(agent.spent_month_micros)}
191− {agent.monthly_micros != null && <span className="text-muted"> of {money(agent.monthly_micros)}</span>}
192− </dd>
193− </div>
194− </dl>
195− </Card>
196− </div>
197−
198− <section className="rounded-xl border border-accent/40 bg-surface p-4">
199− <div className="mb-3 flex flex-wrap items-baseline justify-between gap-2">
200− <h2 className="flex items-center gap-2 text-sm font-medium">
201− <Sparkles size={14} className="text-accent" />
202− What {agent.display_name} knows about its teams
203− </h2>
204− <span className="text-xs text-faint">Told every turn, from the team pages and who's around now</span>
205− </div>
206− {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>}
207− </section>
208− </div>
209− );
210−}
9+import { agentPath } from "../../../lib/people";
21110
212−function Card({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) {
213− return (
214− <section className="rounded-xl border border-line bg-surface p-4">
215− <h2 className="mb-3 flex items-center gap-2 text-sm font-medium text-muted">
216− <span className="text-faint">{icon}</span>
217− {title}
218− </h2>
219− {children}
220− </section>
221− );
222−}
223−
224−function Quiet({ children }: { children: ReactNode }) {
225− return <p className="text-sm text-faint">{children}</p>;
11+export function loader({ params }: Route.LoaderArgs) {
12+ return redirect(agentPath(params.owner.toLowerCase(), params.handle.toLowerCase()), 301);
22613 }
+2−1
44 import type { TeamRole } from "@g1t/contracts";
55
66 import type { Route } from "./+types/members";
7−import { AgentLink, AgentPersonFace, ToldText } from "../../../components/people";
7+import { AgentLink, AgentPersonFace, AgentTag, ToldText } from "../../../components/people";
88 import { useTeam, useTeamAgents } from "../../../components/teams";
99 import { ErrorText, Field, Input, SubmitButton } from "../../../components/ui";
1010 import { Avatar } from "../../../components/ui/avatar";
211211 <AgentLink workspace={team.workspace} agent={agent} className="font-medium" />
212212 <span className="ml-2 text-muted">{agent.title || agent.role}</span>
213213 </div>
214+ <AgentTag />
214215 {leads(team.lead, { id: agent.id }) && <Badge tone="accent">Lead</Badge>}
215216 {manage && (
216217 <Form method="post">
+4−3
448448 told each visible team it is on: who leads it, each person's title, what
449449 they own, who they report to, whether they are around and their local
450450 time, and who to ask when a person is needed. Ask the person who owns
451−something before guessing. The directory of people and agents is
452−`g1t.sh/<owner>/-/people`, profiles `-/people/<username>` and
453−`-/people/agents/<handle>`, the org chart `-/org-chart`. Guide:
451+something before guessing. The directory of people is
452+`g1t.sh/<owner>/-/people` (people only; a team's page lists the agents on
453+it), profiles `-/people/<username>`, the org chart `-/org-chart`; an
454+agent's page is `-/agents/<handle>`. Guide:
454455 https://docs.g1t.sh/guides/people-and-teams/
455456
456457 Anyone creates an agent by describing it at `g1t.sh/<owner>/-/agents/new`: