Add a per-user lead agent and make the right pane task-focused
This commit is contained in:
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+6
-3
@@ -52,7 +52,7 @@ export const put = (path, opts) => request("put", path, opts);
|
||||
export const del = (path, opts) => request("delete", path, opts);
|
||||
const staleAfterMs = 40_000;
|
||||
const retryAfterMs = 2_000;
|
||||
export function subscribe(after, taskId, onEvent, onState) {
|
||||
export function subscribe(after, filter, onEvent, onState) {
|
||||
let last = after;
|
||||
let heard = Date.now();
|
||||
let source;
|
||||
@@ -62,8 +62,11 @@ export function subscribe(after, taskId, onEvent, onState) {
|
||||
source?.close();
|
||||
heard = Date.now();
|
||||
const query = new URLSearchParams({ after: String(last) });
|
||||
if (taskId) {
|
||||
query.set("task_id", taskId);
|
||||
if (filter.task_id) {
|
||||
query.set("task_id", filter.task_id);
|
||||
}
|
||||
if (filter.lead) {
|
||||
query.set("lead", "true");
|
||||
}
|
||||
const s = new EventSource(`/api/events?${query}`);
|
||||
source = s;
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+16
-9
@@ -1,8 +1,8 @@
|
||||
import { get, put, subscribe } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { mountNavbar } from "./navbar.js";
|
||||
import { Pane } from "./panes.js";
|
||||
import { handleEvent, loadInbox, loadMe, loadProjects, notify, on, selectTask, state } from "./state.js";
|
||||
import { leadPane, taskPane } from "./panes.js";
|
||||
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, notify, on, selectTask, setFocus, startLeadStream, state } from "./state.js";
|
||||
function identityBanner(root) {
|
||||
const render = () => {
|
||||
const me = state.me;
|
||||
@@ -33,6 +33,9 @@ function identityBanner(root) {
|
||||
function route() {
|
||||
const match = /^#\/task\/(\w+)$/.exec(location.hash);
|
||||
const taskId = match?.[1];
|
||||
if (taskId !== state.focus.task_id) {
|
||||
setFocus({ task_id: taskId ?? null }).catch((err) => console.error("set focus", err));
|
||||
}
|
||||
if (taskId !== state.selectedTaskId) {
|
||||
selectTask(taskId);
|
||||
}
|
||||
@@ -40,10 +43,16 @@ function route() {
|
||||
async function main() {
|
||||
const navbar = h("header", { class: "navbar" });
|
||||
const banner = h("div", { class: "banner", hidden: true });
|
||||
const left = new Pane("left", "transcript");
|
||||
const right = new Pane("right", "inbox");
|
||||
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
|
||||
mountNavbar(navbar, (view) => right.show(view));
|
||||
const status = await get("/api/admin/status");
|
||||
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions()]);
|
||||
const linked = location.hash;
|
||||
await loadFocus();
|
||||
if (/^#\/task\/\w+$/.test(linked)) {
|
||||
location.hash = linked;
|
||||
}
|
||||
startLeadStream();
|
||||
replace(document.body, navbar, banner, h("div", { class: "panes" }, leadPane(), taskPane()));
|
||||
mountNavbar(navbar);
|
||||
identityBanner(banner);
|
||||
addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Escape") {
|
||||
@@ -52,9 +61,7 @@ async function main() {
|
||||
e.preventDefault();
|
||||
document.querySelector("dialog[open]")?.close();
|
||||
}, { capture: true });
|
||||
const status = await get("/api/admin/status");
|
||||
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
||||
subscribe(status.last_event_seq, undefined, handleEvent, (connected) => {
|
||||
subscribe(status.last_event_seq, {}, handleEvent, (connected) => {
|
||||
state.connected = connected;
|
||||
notify("connection");
|
||||
});
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+21
@@ -55,6 +55,27 @@ function modal(text, buttons) {
|
||||
actions.at(-1)?.focus();
|
||||
});
|
||||
}
|
||||
export function openPanel(title, mount) {
|
||||
if (document.querySelector("dialog.panel[open]")) {
|
||||
return;
|
||||
}
|
||||
const close = h("button", { type: "button", class: "panel-close", title: "Close (Esc)" }, "×");
|
||||
const body = h("div", { class: "panel-body" });
|
||||
const dialog = h("dialog", { class: "modal panel" }, h("header", { class: "panel-head" }, h("h2", {}, title), close), body);
|
||||
close.onclick = () => dialog.close();
|
||||
dialog.onclick = (e) => {
|
||||
if (e.target === dialog || e.target.closest("a[href^='#']")) {
|
||||
dialog.close();
|
||||
}
|
||||
};
|
||||
document.body.append(dialog);
|
||||
const dispose = mount(body);
|
||||
dialog.onclose = () => {
|
||||
dispose();
|
||||
dialog.remove();
|
||||
};
|
||||
dialog.showModal();
|
||||
}
|
||||
export async function showAlert(text) {
|
||||
await modal(text, [{ label: "OK", value: "ok", class: "primary" }]);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+11
-71
@@ -3,44 +3,17 @@ import { mountChanges } from "./changes.js";
|
||||
import { ago, errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { image, inlineMarkdown, markdown } from "./markdown.js";
|
||||
import { diagram } from "./mermaid.js";
|
||||
import { findTask, onEach, state } from "./state.js";
|
||||
import { onEach, state } from "./state.js";
|
||||
const checked = new Map();
|
||||
const texts = new Map();
|
||||
let scope = "all";
|
||||
export function mountInbox(root) {
|
||||
const tabs = h("nav", { class: "scope-tabs" });
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map();
|
||||
const groups = new Map();
|
||||
const empty = h("div", { class: "empty" }, "Nothing waiting on you.");
|
||||
const visible = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (scope === "task" && task) {
|
||||
return state.inbox.filter((i) => i.task_id === task.id);
|
||||
}
|
||||
if (scope === "project" && task) {
|
||||
return state.inbox.filter((i) => i.project_id === task.project_id);
|
||||
}
|
||||
return state.inbox;
|
||||
};
|
||||
const renderTabs = () => {
|
||||
const hasTask = state.selectedTaskId !== undefined;
|
||||
const tab = (s, label) => {
|
||||
const b = h("button", { class: s === scope ? "active" : "", disabled: s !== "all" && !hasTask }, label);
|
||||
b.onclick = () => {
|
||||
scope = s;
|
||||
render();
|
||||
};
|
||||
return b;
|
||||
};
|
||||
replace(tabs, tab("all", "All"), tab("project", "This project"), tab("task", "This task"));
|
||||
};
|
||||
const nothing = h("div", { class: "empty" }, "Nothing waiting on you in this task.");
|
||||
const noTask = h("div", { class: "empty" }, "No task selected.");
|
||||
const render = () => {
|
||||
if (scope !== "all" && !state.selectedTaskId) {
|
||||
scope = "all";
|
||||
}
|
||||
renderTabs();
|
||||
const items = visible();
|
||||
const taskId = state.selectedTaskId;
|
||||
const items = state.inbox.filter((i) => i.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at));
|
||||
const keep = new Set(items.map((i) => i.id));
|
||||
for (const id of cards.keys()) {
|
||||
if (!keep.has(id)) {
|
||||
@@ -64,32 +37,15 @@ export function mountInbox(root) {
|
||||
cards.set(item.id, { sig, el });
|
||||
return el;
|
||||
};
|
||||
const shown = groupByTask(items);
|
||||
const keepGroups = new Set(shown.map((g) => g.taskId));
|
||||
for (const id of groups.keys()) {
|
||||
if (!keepGroups.has(id)) {
|
||||
groups.delete(id);
|
||||
}
|
||||
if (!taskId) {
|
||||
reconcile(list, [noTask]);
|
||||
return;
|
||||
}
|
||||
const els = shown.map((g) => {
|
||||
const projectName = state.projects.find((p) => p.id === g.projectId)?.name ?? g.projectId;
|
||||
const title = findTask(g.taskId)?.title ?? g.taskId;
|
||||
const head = JSON.stringify([projectName, title]);
|
||||
let group = groups.get(g.taskId);
|
||||
if (!group || group.head !== head) {
|
||||
const h3 = h("h3", {}, h("span", { class: "project-name" }, projectName, " › "), h("a", { href: `#/task/${g.taskId}` }, title));
|
||||
group = { head, h3, el: group?.el ?? h("section", {}) };
|
||||
groups.set(g.taskId, group);
|
||||
}
|
||||
group.el.className = `task-group ${g.blocked ? "blocked" : ""}`;
|
||||
reconcile(group.el, [group.h3, ...g.items.map(cardFor)]);
|
||||
return group.el;
|
||||
});
|
||||
reconcile(list, els.length > 0 ? els : [empty]);
|
||||
reconcile(list, items.length > 0 ? items.map(cardFor) : [nothing]);
|
||||
};
|
||||
const off = onEach(["inbox", "selection", "tasks"], render);
|
||||
const off = onEach(["inbox", "selection"], render);
|
||||
const changes = h("section", { class: "inbox-changes" });
|
||||
replace(root, h("div", { class: "inbox-view" }, changes, tabs, list));
|
||||
replace(root, h("div", { class: "inbox-view" }, changes, list));
|
||||
const offChanges = mountChanges(changes, true);
|
||||
render();
|
||||
return () => {
|
||||
@@ -108,22 +64,6 @@ function reconcile(parent, nodes) {
|
||||
parent.lastChild.remove();
|
||||
}
|
||||
}
|
||||
function groupByTask(items) {
|
||||
const groups = new Map();
|
||||
for (const item of [...items].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
|
||||
let g = groups.get(item.task_id);
|
||||
if (!g) {
|
||||
g = { taskId: item.task_id, projectId: item.project_id, blocked: false, items: [] };
|
||||
groups.set(item.task_id, g);
|
||||
}
|
||||
g.items.push(item);
|
||||
g.blocked ||= item.priority === "blocked";
|
||||
}
|
||||
const projectName = (id) => state.projects.find((p) => p.id === id)?.name ?? id;
|
||||
return [...groups.values()].sort((a, b) => Number(b.blocked) - Number(a.blocked) ||
|
||||
projectName(a.projectId).localeCompare(projectName(b.projectId)) ||
|
||||
a.items[0].created_at.localeCompare(b.items[0].created_at));
|
||||
}
|
||||
function clickable(el, go) {
|
||||
el.onclick = (e) => {
|
||||
if (!e.target.closest("a")) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import { post } from "./api.js";
|
||||
import { h } from "./dom.js";
|
||||
import { currentLead, state } from "./state.js";
|
||||
import { compactButton, mountChat } from "./task.js";
|
||||
const leadSource = {
|
||||
key: () => "lead",
|
||||
events: () => state.leadTranscript,
|
||||
session: currentLead,
|
||||
root: () => "",
|
||||
taskId: () => undefined,
|
||||
resetTopics: [],
|
||||
refreshTopics: ["lead.sessions"],
|
||||
transcriptTopic: "lead.transcript",
|
||||
streamTopic: "lead.stream",
|
||||
empty: "",
|
||||
canSend: () => true,
|
||||
placeholder: (s) => (s === "running" ? "Message your lead; it gets this when its turn ends" : "Message your lead. Paste or drop images to attach them."),
|
||||
send: async ({ text, images, model, firstModel, sessionId }) => {
|
||||
await post("/api/me/lead/messages", { body: { text, images, model: sessionId ? model : firstModel } });
|
||||
},
|
||||
};
|
||||
function leadHead() {
|
||||
return {
|
||||
render: (usage) => {
|
||||
const sess = currentLead();
|
||||
const live = sess && sess.state !== "completed" ? sess : undefined;
|
||||
return [h("div", { class: "head-actions session-actions" }, compactButton(live?.id, live?.state, live && usage(live.id)))];
|
||||
},
|
||||
};
|
||||
}
|
||||
export function mountLead(root) {
|
||||
return mountChat(root, leadSource, leadHead);
|
||||
}
|
||||
//# sourceMappingURL=lead.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"lead.js","sourceRoot":"","sources":["../src/lead.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,CAAC,EAAE,MAAM,UAAU,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,SAAS,EAA8B,MAAM,WAAW,CAAC;AAEjF,MAAM,UAAU,GAAe;IAC7B,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM;IACjB,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,cAAc;IAClC,OAAO,EAAE,WAAW;IACpB,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE;IACd,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS;IACvB,WAAW,EAAE,EAAE;IACf,aAAa,EAAE,CAAC,eAAe,CAAC;IAChC,eAAe,EAAE,iBAAiB;IAClC,WAAW,EAAE,aAAa;IAC1B,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;IACnB,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,oDAAoD,CAAC,CAAC,CAAC,yDAAyD,CAAC;IACxJ,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7D,MAAM,IAAI,CAAC,uBAAuB,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACzG,CAAC;CACF,CAAC;AAEF,SAAS,QAAQ;IACf,OAAO;QACL,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAChB,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;YACnE,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,8BAA8B,EAAE,EAAE,aAAa,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7H,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAiB;IACzC,OAAO,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;AAC/C,CAAC"}
|
||||
Vendored
+11
-14
@@ -1,14 +1,15 @@
|
||||
import { post } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { openTranscript } from "./panes.js";
|
||||
import { activeTasks, currentSession, findTask, lastVisits, onEach, state } from "./state.js";
|
||||
export function mountNavbar(root, show) {
|
||||
import { openSettings } from "./settings.js";
|
||||
import { activeTasks, currentSession, findTask, focusTask, lastVisits, onEach, state } from "./state.js";
|
||||
import { openStatus } from "./status.js";
|
||||
export function mountNavbar(root) {
|
||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||
crumb.onclick = openSwitcher;
|
||||
const inbox = h("button", { class: "inbox-count" });
|
||||
inbox.onclick = () => show("inbox");
|
||||
const admin = h("button", { class: "nav-link" }, "Admin");
|
||||
admin.onclick = () => show("admin");
|
||||
const status = h("button", { class: "nav-link" }, "Status");
|
||||
status.onclick = openStatus;
|
||||
const settings = h("button", { class: "nav-link" }, "Settings");
|
||||
settings.onclick = openSettings;
|
||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||
const connLabel = h("span", { class: "connection-label" }, "reconnecting…");
|
||||
const me = h("span", { class: "me" });
|
||||
@@ -16,16 +17,13 @@ export function mountNavbar(root, show) {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
replace(crumb, task ? `${project?.name ?? ""} ▸ ${task.title}` : "No task", " ▾");
|
||||
const blocked = state.inbox.filter((i) => i.priority === "blocked").length;
|
||||
inbox.className = blocked > 0 ? "inbox-count urgent" : "inbox-count";
|
||||
replace(inbox, `Inbox ${state.inbox.length}`, blocked > 0 && h("span", { class: "blocked-count" }, ` · ${blocked} blocked`));
|
||||
conn.className = state.connected ? "connection up" : "connection down";
|
||||
conn.title = state.connected ? "live" : "reconnecting";
|
||||
connLabel.hidden = state.connected;
|
||||
replace(me, state.me?.display_name ?? "");
|
||||
};
|
||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, connLabel, conn, me);
|
||||
onEach(["me", "projects", "tasks", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), status, settings, connLabel, conn, me);
|
||||
render();
|
||||
addEventListener("keydown", (e) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey) && !document.querySelector("dialog[open]")) {
|
||||
@@ -62,8 +60,7 @@ function openSwitcher() {
|
||||
};
|
||||
const go = (taskId) => {
|
||||
close();
|
||||
location.hash = `#/task/${taskId}`;
|
||||
openTranscript();
|
||||
focusTask(taskId);
|
||||
};
|
||||
const create = async (projectId, title) => {
|
||||
close();
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+31
-54
@@ -1,71 +1,48 @@
|
||||
import { mountAdmin } from "./admin.js";
|
||||
import { mountChanges } from "./changes.js";
|
||||
import { h, replace } from "./dom.js";
|
||||
import { mountFiles } from "./files.js";
|
||||
import { mountInbox } from "./inbox.js";
|
||||
import { mountSettings } from "./settings.js";
|
||||
import { mountLead } from "./lead.js";
|
||||
import { focusTab, on, state, tabs } from "./state.js";
|
||||
import { mountTranscript } from "./task.js";
|
||||
import { mountTasks } from "./tasks.js";
|
||||
import { mountTodo } from "./todo.js";
|
||||
const views = {
|
||||
transcript: { label: "Transcript", mount: mountTranscript },
|
||||
changes: { label: "Changes", mount: (root) => mountChanges(root) },
|
||||
files: { label: "Files", mount: mountFiles },
|
||||
tasks: { label: "Tasks", mount: mountTasks },
|
||||
todo: { label: "Todo", mount: mountTodo },
|
||||
inbox: { label: "Inbox", mount: mountInbox },
|
||||
admin: { label: "Admin", mount: mountAdmin },
|
||||
settings: { label: "Settings", mount: mountSettings },
|
||||
todo: { label: "Todo", mount: mountTodo },
|
||||
};
|
||||
const order = ["transcript", "changes", "files", "tasks", "todo", "inbox", "admin", "settings"];
|
||||
const panes = [];
|
||||
export function openTranscript() {
|
||||
if (panes.some((p) => p.current() === "transcript")) {
|
||||
return;
|
||||
}
|
||||
(panes.find((p) => p.current() !== "tasks") ?? panes[0])?.show("transcript");
|
||||
export function leadPane() {
|
||||
const root = h("div", { class: "view view-lead" });
|
||||
mountLead(root);
|
||||
return h("section", { class: "pane" }, h("nav", { class: "pane-tabs" }, h("button", { class: "active" }, "Lead")), h("div", { class: "pane-body" }, root));
|
||||
}
|
||||
export function openChanges(from) {
|
||||
if (panes.some((p) => p.current() === "changes")) {
|
||||
return;
|
||||
}
|
||||
(panes.find((p) => !p.el.contains(from)) ?? panes[0])?.show("changes");
|
||||
}
|
||||
export class Pane {
|
||||
key;
|
||||
el;
|
||||
tabs;
|
||||
body;
|
||||
view;
|
||||
dispose = () => { };
|
||||
constructor(key, fallback) {
|
||||
this.key = key;
|
||||
this.tabs = h("nav", { class: "pane-tabs" });
|
||||
this.body = h("div", { class: "pane-body" });
|
||||
this.el = h("section", { class: "pane" }, this.tabs, this.body);
|
||||
const saved = localStorage.getItem(this.storageKey());
|
||||
this.view = saved && saved in views ? saved : fallback;
|
||||
this.show(this.view);
|
||||
panes.push(this);
|
||||
}
|
||||
current() {
|
||||
return this.view;
|
||||
}
|
||||
show(view) {
|
||||
this.dispose();
|
||||
this.view = view;
|
||||
localStorage.setItem(this.storageKey(), view);
|
||||
replace(this.tabs, ...order.map((v) => {
|
||||
const b = h("button", { class: v === view ? "active" : "" }, views[v].label);
|
||||
b.onclick = () => this.show(v);
|
||||
export function taskPane() {
|
||||
const strip = h("nav", { class: "pane-tabs" });
|
||||
const body = h("div", { class: "pane-body" });
|
||||
let shown;
|
||||
let dispose = () => { };
|
||||
const render = () => {
|
||||
const first = state.focus.tabs.filter((t, i, all) => tabs.includes(t) && all.indexOf(t) === i);
|
||||
const order = [...first, ...tabs.filter((t) => !first.includes(t))];
|
||||
const tab = state.focus.tab;
|
||||
replace(strip, ...order.map((t) => {
|
||||
const b = h("button", { class: t === tab ? "active" : "" }, views[t].label);
|
||||
b.onclick = () => focusTab(t);
|
||||
return b;
|
||||
}));
|
||||
const root = h("div", { class: `view view-${view}` });
|
||||
replace(this.body, root);
|
||||
this.dispose = views[view].mount(root);
|
||||
}
|
||||
storageKey() {
|
||||
return `byre.pane.${this.key}`;
|
||||
}
|
||||
if (tab === shown) {
|
||||
return;
|
||||
}
|
||||
dispose();
|
||||
shown = tab;
|
||||
const root = h("div", { class: `view view-${tab}` });
|
||||
replace(body, root);
|
||||
dispose = views[tab].mount(root);
|
||||
};
|
||||
on("focus", render);
|
||||
render();
|
||||
return h("section", { class: "pane" }, strip, body);
|
||||
}
|
||||
//# sourceMappingURL=panes.js.map
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+5
-2
@@ -1,5 +1,5 @@
|
||||
import { del, get, post, put } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { errorText, h, openPanel, replace, showAlert } from "./dom.js";
|
||||
import { on, state } from "./state.js";
|
||||
const kinds = [
|
||||
{ key: "blocking_questions", label: "Blocking questions" },
|
||||
@@ -8,7 +8,10 @@ const kinds = [
|
||||
{ key: "errors", label: "Session errors" },
|
||||
{ key: "rate_limits", label: "Rate limits" },
|
||||
];
|
||||
export function mountSettings(root) {
|
||||
export function openSettings() {
|
||||
openPanel("Settings", mountSettings);
|
||||
}
|
||||
function mountSettings(root) {
|
||||
const device = h("div", { class: "push-device" });
|
||||
const prefs = h("fieldset", { class: "push-prefs" });
|
||||
const renderPrefs = () => {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+111
-41
@@ -1,4 +1,5 @@
|
||||
import { get, subscribe } from "./api.js";
|
||||
import { get, put, subscribe } from "./api.js";
|
||||
export const tabs = ["transcript", "changes", "files", "inbox", "todo"];
|
||||
export const state = {
|
||||
me: undefined,
|
||||
projects: [],
|
||||
@@ -7,9 +8,11 @@ export const state = {
|
||||
inbox: [],
|
||||
selectedTaskId: undefined,
|
||||
transcript: [],
|
||||
lastTranscriptSeq: 0,
|
||||
pinned: undefined,
|
||||
connected: false,
|
||||
focus: { tabs: ["transcript"], tab: "transcript" },
|
||||
leadSessions: [],
|
||||
leadTranscript: [],
|
||||
};
|
||||
const listeners = new Map();
|
||||
export function on(topic, fn) {
|
||||
@@ -49,6 +52,9 @@ export function findTask(id) {
|
||||
export function currentSession(taskId) {
|
||||
return state.sessions.get(taskId)?.findLast((s) => s.state !== "completed") ?? state.sessions.get(taskId)?.at(-1);
|
||||
}
|
||||
export function currentLead() {
|
||||
return state.leadSessions.findLast((s) => s.state !== "completed") ?? state.leadSessions.at(-1);
|
||||
}
|
||||
export function snapshots() {
|
||||
return state.transcript.filter((e) => e.kind === "snapshot.created").map((e) => e.payload);
|
||||
}
|
||||
@@ -79,9 +85,96 @@ export async function loadInbox() {
|
||||
state.inbox = await get("/api/inbox");
|
||||
notify("inbox");
|
||||
}
|
||||
export async function loadLeadSessions() {
|
||||
state.leadSessions = await get("/api/me/lead/sessions");
|
||||
notify("lead.sessions");
|
||||
}
|
||||
export async function loadFocus() {
|
||||
applyFocus(await get("/api/me/focus"));
|
||||
}
|
||||
export function applyFocus(f) {
|
||||
state.focus = f;
|
||||
const hash = f.task_id ? `#/task/${f.task_id}` : "#/";
|
||||
if (location.hash !== hash && !(hash === "#/" && location.hash === "")) {
|
||||
location.hash = hash;
|
||||
}
|
||||
notify("focus");
|
||||
}
|
||||
export async function setFocus(change) {
|
||||
const taskId = change.task_id === undefined ? state.focus.task_id : (change.task_id ?? undefined);
|
||||
const next = { tabs: state.focus.tabs, tab: change.tab ?? state.focus.tab };
|
||||
if (taskId) {
|
||||
next.task_id = taskId;
|
||||
}
|
||||
applyFocus(next);
|
||||
applyFocus(await put("/api/me/focus", { body: next }));
|
||||
}
|
||||
export function focusTask(taskId, tab = "transcript") {
|
||||
setFocus({ task_id: taskId, tab }).catch((err) => console.error("set focus", err));
|
||||
}
|
||||
export function focusTab(tab) {
|
||||
setFocus({ tab }).catch((err) => console.error("set focus", err));
|
||||
}
|
||||
class Feed {
|
||||
transcriptTopic;
|
||||
streamTopic;
|
||||
store;
|
||||
events = [];
|
||||
last = 0;
|
||||
buffer = [];
|
||||
timer = 0;
|
||||
constructor(transcriptTopic, streamTopic, store) {
|
||||
this.transcriptTopic = transcriptTopic;
|
||||
this.streamTopic = streamTopic;
|
||||
this.store = store;
|
||||
}
|
||||
reset() {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
this.buffer = [];
|
||||
this.events = [];
|
||||
this.last = 0;
|
||||
this.store(this.events);
|
||||
}
|
||||
push = (ev) => {
|
||||
this.buffer.push(ev);
|
||||
if (!this.timer) {
|
||||
this.timer = setTimeout(() => this.flush(), 0);
|
||||
}
|
||||
};
|
||||
flush() {
|
||||
const events = this.buffer;
|
||||
this.buffer = [];
|
||||
this.timer = 0;
|
||||
let stored = [];
|
||||
for (const ev of events) {
|
||||
if (ev.seq !== 0) {
|
||||
stored.push(ev);
|
||||
continue;
|
||||
}
|
||||
this.append(stored);
|
||||
stored = [];
|
||||
notify(this.streamTopic, ev);
|
||||
}
|
||||
this.append(stored);
|
||||
}
|
||||
append(events) {
|
||||
const fresh = events.filter((ev) => ev.seq > this.last);
|
||||
if (fresh.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.events.push(...fresh);
|
||||
this.last = fresh.at(-1).seq;
|
||||
notify(this.transcriptTopic, fresh);
|
||||
}
|
||||
}
|
||||
const taskFeed = new Feed("transcript", "stream", (events) => {
|
||||
state.transcript = events;
|
||||
});
|
||||
const leadFeed = new Feed("lead.transcript", "lead.stream", (events) => {
|
||||
state.leadTranscript = events;
|
||||
});
|
||||
let closeTaskStream;
|
||||
let taskBuffer = [];
|
||||
let taskFlush = 0;
|
||||
const visitsKey = "byre.visits";
|
||||
export function lastVisits() {
|
||||
return JSON.parse(localStorage.getItem(visitsKey) ?? "{}");
|
||||
@@ -97,49 +190,18 @@ export function selectTask(taskId) {
|
||||
markVisited(taskId);
|
||||
closeTaskStream?.();
|
||||
closeTaskStream = undefined;
|
||||
clearTimeout(taskFlush);
|
||||
taskFlush = 0;
|
||||
taskBuffer = [];
|
||||
taskFeed.reset();
|
||||
state.selectedTaskId = taskId;
|
||||
state.transcript = [];
|
||||
state.lastTranscriptSeq = 0;
|
||||
state.pinned = undefined;
|
||||
notify("selection");
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
closeTaskStream = subscribe(0, taskId, queueTaskEvent, () => undefined);
|
||||
closeTaskStream = subscribe(0, { task_id: taskId }, taskFeed.push, () => undefined);
|
||||
}
|
||||
function queueTaskEvent(ev) {
|
||||
taskBuffer.push(ev);
|
||||
if (!taskFlush) {
|
||||
taskFlush = setTimeout(flushTaskEvents, 0);
|
||||
}
|
||||
}
|
||||
function flushTaskEvents() {
|
||||
const events = taskBuffer;
|
||||
taskBuffer = [];
|
||||
taskFlush = 0;
|
||||
let stored = [];
|
||||
for (const ev of events) {
|
||||
if (ev.seq !== 0) {
|
||||
stored.push(ev);
|
||||
continue;
|
||||
}
|
||||
appendTranscript(stored);
|
||||
stored = [];
|
||||
notify("stream", ev);
|
||||
}
|
||||
appendTranscript(stored);
|
||||
}
|
||||
function appendTranscript(events) {
|
||||
const fresh = events.filter((ev) => ev.seq > state.lastTranscriptSeq);
|
||||
if (fresh.length === 0) {
|
||||
return;
|
||||
}
|
||||
state.transcript.push(...fresh);
|
||||
state.lastTranscriptSeq = fresh.at(-1).seq;
|
||||
notify("transcript", fresh);
|
||||
export function startLeadStream() {
|
||||
leadFeed.reset();
|
||||
subscribe(0, { lead: true }, leadFeed.push, () => undefined);
|
||||
}
|
||||
export function handleEvent(ev) {
|
||||
if (ev.seq === 0) {
|
||||
@@ -166,7 +228,10 @@ export function handleEvent(ev) {
|
||||
case "session.created":
|
||||
case "session.state":
|
||||
case "session.model":
|
||||
if (ev.task_id) {
|
||||
if (ev.owner) {
|
||||
void loadLeadSessions();
|
||||
}
|
||||
else if (ev.task_id) {
|
||||
void loadSessions(ev.task_id);
|
||||
}
|
||||
break;
|
||||
@@ -174,6 +239,11 @@ export function handleEvent(ev) {
|
||||
case "inbox.updated":
|
||||
void loadInbox();
|
||||
break;
|
||||
case "focus.updated":
|
||||
if (ev.actor !== state.me?.login) {
|
||||
applyFocus(ev.payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
notify("event", ev);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+14
-11
@@ -1,8 +1,11 @@
|
||||
import { get } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { ago, errorText, h, openPanel, replace } from "./dom.js";
|
||||
import { findTask, on, onEach, state } from "./state.js";
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]);
|
||||
export function mountAdmin(root) {
|
||||
export function openStatus() {
|
||||
openPanel("Status", mountStatus);
|
||||
}
|
||||
function mountStatus(root) {
|
||||
let status;
|
||||
let error = "";
|
||||
let inFlight = false;
|
||||
@@ -13,10 +16,10 @@ export function mountAdmin(root) {
|
||||
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
|
||||
return;
|
||||
}
|
||||
const scrollTop = root.querySelector(".admin-view")?.scrollTop ?? 0;
|
||||
const scrollTop = root.querySelector(".status-view")?.scrollTop ?? 0;
|
||||
const open = new Set([...root.querySelectorAll(".problems details[open]")].map((d) => d.dataset["vendor"] ?? ""));
|
||||
replace(root, h("div", { class: "admin-view" }, error && h("p", { class: "error-text" }, error), problemsSection(status, open), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
|
||||
root.querySelector(".admin-view").scrollTop = scrollTop;
|
||||
replace(root, h("div", { class: "status-view" }, error && h("p", { class: "error-text" }, error), problemsSection(status, open), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status)));
|
||||
root.querySelector(".status-view").scrollTop = scrollTop;
|
||||
};
|
||||
const refresh = async () => {
|
||||
if (inFlight) {
|
||||
@@ -86,7 +89,7 @@ export function mountAdmin(root) {
|
||||
function turnsSection(s) {
|
||||
const vendors = [...new Set([...Object.keys(s.caps.per_vendor), ...s.running_turns.map((t) => t.vendor), ...s.queued_turns.map((t) => t.vendor)])].sort();
|
||||
const runningFor = (vendor) => s.running_turns.filter((t) => t.vendor === vendor).length;
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running.", s.agent_processes), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued.", []));
|
||||
return h("section", { class: "status-section" }, h("h2", {}, "Turns"), h("div", { class: "caps" }, capTile("All vendors", s.running_turns.length, s.caps.global), ...vendors.map((v) => capTile(v, runningFor(v), s.caps.per_vendor[v])), h("div", { class: "cap-tile" }, h("div", { class: "cap-label" }, "Queued"), h("div", { class: s.queued_turns.length > 0 ? "cap-value warn" : "cap-value" }, s.queued_turns.length))), h("h3", {}, "Running"), turnList(s.running_turns, "running", "No turns running.", s.agent_processes), h("h3", {}, "Queued"), turnList(s.queued_turns, "queued", "Nothing queued.", []));
|
||||
}
|
||||
function capTile(label, running, cap) {
|
||||
const full = cap !== undefined && running >= cap;
|
||||
@@ -101,7 +104,7 @@ function turnList(turns, kind, empty, procs) {
|
||||
}
|
||||
function taskLink(taskId) {
|
||||
if (!taskId) {
|
||||
return "—";
|
||||
return "lead";
|
||||
}
|
||||
const task = findTask(taskId);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
@@ -136,7 +139,7 @@ function problemsSection(s, openVendors) {
|
||||
if (s.zombies.length === 0 && vendors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Problems"), h("ul", { class: "problems" }, ...s.zombies.map((z) => h("li", {}, h("span", { class: "danger" }, zombieLabel[z.kind]), " ", z.task_id ? taskLink(z.task_id) : "", " ", h("span", { class: "hint" }, z.detail))), ...vendors.map((v) => {
|
||||
return h("section", { class: "status-section" }, h("h2", {}, "Problems"), h("ul", { class: "problems" }, ...s.zombies.map((z) => h("li", {}, h("span", { class: "danger" }, zombieLabel[z.kind]), " ", z.task_id ? taskLink(z.task_id) : "", " ", h("span", { class: "hint" }, z.detail))), ...vendors.map((v) => {
|
||||
const latest = pe.recent.find((e) => e.vendor === v);
|
||||
const n = pe.counts[v] ?? 0;
|
||||
return h("li", {}, h("span", { class: "danger" }, `${v}: ${n} bad stream line${n === 1 ? "" : "s"}`), latest &&
|
||||
@@ -145,7 +148,7 @@ function problemsSection(s, openVendors) {
|
||||
}
|
||||
function rateLimitsSection(limits) {
|
||||
const vendors = Object.keys(limits).sort();
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."));
|
||||
return h("section", { class: "status-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."));
|
||||
}
|
||||
function rateLimitCard(vendor, info) {
|
||||
const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info;
|
||||
@@ -188,7 +191,7 @@ function serverSection(s) {
|
||||
["Database", bytes(s.db_bytes)],
|
||||
["Last event", `#${s.last_event_seq}`],
|
||||
];
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
|
||||
return h("section", { class: "status-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
|
||||
}
|
||||
function bytes(n) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
@@ -200,4 +203,4 @@ function bytes(n) {
|
||||
}
|
||||
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
//# sourceMappingURL=admin.js.map
|
||||
//# sourceMappingURL=status.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+154
-130
@@ -1,10 +1,117 @@
|
||||
import { get, post } from "./api.js";
|
||||
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { openChanges } from "./panes.js";
|
||||
import { currentSession, findTask, onEach, pinSnapshot, state } from "./state.js";
|
||||
import { currentSession, findTask, focusTab, onEach, pinSnapshot, setFocus, state } from "./state.js";
|
||||
export const models = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1"];
|
||||
const taskSource = {
|
||||
key: () => state.selectedTaskId,
|
||||
events: () => state.transcript,
|
||||
session: () => (state.selectedTaskId ? currentSession(state.selectedTaskId) : undefined),
|
||||
root: () => (state.selectedTaskId ? (findTask(state.selectedTaskId)?.worktree_path ?? "") : ""),
|
||||
taskId: () => state.selectedTaskId,
|
||||
resetTopics: ["selection"],
|
||||
refreshTopics: ["tasks", "sessions"],
|
||||
transcriptTopic: "transcript",
|
||||
streamTopic: "stream",
|
||||
empty: "No task selected. Ask your lead, or pick one with ⌘K.",
|
||||
canSend: (s) => s === "idle",
|
||||
placeholder: (s) => (s === "idle" ? "Message the agent. Paste or drop images to attach them." : "Wait for the turn to end, or interrupt it"),
|
||||
send: async ({ key, sessionId, text, images, model, firstModel }) => {
|
||||
if (sessionId) {
|
||||
await post("/api/sessions/{sessionId}/messages", { path: { sessionId }, body: { text, images, model } });
|
||||
return;
|
||||
}
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId: key }, body: { prompt: text, model: firstModel, images } });
|
||||
},
|
||||
};
|
||||
export function mountTranscript(root) {
|
||||
return mountChat(root, taskSource, taskHead);
|
||||
}
|
||||
function taskHead(rerender) {
|
||||
let unmerged;
|
||||
let timer;
|
||||
const load = async () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId } });
|
||||
if (state.selectedTaskId !== taskId) {
|
||||
return;
|
||||
}
|
||||
const has = u.commits > 0 || u.files > 0;
|
||||
if (unmerged?.taskId === taskId && unmerged.has === has) {
|
||||
return;
|
||||
}
|
||||
unmerged = { taskId, has };
|
||||
rerender();
|
||||
}
|
||||
catch (err) {
|
||||
console.error("load unmerged changes", err);
|
||||
}
|
||||
};
|
||||
return {
|
||||
reset: () => {
|
||||
clearTimeout(timer);
|
||||
void load();
|
||||
},
|
||||
dispose: () => clearTimeout(timer),
|
||||
onEvents: (events) => {
|
||||
if (events.some((ev) => ev.kind === "agent.tool_result" || ev.kind === "turn.ended")) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void load(), 1500);
|
||||
}
|
||||
},
|
||||
render: (usage) => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (!task) {
|
||||
return [];
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
if (task.status !== "active") {
|
||||
return [h("div", { class: "head-actions" }, task.status)];
|
||||
}
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
const busy = sess?.state === "running" || sess?.state === "queued";
|
||||
const merge = h("button", { disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` }, "Merge");
|
||||
merge.onclick = async () => {
|
||||
let coAuthors;
|
||||
try {
|
||||
coAuthors = await get("/api/tasks/{taskId}/co-authors", { path: { taskId: task.id } });
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
await confirmThen(mergeQuestion(task.merge_title, target, coAuthors), "Merge", false, async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
|
||||
});
|
||||
};
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
|
||||
mergeIn.onclick = async () => {
|
||||
mergeIn.disabled = true;
|
||||
try {
|
||||
await post("/api/tasks/{taskId}/merge-in", { path: { taskId: task.id } });
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
mergeIn.disabled = false;
|
||||
}
|
||||
};
|
||||
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
|
||||
discard.onclick = () => confirmThen(`Discard "${task.title}"?`, "Discard", true, async () => {
|
||||
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
|
||||
await setFocus({ task_id: null });
|
||||
});
|
||||
return [
|
||||
h("div", { class: "head-actions session-actions" }, compactButton(sess?.id, sess?.state, sess && usage(sess.id)), mergeIn),
|
||||
h("div", { class: "head-actions" }, merge, discard),
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
export function mountChat(root, src, makeHead) {
|
||||
const head = h("header", { class: "task-header" });
|
||||
const transcript = h("div", { class: "transcript" });
|
||||
const composer = h("div", { class: "composer" });
|
||||
@@ -43,47 +150,23 @@ export function mountTranscript(root) {
|
||||
}
|
||||
ackVisible();
|
||||
}, { root: transcript, threshold: 0.5 });
|
||||
const newCtx = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
return {
|
||||
tools: new Map(),
|
||||
run: undefined,
|
||||
anchors: [],
|
||||
unplaced: [],
|
||||
lastMessage: undefined,
|
||||
turnStart: undefined,
|
||||
root: task?.worktree_path ?? "",
|
||||
watchReport: (el, itemId) => {
|
||||
el.dataset["inboxItem"] = itemId;
|
||||
observer.observe(el);
|
||||
},
|
||||
};
|
||||
};
|
||||
const newCtx = () => ({
|
||||
tools: new Map(),
|
||||
run: undefined,
|
||||
anchors: [],
|
||||
unplaced: [],
|
||||
lastMessage: undefined,
|
||||
turnStart: undefined,
|
||||
root: src.root(),
|
||||
taskId: src.taskId(),
|
||||
watchReport: (el, itemId) => {
|
||||
el.dataset["inboxItem"] = itemId;
|
||||
observer.observe(el);
|
||||
},
|
||||
});
|
||||
let ctx = newCtx();
|
||||
let context = new Map();
|
||||
let unmerged;
|
||||
let unmergedTimer;
|
||||
const loadUnmerged = async () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId } });
|
||||
if (state.selectedTaskId !== taskId) {
|
||||
return;
|
||||
}
|
||||
const has = u.commits > 0 || u.files > 0;
|
||||
if (unmerged?.taskId === taskId && unmerged.has === has) {
|
||||
return;
|
||||
}
|
||||
unmerged = { taskId, has };
|
||||
renderHead();
|
||||
}
|
||||
catch (err) {
|
||||
console.error("load unmerged changes", err);
|
||||
}
|
||||
};
|
||||
const headImpl = makeHead(() => renderHead());
|
||||
const spinner = h("div", { class: "spinner", hidden: true });
|
||||
const setRunning = (running) => {
|
||||
transcript.classList.toggle("running", running);
|
||||
@@ -109,63 +192,25 @@ export function mountTranscript(root) {
|
||||
};
|
||||
const put = (el) => putBefore(el, pending[0]?.el);
|
||||
const renderHead = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
setRunning(task !== undefined && currentSession(task.id)?.state === "running");
|
||||
if (!task) {
|
||||
setRunning(src.session()?.state === "running");
|
||||
if (!src.key()) {
|
||||
replace(head);
|
||||
return;
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
if (task.status !== "active") {
|
||||
replace(head, h("div", { class: "head-actions" }, task.status));
|
||||
return;
|
||||
}
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
const busy = sess?.state === "running" || sess?.state === "queued";
|
||||
const merge = h("button", { disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` }, "Merge");
|
||||
merge.onclick = async () => {
|
||||
let coAuthors;
|
||||
try {
|
||||
coAuthors = await get("/api/tasks/{taskId}/co-authors", { path: { taskId: task.id } });
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
await confirmThen(mergeQuestion(task.merge_title, target, coAuthors), "Merge", false, async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
|
||||
});
|
||||
};
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
|
||||
mergeIn.onclick = async () => {
|
||||
mergeIn.disabled = true;
|
||||
try {
|
||||
await post("/api/tasks/{taskId}/merge-in", { path: { taskId: task.id } });
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
mergeIn.disabled = false;
|
||||
}
|
||||
};
|
||||
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
|
||||
discard.onclick = () => confirmThen(`Discard "${task.title}"?`, "Discard", true, async () => {
|
||||
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
|
||||
location.hash = "#/";
|
||||
});
|
||||
replace(head, h("div", { class: "head-actions session-actions" }, compactButton(sess?.id, sess?.state, sess && context.get(sess.id)), mergeIn), h("div", { class: "head-actions" }, merge, discard));
|
||||
replace(head, ...headImpl.render((id) => context.get(id)));
|
||||
};
|
||||
let composerModel = "";
|
||||
let focusStart = false;
|
||||
const renderComposer = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (!task) {
|
||||
const key = src.key();
|
||||
if (!key) {
|
||||
replace(composer);
|
||||
return;
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
const sess = src.session();
|
||||
const live = sess && sess.state !== "completed" ? sess : undefined;
|
||||
composerModel = live ? live.model || context.get(live.id)?.model || "" : "";
|
||||
const form = composeForm(task.id, live?.id, live?.state ?? "idle", composerModel, showPending);
|
||||
const form = composeForm(src, key, live?.id, live?.state ?? "idle", composerModel, showPending);
|
||||
const old = composer.querySelector("textarea");
|
||||
const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined;
|
||||
replace(composer, form);
|
||||
@@ -290,7 +335,7 @@ export function mountTranscript(root) {
|
||||
}
|
||||
stream.timer = 0;
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
replace(stream.el, markdown(stream.text, "text md", { taskId: state.selectedTaskId ?? undefined }));
|
||||
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
@@ -312,12 +357,9 @@ export function mountTranscript(root) {
|
||||
const appendEvents = (events) => {
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
let contextChanged = false;
|
||||
headImpl.onEvents?.(events);
|
||||
for (const ev of events) {
|
||||
const p = ev.payload;
|
||||
if (ev.kind === "agent.tool_result" || ev.kind === "turn.ended") {
|
||||
clearTimeout(unmergedTimer);
|
||||
unmergedTimer = setTimeout(() => void loadUnmerged(), 1500);
|
||||
}
|
||||
if (ev.kind === "agent.message" || ev.kind === "turn.ended" || ev.kind === "user.message") {
|
||||
dropStream();
|
||||
}
|
||||
@@ -365,7 +407,7 @@ export function mountTranscript(root) {
|
||||
}
|
||||
if (contextChanged) {
|
||||
renderHead();
|
||||
const sess = state.selectedTaskId ? currentSession(state.selectedTaskId) : undefined;
|
||||
const sess = src.session();
|
||||
if (sess && !sess.model && (context.get(sess.id)?.model ?? "") !== composerModel) {
|
||||
renderComposer();
|
||||
}
|
||||
@@ -382,32 +424,25 @@ export function mountTranscript(root) {
|
||||
ctx = newCtx();
|
||||
context = new Map();
|
||||
focusStart = true;
|
||||
clearTimeout(unmergedTimer);
|
||||
void loadUnmerged();
|
||||
headImpl.reset?.();
|
||||
renderHead();
|
||||
if (!state.selectedTaskId) {
|
||||
if (!src.key()) {
|
||||
renderComposer();
|
||||
replace(transcript, h("div", { class: "empty" }, "No task selected. Pick one with ⌘K or from the Tasks tab."));
|
||||
replace(transcript, h("div", { class: "empty" }, src.empty));
|
||||
return;
|
||||
}
|
||||
replace(transcript, spinner);
|
||||
appendEvents(state.transcript);
|
||||
appendEvents(src.events());
|
||||
renderComposer();
|
||||
};
|
||||
const offs = [
|
||||
onEach(["selection"], renderAll),
|
||||
onEach(["tasks"], () => {
|
||||
onEach(src.resetTopics, renderAll),
|
||||
onEach(src.refreshTopics, () => {
|
||||
renderHead();
|
||||
renderComposer();
|
||||
}),
|
||||
onEach(["sessions"], (taskId) => {
|
||||
if (taskId === state.selectedTaskId) {
|
||||
renderHead();
|
||||
renderComposer();
|
||||
}
|
||||
}),
|
||||
onEach(["transcript"], (fresh) => appendEvents(fresh)),
|
||||
onEach(["stream"], (ev) => appendDelta(ev)),
|
||||
onEach([src.transcriptTopic], (fresh) => appendEvents(fresh)),
|
||||
onEach([src.streamTopic], (ev) => appendDelta(ev)),
|
||||
onEach(["inbox"], ackVisible),
|
||||
];
|
||||
document.addEventListener("visibilitychange", ackVisible);
|
||||
@@ -418,7 +453,7 @@ export function mountTranscript(root) {
|
||||
off();
|
||||
}
|
||||
document.removeEventListener("visibilitychange", ackVisible);
|
||||
clearTimeout(unmergedTimer);
|
||||
headImpl.dispose?.();
|
||||
observer.disconnect();
|
||||
dropStream();
|
||||
};
|
||||
@@ -549,13 +584,10 @@ function modelSelect(key, current) {
|
||||
};
|
||||
return select;
|
||||
}
|
||||
function composeForm(taskId, sessionId, sessionState, currentModel, showPending) {
|
||||
const text = h("textarea", {
|
||||
placeholder: sessionState === "idle" ? "Message the agent. Paste or drop images to attach them." : "Wait for the turn to end, or interrupt it",
|
||||
rows: 3,
|
||||
});
|
||||
const clearDraft = keepDraft(text, `message:${taskId}`);
|
||||
const send = h("button", { type: "submit", disabled: sessionState !== "idle" }, "Send");
|
||||
function composeForm(src, key, sessionId, sessionState, currentModel, showPending) {
|
||||
const text = h("textarea", { placeholder: src.placeholder(sessionState), rows: 3 });
|
||||
const clearDraft = keepDraft(text, `message:${key}`);
|
||||
const send = h("button", { type: "submit", disabled: !src.canSend(sessionState) }, "Send");
|
||||
const interrupt = h("button", { type: "button", class: "danger", disabled: sessionState !== "running" && sessionState !== "queued" }, sessionState === "queued" ? "Cancel queued turn" : "Interrupt");
|
||||
interrupt.onclick = async () => {
|
||||
if (!sessionId) {
|
||||
@@ -568,15 +600,15 @@ function composeForm(taskId, sessionId, sessionState, currentModel, showPending)
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
const modelKey = `message:${sessionId ?? taskId}`;
|
||||
const modelKey = `message:${sessionId ?? key}`;
|
||||
const model = modelSelect(modelKey, currentModel);
|
||||
const form = h("form", { class: "compose" });
|
||||
const tray = imageTray(form, text, `message:${taskId}`);
|
||||
const tray = imageTray(form, text, `message:${key}`);
|
||||
form.append(tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const value = text.value;
|
||||
if (!value.trim() && !imageDrafts.has(`message:${taskId}`)) {
|
||||
if (!value.trim() && !imageDrafts.has(`message:${key}`)) {
|
||||
return;
|
||||
}
|
||||
const imgs = tray.take();
|
||||
@@ -585,20 +617,12 @@ function composeForm(taskId, sessionId, sessionState, currentModel, showPending)
|
||||
const cancel = showPending(value, imgs);
|
||||
try {
|
||||
const images = await uploadImages(imgs);
|
||||
if (sessionId) {
|
||||
await post("/api/sessions/{sessionId}/messages", {
|
||||
path: { sessionId },
|
||||
body: { text: value, images, model: model.value !== currentModel ? model.value : undefined },
|
||||
});
|
||||
}
|
||||
else {
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: value, model: model.value, images } });
|
||||
}
|
||||
await src.send({ key, sessionId, text: value, images, model: model.value !== currentModel ? model.value : undefined, firstModel: model.value });
|
||||
modelChoices.delete(modelKey);
|
||||
}
|
||||
catch (err) {
|
||||
cancel();
|
||||
composerDrafts.set(`message:${taskId}`, value);
|
||||
composerDrafts.set(`message:${key}`, value);
|
||||
text.value = value;
|
||||
tray.restore(imgs);
|
||||
void showAlert(errorText(err));
|
||||
@@ -615,7 +639,7 @@ function submitOnEnter(input, form) {
|
||||
}
|
||||
};
|
||||
}
|
||||
function compactButton(sessionId, sessionState, c) {
|
||||
export function compactButton(sessionId, sessionState, c) {
|
||||
const pct = c && c.window > 0 ? Math.min(100, (c.used / c.window) * 100) : 0;
|
||||
const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok";
|
||||
const usage = c && c.window > 0 ? `\ncontext: ${c.used.toLocaleString()} of ${c.window.toLocaleString()} tokens${c.model ? ` (${c.model})` : ""}` : "";
|
||||
@@ -657,7 +681,7 @@ function snapMark(ns) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
pinSnapshot(hi);
|
||||
openChanges(b);
|
||||
focusTab("changes");
|
||||
};
|
||||
return b;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
-50
@@ -1,50 +0,0 @@
|
||||
import { post } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { openTranscript } from "./panes.js";
|
||||
import { currentSession, onEach, state } from "./state.js";
|
||||
export function mountTasks(root) {
|
||||
const render = () => replace(root, h("div", { class: "tasks-view" }, ...state.projects.map(projectBlock), newProjectForm()));
|
||||
const off = onEach(["projects", "tasks", "sessions", "inbox", "selection"], render);
|
||||
render();
|
||||
return off;
|
||||
}
|
||||
function projectBlock(project) {
|
||||
const tasks = (state.tasks.get(project.id) ?? []).filter((t) => t.status === "active");
|
||||
return h("section", { class: "project" }, h("h2", {}, project.name, " ", h("span", { class: "repo" }, project.repo_path)), h("ul", { class: "task-list" }, ...tasks.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", { href: `#/task/${task.id}`, onclick: openTranscript }, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, task.title), h("span", { class: "task-state" }, sess?.state ?? "no agent"), count > 0 && h("span", { class: "badge" }, count)));
|
||||
}), tasks.length === 0 && h("li", { class: "empty-row" }, "No active tasks.")), newTaskForm(project.id));
|
||||
}
|
||||
function newTaskForm(projectId) {
|
||||
const title = h("input", { placeholder: "New task title", required: true });
|
||||
const form = h("form", { class: "inline-form" }, title, h("button", { type: "submit" }, "Create"));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const task = await post("/api/projects/{projectId}/tasks", { path: { projectId }, body: { title: title.value } });
|
||||
location.hash = `#/task/${task.id}`;
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return form;
|
||||
}
|
||||
function newProjectForm() {
|
||||
const name = h("input", { placeholder: "Name", required: true });
|
||||
const path = h("input", { placeholder: "Repo path on the server", required: true });
|
||||
const form = h("form", { class: "stack-form" }, name, path, h("button", { type: "submit" }, "Add project"));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await post("/api/projects", { body: { name: name.value, repo_path: path.value } });
|
||||
form.reset();
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return h("details", { class: "add-project", open: state.projects.length === 0 }, h("summary", {}, "Add project"), form);
|
||||
}
|
||||
//# sourceMappingURL=tasks.js.map
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+26
-8
@@ -1,17 +1,21 @@
|
||||
import { get, post } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { openTranscript } from "./panes.js";
|
||||
import { models } from "./task.js";
|
||||
import { currentSession, on, onEach, state } from "./state.js";
|
||||
import { currentSession, findTask, focusTask, on, onEach, state } from "./state.js";
|
||||
const refreshKinds = new Set(["task.merged", "project.created"]);
|
||||
export function mountTodo(root) {
|
||||
const todos = new Map();
|
||||
const starting = new Set();
|
||||
let disposed = false;
|
||||
const project = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
return state.projects.find((p) => p.id === task?.project_id);
|
||||
};
|
||||
const render = () => {
|
||||
const scrollTop = root.querySelector(".todo-view")?.scrollTop ?? 0;
|
||||
replace(root, h("div", { class: "todo-view" }, ...state.projects.map((p) => projectBlock(p, todos.get(p.id), starting, start))));
|
||||
const p = project();
|
||||
replace(root, h("div", { class: "todo-view" }, p ? projectBlock(p, todos.get(p.id), starting, start) : h("div", { class: "empty" }, "No task selected.")));
|
||||
root.querySelector(".todo-view").scrollTop = scrollTop;
|
||||
};
|
||||
const load = async (projectId) => {
|
||||
@@ -27,7 +31,8 @@ export function mountTodo(root) {
|
||||
}
|
||||
};
|
||||
const refresh = () => {
|
||||
for (const p of state.projects) {
|
||||
const p = project();
|
||||
if (p) {
|
||||
void load(p.id);
|
||||
}
|
||||
};
|
||||
@@ -37,8 +42,7 @@ export function mountTodo(root) {
|
||||
render();
|
||||
try {
|
||||
const task = await post("/api/projects/{projectId}/todo/tasks", { path: { projectId: project.id }, body: { title: item.title, model: models[0] } });
|
||||
location.hash = `#/task/${task.id}`;
|
||||
openTranscript();
|
||||
focusTask(task.id);
|
||||
}
|
||||
catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
@@ -64,7 +68,14 @@ export function mountTodo(root) {
|
||||
}
|
||||
}),
|
||||
on("projects", refresh),
|
||||
onEach(["tasks", "sessions", "selection"], render),
|
||||
on("selection", () => {
|
||||
render();
|
||||
const p = project();
|
||||
if (p && !todos.has(p.id)) {
|
||||
void load(p.id);
|
||||
}
|
||||
}),
|
||||
onEach(["tasks", "sessions"], render),
|
||||
];
|
||||
render();
|
||||
refresh();
|
||||
@@ -106,7 +117,14 @@ function projectBlock(project, todo, starting, start) {
|
||||
let action;
|
||||
if (task) {
|
||||
const sess = currentSession(task.id);
|
||||
action = h("a", { class: "todo-task", href: `#/task/${task.id}`, onclick: openTranscript }, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), sess?.state ?? "no agent");
|
||||
action = h("a", {
|
||||
class: "todo-task",
|
||||
href: `#/task/${task.id}`,
|
||||
onclick: (e) => {
|
||||
e.preventDefault();
|
||||
focusTask(task.id);
|
||||
},
|
||||
}, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), sess?.state ?? "no agent");
|
||||
}
|
||||
else {
|
||||
const busy = starting.has(`${project.id}\n${item.title}`);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+159
-2
@@ -54,6 +54,57 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/me/lead/sessions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description The calling user's lead agent sessions, oldest first. A compaction completes one and starts the next. */
|
||||
get: operations["listLeadSessions"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/me/lead/messages": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** @description Sends a message to the calling user's lead agent, starting a lead session when there is none. While the lead is mid-turn the message is held and sent as its next turn. */
|
||||
post: operations["messageLead"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/me/focus": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["getFocus"];
|
||||
/** @description Records the human switching the right pane's task or tab. The lead agent is told. */
|
||||
put: operations["setFocus"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/push/key": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -595,7 +646,7 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description Server-sent events. Each event's id is its seq; data is the Event as JSON. Events after Last-Event-ID (or the after query parameter) are replayed before live events. With task_id, only that task's events are sent. */
|
||||
/** @description Server-sent events. Each event's id is its seq; data is the Event as JSON. Events after Last-Event-ID (or the after query parameter) are replayed before live events. With task_id, only that task's events are sent; with lead=true, only the events of the caller's lead agent. Events with an owner go only to that user. */
|
||||
get: operations["streamEvents"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
@@ -814,7 +865,10 @@ export interface components {
|
||||
};
|
||||
AgentSession: {
|
||||
id: string;
|
||||
/** @description Empty for a lead session. */
|
||||
task_id: string;
|
||||
/** @description The login whose lead agent this is; empty for a task's session. */
|
||||
owner: string;
|
||||
/** @enum {string} */
|
||||
vendor: "claude";
|
||||
/** @description Requested model; empty means the vendor's default. */
|
||||
@@ -826,7 +880,7 @@ export interface components {
|
||||
*/
|
||||
state: "idle" | "queued" | "running" | "completed";
|
||||
/** @enum {string} */
|
||||
role: "implementer";
|
||||
role: "implementer" | "lead";
|
||||
created_by: string;
|
||||
/** Format: date-time */
|
||||
created_at: string;
|
||||
@@ -927,11 +981,22 @@ export interface components {
|
||||
project_id?: string;
|
||||
task_id?: string;
|
||||
agent_session_id?: string;
|
||||
/** @description Set on a lead agent's events and a user's focus.updated: the login they belong to. Only that user is sent them. */
|
||||
owner?: string;
|
||||
actor: string;
|
||||
payload: Record<string, never>;
|
||||
/** Format: date-time */
|
||||
created_at: string;
|
||||
};
|
||||
Focus: {
|
||||
/** @description The task the right pane shows; absent for none. */
|
||||
task_id?: string;
|
||||
/** @description The tabs the lead put first, in order. The other task tabs follow them. */
|
||||
tabs: components["schemas"]["TaskTab"][];
|
||||
tab: components["schemas"]["TaskTab"];
|
||||
};
|
||||
/** @enum {string} */
|
||||
TaskTab: "transcript" | "changes" | "files" | "inbox" | "todo";
|
||||
Status: {
|
||||
/** @description Agent processes currently running. */
|
||||
live_agents: number;
|
||||
@@ -1151,6 +1216,97 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
listLeadSessions: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The sessions. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AgentSession"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
messageLead: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["MessageCreate"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The lead session the message went to or is held for. */
|
||||
202: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AgentSession"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
getFocus: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description What the calling user's right pane shows. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Focus"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
setFocus: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["Focus"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The stored focus. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["Focus"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Error"];
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
getPushKey: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1971,6 +2127,7 @@ export interface operations {
|
||||
query?: {
|
||||
after?: number;
|
||||
task_id?: string;
|
||||
lead?: boolean;
|
||||
};
|
||||
header?: {
|
||||
"Last-Event-ID"?: number;
|
||||
|
||||
+8
-3
@@ -18,6 +18,8 @@ export type Zombie = Schemas["Zombie"];
|
||||
export type ProtocolErrors = Schemas["ProtocolErrors"];
|
||||
export type Snapshot = Schemas["Snapshot"];
|
||||
export type Participant = Schemas["Participant"];
|
||||
export type Focus = Schemas["Focus"];
|
||||
export type TaskTab = Schemas["TaskTab"];
|
||||
|
||||
type Method = "get" | "post" | "put" | "delete";
|
||||
|
||||
@@ -107,7 +109,7 @@ const retryAfterMs = 2_000;
|
||||
|
||||
export function subscribe(
|
||||
after: number,
|
||||
taskId: string | undefined,
|
||||
filter: { task_id?: string; lead?: boolean },
|
||||
onEvent: (ev: ByreEvent) => void,
|
||||
onState: (connected: boolean) => void,
|
||||
): () => void {
|
||||
@@ -121,8 +123,11 @@ export function subscribe(
|
||||
source?.close();
|
||||
heard = Date.now();
|
||||
const query = new URLSearchParams({ after: String(last) });
|
||||
if (taskId) {
|
||||
query.set("task_id", taskId);
|
||||
if (filter.task_id) {
|
||||
query.set("task_id", filter.task_id);
|
||||
}
|
||||
if (filter.lead) {
|
||||
query.set("lead", "true");
|
||||
}
|
||||
const s = new EventSource(`/api/events?${query}`);
|
||||
source = s;
|
||||
|
||||
+17
-10
@@ -1,8 +1,8 @@
|
||||
import { get, put, subscribe } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { mountNavbar } from "./navbar.js";
|
||||
import { Pane } from "./panes.js";
|
||||
import { handleEvent, loadInbox, loadMe, loadProjects, notify, on, selectTask, state } from "./state.js";
|
||||
import { leadPane, taskPane } from "./panes.js";
|
||||
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, notify, on, selectTask, setFocus, startLeadStream, state } from "./state.js";
|
||||
|
||||
function identityBanner(root: HTMLElement): void {
|
||||
const render = () => {
|
||||
@@ -34,6 +34,9 @@ function identityBanner(root: HTMLElement): void {
|
||||
function route(): void {
|
||||
const match = /^#\/task\/(\w+)$/.exec(location.hash);
|
||||
const taskId = match?.[1];
|
||||
if (taskId !== state.focus.task_id) {
|
||||
setFocus({ task_id: taskId ?? null }).catch((err: unknown) => console.error("set focus", err));
|
||||
}
|
||||
if (taskId !== state.selectedTaskId) {
|
||||
selectTask(taskId);
|
||||
}
|
||||
@@ -42,11 +45,18 @@ function route(): void {
|
||||
async function main(): Promise<void> {
|
||||
const navbar = h("header", { class: "navbar" });
|
||||
const banner = h("div", { class: "banner", hidden: true });
|
||||
const left = new Pane("left", "transcript");
|
||||
const right = new Pane("right", "inbox");
|
||||
replace(document.body, navbar, banner, h("div", { class: "panes" }, left.el, right.el));
|
||||
|
||||
mountNavbar(navbar, (view) => right.show(view));
|
||||
const status = await get("/api/admin/status");
|
||||
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions()]);
|
||||
const linked = location.hash;
|
||||
await loadFocus();
|
||||
if (/^#\/task\/\w+$/.test(linked)) {
|
||||
location.hash = linked;
|
||||
}
|
||||
startLeadStream();
|
||||
|
||||
replace(document.body, navbar, banner, h("div", { class: "panes" }, leadPane(), taskPane()));
|
||||
mountNavbar(navbar);
|
||||
identityBanner(banner);
|
||||
|
||||
addEventListener(
|
||||
@@ -61,10 +71,7 @@ async function main(): Promise<void> {
|
||||
{ capture: true },
|
||||
);
|
||||
|
||||
const status = await get("/api/admin/status");
|
||||
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
||||
|
||||
subscribe(status.last_event_seq, undefined, handleEvent, (connected) => {
|
||||
subscribe(status.last_event_seq, {}, handleEvent, (connected) => {
|
||||
state.connected = connected;
|
||||
notify("connection");
|
||||
});
|
||||
|
||||
@@ -71,6 +71,28 @@ function modal(text: string, buttons: { label: string; value: string; class?: st
|
||||
});
|
||||
}
|
||||
|
||||
export function openPanel(title: string, mount: (root: HTMLElement) => () => void): void {
|
||||
if (document.querySelector("dialog.panel[open]")) {
|
||||
return;
|
||||
}
|
||||
const close = h("button", { type: "button", class: "panel-close", title: "Close (Esc)" }, "×");
|
||||
const body = h("div", { class: "panel-body" });
|
||||
const dialog = h("dialog", { class: "modal panel" }, h("header", { class: "panel-head" }, h("h2", {}, title), close), body);
|
||||
close.onclick = () => dialog.close();
|
||||
dialog.onclick = (e) => {
|
||||
if (e.target === dialog || (e.target as Element).closest("a[href^='#']")) {
|
||||
dialog.close();
|
||||
}
|
||||
};
|
||||
document.body.append(dialog);
|
||||
const dispose = mount(body);
|
||||
dialog.onclose = () => {
|
||||
dispose();
|
||||
dialog.remove();
|
||||
};
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
export async function showAlert(text: string): Promise<void> {
|
||||
await modal(text, [{ label: "OK", value: "ok", class: "primary" }]);
|
||||
}
|
||||
|
||||
+11
-83
@@ -3,51 +3,20 @@ import { mountChanges } from "./changes.js";
|
||||
import { ago, errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { image, inlineMarkdown, markdown } from "./markdown.js";
|
||||
import { diagram } from "./mermaid.js";
|
||||
import { findTask, onEach, state } from "./state.js";
|
||||
|
||||
type Scope = "all" | "project" | "task";
|
||||
import { onEach, state } from "./state.js";
|
||||
|
||||
const checked = new Map<string, string[]>();
|
||||
const texts = new Map<string, string>();
|
||||
let scope: Scope = "all";
|
||||
|
||||
export function mountInbox(root: HTMLElement): () => void {
|
||||
const tabs = h("nav", { class: "scope-tabs" });
|
||||
const list = h("div", { class: "inbox-list" });
|
||||
const cards = new Map<string, { sig: string; el: HTMLElement }>();
|
||||
const groups = new Map<string, { head: string; h3: HTMLElement; el: HTMLElement }>();
|
||||
const empty = h("div", { class: "empty" }, "Nothing waiting on you.");
|
||||
|
||||
const visible = (): InboxItem[] => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (scope === "task" && task) {
|
||||
return state.inbox.filter((i) => i.task_id === task.id);
|
||||
}
|
||||
if (scope === "project" && task) {
|
||||
return state.inbox.filter((i) => i.project_id === task.project_id);
|
||||
}
|
||||
return state.inbox;
|
||||
};
|
||||
|
||||
const renderTabs = () => {
|
||||
const hasTask = state.selectedTaskId !== undefined;
|
||||
const tab = (s: Scope, label: string) => {
|
||||
const b = h("button", { class: s === scope ? "active" : "", disabled: s !== "all" && !hasTask }, label);
|
||||
b.onclick = () => {
|
||||
scope = s;
|
||||
render();
|
||||
};
|
||||
return b;
|
||||
};
|
||||
replace(tabs, tab("all", "All"), tab("project", "This project"), tab("task", "This task"));
|
||||
};
|
||||
const nothing = h("div", { class: "empty" }, "Nothing waiting on you in this task.");
|
||||
const noTask = h("div", { class: "empty" }, "No task selected.");
|
||||
|
||||
const render = () => {
|
||||
if (scope !== "all" && !state.selectedTaskId) {
|
||||
scope = "all";
|
||||
}
|
||||
renderTabs();
|
||||
const items = visible();
|
||||
const taskId = state.selectedTaskId;
|
||||
const items = state.inbox.filter((i) => i.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at));
|
||||
const keep = new Set(items.map((i) => i.id));
|
||||
for (const id of cards.keys()) {
|
||||
if (!keep.has(id)) {
|
||||
@@ -73,35 +42,16 @@ export function mountInbox(root: HTMLElement): () => void {
|
||||
return el;
|
||||
};
|
||||
|
||||
const shown = groupByTask(items);
|
||||
const keepGroups = new Set(shown.map((g) => g.taskId));
|
||||
for (const id of groups.keys()) {
|
||||
if (!keepGroups.has(id)) {
|
||||
groups.delete(id);
|
||||
}
|
||||
if (!taskId) {
|
||||
reconcile(list, [noTask]);
|
||||
return;
|
||||
}
|
||||
|
||||
const els = shown.map((g) => {
|
||||
const projectName = state.projects.find((p) => p.id === g.projectId)?.name ?? g.projectId;
|
||||
const title = findTask(g.taskId)?.title ?? g.taskId;
|
||||
const head = JSON.stringify([projectName, title]);
|
||||
let group = groups.get(g.taskId);
|
||||
if (!group || group.head !== head) {
|
||||
const h3 = h("h3", {}, h("span", { class: "project-name" }, projectName, " › "), h("a", { href: `#/task/${g.taskId}` }, title));
|
||||
group = { head, h3, el: group?.el ?? h("section", {}) };
|
||||
groups.set(g.taskId, group);
|
||||
}
|
||||
group.el.className = `task-group ${g.blocked ? "blocked" : ""}`;
|
||||
reconcile(group.el, [group.h3, ...g.items.map(cardFor)]);
|
||||
return group.el;
|
||||
});
|
||||
|
||||
reconcile(list, els.length > 0 ? els : [empty]);
|
||||
reconcile(list, items.length > 0 ? items.map(cardFor) : [nothing]);
|
||||
};
|
||||
|
||||
const off = onEach(["inbox", "selection", "tasks"], render);
|
||||
const off = onEach(["inbox", "selection"], render);
|
||||
const changes = h("section", { class: "inbox-changes" });
|
||||
replace(root, h("div", { class: "inbox-view" }, changes, tabs, list));
|
||||
replace(root, h("div", { class: "inbox-view" }, changes, list));
|
||||
const offChanges = mountChanges(changes, true);
|
||||
render();
|
||||
return () => {
|
||||
@@ -122,28 +72,6 @@ function reconcile(parent: Element, nodes: Node[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
type Group = { taskId: string; projectId: string; blocked: boolean; items: InboxItem[] };
|
||||
|
||||
function groupByTask(items: InboxItem[]): Group[] {
|
||||
const groups = new Map<string, Group>();
|
||||
for (const item of [...items].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
|
||||
let g = groups.get(item.task_id);
|
||||
if (!g) {
|
||||
g = { taskId: item.task_id, projectId: item.project_id, blocked: false, items: [] };
|
||||
groups.set(item.task_id, g);
|
||||
}
|
||||
g.items.push(item);
|
||||
g.blocked ||= item.priority === "blocked";
|
||||
}
|
||||
const projectName = (id: string) => state.projects.find((p) => p.id === id)?.name ?? id;
|
||||
return [...groups.values()].sort(
|
||||
(a, b) =>
|
||||
Number(b.blocked) - Number(a.blocked) ||
|
||||
projectName(a.projectId).localeCompare(projectName(b.projectId)) ||
|
||||
a.items[0]!.created_at.localeCompare(b.items[0]!.created_at),
|
||||
);
|
||||
}
|
||||
|
||||
function clickable(el: HTMLElement, go: () => void): HTMLElement {
|
||||
el.onclick = (e) => {
|
||||
if (!(e.target as HTMLElement).closest("a")) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { post } from "./api.js";
|
||||
import { h } from "./dom.js";
|
||||
import { currentLead, state } from "./state.js";
|
||||
import { compactButton, mountChat, type ChatSource, type Head } from "./task.js";
|
||||
|
||||
const leadSource: ChatSource = {
|
||||
key: () => "lead",
|
||||
events: () => state.leadTranscript,
|
||||
session: currentLead,
|
||||
root: () => "",
|
||||
taskId: () => undefined,
|
||||
resetTopics: [],
|
||||
refreshTopics: ["lead.sessions"],
|
||||
transcriptTopic: "lead.transcript",
|
||||
streamTopic: "lead.stream",
|
||||
empty: "",
|
||||
canSend: () => true,
|
||||
placeholder: (s) => (s === "running" ? "Message your lead; it gets this when its turn ends" : "Message your lead. Paste or drop images to attach them."),
|
||||
send: async ({ text, images, model, firstModel, sessionId }) => {
|
||||
await post("/api/me/lead/messages", { body: { text, images, model: sessionId ? model : firstModel } });
|
||||
},
|
||||
};
|
||||
|
||||
function leadHead(): Head {
|
||||
return {
|
||||
render: (usage) => {
|
||||
const sess = currentLead();
|
||||
const live = sess && sess.state !== "completed" ? sess : undefined;
|
||||
return [h("div", { class: "head-actions session-actions" }, compactButton(live?.id, live?.state, live && usage(live.id)))];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function mountLead(root: HTMLElement): () => void {
|
||||
return mountChat(root, leadSource, leadHead);
|
||||
}
|
||||
+11
-15
@@ -1,17 +1,18 @@
|
||||
import { post } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { openTranscript, type View } from "./panes.js";
|
||||
import { activeTasks, currentSession, findTask, lastVisits, onEach, state } from "./state.js";
|
||||
import { openSettings } from "./settings.js";
|
||||
import { activeTasks, currentSession, findTask, focusTask, lastVisits, onEach, state } from "./state.js";
|
||||
import { openStatus } from "./status.js";
|
||||
|
||||
export function mountNavbar(root: HTMLElement, show: (view: View) => void): void {
|
||||
export function mountNavbar(root: HTMLElement): void {
|
||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||
crumb.onclick = openSwitcher;
|
||||
|
||||
const inbox = h("button", { class: "inbox-count" });
|
||||
inbox.onclick = () => show("inbox");
|
||||
const status = h("button", { class: "nav-link" }, "Status");
|
||||
status.onclick = openStatus;
|
||||
|
||||
const admin = h("button", { class: "nav-link" }, "Admin");
|
||||
admin.onclick = () => show("admin");
|
||||
const settings = h("button", { class: "nav-link" }, "Settings");
|
||||
settings.onclick = openSettings;
|
||||
|
||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||
const connLabel = h("span", { class: "connection-label" }, "reconnecting…");
|
||||
@@ -22,18 +23,14 @@ export function mountNavbar(root: HTMLElement, show: (view: View) => void): void
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
replace(crumb, task ? `${project?.name ?? ""} ▸ ${task.title}` : "No task", " ▾");
|
||||
|
||||
const blocked = state.inbox.filter((i) => i.priority === "blocked").length;
|
||||
inbox.className = blocked > 0 ? "inbox-count urgent" : "inbox-count";
|
||||
replace(inbox, `Inbox ${state.inbox.length}`, blocked > 0 && h("span", { class: "blocked-count" }, ` · ${blocked} blocked`));
|
||||
|
||||
conn.className = state.connected ? "connection up" : "connection down";
|
||||
conn.title = state.connected ? "live" : "reconnecting";
|
||||
connLabel.hidden = state.connected;
|
||||
replace(me, state.me?.display_name ?? "");
|
||||
};
|
||||
|
||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, connLabel, conn, me);
|
||||
onEach(["me", "projects", "tasks", "selection", "connection"], render);
|
||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), status, settings, connLabel, conn, me);
|
||||
render();
|
||||
|
||||
addEventListener("keydown", (e) => {
|
||||
@@ -75,8 +72,7 @@ function openSwitcher(): void {
|
||||
|
||||
const go = (taskId: string) => {
|
||||
close();
|
||||
location.hash = `#/task/${taskId}`;
|
||||
openTranscript();
|
||||
focusTask(taskId);
|
||||
};
|
||||
|
||||
const create = async (projectId: string, title: string) => {
|
||||
|
||||
+34
-64
@@ -1,86 +1,56 @@
|
||||
import { mountAdmin } from "./admin.js";
|
||||
import type { TaskTab } from "./api.js";
|
||||
import { mountChanges } from "./changes.js";
|
||||
import { h, replace } from "./dom.js";
|
||||
import { mountFiles } from "./files.js";
|
||||
import { mountInbox } from "./inbox.js";
|
||||
import { mountSettings } from "./settings.js";
|
||||
import { mountLead } from "./lead.js";
|
||||
import { focusTab, on, state, tabs } from "./state.js";
|
||||
import { mountTranscript } from "./task.js";
|
||||
import { mountTasks } from "./tasks.js";
|
||||
import { mountTodo } from "./todo.js";
|
||||
|
||||
export type View = "transcript" | "changes" | "files" | "tasks" | "todo" | "inbox" | "admin" | "settings";
|
||||
|
||||
const views: Record<View, { label: string; mount: (root: HTMLElement) => () => void }> = {
|
||||
const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () => void }> = {
|
||||
transcript: { label: "Transcript", mount: mountTranscript },
|
||||
changes: { label: "Changes", mount: (root) => mountChanges(root) },
|
||||
files: { label: "Files", mount: mountFiles },
|
||||
tasks: { label: "Tasks", mount: mountTasks },
|
||||
todo: { label: "Todo", mount: mountTodo },
|
||||
inbox: { label: "Inbox", mount: mountInbox },
|
||||
admin: { label: "Admin", mount: mountAdmin },
|
||||
settings: { label: "Settings", mount: mountSettings },
|
||||
todo: { label: "Todo", mount: mountTodo },
|
||||
};
|
||||
|
||||
const order: View[] = ["transcript", "changes", "files", "tasks", "todo", "inbox", "admin", "settings"];
|
||||
|
||||
const panes: Pane[] = [];
|
||||
|
||||
export function openTranscript(): void {
|
||||
if (panes.some((p) => p.current() === "transcript")) {
|
||||
return;
|
||||
}
|
||||
(panes.find((p) => p.current() !== "tasks") ?? panes[0])?.show("transcript");
|
||||
export function leadPane(): HTMLElement {
|
||||
const root = h("div", { class: "view view-lead" });
|
||||
mountLead(root);
|
||||
return h("section", { class: "pane" }, h("nav", { class: "pane-tabs" }, h("button", { class: "active" }, "Lead")), h("div", { class: "pane-body" }, root));
|
||||
}
|
||||
|
||||
export function openChanges(from: Element): void {
|
||||
if (panes.some((p) => p.current() === "changes")) {
|
||||
return;
|
||||
}
|
||||
(panes.find((p) => !p.el.contains(from)) ?? panes[0])?.show("changes");
|
||||
}
|
||||
|
||||
export class Pane {
|
||||
readonly el: HTMLElement;
|
||||
private readonly tabs: HTMLElement;
|
||||
private readonly body: HTMLElement;
|
||||
private view: View;
|
||||
private dispose: () => void = () => {};
|
||||
|
||||
constructor(private readonly key: string, fallback: View) {
|
||||
this.tabs = h("nav", { class: "pane-tabs" });
|
||||
this.body = h("div", { class: "pane-body" });
|
||||
this.el = h("section", { class: "pane" }, this.tabs, this.body);
|
||||
|
||||
const saved = localStorage.getItem(this.storageKey()) as View | null;
|
||||
this.view = saved && saved in views ? saved : fallback;
|
||||
this.show(this.view);
|
||||
panes.push(this);
|
||||
}
|
||||
|
||||
current(): View {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
show(view: View): void {
|
||||
this.dispose();
|
||||
this.view = view;
|
||||
localStorage.setItem(this.storageKey(), view);
|
||||
export function taskPane(): HTMLElement {
|
||||
const strip = h("nav", { class: "pane-tabs" });
|
||||
const body = h("div", { class: "pane-body" });
|
||||
let shown: TaskTab | undefined;
|
||||
let dispose: () => void = () => {};
|
||||
|
||||
const render = () => {
|
||||
const first = state.focus.tabs.filter((t, i, all) => tabs.includes(t) && all.indexOf(t) === i);
|
||||
const order = [...first, ...tabs.filter((t) => !first.includes(t))];
|
||||
const tab = state.focus.tab;
|
||||
replace(
|
||||
this.tabs,
|
||||
...order.map((v) => {
|
||||
const b = h("button", { class: v === view ? "active" : "" }, views[v].label);
|
||||
b.onclick = () => this.show(v);
|
||||
strip,
|
||||
...order.map((t) => {
|
||||
const b = h("button", { class: t === tab ? "active" : "" }, views[t].label);
|
||||
b.onclick = () => focusTab(t);
|
||||
return b;
|
||||
}),
|
||||
);
|
||||
if (tab === shown) {
|
||||
return;
|
||||
}
|
||||
dispose();
|
||||
shown = tab;
|
||||
const root = h("div", { class: `view view-${tab}` });
|
||||
replace(body, root);
|
||||
dispose = views[tab].mount(root);
|
||||
};
|
||||
|
||||
const root = h("div", { class: `view view-${view}` });
|
||||
replace(this.body, root);
|
||||
this.dispose = views[view].mount(root);
|
||||
}
|
||||
|
||||
private storageKey(): string {
|
||||
return `byre.pane.${this.key}`;
|
||||
}
|
||||
on("focus", render);
|
||||
render();
|
||||
return h("section", { class: "pane" }, strip, body);
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,5 +1,5 @@
|
||||
import { del, get, post, put, type NotificationPrefs } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { errorText, h, openPanel, replace, showAlert } from "./dom.js";
|
||||
import { on, state } from "./state.js";
|
||||
|
||||
const kinds: { key: keyof NotificationPrefs; label: string }[] = [
|
||||
@@ -10,7 +10,11 @@ const kinds: { key: keyof NotificationPrefs; label: string }[] = [
|
||||
{ key: "rate_limits", label: "Rate limits" },
|
||||
];
|
||||
|
||||
export function mountSettings(root: HTMLElement): () => void {
|
||||
export function openSettings(): void {
|
||||
openPanel("Settings", mountSettings);
|
||||
}
|
||||
|
||||
function mountSettings(root: HTMLElement): () => void {
|
||||
const device = h("div", { class: "push-device" });
|
||||
const prefs = h("fieldset", { class: "push-prefs" });
|
||||
|
||||
|
||||
+138
-44
@@ -1,6 +1,23 @@
|
||||
import { get, subscribe, type AgentSession, type ByreEvent, type InboxItem, type Project, type Snapshot, type Task, type User } from "./api.js";
|
||||
import { get, put, subscribe, type AgentSession, type ByreEvent, type Focus, type InboxItem, type Project, type Snapshot, type Task, type TaskTab, type User } from "./api.js";
|
||||
|
||||
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event" | "pin";
|
||||
export type Topic =
|
||||
| "me"
|
||||
| "projects"
|
||||
| "tasks"
|
||||
| "sessions"
|
||||
| "inbox"
|
||||
| "selection"
|
||||
| "transcript"
|
||||
| "stream"
|
||||
| "connection"
|
||||
| "event"
|
||||
| "pin"
|
||||
| "focus"
|
||||
| "lead.sessions"
|
||||
| "lead.transcript"
|
||||
| "lead.stream";
|
||||
|
||||
export const tabs: TaskTab[] = ["transcript", "changes", "files", "inbox", "todo"];
|
||||
|
||||
export const state = {
|
||||
me: undefined as User | undefined,
|
||||
@@ -10,9 +27,11 @@ export const state = {
|
||||
inbox: [] as InboxItem[],
|
||||
selectedTaskId: undefined as string | undefined,
|
||||
transcript: [] as ByreEvent[],
|
||||
lastTranscriptSeq: 0,
|
||||
pinned: undefined as number | undefined,
|
||||
connected: false,
|
||||
focus: { tabs: ["transcript"], tab: "transcript" } as Focus,
|
||||
leadSessions: [] as AgentSession[],
|
||||
leadTranscript: [] as ByreEvent[],
|
||||
};
|
||||
|
||||
const listeners = new Map<Topic, Set<(detail?: unknown) => void>>();
|
||||
@@ -60,6 +79,10 @@ 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 currentLead(): AgentSession | undefined {
|
||||
return state.leadSessions.findLast((s) => s.state !== "completed") ?? state.leadSessions.at(-1);
|
||||
}
|
||||
|
||||
export function snapshots(): Snapshot[] {
|
||||
return state.transcript.filter((e) => e.kind === "snapshot.created").map((e) => e.payload as unknown as Snapshot);
|
||||
}
|
||||
@@ -97,9 +120,106 @@ export async function loadInbox(): Promise<void> {
|
||||
notify("inbox");
|
||||
}
|
||||
|
||||
export async function loadLeadSessions(): Promise<void> {
|
||||
state.leadSessions = await get("/api/me/lead/sessions");
|
||||
notify("lead.sessions");
|
||||
}
|
||||
|
||||
export async function loadFocus(): Promise<void> {
|
||||
applyFocus(await get("/api/me/focus"));
|
||||
}
|
||||
|
||||
export function applyFocus(f: Focus): void {
|
||||
state.focus = f;
|
||||
const hash = f.task_id ? `#/task/${f.task_id}` : "#/";
|
||||
if (location.hash !== hash && !(hash === "#/" && location.hash === "")) {
|
||||
location.hash = hash;
|
||||
}
|
||||
notify("focus");
|
||||
}
|
||||
|
||||
export async function setFocus(change: { task_id?: string | null; tab?: TaskTab }): Promise<void> {
|
||||
const taskId = change.task_id === undefined ? state.focus.task_id : (change.task_id ?? undefined);
|
||||
const next: Focus = { tabs: state.focus.tabs, tab: change.tab ?? state.focus.tab };
|
||||
if (taskId) {
|
||||
next.task_id = taskId;
|
||||
}
|
||||
applyFocus(next);
|
||||
applyFocus(await put("/api/me/focus", { body: next }));
|
||||
}
|
||||
|
||||
export function focusTask(taskId: string, tab: TaskTab = "transcript"): void {
|
||||
setFocus({ task_id: taskId, tab }).catch((err: unknown) => console.error("set focus", err));
|
||||
}
|
||||
|
||||
export function focusTab(tab: TaskTab): void {
|
||||
setFocus({ tab }).catch((err: unknown) => console.error("set focus", err));
|
||||
}
|
||||
|
||||
class Feed {
|
||||
events: ByreEvent[] = [];
|
||||
private last = 0;
|
||||
private buffer: ByreEvent[] = [];
|
||||
private timer = 0;
|
||||
|
||||
constructor(
|
||||
private readonly transcriptTopic: Topic,
|
||||
private readonly streamTopic: Topic,
|
||||
private readonly store: (events: ByreEvent[]) => void,
|
||||
) {}
|
||||
|
||||
reset(): void {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
this.buffer = [];
|
||||
this.events = [];
|
||||
this.last = 0;
|
||||
this.store(this.events);
|
||||
}
|
||||
|
||||
readonly push = (ev: ByreEvent): void => {
|
||||
this.buffer.push(ev);
|
||||
if (!this.timer) {
|
||||
this.timer = setTimeout(() => this.flush(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
private flush(): void {
|
||||
const events = this.buffer;
|
||||
this.buffer = [];
|
||||
this.timer = 0;
|
||||
let stored: ByreEvent[] = [];
|
||||
for (const ev of events) {
|
||||
if (ev.seq !== 0) {
|
||||
stored.push(ev);
|
||||
continue;
|
||||
}
|
||||
this.append(stored);
|
||||
stored = [];
|
||||
notify(this.streamTopic, ev);
|
||||
}
|
||||
this.append(stored);
|
||||
}
|
||||
|
||||
private append(events: ByreEvent[]): void {
|
||||
const fresh = events.filter((ev) => ev.seq > this.last);
|
||||
if (fresh.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.events.push(...fresh);
|
||||
this.last = fresh.at(-1)!.seq;
|
||||
notify(this.transcriptTopic, fresh);
|
||||
}
|
||||
}
|
||||
|
||||
const taskFeed = new Feed("transcript", "stream", (events) => {
|
||||
state.transcript = events;
|
||||
});
|
||||
const leadFeed = new Feed("lead.transcript", "lead.stream", (events) => {
|
||||
state.leadTranscript = events;
|
||||
});
|
||||
|
||||
let closeTaskStream: (() => void) | undefined;
|
||||
let taskBuffer: ByreEvent[] = [];
|
||||
let taskFlush = 0;
|
||||
|
||||
const visitsKey = "byre.visits";
|
||||
|
||||
@@ -119,12 +239,8 @@ export function selectTask(taskId: string | undefined): void {
|
||||
markVisited(taskId);
|
||||
closeTaskStream?.();
|
||||
closeTaskStream = undefined;
|
||||
clearTimeout(taskFlush);
|
||||
taskFlush = 0;
|
||||
taskBuffer = [];
|
||||
taskFeed.reset();
|
||||
state.selectedTaskId = taskId;
|
||||
state.transcript = [];
|
||||
state.lastTranscriptSeq = 0;
|
||||
state.pinned = undefined;
|
||||
notify("selection");
|
||||
|
||||
@@ -132,41 +248,12 @@ export function selectTask(taskId: string | undefined): void {
|
||||
return;
|
||||
}
|
||||
|
||||
closeTaskStream = subscribe(0, taskId, queueTaskEvent, () => undefined);
|
||||
closeTaskStream = subscribe(0, { task_id: taskId }, taskFeed.push, () => undefined);
|
||||
}
|
||||
|
||||
function queueTaskEvent(ev: ByreEvent): void {
|
||||
taskBuffer.push(ev);
|
||||
if (!taskFlush) {
|
||||
taskFlush = setTimeout(flushTaskEvents, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function flushTaskEvents(): void {
|
||||
const events = taskBuffer;
|
||||
taskBuffer = [];
|
||||
taskFlush = 0;
|
||||
let stored: ByreEvent[] = [];
|
||||
for (const ev of events) {
|
||||
if (ev.seq !== 0) {
|
||||
stored.push(ev);
|
||||
continue;
|
||||
}
|
||||
appendTranscript(stored);
|
||||
stored = [];
|
||||
notify("stream", ev);
|
||||
}
|
||||
appendTranscript(stored);
|
||||
}
|
||||
|
||||
function appendTranscript(events: ByreEvent[]): void {
|
||||
const fresh = events.filter((ev) => ev.seq > state.lastTranscriptSeq);
|
||||
if (fresh.length === 0) {
|
||||
return;
|
||||
}
|
||||
state.transcript.push(...fresh);
|
||||
state.lastTranscriptSeq = fresh.at(-1)!.seq;
|
||||
notify("transcript", fresh);
|
||||
export function startLeadStream(): void {
|
||||
leadFeed.reset();
|
||||
subscribe(0, { lead: true }, leadFeed.push, () => undefined);
|
||||
}
|
||||
|
||||
export function handleEvent(ev: ByreEvent): void {
|
||||
@@ -195,7 +282,9 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
case "session.created":
|
||||
case "session.state":
|
||||
case "session.model":
|
||||
if (ev.task_id) {
|
||||
if (ev.owner) {
|
||||
void loadLeadSessions();
|
||||
} else if (ev.task_id) {
|
||||
void loadSessions(ev.task_id);
|
||||
}
|
||||
break;
|
||||
@@ -203,6 +292,11 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
case "inbox.updated":
|
||||
void loadInbox();
|
||||
break;
|
||||
case "focus.updated":
|
||||
if (ev.actor !== state.me?.login) {
|
||||
applyFocus(ev.payload as unknown as Focus);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
notify("event", ev);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { get, type AgentProcess, type ByreEvent, type Status, type TurnRef, type Zombie } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { ago, errorText, h, openPanel, replace } from "./dom.js";
|
||||
import { findTask, on, onEach, state } from "./state.js";
|
||||
|
||||
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]);
|
||||
|
||||
export function mountAdmin(root: HTMLElement): () => void {
|
||||
export function openStatus(): void {
|
||||
openPanel("Status", mountStatus);
|
||||
}
|
||||
|
||||
function mountStatus(root: HTMLElement): () => void {
|
||||
let status: Status | undefined;
|
||||
let error = "";
|
||||
let inFlight = false;
|
||||
@@ -16,13 +20,13 @@ export function mountAdmin(root: HTMLElement): () => void {
|
||||
replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…"));
|
||||
return;
|
||||
}
|
||||
const scrollTop = root.querySelector(".admin-view")?.scrollTop ?? 0;
|
||||
const scrollTop = root.querySelector(".status-view")?.scrollTop ?? 0;
|
||||
const open = new Set([...root.querySelectorAll<HTMLDetailsElement>(".problems details[open]")].map((d) => d.dataset["vendor"] ?? ""));
|
||||
replace(
|
||||
root,
|
||||
h(
|
||||
"div",
|
||||
{ class: "admin-view" },
|
||||
{ class: "status-view" },
|
||||
error && h("p", { class: "error-text" }, error),
|
||||
problemsSection(status, open),
|
||||
turnsSection(status),
|
||||
@@ -30,7 +34,7 @@ export function mountAdmin(root: HTMLElement): () => void {
|
||||
serverSection(status),
|
||||
),
|
||||
);
|
||||
root.querySelector(".admin-view")!.scrollTop = scrollTop;
|
||||
root.querySelector(".status-view")!.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
@@ -108,7 +112,7 @@ function turnsSection(s: Status): HTMLElement {
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ class: "admin-section" },
|
||||
{ class: "status-section" },
|
||||
h("h2", {}, "Turns"),
|
||||
h(
|
||||
"div",
|
||||
@@ -160,7 +164,7 @@ function turnList(turns: TurnRef[], kind: "running" | "queued", empty: string, p
|
||||
|
||||
function taskLink(taskId: string | undefined): HTMLElement | string {
|
||||
if (!taskId) {
|
||||
return "—";
|
||||
return "lead";
|
||||
}
|
||||
const task = findTask(taskId);
|
||||
const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined;
|
||||
@@ -201,7 +205,7 @@ function problemsSection(s: Status, openVendors: Set<string>): HTMLElement | fal
|
||||
}
|
||||
return h(
|
||||
"section",
|
||||
{ class: "admin-section" },
|
||||
{ class: "status-section" },
|
||||
h("h2", {}, "Problems"),
|
||||
h(
|
||||
"ul",
|
||||
@@ -231,7 +235,7 @@ function rateLimitsSection(limits: Status["rate_limits"]): HTMLElement {
|
||||
const vendors = Object.keys(limits).sort();
|
||||
return h(
|
||||
"section",
|
||||
{ class: "admin-section" },
|
||||
{ class: "status-section" },
|
||||
h("h2", {}, "Rate limits"),
|
||||
...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})),
|
||||
vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started."),
|
||||
@@ -313,7 +317,7 @@ function serverSection(s: Status): HTMLElement {
|
||||
["Database", bytes(s.db_bytes)],
|
||||
["Last event", `#${s.last_event_seq}`],
|
||||
];
|
||||
return h("section", { class: "admin-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
|
||||
return h("section", { class: "status-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)])));
|
||||
}
|
||||
|
||||
function bytes(n: number): string {
|
||||
+189
-137
@@ -1,8 +1,32 @@
|
||||
import { get, post, type ByreEvent, type Participant, type Snapshot } from "./api.js";
|
||||
import { get, post, type AgentSession, type ByreEvent, type Participant, type Snapshot } from "./api.js";
|
||||
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { openChanges } from "./panes.js";
|
||||
import { currentSession, findTask, onEach, pinSnapshot, state } from "./state.js";
|
||||
import { currentSession, findTask, focusTab, onEach, pinSnapshot, setFocus, state, type Topic } from "./state.js";
|
||||
|
||||
export type Usage = { used: number; window: number; model: string };
|
||||
|
||||
export type ChatSource = {
|
||||
key: () => string | undefined;
|
||||
events: () => ByreEvent[];
|
||||
session: () => AgentSession | undefined;
|
||||
root: () => string;
|
||||
taskId: () => string | undefined;
|
||||
resetTopics: Topic[];
|
||||
refreshTopics: Topic[];
|
||||
transcriptTopic: Topic;
|
||||
streamTopic: Topic;
|
||||
empty: string;
|
||||
canSend: (sessionState: string) => boolean;
|
||||
placeholder: (sessionState: string) => string;
|
||||
send: (m: { key: string; sessionId: string | undefined; text: string; images: string[] | undefined; model: string | undefined; firstModel: string }) => Promise<void>;
|
||||
};
|
||||
|
||||
export type Head = {
|
||||
render: (usage: (sessionId: string) => Usage | undefined) => (Node | false | null)[];
|
||||
onEvents?: (events: ByreEvent[]) => void;
|
||||
reset?: () => void;
|
||||
dispose?: () => void;
|
||||
};
|
||||
|
||||
export const models = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1"];
|
||||
|
||||
@@ -30,12 +54,127 @@ type Ctx = {
|
||||
anchors: Anchor[];
|
||||
unplaced: Snapshot[];
|
||||
root: string;
|
||||
taskId: string | undefined;
|
||||
lastMessage: HTMLElement | undefined;
|
||||
turnStart: HTMLElement | undefined;
|
||||
watchReport: (el: HTMLElement, itemId: string) => void;
|
||||
};
|
||||
|
||||
const taskSource: ChatSource = {
|
||||
key: () => state.selectedTaskId,
|
||||
events: () => state.transcript,
|
||||
session: () => (state.selectedTaskId ? currentSession(state.selectedTaskId) : undefined),
|
||||
root: () => (state.selectedTaskId ? (findTask(state.selectedTaskId)?.worktree_path ?? "") : ""),
|
||||
taskId: () => state.selectedTaskId,
|
||||
resetTopics: ["selection"],
|
||||
refreshTopics: ["tasks", "sessions"],
|
||||
transcriptTopic: "transcript",
|
||||
streamTopic: "stream",
|
||||
empty: "No task selected. Ask your lead, or pick one with ⌘K.",
|
||||
canSend: (s) => s === "idle",
|
||||
placeholder: (s) => (s === "idle" ? "Message the agent. Paste or drop images to attach them." : "Wait for the turn to end, or interrupt it"),
|
||||
send: async ({ key, sessionId, text, images, model, firstModel }) => {
|
||||
if (sessionId) {
|
||||
await post("/api/sessions/{sessionId}/messages", { path: { sessionId }, body: { text, images, model } });
|
||||
return;
|
||||
}
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId: key }, body: { prompt: text, model: firstModel, images } });
|
||||
},
|
||||
};
|
||||
|
||||
export function mountTranscript(root: HTMLElement): () => void {
|
||||
return mountChat(root, taskSource, taskHead);
|
||||
}
|
||||
|
||||
function taskHead(rerender: () => void): Head {
|
||||
let unmerged: { taskId: string; has: boolean } | undefined;
|
||||
let timer: number | undefined;
|
||||
const load = async () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId } });
|
||||
if (state.selectedTaskId !== taskId) {
|
||||
return;
|
||||
}
|
||||
const has = u.commits > 0 || u.files > 0;
|
||||
if (unmerged?.taskId === taskId && unmerged.has === has) {
|
||||
return;
|
||||
}
|
||||
unmerged = { taskId, has };
|
||||
rerender();
|
||||
} catch (err) {
|
||||
console.error("load unmerged changes", err);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
reset: () => {
|
||||
clearTimeout(timer);
|
||||
void load();
|
||||
},
|
||||
dispose: () => clearTimeout(timer),
|
||||
onEvents: (events) => {
|
||||
if (events.some((ev) => ev.kind === "agent.tool_result" || ev.kind === "turn.ended")) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => void load(), 1500);
|
||||
}
|
||||
},
|
||||
render: (usage) => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (!task) {
|
||||
return [];
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
if (task.status !== "active") {
|
||||
return [h("div", { class: "head-actions" }, task.status)];
|
||||
}
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
const busy = sess?.state === "running" || sess?.state === "queued";
|
||||
const merge = h(
|
||||
"button",
|
||||
{ disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` },
|
||||
"Merge",
|
||||
);
|
||||
merge.onclick = async () => {
|
||||
let coAuthors;
|
||||
try {
|
||||
coAuthors = await get("/api/tasks/{taskId}/co-authors", { path: { taskId: task.id } });
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
await confirmThen(mergeQuestion(task.merge_title, target, coAuthors), "Merge", false, async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
|
||||
});
|
||||
};
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
|
||||
mergeIn.onclick = async () => {
|
||||
mergeIn.disabled = true;
|
||||
try {
|
||||
await post("/api/tasks/{taskId}/merge-in", { path: { taskId: task.id } });
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
mergeIn.disabled = false;
|
||||
}
|
||||
};
|
||||
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
|
||||
discard.onclick = () =>
|
||||
confirmThen(`Discard "${task.title}"?`, "Discard", true, async () => {
|
||||
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
|
||||
await setFocus({ task_id: null });
|
||||
});
|
||||
return [
|
||||
h("div", { class: "head-actions session-actions" }, compactButton(sess?.id, sess?.state, sess && usage(sess.id)), mergeIn),
|
||||
h("div", { class: "head-actions" }, merge, discard),
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerender: () => void) => Head): () => void {
|
||||
const head = h("header", { class: "task-header" });
|
||||
const transcript = h("div", { class: "transcript" });
|
||||
const composer = h("div", { class: "composer" });
|
||||
@@ -78,46 +217,23 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
{ root: transcript, threshold: 0.5 },
|
||||
);
|
||||
|
||||
const newCtx = (): Ctx => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
return {
|
||||
tools: new Map(),
|
||||
run: undefined,
|
||||
anchors: [],
|
||||
unplaced: [],
|
||||
lastMessage: undefined,
|
||||
turnStart: undefined,
|
||||
root: task?.worktree_path ?? "",
|
||||
watchReport: (el, itemId) => {
|
||||
el.dataset["inboxItem"] = itemId;
|
||||
observer.observe(el);
|
||||
},
|
||||
};
|
||||
};
|
||||
const newCtx = (): Ctx => ({
|
||||
tools: new Map(),
|
||||
run: undefined,
|
||||
anchors: [],
|
||||
unplaced: [],
|
||||
lastMessage: undefined,
|
||||
turnStart: undefined,
|
||||
root: src.root(),
|
||||
taskId: src.taskId(),
|
||||
watchReport: (el, itemId) => {
|
||||
el.dataset["inboxItem"] = itemId;
|
||||
observer.observe(el);
|
||||
},
|
||||
});
|
||||
let ctx = newCtx();
|
||||
let context = new Map<string, { used: number; window: number; model: string }>();
|
||||
let unmerged: { taskId: string; has: boolean } | undefined;
|
||||
let unmergedTimer: number | undefined;
|
||||
const loadUnmerged = async () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
if (!taskId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId } });
|
||||
if (state.selectedTaskId !== taskId) {
|
||||
return;
|
||||
}
|
||||
const has = u.commits > 0 || u.files > 0;
|
||||
if (unmerged?.taskId === taskId && unmerged.has === has) {
|
||||
return;
|
||||
}
|
||||
unmerged = { taskId, has };
|
||||
renderHead();
|
||||
} catch (err) {
|
||||
console.error("load unmerged changes", err);
|
||||
}
|
||||
};
|
||||
let context = new Map<string, Usage>();
|
||||
const headImpl = makeHead(() => renderHead());
|
||||
|
||||
const spinner = h("div", { class: "spinner", hidden: true });
|
||||
const setRunning = (running: boolean) => {
|
||||
@@ -143,71 +259,26 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
const put = (el: HTMLElement) => putBefore(el, pending[0]?.el);
|
||||
|
||||
const renderHead = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
setRunning(task !== undefined && currentSession(task.id)?.state === "running");
|
||||
if (!task) {
|
||||
setRunning(src.session()?.state === "running");
|
||||
if (!src.key()) {
|
||||
replace(head);
|
||||
return;
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
if (task.status !== "active") {
|
||||
replace(head, h("div", { class: "head-actions" }, task.status));
|
||||
return;
|
||||
}
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
const busy = sess?.state === "running" || sess?.state === "queued";
|
||||
const merge = h(
|
||||
"button",
|
||||
{ disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` },
|
||||
"Merge",
|
||||
);
|
||||
merge.onclick = async () => {
|
||||
let coAuthors;
|
||||
try {
|
||||
coAuthors = await get("/api/tasks/{taskId}/co-authors", { path: { taskId: task.id } });
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
await confirmThen(mergeQuestion(task.merge_title, target, coAuthors), "Merge", false, async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
|
||||
});
|
||||
};
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
|
||||
mergeIn.onclick = async () => {
|
||||
mergeIn.disabled = true;
|
||||
try {
|
||||
await post("/api/tasks/{taskId}/merge-in", { path: { taskId: task.id } });
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
mergeIn.disabled = false;
|
||||
}
|
||||
};
|
||||
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
|
||||
discard.onclick = () =>
|
||||
confirmThen(`Discard "${task.title}"?`, "Discard", true, async () => {
|
||||
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
|
||||
location.hash = "#/";
|
||||
});
|
||||
replace(
|
||||
head,
|
||||
h("div", { class: "head-actions session-actions" }, compactButton(sess?.id, sess?.state, sess && context.get(sess.id)), mergeIn),
|
||||
h("div", { class: "head-actions" }, merge, discard),
|
||||
);
|
||||
replace(head, ...headImpl.render((id) => context.get(id)));
|
||||
};
|
||||
|
||||
let composerModel = "";
|
||||
let focusStart = false;
|
||||
const renderComposer = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (!task) {
|
||||
const key = src.key();
|
||||
if (!key) {
|
||||
replace(composer);
|
||||
return;
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
const sess = src.session();
|
||||
const live = sess && sess.state !== "completed" ? sess : undefined;
|
||||
composerModel = live ? live.model || context.get(live.id)?.model || "" : "";
|
||||
const form = composeForm(task.id, live?.id, live?.state ?? "idle", composerModel, showPending);
|
||||
const form = composeForm(src, key, live?.id, live?.state ?? "idle", composerModel, showPending);
|
||||
const old = composer.querySelector("textarea");
|
||||
const caret = old !== null && document.activeElement === old ? [old.selectionStart, old.selectionEnd] : undefined;
|
||||
replace(composer, form);
|
||||
@@ -343,7 +414,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
}
|
||||
stream.timer = 0;
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
replace(stream.el, markdown(stream.text, "text md", { taskId: state.selectedTaskId ?? undefined }));
|
||||
replace(stream.el, markdown(stream.text, "text md", { taskId: ctx.taskId }));
|
||||
if (pinned) {
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
}
|
||||
@@ -367,12 +438,9 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
const appendEvents = (events: ByreEvent[]) => {
|
||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||
let contextChanged = false;
|
||||
headImpl.onEvents?.(events);
|
||||
for (const ev of events) {
|
||||
const p = ev.payload as Record<string, unknown>;
|
||||
if (ev.kind === "agent.tool_result" || ev.kind === "turn.ended") {
|
||||
clearTimeout(unmergedTimer);
|
||||
unmergedTimer = setTimeout(() => void loadUnmerged(), 1500);
|
||||
}
|
||||
if (ev.kind === "agent.message" || ev.kind === "turn.ended" || ev.kind === "user.message") {
|
||||
dropStream();
|
||||
}
|
||||
@@ -420,7 +488,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
}
|
||||
if (contextChanged) {
|
||||
renderHead();
|
||||
const sess = state.selectedTaskId ? currentSession(state.selectedTaskId) : undefined;
|
||||
const sess = src.session();
|
||||
if (sess && !sess.model && (context.get(sess.id)?.model ?? "") !== composerModel) {
|
||||
renderComposer();
|
||||
}
|
||||
@@ -438,33 +506,26 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
ctx = newCtx();
|
||||
context = new Map();
|
||||
focusStart = true;
|
||||
clearTimeout(unmergedTimer);
|
||||
void loadUnmerged();
|
||||
headImpl.reset?.();
|
||||
renderHead();
|
||||
if (!state.selectedTaskId) {
|
||||
if (!src.key()) {
|
||||
renderComposer();
|
||||
replace(transcript, h("div", { class: "empty" }, "No task selected. Pick one with ⌘K or from the Tasks tab."));
|
||||
replace(transcript, h("div", { class: "empty" }, src.empty));
|
||||
return;
|
||||
}
|
||||
replace(transcript, spinner);
|
||||
appendEvents(state.transcript);
|
||||
appendEvents(src.events());
|
||||
renderComposer();
|
||||
};
|
||||
|
||||
const offs = [
|
||||
onEach(["selection"], renderAll),
|
||||
onEach(["tasks"], () => {
|
||||
onEach(src.resetTopics, renderAll),
|
||||
onEach(src.refreshTopics, () => {
|
||||
renderHead();
|
||||
renderComposer();
|
||||
}),
|
||||
onEach(["sessions"], (taskId) => {
|
||||
if (taskId === state.selectedTaskId) {
|
||||
renderHead();
|
||||
renderComposer();
|
||||
}
|
||||
}),
|
||||
onEach(["transcript"], (fresh) => appendEvents(fresh as ByreEvent[])),
|
||||
onEach(["stream"], (ev) => appendDelta(ev as ByreEvent)),
|
||||
onEach([src.transcriptTopic], (fresh) => appendEvents(fresh as ByreEvent[])),
|
||||
onEach([src.streamTopic], (ev) => appendDelta(ev as ByreEvent)),
|
||||
onEach(["inbox"], ackVisible),
|
||||
];
|
||||
document.addEventListener("visibilitychange", ackVisible);
|
||||
@@ -476,7 +537,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
off();
|
||||
}
|
||||
document.removeEventListener("visibilitychange", ackVisible);
|
||||
clearTimeout(unmergedTimer);
|
||||
headImpl.dispose?.();
|
||||
observer.disconnect();
|
||||
dropStream();
|
||||
};
|
||||
@@ -623,18 +684,16 @@ function modelSelect(key: string, current: string): HTMLSelectElement {
|
||||
}
|
||||
|
||||
function composeForm(
|
||||
taskId: string,
|
||||
src: ChatSource,
|
||||
key: string,
|
||||
sessionId: string | undefined,
|
||||
sessionState: string,
|
||||
currentModel: string,
|
||||
showPending: (text: string, imgs: PendingImage[]) => () => void,
|
||||
): HTMLElement {
|
||||
const text = h("textarea", {
|
||||
placeholder: sessionState === "idle" ? "Message the agent. Paste or drop images to attach them." : "Wait for the turn to end, or interrupt it",
|
||||
rows: 3,
|
||||
});
|
||||
const clearDraft = keepDraft(text, `message:${taskId}`);
|
||||
const send = h("button", { type: "submit", disabled: sessionState !== "idle" }, "Send");
|
||||
const text = h("textarea", { placeholder: src.placeholder(sessionState), rows: 3 });
|
||||
const clearDraft = keepDraft(text, `message:${key}`);
|
||||
const send = h("button", { type: "submit", disabled: !src.canSend(sessionState) }, "Send");
|
||||
const interrupt = h(
|
||||
"button",
|
||||
{ type: "button", class: "danger", disabled: sessionState !== "running" && sessionState !== "queued" },
|
||||
@@ -651,15 +710,15 @@ function composeForm(
|
||||
}
|
||||
};
|
||||
|
||||
const modelKey = `message:${sessionId ?? taskId}`;
|
||||
const modelKey = `message:${sessionId ?? key}`;
|
||||
const model = modelSelect(modelKey, currentModel);
|
||||
const form = h("form", { class: "compose" });
|
||||
const tray = imageTray(form, text, `message:${taskId}`);
|
||||
const tray = imageTray(form, text, `message:${key}`);
|
||||
form.append(tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const value = text.value;
|
||||
if (!value.trim() && !imageDrafts.has(`message:${taskId}`)) {
|
||||
if (!value.trim() && !imageDrafts.has(`message:${key}`)) {
|
||||
return;
|
||||
}
|
||||
const imgs = tray.take();
|
||||
@@ -668,18 +727,11 @@ function composeForm(
|
||||
const cancel = showPending(value, imgs);
|
||||
try {
|
||||
const images = await uploadImages(imgs);
|
||||
if (sessionId) {
|
||||
await post("/api/sessions/{sessionId}/messages", {
|
||||
path: { sessionId },
|
||||
body: { text: value, images, model: model.value !== currentModel ? model.value : undefined },
|
||||
});
|
||||
} else {
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: value, model: model.value, images } });
|
||||
}
|
||||
await src.send({ key, sessionId, text: value, images, model: model.value !== currentModel ? model.value : undefined, firstModel: model.value });
|
||||
modelChoices.delete(modelKey);
|
||||
} catch (err) {
|
||||
cancel();
|
||||
composerDrafts.set(`message:${taskId}`, value);
|
||||
composerDrafts.set(`message:${key}`, value);
|
||||
text.value = value;
|
||||
tray.restore(imgs);
|
||||
void showAlert(errorText(err));
|
||||
@@ -698,7 +750,7 @@ function submitOnEnter(input: HTMLTextAreaElement, form: HTMLFormElement): void
|
||||
};
|
||||
}
|
||||
|
||||
function compactButton(sessionId: string | undefined, sessionState: string | undefined, c: { used: number; window: number; model: string } | undefined): HTMLElement {
|
||||
export function compactButton(sessionId: string | undefined, sessionState: string | undefined, c: { used: number; window: number; model: string } | undefined): HTMLElement {
|
||||
const pct = c && c.window > 0 ? Math.min(100, (c.used / c.window) * 100) : 0;
|
||||
const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok";
|
||||
const usage = c && c.window > 0 ? `\ncontext: ${c.used.toLocaleString()} of ${c.window.toLocaleString()} tokens${c.model ? ` (${c.model})` : ""}` : "";
|
||||
@@ -751,7 +803,7 @@ function snapMark(ns: number[]): HTMLElement | null {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
pinSnapshot(hi);
|
||||
openChanges(b);
|
||||
focusTab("changes");
|
||||
};
|
||||
return b;
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { post, type Project } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { openTranscript } from "./panes.js";
|
||||
import { currentSession, onEach, state } from "./state.js";
|
||||
|
||||
export function mountTasks(root: HTMLElement): () => void {
|
||||
const render = () => replace(root, h("div", { class: "tasks-view" }, ...state.projects.map(projectBlock), newProjectForm()));
|
||||
const off = onEach(["projects", "tasks", "sessions", "inbox", "selection"], render);
|
||||
render();
|
||||
return off;
|
||||
}
|
||||
|
||||
function projectBlock(project: Project): HTMLElement {
|
||||
const tasks = (state.tasks.get(project.id) ?? []).filter((t) => t.status === "active");
|
||||
|
||||
return h(
|
||||
"section",
|
||||
{ class: "project" },
|
||||
h("h2", {}, project.name, " ", h("span", { class: "repo" }, project.repo_path)),
|
||||
h(
|
||||
"ul",
|
||||
{ class: "task-list" },
|
||||
...tasks.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",
|
||||
{ href: `#/task/${task.id}`, onclick: openTranscript },
|
||||
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
|
||||
h("span", { class: "task-title" }, task.title),
|
||||
h("span", { class: "task-state" }, sess?.state ?? "no agent"),
|
||||
count > 0 && h("span", { class: "badge" }, count),
|
||||
),
|
||||
);
|
||||
}),
|
||||
tasks.length === 0 && h("li", { class: "empty-row" }, "No active tasks."),
|
||||
),
|
||||
newTaskForm(project.id),
|
||||
);
|
||||
}
|
||||
|
||||
function newTaskForm(projectId: string): HTMLElement {
|
||||
const title = h("input", { placeholder: "New task title", required: true });
|
||||
const form = h("form", { class: "inline-form" }, title, h("button", { type: "submit" }, "Create"));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const task = await post("/api/projects/{projectId}/tasks", { path: { projectId }, body: { title: title.value } });
|
||||
location.hash = `#/task/${task.id}`;
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return form;
|
||||
}
|
||||
|
||||
function newProjectForm(): HTMLElement {
|
||||
const name = h("input", { placeholder: "Name", required: true });
|
||||
const path = h("input", { placeholder: "Repo path on the server", required: true });
|
||||
const form = h("form", { class: "stack-form" }, name, path, h("button", { type: "submit" }, "Add project"));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await post("/api/projects", { body: { name: name.value, repo_path: path.value } });
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
return h("details", { class: "add-project", open: state.projects.length === 0 }, h("summary", {}, "Add project"), form);
|
||||
}
|
||||
+27
-8
@@ -1,9 +1,8 @@
|
||||
import { get, post, type ByreEvent, type Project, type Schemas } from "./api.js";
|
||||
import { errorText, h, replace, showAlert } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { openTranscript } from "./panes.js";
|
||||
import { models } from "./task.js";
|
||||
import { currentSession, on, onEach, state } from "./state.js";
|
||||
import { currentSession, findTask, focusTask, on, onEach, state } from "./state.js";
|
||||
|
||||
type Todo = Schemas["Todo"];
|
||||
type TodoItem = Schemas["TodoItem"];
|
||||
@@ -15,9 +14,15 @@ export function mountTodo(root: HTMLElement): () => void {
|
||||
const starting = new Set<string>();
|
||||
let disposed = false;
|
||||
|
||||
const project = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
return state.projects.find((p) => p.id === task?.project_id);
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
const scrollTop = root.querySelector(".todo-view")?.scrollTop ?? 0;
|
||||
replace(root, h("div", { class: "todo-view" }, ...state.projects.map((p) => projectBlock(p, todos.get(p.id), starting, start))));
|
||||
const p = project();
|
||||
replace(root, h("div", { class: "todo-view" }, p ? projectBlock(p, todos.get(p.id), starting, start) : h("div", { class: "empty" }, "No task selected.")));
|
||||
root.querySelector(".todo-view")!.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
@@ -34,7 +39,8 @@ export function mountTodo(root: HTMLElement): () => void {
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
for (const p of state.projects) {
|
||||
const p = project();
|
||||
if (p) {
|
||||
void load(p.id);
|
||||
}
|
||||
};
|
||||
@@ -45,8 +51,7 @@ export function mountTodo(root: HTMLElement): () => void {
|
||||
render();
|
||||
try {
|
||||
const task = await post("/api/projects/{projectId}/todo/tasks", { path: { projectId: project.id }, body: { title: item.title, model: models[0] } });
|
||||
location.hash = `#/task/${task.id}`;
|
||||
openTranscript();
|
||||
focusTask(task.id);
|
||||
} catch (err) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
@@ -72,7 +77,14 @@ export function mountTodo(root: HTMLElement): () => void {
|
||||
}
|
||||
}),
|
||||
on("projects", refresh),
|
||||
onEach(["tasks", "sessions", "selection"], render),
|
||||
on("selection", () => {
|
||||
render();
|
||||
const p = project();
|
||||
if (p && !todos.has(p.id)) {
|
||||
void load(p.id);
|
||||
}
|
||||
}),
|
||||
onEach(["tasks", "sessions"], render),
|
||||
];
|
||||
|
||||
render();
|
||||
@@ -122,7 +134,14 @@ function projectBlock(project: Project, todo: Todo | string | undefined, startin
|
||||
const sess = currentSession(task.id);
|
||||
action = h(
|
||||
"a",
|
||||
{ class: "todo-task", href: `#/task/${task.id}`, onclick: openTranscript },
|
||||
{
|
||||
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",
|
||||
);
|
||||
|
||||
+44
-55
@@ -208,17 +208,6 @@ textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.inbox-count {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.inbox-count.urgent {
|
||||
background: var(--danger-tint);
|
||||
border-color: transparent;
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.connection {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -282,7 +271,6 @@ textarea {
|
||||
}
|
||||
|
||||
.pane-tabs button,
|
||||
.scope-tabs button,
|
||||
.range-tabs button,
|
||||
.modes button {
|
||||
border: none;
|
||||
@@ -297,14 +285,12 @@ textarea {
|
||||
}
|
||||
|
||||
.pane-tabs button:hover:not(:disabled),
|
||||
.scope-tabs button:hover:not(:disabled),
|
||||
.range-tabs button:hover:not(:disabled),
|
||||
.modes button:hover:not(:disabled) {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.pane-tabs button.active,
|
||||
.scope-tabs button.active,
|
||||
.range-tabs button.active,
|
||||
.modes button.active {
|
||||
color: var(--text);
|
||||
@@ -323,11 +309,6 @@ textarea {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tasks-view {
|
||||
overflow-y: auto;
|
||||
padding: 8px 16px 16px;
|
||||
}
|
||||
|
||||
.todo-view {
|
||||
overflow-y: auto;
|
||||
padding: 8px 16px 16px;
|
||||
@@ -1198,17 +1179,6 @@ pre.file-text .line::before {
|
||||
padding: 6px 16px 16px;
|
||||
}
|
||||
|
||||
.scope-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.scope-tabs button {
|
||||
padding: 6px 8px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.range-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
@@ -1260,28 +1230,6 @@ pre.file-text .line::before {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.task-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-group + .task-group {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.task-group h3 {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-group.blocked h3 a {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.context {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -1456,6 +1404,45 @@ pre.file-text .line::before {
|
||||
background: rgb(0 0 0 / 0.35);
|
||||
}
|
||||
|
||||
.modal.panel {
|
||||
width: min(860px, 92vw);
|
||||
height: min(80vh, 900px);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal.panel[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px 8px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.panel-head h2 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.panel-close {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
margin: 0 0 16px;
|
||||
white-space: pre-wrap;
|
||||
@@ -1529,6 +1516,8 @@ button.danger-primary {
|
||||
}
|
||||
|
||||
.settings-view {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 8px 16px 16px;
|
||||
}
|
||||
@@ -1575,19 +1564,19 @@ button.danger-primary {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.admin-view {
|
||||
.status-view {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 8px 16px 16px;
|
||||
}
|
||||
|
||||
.admin-section h2 {
|
||||
.status-section h2 {
|
||||
font-size: 13px;
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.admin-section h3 {
|
||||
.status-section h3 {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin: 12px 0 4px;
|
||||
|
||||
Reference in New Issue
Block a user