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.

375 linesCodeBlameRaw
1/**
2 * Routines: standing work for @g1t. Someone says what to do, when, and
3 * where the result lands, and it runs by itself after that, whether or not
4 * anyone is at a keyboard. A routine is not only about code: a morning
5 * brief, a digest posted to a channel, a watch on production or a topic,
6 * a reminder, as much as triaging work items or fixing CI on main.
7 *
8 * Not on yet. The page is here so Routines has its place in the rail and
9 * its shape is settled: every control is drawn and none can be used (the
10 * composer is a disabled <fieldset>, the lists are empty, the starting
11 * points cannot be picked). Nothing is read or saved.
12 */
13import {
14 AtSign,
15 Bell,
16 CalendarClock,
17 FileText,
18 GitPullRequest,
19 Hash,
20 Lightbulb,
21 type LucideIcon,
22 MousePointerClick,
23 Plus,
24 Repeat,
25 Tag,
26 Users,
27 Zap,
28} from "lucide-react";
29import { data } from "react-router";
30
31import type { Route } from "./+types/routines";
32import { EmptyState, SoonPill } from "../../components/ui";
33import { Alert } from "../../components/ui/alert";
34import { Badge } from "../../components/ui/badge";
35import { Button } from "../../components/ui/button";
36import { Card } from "../../components/ui/card";
37import { Field, FieldDescription, FieldLabel } from "../../components/ui/field";
38import { Input } from "../../components/ui/input";
39import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
40import { Textarea } from "../../components/ui/textarea";
41import { ToggleGroup, ToggleGroupItem } from "../../components/ui/toggle-group";
42import { page } from "../../lib/meta";
43import { getViewer } from "../../lib/session.server";
44
45export function meta({ params, ...args }: Route.MetaArgs) {
46 return page(args, { title: `Routines · ${params.owner} · g1t` });
47}
48
49export async function loader({ params, context }: Route.LoaderArgs) {
50 const viewer = getViewer(context);
51 const slug = params.owner.toLowerCase();
52 const membership = viewer?.workspaces?.find((m) => m.slug === slug);
53 if (!viewer || !membership) throw data(null, { status: 404 });
54 return { slug, name: membership.name?.trim() || slug };
55}
56
57/** A starting point: what it asks, when it runs, and where what it does lands. */
58type Starter = { title: string; prompt: string; when: { icon: LucideIcon; text: string }; lands: { icon: LucideIcon; text: string } };
59
60const SCHEDULE = CalendarClock;
61const EVENT = Zap;
62
63/** Starting points, by what they are for: code is one kind among several. */
64const STARTERS: { title: string; about: string; items: Starter[] }[] = [
65 {
66 title: "Keep up",
67 about: "Yours alone: what needs you, gathered for you.",
68 items: [
69 {
70 title: "Morning brief",
71 prompt: "Tell me what shipped overnight, what is waiting on me, and what I said I would do today.",
72 when: { icon: SCHEDULE, text: "Weekdays at 8:30" },
73 lands: { icon: AtSign, text: "Your DM with @g1t" },
74 },
75 {
76 title: "Follow up for me",
77 prompt: "When a pull request I am reviewing has waited two days, nudge me. After four, tell its author I am behind.",
78 when: { icon: EVENT, text: "When a review waits" },
79 lands: { icon: Bell, text: "Your notifications" },
80 },
81 {
82 title: "Remind me, with a draft",
83 prompt: "Remind me to write the week's notes, with a first draft from what I did this week.",
84 when: { icon: SCHEDULE, text: "Once, Friday at 16:00" },
85 lands: { icon: AtSign, text: "Your DM with @g1t" },
86 },
87 ],
88 },
89 {
90 title: "Find work",
91 about: "Proposals, not changes: each work item waits for your yes, then someone (or @g1t) picks it up.",
92 items: [
93 {
94 title: "What people keep asking for",
95 prompt: "Read the week's #support and #feedback, and propose a work item for each thing that keeps coming up, with who asked.",
96 when: { icon: SCHEDULE, text: "Fridays at 14:00" },
97 lands: { icon: Lightbulb, text: "Work items to approve" },
98 },
99 {
100 title: "Worth fixing",
101 prompt: "Look through TODOs, flaky tests, slow pages and errors in production, and propose the few worth doing, each with how big it is.",
102 when: { icon: SCHEDULE, text: "Mondays at 7:00" },
103 lands: { icon: Lightbulb, text: "Work items to approve" },
104 },
105 {
106 title: "Next steps",
107 prompt: "From our plan and what shipped since, propose the next three work items that move it forward, and say why these first.",
108 when: { icon: EVENT, text: "When a milestone closes" },
109 lands: { icon: Lightbulb, text: "Work items to approve" },
110 },
111 ],
112 },
113 {
114 title: "Team",
115 about: "The workspace's: kept by a team, so they outlive anyone leaving.",
116 items: [
117 {
118 title: "What changed",
119 prompt: "Post what shipped yesterday, what is in review, and what is stuck, with links.",
120 when: { icon: SCHEDULE, text: "Weekdays at 9:00" },
121 lands: { icon: Hash, text: "#general" },
122 },
123 {
124 title: "The week's decisions",
125 prompt: "Collect the decisions made this week in sessions, pull requests and chat into one page, newest first.",
126 when: { icon: SCHEDULE, text: "Fridays at 15:00" },
127 lands: { icon: FileText, text: "An artifact" },
128 },
129 {
130 title: "Thread digest",
131 prompt: "When a thread passes 20 messages, sum up where it landed and what is still open.",
132 when: { icon: EVENT, text: "When a thread grows" },
133 lands: { icon: Hash, text: "A reply in the thread" },
134 },
135 ],
136 },
137 {
138 title: "Code",
139 about: "On a repository: what they change goes through review and checks like anyone's.",
140 items: [
141 {
142 title: "Triage new work items",
143 prompt: "Label each new work item, find what it duplicates, and ask the reporter for what is missing.",
144 when: { icon: EVENT, text: "When a work item opens" },
145 lands: { icon: Tag, text: "Labels and a comment" },
146 },
147 {
148 title: "Fix CI on main",
149 prompt: "Find why the run on main failed and open a pull request that fixes it.",
150 when: { icon: EVENT, text: "When CI fails on main" },
151 lands: { icon: GitPullRequest, text: "A draft pull request" },
152 },
153 {
154 title: "Dependency pass",
155 prompt: "Update what is out of date, read the changelogs, and fix what the updates break.",
156 when: { icon: SCHEDULE, text: "Mondays at 6:00" },
157 lands: { icon: GitPullRequest, text: "A draft pull request" },
158 },
159 {
160 title: "Docs in step",
161 prompt: "When a pull request merges, check the docs about what it changed still say what is true.",
162 when: { icon: EVENT, text: "When a pull request merges" },
163 lands: { icon: GitPullRequest, text: "A draft pull request" },
164 },
165 ],
166 },
167 {
168 title: "Watch",
169 about: "Keeping an eye on something, and saying only when it matters.",
170 items: [
171 {
172 title: "Errors in production",
173 prompt: "When errors jump, find the requests behind them and the change that started it.",
174 when: { icon: EVENT, text: "When errors spike" },
175 lands: { icon: Hash, text: "#incidents" },
176 },
177 {
178 title: "Spend",
179 prompt: "When spend passes 80% of the month's budget, say what is driving it and what could stop.",
180 when: { icon: EVENT, text: "At 80% of budget" },
181 lands: { icon: Users, text: "Workspace owners" },
182 },
183 {
184 title: "A topic",
185 prompt: "Read what is new on a topic through our connectors and keep a running brief, changes at the top.",
186 when: { icon: SCHEDULE, text: "Daily at 7:00" },
187 lands: { icon: FileText, text: "An artifact" },
188 },
189 ],
190 },
191];
192
193/** How a routine works, in three parts. */
194const PARTS = [
195 {
196 icon: FileText,
197 title: "Say what",
198 body: "In plain words, as you would ask a person. About code, or about anything @g1t can reach: chat, artifacts, work items, production, and what your connectors let it read.",
199 },
200 {
201 icon: Repeat,
202 title: "Say when",
203 body: "On a schedule or once; when something happens (a work item opens, CI fails, a thread grows, errors spike); or by hand and through the API.",
204 },
205 {
206 icon: MousePointerClick,
207 title: "Say where it lands",
208 body: "A message, an artifact, a comment, a draft pull request, or work items it proposes for you to approve. What it writes goes through the same review and checks as anyone's.",
209 },
210];
211
212export default function Routines({ loaderData }: Route.ComponentProps) {
213 const { name } = loaderData;
214 return (
215 <main className="mx-auto max-w-5xl px-4 py-8 sm:px-8 sm:py-10">
216 <div className="flex flex-wrap items-end justify-between gap-4">
217 <div>
218 <div className="flex items-center gap-2">
219 <h1 className="text-2xl font-semibold tracking-tight">Routines</h1>
220 <SoonPill />
221 </div>
222 <p className="mt-1 max-w-xl text-sm text-muted">
223 Standing work for @g1t. Say what to do, when, and where it lands, and it gets done whether or not anyone is at a keyboard.
224 </p>
225 </div>
226 <Button disabled>
227 <Plus size={15} />
228 New routine
229 </Button>
230 </div>
231
232 <Alert tone="info" className="mt-6" id="routines-soon">
233 Routines are not on yet. This is what they will be: nothing here can be turned on, and nothing you type is kept.
234 </Alert>
235
236 <Card asChild className="mt-6 p-4 sm:p-5">
237 <section aria-labelledby="compose">
238 <h2 id="compose" className="text-sm font-semibold">
239 New routine
240 </h2>
241 <fieldset disabled aria-describedby="routines-soon" className="mt-4 flex min-w-0 flex-col gap-5">
242 <Field>
243 <FieldLabel htmlFor="routine-prompt">What should it do?</FieldLabel>
244 <Textarea id="routine-prompt" placeholder="Every weekday at 9, post what shipped yesterday and what is waiting on review to #general." />
245 <FieldDescription>Each run starts fresh, so say everything it needs to know.</FieldDescription>
246 </Field>
247 <div className="grid gap-5 md:grid-cols-2">
248 <Field>
249 <FieldLabel>When</FieldLabel>
250 <ToggleGroup className="self-start" type="single" variant="outline" size="sm" defaultValue="schedule" disabled aria-label="When it runs">
251 <ToggleGroupItem value="schedule">On a schedule</ToggleGroupItem>
252 <ToggleGroupItem value="once">Once</ToggleGroupItem>
253 <ToggleGroupItem value="event">When something happens</ToggleGroupItem>
254 <ToggleGroupItem value="manual">By hand</ToggleGroupItem>
255 </ToggleGroup>
256 <Input aria-label="Schedule" placeholder="Every weekday at 9:00" />
257 </Field>
258 <Field>
259 <FieldLabel>Where it lands</FieldLabel>
260 <ToggleGroup className="self-start" type="single" variant="outline" size="sm" defaultValue="dm" disabled aria-label="Where it lands">
261 <ToggleGroupItem value="dm">Your DM</ToggleGroupItem>
262 <ToggleGroupItem value="channel">A channel</ToggleGroupItem>
263 <ToggleGroupItem value="artifact">An artifact</ToggleGroupItem>
264 <ToggleGroupItem value="issues">Work items to approve</ToggleGroupItem>
265 <ToggleGroupItem value="pull">A pull request</ToggleGroupItem>
266 </ToggleGroup>
267 <FieldDescription>It writes nowhere else: anything bigger waits for a person to say yes.</FieldDescription>
268 </Field>
269 </div>
270 <Field>
271 <FieldLabel>Whose it is</FieldLabel>
272 <ToggleGroup className="self-start" type="single" variant="outline" size="sm" defaultValue="mine" disabled aria-label="Whose it is">
273 <ToggleGroupItem value="mine">Yours</ToggleGroupItem>
274 <ToggleGroupItem value="workspace">{name}'s</ToggleGroupItem>
275 </ToggleGroup>
276 <FieldDescription>Yours runs as you, with what you can reach. {name}'s is kept by a team and seen by everyone in it.</FieldDescription>
277 </Field>
278 <div className="flex flex-wrap items-center justify-end gap-2 border-t border-line pt-4">
279 <Button variant="outline">Try it once</Button>
280 <Button>Create routine</Button>
281 </div>
282 </fieldset>
283 </section>
284 </Card>
285
286 <section aria-labelledby="listed" className="mt-10">
287 <h2 id="listed" className="sr-only">
288 Routines and their runs
289 </h2>
290 <Tabs defaultValue="yours">
291 <TabsList aria-label="Which routines">
292 <TabsTrigger value="yours">Yours</TabsTrigger>
293 <TabsTrigger value="workspace" disabled>
294 {name}'s
295 </TabsTrigger>
296 <TabsTrigger value="runs" disabled>
297 Runs
298 </TabsTrigger>
299 </TabsList>
300 <TabsContent value="yours">
301 <EmptyState title="No routines yet">
302 Each one will show here with when it last ran, what it did, and what it cost. Every run is a session you can open and read.
303 </EmptyState>
304 </TabsContent>
305 </Tabs>
306 </section>
307
308 <section aria-labelledby="starters" className="mt-10">
309 <h2 id="starters" className="text-sm font-semibold">
310 Start from one of these
311 </h2>
312 <p className="mt-1 text-sm text-muted">Code is one kind of routine among several.</p>
313 <div className="mt-4 flex flex-col gap-8">
314 {STARTERS.map((group) => (
315 <div key={group.title}>
316 <div className="flex flex-wrap items-baseline gap-x-3 gap-y-0.5">
317 <h3 className="text-xs font-semibold tracking-wide text-muted uppercase">{group.title}</h3>
318 <p className="text-xs text-faint">{group.about}</p>
319 </div>
320 <div className="mt-3 grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-2">
321 {group.items.map((starter) => (
322 <StarterCard key={starter.title} starter={starter} />
323 ))}
324 </div>
325 </div>
326 ))}
327 </div>
328 </section>
329
330 <section aria-labelledby="how" className="mt-12">
331 <h2 id="how" className="text-sm font-semibold">
332 How a routine works
333 </h2>
334 <div className="mt-3 grid gap-2 md:grid-cols-3">
335 {PARTS.map(({ icon: Icon, title, body }) => (
336 <Card key={title} tone="plain" radius="lg" className="p-4">
337 <Icon size={16} className="text-accent" aria-hidden="true" />
338 <h3 className="mt-2 text-sm font-medium">{title}</h3>
339 <p className="mt-1 text-sm text-muted">{body}</p>
340 </Card>
341 ))}
342 </div>
343 <p className="mt-4 max-w-3xl text-sm text-muted">
344 Every run is a session: titled, recorded and on {name}'s bill, so you can see what each routine did, when, and why. It thinks with your workspace's
345 AI, or runs as your own agent on a runner, in a sandbox. Each has a budget per run and per day, and can be paused at any time.
346 </p>
347 </section>
348 </main>
349 );
350}
351
352/** One starting point: its name, what it asks, when, and where it lands. Picking it comes with Routines. */
353function StarterCard({ starter }: { starter: Starter }) {
354 const When = starter.when.icon;
355 const Lands = starter.lands.icon;
356 return (
357 <Card radius="lg" className="flex flex-col p-4" aria-disabled="true">
358 <h4 className="text-sm font-medium">{starter.title}</h4>
359 <p className="mt-1 grow text-sm text-muted">{starter.prompt}</p>
360 <div className="mt-3 flex flex-wrap gap-1.5">
361 <Badge>
362 <When size={11} aria-hidden="true" />
363 {starter.when.text}
364 </Badge>
365 <Badge>
366 <Lands size={11} aria-hidden="true" />
367 {starter.lands.text}
368 </Badge>
369 </div>
370 <Button variant="outline" size="xs" className="mt-3 self-start" disabled>
371 Use this
372 </Button>
373 </Card>
374 );
375}