import { get } from "./api.js"; import { errorText, h, replace } from "./dom.js"; import { onEach, state } from "./state.js"; const places = new Map(); function join(dir, name) { return dir ? `${dir}/${name}` : name; } function parent(path) { return path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : ""; } function bytes(n) { if (n < 1024) { return `${n} B`; } if (n < 1024 * 1024) { return `${(n / 1024).toFixed(1)} KiB`; } return `${(n / 1024 / 1024).toFixed(1)} MiB`; } function shown(c) { return new TextEncoder().encode(c.text).length; } export function mountFiles(root) { const head = h("header", { class: "files-header" }); const body = h("div", { class: "files-body" }); let listing; let content; let error = ""; let target = 0; let timer; let disposed = false; const place = () => { const taskId = state.selectedTaskId; return (taskId && places.get(taskId)) || { dir: "" }; }; const go = (p) => { const taskId = state.selectedTaskId; if (!taskId) { return; } places.set(taskId, p); listing = undefined; content = undefined; error = ""; render(); body.scrollTop = 0; void load(); }; const load = async () => { const taskId = state.selectedTaskId; if (!taskId) { render(); return; } const started = ++target; const p = place(); try { if (p.file !== undefined) { const fresh = await get("/api/tasks/{taskId}/file", { path: { taskId }, query: { path: p.file } }); if (started === target) { content = fresh; listing = undefined; } } else { const fresh = await get("/api/tasks/{taskId}/dir", { path: { taskId }, query: { path: p.dir } }); if (started === target) { listing = fresh; content = undefined; } } if (started === target) { error = ""; } } catch (err) { if (started === target) { error = errorText(err); listing = undefined; content = undefined; } } if (disposed || started !== target) { return; } const scroll = body.scrollTop; render(); body.scrollTop = scroll; }; const soon = () => { clearTimeout(timer); timer = setTimeout(() => void load(), 1500); }; const crumbs = () => { const p = place(); const path = p.file ?? p.dir; const parts = path ? path.split("/") : []; const link = (label, dir, last) => { if (last) { return h("span", { class: "crumb current" }, label); } const b = h("button", { type: "button", class: "crumb" }, label); b.onclick = () => go({ dir }); return b; }; const items = [link("worktree", "", parts.length === 0)]; parts.forEach((part, i) => { items.push("/"); items.push(link(part, parts.slice(0, i + 1).join("/"), i === parts.length - 1)); }); return h("nav", { class: "crumbs" }, ...items); }; const summary = () => { if (error) { return ""; } if (content) { if (content.binary) { return `${bytes(content.size)} · binary`; } return content.truncated ? `${bytes(content.size)} · showing the first ${bytes(shown(content))}` : bytes(content.size); } if (listing) { return `${listing.entries.length}${listing.truncated ? "+" : ""} ${listing.entries.length === 1 ? "entry" : "entries"}`; } return state.selectedTaskId ? "Loading…" : ""; }; const entryRow = (dir, e) => { const label = e.type === "dir" ? `${e.name}/` : e.name; const row = h(e.type === "other" ? "div" : "button", { class: `entry ${e.type}${e.ignored ? " ignored" : ""}`, title: e.type === "other" ? "Leaves the worktree, points nowhere, or isn't a file or directory" : "" }, h("span", { class: "entry-name" }, label, e.symlink ? h("span", { class: "symlink" }, " →") : null), h("span", { class: "entry-meta" }, e.ignored ? "ignored" : null, e.ignored && e.type === "file" ? " · " : null, e.type === "file" ? bytes(e.size) : null)); if (e.type === "dir") { row.onclick = () => go({ dir: join(dir, e.name) }); } else if (e.type === "file") { row.onclick = () => go({ dir, file: join(dir, e.name) }); } return row; }; const render = () => { replace(head, crumbs(), h("span", { class: "files-summary" }, summary())); if (!state.selectedTaskId) { replace(body, h("div", { class: "empty" }, "No task selected.")); return; } if (error) { replace(body, h("div", { class: "empty error-text" }, error)); return; } if (content) { if (content.binary) { replace(body, h("div", { class: "empty" }, "Binary file.")); return; } const lines = content.text.split("\n"); if (lines.at(-1) === "") { lines.pop(); } replace(body, h("pre", { class: "file-text" }, ...lines.map((l) => h("span", { class: "line" }, l || " "))), content.truncated ? h("div", { class: "hint file-cut" }, `Showing the first ${bytes(shown(content))} of ${bytes(content.size)}.`) : null); return; } if (!listing) { replace(body, h("div", { class: "empty" }, "Loading…")); return; } const l = listing; const up = l.path ? h("button", { type: "button", class: "entry dir" }, h("span", { class: "entry-name" }, "../")) : null; if (up) { up.onclick = () => go({ dir: parent(l.path) }); } replace(body, h("div", { class: "entries" }, up, ...l.entries.map((e) => entryRow(l.path, e)), l.entries.length === 0 ? h("div", { class: "empty" }, "Empty directory.") : null, l.truncated ? h("div", { class: "hint" }, `Only the first ${l.entries.length} entries are shown.`) : null)); }; const offs = [ onEach(["selection"], () => { target++; clearTimeout(timer); listing = undefined; content = undefined; error = ""; render(); void load(); }), onEach(["transcript"], (fresh) => { if (fresh.some((ev) => ev.kind === "agent.tool_result" || ev.kind === "turn.ended")) { soon(); } }), ]; replace(root, h("div", { class: "files-view" }, head, body)); render(); void load(); return () => { disposed = true; clearTimeout(timer); for (const off of offs) { off(); } }; } //# sourceMappingURL=files.js.map