Files
byre/web/dist/switcher.js
T

141 lines
6.0 KiB
JavaScript
Raw Normal View History

import { post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
2026-10-15 22:21:20 -07:00
import { focusLead } from "./panes.js";
import { activeTasks, currentSession, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, state } from "./state.js";
import { confirmMerge } from "./task.js";
import { taskStyle } from "./taskcolor.js";
2026-10-15 22:21:20 -07:00
export function bindShortcuts() {
addEventListener("keydown", (e) => {
2026-10-15 11:49:18 -07:00
if (!(e.metaKey || e.ctrlKey) || document.querySelector("dialog[open]")) {
return;
}
if (e.key === "k") {
e.preventDefault();
openSwitcher();
}
2026-10-15 11:49:18 -07:00
else if (e.key === "p") {
e.preventDefault();
focusTab("project");
}
2026-10-15 19:41:26 -07:00
else if (e.key === "l") {
e.preventDefault();
2026-10-15 22:21:20 -07:00
focusLead();
2026-10-15 19:41:26 -07:00
}
else if (e.key === "s") {
e.preventDefault();
if (state.selectedTaskId) {
void confirmMerge(state.selectedTaskId);
}
}
});
}
export 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, state.inbox.some((item) => item.task_id === taskId) ? "inbox" : "transcript");
};
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", { style: taskStyle(task.id) }, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", h("span", { class: "task-name" }, task.title)), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), task.merge_queued_at !== undefined && h("span", { class: "task-state warn", title: "Waiting for the merge in progress in this project to finish" }, "merge held"), 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;
const anchor = document.querySelector(".task-picker").getBoundingClientRect();
box.style.top = `${anchor.bottom + 4}px`;
document.body.append(backdrop, box);
2026-10-15 22:15:36 -07:00
const viewport = document.documentElement.clientWidth;
const left = anchor.left + anchor.width / 2 - box.offsetWidth / 2;
box.style.left = `${Math.max(8, Math.min(left, viewport - box.offsetWidth - 8))}px`;
render();
input.focus();
}
2026-10-15 22:21:20 -07:00
//# sourceMappingURL=switcher.js.map