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) | 1 | import { GripVertical, Pencil, Plus, Trash2, X } from "lucide-react"; |
| 2 | import { useState } from "react"; | |
| 3 | ||
| 4 | import type { AgentRouting, ModelTier, SubagentDef } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { Hint } from "./ui/hint"; | |
| 7 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 8 | import { Sheet, SheetContent, SheetDescription, SheetTitle } from "./ui/sheet"; | |
| 9 | import { MAX_RESPONSIBILITIES, TIER_LABELS, clampRouting, cleanSubagentName } from "../lib/agent-form"; | |
| 10 | ||
| 11 | const FIELD = | |
| 12 | "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"; | |
| 13 | ||
| 14 | /** The departments an agent can sit in when it is on no team, as the role gallery groups them. */ | |
| 15 | export const DEPARTMENTS = ["Engineering", "QA", "Operations", "Docs", "Product", "Customer Support", "Sales"]; | |
| 16 | ||
| 17 | /** | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 18 | * An agent's role: its title, the team it is on (or a department, when it |
| 19 | * is on none), and what it is responsible for, as a list to add to, edit | |
| 20 | * and reorder. | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 21 | */ |
| 22 | export function RoleFields({ | |
| 23 | title, | |
| 24 | team, | |
| 25 | department, | |
| 26 | responsibilities, | |
| 27 | teams, | |
| 28 | errors, | |
| 29 | locked, | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 30 | personal = false, |
| 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) | 31 | }: { |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 32 | /** A personal agent: on no team, so no team to choose. */ |
| 33 | personal?: boolean; | |
| 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) | 34 | title: string; |
| 35 | team: string | null; | |
| 36 | department: string; | |
| 37 | responsibilities: string[]; | |
| 38 | teams: { slug: string; name: string }[]; | |
| 39 | errors: Record<string, string>; | |
| 40 | locked?: boolean; | |
| 41 | }) { | |
| 42 | const [items, setItems] = useState<string[]>(responsibilities.length > 0 ? responsibilities : [""]); | |
| 43 | const [onTeam, setOnTeam] = useState(team ?? "none"); | |
| 44 | const [dragging, setDragging] = useState<number | null>(null); | |
| 45 | const move = (from: number, to: number) => | |
| 46 | setItems((now) => { | |
| 47 | const next = [...now]; | |
| 48 | const [item] = next.splice(from, 1); | |
| 49 | next.splice(to, 0, item!); | |
| 50 | return next; | |
| 51 | }); | |
| 52 | return ( | |
| 53 | <> | |
| 54 | <div className="grid gap-5 sm:grid-cols-2"> | |
| 55 | <div> | |
| 56 | <label htmlFor="title" className="mb-1.5 flex items-baseline justify-between text-sm font-medium text-fg-soft"> | |
| 57 | Title | |
| 58 | {errors.title && <span className="text-xs font-normal text-danger">{errors.title}</span>} | |
| 59 | </label> | |
| 60 | <input | |
| 61 | id="title" | |
| 62 | name="title" | |
| 63 | defaultValue={title} | |
| 64 | readOnly={locked} | |
| 65 | placeholder="QA Engineer" | |
| 66 | className={`${FIELD} ${locked ? "cursor-not-allowed text-muted" : ""}`} | |
| 67 | autoComplete="off" | |
| 68 | data-1p-ignore | |
| 69 | /> | |
| 70 | </div> | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 71 | {personal ? ( |
| 72 | <div> | |
| 73 | <p className="mb-1.5 text-sm font-medium text-fg-soft">Team</p> | |
| 74 | <p className="flex min-h-9 items-center text-[0.8125rem] text-muted">Personal agents join teams once an owner promotes them.</p> | |
| 75 | </div> | |
| 76 | ) : ( | |
| 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) | 77 | <div> |
| 78 | <label htmlFor="team" className="mb-1.5 block text-sm font-medium text-fg-soft"> | |
| 79 | Team | |
| 80 | </label> | |
| 81 | <Select name="team" value={onTeam} onValueChange={setOnTeam} disabled={locked}> | |
| 82 | <SelectTrigger id="team"> | |
| 83 | <SelectValue /> | |
| 84 | </SelectTrigger> | |
| 85 | <SelectContent> | |
| 86 | <SelectItem value="none" description="Placed by a department label instead"> | |
| 87 | No team | |
| 88 | </SelectItem> | |
| 89 | {teams.map((t) => ( | |
| 90 | <SelectItem key={t.slug} value={t.slug}> | |
| 91 | {t.name} | |
| 92 | </SelectItem> | |
| 93 | ))} | |
| 94 | </SelectContent> | |
| 95 | </Select> | |
| 96 | </div> | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 97 | )} |
| 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 | </div> |
| 99 | {onTeam === "none" && ( | |
| 100 | <div> | |
| 101 | <label htmlFor="department" className="mb-1.5 flex items-baseline justify-between text-sm font-medium text-fg-soft"> | |
| 102 | Department | |
| 103 | <span className="text-xs font-normal text-faint">Where it shows in the Agents sidebar</span> | |
| 104 | </label> | |
| 105 | <input | |
| 106 | id="department" | |
| 107 | name="department" | |
| 108 | list="departments" | |
| 109 | defaultValue={department} | |
| 110 | readOnly={locked} | |
| 111 | placeholder="QA" | |
| 112 | className={`${FIELD} ${locked ? "cursor-not-allowed text-muted" : ""}`} | |
| 113 | autoComplete="off" | |
| 114 | data-1p-ignore | |
| 115 | /> | |
| 116 | <datalist id="departments"> | |
| 117 | {DEPARTMENTS.map((d) => ( | |
| 118 | <option key={d} value={d} /> | |
| 119 | ))} | |
| 120 | </datalist> | |
| 121 | </div> | |
| 122 | )} | |
| 123 | <div> | |
| 124 | <p className="mb-1.5 flex items-baseline justify-between text-sm font-medium text-fg-soft"> | |
| 125 | Responsibilities | |
| 126 | <span className="text-xs font-normal text-faint">Broad duties, not tasks. Up to {MAX_RESPONSIBILITIES}.</span> | |
| 127 | </p> | |
| 128 | <ul className="space-y-1.5"> | |
| 129 | {items.map((item, index) => ( | |
| 130 | <li | |
| 131 | key={index} | |
| 132 | draggable={!locked && items.length > 1} | |
| 133 | onDragStart={() => setDragging(index)} | |
| 134 | onDragOver={(event) => dragging != null && event.preventDefault()} | |
| 135 | onDrop={() => { | |
| 136 | if (dragging != null && dragging !== index) move(dragging, index); | |
| 137 | setDragging(null); | |
| 138 | }} | |
| 139 | onDragEnd={() => setDragging(null)} | |
| 140 | className={`group flex items-center gap-1.5 ${dragging === index ? "opacity-50" : ""}`} | |
| 141 | > | |
| 142 | <span className="cursor-grab text-faint opacity-0 transition-opacity group-hover:opacity-100" aria-hidden="true"> | |
| 143 | <GripVertical size={14} /> | |
| 144 | </span> | |
| 145 | <input | |
| 146 | name="responsibility" | |
| 147 | value={item} | |
| 148 | onChange={(event) => setItems((now) => now.map((v, i) => (i === index ? event.target.value : v)))} | |
| 149 | readOnly={locked} | |
| 150 | aria-label={`Responsibility ${index + 1}`} | |
| 151 | placeholder={index === 0 ? "Keep the test suite green and fast" : "Another duty"} | |
| 152 | className={FIELD} | |
| 153 | autoComplete="off" | |
| 154 | data-1p-ignore | |
| 155 | /> | |
| 156 | {!locked && ( | |
| 157 | <Hint label="Remove"> | |
| 158 | <button | |
| 159 | type="button" | |
| 160 | aria-label={`Remove responsibility ${index + 1}`} | |
| 161 | onClick={() => setItems((now) => (now.length > 1 ? now.filter((_, i) => i !== index) : [""]))} | |
| 162 | className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg" | |
| 163 | > | |
| 164 | <X size={14} /> | |
| 165 | </button> | |
| 166 | </Hint> | |
| 167 | )} | |
| 168 | </li> | |
| 169 | ))} | |
| 170 | </ul> | |
| 171 | {!locked && items.length < MAX_RESPONSIBILITIES && ( | |
| 172 | <button | |
| 173 | type="button" | |
| 174 | onClick={() => setItems((now) => [...now, ""])} | |
| 175 | className="mt-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[0.8125rem] text-muted hover:bg-raised hover:text-fg" | |
| 176 | > | |
| 177 | <Plus size={14} /> | |
| 178 | Add a responsibility | |
| 179 | </button> | |
| 180 | )} | |
| 181 | </div> | |
| 182 | </> | |
| 183 | ); | |
| 184 | } | |
| 185 | ||
| 186 | const BLANK_SUBAGENT: SubagentDef = { name: "", description: "", instructions: "", routing: { floor: null, ceiling: null }, max_parallel: 2 }; | |
| 187 | ||
| 188 | /** | |
| 189 | * Subagents: help an agent keeps for its own work, as a list, edited in a | |
| 190 | * drawer. Their routing stays within the agent's own. Posted as JSON. | |
| 191 | */ | |
| 192 | export function SubagentsField({ initial, routing }: { initial: SubagentDef[]; routing: Pick<AgentRouting, "floor" | "ceiling"> }) { | |
| 193 | const [list, setList] = useState<SubagentDef[]>(initial); | |
| 194 | const [editing, setEditing] = useState<{ index: number | null; draft: SubagentDef } | null>(null); | |
| 195 | const [error, setError] = useState<string | null>(null); | |
| 196 | const save = () => { | |
| 197 | if (!editing) return; | |
| 198 | const name = cleanSubagentName(editing.draft.name); | |
| 199 | if (!name) return setError("Give it a name, like flake-hunter."); | |
| 200 | if (list.some((s, i) => s.name === name && i !== editing.index)) return setError(`There's already a ${name}.`); | |
| 201 | const next: SubagentDef = { ...editing.draft, name, routing: clampRouting(editing.draft.routing, routing) }; | |
| 202 | setList((now) => (editing.index == null ? [...now, next] : now.map((s, i) => (i === editing.index ? next : s)))); | |
| 203 | setEditing(null); | |
| 204 | }; | |
| 205 | const tier = (value: string): ModelTier | null => (value === "none" ? null : (value as ModelTier)); | |
| 206 | return ( | |
| 207 | <div> | |
| 208 | <input type="hidden" name="subagents" value={JSON.stringify(list)} /> | |
| 209 | <p className="mb-3 inline-flex items-center gap-1.5 rounded-full border border-line px-2.5 py-0.5 text-xs text-muted">Used inside tasks: coming soon</p> | |
| 210 | {list.length === 0 ? ( | |
| 211 | <p className="rounded-lg border border-dashed border-line px-4 py-5 text-sm text-muted">None yet. A subagent is help it calls on for one kind of work, such as finding flaky tests.</p> | |
| 212 | ) : ( | |
| 213 | <ul className="divide-y divide-line rounded-lg border border-line"> | |
| 214 | {list.map((sub, index) => ( | |
| 215 | <li key={sub.name} className="flex items-center gap-3 px-3.5 py-2.5"> | |
| 216 | <span className="min-w-0 grow"> | |
| 217 | <span className="block font-mono text-sm text-fg">{sub.name}</span> | |
| 218 | <span className="block truncate text-xs text-muted">{sub.description || "No description"}</span> | |
| 219 | </span> | |
| 220 | <span className="hidden text-xs text-faint sm:inline"> | |
| 221 | {sub.routing.floor ?? "any"} to {sub.routing.ceiling ?? "any"} · {sub.max_parallel} at once | |
| 222 | </span> | |
| 223 | <Hint label="Edit"> | |
| 224 | <button | |
| 225 | type="button" | |
| 226 | aria-label={`Edit ${sub.name}`} | |
| 227 | onClick={() => { | |
| 228 | setError(null); | |
| 229 | setEditing({ index, draft: sub }); | |
| 230 | }} | |
| 231 | className="flex size-8 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg" | |
| 232 | > | |
| 233 | <Pencil size={14} /> | |
| 234 | </button> | |
| 235 | </Hint> | |
| 236 | <Hint label="Remove"> | |
| 237 | <button | |
| 238 | type="button" | |
| 239 | aria-label={`Remove ${sub.name}`} | |
| 240 | onClick={() => setList((now) => now.filter((_, i) => i !== index))} | |
| 241 | className="flex size-8 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-danger" | |
| 242 | > | |
| 243 | <Trash2 size={14} /> | |
| 244 | </button> | |
| 245 | </Hint> | |
| 246 | </li> | |
| 247 | ))} | |
| 248 | </ul> | |
| 249 | )} | |
| 250 | <button | |
| 251 | type="button" | |
| 252 | onClick={() => { | |
| 253 | setError(null); | |
| 254 | setEditing({ index: null, draft: { ...BLANK_SUBAGENT, routing: clampRouting(BLANK_SUBAGENT.routing, routing) } }); | |
| 255 | }} | |
| 256 | className="mt-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[0.8125rem] text-muted hover:bg-raised hover:text-fg" | |
| 257 | > | |
| 258 | <Plus size={14} /> | |
| 259 | Add a subagent | |
| 260 | </button> | |
| 261 | <Sheet open={editing != null} onOpenChange={(open) => !open && setEditing(null)}> | |
| 262 | <SheetContent side="right" className="w-[28rem] max-w-[100vw]"> | |
| 263 | {editing && ( | |
| 264 | <div className="flex h-full flex-col"> | |
| 265 | <div className="border-b border-line px-5 py-4"> | |
| 266 | <SheetTitle>{editing.index == null ? "New subagent" : editing.draft.name}</SheetTitle> | |
| 267 | <SheetDescription className="mt-1 text-sm text-muted">Its routing stays within its agent's own limits.</SheetDescription> | |
| 268 | </div> | |
| 269 | <div className="min-h-0 grow space-y-5 overflow-y-auto px-5 py-5"> | |
| 270 | <label className="block"> | |
| 271 | <span className="mb-1.5 block text-sm font-medium text-fg-soft">Name</span> | |
| 272 | <input | |
| 273 | value={editing.draft.name} | |
| 274 | onChange={(event) => setEditing({ ...editing, draft: { ...editing.draft, name: event.target.value } })} | |
| 275 | placeholder="flake-hunter" | |
| 276 | className={`${FIELD} font-mono`} | |
| 277 | autoComplete="off" | |
| 278 | data-1p-ignore | |
| 279 | /> | |
| 280 | </label> | |
| 281 | <label className="block"> | |
| 282 | <span className="mb-1.5 block text-sm font-medium text-fg-soft">Description</span> | |
| 283 | <input | |
| 284 | value={editing.draft.description} | |
| 285 | onChange={(event) => setEditing({ ...editing, draft: { ...editing.draft, description: event.target.value } })} | |
| 286 | placeholder="Finds which tests flake, and why" | |
| 287 | className={FIELD} | |
| 288 | autoComplete="off" | |
| 289 | data-1p-ignore | |
| 290 | /> | |
| 291 | </label> | |
| 292 | <label className="block"> | |
| 293 | <span className="mb-1.5 block text-sm font-medium text-fg-soft">Instructions</span> | |
| 294 | <textarea | |
| 295 | value={editing.draft.instructions} | |
| 296 | onChange={(event) => setEditing({ ...editing, draft: { ...editing.draft, instructions: event.target.value } })} | |
| 297 | rows={6} | |
| 298 | placeholder="Rerun the failing test 20 times; report the failure rate and the first stack trace." | |
| 299 | className={`${FIELD} resize-y font-mono text-[0.8125rem]`} | |
| 300 | /> | |
| 301 | </label> | |
| 302 | <div className="grid grid-cols-2 gap-4"> | |
| 303 | {(["floor", "ceiling"] as const).map((key) => ( | |
| 304 | <div key={key}> | |
| 305 | <span className="mb-1.5 block text-sm font-medium text-fg-soft capitalize">{key}</span> | |
| 306 | <Select | |
| 307 | value={editing.draft.routing[key] ?? "none"} | |
| 308 | onValueChange={(value) => | |
| 309 | setEditing({ ...editing, draft: { ...editing.draft, routing: clampRouting({ ...editing.draft.routing, [key]: tier(value) }, routing) } }) | |
| 310 | } | |
| 311 | > | |
| 312 | <SelectTrigger aria-label={key}> | |
| 313 | <SelectValue /> | |
| 314 | </SelectTrigger> | |
| 315 | <SelectContent> | |
| 316 | <SelectItem value="none">{key === "floor" ? "No floor" : "No ceiling"}</SelectItem> | |
| 317 | {(["small", "large", "frontier"] as const).map((t) => ( | |
| 318 | <SelectItem key={t} value={t}> | |
| 319 | {TIER_LABELS[t].split(":")[0]} | |
| 320 | </SelectItem> | |
| 321 | ))} | |
| 322 | </SelectContent> | |
| 323 | </Select> | |
| 324 | </div> | |
| 325 | ))} | |
| 326 | </div> | |
| 327 | <label className="block max-w-40"> | |
| 328 | <span className="mb-1.5 block text-sm font-medium text-fg-soft">At once, in one task</span> | |
| 329 | <input | |
| 330 | type="number" | |
| 331 | min={1} | |
| 332 | max={8} | |
| 333 | value={editing.draft.max_parallel} | |
| 334 | onChange={(event) => | |
| 335 | setEditing({ ...editing, draft: { ...editing.draft, max_parallel: Math.max(1, Math.min(8, Number(event.target.value) || 1)) } }) | |
| 336 | } | |
| 337 | className={`${FIELD} tabular-nums`} | |
| 338 | /> | |
| 339 | </label> | |
| 340 | </div> | |
| 341 | <div className="flex items-center gap-3 border-t border-line px-5 py-3"> | |
| 342 | {error && <p className="text-sm text-danger">{error}</p>} | |
| 343 | <button | |
| 344 | type="button" | |
| 345 | onClick={() => setEditing(null)} | |
| 346 | className="ml-auto h-9 rounded-md border border-line px-3.5 text-sm font-medium text-fg/90 hover:border-line-strong hover:bg-surface" | |
| 347 | > | |
| 348 | Cancel | |
| 349 | </button> | |
| 350 | <button type="button" onClick={save} className="h-9 rounded-md bg-accent px-3.5 text-sm font-medium text-bg hover:bg-accent-hover"> | |
| 351 | {editing.index == null ? "Add" : "Done"} | |
| 352 | </button> | |
| 353 | </div> | |
| 354 | </div> | |
| 355 | )} | |
| 356 | </SheetContent> | |
| 357 | </Sheet> | |
| 358 | </div> | |
| 359 | ); | |
| 360 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.