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/page.ts4 KB
// The settings page: the header with its back button, the list of sections
// on the left, and the open section's body at the reading width.
// HTM whitespace is significant; keep these templates out of Prettier's HTML formatter.
import { html as htm } from "convergence";
import type { EffectViewContext } from "convergence/effect";
import type { SettingsPage } from "../controller.ts";
import { APP_SECTIONS, pluginSections } from "../model.ts";
import { renderGeneral } from "./general.ts";
import { renderAgents } from "./agents.ts";
import { renderWorkspaces } from "./workspaces.ts";
import { renderPlugins } from "./plugins.ts";
import { renderTools } from "./tools.ts";
import { renderKeys } from "./keys.ts";
import { renderUpdates } from "./updates.ts";
import { renderPluginSection } from "./pluginSection.ts";
import { column, fill, full, reading, row } from "./layout.ts";

function nav(ctx: EffectViewContext<object>, actions: SettingsPage["Service"]) {
  const state = actions.state;
  const entry = (item: { id: string; label: string; icon?: string; plugin?: string; enabled?: boolean }) => {
    const chosen = state.section === item.id;
    const label =
      item.id === "updates" && (state.updates?.appUpdate || state.updates?.pluginUpdate)
        ? `${item.label} •`
        : item.label;
    // A turned-off plugin's mark is faint.
    return htm`
        <item key=${item.id} id=${`nav-${item.id}`} icon=${item.icon ?? "puzzle"} iconTone=${item.enabled === false ? "faint" : undefined}
          label=${label} selected=${chosen} onClick=${() => ctx.run(actions.openSection(item.id, item.plugin))} />`;
  };
  const plugins = pluginSections(state.plugins);
  return htm`
      <div ...${column} gap=${2} padding="small" paddingLeft=${0} width=${210} flexShrink=${0}>
        ${APP_SECTIONS.map(entry)}
        ${
          plugins.length
            ? htm`<item heading label="Plugins" />`
            : null
        }
        ${plugins.map(entry)}
      </div>`;
}

function body(ctx: EffectViewContext<object>, actions: SettingsPage["Service"]) {
  const plugin = actions.sectionPlugin();
  if (plugin) return renderPluginSection(ctx, actions, plugin);
  switch (actions.state.section) {
    case "agents":
      return renderAgents(ctx, actions);
    case "workspaces":
      return renderWorkspaces(ctx, actions);
    case "plugins":
      return renderPlugins(ctx, actions);
    case "tools":
      return renderTools(ctx, actions);
    case "keys":
      return renderKeys(ctx, actions);
    case "updates":
      return renderUpdates(ctx, actions);
    default:
      return renderGeneral(ctx, actions);
  }
}

export function renderSettings(ctx: EffectViewContext<object>, actions: SettingsPage["Service"]) {
  const state = actions.state;
  if (ctx.app.route !== "settings") return null;
  if (!state.loaded) {
    ctx.run(actions.load());
    return htm`<empty>Loading settings…</empty>`;
  }
  // A plugin's section that is gone (the plugin was removed) falls back.
  const open = actions.sectionPlugin();
  if (open && !state.plugins?.some((p) => p.name === open && p.settings)) state.section = "general";
  // The back arrow is as far from the pane's left edge as from its top,
  // and the menu's icons line up under it.
  return htm`
      <div ...${column} ...${fill} ...${full} padding="small">
        <div ...${row} gap="small" padding="small" paddingY="medium" paddingX=${8.5}>
          <button id="settings-back" icon="arrow-left" variant="ghost" small tooltip="Back to chat" onClick=${() => ctx.run(actions.route("chat"))} />
          <text style="title">Settings</text>
        </div>
        <div ...${row} ...${fill} alignItems="start" gap="small" padding="small">
          <div ...${column} height="100%" overflowY="scroll" padding="small" paddingLeft=${0}>${nav(ctx, actions)}</div>
          <div ...${column} ...${fill} height="100%" gap="small" padding="small" overflowY="scroll">
            <div ...${column} ...${reading} gap="large" padding="small" paddingX="large" paddingY="medium">${body(ctx, actions)}</div>
          </div>
        </div>
      </div>`;
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.