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 calendar | 1 | /** |
| 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 | */ | |
| 5 | import type { MessageCard } from "@g1t/contracts"; | |
| 6 | ||
| 7 | import type { DraftRow } from "./cards.ts"; | |
| 8 | ||
| 9 | /** A draft issue's card: what it would file, and the buttons. */ | |
| 10 | export 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. */ | |
| 55 | export 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. */ | |
| 63 | export 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 |