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
117 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 Constants from "expo-constants";
2import * as Device from "expo-device";
3import * as Notifications from "expo-notifications";
4import * as SecureStore from "expo-secure-store";
5import { Platform } from "react-native";
6
7import type { Client } from "./client";
8import { IN_EXPO_GO } from "./oauth";
9import { viewing } from "./viewing";
10
11/**
12 * Notifications on the phone. g1t's notify service sends them through
13 * Expo's push service (FCM on Android, APNs on iOS) under the same rules
14 * as browser push: what the person chose, never in Do Not Disturb, never
15 * for a muted conversation, and not while a g1t tab or this app is in
16 * front of them. The app registers its push token with the site's
17 * `/-/notify` (`register_device`) once allowed, and takes it back on
18 * signing out (`unregister_device`).
19 *
20 * Each push carries in `data` where it goes (`href`, a site path), its
21 * `workspace`, and the `channel_id` and `thread_root` it is about.
22 */
23
24/** Android's channel for messages: what people turn on or off in the system's settings. */
25export const MESSAGES_CHANNEL = "messages";
26
27const TOKEN_KEY = "g1t.push-token";
28
29export type PushData = {
30 href?: string;
31 workspace?: string;
32 kind?: string;
33 channel_id?: string | null;
34 thread_root?: string | null;
35};
36
37// A push that arrives while the app is open shows, unless it is about
38// the conversation on screen.
39Notifications.setNotificationHandler({
40 handleNotification: async (notification) => {
41 const data = (notification.request.content.data ?? {}) as PushData;
42 const here = !!data.channel_id && data.channel_id === viewing.channelId;
43 return { shouldShowBanner: !here, shouldShowList: !here, shouldPlaySound: !here, shouldSetBadge: true };
44 },
45});
46
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)47export type PushState = "unsupported" | "off" | "denied" | "undetermined" | "granted";
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)48
49/** Whether pushes can work here: a real phone, in a build of the app (Expo Go on Android has no push). */
50export function pushSupported(): boolean {
51 return Device.isDevice && !(IN_EXPO_GO && Platform.OS === "android");
52}
53
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)54export async function pushState(server?: { push?: boolean }): Promise<PushState> {
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)55 if (!pushSupported()) return "unsupported";
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)56 if (server?.push === false) return "off";
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)57 const { status } = await Notifications.getPermissionsAsync();
58 return status === "granted" ? "granted" : status === "denied" ? "denied" : "undetermined";
59}
60
61/**
62 * Asks to notify (once; after a no, the system's settings decide), and
63 * registers the phone with g1t. Safe to call on every launch: the token is
64 * registered again, which keeps it fresh.
65 */
66export async function registerForPush(client: Client, ask: boolean): Promise<PushState> {
67 if (!pushSupported()) return "unsupported";
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)68 // A g1t that sends phones nothing (PHONE_PUSH=off): no asking, nothing to register.
69 if (client.server.push === false) return "off";
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)70 if (Platform.OS === "android") {
71 await Notifications.setNotificationChannelAsync(MESSAGES_CHANNEL, {
72 name: "Messages",
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.73 description: "Messages to you, mentions and replies.",
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)74 importance: Notifications.AndroidImportance.HIGH,
75 vibrationPattern: [0, 120, 80, 120],
76 lightColor: "#b6a8ff",
77 showBadge: true,
78 });
79 }
80 let { status } = await Notifications.getPermissionsAsync();
81 if (status !== "granted" && ask) status = (await Notifications.requestPermissionsAsync()).status;
82 if (status !== "granted") return status === "denied" ? "denied" : "undetermined";
83
84 const projectId = (Constants.expoConfig?.extra?.eas as { projectId?: string } | undefined)?.projectId ?? Constants.easConfig?.projectId;
85 const { data: token } = await Notifications.getExpoPushTokenAsync(projectId ? { projectId } : undefined);
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)86 const answer = await client.siteRaw<{ ok: boolean; off?: boolean }>("/-/notify", {
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)87 method: "POST",
88 body: JSON.stringify({
89 intent: "register_device",
90 device: { token, platform: Platform.OS === "ios" ? "ios" : "android", name: Device.deviceName ?? Device.modelName ?? null },
91 }),
92 });
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)93 if (answer.off) return "off";
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)94 await SecureStore.setItemAsync(TOKEN_KEY, token);
95 return "granted";
96}
97
98/** Takes the phone off the person's notifications, on signing out. */
99export async function unregisterPush(client: Client): Promise<void> {
100 const token = await SecureStore.getItemAsync(TOKEN_KEY).catch(() => null);
101 if (!token) return;
102 await client
103 .siteRaw("/-/notify", { method: "POST", body: JSON.stringify({ intent: "unregister_device", token }) })
104 .catch(() => undefined);
105 await SecureStore.deleteItemAsync(TOKEN_KEY).catch(() => undefined);
106 await Notifications.setBadgeCountAsync(0).catch(() => undefined);
107}
108
109/**
110 * Sends a test notification to this person's phones and browsers
111 * (`POST /-/notify` `test`), whatever is in front of them: the way to see
112 * that a build gets pushes. Answers how many it reached.
113 */
114export async function sendTestPush(client: Client): Promise<number> {
115 const answer = await client.siteRaw<{ ok: boolean; pushed: number }>("/-/notify", { method: "POST", body: JSON.stringify({ intent: "test" }) });
116 return answer.pushed ?? 0;
117}

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