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

Agents and memory, checks and conflicts, profiles, slug renames, custom domains1/**
2 * Memory, as a project's and a workspace's pages show it: what agents and
3 * people learned, each with where it came from, to pin, edit or forget.
4 * Both pages post to their own action with the intents in `memoryAction`.
5 */
6import { Bot, Brain, Pencil, Pin, PinOff, Trash2, User } from "lucide-react";
7import { useEffect, useRef, useState } from "react";
8import { Link, useFetcher } from "react-router";
9
10import { MEMORY_KINDS, type Memory, type MemoryKind, type MemoryScope, type Result, type User as Actor } from "@g1t/contracts";
11
Merge branch 'worktree-agent-a985c01f43538af01'12import { SubmitButton, TimeAgo } from "./ui";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains13import {
14 AlertDialog,
15 AlertDialogAction,
16 AlertDialogCancel,
17 AlertDialogContent,
18 AlertDialogDescription,
19 AlertDialogFooter,
20 AlertDialogHeader,
21 AlertDialogTitle,
22 AlertDialogTrigger,
23} from "./ui/alert-dialog";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.24import { Button } from "./ui/button";
25import { Card } from "./ui/card";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains26import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
27import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
Merge branch 'worktree-agent-ae1299e92e4462012'28import { Hint } from "./ui/hint";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains29import { Switch } from "./ui/switch";
30
31const KIND: Record<MemoryKind, { label: string; about: string; tone: string }> = {
32 fact: { label: "Fact", about: "How something is", tone: "text-info border-info/40" },
33 convention: { label: "Convention", about: "How things are done here", tone: "text-accent border-accent/40" },
34 decision: { label: "Decision", about: "Something chosen, and why", tone: "text-merged border-merged/40" },
35 gotcha: { label: "Gotcha", about: "A trap to avoid", tone: "text-warn border-warn/40" },
36};
37
38const TEXTAREA =
39 "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";
40
41type Done = { ok: boolean; error?: string } | undefined;
42
43function KindBadge({ kind }: { kind: MemoryKind }) {
44 return (
45 <span className={`inline-flex shrink-0 items-center rounded-full border px-2 py-px text-[0.6875rem] ${KIND[kind].tone}`}>
46 {KIND[kind].label}
47 </span>
48 );
49}
50
51function KindSelect({ value }: { value?: MemoryKind }) {
52 return (
53 <Select name="kind" defaultValue={value ?? "fact"}>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit54 <SelectTrigger size="sm" className="w-40" aria-label="Kind of memory">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains55 <SelectValue />
56 </SelectTrigger>
57 <SelectContent>
58 {MEMORY_KINDS.map((kind) => (
59 <SelectItem key={kind} value={kind} description={KIND[kind].about}>
60 {KIND[kind].label}
61 </SelectItem>
62 ))}
63 </SelectContent>
64 </Select>
65 );
66}
67
68/** Where a memory came from, linked where it can be. */
69function Source({ memory }: { memory: Memory }) {
70 const { source } = memory;
71 const repo = source.repo;
72 const where =
73 repo && source.number != null ? (
74 <>
75 {" "}
76 on{" "}
77 <Link to={`/${repo.namespace}/${repo.name}/pull/${source.number}`} className="hover:text-fg">
78 {memory.scope === "workspace" && `${repo.name}`}#{source.number}
79 </Link>
80 </>
81 ) : repo && memory.scope === "workspace" ? (
82 <>
83 {" "}
84 in <Link to={`/${repo.namespace}/${repo.name}`} className="hover:text-fg">{repo.name}</Link>
85 </>
86 ) : null;
87 if (source.kind === "person") {
88 return (
89 <span className="inline-flex items-center gap-1">
90 <User size={11} />
91 {memory.createdBy}
92 {where}
93 </span>
94 );
95 }
96 return (
97 <span className="inline-flex items-center gap-1">
98 <Bot size={11} />
99 {source.runId && repo ? (
100 <Link to={`/${repo.namespace}/${repo.name}/agents/runs/${source.runId}`} className="hover:text-fg">
101 {memory.createdBy}'s run
102 </Link>
103 ) : (
104 memory.createdBy
105 )}
106 {where}
107 </span>
108 );
109}
110
111function EditMemory({ memory, action }: { memory: Memory; action: string }) {
112 const fetcher = useFetcher<Done>();
113 const [open, setOpen] = useState(false);
114 useEffect(() => {
115 if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
116 }, [fetcher.state, fetcher.data]);
117 return (
118 <Dialog open={open} onOpenChange={setOpen}>
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 bar119 <DialogTrigger aria-label="Edit" className="rounded p-1 text-faint max-sm:p-2.5 transition-colors hover:bg-raised hover:text-fg">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains120 <Pencil size={13} />
121 </DialogTrigger>
122 <DialogContent>
123 <DialogHeader>
124 <DialogTitle>Edit memory</DialogTitle>
125 <DialogDescription>Agents get the new wording from their next run on.</DialogDescription>
126 </DialogHeader>
127 <fetcher.Form method="post" action={action} className="space-y-3">
128 <input type="hidden" name="intent" value="update" />
129 <input type="hidden" name="id" value={memory.id} />
130 <textarea name="text" required rows={4} maxLength={1000} defaultValue={memory.text} className={TEXTAREA} />
131 <div className="flex items-center justify-between gap-3">
132 <KindSelect value={memory.kind} />
Merge branch 'worktree-agent-a985c01f43538af01'133 <SubmitButton fetcher={fetcher} pending="Saving…">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains134 Save
Merge branch 'worktree-agent-a985c01f43538af01'135 </SubmitButton>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains136 </div>
137 {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
138 </fetcher.Form>
139 </DialogContent>
140 </Dialog>
141 );
142}
143
144function MemoryItem({ memory, action, editable }: { memory: Memory; action: string; editable: boolean }) {
145 const fetcher = useFetcher<Done>();
146 const pinned = fetcher.formData ? fetcher.formData.get("pinned") === "true" : memory.pinned;
147 const gone = fetcher.formData?.get("intent") === "delete";
148 if (gone) return null;
149 return (
150 <li className="group flex gap-3 px-4 py-3">
151 <div className="min-w-0 grow">
152 <div className="flex flex-wrap items-center gap-2">
153 <KindBadge kind={memory.kind} />
154 {pinned && (
155 <span className="inline-flex items-center gap-1 text-[0.6875rem] text-accent">
156 <Pin size={11} />
157 pinned
158 </span>
159 )}
160 </div>
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 bar161 <p className="mt-1.5 text-sm whitespace-pre-wrap wrap-break-word">{memory.text}</p>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains162 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-faint">
163 <Source memory={memory} />
164 <span>
165 added <TimeAgo at={memory.createdAt} />
166 </span>
167 <span>{memory.lastUsedAt ? <>given to an agent <TimeAgo at={memory.lastUsedAt} /></> : "not given to an agent yet"}</span>
168 </p>
169 {fetcher.data?.error && <p className="mt-1.5 text-xs text-danger">{fetcher.data.error}</p>}
170 </div>
171 {editable && (
172 <div className="flex shrink-0 items-start gap-0.5">
Merge branch 'worktree-agent-ae1299e92e4462012'173 <Hint label={pinned ? "Unpin" : "Pin: given to every agent first"}>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.174 <Button
Merge branch 'worktree-agent-ae1299e92e4462012'175 type="button"
176 aria-label={pinned ? "Unpin" : "Pin"}
177 onClick={() => fetcher.submit({ intent: "update", id: memory.id, pinned: String(!pinned) }, { method: "post", action })}
178 disabled={fetcher.state !== "idle"}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.179 variant="ghost"
180 size="inline"
181 className="rounded p-1 text-faint max-sm:p-2.5"
Merge branch 'worktree-agent-ae1299e92e4462012'182 >
183 {pinned ? <PinOff size={13} /> : <Pin size={13} />}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.184 </Button>
Merge branch 'worktree-agent-ae1299e92e4462012'185 </Hint>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains186 <EditMemory memory={memory} action={action} />
187 <AlertDialog>
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 bar188 <AlertDialogTrigger aria-label="Forget" className="rounded p-1 text-faint max-sm:p-2.5 transition-colors hover:bg-raised hover:text-danger">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains189 <Trash2 size={13} />
190 </AlertDialogTrigger>
191 <AlertDialogContent>
192 <AlertDialogHeader>
193 <AlertDialogTitle>Forget this?</AlertDialogTitle>
194 <AlertDialogDescription>
195 No agent is given it again. This cannot be undone.
196 </AlertDialogDescription>
197 </AlertDialogHeader>
198 <AlertDialogFooter>
199 <AlertDialogCancel>Keep it</AlertDialogCancel>
200 <AlertDialogAction onClick={() => fetcher.submit({ intent: "delete", id: memory.id }, { method: "post", action })}>
201 Forget
202 </AlertDialogAction>
203 </AlertDialogFooter>
204 </AlertDialogContent>
205 </AlertDialog>
206 </div>
207 )}
208 </li>
209 );
210}
211
212export function MemoryList({
213 memories,
214 action,
215 editable,
216 empty,
217}: {
218 memories: Memory[];
219 action: string;
220 editable: boolean;
221 empty: string;
222}) {
223 if (memories.length === 0) {
224 return <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">{empty}</p>;
225 }
226 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.227 <Card asChild divided>
228 <ul>
229 {memories.map((memory) => (
230 <MemoryItem key={memory.id} memory={memory} action={action} editable={editable} />
231 ))}
232 </ul>
233 </Card>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains234 );
235}
236
237/** A form to add a memory at one level. */
238export function AddMemory({ scope, action, placeholder }: { scope: MemoryScope; action: string; placeholder: string }) {
239 const fetcher = useFetcher<Done>();
240 const form = useRef<HTMLFormElement>(null);
241 useEffect(() => {
242 if (fetcher.state === "idle" && fetcher.data?.ok) form.current?.reset();
243 }, [fetcher.state, fetcher.data]);
244 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.245 <Card asChild className="p-4">
246 <fetcher.Form ref={form} method="post" action={action}>
247 <input type="hidden" name="intent" value="add" />
248 <input type="hidden" name="scope" value={scope} />
249 <label className="flex items-center gap-2 text-sm font-medium">
250 <Brain size={15} className="text-accent" />
251 Add to {scope === "workspace" ? "the workspace's" : "this project's"} memory
Agents and memory, checks and conflicts, profiles, slug renames, custom domains252 </label>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.253 <textarea name="text" required rows={2} maxLength={1000} placeholder={placeholder} className={`${TEXTAREA} mt-3`} />
254 <div className="mt-3 flex flex-wrap items-center gap-3">
255 <KindSelect />
256 <label className="flex items-center gap-2 text-xs text-muted">
257 <Switch name="pinned" value="true" size="sm" />
258 Pin it
259 </label>
260 <span className="grow" />
261 <SubmitButton fetcher={fetcher} pending="Adding…">
262 Add
263 </SubmitButton>
264 </div>
265 {fetcher.data?.error && <p className="mt-2 text-sm text-danger">{fetcher.data.error}</p>}
266 <p className="mt-2 text-xs text-faint">
267 One fact, convention, decision or gotcha each. Never a secret: say where it lives instead, and
268 g1t refuses anything that looks like a key or a token.
269 </p>
270 </fetcher.Form>
271 </Card>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains272 );
273}
274
275type MemoryApi = {
276 addMemory(actor: Actor, workspace: string, memory: { scope: MemoryScope; repo?: { namespace: string; name: string } | null; text: string; kind?: MemoryKind; pinned?: boolean }): Promise<Result<Memory>>;
277 updateMemory(actor: Actor, workspace: string, id: string, change: { text?: string; kind?: MemoryKind; pinned?: boolean }): Promise<Result<Memory>>;
278 deleteMemory(actor: Actor, workspace: string, id: string): Promise<Result<boolean>>;
279};
280
281/** The action both memory pages share: add, update and delete. */
282export async function memoryAction(
283 api: MemoryApi,
284 actor: Actor,
285 workspace: string,
286 repo: { namespace: string; name: string } | null,
287 form: FormData,
288): Promise<Done> {
289 const intent = String(form.get("intent") ?? "");
290 const kindValue = String(form.get("kind") ?? "");
291 const kind = MEMORY_KINDS.includes(kindValue as MemoryKind) ? (kindValue as MemoryKind) : undefined;
292 if (intent === "add") {
293 const scope: MemoryScope = form.get("scope") === "workspace" ? "workspace" : "project";
294 const added = await api.addMemory(actor, workspace, {
295 scope,
296 repo,
297 text: String(form.get("text") ?? ""),
298 kind,
299 pinned: form.get("pinned") === "true",
300 });
301 return added.ok ? { ok: true } : { ok: false, error: added.error.message };
302 }
303 if (intent === "update") {
304 const pinned = form.get("pinned");
305 const text = form.get("text");
306 const changed = await api.updateMemory(actor, workspace, String(form.get("id") ?? ""), {
307 text: text == null ? undefined : String(text),
308 kind,
309 pinned: pinned == null ? undefined : pinned === "true",
310 });
311 return changed.ok ? { ok: true } : { ok: false, error: changed.error.message };
312 }
313 if (intent === "delete") {
314 const gone = await api.deleteMemory(actor, workspace, String(form.get("id") ?? ""));
315 return gone.ok ? { ok: true } : { ok: false, error: gone.error.message };
316 }
317 return { ok: false, error: "Unknown action." };
318}

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