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.
17 files+180−3990/17 viewed
| 284 | 284 | | To | Where | Who | | |
| 285 | 285 | | --- | --- | --- | | |
| 286 | 286 | | 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 | | |
| 288 | 288 | | 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 | | |
| 289 | 289 | ||
| 290 | 290 | A personal agent is on no team; once an owner | |
| 291 | 291 | [promotes](#promote-a-personal-agent) it, add it to teams like anyone. | |
| 292 | 292 | ||
| 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 | + | ||
| 293 | 299 | The Agents sidebar groups specialists by the teams they are on, each team | |
| 294 | 300 | by name: an agent on two teams shows under both, and agents on none show | |
| 295 | 301 | last, under **Not on a team**. Every turn, an agent is told who is on its | |
| ⋯ | |||
| 841 | 847 | | **Routines** | Work it does on a schedule or when something happens. See [routines](/guides/agent-routines/). | | |
| 842 | 848 | | **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. | | |
| 843 | 849 | | **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). | | |
| 845 | 851 | ||
| 846 | 852 | The **Agents** page above them all shows the agent budget, sessions waiting | |
| 847 | 853 | on you and working now, every agent with its live sessions and its month's | |
| 1 | 1 | --- | |
| 2 | 2 | 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. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 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: | |
| 9 | 11 | ||
| 10 | 12 | | Page | Address | What it is | | |
| 11 | 13 | | --- | --- | --- | | |
| 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. | | |
| 13 | 15 | | **Teams** | `g1t.sh/<workspace>/-/teams` | The workspace's [teams](/guides/teams/). | | |
| 14 | 16 | | **Org chart** | `g1t.sh/<workspace>/-/org-chart` | Who reports to whom. See [the org chart](#the-org-chart). | | |
| 15 | 17 | | **Members and invites** | `g1t.sh/<workspace>/-/members` | Under **Membership**: roles, invitations and leaving. See [members and invites](#members-and-invites). | | |
| ⋯ | |||
| 18 | 20 | ||
| 19 | 21 | ## The directory | |
| 20 | 22 | ||
| 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. | |
| 23 | 25 | ||
| 24 | 26 | 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**. | |
| 28 | 28 | ||
| 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`. | |
| 39 | 32 | ||
| 40 | 33 | Each card shows: | |
| 41 | 34 | ||
| 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; | |
| 44 | 36 | - their name and title; | |
| 45 | 37 | - their teams; | |
| 46 | 38 | - their local time, from the time zone on their | |
| 47 | 39 | [profile](https://g1t.sh/settings/profile); | |
| 48 | 40 | - what they own. | |
| 49 | 41 | ||
| 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 | + | ||
| 50 | 47 | **Org chart** opens [the org chart](#the-org-chart). Owners also see | |
| 51 | 48 | **Invite people**, which opens [Members and invites](#members-and-invites). | |
| 52 | 49 | ||
| ⋯ | |||
| 91 | 88 | When someone leaves the workspace, anyone who reported to them reports to | |
| 92 | 89 | no one, and any team they led has no lead. | |
| 93 | 90 | ||
| 94 | − | ## An agent's profile | |
| 91 | + | ## Agents | |
| 95 | 92 | ||
| 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. | |
| 99 | 101 | ||
| 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. | |
| 108 | 106 | ||
| 109 | − | **Message** opens your direct message with it. **Sessions and settings** | |
| 110 | − | opens its page in [Agents](/guides/agents/). | |
| 111 | − | ||
| 112 | 107 | ## Teams of any mix | |
| 113 | 108 | ||
| 114 | 109 | A [team](/guides/teams/) can be people and agents together, people only, | |
| ⋯ | |||
| 119 | 114 | ||
| 120 | 115 | | | Where | | |
| 121 | 116 | | --- | --- | | |
| 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). | | |
| 123 | 118 | | Set the lead, channel and budget | The team's **Settings**. See [lead, channel and budget](/guides/teams/#lead-channel-and-budget). | | |
| 124 | 119 | | Storage for a team | Coming. | | |
| 125 | 120 | ||
| ⋯ | |||
| 127 | 122 | ||
| 128 | 123 | The org chart, `g1t.sh/<workspace>/-/org-chart`, draws the reporting | |
| 129 | 124 | 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. | |
| 131 | 130 | ||
| 132 | 131 | Under **Teams no person leads** are the teams led by an agent or by no | |
| 133 | 132 | one, each with its agents. | |
| ⋯ | |||
| 180 | 179 | ||
| 181 | 180 | You can read what an agent is told: | |
| 182 | 181 | ||
| 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**; | |
| 184 | 184 | - on a team's **People and agents** tab, under **What the agents on** | |
| 185 | 185 | *team* **know about it**, for that team's part. | |
| 186 | 186 | ||
| 136 | 136 | 3. To take an agent off, choose **Remove** beside it. | |
| 137 | 137 | ||
| 138 | 138 | 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. | |
| 142 | 143 | ||
| 143 | 144 | An agent on a team works with the access of whoever asks it, not with the | |
| 144 | 145 | team's roles on repositories. Being on a team tells the agent about it |
| 487 | 487 | | **Projects** | `/-/projects` | Everyone | Every project you can see. See [the Projects page](#the-projects-page). | | |
| 488 | 488 | | [**Packages**](/guides/packages/) | `/-/packages` | Everyone | What the workspace publishes. A visitor opens it from **Packages** on the workspace's page. | | |
| 489 | 489 | | [**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. | | |
| 492 | 492 | | **Members and invites** | `/-/members` | Members | Who belongs. Owners invite, change roles and remove people here. | | |
| 493 | 493 | | **Insights** | `/-/insights` | Members | Coming soon: how the whole workspace delivers. | | |
| 494 | 494 | | **Settings** | `/-/settings` | Owners | How the workspace is set up and connected (below). | | |
| ⋯ | |||
| 554 | 554 | | **Agents** | The workspace's agents and their sessions. See [agents](/guides/agents/). | | |
| 555 | 555 | | **Code** | Projects, pull requests and checks. Not shown to a member without [Code access](/guides/agent-access/). | | |
| 556 | 556 | | **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/). | | |
| 558 | 558 | | **Workspace** | Usage, billing, integrations, policies and settings. | | |
| 559 | 559 | ||
| 560 | 560 | The app you are in sits on a filled square. Your account is the | |
| 64 | 64 | | [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" /> | | |
| 65 | 65 | | [Chat](/guides/chat/) | Channels, direct messages and threads, live. People and agents are members alike. No seats and no history limit. | <Status is="live" /> | | |
| 66 | 66 | | [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" /> | | |
| 68 | 68 | | [Artifacts](/guides/artifacts/) | Documents written together, live, by people and agents. Private until you share them. | <Status is="live" /> | | |
| 69 | 69 | | More kinds of artifact | Slides, designs and dashboards that keep the query behind every number. | <Status is="coming" /> | | |
| 70 | 70 | | Scratchpads | Tools, reports and agents anyone can build on a copy of the data, used in production only after review. | <Status is="coming" /> | |
| 103 | 103 | ); | |
| 104 | 104 | } | |
| 105 | 105 | ||
| 106 | − | /** An agent's name, linked to its People profile. */ | |
| 106 | + | /** An agent's name, linked to its page in Agents. */ | |
| 107 | 107 | export function AgentLink({ workspace, agent, className }: { workspace: string; agent: Pick<PeopleAgent, "handle" | "display_name">; className?: string }) { | |
| 108 | 108 | return ( | |
| 109 | 109 | <Link to={agentPath(workspace, agent.handle)} prefetch="intent" className={cn("hover:text-accent", className)}> |
| 1342 | 1342 | } | |
| 1343 | 1343 | ||
| 1344 | 1344 | /** | |
| 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. | |
| 1347 | 1348 | */ | |
| 1348 | 1349 | function PeopleSidebar({ slug }: { slug: string }) { | |
| 1349 | 1350 | const { pathname } = useLocation(); |
| 6 | 6 | import { | |
| 7 | 7 | type PeopleAgent, | |
| 8 | 8 | NO_TEAM, | |
| 9 | + | agentPath, | |
| 9 | 10 | agentsOn, | |
| 10 | 11 | groupByTeam, | |
| 11 | 12 | teamsByAgent, | |
| ⋯ | |||
| 115 | 116 | assert.deepEqual(groupByTeam(agents.slice(3), {}).map((g) => g.label), [NO_TEAM]); | |
| 116 | 117 | }); | |
| 117 | 118 | ||
| 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"]); | |
| 122 | 123 | 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"), []); | |
| 126 | 130 | 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"); | |
| 127 | 139 | }); | |
| 128 | 140 | ||
| 129 | 141 | test("the org chart follows reporting lines, with each team's agents beside its lead", () => { | |
| 1 | 1 | /** | |
| 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`. | |
| 5 | 5 | */ | |
| 6 | 6 | import type { DirectoryPerson, DirectoryTeam, PeopleDirectory, TeamLead, WorkspaceAgent } from "@g1t/contracts"; | |
| 7 | 7 | ||
| ⋯ | |||
| 22 | 22 | return `/${workspace}/-/people/${username}`; | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 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. */ | |
| 26 | 26 | export function agentPath(workspace: string, handle: string): string { | |
| 27 | − | return `/${workspace}/-/people/agents/${handle}`; | |
| 27 | + | return `/${workspace}/-/agents/${handle}`; | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | 30 | /** The agents on a team, in the order they were added, as found among `agents`. */ | |
| ⋯ | |||
| 101 | 101 | return words.every((word) => text.includes(word)); | |
| 102 | 102 | } | |
| 103 | 103 | ||
| 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[] }; | |
| 114 | 106 | ||
| 115 | 107 | /** | |
| 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. | |
| 120 | 113 | */ | |
| 121 | − | export function directoryEntries(directory: PeopleDirectory, agents: readonly PeopleAgent[], query: string, kind: DirectoryKind): DirectoryEntry[] { | |
| 114 | + | export function directoryEntries(directory: PeopleDirectory, query: string): DirectoryEntry[] { | |
| 122 | 115 | 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 }); | |
| 136 | 120 | } | |
| 137 | 121 | return out; | |
| 138 | 122 | } | |
| 102 | 102 | assert.equal(modeOf("/acme/-/people", "acme"), "people"); | |
| 103 | 103 | assert.equal(modeOf("/acme/-/teams/web", "acme"), "people"); | |
| 104 | 104 | 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. | |
| 105 | 106 | assert.equal(modeOf("/acme/-/people/agents/margo", "acme"), "people"); | |
| 106 | 107 | assert.equal(modeOf("/acme/-/org-chart", "acme"), "people"); | |
| 107 | 108 | assert.equal(modeOf("/acme/-/members", "acme"), "people"); |
| 94 | 94 | index("routes/workspace/overview.tsx"), | |
| 95 | 95 | // Its pages, each in the sidebar: Overview (above), Projects, People, Insights; Teams and Packages below. | |
| 96 | 96 | 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. | |
| 98 | 99 | route("-/people", "routes/workspace/people.tsx"), | |
| 99 | 100 | route("-/people/agents/:handle", "routes/workspace/people/agent.tsx"), | |
| 100 | 101 | route("-/people/:username", "routes/workspace/people/person.tsx"), |
| 1 | − | import { ChevronRight, History, UsersRound } from "lucide-react"; | |
| 1 | + | import { ChevronRight, History, Sparkles, UsersRound } from "lucide-react"; | |
| 2 | 2 | import { useState } from "react"; | |
| 3 | 3 | import { Form, data, redirect, useNavigation, useOutletContext } from "react-router"; | |
| 4 | 4 | ||
| ⋯ | |||
| 13 | 13 | import { Alert } from "../../../components/ui/alert"; | |
| 14 | 14 | import { Button } from "../../../components/ui/button"; | |
| 15 | 15 | import { Card } from "../../../components/ui/card"; | |
| 16 | + | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../components/ui/collapsible"; | |
| 17 | + | import { ToldText } from "../../../components/people"; | |
| 16 | 18 | import { isOrchestrator } from "../../../components/orchestrator"; | |
| 17 | 19 | import { readAgentForm } from "../../../lib/agent-form"; | |
| 18 | 20 | import { docs, workspaceAgents } from "../../../lib/services.server"; | |
| ⋯ | |||
| 21 | 23 | import { AgentTeamsEditor } from "../../../components/teams"; | |
| 22 | 24 | import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server"; | |
| 23 | 25 | ||
| 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. */ | |
| 25 | 27 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 26 | 28 | const viewer = requireUser(context, request); | |
| 27 | 29 | 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([ | |
| 29 | 31 | workspaceAgents | |
| 30 | 32 | .get(params.owner.toLowerCase(), params.handle.toLowerCase(), viewer) | |
| 31 | 33 | .then((found) => (found.ok && found.value.scope !== "personal" ? agentTeamsFor(viewer, params.owner.toLowerCase(), found.value.id) : null)) | |
| 32 | 34 | .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), | |
| 33 | 37 | readOrNull(workspaceAgents.versions(params.owner.toLowerCase(), params.handle.toLowerCase(), viewer)), | |
| 34 | 38 | readingSpaces(params.owner.toLowerCase(), viewer), | |
| 35 | 39 | // What each effort level has cost it, beside the control. | |
| ⋯ | |||
| 37 | 41 | ]); | |
| 38 | 42 | return { | |
| 39 | 43 | teams, | |
| 44 | + | told, | |
| 40 | 45 | slug: params.owner.toLowerCase(), | |
| 41 | 46 | versions, | |
| 42 | 47 | spaces, | |
| ⋯ | |||
| 140 | 145 | personal={personal} | |
| 141 | 146 | change={actionData && "teamChange" in actionData ? actionData.teamChange : null} | |
| 142 | 147 | /> | |
| 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's around now.</p> | |
| 157 | + | <ToldText text={loaderData.told} /> | |
| 158 | + | </CollapsibleContent> | |
| 159 | + | </Collapsible> | |
| 160 | + | )} | |
| 143 | 161 | </section> | |
| 144 | 162 | </Card> | |
| 145 | 163 | )} | |
| 49 | 49 | <Branch key={root.person.username} slug={slug} node={root} /> | |
| 50 | 50 | ))} | |
| 51 | 51 | </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> | |
| 53 | 53 | </section> | |
| 54 | 54 | </Card> | |
| 55 | 55 | ||
| ⋯ | |||
| 110 | 110 | <span className="inline-flex items-center gap-2"> | |
| 111 | 111 | <span className="flex"> | |
| 112 | 112 | {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`}> | |
| 114 | 114 | <Link to={agentPath(slug, agent.handle)} className={index ? "-ml-1.5" : undefined} aria-label={agent.display_name}> | |
| 115 | 115 | <AgentPersonFace agent={agent} size={22} ring="var(--color-surface)" /> | |
| 116 | 116 | </Link> | |
| 1 | − | import { Network, Search, UserPlus } from "lucide-react"; | |
| 1 | + | import { Bot, Network, Search, UserPlus } from "lucide-react"; | |
| 2 | 2 | import { Link, data, useSearchParams, type ShouldRevalidateFunctionArgs } from "react-router"; | |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/people"; | |
| 5 | − | import { AgentPersonFace, AgentTag, LocalTime, PersonFace } from "../../components/people"; | |
| 5 | + | import { LocalTime, PersonFace } from "../../components/people"; | |
| 6 | 6 | import { ButtonLink, notACredential } from "../../components/ui"; | |
| 7 | 7 | import { Badge } from "../../components/ui/badge"; | |
| 8 | 8 | import { Button } from "../../components/ui/button"; | |
| 9 | 9 | import { Card } from "../../components/ui/card"; | |
| 10 | 10 | 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"; | |
| 13 | 13 | import { getViewer, roleIn, unwrap } from "../../lib/session.server"; | |
| 14 | 14 | ||
| 15 | 15 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 16 | 16 | return page(args, { title: `People · ${params.owner} · g1t` }); | |
| 17 | 17 | } | |
| 18 | 18 | ||
| 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 | + | */ | |
| 19 | 23 | export async function loader({ params, context }: Route.LoaderArgs) { | |
| 20 | 24 | const viewer = getViewer(context); | |
| 21 | 25 | // Who is in a workspace is its members' business. | |
| 22 | 26 | 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 }; | |
| 32 | 29 | } | |
| 33 | 30 | ||
| 34 | 31 | /** Searching changes only the address: everyone is already here. */ | |
| ⋯ | |||
| 37 | 34 | return defaultShouldRevalidate; | |
| 38 | 35 | } | |
| 39 | 36 | ||
| 40 | − | const KINDS: { value: DirectoryKind; label: string }[] = [ | |
| 41 | − | { value: "everyone", label: "Everyone" }, | |
| 42 | − | { value: "people", label: "People" }, | |
| 43 | − | { value: "agents", label: "Agents" }, | |
| 44 | − | ]; | |
| 45 | − | ||
| 46 | 37 | export default function PeopleDirectoryPage({ loaderData }: Route.ComponentProps) { | |
| 47 | − | const { slug, directory, agents } = loaderData; | |
| 38 | + | const { slug, directory } = loaderData; | |
| 48 | 39 | const [params, setParams] = useSearchParams(); | |
| 49 | 40 | 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; | |
| 52 | 43 | const change = (key: string, value: string) => | |
| 53 | 44 | setParams( | |
| 54 | 45 | (current) => { | |
| ⋯ | |||
| 59 | 50 | }, | |
| 60 | 51 | { replace: true, preventScrollReset: true }, | |
| 61 | 52 | ); | |
| 62 | − | const counts = { everyone: directory.people.length + agents.length, people: directory.people.length, agents: agents.length }; | |
| 63 | 53 | ||
| 64 | 54 | return ( | |
| 65 | 55 | <div className="space-y-6"> | |
| 66 | 56 | <div className="flex flex-col gap-3 md:flex-row md:items-center"> | |
| 67 | 57 | <label className="relative block grow"> | |
| 68 | − | <span className="sr-only">Search people and agents</span> | |
| 58 | + | <span className="sr-only">Search people</span> | |
| 69 | 59 | <Search size={15} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" /> | |
| 70 | 60 | <input | |
| 71 | 61 | type="search" | |
| ⋯ | |||
| 77 | 67 | /> | |
| 78 | 68 | </label> | |
| 79 | 69 | <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> | |
| 95 | 73 | <ButtonLink to={`/${slug}/-/org-chart`} variant="outline"> | |
| 96 | 74 | <Network size={15} /> | |
| 97 | 75 | Org chart | |
| ⋯ | |||
| 123 | 101 | ) : ( | |
| 124 | 102 | <ul className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> | |
| 125 | 103 | {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} /> | |
| 128 | 106 | </li> | |
| 129 | 107 | ))} | |
| 130 | 108 | </ul> | |
| 131 | 109 | )} | |
| 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't listed here. The workspace'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> | |
| 132 | 121 | </div> | |
| 133 | 122 | ); | |
| 134 | 123 | } | |
| 135 | 124 | ||
| 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; | |
| 138 | 128 | 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; | |
| 164 | 129 | return ( | |
| 165 | 130 | <Link | |
| 166 | − | to={agentPath(slug, agent.handle)} | |
| 131 | + | to={personPath(slug, person.username)} | |
| 167 | 132 | prefetch="intent" | |
| 168 | 133 | className="flex h-full items-start gap-3 rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong" | |
| 169 | 134 | > | |
| 170 | − | <AgentPersonFace agent={agent} size={44} ring="var(--color-surface)" /> | |
| 135 | + | <PersonFace person={person} size={44} ring="var(--color-surface)" /> | |
| 171 | 136 | <span className="min-w-0 grow"> | |
| 172 | 137 | <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-['·']" /> | |
| 175 | 145 | </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>} | |
| 179 | 147 | </span> | |
| 180 | 148 | </Link> | |
| 181 | 149 | ); | |
| 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"; | |
| 4 | 7 | ||
| 5 | 8 | 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"; | |
| 211 | 10 | ||
| 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); | |
| 226 | 13 | } |
| 4 | 4 | import type { TeamRole } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/members"; | |
| 7 | − | import { AgentLink, AgentPersonFace, ToldText } from "../../../components/people"; | |
| 7 | + | import { AgentLink, AgentPersonFace, AgentTag, ToldText } from "../../../components/people"; | |
| 8 | 8 | import { useTeam, useTeamAgents } from "../../../components/teams"; | |
| 9 | 9 | import { ErrorText, Field, Input, SubmitButton } from "../../../components/ui"; | |
| 10 | 10 | import { Avatar } from "../../../components/ui/avatar"; | |
| ⋯ | |||
| 211 | 211 | <AgentLink workspace={team.workspace} agent={agent} className="font-medium" /> | |
| 212 | 212 | <span className="ml-2 text-muted">{agent.title || agent.role}</span> | |
| 213 | 213 | </div> | |
| 214 | + | <AgentTag /> | |
| 214 | 215 | {leads(team.lead, { id: agent.id }) && <Badge tone="accent">Lead</Badge>} | |
| 215 | 216 | {manage && ( | |
| 216 | 217 | <Form method="post"> | |
| 448 | 448 | told each visible team it is on: who leads it, each person's title, what | |
| 449 | 449 | they own, who they report to, whether they are around and their local | |
| 450 | 450 | 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: | |
| 454 | 455 | https://docs.g1t.sh/guides/people-and-teams/ | |
| 455 | 456 | ||
| 456 | 457 | Anyone creates an agent by describing it at `g1t.sh/<owner>/-/agents/new`: |