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"; import { notifyInbox, notifyTurn } from "./notifications.js"; export type Topic = | "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event" | "pin" | "review" | "focus" | "lead.sessions" | "lead.transcript" | "lead.stream" | "ready" | "lead.ready" | "summaries" | "jump"; export const tabs: TaskTab[] = ["transcript", "changes", "files", "terminal", "inbox", "reviews", "workflow", "project"]; export const state = { me: undefined as User | undefined, projects: [] as Project[], tasks: new Map(), sessions: new Map(), inbox: [] as InboxItem[], selectedTaskId: undefined as string | undefined, transcript: [] as ByreEvent[], pinned: undefined as number | undefined, reviewed: undefined as number | undefined, connected: false, focus: { tabs: ["transcript"], tab: "transcript" } as Focus, leadSessions: [] as AgentSession[], leadTranscript: [] as ByreEvent[], jump: undefined as { taskId: string; seq: number } | undefined, summaryReads: {} as Record, }; const listeners = new Map void>>(); export function on(topic: Topic, fn: (detail?: unknown) => void): () => void { 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: Topic[], fn: (detail?: unknown) => void): () => void { const offs = topics.map((t) => on(t, fn)); return () => { for (const off of offs) { off(); } }; } export function activeTasks(): { project: Project; task: Task }[] { return state.projects.flatMap((project) => (state.tasks.get(project.id) ?? []).filter((t) => t.status === "active").map((task) => ({ project, task }))); } export function notify(topic: Topic, detail?: unknown): void { for (const fn of listeners.get(topic) ?? []) { fn(detail); } } export function findTask(id: string): Task | undefined { 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: string): AgentSession | undefined { return state.sessions.get(taskId)?.findLast((s) => s.state !== "completed") ?? state.sessions.get(taskId)?.at(-1); } const workingTitles: Partial> = { running: "The agent is mid-turn", review_pending: "A review of the turn's work is about to start", reviewing: "A review is running", discarding: "Discarding", }; export type TaskState = { kind: "answer" | "merging" | "held" | "error" | "working" | "idle"; label: string; title: string }; export function taskState(task: Task): TaskState { const open = state.inbox.filter((item) => item.task_id === task.id); if (open.some((item) => item.kind === "question")) { return { kind: "answer", label: "Needs your answer", title: "The agent asked a question in the inbox" }; } if (task.merging) { return { kind: "merging", label: "Merging", title: "The merge turn is running" }; } if (task.merging_into_parent) { return { kind: "merging", label: "Merging", title: "Waiting for the parent task's agent to merge it" }; } if (task.merge_queued_at !== undefined) { return { kind: "held", label: "Merge held", title: "Waiting for the merge in progress in this project to finish" }; } if (open.length > 0) { return { kind: "error", label: "Error", title: "The agent's session failed or hit a rate limit; see the inbox" }; } const working = workingTitles[task.state]; if (working !== undefined) { return { kind: "working", label: "Working", title: working }; } return { kind: "idle", label: "Idle", title: "Waiting for a message" }; } 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); } export function pinSnapshot(n: number | undefined): void { state.pinned = n; notify("pin"); } export function pickReview(n: number): void { state.reviewed = n; notify("review"); } export function showReview(n: number): void { pickReview(n); focusTab("reviews"); } export async function loadMe(): Promise { state.me = await get("/api/me"); notify("me"); } export async function loadProjects(): Promise { state.projects = await get("/api/projects"); await Promise.all(state.projects.map((p) => loadTasks(p.id))); notify("projects"); } const requestSeqs = new Map(); function claimRequest(key: string): () => boolean { const seq = (requestSeqs.get(key) ?? 0) + 1; requestSeqs.set(key, seq); return () => requestSeqs.get(key) === seq; } export async function loadTasks(projectId: string): Promise { const latest = claimRequest(`tasks:${projectId}`); const tasks = await get("/api/projects/{projectId}/tasks", { path: { projectId } }); if (!latest()) { return; } state.tasks.set(projectId, tasks); for (const t of tasks) { if (t.status === "active") { taskFeed(t.id); } } dropClosedFeeds(); await Promise.all(tasks.filter((t) => t.status === "active").map((t) => loadSessions(t.id))); notify("tasks"); } export async function loadSessions(taskId: string): Promise { const latest = claimRequest(`sessions:${taskId}`); const sessions = await get("/api/tasks/{taskId}/sessions", { path: { taskId } }); if (!latest()) { return; } state.sessions.set(taskId, sessions); notify("sessions", taskId); } export async function loadInbox(): Promise { const latest = claimRequest("inbox"); const inbox = await get("/api/inbox"); if (!latest()) { return; } state.inbox = inbox; notify("inbox"); } export async function loadLeadSessions(): Promise { state.leadSessions = await get("/api/me/lead/sessions"); notify("lead.sessions"); } export async function loadFocus(): Promise { const latest = claimRequest("focus"); const focus = await get("/api/me/focus"); if (!latest()) { return; } applyFocus(focus); } export function applyFocus(f: Focus): void { 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(): Project | undefined { return state.projects.find((p) => p.id === state.focus.project_id); } export async function setFocus(change: { task_id?: string | null; project_id?: string | null; tab?: TaskTab }): Promise { 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: Focus = { tabs: state.focus.tabs, tab: change.tab ?? state.focus.tab }; if (taskId) { next.task_id = taskId; } if (projectId) { next.project_id = projectId; } const latest = claimRequest("focus"); applyFocus(next); const focus = await put("/api/me/focus", { body: next }); if (!latest()) { return; } applyFocus(focus); } export function focusTask(taskId: string, tab: TaskTab = "transcript"): void { setFocus({ task_id: taskId, tab }).catch((err: unknown) => console.error("set focus", err)); } export function focusProject(projectId: string): void { setFocus({ task_id: null, project_id: projectId, tab: "project" }).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 { readonly events: ByreEvent[] = []; ready = false; private last = 0; private buffer: ByreEvent[] = []; private timer = 0; constructor( private readonly transcriptTopic: Topic, private readonly streamTopic: Topic, private readonly readyTopic: Topic, private readonly shown: () => boolean, ) {} readonly push = (ev: ByreEvent): void => { if (ev.seq === 0 && (!this.ready || !this.shown())) { return; } this.buffer.push(ev); if (this.ready && !this.timer) { this.timer = setTimeout(() => this.flush(), 0); } }; readonly load = (history: ByreEvent[]): void => { if (this.ready) { return; } this.ready = true; this.buffer = [...history, ...this.buffer]; this.flush(); if (this.shown()) { notify(this.readyTopic); } }; private flush(): void { clearTimeout(this.timer); 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 = []; if (this.shown()) { notify(this.streamTopic, ev); } } this.append(stored); } private append(events: ByreEvent[]): void { const fresh: ByreEvent[] = []; for (const ev of events) { if (ev.seq > this.last) { fresh.push(ev); this.last = ev.seq; } } if (fresh.length === 0) { return; } this.events.push(...fresh); if (this.shown()) { notify(this.transcriptTopic, fresh); } } } const historyPage = 1000; const taskFeeds = new Map(); const leadFeed = new Feed("lead.transcript", "lead.stream", "lead.ready", () => true); state.leadTranscript = leadFeed.events; async function loadHistory(taskId: string, feed: Feed): Promise { const events: ByreEvent[] = []; for (;;) { const page = await get("/api/tasks/{taskId}/events", { path: { taskId }, query: { after: events.at(-1)?.seq ?? 0, limit: historyPage } }); events.push(...page); if (page.length < historyPage) { break; } } feed.load(events); } const historyLoads = new Set(); function taskFeed(taskId: string): Feed { let feed = taskFeeds.get(taskId); if (!feed) { feed = new Feed("transcript", "stream", "ready", () => state.selectedTaskId === taskId); taskFeeds.set(taskId, feed); } if (feed.ready || historyLoads.has(feed)) { return feed; } const loading = feed; historyLoads.add(loading); loadHistory(taskId, loading) .catch((err: unknown) => console.error("load task history", taskId, err)) .finally(() => historyLoads.delete(loading)); return feed; } function dropClosedFeeds(): void { for (const taskId of taskFeeds.keys()) { const task = findTask(taskId); if (taskId !== state.selectedTaskId && task && task.status !== "active") { taskFeeds.delete(taskId); } } } export function transcriptLoading(): boolean { return state.selectedTaskId !== undefined && taskFeeds.get(state.selectedTaskId)?.ready !== true; } export function leadLoading(): boolean { return !leadFeed.ready; } const visitsKey = "byre.visits"; export function lastVisits(): Record { return JSON.parse(localStorage.getItem(visitsKey) ?? "{}") as Record; } function markVisited(taskId: string | undefined): void { if (!taskId) { return; } localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() })); } export async function loadSummaryReads(): Promise { state.summaryReads = await get("/api/me/summary-reads"); notify("summaries"); } export function unreadSummary(task: Task): boolean { return task.workflow === undefined && task.summary_seq > (state.summaryReads[task.id] ?? 0); } function noteSummaryRead(taskId: string, seq: number): boolean { if ((state.summaryReads[taskId] ?? 0) >= seq) { return false; } state.summaryReads[taskId] = seq; notify("summaries"); return true; } export function markSummaryRead(taskId: string, seq: number): void { if (!noteSummaryRead(taskId, seq)) { return; } put("/api/tasks/{taskId}/summary-read", { path: { taskId }, body: { seq } }).catch((err: unknown) => console.error("mark summary read", err)); } export function openSummary(task: Task): void { state.jump = { taskId: task.id, seq: task.summary_seq }; focusTask(task.id, "transcript"); notify("jump"); } export function selectTask(taskId: string | undefined): void { markVisited(state.selectedTaskId); markVisited(taskId); state.selectedTaskId = taskId; state.transcript = taskId ? taskFeed(taskId).events : []; state.pinned = undefined; state.reviewed = undefined; dropClosedFeeds(); notify("selection"); } export function startLeadStream(): void { subscribe(0, { lead: true }, leadFeed.push, () => undefined, () => leadFeed.load([])); } export function handleEvent(ev: ByreEvent): void { if (ev.task_id) { taskFeeds.get(ev.task_id)?.push(ev); } if (ev.seq === 0) { notify("event", ev); 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": case "task.parent_merge_started": case "task.parent_merge_stopped": case "task.merge_started": case "task.merge_ended": case "task.discard_started": case "task.merged": case "task.discarded": case "task.state": if (ev.project_id) { void loadTasks(ev.project_id); } break; case "session.created": case "session.state": 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 as unknown as InboxItem); void loadInbox(); break; case "inbox.updated": void loadInbox(); break; case "turn.ended": { notifyTurn(ev); const task = ev.payload["summary"] === true && ev.task_id ? findTask(ev.task_id) : undefined; if (task && ev.seq > task.summary_seq) { task.summary_seq = ev.seq; notify("summaries"); } break; } case "summary.read": noteSummaryRead(String(ev.payload["task_id"]), Number(ev.payload["seq"])); break; case "focus.updated": if (ev.actor !== state.me?.login) { claimRequest("focus"); applyFocus(ev.payload as unknown as Focus); } break; } notify("event", ev); }