Skip to content
171 linesCodeBlameRaw
1import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
3import { useSignedIn } from "./session";
4
5/**
6 * Code, through the REST API (docs.g1t.sh/reference/api/). The API
7 * answers in snake_case, so these are its shapes, not the camelCase ones
8 * in @g1t/contracts that the services use among themselves. Only the
9 * fields the app shows are named.
10 */
11
12/** `agent` only on what g1t's own agents wrote before they were retired; shown with an Agent badge. */
13export type ApiUser = { id: string; username: string; display_username?: string; kind?: "user" | "workspace" | "agent" | "system" };
14
15export type ApiRepo = {
16 id: string;
17 namespace: string;
18 name: string;
19 description: string | null;
20 is_private: boolean;
21 default_branch: string;
22 created_at: string;
23 archived_at?: string | null;
24};
25
26export type ApiIssue = {
27 id: string;
28 number: number;
29 title: string;
30 body: string;
31 labels: string[];
32 state: "open" | "closed";
33 reason: "completed" | "not_planned" | null;
34 author: ApiUser;
35 created_at: string;
36 updated_at: string;
37 comment_count: number;
38 pull_count: number;
39};
40
41export type CheckStatus = "queued" | "running" | "passed" | "failed" | "errored" | null;
42
43export type ApiPull = {
44 id: string;
45 number: number;
46 issue: number | null;
47 title: string;
48 body: string | null;
49 /** The coding agent it was made with (`claude-code`, `codex`), from the session recorded on it; null when none was. */
50 agent: string | null;
51 status: "draft" | "open" | "merged" | "closed";
52 branch: string | null;
53 base: string;
54 check_status: CheckStatus;
55 files: { path: string; additions: number; deletions: number }[];
56 reviewers: string[];
57 labels: string[];
58 author: ApiUser;
59 created_at: string;
60 updated_at: string;
61 merged_at: string | null;
62};
63
64export type ApiComment = {
65 id: string;
66 kind: "comment" | "event";
67 author: ApiUser;
68 body: string;
69 path: string | null;
70 line: number | null;
71 verdict: string | null;
72 created_at: string;
73};
74
75export type ApiChanges = {
76 base: string;
77 head: string;
78 truncated: boolean;
79 files: {
80 path: string;
81 status: "added" | "modified" | "deleted";
82 additions: number;
83 deletions: number;
84 binary: boolean;
85 /** Past what one answer reads: listed without its lines or counts. */
86 omitted?: boolean;
87 hunks?: { lines: { kind: "context" | "add" | "delete"; old: number | null; new: number | null; text: string }[] }[];
88 }[];
89};
90
91const repoPath = (owner: string, name: string) => `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
92
93/** Repositories you can see, newest first (at most 50), in the workspace chosen. */
94export function useRepos(search: string) {
95 const { client, workspace } = useSignedIn();
96 return useQuery({
97 queryKey: ["code", "repos", search],
98 queryFn: () => client.api<ApiRepo[]>(`/repos${search ? `?q=${encodeURIComponent(search)}` : ""}`),
99 select: (repos) => (workspace ? repos.filter((repo) => repo.namespace.toLowerCase() === workspace.slug.toLowerCase()) : repos),
100 });
101}
102
103export function useRepo(owner: string, name: string) {
104 const { client } = useSignedIn();
105 return useQuery({ queryKey: ["code", "repo", owner, name], queryFn: () => client.api<ApiRepo>(repoPath(owner, name)) });
106}
107
108export function usePulls(owner: string, name: string, state: "open" | "closed" | "all") {
109 const { client } = useSignedIn();
110 return useQuery({
111 queryKey: ["code", "pulls", owner, name, state],
112 queryFn: () => client.api<ApiPull[]>(`${repoPath(owner, name)}/pulls?state=${state}`),
113 });
114}
115
116export function useIssues(owner: string, name: string, state: "open" | "closed" | "all") {
117 const { client } = useSignedIn();
118 return useQuery({
119 queryKey: ["code", "issues", owner, name, state],
120 queryFn: () => client.api<ApiIssue[]>(`${repoPath(owner, name)}/issues?state=${state}`),
121 });
122}
123
124export function usePull(owner: string, name: string, number: number) {
125 const { client } = useSignedIn();
126 return useQuery({
127 queryKey: ["code", "pull", owner, name, number],
128 queryFn: () =>
129 client.api<{ pull: ApiPull; issue: ApiIssue | null; comments: ApiComment[] }>(`${repoPath(owner, name)}/pulls/${number}`),
130 });
131}
132
133export function usePullChanges(owner: string, name: string, number: number, enabled: boolean) {
134 const { client } = useSignedIn();
135 return useQuery({
136 queryKey: ["code", "pull-changes", owner, name, number],
137 queryFn: () => client.api<ApiChanges>(`${repoPath(owner, name)}/pulls/${number}/changes`),
138 enabled,
139 });
140}
141
142export function useIssue(owner: string, name: string, number: number) {
143 const { client } = useSignedIn();
144 return useQuery({
145 queryKey: ["code", "issue", owner, name, number],
146 queryFn: () =>
147 client.api<{ issue: ApiIssue; pulls: ApiPull[]; comments: ApiComment[] }>(`${repoPath(owner, name)}/issues/${number}`),
148 });
149}
150
151/** Comments on an issue (`POST /repos/{owner}/{name}/issues/{number}/comments`). */
152export function useComment(owner: string, name: string, number: number) {
153 const { client } = useSignedIn();
154 const queries = useQueryClient();
155 return useMutation({
156 mutationFn: (body: string) =>
157 client.api<ApiComment>(`${repoPath(owner, name)}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }),
158 onSuccess: () => queries.invalidateQueries({ queryKey: ["code", "issue", owner, name, number] }),
159 });
160}
161
162/** Reviews a pull request (`POST /repos/{owner}/{name}/pulls/{number}/reviews`). */
163export function useReview(owner: string, name: string, number: number) {
164 const { client } = useSignedIn();
165 const queries = useQueryClient();
166 return useMutation({
167 mutationFn: (review: { verdict: "approve" | "request_changes"; body: string }) =>
168 client.api<ApiComment>(`${repoPath(owner, name)}/pulls/${number}/reviews`, { method: "POST", body: JSON.stringify(review) }),
169 onSuccess: () => queries.invalidateQueries({ queryKey: ["code", "pull", owner, name, number] }),
170 });
171}