Skip to content
183 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";
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.11import { monthSpan, spentMicros } from "./spend";
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.12import { agents, billing, deployments, repos, work, workspaceAgents } from "./services.server";
13
14/** Pull requests read per project and list: `LIST_PAGE` in services/work. */
15const PULL_PAGE = 100;
16/** The most projects whose pull requests are read. */
17const MAX_PROJECTS = 50;
18/** The most projects whose builds are read for the span. */
19const MAX_DEPLOY_PROJECTS = 20;
20/** Revise runs and sessions read: the services' most in one list. */
21const RUN_PAGE = 200;
22const SESSION_PAGE = 200;
23const DAY = 24 * 60 * 60 * 1000;
24
25const warn = (what: string) => (error: unknown) => {
26 console.warn(`home: ${what} failed`, error);
27 return null;
28};
29
30/**
31 * When the viewer was last on this workspace's Home, from notify, where it
32 * is kept with them (the same on every device). Null when there is no
33 * visit on record; undefined when notify did not answer.
34 */
35export async function loadLastVisit(viewer: User, slug: string): Promise<number | null | undefined> {
36 if (!env.NOTIFY) return undefined;
37 const visit = await notifyClient(env.NOTIFY).lastVisit(viewer, slug).catch(warn("last visit"));
38 if (!visit) return undefined;
39 return visit.seen_at ? Date.parse(visit.seen_at) : null;
40}
41
42/** Marks the viewer's visit at `at`, the time the page they looked at loaded. */
43export async function markVisit(viewer: User, slug: string, at: number): Promise<boolean> {
44 if (!env.NOTIFY) return false;
45 const marked = await notifyClient(env.NOTIFY).markVisit(viewer, slug, new Date(at).toISOString()).catch(warn("mark visit"));
46 return marked != null;
47}
48
49/**
50 * The workspace's projects' pull requests, open and recently merged or
51 * closed, any author's, with how many times each agent's was sent back to
52 * revise. `complete` when every list reaches back to `reach`. Null when
53 * Code did not answer.
54 */
55export async function loadCodeWork(viewer: User, slug: string, reach: number): Promise<CodeWork | null> {
56 const [list, runs] = await Promise.all([
57 repos.list(viewer, { namespace: slug }).catch(warn("repos")),
58 agents.listRuns(viewer, { workspace: slug, kind: "revise", limit: RUN_PAGE }).catch(warn("revise runs")),
59 ]);
60 if (!list || !runs?.ok) return null;
61 const owned = list.filter((repo) => !repo.forkOf && repo.namespace.toLowerCase() === slug);
62 const chosen = owned.slice(0, MAX_PROJECTS);
63 const byId = new Map<string, Repo>(chosen.map((repo) => [repo.id, repo]));
64 const batch = chosen.length > 0 ? await work.pullsForRepos([...byId.keys()], viewer, PULL_PAGE).catch(warn("pull requests")) : [];
65 if (!batch) return null;
66 const pulls: CodePull[] = [];
67 let complete = owned.length <= MAX_PROJECTS;
68 for (const entry of batch) {
69 const repo = byId.get(entry.repoId);
70 if (!repo) continue;
71 const path = { namespace: repo.namespace, name: repo.name };
72 for (const pull of [...entry.open, ...entry.closed]) {
73 pulls.push({
74 repo: path,
75 number: pull.number,
76 title: pull.title,
77 status: pull.status,
78 mergedAt: pull.mergedAt,
79 mergedBy: pull.mergedBy,
80 createdAt: pull.createdAt,
81 updatedAt: pull.updatedAt,
82 author: { username: pull.author.username, kind: pull.author.kind },
83 });
84 }
85 // A full page that does not reach back far enough may leave some out.
86 for (const list of [entry.open, entry.closed]) {
87 if (list.length >= PULL_PAGE) {
88 const oldest = Math.min(...list.map((pull) => Date.parse(pull.mergedAt ?? pull.updatedAt)));
89 if (oldest > reach) complete = false;
90 }
91 }
92 }
93 const revisions: Record<string, number> = {};
94 for (const run of runs.value) {
95 if (run.number == null) continue;
96 const key = pullKey(run.repo, run.number);
97 revisions[key] = (revisions[key] ?? 0) + 1;
98 }
99 if (runs.value.length >= RUN_PAGE && Date.parse(runs.value[runs.value.length - 1].createdAt) > reach) complete = false;
100 return { pulls, revisions, complete };
101}
102
103/** The workspace's newest sessions; `complete` when the list reaches back before `from`. */
104export async function loadSessions(viewer: User, slug: string, from: number): Promise<SessionWork | null> {
105 const listed = await workspaceAgents.sessions(slug, viewer, { limit: SESSION_PAGE }).catch(warn("sessions"));
106 if (!listed?.ok) return null;
107 const sessions: AgentSession[] = listed.value;
108 const oldest = sessions.at(-1);
109 const complete = sessions.length < SESSION_PAGE || (oldest != null && Date.parse(oldest.created_at) < from);
110 return { sessions, complete };
111}
112
113/** The agents overview: sessions waiting on the viewer, live ones, and the workspace's budget alert. */
114export async function loadAgentsOverview(viewer: User, slug: string): Promise<AgentsOverview | null> {
115 const overview = await workspaceAgents.overview(slug, viewer).catch(warn("agents overview"));
116 return overview?.ok ? overview.value : null;
117}
118
119/** Install requests as the viewer sees them; only an owner's (`can_resolve`) are anyone else's. */
120export async function loadInstallRequests(viewer: User, slug: string): Promise<InstallRequests | null> {
121 const requests = await workspaceAgents.installRequests(slug, viewer).catch(warn("install requests"));
122 return requests?.ok ? requests.value : null;
123}
124
125/** The workspace's memory, for the decisions recorded in it. */
126export async function loadMemories(viewer: User, slug: string): Promise<Memory[] | null> {
127 const memories = await agents.listMemories(viewer, slug, null).catch(warn("memories"));
128 return memories?.ok ? memories.value.workspace : null;
129}
130
131/**
132 * Every project's builds at a glance, and the builds of the projects that
133 * deploy, for the span: the projects whose newest build is older than the
134 * span are not read again.
135 */
136export async function loadDeploys(
137 viewer: User,
138 slug: string,
139 from: number,
140): Promise<{ overview: ProjectDeploys[]; projects: { slug: string; deployments: Deployment[] }[]; complete: boolean } | null> {
141 const overview = await deployments.overview(slug, viewer).catch(warn("deploys overview"));
142 if (!overview?.ok) return null;
143 const recent = overview.value.filter((project) => project.latest && Date.parse(project.latest.finishedAt ?? project.latest.createdAt) >= from);
144 const read = recent.slice(0, MAX_DEPLOY_PROJECTS);
145 const lists = await Promise.all(
146 read.map((project) =>
147 deployments
148 .list({ workspace: slug, slug: project.slug }, viewer)
149 .then((result) => (result.ok ? { slug: project.slug, deployments: result.value.deployments } : null))
150 .catch(warn(`deploys of ${project.slug}`)),
151 ),
152 );
153 const projects = lists.filter((entry) => entry != null);
154 return { overview: overview.value, projects, complete: projects.length === recent.length };
155}
156
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.157/**
158 * The span's spend from the statements of the months it touches, and the
159 * month so far from the usage report the top bar, Spend and Usage read
160 * (`spend.ts` `monthSpan`, `spentMicros`), so Home's figure is theirs. Null
161 * when billing did not answer; the month alone null when only it did not.
162 */
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.163export async function loadSpend(viewer: User, slug: string, span: Pick<Span, "from" | "now">): Promise<Spend | null> {
164 const months = spanMonths(span);
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.165 const [statements, month] = await Promise.all([
166 Promise.all(months.map((month) => billing.statement(slug, viewer, month, "day").catch(warn(`statement ${month}`)))),
167 billing
168 .usageReport(slug, viewer, monthSpan(new Date(span.now)))
169 .then((result) => (result.ok ? spentMicros(result.value) : null))
170 .catch(warn("month usage")),
171 ]);
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.172 if (statements.some((statement) => !statement?.ok)) return null;
173 return spendIn(
174 statements.map((statement) => (statement as Extract<typeof statement, { ok: true }>).value),
175 span,
Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means.176 month,
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.177 );
178}
179
180/** How far back Code is read: 7 days before the span, for the comparison. */
181export function reachFor(span: Pick<Span, "from">): number {
182 return span.from - 7 * DAY;
183}