Skip to content
98 linesCodeBlameRaw
1import { 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";
8import { type AgentDraft, BLANK_DRAFT, readAgentForm } from "../../../lib/agent-form";
9import { channelPath } from "../../../lib/chat";
10import { page } from "../../../lib/meta";
11import { chat, workspaceAgents } from "../../../lib/services.server";
12import { 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 });
21 const templates = await workspaceAgents.templates().catch(() => null);
22 return { templates };
23}
24
25/**
26 * Makes the agent, then opens a direct message with it: talking to it is
27 * how work starts. If chat does not answer, its page instead.
28 */
29export async function action({ params, context, request }: Route.ActionArgs) {
30 assertSameOrigin(request);
31 const viewer = requireUser(context, request);
32 const slug = params.owner.toLowerCase();
33 if (!roleIn(viewer, slug)) throw data(null, { status: 404 });
34 const read = readAgentForm(await request.formData());
35 if (!read.ok) return { errors: read.errors };
36 const made = await workspaceAgents.create(slug, viewer, read.input).catch(() => null);
37 if (!made) return { errors: { form: "The agents service didn't answer. Try again in a moment." } };
38 if (!made.ok) {
39 const field = made.error.code === "conflict" ? "handle" : "form";
40 return { errors: { [field]: made.error.message } };
41 }
42 const dm = await chat.openDm(slug, viewer, [{ kind: "agent", id: made.value.id }]).catch(() => null);
43 throw redirect(dm?.ok ? channelPath(slug, dm.value) : `/${slug}/-/agents/${made.value.handle}`);
44}
45
46function draftFrom(template: AgentTemplate): AgentDraft {
47 return {
48 ...BLANK_DRAFT,
49 handle: template.handle,
50 display_name: template.display_name,
51 role: template.role,
52 instructions: template.instructions,
53 personality_preset: template.personality_preset,
54 routing: template.routing,
55 template: template.id,
56 };
57}
58
59export default function NewAgent({ loaderData, actionData, params }: Route.ComponentProps) {
60 const [search, setSearch] = useSearchParams();
61 const chosen = search.get("template");
62 const templates = loaderData.templates ?? [];
63 const template = templates.find((t) => t.id === chosen);
64 const draft = template ? draftFrom(template) : chosen === "blank" ? BLANK_DRAFT : null;
65 const errors = actionData?.errors;
66 return (
67 <div className="pb-4">
68 <Link to={`/${params.owner}/-/agents`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
69 <ArrowLeft size={14} />
70 Agents
71 </Link>
72 <header className="mt-4 mb-8">
73 <h1 className="text-2xl font-semibold tracking-tight">New agent</h1>
74 <p className="mt-1.5 max-w-2xl text-sm text-muted">
75 A teammate with a name, a job, a voice and a budget. Start from one of g1t's roles and make it yours, or from nothing. Once it's made, you'll be in a direct message with it.
76 </p>
77 </header>
78 {loaderData.templates == null && (
79 <p className="mb-6 rounded-lg border border-line bg-surface px-4 py-3 text-sm text-muted">
80 Templates aren't available right now; the agents service didn't answer. You can still start from a blank agent.
81 </p>
82 )}
83 <TemplateGallery
84 templates={templates}
85 chosen={chosen}
86 onChoose={(id) => setSearch(id ? { template: id } : {}, { preventScrollReset: true, replace: true })}
87 />
88 {draft ? (
89 <div className="mt-10 border-t border-line pt-10">
90 {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>}
91 <AgentForm draft={draft} errors={errors} submit="Create agent" intent="create" formKey={chosen ?? "blank"} />
92 </div>
93 ) : (
94 <p className="mt-6 text-sm text-faint">Choose a starting point to see its settings.</p>
95 )}
96 </div>
97 );
98}