// The side bar: Channels, Agents, Inbox and Briefs. import * as vscode from "vscode"; import { Model } from "./model"; import { when } from "./pure"; abstract class View implements vscode.TreeDataProvider { private readonly emitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.emitter.event; constructor(protected readonly model: Model) { model.onDidChange(() => this.emitter.fire()); } getTreeItem(i: vscode.TreeItem): vscode.TreeItem { return i; } getChildren(): vscode.TreeItem[] { if (this.model.error) { const i = new vscode.TreeItem("Spectra is not answering"); i.description = this.model.error; i.tooltip = this.model.error; i.iconPath = new vscode.ThemeIcon("warning"); i.command = { command: "spectra.refresh", title: "Refresh" }; return [i]; } return this.items(); } protected abstract items(): vscode.TreeItem[]; } const PRESENCE: Record = { online: "charts.green", busy: "charts.purple", idle: "charts.yellow", blocked: "charts.orange", paused: "charts.yellow", offline: "disabledForeground" }; export class Agents extends View { protected items(): vscode.TreeItem[] { return this.model.agents .filter((a) => !a.disabled) .map((a) => { const i = new vscode.TreeItem(a.name); const also = (a.channels ?? []).map((c) => `#${c}`); i.description = `${a.presence} · #${a.project}${also.length ? ` +${also.join(" ")}` : ""}${a.status ? ` · ${a.status}` : ""}`; const expires = a.delegation_expires ? `\nMay act for you until ${new Date(a.delegation_expires).toLocaleString()}` : ""; const state = a.state && a.state !== "ok" ? `\n⚠ ${a.state}` : ""; const may = also.length ? `\nMay also post in ${also.join(", ")}` : ""; i.tooltip = `${a.display}\n${a.kind}${expires}${may}${state}`; i.iconPath = new vscode.ThemeIcon(a.state && a.state !== "ok" ? "warning" : "circle-filled", new vscode.ThemeColor(PRESENCE[a.presence] ?? "disabledForeground")); i.contextValue = "agent"; i.id = `agent:${a.name}`; return i; }); } } export class Channels extends View { protected items(): vscode.TreeItem[] { const mine = this.model.folderProject(); const rows = [...this.model.channels].filter((c) => c.kind !== "system").sort((a, b) => Number(b.name === mine) - Number(a.name === mine)); return rows.map((c) => { const i = new vscode.TreeItem(c.display); const own = this.model.agents.filter((a) => !a.disabled && a.project === c.name).map((a) => a.name); const guests = this.model.agents.filter((a) => !a.disabled && (a.channels ?? []).includes(c.name)).map((a) => a.name); i.description = c.purpose ?? undefined; const who = c.kind === "project" ? `\nAgents who may post: ${[...own, ...guests].join(", ") || "none yet"}` : ""; i.tooltip = `${c.display}${c.purpose ? `\n${c.purpose}` : ""}\n${c.messages} message(s)${who}`; i.contextValue = c.kind === "project" ? "projectChannel" : "channel"; i.iconPath = new vscode.ThemeIcon(c.kind === "direct" ? "mention" : c.name === mine ? "star-full" : "comment-discussion"); i.command = { command: "spectra.openChannel", title: "Open", arguments: [c.name] }; i.id = `channel:${c.name}`; return i; }); } } export class Inbox extends View { protected items(): vscode.TreeItem[] { if (!this.model.inbox.length) { const i = new vscode.TreeItem("Nothing unread"); i.iconPath = new vscode.ThemeIcon("check"); return [i]; } return this.model.inbox.map((m) => { const i = new vscode.TreeItem(m.summary.replace(/\s+/g, " ").slice(0, 120)); i.description = `${m.from_display || m.from} · ${m.channel} · ${when(m.at_ms)}`; i.iconPath = new vscode.ThemeIcon(m.kind === "dm" ? "mail" : "mention"); i.command = { command: "spectra.openChannel", title: "Open", arguments: [m.channel.replace(/^#/, "")] }; return i; }); } } export class Briefs extends View { protected items(): vscode.TreeItem[] { const overall = new vscode.TreeItem("Overall brief"); overall.description = this.model.briefs.overall ? firstLine(this.model.briefs.overall) : "not set"; overall.iconPath = new vscode.ThemeIcon("globe"); overall.command = { command: "spectra.editOverallBrief", title: "Edit" }; const items = [overall]; for (const p of this.model.projects) { const b = this.model.briefs.projects.find((x) => x.project === p.name); const i = new vscode.TreeItem(p.name); i.description = b ? firstLine(b.text) : "not set"; i.iconPath = new vscode.ThemeIcon("note"); i.command = { command: "spectra.editProjectBrief", title: "Edit", arguments: [p.name] }; items.push(i); } return items; } } function firstLine(s: string): string { return s.trim().split("\n")[0].slice(0, 80); }