flagon-io/g1t

public

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/apps/web/app/routes/workspace/context.tsx

148 lines7,323 bytesCodeBlame

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 get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1import { env } from "cloudflare:workers";
2import { Brain, RefreshCw } from "lucide-react";
3import { Link, data, useFetcher, useNavigate } from "react-router";
4
5import { ENTITY_KINDS, type EntityKind } from "@g1t/contracts";
6
7import type { Route } from "./+types/context";
8import { page } from "../../lib/meta";
9import { CatalogView, ReviewQueue, ScorecardsView, SearchView, fixRule, reviewAction } from "../../components/context";
10import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
11import { context as hub, memoryReview, work } from "../../lib/services.server";
12import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
13
14const TABS = ["catalog", "memory", "search", "scorecards"] as const;
15type Tab = (typeof TABS)[number];
16
17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Context · ${params.owner} · g1t` });
19}
20
21export async function loader({ params, context, request }: Route.LoaderArgs) {
22 const viewer = getViewer(context);
23 // What the workspace knows can be internal: members only.
24 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
25 const url = new URL(request.url);
26 const tab: Tab = (TABS as readonly string[]).includes(url.searchParams.get("tab") ?? "") ? (url.searchParams.get("tab") as Tab) : "catalog";
27 const kindParam = url.searchParams.get("kind");
28 const kind = ENTITY_KINDS.includes(kindParam as EntityKind) ? (kindParam as EntityKind) : null;
29 const project = url.searchParams.get("project");
30 const query = url.searchParams.get("q") ?? "";
31 // The first look at a workspace's context starts building it.
32 const status = unwrap(await hub.status(params.owner, viewer));
33 const [catalog, candidates, search, cards] = await Promise.all([
34 tab === "catalog" ? hub.catalog(params.owner, viewer).then(unwrap) : null,
35 tab === "memory" ? memoryReview.listCandidates(viewer, params.owner).then(unwrap) : null,
36 tab === "search" && query.trim() ? hub.search(params.owner, viewer, query).then(unwrap) : null,
37 tab === "scorecards" ? hub.scorecards(params.owner, viewer).then(unwrap) : null,
38 ]);
39 return { tab, kind, project, query, status, catalog, candidates, search, cards };
40}
41
42export async function action({ params, context, request }: Route.ActionArgs) {
43 assertSameOrigin(request);
44 const user = requireUser(context, request);
45 const form = await request.formData();
46 const reviewed = await reviewAction(memoryReview, user, params.owner, form);
47 if (reviewed) return reviewed;
48 const intent = String(form.get("intent") ?? "");
49 if (intent === "backfill") {
50 const started = await hub.backfill(user, params.owner);
51 return started.ok ? { ok: true, message: "Rebuilding the catalog and the search index." } : { ok: false, error: started.error.message };
52 }
53 if (intent === "fix") {
54 return fixRule(
55 {
56 scorecards: () => hub.scorecards(params.owner, user),
57 openIssue: (repo, input) => work.openIssue(user, repo, input),
58 // The issue exists either way; its page shows how to put an agent on it if none could start.
59 assign: (repo, number) => env.RUNNER.run(user, repo, number).catch(() => null),
60 },
61 String(form.get("project") ?? ""),
62 String(form.get("rule") ?? ""),
63 );
64 }
65 return { ok: false, error: "Unknown action." };
66}
67
68function Rebuild({ action, running }: { action: string; running: boolean }) {
69 const fetcher = useFetcher<{ ok: boolean; error?: string; message?: string }>();
70 return (
71 <fetcher.Form method="post" action={action} className="flex items-center gap-2">
72 <input type="hidden" name="intent" value="backfill" />
73 <button
74 type="submit"
75 disabled={running || fetcher.state !== "idle"}
76 className="inline-flex items-center gap-1.5 rounded-md border border-line px-2.5 py-1.5 text-xs text-muted hover:border-line-strong hover:text-fg disabled:opacity-50"
77 >
78 <RefreshCw size={12} className={running ? "animate-spin" : ""} />
79 {running ? "Building…" : "Rebuild"}
80 </button>
81 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
82 </fetcher.Form>
83 );
84}
85
86export default function WorkspaceContext({ loaderData, params }: Route.ComponentProps) {
87 const { tab, kind, project, query, status, catalog, candidates, search, cards } = loaderData;
88 const base = `/${params.owner}/-/context`;
89 const navigate = useNavigate();
90 const backfill = status.backfill;
91 const running = backfill?.status === "running";
92 const total = Object.values(status.counts).reduce((sum, n) => sum + (n ?? 0), 0);
93 return (
94 <div className="space-y-6">
95 <div className="flex flex-wrap items-center justify-between gap-3 text-xs text-muted">
96 <p>
97 {running
98 ? `Building from ${backfill.projects} project${backfill.projects === 1 ? "" : "s"}: ${backfill.done} done.`
99 : `${total} entries in the catalog${backfill?.finishedAt ? `; ${backfill.candidates} memory candidates and ${backfill.kept} kept at the last rebuild` : ""}.`}
100 {!status.semantic && " Search matches words only here."}
101 </p>
102 <Rebuild action={base} running={running} />
103 </div>
104
105 <Tabs value={tab} onValueChange={(value) => navigate(`${base}?tab=${value}`)}>
106 <TabsList>
107 <TabsTrigger value="catalog">Catalog</TabsTrigger>
108 <TabsTrigger value="memory">Memory{candidates && candidates.length ? ` (${candidates.length})` : ""}</TabsTrigger>
109 <TabsTrigger value="search">Search</TabsTrigger>
110 <TabsTrigger value="scorecards">Scorecards</TabsTrigger>
111 </TabsList>
112 </Tabs>
113
114 {tab === "catalog" && catalog && <CatalogView catalog={catalog} kind={kind} project={project} base={base} />}
115
116 {tab === "memory" && candidates && (
117 <div className="space-y-6">
118 <div className="rounded-xl border border-line bg-surface p-4 text-sm">
119 <p className="flex items-center gap-2 font-medium">
120 <Brain size={15} className="text-merged" />
121 Memory fills itself
122 </p>
123 <p className="mt-1 text-muted">
124 Agents say what they learned at the end of every run; corrections in reviews, merged pull requests and each
125 project's docs add more. What two independent sources say, or a project's AGENTS.md and manifests state, is
126 kept at once. The rest waits here: keep it, edit it, or dismiss it so it is never suggested again.
127 </p>
128 </div>
129 <section>
130 <div className="flex items-baseline justify-between">
131 <h2 className="text-sm font-medium">Review queue</h2>
132 <Link to={`/${params.owner}/-/memory`} className="text-xs text-muted hover:text-fg">
133 Kept workspace memory
134 </Link>
135 </div>
136 <div className="mt-3">
137 <ReviewQueue candidates={candidates} action={base} empty="Nothing waiting for review." />
138 </div>
139 </section>
140 </div>
141 )}
142
143 {tab === "search" && <SearchView result={search} query={query} base={base} />}
144
145 {tab === "scorecards" && cards && <ScorecardsView cards={cards} action={base} />}
146 </div>
147 );
148}