Add a per-user lead agent and make the right pane task-focused
This commit is contained in:
+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);
|
||||
|
||||
Reference in New Issue
Block a user