Skip to content
730 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.

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)1import { Activity, Brain, ChevronRight, Dices, Network, Plus, Route as RouteIcon, Sparkles } from "lucide-react";
Chat and workspace agents: channels, DMs and named agents you talk to2import { type ReactNode, useState } from "react";
3import { Form, NavLink, useLocation, useNavigation, useRouteLoaderData } from "react-router";
4
5import type { AgentTemplate, ModelTier, WorkspaceAgent } from "@g1t/contracts";
6
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)7import { type AgentLike, AgentAvatar, PixelCreature } from "./agent-avatar";
8import { StatusDot, statusLabel } from "./chat/marks";
9import { isOrchestrator } from "./orchestrator";
Chat and workspace agents: channels, DMs and named agents you talk to10import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
11import { Skeleton } from "./ui/skeleton";
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)12import { AUTONOMY, type AgentDraft, PRESETS, TIER_LABELS, cleanHandle, dollarsField } from "../lib/agent-form";
13import { Hint } from "./ui/hint";
14import { DEPARTMENTS, RoleFields, SubagentsField } from "./agent-role";
Chat and workspace agents: channels, DMs and named agents you talk to15import type { AgentsLayoutData } from "../routes/workspace/agents/layout";
16
17/** The workspace's agents while an Agents page is open; undefined elsewhere. */
18export function useAgentsData(): AgentsLayoutData | undefined {
19 return useRouteLoaderData("routes/workspace/agents/layout") as AgentsLayoutData | undefined;
20}
21
22/** An agent's face: its picture, or the sparkle mark. */
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)23export function AgentFace({ agent, size = 20 }: { agent: AgentLike; size?: number }) {
24 return <AgentAvatar agent={agent} size={size} />;
Chat and workspace agents: channels, DMs and named agents you talk to25}
26
27function SideLink({ to, end, icon, children, trailing }: { to: string; end?: boolean; icon: ReactNode; children: ReactNode; trailing?: ReactNode }) {
28 return (
29 <NavLink
30 to={to}
31 end={end}
32 prefetch="intent"
33 className={({ isActive }) =>
34 `group flex h-9 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
35 isActive ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"
36 }`
37 }
38 >
39 <span className="flex w-5 shrink-0 justify-center">{icon}</span>
40 <span className="min-w-0 grow truncate">{children}</span>
41 {trailing}
42 </NavLink>
43 );
44}
45
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)46/** An agent as the sidebar lists it: from the Agents pages' own data, or the shell's on any other page. */
47type Listed = Pick<WorkspaceAgent, "id" | "handle" | "display_name" | "avatar" | "role" | "status" | "title" | "team" | "department"> & {
48 builtin?: boolean;
49 avatar_seed?: string | null;
50};
51
52/** Where an agent sits in the org chart: its team, else its department. */
53export function placeOf(agent: Pick<WorkspaceAgent, "team" | "department">): string {
54 if (agent.team) return agent.team.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
55 return agent.department?.trim() || "Unplaced";
56}
57
58/** "Margo · QA Engineer": how an agent reads wherever it shows. */
59export function nameAndTitle(agent: Pick<WorkspaceAgent, "display_name" | "title">): string {
60 return agent.title ? `${agent.display_name} · ${agent.title}` : agent.display_name;
61}
62
Chat and workspace agents: channels, DMs and named agents you talk to63/**
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)64 * Agents mode's sidebar: the fleet's runs, context and memory; g1t, the
65 * orchestrator, pinned at the top; then the specialists, each with its
66 * role and status; and the way to make a new one.
Chat and workspace agents: channels, DMs and named agents you talk to67 */
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)68export function AgentsSidebar({
69 slug,
70 shellAgents,
71 code,
72 owner,
73 phone = false,
74}: {
75 slug: string;
76 /** A phone's Agents tab: the list as the page, with no heading of its own and no fleet row (the fleet is under it). */
77 phone?: boolean;
78 /** The shell's list, for pages outside `-/agents` (context, memory). */
79 shellAgents: Listed[] | null;
80 /** Context and memory are about the code: only with Code access. */
81 code: boolean;
82 owner: boolean;
83}) {
Chat and workspace agents: channels, DMs and named agents you talk to84 const data = useAgentsData();
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)85 const agents: Listed[] | null = data?.agents ?? shellAgents;
86 const orchestrator = agents?.find((agent) => isOrchestrator(agent)) ?? null;
87 const specialists = (agents ?? []).filter((agent) => !isOrchestrator(agent));
88 const row = (agent: Listed) => (
Chat and workspace agents: channels, DMs and named agents you talk to89 <li key={agent.id}>
90 <NavLink
91 to={`/${slug}/-/agents/${agent.handle}`}
92 prefetch="intent"
93 className={({ isActive }) =>
94 `group flex h-11 items-center gap-2.5 rounded-md px-2 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
95 }
96 >
97 <span className="relative shrink-0">
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)98 <AgentFace agent={{ ...agent, builtin: isOrchestrator(agent) }} size={26} />
99 {!isOrchestrator(agent) && (
100 <StatusDot status={agent.status} className="absolute -right-0.5 -bottom-0.5 ring-2 ring-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]" />
101 )}
Chat and workspace agents: channels, DMs and named agents you talk to102 </span>
103 <span className="min-w-0 grow leading-tight">
104 <span className="block truncate text-[0.8125rem] font-medium text-fg">{agent.display_name}</span>
105 <span className="block truncate text-[0.6875rem] text-faint">
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 {isOrchestrator(agent) ? "Orchestrator" : agent.status === "idle" ? agent.title || agent.role : `${statusLabel(agent.status)} · ${agent.title || agent.role}`}
Chat and workspace agents: channels, DMs and named agents you talk to107 </span>
108 </span>
109 </NavLink>
110 </li>
111 );
112 return (
113 <div className="flex h-full flex-col">
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)114 <div className={`flex h-14 shrink-0 items-center justify-between border-b border-line pr-2.5 pl-4 ${phone ? "hidden" : ""}`}>
Chat and workspace agents: channels, DMs and named agents you talk to115 <h2 className="text-[0.9375rem] font-semibold">Agents</h2>
116 <NavLink
117 to={`/${slug}/-/agents/new`}
118 aria-label="New agent"
119 className="flex size-8 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
120 >
121 <Plus size={16} />
122 </NavLink>
123 </div>
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)124 <nav aria-label="Agents" className={`min-h-0 grow overflow-y-auto px-2.5 pt-3 pb-4 [scrollbar-width:thin] ${phone ? "[&_a]:min-h-11" : ""}`}>
Chat and workspace agents: channels, DMs and named agents you talk to125 <div className="space-y-px">
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)126 {!phone && (
127 <SideLink to={`/${slug}/-/agents`} end icon={<Activity size={15} className="text-faint" />}>
128 Agent fleet
129 </SideLink>
130 )}
131 {code && (
132 <>
133 <SideLink to={`/${slug}/-/context`} icon={<Network size={15} className="text-faint" />}>
134 Context
135 </SideLink>
136 <SideLink to={`/${slug}/-/memory`} icon={<Brain size={15} className="text-faint" />}>
137 Memory
138 </SideLink>
139 </>
140 )}
Chat and workspace agents: channels, DMs and named agents you talk to141 </div>
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)142 {agents == null ? (
143 data === undefined && shellAgents === null ? (
144 <p className="mt-4 px-2 text-xs leading-relaxed text-faint">The agents service didn&apos;t answer. Your agents will show here once it does.</p>
145 ) : (
146 <div className="mt-5 space-y-3 px-2" aria-busy="true">
147 {[0, 1, 2].map((i) => (
148 <div key={i} className="flex items-center gap-2.5">
149 <Skeleton className="size-6 rounded-md" />
150 <Skeleton className="h-3 w-28" />
151 </div>
152 ))}
153 </div>
154 )
Chat and workspace agents: channels, DMs and named agents you talk to155 ) : (
156 <>
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)157 {orchestrator && <Group title="Orchestrator">{row(orchestrator)}</Group>}
158 <p className="mt-4 px-2 text-[0.6875rem] leading-snug text-faint">Specialists are colleagues hired into a role. g1t hands them work.</p>
159 {[...orgChart(specialists)].map(([place, members]) => (
160 <Group key={place} title={`${place} · ${members.length}`} collapsible>
161 {members.map(row)}
Chat and workspace agents: channels, DMs and named agents you talk to162 </Group>
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)163 ))}
164 {specialists.length === 0 && <p className="px-2 py-1 text-xs text-faint">No specialists yet. Hire one into a role.</p>}
Chat and workspace agents: channels, DMs and named agents you talk to165 </>
166 )}
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)167 {owner && (
168 <NavLink
169 to={`/${slug}/-/agents/new`}
170 className={({ isActive }) =>
171 `mt-2 flex h-9 items-center gap-2.5 rounded-md border border-dashed px-2 text-[0.8125rem] transition-colors ${
172 isActive ? "border-accent/50 bg-accent/10 text-fg" : "border-line text-muted hover:border-line-strong hover:text-fg"
173 }`
174 }
175 >
176 <span className="flex w-5 justify-center">
177 <Plus size={15} />
178 </span>
179 New agent
180 </NavLink>
181 )}
Chat and workspace agents: channels, DMs and named agents you talk to182 </nav>
183 </div>
184 );
185}
186
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)187/** Specialists by team or department, each group by name, the groups in the gallery's order. */
188function orgChart(agents: Listed[]): Map<string, Listed[]> {
189 const groups = new Map<string, Listed[]>();
190 for (const agent of agents) groups.set(placeOf(agent), [...(groups.get(placeOf(agent)) ?? []), agent]);
191 const rank = (place: string) => {
192 const at = DEPARTMENTS.indexOf(place);
193 return at < 0 ? DEPARTMENTS.length : at;
194 };
195 return new Map(
196 [...groups]
197 .sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b))
198 .map(([place, list]) => [place, list.sort((x, y) => x.display_name.localeCompare(y.display_name))]),
199 );
200}
201
202function Group({ title, children, collapsible }: { title: string; children: ReactNode; collapsible?: boolean }) {
203 const [open, setOpen] = useState(true);
204 if (collapsible) {
205 return (
206 <section className="mt-3">
207 <button
208 type="button"
209 onClick={() => setOpen(!open)}
210 aria-expanded={open}
211 className="mb-1 flex w-full items-center gap-1 rounded px-1 text-xs font-medium text-faint transition-colors hover:text-muted"
212 >
213 <ChevronRight size={12} className={`transition-transform ${open ? "rotate-90" : ""}`} />
214 {title}
215 </button>
216 {open && <ul className="space-y-px border-l border-line pl-1.5 ml-2.5">{children}</ul>}
217 </section>
218 );
219 }
Chat and workspace agents: channels, DMs and named agents you talk to220 return (
221 <section className="mt-4">
222 <h3 className="mb-1 px-2 text-xs font-medium text-faint">{title}</h3>
223 <ul className="space-y-px">{children}</ul>
224 </section>
225 );
226}
227
228/** The templates g1t ships, and a blank one, to start a new agent from. */
229export function TemplateGallery({ templates, chosen, onChoose }: { templates: AgentTemplate[]; chosen: string | null; onChoose: (id: string | null) => void }) {
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)230 // By department, in the order the departments are listed; any other after them.
231 const order = (department: string) => {
232 const at = DEPARTMENTS.indexOf(department);
233 return at < 0 ? DEPARTMENTS.length : at;
234 };
235 const groups = new Map<string, AgentTemplate[]>();
236 for (const template of [...templates].sort((a, b) => order(a.department) - order(b.department))) {
237 const key = template.department || "Other";
238 groups.set(key, [...(groups.get(key) ?? []), template]);
239 }
Chat and workspace agents: channels, DMs and named agents you talk to240 return (
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)241 <div className="space-y-8">
242 {[...groups].map(([department, list]) => (
243 <section key={department}>
244 <h2 className="mb-3 text-xs font-semibold tracking-wide text-faint uppercase">{department}</h2>
245 <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
246 {list.map((template) => (
247 <TemplateCard
248 key={template.id}
249 selected={chosen === template.id}
250 onClick={() => onChoose(template.id)}
251 name={template.display_name}
252 title={template.title}
253 seed={template.handle}
254 duties={template.responsibilities.slice(0, 3)}
255 />
256 ))}
257 </div>
258 </section>
Chat and workspace agents: channels, DMs and named agents you talk to259 ))}
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)260 <section>
261 <h2 className="mb-3 text-xs font-semibold tracking-wide text-faint uppercase">Your own</h2>
262 <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
263 <TemplateCard
264 selected={chosen === "blank"}
265 onClick={() => onChoose("blank")}
266 name="A new role"
267 title="Start from nothing"
268 blank
269 duties={["Give it a name, a title and a team", "Say what it is responsible for"]}
270 />
271 </div>
272 </section>
Chat and workspace agents: channels, DMs and named agents you talk to273 </div>
274 );
275}
276
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)277/** A role to hire into: its face, a name it suggests, its title and what it does. */
Chat and workspace agents: channels, DMs and named agents you talk to278function TemplateCard({
279 selected,
280 onClick,
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)281 name,
Chat and workspace agents: channels, DMs and named agents you talk to282 title,
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)283 seed,
284 duties,
Chat and workspace agents: channels, DMs and named agents you talk to285 blank,
286}: {
287 selected: boolean;
288 onClick: () => void;
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)289 name: string;
Chat and workspace agents: channels, DMs and named agents you talk to290 title: string;
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)291 seed?: string;
292 duties: string[];
Chat and workspace agents: channels, DMs and named agents you talk to293 blank?: boolean;
294}) {
295 return (
296 <button
297 type="button"
298 onClick={onClick}
299 aria-pressed={selected}
300 className={`group flex flex-col items-start rounded-xl border p-4 text-left transition-all ${
301 selected
302 ? "border-accent/60 bg-accent/[0.07] ring-1 ring-accent/40"
303 : "border-line bg-surface hover:-translate-y-px hover:border-line-strong hover:bg-raised/40"
304 }`}
305 >
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)306 <span className="flex w-full items-center gap-3">
Chat and workspace agents: channels, DMs and named agents you talk to307 {blank ? (
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)308 <span className="flex size-10 items-center justify-center rounded-[11px] border border-dashed border-line-strong text-muted">
Chat and workspace agents: channels, DMs and named agents you talk to309 <Plus size={16} />
310 </span>
311 ) : (
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)312 <PixelCreature seed={seed ?? name} size={40} />
Chat and workspace agents: channels, DMs and named agents you talk to313 )}
314 <span className="min-w-0 grow">
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)315 <span className="block truncate text-sm font-semibold">{name}</span>
316 <span className="block truncate text-xs text-muted">{title}</span>
Chat and workspace agents: channels, DMs and named agents you talk to317 </span>
318 <ChevronRight size={15} className={`shrink-0 transition-colors ${selected ? "text-accent" : "text-faint group-hover:text-muted"}`} />
319 </span>
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)320 <ul className="mt-3 space-y-1">
321 {duties.map((duty) => (
322 <li key={duty} className="flex gap-2 text-[0.8125rem] leading-snug text-muted">
323 <span className="mt-[0.45rem] size-1 shrink-0 rounded-full bg-line-strong" aria-hidden="true" />
324 <span className="line-clamp-1">{duty}</span>
325 </li>
326 ))}
327 </ul>
Chat and workspace agents: channels, DMs and named agents you talk to328 </button>
329 );
330}
331
332const FIELD = "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
333
334function Label({ children, hint, error, htmlFor }: { children: ReactNode; hint?: ReactNode; error?: string; htmlFor?: string }) {
335 return (
336 <div className="mb-1.5 flex items-baseline justify-between gap-3">
337 <label htmlFor={htmlFor} className="text-sm font-medium text-fg-soft">
338 {children}
339 </label>
340 {error ? <span className="text-xs text-danger">{error}</span> : hint ? <span className="text-xs text-faint">{hint}</span> : null}
341 </div>
342 );
343}
344
345/** A section of the form: what it is on the left, its fields on the right, on a wide screen. */
346function FormSection({ title, about, children }: { title: string; about: ReactNode; children: ReactNode }) {
347 return (
348 <section className="grid gap-x-10 gap-y-4 border-t border-line py-8 first-of-type:border-t-0 first-of-type:pt-0 md:grid-cols-[13rem_1fr]">
349 <div>
350 <h2 className="text-sm font-semibold">{title}</h2>
351 <p className="mt-1 text-[0.8125rem] leading-relaxed text-muted">{about}</p>
352 </div>
353 <div className="min-w-0 space-y-5">{children}</div>
354 </section>
355 );
356}
357
358function TierSelect({ name, value, none }: { name: string; value: ModelTier | null; none: string }) {
359 return (
360 <Select name={name} defaultValue={value ?? "none"}>
361 <SelectTrigger id={name}>
362 <SelectValue />
363 </SelectTrigger>
364 <SelectContent>
365 <SelectItem value="none">{none}</SelectItem>
366 {(["small", "large", "frontier"] as const).map((tier) => (
367 <SelectItem key={tier} value={tier} description={TIER_LABELS[tier].split(": ")[1]}>
368 {TIER_LABELS[tier].split(":")[0]}
369 </SelectItem>
370 ))}
371 </SelectContent>
372 </Select>
373 );
374}
375
376function Money({ name, value, label, hint, error }: { name: string; value: number | null; label: string; hint: string; error?: string }) {
377 return (
378 <div>
379 <Label htmlFor={name} hint={hint} error={error}>
380 {label}
381 </Label>
382 <div className="flex items-center rounded-md border border-line bg-bg transition-colors focus-within:border-accent-dim hover:border-line-strong">
383 <span className="pl-3 text-sm text-faint">$</span>
384 <input
385 id={name}
386 name={name}
387 inputMode="decimal"
388 defaultValue={dollarsField(value)}
389 placeholder="No cap"
390 autoComplete="off"
391 data-1p-ignore
392 className="min-w-0 grow bg-transparent px-2 py-2 text-sm tabular-nums outline-none placeholder:text-faint"
393 />
394 </div>
395 </div>
396 );
397}
398
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)399/** Names to shuffle through when a template has none of its own. */
400const FALLBACK_NAMES = ["Margo", "Juniper", "Otto", "Wren", "Basil", "Nova", "Clementine", "Fig", "Rook", "Pip"];
401
Chat and workspace agents: channels, DMs and named agents you talk to402/**
403 * Everything that defines an agent, for making one and for its Profile:
404 * who it is, its job, its voice, the limits on routing, its budget, what it
405 * may do alone, and how much it takes on at once.
406 */
407export function AgentForm({
408 draft,
409 errors,
410 submit,
411 intent,
412 formKey,
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)413 nameIdeas = [],
414 locked = false,
415 seed,
416 teams = [],
Chat and workspace agents: channels, DMs and named agents you talk to417}: {
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)418 /** The workspace's teams, to put it on one. */
419 teams?: { slug: string; name: string }[];
Chat and workspace agents: channels, DMs and named agents you talk to420 draft: AgentDraft;
421 errors?: Record<string, string>;
422 submit: string;
423 intent: string;
424 formKey?: string;
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)425 /** Names to shuffle through (a template's `name_ideas`); the dice offers them. */
426 nameIdeas?: string[];
427 /** g1t, the orchestrator: its identity and job are fixed; the rest is editable. */
428 locked?: boolean;
429 /** The agent's own `avatar_seed`, once it has one; a new agent's face follows its handle. */
430 seed?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to431}) {
432 const navigation = useNavigation();
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)433 // The handle follows the name until someone edits it.
434 const [name, setName] = useState(draft.display_name);
435 const [handle, setHandle] = useState(draft.handle);
436 const [handleEdited, setHandleEdited] = useState(Boolean(draft.handle) && cleanHandle(draft.display_name) !== draft.handle);
437 const ideas = nameIdeas.length > 0 ? nameIdeas : FALLBACK_NAMES;
438 const [idea, setIdea] = useState(() => Math.max(0, ideas.indexOf(draft.display_name)));
439 const rename = (next: string) => {
440 setName(next);
441 if (!handleEdited) setHandle(cleanHandle(next));
442 };
443 const shuffle = () => {
444 const at = (idea + 1) % ideas.length;
445 setIdea(at);
446 rename(ideas[at]!);
447 };
Chat and workspace agents: channels, DMs and named agents you talk to448 const { pathname } = useLocation();
449 const busy = navigation.state === "submitting" && navigation.formAction === pathname;
450 const [preset, setPreset] = useState(draft.personality_preset);
451 const [advanced, setAdvanced] = useState(Boolean(draft.routing.pinned));
452 const e = errors ?? {};
453 const presetAbout = PRESETS.find((p) => p.value === preset)?.about;
454 return (
455 <Form method="post" key={formKey} className="pb-24">
456 <input type="hidden" name="intent" value={intent} />
457 <input type="hidden" name="template" value={draft.template ?? ""} />
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)458 <FormSection
459 title="Identity"
460 about={
461 locked
462 ? "g1t is the workspace's orchestrator: its name, handle and role are the same everywhere, and it can't be archived."
463 : "How it shows in chat, mentions, assignees and the audit log. Give it a name people will enjoy saying."
464 }
465 >
466 <div className="flex items-center gap-3">
467 <AgentAvatar agent={{ handle: handle || "agent", avatar_seed: seed ?? handle, builtin: locked }} size={44} />
468 <div className="min-w-0 text-sm">
469 <p className="truncate font-semibold">{name || "Your new agent"}</p>
470 <p className="truncate text-xs text-muted">{handle ? `@${handle}` : "@handle"}</p>
471 </div>
472 </div>
Chat and workspace agents: channels, DMs and named agents you talk to473 <div className="grid gap-5 sm:grid-cols-2">
474 <div>
475 <Label htmlFor="display_name" error={e.display_name}>
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)476 Name
Chat and workspace agents: channels, DMs and named agents you talk to477 </Label>
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)478 <div className="flex gap-2">
479 <input
480 id="display_name"
481 name="display_name"
482 value={name}
483 onChange={(event) => rename(event.target.value)}
484 readOnly={locked}
485 placeholder="Margo"
486 className={`${FIELD} ${locked ? "cursor-not-allowed text-muted" : ""}`}
487 autoComplete="off"
488 data-1p-ignore
489 />
490 {!locked && (
491 <Hint label="Another name">
492 <button
493 type="button"
494 aria-label="Another name"
495 onClick={shuffle}
496 className="flex size-[38px] shrink-0 items-center justify-center rounded-md border border-line text-muted transition-colors hover:border-line-strong hover:bg-raised hover:text-fg active:rotate-12"
497 >
498 <Dices size={17} />
499 </button>
500 </Hint>
501 )}
502 </div>
Chat and workspace agents: channels, DMs and named agents you talk to503 </div>
504 <div>
505 <Label htmlFor="handle" error={e.handle} hint="Mentioned as @handle">
506 Handle
507 </Label>
508 <div className="flex items-center rounded-md border border-line bg-bg transition-colors focus-within:border-accent-dim hover:border-line-strong">
509 <span className="pl-3 font-mono text-sm text-faint">@</span>
510 <input
511 id="handle"
512 name="handle"
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)513 value={handle}
514 onChange={(event) => {
515 setHandle(event.target.value);
516 setHandleEdited(true);
517 }}
518 readOnly={locked}
519 placeholder="margo"
Chat and workspace agents: channels, DMs and named agents you talk to520 autoComplete="off"
521 data-1p-ignore
522 className="min-w-0 grow bg-transparent py-2 pr-3 pl-0.5 font-mono text-sm outline-none placeholder:text-faint"
523 />
524 </div>
525 </div>
526 </div>
527 </FormSection>
528
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)529 {!locked && (
530 <FormSection
531 title="Role"
532 about="Hired into a role, not a task: a title, a team, and what it is responsible for. Shown as its name and title everywhere."
533 >
534 <RoleFields
535 title={draft.title}
536 team={draft.team}
537 department={draft.department}
538 responsibilities={draft.responsibilities}
539 teams={teams}
540 errors={e}
541 locked={locked}
542 />
543 </FormSection>
544 )}
545
546 <FormSection
547 title="Job"
548 about={
549 locked
550 ? "Its job is fixed: know the team, hand work to the right specialist, do it itself when nobody fits, and report. Add your own instructions on top."
551 : "What it is responsible for, how it works, and what good looks like. It reads this before every reply and task."
552 }
553 >
Chat and workspace agents: channels, DMs and named agents you talk to554 <div>
555 <Label htmlFor="instructions" error={e.instructions}>
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)556 {locked ? "Extra instructions" : "Instructions"}
Chat and workspace agents: channels, DMs and named agents you talk to557 </Label>
558 <textarea
559 id="instructions"
560 name="instructions"
561 defaultValue={draft.instructions}
562 rows={9}
563 placeholder={"Cut a release of g1t every Tuesday.\n- Read the merged pull requests since the last tag.\n- Write the release notes.\n- Ask in #releases before tagging."}
564 className={`${FIELD} resize-y font-mono text-[0.8125rem] leading-relaxed`}
565 />
566 </div>
567 </FormSection>
568
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)569 {!locked && (
570 <FormSection title="Subagents" about="Help it calls on for one kind of work inside its own tasks. Never members of the workspace, and never wider than the agent itself.">
571 <SubagentsField initial={draft.subagents} routing={draft.routing} />
572 {e.subagents && <p className="text-sm text-danger">{e.subagents}</p>}
573 </FormSection>
574 )}
575
Chat and workspace agents: channels, DMs and named agents you talk to576 <FormSection title="Personality" about="Its voice only: tone, length, how it asks. A personality never changes what it may do.">
577 <div>
578 <input type="hidden" name="personality_preset" value={preset} />
579 <div role="radiogroup" aria-label="Voice" className="grid grid-cols-2 gap-1 rounded-lg bg-surface p-1 ring-1 ring-line sm:grid-cols-4">
580 {PRESETS.map((option) => (
581 <button
582 key={option.value}
583 type="button"
584 role="radio"
585 aria-checked={preset === option.value}
586 onClick={() => setPreset(option.value)}
587 className={`h-8 rounded-md text-[0.8125rem] font-medium transition-colors ${
588 preset === option.value ? "bg-raised text-fg shadow-sm ring-1 ring-line-strong" : "text-muted hover:text-fg"
589 }`}
590 >
591 {option.label}
592 </button>
593 ))}
594 </div>
595 <p className="mt-2 text-xs text-muted">{presetAbout}</p>
596 </div>
597 <div>
598 <Label htmlFor="personality" hint="Optional">
599 In its own words
600 </Label>
601 <textarea
602 id="personality"
603 name="personality"
604 defaultValue={draft.personality}
605 rows={3}
606 placeholder="Dry humor is fine. No emoji. Asks one question at a time."
607 className={`${FIELD} resize-y`}
608 />
609 </div>
610 </FormSection>
611
612 <FormSection
613 title="Models"
614 about="Nobody picks a model to get work done. g1t routes each step to the tier it needs and records which ran. These only limit the routing."
615 >
616 <div className="flex items-start gap-3 rounded-xl border border-accent/25 bg-accent/[0.06] p-4">
617 <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent/15 text-accent">
618 <RouteIcon size={16} />
619 </span>
620 <div>
621 <p className="text-sm font-semibold">Auto: routed per step by need</p>
622 <p className="mt-0.5 text-[0.8125rem] leading-relaxed text-muted">
623 Replies and triage run on Fast; making changes on Standard; planning, hard reviews and retries after a failure on Most capable. It steps up after a failure and back down when the cheaper tier works.
624 </p>
625 </div>
626 </div>
627 <div className="grid gap-5 sm:grid-cols-3">
628 <div>
629 <Label htmlFor="floor">Floor</Label>
630 <TierSelect name="floor" value={draft.routing.floor} none="No floor" />
631 </div>
632 <div>
633 <Label htmlFor="ceiling" error={e.ceiling}>
634 Ceiling
635 </Label>
636 <TierSelect name="ceiling" value={draft.routing.ceiling} none="No ceiling" />
637 </div>
638 <div>
639 <Label htmlFor="providers">Providers</Label>
640 <Select name="providers" defaultValue={draft.routing.providers[0] ?? "any"}>
641 <SelectTrigger id="providers">
642 <SelectValue />
643 </SelectTrigger>
644 <SelectContent>
645 <SelectItem value="any" description="g1t's hosted models and the workspace's own">
646 Any the workspace allows
647 </SelectItem>
648 <SelectItem value="g1t" description="Never the workspace's own keys">
649 g1t's hosted models
650 </SelectItem>
651 <SelectItem value="workspace" description="Set up under Integrations; billed by the provider">
652 The workspace's own providers
653 </SelectItem>
654 </SelectContent>
655 </Select>
656 </div>
657 </div>
658 <div className="rounded-lg border border-line">
659 <button
660 type="button"
661 onClick={() => setAdvanced(!advanced)}
662 aria-expanded={advanced}
663 className="flex w-full items-center gap-2 px-3.5 py-2.5 text-left text-[0.8125rem] font-medium text-muted hover:text-fg"
664 >
665 <ChevronRight size={14} className={`transition-transform ${advanced ? "rotate-90" : ""}`} />
666 Advanced
667 <span className="font-normal text-faint">Pin one model</span>
668 </button>
669 <div hidden={!advanced} className="border-t border-line px-3.5 py-3.5">
670 <Label htmlFor="pinned" error={e.pinned} hint="For your own endpoints. Turns routing off.">
671 Pinned model
672 </Label>
673 <input id="pinned" name="pinned" defaultValue={draft.routing.pinned ?? ""} placeholder="provider/model" className={`${FIELD} font-mono`} autoComplete="off" data-1p-ignore />
674 </div>
675 </div>
676 </FormSection>
677
678 <FormSection title="Budget" about="Checked before work starts and enforced while it runs. At 80% of the month it tells you; at 100% it takes no new tasks.">
679 <div className="grid gap-5 sm:grid-cols-3">
680 <Money name="monthly" label="Monthly" hint="Per calendar month" value={draft.budget.monthly_micros} error={e.monthly} />
681 <Money name="daily" label="Daily" hint="Optional" value={draft.budget.daily_micros} error={e.daily} />
682 <Money name="task" label="Per task" hint="Going over asks" value={draft.budget.task_micros} error={e.task} />
683 </div>
684 </FormSection>
685
686 <FormSection title="Autonomy" about="What it may do alone and what needs a person. Rulesets and branch protection still apply on top.">
687 <div className="grid gap-5 sm:grid-cols-2">
688 {(Object.keys(AUTONOMY) as (keyof typeof AUTONOMY)[]).map((key) => (
689 <div key={key}>
690 <Label htmlFor={key}>{AUTONOMY[key].label}</Label>
691 <Select name={key} defaultValue={draft.autonomy[key]}>
692 <SelectTrigger id={key}>
693 <SelectValue />
694 </SelectTrigger>
695 <SelectContent>
696 {AUTONOMY[key].options.map(([value, label]) => (
697 <SelectItem key={value} value={value}>
698 {label}
699 </SelectItem>
700 ))}
701 </SelectContent>
702 </Select>
703 </div>
704 ))}
705 </div>
706 </FormSection>
707
708 <FormSection title="Capacity" about="How many tasks it works at once. More wait on its desk, each showing its place in line.">
709 <div className="max-w-40">
710 <Label htmlFor="capacity" error={e.capacity}>
711 Tasks at once
712 </Label>
713 <input id="capacity" name="capacity" type="number" min={1} max={20} defaultValue={draft.capacity} className={`${FIELD} tabular-nums`} />
714 </div>
715 </FormSection>
716
717 <div className="sticky bottom-0 z-10 -mx-4 flex items-center justify-end gap-3 border-t border-line bg-bg/90 px-4 py-3 backdrop-blur sm:-mx-8 sm:px-8">
718 {Object.keys(e).length > 0 && <p className="mr-auto text-sm text-danger">Check the fields marked above.</p>}
719 <button
720 type="submit"
721 disabled={busy}
722 className="inline-flex h-9 items-center gap-2 rounded-md bg-accent px-4 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-60"
723 >
724 <Sparkles size={15} />
725 {busy ? "Saving…" : submit}
726 </button>
727 </div>
728 </Form>
729 );
730}

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