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

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * What a message may hold and how pages of them are read. Pure, so it is
3 * tested apart from the service.
4 */
5
6/** The longest message body, in characters. */
7export const MAX_BODY = 40_000;
8
9/** Messages on one page when none is asked for, and the most on one page. */
10export const DEFAULT_PAGE = 50;
11export const MAX_PAGE = 200;
12
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/** How long "is typing" shows after a person's keystroke. */
Chat and workspace agents: channels, DMs and named agents you talk to14export const PERSON_TYPING_MS = 5_000;
15
16/** A body as kept, or why it cannot be: something to say, and not too much. */
17export function messageBody(body: unknown, hasCard = false): { ok: true; body: string } | { ok: false; message: string } {
18 const text = typeof body === "string" ? body.replace(/\s+$/, "") : "";
19 if (!text.trim() && !hasCard) return { ok: false, message: "A message needs something in it." };
20 if (text.length > MAX_BODY) return { ok: false, message: `A message is at most ${MAX_BODY.toLocaleString("en-US")} characters.` };
21 return { ok: true, body: text };
22}
23
24/** How many messages a page holds: the default, or what was asked within 1 to the most. */
25export function pageSize(limit: unknown): number {
26 const n = typeof limit === "number" && Number.isFinite(limit) ? Math.floor(limit) : DEFAULT_PAGE;
27 return Math.min(MAX_PAGE, Math.max(1, n));
28}
29
30/**
31 * A page read one past its size, newest first: the page, and the cursor
32 * for the next one back (its oldest id), or null when this is the start.
33 */
34export function pageOf<T extends { id: string }>(rows: T[], size: number): { rows: T[]; older: string | null } {
35 if (rows.length <= size) return { rows, older: null };
36 const page = rows.slice(0, size);
37 return { rows: page, older: page[page.length - 1].id };
38}
39
40/**
41 * Whether a frame from a client's socket says it is typing in `channelId`:
42 * `{"type":"typing"}`, optionally naming the channel. A frame naming
43 * another channel is ignored.
44 */
45export function isTypingFrame(frame: unknown, channelId: string): boolean {
46 if (typeof frame !== "string" || frame.length > 1024) return false;
47 let said: unknown;
48 try {
49 said = JSON.parse(frame);
50 } catch {
51 return false;
52 }
53 if (!said || typeof said !== "object") return false;
54 const { type, channel_id } = said as { type?: unknown; channel_id?: unknown };
55 return type === "typing" && (channel_id === undefined || channel_id === null || channel_id === channelId);
56}
57
58/** The UTC day a time falls on, as the meter keys it. */
59export function meterDay(iso: string): string {
60 return iso.slice(0, 10);
61}