g1t/apps/web/app/routes/workspace/context.tsx
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 API | 1 | import { env } from "cloudflare:workers"; |
| 2 | import { Brain, RefreshCw } from "lucide-react"; | |
| 3 | import { Link, data, useFetcher, useNavigate } from "react-router"; | |
| 4 | ||
| 5 | import { ENTITY_KINDS, type EntityKind } from "@g1t/contracts"; | |
| 6 | ||
| 7 | import type { Route } from "./+types/context"; | |
| 8 | import { page } from "../../lib/meta"; | |
| 9 | import { CatalogView, ReviewQueue, ScorecardsView, SearchView, fixRule, reviewAction } from "../../components/context"; | |
| 10 | import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs"; | |
| 11 | import { context as hub, memoryReview, work } from "../../lib/services.server"; | |
| 12 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; | |
| 13 | ||
| 14 | const TABS = ["catalog", "memory", "search", "scorecards"] as const; | |
| 15 | type Tab = (typeof TABS)[number]; | |
| 16 | ||
| 17 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 18 | return page(args, { title: `Context · ${params.owner} · g1t` }); | |
| 19 | } | |
| 20 | ||
| 21 | export 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 | ||
| 42 | export 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 | ||
| 68 | function 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 | ||
| 86 | export 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 | } |