Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

136 linesCodeBlameRaw
1/**
2 * Sessions as the site shows them across a workspace: what each is called
3 * and where it opens, the filters Sessions keeps in its address, and live
4 * ones apart from the rest. Pure, so it is tested under Node.
5 */
6import type { Harness, SessionKind, WorkSession, WorkspaceSession } from "@g1t/contracts";
7
8import { HARNESS_NAMES, sessionTitle } from "./coordination.ts";
9import { sessionPath } from "./work-items.ts";
10
11/** How each kind is named in a filter. */
12export const KIND_NAMES: Record<SessionKind, string> = {
13 harness: "People's harnesses",
14 hosted: "Started on g1t",
15 agent: "g1t's agent",
16 recorded: "Recorded on pull requests",
17};
18
19/** What a session is called, never nothing. */
20export function workspaceSessionTitle(session: WorkspaceSession): string {
21 if (session.session) {
22 const own = session.session;
23 if (!own.title?.trim() && own.parentId) return [own.agentType ?? "Subagent", own.task].filter(Boolean).join(": ");
24 return sessionTitle(own);
25 }
26 return session.title?.trim() || session.pullTitle?.trim() || (session.pull != null ? `Pull request #${session.pull}` : "Untitled session");
27}
28
29/**
30 * What a session is called on a work item's card: its own title, else
31 * what it is doing there. Never a bare "Pull request #N", whose number
32 * would read as the work item's.
33 */
34export function onItemSessionTitle(session: WorkspaceSession): string {
35 if (session.session || session.title?.trim() || session.pullTitle?.trim()) return workspaceSessionTitle(session);
36 return session.pull != null ? `On pull request #${session.pull}` : "Working on it";
37}
38
39/** What runs it, in a few words: the harness, g1t's agent, or the agent that recorded it. */
40export function runsIn(session: Pick<WorkspaceSession, "kind" | "harness" | "agent">): string {
41 if (session.kind === "agent") return "g1t's agent";
42 if (session.harness) return HARNESS_NAMES[session.harness] ?? session.harness;
43 return session.agent ? `${session.agent}, recorded` : "Recorded";
44}
45
46/**
47 * Where a session opens: a harness or hosted session's own page; g1t's
48 * agent and a recorded session are their pull request's Session tab; one
49 * with neither opens nowhere.
50 */
51export function sessionHref(workspace: string, session: Pick<WorkspaceSession, "kind" | "id" | "repo" | "pull">): string | null {
52 if (session.kind === "harness" || session.kind === "hosted") return sessionPath(workspace, session.id);
53 return session.pull != null ? `/${session.repo}/pull/${session.pull}/session` : null;
54}
55
56/** The filters Sessions keeps in its address. */
57export type SessionsFilters = {
58 /** `recent`: every session in one list; otherwise Live now, then what ended. */
59 state: "live" | "recent";
60 repo: string | null;
61 person: string | null;
62 harness: Harness | null;
63 kind: SessionKind | null;
64 /** The work item they are on, by number: a work item's own sessions. */
65 item: number | null;
66};
67
68const HARNESSES = Object.keys(HARNESS_NAMES) as Harness[];
69const KINDS = Object.keys(KIND_NAMES) as SessionKind[];
70
71export function sessionsFilters(query: URLSearchParams): SessionsFilters {
72 const harness = query.get("harness");
73 const kind = query.get("kind");
74 const text = (key: string) => query.get(key)?.trim().replace(/^@/, "") || null;
75 return {
76 state: query.get("state") === "recent" ? "recent" : "live",
77 repo: text("repo"),
78 person: text("person"),
79 harness: harness && (HARNESSES as string[]).includes(harness) ? (harness as Harness) : null,
80 kind: kind && (KINDS as string[]).includes(kind) ? (kind as SessionKind) : null,
81 item: /^\d+$/.test(query.get("item") ?? "") && Number(query.get("item")) > 0 ? Number(query.get("item")) : null,
82 };
83}
84
85/** The address of Sessions with `filters`, leaving out what is as it is by default. */
86export function sessionsQuery(filters: Partial<SessionsFilters>): string {
87 const query = new URLSearchParams();
88 if (filters.state === "recent") query.set("state", "recent");
89 for (const key of ["repo", "person", "harness", "kind", "item"] as const) {
90 const value = filters[key];
91 if (value) query.set(key, String(value));
92 }
93 const text = query.toString();
94 return text ? `?${text}` : "";
95}
96
97/** Whether any filter narrows the list. */
98export function narrowed(filters: SessionsFilters): boolean {
99 return Boolean(filters.repo || filters.person || filters.harness || filters.kind || filters.item);
100}
101
102/**
103 * Live sessions apart: those going now (active first, then idle, each the
104 * most recently heard from first) and those that ended.
105 */
106export function splitLive(sessions: WorkspaceSession[]): { active: WorkspaceSession[]; idle: WorkspaceSession[]; ended: WorkspaceSession[] } {
107 const by = (state: WorkspaceSession["state"]) =>
108 sessions.filter((session) => session.state === state).sort((a, b) => Date.parse(b.lastActivityAt) - Date.parse(a.lastActivityAt));
109 return { active: by("active"), idle: by("idle"), ended: by("ended") };
110}
111
112/** The people running sessions in a list, each once, most active first, for its filter. */
113export function peopleIn(sessions: WorkspaceSession[]): string[] {
114 const counts = new Map<string, number>();
115 for (const session of sessions) if (session.actor) counts.set(session.actor, (counts.get(session.actor) ?? 0) + 1);
116 return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name]) => name);
117}
118
119/**
120 * What a session cost, in a few words: "≈ $4.12 at list price", with
121 * "some usage priced as unknown" when some of it was for models g1t has no
122 * price for. Null when it reported no usage.
123 */
124export function sessionCost(session: Pick<WorkSession, "costUsd" | "costPartial"> | null | undefined): string | null {
125 if (!session) return null;
126 const cost = session.costUsd;
127 if (cost == null || !Number.isFinite(cost)) return session.costPartial ? "Usage priced as unknown" : null;
128 const amount = cost > 0 && cost < 0.01 ? "under $0.01" : `$${cost.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
129 return `≈ ${amount} at list price${session.costPartial ? ", some usage priced as unknown" : ""}`;
130}
131
132/** `acme/web` as the workspace `acme` names it: `web`. */
133export function repoName(path: string, workspace: string): string {
134 const [owner, name] = path.split("/");
135 return owner?.toLowerCase() === workspace.toLowerCase() && name ? name : path;
136}