dsh-mobile-plugin 0.5.28 → 0.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/catalog.json +10 -1
- package/extensions/magpie/agentcompact.js +104 -0
- package/extensions/magpie/app.css +4902 -0
- package/extensions/magpie/app.js +24474 -0
- package/extensions/magpie/autobackup.js +185 -0
- package/extensions/magpie/autoresetown.js +151 -0
- package/extensions/magpie/balfound.js +38 -0
- package/extensions/magpie/boot.js +23 -0
- package/extensions/magpie/cfgblocked.js +39 -0
- package/extensions/magpie/checkinnow.js +111 -0
- package/extensions/magpie/claudemode.js +92 -0
- package/extensions/magpie/compat.js +41 -0
- package/extensions/magpie/context.css +433 -0
- package/extensions/magpie/context.js +1576 -0
- package/extensions/magpie/ctxtext.js +223 -0
- package/extensions/magpie/fonts.js +42 -0
- package/extensions/magpie/i18n.js +18942 -0
- package/extensions/magpie/icons/ai21.svg +1 -0
- package/extensions/magpie/icons/ai302-color.svg +1 -0
- package/extensions/magpie/icons/ai360-color.svg +1 -0
- package/extensions/magpie/icons/aihubmix-color.svg +1 -0
- package/extensions/magpie/icons/aionui.svg +1 -0
- package/extensions/magpie/icons/air.svg +4 -0
- package/extensions/magpie/icons/alibaba-color.svg +1 -0
- package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
- package/extensions/magpie/icons/alma.png +0 -0
- package/extensions/magpie/icons/anthropic.svg +1 -0
- package/extensions/magpie/icons/antigravity-color.svg +1 -0
- package/extensions/magpie/icons/anyscale-color.svg +1 -0
- package/extensions/magpie/icons/app-alma.png +0 -0
- package/extensions/magpie/icons/app-cc-switch.png +0 -0
- package/extensions/magpie/icons/apple-touch-icon.png +0 -0
- package/extensions/magpie/icons/aside.svg +28 -0
- package/extensions/magpie/icons/astrbot.svg +1 -0
- package/extensions/magpie/icons/atomcode.png +0 -0
- package/extensions/magpie/icons/aws.svg +1 -0
- package/extensions/magpie/icons/azure-color.svg +1 -0
- package/extensions/magpie/icons/azureai-color.svg +1 -0
- package/extensions/magpie/icons/baichuan-color.svg +1 -0
- package/extensions/magpie/icons/baidu-color.svg +1 -0
- package/extensions/magpie/icons/baiducloud-color.svg +1 -0
- package/extensions/magpie/icons/bailian-color.svg +1 -0
- package/extensions/magpie/icons/baseten.svg +1 -0
- package/extensions/magpie/icons/bedrock-color.svg +1 -0
- package/extensions/magpie/icons/burncloud-color.svg +1 -0
- package/extensions/magpie/icons/bytedance-color.svg +1 -0
- package/extensions/magpie/icons/cerebras.svg +1 -0
- package/extensions/magpie/icons/chatglm-color.svg +1 -0
- package/extensions/magpie/icons/cherryin-color.svg +1 -0
- package/extensions/magpie/icons/chutes.svg +1 -0
- package/extensions/magpie/icons/cindy.png +0 -0
- package/extensions/magpie/icons/claude-color.svg +1 -0
- package/extensions/magpie/icons/claudecode-color.svg +1 -0
- package/extensions/magpie/icons/cline.svg +1 -0
- package/extensions/magpie/icons/cloudflare-color.svg +1 -0
- package/extensions/magpie/icons/codebuddy-color.svg +1 -0
- package/extensions/magpie/icons/codex-color.svg +1 -0
- package/extensions/magpie/icons/cohere-color.svg +1 -0
- package/extensions/magpie/icons/cometapi-color.svg +1 -0
- package/extensions/magpie/icons/commandcode.svg +1 -0
- package/extensions/magpie/icons/copilot-color.svg +1 -0
- package/extensions/magpie/icons/coze.svg +1 -0
- package/extensions/magpie/icons/crush.png +0 -0
- package/extensions/magpie/icons/curl.svg +1 -0
- package/extensions/magpie/icons/cursor.svg +1 -0
- package/extensions/magpie/icons/deepinfra-color.svg +1 -0
- package/extensions/magpie/icons/deepmind-color.svg +1 -0
- package/extensions/magpie/icons/deepseek-color.svg +1 -0
- package/extensions/magpie/icons/deveco-color.svg +12 -0
- package/extensions/magpie/icons/devin.svg +1 -0
- package/extensions/magpie/icons/doubao-color.svg +1 -0
- package/extensions/magpie/icons/empryo-color.svg +1 -0
- package/extensions/magpie/icons/factory.svg +1 -0
- package/extensions/magpie/icons/favicon.svg +1 -0
- package/extensions/magpie/icons/featherless-color.svg +1 -0
- package/extensions/magpie/icons/fireworks-color.svg +1 -0
- package/extensions/magpie/icons/friendli.svg +1 -0
- package/extensions/magpie/icons/fx.svg +1 -0
- package/extensions/magpie/icons/gemini-color.svg +1 -0
- package/extensions/magpie/icons/geminicli-color.svg +1 -0
- package/extensions/magpie/icons/gemma-color.svg +1 -0
- package/extensions/magpie/icons/giteeai.svg +1 -0
- package/extensions/magpie/icons/github.svg +1 -0
- package/extensions/magpie/icons/githubcopilot.svg +1 -0
- package/extensions/magpie/icons/glama.svg +1 -0
- package/extensions/magpie/icons/gmicloud.svg +1 -0
- package/extensions/magpie/icons/google-color.svg +1 -0
- package/extensions/magpie/icons/googlecloud-color.svg +1 -0
- package/extensions/magpie/icons/goose.svg +1 -0
- package/extensions/magpie/icons/grok.svg +1 -0
- package/extensions/magpie/icons/groq.svg +1 -0
- package/extensions/magpie/icons/hanako.png +0 -0
- package/extensions/magpie/icons/hermes.svg +1 -0
- package/extensions/magpie/icons/huawei-color.svg +1 -0
- package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
- package/extensions/magpie/icons/huggingface-color.svg +1 -0
- package/extensions/magpie/icons/hunyuan-color.svg +1 -0
- package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
- package/extensions/magpie/icons/icon-512.png +0 -0
- package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
- package/extensions/magpie/icons/inference.svg +1 -0
- package/extensions/magpie/icons/infinigence-color.svg +1 -0
- package/extensions/magpie/icons/internlm-color.svg +1 -0
- package/extensions/magpie/icons/jcode.svg +1 -0
- package/extensions/magpie/icons/jina.svg +1 -0
- package/extensions/magpie/icons/kilo.svg +1 -0
- package/extensions/magpie/icons/kilocode.svg +1 -0
- package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
- package/extensions/magpie/icons/kimi.svg +1 -0
- package/extensions/magpie/icons/kiro-color.svg +1 -0
- package/extensions/magpie/icons/kling-color.svg +1 -0
- package/extensions/magpie/icons/lambda.svg +1 -0
- package/extensions/magpie/icons/leptonai-color.svg +1 -0
- package/extensions/magpie/icons/liquid.svg +1 -0
- package/extensions/magpie/icons/liveagent-color.svg +1 -0
- package/extensions/magpie/icons/llmapi.svg +1 -0
- package/extensions/magpie/icons/lmstudio.svg +1 -0
- package/extensions/magpie/icons/lody.png +0 -0
- package/extensions/magpie/icons/longcat.svg +1 -0
- package/extensions/magpie/icons/magpie.svg +1 -0
- package/extensions/magpie/icons/meta-color.svg +1 -0
- package/extensions/magpie/icons/metaai-color.svg +1 -0
- package/extensions/magpie/icons/microsoft-color.svg +1 -0
- package/extensions/magpie/icons/mimocode.svg +1 -0
- package/extensions/magpie/icons/minimax-color.svg +1 -0
- package/extensions/magpie/icons/mistermorph-color.svg +1 -0
- package/extensions/magpie/icons/mistral-color.svg +1 -0
- package/extensions/magpie/icons/mlx-serve.png +0 -0
- package/extensions/magpie/icons/modelscope-color.svg +1 -0
- package/extensions/magpie/icons/monica-color.svg +1 -0
- package/extensions/magpie/icons/moonshot.svg +1 -0
- package/extensions/magpie/icons/morph-color.svg +1 -0
- package/extensions/magpie/icons/mothx.png +0 -0
- package/extensions/magpie/icons/nebius.svg +1 -0
- package/extensions/magpie/icons/newapi-color.svg +1 -0
- package/extensions/magpie/icons/nousresearch.svg +1 -0
- package/extensions/magpie/icons/nova-color.svg +1 -0
- package/extensions/magpie/icons/novita-color.svg +1 -0
- package/extensions/magpie/icons/nvidia-color.svg +1 -0
- package/extensions/magpie/icons/ollama.svg +1 -0
- package/extensions/magpie/icons/omlx.svg +1 -0
- package/extensions/magpie/icons/omo.svg +1 -0
- package/extensions/magpie/icons/omp.svg +1 -0
- package/extensions/magpie/icons/openai.svg +1 -0
- package/extensions/magpie/icons/openchamber.svg +11 -0
- package/extensions/magpie/icons/openclaw.svg +1 -0
- package/extensions/magpie/icons/opencode.svg +1 -0
- package/extensions/magpie/icons/openmausbot.svg +1 -0
- package/extensions/magpie/icons/openrouter.svg +1 -0
- package/extensions/magpie/icons/openwebui.svg +1 -0
- package/extensions/magpie/icons/parasail.svg +1 -0
- package/extensions/magpie/icons/penguin-color.svg +1 -0
- package/extensions/magpie/icons/perplexity-color.svg +1 -0
- package/extensions/magpie/icons/phala-color.svg +1 -0
- package/extensions/magpie/icons/pi.svg +1 -0
- package/extensions/magpie/icons/pipellm-color.svg +5 -0
- package/extensions/magpie/icons/poe.svg +1 -0
- package/extensions/magpie/icons/ppio-color.svg +1 -0
- package/extensions/magpie/icons/prime-agent.svg +1 -0
- package/extensions/magpie/icons/qiniu-color.svg +1 -0
- package/extensions/magpie/icons/qoder.svg +1 -0
- package/extensions/magpie/icons/qwen-color.svg +1 -0
- package/extensions/magpie/icons/qwenpaw.svg +1 -0
- package/extensions/magpie/icons/raycast.svg +1 -0
- package/extensions/magpie/icons/reasonix-color.svg +48 -0
- package/extensions/magpie/icons/reka.svg +1 -0
- package/extensions/magpie/icons/replicate.svg +1 -0
- package/extensions/magpie/icons/roocode.svg +1 -0
- package/extensions/magpie/icons/rtk.png +0 -0
- package/extensions/magpie/icons/sambanova-color.svg +1 -0
- package/extensions/magpie/icons/sensenova-color.svg +1 -0
- package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
- package/extensions/magpie/icons/snow.png +0 -0
- package/extensions/magpie/icons/snowflake-color.svg +1 -0
- package/extensions/magpie/icons/sophnet-color.svg +1 -0
- package/extensions/magpie/icons/spark-color.svg +1 -0
- package/extensions/magpie/icons/stepfun-color.svg +1 -0
- package/extensions/magpie/icons/straico-color.svg +1 -0
- package/extensions/magpie/icons/submodel-color.svg +1 -0
- package/extensions/magpie/icons/t3code.svg +4 -0
- package/extensions/magpie/icons/targon-color.svg +1 -0
- package/extensions/magpie/icons/tencent-color.svg +1 -0
- package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
- package/extensions/magpie/icons/tiangong-color.svg +1 -0
- package/extensions/magpie/icons/together-color.svg +1 -0
- package/extensions/magpie/icons/trae.svg +1 -0
- package/extensions/magpie/icons/typesafe.png +0 -0
- package/extensions/magpie/icons/upstage-color.svg +1 -0
- package/extensions/magpie/icons/venice-color.svg +1 -0
- package/extensions/magpie/icons/vercel.svg +1 -0
- package/extensions/magpie/icons/vertexai-color.svg +1 -0
- package/extensions/magpie/icons/vllm-color.svg +1 -0
- package/extensions/magpie/icons/volcengine-color.svg +1 -0
- package/extensions/magpie/icons/voyage-color.svg +1 -0
- package/extensions/magpie/icons/vscode.svg +1 -0
- package/extensions/magpie/icons/wenxin-color.svg +1 -0
- package/extensions/magpie/icons/windsurf.svg +1 -0
- package/extensions/magpie/icons/workbuddy-color.svg +1 -0
- package/extensions/magpie/icons/workersai-color.svg +1 -0
- package/extensions/magpie/icons/xai.svg +1 -0
- package/extensions/magpie/icons/xiaomimimo.svg +1 -0
- package/extensions/magpie/icons/xinference-color.svg +1 -0
- package/extensions/magpie/icons/yi.svg +1 -0
- package/extensions/magpie/icons/yuanbao-color.svg +1 -0
- package/extensions/magpie/icons/yylx.svg +5 -0
- package/extensions/magpie/icons/zai.svg +1 -0
- package/extensions/magpie/icons/zcode.png +0 -0
- package/extensions/magpie/icons/zed.svg +1 -0
- package/extensions/magpie/icons/zenmux.svg +1 -0
- package/extensions/magpie/icons/zeroone.svg +1 -0
- package/extensions/magpie/icons/zhipu-color.svg +1 -0
- package/extensions/magpie/icons/zoo-code.png +0 -0
- package/extensions/magpie/index.html +572 -0
- package/extensions/magpie/kernel.js +274 -0
- package/extensions/magpie/keyspend.js +63 -0
- package/extensions/magpie/layout.js +26 -0
- package/extensions/magpie/manifest.json +37 -0
- package/extensions/magpie/navlamp.js +47 -0
- package/extensions/magpie/omarchy.css +72 -0
- package/extensions/magpie/omarchy.js +46 -0
- package/extensions/magpie/passerrors.js +21 -0
- package/extensions/magpie/pluginpages.js +405 -0
- package/extensions/magpie/plugins/gateway/client.js +1194 -0
- package/extensions/magpie/plugins/gateway/keys.js +972 -0
- package/extensions/magpie/plugins/gateway/settings.js +459 -0
- package/extensions/magpie/plugins/library/client.js +6788 -0
- package/extensions/magpie/plugins/library/i18n.js +3228 -0
- package/extensions/magpie/plugins/library/library.css +848 -0
- package/extensions/magpie/plugins/otel/i18n.js +48 -0
- package/extensions/magpie/plugins/otel/page.js +189 -0
- package/extensions/magpie/plugins/redact/client.js +46 -0
- package/extensions/magpie/plugins/redact/demo.js +461 -0
- package/extensions/magpie/plugins/redact/i18n.js +432 -0
- package/extensions/magpie/plugins/redact/page.js +985 -0
- package/extensions/magpie/plugins/redact/redact.css +385 -0
- package/extensions/magpie/plugins/redact/settings.js +248 -0
- package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
- package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
- package/extensions/magpie/plugins/routing/client.js +8593 -0
- package/extensions/magpie/plugins/routing/i18n.js +2696 -0
- package/extensions/magpie/plugins/routing/routing.css +1111 -0
- package/extensions/magpie/plugins/search/settings.js +460 -0
- package/extensions/magpie/plugins/sessions/client.js +1622 -0
- package/extensions/magpie/plugins/sessions/i18n.js +360 -0
- package/extensions/magpie/plugins/sessions/sessions.css +181 -0
- package/extensions/magpie/plugins/sessions/settings.js +100 -0
- package/extensions/magpie/plugins/usage/client.js +150 -0
- package/extensions/magpie/plugins/usage/i18n.js +984 -0
- package/extensions/magpie/plugins/usage/page.js +4307 -0
- package/extensions/magpie/plugins.css +319 -0
- package/extensions/magpie/plugins.js +1926 -0
- package/extensions/magpie/profilerename.js +27 -0
- package/extensions/magpie/purposes.js +2 -0
- package/extensions/magpie/quitask.js +79 -0
- package/extensions/magpie/quotaall.js +142 -0
- package/extensions/magpie/ratewindows.js +123 -0
- package/extensions/magpie/registry.js +87 -0
- package/extensions/magpie/resetask.js +137 -0
- package/extensions/magpie/ruleedit.js +53 -0
- package/extensions/magpie/scrollends.js +91 -0
- package/extensions/magpie/settingshints.js +75 -0
- package/extensions/magpie/signsheets.js +86 -0
- package/extensions/magpie/start.js +26 -0
- package/extensions/magpie/syncfiles.js +216 -0
- package/extensions/magpie/theme.js +29 -0
- package/extensions/magpie/traywindows.js +41 -0
- package/extensions/magpie/webfonts.js +130 -0
- package/extensions/magpie/whatsnew.js +252 -0
- package/extensions/magpie.zip +0 -0
- package/knowledge/apps/baijiahao.md +30 -0
- package/knowledge/apps/bilibili.md +1 -0
- package/knowledge/apps/dedao.md +33 -0
- package/knowledge/apps/feishu.md +35 -0
- package/knowledge/apps/gmail.md +33 -0
- package/knowledge/apps/jike.md +31 -0
- package/knowledge/apps/kuaishou.md +34 -0
- package/knowledge/apps/linkedin.md +33 -0
- package/knowledge/apps/qqmail.md +30 -0
- package/knowledge/apps/telegram.md +27 -0
- package/knowledge/apps/toutiao.md +32 -0
- package/knowledge/apps/uber-eats.md +26 -0
- package/knowledge/apps/uber.md +27 -0
- package/knowledge/apps/whatsapp.md +27 -0
- package/knowledge/apps/x.md +32 -0
- package/knowledge/apps/xiaohongshu.md +1 -0
- package/knowledge/apps/zhihu.md +1 -0
- package/knowledge/sites/bilibili.com/SITE.md +50 -0
- package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
- package/knowledge/sites/github.com/SITE.md +51 -0
- package/knowledge/sites/github.com/references/repo-fields.md +13 -0
- package/knowledge/sites/x.com/SITE.md +47 -0
- package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
- package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
- package/knowledge/sites/youtube.com/SITE.md +49 -0
- package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
- package/knowledge/sites/zhihu.com/SITE.md +47 -0
- package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
- package/lib/mobile/extension-catalog.js +41 -1
- package/lib/mobile/extension-data.js +224 -0
- package/lib/mobile/extension-dev.js +3 -1
- package/lib/mobile/file-ops.js +252 -0
- package/lib/mobile/history-derive.js +80 -4
- package/lib/mobile/host-bridge/endpoints.js +48 -6
- package/lib/mobile/host-bridge/event-synthesis.js +33 -6
- package/lib/mobile/jobs-control.js +118 -0
- package/lib/mobile/memory.js +635 -0
- package/lib/mobile/model-badges.js +77 -0
- package/lib/mobile/pet-tools.js +50 -4
- package/lib/mobile/pets.js +214 -18
- package/lib/mobile/phone/access.js +85 -0
- package/lib/mobile/phone/evidence.js +228 -0
- package/lib/mobile/phone/gate.js +290 -0
- package/lib/mobile/phone/grounding.js +314 -0
- package/lib/mobile/phone/guard.js +465 -0
- package/lib/mobile/phone/irreversible.js +88 -8
- package/lib/mobile/phone/knowledge-tools.js +665 -0
- package/lib/mobile/phone/learn.js +329 -0
- package/lib/mobile/phone/redact.js +237 -0
- package/lib/mobile/phone/risk.js +95 -0
- package/lib/mobile/phone/routes.js +28 -1
- package/lib/mobile/phone/service.js +23 -0
- package/lib/mobile/phone/site-commands.js +271 -0
- package/lib/mobile/phone/step-summary.js +201 -0
- package/lib/mobile/phone/stop-summary.js +235 -0
- package/lib/mobile/phone/tool-groups.js +125 -0
- package/lib/mobile/phone/tools.js +589 -0
- package/lib/mobile/phone/untrusted.js +65 -0
- package/lib/mobile/phone-knowledge.js +20 -4
- package/lib/mobile/schedule-edit.js +500 -0
- package/lib/mobile/schedule.js +5 -0
- package/lib/mobile/send-continuation.js +52 -0
- package/lib/mobile/session-models.js +27 -31
- package/lib/mobile/session-send.js +12 -12
- package/lib/mobile/skill-import.js +338 -0
- package/lib/mobile/skill-store.js +524 -0
- package/lib/routes/mobile-api.js +42 -0
- package/package.json +7 -6
- package/skills/skill-authoring/SKILL.md +46 -0
- package/skills/skill-authoring/references/examples.md +82 -0
- package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
- package/skills/skill-authoring/scripts/check_skill.py +167 -0
- package/skills/skill-authoring/scripts/manifest.json +3 -0
- package/knowledge/sites/bilibili.com.md +0 -27
- package/knowledge/sites/github.com.md +0 -27
- package/knowledge/sites/x.com.md +0 -28
- package/knowledge/sites/xiaohongshu.com.md +0 -27
- package/knowledge/sites/youtube.com.md +0 -28
- package/knowledge/sites/zhihu.com.md +0 -27
|
@@ -0,0 +1,1926 @@
|
|
|
1
|
+
// Plugins: the market of OpenCode plugins. Discover lists the suggested
|
|
2
|
+
// ones and finds the rest on npm; Installed is what was added. Each part is
|
|
3
|
+
// drawn as it comes (#488): /api/plugins, /api/plugins/listings, then
|
|
4
|
+
// /api/plugins/npm; a sign-in happens in the Providers add sheet.
|
|
5
|
+
(() => {
|
|
6
|
+
const page = $("#view-plugins");
|
|
7
|
+
if (!page) return;
|
|
8
|
+
window.mdRender = markdown;
|
|
9
|
+
|
|
10
|
+
let mine = null; // /api/plugins: what's installed
|
|
11
|
+
let listings = null; // /api/plugins/listings: the plugins suggested
|
|
12
|
+
let tagged = null; // /api/plugins/github: repositories their authors tagged magpie-plugin, as listings
|
|
13
|
+
let taggedErr = ""; // why the list isn't GitHub's whole answer: it couldn't be asked, or...
|
|
14
|
+
let taggedKind = ""; // its kind: "slow", "limited" or ""
|
|
15
|
+
let taggedMissing = 0; // ...this many repositories it named couldn't be read
|
|
16
|
+
let topic = "magpie-plugin";
|
|
17
|
+
const npm = {}; // /api/plugins/npm: package → what npm says of it
|
|
18
|
+
const heads = {}; // /api/plugins/git: a git plugin's spec → the commit its repository is at
|
|
19
|
+
let failed = ""; // why what's installed couldn't be loaded
|
|
20
|
+
let failedList = ""; // ...and the plugins suggested
|
|
21
|
+
let tab = "discover", regBox;
|
|
22
|
+
try { tab = localStorage.getItem("magpie.pluginTab") || tab; } catch {}
|
|
23
|
+
let query = "";
|
|
24
|
+
let hits = null; // npm's answer for query: { q, list } | { q, loading } | { q, error }
|
|
25
|
+
let searchTimer = 0;
|
|
26
|
+
const busy = new Map(); // package → "add" | "remove" | "upgrade" | "off"
|
|
27
|
+
let editing = null; // { pkg, text, error }: a middleware's options being edited in its row
|
|
28
|
+
let asking = null; // { pkg, op }: a remove or switch-off waiting on the user's yes
|
|
29
|
+
let checking = false; // Check for updates: npm being asked now
|
|
30
|
+
let checked = null; // ...and what it said: { at, by: spec → check }
|
|
31
|
+
let shown = "";
|
|
32
|
+
|
|
33
|
+
const SEARCH = "M7 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M10 10l3 3";
|
|
34
|
+
const DOWN = "M8 3v7.5 M4.8 7.6 8 10.8l3.2-3.2 M3.5 13h9";
|
|
35
|
+
const STAR = "M8 2.2l1.8 3.7 4 .6-2.9 2.8.7 4L8 11.4l-3.6 1.9.7-4L2.2 6.5l4-.6z";
|
|
36
|
+
const SHIELD = "M8 2.3 13 4v3.8c0 3-2.1 5.1-5 5.9-2.9-.8-5-2.9-5-5.9V4z M5.8 8l1.6 1.6 2.9-3";
|
|
37
|
+
const OUTL = "M9.5 3.5h3v3 M12.5 3.5 7.5 8.5 M11 9.5v3H3.5V5h3";
|
|
38
|
+
const PLUG = "M6 2v3 M10 2v3 M4.5 5h7v2.5a3.5 3.5 0 0 1-7 0z M8 11v3";
|
|
39
|
+
const PIPE = "M1.5 8h3 M11.5 8h3 M4.5 4.5h7v7h-7z";
|
|
40
|
+
const TERM = "M2 3.5h12v9H2z M4.5 6.5l2 1.5-2 1.5 M8 9.5h3";
|
|
41
|
+
const PAGE = "M2 3.5h12v9H2z M2 6h12 M4 4.75h.01";
|
|
42
|
+
const TOOL = "M10.6 2.4a3 3 0 0 0-3.4 4L2.6 11a1.2 1.2 0 0 0 1.7 1.7l4.6-4.6a3 3 0 0 0 4-3.4l-1.7 1.7-1.6-.3-.3-1.6z";
|
|
43
|
+
const FOLDER = "M14.7 12.7a1.3 1.3 0 0 1-1.3 1.3H2.6a1.3 1.3 0 0 1-1.3-1.3V3.3A1.3 1.3 0 0 1 2.6 2h3.3l1.3 2h6.2a1.3 1.3 0 0 1 1.3 1.3z";
|
|
44
|
+
|
|
45
|
+
// a folder's path is its name whole, as plugin.Name: C:\Users\me@corp\…
|
|
46
|
+
// has an @ that is no version
|
|
47
|
+
const name = (spec) => { if (isPath(spec)) return spec; const i = spec.lastIndexOf("@"); return i > 0 && !spec.includes("/", i) ? spec.slice(0, i) : spec; };
|
|
48
|
+
// a plugin added from a folder is a folder's path, not a package name.
|
|
49
|
+
// a leading \ is one on every system, as plugin.IsPath (#2229): a UNC
|
|
50
|
+
// share (\\server\share\plugin) and a leading-backslash path
|
|
51
|
+
const isPath = (p) => /^file:\/\//.test(p) || /^\./.test(p) || /^[/\\]/.test(p) || /^[A-Za-z]:[\\/]/.test(p);
|
|
52
|
+
// ...and is known by the folder's name, not its whole path
|
|
53
|
+
const label = (spec) => { const n = name(spec); return isPath(n) ? n.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || n : n; };
|
|
54
|
+
// one added from a git repository (github:owner/repo, git+https://…), as
|
|
55
|
+
// the plugin store tells them apart: known by the package it installed
|
|
56
|
+
const isGit = (p) => !isPath(p) && (/^(?:(?:github|gitlab|bitbucket):|git\+[a-z]+:\/\/|git:\/\/)\S+$/.test(p) || /^https?:\/\/(?:www\.)?(?:github\.com|gitlab\.com|bitbucket\.org)\/\S+$/.test(p) || /^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9_.-]+(?:#\S+)?$/.test(p));
|
|
57
|
+
// ...and its page on the web, for the plugin's Source link
|
|
58
|
+
const gitWeb = (p) => {
|
|
59
|
+
const s = p.replace(/#.*$/, "");
|
|
60
|
+
const m = /^(github|gitlab|bitbucket):(.+)$/.exec(s) || (/^[^:]+\/[^:]+$/.test(s) && ["", "github", s]);
|
|
61
|
+
if (m) return "https://" + { github: "github.com", gitlab: "gitlab.com", bitbucket: "bitbucket.org" }[m[1]] + "/" + m[2];
|
|
62
|
+
const u = s.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
63
|
+
return /^https?:\/\//.test(u) ? u : "";
|
|
64
|
+
};
|
|
65
|
+
const lang = () => (document.documentElement.lang || "en").slice(0, 2);
|
|
66
|
+
const summary = (l) => l.summary?.[lang()] || l.summary?.en || l.npm?.description || "";
|
|
67
|
+
const count = (n) => n >= 1e6 ? (n / 1e6).toFixed(n >= 1e7 ? 0 : 1) + "M" : n >= 1e3 ? (n / 1e3).toFixed(n >= 1e4 ? 0 : 1) + "k" : String(n || 0);
|
|
68
|
+
// a repository tagged magpie-plugin is installed by whichever spec
|
|
69
|
+
// names it (github:owner/repo, its page's or a git URL), never by its
|
|
70
|
+
// package's name, which another repository can give too (#3685)
|
|
71
|
+
const ghRepo = (spec) => { const m = /^(?:github:|(?:git\+)?(?:https?|ssh|git):\/\/(?:[^@/\s]+@)?(?:www\.|ssh\.)?github\.com(?::\d+(?=\/))?[/:])([^/#\s]+\/[^/#\s]+?)(?:\.git)?(?:[/#].*)?$/i.exec(spec) || (isGit(spec) && /^([^:/#]+\/[^:/#]+)(?:#.*)?$/.exec(spec)); return m ? m[1].toLowerCase() : ""; };
|
|
72
|
+
const entryOf = (pkg) => {
|
|
73
|
+
const g = (tagged || []).find((x) => x.package === pkg)?.github;
|
|
74
|
+
return mine?.plugins?.find((e) => name(e.spec) === pkg || (g && ghRepo(e.spec) === g.repo.toLowerCase()));
|
|
75
|
+
};
|
|
76
|
+
// npm has the versions of a package: not a folder's, nor a git one's
|
|
77
|
+
const onNPM = (spec) => !isPath(spec) && !isGit(spec);
|
|
78
|
+
// a git one is out of date when its repository is at another commit than
|
|
79
|
+
// the one installed, whatever its version (ARNO); a short sha names the full one
|
|
80
|
+
const sameCommit = (a, b) => !!a && !!b && (a.length > b.length ? a.startsWith(b) : b.startsWith(a));
|
|
81
|
+
const gitBehind = (e) => isGit(e.spec) && !!e.commit && !!e.head && !sameCommit(e.commit, e.head);
|
|
82
|
+
// the providers a plugin signs in to, as the add sheet knows them
|
|
83
|
+
const subsOf = (pkg) => (providers?.plugins || []).filter((x) => name(x.spec) === pkg);
|
|
84
|
+
// as the Add sheet names it
|
|
85
|
+
const subName = (x) => SUBS.find((y) => y.agent === x.id)?.name || x.name;
|
|
86
|
+
// with several, the reader picks one by name
|
|
87
|
+
const signInTo = (b, outs, ev) => {
|
|
88
|
+
if (outs.length === 1) return pluginSignIn(outs[0].id, ev);
|
|
89
|
+
openProtoMenu(b, outs.map((x) => ({ v: x.id, name: subName(x), literalName: true, note: "" })), "",
|
|
90
|
+
(id, e) => pluginSignIn(id, e), "Sign in to which subscription", "", "right");
|
|
91
|
+
};
|
|
92
|
+
// whether version a comes after b in semver's order, as update.Newer
|
|
93
|
+
const newer = (a, b) => {
|
|
94
|
+
const p = (v) => {
|
|
95
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([^+]*))?(?:\+.*)?$/.exec(String(v || "").trim());
|
|
96
|
+
return m && { n: [+m[1], +m[2], +m[3]], pre: m[4] || "" };
|
|
97
|
+
};
|
|
98
|
+
const [x, y] = [p(a), p(b)];
|
|
99
|
+
if (!x || !y) return false;
|
|
100
|
+
for (let i = 0; i < 3; i++) if (x.n[i] !== y.n[i]) return x.n[i] > y.n[i];
|
|
101
|
+
if (x.pre === y.pre) return false;
|
|
102
|
+
if (!x.pre || !y.pre) return !x.pre;
|
|
103
|
+
const [s, u] = [x.pre.split("."), y.pre.split(".")];
|
|
104
|
+
for (let i = 0; i < s.length && i < u.length; i++) {
|
|
105
|
+
const [d, e] = [/^\d+$/.test(s[i]), /^\d+$/.test(u[i])];
|
|
106
|
+
if (d && e) { if (+s[i] !== +u[i]) return +s[i] > +u[i]; }
|
|
107
|
+
else if (d !== e) return e;
|
|
108
|
+
else if (s[i] !== u[i]) return s[i] > u[i];
|
|
109
|
+
}
|
|
110
|
+
return s.length > u.length;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function glyph(d, size = 14, stroke = 1.5) { return svg(d, size, stroke); }
|
|
114
|
+
// kindChip says what a plugin is, as KINDS' words say
|
|
115
|
+
const KINDS = {
|
|
116
|
+
pv: [PLUG, "Provider", "Signs in to a subscription and serves its models"],
|
|
117
|
+
mw: [PIPE, "Middleware", "Runs in magpie's gateway on the requests your agents send and on the replies"],
|
|
118
|
+
ag: [TERM, "Agent", "Adds an agent magpie has no setup of its own for: pick its model on the Agents page and it goes through magpie"],
|
|
119
|
+
tl: [TOOL, "Tools", "Gives every agent tools of its own, through magpie's MCP server"],
|
|
120
|
+
pg: [PAGE, "Page", "Adds a page of its own to the sidebar. It runs shut off from the rest of magpie and can read only what its row lists"],
|
|
121
|
+
pn: [PAGE, "Panels", "Adds panels to magpie's own pages. They run shut off from the rest of magpie and can read only what its row lists"],
|
|
122
|
+
};
|
|
123
|
+
const kindOf = (l) => ({ middleware: "mw", hooks: "mw", agent: "ag", tools: "tl", page: "pg" })[l.kind] || "pv";
|
|
124
|
+
function kindChip(kind, name) {
|
|
125
|
+
const [d, word, tip] = KINDS[kind];
|
|
126
|
+
const c = el("span", "pm-chip kind " + kind);
|
|
127
|
+
c.append(glyph(d, 10, 1.5), el("span", "", t(name || word)));
|
|
128
|
+
c.title = t(tip);
|
|
129
|
+
return c;
|
|
130
|
+
}
|
|
131
|
+
function logo(ic, big, kind) {
|
|
132
|
+
const box = el("span", "pm-logo" + (big ? " big" : ""));
|
|
133
|
+
if (ic) box.append(icon(ic, !big));
|
|
134
|
+
else box.append(glyph({ mw: PIPE, ag: TERM, tl: TOOL, pg: PAGE }[kind] || PUZZLE, big ? 22 : 17, 1.4));
|
|
135
|
+
return box;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// npm's answers, put where the cards and rows read them: on the
|
|
139
|
+
// listing and the entry themselves, so a plugin's page opened before
|
|
140
|
+
// npm answered has them too
|
|
141
|
+
function merge() {
|
|
142
|
+
for (const l of listings || []) if (npm[l.package]) l.npm = npm[l.package];
|
|
143
|
+
for (const e of mine?.plugins || []) if (onNPM(e.spec) && npm[name(e.spec)]?.version) e.latest = npm[name(e.spec)].version;
|
|
144
|
+
for (const e of mine?.plugins || []) if (isGit(e.spec) && heads[e.spec]) e.head = heads[e.spec];
|
|
145
|
+
}
|
|
146
|
+
async function loadMine() {
|
|
147
|
+
try {
|
|
148
|
+
mine = await api("plugins");
|
|
149
|
+
failed = "";
|
|
150
|
+
// a plugin turned on or off brings its page's tab, or takes it
|
|
151
|
+
window.loadPluginPages?.();
|
|
152
|
+
} catch (e) {
|
|
153
|
+
failed = e.message;
|
|
154
|
+
}
|
|
155
|
+
merge();
|
|
156
|
+
}
|
|
157
|
+
// a repository tagged magpie-plugin as the market's cards read a
|
|
158
|
+
// listing: its owner as the publisher, its version, and installed by its
|
|
159
|
+
// spec (its npm name when published there from the repository, else
|
|
160
|
+
// github:owner/repo); its name and summary its package.json's own (#3608)
|
|
161
|
+
const ghListing = (r) => ({
|
|
162
|
+
package: r.spec, name: r.name || r.repo.split("/")[1], kind: r.kind, github: r, icon: r.icon || undefined,
|
|
163
|
+
summary: r.summary || r.description ? { ...(r.description && { en: r.description }), ...r.summary } : undefined,
|
|
164
|
+
own: !!(r.name || r.summary),
|
|
165
|
+
npm: { version: r.version || "", publisher: r.owner, repository: r.url, license: r.license, weekly: 0 },
|
|
166
|
+
});
|
|
167
|
+
// A failed ask is said, with why, list or not (till on Discord); a list
|
|
168
|
+
// had before stands. again: Try again, which asks GitHub at once.
|
|
169
|
+
async function loadTagged(again) {
|
|
170
|
+
try {
|
|
171
|
+
const r = await api("plugins/github" + (again ? "?again=1" : ""));
|
|
172
|
+
const got = (r.repos || []).map(ghListing);
|
|
173
|
+
if (!(r.error && !got.length && tagged?.length)) tagged = got;
|
|
174
|
+
taggedErr = r.error || "";
|
|
175
|
+
taggedKind = r.why || "";
|
|
176
|
+
taggedMissing = r.missing || 0;
|
|
177
|
+
if (r.topic) topic = r.topic;
|
|
178
|
+
} catch (e) {
|
|
179
|
+
taggedErr = e.message || String(e);
|
|
180
|
+
taggedKind = "";
|
|
181
|
+
taggedMissing = 0;
|
|
182
|
+
tagged = tagged || [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function loadListings() {
|
|
186
|
+
try {
|
|
187
|
+
const r = await api("plugins/listings");
|
|
188
|
+
listings = r.listings || [];
|
|
189
|
+
failedList = "";
|
|
190
|
+
} catch (e) {
|
|
191
|
+
failedList = e.message;
|
|
192
|
+
}
|
|
193
|
+
merge();
|
|
194
|
+
}
|
|
195
|
+
// what npm says of the plugins suggested and those installed: drawn
|
|
196
|
+
// when it comes, never waited on
|
|
197
|
+
let npmTries = 0;
|
|
198
|
+
async function askNPM(only) {
|
|
199
|
+
const names = [...new Set([...(listings || []).map((l) => l.package), ...(mine?.plugins || []).filter((e) => onNPM(e.spec)).map((e) => name(e.spec))])]
|
|
200
|
+
.filter((n) => !only || !npm[n]);
|
|
201
|
+
if (!names.length) return;
|
|
202
|
+
try {
|
|
203
|
+
const r = await api("plugins/npm?names=" + encodeURIComponent(names.join(",")));
|
|
204
|
+
Object.assign(npm, r.npm || {});
|
|
205
|
+
} catch {}
|
|
206
|
+
merge();
|
|
207
|
+
// one npm said nothing of: unknown (Install), asked again
|
|
208
|
+
let u = 0;
|
|
209
|
+
for (const l of listings || []) if (!l.npm || l.npm.u) u = l.npm = { weekly: 0, u: 1 };
|
|
210
|
+
if (u && npmTries++ < 4) setTimeout(() => askNPM(true), 15000);
|
|
211
|
+
drawBody();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// the commit each git plugin's repository is at now: drawn when it
|
|
215
|
+
// comes, never waited on (only: when one, just added, isn't known yet)
|
|
216
|
+
async function askGit(only) {
|
|
217
|
+
if (!(mine?.plugins || []).some((e) => isGit(e.spec) && (!only || !heads[e.spec]))) return;
|
|
218
|
+
try {
|
|
219
|
+
const r = await api("plugins/git");
|
|
220
|
+
Object.assign(heads, r.heads || {});
|
|
221
|
+
} catch { return; }
|
|
222
|
+
merge();
|
|
223
|
+
drawBody();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function load() {
|
|
227
|
+
if (!mine && !listings) draw();
|
|
228
|
+
const parts = [
|
|
229
|
+
loadMine().then(() => { draw(); window.renderPluginDot?.(); }),
|
|
230
|
+
loadListings().then(draw),
|
|
231
|
+
loadTagged().then(drawBody),
|
|
232
|
+
loadBuiltins(),
|
|
233
|
+
providers ? null : loadProviders().then(drawBody, () => {}),
|
|
234
|
+
];
|
|
235
|
+
await Promise.all(parts);
|
|
236
|
+
askNPM();
|
|
237
|
+
askGit();
|
|
238
|
+
}
|
|
239
|
+
window.loadPlugins = load;
|
|
240
|
+
// the sidebar's dot, followed: Plugins opens on Installed, where the
|
|
241
|
+
// plugins with an update are named at the top
|
|
242
|
+
window.pluginTabTo = (id) => { tab = id; };
|
|
243
|
+
// pluginQuery: Discover, looking for q (the add sheet found nothing by it)
|
|
244
|
+
window.pluginQuery = (q) => {
|
|
245
|
+
query = q;
|
|
246
|
+
tab = "discover";
|
|
247
|
+
clearTimeout(searchTimer);
|
|
248
|
+
hits = q.trim().length >= 2 ? { q: q.trim(), loading: true } : null;
|
|
249
|
+
if (hits) searchNPM(hits.q);
|
|
250
|
+
draw();
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// pluginAgentsTo: Discover, at its Agents (the Agents page's More in
|
|
254
|
+
// Plugins). The scroll is the reader's click's, so it is made only while
|
|
255
|
+
// the click is fresh: Agents drawn later, the listings slow, is left
|
|
256
|
+
// where the page is.
|
|
257
|
+
let agentsFor = null; // the click that asked for Agents, till they are drawn
|
|
258
|
+
window.pluginAgentsTo = (ev) => {
|
|
259
|
+
query = "";
|
|
260
|
+
tab = "discover";
|
|
261
|
+
clearTimeout(searchTimer);
|
|
262
|
+
hits = null;
|
|
263
|
+
agentsFor = ev;
|
|
264
|
+
draw();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
async function refresh() {
|
|
268
|
+
// a plugin installed, switched on or off, or removed brings its
|
|
269
|
+
// page's tab, or takes it, now: not on the Plugins page's next load
|
|
270
|
+
window.loadPluginPages?.();
|
|
271
|
+
try {
|
|
272
|
+
const [m, p] = await Promise.all([api("plugins"), api("providers")]);
|
|
273
|
+
mine = m;
|
|
274
|
+
providers = p;
|
|
275
|
+
merge();
|
|
276
|
+
// the Providers page owns the dialog as it draws: only drawn when shown
|
|
277
|
+
if (view === "providers") renderProviders();
|
|
278
|
+
} catch (e) { statusErr(e); }
|
|
279
|
+
draw();
|
|
280
|
+
window.renderPluginDot?.();
|
|
281
|
+
askNPM(true);
|
|
282
|
+
askGit(true);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function act(pkg, op, body, done) {
|
|
286
|
+
if (busy.size && op !== "off") return;
|
|
287
|
+
busy.set(pkg, op);
|
|
288
|
+
draw();
|
|
289
|
+
try {
|
|
290
|
+
await api("plugins/" + op, body);
|
|
291
|
+
busy.delete(pkg);
|
|
292
|
+
for (const e of mine?.plugins || []) if (op === "update" || e.spec === body?.spec) delete heads[e.spec]; // stale once installed (#4802)
|
|
293
|
+
await refresh();
|
|
294
|
+
if (done) done();
|
|
295
|
+
} catch (e) {
|
|
296
|
+
busy.delete(pkg);
|
|
297
|
+
draw();
|
|
298
|
+
statusErr(e);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Check for updates: npm asked now, not what it said within the hour,
|
|
302
|
+
// for each installed plugin's newest version. Each row then says so (an
|
|
303
|
+
// update, up to date, or why it couldn't be checked), and its Update, or
|
|
304
|
+
// Update all, installs it as before. Nothing is installed by the check.
|
|
305
|
+
async function checkUpdates() {
|
|
306
|
+
if (checking || busy.size) return;
|
|
307
|
+
checking = true;
|
|
308
|
+
draw();
|
|
309
|
+
try {
|
|
310
|
+
const r = await api("plugins/check", {});
|
|
311
|
+
mine = r.state || mine;
|
|
312
|
+
const by = {};
|
|
313
|
+
for (const c of r.plugins || []) {
|
|
314
|
+
by[c.spec] = c;
|
|
315
|
+
// npm's answer now is the one the rows read, and a git one's
|
|
316
|
+
// repository's
|
|
317
|
+
if (c.latest) npm[c.package] = { ...(npm[c.package] || {}), version: c.latest };
|
|
318
|
+
if (c.head) heads[c.spec] = c.head;
|
|
319
|
+
}
|
|
320
|
+
checked = { at: r.at, by };
|
|
321
|
+
merge();
|
|
322
|
+
const asked = (r.plugins || []).filter((c) => c.status === "update" || c.status === "current" || c.status === "unknown");
|
|
323
|
+
const ups = asked.filter((c) => c.status === "update" && !c.off).length;
|
|
324
|
+
const unknown = asked.filter((c) => c.status === "unknown");
|
|
325
|
+
if (asked.length && unknown.length === asked.length) {
|
|
326
|
+
status(t("Couldn't check for updates: {error}", { error: whyText(unknown[0]) }), "err", 8000);
|
|
327
|
+
} else {
|
|
328
|
+
// with some not checked, "every plugin" isn't known to be up to
|
|
329
|
+
// date: only the ones checked are (#1942)
|
|
330
|
+
let msg = ups ? (ups === 1 ? t("1 plugin has an update") : t("{n} plugins have updates", { n: ups }))
|
|
331
|
+
: unknown.length ? t("No updates for the plugins checked") : t("Every plugin is up to date");
|
|
332
|
+
if (unknown.length) {
|
|
333
|
+
msg += " · " + t("{n} couldn't be checked: {error}", { n: unknown.length, error: whyText(unknown[0]) });
|
|
334
|
+
status(msg, "warn", 8000);
|
|
335
|
+
} else status(msg, "ok");
|
|
336
|
+
}
|
|
337
|
+
} catch (e) {
|
|
338
|
+
status(t("Couldn't check for updates: {error}", { error: e.message }), "err", 8000);
|
|
339
|
+
}
|
|
340
|
+
checking = false;
|
|
341
|
+
draw();
|
|
342
|
+
window.renderPluginDot?.();
|
|
343
|
+
}
|
|
344
|
+
// why npm said nothing of a plugin, in the page's words; a git one's
|
|
345
|
+
// repository, in its own
|
|
346
|
+
function whyText(c) {
|
|
347
|
+
if (isGit(c.spec || "")) return t("Couldn't ask {repo} for its newest commit: {error}", { repo: c.spec, error: c.limited ? limitedText(c.limited) : c.error || "" });
|
|
348
|
+
switch (c.why) {
|
|
349
|
+
case "offline": return t("npm couldn't be reached — check your connection or proxy");
|
|
350
|
+
case "limited": return t("npm is turning requests away for now (too many); try again in a few minutes");
|
|
351
|
+
case "missing": return t("npm doesn't have it");
|
|
352
|
+
default: return t("npm answered with an error: {error}", { error: c.error || "" });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// GitHub's rate limit used up, in the page's words as the library's
|
|
356
|
+
// skill check says it: when it lifts, and how a token raises it (#1942)
|
|
357
|
+
function limitedText(l) {
|
|
358
|
+
const d = l.until ? new Date(l.until) : null;
|
|
359
|
+
const time = d && !isNaN(d) ? d.toLocaleTimeString(intlLang(), { hour: "2-digit", minute: "2-digit" }) : "";
|
|
360
|
+
if (l.token) return time ? t("GitHub's rate limit for your GitHub token is used up until {time}", { time })
|
|
361
|
+
: t("GitHub's rate limit for your GitHub token is used up for now");
|
|
362
|
+
return time ? t("GitHub allows 60 requests an hour without a token, used up until {time}. Add a GitHub token in Settings → Network and sharing to raise it to 5,000.", { time })
|
|
363
|
+
: t("GitHub allows 60 requests an hour without a token, used up for now. Add a GitHub token in Settings → Network and sharing to raise it to 5,000.");
|
|
364
|
+
}
|
|
365
|
+
const checkedAt = () => checked ? new Date(checked.at).toLocaleTimeString(intlLang(), { hour: "2-digit", minute: "2-digit" }) : "";
|
|
366
|
+
|
|
367
|
+
// the built-ins with accounts this plugin could run in their place
|
|
368
|
+
const movableOf = (pkg) => (mine?.movable || []).filter((c) => c.package === pkg);
|
|
369
|
+
// move: a built-in's accounts onto its plugin, installing it if need be,
|
|
370
|
+
// as its editor's Runs on does
|
|
371
|
+
async function move(c) {
|
|
372
|
+
if (busy.size) return;
|
|
373
|
+
busy.set(c.package, "move");
|
|
374
|
+
asking = null;
|
|
375
|
+
draw();
|
|
376
|
+
try {
|
|
377
|
+
providers = await api("provider/move", { id: c.id });
|
|
378
|
+
busy.delete(c.package);
|
|
379
|
+
await refresh();
|
|
380
|
+
const now = providers.providers.find((x) => x.id === c.id);
|
|
381
|
+
const accts = now?.account?.logins?.length || c.accounts, models = (now?.models || []).filter((x) => x.on).length;
|
|
382
|
+
status(t("{name} now runs on its plugin — {accounts}, {models}. Its editor in Providers moves it back.", {
|
|
383
|
+
name: c.name,
|
|
384
|
+
accounts: t(accts === 1 ? "{n} account" : "{n} accounts", { n: accts }),
|
|
385
|
+
models: t(models === 1 ? "{n} model" : "{n} models", { n: models }),
|
|
386
|
+
}), "ok", 9000);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
busy.delete(c.package);
|
|
389
|
+
draw();
|
|
390
|
+
// no editor here to sign in above: say where
|
|
391
|
+
const why = e.why?.code === "lapsed"
|
|
392
|
+
? t("every {name} account needs signing in again. Sign one in under Providers, then move.", { name: c.name })
|
|
393
|
+
: moveWhy({ why: e.why, error: e.message }, { name: c.name });
|
|
394
|
+
status(t("It stays built-in: {error}", { error: why }), "err", 12000);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const moveButton = (b, c, here) => {
|
|
398
|
+
// one word beside the name, as Install is: the card already says whose
|
|
399
|
+
// accounts, the title says how many and what it does (a whole sentence
|
|
400
|
+
// was a solid bar under the card that broke onto two lines)
|
|
401
|
+
b.classList.add("get", "move");
|
|
402
|
+
b.textContent = t("Move");
|
|
403
|
+
b.title = (here ? t("Move {name} here", { name: c.name }) : t(c.accounts === 1 ? "Move my {n} {name} account" : "Move my {n} {name} accounts", { name: c.name, n: c.accounts }))
|
|
404
|
+
+ " — " + t("Runs {name} on this plugin in place of the built-in, with the same accounts; nothing changes if one doesn't work through it", { name: c.name });
|
|
405
|
+
b.disabled = busy.size > 0;
|
|
406
|
+
b.onclick = (ev) => { ev.stopPropagation(); move(c); };
|
|
407
|
+
return b;
|
|
408
|
+
};
|
|
409
|
+
// the installed plugin spec's Install would replace (plugins/ holds one
|
|
410
|
+
// package of a name, plugin.Add), both named by source (#3685). typed:
|
|
411
|
+
// the field's, where a bare name may be a community plugin's and an
|
|
412
|
+
// unread repository's package is unknown: neither is asked about.
|
|
413
|
+
const sourceOf = (spec) => isGit(spec) ? spec : "npm:" + name(spec.replace(/^npm:\s*/, ""));
|
|
414
|
+
const namesakeOf = (spec, typed) => {
|
|
415
|
+
const r = ghRepo(spec);
|
|
416
|
+
const g = (tagged || []).find((x) => x.package === spec || (r && x.github.repo.toLowerCase() === r))?.github;
|
|
417
|
+
const as = g ? g.package || "" : spec.startsWith("npm:") ? name(spec.slice(4).trim()) : onNPM(spec) && (!typed || spec.startsWith("@")) ? name(spec) : "";
|
|
418
|
+
const e = as && mine?.plugins?.find((x) => (isGit(x.spec) ? x.package : onNPM(x.spec) && name(x.spec)) === as);
|
|
419
|
+
// no other plugin: the same repository, or npm's at another version
|
|
420
|
+
if (!e || (r ? ghRepo(e.spec) === r : !isGit(spec) && !isGit(e.spec))) return null;
|
|
421
|
+
return { old: sourceOf(e.spec), new: sourceOf(spec), package: as, off: !!e.off };
|
|
422
|
+
};
|
|
423
|
+
// false when the user keeps the plugin spec would replace
|
|
424
|
+
const replaceOK = async (spec, typed) => {
|
|
425
|
+
const was = namesakeOf(spec, typed);
|
|
426
|
+
if (!was) return true;
|
|
427
|
+
return confirmAction(t("Replace {old} with {new}?", was), was.off
|
|
428
|
+
? t("{new} and {old} are both the package {package}, and only one of them can be installed: {old} is removed, and its options and sign-ins pass to {new}. {old} is off, so {new} is installed off too.", was)
|
|
429
|
+
: t("{new} and {old} are both the package {package}, and only one of them can be installed: {old} is removed, and its options and sign-ins pass to {new}.", was), t("Replace"));
|
|
430
|
+
};
|
|
431
|
+
const install = async (pkg, nm) => {
|
|
432
|
+
if (!await replaceOK(pkg)) return;
|
|
433
|
+
act(pkg, "add", { spec: pkg }, () => {
|
|
434
|
+
const subs = subsOf(pkg);
|
|
435
|
+
status(subs.length ? t("{name} is installed — sign in to use it", { name: nm }) : t("{name} is installed", { name: nm }), "ok");
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// A plugin's Update, wherever it is offered: npm has a newer version, or
|
|
440
|
+
// a git one's repository a newer commit. Makes b that button and says
|
|
441
|
+
// whether there is one.
|
|
442
|
+
function updateButton(b, e) {
|
|
443
|
+
if (!e || e.off) return false;
|
|
444
|
+
const npm = e.latest && e.version && newer(e.latest, e.version);
|
|
445
|
+
if (!npm && !gitBehind(e)) return false;
|
|
446
|
+
const pkg = name(e.spec);
|
|
447
|
+
const l = (listings || []).find((x) => x.package === pkg);
|
|
448
|
+
b.classList.add("pm-act", "pm-up");
|
|
449
|
+
b.type = "button";
|
|
450
|
+
b.textContent = busy.get(pkg) === "upgrade" || busy.get(e.spec) === "upgrade" ? t("Updating…") : t("Update");
|
|
451
|
+
b.disabled = busy.size > 0 || checking;
|
|
452
|
+
if (npm) {
|
|
453
|
+
b.title = t("Update to {v}", { v: "v" + e.latest });
|
|
454
|
+
// the community's would come by itself: Update brings it now
|
|
455
|
+
if (pkg.startsWith("@magpie-community/") && !/@(?!latest$)[^@/]+$/.test(e.spec.slice(pkg.length))) b.title += "\n" + t("magpie updates it by itself within the hour; Update does it now");
|
|
456
|
+
} else b.title = t("Fetches it from {repo} again", { repo: e.spec }) + "\n" + t("{have} installed, {repo} is at {head}", { have: e.commit, repo: e.spec, head: e.head });
|
|
457
|
+
b.onclick = (ev) => {
|
|
458
|
+
ev.stopPropagation();
|
|
459
|
+
const was = e.commit;
|
|
460
|
+
act(pkg, "upgrade", { spec: e.spec }, () => {
|
|
461
|
+
const now = mine?.plugins?.find((x) => x.spec === e.spec)?.commit;
|
|
462
|
+
status(npm ? t("{name} updated to v{v}", { name: l?.name || pkg, v: e.latest }) : now && was && !sameCommit(now, was) ? t("{name} updated to {commit}", { name: shownName(e), commit: now }) : t("{name} is up to date", { name: shownName(e) }), "ok");
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
b.onkeydown = (ev) => ev.stopPropagation();
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// the one button a plugin shows: what to do with it next
|
|
470
|
+
function actionFor(pkg, nm, listed) {
|
|
471
|
+
const b = el("button", "pm-act");
|
|
472
|
+
const e = entryOf(pkg);
|
|
473
|
+
const op = busy.get(pkg);
|
|
474
|
+
if (op === "add" || op === "upgrade" || op === "move") {
|
|
475
|
+
b.classList.add("busy");
|
|
476
|
+
if (op === "move") b.classList.add("move");
|
|
477
|
+
b.append(el("span", "spin"), el("span", "", op === "move" ? t("Moving…") : op === "upgrade" ? t("Updating…") : mine?.bun ? t("Installing…") : t("Getting Bun…")));
|
|
478
|
+
b.disabled = true;
|
|
479
|
+
if (!mine?.bun) b.title = t("Plugins run on Bun {v}, downloaded once", { v: mine?.bunVersion || "" });
|
|
480
|
+
return b;
|
|
481
|
+
}
|
|
482
|
+
// not known yet whether it's installed, or whether npm has it: a
|
|
483
|
+
// button that waits, rather than one that says the wrong thing
|
|
484
|
+
if (!mine || (!e && listed && !listed.npm)) {
|
|
485
|
+
b.classList.add("busy");
|
|
486
|
+
b.append(el("span", "spin"));
|
|
487
|
+
b.disabled = true;
|
|
488
|
+
return b;
|
|
489
|
+
}
|
|
490
|
+
if (!e) {
|
|
491
|
+
if (listed && !listed.github && !listed.npm?.version && !listed.npm.u) {
|
|
492
|
+
b.textContent = t("Coming soon");
|
|
493
|
+
b.disabled = true;
|
|
494
|
+
b.title = t("Not on npm yet");
|
|
495
|
+
return b;
|
|
496
|
+
}
|
|
497
|
+
const c = movableOf(pkg)[0];
|
|
498
|
+
if (c) return moveButton(b, c);
|
|
499
|
+
b.classList.add("get");
|
|
500
|
+
b.textContent = t("Install");
|
|
501
|
+
b.disabled = busy.size > 0;
|
|
502
|
+
b.onclick = (ev) => { ev.stopPropagation(); install(pkg, nm); };
|
|
503
|
+
return b;
|
|
504
|
+
}
|
|
505
|
+
if (e.off) {
|
|
506
|
+
b.textContent = t("Switch on");
|
|
507
|
+
b.onclick = (ev) => { ev.stopPropagation(); act(pkg, "off", { spec: e.spec, off: false }); };
|
|
508
|
+
return b;
|
|
509
|
+
}
|
|
510
|
+
const subs = subsOf(pkg);
|
|
511
|
+
const out = subs.find((x) => !x.signedIn);
|
|
512
|
+
// signed in to nothing yet, with a built-in's accounts to bring
|
|
513
|
+
const c = !subs.some((x) => x.signedIn) && movableOf(pkg)[0];
|
|
514
|
+
if (c) return moveButton(b, c, true);
|
|
515
|
+
// one of its subscriptions signed in (Qoder, beside Qoder CN; #556): the
|
|
516
|
+
// card says it is, rather than a Sign in that reads as signed out; the
|
|
517
|
+
// Installed row offers the others by name
|
|
518
|
+
const on = subs.find((x) => x.signedIn);
|
|
519
|
+
if (out && !on) {
|
|
520
|
+
b.classList.add("go");
|
|
521
|
+
b.textContent = t("Sign in");
|
|
522
|
+
b.title = subs.length > 1 ? t("Sign in to {name}", { name: subs.map(subName).join(t(", ")) }) : "";
|
|
523
|
+
b.onclick = (ev) => { ev.stopPropagation(); signInTo(b, subs, ev); };
|
|
524
|
+
return b;
|
|
525
|
+
}
|
|
526
|
+
// an update out: the one button says so in place of Signed in or
|
|
527
|
+
// Installed, the size of Install in a colour of its own, as an app
|
|
528
|
+
// store's Update takes Open's place (#1587). Sign in, not done yet,
|
|
529
|
+
// comes first; the Installed row offers its Update then
|
|
530
|
+
if (updateButton(b, e)) return b;
|
|
531
|
+
if (on) {
|
|
532
|
+
b.classList.add("done");
|
|
533
|
+
b.append(glyph(CHECK, 11, 2), el("span", "", t("Signed in")));
|
|
534
|
+
b.title = t("Open {name} in Providers", { name: on.name });
|
|
535
|
+
b.onclick = (ev) => { ev.stopPropagation(); openProvider(on.id); };
|
|
536
|
+
return b;
|
|
537
|
+
}
|
|
538
|
+
b.classList.add("done");
|
|
539
|
+
b.append(glyph(CHECK, 11, 2), el("span", "", t("Installed")));
|
|
540
|
+
b.disabled = true;
|
|
541
|
+
return b;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function card(l) {
|
|
545
|
+
const c = el("div", "pm-card");
|
|
546
|
+
c.tabIndex = 0;
|
|
547
|
+
c.setAttribute("role", "button");
|
|
548
|
+
c.dataset.pkg = l.package;
|
|
549
|
+
const e = entryOf(l.package);
|
|
550
|
+
if (e) c.classList.add("have");
|
|
551
|
+
const top = el("div", "pm-top");
|
|
552
|
+
const who = el("div", "pm-who");
|
|
553
|
+
const nm = el("div", "pm-name");
|
|
554
|
+
nm.append(el("b", "", l.name));
|
|
555
|
+
who.append(nm);
|
|
556
|
+
const by = el("div", "pm-by");
|
|
557
|
+
if (l.community) {
|
|
558
|
+
const v = el("span", "pm-verified");
|
|
559
|
+
v.append(glyph(SHIELD, 11, 1.5), el("span", "", t("magpie community")));
|
|
560
|
+
v.title = t("Written for magpie by its community, in github.com/magpie-community/plugins");
|
|
561
|
+
by.append(v);
|
|
562
|
+
} else by.append(el("span", "", l.npm?.publisher || l.package));
|
|
563
|
+
who.append(by);
|
|
564
|
+
if (l.github) c.classList.add("gh");
|
|
565
|
+
const go = actionFor(l.package, l.name, l);
|
|
566
|
+
top.append(logo(l.icon || l.npm?.icon, false, kindOf(l)), who, go);
|
|
567
|
+
const sum = el("p", "pm-sum", summary(l));
|
|
568
|
+
const meta = el("div", "pm-meta");
|
|
569
|
+
meta.append(kindChip(kindOf(l), l.kind === "hooks" && "Request hook"));
|
|
570
|
+
if (l.own) {
|
|
571
|
+
const o = el("span", "pm-chip", t("Its own words"));
|
|
572
|
+
o.title = t("Its name and summary are what the package says of itself in its package.json: nobody reviewed them");
|
|
573
|
+
meta.append(o);
|
|
574
|
+
}
|
|
575
|
+
if (l.github) meta.append(stars(l.github.stars));
|
|
576
|
+
if (l.npm?.weekly) {
|
|
577
|
+
const d = el("span", "pm-dl");
|
|
578
|
+
d.append(glyph(DOWN, 11, 1.5), el("span", "", t("{n}/week", { n: count(l.npm.weekly) })));
|
|
579
|
+
d.title = t("{n} downloads on npm last week", { n: l.npm.weekly.toLocaleString(intlLang()) });
|
|
580
|
+
meta.append(d);
|
|
581
|
+
}
|
|
582
|
+
if (l.npm?.version) {
|
|
583
|
+
// an update out, and the card's button is its Update: the version it
|
|
584
|
+
// goes to beside the one installed (#1587)
|
|
585
|
+
const up = go.classList.contains("pm-up") && e.latest && e.version && newer(e.latest, e.version);
|
|
586
|
+
meta.append(el("span", "pm-ver", "v" + (e?.version || l.npm.version) + (up ? " → v" + e.latest : "")));
|
|
587
|
+
}
|
|
588
|
+
if (e?.error && !e.off) meta.append(el("span", "pm-chip bad", t("Didn't load")));
|
|
589
|
+
else if (e?.off) meta.append(el("span", "pm-chip", t("Off")));
|
|
590
|
+
if (l.replaces) {
|
|
591
|
+
const b = el("span", "pm-chip soft", t("Replaces built-in"));
|
|
592
|
+
b.title = t("magpie's own sign-in to this subscription is deprecated: this plugin takes its place, and accounts signed in there move to it");
|
|
593
|
+
meta.append(b);
|
|
594
|
+
}
|
|
595
|
+
c.append(top, sum, meta);
|
|
596
|
+
c.onclick = () => detail(l);
|
|
597
|
+
c.onkeydown = (ev) => { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); detail(l); } };
|
|
598
|
+
return c;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function stars(n) {
|
|
602
|
+
const d = el("span", "pm-dl");
|
|
603
|
+
d.append(glyph(STAR, 11, 1.4), el("span", "", count(n)));
|
|
604
|
+
d.title = t("{n} stars on GitHub", { n: (n || 0).toLocaleString(intlLang()) });
|
|
605
|
+
return d;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// cards still to come, drawn as a card is laid out (its picture, name
|
|
609
|
+
// and author, two lines of summary, its facts) for a light to pass over,
|
|
610
|
+
// one card after the other
|
|
611
|
+
function skeleton(n) {
|
|
612
|
+
const g = el("div", "pm-grid");
|
|
613
|
+
g.setAttribute("aria-hidden", "true");
|
|
614
|
+
const bar = (c) => el("div", "sk " + c);
|
|
615
|
+
for (let i = 0; i < n; i++) {
|
|
616
|
+
const c = el("div", "pm-card ghost");
|
|
617
|
+
c.style.setProperty("--i", i);
|
|
618
|
+
const top = el("div", "gtop"), who = el("div", "gwho");
|
|
619
|
+
who.append(bar("gname"), bar("gby"));
|
|
620
|
+
top.append(bar("glogo"), who, bar("gact"));
|
|
621
|
+
const meta = el("div", "gmeta");
|
|
622
|
+
meta.append(bar("gchip"), bar("gchip short"));
|
|
623
|
+
c.append(top, bar("gline"), bar("gline short"), meta);
|
|
624
|
+
g.append(c);
|
|
625
|
+
}
|
|
626
|
+
return g;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function section(title, hint, list) {
|
|
630
|
+
const box = el("section", "pm-sec");
|
|
631
|
+
const h = el("div", "pm-sechead");
|
|
632
|
+
h.append(el("h3", "", title));
|
|
633
|
+
if (hint) h.append(el("span", "", hint));
|
|
634
|
+
const g = el("div", "pm-grid");
|
|
635
|
+
for (const l of list) g.append(card(l));
|
|
636
|
+
box.append(h, g);
|
|
637
|
+
return box;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// GitHub's section: its own kinds, an installed strip and a field, each
|
|
641
|
+
// counting what the others leave; only the section redraws (#1055)
|
|
642
|
+
let ghKind = ""; // "" (all) | "pv" | "mw" | "ag"
|
|
643
|
+
let ghHave = ""; // "" (all) | "have" | "none"
|
|
644
|
+
let ghQuery = "";
|
|
645
|
+
let ghList = []; // what the page's search left of the repositories
|
|
646
|
+
const ghSec = el("section", "pm-sec pm-ghsec");
|
|
647
|
+
const ghHead = el("div", "pm-sechead");
|
|
648
|
+
const ghBar = el("div", "pm-ghbar");
|
|
649
|
+
const ghGrid = el("div", "pm-grid");
|
|
650
|
+
const ghFind = el("label", "pm-find pm-ghfind");
|
|
651
|
+
const ghQ = el("input");
|
|
652
|
+
ghQ.type = "search";
|
|
653
|
+
ghQ.spellcheck = false;
|
|
654
|
+
ghQ.autocomplete = "off";
|
|
655
|
+
ghQ.oninput = () => { ghQuery = ghQ.value; drawGh(); };
|
|
656
|
+
ghQ.onkeydown = (ev) => { ev.stopPropagation(); if (ev.key === "Escape" && ghQ.value) { ghQ.value = ""; ghQ.oninput(); } };
|
|
657
|
+
ghFind.append(glyph(SEARCH, 13, 1.6), ghQ);
|
|
658
|
+
const ghMatch = (l) => {
|
|
659
|
+
const f = ghQuery.trim().toLowerCase();
|
|
660
|
+
return !f || [l.name, l.github.repo, l.github.owner, l.package, summary(l)].join(" ").toLowerCase().includes(f);
|
|
661
|
+
};
|
|
662
|
+
const ghTitle = () => [el("h3", "", t("Unofficial, on GitHub")), el("span", "", t("repositories their authors tagged {topic} — nobody has reviewed them; read the code before you install one", { topic }))];
|
|
663
|
+
// until GitHub answers, its section stands in place, saying it is looking
|
|
664
|
+
let ghArrive = false;
|
|
665
|
+
function ghWaiting() {
|
|
666
|
+
const look = el("span", "pm-ghlook");
|
|
667
|
+
look.setAttribute("role", "status");
|
|
668
|
+
const dots = el("span", "dots");
|
|
669
|
+
dots.setAttribute("aria-hidden", "true");
|
|
670
|
+
dots.append(el("i"), el("i"), el("i"));
|
|
671
|
+
look.append(dots, el("span", "", t("Looking on GitHub")));
|
|
672
|
+
ghHead.replaceChildren(...ghTitle(), look);
|
|
673
|
+
ghSec.setAttribute("aria-busy", "true");
|
|
674
|
+
ghArrive = true;
|
|
675
|
+
replaceKeeping(ghSec, [ghHead, skeleton(3)]);
|
|
676
|
+
return ghSec;
|
|
677
|
+
}
|
|
678
|
+
// why the list isn't GitHub's whole answer; kept: cards are shown. A
|
|
679
|
+
// known kind in the reader's words, the raw error as its hover
|
|
680
|
+
const ghWhy = (kept) => {
|
|
681
|
+
const error = { slow: t("GitHub didn't answer in time"), limited: t("GitHub is turning requests away for now") }[taggedKind] || taggedErr;
|
|
682
|
+
const s = el("span", "", taggedMissing
|
|
683
|
+
? t(kept ? "{n} more on GitHub couldn't be read: {error}" : "GitHub found {n} repositories, but none of them could be read: {error}", { n: taggedMissing, error })
|
|
684
|
+
: t(kept ? "Couldn't ask GitHub, so these are from before: {error}" : "Couldn't ask GitHub: {error}", { error }));
|
|
685
|
+
if (error !== taggedErr) s.title = taggedErr;
|
|
686
|
+
return s;
|
|
687
|
+
};
|
|
688
|
+
// and one GitHub couldn't be asked of says so, with another go
|
|
689
|
+
function ghFailed() {
|
|
690
|
+
ghHead.replaceChildren(...ghTitle());
|
|
691
|
+
ghSec.removeAttribute("aria-busy");
|
|
692
|
+
const box = el("div", "pm-ghnone");
|
|
693
|
+
const again = el("button", "text", t("Try again"));
|
|
694
|
+
again.type = "button";
|
|
695
|
+
again.onclick = () => {
|
|
696
|
+
tagged = null;
|
|
697
|
+
taggedErr = "";
|
|
698
|
+
taggedMissing = 0;
|
|
699
|
+
drawBody();
|
|
700
|
+
loadTagged(true).then(drawBody);
|
|
701
|
+
};
|
|
702
|
+
box.append(ghWhy(false), again);
|
|
703
|
+
replaceKeeping(ghSec, [ghHead, box]);
|
|
704
|
+
return ghSec;
|
|
705
|
+
}
|
|
706
|
+
// the cards GitHub could give, and a line saying what it couldn't, with
|
|
707
|
+
// another go that keeps them until it answers
|
|
708
|
+
function ghNote() {
|
|
709
|
+
const note = el("div", "pm-ghwarn");
|
|
710
|
+
note.setAttribute("role", "status");
|
|
711
|
+
const again = el("button", "text", t("Try again"));
|
|
712
|
+
again.type = "button";
|
|
713
|
+
again.onclick = () => {
|
|
714
|
+
again.disabled = true;
|
|
715
|
+
loadTagged(true).then(drawBody);
|
|
716
|
+
};
|
|
717
|
+
note.append(ghWhy(true), again);
|
|
718
|
+
return note;
|
|
719
|
+
}
|
|
720
|
+
function ghSection(list) {
|
|
721
|
+
ghList = list;
|
|
722
|
+
ghHead.replaceChildren(...ghTitle());
|
|
723
|
+
ghSec.removeAttribute("aria-busy");
|
|
724
|
+
drawGh();
|
|
725
|
+
// the first cards after the wait come in, one after the other; the
|
|
726
|
+
// grid drawn again for a pick doesn't
|
|
727
|
+
if (ghArrive) {
|
|
728
|
+
ghArrive = false;
|
|
729
|
+
ghSec.classList.add("pm-arrive");
|
|
730
|
+
[...ghGrid.children].forEach((c, i) => c.style.setProperty("--i", Math.min(i, 9)));
|
|
731
|
+
setTimeout(() => ghSec.classList.remove("pm-arrive"), 900);
|
|
732
|
+
}
|
|
733
|
+
return ghSec;
|
|
734
|
+
}
|
|
735
|
+
function drawGh() {
|
|
736
|
+
const head = taggedErr ? [ghHead, ghNote()] : [ghHead];
|
|
737
|
+
if ((tagged || []).length <= 6) {
|
|
738
|
+
ghGrid.replaceChildren(...ghList.map(card));
|
|
739
|
+
replaceKeeping(ghSec, [...head, ghGrid]);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// the kinds there are, in the market's order, each with how many the
|
|
743
|
+
// field leaves of it; one kind alone needs no picking
|
|
744
|
+
const found = ghList.filter(ghMatch);
|
|
745
|
+
const kinds = ["pv", "mw", "ag", "tl", "pg"].filter((k) => ghList.some((l) => kindOf(l) === k));
|
|
746
|
+
if (!kinds.includes(ghKind)) ghKind = "";
|
|
747
|
+
// which of them are here, asked once each: the counts below would ask
|
|
748
|
+
// it again for every pick in both strips. Like a kind alone, a list
|
|
749
|
+
// that is all installed or all not has nothing to pick out, and a pick
|
|
750
|
+
// that can no longer be seen doesn't go on narrowing the grid.
|
|
751
|
+
const here = new Map(ghList.map((l) => [l, !!entryOf(l.package)]));
|
|
752
|
+
const have = (l) => here.get(l);
|
|
753
|
+
const both = ghList.some(have) && ghList.some((l) => !have(l));
|
|
754
|
+
if (!both) ghHave = "";
|
|
755
|
+
// the field and the other strip narrow it, so each count is what a
|
|
756
|
+
// pick in this strip would leave; the kind's own pick is the one left
|
|
757
|
+
// out, or every count but All would be the picked kind's alone
|
|
758
|
+
const n = (k, h) => found.filter((l) => (!k || kindOf(l) === k) && (!h || have(l) === (h === "have"))).length;
|
|
759
|
+
const bar = [];
|
|
760
|
+
if (kinds.length > 1) {
|
|
761
|
+
const pick = segs([["", t("All") + " · " + n("", ghHave)], ...kinds.map((k) => [k, t(KINDS[k][1]) + " · " + n(k, ghHave)])], ghKind, (k) => { ghKind = k; drawGh(); });
|
|
762
|
+
pick.classList.add("pm-ghkinds");
|
|
763
|
+
pick.setAttribute("role", "group");
|
|
764
|
+
pick.setAttribute("aria-label", t("Kind of plugin"));
|
|
765
|
+
bar.push(pick);
|
|
766
|
+
}
|
|
767
|
+
// what's installed, on both sides of it: a reader looks for what to
|
|
768
|
+
// add as much as for what they have
|
|
769
|
+
if (both) {
|
|
770
|
+
const pick = segs([["", t("All")], ["have", t("Installed") + " · " + n(ghKind, "have")], ["none", t("Not installed") + " · " + n(ghKind, "none")]], ghHave, (h) => { ghHave = h; drawGh(); });
|
|
771
|
+
pick.classList.add("pm-ghhave");
|
|
772
|
+
pick.setAttribute("role", "group");
|
|
773
|
+
pick.setAttribute("aria-label", t("Installed or not"));
|
|
774
|
+
bar.push(pick);
|
|
775
|
+
}
|
|
776
|
+
bar.push(el("span", "grow"), ghFind);
|
|
777
|
+
ghQ.placeholder = t("Filter by name, author or description");
|
|
778
|
+
ghQ.setAttribute("aria-label", t("Filter the plugins on GitHub"));
|
|
779
|
+
if (ghQ.value !== ghQuery) ghQ.value = ghQuery;
|
|
780
|
+
replaceKeeping(ghBar, bar);
|
|
781
|
+
const shown = found.filter((l) => (!ghKind || kindOf(l) === ghKind) && (!ghHave || have(l) === (ghHave === "have")));
|
|
782
|
+
if (shown.length) {
|
|
783
|
+
ghGrid.replaceChildren(...shown.map(card));
|
|
784
|
+
replaceKeeping(ghSec, [...head, ghBar, ghGrid]);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const none = el("div", "pm-ghnone");
|
|
788
|
+
const all = el("button", "text", t("Show all"));
|
|
789
|
+
all.type = "button";
|
|
790
|
+
all.onclick = () => { ghKind = ""; ghHave = ""; ghQuery = ""; drawGh(); ghQ.focus(); };
|
|
791
|
+
none.append(el("span", "", t("No plugin on GitHub matches")), all);
|
|
792
|
+
replaceKeeping(ghSec, [...head, ghBar, none]);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// the head and its search field are made once and drawn again in
|
|
796
|
+
// place: the field is never taken out of the page, or a key an IME
|
|
797
|
+
// commits while it is out lands twice (#1055)
|
|
798
|
+
const headBox = el("div", "lib-head pm-head");
|
|
799
|
+
const find = el("label", "pm-find");
|
|
800
|
+
const q = el("input");
|
|
801
|
+
q.type = "search";
|
|
802
|
+
q.spellcheck = false;
|
|
803
|
+
q.autocomplete = "off";
|
|
804
|
+
q.oninput = () => {
|
|
805
|
+
query = q.value;
|
|
806
|
+
if (tab !== "discover") { tab = "discover"; draw(); }
|
|
807
|
+
clearTimeout(searchTimer);
|
|
808
|
+
const s = query.trim();
|
|
809
|
+
if (s.length < 2) hits = null;
|
|
810
|
+
else {
|
|
811
|
+
hits = { q: s, loading: true };
|
|
812
|
+
searchTimer = setTimeout(() => searchNPM(s), 350);
|
|
813
|
+
}
|
|
814
|
+
drawBody();
|
|
815
|
+
};
|
|
816
|
+
q.onkeydown = (ev) => { ev.stopPropagation(); if (ev.key === "Escape" && q.value) { q.value = ""; q.oninput(); } };
|
|
817
|
+
find.append(glyph(SEARCH, 13, 1.6), q);
|
|
818
|
+
|
|
819
|
+
function head() {
|
|
820
|
+
const n = mine?.plugins?.length || 0;
|
|
821
|
+
const tabs = segs([["builtin", t("Built-in")], ["discover", t("Discover")], ["installed", t("Installed") + (n ? " · " + n : "")]], tab, (id) => {
|
|
822
|
+
tab = id;
|
|
823
|
+
try { localStorage.setItem("magpie.pluginTab", id); } catch {}
|
|
824
|
+
searchShown();
|
|
825
|
+
drawBody();
|
|
826
|
+
});
|
|
827
|
+
tabs.classList.add("lib-tabs");
|
|
828
|
+
tabs.querySelectorAll(".opt")[2].dataset.sub = "installed";
|
|
829
|
+
q.placeholder = t("Search plugins and npm…");
|
|
830
|
+
if (q.value !== query) q.value = query;
|
|
831
|
+
q.setAttribute("aria-label", t("Search plugins"));
|
|
832
|
+
mirror = mirrorSwitch();
|
|
833
|
+
replaceKeeping(headBox, [tabs, el("span", "grow"), mirror, find]);
|
|
834
|
+
searchShown();
|
|
835
|
+
headBox.classList.toggle("stuck", page.scrollTop > 0);
|
|
836
|
+
return headBox;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 「国内镜像」: the list, npm's packages and answers, and Bun asked of
|
|
840
|
+
// mirrors in China first (npmmirror, jsDelivr); none before what's
|
|
841
|
+
// installed has come, which says how it is set
|
|
842
|
+
let mirrorSaving = false;
|
|
843
|
+
// the built-in plugins are a short list of switches: nothing to search
|
|
844
|
+
// or fetch through a mirror
|
|
845
|
+
let mirror = null;
|
|
846
|
+
function searchShown() {
|
|
847
|
+
find.hidden = tab === "builtin";
|
|
848
|
+
if (mirror) mirror.hidden = !mine || tab === "builtin";
|
|
849
|
+
}
|
|
850
|
+
function mirrorSwitch() {
|
|
851
|
+
const on = !!mine?.mirror;
|
|
852
|
+
const b = el("button", "pm-mirror" + (on ? " on" : ""));
|
|
853
|
+
b.type = "button";
|
|
854
|
+
b.hidden = !mine;
|
|
855
|
+
b.disabled = mirrorSaving;
|
|
856
|
+
b.setAttribute("role", "switch");
|
|
857
|
+
b.setAttribute("aria-checked", on ? "true" : "false");
|
|
858
|
+
b.setAttribute("aria-label", t("Mirrors in China"));
|
|
859
|
+
b.title = t("Download the plugin list, plugins and Bun from mirrors in China first (npmmirror, jsDelivr), their official addresses after. Each is a copy of the very file, checked as the original is");
|
|
860
|
+
const sw = el("span", "lib-switch" + (on ? " on" : ""));
|
|
861
|
+
sw.append(el("i"));
|
|
862
|
+
b.append(el("span", "", t("Mirrors in China")), sw);
|
|
863
|
+
b.onclick = async () => {
|
|
864
|
+
if (mirrorSaving || !mine) return;
|
|
865
|
+
mirrorSaving = true;
|
|
866
|
+
mine.mirror = !on;
|
|
867
|
+
draw();
|
|
868
|
+
try {
|
|
869
|
+
const r = await api("plugins/mirror", { on: !on });
|
|
870
|
+
mine.mirror = r.mirror;
|
|
871
|
+
// the list again, now from the mirror, and npm's answers with it
|
|
872
|
+
if (r.mirror) loadListings().then(() => { draw(); askNPM(); });
|
|
873
|
+
} catch (e) {
|
|
874
|
+
mine.mirror = on;
|
|
875
|
+
statusTErr(e);
|
|
876
|
+
}
|
|
877
|
+
mirrorSaving = false;
|
|
878
|
+
draw();
|
|
879
|
+
};
|
|
880
|
+
return b;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function searchNPM(s) {
|
|
884
|
+
try {
|
|
885
|
+
const r = await api("plugins/search?q=" + encodeURIComponent(s));
|
|
886
|
+
if (hits?.q === s) hits = { q: s, list: r.hits };
|
|
887
|
+
} catch (e) {
|
|
888
|
+
if (hits?.q === s) hits = { q: s, error: e.message };
|
|
889
|
+
}
|
|
890
|
+
if (hits?.q === s) drawBody();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
page.addEventListener("scroll", () => page.querySelector(".pm-head")?.classList.toggle("stuck", page.scrollTop > 0), { passive: true });
|
|
894
|
+
|
|
895
|
+
let body = null;
|
|
896
|
+
function draw() {
|
|
897
|
+
const scroll = page.scrollTop;
|
|
898
|
+
// the body is made once, so the GitHub section's field stays in the page
|
|
899
|
+
body = body || el("div", "lib-body pm-body");
|
|
900
|
+
replaceKeeping(page, [head(), body]);
|
|
901
|
+
drawBody();
|
|
902
|
+
page.scrollTop = scroll;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function drawBody() {
|
|
906
|
+
if (!body) return;
|
|
907
|
+
const key = tab + (query ? ":q" : "");
|
|
908
|
+
body.classList.toggle("enter", shown !== key);
|
|
909
|
+
shown = key;
|
|
910
|
+
// the search field keeps its focus: only the body is redrawn while
|
|
911
|
+
// typing, and the GitHub section's field is kept in it
|
|
912
|
+
if (tab === "installed" || tab === "builtin" || !listings) body.replaceChildren();
|
|
913
|
+
if (tab === "installed") return drawInstalled();
|
|
914
|
+
if (tab === "builtin") return drawBuiltin();
|
|
915
|
+
if (failedList && !listings) {
|
|
916
|
+
body.append(el("p", "pm-empty", t("Couldn't load the plugins: {error}", { error: failedList })));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (!listings) {
|
|
920
|
+
body.append(intro(), skeleton(6));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const f = query.trim().toLowerCase();
|
|
924
|
+
const match = (l) => !f || [l.name, l.package, summary(l), (l.providers || []).join(" "), l.npm?.publisher || ""].join(" ").toLowerCase().includes(f);
|
|
925
|
+
const ls = listings.filter(match);
|
|
926
|
+
// a tagged repository the market already lists is the market's card
|
|
927
|
+
const listed = new Set(listings.map((l) => l.package));
|
|
928
|
+
const gh = (tagged || []).filter((l) => !(l.github.package && listed.has(l.github.package)) && match(l));
|
|
929
|
+
// put in place around the GitHub section, which stays in the page
|
|
930
|
+
const out = [];
|
|
931
|
+
if (!f) {
|
|
932
|
+
out.push(intro());
|
|
933
|
+
// magpie's community's alone: others' plugins are found by a search
|
|
934
|
+
const ours = ls.filter((l) => l.community && kindOf(l) === "pv");
|
|
935
|
+
const mws = ls.filter((l) => l.community && kindOf(l) === "mw");
|
|
936
|
+
const ags = ls.filter((l) => l.community && kindOf(l) === "ag");
|
|
937
|
+
const tls = ls.filter((l) => l.community && kindOf(l) === "tl");
|
|
938
|
+
const pgs = ls.filter((l) => l.community && kindOf(l) === "pg");
|
|
939
|
+
if (ours.length) out.push(section(t("Subscriptions"), t("written for magpie, checked against its own sign-ins"), ours));
|
|
940
|
+
if (mws.length) out.push(section(t("Gateway middleware"), t("runs in magpie's gateway on what every agent sends and gets back, whichever provider serves it"), mws));
|
|
941
|
+
const agSec = ags.length ? section(t("Agents"), t("agents magpie has no setup of its own for, connected to it as its own are"), ags) : null;
|
|
942
|
+
if (agSec) { agSec.classList.add("pm-agents"); out.push(agSec); }
|
|
943
|
+
if (tls.length) out.push(section(t("Tools for agents"), t("tools every agent can call, through magpie's MCP server"), tls));
|
|
944
|
+
if (pgs.length) out.push(section(t("Pages"), t("pages of their own in magpie's window, each shut off from the rest and reading only what it lists"), pgs));
|
|
945
|
+
if (gh.length) out.push(ghSection(gh));
|
|
946
|
+
else if (!tagged) out.push(ghWaiting());
|
|
947
|
+
else if (taggedErr && !tagged.length) out.push(ghFailed());
|
|
948
|
+
out.push(manual());
|
|
949
|
+
if (mine?.registry) {
|
|
950
|
+
out.push(regBox = regBox || el("div", "pm-registry"));
|
|
951
|
+
import("./registry.js").then(() => pmRegistry(regBox, mine, askNPM), statusTErr);
|
|
952
|
+
}
|
|
953
|
+
replaceKeeping(body, out);
|
|
954
|
+
if (agentsFor && agSec) {
|
|
955
|
+
// after the view is put back where the reader was, as showing it
|
|
956
|
+
// does; the section then is the one drawn last (npm's answers draw
|
|
957
|
+
// it again)
|
|
958
|
+
if (window.scrollOnPurpose?.(agentsFor)) requestAnimationFrame(() => { body.querySelector(".pm-agents")?.scrollIntoView({ block: "start" }); window.arrived?.(page); });
|
|
959
|
+
agentsFor = null;
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (ls.length) out.push(section(t("Suggested"), "", ls));
|
|
964
|
+
if (gh.length) out.push(ghSection(gh));
|
|
965
|
+
else if (!tagged) out.push(ghWaiting());
|
|
966
|
+
else if (taggedErr && !tagged.length) out.push(ghFailed());
|
|
967
|
+
// npm's card goes where a card above is its package (a listing, or a
|
|
968
|
+
// tagged repository npm has), not where a repository's package.json
|
|
969
|
+
// only names it (#3685)
|
|
970
|
+
const known = new Set([...listings.map((l) => l.package), ...(tagged || []).map((l) => l.package)]);
|
|
971
|
+
const box = el("section", "pm-sec");
|
|
972
|
+
const h = el("div", "pm-sechead");
|
|
973
|
+
h.append(el("h3", "", t("On npm")), el("span", "", t("OpenCode plugins and pi packages anyone published — read what one does before you install it")));
|
|
974
|
+
box.append(h);
|
|
975
|
+
if (!hits) box.append(el("p", "pm-note", t("Type two letters or more to search npm")));
|
|
976
|
+
else if (hits.loading) box.append(skeleton(2));
|
|
977
|
+
else if (hits.error) box.append(el("p", "pm-note bad", hits.error));
|
|
978
|
+
else {
|
|
979
|
+
const list = hits.list.filter((x) => !known.has(x.package));
|
|
980
|
+
if (!list.length) box.append(el("p", "pm-note", ls.length ? t("Nothing else on npm") : t("No plugin called “{q}”", { q: query.trim() })));
|
|
981
|
+
const g = el("div", "pm-grid");
|
|
982
|
+
for (const x of list) g.append(card({ package: x.package, name: x.package, npm: x }));
|
|
983
|
+
box.append(g);
|
|
984
|
+
}
|
|
985
|
+
out.push(box);
|
|
986
|
+
replaceKeeping(body, out);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function intro() {
|
|
990
|
+
const box = el("div", "pm-intro");
|
|
991
|
+
const text = el("div", "pm-introtext");
|
|
992
|
+
text.append(el("h2", "", t("Plugins extend magpie")));
|
|
993
|
+
text.append(el("p", "", t("Plugins sign in to coding plans and make their requests; the models then work in every agent, like any provider's. They're OpenCode's provider plugins or pi's packages, run on Bun.")));
|
|
994
|
+
const mw = el("p", "");
|
|
995
|
+
mw.append(document.createTextNode(t("A plugin can also run in magpie's gateway on what agents send and get back, pick which account a routing asks, give every agent tools, connect an agent, or add a page of its own.") + " "));
|
|
996
|
+
// the docs in the reader's language: the site has en, zh and ja, and
|
|
997
|
+
// Chinese is nearer a zh-TW reader than English is
|
|
998
|
+
const how = el("a", "pm-link pm-write", "");
|
|
999
|
+
how.href = "https://usemagpie.ai/docs/" + ({ zh: "zh/", "zh-TW": "zh/", ja: "ja/" }[lang()] || "") + "plugins";
|
|
1000
|
+
how.append(el("span", "", t("Write a plugin")), glyph(OUTL, 11, 1.5));
|
|
1001
|
+
how.onclick = (ev) => { ev.preventDefault(); api("open", { url: how.href }).catch(() => {}); };
|
|
1002
|
+
mw.append(how);
|
|
1003
|
+
text.append(mw);
|
|
1004
|
+
const trust = el("p", "pm-trust");
|
|
1005
|
+
trust.append(glyph(SHIELD, 12, 1.5), el("span", "", t("A plugin is someone else's code with your sign-in or your requests: install the ones you trust.")));
|
|
1006
|
+
text.append(trust);
|
|
1007
|
+
const art = el("div", "pm-art");
|
|
1008
|
+
for (const ic of ["zcode", "githubcopilot", "gemini-color", "zed", "kiro-color"]) art.append(logo(ic));
|
|
1009
|
+
box.append(text, art);
|
|
1010
|
+
return box;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// a package or a folder the market doesn't list
|
|
1014
|
+
function manual() {
|
|
1015
|
+
const box = el("form", "pm-manual");
|
|
1016
|
+
box.append(el("span", "pm-mlabel", t("Have one in mind?")));
|
|
1017
|
+
const spec = el("input", "pm-minput");
|
|
1018
|
+
spec.placeholder = t("npm package, GitHub repo (github:owner/repo) or a folder");
|
|
1019
|
+
spec.spellcheck = false;
|
|
1020
|
+
spec.autocomplete = "off";
|
|
1021
|
+
spec.setAttribute("aria-label", t("Plugin"));
|
|
1022
|
+
spec.onkeydown = (ev) => ev.stopPropagation();
|
|
1023
|
+
const go = el("button", "text primary", t("Install"));
|
|
1024
|
+
go.type = "submit";
|
|
1025
|
+
go.disabled = busy.size > 0;
|
|
1026
|
+
if (busy.has("+")) { go.textContent = t("Installing…"); spec.disabled = true; }
|
|
1027
|
+
box.onsubmit = async (ev) => {
|
|
1028
|
+
ev.preventDefault();
|
|
1029
|
+
const s = spec.value.trim();
|
|
1030
|
+
if (!s || busy.size || !await replaceOK(s, true)) return;
|
|
1031
|
+
act("+", "add", { spec: s }, () => status(t("{name} is installed", { name: s }), "ok"));
|
|
1032
|
+
};
|
|
1033
|
+
box.append(spec);
|
|
1034
|
+
// a folder is picked, not typed, where magpie can show the system's
|
|
1035
|
+
// picker (`magpie web` can't, and the market says so)
|
|
1036
|
+
if (mine?.picker) box.append(browse(spec, go));
|
|
1037
|
+
box.append(go);
|
|
1038
|
+
return box;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// the system's folder picker: what it answers goes in the field, for the
|
|
1042
|
+
// Install beside it
|
|
1043
|
+
function browse(spec, go) {
|
|
1044
|
+
const b = el("button", "pm-browse");
|
|
1045
|
+
b.type = "button";
|
|
1046
|
+
b.append(glyph(FOLDER, 14, 1.5));
|
|
1047
|
+
b.title = t("Choose a folder on this computer");
|
|
1048
|
+
b.setAttribute("aria-label", t("Choose a folder"));
|
|
1049
|
+
b.disabled = busy.size > 0 || spec.disabled;
|
|
1050
|
+
b.onclick = async () => {
|
|
1051
|
+
b.disabled = true;
|
|
1052
|
+
try {
|
|
1053
|
+
const r = await api("plugins/choose", {});
|
|
1054
|
+
if (r.dir) {
|
|
1055
|
+
spec.value = r.dir;
|
|
1056
|
+
spec.focus();
|
|
1057
|
+
}
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
statusErr(e);
|
|
1060
|
+
} finally {
|
|
1061
|
+
b.disabled = busy.size > 0 || spec.disabled;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
return b;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Built-in: magpie's own plugins (Sessions, Library, routing groups…),
|
|
1068
|
+
// each with a switch (renderBuiltins in app.js), read afresh each time
|
|
1069
|
+
// the tab is drawn
|
|
1070
|
+
let builtins = null, builtinsFailed = "";
|
|
1071
|
+
async function loadBuiltins() {
|
|
1072
|
+
try {
|
|
1073
|
+
builtins = await api("plugins/builtin");
|
|
1074
|
+
builtinsFailed = "";
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
builtinsFailed = e.message;
|
|
1077
|
+
}
|
|
1078
|
+
if (tab === "builtin") drawBody();
|
|
1079
|
+
}
|
|
1080
|
+
function drawBuiltin() {
|
|
1081
|
+
const say = el("p", "feat-say", t("Parts of magpie not everyone needs. Switched off, a part leaves the sidebar, its pages and Settings; magpie works the same either way."));
|
|
1082
|
+
const list = el("div", "list prefs");
|
|
1083
|
+
list.id = "builtinList";
|
|
1084
|
+
body.append(say, list);
|
|
1085
|
+
if (builtinsFailed && !builtins) list.append(el("p", "pm-empty", t(builtinsFailed)));
|
|
1086
|
+
else if (builtins) window.renderBuiltins(list, builtins, (next) => { builtins = next; drawBody(); });
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function drawInstalled() {
|
|
1090
|
+
if (failed && !mine) {
|
|
1091
|
+
body.append(el("p", "pm-empty", t("Couldn't load the plugins: {error}", { error: failed })));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (!mine) { body.append(skeleton(2)); return; }
|
|
1095
|
+
const es = mine.plugins || [];
|
|
1096
|
+
if (!es.length) {
|
|
1097
|
+
const none = el("div", "pm-none");
|
|
1098
|
+
none.append(logo("", true), el("b", "", t("No plugins yet")), el("p", "", t("Install a subscription or a middleware from Discover; a subscription signs in from here.")));
|
|
1099
|
+
const b = el("button", "pm-act get", t("Discover plugins"));
|
|
1100
|
+
b.onclick = () => { tab = "discover"; draw(); };
|
|
1101
|
+
none.append(b);
|
|
1102
|
+
body.append(none);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
// what the sidebar's dot is about, said where it's acted on: which
|
|
1106
|
+
// plugins have a newer version, and one press to update them all
|
|
1107
|
+
const outdated = es.filter((e) => (e.latest && e.version && newer(e.latest, e.version)) || (gitBehind(e) && !e.off));
|
|
1108
|
+
if (outdated.length) {
|
|
1109
|
+
const bar = el("div", "pm-updates");
|
|
1110
|
+
const names = outdated.map(shownName).join(t(", "));
|
|
1111
|
+
bar.append(el("span", "pm-dot"), el("span", "grow", outdated.length === 1
|
|
1112
|
+
? t("A newer version of {name} is out", { name: names })
|
|
1113
|
+
: t("Newer versions of {n} plugins are out: {names}", { n: outdated.length, names })));
|
|
1114
|
+
const up = el("button", "pm-act pm-up", busy.has("*") ? t("Updating…") : t("Update all ({n})", { n: outdated.length }));
|
|
1115
|
+
up.type = "button";
|
|
1116
|
+
up.disabled = busy.size > 0 || checking;
|
|
1117
|
+
up.onclick = () => act("*", "update", {}, () => status(t("Plugins updated"), "ok"));
|
|
1118
|
+
bar.append(up);
|
|
1119
|
+
body.append(bar);
|
|
1120
|
+
}
|
|
1121
|
+
// by name, in the order the reader picked (#481), not the order they
|
|
1122
|
+
// were installed in
|
|
1123
|
+
const list = el("div", "list pm-list");
|
|
1124
|
+
const fill = () => list.replaceChildren(...[...es].sort(byName(sortOf("plugins"), shownName)).map(installedRow));
|
|
1125
|
+
if (es.length > 1) {
|
|
1126
|
+
const h = el("div", "row-head pm-listhead");
|
|
1127
|
+
h.append(el("span", "grow"), sortBy("plugins", NAME_SORTS, fill));
|
|
1128
|
+
body.append(h);
|
|
1129
|
+
}
|
|
1130
|
+
fill();
|
|
1131
|
+
body.append(list);
|
|
1132
|
+
const foot = el("div", "pm-foot");
|
|
1133
|
+
foot.append(el("span", "", mine.bun ? t("Plugins run on Bun {v}", { v: mine.bunVersion }) : ""), el("span", "grow"));
|
|
1134
|
+
// npm has versions to ask for, and a git one's repository commits; a
|
|
1135
|
+
// folder has none
|
|
1136
|
+
if (es.some((e) => onNPM(e.spec) || isGit(e.spec))) {
|
|
1137
|
+
const c = el("button", "text pm-check" + (checking ? " busy" : ""));
|
|
1138
|
+
if (checking) c.append(el("span", "spin"));
|
|
1139
|
+
c.append(el("span", "", checking ? t("Checking…") : t("Check for updates")));
|
|
1140
|
+
c.title = t("Ask npm now for each plugin's newest version") + (checked ? "\n" + t("Last checked {time}", { time: checkedAt() }) : "");
|
|
1141
|
+
c.disabled = checking || busy.size > 0;
|
|
1142
|
+
c.onclick = () => checkUpdates();
|
|
1143
|
+
foot.append(c);
|
|
1144
|
+
}
|
|
1145
|
+
body.append(foot);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// an installed plugin's name as its row shows it: the market's, else the
|
|
1149
|
+
// one its package.json gives itself (magpie.name, #4558), else the
|
|
1150
|
+
// package's or the folder's
|
|
1151
|
+
function shownName(e) {
|
|
1152
|
+
const pkg = name(e.spec);
|
|
1153
|
+
return (listings || []).find((x) => x.package === pkg)?.name || e.ownName || (isGit(e.spec) && e.package) || label(e.spec);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function installedRow(e) {
|
|
1157
|
+
const pkg = name(e.spec);
|
|
1158
|
+
const l = (listings || []).find((x) => x.package === pkg);
|
|
1159
|
+
const r = el("div", "row pm-row" + (e.off ? " off" : ""));
|
|
1160
|
+
const who = el("div", "who");
|
|
1161
|
+
const nm = el("div", "name");
|
|
1162
|
+
nm.append(el("span", "", shownName(e)));
|
|
1163
|
+
if (e.version) nm.append(el("span", "pm-ver", "v" + e.version));
|
|
1164
|
+
// a git one's commit says which of its repository's it is
|
|
1165
|
+
if (isGit(e.spec) && e.commit) {
|
|
1166
|
+
const c = el("span", "pm-ver", e.commit);
|
|
1167
|
+
c.title = t("The commit of {repo} installed", { repo: e.spec });
|
|
1168
|
+
nm.append(c);
|
|
1169
|
+
}
|
|
1170
|
+
const ck = checked?.by[e.spec];
|
|
1171
|
+
if (gitBehind(e)) {
|
|
1172
|
+
const c = el("span", "pm-chip up", t("New commits"));
|
|
1173
|
+
c.title = t("{have} installed, {repo} is at {head}", { have: e.commit, repo: e.spec, head: e.head });
|
|
1174
|
+
nm.append(c);
|
|
1175
|
+
} else if (e.latest && e.version && newer(e.latest, e.version)) {
|
|
1176
|
+
const c = el("span", "pm-chip up", t("v{v} out", { v: e.latest }));
|
|
1177
|
+
c.title = t("v{have} installed, v{v} on npm", { have: e.version, v: e.latest });
|
|
1178
|
+
nm.append(c);
|
|
1179
|
+
} else if (ck?.status === "unknown") {
|
|
1180
|
+
// asked just now, and npm didn't say: the row says why
|
|
1181
|
+
const c = el("span", "pm-chip warn");
|
|
1182
|
+
c.append(el("span", "dot"), el("span", "", t("Couldn't check")));
|
|
1183
|
+
c.title = whyText(ck);
|
|
1184
|
+
nm.append(c);
|
|
1185
|
+
} else if (e.autoUpdated && e.autoUpdated.to === e.version) {
|
|
1186
|
+
// magpie updated it by itself lately: the row says so, quietly
|
|
1187
|
+
const c = el("span", "pm-chip soft", t("Auto-updated"));
|
|
1188
|
+
c.title = t("magpie updated it from v{from} to v{to} on {date}", { from: e.autoUpdated.from, to: e.autoUpdated.to, date: new Date(e.autoUpdated.at).toLocaleDateString(intlLang(), { month: "short", day: "numeric" }) });
|
|
1189
|
+
nm.append(c);
|
|
1190
|
+
} else if ((ck?.status === "current" || ck?.status === "update") && e.version) {
|
|
1191
|
+
const c = el("span", "pm-chip soft", t("Up to date"));
|
|
1192
|
+
c.title = (isGit(e.spec) ? t("{head} is the newest commit of {repo}", { head: e.head || ck.head || "", repo: e.spec }) : t("v{v} is the newest on npm", { v: e.version })) + "\n" + t("Last checked {time}", { time: checkedAt() });
|
|
1193
|
+
nm.append(c);
|
|
1194
|
+
}
|
|
1195
|
+
// built-ins moved onto it, then its other providers if 2+ (#3902)
|
|
1196
|
+
const moved = (e.moved || []).map((id) => SUBS.find((x) => x.agent === id)?.name || id);
|
|
1197
|
+
const names = moved.join(t(", "));
|
|
1198
|
+
const all = [...moved, ...e.serves || []];
|
|
1199
|
+
if (all[0] && !e.off) {
|
|
1200
|
+
const c = el("span", "pm-chip moved");
|
|
1201
|
+
c.append(el("span", "dot"), el("span", "", t("Serves {names}", { names: all.join(t(", ")) })));
|
|
1202
|
+
if (names) c.title = t("{names} runs on this plugin in place of the built-in", { names });
|
|
1203
|
+
nm.append(c);
|
|
1204
|
+
}
|
|
1205
|
+
// gateway middleware runs in magpie's gateway, not as a subscription:
|
|
1206
|
+
// the row says so, and how it is doing, on a line of its own
|
|
1207
|
+
const mw = e.middleware;
|
|
1208
|
+
const gw = e.gateway;
|
|
1209
|
+
const ag = e.agent;
|
|
1210
|
+
// a provider's or agent's options (plugins#592)
|
|
1211
|
+
const ownOpts = !e.isMiddleware && !e.isGateway && !!(e.options || e.optionsExample);
|
|
1212
|
+
if (mw || e.isMiddleware) nm.append(kindChip("mw"));
|
|
1213
|
+
if (gw || e.isGateway) nm.append(kindChip("mw", "Request hook"));
|
|
1214
|
+
if (ag || e.isAgent) nm.append(kindChip("ag"));
|
|
1215
|
+
// a plugin of tools alone signs in to nothing: no Provider chip
|
|
1216
|
+
const toolsOnly = !!e.tools?.length && !e.providers?.length;
|
|
1217
|
+
if (e.tools?.length) nm.append(kindChip("tl"));
|
|
1218
|
+
if (e.page) nm.append(kindChip("pg"));
|
|
1219
|
+
if (e.panels?.length) nm.append(kindChip("pn"));
|
|
1220
|
+
if (e.providers?.length || !(e.gatewayOnly || e.middlewareOnly || e.inMagpieOnly || e.isAgent || toolsOnly || (mw && !e.isMiddleware) || (ag && !e.isAgent))) nm.append(kindChip("pv"));
|
|
1221
|
+
who.append(nm);
|
|
1222
|
+
const subs = subsOf(pkg);
|
|
1223
|
+
const sub = el("div", "sub");
|
|
1224
|
+
const cands = e.off ? [] : movableOf(pkg);
|
|
1225
|
+
// a provider another installed plugin signs in to as well (neiko on
|
|
1226
|
+
// Discord: a third-party plugin and their own one both name it). One
|
|
1227
|
+
// of them serves it; the row says which, and the other gets a way to
|
|
1228
|
+
// take it over, rather than the provider just going missing
|
|
1229
|
+
const clashes = e.off || e.error ? [] : e.clashes || [];
|
|
1230
|
+
const nameOfSpec = (spec) => { const o = (mine?.plugins || []).find((x) => x.spec === spec); return o ? shownName(o) : label(spec); };
|
|
1231
|
+
const ask = asking?.pkg === pkg && !busy.size && (asking.op === "move" ? cands.length : moved.length) ? asking.op : "";
|
|
1232
|
+
if (ask === "move") {
|
|
1233
|
+
sub.textContent = t("{names} can run on it again, with the same accounts.", { names: cands.map((c) => c.name).join(t(", ")) });
|
|
1234
|
+
sub.classList.add("ask");
|
|
1235
|
+
} else if (ask) {
|
|
1236
|
+
sub.textContent = t(ask === "remove" ? "{names} stops working until the plugin is installed again. Its accounts are kept." : "{names} stops working while the plugin is off. Its accounts are kept.", { names });
|
|
1237
|
+
sub.classList.add("ask");
|
|
1238
|
+
} else if (e.off) sub.textContent = t("Off");
|
|
1239
|
+
else if (e.error) { sub.textContent = t("Didn't load: {error}", { error: e.error }); sub.classList.add("bad"); sub.title = e.error; }
|
|
1240
|
+
else if (subs.length) {
|
|
1241
|
+
for (const [i, x] of subs.entries()) {
|
|
1242
|
+
if (i) sub.append(el("span", "sep", " · "));
|
|
1243
|
+
const s = el("span", "pm-sub" + (x.signedIn ? " in" : ""));
|
|
1244
|
+
// its built-in runs it, with its accounts: nothing to fix here
|
|
1245
|
+
const builtin = !x.signedIn && cands.some((c) => c.id === x.pid);
|
|
1246
|
+
const nx = subName(x);
|
|
1247
|
+
s.append(el("span", "dot"), el("span", "", x.signedIn ? t("{name}: signed in", { name: nx }) : builtin ? t("{name}: on magpie's built-in", { name: nx }) : t("{name}: not signed in", { name: nx })));
|
|
1248
|
+
if (builtin) s.title = t("{name} runs on magpie's built-in, with your accounts; Move {name} here runs it on this plugin", { name: nx });
|
|
1249
|
+
else if (!x.signedIn && subs.some((y) => y.signedIn)) s.title = t("{name} is a subscription of its own; {other} works without it", { name: nx, other: subName(subs.find((y) => y.signedIn)) });
|
|
1250
|
+
sub.append(s);
|
|
1251
|
+
}
|
|
1252
|
+
} else if ((mw || ag || gw || e.isGateway || toolsOnly || (e.page || e.panels?.length) && e.inMagpieOnly) && !e.providers.length) {
|
|
1253
|
+
// only middleware, a request hook, an agent, tools, a page or panels: its own line says what it does
|
|
1254
|
+
} else if (clashes.length && !e.providers.length) {
|
|
1255
|
+
// its own line below says whose it is
|
|
1256
|
+
} else if (e.providers.length) sub.textContent = t("Signs in to {names}", { names: e.providers.join(t(", ")) });
|
|
1257
|
+
// an empty list only means "nothing" once the plugins have answered: a
|
|
1258
|
+
// plugin installed a moment ago has no names yet, and the page says
|
|
1259
|
+
// nothing rather than claiming here that it signs in to nothing magpie
|
|
1260
|
+
// can use, which isn't known until the answer comes (#1112)
|
|
1261
|
+
else if (e.named) sub.textContent = t("Signs in to nothing magpie can use");
|
|
1262
|
+
if (sub.textContent) who.append(sub);
|
|
1263
|
+
if (!ask) for (const g of clashGroups(e, clashes)) who.append(clashLine(e, g, nameOfSpec));
|
|
1264
|
+
if (mw && !e.off && !ask) who.append(mwLine(mw));
|
|
1265
|
+
if (gw && !e.off && !ask) who.append(gatewayLine(gw));
|
|
1266
|
+
if (ag && !e.off && !ask) who.append(agentLine(ag));
|
|
1267
|
+
if (e.tools?.length && !e.off && !ask) who.append(toolsLine(e.tools, e));
|
|
1268
|
+
// off too: what its page may read is said before it is turned on
|
|
1269
|
+
if (e.page && !ask) who.append(pageLine(e.page));
|
|
1270
|
+
if (e.panels?.length && !ask) who.append(panelsLine(e.panels, e.panelReads));
|
|
1271
|
+
if (e.actions?.length && !ask) who.append(actionsLine(e.actions));
|
|
1272
|
+
if ((mw || e.isGateway || ownOpts) && !e.off && !ask && editing?.pkg === pkg) who.append(optionsEditor(e));
|
|
1273
|
+
// the plugin's own picture (package.json's magpie.icon) before its
|
|
1274
|
+
// first provider's, which a plugin of several gives each its own (#1775)
|
|
1275
|
+
r.append(logo(l?.icon || l?.npm?.icon || e.icon || subs[0]?.icon || ag?.icon, false, e.middlewareOnly ? "mw" : e.isAgent && !e.isMiddleware && !e.providers.length ? "ag" : e.inMagpieOnly && (e.page || e.panels?.length) && !e.isAgent && !e.isMiddleware ? "pg" : toolsOnly && !e.page && !mw && !ag ? "tl" : ""), who);
|
|
1276
|
+
const val = el("div", "val");
|
|
1277
|
+
const b = busy.get(pkg) || busy.get(e.spec);
|
|
1278
|
+
// npm's newer version, or a git one's repository at a newer commit
|
|
1279
|
+
// than the one installed (Update shown all the time before, and still
|
|
1280
|
+
// there right after an update): the same Update as a card's (#1587)
|
|
1281
|
+
const up = el("button");
|
|
1282
|
+
if (updateButton(up, e)) val.append(up);
|
|
1283
|
+
const here = !e.off && !e.error && !subs.some((x) => x.signedIn) && cands[0];
|
|
1284
|
+
if (here && !ask) {
|
|
1285
|
+
const mv = el("button", "text primary", b === "move" ? t("Moving…") : t("Move {name} here", { name: here.name }));
|
|
1286
|
+
mv.title = t("Runs {name} on this plugin in place of the built-in, with the same accounts; nothing changes if one doesn't work through it", { name: here.name });
|
|
1287
|
+
mv.disabled = busy.size > 0;
|
|
1288
|
+
mv.onclick = () => move(here);
|
|
1289
|
+
val.append(mv);
|
|
1290
|
+
}
|
|
1291
|
+
for (const c of ask ? [] : clashes.filter((c) => c.by !== e.spec)) {
|
|
1292
|
+
const u = el("button", "text", b === "prefer" ? t("Switching…") : t("Use for {name}", { name: c.name }));
|
|
1293
|
+
u.title = t("{name} runs on this plugin instead of {other}; both stay installed", { name: c.name, other: nameOfSpec(c.by) });
|
|
1294
|
+
u.disabled = busy.size > 0;
|
|
1295
|
+
u.onclick = () => act(pkg, "prefer", { spec: e.spec, provider: c.id }, () => status(t("{name} now runs on {plugin}", { name: c.name, plugin: shownName(e) }), "ok"));
|
|
1296
|
+
val.append(u);
|
|
1297
|
+
}
|
|
1298
|
+
if (!e.off && !e.error && subs.some((x) => !x.signedIn)) {
|
|
1299
|
+
// signed in to one of its subscriptions already (Qoder, beside Qoder
|
|
1300
|
+
// CN; #556): a blue Sign in read as the plugin signed out, so the
|
|
1301
|
+
// button names the one it signs in to and stays quiet
|
|
1302
|
+
const outs = subs.filter((x) => !x.signedIn), some = subs.some((x) => x.signedIn);
|
|
1303
|
+
const s = el("button", "text" + (here || some ? "" : " primary"), !some ? t("Sign in") : outs.length > 1 ? t("Sign in to another…") : t("Sign in to {name}", { name: subName(outs[0]) }));
|
|
1304
|
+
if (outs.length > 1) s.setAttribute("aria-haspopup", "menu");
|
|
1305
|
+
s.onclick = (ev) => { ev.stopPropagation(); signInTo(s, outs, ev); };
|
|
1306
|
+
val.append(s);
|
|
1307
|
+
}
|
|
1308
|
+
// switched on again: a built-in it can carry is offered its way, in
|
|
1309
|
+
// the row
|
|
1310
|
+
const again = () => { if (movableOf(pkg).length) { asking = { pkg, op: "move" }; draw(); } };
|
|
1311
|
+
const off = () => act(pkg, "off", { spec: e.spec, off: !e.off }, e.off ? again : null);
|
|
1312
|
+
const remove = () => act(pkg, "remove", { spec: e.spec }, () => status(t("{name} removed", { name: l?.name || pkg }), "ok"));
|
|
1313
|
+
if (ask) {
|
|
1314
|
+
// asked in the row, not a dialog: the user sees which go back
|
|
1315
|
+
const yes = el("button", "text primary", t(ask === "move" ? "Move" : ask === "remove" ? "Remove" : "Switch off"));
|
|
1316
|
+
yes.onclick = () => { asking = null; ask === "move" ? move(cands[0]) : ask === "remove" ? remove() : off(); };
|
|
1317
|
+
const no = el("button", "text", t(ask === "move" ? "Not now" : "Cancel"));
|
|
1318
|
+
no.onclick = () => { asking = null; draw(); };
|
|
1319
|
+
val.append(no, yes);
|
|
1320
|
+
r.append(val);
|
|
1321
|
+
return r;
|
|
1322
|
+
}
|
|
1323
|
+
if ((e.isGateway || (mw && !mw.error) || ownOpts) && !e.off && editing?.pkg !== pkg) {
|
|
1324
|
+
const o = el("button", "text", t("Options"));
|
|
1325
|
+
o.title = e.options ? t("The options it runs with") : t("It runs with no options; its package suggests some");
|
|
1326
|
+
o.disabled = !!b;
|
|
1327
|
+
o.onclick = () => {
|
|
1328
|
+
editing = { pkg, text: JSON.stringify(e.options || e.optionsExample || {}, null, 2), error: "" };
|
|
1329
|
+
draw();
|
|
1330
|
+
// the field the click opened takes the keys, where it is
|
|
1331
|
+
page.querySelector(".pm-opts textarea")?.focus(NO_SCROLL);
|
|
1332
|
+
};
|
|
1333
|
+
val.append(o);
|
|
1334
|
+
}
|
|
1335
|
+
const onoff = el("button", "text", t(e.off ? "Switch on" : "Switch off"));
|
|
1336
|
+
onoff.disabled = !!b;
|
|
1337
|
+
onoff.onclick = () => { if (!e.off && moved.length) { asking = { pkg, op: "off" }; draw(); } else off(); };
|
|
1338
|
+
const rm = el("button", "text quiet", b === "remove" ? t("Removing…") : t("Remove"));
|
|
1339
|
+
rm.title = moved.length ? t("{names} runs on this plugin: removed, it stops working until the plugin is installed again", { names }) : t("Removes the plugin and what it installed; its sign-ins are kept until you sign out");
|
|
1340
|
+
rm.disabled = busy.size > 0;
|
|
1341
|
+
rm.onclick = async () => {
|
|
1342
|
+
if (moved.length) {
|
|
1343
|
+
asking = { pkg, op: "remove" };
|
|
1344
|
+
draw();
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (await confirmRemoval(l?.name || pkg, rm.title)) remove();
|
|
1348
|
+
};
|
|
1349
|
+
val.append(onoff, rm);
|
|
1350
|
+
r.append(val);
|
|
1351
|
+
r.onclick = (ev) => { if (!ev.target.closest("button, .pm-opts")) detail(l || (isGit(e.spec) ? { package: e.spec, name: shownName(e), icon: e.icon, npm: { version: e.version, repository: gitWeb(e.spec) } } : { package: pkg, name: shownName(e), icon: e.icon, npm: { version: e.latest } })); };
|
|
1352
|
+
return r;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// optionsEditor is a plugin's options as JSON, in its row: what it
|
|
1356
|
+
// runs with, or what its package suggests when it has none. The draft is
|
|
1357
|
+
// kept in editing, so a redraw while typing doesn't lose it.
|
|
1358
|
+
function optionsEditor(e) {
|
|
1359
|
+
const box = el("div", "pm-opts");
|
|
1360
|
+
const ta = document.createElement("textarea");
|
|
1361
|
+
ta.setAttribute("aria-label", t("Options"));
|
|
1362
|
+
ta.setAttribute("aria-describedby", "pluginOptionsNote pluginOptionsError");
|
|
1363
|
+
ta.setAttribute("aria-invalid", String(!!editing.error));
|
|
1364
|
+
ta.spellcheck = false;
|
|
1365
|
+
ta.value = editing.text;
|
|
1366
|
+
ta.rows = Math.min(14, Math.max(4, editing.text.split("\n").length + 1));
|
|
1367
|
+
ta.oninput = () => { editing.text = ta.value; };
|
|
1368
|
+
ta.onkeydown = (ev) => {
|
|
1369
|
+
if (ev.key === "Escape") { editing = null; draw(); }
|
|
1370
|
+
if (ev.key === "Enter" && (ev.metaKey || ev.ctrlKey)) { ev.preventDefault(); save(); }
|
|
1371
|
+
};
|
|
1372
|
+
const err = el("div", "pm-opts-err", editing.error);
|
|
1373
|
+
err.id = "pluginOptionsError";
|
|
1374
|
+
err.setAttribute("role", "alert");
|
|
1375
|
+
const bar = el("div", "pm-opts-bar");
|
|
1376
|
+
const note = el("span", "", e.options ? t("Applied to the next request") : t("Not set: these are its package's example"));
|
|
1377
|
+
note.id = "pluginOptionsNote";
|
|
1378
|
+
const save = () => {
|
|
1379
|
+
let v;
|
|
1380
|
+
try { v = JSON.parse(editing.text || "{}"); } catch (x) { editing.error = t("Not JSON: {error}", { error: x.message }); return draw(); }
|
|
1381
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) { editing.error = t("Options are a JSON object"); return draw(); }
|
|
1382
|
+
editing = null;
|
|
1383
|
+
act(name(e.spec), "options", { spec: e.spec, options: v }, () => status(t("{name}'s options saved", { name: shownName(e) }), "ok"));
|
|
1384
|
+
};
|
|
1385
|
+
const ok = el("button", "text primary", t("Save"));
|
|
1386
|
+
ok.onclick = save;
|
|
1387
|
+
const no = el("button", "text", t("Cancel"));
|
|
1388
|
+
no.onclick = () => { editing = null; draw(); };
|
|
1389
|
+
bar.append(note);
|
|
1390
|
+
if (e.options) {
|
|
1391
|
+
const clr = el("button", "text quiet", t("Clear"));
|
|
1392
|
+
clr.title = t("Runs it with no options");
|
|
1393
|
+
clr.onclick = async () => {
|
|
1394
|
+
if (!await confirmAction(t("Clear"), t("Saved options will be removed. The plugin will run with no options."), t("Clear"))) return;
|
|
1395
|
+
editing = null;
|
|
1396
|
+
act(name(e.spec), "options", { spec: e.spec, options: null }, () => status(t("{name}'s options cleared", { name: shownName(e) }), "ok"));
|
|
1397
|
+
};
|
|
1398
|
+
bar.append(clr);
|
|
1399
|
+
}
|
|
1400
|
+
bar.append(no, ok);
|
|
1401
|
+
box.append(ta, err, bar);
|
|
1402
|
+
return box;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// clashGroups are a row's clashes, one per other plugin and side: a
|
|
1406
|
+
// plugin that signs in to two providers another one does too is one
|
|
1407
|
+
// line naming both, not a line each saying the same thing (#565)
|
|
1408
|
+
function clashGroups(e, clashes) {
|
|
1409
|
+
const out = [];
|
|
1410
|
+
for (const c of clashes) {
|
|
1411
|
+
const here = c.by === e.spec;
|
|
1412
|
+
const others = here ? c.with : [c.by];
|
|
1413
|
+
const g = out.find((x) => x.here === here && x.others.join("\n") === others.join("\n"));
|
|
1414
|
+
if (g) g.names.push(c.name);
|
|
1415
|
+
else out.push({ here, others, names: [c.name] });
|
|
1416
|
+
}
|
|
1417
|
+
return out;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// clashLine is the providers two installed plugins both sign in to, as
|
|
1421
|
+
// either row says it. The other one goes by its name, or by where it
|
|
1422
|
+
// came from when that is this row's name too (one plugin installed
|
|
1423
|
+
// twice: it read as the plugin naming itself, #565)
|
|
1424
|
+
function clashLine(e, g, nameOfSpec) {
|
|
1425
|
+
const d = el("div", "sub pm-clash" + (g.here ? "" : " warn"));
|
|
1426
|
+
const own = shownName(e);
|
|
1427
|
+
const copies = g.others.filter((s) => nameOfSpec(s) === own);
|
|
1428
|
+
const names = g.names.join(t(", "));
|
|
1429
|
+
const many = g.names.length > 1;
|
|
1430
|
+
let text;
|
|
1431
|
+
if (copies.length && copies.length === g.others.length) {
|
|
1432
|
+
const spec = copies.join(t(", "));
|
|
1433
|
+
text = g.here
|
|
1434
|
+
? t("Another copy of this plugin ({spec}) signs in to {names} too; this copy serves them", { spec, names })
|
|
1435
|
+
: t("Another copy of this plugin ({spec}) serves {names}", { spec, names });
|
|
1436
|
+
} else {
|
|
1437
|
+
const other = g.others.map(nameOfSpec).join(t(", "));
|
|
1438
|
+
text = g.here
|
|
1439
|
+
? many ? t("{other} signs in to {names} too; this one serves them", { other, names }) : t("{other} signs in to {name} too; this one serves it", { other, name: names })
|
|
1440
|
+
: many ? t("{names} are served by {other}, which signs in to them too", { other, names }) : t("{name} is served by {other}, which signs in to it too", { other, name: names });
|
|
1441
|
+
}
|
|
1442
|
+
d.append(el("span", "dot"), el("span", "", text));
|
|
1443
|
+
d.title = g.names.map((name) => g.here
|
|
1444
|
+
? t("Two plugins sign in to {name}; magpie runs it on one. Use for {name} on the other one's row switches it", { name })
|
|
1445
|
+
: t("Two plugins sign in to {name}; magpie runs it on one. Use for {name} switches it to this one, or remove the one you don't want", { name })).join("\n");
|
|
1446
|
+
return d;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// agentLine is the agent a plugin adds, as the Agents page lists it, or
|
|
1450
|
+
// why it isn't there
|
|
1451
|
+
function agentLine(a) {
|
|
1452
|
+
const d = el("div", "sub pm-mw");
|
|
1453
|
+
if (a.error) {
|
|
1454
|
+
d.textContent = t("Agent didn't load: {error}", { error: a.error });
|
|
1455
|
+
d.classList.add("bad");
|
|
1456
|
+
d.title = a.error;
|
|
1457
|
+
return d;
|
|
1458
|
+
}
|
|
1459
|
+
// not found here, so Agents has no row (zzZ): say so, and the plugin's notice
|
|
1460
|
+
if (a.missing) {
|
|
1461
|
+
d.classList.add("pm-clash");
|
|
1462
|
+
d.textContent = d.title = [t("{name} isn't in Agents: magpie doesn't find it in {path}", { name: a.name, path: a.looks || "~" }), a.notice].filter(Boolean).join(" ");
|
|
1463
|
+
return d;
|
|
1464
|
+
}
|
|
1465
|
+
d.textContent = d.title = t("Adds {name} to Agents", { name: a.name });
|
|
1466
|
+
return d;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// toolsLine is the tools a plugin gives, which magpie's plugin tools MCP
|
|
1470
|
+
// server offers every agent: each by the name an agent sees, its
|
|
1471
|
+
// description on hover, and the server's address to copy into an agent
|
|
1472
|
+
// and, with its Library server (toolsServer), a switch for every agent
|
|
1473
|
+
function toolsLine(tools, e) {
|
|
1474
|
+
const d = el("div", "sub pm-mw pm-tools"), sv = e.toolsServer;
|
|
1475
|
+
const names = tools.map((x) => x.mcp || x.name).join(t(", "));
|
|
1476
|
+
d.append(el("span", "", t(!sv ? "Tools for every agent: {names}" : sv.on ? "Tools for agents: {names}" : "Tools, hidden from agents: {names}", { names })));
|
|
1477
|
+
d.title = tools.map((x) => (x.mcp || x.name) + (x.description ? " — " + x.description : "")).join("\n");
|
|
1478
|
+
const url = mcpAddress(state.settings || {}, "/mcp/magpie/tools");
|
|
1479
|
+
const b = copyBtn(url, t("MCP server address"));
|
|
1480
|
+
b.title = t("Copy the MCP server's address ({url}): add it to an agent as an MCP server (Streamable HTTP)", { url });
|
|
1481
|
+
d.append(b);
|
|
1482
|
+
if (sv && !gatewayMode && !featuresOff.has("library")) {
|
|
1483
|
+
const f = el("button", "link pm-tools-flip", t(sv.on ? "Hide from agents" : "Give to agents"));
|
|
1484
|
+
f.onclick = (ev) => {
|
|
1485
|
+
ev.stopPropagation();
|
|
1486
|
+
f.disabled = true;
|
|
1487
|
+
api("library/servers/plugin-on", { name: sv.plugin, on: !sv.on }).then(refresh, (err) => { f.disabled = false; statusErr(err); });
|
|
1488
|
+
};
|
|
1489
|
+
d.append(f);
|
|
1490
|
+
}
|
|
1491
|
+
return d;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// what a plugin's page may read of magpie's: what its package.json's
|
|
1495
|
+
// magpie.permissions declares, each a read with no secret in it
|
|
1496
|
+
const READS = { "usage:summary": "your usage totals, by model", "providers:list": "your providers' names" };
|
|
1497
|
+
const PAGES = { sessions: "Sessions", library: "Library", routing: "Routing", usage: "Usage" };
|
|
1498
|
+
function pageLine(p) {
|
|
1499
|
+
const d = el("div", "sub pm-mw pm-page");
|
|
1500
|
+
const reads = (p.reads || []).map((r) => t(READS[r] || r));
|
|
1501
|
+
const page = p.replaces && t(PAGES[p.replaces] || p.replaces);
|
|
1502
|
+
d.textContent = d.title = page
|
|
1503
|
+
? reads.length
|
|
1504
|
+
? t("Takes the place of the {page} page, and can read {reads}", { page, reads: reads.join(t(", ")) })
|
|
1505
|
+
: t("Takes the place of the {page} page, and reads nothing of magpie's", { page })
|
|
1506
|
+
: reads.length
|
|
1507
|
+
? t("Adds a page to the sidebar, which can read {reads}", { reads: reads.join(t(", ")) })
|
|
1508
|
+
: t("Adds a page to the sidebar, which reads nothing of magpie's");
|
|
1509
|
+
return d;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// where a plugin's panels go in magpie's own pages (magpie.panels), and
|
|
1513
|
+
// the row menus its page adds items to (magpie.actions), titled with
|
|
1514
|
+
// their titles; picking an item tells the page the row's name only
|
|
1515
|
+
const SLOTS = { agents: "the Agents page", usage: "the Usage page", settings: "Settings", provider: "each provider's editor" };
|
|
1516
|
+
const MENUS = { provider: "providers' menus", agent: "agents' menus" };
|
|
1517
|
+
function whereLine(xs, key, names, text) {
|
|
1518
|
+
const d = el("div", "sub pm-mw pm-page");
|
|
1519
|
+
d.textContent = text([...new Set(xs.map((x) => x[key]))].map((s) => t(names[s] || s)).join(t(", ")));
|
|
1520
|
+
d.title = xs.map((x) => x.title).filter(Boolean).join("\n") || d.textContent;
|
|
1521
|
+
return d;
|
|
1522
|
+
}
|
|
1523
|
+
function panelsLine(panels, panelReads) {
|
|
1524
|
+
const reads = (panelReads || []).map((r) => t(READS[r] || r)).join(t(", "));
|
|
1525
|
+
return whereLine(panels, "slot", SLOTS, (where) => reads
|
|
1526
|
+
? t("Adds panels to {where}, which can read {reads}", { where, reads })
|
|
1527
|
+
: t("Adds panels to {where}, which read nothing of magpie's", { where }));
|
|
1528
|
+
}
|
|
1529
|
+
function actionsLine(actions) {
|
|
1530
|
+
return whereLine(actions, "menu", MENUS, (where) => t("Adds items to {where}, which open its page", { where }));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// mwLine is a middleware's hooks, calls, time each and failures, or why it didn't load
|
|
1534
|
+
function mwLine(m) {
|
|
1535
|
+
const d = el("div", "sub pm-mw");
|
|
1536
|
+
if (m.error) {
|
|
1537
|
+
d.textContent = t("Middleware didn't load: {error}", { error: m.error });
|
|
1538
|
+
d.classList.add("bad");
|
|
1539
|
+
d.title = m.error;
|
|
1540
|
+
return d;
|
|
1541
|
+
}
|
|
1542
|
+
const lang = intlLang();
|
|
1543
|
+
// a gateway module's hooks, the routings it serves (strategies) and
|
|
1544
|
+
// its usage observer (onUsage), each said when it has it
|
|
1545
|
+
const parts = [];
|
|
1546
|
+
if (m.hooks.length) {
|
|
1547
|
+
let s = t("Gateway middleware: {hooks}", { hooks: m.hooks.join(t(", ")) });
|
|
1548
|
+
if (m.calls) s += " · " + t("{n} calls, {us} µs each", { n: m.calls.toLocaleString(lang), us: m.avgMicros < 10 ? m.avgMicros.toFixed(1) : Math.round(m.avgMicros).toLocaleString(lang) });
|
|
1549
|
+
parts.push(s);
|
|
1550
|
+
}
|
|
1551
|
+
if (m.strategies?.length) parts.push(t("Routings: {routings}", { routings: m.strategies.join(t(", ")) }) + (m.orders ? " · " + t("asked {n} times", { n: m.orders.toLocaleString(lang) }) : ""));
|
|
1552
|
+
if (m.usage) parts.push(t("Usage observer: {n} rows", { n: (m.rows || 0).toLocaleString(lang) }));
|
|
1553
|
+
d.append(el("span", "", parts.join(" · ")));
|
|
1554
|
+
if (m.events?.length) d.title = t("onEvent sees only {events} events", { events: m.events.join(t(", ")) });
|
|
1555
|
+
if (m.failures) {
|
|
1556
|
+
const f = el("span", "pm-mw-fail", t("{n} failed", { n: m.failures.toLocaleString(lang) }));
|
|
1557
|
+
f.title = t("A failed call leaves the request or reply as it was") + (m.lastError ? "\n" + t("Last: {error}", { error: m.lastError }) : "");
|
|
1558
|
+
d.append(el("span", "sep", " · "), f);
|
|
1559
|
+
}
|
|
1560
|
+
if (m.dropped) {
|
|
1561
|
+
const f = el("span", "pm-mw-fail", t("{n} rows dropped", { n: m.dropped.toLocaleString(lang) }));
|
|
1562
|
+
f.title = t("Rows that came while onUsage was still busy are dropped: a request never waits for it");
|
|
1563
|
+
d.append(el("span", "sep", " · "), f);
|
|
1564
|
+
}
|
|
1565
|
+
return d;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function gatewayLine(g) {
|
|
1569
|
+
const d = el("div", "sub pm-mw pm-gateway");
|
|
1570
|
+
const labels = {
|
|
1571
|
+
idle: "Waiting for a request", ready: "Context injected", empty: "No matching context",
|
|
1572
|
+
unbound: "Scope not bound", unauthorized: "Authorization failed",
|
|
1573
|
+
unavailable: "Context service unavailable", invalid: "Invalid context response",
|
|
1574
|
+
skipped: "Request skipped", cancelled: "Request cancelled", timeout: "Recall timed out", error: "Request hook failed",
|
|
1575
|
+
};
|
|
1576
|
+
let text = t("Gateway request hook") + ": " + t(labels[g.lastOutcome] || labels.idle);
|
|
1577
|
+
if (g.calls) text += " · " + t("{n} calls, {ms} ms each", { n: g.calls.toLocaleString(intlLang()), ms: g.avgMillis.toFixed(1) });
|
|
1578
|
+
if (g.lastBytes) text += " · " + t("{n} bytes injected", { n: g.lastBytes.toLocaleString(intlLang()) });
|
|
1579
|
+
d.append(el("span", "", text));
|
|
1580
|
+
if (g.failures) d.append(el("span", "sep", " · "), el("span", "pm-mw-fail", t("{n} failed", { n: g.failures.toLocaleString(intlLang()) })));
|
|
1581
|
+
d.title = t("Diagnostics reset when the plugin host restarts; prompts and recalled text are not stored");
|
|
1582
|
+
return d;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// a README on its way: its lines are strings, and magpie's bird drops in
|
|
1586
|
+
// and bounces down them, each bending under its feet and wobbling after
|
|
1587
|
+
// it; leave() sends it off when the README comes. Its marks are data-*,
|
|
1588
|
+
// not classes, so the dialog's observer doesn't place it again each hop
|
|
1589
|
+
function bounceLoader() {
|
|
1590
|
+
const box = el("div", "pm-bounce"), s = document.createElementNS("http://www.w3.org/2000/svg", "svg"), b = el("div", "sk-bird");
|
|
1591
|
+
s.innerHTML = "<path/><path/><path/><path/>";
|
|
1592
|
+
b.innerHTML = BIRD;
|
|
1593
|
+
// with Settings' Loading bird off, the strings stand still, birdless
|
|
1594
|
+
const off = birdOff();
|
|
1595
|
+
box.append(s);
|
|
1596
|
+
if (!off) box.append(b);
|
|
1597
|
+
const ps = [...s.children], ws = [1, .94, .6, .82], at = [.16, .44, .3, .58];
|
|
1598
|
+
const L = ws.map((w, i) => ({ y: 31 + i * 22, d: 0, v: 0, bx: 0, hold: false }));
|
|
1599
|
+
// the bird is placed by its feet: f faces it, r leans it, sx/sy squash it
|
|
1600
|
+
let W = 0, x = 0, y = 0, f = 1, r = 0, sx = 1, sy = 1, gone = false;
|
|
1601
|
+
const put = (o) => { ({ x = x, y = y, f = f, r = r, sx = sx, sy = sy } = o); b.style.transform = `translate(${x}px,${y}px) rotate(${r * f}deg) scale(${f * sx},${sy})`; };
|
|
1602
|
+
const draw = () => L.forEach((l, i) => {
|
|
1603
|
+
const h = l.y, d = l.d, x1 = W * ws[i] - 5, c = Math.min(x1 - 12, Math.max(17, l.bx));
|
|
1604
|
+
ps[i].setAttribute("d", `M5 ${h}C${(5 + c) / 2} ${h} ${c - (c - 5) * .35} ${h + d} ${c} ${h + d}C${c + (x1 - c) * .35} ${h + d} ${(c + x1) / 2} ${h} ${x1} ${h}`);
|
|
1605
|
+
});
|
|
1606
|
+
// a step over frames; it stops when the box is gone, or the README came
|
|
1607
|
+
const tween = (ms, fn, ease = (t) => t, exit) => new Promise((ok, no) => {
|
|
1608
|
+
const t0 = performance.now();
|
|
1609
|
+
const step = (now) => {
|
|
1610
|
+
if (!box.isConnected || (gone && !exit)) return no();
|
|
1611
|
+
const t = Math.min(1, (now - t0) / ms);
|
|
1612
|
+
fn(ease(t), t);
|
|
1613
|
+
t < 1 ? requestAnimationFrame(step) : ok();
|
|
1614
|
+
};
|
|
1615
|
+
requestAnimationFrame(step);
|
|
1616
|
+
});
|
|
1617
|
+
const easeIn = (t) => t * t * t, easeOut = (t) => 1 - (1 - t) ** 3;
|
|
1618
|
+
// a README waits for three landings (the owner, 10-30), 3s at most
|
|
1619
|
+
let ready;
|
|
1620
|
+
box.ready = new Promise((ok) => { ready = ok; });
|
|
1621
|
+
setTimeout(ready, 3000);
|
|
1622
|
+
(async () => {
|
|
1623
|
+
await new Promise(requestAnimationFrame);
|
|
1624
|
+
if (!box.isConnected || gone) return;
|
|
1625
|
+
W = box.clientWidth;
|
|
1626
|
+
if (off) return draw();
|
|
1627
|
+
if (calm()) return draw(), put({ x: at[0] * W, y: L[0].y - 5 });
|
|
1628
|
+
// each string a damped spring, let go of when the bird leaves it
|
|
1629
|
+
(async () => {
|
|
1630
|
+
let last = performance.now();
|
|
1631
|
+
for (;;) {
|
|
1632
|
+
const now = await new Promise(requestAnimationFrame), dt = Math.min(.032, (now - last) / 1000);
|
|
1633
|
+
if (!box.isConnected || gone) return;
|
|
1634
|
+
last = now;
|
|
1635
|
+
for (const l of L) if (!l.hold) { l.v += (-625 * l.d - 6 * l.v) * dt; l.d += l.v * dt; }
|
|
1636
|
+
draw();
|
|
1637
|
+
}
|
|
1638
|
+
})();
|
|
1639
|
+
for (;;) {
|
|
1640
|
+
W = box.clientWidth;
|
|
1641
|
+
ps.forEach((p) => delete p.dataset.lit);
|
|
1642
|
+
const x0 = at[0] * W;
|
|
1643
|
+
await tween(300, (e, t) => put({ x: x0 - 24 + 24 * t, y: -30 + (L[0].y + 25) * e, f: 1, r: 0, sx: 1 - .05 * t, sy: 1 + .08 * t }), easeIn);
|
|
1644
|
+
for (let i = 0; i < L.length; i++) {
|
|
1645
|
+
const l = L[i], top = l.y - 5, deep = i === L.length - 1 ? 16 : 10;
|
|
1646
|
+
l.hold = true;
|
|
1647
|
+
l.bx = x;
|
|
1648
|
+
ps[i].dataset.lit = "";
|
|
1649
|
+
await tween(140, (e) => { put({ y: top + deep * e, sx: 1 + .16 * e, sy: 1 - .2 * e }); l.d = y - top; }, easeOut);
|
|
1650
|
+
await tween(90, (e) => { put({ y: top + deep * (1 - e), sx: 1.16 - .16 * e, sy: .8 + .3 * e }); l.d = y - top; }, easeIn);
|
|
1651
|
+
l.hold = false;
|
|
1652
|
+
l.v = -deep * 14;
|
|
1653
|
+
if (i === 2) ready();
|
|
1654
|
+
const xa = x, ya = y;
|
|
1655
|
+
if (i + 1 < L.length) {
|
|
1656
|
+
const xb = at[i + 1] * W, yb = L[i + 1].y - 5;
|
|
1657
|
+
put({ f: xb > xa ? 1 : -1 });
|
|
1658
|
+
await tween(340, (e, t) => put({ x: xa + (xb - xa) * t, y: ya + (yb - ya) * t - 72 * t * (1 - t), sx: 1 - .06 * Math.sin(Math.PI * t), sy: 1 + .12 * Math.sin(Math.PI * t), r: (t - .5) * 20 }));
|
|
1659
|
+
} else {
|
|
1660
|
+
// the last one throws it high, out of sight, and it drops in again
|
|
1661
|
+
b.dataset.flap = "";
|
|
1662
|
+
await tween(520, (e) => put({ x: xa + 70 * e, y: ya - (ya + 60) * e, f: 1, r: -12, sx: 1, sy: 1 }), easeOut);
|
|
1663
|
+
delete b.dataset.flap;
|
|
1664
|
+
await tween(260, () => {});
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
})().catch(() => {}).finally(ready);
|
|
1669
|
+
box.leave = () => {
|
|
1670
|
+
if (off) return box.remove();
|
|
1671
|
+
gone = true;
|
|
1672
|
+
box.dataset.gone = b.dataset.flap = "";
|
|
1673
|
+
const xa = x, ya = y;
|
|
1674
|
+
tween(420, (e) => put({ x: xa + (W + 60 - xa) * e, y: ya - (ya + 80) * e, f: 1, r: -14, sx: 1, sy: 1 }), easeIn, true)
|
|
1675
|
+
// gone once the dialog has grown to the README, not while it does
|
|
1676
|
+
.then(() => tween(200, () => {}, undefined, true)).then(() => box.remove(), () => {});
|
|
1677
|
+
};
|
|
1678
|
+
return box;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// a README read once shows at once when its page opens again, birdless,
|
|
1682
|
+
// and is read again quietly (the owner, 10-30). The last 20 opened only,
|
|
1683
|
+
// a Map in the order they were: the oldest goes first
|
|
1684
|
+
const pages = new Map();
|
|
1685
|
+
const keep = (k, p) => { pages.delete(k); pages.set(k, p); if (pages.size > 20) pages.delete(pages.keys().next().value); };
|
|
1686
|
+
|
|
1687
|
+
// the plugin's page: what it is, what npm says of it, and its README
|
|
1688
|
+
async function detail(l) {
|
|
1689
|
+
// a folder plugin: what it is comes from the folder, not from npm; a
|
|
1690
|
+
// git one's from the folder it was installed to
|
|
1691
|
+
const local = isPath(l.package) || isGit(l.package);
|
|
1692
|
+
const ed = el("div", "editor pm-detail");
|
|
1693
|
+
const hd = el("div", "ehead pm-dhead");
|
|
1694
|
+
hd.append(logo(l.icon || l.npm?.icon, true, kindOf(l)));
|
|
1695
|
+
const who = el("div", "pm-who");
|
|
1696
|
+
const nm = el("div", "pm-name");
|
|
1697
|
+
nm.append(el("b", "", l.name));
|
|
1698
|
+
const by = el("div", "pm-by"); // the words in a span of their own, or a flex row cuts them without the ellipsis
|
|
1699
|
+
by.append(el("span", "", l.community ? t("magpie community") + " · " + l.package : [l.npm?.publisher, l.package].filter(Boolean).join(" · ")));
|
|
1700
|
+
who.append(nm, by);
|
|
1701
|
+
const actBox = el("div", "pm-dact");
|
|
1702
|
+
// redrawn only when what it would say changes: a button made afresh
|
|
1703
|
+
// loses the pointer's hover
|
|
1704
|
+
let said = "";
|
|
1705
|
+
const redrawAct = () => {
|
|
1706
|
+
const b = actionFor(l.package, l.name, l.npm ? l : null);
|
|
1707
|
+
const now = b.className + b.textContent + b.disabled;
|
|
1708
|
+
if (now !== said) { said = now; actBox.replaceChildren(b); }
|
|
1709
|
+
};
|
|
1710
|
+
redrawAct();
|
|
1711
|
+
hd.append(who, el("span", "grow"), actBox);
|
|
1712
|
+
ed.append(hd);
|
|
1713
|
+
const main = el("div", "pm-dbody");
|
|
1714
|
+
const sum = summary(l);
|
|
1715
|
+
if (sum) main.append(el("p", "pm-dsum", sum));
|
|
1716
|
+
const facts = el("div", "pm-facts");
|
|
1717
|
+
const fact = (k, v) => { if (!v) return; const f = el("div", "pm-fact"); f.append(el("span", "k", k), typeof v === "string" ? el("span", "v", v) : v); facts.append(f); };
|
|
1718
|
+
fact(t("Version"), l.npm?.version && "v" + l.npm.version);
|
|
1719
|
+
fact(t("Downloads"), l.npm?.weekly ? t("{n}/week", { n: count(l.npm.weekly) }) : "");
|
|
1720
|
+
if (l.github) fact(t("Stars"), count(l.github.stars));
|
|
1721
|
+
fact(t("License"), l.npm?.license);
|
|
1722
|
+
const upd = el("span", "v", "…");
|
|
1723
|
+
fact(t("Updated"), upd);
|
|
1724
|
+
if (l.providers?.length) fact(t("Signs in to"), l.providers.join(t(", ")));
|
|
1725
|
+
main.append(facts);
|
|
1726
|
+
const links = el("div", "pm-links");
|
|
1727
|
+
const link = (label, href) => {
|
|
1728
|
+
if (!href) return;
|
|
1729
|
+
const a = el("a", "pm-link");
|
|
1730
|
+
a.href = href;
|
|
1731
|
+
a.append(el("span", "", label), glyph(OUTL, 11, 1.5));
|
|
1732
|
+
a.onclick = (ev) => { ev.preventDefault(); api("open", { url: href }).catch(() => {}); };
|
|
1733
|
+
links.append(a);
|
|
1734
|
+
};
|
|
1735
|
+
// npm is where a package's page is; a folder on this computer has none
|
|
1736
|
+
if (!local) link("npm", "https://www.npmjs.com/package/" + l.package);
|
|
1737
|
+
link(l.github ? "GitHub" : t("Source"), l.npm?.repository);
|
|
1738
|
+
if (l.npm?.homepage && l.npm.homepage !== l.npm.repository && !l.npm.homepage.startsWith(l.npm.repository + "#")) link(t("Homepage"), l.npm.homepage);
|
|
1739
|
+
main.append(links);
|
|
1740
|
+
if (l.github) {
|
|
1741
|
+
const w = el("p", "pm-note warn");
|
|
1742
|
+
// from npm when its author published it there from the repository
|
|
1743
|
+
const from = isGit(l.package)
|
|
1744
|
+
? "Unofficial: {owner} tagged it {topic} on GitHub, and nobody at magpie has reviewed it. It is installed from the repository as it stands, and runs with your sign-in or your requests."
|
|
1745
|
+
: "Unofficial: {owner} tagged it {topic} on GitHub, and nobody at magpie has reviewed it. It is installed from npm, as its author published it from the repository, and runs with your sign-in or your requests.";
|
|
1746
|
+
w.append(glyph(SHIELD, 12, 1.5), el("span", "", t(from, { owner: l.github.owner, topic })));
|
|
1747
|
+
main.append(w);
|
|
1748
|
+
}
|
|
1749
|
+
if (l.replaces) main.append(el("p", "pm-note", t("magpie's own sign-in to this subscription is deprecated: this plugin takes its place, and accounts signed in there move to it.")));
|
|
1750
|
+
const readme = el("div", "pm-readme");
|
|
1751
|
+
const had = pages.get(l.package);
|
|
1752
|
+
const day = (p) => {
|
|
1753
|
+
const at = l.github && !entryOf(l.package) ? l.github.pushed : p.updated;
|
|
1754
|
+
upd.textContent = at && !at.startsWith("0001") ? new Date(at).toLocaleDateString(intlLang(), { year: "numeric", month: "short", day: "numeric" }) : "—";
|
|
1755
|
+
};
|
|
1756
|
+
if (had) day(had);
|
|
1757
|
+
readme.append(had ? markdown(had.readme || t("No README")) : bounceLoader());
|
|
1758
|
+
main.append(readme);
|
|
1759
|
+
ed.append(main);
|
|
1760
|
+
const bar = el("div", "bar");
|
|
1761
|
+
bar.append(el("span", "note", t("Plugins are other people's code: they sign in and make the requests, or see them as middleware.")), el("span", "grow"));
|
|
1762
|
+
const close = el("button", "text", t("Close"));
|
|
1763
|
+
close.onclick = () => { stop(); closeModal(); };
|
|
1764
|
+
bar.append(close);
|
|
1765
|
+
ed.append(bar);
|
|
1766
|
+
openModal(ed);
|
|
1767
|
+
// the button follows an install or a sign-in made while it's open
|
|
1768
|
+
const tick = setInterval(() => { if (!ed.isConnected) return stop(); redrawAct(); }, 600);
|
|
1769
|
+
function stop() { clearInterval(tick); }
|
|
1770
|
+
// the README takes the skeleton's place, and the dialog grows to it on
|
|
1771
|
+
// the spring it opened on, centred as it goes, not in one jump; the
|
|
1772
|
+
// bird flies off over it
|
|
1773
|
+
const fill = async (n) => {
|
|
1774
|
+
await readme.querySelector(".pm-bounce")?.ready;
|
|
1775
|
+
if (!ed.isConnected) return;
|
|
1776
|
+
const d = ed.parentElement, h = d.offsetHeight, y = d.style.top, wait = readme.querySelector(".pm-bounce");
|
|
1777
|
+
const m = d.parentElement, at = wait?.getBoundingClientRect(), o = m.getBoundingClientRect();
|
|
1778
|
+
readme.replaceChildren(n);
|
|
1779
|
+
if (!ed.isConnected || calm()) return;
|
|
1780
|
+
// it leaves from over the dialog, where it was: anywhere in it, WebKit
|
|
1781
|
+
// lays the fields out 8px short while it is there
|
|
1782
|
+
if (wait) {
|
|
1783
|
+
Object.assign(wait.style, { top: at.top - o.top + m.scrollTop + "px", left: at.left - o.left + m.scrollLeft + "px", width: at.width + "px" });
|
|
1784
|
+
m.append(wait);
|
|
1785
|
+
wait.leave();
|
|
1786
|
+
}
|
|
1787
|
+
holdModal();
|
|
1788
|
+
d.animate([{ height: h + "px", top: y, overflow: "hidden" }, { height: d.offsetHeight + "px", top: d.style.top, overflow: "hidden" }], { duration: 570, easing: SPRING });
|
|
1789
|
+
n.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, easing: "ease-out" });
|
|
1790
|
+
};
|
|
1791
|
+
try {
|
|
1792
|
+
const p = await api("plugins/page?name=" + encodeURIComponent(l.package));
|
|
1793
|
+
keep(l.package, p);
|
|
1794
|
+
day(p);
|
|
1795
|
+
if (!had) fill(markdown(p.readme || t("No README")));
|
|
1796
|
+
else if (p.readme !== had.readme) readme.replaceChildren(markdown(p.readme || t("No README")));
|
|
1797
|
+
} catch (e) {
|
|
1798
|
+
if (had) return;
|
|
1799
|
+
upd.textContent = "—";
|
|
1800
|
+
fill(el("p", "pm-note", t(l.github && isGit(l.package) && !entryOf(l.package) ? "GitHub didn't answer: {error}" : local ? "Couldn't read the folder's README: {error}" : "npm didn't answer: {error}", { error: e.message })));
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// markdown: a README as text, links and code — no HTML of its own, no
|
|
1805
|
+
// pictures (a README's badges and screenshots come from anywhere)
|
|
1806
|
+
function markdown(src) {
|
|
1807
|
+
const root = el("div", "pm-md");
|
|
1808
|
+
const lines = src.replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->|<(script|style)\b[\s\S]*?<\/\1\s*>/gi, "")
|
|
1809
|
+
.replace(/\[\s*!\[[^\]]*\]\([^)]*\)\s*\]\([^)]*\)/g, "") // a badge: a picture in a link
|
|
1810
|
+
.split("\n");
|
|
1811
|
+
// a link: to the web, opened outside; to a heading of the README
|
|
1812
|
+
// ("#-quick-start"), there, as the reader asked
|
|
1813
|
+
const link = (href, text, into) => {
|
|
1814
|
+
const a = el("a", "", "");
|
|
1815
|
+
a.href = href;
|
|
1816
|
+
inline(text.replace(/!\[[^\]]*\]\([^)]*\)/g, "").trim() || href, a);
|
|
1817
|
+
a.onclick = (ev) => {
|
|
1818
|
+
ev.preventDefault();
|
|
1819
|
+
if (href[0] !== "#") return api("open", { url: href }).catch(() => {});
|
|
1820
|
+
const to = root.querySelector(`[data-slug="${CSS.escape(decodeURIComponent(href.slice(1)).toLowerCase())}"]`);
|
|
1821
|
+
if (to && window.scrollOnPurpose?.(ev)) to.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
1822
|
+
};
|
|
1823
|
+
into.append(a);
|
|
1824
|
+
};
|
|
1825
|
+
const inline = (text, into) => {
|
|
1826
|
+
const re = /(`[^`]+`)|\*\*(.+?)\*\*|__(.+?)__|\*([^*\s][^*]*)\*|!\[[^\]]*\]\([^)]*\)|\[([^\]]+)\]\(([^)\s]+)[^)]*\)|<(https?:\/\/[^>\s]+)>|<a\s[^>]*?href\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/a\s*>|<\/?[a-zA-Z][^>]*>/g;
|
|
1827
|
+
let at = 0, m;
|
|
1828
|
+
while ((m = re.exec(text))) {
|
|
1829
|
+
if (m.index > at) into.append(text.slice(at, m.index));
|
|
1830
|
+
if (m[1]) into.append(el("code", "", m[1].slice(1, -1)));
|
|
1831
|
+
else if (m[2] || m[3]) { const b = el("strong"); inline(m[2] || m[3], b); into.append(b); }
|
|
1832
|
+
else if (m[4]) { const e = el("em"); inline(m[4], e); into.append(e); }
|
|
1833
|
+
else if (m[5] || m[8]) {
|
|
1834
|
+
const href = m[6] || m[8], text = m[5] || m[9].replace(/<[^>]*>/g, "");
|
|
1835
|
+
if (/^https?:\/\//.test(href) || /^#./.test(href)) link(href, text, into);
|
|
1836
|
+
else inline(text, into);
|
|
1837
|
+
} else if (m[7]) link(m[7], m[7], into);
|
|
1838
|
+
at = re.lastIndex;
|
|
1839
|
+
}
|
|
1840
|
+
if (at < text.length) into.append(text.slice(at));
|
|
1841
|
+
};
|
|
1842
|
+
// GitHub's anchor for a heading: lower case, its punctuation and
|
|
1843
|
+
// emoji gone, spaces as dashes ("💡 Philosophy" is "-philosophy")
|
|
1844
|
+
const slug = (s) => s.trim().toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s/g, "-");
|
|
1845
|
+
let i = 0, list = null, para = null;
|
|
1846
|
+
const flush = () => { list = null; para = null; };
|
|
1847
|
+
while (i < lines.length) {
|
|
1848
|
+
const line = lines[i];
|
|
1849
|
+
const fence = line.match(/^\s*(```|~~~)\s*([\w+-]*)/);
|
|
1850
|
+
if (fence) {
|
|
1851
|
+
flush();
|
|
1852
|
+
const code = [];
|
|
1853
|
+
i++;
|
|
1854
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(fence[1])) code.push(lines[i++]);
|
|
1855
|
+
i++;
|
|
1856
|
+
const pre = el("pre", "", "");
|
|
1857
|
+
pre.append(el("code", "", code.join("\n")));
|
|
1858
|
+
root.append(pre);
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
const h = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
|
|
1862
|
+
if (h) {
|
|
1863
|
+
flush();
|
|
1864
|
+
const e = el("h" + Math.min(6, h[1].length + 2));
|
|
1865
|
+
inline(h[2], e);
|
|
1866
|
+
e.dataset.slug = slug(e.textContent);
|
|
1867
|
+
if (e.textContent.trim()) root.append(e);
|
|
1868
|
+
i++;
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (/^\s*\|.*\|\s*$/.test(line) && /^\s*\|?\s*:?-{2,}/.test(lines[i + 1] || "")) {
|
|
1872
|
+
flush();
|
|
1873
|
+
const cells = (s) => s.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
|
|
1874
|
+
const wrap = el("div", "pm-table");
|
|
1875
|
+
const table = el("table");
|
|
1876
|
+
const tr = el("tr");
|
|
1877
|
+
for (const c of cells(line)) { const th = el("th"); inline(c, th); tr.append(th); }
|
|
1878
|
+
table.append(tr);
|
|
1879
|
+
i += 2;
|
|
1880
|
+
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) {
|
|
1881
|
+
const r = el("tr");
|
|
1882
|
+
for (const c of cells(lines[i])) { const td = el("td"); inline(c, td); r.append(td); }
|
|
1883
|
+
table.append(r);
|
|
1884
|
+
i++;
|
|
1885
|
+
}
|
|
1886
|
+
wrap.append(table);
|
|
1887
|
+
root.append(wrap);
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
const li = line.match(/^\s*([-*+]|\d+[.)])\s+(.*)$/);
|
|
1891
|
+
if (li) {
|
|
1892
|
+
para = null;
|
|
1893
|
+
const ordered = /\d/.test(li[1]);
|
|
1894
|
+
if (!list || list.tagName !== (ordered ? "OL" : "UL")) { list = el(ordered ? "ol" : "ul"); root.append(list); }
|
|
1895
|
+
const item = el("li");
|
|
1896
|
+
inline(li[2].replace(/^\[( |x)\]\s*/i, (s, x) => x.trim() ? "✓ " : "☐ "), item);
|
|
1897
|
+
list.append(item);
|
|
1898
|
+
i++;
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
const q = line.match(/^\s*>\s?(.*)$/);
|
|
1902
|
+
if (q) {
|
|
1903
|
+
flush();
|
|
1904
|
+
const bq = el("blockquote");
|
|
1905
|
+
const parts = [];
|
|
1906
|
+
while (i < lines.length && /^\s*>/.test(lines[i])) parts.push(lines[i++].replace(/^\s*>\s?/, ""));
|
|
1907
|
+
inline(parts.join(" ").replace(/^\[!(\w+)\]\s*/, (s, k) => k.toUpperCase() + ": "), bq);
|
|
1908
|
+
root.append(bq);
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (/^\s*([-*_])\s*\1\s*\1[\s\1]*$/.test(line)) { flush(); root.append(el("hr")); i++; continue; }
|
|
1912
|
+
if (!line.trim()) { flush(); i++; continue; }
|
|
1913
|
+
if (list && /^\s{2,}\S/.test(line)) { const last = list.lastElementChild; last.append(" "); inline(line.trim(), last); i++; continue; }
|
|
1914
|
+
if (!para) { para = el("p"); root.append(para); list = null; } else para.append(" ");
|
|
1915
|
+
inline(line.trim(), para);
|
|
1916
|
+
if (!para.textContent.trim() && !para.children.length) { para.remove(); para = null; }
|
|
1917
|
+
i++;
|
|
1918
|
+
}
|
|
1919
|
+
for (const p of root.querySelectorAll("p")) if (!p.textContent.trim()) p.remove();
|
|
1920
|
+
return root;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// opened on the Plugins tab (?view=plugins): app.js showed it before
|
|
1924
|
+
// this file was here to load it
|
|
1925
|
+
if (view === "plugins") load();
|
|
1926
|
+
})();
|