Official
market
The plugin marketplace: browse, install through an agent's scan, update and publish plugins, and the marketplace tools of agents in the Plugins workspace.
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/market@0.1.0
Permissions in 0.1.0
Files
controller.ts28.4 KB
// The marketplace page's effects: loading every section through the host
// and running installs, updates, sign-in and bug reports. State mutations
// stay synchronous so views re-render the same way the JavaScript page did;
// only host answers and chat openings are effects.
import * as Effect from "effect/Effect";
import * as Scope from "effect/Scope";
import * as z from "zod";
import { App, Host, Notify, Plugin, parse, promise } from "convergence/effect";
import type { PluginServices } from "convergence/effect";
import type { AppState } from "convergence";
import { MarketState, ROUTE } from "./state.ts";
import type { Page } from "./state.ts";
import { badgeCount, checkedAttachments, effortChoices, resolveChoice, suggestedPlugins } from "./logic.ts";
import type { AgentChoice } from "./logic.ts";
import {
accountSchema,
bugDraftSchema,
bugPreviewSchema,
bugReportsSchema,
categoriesSchema,
commentsSchema,
listingSchema,
loginSchema,
releaseFileSchema,
releaseSchema,
searchAnswerSchema,
settingsContributesSchema,
updatesSchema,
} from "./remote.ts";
import type { Listing, MarketUpdateItem, SentReport } from "./remote.ts";
import {
agentOptions,
defaultChoice,
directInstallOn,
fixWithAgent,
install as installFlow,
setDirectInstall,
update as updateFlow,
} from "./flows.ts";
import { listingUrl } from "./logic.ts";
const messageOf = (error: unknown): string => {
if (error instanceof Error) return error.message;
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string")
return error.message;
return String(error);
};
const marketOpenSchema = z.looseObject({
id: z.string(),
version: z.string().nullable().optional(),
nonce: z.unknown(),
});
export const controller = Effect.fn("Market.controller")(function* () {
const state = yield* MarketState;
const host = yield* Host;
const app = yield* App;
const notify = yield* Notify;
const plugin = yield* Plugin;
const scope = yield* Scope.Scope;
const redraw = (): void => {
for (const view of state.views) view.update();
};
const fail = Effect.fn("Market.fail")(function* (what: string, error: unknown) {
yield* notify.error(`${what}: ${messageOf(error)}`);
redraw();
});
// --- data ------------------------------------------------------------------
const loadAccount = Effect.fn("Market.loadAccount")(function* () {
yield* host.call("host/market.whoami", {}).pipe(
Effect.flatMap((raw) => parse("account", accountSchema, raw)),
Effect.tap((account) =>
Effect.sync(() => {
state.account = account;
}),
),
Effect.catch((error) =>
Effect.sync(() => {
state.account = { signedIn: false as const, error: messageOf(error) };
}),
),
);
redraw();
return state.account;
});
const registry = Effect.fn("Market.registry")(function* () {
return (state.account ?? (yield* loadAccount()))?.registry;
});
const loadCategories = Effect.fn("Market.loadCategories")(function* () {
const raw = yield* host.call("host/market.categories", {}).pipe(Effect.catch(() => Effect.succeed(null)));
if (raw === null) {
// An older or unreachable registry: the built-in list stays.
console.warn("market: the categories could not be read: the registry did not answer");
} else {
const parsed = categoriesSchema.safeParse(raw);
if (parsed.success && parsed.data.categories.length) state.categories = parsed.data.categories;
else console.warn("market: the categories could not be read: the answer did not parse");
}
redraw();
});
const search = Effect.fn("Market.search")(function* (more = false) {
state.searching = true;
state.searchError = null;
redraw();
const page = more ? state.pageNo + 1 : 1;
yield* host
.call("host/market.search", {
query: state.query,
category: state.category ?? undefined,
sort: state.sort || undefined,
page,
})
.pipe(
Effect.flatMap((raw) => parse("market search", searchAnswerSchema, raw)),
Effect.tap((answer) =>
Effect.sync(() => {
state.results = more ? [...(state.results ?? []), ...(answer.results ?? [])] : (answer.results ?? []);
state.total = answer.total ?? state.results.length;
state.pageNo = page;
}),
),
Effect.catch((error) =>
Effect.sync(() => {
state.searchError = messageOf(error);
}),
),
);
state.searching = false;
redraw();
});
const loadMenus = Effect.fn("Market.loadMenus")(function* () {
yield* defaultChoice().pipe(
Effect.tap(({ agents: agents_, options, choice }) =>
Effect.sync(() => {
state.menus = { agents: agents_, options, choice };
}),
),
Effect.catch((error) =>
Effect.sync(() => {
state.menus = {
agents: [],
options: [],
choice: { agentId: null, model: null, effort: null },
error: messageOf(error),
};
}),
),
);
redraw();
});
const chooseAgent = Effect.fn("Market.chooseAgent")(function* (agentId: string) {
const menus = state.menus;
if (!menus) return;
const options = yield* agentOptions(agentId).pipe(Effect.catch(() => Effect.succeed([])));
state.menus = { ...menus, options, choice: resolveChoice({ agentId }, menus.agents, options) };
redraw();
});
const chooseModel = (model: string): void => {
const menus = state.menus;
if (!menus) return;
const efforts = effortChoices(menus.options, model);
const effort = efforts.some((c) => c.value === menus.choice.effort)
? menus.choice.effort
: (efforts[0]?.value ?? null);
state.menus = { ...menus, choice: { ...menus.choice, model, effort } };
redraw();
};
const chooseEffort = (effort: string): void => {
const menus = state.menus;
if (!menus) return;
state.menus = { ...menus, choice: { ...menus.choice, effort } };
redraw();
};
const installableVersions = (listing: Listing): Exclude<Listing["versions"], undefined> =>
(listing.versions ?? []).filter((v) => !v.yanked && !v.malicious);
const openListing = Effect.fn("Market.openListing")(function* (id: string, version: string | null = null) {
state.page = "listing";
state.listingId = id;
state.listing = null;
state.listingError = null;
state.version = version;
state.tab = "permissions";
state.tree = null;
state.treeError = null;
state.file = null;
state.comments = null;
state.report = null;
redraw();
yield* host.call("host/market.listing", { id }).pipe(
Effect.flatMap((raw) => parse("listing", listingSchema, raw)),
Effect.tap((listing) =>
Effect.sync(() => {
if (state.listingId !== id) return;
state.listing = listing;
const installable = installableVersions(listing);
if (!state.version || !listing.versions?.some((v) => v.version === state.version)) {
state.version = listing.latestVersion ?? installable[0]?.version ?? listing.versions?.[0]?.version ?? null;
}
}),
),
Effect.catch((error) =>
Effect.sync(() => {
if (state.listingId === id) state.listingError = messageOf(error);
}),
),
);
if (!state.menus) yield* Effect.forkIn(loadMenus(), scope, { startImmediately: true });
redraw();
});
const loadTree = Effect.fn("Market.loadTree")(function* () {
const { listingId: id, version } = state;
state.tree = "loading";
state.treeError = null;
redraw();
yield* host.call("host/market.read", { id: id ?? "", version: version ?? undefined }).pipe(
Effect.flatMap((raw) => parse("release", releaseSchema, raw)),
Effect.tap((release) =>
Effect.sync(() => {
if (state.listingId === id && state.version === version) state.tree = release;
}),
),
Effect.catch((error) =>
Effect.sync(() => {
state.tree = null;
state.treeError = messageOf(error);
}),
),
);
redraw();
});
const openFile = Effect.fn("Market.openFile")(function* (path: string) {
const { listingId: id, version } = state;
state.file = { path, loading: true };
redraw();
yield* host.call("host/market.read", { id: id ?? "", version: version ?? undefined, path }).pipe(
Effect.flatMap((raw) => parse("release file", releaseFileSchema, raw)),
Effect.tap((file) =>
Effect.sync(() => {
if (state.file?.path === path)
state.file = {
path: file.path ?? path,
loading: false,
text: file.text,
size: file.size,
truncated: file.truncated,
binary: file.binary,
};
}),
),
Effect.catch((error) =>
Effect.sync(() => {
if (state.file?.path === path) state.file = { path, error: messageOf(error), loading: false };
}),
),
);
redraw();
});
const loadComments = Effect.fn("Market.loadComments")(function* () {
const id = state.listingId;
state.comments = "loading";
redraw();
yield* host.call("host/market.comments", { id: id ?? "" }).pipe(
Effect.flatMap((raw) => parse("comments", commentsSchema, raw)),
Effect.tap((comments) =>
Effect.sync(() => {
if (state.listingId === id) state.comments = { threads: comments.threads ?? [] };
}),
),
Effect.catch((error) =>
Effect.sync(() => {
if (state.listingId === id) state.comments = { error: messageOf(error) };
}),
),
);
redraw();
});
const loadUpdates = Effect.fn("Market.loadUpdates")(function* (check = false) {
const generation = state.updatesGeneration;
yield* host.call("host/market.updates", check ? {} : { cached: true }).pipe(
Effect.flatMap((raw) => parse("updates", updatesSchema, raw)),
Effect.tap((answer) =>
Effect.sync(() => {
// A `market_changed` that came meanwhile is newer than this answer.
if (state.updatesGeneration === generation) state.updates = answer.updates ?? [];
}),
),
Effect.tap(() =>
check
? state.updates.length
? notify.info(`${badgeCount(state.updates)} plugin updates.`)
: notify.info("Every plugin is up to date.")
: Effect.void,
),
Effect.catch((error) => (check ? notify.error(`Could not check for updates: ${messageOf(error)}`) : Effect.void)),
);
redraw();
});
const load = Effect.fn("Market.load")(function* () {
state.loaded = true;
state.direct = yield* directInstallOn().pipe(Effect.catch(() => Effect.succeed(false)));
yield* loadAccount();
yield* Effect.forkIn(loadCategories(), scope, { startImmediately: true });
yield* Effect.forkIn(search(), scope, { startImmediately: true });
});
// --- actions ---------------------------------------------------------------
const busy = Effect.fn("Market.busy")(function* (key: string, work: Effect.Effect<unknown, unknown, PluginServices>) {
if (state.busy[key]) return;
state.busy = { ...state.busy, [key]: true };
redraw();
// `Effect.ensuring`, not try/finally: a failed `yield*` does not run a
// JavaScript `finally` block, and a leaked key would pin the button.
yield* work.pipe(
Effect.ensuring(
Effect.sync(() => {
const { [key]: _removed, ...rest } = state.busy;
state.busy = rest;
redraw();
}),
),
);
});
const install = Effect.fn("Market.install")(function* () {
const { listing, version } = state;
if (!listing) return;
const choice: AgentChoice = state.menus?.choice ?? { agentId: null, model: null, effort: null };
const key = `install:${state.listingId}`;
yield* busy(
key,
installFlow(listing, version, choice).pipe(
Effect.tap((result) =>
result.direct
? notify.info(`${listing.id} is downloaded, not enabled. Its card is in the chat.`)
: Effect.void,
),
Effect.catch((error) => fail(`Could not install ${listing.id}`, error)),
),
);
});
const runUpdate = Effect.fn("Market.runUpdate")(function* (item: { readonly folder: string; readonly id: string }) {
if (!state.menus) yield* loadMenus();
const choice: AgentChoice = state.menus?.choice ?? { agentId: null, model: null, effort: null };
yield* busy(
`update:${item.folder}`,
updateFlow(item, choice).pipe(
// The list is read again either way: a direct replace clears the
// row, an agent chat may have merged already.
Effect.tap(() => loadUpdates()),
Effect.catch((error) => fail(`Could not update ${item.folder}`, error)),
),
);
});
const signIn = Effect.fn("Market.signIn")(function* () {
yield* busy(
"sign-in",
host.call("host/market.login", {}).pipe(
Effect.flatMap((raw) => parse("sign-in", loginSchema, raw)),
Effect.tap((code) =>
Effect.sync(() => {
state.signIn = { code: code.userCode, url: code.verificationUriComplete ?? code.verificationUri };
}),
),
Effect.catch((error) => fail("Could not start the sign-in", error)),
),
);
});
const signOut = Effect.fn("Market.signOut")(function* () {
yield* busy(
"sign-out",
Effect.gen(function* () {
yield* host.call("host/market.logout", {}).pipe(Effect.catch((error) => fail("Could not sign out", error)));
yield* loadAccount();
}),
);
});
const openSignInPage = Effect.fn("Market.openSignInPage")(function* () {
const signInState = state.signIn;
if (!signInState) return;
const result = yield* promise("openUrl", () => plugin.api.openUrl(signInState.url));
if (!result.opened)
yield* notify.warning(
`Open ${signInState.url} and enter ${signInState.code}${result.reason ? ` (${result.reason})` : ""}.`,
);
});
const sendReport = Effect.fn("Market.sendReport")(function* () {
const draft = state.report;
if (!draft) return;
yield* busy(
"report",
Effect.gen(function* () {
if (!draft.reason.trim()) {
yield* notify.warning("Say what is wrong first.");
return;
}
yield* host
.call("host/market.request_report", {
id: state.listingId ?? "",
version: state.version ?? undefined,
reason: draft.reason.trim(),
files: String(draft.files ?? "")
.split(/[\n,]+/)
.map((f) => f.trim())
.filter(Boolean),
kind: "user",
})
.pipe(
Effect.tap(() =>
Effect.sync(() => {
state.report = null;
}),
),
Effect.catch((error) => fail("Could not prepare the report", error)),
);
}),
);
});
// --- bug reports (MARKETPLACE.md §14) --------------------------------------
const loadReports = Effect.fn("Market.loadReports")(function* () {
const reports = yield* host.call("host/market.bug_reports", {}).pipe(
Effect.flatMap((raw) => parse("bug reports", bugReportsSchema, raw)),
Effect.map((answer) => answer.reports ?? []),
Effect.catch(() => Effect.succeed([])),
);
state.reports = reports;
redraw();
});
// Opens the form: from the palette, Settings, `cvg bug`, or this page.
// `plugins` and `text` fill it in.
const openBugForm = Effect.fn("Market.openBugForm")(function* (args?: {
readonly plugins?: readonly string[];
readonly text?: string;
readonly chatId?: string | null;
}) {
const { plugins = [], text = "", chatId = null } = args ?? {};
state.page = "bugs";
yield* app.route(ROUTE);
state.bug = {
loading: true,
error: null,
draft: null,
chosen: new Set(plugins),
fullSource: new Set(),
screenshots: [],
text,
email: "",
preview: null,
previewing: false,
previewError: null,
unchecked: new Set(),
open: new Set(),
requestId: null,
fixAfter: false,
sent: null,
};
redraw();
if (!state.menus) yield* Effect.forkIn(loadMenus(), scope, { startImmediately: true });
yield* Effect.forkIn(loadReports(), scope, { startImmediately: true });
yield* host.call("host/market.bug_draft", chatId ? { chatId } : {}).pipe(
Effect.flatMap((raw) => parse("bug draft", bugDraftSchema, raw)),
Effect.tap((draft) =>
Effect.sync(() => {
const bug = state.bug;
if (!bug) return;
bug.draft = draft;
for (const name of suggestedPlugins(draft.plugins)) bug.chosen.add(name);
// A plugin named by id (the web's "Report a bug") is chosen by folder.
for (const draftPlugin of draft.plugins ?? [])
if (bug.chosen.has(draftPlugin.id)) {
bug.chosen.delete(draftPlugin.id);
bug.chosen.add(draftPlugin.name);
}
}),
),
Effect.catch((error) =>
Effect.sync(() => {
if (state.bug) state.bug.error = messageOf(error);
}),
),
);
if (state.bug) state.bug.loading = false;
redraw();
yield* Effect.forkIn(refreshPreview(), scope, { startImmediately: true });
});
// What the chosen plugins, sources and screenshots would attach, with the
// full content (the host removed secrets and home paths).
const refreshPreview = Effect.fn("Market.refreshPreview")(function* () {
const bug = state.bug;
if (!bug) return;
const key = JSON.stringify([[...bug.chosen].sort(), [...bug.fullSource].sort(), bug.screenshots]);
bug.previewKey = key;
bug.previewing = true;
bug.previewError = null;
redraw();
yield* host
.call("host/market.bug_preview", {
plugins: [...bug.chosen],
fullSource: [...bug.fullSource],
screenshots: bug.screenshots,
})
.pipe(
Effect.flatMap((raw) => parse("bug preview", bugPreviewSchema, raw)),
Effect.tap((preview) =>
Effect.sync(() => {
if (state.bug === bug && bug.previewKey === key) {
bug.preview = preview;
// Unchecked by default: what the host marks so (a third party's diff).
for (const attachment of preview.attachments ?? [])
if (!attachment.defaultOn && !bug.seen?.has(attachment.id)) bug.unchecked.add(attachment.id);
bug.seen = new Set((preview.attachments ?? []).map((a) => a.id));
}
}),
),
Effect.catch((error) =>
Effect.sync(() => {
if (state.bug === bug && bug.previewKey === key) bug.previewError = messageOf(error);
}),
),
);
if (state.bug === bug && bug.previewKey === key) bug.previewing = false;
redraw();
});
const togglePlugin = Effect.fn("Market.togglePlugin")(function* (name: string, on: boolean) {
const bug = state.bug;
if (!bug) return;
if (on) bug.chosen.add(name);
else {
bug.chosen.delete(name);
bug.fullSource.delete(name);
}
yield* Effect.forkIn(refreshPreview(), scope, { startImmediately: true });
});
const toggleSource = Effect.fn("Market.toggleSource")(function* (name: string, on: boolean) {
const bug = state.bug;
if (!bug) return;
if (on) bug.fullSource.add(name);
else bug.fullSource.delete(name);
yield* Effect.forkIn(refreshPreview(), scope, { startImmediately: true });
});
const addScreenshots = Effect.fn("Market.addScreenshots")(function* () {
const { paths } = yield* promise("pickImages", () => plugin.api.pickImages());
if (!paths.length) return;
const bug = state.bug;
if (!bug) return;
bug.screenshots = [...new Set([...bug.screenshots, ...paths])].slice(0, 5);
yield* Effect.forkIn(refreshPreview(), scope, { startImmediately: true });
});
// "Review and send…": the native card shows everything that is sent;
// only its Send button sends. With `fixAfter`, the fix chat opens once
// the report is sent.
const reviewReport = Effect.fn("Market.reviewReport")(function* (fixAfter = false) {
const bug = state.bug;
if (!bug) return;
yield* busy(
"bug-review",
Effect.gen(function* () {
if (!bug.text.trim()) {
yield* notify.warning("Say what happened and what you expected first.");
return;
}
if (!bug.chosen.size) {
yield* notify.warning("Choose one or more related plugins.");
return;
}
yield* host
.call("host/market.request_bug_report", {
text: bug.text.trim(),
plugins: [...bug.chosen],
contactEmail: !bug.draft?.signedIn && bug.email.trim() ? bug.email.trim() : undefined,
attachments: checkedAttachments(bug.preview?.attachments, bug.unchecked),
fullSource: [...bug.fullSource],
screenshots: bug.screenshots,
kind: "user",
})
.pipe(
Effect.tap(({ requestId }) =>
Effect.sync(() => {
bug.requestId = requestId;
bug.fixAfter = fixAfter;
}),
),
Effect.catch((error) => fail("Could not prepare the bug report", error)),
);
}),
);
});
const fixReport = Effect.fn("Market.fixReport")(function* (report: SentReport) {
if (!state.menus) yield* loadMenus();
const choice: AgentChoice = state.menus?.choice ?? { agentId: null, model: null, effort: null };
yield* busy(
`fix:${report.id}`,
fixWithAgent(report, choice).pipe(
Effect.catch((error) => fail(`Could not open the fix chat for #${report.number}`, error)),
),
);
});
const attachFix = Effect.fn("Market.attachFix")(function* (report: SentReport) {
yield* busy(
`attach:${report.id}`,
host
.call("host/market.request_attach_fix", { report: report.id })
.pipe(Effect.catch((error) => fail(`Nothing to attach to #${report.number}`, error))),
);
});
// --- events ------------------------------------------------------------------
const onBugForm = Effect.fn("Market.onBugForm")(function* (event: {
readonly plugins?: readonly string[];
readonly text?: string;
}) {
yield* openBugForm({ plugins: event.plugins ?? [], text: event.text ?? "" });
});
const onBugReport = Effect.fn("Market.onBugReport")(function* (event: {
readonly requestId: string;
readonly reportId: string;
readonly number: number;
}) {
yield* loadReports();
const bug = state.bug;
if (!bug || bug.requestId !== event.requestId) return;
bug.sent = state.reports.find((report) => report.id === event.reportId) ?? {
id: event.reportId,
number: event.number,
text: bug.text,
plugins: [],
};
bug.requestId = null;
redraw();
if (bug.fixAfter) yield* fixReport(bug.sent);
});
const onSecurityResolved = (id: string, outcome: string): void => {
if (state.bug?.requestId === id && outcome !== "sent") {
state.bug.requestId = null;
redraw();
}
};
const onMarketChanged = (updates: readonly MarketUpdateItem[]): void => {
state.updatesGeneration++;
state.updates = updates;
redraw();
};
const onMarketAccount = Effect.fn("Market.onMarketAccount")(function* (event: {
readonly signedIn: boolean;
readonly error?: string;
}) {
state.signIn = null;
if (event.error) yield* notify.error(`Marketplace sign-in: ${event.error}`);
else if (event.signedIn) yield* notify.info("Signed in to the marketplace.");
yield* loadAccount();
});
// A listing installed, enabled or removed: its Install row changes.
const refreshListing = Effect.fn("Market.refreshListing")(function* () {
if (state.page === "listing" && state.listing && !state.busy.listing) {
const id = state.listingId;
yield* host.call("host/market.listing", { id: id ?? "" }).pipe(
Effect.flatMap((raw) => parse("listing", listingSchema, raw)),
Effect.tap((listing) =>
Effect.sync(() => {
if (state.listingId === listing.id) state.listing = listing;
redraw();
}),
),
Effect.catch(() => Effect.void),
);
}
});
const setDirect = Effect.fn("Market.setDirect")(function* (on: boolean) {
state.direct = on;
yield* setDirectInstall(on).pipe(Effect.catch((error) => fail("Could not save the setting", error)));
redraw();
});
const selectPage = Effect.fn("Market.selectPage")(function* (page: Page) {
state.page = page;
if ((page === "updates" || page === "bugs") && !state.menus)
yield* Effect.forkIn(loadMenus(), scope, { startImmediately: true });
if (page === "bugs") yield* Effect.forkIn(loadReports(), scope, { startImmediately: true });
redraw();
});
const setListingTab = (tab: string): void => {
state.tab = tab;
redraw();
};
const backToBrowse = Effect.fn("Market.backToBrowse")(function* () {
state.page = "browse";
redraw();
if (!state.results) yield* Effect.forkIn(search(), scope, { startImmediately: true });
});
const toggleRoute = Effect.fn("Market.toggleRoute")(function* (open: boolean) {
yield* app.route(open ? "chat" : ROUTE);
});
const openListingSite = Effect.fn("Market.openListingSite")(function* () {
const id = state.listingId;
if (!id) return;
const site = yield* registry();
yield* promise("openUrl", () => plugin.api.openUrl(listingUrl(site, id)));
});
const followApp = Effect.fn("Market.followApp")(function* (next: AppState) {
const shared = next.shared ?? {};
const link = marketOpenSchema.safeParse(shared.marketOpen);
if (link.success && link.data.id && link.data.nonce !== state.linkNonce) {
state.linkNonce = link.data.nonce;
yield* Effect.forkIn(openListing(link.data.id, link.data.version ?? null), scope, { startImmediately: true });
}
if (next.route === ROUTE && !state.loaded) yield* Effect.forkIn(load(), scope, { startImmediately: true });
});
// The settings a release's permissions name (`${settings.key}`), from
// its plugin.json, so the list shows each one's title and value
// (MARKETPLACE.md §15.3 rule 6). Read once per release: the view runs the
// returned fetch with `ctx.run`, keeping rendering pure.
const settingsOf = (
permissions: unknown,
): { settings: unknown; fetch: Effect.Effect<void, unknown, PluginServices> | null } => {
const { listingId: id, version } = state;
if (!id || !version || !JSON.stringify(permissions ?? {}).includes("${settings."))
return { settings: null, fetch: null };
const key = `${id}@${version}`;
if (state.settingsSchema?.key === key) return { settings: state.settingsSchema.settings, fetch: null };
state.settingsSchema = { key, settings: null };
const fetch = Effect.fn("Market.settingsOf.fetch")(function* () {
const raw = yield* host
.call("host/market.read", { id, version, path: "plugin.json" })
.pipe(Effect.catch(() => Effect.succeed(null)));
if (!raw || state.settingsSchema?.key !== key) return;
const fileText = z.looseObject({ text: z.string().optional() }).safeParse(raw);
const text = (fileText.success ? fileText.data.text : undefined) ?? "";
try {
const parsed = settingsContributesSchema.safeParse(JSON.parse(text));
state.settingsSchema.settings = parsed.success ? (parsed.data.contributes?.settings ?? null) : null;
} catch {
state.settingsSchema.settings = null;
}
redraw();
});
return { settings: null, fetch: fetch() };
};
return {
state,
redraw,
loadAccount,
registry,
loadCategories,
search,
loadMenus,
chooseAgent,
chooseModel,
chooseEffort,
installableVersions,
openListing,
loadTree,
openFile,
loadComments,
loadUpdates,
load,
busy,
install,
runUpdate,
signIn,
signOut,
openSignInPage,
sendReport,
loadReports,
openBugForm,
refreshPreview,
togglePlugin,
toggleSource,
addScreenshots,
reviewReport,
fixReport,
attachFix,
onBugForm,
onBugReport,
onSecurityResolved,
onMarketChanged,
onMarketAccount,
refreshListing,
followApp,
settingsOf,
setDirect,
selectPage,
setListingTab,
backToBrowse,
toggleRoute,
openListingSite,
};
});
export type Controller = Effect.Success<ReturnType<typeof controller>>;Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.1.0latest | Oct 5, 2026 | >=2 <3 | 44.9 KB | 6 permissions | Listed |
No comments yet.