Let task agents create and manage subtasks
This commit is contained in:
+87
-30
@@ -155,16 +155,21 @@ function taskHead(rerender: () => void): Head {
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
const busy = sess?.state === "running";
|
||||
const held = task.merge_queued_at !== undefined;
|
||||
const intoParent = task.merging_into_parent;
|
||||
const merge = h(
|
||||
"button",
|
||||
{
|
||||
class: "merge-button",
|
||||
disabled: busy || held || unmerged?.taskId !== task.id || !unmerged.has,
|
||||
title: held
|
||||
? "Waiting for the merge in progress in this project to finish"
|
||||
: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin; held while another task in the project is merging (⌘S)`,
|
||||
disabled: busy || held || intoParent || unmerged?.taskId !== task.id || !unmerged.has,
|
||||
title: intoParent
|
||||
? "Waiting for the parent task's agent to merge this subtask"
|
||||
: held
|
||||
? "Waiting for the merge in progress in this project to finish"
|
||||
: task.parent_task_id !== undefined
|
||||
? `Merge this subtask into its parent task's branch, or into ${target} (⌘S)`
|
||||
: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin; held while another task in the project is merging (⌘S)`,
|
||||
},
|
||||
held ? "Merge held" : "Merge",
|
||||
intoParent ? "Merging into parent" : held ? "Merge held" : "Merge",
|
||||
);
|
||||
merge.onclick = () => confirmMerge(task.id);
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
|
||||
@@ -1068,30 +1073,28 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
}
|
||||
|
||||
case "task.created": {
|
||||
if (typeof p["forked_from"] !== "string") {
|
||||
return null;
|
||||
if (typeof p["forked_from"] === "string") {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "note" },
|
||||
`${ev.actor} forked this task from `,
|
||||
taskLink(p["forked_from"]),
|
||||
` at snapshot #${String(p["forked_at"])}; its agent continues that task's conversation as it stood there `,
|
||||
when,
|
||||
);
|
||||
}
|
||||
const src = findTask(p["forked_from"]);
|
||||
const title = src?.title ?? p["forked_from"];
|
||||
const link =
|
||||
src?.status === "active"
|
||||
? h(
|
||||
"a",
|
||||
{
|
||||
href: `#/task/${src.id}`,
|
||||
class: "task-link",
|
||||
style: taskStyle(src.id),
|
||||
onclick: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
focusTask(src.id);
|
||||
},
|
||||
},
|
||||
title,
|
||||
)
|
||||
: title;
|
||||
return h("div", { class: "note" }, `${ev.actor} forked this task from `, link, ` at snapshot #${String(p["forked_at"])}; its agent continues that task's conversation as it stood there `, when);
|
||||
if (typeof p["parent_task_id"] === "string") {
|
||||
return h("div", { class: "note" }, "subtask of ", taskLink(p["parent_task_id"]), ", created by its agent ", when);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
case "task.parent_merge_started":
|
||||
return h("div", { class: "note" }, `${ev.actor} asked `, taskLink(String(p["parent_task_id"])), "'s agent to merge this subtask into its branch ", when);
|
||||
|
||||
case "task.parent_merge_stopped":
|
||||
return h("div", { class: "note error" }, "the merge into the parent task didn't start ", when);
|
||||
|
||||
case "task.merge_title":
|
||||
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
|
||||
|
||||
@@ -1113,6 +1116,9 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
}
|
||||
|
||||
case "task.merged":
|
||||
if (typeof p["parent_task_id"] === "string") {
|
||||
return h("div", { class: "note" }, `${ev.actor} merged the task into `, taskLink(p["parent_task_id"]), `, now at ${String(p["commit"]).slice(0, 7)} `, when);
|
||||
}
|
||||
return h(
|
||||
"div",
|
||||
{ class: "note" },
|
||||
@@ -1133,6 +1139,26 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function taskLink(id: string): Node {
|
||||
const task = findTask(id);
|
||||
if (task?.status !== "active") {
|
||||
return document.createTextNode(task?.title ?? id);
|
||||
}
|
||||
return h(
|
||||
"a",
|
||||
{
|
||||
href: `#/task/${task.id}`,
|
||||
class: "task-link",
|
||||
style: taskStyle(task.id),
|
||||
onclick: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
focusTask(task.id);
|
||||
},
|
||||
},
|
||||
task.title,
|
||||
);
|
||||
}
|
||||
|
||||
function messageImages(ids: unknown): HTMLElement | null {
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return null;
|
||||
@@ -1161,7 +1187,12 @@ export async function confirmMerge(taskId: string): Promise<void> {
|
||||
void showAlert("Merge already held behind another merge in this project");
|
||||
return;
|
||||
}
|
||||
if (task.merging_into_parent) {
|
||||
void showAlert("Already waiting for the parent task's agent to merge this subtask");
|
||||
return;
|
||||
}
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
const parent = task.parent_task_id !== undefined ? findTask(task.parent_task_id) : undefined;
|
||||
let coAuthors;
|
||||
try {
|
||||
const [unmerged, people] = await Promise.all([
|
||||
@@ -1196,11 +1227,36 @@ export async function confirmMerge(taskId: string): Promise<void> {
|
||||
field.select();
|
||||
};
|
||||
const warnings = h("ul", { class: "merge-warnings" });
|
||||
const question: Node[] = [h("p", { class: "merge-into" }, `Merge into ${target} as:`), row, warnings];
|
||||
const into = h("p", { class: "merge-into" }, `Merge into ${target} as:`);
|
||||
const co = coAuthorsSummary(coAuthors);
|
||||
if (co) {
|
||||
question.push(co);
|
||||
const toMain: Node[] = co ? [into, row, co] : [into, row];
|
||||
let toParent = false;
|
||||
const question: Node[] = [];
|
||||
if (parent?.status === "active") {
|
||||
toParent = true;
|
||||
const option = (value: boolean, label: string, hint: string) => {
|
||||
const radio = h("input", { type: "radio", name: "merge-target", checked: value === toParent });
|
||||
radio.onchange = () => {
|
||||
toParent = value;
|
||||
for (const n of toMain) {
|
||||
(n as HTMLElement).hidden = toParent;
|
||||
}
|
||||
};
|
||||
return h("label", { class: "merge-target" }, radio, h("span", {}, label, h("span", { class: "merge-target-hint" }, hint)));
|
||||
};
|
||||
question.push(
|
||||
h(
|
||||
"div",
|
||||
{ class: "merge-targets" },
|
||||
option(true, `Into ${parent.title}`, "Its agent merges this subtask's branch into the parent's branch; the subtask closes once it has."),
|
||||
option(false, `Into ${target}`, "Squashed into one commit on the default branch and pushed, like any task."),
|
||||
),
|
||||
);
|
||||
for (const n of toMain) {
|
||||
(n as HTMLElement).hidden = true;
|
||||
}
|
||||
}
|
||||
question.push(...toMain.slice(0, 2), warnings, ...toMain.slice(2));
|
||||
const leave = (go: () => void) => {
|
||||
mergeDrafts.set(taskId, input?.value ?? title);
|
||||
warnings.closest("dialog")?.close("cancel");
|
||||
@@ -1210,7 +1266,8 @@ export async function confirmMerge(taskId: string): Promise<void> {
|
||||
await watch.ready;
|
||||
try {
|
||||
await confirmThen(question, "Merge", false, document.querySelector(".merge-button"), async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId }, body: { merge_title: input?.value ?? title } });
|
||||
const body = toParent ? { target: "parent" as const } : { merge_title: input?.value ?? title };
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId }, body });
|
||||
});
|
||||
} finally {
|
||||
watch.dispose();
|
||||
|
||||
Reference in New Issue
Block a user