Skip to content
769 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;
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions86 const live = data?.live ?? {};
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)87 const orchestrator = agents?.find((agent) => isOrchestrator(agent)) ?? null;
88 const specialists = (agents ?? []).filter((agent) => !isOrchestrator(agent));
89 const row = (agent: Listed) => (
Chat and workspace agents: channels, DMs and named agents you talk to90 <li key={agent.id}>
91 <NavLink
92 to={`/${slug}/-/agents/${agent.handle}`}
93 prefetch="intent"
94 className={({ isActive }) =>
95 `group flex h-11 items-center gap-2.5 rounded-md px-2 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
96 }
97 >
98 <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)99 <AgentFace agent={{ ...agent, builtin: isOrchestrator(agent) }} size={26} />
100 {!isOrchestrator(agent) && (
101 <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))]" />
102 )}
Chat and workspace agents: channels, DMs and named agents you talk to103 </span>
104 <span className="min-w-0 grow leading-tight">
105 <span className="block truncate text-[0.8125rem] font-medium text-fg">{agent.display_name}</span>
106 <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)107 {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 to108 </span>
109 </span>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions110 {(live[agent.id] ?? 0) > 0 && (
111 <Hint label={`${live[agent.id]} live ${live[agent.id] === 1 ? "session" : "sessions"}`}>
112 <span className="flex h-5 min-w-5 shrink-0 items-center justify-center gap-1 rounded-full bg-accent/15 px-1.5 text-[0.6875rem] font-medium text-accent tabular-nums">
113 <span className="size-1.5 animate-pulse rounded-full bg-accent motion-reduce:animate-none" aria-hidden="true" />
114 {live[agent.id]}
115 <span className="sr-only"> live sessions</span>
116 </span>
117 </Hint>
118 )}
Chat and workspace agents: channels, DMs and named agents you talk to119 </NavLink>
120 </li>
121 );
122 return (
123 <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)124 <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 to125 <h2 className="text-[0.9375rem] font-semibold">Agents</h2>
126 <NavLink
127 to={`/${slug}/-/agents/new`}
128 aria-label="New agent"
129 className="flex size-8 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
130 >
131 <Plus size={16} />
132 </NavLink>
133 </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)134 <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 to135 <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)136 {!phone && (
137 <SideLink to={`/${slug}/-/agents`} end icon={<Activity size={15} className="text-faint" />}>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions138 Overview
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)139 </SideLink>
140 )}
141 {code && (
142 <>
143 <SideLink to={`/${slug}/-/context`} icon={<Network size={15} className="text-faint" />}>
144 Context
145 </SideLink>
146 <SideLink to={`/${slug}/-/memory`} icon={<Brain size={15} className="text-faint" />}>
147 Memory
148 </SideLink>
149 </>
150 )}
Chat and workspace agents: channels, DMs and named agents you talk to151 </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)152 {agents == null ? (
153 data === undefined && shellAgents === null ? (
154 <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>
155 ) : (
156 <div className="mt-5 space-y-3 px-2" aria-busy="true">
157 {[0, 1, 2].map((i) => (
158 <div key={i} className="flex items-center gap-2.5">
159 <Skeleton className="size-6 rounded-md" />
160 <Skeleton className="h-3 w-28" />
161 </div>
162 ))}
163 </div>
164 )
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 {orchestrator && <Group title="Orchestrator">{row(orchestrator)}</Group>}
168 <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>
169 {[...orgChart(specialists)].map(([place, members]) => (
170 <Group key={place} title={`${place} · ${members.length}`} collapsible>
171 {members.map(row)}
Chat and workspace agents: channels, DMs and named agents you talk to172 </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)173 ))}
174 {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 to175 </>
176 )}
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)177 {owner && (
178 <NavLink
179 to={`/${slug}/-/agents/new`}
180 className={({ isActive }) =>
181 `mt-2 flex h-9 items-center gap-2.5 rounded-md border border-dashed px-2 text-[0.8125rem] transition-colors ${
182 isActive ? "border-accent/50 bg-accent/10 text-fg" : "border-line text-muted hover:border-line-strong hover:text-fg"
183 }`
184 }
185 >
186 <span className="flex w-5 justify-center">
187 <Plus size={15} />
188 </span>
189 New agent
190 </NavLink>
191 )}
Chat and workspace agents: channels, DMs and named agents you talk to192 </nav>
193 </div>
194 );
195}
196
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)197/** Specialists by team or department, each group by name, the groups in the gallery's order. */
198function orgChart(agents: Listed[]): Map<string, Listed[]> {
199 const groups = new Map<string, Listed[]>();
200 for (const agent of agents) groups.set(placeOf(agent), [...(groups.get(placeOf(agent)) ?? []), agent]);
201 const rank = (place: string) => {
202 const at = DEPARTMENTS.indexOf(place);
203 return at < 0 ? DEPARTMENTS.length : at;
204 };
205 return new Map(
206 [...groups]
207 .sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b))
208 .map(([place, list]) => [place, list.sort((x, y) => x.display_name.localeCompare(y.display_name))]),
209 );
210}
211
212function Group({ title, children, collapsible }: { title: string; children: ReactNode; collapsible?: boolean }) {
213 const [open, setOpen] = useState(true);
214 if (collapsible) {
215 return (
216 <section className="mt-3">
217 <button
218 type="button"
219 onClick={() => setOpen(!open)}
220 aria-expanded={open}
221 className="mb-1 flex w-full items-center gap-1 rounded px-1 text-xs font-medium text-faint transition-colors hover:text-muted"
222 >
223 <ChevronRight size={12} className={`transition-transform ${open ? "rotate-90" : ""}`} />
224 {title}
225 </button>
226 {open && <ul className="space-y-px border-l border-line pl-1.5 ml-2.5">{children}</ul>}
227 </section>
228 );
229 }
Chat and workspace agents: channels, DMs and named agents you talk to230 return (
231 <section className="mt-4">
232 <h3 className="mb-1 px-2 text-xs font-medium text-faint">{title}</h3>
233 <ul className="space-y-px">{children}</ul>
234 </section>
235 );
236}
237
238/** The templates g1t ships, and a blank one, to start a new agent from. */
239export 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)240 // By department, in the order the departments are listed; any other after them.
241 const order = (department: string) => {
242 const at = DEPARTMENTS.indexOf(department);
243 return at < 0 ? DEPARTMENTS.length : at;
244 };
245 const groups = new Map<string, AgentTemplate[]>();
246 for (const template of [...templates].sort((a, b) => order(a.department) - order(b.department))) {
247 const key = template.department || "Other";
248 groups.set(key, [...(groups.get(key) ?? []), template]);
249 }
Chat and workspace agents: channels, DMs and named agents you talk to250 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)251 <div className="space-y-8">
252 {[...groups].map(([department, list]) => (
253 <section key={department}>
254 <h2 className="mb-3 text-xs font-semibold tracking-wide text-faint uppercase">{department}</h2>
255 <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
256 {list.map((template) => (
257 <TemplateCard
258 key={template.id}
259 selected={chosen === template.id}
260 onClick={() => onChoose(template.id)}
261 name={template.display_name}
262 title={template.title}
263 seed={template.handle}
264 duties={template.responsibilities.slice(0, 3)}
265 />
266 ))}
267 </div>
268 </section>
Chat and workspace agents: channels, DMs and named agents you talk to269 ))}
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)270 <section>
271 <h2 className="mb-3 text-xs font-semibold tracking-wide text-faint uppercase">Your own</h2>
272 <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
273 <TemplateCard
274 selected={chosen === "blank"}
275 onClick={() => onChoose("blank")}
276 name="A new role"
277 title="Start from nothing"
278 blank
279 duties={["Give it a name, a title and a team", "Say what it is responsible for"]}
280 />
281 </div>
282 </section>
Chat and workspace agents: channels, DMs and named agents you talk to283 </div>
284 );
285}
286
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)287/** 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 to288function TemplateCard({
289 selected,
290 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)291 name,
Chat and workspace agents: channels, DMs and named agents you talk to292 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)293 seed,
294 duties,
Chat and workspace agents: channels, DMs and named agents you talk to295 blank,
296}: {
297 selected: boolean;
298 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)299 name: string;
Chat and workspace agents: channels, DMs and named agents you talk to300 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)301 seed?: string;
302 duties: string[];
Chat and workspace agents: channels, DMs and named agents you talk to303 blank?: boolean;
304}) {
305 return (
306 <button
307 type="button"
308 onClick={onClick}
309 aria-pressed={selected}
310 className={`group flex flex-col items-start rounded-xl border p-4 text-left transition-all ${
311 selected
312 ? "border-accent/60 bg-accent/[0.07] ring-1 ring-accent/40"
313 : "border-line bg-surface hover:-translate-y-px hover:border-line-strong hover:bg-raised/40"
314 }`}
315 >
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)316 <span className="flex w-full items-center gap-3">
Chat and workspace agents: channels, DMs and named agents you talk to317 {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)318 <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 to319 <Plus size={16} />
320 </span>
321 ) : (
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)322 <PixelCreature seed={seed ?? name} size={40} />
Chat and workspace agents: channels, DMs and named agents you talk to323 )}
324 <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)325 <span className="block truncate text-sm font-semibold">{name}</span>
326 <span className="block truncate text-xs text-muted">{title}</span>
Chat and workspace agents: channels, DMs and named agents you talk to327 </span>
328 <ChevronRight size={15} className={`shrink-0 transition-colors ${selected ? "text-accent" : "text-faint group-hover:text-muted"}`} />
329 </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)330 <ul className="mt-3 space-y-1">
331 {duties.map((duty) => (
332 <li key={duty} className="flex gap-2 text-[0.8125rem] leading-snug text-muted">
333 <span className="mt-[0.45rem] size-1 shrink-0 rounded-full bg-line-strong" aria-hidden="true" />
334 <span className="line-clamp-1">{duty}</span>
335 </li>
336 ))}
337 </ul>
Chat and workspace agents: channels, DMs and named agents you talk to338 </button>
339 );
340}
341
342const 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";
343
344function Label({ children, hint, error, htmlFor }: { children: ReactNode; hint?: ReactNode; error?: string; htmlFor?: string }) {
345 return (
346 <div className="mb-1.5 flex items-baseline justify-between gap-3">
347 <label htmlFor={htmlFor} className="text-sm font-medium text-fg-soft">
348 {children}
349 </label>
350 {error ? <span className="text-xs text-danger">{error}</span> : hint ? <span className="text-xs text-faint">{hint}</span> : null}
351 </div>
352 );
353}
354
355/** A section of the form: what it is on the left, its fields on the right, on a wide screen. */
356function FormSection({ title, about, children }: { title: string; about: ReactNode; children: ReactNode }) {
357 return (
358 <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]">
359 <div>
360 <h2 className="text-sm font-semibold">{title}</h2>
361 <p className="mt-1 text-[0.8125rem] leading-relaxed text-muted">{about}</p>
362 </div>
363 <div className="min-w-0 space-y-5">{children}</div>
364 </section>
365 );
366}
367
368function TierSelect({ name, value, none }: { name: string; value: ModelTier | null; none: string }) {
369 return (
370 <Select name={name} defaultValue={value ?? "none"}>
371 <SelectTrigger id={name}>
372 <SelectValue />
373 </SelectTrigger>
374 <SelectContent>
375 <SelectItem value="none">{none}</SelectItem>
376 {(["small", "large", "frontier"] as const).map((tier) => (
377 <SelectItem key={tier} value={tier} description={TIER_LABELS[tier].split(": ")[1]}>
378 {TIER_LABELS[tier].split(":")[0]}
379 </SelectItem>
380 ))}
381 </SelectContent>
382 </Select>
383 );
384}
385
386function Money({ name, value, label, hint, error }: { name: string; value: number | null; label: string; hint: string; error?: string }) {
387 return (
388 <div>
389 <Label htmlFor={name} hint={hint} error={error}>
390 {label}
391 </Label>
392 <div className="flex items-center rounded-md border border-line bg-bg transition-colors focus-within:border-accent-dim hover:border-line-strong">
393 <span className="pl-3 text-sm text-faint">$</span>
394 <input
395 id={name}
396 name={name}
397 inputMode="decimal"
398 defaultValue={dollarsField(value)}
399 placeholder="No cap"
400 autoComplete="off"
401 data-1p-ignore
402 className="min-w-0 grow bg-transparent px-2 py-2 text-sm tabular-nums outline-none placeholder:text-faint"
403 />
404 </div>
405 </div>
406 );
407}
408
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)409/** Names to shuffle through when a template has none of its own. */
410const FALLBACK_NAMES = ["Margo", "Juniper", "Otto", "Wren", "Basil", "Nova", "Clementine", "Fig", "Rook", "Pip"];
411
Chat and workspace agents: channels, DMs and named agents you talk to412/**
413 * Everything that defines an agent, for making one and for its Profile:
414 * who it is, its job, its voice, the limits on routing, its budget, what it
415 * may do alone, and how much it takes on at once.
416 */
417export function AgentForm({
418 draft,
419 errors,
420 submit,
421 intent,
422 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)423 nameIdeas = [],
424 locked = false,
425 seed,
426 teams = [],
Agents recall what Docs say before they answer or work, and each has required reading427 spaces = [],
Chat and workspace agents: channels, DMs and named agents you talk to428}: {
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)429 /** The workspace's teams, to put it on one. */
430 teams?: { slug: string; name: string }[];
Agents recall what Docs say before they answer or work, and each has required reading431 /** The Docs spaces the person editing can read, for its required reading. */
432 spaces?: { id: string; name: string; kind: string }[];
Chat and workspace agents: channels, DMs and named agents you talk to433 draft: AgentDraft;
434 errors?: Record<string, string>;
435 submit: string;
436 intent: string;
437 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)438 /** Names to shuffle through (a template's `name_ideas`); the dice offers them. */
439 nameIdeas?: string[];
440 /** g1t, the orchestrator: its identity and job are fixed; the rest is editable. */
441 locked?: boolean;
442 /** The agent's own `avatar_seed`, once it has one; a new agent's face follows its handle. */
443 seed?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to444}) {
445 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)446 // The handle follows the name until someone edits it.
447 const [name, setName] = useState(draft.display_name);
448 const [handle, setHandle] = useState(draft.handle);
449 const [handleEdited, setHandleEdited] = useState(Boolean(draft.handle) && cleanHandle(draft.display_name) !== draft.handle);
450 const ideas = nameIdeas.length > 0 ? nameIdeas : FALLBACK_NAMES;
451 const [idea, setIdea] = useState(() => Math.max(0, ideas.indexOf(draft.display_name)));
452 const rename = (next: string) => {
453 setName(next);
454 if (!handleEdited) setHandle(cleanHandle(next));
455 };
456 const shuffle = () => {
457 const at = (idea + 1) % ideas.length;
458 setIdea(at);
459 rename(ideas[at]!);
460 };
Chat and workspace agents: channels, DMs and named agents you talk to461 const { pathname } = useLocation();
462 const busy = navigation.state === "submitting" && navigation.formAction === pathname;
463 const [preset, setPreset] = useState(draft.personality_preset);
464 const [advanced, setAdvanced] = useState(Boolean(draft.routing.pinned));
465 const e = errors ?? {};
466 const presetAbout = PRESETS.find((p) => p.value === preset)?.about;
467 return (
468 <Form method="post" key={formKey} className="pb-24">
469 <input type="hidden" name="intent" value={intent} />
470 <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)471 <FormSection
472 title="Identity"
473 about={
474 locked
475 ? "g1t is the workspace's orchestrator: its name, handle and role are the same everywhere, and it can't be archived."
476 : "How it shows in chat, mentions, assignees and the audit log. Give it a name people will enjoy saying."
477 }
478 >
479 <div className="flex items-center gap-3">
480 <AgentAvatar agent={{ handle: handle || "agent", avatar_seed: seed ?? handle, builtin: locked }} size={44} />
481 <div className="min-w-0 text-sm">
482 <p className="truncate font-semibold">{name || "Your new agent"}</p>
483 <p className="truncate text-xs text-muted">{handle ? `@${handle}` : "@handle"}</p>
484 </div>
485 </div>
Chat and workspace agents: channels, DMs and named agents you talk to486 <div className="grid gap-5 sm:grid-cols-2">
487 <div>
488 <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)489 Name
Chat and workspace agents: channels, DMs and named agents you talk to490 </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)491 <div className="flex gap-2">
492 <input
493 id="display_name"
494 name="display_name"
495 value={name}
496 onChange={(event) => rename(event.target.value)}
497 readOnly={locked}
498 placeholder="Margo"
499 className={`${FIELD} ${locked ? "cursor-not-allowed text-muted" : ""}`}
500 autoComplete="off"
501 data-1p-ignore
502 />
503 {!locked && (
504 <Hint label="Another name">
505 <button
506 type="button"
507 aria-label="Another name"
508 onClick={shuffle}
509 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"
510 >
511 <Dices size={17} />
512 </button>
513 </Hint>
514 )}
515 </div>
Chat and workspace agents: channels, DMs and named agents you talk to516 </div>
517 <div>
518 <Label htmlFor="handle" error={e.handle} hint="Mentioned as @handle">
519 Handle
520 </Label>
521 <div className="flex items-center rounded-md border border-line bg-bg transition-colors focus-within:border-accent-dim hover:border-line-strong">
522 <span className="pl-3 font-mono text-sm text-faint">@</span>
523 <input
524 id="handle"
525 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)526 value={handle}
527 onChange={(event) => {
528 setHandle(event.target.value);
529 setHandleEdited(true);
530 }}
531 readOnly={locked}
532 placeholder="margo"
Chat and workspace agents: channels, DMs and named agents you talk to533 autoComplete="off"
534 data-1p-ignore
535 className="min-w-0 grow bg-transparent py-2 pr-3 pl-0.5 font-mono text-sm outline-none placeholder:text-faint"
536 />
537 </div>
538 </div>
539 </div>
540 </FormSection>
541
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)542 {!locked && (
543 <FormSection
544 title="Role"
545 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."
546 >
547 <RoleFields
548 title={draft.title}
549 team={draft.team}
550 department={draft.department}
551 responsibilities={draft.responsibilities}
552 teams={teams}
553 errors={e}
554 locked={locked}
555 />
556 </FormSection>
557 )}
558
559 <FormSection
560 title="Job"
561 about={
562 locked
563 ? "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."
564 : "What it is responsible for, how it works, and what good looks like. It reads this before every reply and task."
565 }
566 >
Chat and workspace agents: channels, DMs and named agents you talk to567 <div>
568 <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)569 {locked ? "Extra instructions" : "Instructions"}
Chat and workspace agents: channels, DMs and named agents you talk to570 </Label>
571 <textarea
572 id="instructions"
573 name="instructions"
574 defaultValue={draft.instructions}
575 rows={9}
576 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."}
577 className={`${FIELD} resize-y font-mono text-[0.8125rem] leading-relaxed`}
578 />
579 </div>
580 </FormSection>
581
Agents recall what Docs say before they answer or work, and each has required reading582 <FormSection
583 title="Required reading"
584 about="Docs it checks first, every time it answers or works. It also recalls whatever else in Docs fits the question, but only from spaces everyone in the conversation can read."
585 >
586 {spaces.length ? (
587 <div className="grid gap-1.5 sm:grid-cols-2">
588 {spaces.map((space) => (
589 <label
590 key={space.id}
591 className="flex min-h-10 cursor-pointer items-center gap-2.5 rounded-lg border border-line bg-surface px-3 py-2 text-sm has-[:checked]:border-accent/50 has-[:checked]:bg-accent/5"
592 >
593 <input type="checkbox" name="reading" value={space.id} defaultChecked={(draft.reading ?? []).includes(space.id)} className="size-4 accent-[var(--color-accent)]" />
594 <span className="min-w-0 grow truncate">{space.name}</span>
595 <span className="shrink-0 text-xs text-faint capitalize">{space.kind}</span>
596 </label>
597 ))}
598 </div>
599 ) : (
600 <p className="text-sm text-muted">No Docs spaces yet. Once your workspace has some, choose what it should know by heart.</p>
601 )}
602 {(draft.reading ?? []).filter((id) => !spaces.some((space) => space.id === id)).map((id) => (
603 // Spaces it reads that you can't see stay as they are.
604 <input key={id} type="hidden" name="reading" value={id} />
605 ))}
606 </FormSection>
607
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)608 {!locked && (
609 <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.">
610 <SubagentsField initial={draft.subagents} routing={draft.routing} />
611 {e.subagents && <p className="text-sm text-danger">{e.subagents}</p>}
612 </FormSection>
613 )}
614
Chat and workspace agents: channels, DMs and named agents you talk to615 <FormSection title="Personality" about="Its voice only: tone, length, how it asks. A personality never changes what it may do.">
616 <div>
617 <input type="hidden" name="personality_preset" value={preset} />
618 <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">
619 {PRESETS.map((option) => (
620 <button
621 key={option.value}
622 type="button"
623 role="radio"
624 aria-checked={preset === option.value}
625 onClick={() => setPreset(option.value)}
626 className={`h-8 rounded-md text-[0.8125rem] font-medium transition-colors ${
627 preset === option.value ? "bg-raised text-fg shadow-sm ring-1 ring-line-strong" : "text-muted hover:text-fg"
628 }`}
629 >
630 {option.label}
631 </button>
632 ))}
633 </div>
634 <p className="mt-2 text-xs text-muted">{presetAbout}</p>
635 </div>
636 <div>
637 <Label htmlFor="personality" hint="Optional">
638 In its own words
639 </Label>
640 <textarea
641 id="personality"
642 name="personality"
643 defaultValue={draft.personality}
644 rows={3}
645 placeholder="Dry humor is fine. No emoji. Asks one question at a time."
646 className={`${FIELD} resize-y`}
647 />
648 </div>
649 </FormSection>
650
651 <FormSection
652 title="Models"
653 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."
654 >
655 <div className="flex items-start gap-3 rounded-xl border border-accent/25 bg-accent/[0.06] p-4">
656 <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent/15 text-accent">
657 <RouteIcon size={16} />
658 </span>
659 <div>
660 <p className="text-sm font-semibold">Auto: routed per step by need</p>
661 <p className="mt-0.5 text-[0.8125rem] leading-relaxed text-muted">
662 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.
663 </p>
664 </div>
665 </div>
666 <div className="grid gap-5 sm:grid-cols-3">
667 <div>
668 <Label htmlFor="floor">Floor</Label>
669 <TierSelect name="floor" value={draft.routing.floor} none="No floor" />
670 </div>
671 <div>
672 <Label htmlFor="ceiling" error={e.ceiling}>
673 Ceiling
674 </Label>
675 <TierSelect name="ceiling" value={draft.routing.ceiling} none="No ceiling" />
676 </div>
677 <div>
678 <Label htmlFor="providers">Providers</Label>
679 <Select name="providers" defaultValue={draft.routing.providers[0] ?? "any"}>
680 <SelectTrigger id="providers">
681 <SelectValue />
682 </SelectTrigger>
683 <SelectContent>
684 <SelectItem value="any" description="g1t's hosted models and the workspace's own">
685 Any the workspace allows
686 </SelectItem>
687 <SelectItem value="g1t" description="Never the workspace's own keys">
688 g1t's hosted models
689 </SelectItem>
690 <SelectItem value="workspace" description="Set up under Integrations; billed by the provider">
691 The workspace's own providers
692 </SelectItem>
693 </SelectContent>
694 </Select>
695 </div>
696 </div>
697 <div className="rounded-lg border border-line">
698 <button
699 type="button"
700 onClick={() => setAdvanced(!advanced)}
701 aria-expanded={advanced}
702 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"
703 >
704 <ChevronRight size={14} className={`transition-transform ${advanced ? "rotate-90" : ""}`} />
705 Advanced
706 <span className="font-normal text-faint">Pin one model</span>
707 </button>
708 <div hidden={!advanced} className="border-t border-line px-3.5 py-3.5">
709 <Label htmlFor="pinned" error={e.pinned} hint="For your own endpoints. Turns routing off.">
710 Pinned model
711 </Label>
712 <input id="pinned" name="pinned" defaultValue={draft.routing.pinned ?? ""} placeholder="provider/model" className={`${FIELD} font-mono`} autoComplete="off" data-1p-ignore />
713 </div>
714 </div>
715 </FormSection>
716
717 <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.">
718 <div className="grid gap-5 sm:grid-cols-3">
719 <Money name="monthly" label="Monthly" hint="Per calendar month" value={draft.budget.monthly_micros} error={e.monthly} />
720 <Money name="daily" label="Daily" hint="Optional" value={draft.budget.daily_micros} error={e.daily} />
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions721 <Money name="task" label="Per session" hint="Going over asks an owner" value={draft.budget.task_micros} error={e.task} />
Chat and workspace agents: channels, DMs and named agents you talk to722 </div>
723 </FormSection>
724
725 <FormSection title="Autonomy" about="What it may do alone and what needs a person. Rulesets and branch protection still apply on top.">
726 <div className="grid gap-5 sm:grid-cols-2">
727 {(Object.keys(AUTONOMY) as (keyof typeof AUTONOMY)[]).map((key) => (
728 <div key={key}>
729 <Label htmlFor={key}>{AUTONOMY[key].label}</Label>
730 <Select name={key} defaultValue={draft.autonomy[key]}>
731 <SelectTrigger id={key}>
732 <SelectValue />
733 </SelectTrigger>
734 <SelectContent>
735 {AUTONOMY[key].options.map(([value, label]) => (
736 <SelectItem key={value} value={value}>
737 {label}
738 </SelectItem>
739 ))}
740 </SelectContent>
741 </Select>
742 </div>
743 ))}
744 </div>
745 </FormSection>
746
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions747 <FormSection title="Capacity" about="How many sessions it works on at once. More wait in line, queued.">
Chat and workspace agents: channels, DMs and named agents you talk to748 <div className="max-w-40">
749 <Label htmlFor="capacity" error={e.capacity}>
Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions750 Sessions at once
Chat and workspace agents: channels, DMs and named agents you talk to751 </Label>
752 <input id="capacity" name="capacity" type="number" min={1} max={20} defaultValue={draft.capacity} className={`${FIELD} tabular-nums`} />
753 </div>
754 </FormSection>
755
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar756 <div className="sticky bottom-(--tabbar-h) in-data-[keyboard=open]: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">
Chat and workspace agents: channels, DMs and named agents you talk to757 {Object.keys(e).length > 0 && <p className="mr-auto text-sm text-danger">Check the fields marked above.</p>}
758 <button
759 type="submit"
760 disabled={busy}
761 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"
762 >
763 <Sparkles size={15} />
764 {busy ? "Saving…" : submit}
765 </button>
766 </div>
767 </Form>
768 );
769}

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