dsh-mobile-plugin 0.5.27 → 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,53 @@
|
|
|
1
|
+
// A masking rule open to change in its place. app.js imports this on a
|
|
2
|
+
// rule's Edit, so it weighs nothing on the core.
|
|
3
|
+
|
|
4
|
+
// editRedactRule: the rule at n, open to change in its place (nf1qaz2w on
|
|
5
|
+
// Discord: rules could only be removed) — its name, prefix or regex and
|
|
6
|
+
// what to match, laid out as the row that adds one. Save sends it alone,
|
|
7
|
+
// checked as an added rule is: one magpie can't use is said in the row,
|
|
8
|
+
// what was typed kept and every rule as it was. Cancel (or Escape) leaves
|
|
9
|
+
// the rule as it was.
|
|
10
|
+
window.editRedactRule = function (r, n, row) {
|
|
11
|
+
const d = ruleEdit;
|
|
12
|
+
const kind = input(d.kind, "API_KEY");
|
|
13
|
+
const match = input(d.match, d.by === "prefix" ? "oc_sk_" : "oc_sk_[A-Za-z0-9]{20,}");
|
|
14
|
+
kind.className = "words rule-kind editing";
|
|
15
|
+
match.className = "words rule-match editing";
|
|
16
|
+
kind.setAttribute("aria-label", t("Name"));
|
|
17
|
+
match.setAttribute("aria-label", d.by === "prefix" ? t("Prefix") : t("Regex"));
|
|
18
|
+
const by = segs([["prefix", t("Prefix")], ["regex", t("Regex")]], d.by, (v) => {
|
|
19
|
+
d.by = v;
|
|
20
|
+
match.placeholder = v === "prefix" ? "oc_sk_" : "oc_sk_[A-Za-z0-9]{20,}";
|
|
21
|
+
match.setAttribute("aria-label", v === "prefix" ? t("Prefix") : t("Regex"));
|
|
22
|
+
});
|
|
23
|
+
const save = el("button", "text rule-save", t("Save"));
|
|
24
|
+
const cancel = el("button", "text rule-cancel", t("Cancel"));
|
|
25
|
+
cancel.onclick = () => { ruleEdit = null; renderSettings(); };
|
|
26
|
+
save.onclick = () => {
|
|
27
|
+
d.kind = kind.value; d.match = match.value.trim();
|
|
28
|
+
if (!d.match) return match.focus(NO_SCROLL);
|
|
29
|
+
const rule = { kind: d.kind.trim(), [d.by === "prefix" ? "prefix" : "regex"]: d.match };
|
|
30
|
+
save.disabled = true;
|
|
31
|
+
writingPrefs(api("settings/redact-rule", { at: n, was: d.was, rule }))
|
|
32
|
+
.then((ns) => { prefs = ns; ruleEdit = null; renderSettings(); status(t("Saved"), "ok", 1500); })
|
|
33
|
+
.catch((e) => { if (ruleEdit === d) d.err = t(e.message); statusTErr(e); renderSettings(); });
|
|
34
|
+
};
|
|
35
|
+
for (const i of [kind, match]) {
|
|
36
|
+
i.oninput = () => { d.kind = kind.value; d.match = match.value; };
|
|
37
|
+
i.onkeydown = (e) => {
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
if (e.key === "Enter") save.onclick();
|
|
40
|
+
else if (e.key === "Escape") cancel.onclick();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// the match with Save and Cancel, kept together: a narrow window moves
|
|
44
|
+
// them as one under the name and Prefix or Regex
|
|
45
|
+
const tail = el("div", "rule-tail");
|
|
46
|
+
tail.append(match, save, cancel);
|
|
47
|
+
row(r.kind, d.err || (r.prefix ? t("Starts with {p}", { p: r.prefix }) : t("Matches {re}", { re: r.regex })), kind, by, tail);
|
|
48
|
+
// laid out as the row that adds a rule: its words, the fields under them
|
|
49
|
+
const head = $("#redactList").lastElementChild;
|
|
50
|
+
head.classList.add("rule-row", "rule-editing");
|
|
51
|
+
head.querySelector(".val").classList.add("rule-add");
|
|
52
|
+
if (d.err) head.querySelector(".sub").classList.add("err");
|
|
53
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Scroll to top and Scroll to bottom (#1090, emo172): a small capsule at the
|
|
2
|
+
// lower right of the open page, shown once the page is scrolled a screen
|
|
3
|
+
// down and gone again near its top, like a long page's in Safari. app.js
|
|
4
|
+
// imports this on the window's first scroll, so it weighs nothing on the
|
|
5
|
+
// core. A press is the reader's: it goes through scrollOnPurpose, and
|
|
6
|
+
// arrived() keeps the page where it ended. Home and End still scroll the
|
|
7
|
+
// page as they always did; the buttons are for a mouse, and Tab reaches
|
|
8
|
+
// them too. The tray panel has none.
|
|
9
|
+
(() => {
|
|
10
|
+
if (document.body.classList.contains("panel")) return;
|
|
11
|
+
const box = el("div", "view-ends");
|
|
12
|
+
const up = el("button", "ve-up"), down = el("button", "ve-down");
|
|
13
|
+
up.type = down.type = "button";
|
|
14
|
+
up.append(svg("m4.5 9.5 3.5-3.5 3.5 3.5", 14, 1.7));
|
|
15
|
+
down.append(svg("m4.5 6.5 3.5 3.5 3.5-3.5", 14, 1.7));
|
|
16
|
+
box.append(up, el("span", "ve-sep"), down);
|
|
17
|
+
document.body.append(box);
|
|
18
|
+
let lang = null;
|
|
19
|
+
const open = () => [...document.querySelectorAll(".view")].find((v) => !v.hidden);
|
|
20
|
+
const atEnd = (v) => v.scrollTop + v.clientHeight >= v.scrollHeight - 2;
|
|
21
|
+
function paint() {
|
|
22
|
+
if (lang !== locale) {
|
|
23
|
+
lang = locale;
|
|
24
|
+
for (const [b, s] of [[up, "Scroll to top"], [down, "Scroll to bottom"]]) b.title = t(s), b.setAttribute("aria-label", t(s));
|
|
25
|
+
}
|
|
26
|
+
const v = open(), modal = document.getElementById("modal");
|
|
27
|
+
if (v) watch(v);
|
|
28
|
+
const held = box.contains(document.activeElement) || box.matches(":hover");
|
|
29
|
+
let on = !!v && (!modal || modal.hidden) && v.scrollTop > v.clientHeight;
|
|
30
|
+
if (!v || !on && !held) { box.classList.remove("on"); return; }
|
|
31
|
+
const r = v.getBoundingClientRect(), bar = v.offsetWidth - v.clientWidth, pad = parseFloat(getComputedStyle(v).paddingRight) || 0;
|
|
32
|
+
// in the page's right margin when a wide window leaves one; else over the
|
|
33
|
+
// page's corner, and out of the way at the page's foot, where its last
|
|
34
|
+
// row can't be scrolled out from under it. The page's own height is
|
|
35
|
+
// never changed for it: app.js's "where the reader is" measures it.
|
|
36
|
+
const gutter = pad >= box.offsetWidth + 16;
|
|
37
|
+
if (!gutter && v.scrollHeight - v.clientHeight - v.scrollTop < box.offsetHeight + 24) on = false;
|
|
38
|
+
box.classList.toggle("on", on || held);
|
|
39
|
+
let bottom = innerHeight - r.bottom + 16;
|
|
40
|
+
// above a page's own sticky foot under it (Providers' Add buttons, a
|
|
41
|
+
// Routing group editor's Cancel and Add: Jiabubu on Discord)
|
|
42
|
+
for (const foot of v.querySelectorAll(":scope > .after-list:not(.away), .view-foot")) {
|
|
43
|
+
const f = foot.getBoundingClientRect();
|
|
44
|
+
if (getComputedStyle(foot).position === "sticky" && f.bottom > innerHeight - bottom - box.offsetHeight && f.top < innerHeight - bottom) bottom = Math.max(bottom, innerHeight - f.top + 8);
|
|
45
|
+
}
|
|
46
|
+
box.style.right = Math.max(8, innerWidth - r.right + bar + (gutter ? (pad - box.offsetWidth) / 2 : 10)) + "px";
|
|
47
|
+
box.style.bottom = bottom + "px";
|
|
48
|
+
up.setAttribute("aria-disabled", v.scrollTop < 1);
|
|
49
|
+
down.setAttribute("aria-disabled", atEnd(v));
|
|
50
|
+
}
|
|
51
|
+
let queued = false;
|
|
52
|
+
const later = () => { if (!queued) { queued = true; requestAnimationFrame(() => { queued = false; paint(); }); } };
|
|
53
|
+
addEventListener("scroll", later, { capture: true, passive: true });
|
|
54
|
+
addEventListener("resize", later);
|
|
55
|
+
box.addEventListener("focusout", later);
|
|
56
|
+
box.addEventListener("pointerleave", later);
|
|
57
|
+
new MutationObserver(later).observe(document.body, { subtree: true, attributes: true, attributeFilter: ["hidden"] });
|
|
58
|
+
// the page under it changing size with no scroll: a Routing group opened
|
|
59
|
+
// or closed, a row added or taken out (Jiabubu on Discord: it moved off
|
|
60
|
+
// a group editor's Remove only on the next scroll). The open page and
|
|
61
|
+
// each of its own children are measured, so any growth inside shows
|
|
62
|
+
const sizes = new ResizeObserver(later), seen = new WeakSet();
|
|
63
|
+
const kids = new MutationObserver((ms) => {
|
|
64
|
+
for (const m of ms) for (const n of m.removedNodes) if (n instanceof Element) sizes.unobserve(n), seen.delete(n);
|
|
65
|
+
later();
|
|
66
|
+
});
|
|
67
|
+
function watch(v) {
|
|
68
|
+
if (!seen.has(v)) seen.add(v), sizes.observe(v), kids.observe(v, { childList: true });
|
|
69
|
+
for (const c of v.children) if (!seen.has(c)) seen.add(c), sizes.observe(c);
|
|
70
|
+
}
|
|
71
|
+
function go(e, top) {
|
|
72
|
+
const v = open();
|
|
73
|
+
if (!v || e.currentTarget.getAttribute("aria-disabled") === "true") return;
|
|
74
|
+
if (!scrollOnPurpose(e, 2500)) return;
|
|
75
|
+
const to = top ? 0 : v.scrollHeight - v.clientHeight;
|
|
76
|
+
v.scrollTo({ top: to, behavior: calm() ? "auto" : "smooth" });
|
|
77
|
+
// done when it has stood still a few frames (WebKit has no scrollend)
|
|
78
|
+
let last = -1, still = 0;
|
|
79
|
+
const until = performance.now() + 2400;
|
|
80
|
+
const settle = () => {
|
|
81
|
+
if (v.scrollTop === last) still++; else still = 0, last = v.scrollTop;
|
|
82
|
+
if (still < 3 && performance.now() < until) return requestAnimationFrame(settle);
|
|
83
|
+
arrived(v);
|
|
84
|
+
paint();
|
|
85
|
+
};
|
|
86
|
+
requestAnimationFrame(settle);
|
|
87
|
+
}
|
|
88
|
+
up.onclick = (e) => go(e, true);
|
|
89
|
+
down.onclick = (e) => go(e, false);
|
|
90
|
+
paint();
|
|
91
|
+
})();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Settings' explanations, shown on asking (the owner, 11-02: "让整个设计
|
|
2
|
+
// 页面看起来很简洁,很有呼吸感"). app.css keeps a row's lines that explain
|
|
3
|
+
// it out of sight (--hint: 1 on each); the pointer resting on the row, or a
|
|
4
|
+
// key reaching it, shows them here in a card under the row's name, over
|
|
5
|
+
// the page, so no row moves. The card takes no pointer: a click goes to
|
|
6
|
+
// what is under it. It goes when the pointer or focus leaves the row, the
|
|
7
|
+
// page scrolls, a key is pressed or the row is drawn again. Imported by
|
|
8
|
+
// app.js when the pointer or focus first comes to Settings, not at start.
|
|
9
|
+
const view = document.getElementById("view-settings");
|
|
10
|
+
const card = el("div", "set-hint");
|
|
11
|
+
card.setAttribute("aria-hidden", "true"); // a screen reader reads the lines in the row
|
|
12
|
+
document.body.append(card);
|
|
13
|
+
let row = null, wait = 0;
|
|
14
|
+
|
|
15
|
+
const acts = () => {
|
|
16
|
+
for (const s of view.querySelectorAll(".row.pref > .who > .sub")) s.classList.toggle("acts", !!s.querySelector("a, button, input, select, code, .searcher-failed"));
|
|
17
|
+
};
|
|
18
|
+
// before the page is painted again: no frame shows such a line hidden
|
|
19
|
+
new MutationObserver(acts).observe(view, { childList: true, subtree: true });
|
|
20
|
+
acts();
|
|
21
|
+
|
|
22
|
+
const hints = (r) => [...r.querySelectorAll(":scope > .who > .sub")]
|
|
23
|
+
.filter((s) => !s.hidden && getComputedStyle(s).getPropertyValue("--hint").trim() === "1" && s.textContent.trim());
|
|
24
|
+
|
|
25
|
+
function hide() {
|
|
26
|
+
clearTimeout(wait);
|
|
27
|
+
row = null;
|
|
28
|
+
card.classList.remove("on");
|
|
29
|
+
card.hidden = true;
|
|
30
|
+
}
|
|
31
|
+
function show(r) {
|
|
32
|
+
const subs = r.isConnected && !view.hidden ? hints(r) : [];
|
|
33
|
+
if (!subs.length) return hide();
|
|
34
|
+
card.replaceChildren(...subs.map((s) => el("p", "", s.textContent.trim())));
|
|
35
|
+
card.hidden = false;
|
|
36
|
+
const who = r.querySelector(":scope > .who").getBoundingClientRect(), box = r.getBoundingClientRect();
|
|
37
|
+
const w = card.offsetWidth, h = card.offsetHeight;
|
|
38
|
+
const left = Math.max(16, Math.min(who.left, innerWidth - 16 - w));
|
|
39
|
+
// under the row, or over it where the window ends first
|
|
40
|
+
const top = box.bottom + 4 + h <= innerHeight - 8 ? box.bottom + 4 : Math.max(8, box.top - 4 - h);
|
|
41
|
+
card.style.left = left + "px";
|
|
42
|
+
card.style.top = top + "px";
|
|
43
|
+
requestAnimationFrame(() => row === r && card.classList.add("on"));
|
|
44
|
+
}
|
|
45
|
+
function point(r, now) {
|
|
46
|
+
if (r === row) return;
|
|
47
|
+
hide();
|
|
48
|
+
if (!r) return;
|
|
49
|
+
row = r;
|
|
50
|
+
// a pointer passing over rows on its way shows nothing; resting does
|
|
51
|
+
wait = setTimeout(() => show(r), now ? 0 : 450);
|
|
52
|
+
}
|
|
53
|
+
const rowOf = (n) => n instanceof Element ? n.closest("#view-settings .row.pref") : null;
|
|
54
|
+
view.addEventListener("pointerover", (e) => e.pointerType !== "touch" && point(rowOf(e.target)));
|
|
55
|
+
view.addEventListener("pointerleave", () => hide());
|
|
56
|
+
view.addEventListener("pointerdown", () => hide(), true);
|
|
57
|
+
view.addEventListener("focusin", (e) => {
|
|
58
|
+
let key = true;
|
|
59
|
+
try { key = e.target.matches(":focus-visible"); } catch {} // Safari before 15.4
|
|
60
|
+
if (key) point(rowOf(e.target), true);
|
|
61
|
+
});
|
|
62
|
+
view.addEventListener("focusout", (e) => {
|
|
63
|
+
if (!rowOf(e.relatedTarget) || rowOf(e.relatedTarget) !== row) hide();
|
|
64
|
+
});
|
|
65
|
+
// a scroll takes the pointer's card away; focus's follows its row, which
|
|
66
|
+
// a key moving focus scrolls into sight
|
|
67
|
+
view.addEventListener("scroll", () => row?.contains(document.activeElement) ? show(row) : hide(), { passive: true, capture: true });
|
|
68
|
+
addEventListener("keydown", (e) => !/^(Tab|Shift|Alt|Meta|Control)$/.test(e.key) && hide(), true);
|
|
69
|
+
addEventListener("resize", hide);
|
|
70
|
+
hide();
|
|
71
|
+
// the pointer already on a row, or focus, as the import came
|
|
72
|
+
{
|
|
73
|
+
const on = view.querySelector(".row.pref:hover") || rowOf(document.activeElement);
|
|
74
|
+
if (on) point(on);
|
|
75
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Sign-ins that ask one value before they start. app.js imports this only
|
|
2
|
+
// when one is asked for (signAsk), so it weighs nothing on the core.
|
|
3
|
+
|
|
4
|
+
// Copilot's enterprise on GHE.com: its host, asked before its sign-in starts
|
|
5
|
+
window.copilotGHESheet = function () {
|
|
6
|
+
const box = el("div", "signing copilot-ghe");
|
|
7
|
+
const tt = el("span", "tt");
|
|
8
|
+
box.append(tt);
|
|
9
|
+
const flow = signing;
|
|
10
|
+
const label = t("Your enterprise on GHE.com");
|
|
11
|
+
tt.append(el("span", "n", label),
|
|
12
|
+
el("span", "s", t("The address you sign in to GitHub at, like acme.ghe.com. An account on github.com doesn't need it.")));
|
|
13
|
+
const form = el("form", "callback-form");
|
|
14
|
+
const inp = input(flow.host || "", "acme.ghe.com");
|
|
15
|
+
inp.setAttribute("aria-label", label);
|
|
16
|
+
const go = el("button", "text primary", t("Sign in"));
|
|
17
|
+
go.type = "submit";
|
|
18
|
+
go.disabled = !GHE_HOST.test(inp.value.trim());
|
|
19
|
+
const why = el("span", "s why", "");
|
|
20
|
+
inp.oninput = () => { flow.host = inp.value; go.disabled = !GHE_HOST.test(inp.value.trim()); why.textContent = ""; };
|
|
21
|
+
const submit = (e) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
if (signing !== flow) return;
|
|
24
|
+
const host = inp.value.trim().replace(/^https?:\/\//i, "").replace(/\/$/, "").toLowerCase();
|
|
25
|
+
if (!GHE_HOST.test(host)) { why.textContent = t("Type your enterprise's <name>.ghe.com"); return; }
|
|
26
|
+
startSignIn("copilot", true, host);
|
|
27
|
+
};
|
|
28
|
+
form.onsubmit = submit;
|
|
29
|
+
go.onclick = submit;
|
|
30
|
+
form.append(inp, go);
|
|
31
|
+
tt.append(form, why);
|
|
32
|
+
const close = el("button", "text", t("Cancel"));
|
|
33
|
+
close.onclick = cancelSignIn;
|
|
34
|
+
const back = el("button", "text", "github.com");
|
|
35
|
+
back.title = t("Sign in on github.com instead");
|
|
36
|
+
back.onclick = () => startSignIn("copilot", true);
|
|
37
|
+
box.append(close, back);
|
|
38
|
+
setTimeout(() => { if (inp.isConnected) inp.focus({ preventScroll: true }); });
|
|
39
|
+
return box;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// a Claude account from the token `claude setup-token` prints (sfss5362 on
|
|
43
|
+
// X; claude_setup_token.go)
|
|
44
|
+
window.claudeTokenSheet = function () {
|
|
45
|
+
const box = el("div", "signing claude-token");
|
|
46
|
+
const tt = el("span", "tt");
|
|
47
|
+
box.append(tt);
|
|
48
|
+
const flow = signing;
|
|
49
|
+
const label = t("Add a Claude account by setup-token");
|
|
50
|
+
tt.append(el("span", "n", label),
|
|
51
|
+
el("span", "s", t("Paste the token {cmd} printed. magpie gives it to Claude Code for this account's requests only; Claude Code itself stays signed in as it is.", { cmd: "claude setup-token" })),
|
|
52
|
+
el("span", "s", t("A setup-token can't tell its email or allowance: the account is named by the token, and its allowance shows once a reply tells it.")));
|
|
53
|
+
const form = el("form", "callback-form");
|
|
54
|
+
const inp = input(flow.token || "", "sk-ant-oat01-…", "password");
|
|
55
|
+
inp.setAttribute("aria-label", label);
|
|
56
|
+
const go = el("button", "text primary", t("Add"));
|
|
57
|
+
go.type = "submit";
|
|
58
|
+
go.disabled = !inp.value.trim() || !!flow.busy;
|
|
59
|
+
const why = el("span", "s why", flow.error || "");
|
|
60
|
+
inp.oninput = () => { flow.token = inp.value; go.disabled = !inp.value.trim(); why.textContent = ""; };
|
|
61
|
+
const submit = async (e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
if (signing !== flow || flow.busy || !inp.value.trim()) return;
|
|
64
|
+
flow.busy = true;
|
|
65
|
+
go.disabled = true;
|
|
66
|
+
try {
|
|
67
|
+
const r = await api("signin/claude-token", { token: inp.value });
|
|
68
|
+
if (signing !== flow) return;
|
|
69
|
+
await signedIn({ agent: "claude", user: r.user, token: true });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (signing !== flow) return;
|
|
72
|
+
flow.busy = false;
|
|
73
|
+
flow.error = err.message;
|
|
74
|
+
renderProviders();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
form.onsubmit = submit;
|
|
78
|
+
go.onclick = submit;
|
|
79
|
+
form.append(inp, go);
|
|
80
|
+
tt.append(form, why);
|
|
81
|
+
const close = el("button", "text", t("Cancel"));
|
|
82
|
+
close.onclick = cancelSignIn;
|
|
83
|
+
box.append(close);
|
|
84
|
+
setTimeout(() => { if (inp.isConnected) inp.focus({ preventScroll: true }); });
|
|
85
|
+
return box;
|
|
86
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The window's start, loaded last: app.js, the plugins' pages and the
|
|
2
|
+
// scripts after app.js are all there, so the first page shown has its
|
|
3
|
+
// code (the Usage page's is a plugin's, run after app.js).
|
|
4
|
+
// the plugins' pages wired up (kernel.js), before the first is shown
|
|
5
|
+
magpie.start();
|
|
6
|
+
navHides(); // the pages built-in plugins switched off have, from boot.js
|
|
7
|
+
// The window opens on the page the reader last left, as a Mac app comes back
|
|
8
|
+
// as it was (molvqingtai, lc on Discord): a page asked for (?view=, a link
|
|
9
|
+
// from the tray or a reload) first, then the one kept; a page gateway mode
|
|
10
|
+
// leaves out, or one there is no longer, gives way to the first page.
|
|
11
|
+
// the pages the window can open on besides the first (Agents)
|
|
12
|
+
const OPEN_VIEWS = ["providers", "plugins", "settings", ...magpie.pages()];
|
|
13
|
+
function keptView() {
|
|
14
|
+
const asked = params.get("view");
|
|
15
|
+
if (asked === "agents" || OPEN_VIEWS.includes(asked)) return asked;
|
|
16
|
+
let v = "";
|
|
17
|
+
try { v = localStorage.getItem("magpie.lastView") || ""; } catch { /* none kept */ }
|
|
18
|
+
return OPEN_VIEWS.includes(v) ? v : ""; // one gateway mode leaves out, show() makes Providers
|
|
19
|
+
}
|
|
20
|
+
const firstView = mode === "window" ? keptView() : "";
|
|
21
|
+
// from the tray panel's More in Plugins (q): Discover, once show() picked the tab (#3579)
|
|
22
|
+
if (OPEN_VIEWS.includes(firstView)) show(firstView).then(() => view === "plugins" && params.has("q") && window.pluginQuery?.(params.get("q")));
|
|
23
|
+
else if (mode === "window" && !gatewayMode && !sideNav()) slide($("#nav"), "nav");
|
|
24
|
+
// in gateway mode the first page is Providers, Agents' tab gone
|
|
25
|
+
if (gatewayMode) setGatewayMode(true);
|
|
26
|
+
load();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Settings › Sync's forms: the server's (WebDAV, S3 or GitHub), Export and
|
|
2
|
+
// Import, and what putting a file's sign-ins in came to (qianxunji,
|
|
3
|
+
// curtinp118 on Discord). app.js imports this when one of the forms opens
|
|
4
|
+
// (syncFile), so it weighs nothing on the core.
|
|
5
|
+
|
|
6
|
+
// davForm: the kind picked shows one set of fields; drafts survive switching.
|
|
7
|
+
// Inactive servers keep their settings for moving sync back.
|
|
8
|
+
window.davForm = function (v) {
|
|
9
|
+
const ed = el("div", "editor sync-form");
|
|
10
|
+
const active = v.on ? (v.kind || "webdav") : "";
|
|
11
|
+
let kind = active || "webdav";
|
|
12
|
+
const saved = t("saved · type a new one to replace it");
|
|
13
|
+
const servers = syncServers(v);
|
|
14
|
+
const server = (k) => active === k ? v : servers[k] || {};
|
|
15
|
+
const dav = server("webdav"), bk = server("s3"), gh = server("github");
|
|
16
|
+
const url = input(dav.url || "", "https://dav.jianguoyun.com/dav/");
|
|
17
|
+
const user = input(dav.user || "", t("user name"));
|
|
18
|
+
const pass = input("", dav.passwordSet ? saved : t("password, or an app password"), "password");
|
|
19
|
+
// s3://bucket/prefix, as the address is kept
|
|
20
|
+
const [bucketName, prefixName] = (() => { const m = /^s3:\/\/([^/]*)\/?(.*)$/i.exec(bk.url || ""); return m ? [m[1], m[2]] : ["", ""]; })();
|
|
21
|
+
const endpoint = input(bk.endpoint || "", "https://<account>.r2.cloudflarestorage.com");
|
|
22
|
+
const bucket = input(bucketName, t("bucket name"));
|
|
23
|
+
const prefix = input(prefixName, t("optional"));
|
|
24
|
+
const region = input(bk.region || "", t("us-east-1, or auto for R2"));
|
|
25
|
+
const keyID = input(bk.user || "", "AKIA…");
|
|
26
|
+
const secret = input("", bk.passwordSet ? saved : t("secret access key"), "password");
|
|
27
|
+
const [pathL, pathStyle] = tick(t("Path-style: the bucket in the path, not the host name"), !!bk.pathStyle);
|
|
28
|
+
const ghParts = /^github:\/\/([^/]+\/[^/]+)\/?(.*)$/i.exec(gh.url || "");
|
|
29
|
+
const repo = input(ghParts?.[1] || "", "owner/repo");
|
|
30
|
+
const branch = input(gh.branch || "", t("default branch"));
|
|
31
|
+
const ghPrefix = input(ghParts?.[2] || "", t("optional"));
|
|
32
|
+
const token = input("", gh.passwordSet ? saved : t("GitHub token"), "password");
|
|
33
|
+
const phrase = input("", v.passphraseSet ? saved : t("the same on every computer"), "password");
|
|
34
|
+
const [keysL, keys] = tick(t("Providers' API keys"), v.keys !== false);
|
|
35
|
+
const [agentsL, agents] = tick(t("Agents' models"), v.agents !== false);
|
|
36
|
+
const [libL, lib] = tick(t("Library: instructions, MCP servers, skills, hooks and subagents"), v.library !== false);
|
|
37
|
+
// usage: off unless picked, each computer its own (#542)
|
|
38
|
+
const [useL, use] = tick(t("Usage: this computer's calls, for the Usage page on the others"), !!v.usage);
|
|
39
|
+
// sign-ins: off unless picked, sealed with the passphrase like the rest
|
|
40
|
+
// (qianxunji, curtinp118 on Discord); what it lets in is said beside it
|
|
41
|
+
const [siL, si] = tick(t("Sign-ins: subscription accounts and plugin sign-ins"), !!v.signIns);
|
|
42
|
+
const siSay = el("div", "hint sync-signins-say", t("Another computer that syncs is signed in without signing in again. Anyone with the passphrase and the server's file can use these accounts. An account used on two computers can be signed out on one when the other renews it."));
|
|
43
|
+
siSay.hidden = !si.checked;
|
|
44
|
+
si.onchange = () => { siSay.hidden = !si.checked; };
|
|
45
|
+
const what = el("div", "stack");
|
|
46
|
+
what.append(keysL, agentsL, libL, useL, siL, siSay);
|
|
47
|
+
const davFields = [...field(t("Address"), url, t("A folder named magpie is made in it.")),
|
|
48
|
+
...field(t("User"), user),
|
|
49
|
+
...field(t("Password"), pass)];
|
|
50
|
+
const s3Fields = [...field(t("Endpoint"), endpoint, t("Empty for AWS S3; for R2, B2, MinIO and the like, their S3 API address.")),
|
|
51
|
+
...field(t("Bucket"), bucket),
|
|
52
|
+
...field(t("Prefix"), prefix, t("The backup goes in a folder named magpie under it.")),
|
|
53
|
+
...field(t("Region"), region),
|
|
54
|
+
...field(t("Access key"), keyID),
|
|
55
|
+
...field(t("Secret"), secret),
|
|
56
|
+
...field("", pathL, t("MinIO and most NAS servers need it."))];
|
|
57
|
+
const ghFields = [...field(t("Repository"), repo, t("An existing private GitHub repository, in owner/repo form.")),
|
|
58
|
+
...field(t("Branch"), branch, t("Empty uses the repository's default branch. Empty repositories are initialized by the first sync; other branches must already exist.")),
|
|
59
|
+
...field(t("Folder"), ghPrefix, t("The encrypted backup goes in <folder>/magpie/magpie.magpie-backup. Each update creates a commit.")),
|
|
60
|
+
...field(t("GitHub token"), token, t("A fine-grained token for this repository with Contents: read and write. Kept on this computer, separately from the library's GitHub token."))];
|
|
61
|
+
// which one is synced to, and that the other's settings stay
|
|
62
|
+
const to = field(t("Sync to"), segs(Object.entries(syncNames).map(([id, n]) => [id, id === active ? t("{kind} · on", { kind: n }) : n]), kind, (k) => { kind = k; show(); }), " ");
|
|
63
|
+
const where = to[1].querySelector(".hint");
|
|
64
|
+
const save = el("button", "text primary", "");
|
|
65
|
+
const other = Object.keys(servers).map((k) => syncNames[k]).filter(Boolean).join(" / ");
|
|
66
|
+
const show = () => {
|
|
67
|
+
for (const x of davFields) x.hidden = kind !== "webdav";
|
|
68
|
+
for (const x of s3Fields) x.hidden = kind !== "s3";
|
|
69
|
+
for (const x of ghFields) x.hidden = kind !== "github";
|
|
70
|
+
where.textContent = !active ? ""
|
|
71
|
+
: kind === active ? (other ? t("Syncing here now. The {other} settings are kept, not synced to: pick {other} to see them.", { other }) : t("Syncing here now."))
|
|
72
|
+
: t("{active} is synced to now. Saving moves sync here; the {active} settings are kept for moving back.", { active: syncNames[active] });
|
|
73
|
+
where.hidden = !active;
|
|
74
|
+
save.textContent = !active ? t("Turn on") : kind === active ? t("Save") : t("Move sync to {kind}", { kind: syncNames[kind] });
|
|
75
|
+
};
|
|
76
|
+
ed.append(...to,
|
|
77
|
+
...davFields, ...s3Fields, ...ghFields,
|
|
78
|
+
...field(t("Passphrase"), phrase, t("The file is sealed with it on this computer; the server only ever sees it sealed. Keep it: without it the file can't be opened.")),
|
|
79
|
+
...field(t("Also sync"), what, t("Usage goes as a file a day: times, tokens, models and costs, never what was said. Turn it on on each computer that shares its own.")));
|
|
80
|
+
show();
|
|
81
|
+
const off = v.on ? el("button", "text danger", t("Turn off")) : el("span");
|
|
82
|
+
const cancel = el("button", "text", t("Cancel"));
|
|
83
|
+
const say = syncBar(ed, "", off, el("span", "grow"), cancel, save);
|
|
84
|
+
cancel.onclick = () => { syncOpen = ""; renderSync(); };
|
|
85
|
+
off.onclick = async () => { syncOpen = ""; renderSync(await api("davsync/off", {}).catch(() => null) || undefined); };
|
|
86
|
+
save.onclick = async () => {
|
|
87
|
+
const b = bucket.value.trim(), p = prefix.value.trim().replace(/^\/+|\/+$/g, "");
|
|
88
|
+
if (kind === "s3" && !b) return say(t("Name the bucket"));
|
|
89
|
+
const repository = repo.value.trim().replace(/^https:\/\/github\.com\//i, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
90
|
+
const ghFolder = ghPrefix.value.trim().replace(/^\/+|\/+$/g, "");
|
|
91
|
+
if (kind === "github" && !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) return say(t("Name the GitHub repository as owner/repo"));
|
|
92
|
+
if (kind === "github" && !gh.passwordSet && !token.value.trim()) return say(t("Enter a GitHub token with Contents read and write permission"));
|
|
93
|
+
if (!v.passphraseSet && !phrase.value) return say(t("Pick a passphrase: the file is sealed with it"));
|
|
94
|
+
save.classList.add("busy");
|
|
95
|
+
const where = kind === "github"
|
|
96
|
+
? { url: "github://" + repository + (ghFolder ? "/" + ghFolder : ""), branch: branch.value.trim(), password: token.value.trim() }
|
|
97
|
+
: kind === "s3"
|
|
98
|
+
? { url: "s3://" + b + (p ? "/" + p : ""), user: keyID.value.trim(), password: secret.value, endpoint: endpoint.value.trim(), region: region.value.trim(), pathStyle: pathStyle.checked }
|
|
99
|
+
: { url: url.value.trim(), user: user.value.trim(), password: pass.value };
|
|
100
|
+
try {
|
|
101
|
+
const r = await api("davsync/save", { ...where, passphrase: phrase.value, keys: keys.checked, agents: agents.checked, library: lib.checked, ...(use.checked || v.usage ? { usage: use.checked } : {}), ...(si.checked || v.signIns ? { signIns: si.checked } : {}) });
|
|
102
|
+
if (!r.error) syncOpen = "";
|
|
103
|
+
renderSync(r);
|
|
104
|
+
if (r.error) return;
|
|
105
|
+
refreshAfterSync();
|
|
106
|
+
} catch (e) {
|
|
107
|
+
save.classList.remove("busy");
|
|
108
|
+
say(e.message);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return ed;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
window.exportForm = function () {
|
|
115
|
+
const ed = el("div", "editor sync-form");
|
|
116
|
+
const p1 = input("", t("passphrase"), "password");
|
|
117
|
+
const p2 = input("", t("again"), "password");
|
|
118
|
+
const [keysL, keys] = tick(t("With the providers' API keys"), true);
|
|
119
|
+
const [libL, lib] = tick(t("With the library: instructions, MCP servers, skills, hooks and subagents"), true);
|
|
120
|
+
// sign-ins: off unless picked (qianxunji, curtinp118 on Discord)
|
|
121
|
+
const [siL, si] = tick(t("With the sign-ins: subscription accounts and plugin sign-ins"), false);
|
|
122
|
+
const what = el("div", "stack");
|
|
123
|
+
what.append(keysL, libL, siL);
|
|
124
|
+
const passF = field(t("Passphrase"), p1, t("Needed to open the file. Subscriptions aren't in it: sign in to them on the other computer."));
|
|
125
|
+
const passSay = passF[1].querySelector(".hint");
|
|
126
|
+
si.onchange = () => {
|
|
127
|
+
passSay.textContent = si.checked ? t("Needed to open the file. The sign-ins are in it: anyone with the file and the passphrase can use those accounts, so pick a strong one.")
|
|
128
|
+
: t("Needed to open the file. Subscriptions aren't in it: sign in to them on the other computer.");
|
|
129
|
+
};
|
|
130
|
+
ed.append(...passF, ...field("", p2), ...field("", what));
|
|
131
|
+
const go = el("button", "text primary", t("Export"));
|
|
132
|
+
const cancel = el("button", "text", t("Cancel"));
|
|
133
|
+
const say = syncBar(ed, "", el("span", "grow"), cancel, go);
|
|
134
|
+
cancel.onclick = () => { syncOpen = ""; renderSync(); };
|
|
135
|
+
go.onclick = async () => {
|
|
136
|
+
if (!p1.value) return say(t("Pick a passphrase: the file is sealed with it"));
|
|
137
|
+
if (p1.value !== p2.value) return say(t("The two passphrases differ"));
|
|
138
|
+
go.classList.add("busy");
|
|
139
|
+
try {
|
|
140
|
+
const r = await api("backup/export", { pass: p1.value, keys: keys.checked, library: lib.checked, signIns: si.checked });
|
|
141
|
+
ed.replaceChildren(el("div", "done", t("Saved to {path}", { path: r.path })));
|
|
142
|
+
} catch (e) {
|
|
143
|
+
go.classList.remove("busy");
|
|
144
|
+
say(e.message);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return ed;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// signInsSaid: what putting a backup's sign-ins in did, as lines
|
|
151
|
+
window.signInsSaid = function (r) {
|
|
152
|
+
const lines = [t("Sign-ins: {added} added, {updated} updated, {kept} kept as they are here", { added: r.added || 0, updated: r.updated || 0, kept: r.kept || 0 })];
|
|
153
|
+
if (r.kept) lines.push(t("One kept as it is here was renewed later here, or is the account the agent itself is signed in with."));
|
|
154
|
+
if (r.codex) lines.push(t("Codex is signed in with the backup's account."));
|
|
155
|
+
return lines;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
window.importForm = function () {
|
|
159
|
+
const ed = el("div", "editor sync-form");
|
|
160
|
+
let data = "";
|
|
161
|
+
const file = el("input");
|
|
162
|
+
file.type = "file";
|
|
163
|
+
file.accept = ".magpie-backup";
|
|
164
|
+
file.hidden = true;
|
|
165
|
+
const name = el("span", "fname", t("No file chosen"));
|
|
166
|
+
const pick = el("button", "text", t("Choose…"));
|
|
167
|
+
pick.onclick = () => file.click();
|
|
168
|
+
file.onchange = async () => {
|
|
169
|
+
const f = file.files[0];
|
|
170
|
+
if (!f) return;
|
|
171
|
+
// as base64 in JSON: the app's web view drops a File sent as the body
|
|
172
|
+
const bytes = new Uint8Array(await f.arrayBuffer());
|
|
173
|
+
let bin = "";
|
|
174
|
+
for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
175
|
+
data = btoa(bin);
|
|
176
|
+
name.textContent = f.name;
|
|
177
|
+
};
|
|
178
|
+
const pf = el("div", "pair");
|
|
179
|
+
pf.append(name, pick, file);
|
|
180
|
+
const pass = input("", t("passphrase"), "password");
|
|
181
|
+
const [agentsL, agents] = tick(t("Set the agents' models too"), true);
|
|
182
|
+
const [libL, lib] = tick(t("Bring in the library too: instructions, MCP servers, skills, hooks and subagents"), true);
|
|
183
|
+
const [siL, si] = tick(t("Bring in the sign-ins too, when the file has them"), true);
|
|
184
|
+
const what = el("div", "stack");
|
|
185
|
+
what.append(agentsL, libL, siL);
|
|
186
|
+
ed.append(...field(t("File"), pf), ...field(t("Passphrase"), pass), ...field("", what, t("Providers with the same id are replaced; one that came without a key keeps the key it has here. The library replaces the one here, which is kept with its backups.")));
|
|
187
|
+
const go = el("button", "text primary", t("Import"));
|
|
188
|
+
const cancel = el("button", "text", t("Cancel"));
|
|
189
|
+
const say = syncBar(ed, "", el("span", "grow"), cancel, go);
|
|
190
|
+
cancel.onclick = () => { syncOpen = ""; renderSync(); };
|
|
191
|
+
go.onclick = async () => {
|
|
192
|
+
if (!data) return say(t("Choose a file first"));
|
|
193
|
+
go.classList.add("busy");
|
|
194
|
+
try {
|
|
195
|
+
const r = await api("backup/import", { data, pass: pass.value, agents: agents.checked, library: lib.checked, signIns: si.checked });
|
|
196
|
+
const lines = [t("Providers: {added} added, {replaced} replaced; {profiles} profiles; {agents} agent settings changed", { added: r.Added, replaced: r.Replaced, profiles: r.Profiles, agents: r.Agents })];
|
|
197
|
+
if (r.NeedKey?.length) lines.push(t("Needs a key: {names}", { names: r.NeedKey.join(", ") }));
|
|
198
|
+
if (r.SignIns) lines.push(...signInsSaid(r.SignIns));
|
|
199
|
+
// a backup carries a subscription's settings, its sign-in only when
|
|
200
|
+
// made with them
|
|
201
|
+
if (r.SignIn?.length) lines.push(r.SignIns
|
|
202
|
+
? t("This backup's settings use {names}, which isn't signed in on this computer, and the backup has no sign-in for it. To use it here, sign in in Providers → Add a provider; if you don't, nothing needs doing.", { names: subNames(r.SignIn) })
|
|
203
|
+
: t("This backup's settings use {names}, which isn't signed in on this computer. A backup carries settings, not sign-ins: to use it here, sign in in Providers → Add a provider; if you don't, nothing needs doing.", { names: subNames(r.SignIn) }));
|
|
204
|
+
if (r.Plugins?.length) lines.push(t("Plugins added: {names}", { names: r.Plugins.join(", ") }));
|
|
205
|
+
for (const [spec, error] of Object.entries(r.PluginErrors || {})) lines.push(t("Couldn't add the plugin {spec}: {error}", { spec, error }));
|
|
206
|
+
if (r.Library) lines.push(t("Library brought in and written into the agents"));
|
|
207
|
+
for (const p of r.LibraryProblems || []) lines.push(t("{agent} couldn't get {what}: {error}", { agent: p.agent, what: p.what, error: p.error }));
|
|
208
|
+
ed.replaceChildren(...lines.map((l) => el("div", "done", l)));
|
|
209
|
+
refreshAfterSync();
|
|
210
|
+
} catch (e) {
|
|
211
|
+
go.classList.remove("busy");
|
|
212
|
+
say(e.message);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return ed;
|
|
216
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Apply the saved theme and text size (boot.js) before styles first apply.
|
|
2
|
+
// A theme requested by the page's address takes precedence. Waiting for
|
|
3
|
+
// app.js at the foot of the page let the header paint in the system theme
|
|
4
|
+
// and default size first, then change as the page loaded.
|
|
5
|
+
(function () {
|
|
6
|
+
var asked = new URLSearchParams(location.search).get("theme");
|
|
7
|
+
var prefs = window.bootPrefs;
|
|
8
|
+
var saved = prefs && prefs.theme;
|
|
9
|
+
var theme = asked || (saved && saved !== "system" ? saved : "");
|
|
10
|
+
if (theme) document.documentElement.dataset.theme = theme;
|
|
11
|
+
if (prefs && prefs.colorblind) document.documentElement.dataset.colorblind = "1";
|
|
12
|
+
if (prefs && prefs.reduceMotion) document.documentElement.dataset.motion = "reduce";
|
|
13
|
+
if (prefs && prefs.noBird) document.documentElement.dataset.bird = "off";
|
|
14
|
+
if (prefs) document.documentElement.style.setProperty("--zoom", prefs.web ? 1 : (prefs.textSize || 100) / 100);
|
|
15
|
+
})();
|
|
16
|
+
|
|
17
|
+
// Settings' Reduce motion (#3813) applies the stylesheets' reduced-motion
|
|
18
|
+
// rules, their media set to all and back; calm() in app.js follows it
|
|
19
|
+
(function () {
|
|
20
|
+
var held = [];
|
|
21
|
+
window.holdStill = function (on) {
|
|
22
|
+
var root = document.documentElement;
|
|
23
|
+
if (on) root.dataset.motion = "reduce"; else delete root.dataset.motion;
|
|
24
|
+
held.forEach(function (h) { h[0].media.mediaText = h[1]; });
|
|
25
|
+
held = [];
|
|
26
|
+
if (on) for (var i = 0, s = document.styleSheets; i < s.length; i++) for (var j = 0, r = s[i].cssRules; j < r.length; j++)
|
|
27
|
+
if (r[j].media && /reduced-motion: *reduce/.test(r[j].media.mediaText)) held.push([r[j], r[j].media.mediaText]), r[j].media.mediaText = "all";
|
|
28
|
+
};
|
|
29
|
+
})();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Settings › Menu bar windows, and Windows' Tray icon number. app.js imports this only when the menu bar
|
|
2
|
+
// shows a card (renderTrayUsage), so it weighs nothing on the core.
|
|
3
|
+
|
|
4
|
+
// which of a subscription's windows its menu bar card shows, by kind
|
|
5
|
+
// (settings.TrayWindows, StrikingRaw on X): a pill each for those ticked
|
|
6
|
+
// with two or more, every window unless some are picked. A window the card shows only when picked
|
|
7
|
+
// (trayExtra: money aside, one model's, Cursor's Grok Bot, CrazyJal on X)
|
|
8
|
+
// is offered too; its usual windows ticked alone are no pick at all.
|
|
9
|
+
window.trayWindowsRow = function trayWindowsRow(s, keep, ids, mac) {
|
|
10
|
+
// Windows' tray shows no text: the first card's figure is its icon, or
|
|
11
|
+
// the bird (Benjaminliang_1 on X)
|
|
12
|
+
$("#trayNumberRow").hidden = !document.documentElement.classList.contains("win");
|
|
13
|
+
$("#trayNumberSegs").replaceChildren(segs([["off", t("Off")], ["on", t("On")]], s.trayNoNumber ? "off" : "on",
|
|
14
|
+
(v) => savePrefs({ ...keep, trayNoNumber: v === "off" })));
|
|
15
|
+
const pick = s.trayWindows || {}, box = $("#trayWindowsPick"), subs = [];
|
|
16
|
+
for (const pid of new Set(ids.filter((id) => !id.startsWith("usage:")).map((id) => id.split("|")[0]))) {
|
|
17
|
+
const ws = [], q = (quotas || []).find((q) => q.provider === pid);
|
|
18
|
+
for (const w of q?.windows || []) if (w.trayKind && !ws.some((x) => x.v === w.trayKind)) ws.push({ v: w.trayKind, name: w.name, note: "", extra: !!w.trayExtra });
|
|
19
|
+
if (ws.length > 1) subs.push([pid, q.name, ws]);
|
|
20
|
+
}
|
|
21
|
+
// Windows' tray icon draws the first one's figure (traynumber.go), so
|
|
22
|
+
// which window it is matters there too
|
|
23
|
+
$("#trayWindowsRow").hidden = !subs.length;
|
|
24
|
+
if (!quotas && ids.some((id) => !id.startsWith("usage:"))) loadQuotas().then(() => box.isConnected && trayWindowsRow(s, keep, ids, mac));
|
|
25
|
+
box.replaceChildren(...subs.map(([pid, name, ws]) => {
|
|
26
|
+
const on = (pick[pid] || []).filter((k) => ws.some((w) => w.v === k)), b = el("button", "proto pick" + (on.length ? " set" : ""));
|
|
27
|
+
b.type = "button";
|
|
28
|
+
b.append(el("span", "", name + " · " + (on.length ? on.map((k) => t(ws.find((w) => w.v === k).name)).join(" · ") : t("All windows"))), svg(CHEV, 11, 1.6));
|
|
29
|
+
b.onclick = (e) => {
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
if (b.classList.contains("open")) return closeProtoMenu();
|
|
32
|
+
openProtoMenu(b, [{ v: "", name: "All windows", note: "" }, ...ws], on, (v) => {
|
|
33
|
+
const next = { ...pick };
|
|
34
|
+
const usual = ws.filter((w) => !w.extra);
|
|
35
|
+
if (v.length && !(v.length === usual.length && usual.every((w) => v.includes(w.v)))) next[pid] = v; else delete next[pid];
|
|
36
|
+
savePrefs({ ...keep, trayWindows: next });
|
|
37
|
+
}, "Shown in the menu bar");
|
|
38
|
+
};
|
|
39
|
+
return b;
|
|
40
|
+
}));
|
|
41
|
+
};
|