OXYGENHUB
◆ main spectra-vscode / src / extension.ts
6.1 KiB · last changed 0.3.0: choose which channels an agent may post in, from the Agents and Channels views 1 day ago source blame history raw
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 cmd = (id: string, f: (...a: never[]) => unknown) => ctx.subscriptions.push(vscode.commands.registerCommand(id, f));
86
  cmd("spectra.setup", () => vscode.commands.executeCommand("workbench.action.openWalkthrough", `${ctx.extension.id}#spectra.setup`, false));
87
  cmd("spectra.checkBinary", async () => {
88
    await setup.checkBinary();
89
    await check();
90
  });
91
  cmd("spectra.createIdentity", () => setup.createIdentity(hooks));
92
  cmd("spectra.setupHere", () => setup.setupHere(hooks));
93
  cmd("spectra.connectServer", () => setup.connectServer(hooks));
94
  cmd("spectra.registerFolder", () => setup.registerFolder(hooks));
95
  cmd("spectra.linkProject", () => setup.linkProject(hooks));
96
  cmd("spectra.addAgent", () => setup.addAgent(hooks));
97
  cmd("spectra.agentChannels", (item?: vscode.TreeItem) => setup.agentChannels(hooks, typeof item?.label === "string" ? item.label : undefined));
98
  cmd("spectra.channelAgents", (item?: vscode.TreeItem) =>
99
    setup.channelAgents(hooks, typeof item?.label === "string" ? item.label.replace(/^#/, "") : undefined),
100
  );
101
  cmd("spectra.renewAgent", (item?: vscode.TreeItem) => setup.renewAgent(hooks, typeof item?.label === "string" ? item.label : undefined));
102
  cmd("spectra.openChannel", async (name?: string) => {
103
    const pick = name ?? (await vscode.window.showQuickPick(model.channels.map((c) => c.name), { placeHolder: "Which channel?" }));
104
    if (pick) openChannel(ctx, bridge, model, pick);
105
  });
106
  cmd("spectra.openProjectChannel", () => openChannel(ctx, bridge, model, model.folderProject() ?? "general"));
107
  cmd("spectra.say", async () => {
108
    const channel = model.folderProject() ?? "general";
109
    const body = await vscode.window.showInputBox({ prompt: `Say in #${channel}, as yourself` });
110
    if (!body) return;
111
    try {
112
      await bridge.request("POST", `/api/v1/channels/${encodeURIComponent(channel)}/messages`, { body });
113
    } catch (e) {
114
      void vscode.window.showErrorMessage(`Spectra: ${(e as Error).message}`);
115
    }
116
  });
117
  cmd("spectra.editOverallBrief", () => vscode.window.showTextDocument(uriFor()));
118
  cmd("spectra.editProjectBrief", async (project?: string) => {
119
    const p = project ?? model.folderProject() ?? (await vscode.window.showQuickPick(model.projects.map((x) => x.name), { placeHolder: "Which project's brief?" }));
120
    if (p) await vscode.window.showTextDocument(uriFor(p));
121
  });
122
  cmd("spectra.refresh", async () => {
123
    await check();
124
    if (ready) await model.refresh();
125
  });
126
127
  await check();
128
  if (!ready && !ctx.globalState.get("spectra.walkthroughShown")) {
129
    await ctx.globalState.update("spectra.walkthroughShown", true);
130
    void vscode.commands.executeCommand("spectra.setup");
131
  }
132
}
133
134
export function deactivate(): void {
135
  // The bridge ends with its input, which closes when the extension host does.
136
}