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/agents.ts6.1 KB
// The Agents section: every agent the host serves, with its account state,
// plan usage and one-click updates. Registry agents collapse behind one row.
// 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 { AgentEntry, SettingsPage } from "../controller.ts";
import { TITLE_LINE, besideTitle, card, listRow, section, column, fill, row } from "./layout.ts";

// How much of the account's plan is used, once it is worth saying.
function usageRow(a: AgentEntry) {
  const windows = a.usageLimits?.windows ?? [];
  if (!a.enabled || !windows.length) return null;
  const shown = windows.filter((w) => w.usedPercent >= 1 || w.blocked);
  if (!shown.length) return null;
  return htm`
      <div ...${row} gap="medium" padding="small" flexWrap="wrap">
        ${shown.map((w) => {
          const tone = w.blocked ? "danger" : w.usedPercent >= 90 ? "warning" : "muted";
          const resets = w.resetsAt
            ? ` · resets ${new Date(w.resetsAt).toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}`
            : "";
          return htm`<text style="body" size="xs" tone=${tone}>${`${w.label} ${Math.round(w.usedPercent)}%${resets}`}</text>`;
        })}
      </div>`;
}

// The version, and a one-click upgrade only when the installer that owns
// the binary could be proven. Anything else stays manual.
function maintenanceNote(a: AgentEntry) {
  const m = a.maintenance;
  if (!a.enabled || !m) return null;
  const behind = m.latestVersion && m.latestVersion !== m.installedVersion;
  if (!behind) return null;
  const how = m.canUpdate ? "" : " Update it the way you installed it.";
  return htm`<text style="body" size="xs" tone="warning">${`${m.latestVersion} is available.${how}`}</text>`;
}

function maintenanceAction(ctx: EffectViewContext<object>, actions: SettingsPage["Service"], a: AgentEntry) {
  const m = a.maintenance;
  if (!a.enabled || !m?.canUpdate) return null;
  return htm`<button id=${`update-${a.id}`} label=${`Update with ${m.manager}`} small variant="outline" onClick=${() => ctx.run(actions.updateAgent(a))} />`;
}

function agentRow(ctx: EffectViewContext<object>, actions: SettingsPage["Service"], a: AgentEntry) {
  const status = a.status?.state;
  const badge =
    status === "ready"
      ? htm`<badge variant="success">Ready</badge>`
      : status === "auth_required"
        ? htm`<badge variant="warning">Sign in needed</badge>`
        : a.enabled
          ? htm`<badge variant="danger">Unavailable</badge>`
          : null;
  const statusMessage = a.status !== undefined && "message" in a.status ? a.status.message : null;
  const detail = status !== "ready" && a.enabled && statusMessage ? statusMessage : a.description;
  const methods = a.authMethods ?? [];
  const actions$ = [
    ...(a.enabled && status !== "ready" && methods.length
      ? methods.map(
          (m) =>
            htm`<button id=${`auth-${a.id}-${m.id}`} label=${m.name} small variant="primary" onClick=${() => ctx.run(actions.signIn(a, m))} />`,
        )
      : []),
    maintenanceAction(ctx, actions, a),
  ].filter(Boolean);
  // One left edge under the title, the switch centred on the title line.
  return listRow(
    besideTitle(
      htm`<switch id=${`agent-${a.id}`} label="" checked=${a.enabled} onChange=${(e: { value: boolean }) => ctx.run(actions.setAgentEnabled(a, e.value))} />`,
      36,
    ),
    htm`
        <div ...${column} ...${fill} gap=${4}>
          <div ...${row} gap="small" height=${TITLE_LINE}>
            <icon name="bot" small tone="muted" svg=${a.icon} />
            <text style="body" size="sm" bold>${a.name}</text>
            ${a.version ? htm`<text style="body" size="xs" tone="faint">${a.version}</text>` : null}
            <spacer />
            ${badge}
          </div>
          ${detail ? htm`<text style="body" size="xs" tone="muted">${detail}</text>` : null}
          ${usageRow(a)}
          ${maintenanceNote(a)}
          ${actions$.length ? htm`<div ...${row} gap="small" flexWrap="wrap">${actions$}</div>` : null}
        </div>`,
  );
}

export function renderAgents(ctx: EffectViewContext<object>, actions: SettingsPage["Service"]) {
  const state = actions.state;
  // Direct providers first; the long registry list is collapsed by
  // default. The test is the ACP prefix, not any colon: a second account
  // of a direct provider is an id like `codex:personal`.
  const direct = state.agents.filter((a) => !a.id.startsWith("acp:"));
  const registry = state.agents.filter((a) => a.id.startsWith("acp:"));
  const enabledRegistry = registry.filter((a) => a.enabled);
  const others = registry.filter((a) => !a.enabled);
  return htm`
      <div ...${column} gap="large" padding="small">
        ${section("Agents", "Enabled agents start with the app and appear in the agent pickers.", card(direct.map((a) => agentRow(ctx, actions, a))))}
        ${
          registry.length
            ? section(
                "More agents",
                `${registry.length} agents from the Agent Client Protocol registry.${enabledRegistry.length ? "" : " None enabled."}`,
                htm`
              <div ...${column} gap="small">
                ${enabledRegistry.length ? card(enabledRegistry.map((a) => agentRow(ctx, actions, a))) : null}
                <div ...${row}>
                  <button id="registry-agents" label=${state.showRegistry ? `Hide the other ${others.length}` : `Show the other ${others.length}`}
                    icon=${state.showRegistry ? "chevron-down" : "chevron-right"} small variant="ghost" onClick=${() => {
                      state.showRegistry = !state.showRegistry;
                      ctx.update();
                    }} />
                </div>
                ${
                  // At the same left edge as the enabled ones: the kernel's
                  // collapsible indented its body.
                  state.showRegistry ? card(others.map((a) => agentRow(ctx, actions, a))) : null
                }
              </div>`,
              )
            : null
        }
      </div>`;
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.