Skip to content

Commit

Merge archiving an agent from its profile: owners only, asked twice, never g1t

syntaqxcommitted Parents8b6c7ea7bbaf9aBrowse files
1 file+56−20/1 viewed
+56−2
1−import { data, redirect, useOutletContext } from "react-router";
1+import { useState } from "react";
2+import { Form, data, redirect, useNavigation, useOutletContext } from "react-router";
23
34 import type { WorkspaceAgent } from "@g1t/contracts";
45
2223 const viewer = requireUser(context, request);
2324 const slug = params.owner.toLowerCase();
2425 if (!roleIn(viewer, slug)) throw data(null, { status: 404 });
25− const read = readAgentForm(await request.formData(), { orchestrator: params.handle.toLowerCase() === "g1t" });
26+ const form = await request.formData();
27+ // Archiving keeps the agent's history (runs, replies, spend) but takes it
28+ // out of chat and frees its handle. g1t itself can't be archived.
29+ if (form.get("intent") === "archive") {
30+ const archived = await workspaceAgents.archive(slug, params.handle.toLowerCase(), viewer).catch(() => null);
31+ if (!archived) return { errors: { form: "The agents service didn't answer. Try again in a moment." }, saved: false };
32+ if (!archived.ok) return { errors: { form: archived.error.message }, saved: false };
33+ throw redirect(`/${slug}/-/agents`);
34+ }
35+ const read = readAgentForm(form, { orchestrator: params.handle.toLowerCase() === "g1t" });
2636 if (!read.ok) return { errors: read.errors, saved: false };
2737 const saved = await workspaceAgents.update(slug, params.handle.toLowerCase(), viewer, read.input).catch(() => null);
2838 if (!saved) return { errors: { form: "The agents service didn't answer. Try again in a moment." }, saved: false };
5363 teams={loaderData.teams}
5464 seed={agent.avatar_seed || agent.id}
5565 />
66+ {!isOrchestrator(agent) && <Archive name={agent.display_name} />}
5667 </>
5768 );
5869 }
70+
71+/** Retiring an agent: asked twice, since it leaves every channel at once. */
72+function Archive({ name }: { name: string }) {
73+ const [asking, setAsking] = useState(false);
74+ const busy = useNavigation().formData?.get("intent") === "archive";
75+ return (
76+ <section className="mt-12 border-t border-line pt-8 md:grid md:grid-cols-[14rem_1fr] md:gap-10">
77+ <div className="mb-4 md:mb-0">
78+ <h2 className="text-sm font-semibold text-fg">Archive</h2>
79+ <p className="mt-1 text-sm text-muted">Retire this agent. Only owners can.</p>
80+ </div>
81+ <div>
82+ <p className="text-sm text-muted">
83+ {name} stops answering, leaves every channel and DM, and its handle is free to use again. Its runs, replies and spend stay in
84+ the audit log and on Usage.
85+ </p>
86+ {asking ? (
87+ <Form method="post" className="mt-4 flex flex-wrap items-center gap-3">
88+ <input type="hidden" name="intent" value="archive" />
89+ <button
90+ type="submit"
91+ disabled={busy}
92+ className="rounded-lg border border-danger/50 bg-danger/15 px-4 py-2 text-sm font-medium text-danger hover:bg-danger/25 disabled:opacity-60"
93+ >
94+ {busy ? "Archiving…" : `Archive ${name}`}
95+ </button>
96+ <button type="button" onClick={() => setAsking(false)} className="px-2 py-2 text-sm text-muted hover:text-fg">
97+ Keep {name}
98+ </button>
99+ </Form>
100+ ) : (
101+ <button
102+ type="button"
103+ onClick={() => setAsking(true)}
104+ className="mt-4 rounded-lg border border-line px-4 py-2 text-sm text-muted hover:border-danger/50 hover:text-danger"
105+ >
106+ Archive agent…
107+ </button>
108+ )}
109+ </div>
110+ </section>
111+ );
112+}