Skip to content
170 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 front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.1/**
2 * Home'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 ./home.ts.
5 */
6import { env } from "cloudflare:workers";
7
8import { type AgentSession, type AgentsOverview, type Deployment, type InstallRequests, type Memory, type ProjectDeploys, type Repo, type User, notifyClient } from "@g1t/contracts";
9
10import { type CodePull, type CodeWork, type SessionWork, type Span, type Spend, pullKey, spanMonths, spendIn } from "./home";
11import { agents, billing, deployments, repos, work, workspaceAgents } from "./services.server";
12
13/** Pull requests read per project and list: `LIST_PAGE` in services/work. */
14const PULL_PAGE = 100;
15/** The most projects whose pull requests are read. */
16const MAX_PROJECTS = 50;
17/** The most projects whose builds are read for the span. */
18const MAX_DEPLOY_PROJECTS = 20;
19/** Revise runs and sessions read: the services' most in one list. */
20const RUN_PAGE = 200;
21const SESSION_PAGE = 200;
22const DAY = 24 * 60 * 60 * 1000;
23
24const warn = (what: string) => (error: unknown) => {
25 console.warn(`home: ${what} failed`, error);
26 return null;
27};
28
29/**
30 * When the viewer was last on this workspace's Home, from notify, where it
31 * is kept with them (the same on every device). Null when there is no
32 * visit on record; undefined when notify did not answer.
33 */
34export async function loadLastVisit(viewer: User, slug: string): Promise<number | null | undefined> {
35 if (!env.NOTIFY) return undefined;
36 const visit = await notifyClient(env.NOTIFY).lastVisit(viewer, slug).catch(warn("last visit"));
37 if (!visit) return undefined;
38 return visit.seen_at ? Date.parse(visit.seen_at) : null;
39}
40
41/** Marks the viewer's visit at `at`, the time the page they looked at loaded. */
42export async function markVisit(viewer: User, slug: string, at: number): Promise<boolean> {
43 if (!env.NOTIFY) return false;
44 const marked = await notifyClient(env.NOTIFY).markVisit(viewer, slug, new Date(at).toISOString()).catch(warn("mark visit"));
45 return marked != null;
46}
47
48/**
49 * The workspace's projects' pull requests, open and recently merged or
50 * closed, any author's, with how many times each agent's was sent back to
51 * revise. `complete` when every list reaches back to `reach`. Null when
52 * Code did not answer.
53 */
54export async function loadCodeWork(viewer: User, slug: string, reach: number): Promise<CodeWork | null> {
55 const [list, runs] = await Promise.all([
56 repos.list(viewer, { namespace: slug }).catch(warn("repos")),
57 agents.listRuns(viewer, { workspace: slug, kind: "revise", limit: RUN_PAGE }).catch(warn("revise runs")),
58 ]);
59 if (!list || !runs?.ok) return null;
60 const owned = list.filter((repo) => !repo.forkOf && repo.namespace.toLowerCase() === slug);
61 const chosen = owned.slice(0, MAX_PROJECTS);
62 const byId = new Map<string, Repo>(chosen.map((repo) => [repo.id, repo]));
63 const batch = chosen.length > 0 ? await work.pullsForRepos([...byId.keys()], viewer, PULL_PAGE).catch(warn("pull requests")) : [];
64 if (!batch) return null;
65 const pulls: CodePull[] = [];
66 let complete = owned.length <= MAX_PROJECTS;
67 for (const entry of batch) {
68 const repo = byId.get(entry.repoId);
69 if (!repo) continue;
70 const path = { namespace: repo.namespace, name: repo.name };
71 for (const pull of [...entry.open, ...entry.closed]) {
72 pulls.push({
73 repo: path,
74 number: pull.number,
75 title: pull.title,
76 status: pull.status,
77 mergedAt: pull.mergedAt,
78 mergedBy: pull.mergedBy,
79 createdAt: pull.createdAt,
80 updatedAt: pull.updatedAt,
81 author: { username: pull.author.username, kind: pull.author.kind },
82 });
83 }
84 // A full page that does not reach back far enough may leave some out.
85 for (const list of [entry.open, entry.closed]) {
86 if (list.length >= PULL_PAGE) {
87 const oldest = Math.min(...list.map((pull) => Date.parse(pull.mergedAt ?? pull.updatedAt)));
88 if (oldest > reach) complete = false;
89 }
90 }
91 }
92 const revisions: Record<string, number> = {};
93 for (const run of runs.value) {
94 if (run.number == null) continue;
95 const key = pullKey(run.repo, run.number);
96 revisions[key] = (revisions[key] ?? 0) + 1;
97 }
98 if (runs.value.length >= RUN_PAGE && Date.parse(runs.value[runs.value.length - 1].createdAt) > reach) complete = false;
99 return { pulls, revisions, complete };
100}
101
102/** The workspace's newest sessions; `complete` when the list reaches back before `from`. */
103export async function loadSessions(viewer: User, slug: string, from: number): Promise<SessionWork | null> {
104 const listed = await workspaceAgents.sessions(slug, viewer, { limit: SESSION_PAGE }).catch(warn("sessions"));
105 if (!listed?.ok) return null;
106 const sessions: AgentSession[] = listed.value;
107 const oldest = sessions.at(-1);
108 const complete = sessions.length < SESSION_PAGE || (oldest != null && Date.parse(oldest.created_at) < from);
109 return { sessions, complete };
110}
111
112/** The agents overview: sessions waiting on the viewer, live ones, and the workspace's budget alert. */
113export async function loadAgentsOverview(viewer: User, slug: string): Promise<AgentsOverview | null> {
114 const overview = await workspaceAgents.overview(slug, viewer).catch(warn("agents overview"));
115 return overview?.ok ? overview.value : null;
116}
117
118/** Install requests as the viewer sees them; only an owner's (`can_resolve`) are anyone else's. */
119export async function loadInstallRequests(viewer: User, slug: string): Promise<InstallRequests | null> {
120 const requests = await workspaceAgents.installRequests(slug, viewer).catch(warn("install requests"));
121 return requests?.ok ? requests.value : null;
122}
123
124/** The workspace's memory, for the decisions recorded in it. */
125export async function loadMemories(viewer: User, slug: string): Promise<Memory[] | null> {
126 const memories = await agents.listMemories(viewer, slug, null).catch(warn("memories"));
127 return memories?.ok ? memories.value.workspace : null;
128}
129
130/**
131 * Every project's builds at a glance, and the builds of the projects that
132 * deploy, for the span: the projects whose newest build is older than the
133 * span are not read again.
134 */
135export async function loadDeploys(
136 viewer: User,
137 slug: string,
138 from: number,
139): Promise<{ overview: ProjectDeploys[]; projects: { slug: string; deployments: Deployment[] }[]; complete: boolean } | null> {
140 const overview = await deployments.overview(slug, viewer).catch(warn("deploys overview"));
141 if (!overview?.ok) return null;
142 const recent = overview.value.filter((project) => project.latest && Date.parse(project.latest.finishedAt ?? project.latest.createdAt) >= from);
143 const read = recent.slice(0, MAX_DEPLOY_PROJECTS);
144 const lists = await Promise.all(
145 read.map((project) =>
146 deployments
147 .list({ workspace: slug, slug: project.slug }, viewer)
148 .then((result) => (result.ok ? { slug: project.slug, deployments: result.value.deployments } : null))
149 .catch(warn(`deploys of ${project.slug}`)),
150 ),
151 );
152 const projects = lists.filter((entry) => entry != null);
153 return { overview: overview.value, projects, complete: projects.length === recent.length };
154}
155
156/** The span's spend from the statements of the months it touches. Null when billing did not answer. */
157export async function loadSpend(viewer: User, slug: string, span: Pick<Span, "from" | "now">): Promise<Spend | null> {
158 const months = spanMonths(span);
159 const statements = await Promise.all(months.map((month) => billing.statement(slug, viewer, month, "day").catch(warn(`statement ${month}`))));
160 if (statements.some((statement) => !statement?.ok)) return null;
161 return spendIn(
162 statements.map((statement) => (statement as Extract<typeof statement, { ok: true }>).value),
163 span,
164 );
165}
166
167/** How far back Code is read: 7 days before the span, for the comparison. */
168export function reachFor(span: Pick<Span, "from">): number {
169 return span.from - 7 * DAY;
170}