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 -1
View File
@@ -863,7 +863,7 @@ components:
Task:
type: object
required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at]
required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at]
properties:
id: { type: string }
project_id: { type: string }
@@ -873,6 +873,7 @@ components:
worktree_path: { type: string }
base_ref: { type: string }
port_base: { type: integer, description: "The first of the task's block of ports (port_base to port_base+9), passed to its agent and terminal as PORT." }
color: { type: integer, description: "Index into the UI's task palette, assigned at creation: a colour no active task holds, the one given out longest ago." }
status:
type: string
enum: [active, merged, discarded]
+3 -1
View File
@@ -124,10 +124,12 @@ The Status modal (the navbar's **Status** button; Esc, ×, a click outside it or
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `inbox_item_id` when its final message was filed as a `report` item. Activity inside a subagent (`agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`) carries `payload.parent_tool_use_id`, the ID of the tool call that started the subagent, and never updates `session.context`. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header's **Compact** button shows the latest one as a background fill across the button, proportional to the window, tinted amber from 70% and red from 90%; the exact count is in its tooltip.
Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and Merge prompts, Compact's handoff request) show collapsed like a tool call, labelled `byre` with the first line as the brief; expanding it shows the full text. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line, cycling ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. It is one element with one CSS animation: the ten frames are drawn side by side in an inline SVG used as the `.spinner` mask over an accent background, and the `braille` keyframes in `web/style.css` step the mask position through them. Orion freezes animated GIFs and per-dot `step-end` opacity animations partway through a turn, so it stays a single element with a single animation. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A Claude Code subagent call (the `Agent` or `Task` tool) stands on its own, collapsed, labelled with its `subagent_type` and `description`; expanding it shows the prompt and the call's result. Its summary carries a status from the `agent.vendor` `system` events whose `tool_use_id` names it (`task_started`: running, or running in the background; `task_progress`: its `description`; `task_notification`: its `status`, red unless `completed`). The subagent's own messages, thinking and tool calls (events whose `parent_tool_use_id` names the call) stream inline with the main agent's, in arrival order, each marked `↳ <subagent_type>` with the description as its tooltip; its messages also get an accent rule down their left edge. A run of tool calls groups only calls from the same agent, so a call from another agent starts a new group. A subagent's messages never end the main agent's live message, count as a turn's final message or hide a quiet turn. Claude Code streams a background subagent's activity, but for a foreground one only its prompt, so a foreground subagent shows only as its call, its status and its result. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. Selecting a task that has never had a session focuses the composer. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload. The composer has no `@`-file mentions or path picker: text reaches the agent exactly as typed (stream-json doesn't expand `@path` the way the interactive CLI does), and the agent reads any file the message names with its own tools. The composer is the same whether or not the task has a live session: Send posts to the session's messages, or starts a session with the text when there is none or the last one completed. A sent message shows at once as a "sending…" bubble, replaced by the server's `user.message` event with the same text when it arrives, and removed (text and images back in the composer) if the request fails.
Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and Merge prompts, Compact's handoff request) show collapsed like a tool call, labelled `byre` with the first line as the brief; expanding it shows the full text. The transcript view has a border in the task's colour (see "Task colours"; the accent in the lead pane) while the session is `running`, and a braille spinner on its last line in the same colour, cycling ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. It is one element with one CSS animation: the ten frames are drawn side by side in an inline SVG used as the `.spinner` mask over a coloured background, and the `braille` keyframes in `web/style.css` step the mask position through them. Orion freezes animated GIFs and per-dot `step-end` opacity animations partway through a turn, so it stays a single element with a single animation. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A Claude Code subagent call (the `Agent` or `Task` tool) stands on its own, collapsed, labelled with its `subagent_type` and `description`; expanding it shows the prompt and the call's result. Its summary carries a status from the `agent.vendor` `system` events whose `tool_use_id` names it (`task_started`: running, or running in the background; `task_progress`: its `description`; `task_notification`: its `status`, red unless `completed`). The subagent's own messages, thinking and tool calls (events whose `parent_tool_use_id` names the call) stream inline with the main agent's, in arrival order, each marked `↳ <subagent_type>` with the description as its tooltip; its messages also get an accent rule down their left edge. A run of tool calls groups only calls from the same agent, so a call from another agent starts a new group. A subagent's messages never end the main agent's live message, count as a turn's final message or hide a quiet turn. Claude Code streams a background subagent's activity, but for a foreground one only its prompt, so a foreground subagent shows only as its call, its status and its result. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. Selecting a task that has never had a session focuses the composer. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload. The composer has no `@`-file mentions or path picker: text reaches the agent exactly as typed (stream-json doesn't expand `@path` the way the interactive CLI does), and the agent reads any file the message names with its own tools. The composer is the same whether or not the task has a live session: Send posts to the session's messages, or starts a session with the text when there is none or the last one completed. A sent message shows at once as a "sending…" bubble, replaced by the server's `user.message` event with the same text when it arrives, and removed (text and images back in the composer) if the request fails.
The task switcher (⌘K, or the project ▸ task picker at the right end of the right pane's tab strip) lists active tasks with their session state and open inbox item count, and **merge held** on a task with `merge_queued_at`, live while it is open. Tasks with inbox items come first; within each group, the task least recently visited in this browser comes first (never-visited tasks before all others, ties in the server's order: projects by name, tasks newest first). Visit times are kept in the browser's localStorage under `byre.visits`, stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Picking a task points the right pane at it on the Transcript tab (see "Lead agent").
**Task colours.** Each task has a colour, `tasks.color` (`color` in the API), an index into the palette of `--task-0` … `--task-N` custom properties at the top of `web/style.css`, one set for light and one for dark, all at least 4.5:1 against the backgrounds so they work as text. `CreateTask` assigns it in the same transaction as the task's row: of the `store.TaskColors` colours, one no active task holds, and among those the one given out longest ago (the lowest `max(rowid)` of tasks with that colour, never-used first), so a new task doesn't take the colour of one just closed. Active tasks have distinct colours until more than `TaskColors` are active at once. `TaskColors` must match the number of palette entries. An element styled with `taskStyle(id)` (`web/src/taskcolor.ts`) sets `--task` for itself and its children from the task's `color`; `--task` defaults to the accent, which is also what a task the page doesn't know gets. The right pane sets it for the selected task, which tints its tab strip, colours the task picker's text, the selected tab's underline, the composer's border and the running border and spinner. Everywhere a task is named it shows in its colour: task titles in the switcher, the Project tab and the Status modal (`.task-name`), and task links (`a.task-link`), including any Markdown link to `#/task/<id>`. The lead's guidance tells it to link every task it names that way, and clicking one points the right pane at the task.
## Lead agent
Each user has one long-running lead agent, shown in the left pane. It is not tied to a project: it watches every task, tells the human what needs them, points the right pane at it, and acts on the other agents. Its code is `internal/agents/lead.go` (sessions, held input, signals) and `internal/agents/leadtools.go` (its MCP tools).
+1
View File
@@ -29,6 +29,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- Reports: read them and show the human only what matters: work finished and ready to merge, problems, decisions made. Stay quiet about routine progress.
- Dig into context with list_tasks, read_transcript and read_inbox, and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message.
- You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it.
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane.
- Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
- You are long-running. At logical points (a piece of work wrapped up, a topic closed, before turning to something unrelated) call compact_self; when the turn ends, byre replaces you with a fresh session that starts from your handoff.
- Don't commit, push, or touch git branches yourself.`
+157 -153
View File
@@ -812,6 +812,9 @@ type Task struct {
// ClosedAt When the task was merged or discarded.
ClosedAt *time.Time `json:"closed_at,omitempty"`
// Color Index into the UI's task palette, assigned at creation: a colour no active task holds, the one given out longest ago.
Color int `json:"color"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by"`
@@ -2729,159 +2732,160 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"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",
"889Dx6XpH3aH37//fwEAAP//",
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+60
View File
@@ -0,0 +1,60 @@
package store_test
import (
"context"
"fmt"
"path/filepath"
"testing"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/store"
)
func TestTaskColorsAvoidActiveAndRecent(t *testing.T) {
ctx := context.Background()
st, err := store.Open(filepath.Join(t.TempDir(), "byre.db"), 51000, func(api.Event) {})
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() { st.Close() })
now := time.Now().UTC()
_, err = st.EnsureUser(ctx, "alice", "Alice")
if err != nil {
t.Fatalf("ensure user: %v", err)
}
err = st.CreateProject(ctx, api.Project{Id: "p1", Name: "demo", RepoPath: "/nonexistent", DefaultBranch: "main", Remote: "none", CreatedBy: "alice", CreatedAt: now})
if err != nil {
t.Fatalf("create project: %v", err)
}
create := func(id string) int {
err := st.CreateTask(ctx, api.Task{Id: id, ProjectId: "p1", Title: id, Branch: id, WorktreePath: "/nonexistent", BaseRef: "x", Status: api.TaskStatusActive, CreatedBy: "alice", CreatedAt: now}, "alice")
if err != nil {
t.Fatalf("create task %s: %v", id, err)
}
task, err := st.GetTask(ctx, id)
if err != nil {
t.Fatalf("get task %s: %v", id, err)
}
return task.Color
}
if c1, c2 := create("t1"), create("t2"); c1 != 0 || c2 != 1 {
t.Fatalf("colors = %d, %d, want 0, 1", c1, c2)
}
_, err = st.DiscardTask(ctx, "t1", "alice")
if err != nil {
t.Fatalf("discard: %v", err)
}
if c := create("t3"); c != 2 {
t.Fatalf("t3 color = %d, want 2, not the just-freed 0", c)
}
for i := 4; i <= store.TaskColors; i++ {
create(fmt.Sprintf("t%d", i))
}
if c := create("t12"); c != 0 {
t.Fatalf("t12 color = %d, want 0, the only one no active task holds", c)
}
}
+21 -5
View File
@@ -280,11 +280,18 @@ CREATE TABLE previews (
PRIMARY KEY (task_id, name),
UNIQUE (task_id, port)
);
`}, {"add-task-color", `
ALTER TABLE tasks ADD COLUMN color INTEGER NOT NULL DEFAULT 0;
UPDATE tasks SET color = (SELECT count(*) FROM tasks AS earlier WHERE earlier.rowid < tasks.rowid) % 11;
UPDATE tasks SET color = (
SELECT count(*) FROM tasks AS earlier WHERE earlier.status = 'active' AND earlier.rowid < tasks.rowid
) % 11 WHERE status = 'active';
`}}
const (
DefaultPortFirst = 41000
PortBlock = 10
TaskColors = 11
)
type Store struct {
@@ -571,10 +578,19 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, actor string) error
if err != nil {
return err
}
err = tx.QueryRowContext(ctx, `
WITH RECURSIVE palette(c) AS (SELECT 0 UNION ALL SELECT c + 1 FROM palette WHERE c + 1 < ?)
SELECT c FROM palette
ORDER BY c IN (SELECT color FROM tasks WHERE status = ?), coalesce((SELECT max(rowid) FROM tasks WHERE color = c), -1), c
LIMIT 1
`, TaskColors, string(api.TaskStatusActive)).Scan(&t.Color)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at, todo_item, forked_from, forked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt)
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt)
if err != nil {
return err
}
@@ -1090,7 +1106,7 @@ func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) er
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at`
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
@@ -1119,7 +1135,7 @@ func scanTask(row scanner) (api.Task, error) {
forkedFrom := ""
forkedAt := 0
mergeQueuedAt := ""
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt)
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt)
if err != nil {
return t, err
}
+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();
const texts = new Map();
export function mountInbox(root) {
@@ -260,7 +261,7 @@ function attachments(list, taskId) {
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":
+1 -1
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -1,5 +1,6 @@
import { h } from "./dom.js";
import { diagram } from "./mermaid.js";
import { taskStyle } from "./taskcolor.js";
const choiceMarker = /^(\([ *]\)|\[[ xX]\])\s+(\S.*?)\s*$/;
const detailsOpen = /^\s*<details>\s*(?:<summary>(.*?)<\/summary>)?\s*$/i;
const summaryLine = /^\s*<summary>(.*?)<\/summary>\s*$/i;
@@ -270,6 +271,10 @@ function link(href, children) {
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);
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
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) {
const status = h("button", { class: "nav-link" }, "Status");
status.onclick = openStatus;
@@ -92,7 +93,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 sess = currentSession(task.id);
const el = h("li", {}, 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), 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"));
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), " ", 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"));
return { el, pick: () => go(task.id) };
});
for (const p of state.projects.filter((p) => `${p.name} project`.toLowerCase().includes(q))) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+7 -1
View File
@@ -29,11 +29,18 @@ export function taskPane() {
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;
let dispose = () => { };
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"), " ▾");
};
const render = () => {
@@ -60,7 +67,6 @@ export function taskPane() {
onEach(["projects", "tasks", "selection", "focus"], renderPicker);
render();
renderPicker();
const pane = h("section", { class: "pane", tabIndex: -1 }, strip, body);
trackPaneFocus(pane);
return pane;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -3
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";
const refreshKinds = new Set(["task.merged", "project.created"]);
export function mountProject(root) {
const todos = new Map();
@@ -93,13 +94,13 @@ function tasksBlock(active) {
return h("ul", { class: "task-list" }, ...active.map((task) => {
const sess = currentSession(task.id);
const count = state.inbox.filter((item) => item.task_id === task.id).length;
return h("li", { class: task.id === state.selectedTaskId ? "selected" : "" }, h("a", {
return h("li", { class: task.id === state.selectedTaskId ? "selected" : "", style: taskStyle(task.id) }, h("a", {
href: `#/task/${task.id}`,
onclick: (e) => {
e.preventDefault();
focusTask(task.id);
},
}, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, 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")));
}, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), 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")));
}));
}
function todoBlock(project, todo, active, starting, start) {
@@ -146,7 +147,7 @@ function todoBlock(project, todo, active, starting, start) {
button.onclick = () => void start(project, item);
action = button;
}
list.append(h("li", { class: task && task.id === state.selectedTaskId ? "selected" : "" }, h("div", { class: "todo-head" }, h("span", { class: "todo-title" }, item.title), action), item.body && markdown(item.body, "md todo-body")));
list.append(h("li", { 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")));
}
return children;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -1,6 +1,7 @@
import { get } 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"]);
export function openStatus() {
openPanel("Status", mountStatus);
@@ -99,7 +100,7 @@ function turnList(turns, kind, empty, procs) {
return h("ul", { class: "task-list" }, ...turns.map((t, i) => {
const task = findTask(t.task_id);
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
return h("li", {}, 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), liveness(procs.find((p) => p.session_id === t.session_id)), h("span", { class: "task-state" }, t.vendor)));
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} ▸ `), 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)));
}), turns.length === 0 && h("li", { class: "empty-row" }, empty));
}
function taskLink(taskId) {
@@ -108,7 +109,7 @@ function taskLink(taskId) {
}
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) {
const seconds = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
+1 -1
View File
File diff suppressed because one or more lines are too long
+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 } from "./state.js";
import { taskStyle } from "./taskcolor.js";
export const models = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1"];
const quietReply = "NO_REPLY";
const taskSource = {
@@ -831,6 +832,8 @@ function renderEvent(ev, ctx) {
const link = src?.status === "active"
? h("a", {
href: `#/task/${src.id}`,
class: "task-link",
style: taskStyle(src.id),
onclick: (e) => {
e.preventDefault();
focusTask(src.id);
+1 -1
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
import { findTask } from "./state.js";
export function taskStyle(taskId) {
const task = findTask(taskId);
return task ? `--task: var(--task-${task.color})` : "";
}
//# sourceMappingURL=taskcolor.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"taskcolor.js","sourceRoot":"","sources":["../src/taskcolor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC9B,OAAO,IAAI,CAAC,CAAC,CAAC,sBAAsB,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AACzD,CAAC"}
+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})` : "";
}
+51 -2
View File
@@ -49,6 +49,18 @@
--warn: #855600;
--warn-tint: #fdf3dc;
--ok: #1a7f37;
--task-0: #a4273f;
--task-1: #9f3501;
--task-2: #376e00;
--task-3: #027048;
--task-4: #026d64;
--task-5: #046a77;
--task-6: #06658e;
--task-7: #1f58b6;
--task-8: #5849b2;
--task-9: #793c9f;
--task-10: #942e79;
--task: var(--accent);
--shadow: 0 8px 28px rgb(16 24 40 / 0.18);
--font: "Atkinson Hyperlegible Mono", ui-monospace, Menlo, monospace;
font: 13px/1.55 var(--font);
@@ -69,6 +81,17 @@
--warn: #f2c66d;
--warn-tint: #352a14;
--ok: #7fd88f;
--task-0: #fe9fa6;
--task-1: #ffa383;
--task-2: #99d079;
--task-3: #68d7a1;
--task-4: #37d8c9;
--task-5: #2fd4ec;
--task-6: #69caff;
--task-7: #98bffe;
--task-8: #b6b4fe;
--task-9: #d7a6fc;
--task-10: #f59cd8;
--shadow: 0 8px 28px rgb(0 0 0 / 0.5);
}
}
@@ -282,6 +305,24 @@ textarea {
color: var(--text);
}
.task-pane > .pane-tabs {
background: color-mix(in srgb, var(--task) 12%, var(--bg));
}
.task-pane > .pane-tabs .task-picker {
color: var(--task);
font-weight: 700;
}
.task-pane > .pane-tabs button.active {
border-bottom-color: var(--task);
}
.task-name,
a.task-link {
color: var(--task);
}
.pane-tabs button:hover:not(:disabled),
.range-tabs button:hover:not(:disabled),
.modes button:hover:not(:disabled) {
@@ -534,7 +575,7 @@ button.compact.danger {
}
.transcript.running {
border-color: var(--accent);
border-color: var(--task);
}
.spinner {
@@ -543,7 +584,7 @@ button.compact.danger {
width: 5px;
height: 10px;
margin: 5px 1px;
background: var(--accent);
background: var(--task);
-webkit-mask: var(--frames) 0 0 / 50px 10px no-repeat;
mask: var(--frames) 0 0 / 50px 10px no-repeat;
animation: braille 0.8s steps(10) infinite;
@@ -901,6 +942,14 @@ details.note .md {
height: calc(4.5lh + 14px);
}
.task-pane .compose textarea {
border-color: var(--task);
}
.task-pane .compose textarea:focus {
box-shadow: 0 0 0 1px var(--task);
}
.compose-actions {
display: flex;
justify-content: flex-end;