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
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
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 24.7 KB | 7 permissions | Listed |
No comments yet.