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 | ||
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 8 | import { |
| 9 | type ActivityDigest, | |
| 10 | type AgentSession, | |
| 11 | type AgentsOverview, | |
| 12 | type ChatActivity, | |
| 13 | type Deployment, | |
| 14 | type InstallRequests, | |
| 15 | MAX_DIGEST_REPOS, | |
| 16 | type Memory, | |
| 17 | type ProjectDeploys, | |
| 18 | type Repo, | |
| 19 | type RepoPath, | |
| 20 | type User, | |
| 21 | type WorkflowRun, | |
| 22 | type WorkspaceAgent, | |
| 23 | notifyClient, | |
| 24 | } from "@g1t/contracts"; | |
| 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. | 25 | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 26 | import { type CodePull, type CodeWork, G1T_ACTOR, type RepoRef, 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. | 27 | import { monthSpan, spentMicros } from "./spend"; |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 28 | import { actions, agents, billing, chat, deployments, events, identity, repos, work, workspaceAgents } from "./services.server"; |
| 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. | 29 | |
| 30 | /** Pull requests read per project and list: `LIST_PAGE` in services/work. */ | |
| 31 | const PULL_PAGE = 100; | |
| 32 | /** The most projects whose pull requests are read. */ | |
| 33 | const MAX_PROJECTS = 50; | |
| 34 | /** The most projects whose builds are read for the span. */ | |
| 35 | const MAX_DEPLOY_PROJECTS = 20; | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 36 | /** The most projects whose workflow runs are read for Running now: those pushed to most in the span. */ |
| 37 | const MAX_WORKFLOW_PROJECTS = 10; | |
| 38 | /** Runs read per project: enough to find the ones not finished. */ | |
| 39 | const WORKFLOW_RUNS = 20; | |
| 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. | 40 | /** Revise runs and sessions read: the services' most in one list. */ |
| 41 | const RUN_PAGE = 200; | |
| 42 | const SESSION_PAGE = 200; | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 43 | /** The most member keys named in one lookup. */ |
| 44 | const MAX_NAMES = 200; | |
| 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. | 45 | const DAY = 24 * 60 * 60 * 1000; |
| 46 | ||
| 47 | const warn = (what: string) => (error: unknown) => { | |
| 48 | console.warn(`home: ${what} failed`, error); | |
| 49 | return null; | |
| 50 | }; | |
| 51 | ||
| 52 | /** | |
| 53 | * When the viewer was last on this workspace's Home, from notify, where it | |
| 54 | * is kept with them (the same on every device). Null when there is no | |
| 55 | * visit on record; undefined when notify did not answer. | |
| 56 | */ | |
| 57 | export async function loadLastVisit(viewer: User, slug: string): Promise<number | null | undefined> { | |
| 58 | if (!env.NOTIFY) return undefined; | |
| 59 | const visit = await notifyClient(env.NOTIFY).lastVisit(viewer, slug).catch(warn("last visit")); | |
| 60 | if (!visit) return undefined; | |
| 61 | return visit.seen_at ? Date.parse(visit.seen_at) : null; | |
| 62 | } | |
| 63 | ||
| 64 | /** Marks the viewer's visit at `at`, the time the page they looked at loaded. */ | |
| 65 | export async function markVisit(viewer: User, slug: string, at: number): Promise<boolean> { | |
| 66 | if (!env.NOTIFY) return false; | |
| 67 | const marked = await notifyClient(env.NOTIFY).markVisit(viewer, slug, new Date(at).toISOString()).catch(warn("mark visit")); | |
| 68 | return marked != null; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 72 | * The workspace's own repositories the viewer may read (no pull request |
| 73 | * working copies), as every Code section reads them. Null when repos did | |
| 74 | * not answer. | |
| 75 | */ | |
| 76 | export async function loadRepos(viewer: User, slug: string): Promise<Repo[] | null> { | |
| 77 | const list = await repos.list(viewer, { namespace: slug }).catch(warn("repos")); | |
| 78 | return list ? list.filter((repo) => !repo.forkOf && repo.namespace.toLowerCase() === slug) : null; | |
| 79 | } | |
| 80 | ||
| 81 | /** The repositories by id, as the digest's lines and links need them. */ | |
| 82 | export function repoRefs(list: Repo[] | null): Record<string, RepoRef> { | |
| 83 | const refs: Record<string, RepoRef> = {}; | |
| 84 | for (const repo of list ?? []) refs[repo.id] = { namespace: repo.namespace, name: repo.name, defaultBranch: repo.defaultBranch }; | |
| 85 | return refs; | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 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. | 89 | * The workspace's projects' pull requests, open and recently merged or |
| 90 | * closed, any author's, with how many times each agent's was sent back to | |
| 91 | * revise. `complete` when every list reaches back to `reach`. Null when | |
| 92 | * Code did not answer. | |
| 93 | */ | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 94 | export async function loadCodeWork(viewer: User, slug: string, reach: number, owned: Repo[] | null): Promise<CodeWork | null> { |
| 95 | const runs = await agents.listRuns(viewer, { workspace: slug, kind: "revise", limit: RUN_PAGE }).catch(warn("revise runs")); | |
| 96 | if (!owned || !runs?.ok) return null; | |
| 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. | 97 | const chosen = owned.slice(0, MAX_PROJECTS); |
| 98 | const byId = new Map<string, Repo>(chosen.map((repo) => [repo.id, repo])); | |
| 99 | const batch = chosen.length > 0 ? await work.pullsForRepos([...byId.keys()], viewer, PULL_PAGE).catch(warn("pull requests")) : []; | |
| 100 | if (!batch) return null; | |
| 101 | const pulls: CodePull[] = []; | |
| 102 | let complete = owned.length <= MAX_PROJECTS; | |
| 103 | for (const entry of batch) { | |
| 104 | const repo = byId.get(entry.repoId); | |
| 105 | if (!repo) continue; | |
| 106 | const path = { namespace: repo.namespace, name: repo.name }; | |
| 107 | for (const pull of [...entry.open, ...entry.closed]) { | |
| 108 | pulls.push({ | |
| 109 | repo: path, | |
| 110 | number: pull.number, | |
| 111 | title: pull.title, | |
| 112 | status: pull.status, | |
| 113 | mergedAt: pull.mergedAt, | |
| 114 | mergedBy: pull.mergedBy, | |
| 115 | createdAt: pull.createdAt, | |
| 116 | updatedAt: pull.updatedAt, | |
| 117 | author: { username: pull.author.username, kind: pull.author.kind }, | |
| 118 | }); | |
| 119 | } | |
| 120 | // A full page that does not reach back far enough may leave some out. | |
| 121 | for (const list of [entry.open, entry.closed]) { | |
| 122 | if (list.length >= PULL_PAGE) { | |
| 123 | const oldest = Math.min(...list.map((pull) => Date.parse(pull.mergedAt ?? pull.updatedAt))); | |
| 124 | if (oldest > reach) complete = false; | |
| 125 | } | |
| 126 | } | |
| 127 | } | |
| 128 | const revisions: Record<string, number> = {}; | |
| 129 | for (const run of runs.value) { | |
| 130 | if (run.number == null) continue; | |
| 131 | const key = pullKey(run.repo, run.number); | |
| 132 | revisions[key] = (revisions[key] ?? 0) + 1; | |
| 133 | } | |
| 134 | if (runs.value.length >= RUN_PAGE && Date.parse(runs.value[runs.value.length - 1].createdAt) > reach) complete = false; | |
| 135 | return { pulls, revisions, complete }; | |
| 136 | } | |
| 137 | ||
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 138 | /** |
| 139 | * What happened in the span, counted by who did it: the events service's | |
| 140 | * digest of the workspace's repositories (the first MAX_DIGEST_REPOS of | |
| 141 | * them; none without Code access) and of its artifacts. Null when events | |
| 142 | * did not answer. | |
| 143 | */ | |
| 144 | export async function loadActivity(slug: string, span: Pick<Span, "from" | "now">, owned: Repo[] | null): Promise<ActivityDigest | null> { | |
| 145 | const digest = await events | |
| 146 | .activityDigest({ | |
| 147 | repo_ids: (owned ?? []).slice(0, MAX_DIGEST_REPOS).map((repo) => repo.id), | |
| 148 | workspace: slug, | |
| 149 | from: new Date(span.from).toISOString(), | |
| 150 | until: new Date(span.now + 1).toISOString(), | |
| 151 | }) | |
| 152 | .catch(warn("activity digest")); | |
| 153 | if (!digest) return null; | |
| 154 | // More repositories than the digest reads: it says so. | |
| 155 | return owned && owned.length > MAX_DIGEST_REPOS ? { ...digest, complete: false } : digest; | |
| 156 | } | |
| 157 | ||
| 158 | /** What was said in the span where the viewer can read, counted. Null when chat did not answer. */ | |
| 159 | export async function loadChatActivity(viewer: User, slug: string, span: Pick<Span, "from" | "now">): Promise<ChatActivity | null> { | |
| 160 | const activity = await chat | |
| 161 | .activity(slug, viewer, { from: new Date(span.from).toISOString(), until: new Date(span.now + 1).toISOString() }) | |
| 162 | .catch(warn("chat activity")); | |
| 163 | return activity?.ok ? activity.value : null; | |
| 164 | } | |
| 165 | ||
| 166 | /** Every member key the digest and chat name. */ | |
| 167 | export function actorKeys(activity: ActivityDigest | null, chatActivity: ChatActivity | null): string[] { | |
| 168 | const keys = new Set<string>(); | |
| 169 | for (const repo of activity?.repos ?? []) { | |
| 170 | for (const by of [...repo.pushes.by, ...repo.pushes.default_branch.by]) keys.add(by.actor); | |
| 171 | for (const list of [repo.pulls.opened, repo.pulls.merged, repo.pulls.closed, repo.issues.opened, repo.issues.closed, repo.reviews, repo.comments, repo.deployments.succeeded, repo.deployments.failed]) { | |
| 172 | for (const entry of list) keys.add(entry.actor); | |
| 173 | } | |
| 174 | for (const release of repo.releases) keys.add(release.actor); | |
| 175 | for (const pkg of repo.packages) keys.add(pkg.actor); | |
| 176 | } | |
| 177 | for (const entry of activity?.folios?.created ?? []) keys.add(entry.actor); | |
| 178 | for (const folio of activity?.folios?.edited ?? []) for (const author of folio.authors) keys.add(author); | |
| 179 | for (const author of chatActivity?.authors ?? []) keys.add(author.key); | |
| 180 | keys.delete(""); | |
| 181 | return [...keys]; | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * The names behind member keys: people's usernames from identity (one | |
| 186 | * lookup for them all), agents' handles from the workspace's agents, g1t | |
| 187 | * as itself. A key identity does not know is left out, and shows as | |
| 188 | * "someone". | |
| 189 | */ | |
| 190 | export async function loadNames(keys: string[], agentsList: Pick<WorkspaceAgent, "id" | "handle">[] | null): Promise<Record<string, string>> { | |
| 191 | const names: Record<string, string> = {}; | |
| 192 | const userIds: string[] = []; | |
| 193 | const byAgentId = new Map((agentsList ?? []).map((agent) => [agent.id, agent.handle])); | |
| 194 | for (const key of keys.slice(0, MAX_NAMES)) { | |
| Four small things seen today. An agent can attach a file it made to any doc the person who asked can edit, whatever the space's agent mode: attaching changes nothing in the doc, so the suggest mode that stopped every PDF and spreadsheet no longer does; the ability is its own, attach, beside read, suggest and edit. Home no longer counts robots among the people: events that name an agent by its own id, or g1t's upkeep as the workspace or as g1t, read as the agent and as g1t, in the digest and on the page, so the faces say who instead of someone. Tooltips are the inverted bubble, the page's foreground behind the page's background, with no border. The Spend page's slice tabs keep their ring inside the row instead of losing its top to the scroll edge, and the Apps launcher keeps its search box and footer in place while it reads which apps there are, with skeleton tiles between. | 195 | if (key === G1T_ACTOR || key === "user:g1t" || key.startsWith("user:wsp_")) names[key] = "g1t"; |
| 196 | else if (key.startsWith("agent:") || key.startsWith("user:agt_")) { | |
| 197 | // An agent, by its own id: under `agent:` as events name it, or under `user:` from older ones. | |
| 198 | const handle = byAgentId.get(key.replace(/^(agent|user):/, "")); | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 199 | if (handle) names[key] = handle; |
| 200 | } else if (key.startsWith("user:")) userIds.push(key.slice("user:".length)); | |
| 201 | } | |
| 202 | if (userIds.length > 0) { | |
| 203 | const found = await identity.usernames(userIds).catch(warn("usernames")); | |
| 204 | for (const [id, username] of Object.entries(found ?? {})) names[`user:${id}`] = username; | |
| 205 | } | |
| 206 | return names; | |
| 207 | } | |
| 208 | ||
| 209 | /** | |
| 210 | * The newest workflow runs of the projects pushed to most in the span (at | |
| 211 | * most MAX_WORKFLOW_PROJECTS), for the ones still going. A project whose | |
| 212 | * runs could not be read is left out; `complete` says whether every one was. | |
| 213 | */ | |
| 214 | export async function loadWorkflowRuns( | |
| 215 | viewer: User, | |
| 216 | activity: ActivityDigest | null, | |
| 217 | owned: Repo[] | null, | |
| 218 | ): Promise<{ projects: { repo: RepoPath; runs: WorkflowRun[] }[]; complete: boolean }> { | |
| 219 | const byId = new Map((owned ?? []).map((repo) => [repo.id, repo])); | |
| 220 | const active = [...(activity?.repos ?? [])] | |
| 221 | .filter((repo) => repo.pushes.count > 0 && byId.has(repo.repo_id)) | |
| 222 | .sort((a, b) => b.pushes.count - a.pushes.count || a.repo_id.localeCompare(b.repo_id)) | |
| 223 | .slice(0, MAX_WORKFLOW_PROJECTS); | |
| 224 | const lists = await Promise.all( | |
| 225 | active.map((digest) => { | |
| 226 | const repo = byId.get(digest.repo_id)!; | |
| 227 | const path = { namespace: repo.namespace, name: repo.name }; | |
| 228 | return actions | |
| 229 | .runs(path, viewer, { limit: WORKFLOW_RUNS }) | |
| 230 | .then((result) => (result.ok ? { repo: path, runs: result.value } : null)) | |
| 231 | .catch(warn(`workflow runs of ${repo.name}`)); | |
| 232 | }), | |
| 233 | ); | |
| 234 | const projects = lists.filter((entry) => entry != null); | |
| 235 | return { projects, complete: projects.length === active.length }; | |
| 236 | } | |
| 237 | ||
| 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. | 238 | /** The workspace's newest sessions; `complete` when the list reaches back before `from`. */ |
| 239 | export async function loadSessions(viewer: User, slug: string, from: number): Promise<SessionWork | null> { | |
| 240 | const listed = await workspaceAgents.sessions(slug, viewer, { limit: SESSION_PAGE }).catch(warn("sessions")); | |
| 241 | if (!listed?.ok) return null; | |
| 242 | const sessions: AgentSession[] = listed.value; | |
| 243 | const oldest = sessions.at(-1); | |
| 244 | const complete = sessions.length < SESSION_PAGE || (oldest != null && Date.parse(oldest.created_at) < from); | |
| 245 | return { sessions, complete }; | |
| 246 | } | |
| 247 | ||
| 248 | /** The agents overview: sessions waiting on the viewer, live ones, and the workspace's budget alert. */ | |
| 249 | export async function loadAgentsOverview(viewer: User, slug: string): Promise<AgentsOverview | null> { | |
| 250 | const overview = await workspaceAgents.overview(slug, viewer).catch(warn("agents overview")); | |
| 251 | return overview?.ok ? overview.value : null; | |
| 252 | } | |
| 253 | ||
| 254 | /** Install requests as the viewer sees them; only an owner's (`can_resolve`) are anyone else's. */ | |
| 255 | export async function loadInstallRequests(viewer: User, slug: string): Promise<InstallRequests | null> { | |
| 256 | const requests = await workspaceAgents.installRequests(slug, viewer).catch(warn("install requests")); | |
| 257 | return requests?.ok ? requests.value : null; | |
| 258 | } | |
| 259 | ||
| 260 | /** The workspace's memory, for the decisions recorded in it. */ | |
| 261 | export async function loadMemories(viewer: User, slug: string): Promise<Memory[] | null> { | |
| 262 | const memories = await agents.listMemories(viewer, slug, null).catch(warn("memories")); | |
| 263 | return memories?.ok ? memories.value.workspace : null; | |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Every project's builds at a glance, and the builds of the projects that | |
| 268 | * deploy, for the span: the projects whose newest build is older than the | |
| 269 | * span are not read again. | |
| 270 | */ | |
| 271 | export async function loadDeploys( | |
| 272 | viewer: User, | |
| 273 | slug: string, | |
| 274 | from: number, | |
| 275 | ): Promise<{ overview: ProjectDeploys[]; projects: { slug: string; deployments: Deployment[] }[]; complete: boolean } | null> { | |
| 276 | const overview = await deployments.overview(slug, viewer).catch(warn("deploys overview")); | |
| 277 | if (!overview?.ok) return null; | |
| 278 | const recent = overview.value.filter((project) => project.latest && Date.parse(project.latest.finishedAt ?? project.latest.createdAt) >= from); | |
| 279 | const read = recent.slice(0, MAX_DEPLOY_PROJECTS); | |
| 280 | const lists = await Promise.all( | |
| 281 | read.map((project) => | |
| 282 | deployments | |
| 283 | .list({ workspace: slug, slug: project.slug }, viewer) | |
| 284 | .then((result) => (result.ok ? { slug: project.slug, deployments: result.value.deployments } : null)) | |
| 285 | .catch(warn(`deploys of ${project.slug}`)), | |
| 286 | ), | |
| 287 | ); | |
| 288 | const projects = lists.filter((entry) => entry != null); | |
| 289 | return { overview: overview.value, projects, complete: projects.length === recent.length }; | |
| 290 | } | |
| 291 | ||
| 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. | 292 | /** |
| 293 | * The span's spend from the statements of the months it touches, and the | |
| 294 | * month so far from the usage report the top bar, Spend and Usage read | |
| 295 | * (`spend.ts` `monthSpan`, `spentMicros`), so Home's figure is theirs. Null | |
| 296 | * when billing did not answer; the month alone null when only it did not. | |
| 297 | */ | |
| 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. | 298 | export async function loadSpend(viewer: User, slug: string, span: Pick<Span, "from" | "now">): Promise<Spend | null> { |
| 299 | 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. | 300 | const [statements, month] = await Promise.all([ |
| 301 | Promise.all(months.map((month) => billing.statement(slug, viewer, month, "day").catch(warn(`statement ${month}`)))), | |
| 302 | billing | |
| 303 | .usageReport(slug, viewer, monthSpan(new Date(span.now))) | |
| 304 | .then((result) => (result.ok ? spentMicros(result.value) : null)) | |
| 305 | .catch(warn("month usage")), | |
| 306 | ]); | |
| 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. | 307 | if (statements.some((statement) => !statement?.ok)) return null; |
| 308 | return spendIn( | |
| 309 | statements.map((statement) => (statement as Extract<typeof statement, { ok: true }>).value), | |
| 310 | 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. | 311 | 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. | 312 | ); |
| 313 | } | |
| 314 | ||
| 315 | /** How far back Code is read: 7 days before the span, for the comparison. */ | |
| 316 | export function reachFor(span: Pick<Span, "from">): number { | |
| 317 | return span.from - 7 * DAY; | |
| 318 | } |