Add a per-user lead agent and make the right pane task-focused

This commit is contained in:
Ian Gulliver
2026-10-15 09:40:30 -07:00
parent d212590cdc
commit bdd1ae48f0
68 changed files with 3332 additions and 1364 deletions
+11 -71
View File
@@ -3,44 +3,17 @@ import { mountChanges } from "./changes.js";
import { ago, errorText, h, replace, showAlert } from "./dom.js";
import { image, inlineMarkdown, markdown } from "./markdown.js";
import { diagram } from "./mermaid.js";
import { findTask, onEach, state } from "./state.js";
import { onEach, state } from "./state.js";
const checked = new Map();
const texts = new Map();
let scope = "all";
export function mountInbox(root) {
const tabs = h("nav", { class: "scope-tabs" });
const list = h("div", { class: "inbox-list" });
const cards = new Map();
const groups = new Map();
const empty = h("div", { class: "empty" }, "Nothing waiting on you.");
const visible = () => {
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
if (scope === "task" && task) {
return state.inbox.filter((i) => i.task_id === task.id);
}
if (scope === "project" && task) {
return state.inbox.filter((i) => i.project_id === task.project_id);
}
return state.inbox;
};
const renderTabs = () => {
const hasTask = state.selectedTaskId !== undefined;
const tab = (s, label) => {
const b = h("button", { class: s === scope ? "active" : "", disabled: s !== "all" && !hasTask }, label);
b.onclick = () => {
scope = s;
render();
};
return b;
};
replace(tabs, tab("all", "All"), tab("project", "This project"), tab("task", "This task"));
};
const nothing = h("div", { class: "empty" }, "Nothing waiting on you in this task.");
const noTask = h("div", { class: "empty" }, "No task selected.");
const render = () => {
if (scope !== "all" && !state.selectedTaskId) {
scope = "all";
}
renderTabs();
const items = visible();
const taskId = state.selectedTaskId;
const items = state.inbox.filter((i) => i.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at));
const keep = new Set(items.map((i) => i.id));
for (const id of cards.keys()) {
if (!keep.has(id)) {
@@ -64,32 +37,15 @@ export function mountInbox(root) {
cards.set(item.id, { sig, el });
return el;
};
const shown = groupByTask(items);
const keepGroups = new Set(shown.map((g) => g.taskId));
for (const id of groups.keys()) {
if (!keepGroups.has(id)) {
groups.delete(id);
}
if (!taskId) {
reconcile(list, [noTask]);
return;
}
const els = shown.map((g) => {
const projectName = state.projects.find((p) => p.id === g.projectId)?.name ?? g.projectId;
const title = findTask(g.taskId)?.title ?? g.taskId;
const head = JSON.stringify([projectName, title]);
let group = groups.get(g.taskId);
if (!group || group.head !== head) {
const h3 = h("h3", {}, h("span", { class: "project-name" }, projectName, " › "), h("a", { href: `#/task/${g.taskId}` }, title));
group = { head, h3, el: group?.el ?? h("section", {}) };
groups.set(g.taskId, group);
}
group.el.className = `task-group ${g.blocked ? "blocked" : ""}`;
reconcile(group.el, [group.h3, ...g.items.map(cardFor)]);
return group.el;
});
reconcile(list, els.length > 0 ? els : [empty]);
reconcile(list, items.length > 0 ? items.map(cardFor) : [nothing]);
};
const off = onEach(["inbox", "selection", "tasks"], render);
const off = onEach(["inbox", "selection"], render);
const changes = h("section", { class: "inbox-changes" });
replace(root, h("div", { class: "inbox-view" }, changes, tabs, list));
replace(root, h("div", { class: "inbox-view" }, changes, list));
const offChanges = mountChanges(changes, true);
render();
return () => {
@@ -108,22 +64,6 @@ function reconcile(parent, nodes) {
parent.lastChild.remove();
}
}
function groupByTask(items) {
const groups = new Map();
for (const item of [...items].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
let g = groups.get(item.task_id);
if (!g) {
g = { taskId: item.task_id, projectId: item.project_id, blocked: false, items: [] };
groups.set(item.task_id, g);
}
g.items.push(item);
g.blocked ||= item.priority === "blocked";
}
const projectName = (id) => state.projects.find((p) => p.id === id)?.name ?? id;
return [...groups.values()].sort((a, b) => Number(b.blocked) - Number(a.blocked) ||
projectName(a.projectId).localeCompare(projectName(b.projectId)) ||
a.items[0].created_at.localeCompare(b.items[0].created_at));
}
function clickable(el, go) {
el.onclick = (e) => {
if (!e.target.closest("a")) {