Human-facing task state

This commit is contained in:
Ian Gulliver
2026-10-16 10:59:46 -07:00
parent 07780b8701
commit db58d76837
12 changed files with 78 additions and 54 deletions
+9 -13
View File
@@ -2,7 +2,7 @@ import { get, post, type ByreEvent, type Project, type Schemas, type Task } from
import { errorText, h, replace, showAlert } from "./dom.js";
import { markdown } from "./markdown.js";
import { models } from "./task.js";
import { currentSession, findTask, focusedProject, focusTask, on, onEach, state, taskState } from "./state.js";
import { findTask, focusedProject, focusTask, on, onEach, state, taskState, type TaskState } from "./state.js";
import { taskStyle } from "./taskcolor.js";
type Todo = Schemas["Todo"];
@@ -121,13 +121,11 @@ function nested(active: Task[]): { task: Task; depth: number }[] {
return out;
}
const busyStates = new Set(["running", "reviewing", "merging", "discarding"]);
function stateMark(s: string | undefined): HTMLElement {
if (s !== undefined && busyStates.has(s)) {
return h("span", { class: "spinner", title: s });
function stateMark(ts: TaskState): HTMLElement {
if (ts.kind === "working" || ts.kind === "merging") {
return h("span", { class: "spinner", title: ts.title });
}
return h("span", { class: `dot ${s ?? "none"}`, title: s ?? "no session" });
return h("span", { class: `dot ${ts.kind}`, title: ts.title });
}
function tasksBlock(active: Task[]): HTMLElement {
@@ -157,9 +155,7 @@ function tasksBlock(active: Task[]): HTMLElement {
stateMark(ts),
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"),
h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label),
),
);
}),
@@ -196,7 +192,7 @@ function todoBlock(project: Project, todo: Todo | string | undefined, active: Ta
const task = active.find((t) => t.todo_item === item.title);
let action: HTMLElement;
if (task) {
const sess = currentSession(task.id);
const ts = taskState(task);
action = h(
"a",
{
@@ -207,8 +203,8 @@ function todoBlock(project: Project, todo: Todo | string | undefined, active: Ta
focusTask(task.id);
},
},
stateMark(sess?.state),
sess?.state ?? "no agent",
stateMark(ts),
ts.label,
);
} else {
const busy = starting.has(`${project.id}\n${item.title}`);
+20 -6
View File
@@ -84,15 +84,29 @@ export function currentSession(taskId: string): AgentSession | undefined {
return state.sessions.get(taskId)?.findLast((s) => s.state !== "completed") ?? state.sessions.get(taskId)?.at(-1);
}
export function taskState(task: Task): string | undefined {
if (task.discarding) {
return "discarding";
export type TaskState = { kind: "answer" | "merging" | "held" | "error" | "working" | "idle"; label: string; title: string };
export function taskState(task: Task): TaskState {
const open = state.inbox.filter((item) => item.task_id === task.id);
if (open.some((item) => item.kind === "question")) {
return { kind: "answer", label: "Needs your answer", title: "The agent asked a question in the inbox" };
}
if (task.merging) {
return "merging";
return { kind: "merging", label: "Merging", title: "The merge turn is running" };
}
const s = currentSession(task.id)?.state;
return s === "idle" && task.reviewing ? "reviewing" : s;
if (task.merging_into_parent) {
return { kind: "merging", label: "Merging", title: "Waiting for the parent task's agent to merge it" };
}
if (task.merge_queued_at !== undefined) {
return { kind: "held", label: "Merge held", title: "Waiting for the merge in progress in this project to finish" };
}
if (open.length > 0) {
return { kind: "error", label: "Error", title: "The agent's session failed or hit a rate limit; see the inbox" };
}
if (task.discarding || task.reviewing || currentSession(task.id)?.state === "running") {
return { kind: "working", label: "Working", title: task.discarding ? "Discarding" : task.reviewing ? "A review is running" : "The agent is mid-turn" };
}
return { kind: "idle", label: "Idle", title: "Waiting for a message" };
}
export function currentLead():AgentSession | undefined {
+2 -4
View File
@@ -148,7 +148,7 @@ export function openSwitcher(): void {
const el = h(
"li",
{ style: taskStyle(task.id) },
h("span", { class: `dot ${ts ?? "none"}`, title: ts ?? "no session" }),
h("span", { class: `dot ${ts.kind}`, title: ts.title }),
h(
"span",
{ class: "task-title" },
@@ -158,9 +158,7 @@ export function openSwitcher(): void {
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"),
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"),
h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label),
);
return { el, pick: () => go(task.id) };
});