Inbox consideration

This commit is contained in:
Ian Gulliver
2026-10-15 20:20:02 -07:00
parent 35368242e3
commit 8d5ebe727a
13 changed files with 186 additions and 6 deletions
+9
View File
@@ -1,6 +1,7 @@
import { copyable, h } from "./dom.js";
import { diagram } from "./mermaid.js";
import { taskStyle } from "./taskcolor.js";
import { todoLink } from "./todolink.js";
type Block = HTMLElement;
@@ -315,6 +316,14 @@ function link(href: string, children: Node[]): Node {
if (task) {
return h("a", { href, class: "task-link", style: taskStyle(task[1]!) }, ...children);
}
const todo = /^#\/todo\/(\w+)\/(.+)$/.exec(href);
if (todo) {
try {
return todoLink(todo[1]!, decodeURIComponent(todo[2]!), children);
} catch {
return h("span", {}, ...children);
}
}
if (href.startsWith("#/")) {
return h("a", { href }, ...children);
}
+4
View File
@@ -5,6 +5,7 @@ import { previewURL } from "./preview.js";
import { findingCount, reviewCounts } from "./reviews.js";
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, showReview, state, type Topic } from "./state.js";
import { taskStyle } from "./taskcolor.js";
import { fillTodoLinks } from "./todolink.js";
export type Usage = { used: number; window: number; model: string };
@@ -380,6 +381,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
stream.timer = 0;
stream.el.hidden = ctx.held !== undefined || quietReply.startsWith(stream.text.trim());
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
fillTodoLinks(stream.el, models[0]!);
pin();
}, 100);
};
@@ -493,6 +495,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
renderComposer();
}
}
fillTodoLinks(transcript, models[0]!);
pin();
};
@@ -523,6 +526,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
}),
onEach([src.transcriptTopic], (fresh) => appendEvents(fresh as ByreEvent[])),
onEach([src.streamTopic], (ev) => appendDelta(ev as ByreEvent)),
onEach(["tasks", "sessions"], () => fillTodoLinks(transcript, models[0]!)),
];
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));
+65
View File
@@ -0,0 +1,65 @@
import { post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { currentSession, focusTask, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const starting = new Set<string>();
export function todoLink(projectId: string, title: string, children: Node[]): HTMLElement {
return h("span", { class: "todo-link", data: { project: projectId, title } }, h("span", { class: "todo-title" }, ...children), " ", h("span", { class: "todo-action" }));
}
export function fillTodoLinks(root: HTMLElement, model: string): void {
for (const el of root.querySelectorAll<HTMLElement>(".todo-link")) {
fill(el, model);
}
}
function fill(el: HTMLElement, model: string): void {
const projectId = el.dataset["project"]!;
const title = el.dataset["title"]!;
const action = el.querySelector<HTMLElement>(".todo-action")!;
const titleEl = el.querySelector<HTMLElement>(".todo-title")!;
const task = (state.tasks.get(projectId) ?? []).find((t) => t.status === "active" && t.todo_item === title);
if (task) {
const sess = currentSession(task.id);
el.setAttribute("style", taskStyle(task.id));
titleEl.classList.add("task-name");
replace(
action,
h(
"a",
{
class: "todo-task",
href: `#/task/${task.id}`,
onclick: (e: MouseEvent) => {
e.preventDefault();
focusTask(task.id);
},
},
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
sess?.state ?? "no agent",
),
);
return;
}
el.removeAttribute("style");
titleEl.classList.remove("task-name");
const key = `${projectId}\n${title}`;
const busy = starting.has(key);
const button = h("button", { disabled: busy }, busy ? "Starting…" : "Start");
button.onclick = async () => {
starting.add(key);
fill(el, model);
try {
await post("/api/projects/{projectId}/todo/tasks", { path: { projectId }, body: { title, model } });
} catch (err) {
void showAlert(errorText(err));
}
starting.delete(key);
fill(el, model);
};
replace(action, button);
}