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.

113 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, 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/** What runs it, in a few words: the harness, g1t's agent, or the agent that recorded it. */
30export function runsIn(session: Pick<WorkspaceSession, "kind" | "harness" | "agent">): string {
31 if (session.kind === "agent") return "g1t's agent";
32 if (session.harness) return HARNESS_NAMES[session.harness] ?? session.harness;
33 return session.agent ? `${session.agent}, recorded` : "Recorded";
34}
35
36/**
37 * Where a session opens: a harness or hosted session's own page; g1t's
38 * agent and a recorded session are their pull request's Session tab; one
39 * with neither opens nowhere.
40 */
41export function sessionHref(workspace: string, session: Pick<WorkspaceSession, "kind" | "id" | "repo" | "pull">): string | null {
42 if (session.kind === "harness" || session.kind === "hosted") return sessionPath(workspace, session.id);
43 return session.pull != null ? `/${session.repo}/pull/${session.pull}/session` : null;
44}
45
46/** The filters Sessions keeps in its address. */
47export type SessionsFilters = {
48 /** `recent`: every session in one list; otherwise Live now, then what ended. */
49 state: "live" | "recent";
50 repo: string | null;
51 person: string | null;
52 harness: Harness | null;
53 kind: SessionKind | null;
54 /** The work item they are on, by number: a work item's own sessions. */
55 item: number | null;
56};
57
58const HARNESSES = Object.keys(HARNESS_NAMES) as Harness[];
59const KINDS = Object.keys(KIND_NAMES) as SessionKind[];
60
61export function sessionsFilters(query: URLSearchParams): SessionsFilters {
62 const harness = query.get("harness");
63 const kind = query.get("kind");
64 const text = (key: string) => query.get(key)?.trim().replace(/^@/, "") || null;
65 return {
66 state: query.get("state") === "recent" ? "recent" : "live",
67 repo: text("repo"),
68 person: text("person"),
69 harness: harness && (HARNESSES as string[]).includes(harness) ? (harness as Harness) : null,
70 kind: kind && (KINDS as string[]).includes(kind) ? (kind as SessionKind) : null,
71 item: /^\d+$/.test(query.get("item") ?? "") && Number(query.get("item")) > 0 ? Number(query.get("item")) : null,
72 };
73}
74
75/** The address of Sessions with `filters`, leaving out what is as it is by default. */
76export function sessionsQuery(filters: Partial<SessionsFilters>): string {
77 const query = new URLSearchParams();
78 if (filters.state === "recent") query.set("state", "recent");
79 for (const key of ["repo", "person", "harness", "kind", "item"] as const) {
80 const value = filters[key];
81 if (value) query.set(key, String(value));
82 }
83 const text = query.toString();
84 return text ? `?${text}` : "";
85}
86
87/** Whether any filter narrows the list. */
88export function narrowed(filters: SessionsFilters): boolean {
89 return Boolean(filters.repo || filters.person || filters.harness || filters.kind || filters.item);
90}
91
92/**
93 * Live sessions apart: those going now (active first, then idle, each the
94 * most recently heard from first) and those that ended.
95 */
96export function splitLive(sessions: WorkspaceSession[]): { active: WorkspaceSession[]; idle: WorkspaceSession[]; ended: WorkspaceSession[] } {
97 const by = (state: WorkspaceSession["state"]) =>
98 sessions.filter((session) => session.state === state).sort((a, b) => Date.parse(b.lastActivityAt) - Date.parse(a.lastActivityAt));
99 return { active: by("active"), idle: by("idle"), ended: by("ended") };
100}
101
102/** The people running sessions in a list, each once, most active first, for its filter. */
103export function peopleIn(sessions: WorkspaceSession[]): string[] {
104 const counts = new Map<string, number>();
105 for (const session of sessions) if (session.actor) counts.set(session.actor, (counts.get(session.actor) ?? 0) + 1);
106 return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name]) => name);
107}
108
109/** `acme/web` as the workspace `acme` names it: `web`. */
110export function repoName(path: string, workspace: string): string {
111 const [owner, name] = path.split("/");
112 return owner?.toLowerCase() === workspace.toLowerCase() && name ? name : path;
113}