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,985 @@
1
+ // Insights › Masking's code (client.js has its markup and hooks): run after
2
+ // app.js, whose masking helpers it shares (maskTip, maskColor,
3
+ // maskShape, maskValue: Settings' Try it and the masked calls' badges use
4
+ // them too; Try it uses this page's maskMatches in turn). What a
5
+ // placeholder stood for is in this magpie's memory alone, never on disk,
6
+ // so after a restart it is gone; so is the text a request went out with
7
+ // (/api/masking/sent), kept for the newest requests only.
8
+
9
+ const stillMotion = calm;
10
+ // maskMatches is text's stand-ins as matchAll gives them: its {{…}}
11
+ // placeholders, and the made-up keys "Keep a key's form" sent (redact's
12
+ // shape.go), the keys of info that are no placeholder, which only the
13
+ // record's own list tells from the text around them (#3599)
14
+ const maskLookRes = new WeakMap();
15
+ function maskMatches(text, info) {
16
+ let re = maskLookRes.get(info);
17
+ if (!re) {
18
+ const looks = [...info.keys()].filter((p) => p && !p.startsWith("{{")).sort((a, b) => b.length - a.length)
19
+ .map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
20
+ re = looks.length ? new RegExp(PLACEHOLDER.source + "|" + looks.join("|"), "g") : PLACEHOLDER;
21
+ maskLookRes.set(info, re);
22
+ }
23
+ return text.matchAll(re);
24
+ }
25
+ // the providers' names, read once when the page opened before any page
26
+ // that loads them
27
+ let maskProv = null, maskProvAsked = false;
28
+ function maskWho(id, kind) {
29
+ const x = kind === "agent" ? state.agents?.find((a) => a.id === id) : (providers || maskProv)?.providers?.find((p) => p.id === id);
30
+ return { name: x?.name || id || t("Unknown"), icon: x?.icon };
31
+ }
32
+ // what a request was, beside a chat (gateway's noteMasked)
33
+ const MASK_WHAT = { search: "Web search", image: "Image", video: "Video", decide: "System One", embeddings: "Embeddings", audio: "Audio" };
34
+ // maskDayName: Today, Yesterday or the date a masked request went on
35
+ function maskDayName(when) {
36
+ const d = new Date(when), now = new Date();
37
+ const days = Math.round((new Date(now.toDateString()) - new Date(d.toDateString())) / 86400000);
38
+ if (days === 0) return t("Today");
39
+ if (days === 1) return t("Yesterday");
40
+ return d.toLocaleDateString(intlLang(), { weekday: "short", month: "short", day: "numeric" });
41
+ }
42
+
43
+ // ---- the summary: over 7 or 30 days, kept per machine ----
44
+ let maskSumDays = 0, maskAt = 0, maskBusy = null, maskBusyDays = 0;
45
+ let maskDays = 7;
46
+ try { if (localStorage.getItem("magpie.maskDays") === "30") maskDays = 30; } catch {}
47
+ let maskDrawn = ""; // what the card says, so a redraw that changes nothing is skipped
48
+ let maskKeyRead = false; // the address's key=, a masked call opened from the tray panel
49
+ // the list: by request or by value, and what it is narrowed to
50
+ const maskL = { mode: "req", facet: "", kind: "", agent: "", provider: "", records: [], more: false, busy: false, error: "", read: false };
51
+ let maskO = null; // the request open, while it is
52
+
53
+ // maskLoad reads the summary for the days shown, when what is kept is
54
+ // older than 10s or for other days; forced, it reads again whatever is kept
55
+ function maskLoad(force) {
56
+ const days = maskDays;
57
+ if (maskBusy && maskBusyDays === days) return maskBusy;
58
+ if (!force && maskSum && maskSumDays === days && Date.now() - maskAt < 10000) return null;
59
+ maskBusyDays = days;
60
+ const p = maskBusy = api("masking/summary?days=" + days).then((sum) => {
61
+ if (days !== maskDays) return;
62
+ maskSum = { days: [], kinds: [], agents: [], providers: [], values: [], by: {}, n: 0, distinct: 0, requests: 0, ...(sum || {}) };
63
+ maskSumDays = days;
64
+ maskAt = Date.now();
65
+ }).catch(() => {}).finally(() => { if (maskBusy === p) maskBusy = null; });
66
+ return p;
67
+ }
68
+ const maskSettings = () => prefs || state?.settings || {};
69
+ // on as the masker is (masking.Wanted): your own words mask with neither
70
+ // switch on
71
+ const maskOn = () => { const s = maskSettings(); return !!(s.redact || s.redactPersonal || s.redactWords?.length); };
72
+
73
+ function maskPageOpen() {
74
+ if (!providers && !maskProvAsked) {
75
+ maskProvAsked = true;
76
+ api("providers").then((r) => { maskProv = r; maskDrawn = ""; maskDrawAll(); }).catch(() => { maskProvAsked = false; });
77
+ }
78
+ // a masked call in the tray panel opens the window on its request
79
+ if (!maskKeyRead) {
80
+ maskKeyRead = true;
81
+ const k = params.get("key");
82
+ if (k) {
83
+ const q = new URLSearchParams(location.search);
84
+ q.delete("key");
85
+ history.replaceState(null, "", "?" + q);
86
+ maskOneOpen(k);
87
+ }
88
+ }
89
+ return maskPageLoad();
90
+ }
91
+ function maskPageLoad(force) {
92
+ maskDrawAll();
93
+ (prefs ? Promise.resolve() : loadSettings()).then(drawMaskAlert).catch(() => {});
94
+ const sum = maskLoad(force)?.then(() => { drawMaskCard(); drawMaskList(); });
95
+ const recs = maskL.read && force ? maskRefresh() : maskL.read ? null : maskFetch();
96
+ return Promise.all([sum, recs]);
97
+ }
98
+ function maskDrawAll() {
99
+ drawMaskCard();
100
+ drawMaskList();
101
+ if (maskO) drawMaskOne();
102
+ }
103
+ // the Masking alert (Must2333 on X): a notification when a value is
104
+ // masked for the first time, off until turned on. Gateway mode leaves it
105
+ // out, as it does the other alerts its desktop would show
106
+ function drawMaskAlert() {
107
+ const s = prefs || {};
108
+ $("#maskPrefs").hidden = gatewayMode;
109
+ $("#maskAlertSegs").replaceChildren(segs([["off", t("Off")], ["on", t("On")]], s.maskAlert ? "on" : "off",
110
+ (v) => savePrefs({ ...prefsKeep(s), maskAlert: v === "on" }).then(drawMaskAlert)));
111
+ }
112
+ // while the page is on screen it keeps up with requests as they go
113
+ setInterval(() => {
114
+ if (view !== "masking" || document.hidden) return;
115
+ maskLoad(true)?.then(() => { drawMaskCard(); if (maskL.mode === "val" || !maskO) drawMaskList(); });
116
+ if (!maskL.busy) maskRefresh();
117
+ }, 15000);
118
+
119
+ // ---- the card: how much was kept, and where it went ----
120
+ function drawMaskCard() {
121
+ const box = $("#maskCard");
122
+ if (!box) return;
123
+ // until the window's state is read, agents would show as their ids
124
+ const on = maskOn(), sum = maskSumDays === maskDays && load.done ? maskSum : null;
125
+ const key = JSON.stringify([document.documentElement.lang, maskDays, on, !!maskSettings().redactWords?.length, sum, state?.agents?.length, (providers || maskProv)?.providers?.length]);
126
+ if (key === maskDrawn && box.firstChild) return;
127
+ maskDrawn = key;
128
+ // it moves when it first has numbers to say, and for other days: not
129
+ // on every poll
130
+ const move = !!sum && box.dataset.drawn !== String(maskDays) && !stillMotion();
131
+ if (sum) box.dataset.drawn = String(maskDays);
132
+ maskTipOff();
133
+ box.replaceChildren();
134
+ box.classList.remove("off");
135
+ if (sum && !sum.requests && !on) return maskOffCard(box);
136
+
137
+ const head = el("div", "msk-head"), words = el("div", "msk-title");
138
+ words.append(el("h2", "", t("Kept from vendors")),
139
+ el("span", "msk-sub", on ? t("Masked on this computer before each request left it, and put back in each answer") : t("Masking is off: requests go to vendors as they are")));
140
+ head.append(words, el("span", "grow"));
141
+ if (!on) {
142
+ const b = el("button", "text", t("Turn on masking"));
143
+ b.type = "button";
144
+ b.onclick = maskTurnOn;
145
+ head.append(b);
146
+ } else head.append(maskOffButton());
147
+ if (sum?.requests || maskDays === 30) head.append(segs([["7", t("7 days")], ["30", t("30 days")]], String(maskDays), (v) => {
148
+ maskDays = +v;
149
+ try { localStorage.setItem("magpie.maskDays", v); } catch {}
150
+ maskLoad(true)?.then(() => { drawMaskCard(); drawMaskList(); });
151
+ }));
152
+ box.append(head);
153
+ const band = el("div", "msk-band");
154
+ box.append(band);
155
+ if (!sum) {
156
+ band.append(el("span", "skeleton msk-sk-num"), el("span", "skeleton msk-sk-flow"));
157
+ return;
158
+ }
159
+
160
+ // the number, what makes it up, and the days
161
+ const nums = el("div", "msk-nums");
162
+ const num = el("div", "msk-num", sum.n.toLocaleString(intlLang()));
163
+ nums.append(num, el("div", "msk-unit", t(sum.n === 1 ? "value masked" : "values masked")));
164
+ const stats = el("div", "msk-stats");
165
+ const stat = (label, n, cls) => { const s = el("span", cls || ""); s.append(el("b", "", fmtN(n)), document.createTextNode(label)); stats.append(s); };
166
+ stat(t("Different values"), sum.distinct);
167
+ stat(t("Requests"), sum.requests);
168
+ if (sum.blocked) stat(t("Held back"), sum.blocked, "held");
169
+ nums.append(stats);
170
+ const days = el("div", "msk-days");
171
+ nums.append(days);
172
+ band.append(nums);
173
+ maskStrip(days, sum.days, move);
174
+ if (move && sum.n) maskCountUp(num, sum.n);
175
+ if (sum.agents.length && sum.providers.length) band.append(maskFlow(sum, move));
176
+ else band.append(maskNone());
177
+ if (move) for (const [i, n] of [...band.children].entries()) n.animate([{ opacity: 0, transform: "translateY(6px)", filter: "blur(6px)" }, {}], { duration: 900, delay: i * 120, easing: "cubic-bezier(.2, .7, .1, 1)", fill: "backwards" });
178
+ }
179
+
180
+ // masking off and nothing ever masked: what it would do, and the switch
181
+ function maskOffCard(box) {
182
+ box.classList.add("off");
183
+ const s = svg(SHIELD, 40, .9);
184
+ s.classList.add("msk-off-ic");
185
+ const b = el("button", "text primary", t("Turn on masking"));
186
+ b.type = "button";
187
+ b.onclick = maskTurnOn;
188
+ const also = el("span", "msk-also");
189
+ const link = el("button", "text", t("Settings › Privacy"));
190
+ link.type = "button";
191
+ link.onclick = (e) => openSettingsAt(e, "privacy");
192
+ also.append(el("span", "", t("You can also change it in")), link);
193
+ box.append(s, el("h2", "", t("Requests go to vendors as they are")),
194
+ el("p", "", t("Turned on, keys, passwords and tokens become placeholders before they leave this computer, and are put back in each answer. Emails and phone numbers too once Mask personal data is on. What each request had masked shows here.")),
195
+ b, also);
196
+ }
197
+ // maskNone: what the card says while nothing was masked. With secrets
198
+ // alone on, an agent's everyday requests carry nothing to mask (an email
199
+ // or a name isn't a secret), so it says that, and where personal data is
200
+ // turned on, rather than a 0 that reads as masking not working
201
+ function maskNone() {
202
+ const s = maskSettings(), box = el("div", "msk-flow-none");
203
+ if (s.redactPersonal || !s.redact) {
204
+ box.textContent = t("A request that carries a key, a password or personal data shows here as it goes");
205
+ return box;
206
+ }
207
+ box.append(el("span", "", t("Only keys, passwords and tokens are masked now, and a request that carries one shows here as it goes. Emails and phone numbers are masked once Mask personal data is on, in")));
208
+ const link = el("button", "text", t("Settings › Privacy"));
209
+ link.type = "button";
210
+ link.onclick = (e) => openSettingsAt(e, "privacy");
211
+ box.append(" ", link);
212
+ return box;
213
+ }
214
+ // the way off, beside the way on (ZekeXiao on X: "脱敏好像只有一个打开
215
+ // 开关,不能关?"). Both switches go off at once. Your own words mask with neither
216
+ // on, and only Settings › Privacy can take them out, so with words the
217
+ // button opens it there, and says so, rather than leave the page
218
+ // saying off while words still mask
219
+ function maskOffButton() {
220
+ const words = maskSettings().redactWords?.length;
221
+ const b = el("button", "text", t(words ? "Turn off in Settings" : "Turn off masking"));
222
+ b.type = "button";
223
+ if (words) {
224
+ b.title = t("Your own masked words still mask until you take them out, in Settings › Privacy");
225
+ b.onclick = (e) => openSettingsAt(e, "privacy");
226
+ return b;
227
+ }
228
+ b.onclick = () => {
229
+ b.disabled = true;
230
+ (prefs ? Promise.resolve() : loadSettings())
231
+ .then(() => {
232
+ // turned on again from this page, it comes back as it was
233
+ maskWasPersonal = !!prefs.redactPersonal;
234
+ return savePrefs({ ...prefsKeep(prefs), redact: false, redactPersonal: false });
235
+ })
236
+ .then(() => { maskDrawn = ""; drawMaskCard(); })
237
+ .finally(() => { b.disabled = false; });
238
+ };
239
+ return b;
240
+ }
241
+ let maskWasPersonal = false;
242
+ function maskTurnOn(e) {
243
+ const b = e.currentTarget;
244
+ b.disabled = true;
245
+ (prefs ? Promise.resolve() : loadSettings())
246
+ .then(() => {
247
+ const back = maskWasPersonal;
248
+ maskWasPersonal = false;
249
+ if (back) window.hideAccounts?.set(true, true);
250
+ return savePrefs({ ...prefsKeep(prefs), redact: true, ...(back ? { redactPersonal: true } : {}) });
251
+ })
252
+ .then(() => { maskDrawn = ""; drawMaskCard(); })
253
+ .finally(() => { b.disabled = false; });
254
+ }
255
+
256
+ function maskCountUp(node, n) {
257
+ const start = performance.now(), ms = 1100;
258
+ const step = (now) => {
259
+ if (!node.isConnected) return;
260
+ const p = Math.min(1, (now - start) / ms);
261
+ const e = p === 1 ? 1 : 1 - Math.pow(2, -10 * p);
262
+ node.textContent = Math.round(n * e).toLocaleString(intlLang());
263
+ if (p < 1) requestAnimationFrame(step);
264
+ };
265
+ requestAnimationFrame(step);
266
+ }
267
+
268
+ // maskStrip: the values masked each day, a bar a day on a hairline, today
269
+ // in the accent; the day pointed at said over it
270
+ function maskStrip(box, days, move) {
271
+ const total = days.reduce((a, d) => a + d.n, 0);
272
+ box.setAttribute("role", "img");
273
+ box.setAttribute("aria-label", t("{n} values masked over {days} days", { n: fmtN(total), days: days.length }));
274
+ if (!days.length) return;
275
+ const max = Math.max(1, ...days.map((d) => d.n));
276
+ const bars = el("div", "msk-bars" + (days.length > 14 ? " many" : ""));
277
+ const tip = el("div", "msk-ctip");
278
+ tip.hidden = true;
279
+ const fmt = (d) => d === days.at(-1).day ? t("Today") : new Date(d + "T00:00").toLocaleDateString(intlLang(), { month: "short", day: "numeric" });
280
+ days.forEach((d, i) => {
281
+ const b = el("span", "msk-day" + (i === days.length - 1 ? " now" : "") + (d.n ? "" : " zero"));
282
+ const f = el("i");
283
+ if (d.n) f.style.height = Math.max(8, (100 * d.n) / max) + "%";
284
+ b.append(f);
285
+ b.onpointerenter = () => {
286
+ tip.hidden = false;
287
+ tip.textContent = `${fmt(d.day)} · ${t(d.n === 1 ? "1 value masked" : "{n} values masked", { n: fmtN(d.n) })} · ${t(d.requests === 1 ? "1 request" : "{n} requests", { n: fmtN(d.requests) })}`;
288
+ const w = tip.offsetWidth, W = box.clientWidth, x = b.offsetLeft + b.offsetWidth / 2;
289
+ tip.style.left = Math.max(w / 2, Math.min(x, W - w / 2)) + "px";
290
+ };
291
+ b.onpointerleave = () => { tip.hidden = true; };
292
+ if (move && d.n) f.animate([{ transform: "scaleY(0)" }, { transform: "none" }], { duration: 760, delay: 160 + (i * 520) / days.length, easing: "cubic-bezier(.32, .72, 0, 1)", fill: "backwards" });
293
+ bars.append(b);
294
+ });
295
+ const axis = el("div", "msk-axis");
296
+ axis.append(el("span", "", fmt(days[0].day)), el("span", "", fmt(days.at(-1).day)));
297
+ box.append(bars, axis, tip);
298
+ }
299
+
300
+ // maskFlow: hairlines from the agents into this computer, uneven as
301
+ // written, and out of it to the providers, even as placeholders; magpie's
302
+ // bird on the line between, so a screenshot says who masked them
303
+ function maskFlow(sum, move) {
304
+ const sec = el("div", "msk-fl"), flow = el("div", "mo-flow");
305
+ const side = (list, kind) => {
306
+ const shown = list.slice(0, 3), rest = list.slice(3);
307
+ const rows = shown.map((x) => ({ id: x.name, n: x.n, ...maskWho(x.name, kind) }));
308
+ if (rest.length) rows.push({ id: "", n: rest.reduce((a, x) => a + x.n, 0), name: t("{n} more", { n: rest.length }) });
309
+ const col = el("div", "mo-col " + kind);
310
+ for (const r of rows) {
311
+ const b = el("button", "mo-node" + (r.id && maskL[kind] === r.id ? " on" : ""));
312
+ b.type = "button";
313
+ // its logo first, then its name, on both sides; "N more" keeps the
314
+ // logo's room so the names line up
315
+ const ic = r.id ? icon(r.icon || "generic") : el("span", "ic");
316
+ const tx = el("span", "tx");
317
+ tx.append(el("span", "nm", r.name), el("span", "ct", fmtN(r.n)));
318
+ b.append(ic, tx);
319
+ if (r.id) {
320
+ b.title = t("Show what {name} sent masked", { name: r.name });
321
+ b.onclick = () => maskNarrow({ [kind]: maskL[kind] === r.id ? "" : r.id });
322
+ } else b.disabled = true;
323
+ col.append(b);
324
+ }
325
+ return [col, rows];
326
+ };
327
+ const [from, fromRows] = side(sum.agents, "agent");
328
+ const [to, toRows] = side(sum.providers, "provider");
329
+ const mid = el("div", "mo-mid");
330
+ const rowH = 34, H = Math.max(fromRows.length, toRows.length, 3) * rowH;
331
+ const max = Math.max(...fromRows.map((r) => r.n), ...toRows.map((r) => r.n), 1);
332
+ const hub = el("span", "msk-hub");
333
+ hub.title = "magpie";
334
+ hub.innerHTML = '<svg viewBox="0 0 44 44" aria-hidden="true"><use href="#bird"/></svg>';
335
+ // drawn in the middle's own pixels on each resize: a stretched viewBox
336
+ // would bend the hairlines
337
+ const s = sv("svg", { "aria-hidden": "true" });
338
+ let drawnW = 0;
339
+ const draw = () => {
340
+ const W = Math.round(mid.clientWidth);
341
+ if (!W || W === drawnW) return;
342
+ drawnW = W;
343
+ s.setAttribute("viewBox", `0 0 ${W} ${H}`);
344
+ s.replaceChildren();
345
+ const m = W / 2, still = stillMotion(), lines = [];
346
+ const add = (tag, a, to = s) => { const n = sv(tag, a); to.append(n); return n; };
347
+ const curve = (x0, y0, x1, y1) => `M${x0},${y0}C${(x0 + x1) / 2},${y0} ${(x0 + x1) / 2},${y1} ${x1},${y1}`;
348
+ const bundle = (rows, out) => {
349
+ const ks = rows.map((r) => Math.max(1, Math.round(18 * r.n / max))), all = ks.reduce((a, k) => a + k, 0);
350
+ let j = 0, rnd = out ? 11 : 7;
351
+ rows.forEach((r, i) => {
352
+ const y = (H - rows.length * rowH) / 2 + (i + .5) * rowH;
353
+ for (let q = 0; q < ks[i]; q++, j++) {
354
+ const at = all > 1 ? H * (.22 + .56 * j / (all - 1)) : H / 2;
355
+ rnd = (rnd * 16807) % 2147483647;
356
+ const end = y + (q - (ks[i] - 1) / 2) * 1.4 + (out ? 0 : (rnd % 30 - 15) / 10);
357
+ const d = out ? curve(m, at, W, end) : curve(0, end, m, at);
358
+ lines.push(add("path", { d, pathLength: 1, class: "mo-th" + (out ? " out" : "") + (ks[i] === 1 ? " one" : ""), "stroke-opacity": ks[i] === 1 ? (out ? .8 : .4) : out ? .42 : .07 + (rnd % 20) / 100 }));
359
+ // a value goes in round, a placeholder out square
360
+ if (q === ks[i] >> 1 && !still) for (let k = 0, dots = 1 + Math.round(2 * r.n / max); k < dots; k++) {
361
+ const dur = 1.7 + i * .5, begin = (out ? dur : 0) + k * 2 * dur / dots + "s";
362
+ const z = out ? 1.8 : 1.7, dot = add("rect", { x: -z, y: -z, width: 2 * z, height: 2 * z, rx: out ? .6 : z, class: "mo-dot" + (out ? " out" : "") });
363
+ // twice round when drawn, not forever: an endless motion lays the
364
+ // page out each frame, ~16% of a core while it sits open (#3185)
365
+ const a = { dur: dur + "s", begin, repeatCount: 2 };
366
+ add("animateMotion", { path: d, ...a }, dot);
367
+ add("animate", { attributeName: "opacity", values: "0;1;1;0", keyTimes: "0;.1;.85;1", ...a }, dot);
368
+ }
369
+ }
370
+ });
371
+ };
372
+ bundle(fromRows, false);
373
+ bundle(toRows, true);
374
+ // this computer: a ticked line, and a mark going down it
375
+ add("line", { x1: m, y1: 0, x2: m, y2: H, class: "mo-mem" });
376
+ for (let y = 0; y <= H; y += 17) add("line", { x1: m - 3, y1: y, x2: m + 3, y2: y, class: "mo-tick" });
377
+ if (!still) add("rect", { x: m - 1, y: 0, width: 2, height: 18, rx: 1, class: "mo-scan" }).style.setProperty("--h", H - 18 + "px");
378
+ // drawn in once, the way in first
379
+ if (move) lines.forEach((p) => {
380
+ p.animate([{ strokeDashoffset: 1 }, {}], { duration: 2400, delay: p.classList.contains("out") ? 700 : 300, easing: "cubic-bezier(.3, .6, .1, 1)", fill: "backwards" });
381
+ });
382
+ move = false;
383
+ };
384
+ mid.append(s, hub);
385
+ flow.style.height = H + "px";
386
+ flow.append(from, mid, to);
387
+ new ResizeObserver(draw).observe(mid);
388
+ sec.append(flow, el("div", "mo-cap", t("magpie masked these on this computer")));
389
+ return sec;
390
+ }
391
+
392
+ // ---- the list: each request masking changed, or each value ----
393
+ const MASK_FACETS = [
394
+ ["", "All", (s) => s.n],
395
+ ["secret", "Secrets", (s) => (s.kinds || []).filter((k) => MASK_SECRET_KINDS.includes(k.name)).reduce((a, k) => a + k.n, 0)],
396
+ ["rule", "Rules", (s) => s.by?.rule || 0],
397
+ ["model", "Local model", (s) => s.by?.model || 0],
398
+ ["blocked", "Held back", (s) => s.blocked || 0],
399
+ ];
400
+ function maskQuery(before) {
401
+ const q = new URLSearchParams({ limit: "40" });
402
+ if (before) q.set("before", before);
403
+ const f = maskL.facet;
404
+ if (maskL.kind) q.set("kind", maskL.kind);
405
+ else if (f === "secret") q.set("kind", MASK_SECRET_KINDS.join(","));
406
+ if (f === "rule" || f === "model") q.set("by", f);
407
+ if (f === "blocked") q.set("blocked", "1");
408
+ if (maskL.agent) q.set("agent", maskL.agent);
409
+ if (maskL.provider) q.set("provider", maskL.provider);
410
+ return "masking/records?" + q;
411
+ }
412
+ // maskNarrow: the list for other filters, read again from the start
413
+ function maskNarrow(change) {
414
+ Object.assign(maskL, change);
415
+ maskDrawn = "";
416
+ drawMaskCard();
417
+ return maskFetch();
418
+ }
419
+ let maskSeq = 0;
420
+ function maskFetch(more) {
421
+ const seq = ++maskSeq;
422
+ maskL.busy = true;
423
+ maskL.error = "";
424
+ drawMaskList();
425
+ return api(maskQuery(more ? maskL.records.at(-1)?.key : 0))
426
+ .then((r) => {
427
+ if (seq !== maskSeq) return;
428
+ const got = r?.records || [];
429
+ maskL.records = more ? [...maskL.records, ...got] : got;
430
+ maskL.more = !!r?.more;
431
+ })
432
+ .catch((e) => { if (seq === maskSeq) maskL.error = e.message; })
433
+ .finally(() => {
434
+ if (seq !== maskSeq) return;
435
+ maskL.busy = false;
436
+ maskL.read = true;
437
+ drawMaskList();
438
+ if (maskO) drawMaskOne(); // its previous and next
439
+ });
440
+ }
441
+ // new records come in over the ones shown, which keep their place
442
+ function maskRefresh() {
443
+ if (!maskL.records.length) return maskFetch();
444
+ const seq = maskSeq;
445
+ return api(maskQuery(0)).then((r) => {
446
+ if (seq !== maskSeq || !maskL.records.length) return;
447
+ const fresh = (r?.records || []).filter((x) => BigInt(x.key) > BigInt(maskL.records[0].key));
448
+ if (!fresh.length) return;
449
+ maskL.records = [...fresh, ...maskL.records];
450
+ if (!maskO) drawMaskList();
451
+ else drawMaskOne();
452
+ }).catch(() => {});
453
+ }
454
+
455
+ function drawMaskList() {
456
+ const box = $("#maskList");
457
+ if (!box) return;
458
+ const sum = (maskSumDays === maskDays && maskSum) || null;
459
+ maskTipOff();
460
+ box.replaceChildren();
461
+ const bar = el("div", "msk-bar");
462
+ bar.append(segs([["req", t("By request")], ["val", t("By value")]], maskL.mode, (v) => { maskL.mode = v; drawMaskList(); }));
463
+ const live = el("span", "msk-live");
464
+ live.title = t("Read again every 15 seconds while this page is open");
465
+ live.append(el("i"), el("span", "", t("Live")));
466
+ bar.append(live, el("span", "grow"));
467
+ if (maskL.mode === "req") {
468
+ const pick = (key, all, head, opts) => {
469
+ const b = el("button", "sess-pick");
470
+ b.type = "button";
471
+ if (maskL[key] && !opts.some((o) => o.v === maskL[key])) opts.unshift({ v: maskL[key], name: maskWho(maskL[key], key).name });
472
+ sessPick(b, all, maskL[key], opts, head, (v) => maskNarrow({ [key]: v || "" }));
473
+ bar.append(b);
474
+ };
475
+ pick("agent", "Every agent", "Agent", (sum?.agents || []).map((a) => ({ v: a.name, name: maskWho(a.name, "agent").name })));
476
+ pick("provider", "Every provider", "Provider", (sum?.providers || []).map((p) => ({ v: p.name, name: maskWho(p.name, "provider").name })));
477
+ }
478
+ box.append(bar);
479
+
480
+ // what was masked and how: counts that narrow the list
481
+ const facets = el("div", "msk-facets");
482
+ facets.setAttribute("role", "group");
483
+ facets.setAttribute("aria-label", t("Show"));
484
+ for (const [id, name, count] of MASK_FACETS) {
485
+ const n = sum ? count(sum) : 0;
486
+ if (id && !n && maskL.facet !== id) continue;
487
+ const b = el("button", "msk-facet" + (id ? " " + id : ""));
488
+ b.type = "button";
489
+ b.setAttribute("aria-pressed", String(maskL.facet === id && !maskL.kind));
490
+ if (id) b.append(el("span", "msk-fdot"));
491
+ b.append(el("span", "", t(name)));
492
+ if (sum) b.append(el("b", "", fmtN(n)));
493
+ b.onclick = () => maskNarrow({ facet: id, kind: "" });
494
+ facets.append(b);
495
+ }
496
+ if (maskL.kind) {
497
+ const b = el("button", "msk-facet kind");
498
+ b.type = "button";
499
+ b.setAttribute("aria-pressed", "true");
500
+ b.title = t("Clear filter");
501
+ b.append(maskDot(maskL.kind), el("span", "", maskKindName(maskL.kind)), el("span", "x", "×"));
502
+ b.onclick = () => maskNarrow({ kind: "" });
503
+ facets.append(b);
504
+ }
505
+ box.append(facets);
506
+ if (maskL.mode === "val") maskValueList(box, sum);
507
+ else maskRequestList(box);
508
+ const foot = el("div", "msk-foot");
509
+ foot.append(el("span", "", t("Kept on this computer for 30 days, without the values themselves")));
510
+ if (maskL.mode === "req" && maskL.more) {
511
+ const more = el("button", "text", maskL.busy ? t("Loading…") : t("Load more"));
512
+ more.type = "button";
513
+ more.disabled = maskL.busy;
514
+ more.onclick = () => maskFetch(true);
515
+ foot.append(more);
516
+ }
517
+ // what was kept goes whole, after a confirm: maxyou2090's strict masking
518
+ // had kept far more than he wanted to keep
519
+ if (maskL.records.length || maskSum?.requests) {
520
+ const clear = el("button", "text msk-clear", t("Clear records…"));
521
+ clear.type = "button";
522
+ clear.disabled = maskL.busy || maskClearing;
523
+ clear.onclick = maskClear;
524
+ foot.append(clear);
525
+ }
526
+ box.append(foot);
527
+ }
528
+ let maskClearing = false;
529
+ async function maskClear() {
530
+ const ok = await confirmAction(t("Clear masking records?"),
531
+ t("Every request's record of what was masked is deleted from this computer. Masking stays as it is, and requests from now on are recorded again."),
532
+ t("Clear"));
533
+ if (!ok) return;
534
+ maskClearing = true;
535
+ drawMaskList();
536
+ try {
537
+ await api("masking/clear", {});
538
+ status(t("Masking records cleared"), "ok", 1500);
539
+ } catch (e) {
540
+ // what could not be deleted is still listed below
541
+ status(t(e.message), "err");
542
+ }
543
+ maskClearing = false;
544
+ if (maskO) maskOneClose();
545
+ maskSeq++;
546
+ Object.assign(maskL, { records: [], more: false, busy: false, read: false, error: "" });
547
+ maskDrawn = "";
548
+ return maskPageLoad(true);
549
+ }
550
+
551
+ function maskRequestList(box) {
552
+ const filtered = maskL.facet || maskL.kind || maskL.agent || maskL.provider;
553
+ if (maskL.error) return void box.append(el("div", "msk-none err", t(maskL.error)));
554
+ if (!maskL.records.length) return void box.append(el("div", "msk-none", maskL.busy || !maskL.read ? t("Loading…") : filtered ? t("Nothing masked matches these filters") : t("Nothing masked yet")));
555
+ let day = "", list = null;
556
+ for (const r of maskL.records) {
557
+ const d = new Date(r.t).toDateString();
558
+ if (d !== day) {
559
+ day = d;
560
+ const g = el("div", "msk-group");
561
+ g.append(el("div", "msk-gday", maskDayName(r.t)));
562
+ list = el("div", "list");
563
+ g.append(list);
564
+ box.append(g);
565
+ }
566
+ list.append(maskRecordRow(r));
567
+ }
568
+ }
569
+ // who to whom, an icon where there is one
570
+ function maskRoute(into, r) {
571
+ const a = maskWho(r.agent, "agent"), p = maskWho(r.provider, "provider");
572
+ if (r.agent) {
573
+ if (a.icon) into.append(icon(a.icon));
574
+ into.append(el("span", "who", a.name), el("span", "arrow", "→"));
575
+ }
576
+ if (p.icon && r.provider) into.append(icon(p.icon));
577
+ into.append(el("span", "who", r.provider ? p.name : t("A vendor")));
578
+ }
579
+ function maskKindsOf(r) {
580
+ const by = {};
581
+ for (const x of r.values || []) by[x.k] = (by[x.k] || 0) + (x.n || 1);
582
+ return Object.entries(by).sort((x, y) => y[1] - x[1]);
583
+ }
584
+ function maskRecordRow(r) {
585
+ const row = el("button", "msk-row" + (r.blocked ? " held" : ""));
586
+ row.type = "button";
587
+ row.dataset.key = r.key;
588
+ const route = el("span", "msk-route");
589
+ maskRoute(route, r);
590
+ const model = el("span", "msk-model", r.model || "");
591
+ const kinds = el("span", "msk-kinds");
592
+ if (r.blocked) kinds.append(el("span", "msk-held", t("Held back")));
593
+ else if (r.what && r.what !== "chat") kinds.append(el("span", "msk-what", MASK_WHAT[r.what] ? t(MASK_WHAT[r.what]) : r.what));
594
+ const ranked = maskKindsOf(r);
595
+ for (const [k, n] of ranked.slice(0, 3)) {
596
+ const c = el("span", "msk-kind");
597
+ c.append(maskDot(k), el("span", "", maskKindName(k)), el("b", "", "×" + fmtN(n)));
598
+ kinds.append(c);
599
+ }
600
+ if (ranked.length > 3) kinds.append(el("span", "msk-kind more", "+" + (ranked.length - 3)));
601
+ const n = el("span", "msk-n", fmtN(r.n));
602
+ n.title = t(r.n === 1 ? "1 value masked" : "{n} values masked", { n: fmtN(r.n) });
603
+ const chev = el("span", "msk-chev");
604
+ chev.append(svg(CHEV_R, 11, 1.6));
605
+ row.append(el("span", "msk-time", new Date(r.t).toLocaleTimeString(intlLang(), { hour: "2-digit", minute: "2-digit" })), route, model, kinds, n, chev);
606
+ row.onclick = () => maskOneOpen(r.key, r);
607
+ return row;
608
+ }
609
+
610
+ // the values masked most, as the summary ranks them: what a facet can
611
+ // narrow them by; one opens the requests with its kind
612
+ function maskValueList(box, sum) {
613
+ const f = maskL.facet;
614
+ const vs = (sum?.values || []).filter((v) => (!maskL.kind || v.kind === maskL.kind) && (f !== "secret" || MASK_SECRET_KINDS.includes(v.kind)) && (f !== "rule" && f !== "model" || v.by === f) && f !== "blocked");
615
+ if (!sum) return void box.append(el("div", "msk-none", t("Loading…")));
616
+ if (!vs.length) return void box.append(el("div", "msk-none", sum.values?.length ? t("Nothing masked matches these filters") : t("Nothing masked yet")));
617
+ const head = el("div", "msk-vhead");
618
+ head.append(el("span", "", t("Masked most")), el("span", "grow"), el("span", "", t("Point at one to see what it was")));
619
+ const list = el("div", "list msk-vals"), max = vs[0].n || 1;
620
+ for (const [i, v] of vs.entries()) {
621
+ const c = el("button", "msk-vrow"), sh = maskShape(v.head, v.len), bar = el("span", "bar"), fill = el("i");
622
+ c.type = "button";
623
+ c.setAttribute("aria-label", maskKindName(v.kind) + " " + sh);
624
+ fill.style.width = Math.max(3, (100 * v.n) / max) + "%";
625
+ bar.append(fill);
626
+ const kind = el("span", "k");
627
+ kind.append(maskDot(v.kind), el("span", "", maskKindName(v.kind)));
628
+ c.append(el("span", "i", String(i + 1).padStart(2, "0")), el("span", "sh", sh), kind, el("span", "by", maskByName(v.by)), bar, el("span", "x", fmtN(v.n)));
629
+ maskTipOn(c, { p: v.p, kind: v.kind, by: v.by, meta: t(v.requests === 1 ? "In 1 request, {when}" : "In {n} requests, most recently {when}", { n: fmtN(v.requests), when: ago(v.last) }) });
630
+ c.onclick = () => { maskL.mode = "req"; maskNarrow({ kind: v.kind, facet: "" }); };
631
+ list.append(c);
632
+ }
633
+ box.append(head, list);
634
+ }
635
+
636
+ // ---- one request: what it carried, what the vendor got, how ----
637
+ function maskOneOpen(key, r) {
638
+ if (!key) return;
639
+ maskTipOff();
640
+ const o = maskO = { key, r: r || maskL.records.find((x) => x.key === key) || null, sent: undefined, leaves: null, vals: new Map(), view: "both", pick: "", open: new Set(), whole: new Set() };
641
+ $("#maskCard").hidden = $("#maskList").hidden = $("#maskDemo").hidden = $("#maskPrefs").hidden = true;
642
+ xqd?.wake();
643
+ $("#maskOne").hidden = false;
644
+ drawMaskOne();
645
+ (o.r ? Promise.resolve(o.r) : api("masking/record?key=" + key))
646
+ .then((rec) => {
647
+ if (maskO !== o) return;
648
+ o.r = rec;
649
+ drawMaskOne();
650
+ const ps = (rec.values || []).map((x) => x.p);
651
+ const vals = ps.length ? api("masking/values", { ps }).then((x) => {
652
+ for (const [p, v] of Object.entries(x?.values || {})) { o.vals.set(p, v); maskValues.set(p, v); }
653
+ }).catch(() => {}) : null;
654
+ const sent = api("masking/sent?key=" + key).then((x) => (typeof x?.body === "string" ? x.body : null), () => null);
655
+ return Promise.all([sent, vals]).then(([body]) => {
656
+ if (maskO !== o) return;
657
+ o.sent = body;
658
+ o.leaves = body === null ? null : maskLeaves(body);
659
+ if (body === null && !o.vals.size) o.view = "vendor";
660
+ drawMaskOne();
661
+ });
662
+ })
663
+ .catch(() => { if (maskO === o) { o.r = false; drawMaskOne(); } });
664
+ }
665
+ function maskOneClose() {
666
+ maskTipOff();
667
+ maskO = null;
668
+ $("#maskOne").hidden = true;
669
+ $("#maskOne").replaceChildren();
670
+ $("#maskCard").hidden = $("#maskList").hidden = $("#maskDemo").hidden = false;
671
+ $("#maskPrefs").hidden = gatewayMode;
672
+ xqd?.wake();
673
+ drawMaskList();
674
+ }
675
+
676
+ // maskLeaves: each piece of text in the body a request went out with,
677
+ // where it sits and whose it is (the message's role, else its type)
678
+ function maskLeaves(body) {
679
+ let root;
680
+ try { root = JSON.parse(body); } catch { return [{ path: "", role: "", text: body }]; }
681
+ const out = [];
682
+ const walk = (v, path, role) => {
683
+ if (typeof v === "string") return void out.push({ path, role, text: v });
684
+ if (!v || typeof v !== "object") return;
685
+ if (Array.isArray(v)) return void v.forEach((x, i) => walk(x, path + "[" + i + "]", role));
686
+ const r = typeof v.role === "string" ? v.role : role || (typeof v.type === "string" ? v.type : "");
687
+ for (const [k, x] of Object.entries(v)) walk(x, path ? path + "." + k : k, r);
688
+ };
689
+ walk(root, "", "");
690
+ return out;
691
+ }
692
+ const maskTook = (us) => us >= 1e6 ? (us / 1e6).toFixed(1) + " s" : us >= 1000 ? (us / 1000).toFixed(us >= 1e4 ? 0 : 1) + " ms" : Math.max(1, Math.round(us)) + " µs";
693
+
694
+ function drawMaskOne() {
695
+ const box = $("#maskOne"), o = maskO;
696
+ if (!box || !o) return;
697
+ maskTipOff();
698
+ box.replaceChildren();
699
+ const r = o.r;
700
+ // back, when, and the requests beside it in the list
701
+ const bar = el("div", "msk-onebar");
702
+ const back = el("button", "text msk-back");
703
+ back.type = "button";
704
+ back.append(svg("m9.5 3.5-4.5 4.5 4.5 4.5", 12, 1.7), el("span", "", t("Masking")));
705
+ back.onclick = maskOneClose;
706
+ bar.append(back);
707
+ if (r) bar.append(el("span", "msk-crumb", "/"), el("span", "msk-when", maskDayName(r.t) + " " + new Date(r.t).toLocaleTimeString(intlLang(), { hour: "2-digit", minute: "2-digit", second: "2-digit" })));
708
+ bar.append(el("span", "grow"));
709
+ const at = maskL.records.findIndex((x) => x.key === o.key);
710
+ const step = (d, label, path) => {
711
+ const b = el("button", "icon msk-step");
712
+ b.type = "button";
713
+ b.setAttribute("aria-label", t(label));
714
+ b.dataset.tt = "";
715
+ b.append(svg(path, 12, 1.7));
716
+ const to = at < 0 ? null : maskL.records[at + d];
717
+ b.disabled = !to;
718
+ if (to) b.onclick = () => maskOneOpen(to.key, to);
719
+ bar.append(b);
720
+ };
721
+ step(-1, "Newer request", "M3.5 9.5 8 5l4.5 4.5");
722
+ step(1, "Older request", "M3.5 6.5 8 11l4.5-4.5");
723
+ box.append(bar);
724
+ if (r === false) return void box.append(el("div", "msk-none", t("This request's record is gone: records are kept for 30 days")));
725
+ if (!r) return void box.append(el("div", "msk-none", t("Loading…")));
726
+
727
+ const info = new Map((r.values || []).map((x) => [x.p, x]));
728
+ // the request: who to whom, and what happened to it in order
729
+ const card = el("section", "msk-req");
730
+ const who = el("div", "msk-who");
731
+ maskRoute(who, r);
732
+ if (r.model) who.append(el("span", "msk-chip", r.model));
733
+ if (r.what && r.what !== "chat") who.append(el("span", "msk-chip", MASK_WHAT[r.what] ? t(MASK_WHAT[r.what]) : r.what));
734
+ card.append(who, maskSteps(r, o));
735
+ box.append(card);
736
+ if (r.blocked) box.append(maskBlocked(r));
737
+
738
+ const grid = el("div", "msk-onegrid");
739
+ grid.append(maskContent(r, o, info), maskHow(r, o, info));
740
+ box.append(grid);
741
+ maskPick(box, o.pick);
742
+ }
743
+
744
+ // maskSteps: received, found, replaced, sent (or held back), put back
745
+ function maskSteps(r, o) {
746
+ const ol = el("ol", "msk-steps");
747
+ ol.setAttribute("aria-label", t("What happened"));
748
+ const memory = o.sent || o.vals.size || Object.keys(r.snips || {}).length;
749
+ const back = Object.values(r.back || {}).reduce((a, n) => a + n, 0);
750
+ const by = [...new Set((r.values || []).map((x) => maskByName(x.b)))].join(", ");
751
+ const a = maskWho(r.agent, "agent").name, p = r.provider ? maskWho(r.provider, "provider").name : t("A vendor");
752
+ const steps = [
753
+ ["", t("Received"), r.agent ? t("From {agent}", { agent: a }) + (o.sent ? " · " + fmtBytes(new Blob([o.sent]).size) : "") : o.sent ? fmtBytes(new Blob([o.sent]).size) : ""],
754
+ ["", t((r.values || []).length === 1 ? "Found 1 value" : "Found {n} values", { n: fmtN((r.values || []).length) }), by + (r.took ? " · " + maskTook(r.took) : "")],
755
+ ["", t(r.n === 1 ? "Replaced 1 place" : "Replaced {n} places", { n: fmtN(r.n) }), t("With placeholders")],
756
+ r.blocked ? ["held", t("Held back"), t("Not sent to {provider}", { provider: p })] : ["", t("Sent"), p + (r.model ? " · " + r.model : "")],
757
+ r.blocked ? ["none", t("Put back"), t("Nothing was sent")]
758
+ : back ? ["", t(back === 1 ? "Put back 1" : "Put back {n}", { n: fmtN(back) }), t("In the answer, before {agent} got it", { agent: a })]
759
+ : memory ? ["none", t("Nothing to put back"), t("The answer named no placeholder")]
760
+ : ["none", t("Put back"), t("Not known after a restart")],
761
+ ];
762
+ steps.forEach(([cls, title, sub], i) => {
763
+ const li = el("li", cls);
764
+ const mk = el("div", "msk-sdot");
765
+ mk.append(el("span", "", String(i + 1)));
766
+ li.append(mk, el("div", "msk-st", title), el("div", "msk-ss", sub));
767
+ ol.append(li);
768
+ });
769
+ return ol;
770
+ }
771
+
772
+ // a request held back: why, and the way to send it masked instead
773
+ function maskBlocked(r) {
774
+ const box = el("section", "msk-blocked");
775
+ const head = el("div", "msk-bhead"), ic = svg(SHIELD, 20, 1.3);
776
+ ic.append(sv("path", { d: "m6 6 4 4M10 6l-4 4" }));
777
+ head.append(ic, el("h2", "", t("This request was not sent")));
778
+ const kinds = [...new Set((r.values || []).filter((x) => x.a === "block").map((x) => x.k))];
779
+ const a = maskWho(r.agent, "agent").name, p = r.provider ? maskWho(r.provider, "provider").name : t("A vendor");
780
+ box.append(head, el("p", "", t("It carried {kinds}, which you set to hold back, so magpie didn't send it to {provider}. {agent} got an error saying why.", { kinds: kinds.map(maskKindName).join(", ") || t("a value"), provider: p, agent: r.agent ? a : t("The agent") })));
781
+ const tools = el("div", "msk-btools");
782
+ if (kinds.length) {
783
+ const b = el("button", "text primary", t("Mask it instead"));
784
+ b.type = "button";
785
+ b.title = t("From now on {kinds} go to vendors as placeholders, like the other values", { kinds: kinds.map(maskKindName).join(", ") });
786
+ b.onclick = () => {
787
+ b.disabled = true;
788
+ (prefs ? Promise.resolve() : loadSettings())
789
+ .then(() => writingPrefs(api("settings/redact-actions", { actions: { ...(prefs.redactActions || {}), ...Object.fromEntries(kinds.map((k) => [k, "mask"])) } })))
790
+ .then((ns) => { prefs = ns; if (state) state.settings = ns; b.textContent = t("Masked from now on"); status(t("Saved"), "ok", 1500); })
791
+ .catch((e) => { b.disabled = false; status(t(e.message), "err"); });
792
+ };
793
+ tools.append(b);
794
+ }
795
+ const rules = el("button", "text", t("See the rules in Settings"));
796
+ rules.type = "button";
797
+ rules.onclick = (e) => openSettingsAt(e, "privacy", $("#maskActList"));
798
+ tools.append(rules);
799
+ box.append(tools);
800
+ return box;
801
+ }
802
+
803
+ // maskInk: text with this request's placeholders marked, as the vendor
804
+ // got them or, here, as they were written
805
+ function maskInk(into, text, here, o, info) {
806
+ let at = 0;
807
+ for (const m of maskMatches(text, info)) {
808
+ const x = info.get(m[0]);
809
+ if (!x) continue;
810
+ into.append(document.createTextNode(text.slice(at, m.index)));
811
+ let mark;
812
+ if (here) {
813
+ const v = o.vals.get(m[0]);
814
+ mark = el("mark", "msk-orig" + (typeof v === "string" ? "" : " gone"), typeof v === "string" ? v : maskShape(x.h, x.l));
815
+ mark.style.setProperty("--k", maskColor(x.k));
816
+ mark.tabIndex = 0;
817
+ maskTipOn(mark, { p: m[0], kind: x.k, by: x.b });
818
+ } else mark = maskPlace("msk-ph", m[0], x);
819
+ mark.dataset.p = m[0];
820
+ into.append(mark);
821
+ at = m.index + m[0].length;
822
+ }
823
+ into.append(document.createTextNode(text.slice(at)));
824
+ }
825
+ // the parts of a long text around its placeholders, 120 characters each
826
+ // side, the rest left out
827
+ function maskWindows(text, info) {
828
+ const rs = [];
829
+ for (const m of maskMatches(text, info)) if (info.has(m[0])) rs.push([Math.max(0, m.index - 120), Math.min(text.length, m.index + m[0].length + 120)]);
830
+ const out = [];
831
+ for (const r of rs) {
832
+ const last = out.at(-1);
833
+ if (last && r[0] <= last[1] + 20) last[1] = Math.max(last[1], r[1]);
834
+ else out.push([...r]);
835
+ }
836
+ return out;
837
+ }
838
+
839
+ // maskContent: the request's text, only the pieces masking changed, the
840
+ // rest folded; as written, as the vendor got it, or side by side
841
+ function maskContent(r, o, info) {
842
+ const sec = el("section", "msk-content");
843
+ const head = el("div", "msk-chead");
844
+ head.append(el("h2", "", t("Request content")), el("span", "msk-hint", t("Only the parts masking changed")), el("span", "grow"));
845
+ const known = o.sent || o.vals.size;
846
+ const views = [["here", t("Original")], ["vendor", t("What the vendor saw")], ["both", t("Side by side")]];
847
+ const sg = segs(views, o.view, (v) => { o.view = v; drawMaskOne(); });
848
+ if (!known && o.sent !== undefined) for (const b of sg.querySelectorAll("button")) if (!b.getAttribute("aria-pressed") || b.getAttribute("aria-pressed") === "false") b.disabled = true;
849
+ head.append(sg);
850
+ sec.append(head);
851
+ const view = known || o.sent === undefined ? o.view : "vendor";
852
+ if (o.sent === undefined) return sec.append(el("div", "msk-none", t("Loading…"))), sec;
853
+
854
+ let items;
855
+ if (o.leaves) {
856
+ items = [];
857
+ let fold = null;
858
+ for (const l of o.leaves) {
859
+ const changed = [...maskMatches(l.text, info)].some((m) => info.has(m[0]));
860
+ if (changed) { fold = null; items.push({ leaf: l }); continue; }
861
+ if (!fold) items.push(fold = { fold: [] });
862
+ fold.fold.push(l);
863
+ }
864
+ } else {
865
+ // only the lines around each value are left, while memory has the
866
+ // record: the request's whole text was in memory, and is no longer
867
+ items = maskSnipTexts(r).map((text) => ({ leaf: { path: "", role: "", text } }));
868
+ if (items.length && !r.blocked) sec.append(el("p", "msk-gone", o.vals.size
869
+ ? t("This request's whole text is no longer in memory: magpie keeps it for the newest requests only. The lines around each value are still here.")
870
+ : t("What was written is kept in memory only, and magpie has restarted since. Still here: each value's kind, length and first characters, and the line around each placeholder as the vendor got it.")));
871
+ // after a restart nothing of the text is left, by design: the record on
872
+ // disk never holds it (maskrec), so no folder or volume brings it back
873
+ // (brianeee2 on X, in Docker). A request held back sent nothing, and
874
+ // its card says so.
875
+ else if (!items.length && !r.blocked) sec.append(el("p", "msk-gone", t("This request's text and the values masked in it were kept in memory only, never on disk, and magpie has restarted since. The record keeps each value's kind, length and first characters.")));
876
+ }
877
+ const list = el("div", "msk-msgs" + (view === "both" ? " both" : ""));
878
+ if (view === "both") {
879
+ const cols = el("div", "msk-cols");
880
+ cols.append(el("span", "", t("As written here")), el("span", "", t("What the vendor saw")));
881
+ list.append(cols);
882
+ }
883
+ items.forEach((it, i) => {
884
+ if (it.fold) {
885
+ const size = it.fold.reduce((a, l) => a + l.text.length, 0);
886
+ const f = el("div", "msk-fold" + (o.open.has(i) ? " open" : ""));
887
+ const b = el("button", "msk-foldb");
888
+ b.type = "button";
889
+ b.setAttribute("aria-expanded", String(o.open.has(i)));
890
+ b.append(svg(CHEV_R, 10, 1.8), el("span", "", t(it.fold.length === 1 ? "1 part unchanged" : "{n} parts unchanged", { n: fmtN(it.fold.length) })), el("span", "msk-size", fmtBytes(size)));
891
+ b.onclick = () => { if (o.open.has(i)) o.open.delete(i); else o.open.add(i); drawMaskOne(); };
892
+ f.append(b);
893
+ if (o.open.has(i)) {
894
+ const rest = el("div", "msk-same");
895
+ for (const l of it.fold.slice(0, 40)) {
896
+ const p = el("div", "msk-sameline");
897
+ p.append(el("span", "msk-path", l.path), el("span", "", l.text.length > 400 ? l.text.slice(0, 400) + "…" : l.text));
898
+ rest.append(p);
899
+ }
900
+ if (it.fold.length > 40) rest.append(el("div", "msk-path", t("{n} more", { n: fmtN(it.fold.length - 40) })));
901
+ f.append(rest);
902
+ }
903
+ return list.append(f);
904
+ }
905
+ const l = it.leaf, m = el("div", "msk-msg");
906
+ const n = [...maskMatches(l.text, info)].filter((x) => info.has(x[0])).length;
907
+ const mh = el("div", "msk-mhead");
908
+ if (l.role) mh.append(el("span", "msk-role", l.role));
909
+ if (l.path) mh.append(el("span", "msk-path", l.path));
910
+ mh.append(el("span", "grow"), el("span", "msk-count", t(n === 1 ? "1 value" : "{n} values", { n: fmtN(n) })));
911
+ m.append(mh);
912
+ const whole = o.whole.has(i), wins = whole || l.text.length <= 600 ? [[0, l.text.length]] : maskWindows(l.text, info);
913
+ const col = (here) => {
914
+ const c = el("div", "msk-text");
915
+ wins.forEach(([a, b], j) => {
916
+ if (j || a) c.append(el("span", "msk-gap", "…"));
917
+ maskInk(c, l.text.slice(a, b), here, o, info);
918
+ });
919
+ if (wins.at(-1)[1] < l.text.length) c.append(el("span", "msk-gap", "…"));
920
+ return c;
921
+ };
922
+ const body = el("div", "msk-mbody");
923
+ if (view === "both") body.append(col(true), col(false));
924
+ else body.append(col(view === "here"));
925
+ m.append(body);
926
+ if (l.text.length > 600) {
927
+ const b = el("button", "text msk-whole", whole ? t("Show only the changes") : t("Show all of it"));
928
+ b.type = "button";
929
+ b.onclick = () => { if (whole) o.whole.delete(i); else o.whole.add(i); drawMaskOne(); };
930
+ m.append(b);
931
+ }
932
+ list.append(m);
933
+ });
934
+ sec.append(list);
935
+ return sec;
936
+ }
937
+
938
+ // maskHow: each value, what it became, who found it, where it was, and
939
+ // how often the answer had it put back; picked, its places light up
940
+ function maskHow(r, o, info) {
941
+ const aside = el("aside", "msk-how");
942
+ const vs = r.values || [];
943
+ const head = el("div", "msk-chead");
944
+ head.append(el("h2", "", t("How it was masked")), el("span", "msk-hint", t(vs.length === 1 ? "1 value" : "{n} values", { n: fmtN(vs.length) }) + " · " + t(r.n === 1 ? "1 place" : "{n} places", { n: fmtN(r.n) })));
945
+ aside.append(head);
946
+ const where = new Map();
947
+ for (const l of o.leaves || []) for (const m of maskMatches(l.text, info)) if (info.has(m[0]) && l.path) {
948
+ const w = where.get(m[0]) || new Set();
949
+ w.add(l.path);
950
+ where.set(m[0], w);
951
+ }
952
+ const memory = o.sent || o.vals.size;
953
+ for (const x of vs) {
954
+ const b = el("button", "msk-val");
955
+ b.type = "button";
956
+ b.dataset.p = x.p;
957
+ b.style.setProperty("--k", maskColor(x.k));
958
+ b.setAttribute("aria-pressed", String(o.pick === x.p));
959
+ const top = el("div", "msk-vtop");
960
+ top.append(maskDot(x.k), el("span", "msk-vk", maskKindName(x.k)), el("span", "grow"));
961
+ if (x.a === "warn") top.append(el("span", "mr-act warn", t("Sent as written")));
962
+ else if (x.a === "block") top.append(el("span", "mr-act block", t("Held back")));
963
+ top.append(el("span", "msk-vx", "×" + fmtN(x.n || 1)));
964
+ const v = o.vals.get(x.p), shown = o.pick === x.p && typeof v === "string" ? v : maskShape(x.h, x.l);
965
+ const mid = el("div", "msk-vmid");
966
+ mid.append(el("span", "msk-vshown", shown), svg("M1 8h11M9 5l3 3-3 3", 12, 1.3), el("span", "msk-vph", x.a === "warn" ? t("as written") : x.p));
967
+ const meta = el("dl", "msk-vmeta");
968
+ const add = (k, val) => meta.append(el("dt", "", k), el("dd", "", val));
969
+ add(t("Found by"), maskByName(x.b));
970
+ const w = [...(where.get(x.p) || [])];
971
+ if (w.length) add(t("Where"), w[0] + (w.length > 1 ? " +" + (w.length - 1) : ""));
972
+ const n = r.back?.[x.p] || 0;
973
+ add(t("In the answer"), n ? t(n === 1 ? "Put back 1" : "Put back {n}", { n: fmtN(n) }) : r.blocked ? "—" : memory || r.back ? t("Not named") : t("Not known after a restart"));
974
+ b.append(top, mid, meta);
975
+ b.onclick = () => { o.pick = o.pick === x.p ? "" : x.p; drawMaskOne(); };
976
+ aside.append(b);
977
+ }
978
+ aside.append(el("p", "msk-note", t("A value itself is in this computer's memory only: shown while it is pointed at or picked, and gone once magpie restarts. The record on disk keeps its kind, length and first characters.")));
979
+ return aside;
980
+ }
981
+ // a value picked lights its places; the rest fade
982
+ function maskPick(box, p) {
983
+ box.classList.toggle("picking", !!p);
984
+ for (const m of box.querySelectorAll("mark[data-p]")) m.classList.toggle("lit", m.dataset.p === p);
985
+ }