Official

settings

Built-in settings UI (JavaScript UI plugin).

The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/settings@0.2.0

Permissions in 0.2.0

  • Manage plugins plugins.manageOfficial onlyReloads and enables plugins. Only official plugins can have it.List, enable, disable and reload plugins, install updates, and quit to finish an update
  • Change app settings settings.writeHighChanges the app settings.Change the settings and the theme
  • Manage agents agents.manageHighEnables, disables and signs in agents, and changes their settings.Turn agents on and off, sign in to them and update them
  • Control chats chats.controlHighCreates chats, sends prompts and cancels runs.Import an agent's own sessions as chats
  • Read chats chats.readMediumReads your transcripts and chat lists.List the workspaces an agent's sessions can be imported into
  • Read app settings settings.readLowReads the app settings.Show the settings, and the tools and rules of every plugin
  • Show panels ui.slotsLowShows views in the listed parts of the window.Draw the settings pageSlots: center

Files

model.test.ts3.8 KB
// The settings page's pure parts: sections, the shortcut search, and the
// values a setting's field sends to the host.
import assert from "node:assert/strict";
import type { SettingValue } from "convergence";

// `parseInput` answers a value or an error; the tests read either one.
const errorOf = (result: { value?: SettingValue; error?: string }) => result.error;
import { test } from "node:test";

import {
  APP_SECTIONS,
  chordOf,
  matchesCommand,
  optionsOf,
  parseInput,
  pluginSections,
  shortcutGroups,
  textOf,
} from "./model.ts";

test("each plugin with settings has a section, after the app's own", () => {
  const plugins = [
    { name: "chat", settings: "Chat", enabled: true },
    { name: "git", settings: undefined, enabled: true },
    { name: "opencode", settings: "OpenCode", enabled: false },
  ];
  assert.deepEqual(pluginSections(plugins), [
    { id: "plugin:chat", label: "Chat", plugin: "chat", enabled: true },
    { id: "plugin:opencode", label: "OpenCode", plugin: "opencode", enabled: false },
  ]);
  assert.deepEqual(pluginSections(null), []);
  assert.deepEqual(
    APP_SECTIONS.map((s) => s.id),
    ["general", "agents", "workspaces", "plugins", "tools", "keys", "updates"],
  );
});

const commands = [
  {
    id: "kernel.quit",
    title: "Quit Divergence",
    category: "Divergence",
    plugin: "kernel",
    pluginTitle: "Divergence",
    kernel: true,
    keys: ["cmd-q"],
    labels: ["⌘Q"],
  },
  {
    id: "chat.newChat",
    title: "New Chat",
    category: "Chat",
    plugin: "chat",
    pluginTitle: "chat",
    keys: ["cmd-t"],
    labels: ["⌘T"],
    when: null,
  },
  {
    id: "sidebar.moveChatUp",
    title: "Move Chat Up",
    category: "Sidebar",
    plugin: "sidebar",
    pluginTitle: "sidebar",
    keys: ["alt-up"],
    labels: ["⥥↑"],
    when: "!composerFocused",
  },
];

test("the shortcut search reads titles, plugins, ids, whens and keys", () => {
  assert.ok(matchesCommand(commands[1], "new chat"));
  assert.ok(matchesCommand(commands[1], "cmd-t"));
  assert.ok(matchesCommand(commands[1], "⌘T"));
  assert.ok(matchesCommand(commands[2], "composerfocused"));
  assert.ok(matchesCommand(commands[2], "sidebar move"));
  assert.ok(!matchesCommand(commands[2], "sidebar quit"), "every word must match");
  assert.ok(matchesCommand(commands[0], ""));
});

test("shortcuts group by plugin, the app's own first", () => {
  const groups = shortcutGroups(commands, "");
  assert.deepEqual(
    groups.map((g) => g.title),
    ["Divergence", "chat", "sidebar"],
  );
  assert.deepEqual(
    shortcutGroups(commands, "chat").map((g) => g.title),
    ["chat", "sidebar"],
  );
  assert.deepEqual(shortcutGroups(commands, "nothing like this"), []);
});

test("a recorded chord and the values a setting's field sends", () => {
  assert.equal(chordOf(["cmd-k", "cmd-s"]), "cmd-k cmd-s");
  assert.equal(chordOf(["shift-cmd-p"]), "shift-cmd-p");
  const number = { type: "number" as const, minimum: 1, maximum: 100 };
  assert.deepEqual(parseInput(number, "20"), { value: 20 });
  assert.deepEqual(parseInput(number, ""), { value: null }, "empty goes back to the default");
  assert.equal(errorOf(parseInput(number, "abc")), "Enter a number");
  assert.equal(errorOf(parseInput(number, "500")), "At most 100");
  assert.deepEqual(parseInput({ type: "color" }, "#A0B1C2"), { value: "#A0B1C2" });
  assert.ok(errorOf(parseInput({ type: "color" as const }, "red")));
  assert.deepEqual(parseInput({ type: "url" }, "https://api.example.com"), { value: "https://api.example.com" });
  assert.deepEqual(parseInput({ type: "path" }, "  "), { value: null });
  assert.equal(textOf(null), "");
  assert.equal(textOf(3), "3");
  assert.deepEqual(optionsOf({ options: ["a", { value: "b", label: "B" }] }), [
    { value: "a", label: "a" },
    { value: "b", label: "B" },
  ]);
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <324.7 KB7 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.