Skip to content

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

g1t.sh
239 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.

An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.1import { Archive, ArchiveRestore, ChevronRight, Trash2 } from "lucide-react";
2import { useEffect, useId, useState } from "react";
3import { useFetcher, useLocation } from "react-router";
4
5import type { ArchivedAgent } from "@g1t/contracts";
6
7import { AgentAvatar } from "../agent-avatar";
8import {
9 AlertDialog,
10 AlertDialogCancel,
11 AlertDialogContent,
12 AlertDialogDescription,
13 AlertDialogFooter,
14 AlertDialogHeader,
15 AlertDialogTitle,
16 AlertDialogTrigger,
17} from "../ui/alert-dialog";
18import { Badge } from "../ui/badge";
19import { Button } from "../ui/button";
20import { Card } from "../ui/card";
21import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible";
22import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
23import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field";
24import { Input, InputAddon, InputGroup } from "../ui/input";
25import { TimeAgo } from "../ui";
26import { confirmsHandle, countsLine } from "../../lib/agents-archived";
27import { type ActionResult, useDialogFetcher } from "./dialogs";
28
29/**
30 * The Archived section at the foot of the Agents page
31 * (docs.g1t.sh/guides/agents/, "Archive, restore and delete"): the
32 * workspace's archived agents for an owner, a member's own archived
33 * personal agents for anyone else. Folded by default with its count;
34 * `#archived` in the address opens it. Each row restores the agent, or
35 * deletes it for good after its handle is typed back.
36 */
37export function ArchivedSection({ slug, agents, owner }: { slug: string; agents: ArchivedAgent[]; owner: boolean }) {
38 const { hash } = useLocation();
39 const [open, setOpen] = useState(false);
40 useEffect(() => {
41 if (hash === "#archived") setOpen(true);
42 }, [hash]);
43 if (agents.length === 0) return null;
44 return (
45 <section id="archived" aria-labelledby="archived-heading">
46 <Collapsible open={open} onOpenChange={setOpen}>
47 <div className="flex items-baseline justify-between gap-3">
48 <h2 id="archived-heading" className="text-sm font-medium">
49 <CollapsibleTrigger className="font-medium text-fg">
50 <ChevronRight size={14} className="shrink-0 text-faint" />
51 <Archive size={14} className="text-faint" />
52 Archived <span className="font-normal text-faint">· {agents.length}</span>
53 </CollapsibleTrigger>
54 </h2>
55 <span className="text-xs text-faint">{owner ? "Restore one, or delete it for good" : "Your archived personal agents"}</span>
56 </div>
57 <CollapsibleContent>
58 <Card asChild divided className="mt-3 overflow-hidden">
59 <ul>
60 {agents.map((agent) => (
61 <ArchivedRow key={agent.id} slug={slug} agent={agent} />
62 ))}
63 </ul>
64 </Card>
65 </CollapsibleContent>
66 </Collapsible>
67 </section>
68 );
69}
70
71/** One archived agent: its face, dimmed, what it was, when it was archived, what is kept about it, and the two ways out. */
72function ArchivedRow({ slug, agent }: { slug: string; agent: ArchivedAgent }) {
73 return (
74 <li className="flex flex-wrap items-center gap-3 px-4 py-3 sm:flex-nowrap">
75 <span className="shrink-0 opacity-60 grayscale-[0.4]">
76 <AgentAvatar agent={{ ...agent, status: null }} size={32} />
77 </span>
78 <div className="min-w-0 grow basis-48">
79 <p className="flex items-center gap-2 text-sm">
80 <span className="truncate font-medium text-muted">{agent.display_name}</span>
81 <span className="truncate text-faint">@{agent.handle}</span>
82 {agent.scope === "personal" && (
83 <Badge tone="neutral" className="shrink-0 font-normal">
84 {agent.personal_owner ? `@${agent.personal_owner}'s` : "Personal"}
85 </Badge>
86 )}
87 </p>
88 <p className="truncate text-xs text-faint">
89 {agent.title || agent.role}
90 {" · "}
91 Archived <TimeAgo at={agent.archived_at} />
92 {agent.archived_by && ` by @${agent.archived_by}`}
93 </p>
94 <p className="truncate text-xs text-faint tabular-nums">{countsLine(agent.counts)}</p>
95 </div>
96 <div className="flex shrink-0 flex-wrap items-center gap-2">
97 <RestoreButton slug={slug} agent={agent} />
98 <DeleteDialog slug={slug} agent={agent} />
99 </div>
100 </li>
101 );
102}
103
104/**
105 * Restore, in one press. When a live agent holds the handle now, the
106 * service says so (`conflict`) and a dialog asks for a new handle to
107 * restore under instead.
108 */
109function RestoreButton({ slug, agent }: { slug: string; agent: ArchivedAgent }) {
110 const fetcher = useFetcher<ActionResult>({ key: `restore-${agent.id}` });
111 const [rename, setRename] = useState(false);
112 const idle = fetcher.state === "idle";
113 const refused = idle && fetcher.data && !fetcher.data.ok ? fetcher.data : null;
114 useEffect(() => {
115 if (refused?.code === "conflict") setRename(true);
116 }, [refused]);
117 const busy = !idle;
118 return (
119 <>
120 <fetcher.Form method="post" action={`/${slug}/-/agents?index`} className="flex flex-col items-end gap-1">
121 <input type="hidden" name="intent" value="restore" />
122 <input type="hidden" name="id" value={agent.id} />
123 <input type="hidden" name="handle" value={agent.handle} />
124 <Button type="submit" variant="outline" size="sm" disabled={busy}>
125 <ArchiveRestore size={14} />
126 {busy ? "Restoring…" : "Restore"}
127 </Button>
128 {refused && refused.code !== "conflict" && (
129 <p role="alert" className="text-xs text-danger">
130 {refused.error}
131 </p>
132 )}
133 </fetcher.Form>
134 <RenameDialog slug={slug} agent={agent} open={rename} onOpenChange={setRename} why={refused?.code === "conflict" ? refused.error : null} />
135 </>
136 );
137}
138
139/** The handle is taken by a live agent: restore under a new one. */
140function RenameDialog({ slug, agent, open, onOpenChange, why }: { slug: string; agent: ArchivedAgent; open: boolean; onOpenChange: (open: boolean) => void; why: string | null }) {
141 const fetcher = useFetcher<ActionResult>({ key: `restore-as-${agent.id}` });
142 const id = useId();
143 useEffect(() => {
144 if (fetcher.state === "idle" && fetcher.data?.ok) onOpenChange(false);
145 }, [fetcher.state, fetcher.data, onOpenChange]);
146 const error = fetcher.state === "idle" && fetcher.data && !fetcher.data.ok ? fetcher.data.error : null;
147 const busy = fetcher.state !== "idle";
148 return (
149 <Dialog open={open} onOpenChange={onOpenChange}>
150 <DialogContent>
151 <DialogHeader>
152 <DialogTitle>Restore {agent.display_name} under a new handle</DialogTitle>
153 <DialogDescription>{why ?? `A live agent is called @${agent.handle} now. Rename that one first, or restore this one under a new handle.`}</DialogDescription>
154 </DialogHeader>
155 <fetcher.Form method="post" action={`/${slug}/-/agents?index`} className="grid gap-5">
156 <input type="hidden" name="intent" value="restore" />
157 <input type="hidden" name="id" value={agent.id} />
158 <input type="hidden" name="handle" value={agent.handle} />
159 <Field>
160 <FieldLabel htmlFor={`${id}-handle`}>New handle</FieldLabel>
161 <InputGroup>
162 <InputAddon>@</InputAddon>
163 <Input id={`${id}-handle`} name="new_handle" defaultValue={`${agent.handle}-2`} required autoComplete="off" autoFocus />
164 </InputGroup>
165 <FieldDescription>2 to 32 lowercase letters, digits and hyphens. People mention it by this; its name and face stay.</FieldDescription>
166 </Field>
167 <FieldError>{error}</FieldError>
168 <DialogFooter>
169 <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
170 Cancel
171 </Button>
172 <Button type="submit" variant="accent" disabled={busy}>
173 {busy ? "Restoring…" : "Restore"}
174 </Button>
175 </DialogFooter>
176 </fetcher.Form>
177 </DialogContent>
178 </Dialog>
179 );
180}
181
182/**
183 * Delete permanently: asked once more, with what goes and what stays
184 * spelled out, and the handle typed back before the button works.
185 */
186function DeleteDialog({ slug, agent }: { slug: string; agent: ArchivedAgent }) {
187 const { fetcher, open, setOpen, error, busy } = useDialogFetcher(`destroy-${agent.id}`);
188 const [typed, setTyped] = useState("");
189 const id = useId();
190 const kept = countsLine(agent.counts);
191 return (
192 <AlertDialog
193 open={open}
194 onOpenChange={(next) => {
195 setOpen(next);
196 if (!next) setTyped("");
197 }}
198 >
199 <AlertDialogTrigger asChild>
200 <Button type="button" variant="destructive" size="sm">
201 <Trash2 size={14} />
202 Delete permanently
203 </Button>
204 </AlertDialogTrigger>
205 <AlertDialogContent>
206 <AlertDialogHeader>
207 <AlertDialogTitle>Delete {agent.display_name} permanently?</AlertDialogTitle>
208 <AlertDialogDescription asChild>
209 <div className="space-y-3">
210 <p>
211 This deletes <span className="font-medium text-fg">@{agent.handle}</span> for good: its definition and every version, {kept === "Nothing recorded" ? "what little it recorded" : kept.toLowerCase()}, its drafts and suggestions, its budget and its computer&apos;s disk. There is no undo.
212 </p>
213 <p>What stays: its messages in chat, shown under the name it had; what it spent, in the month&apos;s totals on the workspace&apos;s statement and on Usage; and the audit log.</p>
214 </div>
215 </AlertDialogDescription>
216 </AlertDialogHeader>
217 <fetcher.Form method="post" action={`/${slug}/-/agents?index`} className="grid gap-5">
218 <input type="hidden" name="intent" value="destroy" />
219 <input type="hidden" name="id" value={agent.id} />
220 <input type="hidden" name="handle" value={agent.handle} />
221 <Field>
222 <FieldLabel htmlFor={`${id}-confirm`}>
223 Type <span className="font-mono text-fg">@{agent.handle}</span> to confirm
224 </FieldLabel>
225 <Input id={`${id}-confirm`} name="confirm" value={typed} onChange={(event) => setTyped(event.target.value)} autoComplete="off" spellCheck={false} aria-describedby={`${id}-confirm-hint`} />
226 <FieldDescription id={`${id}-confirm-hint`}>Its handle, so nothing goes by accident.</FieldDescription>
227 </Field>
228 <FieldError>{error}</FieldError>
229 <AlertDialogFooter>
230 <AlertDialogCancel type="button">Cancel</AlertDialogCancel>
231 <Button type="submit" variant="destructive" disabled={busy || !confirmsHandle(typed, agent.handle)}>
232 {busy ? "Deleting…" : "Delete"}
233 </Button>
234 </AlertDialogFooter>
235 </fetcher.Form>
236 </AlertDialogContent>
237 </AlertDialog>
238 );
239}