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