import { post } from "./api.js"; import { errorText, h, replace, showAlert } from "./dom.js"; import { openSettings } from "./settings.js"; import { activeTasks, currentSession, findTask, focusedProject, focusProject, focusTask, lastVisits, onEach, state } from "./state.js"; import { openStatus } from "./status.js"; export function mountNavbar(root) { const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" }); crumb.onclick = openSwitcher; const status = h("button", { class: "nav-link" }, "Status"); status.onclick = openStatus; const settings = h("button", { class: "nav-link" }, "Settings"); settings.onclick = openSettings; const conn = h("span", { class: "connection down", title: "connecting" }); const connLabel = h("span", { class: "connection-label" }, "reconnecting…"); const me = h("span", { class: "me" }); const render = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; const project = focusedProject(); replace(crumb, task ? `${project?.name ?? ""} ▸ ${task.title}` : (project?.name ?? "No task"), " ▾"); conn.className = state.connected ? "connection up" : "connection down"; conn.title = state.connected ? "live" : "reconnecting"; connLabel.hidden = state.connected; replace(me, state.me?.display_name ?? ""); }; onEach(["me", "projects", "tasks", "selection", "focus", "connection"], render); replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), status, settings, connLabel, conn, me); render(); addEventListener("keydown", (e) => { if (e.key === "k" && (e.metaKey || e.ctrlKey) && !document.querySelector("dialog[open]")) { e.preventDefault(); openSwitcher(); } }); } function openSwitcher() { if (document.querySelector(".switcher")) { return; } const input = h("input", { placeholder: "Go to a task, or type a title to create one…", autocomplete: "off" }); const list = h("ul", { class: "switcher-list" }); const backdrop = h("div", { class: "switcher-backdrop" }); const box = h("div", { class: "switcher" }, input, list); let selected = 0; let rows = []; const offs = onEach(["projects", "tasks", "sessions", "inbox"], () => render()); const onEscape = (e) => { if (e.key !== "Escape") { return; } e.preventDefault(); e.stopPropagation(); close(); }; addEventListener("keydown", onEscape, { capture: true }); const close = () => { offs(); removeEventListener("keydown", onEscape, { capture: true }); backdrop.remove(); box.remove(); }; const go = (taskId) => { close(); focusTask(taskId); }; const create = async (projectId, title) => { close(); try { const task = await post("/api/projects/{projectId}/tasks", { path: { projectId }, body: { title } }); go(task.id); } catch (err) { void showAlert(errorText(err)); } }; const render = () => { const title = input.value.trim(); const q = title.toLowerCase(); const inbox = (taskId) => state.inbox.filter((item) => item.task_id === taskId).length; const visits = lastVisits(); const matches = activeTasks() .filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q)) .map((m) => ({ ...m, count: inbox(m.task.id), visited: visits[m.task.id] ?? 0 })) .sort((a, b) => Number(b.count > 0) - Number(a.count > 0) || a.visited - b.visited); rows = matches.map(({ project, task, count }) => { const sess = currentSession(task.id); const el = h("li", {}, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.title), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: "task-state" }, sess?.state ?? "no agent")); return { el, pick: () => go(task.id) }; }); for (const p of state.projects.filter((p) => `${p.name} project`.toLowerCase().includes(q))) { const el = h("li", {}, h("span", { class: "dot none" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, p.name)), h("span", { class: "task-state" }, "project")); rows.push({ el, pick: () => { close(); focusProject(p.id); }, }); } const project = focusedProject() ?? state.projects[0]; if (title && project) { const el = h("li", { class: "new-task" }, h("span", { class: "task-title" }, "New task in ", h("span", { class: "project-name" }, project.name), ": ", title)); rows.push({ el, pick: () => void create(project.id, title) }); } selected = Math.min(selected, Math.max(rows.length - 1, 0)); rows.forEach((row, i) => { row.el.classList.toggle("selected", i === selected); row.el.onmousedown = (e) => { e.preventDefault(); row.pick(); }; }); replace(list, ...rows.map((r) => r.el), rows.length === 0 && h("li", { class: "empty-row" }, "No active tasks. Type a title to create one.")); }; input.oninput = () => { selected = 0; render(); }; input.onkeydown = (e) => { if (e.key === "ArrowDown") { e.preventDefault(); selected = Math.min(selected + 1, rows.length - 1); render(); } else if (e.key === "ArrowUp") { e.preventDefault(); selected = Math.max(selected - 1, 0); render(); } else if (e.key === "Enter") { e.preventDefault(); rows[selected]?.pick(); } }; backdrop.onclick = close; document.body.append(backdrop, box); render(); input.focus(); } //# sourceMappingURL=navbar.js.map