Let task agents create and manage subtasks

This commit is contained in:
Ian Gulliver
2026-10-16 10:36:38 -07:00
parent fd63a0dfbc
commit 0f2415570c
30 changed files with 1067 additions and 287 deletions
+19 -4
View File
@@ -2,7 +2,7 @@ import { get, post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { markdown } from "./markdown.js";
import { models } from "./task.js";
import { currentSession, focusedProject, focusTask, on, onEach, state, taskState } from "./state.js";
import { currentSession, findTask, focusedProject, focusTask, on, onEach, state, taskState } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const refreshKinds = new Set(["task.merged", "project.created"]);
export function mountProject(root) {
@@ -86,20 +86,35 @@ function projectBlock(project, todo, starting, start) {
const active = (state.tasks.get(project.id) ?? []).filter((t) => t.status === "active");
return h("section", { class: "project" }, h("h2", {}, project.name, " ", h("span", { class: "repo" }, project.default_branch)), h("h3", {}, "Tasks"), tasksBlock(active), h("h3", {}, "TODO"), ...todoBlock(project, todo, active, starting, start));
}
function nested(active) {
const ids = new Set(active.map((t) => t.id));
const out = [];
const add = (task, depth) => {
out.push({ task, depth });
for (const child of active.filter((t) => t.parent_task_id === task.id).reverse()) {
add(child, depth + 1);
}
};
for (const task of active.filter((t) => t.parent_task_id === undefined || !ids.has(t.parent_task_id))) {
add(task, 0);
}
return out;
}
function tasksBlock(active) {
if (active.length === 0) {
return h("p", { class: "empty-row" }, "No active tasks.");
}
return h("ul", { class: "task-list" }, ...active.map((task) => {
return h("ul", { class: "task-list" }, ...nested(active).map(({ task, depth }) => {
const ts = taskState(task);
const count = state.inbox.filter((item) => item.task_id === task.id).length;
return h("li", { class: task.id === state.selectedTaskId ? "selected" : "", style: taskStyle(task.id) }, h("a", {
const classes = [task.id === state.selectedTaskId && "selected", task.parent_task_id !== undefined && "subtask"].filter(Boolean).join(" ");
return h("li", { class: classes, style: `${taskStyle(task.id)}; --depth: ${depth}` }, h("a", {
href: `#/task/${task.id}`,
onclick: (e) => {
e.preventDefault();
focusTask(task.id);
},
}, h("span", { class: `dot ${ts ?? "none"}`, title: ts ?? "no session" }), h("span", { class: "task-title 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" }, ts ?? "no agent")));
}, task.parent_task_id !== undefined && h("span", { class: "subtask-mark", title: `Subtask of ${findTask(task.parent_task_id)?.title ?? task.parent_task_id}` }, "↳"), h("span", { class: `dot ${ts ?? "none"}`, title: ts ?? "no session" }), h("span", { class: "task-title 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"), task.merging_into_parent && h("span", { class: "task-state warn", title: "Waiting for the parent task's agent to merge it" }, "merging into parent"), h("span", { class: "task-state" }, ts ?? "no agent")));
}));
}
function todoBlock(project, todo, active, starting, start) {