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
217 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.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1/**
2 * What the notify service is told about a new message, or a read: every
3 * person in the conversation has its counts moved, and those it is for are
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.4 * notified (docs.g1t.sh/guides/chat/, "Notifications").
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)5 *
6 * - A direct message notifies everyone else in it, muted or not.
7 * - An @mention notifies the person named, muted or not.
8 * - A reply notifies the people already in its thread (who started it or
9 * replied), unless they muted the conversation.
10 * - Everyone else in it only has their counts moved.
11 * - The author is never told of their own message; their own count for
12 * the conversation goes to nothing (what you wrote, you have read).
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.13 * - Only people are told.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)14 *
15 * `recipients` is pure, so the rules are tested apart from the service.
16 */
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.17import type { FeedDelivery, FeedNotification, MemberProfile, NotificationKind } from "@g1t/contracts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)18
Merge main into Artifacts Phase 219import { plainText } from "@g1t/contracts/chat-markdown";
20
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)21import { tally, type UnreadRow } from "./unread.ts";
22
23export type Person = { key: string; user_id: string; username: string; muted: boolean };
24
25export type Recipient = { user_id: string; kind: NotificationKind | null; mentioned: boolean; muted: boolean };
26
27export function recipients(input: {
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.28 /** `user:<id>`. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)29 author: string;
30 channelKind: "channel" | "dm";
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.31 /** The people in the conversation. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)32 people: Person[];
33 /** Handles the message mentions, lowercased. */
34 mentioned: string[];
35 /** For a reply: the people in its thread, by key; null for a top-level message. */
36 thread: ReadonlySet<string> | null;
37}): Recipient[] {
38 const named = new Set(input.mentioned.map((h) => h.toLowerCase()));
39 const out: Recipient[] = [];
40 for (const person of input.people) {
41 if (person.key === input.author) continue;
42 const mentioned = named.has(person.username.toLowerCase());
43 let kind: NotificationKind | null = null;
44 if (input.channelKind === "dm") kind = "dm";
45 else if (mentioned) kind = "mention";
46 else if (input.thread?.has(person.key) && !person.muted) kind = "thread_reply";
47 out.push({ user_id: person.user_id, kind, mentioned, muted: person.muted });
48 }
49 return out;
50}
51
Merge main into Artifacts Phase 252/** A message's text as a notification shows it: one line, the Markdown's marks gone, short. */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)53export function preview(body: string, max = 140): string {
Merge main into Artifacts Phase 254 return plainText(String(body ?? ""), max);
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread55}
56
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)57/** Where a conversation, or a thread in it, is on the site. */
58export function conversationHref(slug: string, channel: { id: string; kind: "channel" | "dm"; name: string | null }, threadRoot: string | null): string {
59 const base = channel.kind === "dm" || !channel.name ? `/${slug}/-/chat/dm/${channel.id}` : `/${slug}/-/chat/${channel.name}`;
60 return threadRoot ? `${base}?thread=${encodeURIComponent(threadRoot)}` : base;
61}
62
63/** The deliveries for one new message. */
64export function messageDeliveries(input: {
65 slug: string;
66 channel: { id: string; kind: "channel" | "dm"; name: string | null };
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.67 message: { id: string; author: string; body: string; thread_root: string | null; created_at: string };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)68 author: MemberProfile;
69 recipients: Recipient[];
70}): FeedDelivery[] {
71 const { slug, channel, message, author } = input;
72 const where = channel.kind === "dm" ? "" : ` in #${channel.name}`;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar73 // The service sets `display_name` by `memberName`'s rule (display name,
74 // else the username in its chosen case), so pushes match the chat.
75 const shown = author.display_name.trim() || author.display_username || author.name;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)76 const deliveries: FeedDelivery[] = input.recipients.map((r) => {
77 const notification: FeedNotification | null = r.kind
78 ? {
79 id: message.id,
80 kind: r.kind,
81 workspace: slug,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar82 title: r.kind === "thread_reply" ? `${shown} replied${where}` : `${shown}${where}`,
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.83 body: preview(message.body),
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)84 href: conversationHref(slug, channel, message.thread_root),
85 actor: {
86 kind: author.kind,
87 id: author.id,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar88 name: shown,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)89 avatar: author.avatar,
90 avatar_seed: author.avatar_seed ?? null,
91 },
92 channel_id: channel.id,
93 thread_root: message.thread_root,
94 created_at: message.created_at,
95 }
96 : null;
97 return {
98 user_id: r.user_id,
99 workspace: slug,
100 counts: { channel_id: channel.id, unread: 1, mentions: r.mentioned ? 1 : 0, muted: r.muted },
101 notification,
102 };
103 });
104 // The author has read the conversation by writing in it.
105 if (message.author.startsWith("user:")) {
106 deliveries.push({
107 user_id: message.author.slice("user:".length),
108 workspace: slug,
109 counts: { channel_id: channel.id, unread: 0, mentions: 0, set: true },
110 notification: null,
111 });
112 }
113 return deliveries;
114}
115
116/** A person's counts for one conversation after they read up to `lastReadId`, from the rows after it. */
117export function countsAfterRead(rows: UnreadRow[], channelId: string, member: string, username: string, lastReadId: string): { unread: number; mentions: number } {
118 return tally(rows, member, username, new Map([[channelId, lastReadId]])).get(channelId) ?? { unread: 0, mentions: 0 };
119}
120
121
122// ── Wiring, used by src/index.ts ─────────────────────────────────────────
123
124type Db = D1Database;
125type Notify = { fetch(input: string, init?: RequestInit): Promise<Response> };
126
127async function deliver(notify: Notify, items: FeedDelivery[]): Promise<void> {
128 if (!items.length) return;
129 const response = await notify.fetch("https://service/rpc/deliver", {
130 method: "POST",
131 headers: { "content-type": "application/json" },
132 body: JSON.stringify({ items }),
133 });
134 if (!response.ok) throw new Error(`deliver failed with status ${response.status}`);
135}
136
137/** Tells notify of a new message: one batched call for everyone in the conversation. */
138export async function notifyMessage(
139 db: Db,
140 notify: Notify | undefined,
141 profiles: (keys: string[]) => Promise<Map<string, MemberProfile>>,
142 input: {
143 slug: string;
144 channel: { id: string; kind: "channel" | "dm"; name: string | null };
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.145 row: { id: string; author: string; body: string; thread_root: string | null; created_at: string };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)146 handles: string[];
147 },
148): Promise<void> {
149 if (!notify) return;
150 const { channel, row } = input;
151 const [members, thread] = await Promise.all([
152 db
153 .prepare("SELECT principal, muted FROM channel_members WHERE channel_id = ? AND principal LIKE 'user:%'")
154 .bind(channel.id)
155 .all<{ principal: string; muted: number }>(),
156 row.thread_root
157 ? db
158 .prepare("SELECT DISTINCT author FROM messages WHERE channel_id = ?1 AND (id = ?2 OR thread_root = ?2) AND author LIKE 'user:%'")
159 .bind(channel.id, row.thread_root)
160 .all<{ author: string }>()
161 : Promise.resolve(null),
162 ]);
163 const keys = members.results.map((m) => m.principal);
164 const found = await profiles([...keys, row.author]);
165 const people: Person[] = members.results.map((m) => ({
166 key: m.principal,
167 user_id: m.principal.slice("user:".length),
168 username: found.get(m.principal)?.name ?? "",
169 muted: !!m.muted,
170 }));
171 const author = found.get(row.author);
172 if (!author) return;
173 await deliver(
174 notify,
175 messageDeliveries({
176 slug: input.slug,
177 channel,
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.178 message: { id: row.id, author: row.author, body: row.body, thread_root: row.thread_root, created_at: row.created_at },
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)179 author,
180 recipients: recipients({
181 author: row.author,
182 channelKind: channel.kind,
183 people,
184 mentioned: input.handles,
185 thread: thread ? new Set(thread.results.map((r) => r.author)) : null,
186 }),
187 }),
188 );
189}
190
191/** After a read: the person's counts for the conversation, as they now are, in every tab. */
192export async function notifyRead(
193 db: Db,
194 notify: Notify | undefined,
195 input: { slug: string; channel_id: string; user_id: string; username: string; last_read_id: string },
196): Promise<void> {
197 if (!notify) return;
198 const member = `user:${input.user_id}`;
199 const rows = await db
200 .prepare(
201 "SELECT channel_id, id, author, mentions FROM messages WHERE channel_id = ? AND id > ? AND deleted_at IS NULL AND author != ? LIMIT 5000",
202 )
203 .bind(input.channel_id, input.last_read_id, member)
204 .all<UnreadRow>();
205 const counts = countsAfterRead(rows.results, input.channel_id, member, input.username, input.last_read_id);
206 await deliver(notify, [
207 { user_id: input.user_id, workspace: input.slug, counts: { channel_id: input.channel_id, ...counts, set: true }, notification: null },
208 ]);
209}
210
211/** After muting or unmuting: the badge counts the conversation, or leaves it out, in every tab. */
212export async function notifyMuted(notify: Notify | undefined, input: { slug: string; channel_id: string; user_id: string; muted: boolean }): Promise<void> {
213 if (!notify) return;
214 await deliver(notify, [
215 { user_id: input.user_id, workspace: input.slug, counts: { channel_id: input.channel_id, unread: 0, mentions: 0, muted: input.muted }, notification: null },
216 ]);
217}

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