Give each task an assigned colour across the UI; lead links tasks

This commit is contained in:
Ian Gulliver
2026-10-15 13:30:09 -07:00
parent 74ce1dd6ec
commit 696385fced
32 changed files with 375 additions and 193 deletions
+2
View File
@@ -814,6 +814,8 @@ export interface components {
base_ref: string;
/** @description The first of the task's block of ports (port_base to port_base+9), passed to its agent and terminal as PORT. */
port_base: number;
/** @description Index into the UI's task palette, assigned at creation: a colour no active task holds, the one given out longest ago. */
color: number;
/** @enum {string} */
status: "active" | "merged" | "discarded";
created_by: string;
+2 -1
View File
@@ -4,6 +4,7 @@ import { ago, errorText, h, replace, showAlert } from "./dom.js";
import { image, inlineMarkdown, markdown } from "./markdown.js";
import { diagram } from "./mermaid.js";
import { onEach, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const checked = new Map<string, string[]>();
const texts = new Map<string, string>();
@@ -307,7 +308,7 @@ function attachments(list: Attachment[], taskId: string | undefined): HTMLElemen
case "diagram":
return h("li", {}, diagram(a.mermaid ?? ""), caption);
case "task":
return h("li", {}, h("a", { href: `#/task/${a.task_id}` }, "task ", a.task_id ?? ""), caption);
return h("li", {}, h("a", { href: `#/task/${a.task_id}`, class: "task-link", style: taskStyle(a.task_id ?? "") }, "task ", a.task_id ?? ""), caption);
case "snapshot":
return h("li", {}, "snapshot ", h("code", {}, a.snapshot_id ?? ""), caption);
case "url":
+5
View File
@@ -1,5 +1,6 @@
import { h } from "./dom.js";
import { diagram } from "./mermaid.js";
import { taskStyle } from "./taskcolor.js";
type Block = HTMLElement;
@@ -310,6 +311,10 @@ function link(href: string, children: Node[]): Node {
if (/^https?:\/\//i.test(href)) {
return h("a", { href, target: "_blank", rel: "noopener noreferrer" }, ...children);
}
const task = /^#\/task\/(\w+)$/.exec(href);
if (task) {
return h("a", { href, class: "task-link", style: taskStyle(task[1]!) }, ...children);
}
if (href.startsWith("#/")) {
return h("a", { href }, ...children);
}
+3 -2
View File
@@ -4,6 +4,7 @@ import { openSettings } from "./settings.js";
import { activeTasks, currentSession, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, state } from "./state.js";
import { openStatus } from "./status.js";
import { confirmMerge } from "./task.js";
import { taskStyle } from "./taskcolor.js";
export function mountNavbar(root: HTMLElement): void {
const status = h("button", { class: "nav-link" }, "Status");
@@ -103,9 +104,9 @@ export function openSwitcher(): void {
const sess = currentSession(task.id);
const el = h(
"li",
{},
{ style: taskStyle(task.id) },
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.title),
h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", 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"),
h("span", { class: "task-state" }, sess?.state ?? "no agent"),
+7 -3
View File
@@ -8,8 +8,7 @@ import { openSwitcher } from "./navbar.js";
import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
import { mountPreview } from "./preview.js";
import { mountProject } from "./project.js";
import { mountTranscript } from "./task.js";
import { mountTerminal } from "./terminal.js";
import { mountTranscript } from "./task.js";import { mountTerminal } from "./terminal.js";
const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () => void }> = {
transcript: { label: "Transcript", mount: mountTranscript },
@@ -32,12 +31,18 @@ export function taskPane(): HTMLElement {
const body = h("div", { class: "pane-body" });
const picker = h("button", { class: "task-picker", title: "Switch task (⌘K)" });
picker.onclick = openSwitcher;
const pane = h("section", { class: "pane task-pane", tabIndex: -1 }, strip, body);
let shown: TaskTab | undefined;
let dispose: () => void = () => {};
const renderPicker = () => {
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
const project = focusedProject();
if (task) {
pane.style.setProperty("--task", `var(--task-${task.color})`);
} else {
pane.style.removeProperty("--task");
}
replace(picker, task ? `${project?.name ?? ""} ▸ ${task.title}` : (project?.name ?? "No task"), " ▾");
};
@@ -76,7 +81,6 @@ export function taskPane(): HTMLElement {
onEach(["projects", "tasks", "selection", "focus"], renderPicker);
render();
renderPicker();
const pane = h("section", { class: "pane", tabIndex: -1 }, strip, body);
trackPaneFocus(pane);
return pane;
}
+5 -4
View File
@@ -3,6 +3,7 @@ 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 } from "./state.js";
import { taskStyle } from "./taskcolor.js";
type Todo = Schemas["Todo"];
type TodoItem = Schemas["TodoItem"];
@@ -118,7 +119,7 @@ function tasksBlock(active: Task[]): HTMLElement {
const count = state.inbox.filter((item) => item.task_id === task.id).length;
return h(
"li",
{ class: task.id === state.selectedTaskId ? "selected" : "" },
{ class: task.id === state.selectedTaskId ? "selected" : "", style: taskStyle(task.id) },
h(
"a",
{
@@ -129,7 +130,7 @@ function tasksBlock(active: Task[]): HTMLElement {
},
},
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
h("span", { class: "task-title" }, task.title),
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" }, sess?.state ?? "no agent"),
@@ -193,8 +194,8 @@ function todoBlock(project: Project, todo: Todo | string | undefined, active: Ta
list.append(
h(
"li",
{ class: task && task.id === state.selectedTaskId ? "selected" : "" },
h("div", { class: "todo-head" }, h("span", { class: "todo-title" }, item.title), action),
{ class: task && task.id === state.selectedTaskId ? "selected" : "", style: task ? taskStyle(task.id) : "" },
h("div", { class: "todo-head" }, h("span", { class: task ? "todo-title task-name" : "todo-title" }, item.title), action),
item.body && markdown(item.body, "md todo-body"),
),
);
+4 -3
View File
@@ -1,6 +1,7 @@
import { get, type AgentProcess, type ByreEvent, type Status, type TurnRef, type Zombie } from "./api.js";
import { ago, errorText, h, openPanel, replace } from "./dom.js";
import { findTask, on, onEach, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]);
@@ -147,12 +148,12 @@ function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string, p
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h(
"li",
{},
{ style: taskStyle(t.task_id) },
h(
"a",
{ href: `#/task/${t.task_id}` },
kind === "queued" ? h("span", { class: "queue-pos" }, `${i + 1}.`) : h("span", { class: "dot running", title: "running" }),
h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), task?.title ?? t.task_id),
h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), h("span", { class: "task-name" }, task?.title ?? t.task_id)),
liveness(procs.find((p) => p.session_id === t.session_id)),
h("span", { class: "task-state" }, t.vendor),
),
@@ -168,7 +169,7 @@ function taskLink(taskId: string | undefined): HTMLElement | string {
}
const task = findTask(taskId);
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h("a", { href: `#/task/${taskId}` }, project ? `${project.name} ▸ ` : "", task?.title ?? taskId);
return h("a", { href: `#/task/${taskId}`, class: "task-link", style: taskStyle(taskId) }, project ? `${project.name} ▸ ` : "", task?.title ?? taskId);
}
function since(iso: string): string {
+3
View File
@@ -3,6 +3,7 @@ import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
import { markdown } from "./markdown.js";
import { previewURL } from "./preview.js";
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, state, type Topic } from "./state.js";
import { taskStyle } from "./taskcolor.js";
export type Usage = { used: number; window: number; model: string };
@@ -987,6 +988,8 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
"a",
{
href: `#/task/${src.id}`,
class: "task-link",
style: taskStyle(src.id),
onclick: (e: MouseEvent) => {
e.preventDefault();
focusTask(src.id);
+6
View File
@@ -0,0 +1,6 @@
import { findTask } from "./state.js";
export function taskStyle(taskId: string): string {
const task = findTask(taskId);
return task ? `--task: var(--task-${task.color})` : "";
}