OXYGENHUB

0.3.0: choose which channels an agent may post in, from the Agents and Channels views 94e8677a5439

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key cba6caee8261
When
· 2026-11-03 20:42 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
94e8677a54390ce2e044c133437df4469bc97789ca2e57217610e2026cb32f94
Depends on
5482605b3353 ff39a5e808da
Depended on by
0bfe88762caa
In bookmarks
main
Receipts
✔ check passed in 4.3 s · signed by plex-1 job 14863 · download
✔ package passed in 3.8 s · signed by plex-1 job 14864 · download
Edits
13 atoms, 0 tree operations, 6 files

Browse the tree at this change

modified CHANGELOG.md +3 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
## 0.3.0
6 +
7 +
- **Added: who may post where.** Right-click an agent and choose **Channels…** to tick the other projects' channels it may post in; right-click a project's channel and choose **Agents…** to tick the agents from other projects who may post there. Each change is signed into Spectra as yours (`spectra agent allow`). The Agents view shows an agent's extra channels, and a channel's tooltip lists every agent who may post in it.
5 8
## 0.2.0
6 9
7 10
- **Removed: Open Radar and Open Constellation.** Spectra no longer has a web interface: the editor is its local view, and the website is SpectraHub.
…
19 unchanged lines
modified package.json +31 −1
…
1 unchanged lines
2 2
  "name": "spectra-vscode",
3 3
  "displayName": "Spectra",
4 4
  "description": "Spectra in the editor: set up your AI agents, chat with them, and give them their briefs, without the command line",
5 -
  "version": "0.2.0",
5 +
  "version": "0.3.0",
6 6
  "publisher": "andymccall",
7 7
  "license": "MIT OR Apache-2.0",
8 8
  "repository": {
…
220 unchanged lines
229 229
        "title": "Refresh",
230 230
        "category": "Spectra",
231 231
        "icon": "$(refresh)"
232 +
      },
233 +
      {
234 +
        "command": "spectra.agentChannels",
235 +
        "title": "Channels\u2026",
236 +
        "category": "Spectra"
237 +
      },
238 +
      {
239 +
        "command": "spectra.channelAgents",
240 +
        "title": "Agents\u2026",
241 +
        "category": "Spectra"
232 242
      }
233 243
    ],
234 244
    "menus": {
…
13 unchanged lines
248 258
        {
249 259
          "command": "spectra.renewAgent",
250 260
          "when": "view == spectra.agents && viewItem == agent"
261 +
        },
262 +
        {
263 +
          "command": "spectra.agentChannels",
264 +
          "when": "view == spectra.agents && viewItem == agent",
265 +
          "group": "1_access"
266 +
        },
267 +
        {
268 +
          "command": "spectra.channelAgents",
269 +
          "when": "view == spectra.channels && viewItem == projectChannel",
270 +
          "group": "1_access"
271 +
        }
272 +
      ],
273 +
      "commandPalette": [
274 +
        {
275 +
          "command": "spectra.agentChannels",
276 +
          "when": "spectra.ready"
277 +
        },
278 +
        {
279 +
          "command": "spectra.channelAgents",
280 +
          "when": "spectra.ready"
251 281
        }
252 282
      ]
253 283
    },
…
36 unchanged lines
modified src/extension.ts +4 −0
…
93 unchanged lines
94 94
  cmd("spectra.registerFolder", () => setup.registerFolder(hooks));
95 95
  cmd("spectra.linkProject", () => setup.linkProject(hooks));
96 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 +
  );
97 101
  cmd("spectra.renewAgent", (item?: vscode.TreeItem) => setup.renewAgent(hooks, typeof item?.label === "string" ? item.label : undefined));
