OXYGENHUB
# 06699bcc6e2e spectra-vscode / src / extension.ts
6.1 KiB · last changed Spectra for VS Code 0.1: set up and chat without the command line (#6) 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 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
}