Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
147 linesCodeBlameRaw
1/**
2 * Agent templates: the roles g1t provides to start a new agent from, by
3 * title, with the agents in the workspace that started from each; and the
4 * subagents the plugins the workspace installed bring, hireable the same
5 * way. A template isn't installed: an owner starts an agent from one (a
6 * member, a personal agent) and configures it, and the agent is its own
7 * from then on.
8 */
9import { ArrowLeft, Bot, Plus, UserPlus } from "lucide-react";
10import { Link, data } from "react-router";
11
12import type { PluginAgentTemplate } from "@g1t/contracts";
13import { customizePath } from "@g1t/contracts/customize";
14
15import type { Route } from "./+types/templates";
16import { TemplateCard } from "../../../components/agent-templates";
17import { readOrNull } from "../../../components/agents/actions.server";
18import { ButtonLink, EmptyState } from "../../../components/ui";
19import { Badge } from "../../../components/ui/badge";
20import { Card } from "../../../components/ui/card";
21import { templateListings } from "../../../lib/agent-templates";
22import { loadTemplates } from "../../../lib/agent-templates.server";
23import { pluginPath } from "../../../lib/customize";
24import { workspaceAgents } from "../../../lib/services.server";
25import { page } from "../../../lib/meta";
26import { requireUser, roleIn } from "../../../lib/session.server";
27
28export function meta({ params, ...args }: Route.MetaArgs) {
29 return page(args, { title: `Agent templates · ${params.owner} · g1t` });
30}
31
32export async function loader({ params, context, request }: Route.LoaderArgs) {
33 const viewer = requireUser(context, request);
34 const slug = params.owner.toLowerCase();
35 const role = roleIn(viewer, slug);
36 if (!role) throw data(null, { status: 404 });
37 const [loaded, fromPlugins] = await Promise.all([loadTemplates(slug, viewer), readOrNull(workspaceAgents.pluginAgents(slug, viewer))]);
38 // Owners start agents; members start personal ones, unless owners turned that off.
39 return { slug, owner: role === "owner" || loaded.members_create_agents, fromPlugins, ...loaded };
40}
41
42export default function AgentTemplates({ loaderData }: Route.ComponentProps) {
43 const { slug, owner, templates, agents, fromPlugins } = loaderData;
44 const listings = templateListings(templates ?? [], agents);
45 return (
46 <div className="pb-4">
47 <Link to={`/${slug}/-/agents`} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
48 <ArrowLeft size={14} />
49 Agents
50 </Link>
51 <header className="mt-4 mb-8 flex flex-wrap items-end justify-between gap-4">
52 <div className="min-w-0 grow basis-lg">
53 <h1 className="text-2xl font-semibold tracking-tight">Templates</h1>
54 <p className="mt-1.5 max-w-2xl text-sm text-muted">
55 Starting points for a new agent, each a role with responsibilities, helpers, a voice, model limits and instructions. Start an agent from one and make it yours: its name, instructions, voice, models and budget are all
56 yours to change, then and later. A template is never updated under an agent started from it.
57 </p>
58 </div>
59 {owner && (
60 <Link to={`/${slug}/-/agents/new`} className="inline-flex h-9 shrink-0 items-center gap-1.5 rounded-md bg-fg px-4 text-sm font-medium text-bg hover:bg-fg-hover">
61 <Plus size={15} />
62 New agent
63 </Link>
64 )}
65 </header>
66 {templates == null ? (
67 <EmptyState title="Templates can't be shown right now">The agents service didn't answer. Reload in a minute.</EmptyState>
68 ) : (
69 <div className="space-y-10">
70 {/* One grid, in the order g1t lists the roles: a template names a role, never a team. */}
71 <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
72 {listings.map((listing) => (
73 <TemplateCard key={listing.template.id} listing={listing} slug={slug} owner={owner} />
74 ))}
75 </div>
76 <section aria-labelledby="from-plugins">
77 <div className="mb-3 flex flex-wrap items-baseline justify-between gap-2">
78 <h2 id="from-plugins" className="text-xs font-semibold tracking-wide text-faint uppercase">
79 From plugins {fromPlugins && fromPlugins.length > 0 && <span className="font-normal">{fromPlugins.length}</span>}
80 </h2>
81 <Link to={`${customizePath("plugins", slug)}?view=discover`} className="text-xs text-muted hover:text-fg">
82 Find plugins in Customize
83 </Link>
84 </div>
85 {fromPlugins == null ? (
86 <EmptyState title="Plugin agents can't be shown right now">The agents service didn't answer. Reload in a minute.</EmptyState>
87 ) : fromPlugins.length === 0 ? (
88 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted">
89 <p>Subagents the plugins you install bring show here, to hire as a starting point like any role.</p>
90 </Card>
91 ) : (
92 <Card asChild divided className="overflow-hidden">
93 <ul>
94 {fromPlugins.map((agent) => (
95 <PluginAgentRow key={agent.ref} agent={agent} slug={slug} owner={owner} />
96 ))}
97 </ul>
98 </Card>
99 )}
100 </section>
101 {owner && (
102 <section aria-label="Your own">
103 <h2 className="mb-3 text-xs font-semibold tracking-wide text-faint uppercase">Your own</h2>
104 <Link
105 to={`/${slug}/-/agents/new`}
106 className="flex max-w-md items-center gap-3 rounded-xl border border-dashed border-line-strong p-4 text-sm transition-colors hover:border-fg/40 hover:bg-raised/40"
107 >
108 <span className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-raised text-muted">
109 <Plus size={18} />
110 </span>
111 <span className="min-w-0">
112 <span className="block font-medium">Describe it</span>
113 <span className="block text-xs text-muted">Say what it should do, and g1t drafts the rest for you to change and try.</span>
114 </span>
115 </Link>
116 </section>
117 )}
118 </div>
119 )}
120 </div>
121 );
122}
123
124/** A subagent an installed plugin brings: its name, what it does, the model it names, and Hire, which opens the new-agent form filled from it. */
125function PluginAgentRow({ agent, slug, owner }: { agent: PluginAgentTemplate; slug: string; owner: boolean }) {
126 return (
127 <li className="flex flex-wrap items-start gap-x-4 gap-y-2 px-4 py-3.5">
128 <Bot size={16} className="mt-0.5 shrink-0 text-faint" aria-hidden />
129 <div className="min-w-0 grow">
130 <p className="flex flex-wrap items-center gap-2">
131 <span className="font-mono text-sm font-medium">{agent.name}</span>
132 <Link to={pluginPath(slug, `${agent.plugin}@${agent.marketplace}`)} className="inline-flex" prefetch="intent">
133 <Badge tone="accent">{agent.plugin}</Badge>
134 </Link>
135 {agent.model && <Badge>{agent.model}</Badge>}
136 </p>
137 {agent.description && <p className="mt-0.5 text-sm text-muted">{agent.description}</p>}
138 </div>
139 {owner && (
140 <ButtonLink to={`/${slug}/-/agents/new?plugin=${encodeURIComponent(`${agent.plugin}@${agent.marketplace}`)}&agent=${encodeURIComponent(agent.name)}`} variant="outline" size="sm">
141 <UserPlus size={14} />
142 Hire
143 </ButtonLink>
144 )}
145 </li>
146 );
147}