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
The phone app no longer has g1t's own agents, to match the site. The Agents tab is gone from the bar, and Customize (skills, connectors and plugins) is gone with its row in More. New message finds and messages people only, and its search and heading say so. A card an agent posted earlier still shows with its title, state, details and links, but its other buttons are no longer drawn, since chat no longer answers them; an old agent's messages and direct messages still show, under the name chat gives them, marked Deleted agent. The app stops asking for agents:run when it signs in. An old g1t:// link to Agents, the skill library, Customize or the Marketplace opens Home, while the integrations directory opens on the site. Notifications, the sign-in screen and the access-token error no longer mention agents waiting on you or agent tokens, and the sign-in screen speaks of reviewing what you and your coding agents make. Pull requests still show the coding agent they were made with.12/** `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;
The phone app no longer has g1t's own agents, to match the site. The Agents tab is gone from the bar, and Customize (skills, connectors and plugins) is gone with its row in More. New message finds and messages people only, and its search and heading say so. A card an agent posted earlier still shows with its title, state, details and links, but its other buttons are no longer drawn, since chat no longer answers them; an old agent's messages and direct messages still show, under the name chat gives them, marked Deleted agent. The app stops asking for agents:run when it signs in. An old g1t:// link to Agents, the skill library, Customize or the Marketplace opens Home, while the integrations directory opens on the site. Notifications, the sign-in screen and the access-token error no longer mention agents waiting on you or agent tokens, and the sign-in screen speaks of reviewing what you and your coding agents make. Pull requests still show the coding agent they were made with.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;
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.