Human-facing task state
This commit is contained in:
Vendored
+8
-9
@@ -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, findTask, focusedProject, focusTask, on, onEach, state, taskState } from "./state.js";
|
||||
import { 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) {
|
||||
@@ -100,12 +100,11 @@ function nested(active) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const busyStates = new Set(["running", "reviewing", "merging", "discarding"]);
|
||||
function stateMark(s) {
|
||||
if (s !== undefined && busyStates.has(s)) {
|
||||
return h("span", { class: "spinner", title: s });
|
||||
function stateMark(ts) {
|
||||
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) {
|
||||
if (active.length === 0) {
|
||||
@@ -121,7 +120,7 @@ function tasksBlock(active) {
|
||||
e.preventDefault();
|
||||
focusTask(task.id);
|
||||
},
|
||||
}, task.parent_task_id !== undefined && h("span", { class: "subtask-mark", title: `Subtask of ${findTask(task.parent_task_id)?.title ?? task.parent_task_id}` }, "↳"), 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")));
|
||||
}, task.parent_task_id !== undefined && h("span", { class: "subtask-mark", title: `Subtask of ${findTask(task.parent_task_id)?.title ?? task.parent_task_id}` }, "↳"), stateMark(ts), h("span", { class: "task-title task-name" }, task.title), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label)));
|
||||
}));
|
||||
}
|
||||
function todoBlock(project, todo, active, starting, start) {
|
||||
@@ -152,7 +151,7 @@ function todoBlock(project, todo, active, starting, start) {
|
||||
const task = active.find((t) => t.todo_item === item.title);
|
||||
let action;
|
||||
if (task) {
|
||||
const sess = currentSession(task.id);
|
||||
const ts = taskState(task);
|
||||
action = h("a", {
|
||||
class: "todo-task",
|
||||
href: `#/task/${task.id}`,
|
||||
@@ -160,7 +159,7 @@ function todoBlock(project, todo, active, starting, start) {
|
||||
e.preventDefault();
|
||||
focusTask(task.id);
|
||||
},
|
||||
}, stateMark(sess?.state), sess?.state ?? "no agent");
|
||||
}, stateMark(ts), ts.label);
|
||||
}
|
||||
else {
|
||||
const busy = starting.has(`${project.id}\n${item.title}`);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+17
-5
@@ -55,14 +55,26 @@ 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";
|
||||
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() {
|
||||
return state.leadSessions.findLast((s) => s.state !== "completed") ?? state.leadSessions.at(-1);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -132,7 +132,7 @@ export function openSwitcher() {
|
||||
.sort((a, b) => Number(b.count > 0) - Number(a.count > 0) || a.visited - b.visited);
|
||||
rows = matches.map(({ project, task, count }) => {
|
||||
const ts = taskState(task);
|
||||
const el = h("li", { style: taskStyle(task.id) }, h("span", { class: `dot ${ts ?? "none"}`, title: ts ?? "no session" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.parent_task_id !== undefined && h("span", { class: "subtask-mark" }, `${findTask(task.parent_task_id)?.title ?? task.parent_task_id} ↳ `), 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"));
|
||||
const el = h("li", { style: taskStyle(task.id) }, h("span", { class: `dot ${ts.kind}`, title: ts.title }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.parent_task_id !== undefined && h("span", { class: "subtask-mark" }, `${findTask(task.parent_task_id)?.title ?? task.parent_task_id} ↳ `), h("span", { class: "task-name" }, task.title)), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label));
|
||||
return { el, pick: () => go(task.id) };
|
||||
});
|
||||
for (const p of (targets ?? state.projects).filter((p) => `${p.name} project`.toLowerCase().includes(q))) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+9
-13
@@ -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
@@ -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
@@ -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) };
|
||||
});
|
||||
|
||||
+11
-6
@@ -568,18 +568,22 @@ a.task-link {
|
||||
}
|
||||
|
||||
.dot.running,
|
||||
.dot.reviewing,
|
||||
.dot.merging,
|
||||
.dot.discarding {
|
||||
.dot.working,
|
||||
.dot.merging {
|
||||
background: var(--ok);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dot.queued {
|
||||
.dot.answer {
|
||||
background: var(--warn);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dot.error {
|
||||
background: var(--danger);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -2301,14 +2305,15 @@ button.danger-primary {
|
||||
|
||||
.rl-status.warn,
|
||||
.rl-windows .warn,
|
||||
.task-state.warn {
|
||||
.task-state.answer {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.rl-status.danger,
|
||||
.rl-windows .danger,
|
||||
.problems .danger,
|
||||
.task-state.danger {
|
||||
.task-state.danger,
|
||||
.task-state.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user