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.

97 linesCodeBlameRaw
1import { data } from "react-router";
2
3import type { Decision, SessionEntry } from "@g1t/contracts";
4
5import type { Route } from "./+types/why";
6import { showOneCommit } from "../../lib/commit-people.server";
7import { pullForCommit } from "../../lib/provenance.server";
8import { repos, work } from "../../lib/services.server";
9import { getViewer } from "../../lib/session.server";
10
11/** How many steps of the agent's session are shown for one commit. */
12const MAX_STEPS = 8;
13/** Longest a step's text is shown. */
14const MAX_STEP_CHARS = 600;
15
16/**
17 * The steps of a session behind `hash`: what the agent said and did
18 * between the previous commit and this one. Entries carry the head commit
19 * when they were recorded, so the work that produced `hash` is the run of
20 * entries just before the first one recorded at it. Steps that touch
21 * `file` come first.
22 */
23function stepsFor(session: SessionEntry[], hash: string, file: string): SessionEntry[] {
24 // The sandbox notes each push: "Pushed b1c42490059b." The work behind a
25 // commit runs from the push before it to the push that carried it.
26 const pushed = (entry: SessionEntry) => (entry.kind === "note" ? (/^Pushed ([0-9a-f]{7,40})/.exec(entry.text)?.[1] ?? null) : null);
27 const end = session.findIndex((entry) => {
28 const pushedHash = pushed(entry);
29 return entry.commit === hash || (pushedHash != null && hash.startsWith(pushedHash));
30 });
31 let window = session;
32 if (end >= 0) {
33 let start = end - 1;
34 while (start >= 0 && pushed(session[start]!) == null) start--;
35 window = session.slice(start + 1, end + 1);
36 }
37 const name = file.split("/").pop() ?? file;
38 const useful = window.filter((entry) => entry.kind === "message" || (entry.kind === "tool_call" && (entry.text.includes(file) || entry.text.includes(name))));
39 const touching = useful.filter((entry) => entry.text.includes(name));
40 const chosen = (touching.length > 0 ? [...touching, ...useful.filter((e) => e.kind === "message")] : useful)
41 .filter((entry, index, all) => all.findIndex((other) => other.seq === entry.seq) === index)
42 .sort((a, b) => a.seq - b.seq)
43 .slice(-MAX_STEPS);
44 return chosen.map((entry) => ({
45 ...entry,
46 text: entry.text.length > MAX_STEP_CHARS ? `${entry.text.slice(0, MAX_STEP_CHARS)}…` : entry.text,
47 }));
48}
49
50/**
51 * Why a line is the way it is: the commit that last changed it, the pull
52 * request and work item it came from, and what the agent was thinking.
53 * Fetched by the blame view when a line is picked.
54 */
55export async function loader({ params, request, context }: Route.LoaderArgs) {
56 const viewer = getViewer(context);
57 const path = { namespace: params.owner, name: params.repo };
58 const file = new URL(request.url).searchParams.get("path") ?? "";
59 if (!/^[0-9a-f]{40}$/i.test(params.hash)) throw data(null, { status: 404 });
60 const [log, pull] = await Promise.all([repos.log(path, viewer, params.hash, 1), pullForCommit(path, viewer, params.hash)]);
61 const commit = await showOneCommit(log.ok ? log.value[0] : undefined);
62 if (!commit) throw data(null, { status: 404 });
63 if (!pull) return { commit, pull: null, item: null, steps: [] as SessionEntry[], decisions: [] as Decision[] };
64 const [detail, session, decisions] = await Promise.all([
65 work.getPull(path, pull.number, viewer),
66 pull.runtime === "hosted" || pull.agent ? work.readSession(path, pull.number, viewer) : null,
67 // The decisions behind it, oldest first: on its work item and every pull
68 // request for it (the other proposals, the failed combination, the fix,
69 // the approval and the merge), or on the pull request alone.
70 work
71 .listDecisions(pull.item != null ? { workspace: params.owner.toLowerCase(), number: pull.item } : { repo: path, number: pull.number }, viewer)
72 .catch(() => null),
73 ]);
74 const item = detail.ok ? detail.value.item : null;
75 return {
76 commit,
77 pull: {
78 number: pull.number,
79 title: pull.title,
80 agent: pull.agent,
81 runtime: pull.runtime,
82 author: pull.author.username,
83 requestedBy: pull.requestedBy?.username ?? null,
84 mergedBy: pull.mergedBy,
85 mergedAt: pull.mergedAt,
86 },
87 item: item && {
88 workspace: item.workspace,
89 number: item.number,
90 title: item.title,
91 body: item.body.length > 900 ? `${item.body.slice(0, 900)}…` : item.body,
92 author: item.author.username,
93 },
94 steps: session?.ok ? stepsFor(session.value, commit.hash, file) : [],
95 decisions: decisions?.ok ? decisions.value : [],
96 };
97}