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

views/layout.ts4.5 KB
// The page's building blocks: sections, cards, settings rows and the small
// controls (a segmented picker, a preset limit) every section shares.
// HTM whitespace is significant; keep these templates out of Prettier's HTML formatter.
import { html as htm } from "convergence";
import type { Element } from "convergence";

export const READING_WIDTH = 760;

// Layout for the templates below: a flex column, a row that centres its
// children across, a child that takes the free space (and may shrink below
// its content), the whole of the parent, and a column of the reading width
// centred in its parent.
export const column = { display: "flex", flexDirection: "column" } as const;
export const row = { display: "flex", flexDirection: "row", alignItems: "center" } as const;
export const fill = { flexGrow: 1, flexShrink: 1, flexBasis: "0%", minWidth: 0, minHeight: 0 } as const;
export const full = { width: "100%", height: "100%" } as const;
export const reading = { maxWidth: READING_WIDTH, width: "100%", marginX: "auto" } as const;
// The colour of a risk dot, from its tone.
export const DOT = { muted: "mutedForeground", warning: "warning", danger: "danger" } as const;

// A section: its title and one line of help in the type of the rows below
// (the kernel's setting heading), then its card.
export function section(title: string, hint: string | null, card: Element) {
  return htm`
      <div ...${column} gap=${0} padding="small">
        <setting heading title=${title} description=${hint ?? undefined} />
        ${card}
      </div>`;
}

// A card of rows, a line between them: the kernel's card.
export function card(rows: ReadonlyArray<Element | null>) {
  return htm`<card divided>${rows.filter(Boolean)}</card>`;
}

export interface SettingParts {
  title: string;
  description?: string | null;
  control?: Element | null;
  wide?: boolean;
}

// One setting: what it is on the left, what it is set to on the right
// (the kernel's setting; a wide control is a text field's 260px).
export function setting({ title, description, control, wide }: SettingParts) {
  return htm`<setting title=${title} description=${description ?? undefined} wide=${wide}>${control ?? null}</setting>`;
}

// A row of a list inside a card: a switch or a mark on the left, then
// what the row is about.
export const listRow = (...children: Element[]) =>
  htm`<div ...${row} gap="medium" padding="small" paddingX="medium" paddingY="medium" alignItems="start">${children}</div>`;

/// The height of a list row's title line. A row's leading switch and
/// trailing button sit in a box of this height, so they centre on the
/// title rather than on the top of the row.
export const TITLE_LINE = 24;

/// A switch or button beside a list row's title line, centred on it.
export const besideTitle = (control: Element | null, width?: number) =>
  htm`<div ...${row} height=${TITLE_LINE} width=${width} flexShrink=${0} justifyContent="center">${control}</div>`;

// Three choices are a segmented control, not a dropdown; the kernel draws it.
export function segmented<V extends string>(
  id: string,
  value: V,
  choices: ReadonlyArray<{ value: V; label: string }>,
  onChange: (value: V) => void,
) {
  return htm`<tabs id=${id} segmented active=${value} tabs=${choices.map((c) => ({ id: c.value, label: c.label }))}
    onChange=${(e: { value: string }) => onChange(e.value as V)} />`;
}

// A number setting picked from presets, where `null` means no limit. A
// value set by hand in settings.json is offered too, so it shows.
export function limit(
  id: string,
  value: number | null,
  presets: readonly number[],
  label: (value: number) => string,
  unlimited: string,
  onChange: (value: number | null) => void,
) {
  const values = value == null || presets.includes(value) ? presets : [...presets, value].sort((a, b) => a - b);
  return htm`
      <select id=${id} value=${value == null ? "" : String(value)} small
        options=${[...values.map((v) => ({ value: String(v), label: label(v) })), { value: "", label: unlimited }]}
        onChange=${(e: { value: string }) => onChange(e.value === "" ? null : Number(e.value))} />`;
}

export function pluginChip(name: string) {
  return htm`
      <div ...${column} gap="small" padding="small" paddingX="small" background="subtle" borderRadius="sm">
        <text style="body" size="xs" tone="muted">${name}</text>
      </div>`;
}

export function notes(text: string | null) {
  return text
    ? htm`<div ...${column} gap="small" padding="small" paddingX="medium" paddingY="small"><markdown>${text}</markdown></div>`
    : null;
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.