Files
byre/web/dist/changes.js
T
2026-10-14 19:35:34 -07:00

205 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { get } from "./api.js";
import { errorText, h, replace } from "./dom.js";
import { onEach, state } from "./state.js";
const statusLetter = {
added: "A",
modified: "M",
deleted: "D",
renamed: "R",
type_changed: "T",
};
const open = new Set();
let range = "main";
function turnStart() {
const ev = state.transcript.findLast((e) => e.kind === "user.message" && typeof e.payload["head"] === "string");
return ev?.payload["head"];
}
export function mountChanges(root, compact = false) {
const head = h("header", { class: "changes-header" });
const list = h("div", { class: "changes-list" });
const diffs = new Map();
let changes;
let error = "";
let loading = false;
let again = false;
let timer;
let disposed = false;
let loadedSince;
const mode = () => (compact ? "main" : range);
const load = async () => {
const taskId = state.selectedTaskId;
if (!taskId) {
changes = undefined;
render();
return;
}
const since = mode() === "turn" ? turnStart() : undefined;
if (mode() === "turn" && !since) {
changes = undefined;
error = "No turn has started in this task yet.";
render();
return;
}
if (loading) {
again = true;
return;
}
loading = true;
renderHead();
try {
const fresh = await get("/api/tasks/{taskId}/changes", { path: { taskId }, query: { since } });
if (disposed || state.selectedTaskId !== taskId) {
return;
}
loadedSince = since;
if (fresh.tree !== changes?.tree || fresh.base !== changes?.base) {
diffs.clear();
}
changes = fresh;
error = "";
}
catch (err) {
error = errorText(err);
}
finally {
loading = false;
}
render();
if (again) {
again = false;
void load();
}
};
const soon = () => {
clearTimeout(timer);
timer = setTimeout(() => void load(), 1500);
};
const toggle = () => {
if (compact) {
return null;
}
const button = (r, label) => {
const b = h("button", { type: "button", class: r === range ? "active" : "" }, label);
b.onclick = () => {
if (range === r) {
return;
}
range = r;
changes = undefined;
diffs.clear();
error = "";
render();
void load();
};
return b;
};
return h("nav", { class: "range-tabs" }, button("main", "Since main"), button("turn", "This turn"));
};
const renderHead = () => {
if (!changes) {
replace(head, h("span", { class: "changes-summary" }, error || (state.selectedTaskId ? "Loading changes…" : "No task selected.")), toggle());
return;
}
const adds = changes.files.reduce((n, f) => n + f.additions, 0);
const dels = changes.files.reduce((n, f) => n + f.deletions, 0);
replace(head, h("span", { class: "changes-summary" }, `${changes.files.length} ${changes.files.length === 1 ? "file" : "files"} `, h("span", { class: "adds" }, `+${adds}`), " ", h("span", { class: "dels" }, `−${dels}`), h("span", { class: "base", title: changes.base }, ` vs ${changes.base.slice(0, 8)}`), error && h("span", { class: "error-text" }, ` ${error}`)), toggle());
};
const fileRow = (c, f) => {
const body = h("div", { class: "file-diff" });
const details = h("details", { class: `file ${f.status}`, open: open.has(f.path) }, h("summary", {}, h("span", { class: `status ${f.status}`, title: f.status.replace("_", " ") }, statusLetter[f.status]), h("span", { class: "file-path" }, f.old_path ? `${f.old_path} → ${f.path}` : f.path), f.binary ? h("span", { class: "binary" }, "binary") : h("span", { class: "counts" }, h("span", { class: "adds" }, `+${f.additions}`), " ", h("span", { class: "dels" }, `−${f.deletions}`))), body);
const show = async () => {
const cached = diffs.get(f.path);
if (cached !== undefined) {
replace(body, renderPatch(cached));
return;
}
replace(body, h("div", { class: "hint" }, "loading…"));
try {
const d = await get("/api/tasks/{taskId}/diff", {
path: { taskId: state.selectedTaskId },
query: { base: c.base, tree: c.tree, path: f.path, old_path: f.old_path },
});
diffs.set(f.path, d.patch);
replace(body, renderPatch(d.patch));
}
catch (err) {
replace(body, h("div", { class: "error-text" }, errorText(err)));
}
};
details.ontoggle = () => {
if (details.open) {
open.add(f.path);
void show();
}
else {
open.delete(f.path);
}
};
if (details.open) {
void show();
}
return details;
};
const render = () => {
renderHead();
if (compact) {
return;
}
if (!state.selectedTaskId) {
replace(list, h("div", { class: "empty" }, "No task selected."));
return;
}
if (!changes) {
replace(list, h("div", { class: "empty" }, error ? "" : "Loading…"));
return;
}
if (changes.files.length === 0) {
replace(list, h("div", { class: "empty" }, mode() === "turn" ? "No changes this turn." : "No changes from the default branch."));
return;
}
const c = changes;
const files = [...c.files].sort((a, b) => a.path.localeCompare(b.path));
replace(list, ...files.map((f) => fileRow(c, f)));
};
const offs = [
onEach(["selection"], () => {
changes = undefined;
diffs.clear();
error = "";
render();
void load();
}),
onEach(["transcript"], (fresh) => {
if (mode() === "turn" && turnStart() !== loadedSince) {
void load();
return;
}
if (fresh.some((ev) => ev.kind === "agent.tool_result" || ev.kind === "turn.ended")) {
soon();
}
}),
];
replace(root, compact ? h("div", { class: "changes-view compact" }, head) : h("div", { class: "changes-view" }, head, list));
render();
void load();
return () => {
disposed = true;
clearTimeout(timer);
for (const off of offs) {
off();
}
};
}
function renderPatch(patch) {
const lines = patch.split("\n");
const start = lines.findIndex((l) => l.startsWith("@@"));
const body = start === -1 ? [] : lines.slice(start);
if (body.length === 0) {
return h("div", { class: "hint" }, "No textual changes.");
}
return h("pre", { class: "diff" }, ...body.map((l) => {
const cls = l.startsWith("@@") ? "hunk" : l.startsWith("+") ? "add" : l.startsWith("-") ? "del" : l.startsWith("\\") ? "meta" : "ctx";
return h("span", { class: `line ${cls}` }, l || " ");
}));
}
//# sourceMappingURL=changes.js.map