Skip to content

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.sh
169 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
Merge branch 'mobile-agent-leftovers' into rm-agents12/** `agent` only on what g1t's own agents wrote before they were retired; shown with an Agent badge. */
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)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;
Merge branch 'mobile-agent-leftovers' into rm-agents49 /** The coding agent it was made with (`claude-code`, `codex`), from the session recorded on it; null when none was. */
50 agent: string | null;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)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 hunks?: { lines: { kind: "context" | "add" | "delete"; old: number | null; new: number | null; text: string }[] }[];
86 }[];
87};
88
89const repoPath = (owner: string, name: string) => `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
90
91/** Repositories you can see, newest first (at most 50), in the workspace chosen. */
92export function useRepos(search: string) {
93 const { client, workspace } = useSignedIn();
94 return useQuery({
95 queryKey: ["code", "repos", search],
96 queryFn: () => client.api<ApiRepo[]>(`/repos${search ? `?q=${encodeURIComponent(search)}` : ""}`),
97 select: (repos) => (workspace ? repos.filter((repo) => repo.namespace.toLowerCase() === workspace.slug.toLowerCase()) : repos),
98 });
99}
100
101export function useRepo(owner: string, name: string) {
102 const { client } = useSignedIn();
103 return useQuery({ queryKey: ["code", "repo", owner, name], queryFn: () => client.api<ApiRepo>(repoPath(owner, name)) });
104}
105
106export function usePulls(owner: string, name: string, state: "open" | "closed" | "all") {
107 const { client } = useSignedIn();
108 return useQuery({
109 queryKey: ["code", "pulls", owner, name, state],
110 queryFn: () => client.api<ApiPull[]>(`${repoPath(owner, name)}/pulls?state=${state}`),
111 });
112}
113
114export function useIssues(owner: string, name: string, state: "open" | "closed" | "all") {
115 const { client } = useSignedIn();
116 return useQuery({
117 queryKey: ["code", "issues", owner, name, state],
118 queryFn: () => client.api<ApiIssue[]>(`${repoPath(owner, name)}/issues?state=${state}`),
119 });
120}
121
122export function usePull(owner: string, name: string, number: number) {
123 const { client } = useSignedIn();
124 return useQuery({
125 queryKey: ["code", "pull", owner, name, number],
126 queryFn: () =>
127 client.api<{ pull: ApiPull; issue: ApiIssue | null; comments: ApiComment[] }>(`${repoPath(owner, name)}/pulls/${number}`),
128 });
129}
130
131export function usePullChanges(owner: string, name: string, number: number, enabled: boolean) {
132 const { client } = useSignedIn();
133 return useQuery({
134 queryKey: ["code", "pull-changes", owner, name, number],
135 queryFn: () => client.api<ApiChanges>(`${repoPath(owner, name)}/pulls/${number}/changes`),
136 enabled,
137 });
138}
139
140export function useIssue(owner: string, name: string, number: number) {
141 const { client } = useSignedIn();
142 return useQuery({
143 queryKey: ["code", "issue", owner, name, number],
144 queryFn: () =>
145 client.api<{ issue: ApiIssue; pulls: ApiPull[]; comments: ApiComment[] }>(`${repoPath(owner, name)}/issues/${number}`),
146 });
147}
148
149/** Comments on an issue (`POST /repos/{owner}/{name}/issues/{number}/comments`). */
150export function useComment(owner: string, name: string, number: number) {
151 const { client } = useSignedIn();
152 const queries = useQueryClient();
153 return useMutation({
154 mutationFn: (body: string) =>
155 client.api<ApiComment>(`${repoPath(owner, name)}/issues/${number}/comments`, { method: "POST", body: JSON.stringify({ body }) }),
156 onSuccess: () => queries.invalidateQueries({ queryKey: ["code", "issue", owner, name, number] }),
157 });
158}
159
160/** Reviews a pull request (`POST /repos/{owner}/{name}/pulls/{number}/reviews`). */
161export function useReview(owner: string, name: string, number: number) {
162 const { client } = useSignedIn();
163 const queries = useQueryClient();
164 return useMutation({
165 mutationFn: (review: { verdict: "approve" | "request_changes"; body: string }) =>
166 client.api<ApiComment>(`${repoPath(owner, name)}/pulls/${number}/reviews`, { method: "POST", body: JSON.stringify(review) }),
167 onSuccess: () => queries.invalidateQueries({ queryKey: ["code", "pull", owner, name, number] }),
168 });
169}

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