dsh-mobile-plugin 0.5.28 → 0.5.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/extensions/catalog.json +10 -1
  2. package/extensions/magpie/agentcompact.js +104 -0
  3. package/extensions/magpie/app.css +4902 -0
  4. package/extensions/magpie/app.js +24474 -0
  5. package/extensions/magpie/autobackup.js +185 -0
  6. package/extensions/magpie/autoresetown.js +151 -0
  7. package/extensions/magpie/balfound.js +38 -0
  8. package/extensions/magpie/boot.js +23 -0
  9. package/extensions/magpie/cfgblocked.js +39 -0
  10. package/extensions/magpie/checkinnow.js +111 -0
  11. package/extensions/magpie/claudemode.js +92 -0
  12. package/extensions/magpie/compat.js +41 -0
  13. package/extensions/magpie/context.css +433 -0
  14. package/extensions/magpie/context.js +1576 -0
  15. package/extensions/magpie/ctxtext.js +223 -0
  16. package/extensions/magpie/fonts.js +42 -0
  17. package/extensions/magpie/i18n.js +18942 -0
  18. package/extensions/magpie/icons/ai21.svg +1 -0
  19. package/extensions/magpie/icons/ai302-color.svg +1 -0
  20. package/extensions/magpie/icons/ai360-color.svg +1 -0
  21. package/extensions/magpie/icons/aihubmix-color.svg +1 -0
  22. package/extensions/magpie/icons/aionui.svg +1 -0
  23. package/extensions/magpie/icons/air.svg +4 -0
  24. package/extensions/magpie/icons/alibaba-color.svg +1 -0
  25. package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
  26. package/extensions/magpie/icons/alma.png +0 -0
  27. package/extensions/magpie/icons/anthropic.svg +1 -0
  28. package/extensions/magpie/icons/antigravity-color.svg +1 -0
  29. package/extensions/magpie/icons/anyscale-color.svg +1 -0
  30. package/extensions/magpie/icons/app-alma.png +0 -0
  31. package/extensions/magpie/icons/app-cc-switch.png +0 -0
  32. package/extensions/magpie/icons/apple-touch-icon.png +0 -0
  33. package/extensions/magpie/icons/aside.svg +28 -0
  34. package/extensions/magpie/icons/astrbot.svg +1 -0
  35. package/extensions/magpie/icons/atomcode.png +0 -0
  36. package/extensions/magpie/icons/aws.svg +1 -0
  37. package/extensions/magpie/icons/azure-color.svg +1 -0
  38. package/extensions/magpie/icons/azureai-color.svg +1 -0
  39. package/extensions/magpie/icons/baichuan-color.svg +1 -0
  40. package/extensions/magpie/icons/baidu-color.svg +1 -0
  41. package/extensions/magpie/icons/baiducloud-color.svg +1 -0
  42. package/extensions/magpie/icons/bailian-color.svg +1 -0
  43. package/extensions/magpie/icons/baseten.svg +1 -0
  44. package/extensions/magpie/icons/bedrock-color.svg +1 -0
  45. package/extensions/magpie/icons/burncloud-color.svg +1 -0
  46. package/extensions/magpie/icons/bytedance-color.svg +1 -0
  47. package/extensions/magpie/icons/cerebras.svg +1 -0
  48. package/extensions/magpie/icons/chatglm-color.svg +1 -0
  49. package/extensions/magpie/icons/cherryin-color.svg +1 -0
  50. package/extensions/magpie/icons/chutes.svg +1 -0
  51. package/extensions/magpie/icons/cindy.png +0 -0
  52. package/extensions/magpie/icons/claude-color.svg +1 -0
  53. package/extensions/magpie/icons/claudecode-color.svg +1 -0
  54. package/extensions/magpie/icons/cline.svg +1 -0
  55. package/extensions/magpie/icons/cloudflare-color.svg +1 -0
  56. package/extensions/magpie/icons/codebuddy-color.svg +1 -0
  57. package/extensions/magpie/icons/codex-color.svg +1 -0
  58. package/extensions/magpie/icons/cohere-color.svg +1 -0
  59. package/extensions/magpie/icons/cometapi-color.svg +1 -0
  60. package/extensions/magpie/icons/commandcode.svg +1 -0
  61. package/extensions/magpie/icons/copilot-color.svg +1 -0
  62. package/extensions/magpie/icons/coze.svg +1 -0
  63. package/extensions/magpie/icons/crush.png +0 -0
  64. package/extensions/magpie/icons/curl.svg +1 -0
  65. package/extensions/magpie/icons/cursor.svg +1 -0
  66. package/extensions/magpie/icons/deepinfra-color.svg +1 -0
  67. package/extensions/magpie/icons/deepmind-color.svg +1 -0
  68. package/extensions/magpie/icons/deepseek-color.svg +1 -0
  69. package/extensions/magpie/icons/deveco-color.svg +12 -0
  70. package/extensions/magpie/icons/devin.svg +1 -0
  71. package/extensions/magpie/icons/doubao-color.svg +1 -0
  72. package/extensions/magpie/icons/empryo-color.svg +1 -0
  73. package/extensions/magpie/icons/factory.svg +1 -0
  74. package/extensions/magpie/icons/favicon.svg +1 -0
  75. package/extensions/magpie/icons/featherless-color.svg +1 -0
  76. package/extensions/magpie/icons/fireworks-color.svg +1 -0
  77. package/extensions/magpie/icons/friendli.svg +1 -0
  78. package/extensions/magpie/icons/fx.svg +1 -0
  79. package/extensions/magpie/icons/gemini-color.svg +1 -0
  80. package/extensions/magpie/icons/geminicli-color.svg +1 -0
  81. package/extensions/magpie/icons/gemma-color.svg +1 -0
  82. package/extensions/magpie/icons/giteeai.svg +1 -0
  83. package/extensions/magpie/icons/github.svg +1 -0
  84. package/extensions/magpie/icons/githubcopilot.svg +1 -0
  85. package/extensions/magpie/icons/glama.svg +1 -0
  86. package/extensions/magpie/icons/gmicloud.svg +1 -0
  87. package/extensions/magpie/icons/google-color.svg +1 -0
  88. package/extensions/magpie/icons/googlecloud-color.svg +1 -0
  89. package/extensions/magpie/icons/goose.svg +1 -0
  90. package/extensions/magpie/icons/grok.svg +1 -0
  91. package/extensions/magpie/icons/groq.svg +1 -0
  92. package/extensions/magpie/icons/hanako.png +0 -0
  93. package/extensions/magpie/icons/hermes.svg +1 -0
  94. package/extensions/magpie/icons/huawei-color.svg +1 -0
  95. package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
  96. package/extensions/magpie/icons/huggingface-color.svg +1 -0
  97. package/extensions/magpie/icons/hunyuan-color.svg +1 -0
  98. package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
  99. package/extensions/magpie/icons/icon-512.png +0 -0
  100. package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
  101. package/extensions/magpie/icons/inference.svg +1 -0
  102. package/extensions/magpie/icons/infinigence-color.svg +1 -0
  103. package/extensions/magpie/icons/internlm-color.svg +1 -0
  104. package/extensions/magpie/icons/jcode.svg +1 -0
  105. package/extensions/magpie/icons/jina.svg +1 -0
  106. package/extensions/magpie/icons/kilo.svg +1 -0
  107. package/extensions/magpie/icons/kilocode.svg +1 -0
  108. package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
  109. package/extensions/magpie/icons/kimi.svg +1 -0
  110. package/extensions/magpie/icons/kiro-color.svg +1 -0
  111. package/extensions/magpie/icons/kling-color.svg +1 -0
  112. package/extensions/magpie/icons/lambda.svg +1 -0
  113. package/extensions/magpie/icons/leptonai-color.svg +1 -0
  114. package/extensions/magpie/icons/liquid.svg +1 -0
  115. package/extensions/magpie/icons/liveagent-color.svg +1 -0
  116. package/extensions/magpie/icons/llmapi.svg +1 -0
  117. package/extensions/magpie/icons/lmstudio.svg +1 -0
  118. package/extensions/magpie/icons/lody.png +0 -0
  119. package/extensions/magpie/icons/longcat.svg +1 -0
  120. package/extensions/magpie/icons/magpie.svg +1 -0
  121. package/extensions/magpie/icons/meta-color.svg +1 -0
  122. package/extensions/magpie/icons/metaai-color.svg +1 -0
  123. package/extensions/magpie/icons/microsoft-color.svg +1 -0
  124. package/extensions/magpie/icons/mimocode.svg +1 -0
  125. package/extensions/magpie/icons/minimax-color.svg +1 -0
  126. package/extensions/magpie/icons/mistermorph-color.svg +1 -0
  127. package/extensions/magpie/icons/mistral-color.svg +1 -0
  128. package/extensions/magpie/icons/mlx-serve.png +0 -0
  129. package/extensions/magpie/icons/modelscope-color.svg +1 -0
  130. package/extensions/magpie/icons/monica-color.svg +1 -0
  131. package/extensions/magpie/icons/moonshot.svg +1 -0
  132. package/extensions/magpie/icons/morph-color.svg +1 -0
  133. package/extensions/magpie/icons/mothx.png +0 -0
  134. package/extensions/magpie/icons/nebius.svg +1 -0
  135. package/extensions/magpie/icons/newapi-color.svg +1 -0
  136. package/extensions/magpie/icons/nousresearch.svg +1 -0
  137. package/extensions/magpie/icons/nova-color.svg +1 -0
  138. package/extensions/magpie/icons/novita-color.svg +1 -0
  139. package/extensions/magpie/icons/nvidia-color.svg +1 -0
  140. package/extensions/magpie/icons/ollama.svg +1 -0
  141. package/extensions/magpie/icons/omlx.svg +1 -0
  142. package/extensions/magpie/icons/omo.svg +1 -0
  143. package/extensions/magpie/icons/omp.svg +1 -0
  144. package/extensions/magpie/icons/openai.svg +1 -0
  145. package/extensions/magpie/icons/openchamber.svg +11 -0
  146. package/extensions/magpie/icons/openclaw.svg +1 -0
  147. package/extensions/magpie/icons/opencode.svg +1 -0
  148. package/extensions/magpie/icons/openmausbot.svg +1 -0
  149. package/extensions/magpie/icons/openrouter.svg +1 -0
  150. package/extensions/magpie/icons/openwebui.svg +1 -0
  151. package/extensions/magpie/icons/parasail.svg +1 -0
  152. package/extensions/magpie/icons/penguin-color.svg +1 -0
  153. package/extensions/magpie/icons/perplexity-color.svg +1 -0
  154. package/extensions/magpie/icons/phala-color.svg +1 -0
  155. package/extensions/magpie/icons/pi.svg +1 -0
  156. package/extensions/magpie/icons/pipellm-color.svg +5 -0
  157. package/extensions/magpie/icons/poe.svg +1 -0
  158. package/extensions/magpie/icons/ppio-color.svg +1 -0
  159. package/extensions/magpie/icons/prime-agent.svg +1 -0
  160. package/extensions/magpie/icons/qiniu-color.svg +1 -0
  161. package/extensions/magpie/icons/qoder.svg +1 -0
  162. package/extensions/magpie/icons/qwen-color.svg +1 -0
  163. package/extensions/magpie/icons/qwenpaw.svg +1 -0
  164. package/extensions/magpie/icons/raycast.svg +1 -0
  165. package/extensions/magpie/icons/reasonix-color.svg +48 -0
  166. package/extensions/magpie/icons/reka.svg +1 -0
  167. package/extensions/magpie/icons/replicate.svg +1 -0
  168. package/extensions/magpie/icons/roocode.svg +1 -0
  169. package/extensions/magpie/icons/rtk.png +0 -0
  170. package/extensions/magpie/icons/sambanova-color.svg +1 -0
  171. package/extensions/magpie/icons/sensenova-color.svg +1 -0
  172. package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
  173. package/extensions/magpie/icons/snow.png +0 -0
  174. package/extensions/magpie/icons/snowflake-color.svg +1 -0
  175. package/extensions/magpie/icons/sophnet-color.svg +1 -0
  176. package/extensions/magpie/icons/spark-color.svg +1 -0
  177. package/extensions/magpie/icons/stepfun-color.svg +1 -0
  178. package/extensions/magpie/icons/straico-color.svg +1 -0
  179. package/extensions/magpie/icons/submodel-color.svg +1 -0
  180. package/extensions/magpie/icons/t3code.svg +4 -0
  181. package/extensions/magpie/icons/targon-color.svg +1 -0
  182. package/extensions/magpie/icons/tencent-color.svg +1 -0
  183. package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
  184. package/extensions/magpie/icons/tiangong-color.svg +1 -0
  185. package/extensions/magpie/icons/together-color.svg +1 -0
  186. package/extensions/magpie/icons/trae.svg +1 -0
  187. package/extensions/magpie/icons/typesafe.png +0 -0
  188. package/extensions/magpie/icons/upstage-color.svg +1 -0
  189. package/extensions/magpie/icons/venice-color.svg +1 -0
  190. package/extensions/magpie/icons/vercel.svg +1 -0
  191. package/extensions/magpie/icons/vertexai-color.svg +1 -0
  192. package/extensions/magpie/icons/vllm-color.svg +1 -0
  193. package/extensions/magpie/icons/volcengine-color.svg +1 -0
  194. package/extensions/magpie/icons/voyage-color.svg +1 -0
  195. package/extensions/magpie/icons/vscode.svg +1 -0
  196. package/extensions/magpie/icons/wenxin-color.svg +1 -0
  197. package/extensions/magpie/icons/windsurf.svg +1 -0
  198. package/extensions/magpie/icons/workbuddy-color.svg +1 -0
  199. package/extensions/magpie/icons/workersai-color.svg +1 -0
  200. package/extensions/magpie/icons/xai.svg +1 -0
  201. package/extensions/magpie/icons/xiaomimimo.svg +1 -0
  202. package/extensions/magpie/icons/xinference-color.svg +1 -0
  203. package/extensions/magpie/icons/yi.svg +1 -0
  204. package/extensions/magpie/icons/yuanbao-color.svg +1 -0
  205. package/extensions/magpie/icons/yylx.svg +5 -0
  206. package/extensions/magpie/icons/zai.svg +1 -0
  207. package/extensions/magpie/icons/zcode.png +0 -0
  208. package/extensions/magpie/icons/zed.svg +1 -0
  209. package/extensions/magpie/icons/zenmux.svg +1 -0
  210. package/extensions/magpie/icons/zeroone.svg +1 -0
  211. package/extensions/magpie/icons/zhipu-color.svg +1 -0
  212. package/extensions/magpie/icons/zoo-code.png +0 -0
  213. package/extensions/magpie/index.html +572 -0
  214. package/extensions/magpie/kernel.js +274 -0
  215. package/extensions/magpie/keyspend.js +63 -0
  216. package/extensions/magpie/layout.js +26 -0
  217. package/extensions/magpie/manifest.json +37 -0
  218. package/extensions/magpie/navlamp.js +47 -0
  219. package/extensions/magpie/omarchy.css +72 -0
  220. package/extensions/magpie/omarchy.js +46 -0
  221. package/extensions/magpie/passerrors.js +21 -0
  222. package/extensions/magpie/pluginpages.js +405 -0
  223. package/extensions/magpie/plugins/gateway/client.js +1194 -0
  224. package/extensions/magpie/plugins/gateway/keys.js +972 -0
  225. package/extensions/magpie/plugins/gateway/settings.js +459 -0
  226. package/extensions/magpie/plugins/library/client.js +6788 -0
  227. package/extensions/magpie/plugins/library/i18n.js +3228 -0
  228. package/extensions/magpie/plugins/library/library.css +848 -0
  229. package/extensions/magpie/plugins/otel/i18n.js +48 -0
  230. package/extensions/magpie/plugins/otel/page.js +189 -0
  231. package/extensions/magpie/plugins/redact/client.js +46 -0
  232. package/extensions/magpie/plugins/redact/demo.js +461 -0
  233. package/extensions/magpie/plugins/redact/i18n.js +432 -0
  234. package/extensions/magpie/plugins/redact/page.js +985 -0
  235. package/extensions/magpie/plugins/redact/redact.css +385 -0
  236. package/extensions/magpie/plugins/redact/settings.js +248 -0
  237. package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
  238. package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
  239. package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
  240. package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
  241. package/extensions/magpie/plugins/routing/client.js +8593 -0
  242. package/extensions/magpie/plugins/routing/i18n.js +2696 -0
  243. package/extensions/magpie/plugins/routing/routing.css +1111 -0
  244. package/extensions/magpie/plugins/search/settings.js +460 -0
  245. package/extensions/magpie/plugins/sessions/client.js +1622 -0
  246. package/extensions/magpie/plugins/sessions/i18n.js +360 -0
  247. package/extensions/magpie/plugins/sessions/sessions.css +181 -0
  248. package/extensions/magpie/plugins/sessions/settings.js +100 -0
  249. package/extensions/magpie/plugins/usage/client.js +150 -0
  250. package/extensions/magpie/plugins/usage/i18n.js +984 -0
  251. package/extensions/magpie/plugins/usage/page.js +4307 -0
  252. package/extensions/magpie/plugins.css +319 -0
  253. package/extensions/magpie/plugins.js +1926 -0
  254. package/extensions/magpie/profilerename.js +27 -0
  255. package/extensions/magpie/purposes.js +2 -0
  256. package/extensions/magpie/quitask.js +79 -0
  257. package/extensions/magpie/quotaall.js +142 -0
  258. package/extensions/magpie/ratewindows.js +123 -0
  259. package/extensions/magpie/registry.js +87 -0
  260. package/extensions/magpie/resetask.js +137 -0
  261. package/extensions/magpie/ruleedit.js +53 -0
  262. package/extensions/magpie/scrollends.js +91 -0
  263. package/extensions/magpie/settingshints.js +75 -0
  264. package/extensions/magpie/signsheets.js +86 -0
  265. package/extensions/magpie/start.js +26 -0
  266. package/extensions/magpie/syncfiles.js +216 -0
  267. package/extensions/magpie/theme.js +29 -0
  268. package/extensions/magpie/traywindows.js +41 -0
  269. package/extensions/magpie/webfonts.js +130 -0
  270. package/extensions/magpie/whatsnew.js +252 -0
  271. package/extensions/magpie.zip +0 -0
  272. package/knowledge/apps/baijiahao.md +30 -0
  273. package/knowledge/apps/bilibili.md +1 -0
  274. package/knowledge/apps/dedao.md +33 -0
  275. package/knowledge/apps/feishu.md +35 -0
  276. package/knowledge/apps/gmail.md +33 -0
  277. package/knowledge/apps/jike.md +31 -0
  278. package/knowledge/apps/kuaishou.md +34 -0
  279. package/knowledge/apps/linkedin.md +33 -0
  280. package/knowledge/apps/qqmail.md +30 -0
  281. package/knowledge/apps/telegram.md +27 -0
  282. package/knowledge/apps/toutiao.md +32 -0
  283. package/knowledge/apps/uber-eats.md +26 -0
  284. package/knowledge/apps/uber.md +27 -0
  285. package/knowledge/apps/whatsapp.md +27 -0
  286. package/knowledge/apps/x.md +32 -0
  287. package/knowledge/apps/xiaohongshu.md +1 -0
  288. package/knowledge/apps/zhihu.md +1 -0
  289. package/knowledge/sites/bilibili.com/SITE.md +50 -0
  290. package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
  291. package/knowledge/sites/github.com/SITE.md +51 -0
  292. package/knowledge/sites/github.com/references/repo-fields.md +13 -0
  293. package/knowledge/sites/x.com/SITE.md +47 -0
  294. package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
  295. package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
  296. package/knowledge/sites/youtube.com/SITE.md +49 -0
  297. package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
  298. package/knowledge/sites/zhihu.com/SITE.md +47 -0
  299. package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
  300. package/lib/mobile/extension-catalog.js +41 -1
  301. package/lib/mobile/extension-data.js +224 -0
  302. package/lib/mobile/extension-dev.js +3 -1
  303. package/lib/mobile/file-ops.js +252 -0
  304. package/lib/mobile/history-derive.js +80 -4
  305. package/lib/mobile/host-bridge/endpoints.js +48 -6
  306. package/lib/mobile/host-bridge/event-synthesis.js +33 -6
  307. package/lib/mobile/jobs-control.js +118 -0
  308. package/lib/mobile/memory.js +635 -0
  309. package/lib/mobile/model-badges.js +77 -0
  310. package/lib/mobile/pet-tools.js +50 -4
  311. package/lib/mobile/pets.js +214 -18
  312. package/lib/mobile/phone/access.js +85 -0
  313. package/lib/mobile/phone/evidence.js +228 -0
  314. package/lib/mobile/phone/gate.js +290 -0
  315. package/lib/mobile/phone/grounding.js +314 -0
  316. package/lib/mobile/phone/guard.js +465 -0
  317. package/lib/mobile/phone/irreversible.js +88 -8
  318. package/lib/mobile/phone/knowledge-tools.js +665 -0
  319. package/lib/mobile/phone/learn.js +329 -0
  320. package/lib/mobile/phone/redact.js +237 -0
  321. package/lib/mobile/phone/risk.js +95 -0
  322. package/lib/mobile/phone/routes.js +28 -1
  323. package/lib/mobile/phone/service.js +23 -0
  324. package/lib/mobile/phone/site-commands.js +271 -0
  325. package/lib/mobile/phone/step-summary.js +201 -0
  326. package/lib/mobile/phone/stop-summary.js +235 -0
  327. package/lib/mobile/phone/tool-groups.js +125 -0
  328. package/lib/mobile/phone/tools.js +589 -0
  329. package/lib/mobile/phone/untrusted.js +65 -0
  330. package/lib/mobile/phone-knowledge.js +20 -4
  331. package/lib/mobile/schedule-edit.js +500 -0
  332. package/lib/mobile/schedule.js +5 -0
  333. package/lib/mobile/send-continuation.js +52 -0
  334. package/lib/mobile/session-models.js +27 -31
  335. package/lib/mobile/session-send.js +12 -12
  336. package/lib/mobile/skill-import.js +338 -0
  337. package/lib/mobile/skill-store.js +524 -0
  338. package/lib/routes/mobile-api.js +42 -0
  339. package/package.json +7 -6
  340. package/skills/skill-authoring/SKILL.md +46 -0
  341. package/skills/skill-authoring/references/examples.md +82 -0
  342. package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
  343. package/skills/skill-authoring/scripts/check_skill.py +167 -0
  344. package/skills/skill-authoring/scripts/manifest.json +3 -0
  345. package/knowledge/sites/bilibili.com.md +0 -27
  346. package/knowledge/sites/github.com.md +0 -27
  347. package/knowledge/sites/x.com.md +0 -28
  348. package/knowledge/sites/xiaohongshu.com.md +0 -27
  349. package/knowledge/sites/youtube.com.md +0 -28
  350. package/knowledge/sites/zhihu.com.md +0 -27
