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