This commit is contained in:
Ian Gulliver
2026-10-15 22:21:20 -07:00
parent 84de97cec9
commit a1a4b83f39
22 changed files with 171 additions and 204 deletions
+3 -4
View File
@@ -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 { leadPane, taskPane } from "./panes.js";
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, notify, on, selectTask, setFocus, startLeadStream, state, tabs } from "./state.js";
import { bindShortcuts } from "./switcher.js";
function identityBanner(root: HTMLElement): void {
const render = () => {
@@ -49,7 +49,6 @@ function route(): void {
}
async function main(): Promise<void> {
const navbar = h("header", { class: "navbar" });
const banner = h("div", { class: "banner", hidden: true });
const status = await get("/api/admin/status");
@@ -61,8 +60,8 @@ async function main(): Promise<void> {
}
startLeadStream();
replace(document.body, navbar, banner, h("div", { class: "panes" }, leadPane(), taskPane()));
mountNavbar(navbar);
replace(document.body, banner, h("div", { class: "panes" }, leadPane(), taskPane()));
bindShortcuts();
identityBanner(banner);
addEventListener(
-22
View File
@@ -109,28 +109,6 @@ function modal(content: string | Node[], buttons: { label: string; value: string
});
}
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" }]);
}
+70 -4
View File
@@ -4,11 +4,13 @@ import { h, replace } from "./dom.js";
import { mountFiles } from "./files.js";
import { mountInbox } from "./inbox.js";
import { mountLead } from "./lead.js";
import { openSwitcher } from "./navbar.js";
import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
import { mountPreview } from "./preview.js";
import { mountProject } from "./project.js";
import { isReviewEvent, latestFindingCount, mountReviews } from "./reviews.js";
import { mountSettings } from "./settings.js";
import { mountStatus } from "./status.js";
import { openSwitcher } from "./switcher.js";
import { mountTranscript } from "./task.js";
import { mountTerminal } from "./terminal.js";
@@ -23,10 +25,74 @@ const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () =
project: { label: "Project", mount: mountProject },
};
type LeftTab = "lead" | "status" | "settings";
const leftViews: Record<Exclude<LeftTab, "lead">, { label: string; mount: (root: HTMLElement) => () => void }> = {
status: { label: "Status", mount: mountStatus },
settings: { label: "Settings", mount: mountSettings },
};
let showLeft: (tab: LeftTab) => void = () => {};
export function focusLead(): void {
showLeft("lead");
document.querySelector<HTMLTextAreaElement>(".view-lead .composer textarea")?.focus();
}
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));
const lead = h("div", { class: "view view-lead" });
mountLead(lead);
let other = h("div", { class: "view", hidden: true });
const buttons: Record<LeftTab, HTMLButtonElement> = {
lead: h("button", {}, "Lead"),
status: h("button", {}, leftViews.status.label),
settings: h("button", {}, leftViews.settings.label),
};
let shown: LeftTab | undefined;
let dispose: () => void = () => {};
showLeft = (tab) => {
if (tab === shown) {
return;
}
dispose();
dispose = () => {};
shown = tab;
for (const [t, button] of Object.entries(buttons)) {
button.classList.toggle("active", t === tab);
}
lead.hidden = tab !== "lead";
const root = h("div", { class: tab === "lead" ? "view" : `view view-${tab}`, hidden: tab === "lead" });
other.replaceWith(root);
other = root;
if (tab === "lead") {
return;
}
dispose = leftViews[tab].mount(root);
};
for (const [t, button] of Object.entries(buttons)) {
button.onclick = () => showLeft(t as LeftTab);
}
const conn = h("span", { class: "connection down", title: "connecting" });
const connLabel = h("span", { class: "connection-label" }, "reconnecting…");
const me = h("span", { class: "me" });
const renderConn = () => {
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", "connection"], renderConn);
renderConn();
showLeft("lead");
return h(
"section",
{ class: "pane" },
h("nav", { class: "pane-tabs" }, buttons.lead, buttons.status, buttons.settings, h("span", { class: "pane-status" }, connLabel, conn, me)),
h("div", { class: "pane-body" }, lead, other),
);
}
export function taskPane(): HTMLElement {
+2 -6
View File
@@ -1,5 +1,5 @@
import { put, type NotificationPrefs } from "./api.js";
import { errorText, h, openPanel, replace, showAlert } from "./dom.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { on, state } from "./state.js";
const kinds: { key: keyof NotificationPrefs; label: string }[] = [
@@ -10,11 +10,7 @@ const kinds: { key: keyof NotificationPrefs; label: string }[] = [
{ key: "rate_limits", label: "Rate limits" },
];
export function openSettings(): void {
openPanel("Settings", mountSettings);
}
function mountSettings(root: HTMLElement): () => void {
export function mountSettings(root: HTMLElement): () => void {
const device = h("div", { class: "push-device" });
const prefs = h("fieldset", { class: "push-prefs" });
+2 -6
View File
@@ -1,15 +1,11 @@
import { get, type AgentProcess, type ByreEvent, type Status, type TurnRef, type Zombie } from "./api.js";
import { ago, errorText, h, openPanel, replace } from "./dom.js";
import { ago, errorText, h, replace } from "./dom.js";
import { findTask, on, onEach, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]);
export function openStatus(): void {
openPanel("Status", mountStatus);
}
function mountStatus(root: HTMLElement): () => void {
export function mountStatus(root: HTMLElement): () => void {
let status: Status | undefined;
let error = "";
let inFlight = false;
+3 -25
View File
@@ -1,33 +1,11 @@
import { post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { openSettings } from "./settings.js";
import { focusLead } from "./panes.js";
import { activeTasks, currentSession, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, state } from "./state.js";
import { openStatus } from "./status.js";
import { confirmMerge } from "./task.js";
import { taskStyle } from "./taskcolor.js";
export function mountNavbar(root: HTMLElement): void {
const status = h("button", { class: "nav-link" }, "Status");
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" });
const render = () => {
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", "connection"], render);
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), h("span", { class: "spacer" }), status, settings, connLabel, conn, me);
render();
export function bindShortcuts(): void {
addEventListener("keydown", (e) => {
if (!(e.metaKey || e.ctrlKey) || document.querySelector("dialog[open]")) {
return;
@@ -40,7 +18,7 @@ export function mountNavbar(root: HTMLElement): void {
focusTab("project");
} else if (e.key === "l") {
e.preventDefault();
document.querySelector<HTMLTextAreaElement>(".view-lead .composer textarea")?.focus();
focusLead();
} else if (e.key === "s") {
e.preventDefault();
if (state.selectedTaskId) {