Skip to content
113 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Chat and workspace agents: channels, DMs and named agents you talk to1import { ArrowLeft } from "lucide-react";
2import { Link, data, redirect, useSearchParams } from "react-router";
3
4import type { AgentTemplate } from "@g1t/contracts";
5
6import type { Route } from "./+types/new";
7import { AgentForm, TemplateGallery } from "../../../components/agents-mode";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)8import { type AgentDraft, BLANK_DRAFT, cleanHandle, readAgentForm } from "../../../lib/agent-form";
Chat and workspace agents: channels, DMs and named agents you talk to9import { channelPath } from "../../../lib/chat";
10import { page } from "../../../lib/meta";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)11import { chat, identity, workspaceAgents } from "../../../lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to12import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `New agent · ${params.owner} · g1t` });
16}
17
18export async function loader({ params, context, request }: Route.LoaderArgs) {
19 const viewer = requireUser(context, request);
20 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)21 const [templates, teams] = await Promise.all([
22 workspaceAgents.templates().catch(() => null),
23 identity.listTeams(viewer, params.owner).catch(() => null),
24 ]);
25 return { templates, teams: teams?.ok ? teams.value.map((team) => ({ slug: team.slug, name: team.name })) : [] };
Chat and workspace agents: channels, DMs and named agents you talk to26}
27
28/**
29 * Makes the agent, then opens a direct message with it: talking to it is
30 * how work starts. If chat does not answer, its page instead.
31 */
32export async function action({ params, context, request }: Route.ActionArgs) {
33 assertSameOrigin(request);
34 const viewer = requireUser(context, request);
35 const slug = params.owner.toLowerCase();
36 if (!roleIn(viewer, slug)) throw data(null, { status: 404 });
37 const read = readAgentForm(await request.formData());
38 if (!read.ok) return { errors: read.errors };
39 const made = await workspaceAgents.create(slug, viewer, read.input).catch(() => null);
40 if (!made) return { errors: { form: "The agents service didn't answer. Try again in a moment." } };
41 if (!made.ok) {
42 const field = made.error.code === "conflict" ? "handle" : "form";
43 return { errors: { [field]: made.error.message } };
44 }
45 const dm = await chat.openDm(slug, viewer, [{ kind: "agent", id: made.value.id }]).catch(() => null);
46 throw redirect(dm?.ok ? channelPath(slug, dm.value) : `/${slug}/-/agents/${made.value.handle}`);
47}
48
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)49/** A template's suggested names (`name_ideas`), when the agents service sends them. */
50function ideasOf(template: AgentTemplate | undefined): string[] {
51 if (!template) return [];
52 return [template.display_name, ...(template.name_ideas ?? []).filter((name) => name !== template.display_name)];
53}
54
Chat and workspace agents: channels, DMs and named agents you talk to55function draftFrom(template: AgentTemplate): AgentDraft {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)56 // A name to enjoy first, its handle after it; the template's own name when it has none.
57 const name = ideasOf(template)[0] ?? template.display_name;
Chat and workspace agents: channels, DMs and named agents you talk to58 return {
59 ...BLANK_DRAFT,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)60 handle: cleanHandle(name),
61 display_name: name,
62 title: template.title,
63 department: template.department,
64 responsibilities: template.responsibilities,
65 subagents: template.subagents,
Chat and workspace agents: channels, DMs and named agents you talk to66 role: template.role,
67 instructions: template.instructions,
68 personality_preset: template.personality_preset,
69 routing: template.routing,
70 template: template.id,
71 };
72}
73
74export default function NewAgent({ loaderData, actionData, params }: Route.ComponentProps) {
75 const [search, setSearch] = useSearchParams();
76 const chosen = search.get("template");
77 const templates = loaderData.templates ?? [];
78 const template = templates.find((t) => t.id === chosen);
79 const draft = template ? draftFrom(template) : chosen === "blank" ? BLANK_DRAFT : null;
80 const errors = actionData?.errors;
81 return (
82 <div className="pb-4">
83 <Link to={`/${params.owner}/-/agents`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
84 <ArrowLeft size={14} />
85 Agents
86 </Link>
87 <header className="mt-4 mb-8">
88 <h1 className="text-2xl font-semibold tracking-tight">New agent</h1>
89 <p className="mt-1.5 max-w-2xl text-sm text-muted">
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)90 Hire a colleague into a role: a name, a title, a team and what it is responsible for. Start from one of these roles and make it yours, or from nothing. Once it is hired, you will be in a direct message with it.
Chat and workspace agents: channels, DMs and named agents you talk to91 </p>
92 </header>
93 {loaderData.templates == null && (
94 <p className="mb-6 rounded-lg border border-line bg-surface px-4 py-3 text-sm text-muted">
95 Templates aren't available right now; the agents service didn't answer. You can still start from a blank agent.
96 </p>
97 )}
98 <TemplateGallery
99 templates={templates}
100 chosen={chosen}
101 onChoose={(id) => setSearch(id ? { template: id } : {}, { preventScrollReset: true, replace: true })}
102 />
103 {draft ? (
104 <div className="mt-10 border-t border-line pt-10">
105 {errors?.form && <p className="mb-6 rounded-lg border border-danger/40 bg-danger/10 px-4 py-3 text-sm text-danger">{errors.form}</p>}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)106 <AgentForm draft={draft} errors={errors} submit="Create agent" intent="create" formKey={chosen ?? "blank"} nameIdeas={ideasOf(template)} teams={loaderData.teams} />
Chat and workspace agents: channels, DMs and named agents you talk to107 </div>
108 ) : (
109 <p className="mt-6 text-sm text-faint">Choose a starting point to see its settings.</p>
110 )}
111 </div>
112 );
113}

This file's history is long; its oldest lines are credited to the oldest commit read.