// Spectra for VS Code: set Spectra up, chat with your agents, and give // them their briefs, without the command line (spectra-vscode spec). import * as vscode from "vscode"; import { Bridge } from "./bridge"; import { BriefFs, SCHEME, uriFor } from "./briefs"; import { openChannel } from "./chat"; import * as cli from "./cli"; import { Model } from "./model"; import * as setup from "./setup"; import * as views from "./views"; export async function activate(ctx: vscode.ExtensionContext): Promise { const log = vscode.window.createOutputChannel("Spectra"); const bridge = new Bridge(log); const model = new Model(bridge); const fsp = new BriefFs(bridge); ctx.subscriptions.push(log, bridge, model, vscode.workspace.registerFileSystemProvider(SCHEME, fsp, { isCaseSensitive: true })); for (const [id, view] of [ ["spectra.channels", new views.Channels(model)], ["spectra.agents", new views.Agents(model)], ["spectra.inbox", new views.Inbox(model)], ["spectra.briefs", new views.Briefs(model)], ] as const) { ctx.subscriptions.push(vscode.window.registerTreeDataProvider(id, view)); } const bar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); bar.command = "workbench.view.extension.spectra"; ctx.subscriptions.push(bar); const set = (key: string, v: boolean) => vscode.commands.executeCommand("setContext", key, v); let ready = false; // What is set up, as contexts the walkthrough and views follow. async function check(): Promise { const hasBinary = !!cli.binary(); await set("spectra.hasBinary", hasBinary); const st = hasBinary ? await cli.status().catch(() => undefined) : undefined; await set("spectra.hasIdentity", !!st?.identity); const nowReady = !!st?.identity && !!(st.server || st.local.initialised) && st.reachable; if (nowReady !== ready) { ready = nowReady; if (ready) model.start(); else model.stop(); } await set("spectra.ready", ready); if (!ready) { bar.text = "$(radio-tower) Spectra: set up"; bar.tooltip = st?.identity && (st.server || st.local.initialised) ? "Spectra is not answering" : "Set Spectra up"; bar.command = st?.identity && (st.server || st.local.initialised) ? "spectra.refresh" : "spectra.setup"; bar.show(); } } model.onDidChange(async () => { await set("spectra.folderIsProject", !!model.folderProject()); await set("spectra.hasAgent", model.agents.some((a) => !a.disabled)); fsp.changed(); if (model.error) { bar.text = "$(warning) Spectra"; bar.tooltip = model.error; } else { const online = model.agents.filter((a) => !a.disabled && a.presence !== "offline").length; const unread = model.inbox.length; bar.text = `$(radio-tower) ${online} agent${online === 1 ? "" : "s"} ยท ${unread} unread`; bar.tooltip = `Spectra: ${model.agents.filter((a) => !a.disabled).length} agents, ${online} online; ${unread} unread for you`; bar.command = unread ? "spectra.inbox.focus" : "workbench.view.extension.spectra"; } bar.show(); }); const hooks: setup.Hooks = { changed: async () => { bridge.restart(); if (ready) model.start(); await check(); await model.refresh(); }, projects: () => model.projects, agents: () => model.agents, folderProject: () => model.folderProject(), }; const radar = (page?: string) => { const bin = cli.binary(); if (!bin) return; const t = vscode.window.createTerminal({ name: "Spectra Radar", shellPath: bin, shellArgs: ["open", ...(page ? ["--page", page] : [])] }); t.show(true); }; const cmd = (id: string, f: (...a: never[]) => unknown) => ctx.subscriptions.push(vscode.commands.registerCommand(id, f)); cmd("spectra.setup", () => vscode.commands.executeCommand("workbench.action.openWalkthrough", `${ctx.extension.id}#spectra.setup`, false)); cmd("spectra.checkBinary", async () => { await setup.checkBinary(); await check(); }); cmd("spectra.createIdentity", () => setup.createIdentity(hooks)); cmd("spectra.setupHere", () => setup.setupHere(hooks)); cmd("spectra.connectServer", () => setup.connectServer(hooks)); cmd("spectra.registerFolder", () => setup.registerFolder(hooks)); cmd("spectra.linkProject", () => setup.linkProject(hooks)); cmd("spectra.addAgent", () => setup.addAgent(hooks)); cmd("spectra.renewAgent", (item?: vscode.TreeItem) => setup.renewAgent(hooks, typeof item?.label === "string" ? item.label : undefined)); cmd("spectra.openChannel", async (name?: string) => { const pick = name ?? (await vscode.window.showQuickPick(model.channels.map((c) => c.name), { placeHolder: "Which channel?" })); if (pick) openChannel(ctx, bridge, model, pick); }); cmd("spectra.openProjectChannel", () => openChannel(ctx, bridge, model, model.folderProject() ?? "general")); cmd("spectra.say", async () => { const channel = model.folderProject() ?? "general"; const body = await vscode.window.showInputBox({ prompt: `Say in #${channel}, as yourself` }); if (!body) return; try { await bridge.request("POST", `/api/v1/channels/${encodeURIComponent(channel)}/messages`, { body }); } catch (e) { void vscode.window.showErrorMessage(`Spectra: ${(e as Error).message}`); } }); cmd("spectra.editOverallBrief", () => vscode.window.showTextDocument(uriFor())); cmd("spectra.editProjectBrief", async (project?: string) => { const p = project ?? model.folderProject() ?? (await vscode.window.showQuickPick(model.projects.map((x) => x.name), { placeHolder: "Which project's brief?" })); if (p) await vscode.window.showTextDocument(uriFor(p)); }); cmd("spectra.openRadar", () => radar()); cmd("spectra.openConstellation", () => radar("constellation")); cmd("spectra.refresh", async () => { await check(); if (ready) await model.refresh(); }); await check(); if (!ready && !ctx.globalState.get("spectra.walkthroughShown")) { await ctx.globalState.update("spectra.walkthroughShown", true); void vscode.commands.executeCommand("spectra.setup"); } } export function deactivate(): void { // The bridge ends with its input, which closes when the extension host does. }