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.
Files changed (350) hide show
  1. package/extensions/catalog.json +10 -1
  2. package/extensions/magpie/agentcompact.js +104 -0
  3. package/extensions/magpie/app.css +4902 -0
  4. package/extensions/magpie/app.js +24474 -0
  5. package/extensions/magpie/autobackup.js +185 -0
  6. package/extensions/magpie/autoresetown.js +151 -0
  7. package/extensions/magpie/balfound.js +38 -0
  8. package/extensions/magpie/boot.js +23 -0
  9. package/extensions/magpie/cfgblocked.js +39 -0
  10. package/extensions/magpie/checkinnow.js +111 -0
  11. package/extensions/magpie/claudemode.js +92 -0
  12. package/extensions/magpie/compat.js +41 -0
  13. package/extensions/magpie/context.css +433 -0
  14. package/extensions/magpie/context.js +1576 -0
  15. package/extensions/magpie/ctxtext.js +223 -0
  16. package/extensions/magpie/fonts.js +42 -0
  17. package/extensions/magpie/i18n.js +18942 -0
  18. package/extensions/magpie/icons/ai21.svg +1 -0
  19. package/extensions/magpie/icons/ai302-color.svg +1 -0
  20. package/extensions/magpie/icons/ai360-color.svg +1 -0
  21. package/extensions/magpie/icons/aihubmix-color.svg +1 -0
  22. package/extensions/magpie/icons/aionui.svg +1 -0
  23. package/extensions/magpie/icons/air.svg +4 -0
  24. package/extensions/magpie/icons/alibaba-color.svg +1 -0
  25. package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
  26. package/extensions/magpie/icons/alma.png +0 -0
  27. package/extensions/magpie/icons/anthropic.svg +1 -0
  28. package/extensions/magpie/icons/antigravity-color.svg +1 -0
  29. package/extensions/magpie/icons/anyscale-color.svg +1 -0
  30. package/extensions/magpie/icons/app-alma.png +0 -0
  31. package/extensions/magpie/icons/app-cc-switch.png +0 -0
  32. package/extensions/magpie/icons/apple-touch-icon.png +0 -0
  33. package/extensions/magpie/icons/aside.svg +28 -0
  34. package/extensions/magpie/icons/astrbot.svg +1 -0
  35. package/extensions/magpie/icons/atomcode.png +0 -0
  36. package/extensions/magpie/icons/aws.svg +1 -0
  37. package/extensions/magpie/icons/azure-color.svg +1 -0
  38. package/extensions/magpie/icons/azureai-color.svg +1 -0
  39. package/extensions/magpie/icons/baichuan-color.svg +1 -0
  40. package/extensions/magpie/icons/baidu-color.svg +1 -0
  41. package/extensions/magpie/icons/baiducloud-color.svg +1 -0
  42. package/extensions/magpie/icons/bailian-color.svg +1 -0
  43. package/extensions/magpie/icons/baseten.svg +1 -0
  44. package/extensions/magpie/icons/bedrock-color.svg +1 -0
  45. package/extensions/magpie/icons/burncloud-color.svg +1 -0
  46. package/extensions/magpie/icons/bytedance-color.svg +1 -0
  47. package/extensions/magpie/icons/cerebras.svg +1 -0
  48. package/extensions/magpie/icons/chatglm-color.svg +1 -0
  49. package/extensions/magpie/icons/cherryin-color.svg +1 -0
  50. package/extensions/magpie/icons/chutes.svg +1 -0
  51. package/extensions/magpie/icons/cindy.png +0 -0
  52. package/extensions/magpie/icons/claude-color.svg +1 -0
  53. package/extensions/magpie/icons/claudecode-color.svg +1 -0
  54. package/extensions/magpie/icons/cline.svg +1 -0
  55. package/extensions/magpie/icons/cloudflare-color.svg +1 -0
  56. package/extensions/magpie/icons/codebuddy-color.svg +1 -0
  57. package/extensions/magpie/icons/codex-color.svg +1 -0
  58. package/extensions/magpie/icons/cohere-color.svg +1 -0
  59. package/extensions/magpie/icons/cometapi-color.svg +1 -0
  60. package/extensions/magpie/icons/commandcode.svg +1 -0
  61. package/extensions/magpie/icons/copilot-color.svg +1 -0
  62. package/extensions/magpie/icons/coze.svg +1 -0
  63. package/extensions/magpie/icons/crush.png +0 -0
  64. package/extensions/magpie/icons/curl.svg +1 -0
  65. package/extensions/magpie/icons/cursor.svg +1 -0
  66. package/extensions/magpie/icons/deepinfra-color.svg +1 -0
  67. package/extensions/magpie/icons/deepmind-color.svg +1 -0
  68. package/extensions/magpie/icons/deepseek-color.svg +1 -0
  69. package/extensions/magpie/icons/deveco-color.svg +12 -0
  70. package/extensions/magpie/icons/devin.svg +1 -0
  71. package/extensions/magpie/icons/doubao-color.svg +1 -0
  72. package/extensions/magpie/icons/empryo-color.svg +1 -0
  73. package/extensions/magpie/icons/factory.svg +1 -0
  74. package/extensions/magpie/icons/favicon.svg +1 -0
  75. package/extensions/magpie/icons/featherless-color.svg +1 -0
  76. package/extensions/magpie/icons/fireworks-color.svg +1 -0
  77. package/extensions/magpie/icons/friendli.svg +1 -0
  78. package/extensions/magpie/icons/fx.svg +1 -0
  79. package/extensions/magpie/icons/gemini-color.svg +1 -0
  80. package/extensions/magpie/icons/geminicli-color.svg +1 -0
  81. package/extensions/magpie/icons/gemma-color.svg +1 -0
  82. package/extensions/magpie/icons/giteeai.svg +1 -0
  83. package/extensions/magpie/icons/github.svg +1 -0
  84. package/extensions/magpie/icons/githubcopilot.svg +1 -0
  85. package/extensions/magpie/icons/glama.svg +1 -0
  86. package/extensions/magpie/icons/gmicloud.svg +1 -0
  87. package/extensions/magpie/icons/google-color.svg +1 -0
  88. package/extensions/magpie/icons/googlecloud-color.svg +1 -0
  89. package/extensions/magpie/icons/goose.svg +1 -0
  90. package/extensions/magpie/icons/grok.svg +1 -0
  91. package/extensions/magpie/icons/groq.svg +1 -0
  92. package/extensions/magpie/icons/hanako.png +0 -0
  93. package/extensions/magpie/icons/hermes.svg +1 -0
  94. package/extensions/magpie/icons/huawei-color.svg +1 -0
  95. package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
  96. package/extensions/magpie/icons/huggingface-color.svg +1 -0
  97. package/extensions/magpie/icons/hunyuan-color.svg +1 -0
  98. package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
  99. package/extensions/magpie/icons/icon-512.png +0 -0
  100. package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
  101. package/extensions/magpie/icons/inference.svg +1 -0
  102. package/extensions/magpie/icons/infinigence-color.svg +1 -0
  103. package/extensions/magpie/icons/internlm-color.svg +1 -0
  104. package/extensions/magpie/icons/jcode.svg +1 -0
  105. package/extensions/magpie/icons/jina.svg +1 -0
  106. package/extensions/magpie/icons/kilo.svg +1 -0
  107. package/extensions/magpie/icons/kilocode.svg +1 -0
  108. package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
  109. package/extensions/magpie/icons/kimi.svg +1 -0
  110. package/extensions/magpie/icons/kiro-color.svg +1 -0
  111. package/extensions/magpie/icons/kling-color.svg +1 -0
  112. package/extensions/magpie/icons/lambda.svg +1 -0
  113. package/extensions/magpie/icons/leptonai-color.svg +1 -0
  114. package/extensions/magpie/icons/liquid.svg +1 -0
  115. package/extensions/magpie/icons/liveagent-color.svg +1 -0
  116. package/extensions/magpie/icons/llmapi.svg +1 -0
  117. package/extensions/magpie/icons/lmstudio.svg +1 -0
  118. package/extensions/magpie/icons/lody.png +0 -0
  119. package/extensions/magpie/icons/longcat.svg +1 -0
  120. package/extensions/magpie/icons/magpie.svg +1 -0
  121. package/extensions/magpie/icons/meta-color.svg +1 -0
  122. package/extensions/magpie/icons/metaai-color.svg +1 -0
  123. package/extensions/magpie/icons/microsoft-color.svg +1 -0
  124. package/extensions/magpie/icons/mimocode.svg +1 -0
  125. package/extensions/magpie/icons/minimax-color.svg +1 -0
  126. package/extensions/magpie/icons/mistermorph-color.svg +1 -0
  127. package/extensions/magpie/icons/mistral-color.svg +1 -0
  128. package/extensions/magpie/icons/mlx-serve.png +0 -0
  129. package/extensions/magpie/icons/modelscope-color.svg +1 -0
  130. package/extensions/magpie/icons/monica-color.svg +1 -0
  131. package/extensions/magpie/icons/moonshot.svg +1 -0
  132. package/extensions/magpie/icons/morph-color.svg +1 -0
  133. package/extensions/magpie/icons/mothx.png +0 -0
  134. package/extensions/magpie/icons/nebius.svg +1 -0
  135. package/extensions/magpie/icons/newapi-color.svg +1 -0
  136. package/extensions/magpie/icons/nousresearch.svg +1 -0
  137. package/extensions/magpie/icons/nova-color.svg +1 -0
  138. package/extensions/magpie/icons/novita-color.svg +1 -0
  139. package/extensions/magpie/icons/nvidia-color.svg +1 -0
  140. package/extensions/magpie/icons/ollama.svg +1 -0
  141. package/extensions/magpie/icons/omlx.svg +1 -0
  142. package/extensions/magpie/icons/omo.svg +1 -0
  143. package/extensions/magpie/icons/omp.svg +1 -0
  144. package/extensions/magpie/icons/openai.svg +1 -0
  145. package/extensions/magpie/icons/openchamber.svg +11 -0
  146. package/extensions/magpie/icons/openclaw.svg +1 -0
  147. package/extensions/magpie/icons/opencode.svg +1 -0
  148. package/extensions/magpie/icons/openmausbot.svg +1 -0
  149. package/extensions/magpie/icons/openrouter.svg +1 -0
  150. package/extensions/magpie/icons/openwebui.svg +1 -0
  151. package/extensions/magpie/icons/parasail.svg +1 -0
  152. package/extensions/magpie/icons/penguin-color.svg +1 -0
  153. package/extensions/magpie/icons/perplexity-color.svg +1 -0
  154. package/extensions/magpie/icons/phala-color.svg +1 -0
  155. package/extensions/magpie/icons/pi.svg +1 -0
  156. package/extensions/magpie/icons/pipellm-color.svg +5 -0
  157. package/extensions/magpie/icons/poe.svg +1 -0
  158. package/extensions/magpie/icons/ppio-color.svg +1 -0
  159. package/extensions/magpie/icons/prime-agent.svg +1 -0
  160. package/extensions/magpie/icons/qiniu-color.svg +1 -0
  161. package/extensions/magpie/icons/qoder.svg +1 -0
  162. package/extensions/magpie/icons/qwen-color.svg +1 -0
  163. package/extensions/magpie/icons/qwenpaw.svg +1 -0
  164. package/extensions/magpie/icons/raycast.svg +1 -0
  165. package/extensions/magpie/icons/reasonix-color.svg +48 -0
  166. package/extensions/magpie/icons/reka.svg +1 -0
  167. package/extensions/magpie/icons/replicate.svg +1 -0
  168. package/extensions/magpie/icons/roocode.svg +1 -0
  169. package/extensions/magpie/icons/rtk.png +0 -0
  170. package/extensions/magpie/icons/sambanova-color.svg +1 -0
  171. package/extensions/magpie/icons/sensenova-color.svg +1 -0
  172. package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
  173. package/extensions/magpie/icons/snow.png +0 -0
  174. package/extensions/magpie/icons/snowflake-color.svg +1 -0
  175. package/extensions/magpie/icons/sophnet-color.svg +1 -0
  176. package/extensions/magpie/icons/spark-color.svg +1 -0
  177. package/extensions/magpie/icons/stepfun-color.svg +1 -0
  178. package/extensions/magpie/icons/straico-color.svg +1 -0
  179. package/extensions/magpie/icons/submodel-color.svg +1 -0
  180. package/extensions/magpie/icons/t3code.svg +4 -0
  181. package/extensions/magpie/icons/targon-color.svg +1 -0
  182. package/extensions/magpie/icons/tencent-color.svg +1 -0
  183. package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
  184. package/extensions/magpie/icons/tiangong-color.svg +1 -0
  185. package/extensions/magpie/icons/together-color.svg +1 -0
  186. package/extensions/magpie/icons/trae.svg +1 -0
  187. package/extensions/magpie/icons/typesafe.png +0 -0
  188. package/extensions/magpie/icons/upstage-color.svg +1 -0
  189. package/extensions/magpie/icons/venice-color.svg +1 -0
  190. package/extensions/magpie/icons/vercel.svg +1 -0
  191. package/extensions/magpie/icons/vertexai-color.svg +1 -0
  192. package/extensions/magpie/icons/vllm-color.svg +1 -0
  193. package/extensions/magpie/icons/volcengine-color.svg +1 -0
  194. package/extensions/magpie/icons/voyage-color.svg +1 -0
  195. package/extensions/magpie/icons/vscode.svg +1 -0
  196. package/extensions/magpie/icons/wenxin-color.svg +1 -0
  197. package/extensions/magpie/icons/windsurf.svg +1 -0
  198. package/extensions/magpie/icons/workbuddy-color.svg +1 -0
  199. package/extensions/magpie/icons/workersai-color.svg +1 -0
  200. package/extensions/magpie/icons/xai.svg +1 -0
  201. package/extensions/magpie/icons/xiaomimimo.svg +1 -0
  202. package/extensions/magpie/icons/xinference-color.svg +1 -0
  203. package/extensions/magpie/icons/yi.svg +1 -0
  204. package/extensions/magpie/icons/yuanbao-color.svg +1 -0
  205. package/extensions/magpie/icons/yylx.svg +5 -0
  206. package/extensions/magpie/icons/zai.svg +1 -0
  207. package/extensions/magpie/icons/zcode.png +0 -0
  208. package/extensions/magpie/icons/zed.svg +1 -0
  209. package/extensions/magpie/icons/zenmux.svg +1 -0
  210. package/extensions/magpie/icons/zeroone.svg +1 -0
  211. package/extensions/magpie/icons/zhipu-color.svg +1 -0
  212. package/extensions/magpie/icons/zoo-code.png +0 -0
  213. package/extensions/magpie/index.html +572 -0
  214. package/extensions/magpie/kernel.js +274 -0
  215. package/extensions/magpie/keyspend.js +63 -0
  216. package/extensions/magpie/layout.js +26 -0
  217. package/extensions/magpie/manifest.json +37 -0
  218. package/extensions/magpie/navlamp.js +47 -0
  219. package/extensions/magpie/omarchy.css +72 -0
  220. package/extensions/magpie/omarchy.js +46 -0
  221. package/extensions/magpie/passerrors.js +21 -0
  222. package/extensions/magpie/pluginpages.js +405 -0
  223. package/extensions/magpie/plugins/gateway/client.js +1194 -0
  224. package/extensions/magpie/plugins/gateway/keys.js +972 -0
  225. package/extensions/magpie/plugins/gateway/settings.js +459 -0
  226. package/extensions/magpie/plugins/library/client.js +6788 -0
  227. package/extensions/magpie/plugins/library/i18n.js +3228 -0
  228. package/extensions/magpie/plugins/library/library.css +848 -0
  229. package/extensions/magpie/plugins/otel/i18n.js +48 -0
  230. package/extensions/magpie/plugins/otel/page.js +189 -0
  231. package/extensions/magpie/plugins/redact/client.js +46 -0
  232. package/extensions/magpie/plugins/redact/demo.js +461 -0
  233. package/extensions/magpie/plugins/redact/i18n.js +432 -0
  234. package/extensions/magpie/plugins/redact/page.js +985 -0
  235. package/extensions/magpie/plugins/redact/redact.css +385 -0
  236. package/extensions/magpie/plugins/redact/settings.js +248 -0
  237. package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
  238. package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
  239. package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
  240. package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
  241. package/extensions/magpie/plugins/routing/client.js +8593 -0
  242. package/extensions/magpie/plugins/routing/i18n.js +2696 -0
  243. package/extensions/magpie/plugins/routing/routing.css +1111 -0
  244. package/extensions/magpie/plugins/search/settings.js +460 -0
  245. package/extensions/magpie/plugins/sessions/client.js +1622 -0
  246. package/extensions/magpie/plugins/sessions/i18n.js +360 -0
  247. package/extensions/magpie/plugins/sessions/sessions.css +181 -0
  248. package/extensions/magpie/plugins/sessions/settings.js +100 -0
  249. package/extensions/magpie/plugins/usage/client.js +150 -0
  250. package/extensions/magpie/plugins/usage/i18n.js +984 -0
  251. package/extensions/magpie/plugins/usage/page.js +4307 -0
  252. package/extensions/magpie/plugins.css +319 -0
  253. package/extensions/magpie/plugins.js +1926 -0
  254. package/extensions/magpie/profilerename.js +27 -0
  255. package/extensions/magpie/purposes.js +2 -0
  256. package/extensions/magpie/quitask.js +79 -0
  257. package/extensions/magpie/quotaall.js +142 -0
  258. package/extensions/magpie/ratewindows.js +123 -0
  259. package/extensions/magpie/registry.js +87 -0
  260. package/extensions/magpie/resetask.js +137 -0
  261. package/extensions/magpie/ruleedit.js +53 -0
  262. package/extensions/magpie/scrollends.js +91 -0
  263. package/extensions/magpie/settingshints.js +75 -0
  264. package/extensions/magpie/signsheets.js +86 -0
  265. package/extensions/magpie/start.js +26 -0
  266. package/extensions/magpie/syncfiles.js +216 -0
  267. package/extensions/magpie/theme.js +29 -0
  268. package/extensions/magpie/traywindows.js +41 -0
  269. package/extensions/magpie/webfonts.js +130 -0
  270. package/extensions/magpie/whatsnew.js +252 -0
  271. package/extensions/magpie.zip +0 -0
  272. package/knowledge/apps/baijiahao.md +30 -0
  273. package/knowledge/apps/bilibili.md +1 -0
  274. package/knowledge/apps/dedao.md +33 -0
  275. package/knowledge/apps/feishu.md +35 -0
  276. package/knowledge/apps/gmail.md +33 -0
  277. package/knowledge/apps/jike.md +31 -0
  278. package/knowledge/apps/kuaishou.md +34 -0
  279. package/knowledge/apps/linkedin.md +33 -0
  280. package/knowledge/apps/qqmail.md +30 -0
  281. package/knowledge/apps/telegram.md +27 -0
  282. package/knowledge/apps/toutiao.md +32 -0
  283. package/knowledge/apps/uber-eats.md +26 -0
  284. package/knowledge/apps/uber.md +27 -0
  285. package/knowledge/apps/whatsapp.md +27 -0
  286. package/knowledge/apps/x.md +32 -0
  287. package/knowledge/apps/xiaohongshu.md +1 -0
  288. package/knowledge/apps/zhihu.md +1 -0
  289. package/knowledge/sites/bilibili.com/SITE.md +50 -0
  290. package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
  291. package/knowledge/sites/github.com/SITE.md +51 -0
  292. package/knowledge/sites/github.com/references/repo-fields.md +13 -0
  293. package/knowledge/sites/x.com/SITE.md +47 -0
  294. package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
  295. package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
  296. package/knowledge/sites/youtube.com/SITE.md +49 -0
  297. package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
  298. package/knowledge/sites/zhihu.com/SITE.md +47 -0
  299. package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
  300. package/lib/mobile/extension-catalog.js +41 -1
  301. package/lib/mobile/extension-data.js +224 -0
  302. package/lib/mobile/extension-dev.js +3 -1
  303. package/lib/mobile/file-ops.js +252 -0
  304. package/lib/mobile/history-derive.js +80 -4
  305. package/lib/mobile/host-bridge/endpoints.js +48 -6
  306. package/lib/mobile/host-bridge/event-synthesis.js +33 -6
  307. package/lib/mobile/jobs-control.js +118 -0
  308. package/lib/mobile/memory.js +635 -0
  309. package/lib/mobile/model-badges.js +77 -0
  310. package/lib/mobile/pet-tools.js +50 -4
  311. package/lib/mobile/pets.js +214 -18
  312. package/lib/mobile/phone/access.js +85 -0
  313. package/lib/mobile/phone/evidence.js +228 -0
  314. package/lib/mobile/phone/gate.js +290 -0
  315. package/lib/mobile/phone/grounding.js +314 -0
  316. package/lib/mobile/phone/guard.js +465 -0
  317. package/lib/mobile/phone/irreversible.js +88 -8
  318. package/lib/mobile/phone/knowledge-tools.js +665 -0
  319. package/lib/mobile/phone/learn.js +329 -0
  320. package/lib/mobile/phone/redact.js +237 -0
  321. package/lib/mobile/phone/risk.js +95 -0
  322. package/lib/mobile/phone/routes.js +28 -1
  323. package/lib/mobile/phone/service.js +23 -0
  324. package/lib/mobile/phone/site-commands.js +271 -0
  325. package/lib/mobile/phone/step-summary.js +201 -0
  326. package/lib/mobile/phone/stop-summary.js +235 -0
  327. package/lib/mobile/phone/tool-groups.js +125 -0
  328. package/lib/mobile/phone/tools.js +589 -0
  329. package/lib/mobile/phone/untrusted.js +65 -0
  330. package/lib/mobile/phone-knowledge.js +20 -4
  331. package/lib/mobile/schedule-edit.js +500 -0
  332. package/lib/mobile/schedule.js +5 -0
  333. package/lib/mobile/send-continuation.js +52 -0
  334. package/lib/mobile/session-models.js +27 -31
  335. package/lib/mobile/session-send.js +12 -12
  336. package/lib/mobile/skill-import.js +338 -0
  337. package/lib/mobile/skill-store.js +524 -0
  338. package/lib/routes/mobile-api.js +42 -0
  339. package/package.json +7 -6
  340. package/skills/skill-authoring/SKILL.md +46 -0
  341. package/skills/skill-authoring/references/examples.md +82 -0
  342. package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
  343. package/skills/skill-authoring/scripts/check_skill.py +167 -0
  344. package/skills/skill-authoring/scripts/manifest.json +3 -0
  345. package/knowledge/sites/bilibili.com.md +0 -27
  346. package/knowledge/sites/github.com.md +0 -27
  347. package/knowledge/sites/x.com.md +0 -28
  348. package/knowledge/sites/xiaohongshu.com.md +0 -27
  349. package/knowledge/sites/youtube.com.md +0 -28
  350. package/knowledge/sites/zhihu.com.md +0 -27
