Skip to content
97 linesCodeBlameRaw
1import { data } from "react-router";
2
3import type { 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) =>
27 entry.kind === "note" ? /^Pushed ([0-9a-f]{7,40})/.exec(entry.text)?.[1] ?? null : null;
28 const end = session.findIndex((entry) => {
29 const pushedHash = pushed(entry);
30 return entry.commit === hash || (pushedHash != null && hash.startsWith(pushedHash));
31 });
32 let window = session;
33 if (end >= 0) {
34 let start = end - 1;
35 while (start >= 0 && pushed(session[start]!) == null) start--;
36 window = session.slice(start + 1, end + 1);
37 }
38 const name = file.split("/").pop() ?? file;
39 const useful = window.filter(
40 (entry) =>
41 entry.kind === "message" ||
42 (entry.kind === "tool_call" && (entry.text.includes(file) || entry.text.includes(name))),
43 );
44 const touching = useful.filter((entry) => entry.text.includes(name));
45 const chosen = (touching.length > 0 ? [...touching, ...useful.filter((e) => e.kind === "message")] : useful)
46 .filter((entry, index, all) => all.findIndex((other) => other.seq === entry.seq) === index)
47 .sort((a, b) => a.seq - b.seq)
48 .slice(-MAX_STEPS);
49 return chosen.map((entry) => ({
50 ...entry,
51 text: entry.text.length > MAX_STEP_CHARS ? `${entry.text.slice(0, MAX_STEP_CHARS)}…` : entry.text,
52 }));
53}
54
55/**
56 * Why a line is the way it is: the commit that last changed it, the pull
57 * request and issue it came from, and what the agent was thinking.
58 * Fetched by the blame view when a line is picked.
59 */
60export async function loader({ params, request, context }: Route.LoaderArgs) {
61 const viewer = getViewer(context);
62 const path = { namespace: params.owner, name: params.repo };
63 const file = new URL(request.url).searchParams.get("path") ?? "";
64 if (!/^[0-9a-f]{40}$/i.test(params.hash)) throw data(null, { status: 404 });
65 const [log, pull] = await Promise.all([
66 repos.log(path, viewer, params.hash, 1),
67 pullForCommit(path, viewer, params.hash),
68 ]);
69 const commit = await showOneCommit(log.ok ? log.value[0] : undefined);
70 if (!commit) throw data(null, { status: 404 });
71 if (!pull) return { commit, pull: null, issue: null, steps: [] as SessionEntry[] };
72 const [detail, session] = await Promise.all([
73 work.getPull(path, pull.number, viewer),
74 pull.runtime === "hosted" || pull.agent ? work.readSession(path, pull.number, viewer) : null,
75 ]);
76 const issue = detail.ok ? detail.value.issue : null;
77 return {
78 commit,
79 pull: {
80 number: pull.number,
81 title: pull.title,
82 agent: pull.agent,
83 runtime: pull.runtime,
84 author: pull.author.username,
85 requestedBy: pull.requestedBy?.username ?? null,
86 mergedBy: pull.mergedBy,
87 mergedAt: pull.mergedAt,
88 },
89 issue: issue && {
90 number: issue.number,
91 title: issue.title,
92 body: issue.body.length > 900 ? `${issue.body.slice(0, 900)}…` : issue.body,
93 author: issue.author.username,
94 },
95 steps: session?.ok ? stepsFor(session.value, commit.hash, file) : [],
96 };
97}