Files
byre/web/dist/state.js
T

292 lines
8.7 KiB
JavaScript
Raw Normal View History

import { get, put, subscribe } from "./api.js";
import { notifyInbox, notifyTurn } from "./notifications.js";
2026-10-16 07:04:24 -07:00
export const tabs = ["transcript", "changes", "files", "terminal", "inbox", "reviews", "project"];
export const state = {
me: undefined,
projects: [],
tasks: new Map(),
sessions: new Map(),
inbox: [],
selectedTaskId: undefined,
transcript: [],
2026-10-15 00:08:32 -07:00
pinned: undefined,
reviewed: undefined,
connected: false,
focus: { tabs: ["transcript"], tab: "transcript" },
leadSessions: [],
leadTranscript: [],
};
const listeners = new Map();
export function on(topic, fn) {
let set = listeners.get(topic);
if (!set) {
set = new Set();
listeners.set(topic, set);
}
set.add(fn);
return () => set.delete(fn);
}
export function onEach(topics, fn) {
const offs = topics.map((t) => on(t, fn));
return () => {
for (const off of offs) {
off();
}
};
}
export function activeTasks() {
return state.projects.flatMap((project) => (state.tasks.get(project.id) ?? []).filter((t) => t.status === "active").map((task) => ({ project, task })));
}
export function notify(topic, detail) {
for (const fn of listeners.get(topic) ?? []) {
fn(detail);
}
}
export function findTask(id) {
for (const tasks of state.tasks.values()) {
const task = tasks.find((t) => t.id === id);
if (task) {
return task;
}
}
return undefined;
}
export function currentSession(taskId) {
return state.sessions.get(taskId)?.findLast((s) => s.state !== "completed") ?? state.sessions.get(taskId)?.at(-1);
}
2026-10-16 08:37:24 -07:00
export function taskState(task) {
const s = currentSession(task.id)?.state;
return s === "idle" && task.reviewing ? "reviewing" : s;
}
export function currentLead() {
return state.leadSessions.findLast((s) => s.state !== "completed") ?? state.leadSessions.at(-1);
}
2026-10-15 00:08:32 -07:00
export function snapshots() {
return state.transcript.filter((e) => e.kind === "snapshot.created").map((e) => e.payload);
}
export function pinSnapshot(n) {
state.pinned = n;
notify("pin");
}
export function pickReview(n) {
state.reviewed = n;
notify("review");
}
export function showReview(n) {
pickReview(n);
focusTab("reviews");
}
export async function loadMe() {
state.me = await get("/api/me");
notify("me");
}
export async function loadProjects() {
state.projects = await get("/api/projects");
await Promise.all(state.projects.map((p) => loadTasks(p.id)));
notify("projects");
}
export async function loadTasks(projectId) {
const tasks = await get("/api/projects/{projectId}/tasks", { path: { projectId } });
state.tasks.set(projectId, tasks);
await Promise.all(tasks.filter((t) => t.status === "active").map((t) => loadSessions(t.id)));
notify("tasks");
}
export async function loadSessions(taskId) {
state.sessions.set(taskId, await get("/api/tasks/{taskId}/sessions", { path: { taskId } }));
notify("sessions", taskId);
}
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}` : f.project_id ? `#/project/${f.project_id}` : "#/";
if (location.hash !== hash && !(hash === "#/" && location.hash === "")) {
location.hash = hash;
}
notify("focus");
}
export function focusedProject() {
return state.projects.find((p) => p.id === state.focus.project_id);
}
export async function setFocus(change) {
const taskId = change.task_id === undefined ? state.focus.task_id : (change.task_id ?? undefined);
const projectId = taskId ? findTask(taskId)?.project_id : change.project_id === undefined ? state.focus.project_id : (change.project_id ?? undefined);
const next = { tabs: state.focus.tabs, tab: change.tab ?? state.focus.tab };
if (taskId) {
next.task_id = taskId;
}
if (projectId) {
next.project_id = projectId;
}
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 focusProject(projectId) {
setFocus({ task_id: null, project_id: projectId, tab: "project" }).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;
const visitsKey = "byre.visits";
export function lastVisits() {
return JSON.parse(localStorage.getItem(visitsKey) ?? "{}");
}
function markVisited(taskId) {
if (!taskId) {
return;
}
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
}
export function selectTask(taskId) {
markVisited(state.selectedTaskId);
markVisited(taskId);
closeTaskStream?.();
closeTaskStream = undefined;
taskFeed.reset();
state.selectedTaskId = taskId;
2026-10-15 00:08:32 -07:00
state.pinned = undefined;
state.reviewed = undefined;
notify("selection");
if (!taskId) {
return;
}
closeTaskStream = subscribe(0, { task_id: taskId }, taskFeed.push, () => undefined);
}
export function startLeadStream() {
leadFeed.reset();
subscribe(0, { lead: true }, leadFeed.push, () => undefined);
}
export function handleEvent(ev) {
2026-10-14 19:35:34 -07:00
if (ev.seq === 0) {
notify("event", ev);
2026-10-14 19:35:34 -07:00
return;
}
switch (ev.kind) {
case "user.updated":
if (ev.payload["login"] === state.me?.login) {
void loadMe();
}
break;
case "project.created":
void loadProjects();
break;
case "task.created":
case "task.merge_title":
case "task.merge_queued":
case "task.merge_unqueued":
2026-10-14 19:35:34 -07:00
case "task.merged":
case "task.discarded":
if (ev.project_id) {
void loadTasks(ev.project_id);
}
break;
case "session.created":
case "session.state":
2026-10-14 19:35:34 -07:00
case "session.model":
if (ev.owner) {
void loadLeadSessions();
}
else if (ev.task_id) {
void loadSessions(ev.task_id);
}
break;
case "inbox.created":
notifyInbox(ev.payload);
void loadInbox();
break;
case "inbox.updated":
void loadInbox();
break;
case "turn.ended":
notifyTurn(ev);
break;
case "review.started":
case "review.finished":
2026-10-16 08:37:24 -07:00
if (ev.project_id) {
void loadTasks(ev.project_id);
}
if (state.inbox.some((i) => i.task_id === ev.task_id && i.kind === "question")) {
void loadInbox();
}
break;
case "focus.updated":
if (ev.actor !== state.me?.login) {
applyFocus(ev.payload);
}
break;
}
2026-10-14 21:06:30 -07:00
notify("event", ev);
}
//# sourceMappingURL=state.js.map