import type { AgentProgress, SubagentLifecyclePayload, SubagentProgressPayload } from "../task";
import { TASK_SUBAGENT_LIFECYCLE_CHANNEL, TASK_SUBAGENT_PROGRESS_CHANNEL } from "../task";
import type { EventBus } from "../utils/event-bus";
export interface ObservableSession {
id: string;
kind: "main" | "subagent";
label: string;
agent?: string;
description?: string;
status: "active" | "completed" | "failed" | "aborted";
sessionFile?: string;
lastUpdate: number;
progress?: AgentProgress;
}
const STATUS_MAP: Record<string, ObservableSession["status"]> = {
started: "active",
completed: "completed",
failed: "failed",
aborted: "aborted",
};
export class SessionObserverRegistry {
#sessions = new Map<string, ObservableSession>();
#listeners = new Set<() => void>();
#eventBusUnsubscribers: Array<() => void> = [];
onChange(cb: () => void): () => void {
this.#listeners.add(cb);
return () => this.#listeners.delete(cb);
}
#notifyListeners(): void {
for (const cb of this.#listeners) cb();
}
setMainSession(sessionFile?: string): void {
const existing = this.#sessions.get("main");
this.#sessions.set("main", {
id: "main",
kind: "main",
label: "Main Session",
status: "active",
sessionFile: sessionFile ?? existing?.sessionFile,
lastUpdate: Date.now(),
});
this.#notifyListeners();
}
getSessions(): ObservableSession[] {
const sessions = [...this.#sessions.values()];
sessions.sort((a, b) => {
if (a.kind === "main") return -1;
if (b.kind === "main") return 1;
return a.lastUpdate - b.lastUpdate;
});
return sessions;
}
getActiveSubagentCount(): number {
let count = 0;
for (const s of this.#sessions.values()) {
if (s.kind === "subagent" && s.status === "active") count++;
}
return count;
}
resetSessions(): void {
this.#sessions.clear();
this.#notifyListeners();
}
dispose(): void {
for (const unsub of this.#eventBusUnsubscribers) unsub();
this.#eventBusUnsubscribers = [];
this.#sessions.clear();
this.#listeners.clear();
}
subscribeToEventBus(eventBus: EventBus): void {
for (const unsub of this.#eventBusUnsubscribers) unsub();
this.#eventBusUnsubscribers = [];
this.#eventBusUnsubscribers.push(
eventBus.on(TASK_SUBAGENT_LIFECYCLE_CHANNEL, data => {
const payload = data as SubagentLifecyclePayload;
const status = STATUS_MAP[payload.status];
if (!status) return;
const existing = this.#sessions.get(payload.id);
if (existing) {
existing.status = status;
existing.lastUpdate = Date.now();
if (payload.description) existing.description = payload.description;
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
} else {
this.#sessions.set(payload.id, {
id: payload.id,
kind: "subagent",
label: payload.description ?? `Subagent #${payload.index}`,
agent: payload.agent,
description: payload.description,
status,
sessionFile: payload.sessionFile,
lastUpdate: Date.now(),
});
}
this.#notifyListeners();
}),
);
this.#eventBusUnsubscribers.push(
eventBus.on(TASK_SUBAGENT_PROGRESS_CHANNEL, data => {
const payload = data as SubagentProgressPayload;
const progress = payload.progress;
const id = progress.id;
const existing = this.#sessions.get(id);
if (existing) {
existing.lastUpdate = Date.now();
existing.progress = progress;
if (progress.description) existing.description = progress.description;
if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
} else {
this.#sessions.set(id, {
id,
kind: "subagent",
label: progress.description ?? `Subagent #${payload.index}`,
agent: payload.agent,
description: progress.description,
status: "active",
sessionFile: payload.sessionFile,
lastUpdate: Date.now(),
progress,
});
}
this.#notifyListeners();
}),
);
}
}