Skip to content
98 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.

g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.1/**
2 * Today's reads: each section's data from its own service, every call on
3 * its own so one that fails leaves only its section saying so. The shaping
4 * is in ./today.ts.
5 */
6import type { AgentSession, Repo, User } from "@g1t/contracts";
7
8import { type CodePull, type CodeWork, type SessionWork, type Spend, dayIn, pullKey, spendToday } from "./today";
9import { agents, billing, repos, work, workspaceAgents } from "./services.server";
10
11/** Pull requests read per project and list: `LIST_PAGE` in services/work. */
12const PULL_PAGE = 100;
13/** The most projects whose pull requests are read. */
14const MAX_PROJECTS = 50;
15/** Revise runs and sessions read: the services' most in one list. */
16const RUN_PAGE = 200;
17const SESSION_PAGE = 200;
18const DAY = 24 * 60 * 60 * 1000;
19
20const warn = (what: string) => (error: unknown) => {
21 console.warn(`today: ${what} failed`, error);
22 return null;
23};
24
25/**
26 * The agents' pull requests in the workspace's projects, open and recently
27 * merged or closed, with how many times each was sent back to revise.
28 * Null when Code did not answer.
29 */
30export async function loadCodeWork(viewer: User, slug: string, now: number): Promise<CodeWork | null> {
31 const [list, runs] = await Promise.all([
32 repos.list(viewer, { namespace: slug }).catch(warn("repos")),
33 agents.listRuns(viewer, { workspace: slug, kind: "revise", limit: RUN_PAGE }).catch(warn("revise runs")),
34 ]);
35 if (!list || !runs?.ok) return null;
36 const chosen = list.filter((repo) => !repo.forkOf && repo.namespace.toLowerCase() === slug).slice(0, MAX_PROJECTS);
37 const byId = new Map<string, Repo>(chosen.map((repo) => [repo.id, repo]));
38 const batch = chosen.length > 0 ? await work.pullsForRepos([...byId.keys()], viewer, PULL_PAGE).catch(warn("pull requests")) : [];
39 if (!batch) return null;
40 const pulls: CodePull[] = [];
41 const since = now - 8 * DAY;
42 let complete = list.filter((repo) => !repo.forkOf && repo.namespace.toLowerCase() === slug).length <= MAX_PROJECTS;
43 for (const entry of batch) {
44 const repo = byId.get(entry.repoId);
45 if (!repo) continue;
46 const path = { namespace: repo.namespace, name: repo.name };
47 for (const pull of [...entry.open, ...entry.closed]) {
48 pulls.push({
49 repo: path,
50 number: pull.number,
51 title: pull.title,
52 status: pull.status,
53 mergedAt: pull.mergedAt,
54 createdAt: pull.createdAt,
55 updatedAt: pull.updatedAt,
56 author: { username: pull.author.username, kind: pull.author.kind },
57 });
58 }
59 // A full page that does not reach back eight days may leave some out.
60 for (const list of [entry.open, entry.closed]) {
61 if (list.length >= PULL_PAGE) {
62 const oldest = Math.min(...list.map((pull) => Date.parse(pull.mergedAt ?? pull.updatedAt)));
63 if (oldest > since) complete = false;
64 }
65 }
66 }
67 const revisions: Record<string, number> = {};
68 for (const run of runs.value) {
69 if (run.number == null) continue;
70 const key = pullKey(run.repo, run.number);
71 revisions[key] = (revisions[key] ?? 0) + 1;
72 }
73 if (runs.value.length >= RUN_PAGE && Date.parse(runs.value[runs.value.length - 1].createdAt) > since) complete = false;
74 return { pulls, revisions, complete };
75}
76
77/** The workspace's newest sessions; `complete` when the list reaches back before today. */
78export async function loadSessions(viewer: User, slug: string, now: number, timeZone: string | null): Promise<SessionWork | null> {
79 const listed = await workspaceAgents.sessions(slug, viewer, { limit: SESSION_PAGE }).catch(warn("sessions"));
80 if (!listed?.ok) return null;
81 const sessions: AgentSession[] = listed.value;
82 const oldest = sessions.at(-1);
83 const complete = sessions.length < SESSION_PAGE || (oldest != null && dayIn(Date.parse(oldest.created_at), timeZone) !== dayIn(now, timeZone));
84 return { sessions, complete };
85}
86
87/** Sessions stopped at their cap that the viewer may approve more for, and whether they may manage agents. */
88export async function loadCapped(viewer: User, slug: string): Promise<{ capped: AgentSession[]; canManage: boolean } | null> {
89 const overview = await workspaceAgents.overview(slug, viewer).catch(warn("agents overview"));
90 if (!overview?.ok) return null;
91 return { capped: overview.value.waiting_on_you, canManage: overview.value.can_manage };
92}
93
94/** Today's spend from this month's statement. Null when billing did not answer. */
95export async function loadSpend(viewer: User, slug: string, now: number): Promise<Spend | null> {
96 const statement = await billing.statement(slug, viewer, null, "day").catch(warn("statement"));
97 return statement?.ok ? spendToday(statement.value, now) : null;
98}