@@ -0,0 +1,185 @@
1
+ // Settings › Sync's Back up by itself (jwaitq on Discord: 定期自动备份,
2
+ // 支持回退选择版本): the setup backed up into a folder on this computer on a
3
+ // schedule, the last few kept, and any of them put back, what is here being
4
+ // backed up first so that can be undone. app.js imports this when Settings
5
+ // draws Sync, so it weighs nothing on the core.
6
+
7
+ // the server's view, kept over Settings' redraws and read again when it is
8
+ // half a minute old; the read under way, which a redraw while it is out
9
+ // draws with too; what the last restore came to, said once under the
10
+ // versions
11
+ let autoBackupView = null;
12
+ let autoBackupAt = 0;
13
+ let autoBackupLoad = null;
14
+ let autoBackupSaid = null;
15
+ // the passphrase asked for as the sign-ins are turned on, when none is set
16
+ let autoBackupPhrase = false;
17
+
18
+ // autoBackupFill puts the rows in place of at, the empty element renderSync
19
+ // leaves at the end of Sync's list: rows, so the list's lines run between
20
+ // them as between Sync's own.
21
+ window.autoBackupFill = function (at) {
22
+ let drawn = [at];
23
+ const draw = () => {
24
+ if (!drawn[0].isConnected) return;
25
+ const rows = autoBackupRows(autoBackupView);
26
+ drawn[0].before(...rows);
27
+ for (const r of drawn) r.remove();
28
+ drawn = rows;
29
+ };
30
+ if (autoBackupView) draw();
31
+ if (Date.now() - autoBackupAt >= 30000) {
32
+ autoBackupAt = Date.now();
33
+ const load = api("autobackup").then((v) => { autoBackupView = v; }, (e) => { autoBackupView = { error: e.message, backups: [] }; });
34
+ autoBackupLoad = load;
35
+ load.then(() => { if (autoBackupLoad === load) autoBackupLoad = null; });
36
+ }
37
+ autoBackupLoad?.then(draw);
38
+ };
39
+
40
+ // autoBackupDo posts one action and draws Sync again with what it says
41
+ async function autoBackupDo(action, body, b) {
42
+ b?.classList.add("busy");
43
+ try {
44
+ autoBackupView = await api("autobackup/" + action, body || {});
45
+ autoBackupAt = Date.now();
46
+ } catch (e) {
47
+ statusErr(e);
48
+ }
49
+ renderSync();
50
+ }
51
+
52
+ function autoBackupRows(v) {
53
+ const rows = [];
54
+ const row = (name, sub, ...tools) => {
55
+ const r = el("div", "row pref auto-backup");
56
+ const who = el("div", "who");
57
+ const s = el("div", "sub", sub);
58
+ who.append(el("div", "name", name), s);
59
+ const val = el("div", "val");
60
+ val.append(...tools);
61
+ r.append(who, val);
62
+ rows.push(r);
63
+ return r;
64
+ };
65
+ const btn = (label, fn) => { const b = el("button", "text", label); b.type = "button"; b.onclick = (e) => fn(b, e); return b; };
66
+ const on = !!v.on;
67
+ const head = row(t("Back up by itself"), v.error ? t("Couldn't back up: {error}", { error: v.error })
68
+ : on ? (v.signIns ? t("Your providers, settings and profiles, without API keys, and the sign-ins sealed with a passphrase, into a folder on this computer")
69
+ : t("Your providers, settings and profiles, without API keys or sign-ins, into a folder on this computer")) : t("Off: no backups are made by themselves"),
70
+ segs([[0, t("Off")], [1, t("1 hour")], [6, t("6 hours")], [24, t("1 day")], [168, t("1 week")]], on ? v.hours : 0,
71
+ (h) => autoBackupDo("set", h ? { on: true, hours: h } : { on: false })));
72
+ if (v.error) head.querySelector(".sub").classList.add("bad", "wraps");
73
+ if (on) {
74
+ row(t("Versions kept"), t("The oldest is removed when a new one is made; one is made only when something changed"),
75
+ segs([3, 7, 14, 30].map((n) => [n, String(n)]), v.keep, (n) => autoBackupDo("set", { keep: n })));
76
+ // the sign-ins (qianxunji, curtinp118 on Discord): off unless picked,
77
+ // and sealed with a passphrase asked for the first time
78
+ const asking = autoBackupPhrase && !v.signIns;
79
+ row(t("Sign-ins in backups"), v.signIns ? t("Subscription accounts and plugin sign-ins, sealed with the passphrase set here")
80
+ : t("Off: after a restore on another computer, sign in again"),
81
+ segs([["off", t("Off")], ["on", t("On")]], v.signIns || asking ? "on" : "off", (x) => {
82
+ if (x === "on" && !v.passphrase) { autoBackupPhrase = true; renderSync(); return; }
83
+ autoBackupPhrase = false;
84
+ autoBackupDo("set", { signIns: x === "on" });
85
+ }));
86
+ if (asking) rows.push(autoBackupPhraseForm());
87
+ }
88
+ if (on || v.backups?.length) {
89
+ const tools = [btn(t("Change…"), (b) => autoBackupDo("choose", {}, b)), btn(t("Show"), () => api("autobackup/reveal", {}).catch(statusErr))];
90
+ if (v.custom) tools.push(btn(t("Default"), (b) => autoBackupDo("set", { dir: "" }, b)));
91
+ const dir = row(t("Backup folder"), v.dir || "", ...tools).querySelector(".sub");
92
+ dir.classList.add("wraps", "auto-backup-dir");
93
+ // in magpie's own folder, the default: a reinstalled system takes the
94
+ // backups with it (two_2025. on Discord, magpie on C:)
95
+ if (v.inside) dir.after(el("div", "sub wraps auto-backup-inside", t("In magpie's own folder: a reinstalled system or a lost disk takes these backups too. Pick a folder on another disk, or one a cloud drive syncs")));
96
+ }
97
+ const list = v.backups || [];
98
+ const sub = row(t("Versions"), list.length ? t("{n} backups · newest {when}", { n: list.length, when: syncWhen(list[0].made) }) : t("None yet"),
99
+ btn(t("Back up now"), (b) => autoBackupDo("now", {}, b))).querySelector(".sub");
100
+ if (autoBackupSaid) {
101
+ sub.after(...autoBackupSaid.map((l) => el("div", "sub wraps auto-backup-said", l)));
102
+ autoBackupSaid = null;
103
+ }
104
+ for (const b of list) {
105
+ const when = autoBackupWhen(b.made);
106
+ const what = [t(b.providers === 1 ? "1 provider" : "{n} providers", { n: b.providers })];
107
+ if (b.reason === "restore") what.unshift(t("What was here before a restore"));
108
+ if (b.signIns) what.push(t("sign-ins"));
109
+ row(when, what.join(" · "), btn(t("Restore…"), () => autoBackupAsk(b, when))).classList.add("auto-backup-version");
110
+ }
111
+ return rows;
112
+ }
113
+
114
+ // autoBackupWhen names a version by when it was made, as its row does
115
+ const autoBackupWhen = (made) => new Date(made).toLocaleString(intlLang(), { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
116
+
117
+ // autoBackupAsk asks before a version is put back, in the app's own
118
+ // dialog, saying what changes and that what is here is backed up first.
119
+ function autoBackupAsk(b, when) {
120
+ const ed = el("div", "editor restore-ask auto-backup-ask");
121
+ const head = el("div", "ehead");
122
+ head.append(el("b", "", t("Restore the backup of {when}?", { when })));
123
+ ed.append(head);
124
+ ed.append(el("p", "lib-confirm", t("Your providers, settings, profiles and agents' models become this backup's. Providers added since are kept, and each keeps its API keys here.")));
125
+ if (b.signIns) ed.append(el("p", "lib-confirm", t("Its sign-ins are put in where they were renewed later than the ones here; none here is removed.")));
126
+ ed.append(el("p", "lib-confirm", t("What is here now is backed up first: restore that one to undo this.")));
127
+ const err = el("p", "editor-error", "");
128
+ ed.append(err);
129
+ const bar = el("div", "bar");
130
+ const go = el("button", "text primary danger-fill", t("Restore"));
131
+ go.onclick = async (e) => {
132
+ e.stopPropagation();
133
+ go.disabled = true;
134
+ go.classList.add("busy");
135
+ try {
136
+ const r = await api("autobackup/restore", { name: b.name });
137
+ closeConfirmAsk();
138
+ autoBackupView = r.view;
139
+ autoBackupAt = Date.now();
140
+ const x = r.result || {};
141
+ const lines = [t("Restored the backup of {when}", { when }) + " · " + t("Providers: {added} added, {replaced} replaced; {profiles} profiles; {agents} agent settings changed", { added: x.Added, replaced: x.Replaced, profiles: x.Profiles, agents: x.Agents })];
142
+ if (x.NeedKey?.length) lines.push(t("Needs a key: {names}", { names: x.NeedKey.join(", ") }));
143
+ if (x.SignIns) { await import("./syncfiles.js"); lines.push(...signInsSaid(x.SignIns)); }
144
+ if (x.SignInsLocked) lines.push(t("This backup's sign-ins are sealed with another passphrase, so they were left out."));
145
+ const before = r.view?.backups?.find((k) => k.name === r.before);
146
+ if (before) lines.push(t("What was here before is the version of {when}: restore it to undo.", { when: autoBackupWhen(before.made) }));
147
+ autoBackupSaid = lines;
148
+ renderSync();
149
+ refreshAfterSync();
150
+ } catch (x) {
151
+ go.disabled = false;
152
+ go.classList.remove("busy");
153
+ err.textContent = x.message;
154
+ }
155
+ };
156
+ const cancel = el("button", "text", t("Cancel"));
157
+ cancel.onclick = cancelAsk;
158
+ bar.append(el("span", "grow"), cancel, go);
159
+ ed.append(bar);
160
+ confirmAsk = ed;
161
+ openModal(ed);
162
+ $("#modal").classList.add("lib");
163
+ cancel.focus(NO_SCROLL);
164
+ }
165
+
166
+ // autoBackupPhraseForm asks for the passphrase the backups' sign-ins are
167
+ // sealed with, as they are turned on
168
+ function autoBackupPhraseForm() {
169
+ const ed = el("div", "editor sync-form auto-backup-phrase");
170
+ const p1 = input("", t("passphrase"), "password");
171
+ const p2 = input("", t("again"), "password");
172
+ ed.append(...field(t("Passphrase"), p1, t("The sign-ins in each backup are sealed with it. Anyone with a backup and the passphrase can use those accounts. Keep it: without it a backup's sign-ins can't be opened.")),
173
+ ...field("", p2));
174
+ const go = el("button", "text primary", t("Turn on"));
175
+ const cancel = el("button", "text", t("Cancel"));
176
+ const say = syncBar(ed, "", el("span", "grow"), cancel, go);
177
+ cancel.onclick = () => { autoBackupPhrase = false; renderSync(); };
178
+ go.onclick = async () => {
179
+ if (!p1.value) return say(t("Pick a passphrase: the file is sealed with it"));
180
+ if (p1.value !== p2.value) return say(t("The two passphrases differ"));
181
+ autoBackupPhrase = false;
182
+ await autoBackupDo("set", { signIns: true, passphrase: p1.value }, go);
183
+ };
184
+ return ed;
185
+ }
@@ -0,0 +1,151 @@
1
+ // Settings › Usage's Codex auto-use rows: each ChatGPT account's switches
2
+ // and, under them, how early a reset about to expire is used. app.js
3
+ // imports this when Settings draws them (renderAutoResetOwn), so it weighs
4
+ // nothing on the core.
5
+
6
+ // renderAutoResetOwn lists each ChatGPT account under Settings › Usage's
7
+ // "Codex: auto-use resets", with its switch, the same say as on the
8
+ // account's card (settings' codexAutoReset, the CLI's quota auto-reset).
9
+ // Avalon on Discord: it was only on the accounts' cards, not in the
10
+ // window's Settings. The row shows while a Codex
11
+ // account is signed in, or one is still on.
12
+ window.autoResetOwnPaint = function (s) {
13
+ const head = $("#codexAutoResetRow");
14
+ const list = head.parentElement;
15
+ list.querySelectorAll(".ar-own").forEach((r) => r.remove());
16
+ const on = s.codexAutoReset || [];
17
+ const users = [...(s.codexUsers || [])];
18
+ for (const u of on) if (!users.some((x) => x.toLowerCase() === u)) users.push(u);
19
+ head.hidden = !users.length;
20
+ // each account's resets, read as the providers' editor reads them,
21
+ // with Use beside them (husky_bobo on X looked for it here)
22
+ const held = users.length ? loginUsageOf("codex") : null;
23
+ if (users.length && !held) loginUsage.codex?.loading?.then(() => { if (!head.hidden && loginUsage.codex?.data) autoResetOwnPaint(prefs || s); });
24
+ let after = head;
25
+ for (const user of users) {
26
+ const r = el("div", "row pref ar-own");
27
+ r.dataset.user = user;
28
+ const who = el("div", "who");
29
+ const name = el("div", "name", user);
30
+ name.title = user;
31
+ who.append(name);
32
+ const q = held && Object.entries(held).find(([u]) => u.toLowerCase() === user.toLowerCase())?.[1];
33
+ if (q?.resets?.count && !q.error && !q.resets.byWindow) {
34
+ const qu = { ...q, provider: "codex", user };
35
+ const sub = el("div", "sub ar-own-resets");
36
+ const use = el("button", "text", t("Use a reset"));
37
+ use.title = resetUseTitle(qu);
38
+ use.setAttribute("aria-haspopup", "dialog");
39
+ use.onclick = (e) => { e.stopPropagation(); askReset(qu); };
40
+ sub.append(resetsWords(qu.resets, qu), " ", use);
41
+ who.append(sub);
42
+ }
43
+ // each of the two times its own Off/On (#2078)
44
+ const was = on.includes(user.toLowerCase()) ? autoResetWhenOf(s.codexAutoResetOnly?.[user.toLowerCase()]) : { week: false, expiring: false };
45
+ const cases = el("div", "ar-own-cases");
46
+ for (const c of AUTO_RESET_CASES) {
47
+ const line = el("div", "ar-own-case");
48
+ line.dataset.case = c.id;
49
+ const say = el("span", "ar-own-say", t(c.say));
50
+ say.title = t(was[c.id] ? c.on : c.off, resetLeadVars());
51
+ const pick = segs([["off", t("Off")], ["on", t("On")]], was[c.id] ? "on" : "off", async (v) => {
52
+ try {
53
+ prefs = await writingPrefs(api("settings/codex-auto-reset", { user, on: v === "on", when: c.id }));
54
+ if (state) state.settings = prefs;
55
+ const now = (prefs.codexAutoReset || []).includes(user.toLowerCase()) ? autoResetWhenOf(prefs.codexAutoResetOnly?.[user.toLowerCase()]) : { week: false, expiring: false };
56
+ status(autoResetStatus(user, now), "ok");
57
+ renderSettings();
58
+ if (quotas) renderQuotas(); // the account's card says the same
59
+ } catch (e) {
60
+ statusTErr(e);
61
+ renderSettings();
62
+ }
63
+ });
64
+ pick.setAttribute("aria-label", t("Auto-use resets") + " · " + t(c.say) + " · " + user);
65
+ line.append(say, pick);
66
+ cases.append(line);
67
+ }
68
+ r.append(who, cases);
69
+ after.after(r);
70
+ after = r;
71
+ }
72
+ renderResetLead(s, head.hidden);
73
+ };
74
+
75
+ // renderResetLead: how long before a reset about to run out unused it is
76
+ // used, under the accounts' switches, one for them all; a pick from the
77
+ // app's menu (settings/codex-reset-lead). A value set outside the choices
78
+ // (Custom…, or the CLI) is shown and kept (shangyuan0818 on Discord).
79
+ function renderResetLead(s, hidden) {
80
+ const row = $("#codexResetLeadRow");
81
+ if (!row) return;
82
+ row.hidden = hidden;
83
+ // a redraw leaves Custom…'s field being typed in
84
+ const own = $("#codexResetLeadPick .reset-lead-own");
85
+ if (own && !own.dataset.done) return;
86
+ const cur = resetLeadMinutes(s);
87
+ const leads = RESET_LEADS.includes(cur) ? RESET_LEADS : [...RESET_LEADS, cur].sort((a, b) => a - b);
88
+ const name = (m) => t("{lead} before", { lead: resetLeadWords(m) });
89
+ const b = el("button", "sess-pick reset-lead");
90
+ b.type = "button";
91
+ b.setAttribute("aria-haspopup", "menu");
92
+ b.setAttribute("aria-label", t("Codex: how early before one expires") + ": " + name(cur));
93
+ // every choice in one cell, the current one seen: the button is as wide
94
+ // as the widest, so a pick doesn't rewrap the row and move the page
95
+ const say = el("span", "rl-say");
96
+ for (const m of leads) {
97
+ const w = el("span", m === cur ? "on" : "", name(m));
98
+ if (m !== cur) w.setAttribute("aria-hidden", "true");
99
+ say.append(w);
100
+ }
101
+ b.append(say, svg(CHEV, 11, 1.6));
102
+ b.onclick = (e) => {
103
+ e.stopPropagation();
104
+ if (b.classList.contains("open")) return closeProtoMenu();
105
+ const opts = leads.map((m) => ({ v: String(m), name: name(m), note: m === 30 ? "Default" : "", literalName: true }));
106
+ openProtoMenu(b, [...opts, { v: "\x01", name: "Custom…" }], String(cur), (v) => v === "\x01" ? resetLeadOwn(b, cur, set) : set(Number(v)), "Used before it expires", "sess-menu", "right");
107
+ };
108
+ const set = async (m) => {
109
+ if (m === cur) return renderSettings();
110
+ try {
111
+ prefs = await writingPrefs(api("settings/codex-reset-lead", { minutes: m }));
112
+ if (state) state.settings = prefs;
113
+ renderSettings();
114
+ if (quotas) renderQuotas(); // the cards' titles say it too
115
+ } catch (err) {
116
+ statusTErr(err);
117
+ renderSettings();
118
+ }
119
+ };
120
+ $("#codexResetLeadPick").replaceChildren(b);
121
+ }
122
+
123
+ // resetLeadOwn: Custom…'s field of minutes in the button's place; Enter or
124
+ // leaving saves, Escape doesn't, one out of range is said, not written.
125
+ function resetLeadOwn(b, cur, set) {
126
+ const least = RESET_LEADS[0], most = RESET_LEADS.at(-1);
127
+ const i = input(String(cur), least + "–" + most);
128
+ i.className = "words compact-num reset-lead-own";
129
+ i.inputMode = "numeric";
130
+ i.setAttribute("aria-label", t("Minutes"));
131
+ let done = false;
132
+ const finish = (save) => {
133
+ if (done) return;
134
+ done = true;
135
+ i.dataset.done = "1";
136
+ const v = i.value.trim(), m = /^\d+$/.test(v) ? Number(v) : NaN;
137
+ if (!save || !v) return renderSettings();
138
+ if (!(m >= least && m <= most)) {
139
+ status(t("Minutes is a whole number from 3 to 1440"), "err");
140
+ return renderSettings();
141
+ }
142
+ set(m);
143
+ };
144
+ i.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") finish(true); else if (e.key === "Escape") finish(false); };
145
+ i.onblur = () => finish(true);
146
+ // in the button's box: the row doesn't rewrap and move the page
147
+ i.style.width = b.offsetWidth + "px";
148
+ i.style.height = b.offsetHeight + "px";
149
+ b.replaceWith(i);
150
+ i.focus(NO_SCROLL);
151
+ }
@@ -0,0 +1,38 @@
1
+ // A relay added by hand with no Balance URL has its balance found by
2
+ // magpie where relays tell a key it (provider/balance_probe.go). Under the
3
+ // Balance URL the editor says where, with Use to copy it into the fields,
4
+ // so an amount read at the wrong query can be put right. app.js imports
5
+ // this when the editor draws a relay's balance with no Balance URL.
6
+ let balFoundAt = null;
7
+ window.balFound = function (box, p, draft, bal, pathOf) {
8
+ const line = el("div", "bal-found");
9
+ line.hidden = true;
10
+ balFoundAt = { line, draft, bal, pathOf };
11
+ box.append(line);
12
+ bal.addEventListener("input", () => { line.hidden = !line.dataset.url || !!bal.value.trim(); });
13
+ window.balFoundShow({ found: p.balanceFound, foundField: p.balanceFoundField });
14
+ };
15
+ // what the editor's Check balance found (app.js), drawn again
16
+ window.balFoundShow = function (r) {
17
+ const s = balFoundAt;
18
+ if (!s || !s.line.isConnected) return;
19
+ const { line, draft, bal, pathOf } = s;
20
+ line.replaceChildren();
21
+ line.dataset.url = r.found || "";
22
+ line.hidden = !r.found || !!(draft.balanceURL || "").trim();
23
+ if (!r.found) return;
24
+ const txt = el("span", "", t("Found by magpie: {url}", { url: r.found + (r.foundField ? " " + r.foundField : "") }));
25
+ txt.title = t("magpie asked this relay where relays tell a key its balance, and reads it here. If the amount is wrong, put in your own Balance URL and field; magpie looks again each day");
26
+ const use = el("button", "text action", t("Use"));
27
+ use.type = "button";
28
+ use.title = t("Put this query in the Balance URL and Balance field, to change it");
29
+ use.onclick = () => {
30
+ draft.balanceURL = bal.value = r.found;
31
+ draft.balancePath = r.foundField || "";
32
+ const f = pathOf();
33
+ if (f) { f.value = draft.balancePath; f.placeholder = balanceFieldOf(r.found); }
34
+ line.hidden = true;
35
+ bal.focus();
36
+ };
37
+ line.append(txt, use);
38
+ };
@@ -0,0 +1,23 @@
1
+ // magpie 网页版启动时由 Go 侧动态生成的那一段(原产在 internal/gui/api.go 的 `GET /boot.js`)。
2
+ //
3
+ // 上游仓库里没有这个文件:它是宿主在运行时拼出来的,内容是**页面开始绘制之前**要拿到的那
4
+ // 几项(语言、主题、字号、是不是网页版、这个版本号,以及内置插件被关掉的那几张页)。
5
+ // 我们这套是本地跑的原版前端,宿主换成我们自己,所以按同样的形状在这里静态给一份 ——
6
+ // 少了它,`bootPrefs` 是 undefined,页面会按英文与浅色画第一帧,之后设置回来了再跳一次。
7
+ //
8
+ // 形状必须与上游一致(`window.bootPrefs = {...}` 一行)。想改哪一项就改下面这张表,
9
+ // 别再往页面里塞第二份。
10
+ window.bootPrefs = {
11
+ // 页面语言。上游取的是用户在 magpie 里存的那门;这里给中文,与两边界面一致。
12
+ lang: 'zh',
13
+ // 'light' / 'dark' / '' 跟随系统
14
+ theme: '',
15
+ // 100 = 正常大小;上游是用户在设置里调的那个百分比
16
+ textSize: 100,
17
+ // true = 这一页由 `magpie web` 端出来(没有 App 窗口那一层)。原版前端靠它决定
18
+ // 「链接自己开、属于桌面的那几张页不画」,我们正是这个形态。
19
+ web: true,
20
+ // 端出这一页的那个 magpie 的版本。前端用它判断「是不是换了一个进程在答」。
21
+ // 真值由本地代理盖掉(见 scripts/serve.mjs),这里是兜底。
22
+ version: '',
23
+ };
@@ -0,0 +1,39 @@
1
+ // The Repair dialog for a folder on the way to magpie's own that isn't the
2
+ // user's, most often a ~/.config a command run with sudo made root's
3
+ // (YouWhat on Discord: adding a provider said "mkdir ~/.config/magpie:
4
+ // permission denied"). app.js imports this only when /api/state names such
5
+ // a folder or a save fails for it (configBlockedAsk), so it weighs nothing
6
+ // on the core. Repair makes the folder the user's again, after the system's
7
+ // password prompt; asked once a load, and again over whatever save failed,
8
+ // which "Not now" goes back to as it was left.
9
+ window.cfgBlockedAsk = function (path) {
10
+ if (confirmAsk?.classList.contains("cfg-blocked")) return;
11
+ configBlockedAsked = true;
12
+ const m = $("#modal"), under = !m.hidden && !m.classList.contains("out") && !confirmAsk && m.firstElementChild.firstElementChild;
13
+ const ed = el("div", "editor cfg-blocked"), head = el("div", "ehead"), bar = el("div", "bar");
14
+ head.append(el("b", "", t("magpie can't save its settings")));
15
+ ed.append(head, el("p", "lib-confirm", t("{path} belongs to another user, often after a command was run with sudo, so magpie can't save providers or settings there. Repair makes it yours again; the system may ask for your password.", { path })));
16
+ const later = el("button", "text", t("Not now")), go = el("button", "text primary", t("Repair"));
17
+ later.onclick = cancelAsk;
18
+ go.onclick = async (e) => {
19
+ e.stopPropagation();
20
+ go.disabled = true;
21
+ try {
22
+ if ((await api("config/repair", {}))?.canceled) return;
23
+ if (state) state.configBlocked = "";
24
+ closeConfirmAsk();
25
+ status(t("Repaired: magpie can save again"), "ok");
26
+ } catch (err) {
27
+ status(err.message, "err", 12000);
28
+ } finally {
29
+ go.disabled = false;
30
+ }
31
+ };
32
+ bar.append(el("span", "grow"), later, go);
33
+ ed.append(bar);
34
+ if (under) ed.back = () => openModal(under); // the editor whose save failed, as it was left
35
+ confirmAsk = ed;
36
+ openModal(ed);
37
+ m.classList.add("lib");
38
+ go.focus({ preventScroll: true });
39
+ };
@@ -0,0 +1,111 @@
1
+ // Settings' Check in now for one account (xiaowu_95660: 手动签到一次):
2
+ // its row, the press and what it came to. app.js imports this when
3
+ // Settings draws a vendor's check-in (checkinNowRow), so it weighs nothing
4
+ // on the core.
5
+
6
+ // what each vendor's last press said, kept over Settings' redraws
7
+ const checkinNowSaid = {};
8
+
9
+ // ciNowFill draws Check in now into row, for vendor by's accounts: one
10
+ // account checked in once now, picked from the app menu when there are
11
+ // several, its switch and the account's left as they are; what came of it
12
+ // is said in the row.
13
+ window.ciNowFill = function (row, s, by) {
14
+ const accts = (s.checkinAccounts || []).filter((a) => (a.by || "") === by);
15
+ row.dataset.by = by;
16
+ const sub = el("div", "sub");
17
+ const who = el("div", "who");
18
+ who.append(el("div", "name", t("Check in now")), sub);
19
+ const box = el("div");
20
+ row.append(who, box);
21
+ const draw = () => {
22
+ const said = checkinNowSaid[by];
23
+ sub.textContent = said ? said.text : t("Checks one account in once now; the daily check-in's switches stay as they are");
24
+ sub.classList.toggle("err", !!said?.bad);
25
+ sub.classList.toggle("say", !!said); // what came of it: a state, kept in sight
26
+ const b = el("button", "text ci-now-go", t(accts.length > 1 ? "Check in…" : "Check in"));
27
+ b.type = "button";
28
+ b.disabled = !!said?.busy;
29
+ b.classList.toggle("busy", !!said?.busy);
30
+ if (accts.length > 1) b.setAttribute("aria-haspopup", "menu");
31
+ b.onclick = (e) => {
32
+ e.stopPropagation();
33
+ if (accts.length === 1) return send(accts[0]);
34
+ if (b.classList.contains("open")) return closeProtoMenu();
35
+ openProtoMenu(b, accts.map((a) => ({ v: a.key, name: a.user || a.key, note: "", literalName: true })), null,
36
+ (k) => send(accts.find((a) => a.key === k)), "Check in which account", "sess-menu", "right");
37
+ };
38
+ box.replaceChildren(b);
39
+ };
40
+ const send = (a) => ciNowSend(a, by, (o) => { checkinNowSaid[by] = o; if (row.isConnected) draw(); });
41
+ draw();
42
+ row.hidden = false;
43
+ };
44
+
45
+ // ciAcctFill puts an account's own Check in now in slot, on its row in
46
+ // the provider editor (xwoffice_91592): q is its reading, which carries
47
+ // its check-in (checkinBy, checkinKey). The press is Settings' one,
48
+ // said the same way, and the row's reading is asked again after.
49
+ window.ciAcctFill = function (slot, q, agent, user) {
50
+ const b = el("button", "text ci-acct", t("Check in now"));
51
+ b.type = "button";
52
+ b.title = t("Checks one account in once now; the daily check-in's switches stay as they are");
53
+ b.onclick = (e) => {
54
+ e.stopPropagation();
55
+ ciNowSend({ key: q.checkinKey, user }, q.checkinBy || "", (o) => {
56
+ b.disabled = !!o.busy;
57
+ b.classList.toggle("busy", !!o.busy);
58
+ if (o.busy) return;
59
+ if (loginUsage[agent]) loginUsage[agent].at = 0;
60
+ loginUsageOf(agent);
61
+ });
62
+ };
63
+ slot.replaceWith(b);
64
+ };
65
+
66
+ // ciNowSend checks account a of vendor by in once now and says, through
67
+ // say, how it went; no switch is changed.
68
+ window.ciNowSend = async function (a, by, say) {
69
+ if (!a) return;
70
+ const name = a.user || a.key;
71
+ say({ busy: true, text: t("Checking in {who}…", { who: name }) });
72
+ try {
73
+ const r = await api("usage/checkin-account", { by, key: a.key });
74
+ const said = checkinNowLine(r, name);
75
+ say(said);
76
+ status(said.text, said.bad ? "err" : "ok");
77
+ } catch (err) {
78
+ const text = t("Couldn't check in {who}: {why}", { who: name, why: t(err.message) });
79
+ say({ bad: true, text });
80
+ status(text, "err");
81
+ }
82
+ // the Usage cards' lines read again; Settings isn't drawn again under
83
+ // the button just pressed, which a redraw moved at 420px
84
+ loadQuotas();
85
+ };
86
+
87
+ // checkinNowLine is what one account's press came to: checked in now,
88
+ // already in today, not eligible or no event, its own app's, or failed,
89
+ // with the vendor's reason.
90
+ function checkinNowLine(r, who) {
91
+ const why = r?.msg ? " · " + r.msg : "";
92
+ switch (r?.outcome) {
93
+ case "claimed":
94
+ case "done":
95
+ if (r.again) return { text: t("{user}: {name} says already checked in; magpie asks again later", { user: who, name: checkinVendorName(r) }) };
96
+ if (r.outcome === "claimed" && r.asked) return { text: t("Checked in {who}", { who }) + (r.credit ? " +" + r.credit : "") + (r.streak ? " · " + t("{n}-day streak", { n: r.streak }) : "") };
97
+ return { text: t("{who} was already checked in today", { who }) };
98
+ case "ineligible":
99
+ return { text: t("{user} is not eligible", { user: who }) + why };
100
+ case "inactive":
101
+ return { text: t("{user}: no check-in event now", { user: who }) + why };
102
+ case "captcha":
103
+ return { text: t("{user} is asked for a captcha; check in in the app", { user: who }) };
104
+ case "own-app":
105
+ return { text: r.by === "trae"
106
+ ? t("{user}: Trae CN refused this check-in (9074: too many taking part); check in in the Trae CN app", { user: who })
107
+ : t("{user}: {name} only gives check-in credits to its own app; check in in the {name} app", { user: who, name: checkinVendorName(r) }) };
108
+ default:
109
+ return { bad: true, text: t("Couldn't check in {who}: {why}", { who, why: r?.msg || t("Check-in failed") }) };
110
+ }
111
+ }
@@ -0,0 +1,92 @@
1
+ // The Claude Code provider editor's "Claude Code runs as" row: whether a
2
+ // Claude subscription's runs are Claude Code's interactive mode (TUI) or
3
+ // claude -p, switched right there, how the last request went, and on hover
4
+ // what each is and what it is good for (the owner, 10-31, 11-01). With the
5
+ // interactive mode on a request it can't take fails, saying why (the owner,
6
+ // 11-01); a turn Anthropic refuses there as a third-party app's runs as -p
7
+ // (#4503). app.js imports this with the row's first
8
+ // read of /api/claude/mode (claudeModeRow), so it weighs nothing on the
9
+ // core.
10
+
11
+ // why the interactive mode can't run here, or is off, by the gateway's
12
+ // code (claudeMachineWhy)
13
+ const CC_WHY = {
14
+ off: "the interactive mode is off in Settings",
15
+ os: "this system can't run the interactive mode",
16
+ missing: "Claude Code isn't installed on this computer",
17
+ };
18
+ const ccWhy = (code) => t(CC_WHY[code] || CC_WHY.os);
19
+
20
+ // ccTip: the two modes side by side, the one chosen marked
21
+ let ccTipEl = null;
22
+ function ccTip(anchor, tui) {
23
+ ccTipOff();
24
+ const tip = ccTipEl = el("div", "cc-tip");
25
+ tip.setAttribute("role", "tooltip");
26
+ const mode = (on, name, what, good, bad) => {
27
+ const m = el("div", "cc-m" + (on ? " on" : ""));
28
+ const h = el("div", "h");
29
+ h.append(el("b", "", name));
30
+ if (on) h.append(el("span", "now", t("In use")));
31
+ m.append(h, el("p", "", what), el("p", "good", good));
32
+ if (bad) m.append(el("p", "bad", bad));
33
+ return m;
34
+ };
35
+ tip.append(
36
+ mode(tui, t("Interactive mode (TUI)"),
37
+ t("magpie opens Claude Code as a person does, in a terminal of its own that you don't see, types each request in and reads the reply from its transcript."),
38
+ t("Your plan counts these requests as Claude Code's own use, at about half of the 5-hour allowance that -p uses for the same work.")),
39
+ mode(!tui, "claude -p",
40
+ t("Claude Code's print mode, the one scripts and the Agent SDK use: a request in, its reply out."),
41
+ t("It starts at once, with no terminal of its own to open first."),
42
+ t("The plan counts a -p request at about twice the interactive mode's.")),
43
+ el("div", "foot", t("Switched here or in Settings › Models › Replies. With the interactive mode on, a request it can't take, or one it fails to start for, fails saying why. A turn Anthropic refuses there as a third-party app's runs as -p.")));
44
+ document.body.append(tip);
45
+ const a = anchor.getBoundingClientRect(), b = tip.getBoundingClientRect();
46
+ tip.style.left = Math.max(8, Math.min(a.left, innerWidth - b.width - 8)) + "px";
47
+ tip.style.top = (a.bottom + 8 + b.height < innerHeight - 8 ? a.bottom + 8 : Math.max(8, a.top - b.height - 8)) + "px";
48
+ }
49
+ function ccTipOff() {
50
+ ccTipEl?.remove();
51
+ ccTipEl = null;
52
+ }
53
+
54
+ // claudeModeDraw fills box, the row app.js put in the editor, from m, a
55
+ // read of /api/claude/mode
56
+ window.claudeModeDraw = function (box, m) {
57
+ if (!m) return;
58
+ const v = el("div", "cc-run");
59
+ // on unless turned off, whether or not this machine can run it: on where
60
+ // it can't, Claude requests fail
61
+ const on = m.why !== "off";
62
+ const pick = segs([["tui", t("Interactive mode (TUI)")], ["p", "claude -p"]], on ? "tui" : "p", async (id) => {
63
+ ccTipOff();
64
+ if ((id === "tui") === on) return;
65
+ // the settings are read with the Settings page: one not opened yet has
66
+ // none to keep
67
+ if (!prefs) prefs = state.settings = await api("settings");
68
+ await savePrefs({ ...prefsKeep(prefs), claudePrintMode: id === "p" });
69
+ claudeMode = await api("claude/mode").catch(() => null);
70
+ claudeModeAt = Date.now();
71
+ if (box.isConnected) window.claudeModeDraw(box, claudeMode);
72
+ });
73
+ pick.classList.add("cc-pick");
74
+ pick.classList.toggle("tui", m.tui);
75
+ // magpie web shows the gateway's, which only its own Settings change
76
+ for (const b of pick.querySelectorAll(".opt")) b.disabled = gatewayMode;
77
+ // Chromium has no onfocusin property: the events are listened for
78
+ pick.onmouseenter = () => ccTip(pick, on);
79
+ pick.addEventListener("focusin", () => ccTip(pick, on));
80
+ pick.onmouseleave = () => ccTipOff();
81
+ pick.addEventListener("focusout", (e) => { if (!pick.contains(e.relatedTarget)) ccTipOff(); });
82
+ v.append(pick);
83
+ const last = m.last;
84
+ let said = "";
85
+ if (on && !m.tui) said = t("Claude requests fail: {why}", { why: ccWhy(m.why) });
86
+ else if (!on) said = t("Every request runs as claude -p");
87
+ else if (last && last.tui) said = t("Last request {ago}: interactive mode", { ago: ago(last.at) });
88
+ if (said) v.append(el("span", "cc-said" + (on && !m.tui ? " bad" : ""), said));
89
+ box.replaceChildren(...field(t("Claude Code runs as"), v, ""));
90
+ };
91
+ // the editor going away takes its tip with it
92
+ addEventListener("pointerdown", (e) => { if (!e.target.closest?.(".cc-pick")) ccTipOff(); }, true);