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 | */ | |
| 6 | import { env } from "cloudflare:workers"; | |
| 7 | ||
| 8 | import { type AgentSession, type AgentsOverview, type Deployment, type InstallRequests, type Memory, type ProjectDeploys, type Repo, type User, notifyClient } from "@g1t/contracts"; | |
| 9 | ||
| 10 | import { type CodePull, type CodeWork, type SessionWork, type Span, type Spend, pullKey, spanMonths, spendIn } from "./home"; | |
| 11 | import { agents, billing, deployments, repos, work, workspaceAgents } from "./services.server"; | |
| 12 | ||
| 13 | /** Pull requests read per project and list: `LIST_PAGE` in services/work. */ | |
| 14 | const PULL_PAGE = 100; | |
| 15 | /** The most projects whose pull requests are read. */ | |
| 16 | const MAX_PROJECTS = 50; | |
| 17 | /** The most projects whose builds are read for the span. */ | |
| 18 | const MAX_DEPLOY_PROJECTS = 20; | |
| 19 | /** Revise runs and sessions read: the services' most in one list. */ | |
| 20 | const RUN_PAGE = 200; | |
| 21 | const SESSION_PAGE = 200; | |
| 22 | const DAY = 24 * 60 * 60 * 1000; | |
| 23 | ||
| 24 | const 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 | */ | |
| 34 | export 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. */ | |
| 42 | export 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 | */ | |
| 54 | export 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`. */ | |
| 103 | export 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. */ | |
| 113 | export 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. */ | |
| 119 | export 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. */ | |
| 125 | export 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 | */ | |
| 135 | export 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. */ | |
| 157 | export 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. */ | |
| 168 | export function reachFor(span: Pick<Span, "from">): number { | |
| 169 | return span.from - 7 * DAY; | |
| 170 | } |