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.
| Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all | 1 | /** |
| 2 | * The inbox: what needs a person, or what they follow, as it happens. The | |
| 3 | * events service keeps it and writes items as events arrive. Mirrors | |
| 4 | * `crates/contracts/src/inbox.rs`, which says who is told of what. | |
| 5 | */ | |
| 6 | import type { ServiceBinding } from "./clients"; | |
| 7 | import type { User } from "./identity"; | |
| 8 | ||
| 9 | /** | |
| 10 | * How much an item matters: a failure, something a person must answer (an | |
| 11 | * agent waiting on them), something that went well, or something to know. | |
| 12 | */ | |
| 13 | export type InboxSeverity = "error" | "warning" | "success" | "info"; | |
| 14 | ||
| 15 | export const INBOX_SEVERITIES: InboxSeverity[] = ["error", "warning", "success", "info"]; | |
| 16 | ||
| 17 | export type InboxSubjectKind = "issue" | "pull" | "run"; | |
| 18 | ||
| 19 | export type InboxItem = { | |
| 20 | id: string; | |
| 21 | /** Why they were told, such as `checks_failed`, `agent_asked` or `mentioned`. */ | |
| 22 | reason: string; | |
| 23 | severity: InboxSeverity; | |
| 24 | /** One line: what happened, and where. */ | |
| 25 | title: string; | |
| 26 | /** One line: what it happened to, such as the pull request's title. */ | |
| 27 | body: string; | |
| 28 | /** `owner/name`. */ | |
| 29 | repo: string | null; | |
| 30 | workspace: string | null; | |
| 31 | subject: InboxSubjectKind | null; | |
| 32 | number: number | null; | |
| 33 | /** A path on g1t.sh, such as `/acme/rocket/pull/12`. */ | |
| 34 | url: string; | |
| 35 | /** A username, or `g1t`. */ | |
| 36 | actor: string | null; | |
| 37 | /** RFC 3339. */ | |
| 38 | createdAt: string; | |
| 39 | readAt: string | null; | |
| 40 | doneAt: string | null; | |
| 41 | saved: boolean; | |
| 42 | snoozedUntil: string | null; | |
| 43 | }; | |
| 44 | ||
| 45 | /** The inbox itself (not done, not snoozed), what was saved, or what is done. */ | |
| 46 | export type InboxView = "inbox" | "saved" | "done"; | |
| 47 | ||
| 48 | export type InboxQuery = { | |
| 49 | view?: InboxView; | |
| 50 | severity?: InboxSeverity | null; | |
| 51 | unread?: boolean; | |
| 52 | /** The `next` of the page before. */ | |
| 53 | before?: string | null; | |
| 54 | /** At most 100. */ | |
| 55 | limit?: number; | |
| 56 | }; | |
| 57 | ||
| 58 | export type InboxPage = { items: InboxItem[]; next: string | null }; | |
| 59 | ||
| 60 | /** Unread items in the inbox, by severity. */ | |
| 61 | export type InboxCounts = { unread: number } & Record<InboxSeverity, number>; | |
| 62 | ||
| 63 | export type InboxMark = "read" | "unread" | "done" | "undone" | "save" | "unsave" | "snooze"; | |
| 64 | ||
| 65 | export type InboxMarkArgs = { | |
| 66 | mark: InboxMark; | |
| 67 | /** At most 100. */ | |
| 68 | ids?: string[]; | |
| 69 | /** Every item in the inbox, when `ids` is empty: Mark all read. */ | |
| 70 | all?: boolean; | |
| 71 | severity?: InboxSeverity | null; | |
| 72 | /** For `snooze`: RFC 3339, later than now. */ | |
| 73 | until?: string | null; | |
| 74 | }; | |
| 75 | ||
| 76 | export interface InboxApi { | |
| 77 | /** | |
| 78 | * Newest first; in the inbox unfiltered, unread warnings first. Items about | |
| 79 | * a repository the viewer can no longer read are dropped. | |
| 80 | */ | |
| 81 | list(viewer: User, query?: InboxQuery): Promise<InboxPage>; | |
| 82 | counts(username: string): Promise<InboxCounts>; | |
| 83 | /** Changes the person's own items. Returns how many changed. */ | |
| 84 | mark(username: string, args: InboxMarkArgs): Promise<number>; | |
| 85 | } | |
| 86 | ||
| 87 | /** The inbox, which the events service keeps. */ | |
| 88 | export function inboxClient(events: ServiceBinding): InboxApi { | |
| 89 | const call = async <T>(method: string, args: object): Promise<T> => { | |
| 90 | const response = await events.fetch(`https://service/rpc/${method}`, { | |
| 91 | method: "POST", | |
| 92 | headers: { "content-type": "application/json" }, | |
| 93 | body: JSON.stringify(args), | |
| 94 | }); | |
| 95 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 96 | return (await response.json()) as T; | |
| 97 | }; | |
| 98 | return { | |
| 99 | list: (viewer, query = {}) => call("inbox_list", { viewer, ...query }), | |
| 100 | counts: (username) => call("inbox_counts", { username }), | |
| 101 | mark: (username, args) => call("inbox_mark", { username, ...args }), | |
| 102 | }; | |
| 103 | } |