@@ -0,0 +1,1576 @@
1
+ "use strict";
2
+ // Context windows: what a request's prompt holds, as the gateway read it
3
+ // (gateway.Route.Prompt), drawn as a card — how full the window is, how
4
+ // much the vendor read from its cache, a grid of the window with a cell
5
+ // for each 1/400 of it coloured by what fills it, and the contents part by
6
+ // part. Routing shows the card for the request it tells of; the Usage
7
+ // page's Context tab scores each agent's use of its window, tags it, and
8
+ // lists its sessions' fill request by request.
9
+ (() => {
10
+ const PARTS = ["system", "tools", "skills", "memory", "files", "results", "chat"];
11
+ const PART_NAMES = {
12
+ system: "System prompt", tools: "Tools", skills: "Skills", memory: "Memory",
13
+ files: "Files", results: "Tool results", chat: "Conversation",
14
+ };
15
+ const PART_NOTES = {
16
+ system: "The agent's own instructions",
17
+ tools: "The definitions of the tools it can call",
18
+ skills: "The skills it can load, by name and what each is for",
19
+ memory: "Instruction files, memory and reminders",
20
+ files: "Files it read, and images",
21
+ results: "What the other tools it called returned",
22
+ chat: "What you and the model said, turn by turn",
23
+ };
24
+ const CELLS = 400;
25
+ const partName = (k) => t(PART_NAMES[k] || k);
26
+ // the Codexes here before 0.163 or of no known version (app.js too)
27
+ const codexHeld = window.codexHeld = (held) => t("{who}: the share needs Codex 0.163 or later; until it's updated magpie writes only async_compaction = true, at 65%", { who: held.map((r) => t({ app: "Codex app", ide: "Codex editor extension", wsl: "Codex in WSL" }[r.kind] || "Codex CLI") + (r.version ? " " + r.version : "")).join(", ") });
28
+
29
+ const fmtK = (n) => {
30
+ n = Math.max(0, Math.round(n || 0));
31
+ const short = (x, d) => x.toFixed(d).replace(/\.0+$/, "");
32
+ if (n >= 1e6) return short(n / 1e6, n >= 1e7 ? 1 : 2) + "M";
33
+ if (n >= 1e3) return short(n / 1e3, n >= 1e5 ? 0 : 1) + "K";
34
+ return String(n);
35
+ };
36
+ const pct = (x) => Math.round(x * 100) + "%";
37
+ // a count of tokens that a redraw counts on from what it said (morph)
38
+ const num = (tag, cls, n) => { const e = el(tag, cls, fmtK(n)); e.dataset.n = Math.round(n); return e; };
39
+ const median = (xs) => {
40
+ if (!xs.length) return 0;
41
+ const s = [...xs].sort((a, b) => a - b);
42
+ return s[s.length >> 1];
43
+ };
44
+ const agentMeta = (id) => (state.clients || state.agents || []).find((a) => a.id === id);
45
+ // "scripts": requests naming only a library (context.go's scriptsAgent)
46
+ const agentLabel = (id) => (id === "scripts" ? t("Scripts") : agentMeta(id)?.name || id || t("your agent"));
47
+ const agentIcon = (id) => icon(agentMeta(id)?.icon || "generic");
48
+ // the lab a model id is from, by its name after the provider's; none when
49
+ // it isn't known, so a wrong logo never stands for it
50
+ const LABS = [[/claude/, "claude-color"], [/gpt|^o\d|codex/, "openai"], [/gemma/, "gemma-color"], [/gemini/, "gemini-color"],
51
+ [/deepseek/, "deepseek-color"], [/qwen|qwq/, "qwen-color"], [/kimi|moonshot/, "kimi"], [/glm/, "zhipu-color"], [/grok/, "grok"],
52
+ [/minimax|abab/, "minimax-color"], [/doubao|seed/, "doubao-color"], [/stral/, "mistral-color"], [/llama/, "meta-color"],
53
+ [/mimo/, "xiaomimimo"], [/^step/, "stepfun-color"], [/hunyuan/, "hunyuan-color"]];
54
+ const labIcon = (model) => {
55
+ const m = model.toLowerCase().split("/").pop();
56
+ const hit = LABS.find(([re]) => re.test(m));
57
+ return hit ? [icon(hit[1])] : [];
58
+ };
59
+
60
+ // what an item of the prompt is called: a file's path or a tool's name
61
+ // as it is; magpie's own names for the rest in the reader's language
62
+ const ITEM_NAMES = {
63
+ prompt: "Agent prompt", harness: "Harness messages", billing: "Billing header",
64
+ environment: "Environment notes", context: "Context", skills: "Skills",
65
+ reminders: "System reminders", compacted: "Compacted summary", image: "Image", document: "Document",
66
+ };
67
+ function itemName(it) {
68
+ if (it.tag === "turn") return t("Turn {n}", { n: it.name });
69
+ if (it.tag === "turns") return t("{n} earlier turns", { n: it.n });
70
+ if (it.tag === "more") return t("{n} more", { n: it.n });
71
+ return ITEM_NAMES[it.name] ? t(ITEM_NAMES[it.name]) : it.name;
72
+ }
73
+ // a path's file name first, where it is after it
74
+ function itemLabel(it, kind) {
75
+ const name = itemName(it);
76
+ const e = el("span", "ctx-name");
77
+ if ((kind === "files" || kind === "memory") && name.includes("/")) {
78
+ const at = name.lastIndexOf("/");
79
+ e.append(el("b", "", name.slice(at + 1)), el("small", "", name.slice(0, at + 1)));
80
+ } else e.append(el("b", "", name));
81
+ e.title = name;
82
+ return e;
83
+ }
84
+ const TAG_NAMES = {
85
+ mcp: "MCP", namespace: "namespace", read: "read", shell: "shell", result: "result",
86
+ user: "user", project: "project", local: "local", memory: "memory", skills: "skills",
87
+ reminder: "reminder", image: "image", turn: "", turns: "", more: "", compacted: "",
88
+ };
89
+ const tagOf = (it) => it.tag in TAG_NAMES ? TAG_NAMES[it.tag] : it.tag || "";
90
+
91
+ const health = (fill) => fill < 0.75 ? ["ok", "Healthy"] : fill < 0.9 ? ["warn", "Filling up"] : ["bad", "Near full"];
92
+
93
+ // the cache a route's vendor read: its last answer's usage
94
+ function cacheOf(r) {
95
+ const u = r.usage?.[r.usage.length - 1];
96
+ // a vendor that counts no cache has none to show, not 0% (#3464)
97
+ if (!u || u.cache_unknown) return null;
98
+ const total = (u.in || 0) + (u.cache_read || 0) + (u.cache_write || 0);
99
+ if (!total) return null;
100
+ return { read: u.cache_read || 0, total };
101
+ }
102
+
103
+ // the cells of the grid, each the part and item it shows; a window
104
+ // smaller than the prompt (not known, or overrun) is the prompt
105
+ function cellsOf(p, window) {
106
+ const scale = Math.max(window, p.tokens, 1);
107
+ const used = Math.min(CELLS, Math.round((p.tokens / scale) * CELLS));
108
+ const parts = PARTS.map((k) => p.parts.find((x) => x.kind === k)).filter((x) => x && x.tokens > 0);
109
+ const sum = parts.reduce((s, x) => s + x.tokens, 0) || 1;
110
+ // largest remainder, so the cells add up to used and a part of any
111
+ // size has at least one
112
+ const want = parts.map((x) => (x.tokens / sum) * used);
113
+ const got = want.map((w) => Math.max(1, Math.floor(w)));
114
+ let left = used - got.reduce((s, n) => s + n, 0);
115
+ const order = want.map((w, i) => [w - Math.floor(w), i]).sort((a, b) => b[0] - a[0]);
116
+ for (let j = 0; left > 0 && order.length; j = (j + 1) % order.length, left--) got[order[j][1]]++;
117
+ for (let j = got.length - 1; left < 0 && j >= 0; j--) while (left < 0 && got[j] > 1) { got[j]--; left++; }
118
+ const cells = [];
119
+ parts.forEach((x, pi) => {
120
+ const items = [...(x.items || [])].sort((a, b) => b.tokens - a.tokens);
121
+ const isum = items.reduce((s, it) => s + it.tokens, 0) || 1;
122
+ let at = 0, acc = 0;
123
+ for (let c = 0; c < got[pi]; c++) {
124
+ // the item whose share the cell's middle falls in
125
+ const mid = ((c + 0.5) / got[pi]) * isum;
126
+ while (at < items.length - 1 && acc + items[at].tokens < mid) acc += items[at++].tokens;
127
+ cells.push({ kind: x.kind, item: items.length ? at : -1 });
128
+ }
129
+ });
130
+ while (cells.length < CELLS) cells.push(null);
131
+ return cells.slice(0, CELLS);
132
+ }
133
+
134
+ // the headroom: how many more requests fit, at the pace the session's
135
+ // prompts grew
136
+ function headroom(series, free) {
137
+ const steps = [];
138
+ for (let i = 1; i < series.length; i++) {
139
+ const d = series[i].tokens - series[i - 1].tokens;
140
+ if (d > 0 && series[i].tokens >= series[i - 1].tokens * 0.6) steps.push(d);
141
+ }
142
+ const g = median(steps);
143
+ return g > 0 ? Math.floor(free / g) : null;
144
+ }
145
+
146
+ // count words a number with the phrase for one, or for many
147
+ const count = (n, one, many) => n === 1 ? t(one) : t(many, { n });
148
+
149
+ // spark draws a session's prompts, request by request, against its
150
+ // window; the one the card tells of is marked, and a point clicked opens
151
+ // its request
152
+ function spark(series, at, window, onPoint) {
153
+ const W = 240, H = 34, pad = 3;
154
+ // scaled to the session's peak, so a session that never came near its
155
+ // window still shows how it grew; the window's line shows once it is near
156
+ const peak = Math.max(...series.map((p) => p.tokens), 1);
157
+ const top = window && peak > window * 0.6 ? Math.max(window, peak) : peak * 1.2;
158
+ const x = (i) => series.length < 2 ? W / 2 : pad + (i / (series.length - 1)) * (W - pad * 2);
159
+ const y = (n) => H - pad - (n / top) * (H - pad * 2);
160
+ const box = el("span", "ctx-spark");
161
+ const s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
162
+ s.setAttribute("viewBox", `0 0 ${W} ${H}`);
163
+ s.setAttribute("preserveAspectRatio", "none");
164
+ box.append(s);
165
+ const ns = (tag, attrs) => {
166
+ const e = document.createElementNS("http://www.w3.org/2000/svg", tag);
167
+ for (const [k, v] of Object.entries(attrs)) e.setAttribute(k, v);
168
+ s.append(e);
169
+ return e;
170
+ };
171
+ if (window && window <= top) ns("line", { x1: 0, x2: W, y1: y(window), y2: y(window), class: "win" });
172
+ const pts = series.map((p, i) => `${x(i).toFixed(1)},${y(p.tokens).toFixed(1)}`);
173
+ if (series.length > 1) {
174
+ ns("path", { d: `M${pts[0]} L${pts.slice(1).join(" L")} L${x(series.length - 1).toFixed(1)},${H} L${x(0).toFixed(1)},${H} Z`, class: "area" });
175
+ ns("polyline", { points: pts.join(" "), class: "line" });
176
+ }
177
+ series.forEach((p, i) => {
178
+ const on = p.id === at;
179
+ // the marker is a dot over the stretched svg, so it stays round
180
+ if (on) {
181
+ const dot = el("span", "at");
182
+ dot.style.left = (x(i) / W * 100).toFixed(2) + "%";
183
+ dot.style.top = (y(p.tokens) / H * 100).toFixed(2) + "%";
184
+ box.append(dot);
185
+ }
186
+ if (!onPoint) return;
187
+ const hit = ns("rect", { x: x(i) - Math.max(2, W / series.length / 2), y: 0, width: Math.max(4, W / series.length), height: H, class: "hit" + (on ? " on" : "") });
188
+ const tip = document.createElementNS("http://www.w3.org/2000/svg", "title");
189
+ tip.textContent = t("Request {n}: {tokens} tokens", { n: i + 1, tokens: fmtK(p.tokens) });
190
+ hit.append(tip);
191
+ hit.onclick = (e) => { e.stopPropagation(); onPoint(p); };
192
+ });
193
+ return box;
194
+ }
195
+
196
+ // Narrow layouts go by the card's own width, as classes (max520), not
197
+ // container queries: WebKit pulled a view scrolled to its end up by a
198
+ // card's height on each redraw (#1249). A redrawn card starts at the last
199
+ // width in its place, so it never lays out wide and pulls the page up
200
+ const cardWidth = {};
201
+ const sizeCard = (card, w) => {
202
+ card.classList.toggle("max520", w <= 520);
203
+ card.classList.toggle("max420", w <= 420);
204
+ };
205
+ const cardSizes = new ResizeObserver((all) => {
206
+ for (const e of all) {
207
+ if (!e.target.isConnected) { cardSizes.unobserve(e.target); continue; }
208
+ const card = e.target, w = e.contentRect.width;
209
+ if (!w) continue; // out of sight (#4652)
210
+ cardWidth[card.dataset.place] = w;
211
+ // on the next frame: a class changed in the observer's own call
212
+ // changes the card's height, which WebKit reports as a
213
+ // ResizeObserver loop (see quotaFit)
214
+ requestAnimationFrame(() => sizeCard(card, w));
215
+ }
216
+ });
217
+
218
+ // morph makes old what neu is, keeping old's nodes of the same kind: a
219
+ // redrawn card is patched in place (Zhenzhen on Discord: each live
220
+ // request flashed the whole card). Only handlers set as properties come
221
+ // along, so nothing morphed uses addEventListener.
222
+ const HANDLERS = ["onclick", "onpointerenter", "onpointerleave", "onkeydown"];
223
+ function morph(old, neu) {
224
+ if (old.nodeName !== neu.nodeName || (old.nodeType !== 1 && old.nodeType !== 3)) { old.replaceWith(neu); return neu; }
225
+ if (old.nodeType === 3) { if (old.data !== neu.data) old.data = neu.data; return old; }
226
+ // a count (data-n) is counted on from what it said to the new one
227
+ const from = old._at ?? (old.dataset?.n ? +old.dataset.n : undefined);
228
+ for (const a of [...old.attributes]) if (!neu.hasAttribute(a.name)) old.removeAttribute(a.name);
229
+ for (const a of neu.attributes) if (old.getAttribute(a.name) !== a.value) old.setAttribute(a.name, a.value);
230
+ for (const h of HANDLERS) if (old[h] !== neu[h]) old[h] = neu[h];
231
+ morphKids(old, [...neu.childNodes]);
232
+ if (neu.dataset?.n) roll(old, from, +neu.dataset.n, fmtK, calm());
233
+ return old;
234
+ }
235
+ // roll counts node's text from one number to another as the map's tiles
236
+ // glide, in their time (flohoeller's map counts up as its window fills);
237
+ // fmt says a number. quiet, or with nothing to count from, it says the
238
+ // new one at once
239
+ function roll(node, from, to, fmt, quiet) {
240
+ cancelAnimationFrame(node._raf);
241
+ if (quiet || from === undefined || from === to || !node.isConnected) {
242
+ delete node._at;
243
+ if (node.textContent !== fmt(to)) node.textContent = fmt(to);
244
+ return;
245
+ }
246
+ const t0 = performance.now();
247
+ const step = (now) => {
248
+ // a frame's time can be before t0 (it is when the frame began)
249
+ const k = Math.min(1, Math.max(0, (now - t0) / 620));
250
+ node._at = from + (to - from) * (1 - Math.pow(1 - k, 4));
251
+ node.textContent = fmt(node._at);
252
+ if (k < 1) node._raf = requestAnimationFrame(step);
253
+ else delete node._at;
254
+ };
255
+ step(t0);
256
+ }
257
+ function morphKids(box, kids) {
258
+ const old = [...box.childNodes];
259
+ kids.forEach((k, i) => { if (i < old.length) morph(old[i], k); else box.append(k); });
260
+ for (let i = kids.length; i < old.length; i++) old[i].remove();
261
+ }
262
+
263
+ // ---------- the map: the window as tiles (flohoeller's context map) ----------
264
+
265
+ // The card's window is drawn one of two ways, the reader's pick: the
266
+ // map, each item a tile sized by its tokens inside its part's, the free
267
+ // space a hatched tile of its share; or the grid, a cell for each 1/400
268
+ // of the window. The map is the default; the grid is kept for those who
269
+ // had it.
270
+ let ctxLook = "map";
271
+ try { if (localStorage.getItem("magpie.ctxLook") === "grid") ctxLook = "grid"; } catch {}
272
+
273
+ // squarify lays vals ([{v}]) in the rect x, y, w, h as rects whose sides
274
+ // are as near alike as it can (Bruls, Huizing, van Wijk), in vals' order
275
+ function squarify(vals, x, y, w, h) {
276
+ const out = [];
277
+ vals = vals.filter((a) => a.v > 0);
278
+ const total = vals.reduce((s, a) => s + a.v, 0);
279
+ if (!total || w <= 0 || h <= 0) return out;
280
+ const k = (w * h) / total;
281
+ const worst = (row, side) => {
282
+ let sum = 0, hi = 0, lo = Infinity;
283
+ for (const a of row) { const s = a.v * k; sum += s; hi = Math.max(hi, s); lo = Math.min(lo, s); }
284
+ return Math.max((side * side * hi) / (sum * sum), (sum * sum) / (side * side * lo));
285
+ };
286
+ const lay = (row) => {
287
+ const sum = row.reduce((s, a) => s + a.v * k, 0);
288
+ if (w >= h) {
289
+ const cw = sum / h;
290
+ let cy = y;
291
+ for (const a of row) { const ch = (a.v * k) / cw; out.push({ a, x, y: cy, w: cw, h: ch }); cy += ch; }
292
+ x += cw; w -= cw;
293
+ } else {
294
+ const rh = sum / w;
295
+ let cx = x;
296
+ for (const a of row) { const cw = (a.v * k) / rh; out.push({ a, x: cx, y, w: cw, h: rh }); cx += cw; }
297
+ y += rh; h -= rh;
298
+ }
299
+ };
300
+ let row = [];
301
+ for (const a of vals) {
302
+ const side = Math.min(w, h);
303
+ if (!row.length || worst([...row, a], side) <= worst(row, side)) { row.push(a); continue; }
304
+ lay(row);
305
+ row = [a];
306
+ }
307
+ if (row.length) lay(row);
308
+ return out;
309
+ }
310
+
311
+ // tilesOf lays a prompt out on a map W×H px: the used share of the
312
+ // window and the free space side by side along its long side (the
313
+ // used share eased, see usedLen), the used
314
+ // parts squarified in it, a part's items in the part. An item too small
315
+ // to read goes in one tile with the part's other small ones; a part too
316
+ // small for its items is one tile. Each tile: key (stays across draws),
317
+ // kind, its item (it), the items it stands for (its), rect in px.
318
+ const TILE_MIN = 1500, TILE_MAX = 12, TILE_FLOOR = 700, TILE_SIDE = 16;
319
+ // the look switch's marks: tiles, and a grid of cells
320
+ const LOOK_MAP = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><rect x="1.5" y="2" width="5.5" height="12" rx="1.6"/><rect x="8.5" y="2" width="6" height="6.5" rx="1.6"/><rect x="8.5" y="10" width="6" height="4" rx="1.4"/></svg>';
321
+ const LOOK_GRID = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">' +
322
+ [2, 6.5, 11].flatMap((y) => [2, 6.5, 11].map((x) => `<rect x="${x}" y="${y}" width="3" height="3" rx=".8"/>`)).join("") + "</svg>";
323
+ function tilesOf(p, window, W, H) {
324
+ const scale = Math.max(window, p.tokens, 1);
325
+ const across = W >= H;
326
+ const long = across ? W : H;
327
+ // the used share drawn on a gentle curve (share^0.35): a window 8%
328
+ // full still has room to read its tiles (41% of the map), a full one
329
+ // stays full, and the free tile says its own share in words
330
+ let usedLen = long;
331
+ if (p.tokens < scale) usedLen = Math.min(long, Math.max(long * Math.pow(p.tokens / scale, 0.35), Math.min(long, 36)));
332
+ const tiles = [];
333
+ const parts = PARTS.map((k) => p.parts.find((x) => x.kind === k)).filter((x) => x && x.tokens > 0);
334
+ const ur = across ? [0, 0, usedLen, H] : [0, 0, W, usedLen];
335
+ // a tile is never laid smaller than TILE_FLOOR px² (a sliver can't be
336
+ // pointed at): its value raised to that share of the rect it is in
337
+ const floored = (vals, area) => {
338
+ const sum = vals.reduce((s, a) => s + a.v, 0);
339
+ const lo = (sum * TILE_FLOOR) / Math.max(1, area);
340
+ return vals.map((a) => ({ ...a, v: Math.max(a.v, lo) }));
341
+ };
342
+ // and a part's tile is TILE_SIDE px across both ways: the floor is an
343
+ // area, and a part laid in a long row beside large ones met it as a
344
+ // strip (49 tokens of memory beside 70K: 190×4px, under the tiles' 3px
345
+ // gap, so it was hidden; AIGCFREE on Discord). One too thin has its
346
+ // value raised by as much and the parts are laid again.
347
+ const partVals = floored(parts.map((x) => ({ v: x.tokens, x })), ur[2] * ur[3]);
348
+ let partRects = squarify(partVals, ...ur);
349
+ for (let n = 0; n < 8; n++) {
350
+ const thin = partRects.filter((r) => Math.min(r.w, r.h) < TILE_SIDE);
351
+ if (!thin.length) break;
352
+ for (const r of thin) r.a.v *= (TILE_SIDE / Math.max(1, Math.min(r.w, r.h))) * 1.05;
353
+ partRects = squarify(partVals, ...ur);
354
+ }
355
+ for (const r of partRects) {
356
+ const x = r.a.x;
357
+ const items = (x.items || []).filter((it) => it.tokens > 0).sort((a, b) => b.tokens - a.tokens);
358
+ const isum = items.reduce((s, it) => s + it.tokens, 0);
359
+ const area = r.w * r.h;
360
+ if (!items.length || area < TILE_MIN * 2) {
361
+ tiles.push({ key: x.kind, kind: x.kind, its: items, tokens: x.tokens, x: r.x, y: r.y, w: r.w, h: r.h });
362
+ continue;
363
+ }
364
+ // an item's share of the part's tokens, its tile's share of the rect
365
+ const kept = [], rest = [];
366
+ for (const it of items) {
367
+ if (kept.length < TILE_MAX && (it.tokens / isum) * area >= TILE_MIN) kept.push(it);
368
+ else rest.push(it);
369
+ }
370
+ const vals = kept.map((it) => ({ v: it.tokens, it }));
371
+ if (rest.length === 1 && !kept.length) vals.push({ v: rest[0].tokens, it: rest[0] });
372
+ else if (rest.length) vals.push({ v: rest.reduce((s, it) => s + it.tokens, 0), rest });
373
+ // a part whose items would leave one a strip too thin to read or
374
+ // point at is one tile
375
+ const laid = squarify(floored(vals, area), r.x, r.y, r.w, r.h);
376
+ if (laid.some((q) => Math.min(q.w, q.h) < 14)) {
377
+ tiles.push({ key: x.kind, kind: x.kind, its: items, tokens: x.tokens, x: r.x, y: r.y, w: r.w, h: r.h });
378
+ continue;
379
+ }
380
+ for (const q of laid) {
381
+ const t0 = q.a;
382
+ const it = t0.it;
383
+ tiles.push({
384
+ key: x.kind + "\x00" + (it ? (it.tag || "") + "\x00" + it.name : "\x01rest"),
385
+ kind: x.kind, it, its: it ? [it] : t0.rest, tokens: ((it ? it.tokens : t0.rest.reduce((s, y) => s + y.tokens, 0)) / isum) * x.tokens,
386
+ x: q.x, y: q.y, w: q.w, h: q.h,
387
+ });
388
+ }
389
+ }
390
+ if (p.tokens < scale) {
391
+ const fr = across ? [usedLen, 0, W - usedLen, H] : [0, usedLen, W, H - usedLen];
392
+ tiles.push({ key: "\x00free", kind: "", free: true, tokens: scale - p.tokens, x: fr[0], y: fr[1], w: fr[2], h: fr[3] });
393
+ }
394
+ return tiles;
395
+ }
396
+
397
+ // ctxCard draws a route's prompt. opts: series (the session's prompts,
398
+ // [{id, tokens, time}]), onPoint (a point of it clicked), cache ({read,
399
+ // total}, when the route carries no usage), crumbs (the footer's path),
400
+ // place (for its width: see cardWidth). The card's ctxUpdate(r, opts)
401
+ // draws another route in it, keeping the nodes that are the same (grid
402
+ // cells too), so nothing flashes and the grid keeps its height.
403
+ // A foldable card (Routing's) keeps the reader's choice: folded, it is its
404
+ // head alone with how full the window is, so the request list has room
405
+ // (0xBCD18E on X: it took most of the height)
406
+ let ctxShut = false;
407
+ try { ctxShut = localStorage.getItem("magpie.ctxShut") === "1"; } catch {}
408
+ // opts.open: this request was opened to see its context window (the
409
+ // Usage page's Context tab), so it shows unfolded, it alone: the next
410
+ // request drawn in the card takes the reader's choice again
411
+ // a window as a thin bar, a part each
412
+ function stackBar(p) {
413
+ const w = p.window || 0, bar = el("span", "ctx-stack"), scale = Math.max(w, p.tokens, 1);
414
+ bar.setAttribute("role", "img");
415
+ bar.setAttribute("aria-label", t("{used} of {window} tokens used", { used: fmtK(p.tokens), window: fmtK(w || p.tokens) }));
416
+ for (const k of PARTS) {
417
+ const i = el("i", "k-" + k), n = (p.parts || []).find((x) => x.kind === k)?.tokens || 0;
418
+ i.title = partName(k);
419
+ i.style.width = +(Math.max(0, n) / scale * 100).toFixed(2) + "%";
420
+ bar.append(i);
421
+ }
422
+ return bar;
423
+ }
424
+ window.ctxStack = stackBar;
425
+ function ctxCard(r, opts = {}) {
426
+ const card = el("div", "ctx-card" + (opts.foldable && ctxShut && !opts.open ? " shut" : ""));
427
+ card.dataset.place = opts.place || "";
428
+ if (cardWidth[card.dataset.place] !== undefined) sizeCard(card, cardWidth[card.dataset.place]);
429
+ cardSizes.observe(card);
430
+
431
+ // what stays across draws: the grid and its cells, its tip, the
432
+ // parts around them, and the tab the reader is on
433
+ const grid = el("div", "ctx-waffle");
434
+ grid.setAttribute("role", "img");
435
+ const map = el("div", "ctx-map");
436
+ map.setAttribute("role", "img");
437
+ // the map and the grid in one place, the reader's look shown
438
+ const viz = el("div", "ctx-viz");
439
+ viz.dataset.look = ctxLook;
440
+ viz.append(map, grid);
441
+ // the map's tiles by key, kept across draws: a live request's tiles
442
+ // glide to their new sizes, and only a new item's comes in
443
+ const tiles = new Map();
444
+ const focusOn = (f) => { grid.dataset.f = map.dataset.f = f; };
445
+ const unfocus = () => {
446
+ delete grid.dataset.f;
447
+ delete map.dataset.f;
448
+ for (const e of viz.querySelectorAll(".on")) e.classList.remove("on");
449
+ };
450
+ const tip = el("div", "ctx-tip");
451
+ tip.hidden = true;
452
+ const contents = el("div", "ctx-contents"), ch = el("div", "ctx-contents-head"), list = el("div", "ctx-rows");
453
+ contents.append(ch, list);
454
+ let parts = null, cur = null, cacheShown = false, lastCache = null, cacheWhose = "", chKey = "", rid = 0;
455
+ let tab = opts.tab || "all", more = false;
456
+
457
+ const showTip = (target) => {
458
+ const { p, window, cells, partOf } = cur;
459
+ const idx = [...grid.children].indexOf(target);
460
+ const c = cells[idx];
461
+ tip.replaceChildren();
462
+ const th = el("div", "ctx-tip-head");
463
+ if (c) {
464
+ const part = partOf.get(c.kind);
465
+ th.append(el("i", "dot k-" + c.kind), el("b", "", partName(c.kind)), el("span", "grow"),
466
+ el("span", "", fmtK(part.tokens) + " · " + pct(part.tokens / Math.max(1, p.tokens))));
467
+ tip.append(th);
468
+ const items = [...(part.items || [])].sort((a, b) => b.tokens - a.tokens);
469
+ const shown = items.slice(0, 4);
470
+ const here = items[c.item];
471
+ if (here && !shown.includes(here)) shown[3] = here;
472
+ for (const it of shown) {
473
+ const row = el("div", "ctx-tip-row" + (it === here ? " on" : ""));
474
+ row.append(el("span", "n", itemName(it)), el("span", "v", fmtK(it.tokens)));
475
+ tip.append(row);
476
+ }
477
+ if (items.length > shown.length) tip.append(el("div", "ctx-tip-more", t("{n} more", { n: items.length - shown.length })));
478
+ grid.dataset.f = c.kind;
479
+ } else {
480
+ th.append(el("i", "dot free"), el("b", "", t("Free space")), el("span", "grow"),
481
+ el("span", "", fmtK(Math.max(0, window - p.tokens))));
482
+ tip.append(th);
483
+ grid.dataset.f = "free";
484
+ }
485
+ placeTip(target);
486
+ };
487
+ // the tip over what it tells of, or under it where there's no room
488
+ const placeTip = (target) => {
489
+ tip.hidden = false;
490
+ const b = target.getBoundingClientRect(), cr = card.getBoundingClientRect();
491
+ const w = tip.offsetWidth, h = tip.offsetHeight;
492
+ let left = b.left - cr.left + b.width / 2 - w / 2;
493
+ left = Math.max(8, Math.min(cr.width - w - 8, left));
494
+ let topY = b.top - cr.top - h - 8;
495
+ if (topY < 4) topY = b.bottom - cr.top + 8;
496
+ tip.style.left = left + "px";
497
+ tip.style.top = topY + "px";
498
+ };
499
+ // a tile's tip: the item, its part and share; a part's or the small
500
+ // ones' tile, its largest items
501
+ const showMapTip = (n) => {
502
+ const { p, window } = cur;
503
+ const tl = n._tile;
504
+ tip.replaceChildren();
505
+ const th = el("div", "ctx-tip-head");
506
+ if (tl.free) {
507
+ th.append(el("i", "dot free"), el("b", "", t("Free space")), el("span", "grow"),
508
+ el("span", "", fmtK(Math.max(0, window - p.tokens))));
509
+ tip.append(th);
510
+ } else if (tl.it) {
511
+ th.append(el("i", "dot k-" + tl.kind), el("b", "", itemName(tl.it)));
512
+ tip.append(th, el("div", "ctx-tip-sub", partName(tl.kind) + " · " + fmtK(tl.it.tokens) + " " + t("tokens") + " · " + pct(tl.it.tokens / Math.max(1, p.tokens))));
513
+ } else {
514
+ th.append(el("i", "dot k-" + tl.kind), el("b", "", partName(tl.kind)), el("span", "grow"),
515
+ el("span", "", fmtK(tl.tokens) + " · " + pct(tl.tokens / Math.max(1, p.tokens))));
516
+ tip.append(th);
517
+ const shown = tl.its.slice(0, 5);
518
+ for (const it of shown) {
519
+ const row = el("div", "ctx-tip-row");
520
+ row.append(el("span", "n", itemName(it)), el("span", "v", fmtK(it.tokens)));
521
+ tip.append(row);
522
+ }
523
+ if (tl.its.length > shown.length) tip.append(el("div", "ctx-tip-more", t("{n} more", { n: tl.its.length - shown.length })));
524
+ }
525
+ placeTip(n);
526
+ };
527
+ // the tip told again as its tile grows, and kept over it as it glides
528
+ const followTip = (n) => {
529
+ if (!n.isConnected || n.classList.contains("gone")) { tip.hidden = true; return; }
530
+ showMapTip(n);
531
+ const t0 = performance.now();
532
+ const go = (now) => {
533
+ if (tip.hidden || !n.classList.contains("on")) return;
534
+ placeTip(n);
535
+ if (now - t0 < 660) requestAnimationFrame(go);
536
+ };
537
+ requestAnimationFrame(go);
538
+ };
539
+ const tileAt = (e) => {
540
+ const n = e.target.closest?.(".ctx-tile");
541
+ return cur && n && n.parentElement === map && n._tile && !n.classList.contains("gone") ? n : null;
542
+ };
543
+ map.addEventListener("pointerover", (e) => {
544
+ const n = tileAt(e);
545
+ if (!n) return;
546
+ map.dataset.f = "tile";
547
+ for (const x of tiles.values()) x.classList.toggle("on", x === n);
548
+ showMapTip(n);
549
+ });
550
+ map.addEventListener("pointerleave", () => { tip.hidden = true; unfocus(); });
551
+ // a tile clicked opens the text of what it shows, as a cell does
552
+ map.addEventListener("click", (e) => {
553
+ const n = tileAt(e);
554
+ if (!n || n._tile.free) return;
555
+ e.stopPropagation();
556
+ tip.hidden = true;
557
+ openCtxText(rid, n._tile.kind, n._tile.it);
558
+ });
559
+
560
+ // drawMap lays the map out at its size: a tile kept glides to its new
561
+ // place (CSS), a new one comes in, one gone fades. enter: every tile
562
+ // comes in, one after another (the first draw, or the map picked)
563
+ const drawMap = (enter) => {
564
+ if (!cur || viz.dataset.look !== "map") return;
565
+ const W = map.clientWidth, H = map.clientHeight;
566
+ // folded or not in the page yet: drawn once it has a size
567
+ if (!W || !H) return;
568
+ map.dataset.size = W + "x" + H;
569
+ const { p, window } = cur;
570
+ const quiet = opts.still || calm();
571
+ enter = enter || !tiles.size;
572
+ const seen = new Set();
573
+ tilesOf(p, window, W, H).forEach((tl, i) => {
574
+ seen.add(tl.key);
575
+ let n = tiles.get(tl.key);
576
+ const fresh = !n;
577
+ if (fresh) {
578
+ n = el("div", "ctx-tile");
579
+ n.append(el("b", "n"), el("span", "v"));
580
+ tiles.set(tl.key, n);
581
+ map.append(n);
582
+ }
583
+ n._tile = tl;
584
+ const cls = "ctx-tile " + (tl.free ? "free" : "k-" + tl.kind) + (tl.w < 54 || tl.h < 30 ? " bare" : tl.h < 46 ? " low" : "") + (n.classList.contains("on") ? " on" : "");
585
+ if (n.className !== cls) n.className = cls;
586
+ // the value counts on to its tokens as the tile glides (roll)
587
+ let name, v = (x) => fmtK(x) + " · " + partName(tl.kind);
588
+ if (tl.free) { name = t("Free space"); v = (x) => fmtK(x) + " · " + pct(x / Math.max(window, p.tokens, 1)); }
589
+ else if (tl.it) {
590
+ name = itemName(tl.it);
591
+ if (tl.kind === "files" || tl.kind === "memory") name = name.slice(name.lastIndexOf("/", name.length - 2) + 1) || name;
592
+ // a tool's results gathered, how many: "web__run ×20"
593
+ if (tl.it.n > 1 && tl.it.tag !== "turns" && tl.it.tag !== "more") name += " ×" + tl.it.n;
594
+ } else if (tl.key === tl.kind) { name = partName(tl.kind); v = fmtK; }
595
+ else {
596
+ // the small files gathered: their folder, when they share one
597
+ // ("src/api/ · 12 files"), as the reader would name them
598
+ const dirs = new Set(tl.its.map((it) => it.name.slice(0, it.name.lastIndexOf("/") + 1)));
599
+ const [dir] = dirs;
600
+ if (tl.kind === "files" && dirs.size === 1 && dir) {
601
+ name = dir.slice(dir.lastIndexOf("/", dir.length - 2) + 1);
602
+ v = (x) => fmtK(x) + " · " + t("{n} files", { n: tl.its.length });
603
+ } else name = t("{n} more", { n: tl.its.length });
604
+ }
605
+ if (n.firstChild.textContent !== name) n.firstChild.textContent = name;
606
+ roll(n.lastChild, fresh ? undefined : n.lastChild._at ?? n.lastChild._n, tl.tokens, v, quiet || enter);
607
+ n.lastChild._n = tl.tokens;
608
+ // a gap of 3px between tiles: each 1.5px in from its rect
609
+ const pc = (a, b) => +((a / b) * 100).toFixed(3);
610
+ n.style.left = `calc(${pc(tl.x, W)}% + 1.5px)`;
611
+ n.style.top = `calc(${pc(tl.y, H)}% + 1.5px)`;
612
+ n.style.width = `max(0px, calc(${pc(tl.w, W)}% - 3px))`;
613
+ n.style.height = `max(0px, calc(${pc(tl.h, H)}% - 3px))`;
614
+ if (quiet || !n.animate || !(fresh || enter)) return;
615
+ n.animate([{ opacity: 0, transform: "scale(.94)", filter: "blur(4px)" }, { opacity: 1, transform: "none", filter: "none" }],
616
+ { duration: 560, easing: IOS_EASE, delay: enter ? 40 + Math.min(i, 16) * 32 : 0, fill: "backwards" });
617
+ });
618
+ for (const [k, n] of tiles) {
619
+ if (seen.has(k)) continue;
620
+ tiles.delete(k);
621
+ n.classList.add("gone");
622
+ if (quiet || !n.animate) { n.remove(); continue; }
623
+ n.animate([{ opacity: 1 }, { opacity: 0, transform: "scale(.96)" }], { duration: 260, easing: IOS_EASE, fill: "forwards" })
624
+ .finished.then(() => n.remove(), () => n.remove());
625
+ }
626
+ };
627
+ // the map's size changed (the card unfolded, the window resized): laid
628
+ // out again for it, its tiles' labels shown where they fit
629
+ const mapSizes = new ResizeObserver(() => {
630
+ if (!map.isConnected) return;
631
+ if (map.dataset.size !== map.clientWidth + "x" + map.clientHeight) drawMap(false);
632
+ });
633
+ mapSizes.observe(map);
634
+
635
+ // setLook shows the map or the grid, the reader's pick for every card;
636
+ // the place eases to the other's height, the map's tiles come in
637
+ const setLook = (k) => {
638
+ if (viz.dataset.look === k) return;
639
+ ctxLook = k;
640
+ try { localStorage.setItem("magpie.ctxLook", k); } catch {}
641
+ const quiet = opts.still || calm();
642
+ const h0 = viz.offsetHeight;
643
+ tip.hidden = true;
644
+ unfocus();
645
+ viz.dataset.look = k;
646
+ for (const b of card.querySelectorAll(".ctx-look-b")) {
647
+ const on = b.dataset.k === k;
648
+ b.classList.toggle("on", on);
649
+ b.setAttribute("aria-pressed", String(on));
650
+ }
651
+ if (k === "map") { for (const n of tiles.values()) n.remove(); tiles.clear(); drawMap(true); }
652
+ if (quiet || !viz.animate) return;
653
+ const h1 = viz.offsetHeight;
654
+ if (h0 !== h1) {
655
+ viz.style.overflow = "hidden";
656
+ viz.animate([{ height: h0 + "px" }, { height: h1 + "px" }], { duration: 520, easing: IOS_EASE })
657
+ .finished.finally(() => { viz.style.overflow = ""; }).catch(() => {});
658
+ }
659
+ };
660
+
661
+ grid.addEventListener("pointerover", (e) => { if (cur && e.target.parentElement === grid) showTip(e.target); });
662
+ grid.addEventListener("pointerleave", () => { tip.hidden = true; delete grid.dataset.f; });
663
+ // a cell clicked opens the text of what it shows (masaka on Discord)
664
+ grid.addEventListener("click", (e) => {
665
+ if (!cur || e.target.parentElement !== grid) return;
666
+ const c = cur.cells[[...grid.children].indexOf(e.target)];
667
+ if (!c) return;
668
+ e.stopPropagation();
669
+ const items = [...(cur.partOf.get(c.kind)?.items || [])].sort((a, b) => b.tokens - a.tokens);
670
+ tip.hidden = true;
671
+ openCtxText(rid, c.kind, items[c.item]);
672
+ });
673
+
674
+ // the contents, part by part
675
+ const drawRows = () => {
676
+ const { cells, partOf, p } = cur;
677
+ let rows;
678
+ if (tab === "all") {
679
+ rows = p.parts.flatMap((x) => (x.items || []).map((it, i) => ({ it, i, kind: x.kind })));
680
+ } else {
681
+ const x = partOf.get(tab);
682
+ rows = (x?.items || []).map((it, i) => ({ it, i, kind: tab }));
683
+ }
684
+ rows.sort((a, b) => b.it.tokens - a.it.tokens);
685
+ const most = Math.max(1, ...rows.map((x) => x.it.tokens));
686
+ const n = more ? rows.length : Math.min(rows.length, 8);
687
+ const out = rows.slice(0, n).map(({ it, i, kind }) => {
688
+ const row = el("div", "ctx-row");
689
+ const tg = tagOf(it);
690
+ const bar = el("span", "ctx-mini");
691
+ const b = el("i", "k-" + kind);
692
+ b.style.width = Math.max(2, (it.tokens / most) * 100).toFixed(1) + "%";
693
+ bar.append(b);
694
+ row.append(el("i", "dot k-" + kind), itemLabel(it, kind));
695
+ if (it.n > 1 && it.tag !== "turns" && it.tag !== "more") row.append(el("span", "ctx-x", "×" + it.n));
696
+ if (tg) row.append(el("code", "ctx-tag", tg));
697
+ row.append(bar, el("span", "ctx-v", fmtK(it.tokens)));
698
+ // clicked, its text
699
+ clickable(row, () => openCtxText(rid, kind, it));
700
+ // its cells light up
701
+ row.onpointerenter = () => {
702
+ grid.dataset.f = "item";
703
+ for (const [j, c] of cells.entries()) grid.children[j].classList.toggle("on", !!c && c.kind === kind && c.item === i);
704
+ map.dataset.f = "tile";
705
+ for (const n of tiles.values()) n.classList.toggle("on", n._tile.kind === kind && !!n._tile.its?.includes(it));
706
+ };
707
+ row.onpointerleave = unfocus;
708
+ return row;
709
+ });
710
+ if (!rows.length) out.push(el("div", "ctx-empty", t("Nothing in it")));
711
+ if (rows.length > 8) {
712
+ const b = el("button", "text ctx-more", more ? t("Show fewer") : t("Show all {n}", { n: rows.length }));
713
+ b.type = "button";
714
+ b.onclick = () => { more = !more; drawRows(); };
715
+ out.push(b);
716
+ }
717
+ morphKids(list, out);
718
+ };
719
+
720
+ const fill = (r, opts) => {
721
+ const p = r.prompt;
722
+ if (!p) {
723
+ card.replaceChildren();
724
+ parts = cur = null;
725
+ return;
726
+ }
727
+ rid = r;
728
+ const window = p.window || 0;
729
+ const full = window ? p.tokens / window : 0;
730
+ const live = !r.done;
731
+
732
+ // the head: what it is and how it was counted
733
+ if (opts.foldable) card.classList.toggle("shut", ctxShut && !opts.open);
734
+ const head = el("div", "ctx-head");
735
+ let title = el("span", "ctx-title", t("Context window"));
736
+ if (opts.foldable) {
737
+ const fold = el("button", "ctx-fold");
738
+ fold.type = "button";
739
+ fold.setAttribute("aria-expanded", String(!card.classList.contains("shut")));
740
+ const tw = el("span", "tw");
741
+ tw.append(svg(CHEV, 10, 1.7));
742
+ fold.append(tw, title);
743
+ // the head is morphed, so the button that is there is the target
744
+ fold.onclick = (e) => {
745
+ ctxShut = !card.classList.contains("shut");
746
+ card.classList.toggle("shut", ctxShut);
747
+ tip.hidden = true;
748
+ e.currentTarget.setAttribute("aria-expanded", String(!ctxShut));
749
+ try { localStorage.setItem("magpie.ctxShut", ctxShut ? "1" : "0"); } catch {}
750
+ opts.onFold?.(ctxShut);
751
+ };
752
+ title = fold;
753
+ }
754
+ // the state's words are all there, one shown: the pill is as wide as
755
+ // the widest in any language, so the model beside it doesn't move as
756
+ // a request goes from Live to Counted (dumplings on Discord: each
757
+ // request made the card jitter)
758
+ const state = live ? "live" : p.counted ? "counted" : "est";
759
+ const st = el("span", "ctx-state " + state);
760
+ const words = el("span", "ctx-state-w");
761
+ for (const [k, w] of [["live", "Live"], ["counted", "Counted"], ["est", "Estimated"]]) {
762
+ const word = el("span", k === state ? "on" : "", t(w));
763
+ if (k !== state) word.setAttribute("aria-hidden", "true");
764
+ words.append(word);
765
+ }
766
+ st.append(el("i"), words);
767
+ st.title = t(live ? "The request is on its way: the prompt is estimated from what the agent sent"
768
+ : p.counted ? "As many tokens as the vendor counted; the parts are measured from the request and scaled to it"
769
+ : "Estimated from what the agent sent: the vendor didn't say how many tokens it read");
770
+ head.append(title);
771
+ if (opts.foldable) {
772
+ const short = el("span", "ctx-short");
773
+ short.append(num("span", "", p.tokens), window ? " / " + fmtK(window) + " · " + pct(full) : "");
774
+ head.append(short);
775
+ // folded, the bar: morphed, so a live request moves widths only
776
+ head.append(stackBar(p));
777
+ }
778
+ head.append(el("span", "grow"));
779
+ // whose request it is and the model it went to; in a narrow card the
780
+ // agent's name goes first, its icon stays
781
+ const route = el("span", "ctx-route");
782
+ if (r.agent) {
783
+ const who = el("span", "ctx-who");
784
+ who.append(agentIcon(r.agent), el("span", "ctx-who-n", agentLabel(r.agent)));
785
+ route.append(who);
786
+ }
787
+ if (r.model) {
788
+ const what = el("span", "ctx-what");
789
+ what.append(...labIcon(r.model), el("code", "ctx-model", r.model));
790
+ if (r.agent) route.append(el("span", "ctx-sep", "›"));
791
+ route.append(what);
792
+ }
793
+ // the route and the state go together: where the title leaves them
794
+ // too little room they take a line of their own, the model id cut
795
+ // to fit there, and nothing is drawn over the title (packinggoods on
796
+ // Discord: in a narrow window the provider's icon sat on it)
797
+ const end = el("span", "ctx-end");
798
+ end.append(route, st);
799
+ head.append(end);
800
+
801
+ // how full, and the cache
802
+ const top = el("div", "ctx-top");
803
+ const used = el("div", "ctx-stat");
804
+ const k = el("div", "ctx-k");
805
+ k.append(el("span", "", t("Context used")));
806
+ if (window) {
807
+ const [tone, word] = health(full);
808
+ const h = el("span", "ctx-health " + tone, t(word));
809
+ k.append(h);
810
+ }
811
+ const big = el("div", "ctx-big");
812
+ big.append(num("b", "", p.tokens));
813
+ big.append(el("span", "", window ? "/ " + fmtK(window) + " " + t("tokens") : t("tokens")));
814
+ const sub = el("div", "ctx-sub");
815
+ if (window) {
816
+ const bits = [t("{pct} full", { pct: pct(full) })];
817
+ const turns = opts.series ? headroom(opts.series, Math.max(0, window - p.tokens)) : null;
818
+ if (turns !== null) bits.push(turns >= 999 ? t("plenty of headroom") : t("~{n} requests of headroom", { n: turns }));
819
+ else bits.push(t("{tokens} free", { tokens: fmtK(Math.max(0, window - p.tokens)) }));
820
+ sub.textContent = bits.join(" · ");
821
+ } else sub.textContent = t("the model's window isn't known");
822
+ used.append(k, big, sub);
823
+ top.append(used);
824
+
825
+ const cache = cacheOf(r) || opts.cache;
826
+ // another agent or session drawn in the card has no last request's
827
+ // cache to show: its place stays, with a dash till it is read
828
+ const whose = r.agent + " " + (opts.series?.[0]?.id ?? "");
829
+ if (whose !== cacheWhose) lastCache = null;
830
+ cacheWhose = whose;
831
+ const known = cache && p.counted !== false ? cache : null;
832
+ if (known || cacheShown) {
833
+ // once shown, the cache keeps its place, so nothing under it moves
834
+ // up and back down. A request under way has read no cache yet:
835
+ // the last request's stays, dimmed and named as the last one's,
836
+ // until this one's answer says (dumplings on Discord: each new
837
+ // request flashed it to "—" and an empty bar, the dash against
838
+ // 命中). Nothing to show is said in words, with no bar
839
+ const shown = known || (live ? lastCache : null);
840
+ const c = el("div", "ctx-stat ctx-cache" + (!known && shown ? " was" : shown ? "" : " none"));
841
+ c.append(el("div", "ctx-k", t("Prompt cache")));
842
+ const cb = el("div", "ctx-big");
843
+ cb.append(el("b", "", shown ? pct(shown.read / shown.total) : "—"), el("span", "", t("hit")));
844
+ const bar = el("div", "ctx-bar");
845
+ const fillBar = el("i");
846
+ fillBar.style.width = (shown ? (shown.read / shown.total) * 100 : 0).toFixed(1) + "%";
847
+ bar.append(fillBar);
848
+ c.append(cb, bar, el("div", "ctx-sub", known ? t("{cached} cached · {fresh} new", { cached: fmtK(known.read), fresh: fmtK(known.total - known.read) })
849
+ : shown ? t("The last request's · this one is on its way")
850
+ : live ? t("Read once the vendor answers") : t("The vendor didn't say")));
851
+ top.append(c);
852
+ cacheShown = true;
853
+ if (known) lastCache = known;
854
+ }
855
+
856
+ // the grid: its cells made once, each patched to what it shows now
857
+ grid.setAttribute("aria-label", t("{used} of {window} tokens used", { used: fmtK(p.tokens), window: fmtK(window || p.tokens) }));
858
+ const cells = cellsOf(p, window);
859
+ const partOf = new Map(p.parts.map((x) => [x.kind, x]));
860
+ const made = grid.children.length;
861
+ cells.forEach((c, i) => {
862
+ let e = grid.children[i];
863
+ if (!e) {
864
+ e = el("i");
865
+ if (!made && !opts.still) e.style.setProperty("--d", Math.round(i * 1.4) + "ms");
866
+ grid.append(e);
867
+ }
868
+ const cls = c ? "k-" + c.kind : "free";
869
+ if (e.className !== cls) e.className = cls;
870
+ if (!c) delete e.dataset.it;
871
+ else if (e.dataset.it !== String(c.item)) e.dataset.it = c.item;
872
+ });
873
+ cur = { p, window, cells, partOf };
874
+ // a tile pointed at keeps its tip through a live request's draws
875
+ const over = !tip.hidden && map.dataset.f === "tile" ? [...tiles.values()].find((n) => n.classList.contains("on")) : null;
876
+ if (!tip.hidden && !over) tip.hidden = true;
877
+ map.setAttribute("aria-label", grid.getAttribute("aria-label"));
878
+ drawMap(false);
879
+ if (over) followTip(over);
880
+
881
+ // the legend: a part hovered lights its cells
882
+ const legend = el("div", "ctx-legend");
883
+ for (const kind of PARTS) {
884
+ const part = partOf.get(kind);
885
+ if (!part || part.tokens <= 0) continue;
886
+ const li = el("span", "ctx-leg");
887
+ li.title = t(PART_NOTES[kind]);
888
+ li.append(el("i", "dot k-" + kind), el("span", "n", partName(kind)), num("span", "v", part.tokens));
889
+ clickable(li, () => openCtxText(rid, kind));
890
+ li.onpointerenter = () => focusOn(kind);
891
+ li.onpointerleave = unfocus;
892
+ legend.append(li);
893
+ }
894
+ if (window && window > p.tokens) {
895
+ const li = el("span", "ctx-leg");
896
+ li.append(el("i", "dot free"), el("span", "n", t("Free space")), num("span", "v", window - p.tokens));
897
+ li.onpointerenter = () => focusOn("free");
898
+ li.onpointerleave = unfocus;
899
+ legend.append(li);
900
+ }
901
+ // the look: the map or the grid
902
+ const look = el("span", "ctx-look");
903
+ look.setAttribute("role", "group");
904
+ for (const [k, name, ic] of [["map", "Context map", LOOK_MAP], ["grid", "Block grid", LOOK_GRID]]) {
905
+ const on = viz.dataset.look === k;
906
+ const b = el("button", "ctx-look-b" + (on ? " on" : ""));
907
+ b.type = "button";
908
+ b.dataset.k = k;
909
+ b.title = t(name);
910
+ b.setAttribute("aria-label", t(name));
911
+ b.setAttribute("aria-pressed", String(on));
912
+ b.innerHTML = ic;
913
+ b.onclick = (e) => { e.stopPropagation(); setLook(k); };
914
+ look.append(b);
915
+ }
916
+ legend.append(look);
917
+
918
+ // the contents' tabs, drawn again only when they change
919
+ const tabs = [["all", t("All")], ...PARTS.filter((k) => partOf.get(k)?.tokens > 0).map((k) => [k, partName(k)])];
920
+ if (!tabs.some(([id]) => id === tab)) { tab = "all"; more = false; }
921
+ const key = JSON.stringify([tabs, tab]);
922
+ if (key !== chKey) {
923
+ chKey = key;
924
+ ch.replaceChildren(el("span", "ctx-k", t("Contents")), el("span", "grow"),
925
+ segs(tabs, tab, (id) => { tab = id; more = false; chKey = JSON.stringify([tabs, tab]); opts.onTab?.(id); drawRows(); }));
926
+ }
927
+ drawRows();
928
+
929
+ // the foot: where it is, and the session's line
930
+ const foot = el("div", "ctx-foot");
931
+ const crumbs = el("code", "ctx-crumbs");
932
+ crumbs.textContent = (opts.crumbs || [agentLabel(r.agent)]).filter(Boolean).join(" › ");
933
+ const notes = [];
934
+ if (p.held) notes.push(t("Earlier turns are held by the vendor"));
935
+ if (p.turns) notes.push(count(p.turns, "1 turn", "{n} turns"));
936
+ foot.append(crumbs, el("span", "grow"));
937
+ if (notes.length) foot.append(el("span", "ctx-note", notes.join(" · ")));
938
+ let sp = null;
939
+ if (opts.series && opts.series.length > 1) {
940
+ sp = el("div", "ctx-session");
941
+ sp.append(el("span", "ctx-k", t("This session")), spark(opts.series, r.id, window, opts.onPoint),
942
+ el("span", "ctx-sub", count(opts.series.length, "1 request", "{n} requests")));
943
+ }
944
+
945
+ if (!parts) {
946
+ parts = { head, top, legend, foot, sp };
947
+ card.append(head, top, viz, legend, contents, foot, ...(sp ? [sp] : []), tip);
948
+ return;
949
+ }
950
+ for (const k of ["head", "top", "legend", "foot"]) parts[k] = morph(parts[k], { head, top, legend, foot }[k]);
951
+ if (sp && parts.sp) parts.sp = morph(parts.sp, sp);
952
+ else if (sp) card.insertBefore(parts.sp = sp, tip);
953
+ else if (parts.sp) { parts.sp.remove(); parts.sp = null; }
954
+ };
955
+ card.ctxUpdate = fill;
956
+ fill(r, opts);
957
+ return card;
958
+ }
959
+ window.ctxCard = ctxCard;
960
+
961
+ // clickable makes e a button for the keyboard too, its handlers set as
962
+ // properties, so a morphed card keeps them
963
+ function clickable(e, go) {
964
+ e.setAttribute("role", "button");
965
+ e.tabIndex = 0;
966
+ e.classList.add("ctx-click");
967
+ e.onclick = (ev) => { ev.stopPropagation(); go(); };
968
+ e.onkeydown = (ev) => {
969
+ if (ev.key !== "Enter" && ev.key !== " ") return;
970
+ ev.preventDefault();
971
+ go();
972
+ };
973
+ }
974
+
975
+ // ---------- the text of a part (masaka on Discord) ----------
976
+
977
+ // A part of the card clicked opens what the request sent there, read
978
+ // from the request's body: an item alone (a tool, a file, a turn), or
979
+ // the part's items, each opened to its text (ctxtext.js, loaded then)
980
+ const openCtxText = (...a) => import("./ctxtext.js").then((m) => m.open({ fmtK, partName, itemName, itemLabel, tagOf }, ...a)).catch(statusErr);
981
+ window.openCtxText = openCtxText;
982
+ window.ctxFmt = fmtK;
983
+
984
+ // ---------- the Usage page's Context tab ----------
985
+
986
+ const CTX_RANGES = [["1", "Today"], ["7", "7 days"], ["30", "30 days"]];
987
+ let ctxDays = "7";
988
+ try { const d = localStorage.getItem("magpie.ctxDays"); if (CTX_RANGES.some(([id]) => id === d)) ctxDays = d; } catch {}
989
+ let ctxData = null, ctxJSON = "", ctxRead = 0, ctxOpen = "", ctxAgent = "all";
990
+ // the days the pane shows "Reading…" for: a second read before the history
991
+ // answers (the page opened, then the first state's refresh) leaves it be,
992
+ // so the advice drawn meanwhile keeps its entrance
993
+ let ctxWaiting = "";
994
+
995
+ const SCORE_NAMES = { cache: "Cache", lean: "Lean start", pace: "Pace", reliable: "Reliability" };
996
+ const SCORE_NOTES = {
997
+ cache: "How much of its prompts the vendor read from its cache: full marks at 90%",
998
+ lean: "What every request starts with — system prompt, tools, memory — against the window: full marks at 5% or less",
999
+ pace: "How much a request adds to the one before it: full marks at 1% of the window or less",
1000
+ reliable: "Its requests that didn't fail: none at 20% failing",
1001
+ };
1002
+ const TAGS = {
1003
+ "cache-friendly": ["Cache-friendly", (v) => t("{pct} of its prompts were read from the vendor's cache", { pct: pct(v) })],
1004
+ "cache-misses": ["Cache misses", (v) => t("Only {pct} of its prompts were read from the cache: each request pays for most of its prompt again", { pct: pct(v) })],
1005
+ lean: ["Lean start", (v) => t("Every request starts with {tokens} of system prompt, tools and memory", { tokens: fmtK(v) })],
1006
+ "heavy-start": ["Heavy start", (v) => t("Every request starts with {tokens} of system prompt, tools and memory, before a word of yours", { tokens: fmtK(v) })],
1007
+ "mcp-heavy": ["MCP-heavy", (v) => t("MCP servers' tool definitions take {tokens} of every request", { tokens: fmtK(v) })],
1008
+ "tool-heavy": ["Tool-heavy", (v) => t("Tool results are {pct} of its prompts", { pct: pct(v) })],
1009
+ "file-reader": ["Reads files", (v) => t("Files it read are {pct} of its prompts", { pct: pct(v) })],
1010
+ "memory-heavy": ["Memory-heavy", (v) => t("Instruction files and memory are {pct} of its prompts", { pct: pct(v) })],
1011
+ "fills-fast": ["Fills fast", (v) => t("A request adds {tokens} to the one before it, the median", { tokens: fmtK(v) })],
1012
+ "near-full": ["Runs near full", (v) => t("A session reached {pct} of its window", { pct: pct(v) })],
1013
+ compacts: ["Compacts", (v) => t("Its context was compacted {n} times", { n: v })],
1014
+ steady: ["Steady", () => t("Fewer than 1% of its requests failed")],
1015
+ flaky: ["Flaky", (v) => t("{pct} of its requests failed", { pct: pct(v) })],
1016
+ };
1017
+ const grade = (s) => s >= 85 ? ["great", "Excellent"] : s >= 70 ? ["good", "Good"] : s >= 50 ? ["fair", "Fair"] : ["poor", "Needs care"];
1018
+
1019
+ function ring(score) {
1020
+ const r = 26, c = 2 * Math.PI * r;
1021
+ const [tone] = grade(score);
1022
+ const box = el("div", "ctx-ring " + tone);
1023
+ const s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1024
+ s.setAttribute("viewBox", "0 0 64 64");
1025
+ s.innerHTML = `<circle cx="32" cy="32" r="${r}" class="track"/><circle cx="32" cy="32" r="${r}" class="arc" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${c.toFixed(2)}" style="--to:${(c * (1 - score / 100)).toFixed(2)}"/>`;
1026
+ box.append(s, el("b", "", String(score)));
1027
+ return box;
1028
+ }
1029
+
1030
+ function shareBar(shares) {
1031
+ const bar = el("div", "ctx-share");
1032
+ for (const k of PARTS) {
1033
+ const v = shares?.[k] || 0;
1034
+ if (v <= 0.002) continue;
1035
+ const i = el("i", "k-" + k);
1036
+ i.style.flexGrow = v;
1037
+ i.title = partName(k) + " · " + pct(v);
1038
+ bar.append(i);
1039
+ }
1040
+ return bar;
1041
+ }
1042
+
1043
+ function agentCard(a) {
1044
+ const card = el("div", "ctx-agent");
1045
+ const head = el("div", "ctx-agent-head");
1046
+ const who = el("div", "ctx-who");
1047
+ who.append(agentIcon(a.agent), el("b", "", agentLabel(a.agent)));
1048
+ const cl = a.clients || [];
1049
+ const meta = el("div", "ctx-who-sub", [...(cl.length ? [cl.slice(0, 3).join(", ") + (cl.length > 3 ? " +" + (cl.length - 3) : "")] : []), count(a.requests, "1 request", "{n} requests"), count(a.sessions, "1 session", "{n} sessions")].join(" · "));
1050
+ if (cl.length) who.title = t("Requests whose User-Agent names only an HTTP library or an SDK, not an agent: {clients}", { clients: cl.join(", ") });
1051
+ const name = el("div", "ctx-who-box");
1052
+ name.append(who, meta);
1053
+ const [tone, word] = grade(a.score);
1054
+ const g = el("div", "ctx-grade");
1055
+ g.append(ring(a.score), el("span", "ctx-grade-word " + tone, t(word)));
1056
+ head.append(name, el("span", "grow"), g);
1057
+
1058
+ const scores = el("div", "ctx-scores");
1059
+ for (const s of a.scores || []) {
1060
+ const r = s.most ? s.points / s.most : 0;
1061
+ const row = el("div", "ctx-score " + (r >= 0.8 ? "ok" : r >= 0.5 ? "warn" : "bad"));
1062
+ row.title = t(SCORE_NOTES[s.key] || "");
1063
+ const bar = el("span", "ctx-bar");
1064
+ const f = el("i");
1065
+ f.style.width = ((s.points / s.most) * 100).toFixed(1) + "%";
1066
+ bar.append(f);
1067
+ row.append(el("span", "n", t(SCORE_NAMES[s.key] || s.key)), bar, el("span", "v", `${s.points}/${s.most}`));
1068
+ scores.append(row);
1069
+ }
1070
+
1071
+ const tags = el("div", "ctx-tags");
1072
+ for (const tg of a.tags || []) {
1073
+ const def = TAGS[tg.key];
1074
+ const chip = el("span", "ctx-chip " + tg.tone);
1075
+ chip.append(el("i"), def ? t(def[0]) : tg.key);
1076
+ chip.dataset.tt = def ? def[1](tg.value || 0) : tg.key;
1077
+ tags.append(chip);
1078
+ }
1079
+
1080
+ const facts = el("div", "ctx-facts");
1081
+ const fact = (label, value, note) => {
1082
+ const f = el("div", "ctx-fact");
1083
+ f.append(el("span", "ctx-k", t(label)), el("b", "", value));
1084
+ if (note) f.dataset.tt = t(note);
1085
+ facts.append(f);
1086
+ };
1087
+ fact("Typical prompt", fmtK(a.median), "The median request's prompt");
1088
+ fact("Largest", fmtK(a.p90), "90% of its requests' prompts are this size or smaller");
1089
+ fact("Starts with", fmtK(a.baseline), "System prompt, tools and memory: what every request carries before the conversation");
1090
+ fact("Grows by", a.growth ? "+" + fmtK(a.growth) : "—", "How much a request's prompt adds to the one before it, the median");
1091
+ fact("Cache hit", a.scores?.find((s) => s.key === "cache") && a.cache ? pct(a.cache) : "—", "Of its prompts, what the vendor read from its cache");
1092
+ fact("Window size", a.window ? fmtK(a.window) : "—", "The largest context window of the models it used");
1093
+
1094
+ const mix = el("div", "ctx-mix");
1095
+ mix.append(el("span", "ctx-k", t("What its prompts hold")), shareBar(a.shares));
1096
+ const keys = el("div", "ctx-mix-keys");
1097
+ for (const k of PARTS) {
1098
+ const v = a.shares?.[k] || 0;
1099
+ if (v < 0.01) continue;
1100
+ const s = el("span", "");
1101
+ s.append(el("i", "dot k-" + k), partName(k) + " " + pct(v));
1102
+ keys.append(s);
1103
+ }
1104
+ mix.append(keys);
1105
+
1106
+ const foot = el("div", "ctx-agent-foot");
1107
+ const models = el("span", "ctx-models");
1108
+ // by the model's own name: "live/claude/claude-sonnet-4-5" cut short
1109
+ // read the same for each of its models; the whole id in its tooltip
1110
+ for (const m of a.models || []) {
1111
+ const c = el("code", "", m.split("/").pop());
1112
+ c.title = m;
1113
+ models.append(c);
1114
+ }
1115
+ foot.append(models, el("span", "grow"));
1116
+ if (a.latestId) {
1117
+ const go = el("button", "text", t("Latest request"));
1118
+ go.type = "button";
1119
+ go.dataset.feature = "routing"; // it opens on the Routing page
1120
+ go.onclick = () => window.openRoute(a.latestId, a.latestTime, { context: true }).catch(statusErr);
1121
+ foot.append(go);
1122
+ }
1123
+ card.append(head, scores, tags, facts, mix, foot);
1124
+ return card;
1125
+ }
1126
+
1127
+ function sessionRow(s) {
1128
+ const open = ctxOpen === s.agent + "\x00" + s.key;
1129
+ const box = el("div", "ctx-sess" + (open ? " open" : ""));
1130
+ const row = el("button", "ctx-sess-row");
1131
+ row.type = "button";
1132
+ row.setAttribute("aria-expanded", String(open));
1133
+ const name = el("span", "ctx-sess-name");
1134
+ // an untitled session goes by the head of its id, the way the footer names it
1135
+ name.append(s.title ? el("b", "", s.title) : el("b", "raw", s.key.slice(0, 8)));
1136
+ name.append(el("code", "", s.model));
1137
+ const fill = s.window ? s.peak / s.window : 0;
1138
+ const [tone] = health(fill);
1139
+ const peak = el("span", "ctx-sess-peak " + tone, s.window ? pct(fill) : fmtK(s.peak));
1140
+ peak.dataset.tt = t("Its largest prompt: {tokens} of {window}", { tokens: fmtK(s.peak), window: fmtK(s.window) });
1141
+ const meta = el("span", "ctx-sess-meta", count(s.requests, "1 request", "{n} requests") + (s.compacts ? " · " + count(s.compacts, "1 compaction", "{n} compactions") : ""));
1142
+ row.append(agentIcon(s.agent), name, spark(s.points, s.latestId, s.window), peak, meta, el("span", "ctx-sess-when", ago(s.last)));
1143
+ row.onclick = () => {
1144
+ ctxOpen = open ? "" : s.agent + "\x00" + s.key;
1145
+ renderContext();
1146
+ };
1147
+ box.append(row);
1148
+ if (open && s.latest) {
1149
+ const last = s.points[s.points.length - 1];
1150
+ const r = { id: s.latestId, time: s.last, done: true, agent: s.agent, model: s.model, prompt: s.latest };
1151
+ const cache = last && s.latest.counted && last.tokens && !last.cache_unknown ? { read: last.cache || 0, total: last.tokens } : null;
1152
+ box.append(ctxCard(r, {
1153
+ still: true, cache, place: "session",
1154
+ series: s.points,
1155
+ crumbs: [agentLabel(s.agent), s.title || s.key.slice(0, 12), t("latest request")],
1156
+ onPoint: (pt) => { if (!featuresOff.has("routing")) window.openRoute(pt.id, pt.time, { context: true }).catch(statusErr); },
1157
+ }));
1158
+ }
1159
+ return box;
1160
+ }
1161
+
1162
+
1163
+ // ---------- tuned for you: the settings that would have spent the fewest tokens ----------
1164
+
1165
+ let tuneData = null, tuneJSON = "", tuneRead = 0, tuneBusy = "";
1166
+ // whether the last draw showed the history and the advice: what was on
1167
+ // the pane already doesn't play its entrance again when the pane is
1168
+ // redrawn (the other answering, the state, a filter, a click)
1169
+ let ctxDrawn = false, tuneDrawn = false;
1170
+
1171
+ const KNOBS = {
1172
+ compact: "Auto-compact",
1173
+ cache_ttl: "Prompt cache · main conversation",
1174
+ subagent_cache_ttl: "Prompt cache · subagents",
1175
+ };
1176
+ const ttlName = (v) => v === "1h" ? t("1 hour") : v === "5m" ? t("5 min") : v || "—";
1177
+
1178
+ // chart draws what each setting would have cost over the range, as the
1179
+ // tokens it spends past the advice: the area over the advice's level is
1180
+ // what the others waste, and the one set now says how much
1181
+ let chartN = 0;
1182
+ // how far apart, as a share of the axis, two of its labels must sit: a
1183
+ // label is about 30px wide, the narrowest card's axis about 280px
1184
+ const TICK_GAP = 0.16;
1185
+ function chart(pts, current, best, show, timed) {
1186
+ if (pts.length < 2) return null;
1187
+ const near = (v) => pts.reduce((a, p) => Math.abs(p.value - v) < Math.abs(a.value - v) ? p : a, pts[0]);
1188
+ const now = typeof current === "number" ? near(current) : pts.find((p) => p.value === current);
1189
+ const top = typeof best === "number" ? near(best) : pts.find((p) => p.value === best);
1190
+ if (!top) return null;
1191
+ // measured from the advice, not the cheapest point: tune takes the
1192
+ // largest window within 1% of it, and those cost nothing more here.
1193
+ // Timed, the advice may pass a cheaper window that waits longer, so
1194
+ // each is said against the cheapest
1195
+ const low = timed ? Math.min(...pts.map((p) => p.cost)) : top.cost;
1196
+ const extra = (p) => Math.max(0, p.cost - low);
1197
+ const most = Math.max(...pts.map(extra));
1198
+ if (!(most > 0)) return null;
1199
+ const worst = pts.reduce((a, p) => p.cost > a.cost ? p : a, pts[0]);
1200
+ const W = 300, H = 100, top0 = 22, base = 84;
1201
+ const X = (i) => (i / (pts.length - 1)) * W;
1202
+ const Y = (p) => base - (extra(p) / most) * (base - top0);
1203
+ const xy = pts.map((p, i) => [X(i), Y(p)]);
1204
+ // a smooth line through every point that never swings past its neighbours
1205
+ let d = `M${xy[0][0].toFixed(1)} ${xy[0][1].toFixed(1)}`;
1206
+ for (let i = 1; i < xy.length; i++) {
1207
+ const [x0, y0] = xy[i - 1], [x1, y1] = xy[i], m = (x1 - x0) / 2;
1208
+ d += ` C${(x0 + m).toFixed(1)} ${y0.toFixed(1)} ${(x1 - m).toFixed(1)} ${y1.toFixed(1)} ${x1.toFixed(1)} ${y1.toFixed(1)}`;
1209
+ }
1210
+ const id = "ctx-waste-" + ++chartN;
1211
+ const box = el("div", "ctx-tune-chart");
1212
+ const s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1213
+ s.setAttribute("viewBox", `0 0 ${W} ${H}`);
1214
+ s.setAttribute("preserveAspectRatio", "none");
1215
+ s.innerHTML = `<defs><linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1">` +
1216
+ `<stop offset="0" class="hot"/><stop offset="1" class="cool"/></linearGradient></defs>` +
1217
+ `<line class="level" x1="0" x2="${W}" y1="${base}" y2="${base}"/>` +
1218
+ `<path class="area" fill="url(#${id})" d="${d} L${W} ${base} L0 ${base} Z"/>` +
1219
+ `<path class="line" d="${d}"/>`;
1220
+ const plot = el("div", "ctx-tc-plot");
1221
+ plot.append(s);
1222
+ box.append(plot);
1223
+ const at = (p, cls, label) => {
1224
+ const i = pts.indexOf(p);
1225
+ const dot = el("span", "ctx-tc-dot " + cls);
1226
+ dot.style.left = (i / (pts.length - 1)) * 100 + "%";
1227
+ dot.style.top = (Y(p) / H) * 100 + "%";
1228
+ if (label) {
1229
+ const tag = el("b", "ctx-tc-tag", label);
1230
+ if (i === 0) tag.classList.add("start");
1231
+ if (i === pts.length - 1) tag.classList.add("end");
1232
+ dot.append(tag);
1233
+ }
1234
+ dot.dataset.tt = p === top
1235
+ ? timed ? t("{value}: the fewest tokens without waiting longer on compactions", { value: show(p.value) })
1236
+ : t("{value}: the fewest tokens for how you work", { value: show(p.value) })
1237
+ : t("{value}: {tokens} more tokens over the range (+{pct})", { value: show(p.value), tokens: fmtK(extra(p)), pct: pct(extra(p) / low) });
1238
+ plot.append(dot);
1239
+ };
1240
+ // the advice's value, the setting now, the range's ends: one too close
1241
+ // to one before is left off, so labels never overlap (#1549, Ti800)
1242
+ const along = (p) => pts.indexOf(p) / (pts.length - 1);
1243
+ // the tags the same way: Leanest, the setting now, the costliest; one
1244
+ // near a tag that matters more keeps its dot, not its tag (#1549)
1245
+ const tagged = [];
1246
+ const tag = (p, label) => {
1247
+ if (tagged.some((k) => Math.abs(along(k) - along(p)) < TICK_GAP && Math.abs(Y(k) - Y(p)) < 20)) return "";
1248
+ tagged.push(p);
1249
+ return label;
1250
+ };
1251
+ const bestTag = tag(top, timed ? t("Tokens + wait") : t("Leanest"));
1252
+ const nowTag = now && now !== top ? tag(now, "+" + pct(extra(now) / low)) : "";
1253
+ const worstTag = worst !== now && worst !== top ? tag(worst, "+" + pct(extra(worst) / low)) : "";
1254
+ if (worst !== now && worst !== top) at(worst, "worst", worstTag);
1255
+ at(top, "best", bestTag);
1256
+ if (now && now !== top) at(now, "now", nowTag);
1257
+ const axis = el("div", "ctx-tc-x");
1258
+ const kept = [];
1259
+ for (const p of new Set([top, now, pts[0], pts[pts.length - 1]].filter(Boolean))) {
1260
+ if (kept.every((k) => Math.abs(along(k) - along(p)) >= TICK_GAP)) kept.push(p);
1261
+ }
1262
+ for (const p of new Set([pts[0], top, now, pts[pts.length - 1]].filter(Boolean))) {
1263
+ const i = pts.indexOf(p);
1264
+ if (!kept.includes(p)) continue;
1265
+ const tick = el("span", (p === top ? "best" : p === now ? "now" : "") + (i === 0 ? " start" : i === pts.length - 1 ? " end" : ""), show(p.value));
1266
+ tick.style.left = (i / (pts.length - 1)) * 100 + "%";
1267
+ axis.append(tick);
1268
+ }
1269
+ box.append(axis);
1270
+ box.dataset.tt = t("Tokens each setting would have spent past the leanest, on your own calls");
1271
+ return box;
1272
+ }
1273
+
1274
+ function adviceRow(agent, a) {
1275
+ // few: put in by tune, too few calls to advise on; kept for Undo (linkaww)
1276
+ const r = a.compact || a.ttl || { current: a.setting.value, best: a.setting.value, cost: 0, low: 0 };
1277
+ const ttl = a.knob != "compact";
1278
+ const show = (v) => ttl ? (v ? ttlName(v) : t("Default")) : fmtK(+v);
1279
+ const cur = ttl ? r.current : r.current, best = r.best;
1280
+ const same = String(cur) === String(best);
1281
+ const saves = r.cost > 0 ? (r.cost - r.low) / r.cost : 0;
1282
+ const set = a.setting || {};
1283
+ // a model's own Compact at is where Codex runs, over Long conversations
1284
+ // (hoobnn on Discord: "In use" for 170K beside "Now 430K")
1285
+ const own = a.own;
1286
+ const held = !!(set.locked || own);
1287
+ const applied = !own && set.ours && String(set.value) === String(best);
1288
+ const row = el("div", "ctx-tune-row" + (same || applied ? " done" : ""));
1289
+
1290
+ const title = el("div", "ctx-tune-title");
1291
+ title.append(el("b", "", t(KNOBS[a.knob] || a.knob)));
1292
+ if (a.model) title.append(el("code", "", a.model));
1293
+ row.append(title);
1294
+
1295
+ const vals = el("div", "ctx-tune-vals");
1296
+ const val = (label, v, cls) => {
1297
+ const f = el("div", "ctx-tune-val " + cls);
1298
+ f.append(el("span", "ctx-k", t(label)), el("b", "", show(v)));
1299
+ return f;
1300
+ };
1301
+ vals.append(val("Now", cur, "now"));
1302
+ // named for what it is measured by, tokens alone, not "best" (#1854);
1303
+ // with the agent's compactions timed, tokens and the wait on them
1304
+ const timed = !!(a.compact && (r.facts || {}).took > 0);
1305
+ if (!same) vals.append(el("span", "ctx-tune-arrow", "→"), val(timed ? "Tokens + wait" : "Fewest tokens", best, "best"));
1306
+ row.append(vals);
1307
+ const c = a.compact
1308
+ ? chart(a.compact.curve || [], r.current, r.best, show, timed)
1309
+ : chart([{ value: "5m", cost: r.short }, { value: "1h", cost: r.hour }], r.current, r.best, show);
1310
+ if (c) row.append(c);
1311
+ // the leanest band: within 3% of the cheapest (yapechan on Discord)
1312
+ const curve = (a.compact && a.compact.curve) || [];
1313
+ const fewest = curve.length ? Math.min(...curve.map((p) => p.cost)) : 0;
1314
+ const band = curve.filter((p) => p.cost <= fewest * 1.03);
1315
+ const lean = same && timed && band.length && r.cost > fewest * 1.03 ? band[band.length - 1] : null;
1316
+ const bandName = lean ? (band[0] === lean ? show(lean.value) : show(band[0].value) + "–" + show(lean.value)) : "";
1317
+
1318
+ const why = el("p", "ctx-tune-why");
1319
+ const f = r.facts || {};
1320
+ if (a.few) why.textContent = t("Not enough calls in this range to tune on");
1321
+ else if (a.compact) {
1322
+ why.textContent = f.compacts
1323
+ ? t("Your conversations grow {growth} a call; a compaction leaves {after}, and reading back what it dropped costs {rework}. {n} compactions in the range.", { growth: fmtK(f.growth), after: fmtK(f.after), rework: fmtK(f.rework), n: f.compacts })
1324
+ : t("Your conversations grow {growth} a call, and none was compacted in the range.", { growth: fmtK(f.growth) });
1325
+ // how long the agent's own compactions took, from its files: what a
1326
+ // window that compacts more often would have kept the reader waiting
1327
+ if (timed) {
1328
+ const secs = f.took < 120 ? t("{n}s", { n: f.took }) : fmtDur(f.took);
1329
+ why.textContent += " " + (same
1330
+ ? t("Each compaction kept you waiting about {secs} ({n} timed). Over the range that's about {wait} at {value}.", { secs, n: f.timed, wait: fmtDur(r.wait || 0), value: show(cur) })
1331
+ : t("Each compaction kept you waiting about {secs} ({n} timed). Over the range that's about {wait} at {best}, against {now} at {value}.", { secs, n: f.timed, wait: fmtDur(r.wait_best || 0), best: show(best), now: fmtDur(r.wait || 0), value: show(cur) }));
1332
+ // the cheaper window the advice passed over, for waiting longer
1333
+ const cheap = (a.compact.curve || []).find((p) => p.value === r.cheapest);
1334
+ const adv = same ? r.cost : r.low;
1335
+ if (cheap && r.cheapest !== best && adv > 0 && (adv - cheap.cost) / adv >= 0.03 && (r.wait_cheapest || 0) > (same ? r.wait || 0 : r.wait_best || 0))
1336
+ why.textContent += " " + t("{value} would spend {pct} fewer tokens, but wait about {wait}.", { value: show(r.cheapest), pct: pct((adv - cheap.cost) / adv), wait: fmtDur(r.wait_cheapest) });
1337
+ }
1338
+ } else {
1339
+ why.textContent = t("{quick} of your calls came within 5 minutes of the one before, {pause} after a pause of 5 to 60 minutes.", { quick: pct(f.quick || 0), pause: pct(f.pause || 0) });
1340
+ }
1341
+ row.append(why);
1342
+ // one with its own Compact at, which Use doesn't move (hoobnn)
1343
+ for (const m of a.apart || []) row.append(el("p", "ctx-tune-caveat ctx-tune-apart", t("{model} compacts at its own {value}, which this doesn't move", { model: m.model, value: fmtK(m.value) })));
1344
+ // a window advised is weighed by tokens alone: what a compaction loses of
1345
+ // the task isn't measured, and a smaller one compacts more often (#1854)
1346
+ if (a.compact && (!same || lean)) row.append(el("p", "ctx-tune-caveat", timed
1347
+ ? t("Tokens and waiting only: it doesn't measure how well the agent keeps the task after a compaction. The lower the window, the more often it compacts.")
1348
+ : t("A token estimate only: it doesn't measure how well the agent keeps the task after a compaction. The lower the window, the more often it compacts.")));
1349
+
1350
+ const foot = el("div", "ctx-tune-foot");
1351
+ // the advice magpie put in is "In use" in every range, Now at it too
1352
+ // (hoobnn: over 7 and 30 days it read "Already the leanest")
1353
+ const verdict = !lean && (same || applied)
1354
+ ? el("span", "ctx-chip good", "")
1355
+ : el("span", "ctx-chip info", "");
1356
+ verdict.append(el("i"), lean ? t("Not the leanest: {range} spends the fewest tokens", { range: bandName })
1357
+ : applied ? t("In use") : same ? t("Already the leanest for how you work") : t("{pct} fewer tokens · {tokens} over the range", { pct: pct(saves), tokens: fmtK(Math.round(r.cost - r.low)) }));
1358
+ verdict.hidden = !!a.few && !applied;
1359
+ foot.append(verdict, el("span", "grow"));
1360
+ const note = el("div", "ctx-tune-src");
1361
+ title.after(note);
1362
+ if (set.locked) note.textContent = t("{name} is set and comes first", { name: set.locked });
1363
+ else if (own) note.textContent = t("{model} compacts at its own {value}, which comes first", { model: own.model, value: fmtK(own.value) });
1364
+ else if (set.source == "long") note.textContent = t("Long conversations, shared by Codex, Claude Code and Grok Build");
1365
+ else if (set.ours) note.textContent = t("Set by magpie");
1366
+ else if (set.value && a.compact && +set.value > a.compact.window && a.compact.window) note.textContent = t("You set {value}; the model's {window} comes first", { value: show(set.value), window: fmtK(a.compact.window) });
1367
+ else if (set.value) note.textContent = t("You set {value}", { value: show(set.value) });
1368
+ else note.textContent = t("The agent's default");
1369
+ const key = [agent, a.knob, a.model || ""].join("\x00");
1370
+ const send = async (path, value, knob = a.knob) => {
1371
+ tuneBusy = key;
1372
+ renderContext();
1373
+ try {
1374
+ await api("tune/" + path, { agent, knob, model: a.model || "", value: String(value ?? "") });
1375
+ status(path === "apply" ? t("Saved: {agent} reads it from its next session", { agent: agentLabel(agent) }) : t("Put back"), "ok");
1376
+ } catch (e) {
1377
+ statusErr(e);
1378
+ }
1379
+ tuneBusy = "";
1380
+ await loadTune(true);
1381
+ };
1382
+ if (set.ours) {
1383
+ const undo = el("button", "text", t("Undo"));
1384
+ undo.type = "button";
1385
+ undo.disabled = tuneBusy === key;
1386
+ undo.onclick = () => send("undo");
1387
+ foot.append(undo);
1388
+ }
1389
+ const use = (v, tt) => {
1390
+ const go = el("button", "text primary", t("Use {value}", { value: show(v) }));
1391
+ go.type = "button";
1392
+ go.disabled = tuneBusy === key;
1393
+ if (tt) go.dataset.tt = tt;
1394
+ go.onclick = () => send("apply", v);
1395
+ foot.append(go);
1396
+ };
1397
+ const yours = set.value && !set.ours && t("Replaces your {value}; Undo puts it back", { value: show(set.value) });
1398
+ if (!same && !applied && !held) use(best, yours);
1399
+ // an hour no Undo puts back, too few calls to weigh (linkaww)
1400
+ if (a.few && ttl && cur === "1h" && !held && !set.ours) use("5m", yours);
1401
+ if (lean && !held) use(lean.value, yours || t("Fewer tokens, but more compactions to wait on"));
1402
+ // a window of the reader's own (#1860), against the setting now (hayden4608)
1403
+ const pts = (a.compact && a.compact.curve) || [];
1404
+ if (pts.length > 1 && !held) {
1405
+ const other = el("button", "text ctx-tune-pick", t("Other window…"));
1406
+ other.type = "button";
1407
+ other.disabled = tuneBusy === key;
1408
+ other.setAttribute("aria-haspopup", "menu");
1409
+ other.setAttribute("aria-expanded", "false");
1410
+ other.onclick = (e) => {
1411
+ e.stopPropagation();
1412
+ // the fewest tokens: timed, the advice may spend over the cheapest
1413
+ // to wait no longer, so the cheapest
1414
+ const cheapest = pts.reduce((x, p) => p.cost < x.cost ? p : x, pts[0]);
1415
+ const fewest = timed ? cheapest.value : best;
1416
+ const now = set.ours && set.value != null ? +set.value : cur;
1417
+ const near = pts.reduce((x, p) => Math.abs(p.value - now) < Math.abs(x.value - now) ? p : x, pts[0]);
1418
+ // the setting now's cost: its own point, else the replay's at it
1419
+ const base = near.value === now ? near.cost : r.cost > 0 ? r.cost : near.cost;
1420
+ const opts = pts.map((p) => {
1421
+ const diff = p.cost - base, share = base > 0 ? Math.abs(diff) / base : 0;
1422
+ const spend = p.value === now ? t("Your setting now")
1423
+ : share < 0.005 ? t("As few tokens")
1424
+ : diff < 0 ? t("−{tokens} tokens (−{pct})", { tokens: fmtK(-diff), pct: pct(share) })
1425
+ : t("+{tokens} tokens (+{pct})", { tokens: fmtK(diff), pct: pct(share) });
1426
+ const often = p.compacts == null ? "" : p.compacts ? count(p.compacts, "about 1 compaction", "about {n} compactions") : t("no compaction");
1427
+ const wait = timed && p.compacts ? t("about {d} waiting", { d: fmtDur(p.wait || 0) }) : "";
1428
+ return { v: String(p.value), name: show(p.value), literalName: true, note: [spend, p.value === fewest ? t("Fewest tokens") : "", often, wait].filter(Boolean).join(" · ") };
1429
+ });
1430
+ openProtoMenu(other, opts, String(near.value), (v) => {
1431
+ if (v !== String(now)) send("apply", v);
1432
+ }, "Compact at · against the setting now, from your calls of the range", "ctx-tune-menu");
1433
+ };
1434
+ foot.append(other);
1435
+ }
1436
+ // Codex's background compaction at the advice, the limit kept (herman_hong)
1437
+ const as = a.async, aset = as?.setting || {}, cav = (s) => row.append(el("p", "ctx-tune-caveat ctx-tune-async", s));
1438
+ const btn = (label, go, c = "") => {
1439
+ const b = el("button", "text" + c, label);
1440
+ b.type = "button";
1441
+ b.disabled = tuneBusy === key;
1442
+ b.onclick = go;
1443
+ return foot.appendChild(b);
1444
+ };
1445
+ if (!as) {} else if (aset.locked) cav(t("Background compaction: Codex's config.toml sets {key}, which comes first", { key: aset.locked }));
1446
+ else if (as.held?.length) cav(codexHeld(as.held));
1447
+ else if (aset.ours && +aset.value === as.percent) {
1448
+ verdict.after(el("span", "ctx-chip good ctx-tune-async", t("In the background at {value}", { value: fmtK(as.value) })));
1449
+ verdict.nextSibling.prepend(el("i"));
1450
+ btn(t("Undo background"), () => send("undo", "", "async_compact"));
1451
+ } else btn(t("Use {value} in the background", { value: fmtK(as.value) }), () => send("apply", as.percent, "async_compact"), " ctx-tune-async").dataset.tt = t("Codex compacts in the background past {pct} of the model's window, about {value} of {window}, while the turn goes on; {now} stays as the foreground limit, for when the background one isn't ready. A Codex feature still under development, 0.163 or later.", { pct: as.percent + "%", value: fmtK(as.value), window: fmtK(as.window), now: show(cur) });
1452
+ row.append(foot);
1453
+ return row;
1454
+ }
1455
+
1456
+ function tuneCard(ag) {
1457
+ const card = el("div", "ctx-tune");
1458
+ const head = el("div", "ctx-agent-head");
1459
+ const who = el("div", "ctx-who");
1460
+ who.append(agentIcon(ag.agent), el("b", "", agentLabel(ag.agent)));
1461
+ const name = el("div", "ctx-who-box");
1462
+ name.append(who, el("div", "ctx-who-sub", count(ag.calls, "1 call", "{n} calls")));
1463
+ head.append(name, el("span", "grow"));
1464
+ const total = ag.advice.reduce((s, a) => { const r = a.compact || a.ttl || {}; return s + Math.max(0, r.cost - r.low || 0); }, 0);
1465
+ const all = ag.advice.reduce((s, a) => s + ((a.compact || a.ttl)?.cost || 0), 0);
1466
+ // a share that rounds to nothing is no promise
1467
+ if (all > 0 && total / all >= 0.01) head.append(el("span", "ctx-tune-total", t("Up to {pct} fewer", { pct: pct(total / all) })));
1468
+ card.append(head);
1469
+ for (const a of ag.advice) card.append(adviceRow(ag.agent, a));
1470
+ if (!ag.advice.length) card.append(el("p", "usage-note", t("Not enough calls in this range to tune on")));
1471
+ return card;
1472
+ }
1473
+
1474
+ function renderTune(pane) {
1475
+ const head = el("div", "row-head ctx-sess-head");
1476
+ head.append(el("span", "label", t("Tuned for you")), el("span", "grow"));
1477
+ pane.append(head);
1478
+ pane.append(el("p", "usage-note ctx-tune-note", t("Your own calls of the range, replayed setting by setting from your agents' session files: the settings that would have spent the fewest tokens, for how you work. Tokens are weighed as the vendor charges them: a cache read is a tenth of one.")));
1479
+ if (!tuneData) {
1480
+ pane.append(el("p", "usage-note", t("Working it out from your sessions…")));
1481
+ return;
1482
+ }
1483
+ const shown = tuneData.agents.filter((a) => a.advice.length);
1484
+ if (!shown.length) {
1485
+ pane.append(el("p", "usage-note", t("Not enough calls in this range to tune on")));
1486
+ return;
1487
+ }
1488
+ const grid = el("div", "ctx-agents ctx-tunes");
1489
+ for (const a of shown) grid.append(tuneCard(a));
1490
+ pane.append(grid);
1491
+ }
1492
+
1493
+ // what this page can draw: another version's reply can hold other rows
1494
+ // (Linka on Discord: a page from before few rows threw on one, "r.cost")
1495
+ const obj = (r) => !!r && typeof r == "object", objs = (l) => Array.isArray(l) ? l.filter(obj) : [];
1496
+
1497
+ async function loadTune(force) {
1498
+ const read = ++tuneRead, days = ctxDays;
1499
+ if (tuneData && tuneData.days !== +days) tuneData = null;
1500
+ const data = await api("tune?days=" + days);
1501
+ if (read !== tuneRead) return;
1502
+ const json = JSON.stringify(data);
1503
+ if (!force && tuneData && json === tuneJSON) return;
1504
+ tuneData = { days: +data.days || +days, agents: objs(data?.agents).map((a) => ({ ...a, advice: objs(a.advice).filter((r) => obj(r.compact) || obj(r.ttl) || r.few).map((r) => ({ ...r, setting: obj(r.setting) ? r.setting : {} })) })) }, tuneJSON = json;
1505
+ renderContext();
1506
+ }
1507
+
1508
+ function renderContext() {
1509
+ const pane = $("#contextPane");
1510
+ if (!pane) return;
1511
+ // the range sits in the Usage page's own head, where the other tabs keep
1512
+ // theirs, so no row of its own stands empty above the pane
1513
+ $("#ctxRange")?.replaceChildren(segs(CTX_RANGES.map(([id, n]) => [id, t(n)]), ctxDays, (id) => {
1514
+ ctxDays = id;
1515
+ try { localStorage.setItem("magpie.ctxDays", id); } catch {}
1516
+ loadContext();
1517
+ }));
1518
+ pane.replaceChildren();
1519
+ pane.classList.toggle("still", ctxDrawn && !!ctxData);
1520
+ pane.classList.toggle("tune-still", tuneDrawn && !!tuneData);
1521
+ ctxDrawn = !!ctxData, tuneDrawn = !!tuneData;
1522
+ renderTune(pane);
1523
+ if (!ctxData) {
1524
+ pane.append(el("p", "usage-note", t("Reading the routing history…")));
1525
+ return;
1526
+ }
1527
+ const seen = el("div", "row-head ctx-sess-head");
1528
+ seen.append(el("span", "label", t("Through magpie")), el("span", "grow"));
1529
+ pane.append(seen);
1530
+ pane.append(el("p", "usage-note", t("What each agent's prompts hold, as magpie's gateway read them: the score and tags are worked out from the requests of the range.")));
1531
+ if (!ctxData.agents.length) {
1532
+ const empty = el("div", "ctx-none");
1533
+ empty.append(el("b", "", t("No prompts read yet")), el("span", "", t("Requests your agents send through magpie show here.")));
1534
+ pane.append(empty);
1535
+ return;
1536
+ }
1537
+ const grid = el("div", "ctx-agents");
1538
+ for (const a of ctxData.agents) grid.append(agentCard(a));
1539
+ pane.append(grid);
1540
+
1541
+ const agents = [...new Set(ctxData.sessions.map((s) => s.agent))];
1542
+ const head = el("div", "row-head ctx-sess-head");
1543
+ head.append(el("span", "label", t("Sessions")), el("span", "grow"));
1544
+ if (agents.length > 1) {
1545
+ if (ctxAgent !== "all" && !agents.includes(ctxAgent)) ctxAgent = "all";
1546
+ head.append(segs([["all", t("All")], ...agents.map((id) => [id, agentLabel(id)])], ctxAgent, (id) => { ctxAgent = id; renderContext(); }));
1547
+ }
1548
+ pane.append(head);
1549
+ const list = el("div", "ctx-sess-list");
1550
+ const shown = ctxData.sessions.filter((s) => ctxAgent === "all" || s.agent === ctxAgent);
1551
+ for (const s of shown) list.append(sessionRow(s));
1552
+ if (!shown.length) list.append(el("p", "usage-note", t("No sessions in this range")));
1553
+ pane.append(list);
1554
+ }
1555
+
1556
+ async function loadContext() {
1557
+ const read = ++ctxRead, days = ctxDays;
1558
+ if (tuneData && tuneData.days !== +days) tuneData = null;
1559
+ if (ctxData && ctxData.days !== +days) ctxData = null;
1560
+ if (!ctxData && (ctxWaiting !== days || !$("#contextPane")?.childElementCount)) { ctxWaiting = days; renderContext(); }
1561
+ loadTune().catch(statusErr);
1562
+ const data = await api("context?days=" + days);
1563
+ if (read !== ctxRead) return;
1564
+ // the auto refresh redraws only what changed: the same answer keeps the
1565
+ // pane (and what the pointer is on), and a new one comes in without its
1566
+ // entrance animations. What it is drawn with counts too: the agents'
1567
+ // names and icons come with the state, which can answer after a first
1568
+ // history, drawn by the agents' ids until then
1569
+ const json = JSON.stringify([data, (state.clients || state.agents || []).map((a) => [a.id, a.name, a.icon])]);
1570
+ if (ctxData && json === ctxJSON) return;
1571
+ ctxData = data, ctxJSON = json;
1572
+ renderContext();
1573
+ }
1574
+ window.loadContext = loadContext;
1575
+ window.renderContext = renderContext;
1576
+ })();