Add task summaries, closed-task search and a summary turn before discard

This commit is contained in:
Ian Gulliver
2026-10-16 10:08:08 -07:00
parent 2107a43ef2
commit fd63a0dfbc
25 changed files with 1013 additions and 268 deletions
+9
View File
@@ -55,6 +55,12 @@ export function currentSession(taskId) {
return state.sessions.get(taskId)?.findLast((s) => s.state !== "completed") ?? state.sessions.get(taskId)?.at(-1);
}
export function taskState(task) {
if (task.discarding) {
return "discarding";
}
if (task.merging) {
return "merging";
}
const s = currentSession(task.id)?.state;
return s === "idle" && task.reviewing ? "reviewing" : s;
}
@@ -246,6 +252,9 @@ export function handleEvent(ev) {
case "task.merge_title":
case "task.merge_queued":
case "task.merge_unqueued":
case "task.merge_started":
case "task.merge_ended":
case "task.discard_started":
case "task.merged":
case "task.discarded":
if (ev.project_id) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -112,7 +112,7 @@ function taskHead(rerender) {
mergeIn.disabled = false;
}
};
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
const discard = h("button", { class: "danger", disabled: busy, title: "The agent writes the task summary, then byre deletes the worktree and branch without merging" }, "Discard");
discard.onclick = () => confirmThen(`Discard "${task.title}"?`, "Discard", true, discard, async () => {
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
await setFocus({ task_id: null });
@@ -944,6 +944,10 @@ function renderEvent(ev, ctx) {
}
case "task.merged":
return h("div", { class: "note" }, `${ev.actor} merged the task${p["commit"] ? ` as ${String(p["commit"]).slice(0, 7)}` : ""}${p["pushed"] ? ` and pushed it to ${String(p["pushed"])}` : ""} `, when);
case "task.summary":
return h("div", { class: "note" }, `${ev.actor} set the task summary `, when);
case "task.discard_started":
return h("div", { class: "note" }, `${ev.actor} asked to discard the task; the agent writes the task summary first `, when);
case "task.discarded":
return h("div", { class: "note" }, `${ev.actor} discarded the task `, when);
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -3
View File
@@ -320,7 +320,7 @@ export interface paths {
};
get?: never;
put?: never;
/** @description Removes the task's worktree and branch without merging. The task's record and events remain. */
/** @description Gives the task's agent one dedicated turn to write the task summary, started at once when its session is idle (or in a new session when it has none) and otherwise as soon as the running turn ends, with no other turn admitted meanwhile; drops any held merge; and when that turn ends however it ends, removes the task's worktree and branch without merging and marks the task discarded. The task's record, summary and events remain. Refused while the task is merging or already being discarded; on a task whose cleanup failed, retries the cleanup. */
post: operations["discardTask"];
delete?: never;
options?: never;
@@ -874,6 +874,10 @@ export interface components {
workflow_stage?: string;
/** @description Whether a review of one of the task's snapshots is running. */
reviewing: boolean;
/** @description Whether a Merge was started and its turn hasn't ended yet. */
merging: boolean;
/** @description Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed. */
discarding: boolean;
};
Todo: {
/** @description Whether the default branch has a TODO.md at its root. */
@@ -1818,8 +1822,8 @@ export interface operations {
};
requestBody?: never;
responses: {
/** @description The discarded task. */
200: {
/** @description The task, still active, whose discard was started. */
202: {
headers: {
[name: string]: unknown;
};
+9
View File
@@ -83,6 +83,12 @@ export function currentSession(taskId: string): AgentSession | undefined {
}
export function taskState(task: Task): string | undefined {
if (task.discarding) {
return "discarding";
}
if (task.merging) {
return "merging";
}
const s = currentSession(task.id)?.state;
return s === "idle" && task.reviewing ? "reviewing" : s;
}
@@ -306,6 +312,9 @@ export function handleEvent(ev: ByreEvent): void {
case "task.merge_title":
case "task.merge_queued":
case "task.merge_unqueued":
case "task.merge_started":
case "task.merge_ended":
case "task.discard_started":
case "task.merged":
case "task.discarded":
if (ev.project_id) {
+7 -1
View File
@@ -177,7 +177,7 @@ function taskHead(rerender: () => void): Head {
mergeIn.disabled = false;
}
};
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
const discard = h("button", { class: "danger", disabled: busy, title: "The agent writes the task summary, then byre deletes the worktree and branch without merging" }, "Discard");
discard.onclick = () =>
confirmThen(`Discard "${task.title}"?`, "Discard", true, discard, async () => {
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
@@ -1120,6 +1120,12 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
when,
);
case "task.summary":
return h("div", { class: "note" }, `${ev.actor} set the task summary `, when);
case "task.discard_started":
return h("div", { class: "note" }, `${ev.actor} asked to discard the task; the agent writes the task summary first `, when);
case "task.discarded":
return h("div", { class: "note" }, `${ev.actor} discarded the task `, when);
}
+3 -1
View File
@@ -560,7 +560,9 @@ a.task-link {
}
.dot.running,
.dot.reviewing {
.dot.reviewing,
.dot.merging,
.dot.discarding {
background: var(--ok);
box-shadow: none;
}