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

The phone app: Chat in full with phone notifications, plus Code and Customize (#4)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
12export type ApiUser = { id: string; username: string; display_username?: string; kind?: "user" | "workspace" | "agent" | "system" };
13
14export type ApiRepo = {
15 id: string;
16 namespace: string;
17 name: string;
18 description: string | null;
19 is_private: boolean;
20 default_branch: string;
21 created_at: string;
22 archived_at?: string | null;
23};
24
25export type ApiIssue = {
26 id: string;
27 number: number;
28 title: string;
29 body: string;
30 labels: string[];
31 state: "open" | "closed";
32 reason: "completed" | "not_planned" | null;
33 author: ApiUser;
34 created_at: string;
35 updated_at: string;
36 comment_count: number;
37 pull_count: number;
38};
39
40export type CheckStatus = "queued" | "running" | "passed" | "failed" | "errored" | null;
41
42export type ApiPull = {
43 id: string;
44 number: number;
45 issue: number | null;
46 title: string;
47 body: string | null;
48 agent: string;
49 status: "draft" | "open" | "merged" | "closed";
50 branch: string | null;
51 base: string;
52 check_status: CheckStatus;
53 files: { path: string; additions: number; deletions: number }[];
54 reviewers: string[];
55 labels: string[];
56 author: ApiUser;
57 created_at: string;
58 updated_at: string;
59 merged_at: string | null;
60};
61
62export type ApiComment = {
63 id: string;
64 kind: "comment" | "event";
65 author: ApiUser;
66 body: string;
67 path: string | null;
68 line: number | null;
69 verdict: string | null;
70 created_at: string;
71};
72
73export type ApiChanges = {
74 base: string;
75 head: string;
76 truncated: boolean;
77 files: {
78 path: string;
79 status: "added" | "modified" | "deleted";
80 additions: number;
81 deletions: number;
82 binary: boolean;
83 hunks?: { lines: { kind: "context" | "add" | "delete"; old: number | null; new: number | null; text: string }[] }[];
84 }[];
85};
86
87const repoPath = (owner: string, name: string) => `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
88
89/** Repositories you can see, newest first (at most 50), in the workspace chosen. */
90export function useRepos(search: string) {
91 const { client, workspace } = useSignedIn();
92 return useQuery({
93 queryKey: ["code", "repos", search],
94 queryFn: () => client.api<ApiRepo[]>(`/repos${search ? `?q=${encodeURIComponent(search)}` : ""}`),
95 select: (repos) => (workspace ? repos.filter((repo) => repo.namespace.toLowerCase() === workspace.slug.toLowerCase()) : repos),
96 });
97}
98
99export function useRepo(owner: string, name: string) {
100 const { client } = useSignedIn();
101 return useQuery({ queryKey: ["code", "repo", owner, name], queryFn: () => client.api<ApiRepo>(repoPath(owner, name)) });
102}
103
104export function usePulls(owner: string, name: string, state: "open" | "closed" | "all") {
105 const { client } = useSignedIn();
106 return useQuery({
107 queryKey: ["code", "pulls", owner, name, state],
108 queryFn: () => client.api<ApiPull[]>(`${repoPath(owner, name)}/pulls?state=${state}`),
109 });
110}
111
112export function useIssues(owner: string, name: string, state: "open" | "closed" | "all") {
113 const { client } = useSignedIn();
114 return useQuery({
115 queryKey: ["code", "issues", owner, name, state],
116 queryFn: () => client.api<ApiIssue[]>(`${repoPath(owner, name)}/issues?state=${state}`),
117 });
118}
119
120export function usePull(owner: string, name: string, number: number) {
121 const { client } = useSignedIn();
122 return useQuery({
123 queryKey: ["code", "pull", owner, name, number],
124 queryFn: () =>
125 client.api<{ pull: ApiPull; issue: ApiIssue | null; comments: ApiComment[] }>(`${repoPath(owner, name)}/pulls/${number}`),
126 });
127}
128
129export function usePullChanges(owner: string, name: string, number: number, enabled: boolean) {
130 const { client } = useSignedIn();
131 return useQuery({
132 queryKey: ["code", "pull-changes", owner, name, number],
133 queryFn: () => client.api<ApiChanges>(`${repoPath(owner, name)}/pulls/${number}/changes`),
134 enabled,
135 });
136}
137
138export function useIssue(owner: string, name: string, number: number) {
139 const { client } = useSignedIn();
140 return useQuery({
141 queryKey: ["code", "issue", owner, name, number],
142 queryFn: () =>
143 client.api<{ issue: ApiIssue; pulls: ApiPull[]; comments: ApiComment[] }>(`${repoPath(owner, name)}/issues/${number}`),
144 });
145}
146
147/** Comments on an issue (`POST /repos/{owner}/{name}/issues/{number}/comments`). */
148export function useComment(owner: string, name: string, number: number) {
149 const { client } = useSignedIn();
150 const queries = useQueryClient();
151 return useMutation({
152 mutationFn: (body: string) =>
153 client.api<ApiComment>(`${repoPath(owner, name)}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }),
154 onSuccess: () => queries.invalidateQueries({ queryKey: ["code", "issue", owner, name, number] }),
155 });
156}
157
158/** Reviews a pull request (`POST /repos/{owner}/{name}/pulls/{number}/reviews`). */
159export function useReview(owner: string, name: string, number: number) {
160 const { client } = useSignedIn();
161 const queries = useQueryClient();
162 return useMutation({
163 mutationFn: (review: { verdict: "approve" | "request_changes"; body: string }) =>
164 client.api<ApiComment>(`${repoPath(owner, name)}/pulls/${number}/reviews`, { method: "POST", body: JSON.stringify(review) }),
165 onSuccess: () => queries.invalidateQueries({ queryKey: ["code", "pull", owner, name, number] }),
166 });
167}

This file's history is long; its oldest lines are credited to the oldest commit read.