Skip to content

g1t/packages/contracts/src/inbox.ts

103 lines3,515 bytesCodeBlame

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 all1/**
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 */
6import type { ServiceBinding } from "./clients";
7import 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 */
13export type InboxSeverity = "error" | "warning" | "success" | "info";
14
15export const INBOX_SEVERITIES: InboxSeverity[] = ["error", "warning", "success", "info"];
16
17export type InboxSubjectKind = "issue" | "pull" | "run";
18
19export 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. */
46export type InboxView = "inbox" | "saved" | "done";
47
48export 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
58export type InboxPage = { items: InboxItem[]; next: string | null };
59
60/** Unread items in the inbox, by severity. */
61export type InboxCounts = { unread: number } & Record<InboxSeverity, number>;
62
63export type InboxMark = "read" | "unread" | "done" | "undone" | "save" | "unsave" | "snooze";
64
65export 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
76export 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. */
88export 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}