| 1 |
// Spectra for VS Code: set Spectra up, chat with your agents, and give |
| 2 |
// them their briefs, without the command line (spectra-vscode spec). |
| 3 |
import * as vscode from "vscode"; |
| 4 |
import { Bridge } from "./bridge"; |
| 5 |
import { BriefFs, SCHEME, uriFor } from "./briefs"; |
| 6 |
import { openChannel } from "./chat"; |
| 7 |
import * as cli from "./cli"; |
| 8 |
import { Model } from "./model"; |
| 9 |
import * as setup from "./setup"; |
| 10 |
import * as views from "./views"; |
| 11 |
|
| 12 |
export async function activate(ctx: vscode.ExtensionContext): Promise<void> { |
| 13 |
const log = vscode.window.createOutputChannel("Spectra"); |
| 14 |
const bridge = new Bridge(log); |
| 15 |
const model = new Model(bridge); |
| 16 |
const fsp = new BriefFs(bridge); |
| 17 |
ctx.subscriptions.push(log, bridge, model, vscode.workspace.registerFileSystemProvider(SCHEME, fsp, { isCaseSensitive: true })); |
| 18 |
|
| 19 |
for (const [id, view] of [ |
| 20 |
["spectra.channels", new views.Channels(model)], |
| 21 |
["spectra.agents", new views.Agents(model)], |
| 22 |
["spectra.inbox", new views.Inbox(model)], |
| 23 |
["spectra.briefs", new views.Briefs(model)], |
| 24 |
] as const) { |
| 25 |
ctx.subscriptions.push(vscode.window.registerTreeDataProvider(id, view)); |
| 26 |
} |
| 27 |
|
| 28 |
const bar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50); |
| 29 |
bar.command = "workbench.view.extension.spectra"; |
| 30 |
ctx.subscriptions.push(bar); |
| 31 |
|
| 32 |
const set = (key: string, v: boolean) => vscode.commands.executeCommand("setContext", key, v); |
| 33 |
let ready = false; |
| 34 |
|
| 35 |
// What is set up, as contexts the walkthrough and views follow. |
| 36 |
async function check(): Promise<void> { |
| 37 |
const hasBinary = !!cli.binary(); |
| 38 |
await set("spectra.hasBinary", hasBinary); |
| 39 |
const st = hasBinary ? await cli.status().catch(() => undefined) : undefined; |
| 40 |
await set("spectra.hasIdentity", !!st?.identity); |
| 41 |
const nowReady = !!st?.identity && !!(st.server || st.local.initialised) && st.reachable; |
| 42 |
if (nowReady !== ready) { |
| 43 |
ready = nowReady; |
| 44 |
if (ready) model.start(); |
| 45 |
else model.stop(); |
| 46 |
} |
| 47 |
await set("spectra.ready", ready); |
| 48 |
if (!ready) { |
| 49 |
bar.text = "$(radio-tower) Spectra: set up"; |
| 50 |
bar.tooltip = st?.identity && (st.server || st.local.initialised) ? "Spectra is not answering" : "Set Spectra up"; |
| 51 |
bar.command = st?.identity && (st.server || st.local.initialised) ? "spectra.refresh" : "spectra.setup"; |
| 52 |
bar.show(); |
| 53 |
} |
| 54 |
} |
| 55 |
|
| 56 |
model.onDidChange(async () => { |
| 57 |
await set("spectra.folderIsProject", !!model.folderProject()); |
| 58 |
await set("spectra.hasAgent", model.agents.some((a) => !a.disabled)); |
| 59 |
fsp.changed(); |
| 60 |
if (model.error) { |
| 61 |
bar.text = "$(warning) Spectra"; |
| 62 |
bar.tooltip = model.error; |
| 63 |
} else { |
| 64 |
const online = model.agents.filter((a) => !a.disabled && a.presence !== "offline").length; |
| 65 |
const unread = model.inbox.length; |
| 66 |
bar.text = `$(radio-tower) ${online} agent${online === 1 ? "" : "s"} · ${unread} unread`; |
| 67 |
bar.tooltip = `Spectra: ${model.agents.filter((a) => !a.disabled).length} agents, ${online} online; ${unread} unread for you`; |
| 68 |
bar.command = unread ? "spectra.inbox.focus" : "workbench.view.extension.spectra"; |
| 69 |
} |
| 70 |
bar.show(); |
| 71 |
}); |
| 72 |
|
| 73 |
const hooks: setup.Hooks = { |
| 74 |
changed: async () => { |
| 75 |
bridge.restart(); |
| 76 |
if (ready) model.start(); |
| 77 |
await check(); |
| 78 |
await model.refresh(); |
| 79 |
}, |
| 80 |
projects: () => model.projects, |
| 81 |
agents: () => model.agents, |
| 82 |
folderProject: () => model.folderProject(), |
| 83 |
}; |
| 84 |
|
| 85 |
const radar = (page?: string) => { |
| 86 |
const bin = cli.binary(); |
| 87 |
if (!bin) return; |
| 88 |
const t = vscode.window.createTerminal({ name: "Spectra Radar", shellPath: bin, shellArgs: ["open", ...(page ? ["--page", page] : [])] }); |
| 89 |
t.show(true); |
| 90 |
}; |
| 91 |
|
| 92 |
const cmd = (id: string, f: (...a: never[]) => unknown) => ctx.subscriptions.push(vscode.commands.registerCommand(id, f)); |
| 93 |
cmd("spectra.setup", () => vscode.commands.executeCommand("workbench.action.openWalkthrough", `${ctx.extension.id}#spectra.setup`, false)); |
| 94 |
cmd("spectra.checkBinary", async () => { |
| 95 |
await setup.checkBinary(); |
| 96 |
await check(); |
| 97 |
}); |
| 98 |
cmd("spectra.createIdentity", () => setup.createIdentity(hooks)); |
| 99 |
cmd("spectra.setupHere", () => setup.setupHere(hooks)); |
| 100 |
cmd("spectra.connectServer", () => setup.connectServer(hooks)); |
| 101 |
cmd("spectra.registerFolder", () => setup.registerFolder(hooks)); |
| 102 |
cmd("spectra.linkProject", () => setup.linkProject(hooks)); |
| 103 |
cmd("spectra.addAgent", () => setup.addAgent(hooks)); |
| 104 |
cmd("spectra.renewAgent", (item?: vscode.TreeItem) => setup.renewAgent(hooks, typeof item?.label === "string" ? item.label : undefined)); |
| 105 |
cmd("spectra.openChannel", async (name?: string) => { |
| 106 |
const pick = name ?? (await vscode.window.showQuickPick(model.channels.map((c) => c.name), { placeHolder: "Which channel?" })); |
| 107 |
if (pick) openChannel(ctx, bridge, model, pick); |
| 108 |
}); |
| 109 |
cmd("spectra.openProjectChannel", () => openChannel(ctx, bridge, model, model.folderProject() ?? "general")); |
| 110 |
cmd("spectra.say", async () => { |
| 111 |
const channel = model.folderProject() ?? "general"; |
| 112 |
const body = await vscode.window.showInputBox({ prompt: `Say in #${channel}, as yourself` }); |
| 113 |
if (!body) return; |
| 114 |
try { |
| 115 |
await bridge.request("POST", `/api/v1/channels/${encodeURIComponent(channel)}/messages`, { body }); |
| 116 |
} catch (e) { |
| 117 |
void vscode.window.showErrorMessage(`Spectra: ${(e as Error).message}`); |
| 118 |
} |
| 119 |
}); |
| 120 |
cmd("spectra.editOverallBrief", () => vscode.window.showTextDocument(uriFor())); |
| 121 |
cmd("spectra.editProjectBrief", async (project?: string) => { |
| 122 |
const p = project ?? model.folderProject() ?? (await vscode.window.showQuickPick(model.projects.map((x) => x.name), { placeHolder: "Which project's brief?" })); |
| 123 |
if (p) await vscode.window.showTextDocument(uriFor(p)); |
| 124 |
}); |
| 125 |
cmd("spectra.openRadar", () => radar()); |
| 126 |
cmd("spectra.openConstellation", () => radar("constellation")); |
| 127 |
cmd("spectra.refresh", async () => { |
| 128 |
await check(); |
| 129 |
if (ready) await model.refresh(); |
| 130 |
}); |
| 131 |
|
| 132 |
await check(); |
| 133 |
if (!ready && !ctx.globalState.get("spectra.walkthroughShown")) { |
| 134 |
await ctx.globalState.update("spectra.walkthroughShown", true); |
| 135 |
void vscode.commands.executeCommand("spectra.setup"); |
| 136 |
} |
| 137 |
} |
| 138 |
|
| 139 |
export function deactivate(): void { |
| 140 |
// The bridge ends with its input, which closes when the extension host does. |
| 141 |
} |