Skip to content
89 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.

Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1/**
2 * How agents' cards look, and what they offer, by state: pure, so it is
3 * tested on its own (cards.ts acts on them, sessions.ts posts them).
4 */
5import type { MessageCard } from "@g1t/contracts";
6
7import type { DraftRow } from "./cards.ts";
8
9/** A draft issue's card: what it would file, and the buttons. */
10export function draftCard(draft: Pick<DraftRow, "id" | "repo" | "title" | "body" | "labels" | "status" | "filed_by" | "number">): MessageCard {
11 const labels = (() => {
12 try {
13 return (JSON.parse(draft.labels || "[]") as string[]).join(", ");
14 } catch {
15 return "";
16 }
17 })();
18 const fields = [{ label: "Repository", value: draft.repo }, ...(labels ? [{ label: "Labels", value: labels }] : [])];
19 const preview = draft.body.length > 900 ? `${draft.body.slice(0, 900)}…` : draft.body;
20 if (draft.status === "filed" && draft.number) {
21 const href = `/${draft.repo}/issues/${draft.number}`;
22 return {
23 kind: "draft_issue",
24 title: draft.title,
25 detail: `Filed as ${draft.repo}#${draft.number}${draft.filed_by ? ` by @${draft.filed_by}` : ""}`,
26 state: "Filed",
27 href,
28 fields,
29 actions: [{ id: "open", label: "Open issue", href }],
30 owner: "agents",
31 ref: draft.id,
32 };
33 }
34 if (draft.status === "discarded") {
35 return { kind: "draft_issue", title: draft.title, detail: "Discarded", state: "Discarded", href: null, owner: "agents", ref: draft.id };
36 }
37 return {
38 kind: "draft_issue",
39 title: draft.title,
40 detail: `A draft issue for ${draft.repo}. Filing it files it as you.`,
41 state: "Draft",
42 href: null,
43 body: preview,
44 fields,
45 actions: [
46 { id: "file", label: "File issue", style: "primary" },
47 { id: "discard", label: "Discard" },
48 ],
49 owner: "agents",
50 ref: draft.id,
51 };
52}
53
54/** Money as people type it ("5", "$12.50") in micro-dollars, or null. */
55export function parseMoney(input: string | null): number | null {
56 const text = (input ?? "").trim().replace(/^\$/, "").replace(/,/g, "");
57 if (!/^\d+(\.\d{1,2})?$/.test(text)) return null;
58 const micros = Math.round(Number(text) * 1_000_000);
59 return micros > 0 ? micros : null;
60}
61
62/** What people can do on a session's card, by where it is. */
63export function sessionActions(status: string, cap: number | null, spent: number, href: string): NonNullable<MessageCard["actions"]> {
64 const open = { id: "open", label: "Open", href };
65 const live = status === "queued" || status === "working" || status === "waiting";
66 if (status === "needs_approval") {
67 const suggested = Math.max(spent + 1_000_000, (cap ?? spent) * 2);
68 return [
69 {
70 id: "approve",
71 label: "Approve more",
72 style: "primary",
73 input: { kind: "money", label: "New cap", placeholder: "5.00", initial: (suggested / 1_000_000).toFixed(2) },
74 },
75 { id: "stop", label: "Stop", style: "danger", confirm: "Stop this session and everything under it?" },
76 open,
77 ];
78 }
79 if (live) {
80 return [
81 { id: "steer", label: "Message", input: { kind: "text", label: "Tell it", placeholder: "Also check the mobile app…" } },
82 { id: "stop", label: "Stop", style: "danger", confirm: "Stop this session and everything under it?" },
83 open,
84 ];
85 }
86 // Over: carry on with a message (it picks up with its context), or read it.
87 return [{ id: "steer", label: "Follow up", input: { kind: "text", label: "Ask it to", placeholder: "Now write the release note…" } }, open];
88}
89