Skip to content
84 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/**
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.2 * Cards people can act on in chat: how a
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar3 * card's state reads, which of its actions are links and which run, and
4 * the money and text people type into them. Pure, so it is tested on its
5 * own; components/chat/card.tsx draws them.
6 */
7import type { CardAction, MessageCard } from "@g1t/contracts";
8
9import type { SessionChip } from "./session-card";
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.10import { microsOf, plainDollars } from "./money.ts";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar11
12/** A card's state chip, as a session's is, outlined only when it is waiting quietly. */
13export type CardChip = { tone: SessionChip["tone"] | "merged"; live: boolean; outline?: boolean };
14
15const DRAFT_CHIPS: Record<string, CardChip> = {
16 // Waiting on someone to file it: lavender, outlined only.
17 draft: { tone: "accent", live: false, outline: true },
18 filed: { tone: "success", live: false },
19 discarded: { tone: "neutral", live: false },
20};
21
22/**
23 * How a draft issue's state reads; null for other kinds (a session's chip
24 * is lib/session-card.ts's, others are guessed from their words).
25 */
26export function cardChip(card: Pick<MessageCard, "kind" | "state">): CardChip | null {
27 if (!card.state) return null;
28 if (card.kind === "draft_issue") return DRAFT_CHIPS[card.state.trim().toLowerCase()] ?? { tone: "neutral", live: false };
29 return null;
30}
31
32/** What pressing an action does: opens a place, asks first, asks for a value first, or just runs. */
33export type ActionMode = "link" | "confirm" | "input" | "run";
34
35export function actionMode(action: CardAction): ActionMode {
36 if (action.href) return "link";
37 if (action.input) return "input";
38 if (action.confirm) return "confirm";
39 return "run";
40}
41
42/**
43 * The actions a card shows: links always; the rest only when the card has
44 * an owner to answer them (chat drops them otherwise, this is the same rule
45 * for a card that never went through it).
46 */
47export function shownActions(card: Pick<MessageCard, "actions" | "owner">): CardAction[] {
48 return (card.actions ?? []).filter((a) => !!a.href || !!card.owner);
49}
50
51/** Dollars as people type them ("5", "$12.5", "1,200") as "12.50", or null when it is not an amount. */
52export function moneyValue(input: string | null | undefined): string | null {
53 const text = String(input ?? "")
54 .trim()
55 .replace(/^\$\s*/, "")
56 .replace(/,/g, "");
57 if (!/^\d+(\.\d{0,2})?$|^\.\d{1,2}$/.test(text)) return null;
58 const amount = Number(text);
59 if (!Number.isFinite(amount) || amount <= 0) return null;
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.60 return plainDollars(microsOf(amount));
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar61}
62
63/** What a money field starts with: the suggestion as an amount, or empty. */
64export function moneyInitial(initial: string | null | undefined): string {
65 return moneyValue(initial) ?? "";
66}
67
68/** What is typed into a text action, trimmed, or null when there is nothing. */
69export function textValue(input: string | null | undefined): string | null {
70 const text = String(input ?? "").trim();
71 return text ? text : null;
72}
73
74/** The value an input action sends, or null when it is not ready to send. */
75export function inputValue(action: CardAction, typed: string): string | null {
76 if (!action.input) return null;
77 return action.input.kind === "money" ? moneyValue(typed) : textValue(typed);
78}
79
80/** Whether a body is long enough that its preview starts folded (over about eight lines). */
81export function foldsBody(body: string, lines = 8): boolean {
82 const rows = body.split("\n").reduce((n, line) => n + Math.max(1, Math.ceil(line.length / 80)), 0);
83 return rows > lines;
84}