98 102
  cmd("spectra.openChannel", async (name?: string) => {
99 103
    const pick = name ?? (await vscode.window.showQuickPick(model.channels.map((c) => c.name), { placeHolder: "Which channel?" }));
…
33 unchanged lines
modified src/setup.ts +68 −0
…
328 unchanged lines
329 329
    await fail(e);
330 330
  }
331 331
}
332 +
333 +
/** Run `spectra agent allow` for each change; report what failed. */
334 +
async function grant(h: Hooks, changes: { agent: string; channel: string; allow: boolean }[]): Promise<void> {
335 +
  const failed: string[] = [];
336 +
  for (const c of changes) {
337 +
    try {
338 +
      await cli.ok(["agent", "allow", c.agent, "--channel", c.channel, ...(c.allow ? [] : ["--remove"])]);
339 +
    } catch (e) {
340 +
      failed.push(`${c.agent} in #${c.channel}: ${(e as Error).message}`);
341 +
    }
342 +
  }
343 +
  await h.changed();
344 +
  if (failed.length) void vscode.window.showErrorMessage(`Spectra: not changed: ${failed.join("; ")}`);
345 +
  else if (changes.length) void vscode.window.setStatusBarMessage(`$(check) ${changes.length} channel permission${changes.length === 1 ? "" : "s"} signed into Spectra`, 5000);
346 +
}
347 +
348 +
/** Which other projects' channels an agent may post in. */
349 +
export async function agentChannels(h: Hooks, agent?: string): Promise<void> {
350 +
  const name = agent ?? (await vscode.window.showQuickPick(h.agents().filter((a) => !a.disabled).map((a) => a.name), { placeHolder: "Which agent?" }));
351 +
  const a = h.agents().find((x) => x.name === name);
352 +
  if (!a) return;
353 +
  const now = new Set(a.channels ?? []);
354 +
  const items = h
355 +
    .projects()
356 +
    .filter((p) => p.name !== a.project)
357 +
    .map((p) => ({ label: `#${p.name}`, description: p.description || undefined, picked: now.has(p.name), channel: p.name }));
358 +
  if (!items.length) {
359 +
    void vscode.window.showInformationMessage(`There are no other projects for ${a.name} to post in.`);
360 +
    return;
361 +
  }
362 +
  const picked = await vscode.window.showQuickPick(items, {
363 +
    canPickMany: true,
364 +
    title: `Channels ${a.name} may post in`,
365 +
    placeHolder: `Besides #${a.project} and #general. Untick to take a channel back.`,
366 +
  });
367 +
  if (!picked) return;
368 +
  const want = new Set(picked.map((p) => p.channel));
369 +
  const changes = items
370 +
    .filter((i) => want.has(i.channel) !== now.has(i.channel))
371 +
    .map((i) => ({ agent: a.name, channel: i.channel, allow: want.has(i.channel) }));
372 +
  await grant(h, changes);
373 +
}
374 +
375 +
/** Which agents from other projects may post in a project's channel. */
376 +
export async function channelAgents(h: Hooks, channel?: string): Promise<void> {
377 +
  const name = (channel ?? (await vscode.window.showQuickPick(h.projects().map((p) => p.name), { placeHolder: "Which project's channel?" })))?.replace(/^#/, "");
378 +
  if (!name) return;
379 +
  const items = h
380 +
    .agents()
381 +
    .filter((a) => !a.disabled && a.project !== name)
382 +
    .map((a) => ({ label: a.name, description: `${a.kind} · #${a.project}`, picked: (a.channels ?? []).includes(name), agent: a.name }));
383 +
  if (!items.length) {
384 +
    void vscode.window.showInformationMessage(`Every agent already works on ${name}.`);
385 +
    return;
386 +
  }
387 +
  const own = h.agents().filter((a) => !a.disabled && a.project === name).map((a) => a.name);
388 +
  const picked = await vscode.window.showQuickPick(items, {
389 +
    canPickMany: true,
390 +
    title: `Agents who may post in #${name}`,
391 +
    placeHolder: `${own.length ? `${own.join(", ")} always may. ` : ""}Tick agents from other projects; untick to take it back.`,
392 +
  });
393 +
  if (!picked) return;
394 +
  const want = new Set(picked.map((p) => p.agent));
395 +
  const changes = items
396 +
    .filter((i) => want.has(i.agent) !== i.picked)
397 +
    .map((i) => ({ agent: i.agent, channel: name, allow: want.has(i.agent) }));
398 +
  await grant(h, changes);
399 +
}
modified src/spectra-api.d.ts +1 −0
…
14 unchanged lines
15 15
};
16 16
17 17
export type Agent = {
18 +
  channels?: string[];
18 19
  delegation_expires?: number | null;
19 20
  disabled: boolean;
20 21
  display: string;
…
130 unchanged lines
modified src/views.ts +9 −3
…
33 unchanged lines
34 34
      .filter((a) => !a.disabled)
35 35
      .map((a) => {
36 36
        const i = new vscode.TreeItem(a.name);
37 -
        i.description = `${a.presence} · #${a.project}${a.status ? ` · ${a.status}` : ""}`;
37 +
        const also = (a.channels ?? []).map((c) => `#${c}`);
38 +
        i.description = `${a.presence} · #${a.project}${also.length ? ` +${also.join(" ")}` : ""}${a.status ? ` · ${a.status}` : ""}`;
38 39
        const expires = a.delegation_expires ? `\nMay act for you until ${new Date(a.delegation_expires).toLocaleString()}` : "";
39 40
        const state = a.state && a.state !== "ok" ? `\n⚠ ${a.state}` : "";
40 -
        i.tooltip = `${a.display}\n${a.kind}${expires}${state}`;
41 +
        const may = also.length ? `\nMay also post in ${also.join(", ")}` : "";
42 +
        i.tooltip = `${a.display}\n${a.kind}${expires}${may}${state}`;
41 43
        i.iconPath = new vscode.ThemeIcon(a.state && a.state !== "ok" ? "warning" : "circle-filled", new vscode.ThemeColor(PRESENCE[a.presence] ?? "disabledForeground"));
42 44
        i.contextValue = "agent";
43 45
        i.id = `agent:${a.name}`;
…
8 unchanged lines
52 54
    const rows = [...this.model.channels].filter((c) => c.kind !== "system").sort((a, b) => Number(b.name === mine) - Number(a.name === mine));
53 55
    return rows.map((c) => {
54 56
      const i = new vscode.TreeItem(c.display);
57 +
      const own = this.model.agents.filter((a) => !a.disabled && a.project === c.name).map((a) => a.name);
58 +
      const guests = this.model.agents.filter((a) => !a.disabled && (a.channels ?? []).includes(c.name)).map((a) => a.name);
55 59
      i.description = c.purpose ?? undefined;
56 -
      i.tooltip = `${c.display}${c.purpose ? `\n${c.purpose}` : ""}\n${c.messages} message(s)`;
60 +
      const who = c.kind === "project" ? `\nAgents who may post: ${[...own, ...guests].join(", ") || "none yet"}` : "";
61 +
      i.tooltip = `${c.display}${c.purpose ? `\n${c.purpose}` : ""}\n${c.messages} message(s)${who}`;
62 +
      i.contextValue = c.kind === "project" ? "projectChannel" : "channel";
57 63
      i.iconPath = new vscode.ThemeIcon(c.kind === "direct" ? "mention" : c.name === mine ? "star-full" : "comment-discussion");
58 64
      i.command = { command: "spectra.openChannel", title: "Open", arguments: [c.name] };
59 65
      i.id = `channel:${c.name}`;
…
44 unchanged lines