Bold questions
This commit is contained in:
Vendored
+205
@@ -0,0 +1,205 @@
|
||||
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
|
||||
Reference in New Issue
Block a user