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

Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.1/**
2 * Docs and code: citing code in a doc (the Cite code dialog), and showing
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.3 * a project's docs folder in Artifacts.
Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash.4 */
5import { DOC_CITATION_KIND_LABELS, type DocCitationKind, type DocRepoSpace } from "@g1t/contracts";
6import { FileCode2, FolderGit2 } from "lucide-react";
7import { useEffect, useState } from "react";
8
9import { Button, ErrorText, Field, Input } from "../../ui";
10import { Combobox } from "../../ui/combobox";
11import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog";
12import { SelectField } from "../../ui/select";
13import { foliosQuery as docsQuery, foliosRequest as docsRequest } from "../actions";
14
15export type RepoChoice = { repo: string; default_branch: string; private: boolean };
16
17/** The workspace's repositories the viewer can read, asked for once the dialog that needs them opens. */
18export function useRepoChoices(slug: string, wanted: boolean): { repos: RepoChoice[] | null; error: string | null } {
19 const [repos, setRepos] = useState<RepoChoice[] | null>(null);
20 const [error, setError] = useState<string | null>(null);
21 useEffect(() => {
22 if (!wanted || repos) return;
23 let cancelled = false;
24 void docsQuery<RepoChoice[]>(slug, { repos: "1" }).then((r) => {
25 if (cancelled) return;
26 if (r.ok) setRepos(r.value);
27 else setError(r.error.message);
28 });
29 return () => {
30 cancelled = true;
31 };
32 }, [slug, wanted, repos]);
33 return { repos, error };
34}
35
36function RepoPicker({ repos, value, onChange, id }: { repos: RepoChoice[] | null; value: string; onChange: (repo: string) => void; id?: string }) {
37 return (
38 <Combobox
39 id={id}
40 value={value}
41 onValueChange={onChange}
42 placeholder={repos ? "Choose a repository" : "Loading repositories…"}
43 searchPlaceholder="Find a repository"
44 emptyText="No repository you can read matches."
45 disabled={!repos}
46 options={(repos ?? []).map((r) => ({ value: r.repo, label: r.repo, description: r.private ? "Private" : undefined, icon: <FolderGit2 size={14} /> }))}
47 className="w-full"
48 />
49 );
50}
51
52/** What the Cite code dialog inserts: a citation chip's attributes. */
53export type CitationInput = { repo: string; path: string; kind: DocCitationKind; label: string; ref: string };
54
55const LABEL_HINTS: Record<DocCitationKind, string> = {
56 path: "",
57 symbol: "The function, type or class, e.g. exportCsv",
58 endpoint: "The method and route, e.g. POST /v1/exports",
59 env: "The variable's name, e.g. EXPORT_BUCKET",
60};
61
62/**
63 * Cite code: a repository, a path in it (a file, a folder, or a pattern
64 * like `src/export/**`), and what there the doc describes. The citation
65 * is pinned to the default branch's head, and the doc is marked possibly
66 * out of date when a merge changes it.
67 */
68export function CiteDialog({ slug, open, onOpenChange, onCite, projects = [] }: { slug: string; open: boolean; onOpenChange: (open: boolean) => void; onCite: (c: CitationInput) => void; projects?: string[] }) {
69 const { repos, error: loadError } = useRepoChoices(slug, open);
70 const [repo, setRepo] = useState("");
71 const [path, setPath] = useState("");
72 const [kind, setKind] = useState<DocCitationKind>("path");
73 const [label, setLabel] = useState("");
74 const [busy, setBusy] = useState(false);
75 const [error, setError] = useState<string | null>(null);
76 useEffect(() => {
77 if (!open) return;
78 setError(null);
79 setPath("");
80 setLabel("");
81 setKind("path");
82 }, [open]);
83 // The doc's own project first, when it has one.
84 useEffect(() => {
85 if (!repo && repos?.length) setRepo(repos.find((r) => projects.includes(r.repo))?.repo ?? repos[0]!.repo);
86 }, [repos, repo, projects]);
87 const submit = async () => {
88 if (!repo || !path.trim()) return setError("Choose a repository and name a path in it.");
89 if (kind !== "path" && !label.trim()) return setError(`Name the ${kind === "env" ? "variable" : kind}.`);
90 setBusy(true);
91 setError(null);
92 const found = await docsQuery<{ repo: string; path: string; ref: string | null }>(slug, { cite: repo, path: path.trim() });
93 setBusy(false);
94 if (!found.ok) return setError(found.error.message);
95 onCite({ repo: found.value.repo, path: found.value.path, kind, label: kind === "path" ? "" : label.trim(), ref: found.value.ref ?? "" });
96 onOpenChange(false);
97 };
98 return (
99 <Dialog open={open} onOpenChange={onOpenChange}>
100 <DialogContent className="max-w-lg">
101 <DialogHeader>
102 <DialogTitle className="flex items-center gap-2">
103 <FileCode2 size={16} /> Cite code
104 </DialogTitle>
105 <DialogDescription>Link this doc to the code it describes. When a merged pull request changes it, the doc&apos;s owner hears that it may be out of date.</DialogDescription>
106 </DialogHeader>
107 <form
108 className="space-y-4"
109 onSubmit={(e) => {
110 e.preventDefault();
111 void submit();
112 }}
113 >
114 <Field label="Repository">
115 <RepoPicker repos={repos} value={repo} onChange={setRepo} />
116 </Field>
117 <Field label="Path" hint="A file, a folder (everything in it), or a pattern such as src/export/** or *.sql.">
118 <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="src/export.ts" autoComplete="off" spellCheck={false} className="font-mono" />
119 </Field>
120 <Field label="What it describes">
121 <SelectField value={kind} onValueChange={(v) => setKind(v as DocCitationKind)} options={(Object.keys(DOC_CITATION_KIND_LABELS) as DocCitationKind[]).map((k) => ({ value: k, label: DOC_CITATION_KIND_LABELS[k] }))} className="w-full" aria-label="What it describes" />
122 </Field>
123 {kind !== "path" && (
124 <Field label={kind === "symbol" ? "Symbol" : kind === "endpoint" ? "Endpoint" : "Variable"} hint={LABEL_HINTS[kind]}>
125 <Input value={label} onChange={(e) => setLabel(e.target.value)} autoComplete="off" spellCheck={false} className="font-mono" />
126 </Field>
127 )}
128 {(error || loadError) && <ErrorText>{error ?? loadError}</ErrorText>}
129 <DialogFooter>
130 <Button type="submit" variant="accent" disabled={busy || !repos}>
131 {busy ? "Checking…" : "Cite"}
132 </Button>
133 </DialogFooter>
134 </form>
135 </DialogContent>
136 </Dialog>
137 );
138}
139
140/** Show a project's docs: choose a repository the viewer can read; its `docs/` folder and README appear in the sidebar and search. */
141export function RepoDocsDialog({ slug, open, onOpenChange, shown, onAdded }: { slug: string; open: boolean; onOpenChange: (open: boolean) => void; shown: string[]; onAdded: (space: DocRepoSpace) => void }) {
142 const { repos, error: loadError } = useRepoChoices(slug, open);
143 const [repo, setRepo] = useState("");
144 const [busy, setBusy] = useState(false);
145 const [error, setError] = useState<string | null>(null);
146 const choices = repos?.filter((r) => !shown.includes(r.repo)) ?? null;
147 return (
148 <Dialog open={open} onOpenChange={onOpenChange}>
149 <DialogContent className="max-w-lg">
150 <DialogHeader>
151 <DialogTitle className="flex items-center gap-2">
152 <FolderGit2 size={16} /> Show a project&apos;s docs
153 </DialogTitle>
154 <DialogDescription>The repository&apos;s docs folder and README, read from its default branch, appear beside your spaces and in search, and follow every push. Only people who can read the repository see them. Changes go through the repository.</DialogDescription>
155 </DialogHeader>
156 <form
157 className="space-y-4"
158 onSubmit={async (e) => {
159 e.preventDefault();
160 if (!repo) return;
161 setBusy(true);
162 setError(null);
163 const added = await docsRequest<DocRepoSpace>(slug, "add_repo_space", { repo });
164 setBusy(false);
165 if (!added.ok) return setError(added.error.message);
166 onAdded(added.value);
167 onOpenChange(false);
168 }}
169 >
170 <Field label="Repository">
171 <RepoPicker repos={choices} value={repo} onChange={setRepo} />
172 </Field>
173 {choices && choices.length === 0 && <p className="text-xs text-faint">Every repository you can read is already shown.</p>}
174 {(error || loadError) && <ErrorText>{error ?? loadError}</ErrorText>}
175 <DialogFooter>
176 <Button type="submit" variant="accent" disabled={busy || !repo}>
177 {busy ? "Reading its docs…" : "Show its docs"}
178 </Button>
179 </DialogFooter>
180 </form>
181 </DialogContent>
182 </Dialog>
183 );
184}