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.
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,459 @@
1
+ // Settings › Network and sharing's gateway rows (the gateway plugin's part
2
+ // of Settings): its port, model ids without the provider, sharing on the
3
+ // local network and the addresses let in, and the web pages that may call
4
+ // it. Run after app.js, whose helpers it shares (el, segs, input, api,
5
+ // writingPrefs), and before start.js draws Settings.
6
+
7
+ // renderPort: the gateway's address on this computer (Magic_zero on Discord:
8
+ // 3425 taken by another program, or one easier to tell apart). Only its
9
+ // port, 1024 up, is typed here; magpie moves its gateway there and every
10
+ // agent it connected with it, or says why it can't (another program has
11
+ // it). MAGPIE_ADDR, a whole host:port, comes first and the field shows it,
12
+ // off (Chicring on Discord: the row was titled a port while it showed one).
13
+ let portDraft = { value: null, err: "" };
14
+ // portSays is the gateway's word on a port it couldn't take, in the page's
15
+ // language: it says the port, so it is matched rather than looked up
16
+ function portSays(m) {
17
+ let x;
18
+ if ((x = /^port (\d+) is in use by another program/.exec(m))) return t("Port {p} is in use by another program: pick another one", { p: x[1] });
19
+ if ((x = /^another magpie serves the gateway at (\S+):/.exec(m))) return t("Another magpie serves the gateway at {url}: set its port there, or quit it first", { url: x[1] });
20
+ if (/from 1024 to 65535/.test(m)) return t("A port is a number from 1024 to 65535");
21
+ return t(m);
22
+ }
23
+ function renderPort(s) {
24
+ const box = $("#portList");
25
+ const r = el("div", "row pref port-row");
26
+ const who = el("div", "who");
27
+ who.append(el("div", "name", t("Gateway address")));
28
+ const sub = el("div", "sub");
29
+ const d = portDraft;
30
+ const now = s.addrEnv ? "" : String(s.port || 3425);
31
+ const field = input(d.value ?? now, "3425");
32
+ field.className = "words gateway-port";
33
+ field.inputMode = "numeric";
34
+ field.setAttribute("aria-label", t("Gateway address"));
35
+ const save = el("button", "text", t("Apply"));
36
+ if (s.addrEnv) {
37
+ field.value = s.addrEnv;
38
+ field.disabled = save.disabled = true;
39
+ sub.textContent = t("MAGPIE_ADDR={addr} sets the gateway’s whole address, host:port: unset it to set the port here", { addr: s.addrEnv });
40
+ } else if (d.err) {
41
+ sub.textContent = d.err;
42
+ sub.classList.add("err");
43
+ } else sub.textContent = t("Only the port is set here, 1024 to 65535; MAGPIE_ADDR=host:port sets the whole address instead. Agents magpie connected move with it. Now {url}", { url: s.gateway || "" });
44
+ who.append(sub);
45
+ save.onclick = () => {
46
+ const p = Number(field.value.trim());
47
+ if (!Number.isInteger(p) || p < 1024 || p > 65535) {
48
+ d.value = field.value; d.err = t("A port is a number from 1024 to 65535");
49
+ return renderPort(s);
50
+ }
51
+ if (p === (s.port || 3425)) { d.value = null; d.err = ""; return renderPort(s); }
52
+ save.disabled = true;
53
+ writingPrefs(api("settings/port", { port: p }).then((res) => res.settings && (prefs = res.settings, res)))
54
+ .then((res) => {
55
+ portDraft = { value: null, err: "" };
56
+ const n = res.port?.moved?.length || 0;
57
+ if (res.port?.error) status(res.port.error, "err");
58
+ else status(n ? t("Gateway on port {p}; {n} agents moved with it", { p, n }) : t("Gateway on port {p}", { p }), "ok", 2500);
59
+ // the sidebar's gateway line, and the list's address the pages
60
+ // copy, say where it is now, not the port before (noahhrreion on
61
+ // Discord: it kept the old one until Providers was opened)
62
+ if (res.settings?.gateway) renderSideGw({ ...sideGwAt, url: res.settings.gateway, running: true });
63
+ if (providers) loadProviders().catch(() => {});
64
+ renderSettings();
65
+ })
66
+ .catch((e) => { d.value = field.value; d.err = portSays(e.message); status(d.err, "err"); renderSettings(); });
67
+ };
68
+ field.oninput = () => { d.value = field.value; };
69
+ field.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") save.onclick(); if (e.key === "Escape") { portDraft = { value: null, err: "" }; renderPort(s); } };
70
+ const val = el("div", "val");
71
+ val.append(field, save);
72
+ r.append(who, val);
73
+ box.replaceChildren(r); // swapped whole: the list is never laid out empty
74
+ }
75
+
76
+ // renderBareIds: Model ids without the provider (settings.bareModelIDs,
77
+ // love1sbug on X, moving from CLIProxyAPI): the gateway's /v1/models
78
+ // lists each model by its own id as well, gpt-5.6-sol beside
79
+ // codex/gpt-5.6-sol. Posted on its own; off by default.
80
+ function renderBareIds(s) {
81
+ renderBusyLoad(s); // the row under it
82
+ $("#bareIdsSegs").replaceChildren(segs([[false, t("Off")], [true, t("On")]], !!s.bareModelIDs, (on) =>
83
+ writingPrefs(api("settings/bare-ids", { on })).then((ns) => { prefs = state.settings = ns; renderSettings(); })
84
+ .catch((e) => { status(t(e.message), "err"); renderSettings(); })));
85
+ }
86
+
87
+ // renderBusyLoad: the load past which a model its plugin says is busy
88
+ // (magpie.load) goes behind the others (settings.BusyLoad, axelburks on
89
+ // Discord). Custom… opens a field for any other number, 1 to 9999, which
90
+ // is then shown as its own choice; Enter or Apply saves it, Escape closes
91
+ let busyDraft = null;
92
+ const BUSY_CUSTOM = -1;
93
+ function renderBusyLoad(s) {
94
+ const at = s.busyLoad || 0, sub = $("#busyLoadRow .sub"), d = busyDraft;
95
+ const put = (n) => writingPrefs(api("settings/busy-load", { at: n })).then((ns) => { busyDraft = null; prefs = state.settings = ns; renderSettings(); })
96
+ .catch((e) => { status(t(e.message), "err"); renderSettings(); });
97
+ sub.dataset.en ??= sub.textContent; // its English, as translateStatic keeps it
98
+ sub.classList.toggle("err", !!d?.err);
99
+ sub.textContent = d ? d.err || t("A load is in percent, as the plugin reports it: 100 is all its vendor serves at once, and past 100 requests queue (200 is twice that). Type 1 to 9999") : t(sub.dataset.en);
100
+ if (d) {
101
+ const field = input(d.value, "150");
102
+ field.className = "words busy-load-at";
103
+ field.style.cssText = "width:84px;flex:none";
104
+ field.inputMode = "numeric";
105
+ field.setAttribute("aria-label", t("Busy models last"));
106
+ const save = el("button", "text", t("Apply")), no = el("button", "text", t("Cancel"));
107
+ const close = () => { busyDraft = null; renderBusyLoad(s); };
108
+ save.onclick = () => {
109
+ const v = field.value.trim(), n = Number(v);
110
+ if (!/^\d+$/.test(v) || n < 1 || n > 9999) { busyDraft = { value: field.value, err: t("Type a whole number from 1 to 9999") }; renderBusyLoad(s); return $("#busyLoadRow .busy-load-at").focus(NO_SCROLL); }
111
+ save.disabled = true;
112
+ put(n);
113
+ };
114
+ no.onclick = close;
115
+ field.oninput = () => { d.value = field.value; };
116
+ field.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") save.onclick(); if (e.key === "Escape") close(); };
117
+ const val = el("div", "val");
118
+ val.append(field, save, no);
119
+ return $("#busyLoadSegs").replaceChildren(val);
120
+ }
121
+ const opts = [0, 50, 70, 80, 90];
122
+ if (!opts.includes(at)) opts.push(at);
123
+ opts.sort((a, b) => a - b);
124
+ $("#busyLoadSegs").replaceChildren(segs([...opts.map((n) => [n, n ? t("Over {n}%", { n }) : t("Off")]), [BUSY_CUSTOM, t("Custom…")]], at, (n) => {
125
+ if (n !== BUSY_CUSTOM) return put(n);
126
+ busyDraft = { value: [0, 50, 70, 80, 90].includes(at) ? "" : String(at), err: "" };
127
+ renderBusyLoad(s);
128
+ $("#busyLoadRow .busy-load-at").focus(NO_SCROLL);
129
+ }));
130
+ }
131
+
132
+ // renderCORS: the web pages whose scripts may call the gateway from a
133
+ // browser (#1051), by origin — a row to add one, and one row each to take
134
+ // it away. A page listed calls with a gateway key; none listed, no page
135
+ // can read what the gateway answers, as before.
136
+ let corsDraft = { value: "", err: "" };
137
+ // corsSays is the gateway's word on an origin it didn't take, in the
138
+ // page's language
139
+ function corsSays(m) {
140
+ if (/wildcard/.test(m)) return t("Name each origin: a wildcard would let every web page use magpie");
141
+ if (/is not an (extension's )?origin/.test(m)) return t("Write an origin as http://localhost:3000 or https://app.example.com, or a browser extension's as chrome-extension://, moz-extension:// or safari-web-extension:// followed by its ID");
142
+ return t(m);
143
+ }
144
+ function renderCORS(s) {
145
+ renderWebPage(); // Browser access, the section under this one
146
+ const box = $("#corsList");
147
+ box.replaceChildren();
148
+ const origins = s.corsOrigins || [];
149
+ const d = corsDraft;
150
+ const set = (next, done) => writingPrefs(api("settings/cors", { origins: next }))
151
+ .then((ns) => { prefs = ns; d.err = ""; done?.(); renderSettings(); status(t("Saved"), "ok", 1500); })
152
+ .catch((e) => { d.err = corsSays(e.message); status(d.err, "err"); renderSettings(); });
153
+ const field = input(d.value, "http://localhost:3000");
154
+ field.className = "words rule-match";
155
+ field.setAttribute("aria-label", t("Web page origin"));
156
+ field.autocomplete = "off";
157
+ field.spellcheck = false;
158
+ const add = el("button", "text", t("Add"));
159
+ add.onclick = () => {
160
+ d.value = field.value.trim();
161
+ if (!d.value) return field.focus();
162
+ set([...origins, d.value], () => { corsDraft = { value: "", err: "" }; });
163
+ };
164
+ field.oninput = () => { d.value = field.value; };
165
+ field.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") add.onclick(); };
166
+ const r = el("div", "row pref rule-row");
167
+ const who = el("div", "who");
168
+ who.append(el("div", "name", t("Web pages that may call magpie")),
169
+ el("div", "sub" + (d.err ? " err" : ""), d.err || t("Scripts on these origins can call the gateway from a browser, each request with a gateway key from Gateway. A page from any other site is refused; pages on this computer are not")));
170
+ const val = el("div", "val rule-add");
171
+ val.append(field, add);
172
+ r.append(who, val);
173
+ box.append(r);
174
+ for (const o of origins) {
175
+ const row = el("div", "row pref cors-origin"); // an extension's id runs long: wrapped, never cut
176
+ const w = el("div", "who");
177
+ w.append(el("div", "name", o));
178
+ const x = el("button", "text", t("Remove"));
179
+ x.onclick = () => set(origins.filter((v) => v !== o));
180
+ const v = el("div", "val");
181
+ v.append(x);
182
+ row.append(w, v);
183
+ box.append(row);
184
+ }
185
+ }
186
+
187
+ // renderAllow: the addresses other computers may reach magpie from at
188
+ // all (lc on Discord: magpie in Docker on a public host), as IPs and CIDR
189
+ // ranges, under Share on local network — a row to add one, and one row
190
+ // each to take it away. None listed lets every address in, with a gateway
191
+ // key, as before; this computer is always let in. MAGPIE_ALLOW_IPS, where
192
+ // set, is the list, shown and not edited here.
193
+ // A list that would shut out the page saving it names the address magpie
194
+ // sees the page from (Mathresearcher on Discord: which address?), and
195
+ // offers to add it to the list that was refused; behind Docker or a proxy
196
+ // that is the proxy's, not the user's computer's (gateway.peerAddr).
197
+ let allowDraft = { value: "", err: "", addr: "", tried: null };
198
+ function allowSays(m) {
199
+ if (/is not an IP address/.test(m)) return t("Write an address as 203.0.113.7, or a range as 10.0.0.0/8 or 2001:db8::/32");
200
+ const out = /shut out (\S+), the address/.exec(m);
201
+ if (out) return t("That list leaves out {addr}, the address magpie sees this page open from: add it first, or this page can’t reach magpie", { addr: out[1] });
202
+ if (/MAGPIE_ALLOW_IPS/.test(m)) return t("MAGPIE_ALLOW_IPS sets this list; change it there");
203
+ return t(m);
204
+ }
205
+ function renderAllow(s) {
206
+ const box = $("#allowList");
207
+ box.replaceChildren();
208
+ const env = s.allowIPsEnv || "";
209
+ if (!s.lan && !s.onNetwork && !env) return;
210
+ const list = env ? (s.allowIPsFromEnv || []) : (s.allowIPs || []);
211
+ const d = allowDraft;
212
+ const set = (next, done) => writingPrefs(api("settings/allowips", { ips: next }))
213
+ .then((ns) => { prefs = ns; d.err = d.addr = ""; d.tried = null; done?.(); renderSettings(); status(t("Saved"), "ok", 1500); })
214
+ .catch((e) => {
215
+ d.err = allowSays(e.message);
216
+ d.addr = /shut out (\S+), the address/.exec(e.message)?.[1] || "";
217
+ d.tried = d.addr ? { next, done } : null;
218
+ status(d.err, "err"); renderSettings();
219
+ });
220
+ const r = el("div", "row pref rule-row");
221
+ const who = el("div", "who");
222
+ const sub = d.err || (env ? t("Set by MAGPIE_ALLOW_IPS. Other computers reach magpie only from these addresses, even with a gateway key; this computer always can")
223
+ : t("Other computers reach magpie only from these addresses or ranges (CIDR), even with a gateway key; this computer always can. None listed lets every address in"));
224
+ who.append(el("div", "name", t("Allowed addresses")), el("div", "sub" + (d.err ? " err" : ""), sub));
225
+ // the address the refusal named, added to the list that was refused in
226
+ // one click
227
+ if (d.err && d.addr && d.tried && !env) {
228
+ const fix = el("button", "text allow-fix", t("Add {addr}", { addr: d.addr }));
229
+ fix.type = "button";
230
+ fix.title = t("Behind Docker or a reverse proxy, this is the proxy’s address, not your computer’s: adding it lets in everyone who comes through that proxy");
231
+ const { next, done } = d.tried;
232
+ fix.onclick = () => set([...next, d.addr], done);
233
+ who.append(fix);
234
+ }
235
+ r.append(who);
236
+ if (!env) {
237
+ const field = input(d.value, "203.0.113.7, 10.0.0.0/8");
238
+ field.className = "words rule-match allow-ip";
239
+ field.setAttribute("aria-label", t("IP address or range"));
240
+ field.autocomplete = "off";
241
+ field.spellcheck = false;
242
+ const add = el("button", "text", t("Add"));
243
+ add.onclick = () => {
244
+ d.value = field.value.trim();
245
+ if (!d.value) return field.focus();
246
+ set([...list, ...d.value.split(/[\s,;]+/).filter(Boolean)], () => { allowDraft = { value: "", err: "" }; });
247
+ };
248
+ field.oninput = () => { d.value = field.value; };
249
+ field.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") add.onclick(); };
250
+ const val = el("div", "val rule-add");
251
+ val.append(field, add);
252
+ r.append(val);
253
+ }
254
+ box.append(r);
255
+ for (const a of list) {
256
+ const row = el("div", "row pref allow-ip-row");
257
+ const w = el("div", "who");
258
+ w.append(el("div", "name", a));
259
+ const v = el("div", "val");
260
+ if (!env) {
261
+ const x = el("button", "text", t("Remove"));
262
+ x.onclick = () => set(list.filter((o) => o !== a));
263
+ v.append(x);
264
+ }
265
+ row.append(w, v);
266
+ box.append(row);
267
+ }
268
+ }
269
+
270
+ // renderLAN: the gateway shared on the local network, for agents on other
271
+ // machines. Gateway keys and connection examples live together in Gateway.
272
+ let lanSelectedURL = "", lanProtocol = "openai";
273
+ // the address picked under Share on local network is kept like Connect's
274
+ let lanWant = "";
275
+ try { lanWant = localStorage.getItem("magpie.lanAddress") || ""; } catch {}
276
+ // lanShown: the addresses the gateway is offered at beyond this computer,
277
+ // those of them for other containers on this host, and a line on them
278
+ // when magpie runs in a container, for Settings, Gateway, Connect, the
279
+ // sidebar and the key page alike. In a container magpie finds only its
280
+ // own addresses; the page opened over the network was reached at the
281
+ // host's, so the gateway is offered there, on its port. Other containers
282
+ // on the host reach it at the host's Docker bridge address, the
283
+ // container's default route (jiezhengj on Discord: other containers on a
284
+ // NAS needed http://172.17.0.1:3425), which follows, said to be theirs.
285
+ function lanShown(urls, container, bridge) {
286
+ const others = bridge ? [bridge] : [];
287
+ if (!container) return { urls: [...urls, ...others.filter((u) => !urls.includes(u))], others, sub: "" };
288
+ const host = location.hostname, at = urls[0] || bridge ? new URL(urls[0] || bridge) : null;
289
+ if (web && host && at?.port && !/^(localhost|127\.|\[::1\]$)/.test(host))
290
+ return { urls: [at.protocol + "//" + host + ":" + at.port, ...others], others, // an IPv6 hostname comes bracketed
291
+ sub: others.length ? t("Other devices use the host’s address this page was opened at, and other containers on this host the host’s Docker bridge address, both on the gateway’s port; MAGPIE_PUBLIC_URL sets another")
292
+ : t("Where this page was opened, on the gateway’s port; MAGPIE_PUBLIC_URL sets another") };
293
+ if (others.length) return { urls: others, others,
294
+ sub: t("Other containers on this host use the host’s Docker bridge address, on the gateway’s port; other devices use the host’s own, which MAGPIE_PUBLIC_URL puts here") };
295
+ return { urls, others, sub: t("The container’s own addresses, which other devices can’t reach: set MAGPIE_PUBLIC_URL to the host’s") };
296
+ }
297
+ // gwLAN: lanShown for the gateway's read (providers', or boot.js's line),
298
+ // which says whether its addresses are a container's and where other
299
+ // containers reach it
300
+ function gwLAN(g) {
301
+ return lanShown(g?.lan && g.lanURLs || [], !!g?.container, g?.lan && g.containerHost || "");
302
+ }
303
+ // gwFar: where other machines reach the gateway, for the sidebar and the
304
+ // key page: other containers' address only when there is no other
305
+ function gwFar(g) {
306
+ const { urls, others } = gwLAN(g), far = urls.filter((u) => !others.includes(u));
307
+ return far.length ? far : urls;
308
+ }
309
+ // sideLAN: where other machines reach the gateway while it is shared, for
310
+ // the sidebar's line (coeo91 on Discord): MAGPIE_PUBLIC_URL, else the
311
+ // address picked here, else the first
312
+ function sideLAN(g) {
313
+ const u = gwFar(g);
314
+ return u[0] && (g.publicURL || (u.includes(lanWant) ? lanWant : u[0]));
315
+ }
316
+ // the line was drawn from boot.js before this script came
317
+ if (sideGwAt) renderSideGw();
318
+ function renderLAN(s) {
319
+ const box = $("#lanList");
320
+ box.replaceChildren();
321
+ const row = (name, sub, value, ...tools) => {
322
+ const r = el("div", "row pref");
323
+ const who = el("div", "who");
324
+ who.append(el("div", "name", name));
325
+ if (sub) who.append(el("div", "sub", sub));
326
+ const val = el("div", "val");
327
+ if (value) val.append(el("code", "", value));
328
+ val.append(...tools);
329
+ r.append(who, val);
330
+ box.append(r);
331
+ return r;
332
+ };
333
+ const set = (body) => writingPrefs(api("settings/lan", body)).then((ns) => {
334
+ prefs = ns; renderSettings();
335
+ // the sidebar's gateway names the address shared, or this computer's
336
+ api("providers").then((r) => renderSideGw(r?.gateway), () => {});
337
+ if (ns.rekeyed) status(ns.rekeyed, "err");
338
+ })
339
+ .catch((e) => { status(t(e.message), "err"); renderSettings(); });
340
+ row(t("Share on local network"), t("Agents on other computers on this network, or through a tunnel to this one such as Cloudflare Tunnel, can use magpie’s models with a gateway key from Gateway"), "",
341
+ segs([["off", t("Off")], ["on", t("On")]], s.lan ? "on" : "off", (v) => set({ on: v === "on" })));
342
+ if (!s.lan) return;
343
+ const { urls, others, sub } = lanShown(s.lanURLs || [], s.lanContainer, s.lanContainerHost);
344
+ if (!urls.length) row(t("Address"), t("This computer has no local network address right now"), "");
345
+ else {
346
+ lanSelectedURL = urls.includes(lanWant) ? lanWant : urls[0];
347
+ // an interface can hold an internet address (an ISP's static one, a
348
+ // VPN's): the gateway answers there too, so the row says so
349
+ const far = new Set(s.lanPublic || []);
350
+ const listed = (s.allowIPsEnv ? s.allowIPsFromEnv : s.allowIPs)?.length > 0;
351
+ const controls = el("div", "lan-address-controls");
352
+ const address = urls.length === 1 ? el("code", "lan-address-text") : el("button", "proto pick lan-interface");
353
+ if (urls.length > 1) {
354
+ address.type = "button";
355
+ address.setAttribute("aria-label", t("Address"));
356
+ address.onclick = (e) => {
357
+ e.stopPropagation();
358
+ if (address.classList.contains("open")) return closeProtoMenu();
359
+ const suffix = lanProtocol === "openai" ? "/v1" : "";
360
+ openProtoMenu(address, urls.map((u) => ({ v: u, name: u + suffix, note: far.has(u) ? t("On the internet") : others.includes(u) ? t("Other containers on this host") : "" })), lanSelectedURL,
361
+ (v) => { lanSelectedURL = lanWant = v; keepPick("magpie.lanAddress", v); update(); renderSideGw(); }, "Address", "lan-address-menu");
362
+ };
363
+ }
364
+ const copyControl = el("span", "lan-copy");
365
+ const update = () => {
366
+ const label = lanProtocol === "openai" ? "OpenAI" : "Anthropic";
367
+ const url = lanSelectedURL + (lanProtocol === "openai" ? "/v1" : "");
368
+ if (urls.length === 1) address.textContent = url;
369
+ else address.replaceChildren(el("span", "lan-url", url), svg(CHEV, 11, 1.6));
370
+ address.title = url;
371
+ const what = t("{label} address", { label });
372
+ const button = copyBtn(url, what, t("Copied {label} address", { label }));
373
+ button.setAttribute("aria-label", t("Copy") + " " + what);
374
+ copyControl.replaceChildren(button);
375
+ const who = addressRow?.querySelector(".who");
376
+ if (!who) return;
377
+ who.querySelector(".sub")?.remove();
378
+ if (far.has(lanSelectedURL)) who.append(el("div", "sub", listed
379
+ ? t("An internet address: only the addresses under Allowed addresses get in")
380
+ : t("An internet address: anyone online can reach magpie here with a gateway key. To keep it to this network, list its range under Allowed addresses")));
381
+ };
382
+ controls.append(segs([["openai", "OpenAI"], ["anthropic", "Anthropic"]], lanProtocol,
383
+ (v) => { lanProtocol = v; update(); }), address, copyControl);
384
+ const addressRow = row(t("Address"), "", "", controls);
385
+ addressRow.classList.add("lan-address-row");
386
+ update();
387
+ }
388
+ if (sub) row(t("In a container"), sub, "").classList.add("lan-container");
389
+ }
390
+
391
+ // renderWebPage: Browser access, the app's own page served to a browser
392
+ // (apppage.go; jiakun_zhao on X managed magpie through a Cloudflare tunnel
393
+ // with `magpie web` kept running in a terminal beside the app). Off, this
394
+ // computer alone (for a tunnel or an SSH forward to reach), or the local
395
+ // network. The page always asks for its key: the link carries it, and a
396
+ // browser without it gets nothing. The app's windows alone draw it; a
397
+ // browser on the page can't turn it off under itself.
398
+ let webPageState = null, webPagePort = null;
399
+ // Each draw asks again, as a port another program held can come free; the
400
+ // answer draws without asking.
401
+ function renderWebPage(asked) {
402
+ const head = $("#webPageHead"), box = $("#webPageList");
403
+ if (!box) return;
404
+ head.hidden = box.hidden = web;
405
+ if (web) return;
406
+ if (!asked) api("web-page").then((r) => { webPageState = r; renderWebPage(true); }).catch(() => {});
407
+ const s = webPageState;
408
+ if (!s) return;
409
+ const set = (body) => writingPrefs(api("web-page", body))
410
+ .then((r) => { webPageState = r; webPagePort = null; renderWebPage(true); })
411
+ .catch((e) => { status(t(e.message), "err"); webPageState = null; renderWebPage(); });
412
+ const rows = [];
413
+ const row = (name, sub, ...tools) => {
414
+ const r = el("div", "row pref");
415
+ const who = el("div", "who");
416
+ who.append(el("div", "name", name));
417
+ if (sub) who.append(sub instanceof Node ? sub : el("div", "sub", sub));
418
+ const val = el("div", "val");
419
+ val.append(...tools);
420
+ r.append(who, val);
421
+ rows.push(r);
422
+ return r;
423
+ };
424
+ row(t("Open magpie in a browser"),
425
+ t("The app serves this window’s page with a key in its link, as magpie web does, while it runs. For a tunnel such as Cloudflare Tunnel, point it at http://127.0.0.1:{port}", { port: s.port }),
426
+ segs([["", t("Off")], ["local", t("This computer")], ["lan", t("Local network")]], s.mode || "", (mode) => set({ mode }))).classList.add("web-page-mode");
427
+ if (s.mode) {
428
+ const why = s.error ? el("div", "sub err", s.error.startsWith("port ") ? t("Port {p} is taken by another program, magpie web perhaps: magpie tries again every few seconds", { p: s.port }) : t(s.error))
429
+ : el("div", "sub say", s.serving ? t("Served; anyone with this link can change every setting") : t("Starting…"));
430
+ const links = [s.link, ...(s.mode === "lan" ? s.network || [] : [])].filter(Boolean);
431
+ const box2 = el("div", "web-page-links");
432
+ for (const l of links) {
433
+ const line = el("span", "web-page-link");
434
+ line.append(el("code", "", l.replace(/\?k=.*/, "?k=…")), copyBtn(l, t("Link"), t("Copied")));
435
+ box2.append(line);
436
+ }
437
+ const rekey = el("button", "text", t("New key"));
438
+ rekey.onclick = () => set({ newKey: true });
439
+ row(t("Link"), why, box2, rekey).classList.add("web-page-link-row");
440
+ const field = input(webPagePort ?? String(s.port), "3430");
441
+ field.className = "words gateway-port";
442
+ field.inputMode = "numeric";
443
+ field.setAttribute("aria-label", t("Port"));
444
+ const save = el("button", "text", t("Apply"));
445
+ save.onclick = () => {
446
+ const p = Number(field.value.trim());
447
+ if (!Number.isInteger(p) || p < 1024 || p > 65535) return status(t("A port is a number from 1024 to 65535"), "err");
448
+ set({ port: p });
449
+ };
450
+ field.oninput = () => { webPagePort = field.value; };
451
+ field.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") save.onclick(); };
452
+ row(t("Port"), "", field, save);
453
+ if (s.mode === "lan" && !s.tls) {
454
+ const r = row(t("Not encrypted"), t("Other computers reach the page over plain HTTP, so anyone on this network can read its key: on a network you don’t trust, keep it on This computer and reach it through a tunnel"));
455
+ r.classList.add("web-page-warn");
456
+ }
457
+ }
458
+ box.replaceChildren(...rows);
459
+ }